diff --git a/index.html b/index.html index ff863cd..5fe3f70 100644 --- a/index.html +++ b/index.html @@ -1,19 +1,24 @@ - - - - - - - - - - - - FreshScan AI | Edge-AI Fish Freshness Assessment - - -
- - - + + + + + + + + + + + + FreshScan AI | Edge-AI Fish Freshness Assessment + + + +
+ + + + \ No newline at end of file diff --git a/public/image_192.png b/public/image_192.png new file mode 100644 index 0000000..71b86f4 Binary files /dev/null and b/public/image_192.png differ diff --git a/public/image_512.png b/public/image_512.png new file mode 100644 index 0000000..b234b60 Binary files /dev/null and b/public/image_512.png differ diff --git a/public/manifest.json b/public/manifest.json index 6f54492..ca5ff90 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -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" } ] } diff --git a/src/App.tsx b/src/App.tsx index 078ad59..cf051cb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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() { @@ -36,7 +37,7 @@ useEffect(() => { {/* Fires a $pageview event to PostHog on every SPA route change */} - + }> } /> diff --git a/src/components/InstallPrompt.tsx b/src/components/InstallPrompt.tsx new file mode 100644 index 0000000..4828ab0 --- /dev/null +++ b/src/components/InstallPrompt.tsx @@ -0,0 +1,141 @@ +import { useState, useEffect } from 'react'; + +type PromptType = "ios" | "android" | null; + +const IOSMsg = "Add FreshScan AI to your Home Screen for one-tap fish freshness analysis. Tap Share and select Add to Home Screen." +const AndroidMsg = "Add FreshScan AI to your home screen for instant fish freshness analysis in one tap." + +interface BeforeInstallPromptEvent extends Event { + readonly platforms: string[]; + readonly userChoice: Promise<{ + outcome: 'accepted' | 'dismissed'; + platform: string; + }>; + prompt(): Promise; +} + +interface NavigatorWithStandalone extends Navigator { + standalone?: boolean; +} + +export default function InstallPrompt() { + const [promptType, setpromptType] = useState(() => { + if (typeof window === 'undefined') return null; + + const userAgent = navigator.userAgent || navigator.vendor || ''; + const ios = /iphone|ipad|ipod/i.test(userAgent); + const standalone = window.matchMedia('(display-mode: standalone)').matches || (navigator as NavigatorWithStandalone).standalone; + + return (ios && !standalone) ? "ios" : null; + }); + + const [showInstallPrompt, setShowInstallPrompt] = useState(() => { + return promptType === "ios"; + }); + const [deferredPrompt, setDeferredPrompt] = useState(null); + + useEffect(() => { + const userAgent = navigator.userAgent || navigator.vendor || ''; + const handler = (e: Event) => { + e.preventDefault(); + + const installEvent = e as BeforeInstallPromptEvent; + + const isMobileOrTablet = /android|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent); + const hasTouchScreen = window.matchMedia('(pointer: coarse)').matches; + + if (isMobileOrTablet && hasTouchScreen) { + setpromptType("android"); + setDeferredPrompt(installEvent); + setShowInstallPrompt(true); + } + }; + + 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); + }; + + const renderFunction = (Message: string) => { + return ( +
+
+ FreshScan AI + +
+

+ FreshScan AI +

+ +

+ Install App +

+
+
+ +

+ {Message} +

+ +
+ {(promptType == 'android') && ( + + )} + + +
+
+ ) + } + + const message = promptType === 'ios' ? IOSMsg : AndroidMsg; + return ( + <> + {showInstallPrompt && renderFunction(message)} + + ); +} \ No newline at end of file diff --git a/src/pages/AuthPage.tsx b/src/pages/AuthPage.tsx index 616e2ec..11babb3 100644 --- a/src/pages/AuthPage.tsx +++ b/src/pages/AuthPage.tsx @@ -11,8 +11,17 @@ const IS_DEV_MODE = import.meta.env.VITE_DEV_MODE === 'true'; export default function AuthPage() { const navigate = useNavigate(); const posthog = usePostHog(); - const [status, setStatus] = useState<'idle' | 'processing' | 'error'>('idle'); - const [errorMsg, setErrorMsg] = useState(''); + const [status, setStatus] = useState<'idle' | 'processing' | 'error'>(() => { + const params = new URLSearchParams(window.location.search); + if (params.get('error')) return 'error'; + if (params.get('access_token')) return 'processing'; + return 'idle'; + }); + + const [errorMsg, setErrorMsg] = useState(() => { + const params = new URLSearchParams(window.location.search); + return params.get('error') ? 'Authentication failed. Please try again.' : ''; + }); // Handle redirect from backend OAuth callback useEffect(() => { @@ -21,14 +30,11 @@ export default function AuthPage() { const error = params.get('error'); if (error) { - setStatus('error'); - setErrorMsg('Authentication failed. Please try again.'); window.history.replaceState({}, '', '/auth'); return; } if (accessToken) { - setStatus('processing'); setToken(accessToken); window.history.replaceState({}, '', '/auth'); navigate('/mode', { replace: true }); @@ -44,11 +50,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) {