Skip to content

Commit 51c6aa3

Browse files
committed
check all changes again
1 parent cfc7c69 commit 51c6aa3

10 files changed

Lines changed: 203 additions & 136 deletions

File tree

.github/workflows/ci.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,39 @@ jobs:
2929
else
3030
echo "No tracked .env* files found."
3131
fi
32+
33+
lint:
34+
name: ESLint
35+
runs-on: ubuntu-latest
36+
steps:
37+
- uses: actions/checkout@v4
38+
- uses: actions/setup-node@v4
39+
with:
40+
node-version: '20'
41+
cache: 'yarn'
42+
- run: yarn install --frozen-lockfile
43+
- run: yarn lint
44+
45+
typecheck:
46+
name: TypeScript Check
47+
runs-on: ubuntu-latest
48+
steps:
49+
- uses: actions/checkout@v4
50+
- uses: actions/setup-node@v4
51+
with:
52+
node-version: '20'
53+
cache: 'yarn'
54+
- run: yarn install --frozen-lockfile
55+
- run: yarn typecheck
56+
57+
build:
58+
name: Build Check
59+
runs-on: ubuntu-latest
60+
steps:
61+
- uses: actions/checkout@v4
62+
- uses: actions/setup-node@v4
63+
with:
64+
node-version: '20'
65+
cache: 'yarn'
66+
- run: yarn install --frozen-lockfile
67+
- run: yarn build

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "nextn",
2+
"name": "eatinformed",
33
"version": "0.1.0",
44
"private": true,
55
"scripts": {

src/ai/flows/assess-health-safety.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ export async function assessHealthSafety(input: AssessHealthSafetyInput): Promis
3636
if (!input.ingredients || input.ingredients.trim() === '') {
3737
return {
3838
rating: 0,
39-
pros: ["None (no data to analyze)."],
40-
cons: ["None (no data to analyze)."],
39+
pros: [],
40+
cons: [],
4141
warnings: ["Unable to evaluate due to missing or unreadable label. Please upload a clear image."],
4242
dietaryInfo: {
4343
allergens: [],
@@ -115,18 +115,19 @@ Your analysis must be objective and based on general nutritional science. Be con
115115
return await assessHealthSafetyFlow(input);
116116
} catch (error: any) {
117117
console.error("Error in assessHealthSafetyFlow:", error);
118-
// Construct a user-friendly error response that fits the schema
119118
let warningMessage = "The AI model failed to provide an assessment due to an unexpected error.";
120-
if (error.message && (error.message.includes('503') || error.message.includes('Service Unavailable'))) {
121-
warningMessage = "The AI analysis service is temporarily overloaded. Please wait a moment and try again.";
122-
} else if (error.message && error.message.includes('Deadline exceeded')) {
123-
warningMessage = "The analysis took too long to complete. Please try again.";
119+
if (error.message) {
120+
if (error.message.includes('503') || error.message.toLowerCase().includes('service unavailable')) {
121+
warningMessage = "The AI analysis service is temporarily overloaded. Please wait a moment and try again.";
122+
} else if (error.message.toLowerCase().includes('deadline exceeded')) {
123+
warningMessage = "The analysis took too long to complete. Please try again.";
124+
}
124125
}
125126

126127
return {
127128
rating: 0,
128-
pros: ["None (analysis failed)."],
129-
cons: ["None (analysis failed)."],
129+
pros: [],
130+
cons: [],
130131
warnings: [warningMessage],
131132
dietaryInfo: {
132133
allergens: [],

src/ai/flows/extract-ingredients.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,9 @@ import { ExtractIngredientsInput, ExtractIngredientsInputSchema, ExtractIngredie
1111
export async function extractIngredients(input: ExtractIngredientsInput): Promise<ExtractIngredientsOutput> {
1212
if (!ai) {
1313
console.error("AI system not initialized. Check GOOGLE_API_KEY.");
14-
// Return a structured error if AI is offline
1514
return {
1615
ingredients: [],
17-
nutrition: { rawText: "AI system is offline.", nutrients: [] },
16+
nutrition: { rawText: "AI system is offline. The administrator needs to configure the GOOGLE_API_KEY.", nutrients: [] },
1817
status: 'unreadable',
1918
};
2019
}
@@ -51,14 +50,10 @@ Image to analyze: {{media url=image}}`,
5150
async (input) => {
5251
const {output} = await prompt(input);
5352
if (!output) {
54-
return {
55-
ingredients: [],
56-
nutrition: undefined,
57-
status: 'unreadable',
58-
};
53+
throw new Error('The AI model failed to provide an output.');
5954
}
6055
// A simple check to refine status if model returns success but no data
61-
if (output.status === 'success' && output.ingredients.length === 0 && !output.nutrition?.rawText && !output.nutrition?.nutrients) {
56+
if (output.status === 'success' && output.ingredients.length === 0 && (!output.nutrition || (!output.nutrition.rawText && (!output.nutrition.nutrients || output.nutrition.nutrients.length === 0)))) {
6257
output.status = 'no_data';
6358
}
6459

@@ -68,12 +63,19 @@ Image to analyze: {{media url=image}}`,
6863

6964
try {
7065
return await extractIngredientsFlow(input);
71-
} catch (error) {
66+
} catch (error: any) {
7267
console.error("Error in extractIngredientsFlow:", error);
73-
// Return a structured error on any unexpected exception
68+
let errorMessage = 'The AI model failed to process the image due to an unexpected error.';
69+
if (error.message) {
70+
if (error.message.includes('503') || error.message.toLowerCase().includes('service unavailable')) {
71+
errorMessage = "The AI service is temporarily overloaded. Please wait a moment and try again.";
72+
} else if (error.message.toLowerCase().includes('deadline exceeded')) {
73+
errorMessage = "The analysis took too long to complete. Please try again.";
74+
}
75+
}
7476
return {
7577
ingredients: [],
76-
nutrition: { rawText: 'The AI model failed to process the image due to an unexpected error.', nutrients: [] },
78+
nutrition: { rawText: errorMessage, nutrients: [] },
7779
status: 'unreadable',
7880
};
7981
}
Lines changed: 46 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,73 @@
11

2-
import { auth as adminAuth } from 'firebase-admin';
2+
import { adminAuth } from '@/lib/firebase-admin';
33
import { cookies } from 'next/headers';
44
import { NextRequest, NextResponse } from 'next/server';
5-
import { initAdmin } from '@/lib/firebase-admin';
65

7-
// Initialize Firebase Admin SDK
8-
initAdmin();
6+
const SESSION_COOKIE_NAME = 'session';
97

108
// Define cookie options
119
const getCookieOptions = (expires: number) => ({
12-
name: 'session',
10+
name: SESSION_COOKIE_NAME,
1311
value: '',
1412
httpOnly: true,
1513
secure: process.env.NODE_ENV === 'production',
1614
path: '/',
1715
expires,
1816
});
1917

20-
export async function POST(request: NextRequest, { params }: { params: { action: string[] } }) {
21-
const action = params.action[0];
18+
export async function POST(request: NextRequest) {
19+
if (!adminAuth) {
20+
return NextResponse.json({ error: 'Firebase Admin not initialized' }, { status: 500 });
21+
}
2222

23-
if (action === 'login') {
24-
try {
25-
const { idToken } = await request.json();
26-
if (!idToken) {
27-
return NextResponse.json({ error: 'idToken is required' }, { status: 400 });
28-
}
23+
try {
24+
const { idToken } = await request.json();
25+
if (!idToken) {
26+
return NextResponse.json({ error: 'idToken is required' }, { status: 400 });
27+
}
2928

30-
const expiresIn = 60 * 60 * 24 * 14 * 1000; // 14 days
31-
const decodedIdToken = await adminAuth().verifyIdToken(idToken);
32-
const sessionCookie = await adminAuth().createSessionCookie(idToken, { expiresIn });
29+
const expiresIn = 60 * 60 * 24 * 14 * 1000; // 14 days
30+
const sessionCookie = await adminAuth.createSessionCookie(idToken, { expiresIn });
3331

34-
const options = getCookieOptions(Date.now() + expiresIn);
35-
options.value = sessionCookie;
32+
const options = getCookieOptions(Date.now() + expiresIn);
33+
options.value = sessionCookie;
3634

37-
cookies().set(options.name, options.value, options);
35+
cookies().set(options.name, options.value, options);
3836

39-
return NextResponse.json({ status: 'success' }, { status: 200 });
40-
} catch (error: any) {
41-
console.error('Session login error:', error);
42-
return NextResponse.json({ error: error.message }, { status: 401 });
37+
return NextResponse.json({ status: 'success' }, { status: 200 });
38+
} catch (error: any) {
39+
console.error('Session login error:', error);
40+
let errorMessage = 'An unexpected error occurred.';
41+
if (error.code === 'auth/id-token-revoked') {
42+
errorMessage = 'The ID token has been revoked. Please re-authenticate.';
43+
} else if (error.code === 'auth/argument-error') {
44+
errorMessage = 'Invalid ID token provided.';
4345
}
46+
return NextResponse.json({ error: errorMessage }, { status: 401 });
4447
}
45-
46-
return NextResponse.json({ error: 'Invalid action' }, { status: 400 });
4748
}
4849

49-
export async function GET(request: NextRequest, { params }: { params: { action: string[] } }) {
50-
const action = params.action[0];
51-
52-
if (action === 'logout') {
53-
try {
54-
const sessionCookie = cookies().get('session')?.value;
55-
if (sessionCookie) {
56-
const decodedClaims = await adminAuth().verifySessionCookie(sessionCookie).catch(() => null);
57-
if (decodedClaims) {
58-
await adminAuth().revokeRefreshTokens(decodedClaims.sub);
59-
}
60-
}
61-
62-
const options = getCookieOptions(0); // Expire the cookie
63-
cookies().set(options.name, '', options);
50+
export async function GET(request: NextRequest) {
51+
if (!adminAuth) {
52+
return NextResponse.json({ error: 'Firebase Admin not initialized' }, { status: 500 });
53+
}
6454

65-
return NextResponse.json({ status: 'success' }, { status: 200 });
66-
} catch (error: any) {
67-
console.error('Session logout error:', error);
68-
return NextResponse.json({ error: error.message }, { status: 500 });
55+
try {
56+
const sessionCookie = cookies().get(SESSION_COOKIE_NAME)?.value;
57+
if (sessionCookie) {
58+
// It's good practice to verify the cookie before revoking tokens
59+
const decodedClaims = await adminAuth.verifySessionCookie(sessionCookie).catch(() => null);
60+
if (decodedClaims) {
61+
await adminAuth.revokeRefreshTokens(decodedClaims.sub);
6962
}
7063
}
71-
72-
return NextResponse.json({ error: 'Invalid action' }, { status: 400 });
64+
65+
const options = getCookieOptions(0); // Expire the cookie immediately
66+
cookies().set(options.name, '', options);
67+
68+
return NextResponse.json({ status: 'success' }, { status: 200 });
69+
} catch (error: any) {
70+
console.error('Session logout error:', error);
71+
return NextResponse.json({ error: 'Failed to log out.' }, { status: 500 });
7372
}
73+
}

src/components/features/ImageUploadForm.tsx

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ export function CheckPageClient() {
4141
if (videoRef.current) {
4242
videoRef.current.srcObject = null;
4343
}
44-
// Reset permission state so it can be re-requested if the user revisits the tab.
4544
setHasCameraPermission(null);
4645
}, []);
4746

@@ -53,6 +52,16 @@ export function CheckPageClient() {
5352
}, [stopCameraStream]);
5453

5554
const getCameraPermission = useCallback(async () => {
55+
if (!navigator.mediaDevices?.getUserMedia) {
56+
setHasCameraPermission(false);
57+
toast({
58+
variant: 'destructive',
59+
title: 'Camera Not Supported',
60+
description: 'Your browser does not support camera access. Please use the upload option.',
61+
});
62+
return;
63+
}
64+
5665
// Prefer the rear-facing camera for scanning product labels on mobile.
5766
const videoConstraints = {
5867
video: { facingMode: 'environment' }
@@ -61,17 +70,16 @@ export function CheckPageClient() {
6170
try {
6271
// First, try to get the rear camera
6372
const stream = await navigator.mediaDevices.getUserMedia(videoConstraints);
64-
streamRef.current = stream; // Store the stream
73+
streamRef.current = stream;
6574
if (videoRef.current) {
6675
videoRef.current.srcObject = stream;
6776
}
6877
setHasCameraPermission(true);
6978
} catch (err) {
70-
console.error('Failed to get rear camera, trying default camera:', err);
71-
// If the rear camera fails (e.g., on a desktop), fall back to any available camera.
79+
console.warn('Failed to get rear camera, trying default camera:', err);
7280
try {
7381
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
74-
streamRef.current = stream; // Store the stream
82+
streamRef.current = stream;
7583
if (videoRef.current) {
7684
videoRef.current.srcObject = stream;
7785
}
@@ -82,17 +90,16 @@ export function CheckPageClient() {
8290
toast({
8391
variant: 'destructive',
8492
title: 'Camera Access Denied',
85-
description: 'Could not access camera. Please enable camera permissions in your browser settings to use this feature.',
93+
description: 'Could not access camera. Please enable permissions in your browser settings.',
8694
});
8795
}
8896
}
8997
}, [toast]);
9098

91-
// Request camera permission when camera tab is activated, and clean up when leaving it.
9299
const handleTabChange = useCallback((value: string) => {
93100
if (value === 'camera' && hasCameraPermission === null) {
94101
getCameraPermission();
95-
} else if (value !== 'camera') {
102+
} else if (value !== 'camera' && streamRef.current) {
96103
stopCameraStream();
97104
}
98105
}, [getCameraPermission, hasCameraPermission, stopCameraStream]);
@@ -157,7 +164,7 @@ export function CheckPageClient() {
157164
context.drawImage(video, 0, 0, canvas.width, canvas.height);
158165
const dataUri = canvas.toDataURL('image/png');
159166
setImagePreviewUrl(dataUri);
160-
stopCameraStream(); // Stop the camera after capture
167+
stopCameraStream();
161168
}
162169
}
163170
};
@@ -179,9 +186,9 @@ export function CheckPageClient() {
179186
} else if (extracted.status === 'no_data') {
180187
setError("We couldn't find any ingredient or nutrition text on the label. Please try a different image.");
181188
}
182-
setIngredientsData(extracted); // Still set data to show raw text if available
189+
setIngredientsData(extracted);
183190
setIsLoading(false);
184-
return; // Stop the process
191+
return;
185192
}
186193

187194
setIngredientsData(extracted);
@@ -205,7 +212,7 @@ export function CheckPageClient() {
205212
}, [toast]);
206213

207214
const handleReset = () => {
208-
stopCameraStream(); // Ensure camera is off on reset
215+
stopCameraStream();
209216
setImagePreviewUrl(null);
210217
setIngredientsData(null);
211218
setAssessmentData(null);
@@ -319,13 +326,18 @@ export function CheckPageClient() {
319326
</AlertDescription>
320327
</Alert>
321328
)}
322-
{hasCameraPermission && (
329+
{hasCameraPermission === true && (
323330
<div className="mt-4 flex justify-center">
324331
<Button onClick={handleCapture} size="lg" className="rounded-full">
325332
<Camera className="mr-2 h-5 w-5" /> Capture Image
326333
</Button>
327334
</div>
328335
)}
336+
{hasCameraPermission === null && (
337+
<div className="absolute inset-0 flex items-center justify-center">
338+
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
339+
</div>
340+
)}
329341
</div>
330342
</TabsContent>
331343
</Tabs>

0 commit comments

Comments
 (0)