From 9b35f6222f55d92b8b3b702bdab506782f3a7a2b Mon Sep 17 00:00:00 2001 From: Claudio Schaad Date: Tue, 21 Jul 2026 20:23:14 +0200 Subject: [PATCH] Add a city list view New nav item between Map and Users shows every tracked city as a table (photo, name, country, status, visit dates, co-travellers), ordered with planned trips first and newest visits on top within each group. --- client/src/App.tsx | 2 + client/src/components/layout/AppShell.tsx | 1 + client/src/pages/CityListPage.tsx | 95 +++++++++++++++++++++++ client/src/styles/global.css | 69 ++++++++++++++++ 4 files changed, 167 insertions(+) create mode 100644 client/src/pages/CityListPage.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index 19ac239..72d9140 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -6,6 +6,7 @@ import { AppShell } from './components/layout/AppShell'; import { LoginPage } from './pages/LoginPage'; import { SetupPage } from './pages/SetupPage'; import { MapDashboardPage } from './pages/MapDashboardPage'; +import { CityListPage } from './pages/CityListPage'; import { CityDetailPage } from './pages/CityDetailPage'; import { AdminUsersPage } from './pages/AdminUsersPage'; @@ -30,6 +31,7 @@ function AppRoutes() { } > } /> + } /> } /> } /> } /> diff --git a/client/src/components/layout/AppShell.tsx b/client/src/components/layout/AppShell.tsx index 468d822..aba62ae 100644 --- a/client/src/components/layout/AppShell.tsx +++ b/client/src/components/layout/AppShell.tsx @@ -18,6 +18,7 @@ export function AppShell() { Map + List {user?.isAdmin && Users}
diff --git a/client/src/pages/CityListPage.tsx b/client/src/pages/CityListPage.tsx new file mode 100644 index 0000000..65720b9 --- /dev/null +++ b/client/src/pages/CityListPage.tsx @@ -0,0 +1,95 @@ +import { useMemo } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAuth } from '../context/AuthContext'; +import { useCities } from '../hooks/useCities'; + +export function CityListPage() { + const navigate = useNavigate(); + const { user } = useAuth(); + const { data: cities, isLoading } = useCities({}); + + const sortedCities = useMemo( + () => + [...(cities ?? [])].sort((a, b) => { + if (a.status !== b.status) return a.status === 'planned' ? -1 : 1; + if (!a.visitDateStart && !b.visitDateStart) return 0; + if (!a.visitDateStart) return 1; + if (!b.visitDateStart) return -1; + return b.visitDateStart.localeCompare(a.visitDateStart); + }), + [cities], + ); + + return ( +
+

Cities

+ + {isLoading &&
Loading cities…
} + + {!isLoading && sortedCities.length === 0 &&

No cities yet.

} + + {!isLoading && sortedCities.length > 0 && ( + + + + + + + + + + + + + {sortedCities.map((city) => ( + navigate(`/cities/${city.id}`)} + > + + + + + + + + ))} + +
NameCountryStatusVisitedWith
+ {city.photoFilename ? ( + + ) : ( +
+ )} +
+ {city.name} + {city.liked && ♥} + {city.country} + + {city.status === 'visited' ? 'Visited' : 'Planned'} + + + {city.visitDateStart + ? `${city.visitDateStart}${ + city.visitDateEnd && city.visitDateEnd !== city.visitDateStart + ? ` – ${city.visitDateEnd}` + : '' + }` + : '—'} + + {(() => { + const others = city.participants.filter((p) => p.id !== user?.id); + return others.length > 0 + ? others.map((p) => p.displayName || p.username).join(', ') + : '—'; + })()} +
+ )} +
+ ); +} diff --git a/client/src/styles/global.css b/client/src/styles/global.css index 3d18051..983815c 100644 --- a/client/src/styles/global.css +++ b/client/src/styles/global.css @@ -572,6 +572,75 @@ a { margin-bottom: var(--spacing-2); } +/* City list */ + +.city-list-page { + max-width: 900px; + margin: 0 auto; + padding: var(--spacing-4); + display: flex; + flex-direction: column; + gap: var(--spacing-4); +} + +.cities-table { + width: 100%; + border-collapse: collapse; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius); + overflow: hidden; +} + +.cities-table th, +.cities-table td { + text-align: left; + padding: var(--spacing-2) var(--spacing-3); + border-bottom: 1px solid var(--color-border); + font-size: 0.9rem; +} + +.cities-table-row { + cursor: pointer; +} + +.cities-table-row:hover { + background: var(--color-bg); +} + +.cities-table-photo { + width: 40px; + height: 40px; + border-radius: var(--radius); + object-fit: cover; + display: block; +} + +.cities-table-photo-empty { + background: var(--color-bg); + border: 1px solid var(--color-border); +} + +.cities-table-heart { + color: #e64980; +} + +.badge { + display: inline-block; + padding: 2px var(--spacing-2); + border-radius: 999px; + font-size: 0.8rem; + color: #fff; +} + +.badge-visited { + background: var(--color-success); +} + +.badge-planned { + background: var(--color-primary); +} + /* Admin users */ .admin-users-page {