Implement custom header formatting (v1.33.0)

This commit is contained in:
mARTin 2026-03-13 15:18:32 +01:00
parent 889282381f
commit b827f39722
2 changed files with 82 additions and 4 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "my-weekly-todo-list", "name": "my-weekly-todo-list",
"version": "1.32.0", "version": "1.33.0",
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@ -263,6 +263,8 @@ const translations: Record<string, any> = {
headerDisplayMonth: "Month Name", headerDisplayMonth: "Month Name",
headerDisplayMonthYear: "Month & Year", headerDisplayMonthYear: "Month & Year",
headerDisplayDate: "Full Date", headerDisplayDate: "Full Date",
headerDisplayCustom: "Custom",
headerCustomFormatLabel: "Format string (e.g. DD.MM.YYYY)",
language: "Language", language: "Language",
dateFormat: "Date Format", dateFormat: "Date Format",
timeFormat: "Time Format", timeFormat: "Time Format",
@ -471,6 +473,8 @@ const translations: Record<string, any> = {
headerDisplayMonth: "Monatsname", headerDisplayMonth: "Monatsname",
headerDisplayMonthYear: "Monat & Jahr", headerDisplayMonthYear: "Monat & Jahr",
headerDisplayDate: "Vollständiges Datum", headerDisplayDate: "Vollständiges Datum",
headerDisplayCustom: "Benutzerdefiniert",
headerCustomFormatLabel: "Format (z.B. DD.MM.YYYY)",
listView: "Liste", listView: "Liste",
notes: "Notizen", notes: "Notizen",
notesSidebar: "Notizen-Seitenleiste", notesSidebar: "Notizen-Seitenleiste",
@ -682,6 +686,8 @@ const translations: Record<string, any> = {
headerDisplayMonth: "Nom du mois", headerDisplayMonth: "Nom du mois",
headerDisplayMonthYear: "Mois & Année", headerDisplayMonthYear: "Mois & Année",
headerDisplayDate: "Date complète", headerDisplayDate: "Date complète",
headerDisplayCustom: "Personnalisé",
headerCustomFormatLabel: "Format (ex: DD.MM.YYYY)",
language: "Langue", language: "Langue",
dateFormat: "Format de date", dateFormat: "Format de date",
timeFormat: "Format d'heure", timeFormat: "Format d'heure",
@ -891,6 +897,8 @@ const translations: Record<string, any> = {
headerDisplayMonth: "Nombre del mes", headerDisplayMonth: "Nombre del mes",
headerDisplayMonthYear: "Mes y Año", headerDisplayMonthYear: "Mes y Año",
headerDisplayDate: "Fecha completa", headerDisplayDate: "Fecha completa",
headerDisplayCustom: "Personalizado",
headerCustomFormatLabel: "Formato (ej. DD.MM.YYYY)",
language: "Idioma", language: "Idioma",
dateFormat: "Formato de fecha", dateFormat: "Formato de fecha",
timeFormat: "Formato de hora", timeFormat: "Formato de hora",
@ -1100,6 +1108,8 @@ const translations: Record<string, any> = {
headerDisplayMonth: "Nome del mese", headerDisplayMonth: "Nome del mese",
headerDisplayMonthYear: "Mese e Anno", headerDisplayMonthYear: "Mese e Anno",
headerDisplayDate: "Data completa", headerDisplayDate: "Data completa",
headerDisplayCustom: "Personalizzato",
headerCustomFormatLabel: "Formato (es. DD.MM.YYYY)",
language: "Lingua", language: "Lingua",
dateFormat: "Formato data", dateFormat: "Formato data",
timeFormat: "Formato ora", timeFormat: "Formato ora",
@ -1424,6 +1434,37 @@ function getCWReferenceDate(days: Date[]): Date {
return new Date(first.getTime() - distToMonday * 86400000); return new Date(first.getTime() - distToMonday * 86400000);
} }
// Format a custom header string using tokens
function formatCustomHeader(format: string, days: Date[], language: string, t: any): string {
if (!format) return "";
const refDate = getCWReferenceDate(days);
const today = new Date();
let result = format;
// [TODAY] token
result = result.replace(/\[TODAY\]/g, today.toLocaleDateString(language, { day: '2-digit', month: '2-digit', year: 'numeric' }));
// YYYY
result = result.replace(/YYYY/g, refDate.getFullYear().toString());
// WW (ISO Week)
result = result.replace(/WW/g, getWeekNumber(refDate).toString().padStart(2, '0'));
result = result.replace(/W/g, getWeekNumber(refDate).toString());
// MMMM (Month Name Long)
result = result.replace(/MMMM/g, refDate.toLocaleDateString(language, { month: 'long' }));
result = result.replace(/MMM/g, refDate.toLocaleDateString(language, { month: 'short' }));
result = result.replace(/MM/g, (refDate.getMonth() + 1).toString().padStart(2, '0'));
result = result.replace(/M/g, (refDate.getMonth() + 1).toString());
// DD (Day)
result = result.replace(/DD/g, refDate.getDate().toString().padStart(2, '0'));
result = result.replace(/D/g, refDate.getDate().toString());
return result;
}
// Check if an event is an all-day event // Check if an event is an all-day event
// Defined outside component to avoid stale closure issues in useCallbacks // Defined outside component to avoid stale closure issues in useCallbacks
const isAllDayEvent = (event: CalendarEvent): boolean => { const isAllDayEvent = (event: CalendarEvent): boolean => {
@ -1803,7 +1844,8 @@ export default function WeeklyView() {
weekdayCase?: "normal" | "capitalize" | "uppercase"; weekdayCase?: "normal" | "capitalize" | "uppercase";
customWeekdayNames?: string; customWeekdayNames?: string;
dateVerticalAlign?: "top" | "middle" | "bottom"; dateVerticalAlign?: "top" | "middle" | "bottom";
headerDisplay?: "kw" | "month" | "month_year" | "date"; headerDisplay?: "kw" | "month" | "month_year" | "date" | "custom";
headerCustomFormat?: string;
lightTheme?: any; lightTheme?: any;
darkTheme?: any; darkTheme?: any;
}>({ }>({
@ -5474,6 +5516,8 @@ export default function WeeklyView() {
<span>{getCWReferenceDate(getVisibleDays()).toLocaleDateString(language, { day: '2-digit', month: '2-digit', year: 'numeric' })}</span> <span>{getCWReferenceDate(getVisibleDays()).toLocaleDateString(language, { day: '2-digit', month: '2-digit', year: 'numeric' })}</span>
) : profile.headerDisplay === "month_year" ? ( ) : profile.headerDisplay === "month_year" ? (
<span>{getCWReferenceDate(getVisibleDays()).toLocaleDateString(language, { month: 'long', year: 'numeric' })}</span> <span>{getCWReferenceDate(getVisibleDays()).toLocaleDateString(language, { month: 'long', year: 'numeric' })}</span>
) : profile.headerDisplay === "custom" ? (
<span>{formatCustomHeader(profile.headerCustomFormat || "KW WW | YYYY", getVisibleDays(), language, t)}</span>
) : ( ) : (
<> <>
<span>KW{getWeekNumber(getCWReferenceDate(getVisibleDays())).toString().padStart(2, "0")}</span> <span>KW{getWeekNumber(getCWReferenceDate(getVisibleDays())).toString().padStart(2, "0")}</span>
@ -5731,7 +5775,9 @@ export default function WeeklyView() {
? getCWReferenceDate(getVisibleDays()).toLocaleDateString(language, { month: "long", year: "numeric" }) ? getCWReferenceDate(getVisibleDays()).toLocaleDateString(language, { month: "long", year: "numeric" })
: profile.headerDisplay === "date" : profile.headerDisplay === "date"
? getCWReferenceDate(getVisibleDays()).toLocaleDateString(language, { day: '2-digit', month: '2-digit', year: 'numeric' }) ? getCWReferenceDate(getVisibleDays()).toLocaleDateString(language, { day: '2-digit', month: '2-digit', year: 'numeric' })
: `KW ${getWeekNumber(getCWReferenceDate(getVisibleDays())).toString().padStart(2, "0")}` : profile.headerDisplay === "custom"
? formatCustomHeader(profile.headerCustomFormat || "KW WW | YYYY", getVisibleDays(), language, t)
: `KW ${getWeekNumber(getCWReferenceDate(getVisibleDays())).toString().padStart(2, "0")}`
} }
</span> </span>
{(profile.headerDisplay === "kw" || profile.headerDisplay === "month" || !profile.headerDisplay) && ( {(profile.headerDisplay === "kw" || profile.headerDisplay === "month" || !profile.headerDisplay) && (
@ -9694,7 +9740,8 @@ function SettingsSidebar({
weekdayCase?: "normal" | "capitalize" | "uppercase"; weekdayCase?: "normal" | "capitalize" | "uppercase";
customWeekdayNames?: string; customWeekdayNames?: string;
dateVerticalAlign?: "top" | "middle" | "bottom"; dateVerticalAlign?: "top" | "middle" | "bottom";
headerDisplay?: "kw" | "month" | "month_year" | "date"; headerDisplay?: "kw" | "month" | "month_year" | "date" | "custom";
headerCustomFormat?: string;
}>({ }>({
name: "", name: "",
email: "", email: "",
@ -9709,6 +9756,7 @@ function SettingsSidebar({
focusTimerDuration: 25, focusTimerDuration: 25,
focusBreakDuration: 5, focusBreakDuration: 5,
headerDisplay: "kw", headerDisplay: "kw",
headerCustomFormat: "KW WW | YYYY",
cellDuration: 30, cellDuration: 30,
viewStyle: "list", viewStyle: "list",
fontSize: "M", fontSize: "M",
@ -10852,7 +10900,37 @@ function SettingsSidebar({
> >
{t.headerDisplayDate} {t.headerDisplayDate}
</button> </button>
<button
onClick={() => {
setProfile({ ...profile, headerDisplay: "custom" });
saveSetting("headerDisplay", "custom");
}}
className={`px-4 py-2 text-sm rounded transition-colors ${profile.headerDisplay === "custom" ? "bg-white shadow-sm font-bold text-black" : "text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700"}`}
>
{t.headerDisplayCustom}
</button>
</div> </div>
{profile.headerDisplay === "custom" && (
<div className="mt-2">
<label style={{ fontSize: "0.75rem", color: "var(--weekly-settings-label)", display: "block", marginBottom: "4px" }}>
{t.headerCustomFormatLabel}
</label>
<input
type="text"
value={profile.headerCustomFormat || ""}
onChange={(e) => {
const val = e.target.value;
setProfile({ ...profile, headerCustomFormat: val });
saveSetting("headerCustomFormat", val);
}}
placeholder="KW WW | YYYY or DD.MM.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)
</div>
</div>
)}
</div> </div>
{/* Kanban Stages Settings */} {/* Kanban Stages Settings */}