From 9a044ada28fb303b828658ea3d84dd2675f96aca Mon Sep 17 00:00:00 2001 From: Maurice Date: Mon, 30 Mar 2026 11:47:05 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20blur=20booking=20codes=20setting=20+=20?= =?UTF-8?q?two-column=20settings=20page=20=E2=80=94=20closes=20#114?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New display setting "Blur Booking Codes" (off by default) - When enabled, confirmation codes are blurred across all views (ReservationsPanel, DayDetailPanel, Transport detail modal) - Hover or click reveals the code (click toggles on mobile) - Settings page uses masonry two-column layout on desktop, single column on mobile (<900px) - Fix hardcoded admin page title to use i18n key --- .../src/components/Planner/DayDetailPanel.tsx | 8 +++- .../src/components/Planner/DayPlanSidebar.tsx | 27 +++++++++---- .../components/Planner/ReservationsPanel.tsx | 16 +++++++- client/src/i18n/translations/de.ts | 1 + client/src/i18n/translations/en.ts | 1 + client/src/pages/AdminPage.tsx | 2 +- client/src/pages/SettingsPage.tsx | 40 +++++++++++++++++-- client/src/types.ts | 1 + 8 files changed, 83 insertions(+), 13 deletions(-) diff --git a/client/src/components/Planner/DayDetailPanel.tsx b/client/src/components/Planner/DayDetailPanel.tsx index d36cba9..3001b8a 100644 --- a/client/src/components/Planner/DayDetailPanel.tsx +++ b/client/src/components/Planner/DayDetailPanel.tsx @@ -56,6 +56,7 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri const { t, language, locale } = useTranslation() const isFahrenheit = useSettingsStore(s => s.settings.temperature_unit) === 'fahrenheit' const is12h = useSettingsStore(s => s.settings.time_format) === '12h' + const blurCodes = useSettingsStore(s => s.settings.blur_booking_codes) const fmtTime = (v) => formatTime12(v, is12h) const unit = isFahrenheit ? '°F' : '°C' const [weather, setWeather] = useState(null) @@ -368,7 +369,12 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri
{linked.title}
{confirmed ? t('reservations.confirmed') : t('reservations.pending')} - {linked.confirmation_number && #{linked.confirmation_number}} + {linked.confirmation_number && { if (blurCodes) e.currentTarget.style.filter = 'none' }} + onMouseLeave={e => { if (blurCodes) e.currentTarget.style.filter = 'blur(4px)' }} + onClick={e => { if (blurCodes) { const el = e.currentTarget; el.style.filter = el.style.filter === 'none' ? 'blur(4px)' : 'none' } }} + style={{ filter: blurCodes ? 'blur(4px)' : 'none', transition: 'filter 0.2s', cursor: blurCodes ? 'pointer' : 'default' }} + >#{linked.confirmation_number}}
diff --git a/client/src/components/Planner/DayPlanSidebar.tsx b/client/src/components/Planner/DayPlanSidebar.tsx index 277662f..1afcb48 100644 --- a/client/src/components/Planner/DayPlanSidebar.tsx +++ b/client/src/components/Planner/DayPlanSidebar.tsx @@ -1449,7 +1449,7 @@ export default function DayPlanSidebar({ if (meta.platform) detailFields.push({ label: t('reservations.meta.platform'), value: meta.platform }) if (meta.seat) detailFields.push({ label: t('reservations.meta.seat'), value: meta.seat }) } - if (res.confirmation_number) detailFields.push({ label: t('reservations.confirmationCode'), value: res.confirmation_number }) + if (res.confirmation_number) detailFields.push({ label: t('reservations.confirmationCode'), value: res.confirmation_number, sensitive: true }) if (res.location) detailFields.push({ label: t('reservations.locationAddress'), value: res.location }) return ( @@ -1486,12 +1486,25 @@ export default function DayPlanSidebar({ {/* Detail-Felder */} {detailFields.length > 0 && (
- {detailFields.map((f, i) => ( -
-
{f.label}
-
{f.value}
-
- ))} + {detailFields.map((f, i) => { + const shouldBlur = f.sensitive && useSettingsStore.getState().settings.blur_booking_codes + return ( +
+
{f.label}
+
{ if (shouldBlur) e.currentTarget.style.filter = 'none' }} + onMouseLeave={e => { if (shouldBlur) e.currentTarget.style.filter = 'blur(5px)' }} + onClick={e => { if (shouldBlur) { const el = e.currentTarget; el.style.filter = el.style.filter === 'none' ? 'blur(5px)' : 'none' } }} + style={{ + fontSize: 12, fontWeight: 500, color: 'var(--text-primary)', wordBreak: 'break-word', + filter: shouldBlur ? 'blur(5px)' : 'none', transition: 'filter 0.2s', + cursor: shouldBlur ? 'pointer' : 'default', + userSelect: shouldBlur ? 'none' : 'auto', + }} + >{f.value}
+
+ ) + })}
)} diff --git a/client/src/components/Planner/ReservationsPanel.tsx b/client/src/components/Planner/ReservationsPanel.tsx index cd36f06..987ff18 100644 --- a/client/src/components/Planner/ReservationsPanel.tsx +++ b/client/src/components/Planner/ReservationsPanel.tsx @@ -63,6 +63,8 @@ function ReservationCard({ r, tripId, onEdit, onDelete, files = [], onNavigateTo const toast = useToast() const { t, locale } = useTranslation() const timeFormat = useSettingsStore(s => s.settings.time_format) || '24h' + const blurCodes = useSettingsStore(s => s.settings.blur_booking_codes) + const [codeRevealed, setCodeRevealed] = useState(false) const typeInfo = getType(r.type) const TypeIcon = typeInfo.Icon const confirmed = r.status === 'confirmed' @@ -136,7 +138,19 @@ function ReservationCard({ r, tripId, onEdit, onDelete, files = [], onNavigateTo {r.confirmation_number && (
{t('reservations.confirmationCode')}
-
{r.confirmation_number}
+
blurCodes && setCodeRevealed(true)} + onMouseLeave={() => blurCodes && setCodeRevealed(false)} + onClick={() => blurCodes && setCodeRevealed(v => !v)} + style={{ + fontSize: 11, fontWeight: 600, color: 'var(--text-primary)', marginTop: 1, + filter: blurCodes && !codeRevealed ? 'blur(5px)' : 'none', + cursor: blurCodes ? 'pointer' : 'default', + transition: 'filter 0.2s', + }} + > + {r.confirmation_number} +
)} diff --git a/client/src/i18n/translations/de.ts b/client/src/i18n/translations/de.ts index 0aea7ff..9449693 100644 --- a/client/src/i18n/translations/de.ts +++ b/client/src/i18n/translations/de.ts @@ -139,6 +139,7 @@ const de: Record = { 'settings.temperature': 'Temperatureinheit', 'settings.timeFormat': 'Zeitformat', 'settings.routeCalculation': 'Routenberechnung', + 'settings.blurBookingCodes': 'Buchungscodes verbergen', 'settings.on': 'An', 'settings.off': 'Aus', 'settings.account': 'Konto', diff --git a/client/src/i18n/translations/en.ts b/client/src/i18n/translations/en.ts index 46e6310..95e2018 100644 --- a/client/src/i18n/translations/en.ts +++ b/client/src/i18n/translations/en.ts @@ -139,6 +139,7 @@ const en: Record = { 'settings.temperature': 'Temperature Unit', 'settings.timeFormat': 'Time Format', 'settings.routeCalculation': 'Route Calculation', + 'settings.blurBookingCodes': 'Blur Booking Codes', 'settings.on': 'On', 'settings.off': 'Off', 'settings.account': 'Account', diff --git a/client/src/pages/AdminPage.tsx b/client/src/pages/AdminPage.tsx index b63306f..b729a61 100644 --- a/client/src/pages/AdminPage.tsx +++ b/client/src/pages/AdminPage.tsx @@ -333,7 +333,7 @@ export default function AdminPage(): React.ReactElement {
-

Administration

+

{t('admin.title')}

{t('admin.subtitle')}

diff --git a/client/src/pages/SettingsPage.tsx b/client/src/pages/SettingsPage.tsx index 7846f5e..49b5c8a 100644 --- a/client/src/pages/SettingsPage.tsx +++ b/client/src/pages/SettingsPage.tsx @@ -34,7 +34,7 @@ interface SectionProps { function Section({ title, icon: Icon, children }: SectionProps): React.ReactElement { return ( -
+

{title}

@@ -220,12 +220,15 @@ export default function SettingsPage(): React.ReactElement {
-
-
+
+ +

{t('settings.title')}

{t('settings.subtitle')}

+
+ {/* Map settings */}
@@ -439,6 +442,36 @@ export default function SettingsPage(): React.ReactElement { ))}
+ + {/* Blur Booking Codes */} +
+ +
+ {[ + { value: true, label: t('settings.on') || 'On' }, + { value: false, label: t('settings.off') || 'Off' }, + ].map(opt => ( + + ))} +
+
{/* Immich — only when Memories addon is enabled */} @@ -888,6 +921,7 @@ export default function SettingsPage(): React.ReactElement {
)} +
diff --git a/client/src/types.ts b/client/src/types.ts index e5913ab..f0da1ee 100644 --- a/client/src/types.ts +++ b/client/src/types.ts @@ -171,6 +171,7 @@ export interface Settings { time_format: string show_place_description: boolean route_calculation?: boolean + blur_booking_codes?: boolean } export interface AssignmentsMap {