Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 23 additions & 18 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/gif" href="/fish.gif" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="description" content="FreshScan AI — Edge-AI Progressive Web App for real-time fish freshness assessment using biomarker analysis." />
<meta name="theme-color" content="#131313" />
<link rel="manifest" href="/manifest.json" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=Inter:wght@400;500;600;700&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet" />
<title>FreshScan AI | Edge-AI Fish Freshness Assessment</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/gif" href="/fish.gif" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="description"
content="FreshScan AI — Edge-AI Progressive Web App for real-time fish freshness assessment using biomarker analysis." />
<meta name="theme-color" content="#131313" />
<link rel="manifest" href="/manifest.json" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=Inter:wght@400;500;600;700&family=Space+Mono:wght@400;700&display=swap"
rel="stylesheet" />
<title>FreshScan AI | Edge-AI Fish Freshness Assessment</title>
</head>

<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>

</html>
1,925 changes: 1,346 additions & 579 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
"supabase:reset": "cd backend && supabase db reset"
},
"dependencies": {
"@emnapi/core": "1.10.0",
"@emnapi/runtime": "1.10.0",
"@emnapi/wasi-threads": "1.2.1",
"@tailwindcss/vite": "^4.2.2",
"framer-motion": "^12.38.0",
"i18next": "^26.3.0",
Expand Down
Binary file added public/image_192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/image_512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 4 additions & 4 deletions public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@
"orientation": "portrait",
"icons": [
{
"src": "/fish.gif",
"src": "/image_192.png",
"sizes": "192x192",
"type": "image/gif"
"type": "image/png"
},
{
"src": "/fish.gif",
"src": "/image_512.png",
"sizes": "512x512",
"type": "image/gif"
"type": "image/png"
}
]
}
43 changes: 43 additions & 0 deletions public/sw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
const CACHE_NAME = "freshscan-pwa-v1";
const ASSETS_TO_CACHE = [
"/",
"/index.html"
];

self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(ASSETS_TO_CACHE);
})
);
self.skipWaiting();
});

self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cache) => {
if (cache !== CACHE_NAME) {
console.log('[Service Worker] Clearing old cache:', cache);
return caches.delete(cache);
}
})
);
})
);
self.clients.claim();
});

self.addEventListener('fetch', (event) => {
if (!event.request.url.startsWith(self.location.origin)) return;

event.respondWith(
fetch(event.request)
.catch(() => {
return caches.match(event.request).then((response) => {
return response || caches.match('/index.html');
});
})
);
});
3 changes: 2 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import ResultsPage from './pages/ResultsPage';
import Leaderboard from './pages/Leaderboard';
import PostHogPageView from './components/PostHogPageView';
import NotFound from './pages/NotFound';
import InstallPrompt from './components/InstallPrompt';
import PublicReport from "./pages/PublicReport";

export default function App() {
Expand All @@ -36,7 +37,7 @@ useEffect(() => {

{/* Fires a $pageview event to PostHog on every SPA route change */}
<PostHogPageView />

<InstallPrompt />
<Routes>
<Route element={<Layout />}>
<Route path="/" element={<LandingPage />} />
Expand Down
119 changes: 119 additions & 0 deletions src/components/InstallPrompt.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { useState, useEffect } from 'react';

interface BeforeInstallPromptEvent extends Event {
readonly platforms: string[];
readonly userChoice: Promise<{
outcome: 'accepted' | 'dismissed';
platform: string;
}>;
prompt(): Promise<void>;
}

export default function InstallPrompt() {
console.log("InstallPrompt Rendered");
const [showInstallPrompt, setShowInstallPrompt] = useState(false);
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null);

useEffect(() => {
const handler = (e: Event) => {
e.preventDefault();

const installEvent = e as BeforeInstallPromptEvent;

const userAgent = navigator.userAgent || navigator.vendor || '';

const isMobileOrTablet = /android|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent);
const hasTouchScreen = window.matchMedia('(pointer: coarse)').matches;

if (isMobileOrTablet && hasTouchScreen) {
setDeferredPrompt(installEvent);
setShowInstallPrompt(true);
console.log("Is Mobile or Tablet : ", isMobileOrTablet);
console.log("User Agent : ", userAgent);
}

console.log('beforeinstallprompt fired');
};

window.addEventListener('beforeinstallprompt', handler as EventListener);

return () => {
window.removeEventListener('beforeinstallprompt', handler as EventListener);
};
}, []);

const handleInstallClick = async () => {
if (!deferredPrompt) return;

try {
await deferredPrompt.prompt();

const choiceResult = await deferredPrompt.userChoice;

if (choiceResult.outcome === 'accepted') {
console.log('🔥 User installed the PWA!');
} else {
console.log('❌ User dismissed the install dialog.');
}
} catch (err) {
console.error("Error triggering the install prompt:", err);
}

setDeferredPrompt(null);
setShowInstallPrompt(false);
};

const handleNotNow = () => {
setShowInstallPrompt(false);
};

return (
<>
{showInstallPrompt && (
<div
className="fixed bottom-5 left-5 right-5 max-w-sm z-9999 border-4 border-black bg-gray-800 p-5 shadow-[8px_8px_0px_0px_black]"
>
<div className="flex items-center gap-4 border-b-4 border-black pb-4">
<img
src="/fish.gif"
alt="FreshScan AI"
width={56}
height={56}
/>

<div>
<p className="text-xs font-black tracking-widest uppercase">
FreshScan AI
</p>

<h2 className="text-xl font-black uppercase">
Install App
</h2>
</div>
</div>

<p className="mt-4 text-base font-bold leading-relaxed">
Add FreshScan AI to your home screen for instant fish freshness
analysis in one tap.
</p>

<div className="mt-5 flex gap-3">
<button
onClick={handleInstallClick}
className="flex-1 border-4 border-black bg-lime-300 px-4 py-3 text-black uppercase shadow-[4px_4px_0px_0px_black] active:translate-x-1 active:translate-y-1 active:shadow-none"
>
Install
</button>

<button
onClick={handleNotNow}
className="px-4 py-3 font-black uppercase"
>
Not Now
</button>
</div>
</div>
)}
</>
);
}
14 changes: 14 additions & 0 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,20 @@ if (POSTHOG_KEY) {
});
}

if ('serviceWorker' in navigator) {
try {
navigator.serviceWorker
.register('/sw.js')
.then((reg) => {
console.log('SW registered', reg);
})
.catch(console.error);
}
catch (e) {
console.log("Error occured during the registration of sw", e)
}
}

createRoot(document.getElementById('root')!).render(
<StrictMode>
<PostHogProvider client={posthog}>
Expand Down
14 changes: 9 additions & 5 deletions src/pages/AuthPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,18 @@ export default function AuthPage() {
const error = params.get('error');

if (error) {
setStatus('error');
setErrorMsg('Authentication failed. Please try again.');
setTimeout(() => {
setStatus('error');
setErrorMsg('Authentication failed. Please try again.');
}, 0);
window.history.replaceState({}, '', '/auth');
return;
}

if (accessToken) {
setStatus('processing');
setTimeout(() => {
setStatus('processing');
}, 0);
setToken(accessToken);
window.history.replaceState({}, '', '/auth');
navigate('/mode', { replace: true });
Expand All @@ -44,11 +48,11 @@ export default function AuthPage() {
try {
setStatus('processing');
const loginUrl = api.loginUrl();

if (!loginUrl) {
throw new Error("Login URL configuration missing");
}

// Force full browser navigation for OAuth
window.location.href = loginUrl;
} catch (err) {
Expand Down
Loading