feat: overhaul header display options with mobile portrait/landscape settings

- 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
This commit is contained in:
mARTin 2026-03-31 09:14:16 +02:00
parent 033603e899
commit e800d87235
6 changed files with 181 additions and 99 deletions

View File

@ -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": {

View File

@ -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")

View File

@ -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,

View File

@ -900,6 +900,27 @@ function SettingsSidebar({
<option value="none">{t.headerDisplayNone}</option>
</select>
</div>
{profile.headerDisplay === "current_day" && (
<div className="mt-2">
<label style={{ fontSize: "0.75rem", color: "var(--weekly-settings-label)", display: "block", marginBottom: "4px" }}>
{t.headerCurrentDayFormatLabel}
</label>
<input
type="text"
value={profile.headerCurrentDayFormat || ""}
onChange={(e) => {
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"
/>
<div className="mt-1 text-[10px] text-gray-500 leading-tight">
Tokens: DDDD (Montag), DDD (Mo.), DD (30), MMMM (März), MMM (Mär), MM (03), YYYY (2026)
</div>
</div>
)}
{profile.headerDisplay === "custom" && (
<div className="mt-2">
<label style={{ fontSize: "0.75rem", color: "var(--weekly-settings-label)", display: "block", marginBottom: "4px" }}>
@ -913,14 +934,61 @@ function SettingsSidebar({
setProfile({ ...profile, headerCustomFormat: val });
saveSetting("headerCustomFormat", val);
}}
placeholder="KW WW | YYYY or DD.MM.YYYY"
placeholder="KW WW | 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"
/>
<div className="mt-1 text-[10px] text-gray-500 leading-tight">
Tokens: WW (Week), YYYY (Year), MMMM (Month Name), MM (Month Num), DD (Day), [TODAY] (Active Date)
Tokens: WW (KW), YYYY (Jahr), MMMM (März), MM (03), DD (30), [TODAY] (Heute)
</div>
</div>
)}
{/* Mobile Portrait/Landscape overrides */}
<div className="mt-3 grid grid-cols-2 gap-2">
<div>
<label style={{ fontSize: "0.75rem", color: "var(--weekly-settings-label)", display: "block", marginBottom: "4px" }}>
{t.headerMobilePortrait}
</label>
<select
value={profile.mobilePortraitHeaderDisplay || "current_day"}
onChange={(e) => {
const val = e.target.value as any;
setProfile({ ...profile, mobilePortraitHeaderDisplay: val });
saveSetting("mobilePortraitHeaderDisplay", val);
}}
className="weekly-input w-full p-1.5 text-xs border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white rounded"
>
<option value="kw">{t.headerDisplayKW}</option>
<option value="month">{t.headerDisplayMonth}</option>
<option value="month_year">{t.headerDisplayMonthYear}</option>
<option value="date">{t.headerDisplayDate}</option>
<option value="current_day">{t.headerDisplayCurrentDay}</option>
<option value="custom">{t.headerDisplayCustom}</option>
<option value="none">{t.headerDisplayNone}</option>
</select>
</div>
<div>
<label style={{ fontSize: "0.75rem", color: "var(--weekly-settings-label)", display: "block", marginBottom: "4px" }}>
{t.headerMobileLandscape}
</label>
<select
value={profile.mobileLandscapeHeaderDisplay || "kw"}
onChange={(e) => {
const val = e.target.value as any;
setProfile({ ...profile, mobileLandscapeHeaderDisplay: val });
saveSetting("mobileLandscapeHeaderDisplay", val);
}}
className="weekly-input w-full p-1.5 text-xs border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white rounded"
>
<option value="kw">{t.headerDisplayKW}</option>
<option value="month">{t.headerDisplayMonth}</option>
<option value="month_year">{t.headerDisplayMonthYear}</option>
<option value="date">{t.headerDisplayDate}</option>
<option value="current_day">{t.headerDisplayCurrentDay}</option>
<option value="custom">{t.headerDisplayCustom}</option>
<option value="none">{t.headerDisplayNone}</option>
</select>
</div>
</div>
</div>
{/* Push Notifications */}

View File

@ -380,7 +380,7 @@ 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.
@ -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<string, string> = {
@ -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 && <AlertCircle size={14} className="text-red-500" />}
<span>
{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()}`;
})()}
</span>
<ChevronDown size={12} style={{ opacity: 0.5 }} />
</button>
@ -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")}`;
})()}
</span>
{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) && (
<>
<span className="text-gray-400">|</span>
<span
@ -5808,26 +5793,28 @@ export default function WeeklyView() {
</>
)}
</div>
{/* Refresh / sync status — fixed-width slot so the date never shifts */}
<div style={{ width: 28, display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
{syncError ? (
<div title={syncError} className="flex items-center text-red-500">
<AlertCircle size={14} />
</div>
) : (isLoading || isSyncing || syncStatus === "syncing") ? (
<div className="weekly-spinner" title="Syncing..." style={{ width: 14, height: 14 }}></div>
) : (
<button
onClick={() => { fetchCalendarEvents(true); fetchTasks(); }}
className="p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition-all text-gray-400 hover:text-gray-600 opacity-0 group-hover:opacity-100"
title="Refresh Calendar & Tasks"
>
<RefreshCcw size={14} />
</button>
)}
</div>
</div>
{/* Goal — hidden on tablet to save space */}
<div className="header-desktop-only items-center text-sm">
{syncError ? (
<div className="flex items-center gap-1 text-red-500 mr-2" title={syncError}>
<AlertCircle size={14} />
<span className="text-xs">{syncError}</span>
</div>
) : (isLoading || isSyncing || syncStatus === "syncing") ? (
<div className="weekly-spinner mr-2" title="Syncing..."></div>
) : (
<button
onClick={() => { fetchCalendarEvents(true); fetchTasks(); }}
className="p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition-all text-gray-400 hover:text-gray-600 opacity-0 group-hover:opacity-100 mr-1"
title="Refresh Calendar & Tasks"
>
<RefreshCcw size={14} />
</button>
)}
{isEditingGoal ? (
<input
type="text"

View File

@ -27,13 +27,16 @@ export const translations: Record<string, any> = {
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<string, any> = {
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<string, any> = {
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<string, any> = {
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<string, any> = {
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",