Skip to content
Merged
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">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add the missing DOCTYPE declaration.

HTML5 documents should begin with <!DOCTYPE html>. Without it, browsers may enter quirks mode, leading to inconsistent rendering behavior across platforms.

🔧 Proposed fix
+<!DOCTYPE html>
 <html lang="en">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<html lang="en">
<!DOCTYPE html>
<html lang="en">
🧰 Tools
🪛 HTMLHint (1.9.2)

[error] 1-1: Doctype must be declared before any non-comment content.

(doctype-first)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@index.html` at line 1, The document is missing the HTML5 DOCTYPE which can
trigger quirks mode; add the declaration <!DOCTYPE html> as the very first line
before the existing <html lang="en"> tag in index.html so browsers render in
standards mode and avoid inconsistent layout.

<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" />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the new PNG icon files exist in the public directory
fd -t f '^(image_192\.png|image_512\.png|fish\.gif)$' public/

Repository: jpdevhub/FreshScanAi

Length of output: 122


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find manifest file referenced by index.html
echo "== index.html manifest references =="
rg -n --hidden --no-ignore -S '<link[^>]+rel=["'\'']manifest["'\'']' -g'index.html' . || true

# Locate manifest file(s)
echo
echo "== candidate manifest files =="
fd -t f -e json -e webmanifest -e manifest -d 4 'manifest*.{json,webmanifest}' . || true
fd -t f -d 4 -e json -e webmanifest '*manifest*.{json,webmanifest}' . || true

# Print the manifest icon entries (paths only)
echo
echo "== manifest icon references (paths) =="
for f in $(fd -t f 'manifest*.json' . -0 2>/dev/null | tr '\0' '\n' || true); do
  echo "--- $f ---"
  # extract icon src fields and sizes
  rg -n '"icons"\s*:' "$f" || true
  rg -n '"src"\s*:' "$f" || true
done

for f in $(fd -t f 'manifest*.webmanifest' . -0 2>/dev/null | tr '\0' '\n' || true); do
  echo "--- $f ---"
  rg -n '"src"\s*:' "$f" || true
done

# Show the favicon line from index.html
echo
echo "== favicon line in index.html =="
rg -n --hidden --no-ignore -S '<link[^>]+rel=["'\'']icon["'\'']' index.html || true

Repository: jpdevhub/FreshScanAi

Length of output: 1097


Fix favicon to match PWA manifest icon assets.

index.html still sets the tab favicon to /fish.gif, while public/manifest.json references /image_192.png and /image_512.png for the installed PWA icons. This leaves the browser-tab icon out of sync with the PWA icon set.

🔧 Recommended fix to align favicon with manifest
-  <link rel="icon" type="image/gif" href="/fish.gif" />
+  <link rel="icon" type="image/png" href="/image_192.png" />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<link rel="icon" type="image/gif" href="/fish.gif" />
<link rel="icon" type="image/png" href="/image_192.png" />
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@index.html` at line 5, Update the favicon link in index.html so the tab icon
matches the PWA manifest icons: replace the current <link rel="icon" ...
href="/fish.gif"> reference with a link to one of the manifest assets (e.g.,
/image_192.png or /image_512.png) or add multiple rel="icon" links for different
sizes to mirror manifest.json entries; ensure the hrefs exactly match the
filenames used in manifest.json (image_192.png, image_512.png) so the browser
tab icon and installed PWA icons stay in sync.

<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>
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"
}
]
}
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
141 changes: 141 additions & 0 deletions src/components/InstallPrompt.tsx
Original file line number Diff line number Diff line change
@@ -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<void>;
}

interface NavigatorWithStandalone extends Navigator {
standalone?: boolean;
}

export default function InstallPrompt() {
const [promptType, setpromptType] = useState<PromptType>(() => {
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<BeforeInstallPromptEvent | null>(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 (
<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">
{Message}
</p>

<div className="mt-5 flex gap-3">
{(promptType == 'android') && (
<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>
)
}

const message = promptType === 'ios' ? IOSMsg : AndroidMsg;
return (
<>
{showInstallPrompt && renderFunction(message)}
</>
);
}
20 changes: 13 additions & 7 deletions src/pages/AuthPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand All @@ -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 });
Expand All @@ -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) {
Expand Down
Loading