diff --git a/src/App.tsx b/src/App.tsx index 93607a0..9d5b9db 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,7 @@ import { PlaceDetail, type ReachState, type RefreshState } from './components/Pl import { DEFAULT_FILTERS, PlacesView, filterPlaces, type Filters } from './components/PlacesView'; import { ProfileView } from './components/ProfileView'; import { RouteView, type RouteForm } from './components/RouteView'; +import { LangToggle } from './components/LangToggle'; import { SettingsPanel } from './components/SettingsPanel'; import { SheetHandle, type Sheet } from './components/SheetHandle'; import { Alert, Icon } from './components/ui'; @@ -19,6 +20,7 @@ import type { LatLon } from './lib/geo'; import { I18nContext, formatDate, makeT, type I18n, type Key } from './lib/i18n'; import { buildLabels } from './lib/labels'; import { refreshPlace } from './lib/live'; +import { withCityOffer } from './lib/osmPlace'; import { DEFAULT_PROFILE, isProfile } from './lib/profile'; import { computeReach, placesInReach, type Reach } from './lib/reach'; import { buildGraph, type RouteOption } from './lib/route'; @@ -109,7 +111,7 @@ export default function App() { const index = useMemo(() => (snap ? buildIndex(snap) : null), [snap]); const graph = useMemo(() => (snap ? buildGraph(snap) : null), [snap]); const [overrides, setOverrides] = useState>({}); - const realPlaces = useMemo(() => (snap ? snap.places.map((p) => overrides[p.id] ?? p) : []), [snap, overrides]); + const realPlaces = useMemo(() => (snap ? snap.places.map((p) => withCityOffer(overrides[p.id] ?? p)) : []), [snap, overrides]); const places = useMemo(() => (settings.showDemoPlaces ? [...DEMO_PLACES, ...realPlaces] : realPlaces), [realPlaces, settings.showDemoPlaces]); const reports = useMemo( () => [...DEMO_REPORTS, ...userReports].map((r) => (decisions[r.id] ? { ...r, ...decisions[r.id] } : r)), @@ -133,6 +135,11 @@ export default function App() { const [filters, setFilters] = useState(DEFAULT_FILTERS); const favSet = useMemo(() => new Set(favorites), [favorites]); const results = useMemo(() => filterPlaces(places, assessments, filters, favSet), [places, assessments, filters, favSet]); + const catCounts = useMemo(() => { + const c: Record = {}; + for (const p of filterPlaces(places, assessments, { ...filters, cat: 'all' }, favSet)) { c[p.cat] = (c[p.cat] ?? 0) + 1; c.all = (c.all ?? 0) + 1; } + return c; + }, [places, assessments, filters, favSet]); const mapPlaces = useMemo( () => results.flatMap((place) => { const a = assessments.get(place.id); return a ? [{ place, verdict: a.verdict }] : []; }), [results, assessments], @@ -386,17 +393,10 @@ export default function App() { {account.id === 'guest' ? t('auth.role.guest') : t(`auth.${account.id}` as Key)} -
- {(['pl', 'en'] as const).map((l) => ( - - ))} -
+ + @@ -481,7 +481,6 @@ export default function App() { onClearReach={clearReach} onBack={back} onRouteTo={() => routeTo(selected)} - onShowOnMap={() => setPane('map')} onRefresh={() => void doRefresh(selected)} onAddReport={(r, photo) => void addReport(r, photo)} onRemoveReport={(id) => { void deletePhoto(id); setUserReports((cur) => cur.filter((r) => r.id !== id)); }} @@ -489,7 +488,7 @@ export default function App() { ) : ( ))} {view === 'route' && ( diff --git a/src/components/AccessCard.tsx b/src/components/AccessCard.tsx index 6ac9ebf..6fc08a4 100644 --- a/src/components/AccessCard.tsx +++ b/src/components/AccessCard.tsx @@ -1,8 +1,11 @@ import { useEffect, useRef } from 'react'; import { useI18n } from '../lib/i18n'; import type { Assessment, Place, Profile } from '../lib/types'; +import { BrandMark } from './BrandMark'; import { Icon, SourceLine, StatusLabel, VerdictTag } from './ui'; +const VERDICT_COLOR: Record = { match: '#00a91c', partial: '#ffbe2e', conflict: '#a2461c', incomplete: '#71767a', fail: '#b50909' }; + interface Props { place: Place; assessment: Assessment; profile: Profile; onClose: () => void } /** Printable one-page summary: for a venue's website, a carer, or a council officer. */ @@ -22,32 +25,47 @@ export function AccessCard({ place, assessment, profile, onClose }: Props) {
-

{t('card.title')} · {t('app.name')}

-

{place.name}

- {address &&

{address}

} -

{t('card.for', { profile: t(`profile.${profile.kind}`) })}

-

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

-
    +
    + +
    + {t('app.name')} + {t('card.title')} +
    + {date(new Date().toISOString().slice(0, 10))} +
    + +

    {place.name}

    + {address &&

    {address}

    } + +
    +

    {t('card.for', { profile: t(`profile.${profile.kind}`) })}

    +

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

    +
    + +
      {assessment.facts.map((f) => ( -
    • -
      +
    • +
      {t(`fact.title.${f.key}`)}

      {tm(f.headline)}

      - {f.alt &&

      {t('fact.otherSource')}: {tm(f.alt.headline)}

      } + {f.alt &&

      {t('fact.otherSource')}: {tm(f.alt.headline)}

      }
    • ))}
    -

    {t('card.legend')}

    -

    {t('card.generated', { date: date(new Date().toISOString().slice(0, 10)) })} {t('footer.attr')}

    + +
    +

    {t('card.legend')}

    +

    {t('card.generated', { date: date(new Date().toISOString().slice(0, 10)) })} {t('footer.attr')}

    +
); diff --git a/src/components/BrandMark.tsx b/src/components/BrandMark.tsx new file mode 100644 index 0000000..f04370b --- /dev/null +++ b/src/components/BrandMark.tsx @@ -0,0 +1,12 @@ +/** App logo (same artwork as public/icons/icon.svg): decorative, the app name is always written next to it. */ +export function BrandMark({ size = 40, className = '' }: { size?: number; className?: string }) { + return ( + + ); +} diff --git a/src/components/DashData.tsx b/src/components/DashData.tsx index 8619715..c5704f2 100644 --- a/src/components/DashData.tsx +++ b/src/components/DashData.tsx @@ -1,7 +1,7 @@ import { useI18n, type Key } from '../lib/i18n'; import type { Stats } from '../lib/civic'; import type { Place, UserReport } from '../lib/types'; -import { Alert } from './ui'; +import { Alert, Icon } from './ui'; const pct = (n: number, d: number) => (d ? Math.round((n / d) * 100) : 0); @@ -18,80 +18,124 @@ function agreement(reports: UserReport[]): { agree: number; total: number } { return { agree, total }; } -/** Data-quality overview: KPIs, the transit finding, gaps by category and anonymous demand signals. */ -export function DashData({ stats, reports, demand }: { stats: Stats; reports: UserReport[]; demand: { place: Place; views: number }[] }) { +/** Decorative bar; the numbers next to it carry the information. */ +function Meter({ value, className = '' }: { value: number; className?: string }) { + return