Release / Build and publish image (push) Failing after 9m56s
Fundet af prøverne, som for første gang kørte forbi midnat. Dagen blev læst som `completed_at.slice(0, 10)` — de ti første tegn af et tidsstempel, der er i UTC. Overskriften sammenlignede med *lokal* dato. I dansk sommertid er de to ikke den samme dag i de første to timer efter midnat, så en opgave lukket 00.30 blev sat under "I går", indtil klokken blev to. Tyve timer i døgnet er de ens, hvilket er derfor prøven har været grøn hver eneste gang før. Dagen udregnes nu lokalt, som den overskrift den skal svare til. Og prøven for invitationslinket kunne kun bestå seks dage om ugen. Den ledte efter "males inden fredag" som én tekst, men hurtig tilføjelse læser "fredag" som forfaldsdatoen og tager ordet ud af titlen — tilbage står "males inden" plus et mærke, hvis ord skifter med ugedagen. Kørt på en torsdag hedder mærket "I morgen", og så fandt den ingenting. Prøven handler om invitationer, ikke om datoer, så den ser nu på titlen alene. Uddraget i notelisten viste `;
|
|
|
|
$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')} — verdande</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>
|