import { forwardRef, useState } from 'react'; import { googleMapsPlaceUrl } from '../lib/links'; import { useI18n, type Key } from '../lib/i18n'; import { DEMO_PLACES } from '../lib/demo'; import type { Assessment, Place, ReportAttr, Status, UserReport } from '../lib/types'; import { Alert, FactCard, Icon, VERDICT_STYLE, VerdictTag, VettingSummary } from './ui'; import { ReportForm } from './ReportForm'; import type { ProcessedPhoto } from '../lib/photo'; import { usePhoto } from '../lib/usePhoto'; export interface ReachState { minutes: number | null; summary: { n: number; fit: number } | 'stuck' | null } export type RefreshState = | { kind: 'idle' } | { kind: 'loading' } | { kind: 'ok'; at: string } | { kind: 'fail' }; interface Props { place: Place; assessment: Assessment; reports: UserReport[]; snapshotDate: string; refresh: RefreshState; saved: boolean; onToggleSave: () => void; onCard: () => void; reach: ReachState; onReach: (minutes: number) => void; onClearReach: () => void; onBack: () => void; onRouteTo: () => void; onRefresh: () => void; onAddReport: (r: Omit, photo: ProcessedPhoto | null) => void; onRemoveReport: (id: string) => void; } /** Worst news first: barriers, then conflicting data, cautions, what is fine, and what is unknown. */ const FACT_GROUPS: Status[] = ['fail', 'conflict', 'warn', 'ok', 'unknown']; const FACT_ICON: Record = { fail: 'error', conflict: 'report', warn: 'warning', ok: 'check_circle', unknown: 'help' }; const FACT_TONE: Record = { fail: 'text-error-dark', conflict: 'text-accent-warm-darker', warn: 'text-warning-darker', ok: 'text-success-dark', unknown: 'text-base-dark' }; export const PlaceDetail = forwardRef(function PlaceDetail(p, headingRef) { const { t, tm, date } = useI18n(); const [reporting, setReporting] = useState(null); const [thanks, setThanks] = useState(false); const { place, assessment } = p; const demo = DEMO_PLACES.find((d) => d.id === place.id); const address = [place.tags['addr:street'], place.tags['addr:housenumber']].filter(Boolean).join(' '); const gmaps = googleMapsPlaceUrl([place.lat, place.lon]); const [reachOpen, setReachOpen] = useState(false); const mine = p.reports.filter((r) => r.placeId === place.id); const missing = assessment.facts.filter((f) => f.required && f.status === 'unknown').map((f) => t(`fact.title.${f.key}`).toLowerCase()); return (

{place.name ?? t('common.noName')} {t('detail.gmaps')}

{address && (

{address} {t('detail.gmaps')}

)} {demo && (
{t('detail.demo')}

{t('detail.demoNote', { state: t(`verdict.${demoState(demo.demoState)}`) })}

)}

{t(`verdict.${assessment.verdict}.long`)}

{missing.length > 0 &&

{t('verdict.missing', { list: missing.join(', ') })}

}
setReachOpen(e.currentTarget.open)} > {t('reach.title')}

{t('reach.intro')}

    {[5, 10, 15].map((m) => (
  • ))}
{p.reach.summary === 'stuck' &&

{t('reach.stuck')}

} {p.reach.summary && p.reach.summary !== 'stuck' && p.reach.minutes && ( <>

{p.reach.summary.n === 0 ? t('reach.result.zero') : t('reach.result', { min: p.reach.minutes, n: p.reach.summary.n, fit: p.reach.summary.fit })}

{t('reach.show')} {t('reach.caveat')}

)}

{t('detail.facts')}

{FACT_GROUPS.map((status) => { const facts = assessment.facts.filter((f) => f.status === status); if (!facts.length) return null; return (

{t(`detail.group.${status}` as Key)} ({facts.length})

    {facts.map((f) => ( { setReporting(f.key as ReportAttr); setThanks(false); }} /> ))}
); })} {thanks && {t('rep.thanks')}} {reporting && ( setReporting(null)} onSubmit={(r, photo) => { p.onAddReport(r, photo); setReporting(null); setThanks(true); }} /> )}

{t('detail.reports')}

{t('detail.reports.note')}

{mine.length === 0 ? (

{t('detail.reports.none')}

) : (
    {mine.map((r) => p.onRemoveReport(r.id)} />)}
)}

{t('detail.tools')}

    {place.demo ? (
  • {t('detail.refreshDemo')}

  • ) : ( <>
  • {t('detail.openOsm')}
  • )}
{p.refresh.kind === 'ok' && {t('detail.refreshed', { time: new Date(p.refresh.at).toLocaleTimeString() })}} {p.refresh.kind === 'fail' && {t('detail.refreshFail', { date: date(p.snapshotDate) })}}
); }); const demoState = (s: string) => (s === 'verified' ? 'match' : s === 'conflict' ? 'conflict' : 'incomplete'); function osmPath(id: string) { const kind = id[0] === 'n' ? 'node' : id[0] === 'w' ? 'way' : 'relation'; return `${kind}/${id.slice(1)}`; } function ReportItem({ r, onRemove }: { r: UserReport; onRemove: () => void }) { const { t, date } = useI18n(); const photo = usePhoto(r.photoId); return (
  • {photo && {t('rep.photo.alt')}}
    {t(`rep.attr.${r.attr}`)}:{' '} {typeof r.value === 'number' ? `${r.value} ${t('unit.cm')}` : t(`rep.${r.value}`)} {r.demo && {t('detail.reports.demo')}} — {date(r.createdAt.slice(0, 10))} {r.measure &&
    {t(`vet.measure.${r.measure.kind}`)}: {r.measure.value} {r.measure.unit === 'cm' ? 'cm' : '%'} ({t(`vet.method.${r.measure.method}`)})
    } {r.note &&
    {r.note}
    } {!r.demo && ( )}
  • ); }