mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
feat: blur booking codes setting + two-column settings page — closes #114
- 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
This commit is contained in:
@@ -56,6 +56,7 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri
|
|||||||
const { t, language, locale } = useTranslation()
|
const { t, language, locale } = useTranslation()
|
||||||
const isFahrenheit = useSettingsStore(s => s.settings.temperature_unit) === 'fahrenheit'
|
const isFahrenheit = useSettingsStore(s => s.settings.temperature_unit) === 'fahrenheit'
|
||||||
const is12h = useSettingsStore(s => s.settings.time_format) === '12h'
|
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 fmtTime = (v) => formatTime12(v, is12h)
|
||||||
const unit = isFahrenheit ? '°F' : '°C'
|
const unit = isFahrenheit ? '°F' : '°C'
|
||||||
const [weather, setWeather] = useState(null)
|
const [weather, setWeather] = useState(null)
|
||||||
@@ -368,7 +369,12 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri
|
|||||||
<div style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-primary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{linked.title}</div>
|
<div style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-primary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{linked.title}</div>
|
||||||
<div style={{ fontSize: 9, color: 'var(--text-faint)', display: 'flex', gap: 6, marginTop: 1 }}>
|
<div style={{ fontSize: 9, color: 'var(--text-faint)', display: 'flex', gap: 6, marginTop: 1 }}>
|
||||||
<span>{confirmed ? t('reservations.confirmed') : t('reservations.pending')}</span>
|
<span>{confirmed ? t('reservations.confirmed') : t('reservations.pending')}</span>
|
||||||
{linked.confirmation_number && <span>#{linked.confirmation_number}</span>}
|
{linked.confirmation_number && <span
|
||||||
|
onMouseEnter={e => { 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}</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1449,7 +1449,7 @@ export default function DayPlanSidebar({
|
|||||||
if (meta.platform) detailFields.push({ label: t('reservations.meta.platform'), value: meta.platform })
|
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 (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 })
|
if (res.location) detailFields.push({ label: t('reservations.locationAddress'), value: res.location })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1486,12 +1486,25 @@ export default function DayPlanSidebar({
|
|||||||
{/* Detail-Felder */}
|
{/* Detail-Felder */}
|
||||||
{detailFields.length > 0 && (
|
{detailFields.length > 0 && (
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
|
||||||
{detailFields.map((f, i) => (
|
{detailFields.map((f, i) => {
|
||||||
<div key={i} style={{ padding: '8px 10px', background: 'var(--bg-tertiary)', borderRadius: 8 }}>
|
const shouldBlur = f.sensitive && useSettingsStore.getState().settings.blur_booking_codes
|
||||||
<div style={{ fontSize: 9, fontWeight: 600, color: 'var(--text-faint)', textTransform: 'uppercase', letterSpacing: '0.03em', marginBottom: 3 }}>{f.label}</div>
|
return (
|
||||||
<div style={{ fontSize: 12, fontWeight: 500, color: 'var(--text-primary)', wordBreak: 'break-word' }}>{f.value}</div>
|
<div key={i} style={{ padding: '8px 10px', background: 'var(--bg-tertiary)', borderRadius: 8 }}>
|
||||||
</div>
|
<div style={{ fontSize: 9, fontWeight: 600, color: 'var(--text-faint)', textTransform: 'uppercase', letterSpacing: '0.03em', marginBottom: 3 }}>{f.label}</div>
|
||||||
))}
|
<div
|
||||||
|
onMouseEnter={e => { 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}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ function ReservationCard({ r, tripId, onEdit, onDelete, files = [], onNavigateTo
|
|||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const { t, locale } = useTranslation()
|
const { t, locale } = useTranslation()
|
||||||
const timeFormat = useSettingsStore(s => s.settings.time_format) || '24h'
|
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 typeInfo = getType(r.type)
|
||||||
const TypeIcon = typeInfo.Icon
|
const TypeIcon = typeInfo.Icon
|
||||||
const confirmed = r.status === 'confirmed'
|
const confirmed = r.status === 'confirmed'
|
||||||
@@ -136,7 +138,19 @@ function ReservationCard({ r, tripId, onEdit, onDelete, files = [], onNavigateTo
|
|||||||
{r.confirmation_number && (
|
{r.confirmation_number && (
|
||||||
<div style={{ flex: 1, padding: '5px 10px', textAlign: 'center' }}>
|
<div style={{ flex: 1, padding: '5px 10px', textAlign: 'center' }}>
|
||||||
<div style={{ fontSize: 9, fontWeight: 600, color: 'var(--text-faint)', textTransform: 'uppercase', letterSpacing: '0.03em' }}>{t('reservations.confirmationCode')}</div>
|
<div style={{ fontSize: 9, fontWeight: 600, color: 'var(--text-faint)', textTransform: 'uppercase', letterSpacing: '0.03em' }}>{t('reservations.confirmationCode')}</div>
|
||||||
<div style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-primary)', marginTop: 1 }}>{r.confirmation_number}</div>
|
<div
|
||||||
|
onMouseEnter={() => 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}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'settings.temperature': 'Temperatureinheit',
|
'settings.temperature': 'Temperatureinheit',
|
||||||
'settings.timeFormat': 'Zeitformat',
|
'settings.timeFormat': 'Zeitformat',
|
||||||
'settings.routeCalculation': 'Routenberechnung',
|
'settings.routeCalculation': 'Routenberechnung',
|
||||||
|
'settings.blurBookingCodes': 'Buchungscodes verbergen',
|
||||||
'settings.on': 'An',
|
'settings.on': 'An',
|
||||||
'settings.off': 'Aus',
|
'settings.off': 'Aus',
|
||||||
'settings.account': 'Konto',
|
'settings.account': 'Konto',
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'settings.temperature': 'Temperature Unit',
|
'settings.temperature': 'Temperature Unit',
|
||||||
'settings.timeFormat': 'Time Format',
|
'settings.timeFormat': 'Time Format',
|
||||||
'settings.routeCalculation': 'Route Calculation',
|
'settings.routeCalculation': 'Route Calculation',
|
||||||
|
'settings.blurBookingCodes': 'Blur Booking Codes',
|
||||||
'settings.on': 'On',
|
'settings.on': 'On',
|
||||||
'settings.off': 'Off',
|
'settings.off': 'Off',
|
||||||
'settings.account': 'Account',
|
'settings.account': 'Account',
|
||||||
|
|||||||
@@ -333,7 +333,7 @@ export default function AdminPage(): React.ReactElement {
|
|||||||
<Shield className="w-5 h-5 text-slate-700" />
|
<Shield className="w-5 h-5 text-slate-700" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-slate-900">Administration</h1>
|
<h1 className="text-2xl font-bold text-slate-900">{t('admin.title')}</h1>
|
||||||
<p className="text-slate-500 text-sm">{t('admin.subtitle')}</p>
|
<p className="text-slate-500 text-sm">{t('admin.subtitle')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ interface SectionProps {
|
|||||||
|
|
||||||
function Section({ title, icon: Icon, children }: SectionProps): React.ReactElement {
|
function Section({ title, icon: Icon, children }: SectionProps): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border overflow-hidden" style={{ background: 'var(--bg-card)', borderColor: 'var(--border-primary)' }}>
|
<div className="rounded-xl border overflow-hidden" style={{ background: 'var(--bg-card)', borderColor: 'var(--border-primary)', breakInside: 'avoid', marginBottom: 24 }}>
|
||||||
<div className="px-6 py-4 border-b flex items-center gap-2" style={{ borderColor: 'var(--border-secondary)' }}>
|
<div className="px-6 py-4 border-b flex items-center gap-2" style={{ borderColor: 'var(--border-secondary)' }}>
|
||||||
<Icon className="w-5 h-5" style={{ color: 'var(--text-secondary)' }} />
|
<Icon className="w-5 h-5" style={{ color: 'var(--text-secondary)' }} />
|
||||||
<h2 className="font-semibold" style={{ color: 'var(--text-primary)' }}>{title}</h2>
|
<h2 className="font-semibold" style={{ color: 'var(--text-primary)' }}>{title}</h2>
|
||||||
@@ -220,12 +220,15 @@ export default function SettingsPage(): React.ReactElement {
|
|||||||
<Navbar />
|
<Navbar />
|
||||||
|
|
||||||
<div style={{ paddingTop: 'var(--nav-h)' }}>
|
<div style={{ paddingTop: 'var(--nav-h)' }}>
|
||||||
<div className="max-w-2xl mx-auto px-4 py-8 space-y-6">
|
<div className="max-w-5xl mx-auto px-4 py-8">
|
||||||
<div>
|
<style>{`@media (max-width: 900px) { .settings-columns { column-count: 1 !important; } }`}</style>
|
||||||
|
<div style={{ marginBottom: 24 }}>
|
||||||
<h1 className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>{t('settings.title')}</h1>
|
<h1 className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>{t('settings.title')}</h1>
|
||||||
<p className="text-sm mt-0.5" style={{ color: 'var(--text-muted)' }}>{t('settings.subtitle')}</p>
|
<p className="text-sm mt-0.5" style={{ color: 'var(--text-muted)' }}>{t('settings.subtitle')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="settings-columns" style={{ columnCount: 2, columnGap: 24 }}>
|
||||||
|
|
||||||
{/* Map settings */}
|
{/* Map settings */}
|
||||||
<Section title={t('settings.map')} icon={Map}>
|
<Section title={t('settings.map')} icon={Map}>
|
||||||
<div>
|
<div>
|
||||||
@@ -439,6 +442,36 @@ export default function SettingsPage(): React.ReactElement {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Blur Booking Codes */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-secondary)' }}>{t('settings.blurBookingCodes')}</label>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
{[
|
||||||
|
{ value: true, label: t('settings.on') || 'On' },
|
||||||
|
{ value: false, label: t('settings.off') || 'Off' },
|
||||||
|
].map(opt => (
|
||||||
|
<button
|
||||||
|
key={String(opt.value)}
|
||||||
|
onClick={async () => {
|
||||||
|
try { await updateSetting('blur_booking_codes', opt.value) }
|
||||||
|
catch (e: unknown) { toast.error(e instanceof Error ? e.message : 'Error') }
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
display: 'flex', alignItems: 'center', gap: 8,
|
||||||
|
padding: '10px 20px', borderRadius: 10, cursor: 'pointer',
|
||||||
|
fontFamily: 'inherit', fontSize: 14, fontWeight: 500,
|
||||||
|
border: (!!settings.blur_booking_codes) === opt.value ? '2px solid var(--text-primary)' : '2px solid var(--border-primary)',
|
||||||
|
background: (!!settings.blur_booking_codes) === opt.value ? 'var(--bg-hover)' : 'var(--bg-card)',
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
transition: 'all 0.15s',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{opt.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
{/* Immich — only when Memories addon is enabled */}
|
{/* Immich — only when Memories addon is enabled */}
|
||||||
@@ -888,6 +921,7 @@ export default function SettingsPage(): React.ReactElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ export interface Settings {
|
|||||||
time_format: string
|
time_format: string
|
||||||
show_place_description: boolean
|
show_place_description: boolean
|
||||||
route_calculation?: boolean
|
route_calculation?: boolean
|
||||||
|
blur_booking_codes?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AssignmentsMap {
|
export interface AssignmentsMap {
|
||||||
|
|||||||
Reference in New Issue
Block a user