Files
verdande/web/src/routes/faerdige/+page.svelte
T
kristianwindandClaude Opus 5 6154e6b247 feat(udgaver): urd — noter-udgaven siger sit eget navn
Verdande og Urd er to af nornerne i Völuspá, og digtet har dem skære
mærker i træ: *skáru á skíði*. Verdande er det der bliver; Urd er det der
blev, og blev gemt. En instans startet uden opgaveruterne er et andet
produkt for den der bruger den, så den siger et andet navn.

Navnet læses fra udgaven i ÉT sted — Config.ProductName/ProductSlug —
frem for at stå skrevet i teksterne. Frontend-bygget er byte-identisk i
begge udgaver; serveren omdøber de to filer, browseren læser før noget
JavaScript kører.

Der er to titler, og det blev først klart, da e2e-prøven sagde det:
skallens <title>, som serveren omdøber, og rutens egen i <svelte:head>,
som appen sætter bagefter og som overskriver den første. Go-prøven var
grøn over de bytes, serveren sendte, mens fanen sagde "Noter · verdande".
Begge prøver spørger nu om begge.

Omdøbningen er ankret på hele strenge frem for på ordet "verdande", fordi
app.html også indeholder localStorage.getItem('verdande:theme'). Omdøbes
præfikset der og ikke i resten af appen, bliver temaet sat fra én nøgle og
læst fra en anden, og det eneste symptom er det hvide blink, det
indlejrede script findes for at undgå. Tre plantede fejl bekræfter, at det
er de rigtige prøver, der fanger det: et flyttet anker i app.html, en bred
søg-erstat, og en applyRebrands uden fejlretur.

Mailen var det vigtigste sted. appName var et felt med en værdi og ingen
læser, og kommentaren ved siden af sagde, at det stod i hver besked — de
tre steder havde ordet skrevet ud i stedet. En invitation fra en
urd-instans går til et andet menneskes indbakke.

Også omdøbt: web-manifestet (og sendt som application/manifest+json — Go
kender ikke .webmanifest og sniffer den til text/plain, hvorefter
browseren ignorerer manifestet uden at sige noget, og installationsknappen
bare ikke er der), mærket i menuen (ᚢ uruz frem for ᚹ wunjo),
opstartsloggen, passkey-promptens visningsnavn, ICS-kalendernavnet og
udseendet, der er opkaldt efter programmet.

Ikonet er IKKE med: icon.svg er stadig ᚹ, og de tre PNG'er genereres fra
den. Det rører binære, genererede filer og hører i sin egen ændring.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-07 01:11:25 +02:00

141 lines
4.0 KiB
Svelte

<script>
/**
* Færdige: everything that has been closed, newest first.
*
* Completing a task is one click on a small circle beside a row you were only
* reading, and it takes the row off the screen — so the mistake and the evidence
* of it leave together. Somebody who clicked the wrong one had no name to search
* for and nowhere to look: the project page can show its own closed tasks, but
* only if you already know which project it was in.
*
* The toast that appears on completing offers an undo, which covers the mistake
* you notice at once. This covers the one you notice tomorrow.
*
* Newest first, decided by the server. A record of what has been finished is
* read backwards from now, not by a due date the task no longer has.
*/
import { api } from '$lib/api.js';
import { app } from '$lib/stores.svelte.js';
import { t, tag } from '$lib/i18n.svelte.js';
import TaskRow from '$lib/components/TaskRow.svelte';
let loaded = $state(false);
$effect(() => {
app.loadTasks({ completed: 'only', limit: 200 }).then(() => (loaded = true));
});
// Read back out of the store, so a task reopened from here leaves the list at
// once rather than on the next load — which is the whole reason to be here.
let done = $derived(app.tasks.filter((task) => task.completed));
/**
* Grouped by the day it was closed.
*
* A flat list of two hundred rows says when nothing. "I dag" and "I går" are
* how anybody looking for something they just closed would describe it.
*/
/**
* Dagen en opgave blev lukket, målt hvor personen står.
*
* Ikke `completed_at.slice(0, 10)`. Tidsstemplet er i UTC, og de ti første tegn
* er derfor UTC's dato — mens overskriften nedenfor sammenligner med *lokal*
* dato. I to timer efter midnat i dansk sommertid er de to ikke den samme dag,
* så en opgave lukket 00.30 lå under "I går", indtil klokken blev to.
*
* Fundet af prøven, som kørte forbi midnat for første gang. Den havde været
* grøn hver eneste gang før, fordi de to datoer er ens tyve timer i døgnet.
*/
const localDay = (iso) => {
if (!iso) return '';
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return '';
const pad = (n) => String(n).padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
};
let days = $derived.by(() => {
const groups = new Map();
for (const task of done) {
const key = localDay(task.completed_at);
if (!groups.has(key)) groups.set(key, []);
groups.get(key).push(task);
}
return [...groups.entries()].map(([date, tasks]) => ({ date, tasks }));
});
function heading(date) {
if (!date) return t('done.unknownDay');
const day = new Date(date + 'T00:00:00');
const today = new Date(new Date().toDateString());
const diff = Math.round((day - today) / 86400000);
if (diff === 0) return t('task.today');
if (diff === -1) return t('task.yesterday');
return day.toLocaleDateString(tag(), { weekday: 'long', day: 'numeric', month: 'long' });
}
</script>
<svelte:head><title>{t('done.title')} — {app.productSlug}</title></svelte:head>
<div class="view">
<header>
<h1>{t('done.title')}</h1>
<p>{t('done.hint')}</p>
</header>
{#each days as day (day.date)}
<section>
<h2>{heading(day.date)}</h2>
{#each day.tasks as task (task.id)}
<TaskRow {task} />
{/each}
</section>
{/each}
{#if loaded && !done.length}
<p class="empty">{t('done.empty')}</p>
{/if}
</div>
<style>
.view {
max-width: var(--content-max);
margin: 0 auto;
padding: var(--s6) var(--s4) var(--s8);
}
header {
margin-bottom: var(--s5);
}
h1 {
font-size: var(--text-2xl);
}
header p {
margin: var(--s1) 0 0;
font-size: var(--text-sm);
color: var(--ink-muted);
}
section {
margin-bottom: var(--s5);
}
h2 {
font-size: var(--text-xs);
font-weight: 560;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--ink-faint);
padding-bottom: var(--s2);
border-bottom: 1px solid var(--line);
margin-bottom: var(--s2);
}
.empty {
font-size: var(--text-sm);
color: var(--ink-faint);
}
</style>