diff --git a/frontend/messages/de.json b/frontend/messages/de.json index 9a7d374e..39778cd0 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -384,7 +384,11 @@ "emptyMessage": "Scannen Sie einen Barcode, um Ihren Verlauf aufzubauen.", "startScanning": "Scannen starten →", "submissionStatus": "Einreichung: {status}", - "submit": "Einreichen →" + "submit": "Einreichen →", + "today": "Heute", + "yesterday": "Gestern", + "thisWeek": "Diese Woche", + "earlier": "Früher" }, "submit": { "title": "Produkt einreichen", @@ -1988,5 +1992,9 @@ "bronze": "Bronze-Beiträger", "silver": "Silber-Beiträger", "gold": "Gold-Beiträger" + }, + "categoryPicker": { + "showAll": "Alle Kategorien anzeigen", + "showLess": "Weniger anzeigen" } } diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 10541135..d29a0b22 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -384,7 +384,11 @@ "emptyMessage": "Scan a barcode to start building your history.", "startScanning": "Start scanning →", "submissionStatus": "Submission: {status}", - "submit": "Submit →" + "submit": "Submit →", + "today": "Today", + "yesterday": "Yesterday", + "thisWeek": "This Week", + "earlier": "Earlier" }, "submit": { "title": "Submit Product", @@ -1988,5 +1992,9 @@ "bronze": "Bronze Contributor", "silver": "Silver Contributor", "gold": "Gold Contributor" + }, + "categoryPicker": { + "showAll": "Show all categories", + "showLess": "Show less" } } diff --git a/frontend/messages/pl.json b/frontend/messages/pl.json index 28bd4efa..2e781ce5 100644 --- a/frontend/messages/pl.json +++ b/frontend/messages/pl.json @@ -384,7 +384,11 @@ "emptyMessage": "Zeskanuj kod kreskowy, aby rozpocząć budowanie historii.", "startScanning": "Rozpocznij skanowanie →", "submissionStatus": "Zgłoszenie: {status}", - "submit": "Zgłoś →" + "submit": "Zgłoś →", + "today": "Dzisiaj", + "yesterday": "Wczoraj", + "thisWeek": "Ten tydzień", + "earlier": "Wcześniej" }, "submit": { "title": "Zgłoś produkt", @@ -1988,5 +1992,9 @@ "bronze": "Brązowy Kontrybutor", "silver": "Srebrny Kontrybutor", "gold": "Złoty Kontrybutor" + }, + "categoryPicker": { + "showAll": "Pokaż wszystkie kategorie", + "showLess": "Pokaż mniej" } } diff --git a/frontend/src/app/app/scan/history/page.test.tsx b/frontend/src/app/app/scan/history/page.test.tsx index fe586509..b96404cd 100644 --- a/frontend/src/app/app/scan/history/page.test.tsx +++ b/frontend/src/app/app/scan/history/page.test.tsx @@ -11,9 +11,18 @@ vi.mock("@/lib/supabase/client", () => ({ createClient: () => ({}), })); +vi.mock("@/lib/i18n", () => ({ + useTranslation: () => ({ + t: (key: string, params?: Record) => { + if (params) return `${key}:${JSON.stringify(params)}`; + return key; + }, + }), +})); + const mockPush = vi.fn(); vi.mock("next/navigation", () => ({ - useRouter: () => ({ push: mockPush }), + useRouter: () => ({ push: mockPush, back: vi.fn() }), })); vi.mock("next/link", () => ({ @@ -35,6 +44,53 @@ vi.mock("@/components/common/skeletons", () => ({ ScanHistorySkeleton: () =>
, })); +vi.mock("@/components/common/PullToRefresh", () => ({ + PullToRefresh: ({ children }: { children: React.ReactNode }) =>
{children}
, +})); + +vi.mock("@/components/common/EmptyState", () => ({ + EmptyState: ({ titleKey, action }: { titleKey: string; action?: { labelKey: string } }) => ( +
+

{titleKey}

+ {action && } +
+ ), +})); + +vi.mock("@/components/common/EmptyStateIllustration", () => ({ + EmptyStateIllustration: ({ titleKey, action }: { titleKey: string; action?: { labelKey: string; href?: string } }) => ( +
+

{titleKey}

+ {action && {action.labelKey}} +
+ ), +})); + +vi.mock("@/components/layout/Breadcrumbs", () => ({ + Breadcrumbs: () =>
)} + ); } @@ -182,6 +186,45 @@ function groupScans(scans: ScanHistoryItem[]): GroupedScan[] { return grouped; } +// ─── Date grouping helper ──────────────────────────────────────────────────── + +type DateGroup = { labelKey: string; scans: GroupedScan[] }; + +function groupByDate(scans: GroupedScan[]): DateGroup[] { + const now = new Date(); + const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + const yesterday = new Date(today); + yesterday.setDate(yesterday.getDate() - 1); + const weekAgo = new Date(today); + weekAgo.setDate(weekAgo.getDate() - 7); + + const groups: Record = { + today: [], + yesterday: [], + thisWeek: [], + earlier: [], + }; + + for (const scan of scans) { + const d = new Date(scan.scanned_at); + if (d >= today) groups.today.push(scan); + else if (d >= yesterday) groups.yesterday.push(scan); + else if (d >= weekAgo) groups.thisWeek.push(scan); + else groups.earlier.push(scan); + } + + const keys: { key: string; labelKey: string }[] = [ + { key: "today", labelKey: "scanHistory.today" }, + { key: "yesterday", labelKey: "scanHistory.yesterday" }, + { key: "thisWeek", labelKey: "scanHistory.thisWeek" }, + { key: "earlier", labelKey: "scanHistory.earlier" }, + ]; + + return keys + .filter(({ key }) => groups[key].length > 0) + .map(({ key, labelKey }) => ({ labelKey, scans: groups[key] })); +} + function ScanList({ scans, onNavigate, @@ -189,18 +232,40 @@ function ScanList({ scans: ScanHistoryItem[]; onNavigate: (productId: number) => void; }>) { + const { t } = useTranslation(); const grouped = useMemo(() => groupScans(scans), [scans]); + const dateGroups = useMemo(() => groupByDate(grouped), [grouped]); + const groupStartIndexes = useMemo(() => { + const starts: number[] = []; + dateGroups.reduce((acc, g) => { + starts.push(acc); + return acc + g.scans.length; + }, 0); + return starts; + }, [dateGroups]); return ( - +
+ {dateGroups.map((group, gi) => { + const startIndex = groupStartIndexes[gi]; + return ( +
+

+ {t(group.labelKey)} +

+
    + {group.scans.map((scan, idx) => ( + + ))} +
+
+ ); + })} +
); } @@ -239,6 +304,17 @@ function ScanRow({ {scan.nutri_score} )} + {/* TryVit Score badge */} + {scan.unhealthiness_score != null && (() => { + const band = getScoreBand(scan.unhealthiness_score); + return band ? ( + + {toTryVitScore(scan.unhealthiness_score)} + + ) : null; + })()}

{scan.product_name} diff --git a/frontend/src/app/app/scan/page.tsx b/frontend/src/app/app/scan/page.tsx index dac157d3..2ce27900 100644 --- a/frontend/src/app/app/scan/page.tsx +++ b/frontend/src/app/app/scan/page.tsx @@ -11,6 +11,7 @@ import { usePreferences } from "@/components/common/RouteGuard"; import { Breadcrumbs } from "@/components/layout/Breadcrumbs"; import { ScannerErrorState } from "@/components/scan/ScannerErrorState"; import { + FadeSlideIn, ScanErrorView, ScanFoundView, ScanLookingUpView, @@ -326,11 +327,13 @@ export default function ScanPage() { {mode === "camera" ? (

{cameraError ? ( - { clearError(); startScanner(); }} - onManualEntry={() => { clearError(); setMode("manual"); }} - /> + + { clearError(); startScanner(); }} + onManualEntry={() => { clearError(); setMode("manual"); }} + /> + ) : ( <>
@@ -343,8 +346,8 @@ export default function ScanPage() { /> {/* Viewfinder overlay with alignment guides */}
-
-
+
div:first-child]:border-green-400" : ""}`}> +
{/* Corner guides */}
@@ -365,7 +368,7 @@ export default function ScanPage() {