From 2968f9ea3763613cee82cef1afadd27273d37396 Mon Sep 17 00:00:00 2001 From: Mao Nakamoto <41178744+maonakamoto@users.noreply.github.com> Date: Wed, 2 Sep 2026 01:17:35 +0200 Subject: [PATCH] feat(passenger): show what I reported and how it ended MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 7cbc326 got the crew's answer to the passenger, but only as a four-second toast. Look away, or have the phone asleep when the crew presses Gefunden, and there was no way to find out afterwards — the answer existed in storage and the passenger app never showed it. The Reisen tab now carries "Meine Verlustmeldungen" under the trips: what was reported, where, when, the status badge, and the crew's note when they left one ("Unter Sitz 42 gefunden, liegt beim Zugpersonal" is worth more to a passenger than the word Gefunden). It reads the same handover the toast reads, so there is no second copy of the answer, and it updates live — an answer landing in the crew tab flips the badge without a reload. Nothing renders before the first report: an empty box explaining its own emptiness has no business on a screen opened to see trips. lib/hooks/useDemoReports.ts holds the read, so the component stays render-only and the storage subscription is not duplicated per consumer. It starts empty and fills after mount — localStorage does not exist on the server, and reading during render would make the first paint disagree with the HTML Next sent. Verified in a browser against the dev server, no backend: reported "Kopfhörer, weiss, in Ladecase", saw it listed as Neu, answered Gefunden with a note from /staff in a second tab, watched the badge and note appear without a reload — then reloaded / and the record was still there, which is the whole point. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014MT1aaWfJeDUZpTxZ3DfHg --- frontend/app/page.tsx | 4 ++ frontend/components/passenger/MyReports.tsx | 73 +++++++++++++++++++++ frontend/lib/hooks/index.ts | 2 + frontend/lib/hooks/useDemoReports.ts | 28 ++++++++ frontend/lib/labels.ts | 7 ++ 5 files changed, 114 insertions(+) create mode 100644 frontend/components/passenger/MyReports.tsx create mode 100644 frontend/lib/hooks/useDemoReports.ts diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index c114e49..5929678 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -5,6 +5,7 @@ import { StatusBar } from '@/components/ui/StatusBar'; import { Header } from '@/components/passenger/Header'; import { TripCard } from '@/components/passenger/TripCard'; import { LostItemModal } from '@/components/passenger/LostItemModal'; +import { MyReports } from '@/components/passenger/MyReports'; import { BottomNav, type NavTab } from '@/components/ui/BottomNav'; import { Toast } from '@/components/ui/Toast'; import { mockUser, formatRelativeTime } from '@/lib/mock-data'; @@ -298,6 +299,9 @@ export default function PassengerApp() { ))} + + {/* What this passenger reported, and how each one ended */} + ); }; diff --git a/frontend/components/passenger/MyReports.tsx b/frontend/components/passenger/MyReports.tsx new file mode 100644 index 0000000..55533cd --- /dev/null +++ b/frontend/components/passenger/MyReports.tsx @@ -0,0 +1,73 @@ +'use client'; + +import { ITEM_CATEGORY_CONFIG } from '@/lib/types'; +import { NOTIFICATION_STATUS_CONFIG, UI_LABELS } from '@/lib/labels'; +import { formatRelativeTime } from '@/lib/mock-data'; +import { useDemoReports } from '@/lib/hooks'; + +/** + * What this passenger reported, and how each one ended. + * + * The crew's answer used to reach the passenger as a four-second toast and + * nothing else: look away, or have the phone asleep, and there was no way to + * find out afterwards what the crew said. This is the record that outlives the + * toast — same data, read back from the handover. + * + * Renders nothing before the first report, rather than an empty box explaining + * its own emptiness on a screen the passenger opened to see their trips. + */ +export function MyReports() { + const reports = useDemoReports(); + + if (reports.length === 0) return null; + + return ( +
+

{UI_LABELS.myReports.title}

+
    + {reports.map((report) => { + const status = NOTIFICATION_STATUS_CONFIG[report.status]; + const category = ITEM_CATEGORY_CONFIG[report.category]; + + return ( +
  • +
    + + +
    +
    +

    + {report.message} +

    + + {status.label} + +
    + +

    {report.location}

    +

    + {UI_LABELS.myReports.reportedAt} {formatRelativeTime(report.createdAt)} + {report.passengerInfo?.tripRoute && ` • ${report.passengerInfo.tripRoute}`} +

    + + {report.response?.notes && ( +
    +

    + {UI_LABELS.myReports.staffNote} +

    +

    {report.response.notes}

    +
    + )} +
    +
    +
  • + ); + })} +
+
+ ); +} diff --git a/frontend/lib/hooks/index.ts b/frontend/lib/hooks/index.ts index c45a533..db09855 100644 --- a/frontend/lib/hooks/index.ts +++ b/frontend/lib/hooks/index.ts @@ -12,3 +12,5 @@ export { useReportLostItem, useApiHealth, } from './useApi'; + +export { useDemoReports } from './useDemoReports'; diff --git a/frontend/lib/hooks/useDemoReports.ts b/frontend/lib/hooks/useDemoReports.ts new file mode 100644 index 0000000..3f58e79 --- /dev/null +++ b/frontend/lib/hooks/useDemoReports.ts @@ -0,0 +1,28 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { readReports, subscribeReports } from '../demo-bus'; +import type { StaffNotification } from '../types'; + +/** + * The reports filed from this browser, with whatever the crew answered. + * + * Empty until mount by design: localStorage does not exist on the server, so + * reading it during render would make the first paint disagree with the HTML + * Next sent. The subscription then keeps the list live — a crew answer landing + * in the other tab updates it without a reload. + */ +export function useDemoReports(): StaffNotification[] { + const [reports, setReports] = useState([]); + + useEffect(() => { + // Loading from an external system, which is the case the + // set-state-in-effect rule's own docs call legitimate — there is no way to + // express "read storage on mount" without it. + // eslint-disable-next-line react-hooks/set-state-in-effect + setReports(readReports()); + return subscribeReports(setReports); + }, []); + + return reports; +} diff --git a/frontend/lib/labels.ts b/frontend/lib/labels.ts index 07f27c1..242c0e2 100644 --- a/frontend/lib/labels.ts +++ b/frontend/lib/labels.ts @@ -159,6 +159,13 @@ export const UI_LABELS = { step3: 'Abholung koordinieren', }, + // The passenger's own reports and how each one ended + myReports: { + title: 'Meine Verlustmeldungen', + staffNote: 'Rückmeldung des Personals', + reportedAt: 'Gemeldet', + }, + // Time time: { justNow: 'gerade eben',