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 */}