From e91b79ebfc50280051ccbf079df8cfe7a725a928 Mon Sep 17 00:00:00 2001 From: Maurice Date: Sun, 29 Mar 2026 11:10:33 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20add=20list/grid=20view=20toggle=20on=20?= =?UTF-8?q?dashboard=20=E2=80=94=20closes=20#73?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/i18n/translations/de.ts | 2 + client/src/i18n/translations/en.ts | 2 + client/src/i18n/translations/es.ts | 2 + client/src/i18n/translations/fr.ts | 2 + client/src/i18n/translations/nl.ts | 2 + client/src/i18n/translations/ru.ts | 2 + client/src/i18n/translations/zh.ts | 2 + client/src/pages/DashboardPage.tsx | 172 ++++++++++++++++++++++++++--- 8 files changed, 169 insertions(+), 17 deletions(-) diff --git a/client/src/i18n/translations/de.ts b/client/src/i18n/translations/de.ts index b0268ef..b18252c 100644 --- a/client/src/i18n/translations/de.ts +++ b/client/src/i18n/translations/de.ts @@ -51,6 +51,8 @@ const de: Record = { 'dashboard.subtitle.activeMany': '{count} aktive Reisen', 'dashboard.subtitle.archivedSuffix': ' · {count} archiviert', 'dashboard.newTrip': 'Neue Reise', + 'dashboard.gridView': 'Kachelansicht', + 'dashboard.listView': 'Listenansicht', 'dashboard.currency': 'Währung', 'dashboard.timezone': 'Zeitzonen', 'dashboard.localTime': 'Lokal', diff --git a/client/src/i18n/translations/en.ts b/client/src/i18n/translations/en.ts index 48fb3c4..a58eb0b 100644 --- a/client/src/i18n/translations/en.ts +++ b/client/src/i18n/translations/en.ts @@ -51,6 +51,8 @@ const en: Record = { 'dashboard.subtitle.activeMany': '{count} active trips', 'dashboard.subtitle.archivedSuffix': ' · {count} archived', 'dashboard.newTrip': 'New Trip', + 'dashboard.gridView': 'Grid view', + 'dashboard.listView': 'List view', 'dashboard.currency': 'Currency', 'dashboard.timezone': 'Timezones', 'dashboard.localTime': 'Local', diff --git a/client/src/i18n/translations/es.ts b/client/src/i18n/translations/es.ts index 650edc9..a82e25e 100644 --- a/client/src/i18n/translations/es.ts +++ b/client/src/i18n/translations/es.ts @@ -52,6 +52,8 @@ const es: Record = { 'dashboard.subtitle.activeMany': '{count} viajes activos', 'dashboard.subtitle.archivedSuffix': ' · {count} archivados', 'dashboard.newTrip': 'Nuevo viaje', + 'dashboard.gridView': 'Vista de cuadrícula', + 'dashboard.listView': 'Vista de lista', 'dashboard.currency': 'Divisa', 'dashboard.timezone': 'Zonas horarias', 'dashboard.localTime': 'Hora local', diff --git a/client/src/i18n/translations/fr.ts b/client/src/i18n/translations/fr.ts index 12dcc79..59aa4ce 100644 --- a/client/src/i18n/translations/fr.ts +++ b/client/src/i18n/translations/fr.ts @@ -51,6 +51,8 @@ const fr: Record = { 'dashboard.subtitle.activeMany': '{count} voyages actifs', 'dashboard.subtitle.archivedSuffix': ' · {count} archivés', 'dashboard.newTrip': 'Nouveau voyage', + 'dashboard.gridView': 'Vue en grille', + 'dashboard.listView': 'Vue en liste', 'dashboard.currency': 'Devise', 'dashboard.timezone': 'Fuseaux horaires', 'dashboard.localTime': 'Local', diff --git a/client/src/i18n/translations/nl.ts b/client/src/i18n/translations/nl.ts index 42e8182..8841a89 100644 --- a/client/src/i18n/translations/nl.ts +++ b/client/src/i18n/translations/nl.ts @@ -51,6 +51,8 @@ const nl: Record = { 'dashboard.subtitle.activeMany': '{count} actieve reizen', 'dashboard.subtitle.archivedSuffix': ' · {count} gearchiveerd', 'dashboard.newTrip': 'Nieuwe reis', + 'dashboard.gridView': 'Rasterweergave', + 'dashboard.listView': 'Lijstweergave', 'dashboard.currency': 'Valuta', 'dashboard.timezone': 'Tijdzones', 'dashboard.localTime': 'Lokaal', diff --git a/client/src/i18n/translations/ru.ts b/client/src/i18n/translations/ru.ts index 1610762..78235b3 100644 --- a/client/src/i18n/translations/ru.ts +++ b/client/src/i18n/translations/ru.ts @@ -51,6 +51,8 @@ const ru: Record = { 'dashboard.subtitle.activeMany': '{count} активных поездок', 'dashboard.subtitle.archivedSuffix': ' · {count} в архиве', 'dashboard.newTrip': 'Новая поездка', + 'dashboard.gridView': 'Плитка', + 'dashboard.listView': 'Список', 'dashboard.currency': 'Валюта', 'dashboard.timezone': 'Часовые пояса', 'dashboard.localTime': 'Местное', diff --git a/client/src/i18n/translations/zh.ts b/client/src/i18n/translations/zh.ts index 8629767..7de8387 100644 --- a/client/src/i18n/translations/zh.ts +++ b/client/src/i18n/translations/zh.ts @@ -51,6 +51,8 @@ const zh: Record = { 'dashboard.subtitle.activeMany': '{count} 个进行中的旅行', 'dashboard.subtitle.archivedSuffix': ' · {count} 已归档', 'dashboard.newTrip': '新建旅行', + 'dashboard.gridView': '网格视图', + 'dashboard.listView': '列表视图', 'dashboard.currency': '货币', 'dashboard.timezone': '时区', 'dashboard.localTime': '本地', diff --git a/client/src/pages/DashboardPage.tsx b/client/src/pages/DashboardPage.tsx index c9b2e75..4dc144a 100644 --- a/client/src/pages/DashboardPage.tsx +++ b/client/src/pages/DashboardPage.tsx @@ -14,6 +14,7 @@ import { useToast } from '../components/shared/Toast' import { Plus, Calendar, Trash2, Edit2, Map, ChevronDown, ChevronUp, Archive, ArchiveRestore, Clock, MapPin, Settings, X, ArrowRightLeft, + LayoutGrid, List, } from 'lucide-react' interface DashboardTrip { @@ -315,6 +316,102 @@ function TripCard({ trip, onEdit, onDelete, onArchive, onClick, t, locale }: Omi ) } +// ── List View Item ────────────────────────────────────────────────────────── +function TripListItem({ trip, onEdit, onDelete, onArchive, onClick, t, locale }: Omit): React.ReactElement { + const status = getTripStatus(trip) + const [hovered, setHovered] = useState(false) + + const coverBg = trip.cover_image + ? `url(${trip.cover_image}) center/cover no-repeat` + : tripGradient(trip.id) + + return ( +
setHovered(true)} + onMouseLeave={() => setHovered(false)} + onClick={() => onClick(trip)} + style={{ + display: 'flex', alignItems: 'center', gap: 14, padding: '10px 16px', + background: hovered ? 'var(--bg-tertiary)' : 'var(--bg-card)', borderRadius: 14, + border: `1px solid ${hovered ? 'var(--text-faint)' : 'var(--border-primary)'}`, + cursor: 'pointer', transition: 'all 0.15s', + boxShadow: hovered ? '0 4px 16px rgba(0,0,0,0.08)' : '0 1px 3px rgba(0,0,0,0.03)', + }} + > + {/* Cover thumbnail */} +
+ {status === 'ongoing' && ( + + )} +
+ + {/* Title & description */} +
+
+ + {trip.title} + + {!trip.is_owner && ( + + {t('dashboard.shared')} + + )} + {status && ( + + {status === 'ongoing' ? t('dashboard.status.ongoing') + : status === 'today' ? t('dashboard.status.today') + : status === 'tomorrow' ? t('dashboard.status.tomorrow') + : status === 'future' ? t('dashboard.status.daysLeft', { count: daysUntil(trip.start_date) }) + : t('dashboard.status.past')} + + )} +
+ {trip.description && ( +

+ {trip.description} +

+ )} +
+ + {/* Date & stats */} +
+ {trip.start_date && ( +
+ + {formatDateShort(trip.start_date, locale)} + {trip.end_date && <> — {formatDateShort(trip.end_date, locale)}} +
+ )} +
+ {trip.day_count || 0} +
+
+ {trip.place_count || 0} +
+
+ + {/* Actions */} +
e.stopPropagation()}> + onEdit(trip)} icon={} label="" /> + onArchive(trip.id)} icon={} label="" /> + onDelete(trip)} icon={} label="" danger /> +
+
+ ) +} + // ── Archived Trip Row ──────────────────────────────────────────────────────── interface ArchivedRowProps { trip: DashboardTrip @@ -429,6 +526,15 @@ export default function DashboardPage(): React.ReactElement { const [editingTrip, setEditingTrip] = useState(null) const [showArchived, setShowArchived] = useState(false) const [showWidgetSettings, setShowWidgetSettings] = useState(false) + const [viewMode, setViewMode] = useState<'grid' | 'list'>(() => (localStorage.getItem('trek_dashboard_view') as 'grid' | 'list') || 'grid') + + const toggleViewMode = () => { + setViewMode(prev => { + const next = prev === 'grid' ? 'list' : 'grid' + localStorage.setItem('trek_dashboard_view', next) + return next + }) + } const navigate = useNavigate() const toast = useToast() @@ -554,6 +660,22 @@ export default function DashboardPage(): React.ReactElement {

+ {/* View mode toggle */} + {/* Widget settings */}
)} - {/* Spotlight */} - {!isLoading && spotlight && ( + {/* Spotlight (grid mode only) */} + {!isLoading && spotlight && viewMode === 'grid' && ( )} - {/* Rest grid */} - {!isLoading && rest.length > 0 && ( -
- {rest.map(trip => ( - { setEditingTrip(tr); setShowForm(true) }} - onDelete={handleDelete} - onArchive={handleArchive} - onClick={tr => navigate(`/trips/${tr.id}`)} - /> - ))} -
+ {/* Trips — grid or list */} + {!isLoading && (viewMode === 'grid' ? rest : trips).length > 0 && ( + viewMode === 'grid' ? ( +
+ {rest.map(trip => ( + { setEditingTrip(tr); setShowForm(true) }} + onDelete={handleDelete} + onArchive={handleArchive} + onClick={tr => navigate(`/trips/${tr.id}`)} + /> + ))} +
+ ) : ( +
+ {trips.map(trip => ( + { setEditingTrip(tr); setShowForm(true) }} + onDelete={handleDelete} + onArchive={handleArchive} + onClick={tr => navigate(`/trips/${tr.id}`)} + /> + ))} +
+ ) )} {/* Archived section */}