From e800d87235e6a6c94c7e5a0c17cc18beba9651a3 Mon Sep 17 00:00:00 2001 From: mARTin Date: Tue, 31 Mar 2026 09:14:16 +0200 Subject: [PATCH] feat: overhaul header display options with mobile portrait/landscape settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename options to be clearer: - "Vollständiges Datum" → "Heutiges Datum" (always today's date) - "Aktueller Tag" → "Selektierter Tag" with custom format input - "Benutzerdefiniert" → "Woche Benutzerdefiniert" (week-based tokens) - Add custom format input for "Selektierter Tag" (headerCurrentDayFormat) with tokens: DDDD, DDD, DD, MMMM, MMM, MM, YYYY, WW - Add separate header display settings for Mobile Portrait and Mobile Landscape (mobilePortraitHeaderDisplay, mobileLandscapeHeaderDisplay) - Move refresh/sync icon to fixed-width slot right of date — date no longer shifts when the icon appears or the spinner replaces it - Add 3 new Prisma fields (db push required on deploy) - All 5 languages updated (EN, DE, FR, ES, IT) v1.81.0 --- package.json | 2 +- prisma/schema.prisma | 5 +- src/app/api/user/profile/route.ts | 11 ++- src/components/SettingsSidebar.tsx | 72 +++++++++++++++- src/components/WeeklyView.tsx | 127 +++++++++++++---------------- src/lib/weeklyViewTranslations.ts | 63 ++++++++------ 6 files changed, 181 insertions(+), 99 deletions(-) diff --git a/package.json b/package.json index ad978f3..89a4bb9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "my-weekly-todo-list", - "version": "1.80.4", + "version": "1.81.0", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view", "main": "index.js", "scripts": { diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 422da7e..76fb73e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -80,7 +80,10 @@ model User { dateAlignment String @default("center") dateVerticalAlign String? @default("middle") headerDisplay String? @default("kw") - headerCustomFormat String? @default("KW WW | YYYY") + headerCustomFormat String? @default("KW WW | YYYY") + headerCurrentDayFormat String? @default("DDD, DD. MMMM YYYY") + mobilePortraitHeaderDisplay String? @default("current_day") + mobileLandscapeHeaderDisplay String? @default("kw") hourLabelFormat String @default("short") showSubHourSlots Boolean @default(true) allDayPosition String @default("above") diff --git a/src/app/api/user/profile/route.ts b/src/app/api/user/profile/route.ts index 1398490..4f9b142 100644 --- a/src/app/api/user/profile/route.ts +++ b/src/app/api/user/profile/route.ts @@ -48,6 +48,9 @@ export async function GET(request: NextRequest) { dateVerticalAlign: true, headerDisplay: true, headerCustomFormat: true, + headerCurrentDayFormat: true, + mobilePortraitHeaderDisplay: true, + mobileLandscapeHeaderDisplay: true, headlineFont: true, headlineFontSize: true, headlineFontWeight: true, @@ -141,7 +144,7 @@ export async function PATCH(request: NextRequest) { weekdayColor, dateColor, taskColor, todayHighlightColor, pastDayColor, goalFallbackType, goalDefaultSentence, goalFontFamily, goalFontSize, goalFontWeight, goalScope, - dateLayout, mobileDateLayout, dateVerticalAlign, headerDisplay, headerCustomFormat, + dateLayout, mobileDateLayout, dateVerticalAlign, headerDisplay, headerCustomFormat, headerCurrentDayFormat, mobilePortraitHeaderDisplay, mobileLandscapeHeaderDisplay, hourLabelFormat, showSubHourSlots, allDayPosition, cwFontFamily, cwFontSize, cwFontWeight, cwColor, yearFontFamily, yearFontSize, yearFontWeight, yearColor, @@ -211,6 +214,9 @@ export async function PATCH(request: NextRequest) { ...(dateVerticalAlign !== undefined && { dateVerticalAlign }), ...(headerDisplay !== undefined && { headerDisplay }), ...(headerCustomFormat !== undefined && { headerCustomFormat }), + ...(headerCurrentDayFormat !== undefined && { headerCurrentDayFormat }), + ...(mobilePortraitHeaderDisplay !== undefined && { mobilePortraitHeaderDisplay }), + ...(mobileLandscapeHeaderDisplay !== undefined && { mobileLandscapeHeaderDisplay }), ...(hourLabelFormat !== undefined && { hourLabelFormat }), ...(showSubHourSlots !== undefined && { showSubHourSlots }), ...(allDayPosition !== undefined && { allDayPosition }), @@ -314,6 +320,9 @@ export async function PATCH(request: NextRequest) { dateVerticalAlign: true, headerDisplay: true, headerCustomFormat: true, + headerCurrentDayFormat: true, + mobilePortraitHeaderDisplay: true, + mobileLandscapeHeaderDisplay: true, cwFontFamily: true, cwFontSize: true, cwFontWeight: true, diff --git a/src/components/SettingsSidebar.tsx b/src/components/SettingsSidebar.tsx index b7d43f4..6ceb66d 100644 --- a/src/components/SettingsSidebar.tsx +++ b/src/components/SettingsSidebar.tsx @@ -900,6 +900,27 @@ function SettingsSidebar({ + {profile.headerDisplay === "current_day" && ( +
+ + { + const val = e.target.value; + setProfile({ ...profile, headerCurrentDayFormat: val }); + saveSetting("headerCurrentDayFormat", val); + }} + placeholder="DDD, DD. MMMM YYYY" + className="w-full px-3 py-1.5 text-sm rounded border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white" + /> +
+ Tokens: DDDD (Montag), DDD (Mo.), DD (30), MMMM (März), MMM (Mär), MM (03), YYYY (2026) +
+
+ )} {profile.headerDisplay === "custom" && (
)} + {/* Mobile Portrait/Landscape overrides */} +
+
+ + +
+
+ + +
+
{/* Push Notifications */} diff --git a/src/components/WeeklyView.tsx b/src/components/WeeklyView.tsx index 908edd3..a14ac30 100644 --- a/src/components/WeeklyView.tsx +++ b/src/components/WeeklyView.tsx @@ -380,9 +380,9 @@ function getCWReferenceDate(days: Date[]): Date { } // Format a custom header string using tokens -function formatCustomHeader(format: string, days: Date[], language: string, t: any): string { +function formatCustomHeader(format: string, days: Date[], language: string, t: any, refDateOverride?: Date): string { if (!format) return ""; - + // Choose the reference date: if today is within the visible days, use today. // Otherwise, use the standard CW reference date (start of week). const today = new Date(); @@ -391,7 +391,7 @@ function formatCustomHeader(format: string, days: Date[], language: string, t: a d.getMonth() === today.getMonth() && d.getFullYear() === today.getFullYear() ); - const refDate = isTodayInWeek ? today : getCWReferenceDate(days); + const refDate = refDateOverride ?? (isTodayInWeek ? today : getCWReferenceDate(days)); // Define token mappings const tokens: Record = { @@ -415,6 +415,16 @@ function formatCustomHeader(format: string, days: Date[], language: string, t: a return format.replace(regex, (match) => tokens[match] || match); } +// Get the "selected" day for current_day header: today if visible, otherwise first visible day +function getSelectedDay(days: Date[]): Date { + const today = new Date(); + return days.some(d => + d.getDate() === today.getDate() && + d.getMonth() === today.getMonth() && + d.getFullYear() === today.getFullYear() + ) ? today : days[0]; +} + // Check if an event is an all-day event // Defined outside component to avoid stale closure issues in useCallbacks const isAllDayEvent = (event: CalendarEvent): boolean => { @@ -5603,33 +5613,22 @@ export default function WeeklyView() { )} {syncError && } - {isPortrait ? (() => { - const shownDay = getVisibleDays()[0]; - return shownDay.toLocaleDateString(profile.language || "de-DE", { weekday: "short", day: "2-digit", month: "long", year: "numeric" }); - })() : - profile.headerDisplay === "none" ? "" : - profile.headerDisplay === "current_day" ? (() => { - const shownDay = getVisibleDays()[0]; - return shownDay.toLocaleDateString(profile.language || "de-DE", { weekday: "short", day: "2-digit", month: "long", year: "numeric" }); - })() : - profile.headerDisplay === "date" ? (() => { - const today = new Date(); - const isTodayInWeek = getVisibleDays().some(d => - d.getDate() === today.getDate() && - d.getMonth() === today.getMonth() && - d.getFullYear() === today.getFullYear() - ); - const refDate = isTodayInWeek ? today : getCWReferenceDate(getVisibleDays()); - return refDate.toLocaleDateString(profile.language, { day: '2-digit', month: '2-digit', year: 'numeric' }); - })() : - profile.headerDisplay === "month_year" ? - getCWReferenceDate(getVisibleDays()).toLocaleDateString(profile.language, { month: 'long', year: 'numeric' }) : - profile.headerDisplay === "custom" ? - formatCustomHeader(profile.headerCustomFormat || "KW WW | YYYY", getVisibleDays(), profile.language, t) : - profile.headerDisplay === "month" ? - getCWReferenceDate(getVisibleDays()).toLocaleDateString(profile.language, { month: "long" }) : - `KW ${getWeekNumber(getCWReferenceDate(getVisibleDays())).toString().padStart(2, "0")} | ${getCWReferenceDate(getVisibleDays()).getFullYear()}` - } + {(() => { + const visibleDays = getVisibleDays(); + const mobileDisplay = isPortrait + ? (profile.mobilePortraitHeaderDisplay || "current_day") + : (profile.mobileLandscapeHeaderDisplay || profile.headerDisplay || "kw"); + if (mobileDisplay === "none") return ""; + if (mobileDisplay === "current_day") { + const fmt = profile.headerCurrentDayFormat || "DDD, DD. MMMM YYYY"; + return formatCustomHeader(fmt, visibleDays, profile.language, t, getSelectedDay(visibleDays)); + } + if (mobileDisplay === "date") return new Date().toLocaleDateString(profile.language, { day: '2-digit', month: '2-digit', year: 'numeric' }); + if (mobileDisplay === "month_year") return getCWReferenceDate(visibleDays).toLocaleDateString(profile.language, { month: 'long', year: 'numeric' }); + if (mobileDisplay === "month") return getCWReferenceDate(visibleDays).toLocaleDateString(profile.language, { month: "long" }); + if (mobileDisplay === "custom") return formatCustomHeader(profile.headerCustomFormat || "KW WW | YYYY", visibleDays, profile.language, t); + return `KW ${getWeekNumber(getCWReferenceDate(visibleDays)).toString().padStart(2, "0")} | ${getCWReferenceDate(visibleDays).getFullYear()}`; + })()} @@ -5763,35 +5762,21 @@ export default function WeeklyView() { filter: "brightness(var(--weekly-header-brightness, 1))" }}> {(() => { - const effectiveDisplay = profile.viewStyle === "kanban" && !profile.headerDisplay ? "current_day" : (profile.headerDisplay || "kw"); + const effectiveDisplay = (profile.viewStyle === "kanban" && !profile.headerDisplay) ? "current_day" : (profile.headerDisplay || "kw"); + const visibleDays = getVisibleDays(); if (effectiveDisplay === "none") return ""; if (effectiveDisplay === "current_day") { - const today = new Date(); - const visibleDays = getVisibleDays(); - const shownDay = visibleDays.some(d => - d.getDate() === today.getDate() && - d.getMonth() === today.getMonth() && - d.getFullYear() === today.getFullYear() - ) ? today : visibleDays[0]; - return shownDay.toLocaleDateString(profile.language || "de-DE", { weekday: "short", day: "2-digit", month: "long", year: "numeric" }); + const fmt = profile.headerCurrentDayFormat || "DDD, DD. MMMM YYYY"; + return formatCustomHeader(fmt, visibleDays, profile.language, t, getSelectedDay(visibleDays)); } - if (effectiveDisplay === "month") return getCWReferenceDate(getVisibleDays()).toLocaleDateString(profile.language, { month: "long" }); - if (effectiveDisplay === "month_year") return getCWReferenceDate(getVisibleDays()).toLocaleDateString(profile.language, { month: "long", year: "numeric" }); - if (effectiveDisplay === "date") { - const today = new Date(); - const isTodayInWeek = getVisibleDays().some(d => - d.getDate() === today.getDate() && - d.getMonth() === today.getMonth() && - d.getFullYear() === today.getFullYear() - ); - const refDate = isTodayInWeek ? today : getCWReferenceDate(getVisibleDays()); - return refDate.toLocaleDateString(profile.language, { day: '2-digit', month: '2-digit', year: 'numeric' }); - } - if (effectiveDisplay === "custom") return formatCustomHeader(profile.headerCustomFormat || "KW WW | YYYY", getVisibleDays(), profile.language, t); - return `KW ${getWeekNumber(getCWReferenceDate(getVisibleDays())).toString().padStart(2, "0")}`; + if (effectiveDisplay === "month") return getCWReferenceDate(visibleDays).toLocaleDateString(profile.language, { month: "long" }); + if (effectiveDisplay === "month_year") return getCWReferenceDate(visibleDays).toLocaleDateString(profile.language, { month: "long", year: "numeric" }); + if (effectiveDisplay === "date") return new Date().toLocaleDateString(profile.language, { day: '2-digit', month: '2-digit', year: 'numeric' }); + if (effectiveDisplay === "custom") return formatCustomHeader(profile.headerCustomFormat || "KW WW | YYYY", visibleDays, profile.language, t); + return `KW ${getWeekNumber(getCWReferenceDate(visibleDays)).toString().padStart(2, "0")}`; })()} - {profile.headerDisplay !== "none" && profile.headerDisplay !== "current_day" && (profile.headerDisplay === "kw" || profile.headerDisplay === "month" || !profile.headerDisplay) && ( + {profile.headerDisplay !== "none" && profile.headerDisplay !== "current_day" && profile.headerDisplay !== "date" && profile.headerDisplay !== "month_year" && (profile.headerDisplay === "kw" || profile.headerDisplay === "month" || !profile.headerDisplay) && ( <> | )} + {/* Refresh / sync status — fixed-width slot so the date never shifts */} +
+ {syncError ? ( +
+ +
+ ) : (isLoading || isSyncing || syncStatus === "syncing") ? ( +
+ ) : ( + + )} +
{/* Goal — hidden on tablet to save space */}
- {syncError ? ( -
- - {syncError} -
- ) : (isLoading || isSyncing || syncStatus === "syncing") ? ( -
- ) : ( - - )} {isEditingGoal ? ( = { noStage: "No stage", headerDisplay: "Header Display", headerDisplayKW: "Calendar Week (KW)", - headerDisplayMonth: "Month Name - March", - headerDisplayMonthYear: "Month & Year - March | 2026", - headerDisplayDate: "Full Date - 13.03.2026", - headerDisplayCustom: "Custom - Friday - 13. March", - headerDisplayCurrentDay: "Current Day - Friday, 13. March 2026", + headerDisplayMonth: "Month Name — March", + headerDisplayMonthYear: "Month & Year — March | 2026", + headerDisplayDate: "Today's Date — 30.03.2026", + headerDisplayCurrentDay: "Selected Day — Mo., 30. March 2026", + headerDisplayCustom: "Custom Week Format", headerDisplayNone: "None", - headerCustomFormatLabel: "Format string (e.g. DD.MM.YYYY)", + headerCustomFormatLabel: "Week format (e.g. KW WW | YYYY)", + headerCurrentDayFormatLabel: "Day format (e.g. DDD, DD. MMMM YYYY)", + headerMobilePortrait: "Mobile Portrait", + headerMobileLandscape: "Mobile Landscape", language: "Language", dateFormat: "Date Format", timeFormat: "Time Format", @@ -270,13 +273,16 @@ export const translations: Record = { noStage: "Keine Phase", headerDisplay: "Kopfzeile", headerDisplayKW: "Kalenderwoche (KW)", - headerDisplayMonth: "Monatsname - März", - headerDisplayMonthYear: "Monat & Jahr - März | 2026", - headerDisplayDate: "Vollständiges Datum - 13.03.2026", - headerDisplayCustom: "Benutzerdefiniert - Freitag - 13. März", - headerDisplayCurrentDay: "Aktueller Tag - Freitag, 13. März 2026", + headerDisplayMonth: "Monatsname — März", + headerDisplayMonthYear: "Monat & Jahr — März | 2026", + headerDisplayDate: "Heutiges Datum — 30.03.2026", + headerDisplayCurrentDay: "Selektierter Tag — Mo., 30. März 2026", + headerDisplayCustom: "Woche Benutzerdefiniert", headerDisplayNone: "Nichts", - headerCustomFormatLabel: "Format (z.B. DD.MM.YYYY)", + headerCustomFormatLabel: "Wochenformat (z.B. KW WW | YYYY)", + headerCurrentDayFormatLabel: "Tagesformat (z.B. DDD, DD. MMMM YYYY)", + headerMobilePortrait: "Mobile Hochformat", + headerMobileLandscape: "Mobile Querformat", listView: "Liste", notes: "Notizen", notesSidebar: "Notizen-Seitenleiste", @@ -517,11 +523,14 @@ export const translations: Record = { headerDisplayKW: "Semaine calendaire (KW)", headerDisplayMonth: "Nom du mois - Mars", headerDisplayMonthYear: "Mois & Année - Mars | 2026", - headerDisplayDate: "Date complète - 13.03.2026", - headerDisplayCustom: "Personnalisé - Vendredi - 13 Mars", - headerDisplayCurrentDay: "Jour actuel - Vendredi, 13 Mars 2026", + headerDisplayDate: "Date du jour — 30.03.2026", + headerDisplayCurrentDay: "Jour sélectionné — Lu., 30 Mars 2026", + headerDisplayCustom: "Semaine personnalisée", headerDisplayNone: "Aucun", - headerCustomFormatLabel: "Format (ex: DD.MM.YYYY)", + headerCustomFormatLabel: "Format semaine (ex: KW WW | YYYY)", + headerCurrentDayFormatLabel: "Format jour (ex: DDD, DD. MMMM YYYY)", + headerMobilePortrait: "Mobile Portrait", + headerMobileLandscape: "Mobile Paysage", language: "Langue", dateFormat: "Format de date", timeFormat: "Format d'heure", @@ -735,11 +744,14 @@ export const translations: Record = { headerDisplayKW: "Semana calendario (KW)", headerDisplayMonth: "Nombre del mes - Marzo", headerDisplayMonthYear: "Mes y Año - Marzo | 2026", - headerDisplayDate: "Fecha completa - 13.03.2026", - headerDisplayCustom: "Personalizado - Viernes - 13 Marzo", - headerDisplayCurrentDay: "Día actual - Viernes, 13 Marzo 2026", + headerDisplayDate: "Fecha de hoy — 30.03.2026", + headerDisplayCurrentDay: "Día seleccionado — Lu., 30 Marzo 2026", + headerDisplayCustom: "Semana personalizada", headerDisplayNone: "Ninguno", - headerCustomFormatLabel: "Formato (ej. DD.MM.YYYY)", + headerCustomFormatLabel: "Formato semana (ej. KW WW | YYYY)", + headerCurrentDayFormatLabel: "Formato día (ej. DDD, DD. MMMM YYYY)", + headerMobilePortrait: "Móvil Retrato", + headerMobileLandscape: "Móvil Paisaje", language: "Idioma", dateFormat: "Formato de fecha", timeFormat: "Formato de hora", @@ -953,11 +965,14 @@ export const translations: Record = { headerDisplayKW: "Settimana calendario (KW)", headerDisplayMonth: "Nome del mese - Marzo", headerDisplayMonthYear: "Mese e Anno - Marzo | 2026", - headerDisplayDate: "Data completa - 13.03.2026", - headerDisplayCustom: "Personalizzato - Venerdì - 13 Marzo", - headerDisplayCurrentDay: "Giorno corrente - Venerdì, 13 Marzo 2026", + headerDisplayDate: "Data di oggi — 30.03.2026", + headerDisplayCurrentDay: "Giorno selezionato — Lu., 30 Marzo 2026", + headerDisplayCustom: "Settimana personalizzata", headerDisplayNone: "Nessuno", - headerCustomFormatLabel: "Formato (es. DD.MM.YYYY)", + headerCustomFormatLabel: "Formato settimana (es. KW WW | YYYY)", + headerCurrentDayFormatLabel: "Formato giorno (es. DDD, DD. MMMM YYYY)", + headerMobilePortrait: "Mobile Verticale", + headerMobileLandscape: "Mobile Orizzontale", language: "Lingua", dateFormat: "Formato data", timeFormat: "Formato ora",