feat(projects): et projekts kalender kan vises som en uge

Ugen fandtes på Kommende og på Kalender, men ikke dér hvor et projekt bor — og det
er dér, "hvornår sker de her fire ting" oftest bliver spurgt.

Inde i kalendervisningen frem for som en fjerde `view_mode`. `view_mode` er
projektets og delt med alle, der kan se det, mens uge eller måned er et spørgsmål
om hvor bred skærmen er, foran hvilken der sidder én person — samme grund som
sidebjælkens bredde ligger i localStorage af. Og en fjerde værdi ville kræve en
tabelombygning: `CHECK (view_mode IN ('list','board','calendar'))` kan SQLite ikke
ændre på plads.

**Og så fandt den en fejl, der har ligget og ventet.** `cursor` blev sat med
`span === 'week' ? … : …`, hvilket læser `span` i det øjeblik komponenten bliver
til og fanger den værdi for altid. Byggeriet har advaret om det lige så længe. Så
længe hver visning havde sin egen komponent, gjorde det ingen forskel — og det
gjorde det i samme sekund, en vælger begyndte at skifte `span` på den samme.
Markøren stod på den 1. i måneden, og et skift til uge landede på ugen omkring den
1.: "27. jul.–2. aug." for en person, der kiggede på august.

Rettet, med den ene undtagelse skrevet ned: en månedsvisnings markør *er* den
første i måneden, så et skift til uge, mens man står i den indeværende måned,
skal lande på den uge, i dag ligger i — ikke på ugen omkring den 1.

Og en anden fælde, jeg selv gik i undervejs: en underklasses felter sættes *efter*
basens konstruktør er kørt. Nøglen til localStorage som et felt på en underklasse
betød, at valget blev gemt ét sted og læst et andet — en uge, man havde valgt, var
en måned igen efter en genindlæsning. Nøglen er et argument nu; et argument har
ingen rækkefølge at tage fejl af.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kristian Wind
2026-08-20 21:45:38 +02:00
co-authored by Claude Opus 5
parent dcb71f4c14
commit 868588de10
4 changed files with 146 additions and 8 deletions
+22 -2
View File
@@ -64,7 +64,14 @@
events = []
} = $props();
let cursor = $state(span === 'week' ? startOfWeek(new Date()) : startOfMonth(new Date()));
// Sat til i dag og lagt til rette af effekten nedenfor.
//
// Ikke `span === 'week' ? … : …` her: den læser `span`, som den er i det
// øjeblik komponenten bliver til, og fanger den værdi for altid — hvilket
// byggeriet har advaret om lige så længe. Så længe hver visning havde sin egen
// komponent, gjorde det ingen forskel; det gjorde det i samme sekund, et
// projekts kalender fik en uge/måned-vælger, der skifter `span` på den samme.
let cursor = $state(new Date());
/**
* The weekday names, from Intl rather than written out.
@@ -101,13 +108,26 @@
// week with a cursor on the 1st would otherwise show the week containing the
// 1st rather than the week you were looking at, and switching back would land
// on whichever month that week happened to start in.
//
// Med én undtagelse, og den er hele grunden til at det her er en effekt og ikke
// en udregning: en månedsvisnings markør *er* den første i måneden. Skifter man
// til uge, mens man står i den indeværende måned, er den uge man kigger på ikke
// ugen omkring den 1. — det er den, i dag ligger i. Uden det her landede et
// projekts kalender på "27. jul.–2. aug." for en person, der kiggede på august.
$effect(() => {
const aligned = startOfSpan(cursor);
const anchor =
span === 'week' && isMonthStart(cursor) && sameMonth(cursor, new Date())
? new Date()
: cursor;
const aligned = startOfSpan(anchor);
// Compared by value, not identity: assigning a fresh Date on every run would
// re-trigger this effect forever, since the effect reads what it writes.
if (aligned.getTime() !== cursor.getTime()) cursor = aligned;
});
const isMonthStart = (d) => d.getDate() === 1;
const sameMonth = (a, b) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth();
function iso(d) {
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(
d.getDate()