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 && (
+
+
+
+ |
+ Name |
+ Country |
+ Status |
+ Visited |
+ With |
+
+
+
+ {sortedCities.map((city) => (
+ navigate(`/cities/${city.id}`)}
+ >
+
+ {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 {