fix: restore full day names, fix Oswald font, add someday page to PDF
- Always use full weekday names (MONTAG, DIENSTAG…) at all column counts; adaptive font size 8.5pt–16pt based on number of columns - Fix Oswald detection: Font.getRegisteredFontFamilies() doesn't exist in react-pdf — now use module-level _oswaldRegistered flag set on successful Font.register() call, so Oswald actually gets applied - Fix headline fallback: Helvetica-Bold (not plain Helvetica) when no Oswald - Increase strip heights: ALL_DAY_H 38→40, NO_TIME_H 28→52, DAY_HDR_H 40→44 - Show 4 tasks in both all-day and anyday strips (was 2/3) - Add someday page: last page (portrait) with all SomedayList tasks grouped by list, shown when showNoTime is enabled v1.95.3
This commit is contained in:
parent
9703d9ffa3
commit
3fd738b185
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "my-weekly-todo-list",
|
"name": "my-weekly-todo-list",
|
||||||
"version": "1.95.2",
|
"version": "1.95.3",
|
||||||
"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": {
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {
|
|||||||
// ── Font registration (Oswald from Google Fonts, one-time per process) ───────
|
// ── Font registration (Oswald from Google Fonts, one-time per process) ───────
|
||||||
|
|
||||||
let _fontPromise: Promise<void> | null = null;
|
let _fontPromise: Promise<void> | null = null;
|
||||||
|
let _oswaldRegistered = false; // tracked explicitly — Font has no public getRegisteredFamilies API
|
||||||
|
|
||||||
function ensureFonts(): Promise<void> {
|
function ensureFonts(): Promise<void> {
|
||||||
if (!_fontPromise) {
|
if (!_fontPromise) {
|
||||||
@ -23,7 +24,6 @@ function ensureFonts(): Promise<void> {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
const css = await res.text();
|
const css = await res.text();
|
||||||
// Extract all woff2 URLs with their weights
|
|
||||||
const entries: { src: string; fontWeight: number }[] = [];
|
const entries: { src: string; fontWeight: number }[] = [];
|
||||||
const re = /font-weight:\s*(\d+)[\s\S]*?url\((https:\/\/fonts\.gstatic\.com\/[^)'"]+)\)/g;
|
const re = /font-weight:\s*(\d+)[\s\S]*?url\((https:\/\/fonts\.gstatic\.com\/[^)'"]+)\)/g;
|
||||||
let m: RegExpExecArray | null;
|
let m: RegExpExecArray | null;
|
||||||
@ -32,9 +32,10 @@ function ensureFonts(): Promise<void> {
|
|||||||
}
|
}
|
||||||
if (entries.length > 0) {
|
if (entries.length > 0) {
|
||||||
Font.register({ family: 'Oswald', fonts: entries });
|
Font.register({ family: 'Oswald', fonts: entries });
|
||||||
|
_oswaldRegistered = true;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// No Oswald — PDF will use Helvetica-Bold as fallback
|
// Falls back to Helvetica-Bold
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
@ -51,9 +52,9 @@ const PAD_V = 14;
|
|||||||
const FOOTER_H = 18;
|
const FOOTER_H = 18;
|
||||||
|
|
||||||
// Fixed structural heights
|
// Fixed structural heights
|
||||||
const DAY_HDR_H = 40; // column header: weekday name + date
|
const DAY_HDR_H = 44; // column header: weekday name + date
|
||||||
const ALL_DAY_H = 38; // all-day strip (external calendar events)
|
const ALL_DAY_H = 40; // all-day strip (external calendar events)
|
||||||
const NO_TIME_H = 28; // anyday strip (user tasks without a time)
|
const NO_TIME_H = 52; // anyday strip (user tasks without a time, shows ~4)
|
||||||
const TIME_COL_W = 32; // left time-label column
|
const TIME_COL_W = 32; // left time-label column
|
||||||
|
|
||||||
// Derived
|
// Derived
|
||||||
@ -84,17 +85,12 @@ function lighten(hex: string, amount: number): string {
|
|||||||
|
|
||||||
// ── Weekday label helpers ─────────────────────────────────────────────────────
|
// ── Weekday label helpers ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
const WDAY_LONG_DE = ['Sonntag','Montag','Dienstag','Mittwoch','Donnerstag','Freitag','Samstag'];
|
const WDAY_LONG_DE = ['Sonntag','Montag','Dienstag','Mittwoch','Donnerstag','Freitag','Samstag'];
|
||||||
const WDAY_LONG_EN = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
|
const WDAY_LONG_EN = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
|
||||||
const WDAY_SHORT_DE = ['So','Mo','Di','Mi','Do','Fr','Sa'];
|
|
||||||
const WDAY_SHORT_EN = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
|
|
||||||
|
|
||||||
function weekdayLabel(d: Date, de: boolean, numDays: number): string {
|
// Always use full names — columns are wide enough even at 7 days
|
||||||
const idx = d.getDay();
|
function weekdayLabel(d: Date, de: boolean): string {
|
||||||
// Use full names for ≤5 columns, short for 6-7
|
return (de ? WDAY_LONG_DE : WDAY_LONG_EN)[d.getDay()].toUpperCase();
|
||||||
return numDays <= 5
|
|
||||||
? (de ? WDAY_LONG_DE : WDAY_LONG_EN)[idx].toUpperCase()
|
|
||||||
: (de ? WDAY_SHORT_DE : WDAY_SHORT_EN)[idx].toUpperCase();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function dayDateLabel(d: Date, de: boolean): string {
|
function dayDateLabel(d: Date, de: boolean): string {
|
||||||
@ -204,6 +200,12 @@ function dayColor(d: Date, isToday: boolean, us: UserStyle): string {
|
|||||||
|
|
||||||
// ── PDF Document ──────────────────────────────────────────────────────────────
|
// ── PDF Document ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type SomedayList = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
tasks: { id: string; title: string; completed: boolean; project: { name: string; color: string | null } | null }[];
|
||||||
|
};
|
||||||
|
|
||||||
function WeekCalendarPDF({
|
function WeekCalendarPDF({
|
||||||
pages,
|
pages,
|
||||||
tasksByDay,
|
tasksByDay,
|
||||||
@ -215,6 +217,7 @@ function WeekCalendarPDF({
|
|||||||
de,
|
de,
|
||||||
userName,
|
userName,
|
||||||
userStyle,
|
userStyle,
|
||||||
|
somedayLists,
|
||||||
totalPages,
|
totalPages,
|
||||||
}: {
|
}: {
|
||||||
pages: Date[][];
|
pages: Date[][];
|
||||||
@ -227,6 +230,7 @@ function WeekCalendarPDF({
|
|||||||
de: boolean;
|
de: boolean;
|
||||||
userName: string;
|
userName: string;
|
||||||
userStyle: UserStyle;
|
userStyle: UserStyle;
|
||||||
|
somedayLists: SomedayList[];
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
}) {
|
}) {
|
||||||
const todayKey = new Date().toLocaleDateString('en-CA');
|
const todayKey = new Date().toLocaleDateString('en-CA');
|
||||||
@ -234,8 +238,8 @@ function WeekCalendarPDF({
|
|||||||
const numHours = endHour - startHour;
|
const numHours = endHour - startHour;
|
||||||
const totalMins = numHours * 60;
|
const totalMins = numHours * 60;
|
||||||
|
|
||||||
// Headline font (Oswald if available, else Helvetica-Bold)
|
// Headline font: Oswald if loaded, else Helvetica-Bold (NOT plain Helvetica)
|
||||||
const headlineFontFamily = userStyle.useOswald ? 'Oswald' : 'Helvetica';
|
const headlineFontFamily = userStyle.useOswald ? 'Oswald' : 'Helvetica-Bold';
|
||||||
const headlineFW = userStyle.useOswald
|
const headlineFW = userStyle.useOswald
|
||||||
? (parseInt(userStyle.headlineFontWeight) >= 600 ? 700 : 400)
|
? (parseInt(userStyle.headlineFontWeight) >= 600 ? 700 : 400)
|
||||||
: undefined;
|
: undefined;
|
||||||
@ -260,8 +264,8 @@ function WeekCalendarPDF({
|
|||||||
const numDays = days.length;
|
const numDays = days.length;
|
||||||
const dayColW = (USABLE_W - TIME_COL_W) / numDays;
|
const dayColW = (USABLE_W - TIME_COL_W) / numDays;
|
||||||
|
|
||||||
// Responsive day-name size
|
// Adaptive font size — full names always, size scales with available column width
|
||||||
const dayNameSize = numDays <= 3 ? 16 : numDays <= 5 ? 12 : 9;
|
const dayNameSize = numDays <= 2 ? 16 : numDays <= 3 ? 13 : numDays <= 5 ? 10 : 8.5;
|
||||||
|
|
||||||
const calendarH = DAY_HDR_H + (showAllDay ? ALL_DAY_H : 0) + (showNoTime ? NO_TIME_H : 0) + GRID_H;
|
const calendarH = DAY_HDR_H + (showAllDay ? ALL_DAY_H : 0) + (showNoTime ? NO_TIME_H : 0) + GRID_H;
|
||||||
|
|
||||||
@ -415,7 +419,7 @@ function WeekCalendarPDF({
|
|||||||
const noTimeTasks = dayData?.noTime || [];
|
const noTimeTasks = dayData?.noTime || [];
|
||||||
const timedTasks = dayData?.timed || [];
|
const timedTasks = dayData?.timed || [];
|
||||||
|
|
||||||
const dayName = weekdayLabel(d, de, numDays);
|
const dayName = weekdayLabel(d, de);
|
||||||
const dateStr = dayDateLabel(d, de);
|
const dateStr = dayDateLabel(d, de);
|
||||||
|
|
||||||
const todayColBg = '#f0f7ff';
|
const todayColBg = '#f0f7ff';
|
||||||
@ -480,7 +484,7 @@ function WeekCalendarPDF({
|
|||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
...allDayTasks.slice(0, 3).map(t => {
|
...allDayTasks.slice(0, 4).map(t => {
|
||||||
const c = t.completed ? DONE_BG : (t.project?.color || '#3b82f6');
|
const c = t.completed ? DONE_BG : (t.project?.color || '#3b82f6');
|
||||||
return React.createElement(View, {
|
return React.createElement(View, {
|
||||||
key: t.id,
|
key: t.id,
|
||||||
@ -503,9 +507,9 @@ function WeekCalendarPDF({
|
|||||||
}, (t.title || '').slice(0, 36)),
|
}, (t.title || '').slice(0, 36)),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
allDayTasks.length > 3 ? React.createElement(Text, {
|
allDayTasks.length > 4 ? React.createElement(Text, {
|
||||||
style: { fontSize: 6, color: '#3b82f6', marginTop: 1 },
|
style: { fontSize: 6, color: '#3b82f6', marginTop: 1 },
|
||||||
}, `+${allDayTasks.length - 3} ${de ? 'weitere' : 'more'}`) : null,
|
}, `+${allDayTasks.length - 4} ${de ? 'weitere' : 'more'}`) : null,
|
||||||
),
|
),
|
||||||
] : []),
|
] : []),
|
||||||
|
|
||||||
@ -522,7 +526,7 @@ function WeekCalendarPDF({
|
|||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
...noTimeTasks.slice(0, 2).map(t => {
|
...noTimeTasks.slice(0, 4).map(t => {
|
||||||
const c = t.completed ? DONE_BG : (t.project?.color || userStyle.taskColor);
|
const c = t.completed ? DONE_BG : (t.project?.color || userStyle.taskColor);
|
||||||
return React.createElement(View, {
|
return React.createElement(View, {
|
||||||
key: t.id,
|
key: t.id,
|
||||||
@ -545,9 +549,9 @@ function WeekCalendarPDF({
|
|||||||
}, (t.title || '').slice(0, 36)),
|
}, (t.title || '').slice(0, 36)),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
noTimeTasks.length > 2 ? React.createElement(Text, {
|
noTimeTasks.length > 4 ? React.createElement(Text, {
|
||||||
style: { fontSize: 6, color: MUTED, marginTop: 1 },
|
style: { fontSize: 6, color: MUTED, marginTop: 1 },
|
||||||
}, `+${noTimeTasks.length - 2} ${de ? 'weitere' : 'more'}`) : null,
|
}, `+${noTimeTasks.length - 4} ${de ? 'weitere' : 'more'}`) : null,
|
||||||
),
|
),
|
||||||
] : []),
|
] : []),
|
||||||
|
|
||||||
@ -696,7 +700,143 @@ function WeekCalendarPDF({
|
|||||||
),
|
),
|
||||||
|
|
||||||
); // end Page
|
); // end Page
|
||||||
})
|
}),
|
||||||
|
|
||||||
|
// ── Someday page (portrait A4, one column per list)
|
||||||
|
...(somedayLists.length > 0 && showNoTime ? [
|
||||||
|
React.createElement(Page, {
|
||||||
|
key: 'someday',
|
||||||
|
size: 'A4',
|
||||||
|
orientation: 'portrait',
|
||||||
|
style: {
|
||||||
|
fontFamily: 'Helvetica',
|
||||||
|
fontSize: 8,
|
||||||
|
color: '#1e293b',
|
||||||
|
padding: 28,
|
||||||
|
backgroundColor: WHITE,
|
||||||
|
flexDirection: 'column',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Title
|
||||||
|
React.createElement(View, {
|
||||||
|
style: {
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: BORDER,
|
||||||
|
paddingBottom: 6,
|
||||||
|
marginBottom: 12,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'flex-end',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
React.createElement(Text, {
|
||||||
|
style: {
|
||||||
|
fontFamily: headlineFontFamily,
|
||||||
|
fontWeight: headlineFW,
|
||||||
|
fontSize: 16,
|
||||||
|
color: userStyle.weekdayColor,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
},
|
||||||
|
}, (de ? 'Irgendwann' : 'Someday').toUpperCase()),
|
||||||
|
React.createElement(Text, {
|
||||||
|
style: { fontSize: 7, color: '#94a3b8' },
|
||||||
|
}, userName),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Lists in columns (flex wrap)
|
||||||
|
React.createElement(View, {
|
||||||
|
style: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: 12,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...somedayLists.map(list =>
|
||||||
|
React.createElement(View, {
|
||||||
|
key: list.id,
|
||||||
|
style: {
|
||||||
|
width: '30%',
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// List name
|
||||||
|
React.createElement(View, {
|
||||||
|
style: {
|
||||||
|
borderLeftWidth: 3,
|
||||||
|
borderLeftColor: list.tasks[0]?.project?.color || userStyle.weekdayColor,
|
||||||
|
paddingLeft: 6,
|
||||||
|
marginBottom: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
React.createElement(Text, {
|
||||||
|
style: {
|
||||||
|
fontFamily: 'Helvetica-Bold',
|
||||||
|
fontSize: 8,
|
||||||
|
color: '#374151',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: 0.4,
|
||||||
|
},
|
||||||
|
}, list.title),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Task rows
|
||||||
|
...list.tasks.slice(0, 20).map(t =>
|
||||||
|
React.createElement(View, {
|
||||||
|
key: t.id,
|
||||||
|
style: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
paddingVertical: 2.5,
|
||||||
|
paddingHorizontal: 4,
|
||||||
|
marginBottom: 1,
|
||||||
|
borderRadius: 2,
|
||||||
|
backgroundColor: '#f8fafc',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
React.createElement(View, {
|
||||||
|
style: {
|
||||||
|
width: 6, height: 6, borderRadius: 3,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: t.project?.color || MUTED,
|
||||||
|
marginRight: 5,
|
||||||
|
marginTop: 0.5,
|
||||||
|
flexShrink: 0,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
React.createElement(Text, {
|
||||||
|
style: { fontSize: 7.5, color: '#374151', lineHeight: 1.3, flex: 1 },
|
||||||
|
}, (t.title || '').slice(0, 55)),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
list.tasks.length > 20 ? React.createElement(Text, {
|
||||||
|
style: { fontSize: 6.5, color: MUTED, marginTop: 2, paddingLeft: 4 },
|
||||||
|
}, `+${list.tasks.length - 20} ${de ? 'weitere' : 'more'}`) : null,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Footer
|
||||||
|
React.createElement(View, {
|
||||||
|
style: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 20, left: 28, right: 28,
|
||||||
|
borderTopWidth: 1, borderTopColor: BORDER,
|
||||||
|
paddingTop: 3,
|
||||||
|
flexDirection: 'row', justifyContent: 'space-between',
|
||||||
|
},
|
||||||
|
fixed: true,
|
||||||
|
},
|
||||||
|
React.createElement(Text, {
|
||||||
|
style: { fontSize: 7, color: '#94a3b8' },
|
||||||
|
}, `${de ? 'Irgendwann' : 'Someday'} · ${userName}`),
|
||||||
|
React.createElement(Text, {
|
||||||
|
style: { fontSize: 7, color: '#94a3b8' },
|
||||||
|
render: ({ pageNumber, totalPages: tp }: { pageNumber: number; totalPages: number }) =>
|
||||||
|
`${pageNumber} / ${tp}`,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
] : []),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -743,9 +883,8 @@ export async function GET(request: Request) {
|
|||||||
|
|
||||||
const userTz = (user as any).timezone || 'UTC';
|
const userTz = (user as any).timezone || 'UTC';
|
||||||
|
|
||||||
// Register Oswald font (best effort)
|
// Register Oswald font (best effort) — use module-level flag, not Font's internal API
|
||||||
await ensureFonts();
|
await ensureFonts();
|
||||||
const oswaldAvailable = Font.getRegisteredFontFamilies?.()?.includes?.('Oswald') ?? false;
|
|
||||||
|
|
||||||
const userStyle: UserStyle = {
|
const userStyle: UserStyle = {
|
||||||
weekdayColor: (user as any).weekdayColor || '#0ea5e9',
|
weekdayColor: (user as any).weekdayColor || '#0ea5e9',
|
||||||
@ -754,7 +893,7 @@ export async function GET(request: Request) {
|
|||||||
dateColor: (user as any).dateColor || '#888888',
|
dateColor: (user as any).dateColor || '#888888',
|
||||||
taskColor: (user as any).taskColor || '#6366f1',
|
taskColor: (user as any).taskColor || '#6366f1',
|
||||||
headlineFontWeight: (user as any).headlineFontWeight || '900',
|
headlineFontWeight: (user as any).headlineFontWeight || '900',
|
||||||
useOswald: oswaldAvailable,
|
useOswald: _oswaldRegistered,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Fetch tasks (expanded range for timezone safety)
|
// ── Fetch tasks (expanded range for timezone safety)
|
||||||
@ -822,6 +961,34 @@ export async function GET(request: Request) {
|
|||||||
} catch { /* silent */ }
|
} catch { /* silent */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Fetch someday list tasks (unscheduled, in named lists)
|
||||||
|
type SomedayListWithTasks = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
tasks: { id: string; title: string; completed: boolean; project: { name: string; color: string | null } | null }[];
|
||||||
|
};
|
||||||
|
let somedayLists: SomedayListWithTasks[] = [];
|
||||||
|
if (showNoTime) {
|
||||||
|
const raw = await prisma.somedayList.findMany({
|
||||||
|
where: { userId: user.id },
|
||||||
|
orderBy: { order: 'asc' },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
tasks: {
|
||||||
|
where: { deletedAt: null, completed: false },
|
||||||
|
orderBy: { order: 'asc' },
|
||||||
|
select: {
|
||||||
|
id: true, title: true, completed: true,
|
||||||
|
project: { select: { name: true, color: true } },
|
||||||
|
},
|
||||||
|
take: 60,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
somedayLists = raw.filter(l => l.tasks.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Build pages & render
|
// ── Build pages & render
|
||||||
const pages = groupIntoPages(
|
const pages = groupIntoPages(
|
||||||
new Date(startDate + 'T00:00:00'),
|
new Date(startDate + 'T00:00:00'),
|
||||||
@ -834,6 +1001,7 @@ export async function GET(request: Request) {
|
|||||||
startHour, endHour, showAllDay, showNoTime, de,
|
startHour, endHour, showAllDay, showNoTime, de,
|
||||||
userName: user.name || user.email || '',
|
userName: user.name || user.email || '',
|
||||||
userStyle,
|
userStyle,
|
||||||
|
somedayLists,
|
||||||
totalPages: pages.length,
|
totalPages: pages.length,
|
||||||
}) as React.ReactElement<any>
|
}) as React.ReactElement<any>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user