import { forwardRef, useState } from 'react'; import { useI18n } from '../lib/i18n'; import { DEMO_PLACES } from '../lib/demo'; import type { Assessment, Place, ReportAttr, 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; onShowOnMap: () => void; onRefresh: () => void; onAddReport: (r: Omit, photo: ProcessedPhoto | null) => void; onRemoveReport: (id: string) => void; } 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 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')}

{address &&

{address}

} {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(', ') })}

}

{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')}

    {assessment.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)} />)}
)}
{!place.demo && ( <> {t('detail.openOsm')} )} {place.demo &&

{t('detail.refreshDemo')}

}
{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 && ( )}
  • ); }