All checks were successful
Deploy production / deploy (push) Successful in 2m32s
Lists contracts/pension docs with written summaries, an inline viewer and download at /trackers/money/documents. Files and summaries live on the host (MONEY_DOCUMENTS_ROOT, bind-mounted read-only by deploy-remote.sh), never in git or static/; the file endpoint is admin-only (401/403) and private, no-store.
129 lines
3.7 KiB
Svelte
129 lines
3.7 KiB
Svelte
<script lang="ts">
|
|
import { resolve } from '$app/paths';
|
|
import { Badge } from '#lib/components/ui/badge/index.js';
|
|
import { Button } from '#lib/components/ui/button/index.js';
|
|
import ArrowLeft from 'lucide-svelte/icons/arrow-left';
|
|
import Download from 'lucide-svelte/icons/download';
|
|
import Eye from 'lucide-svelte/icons/eye';
|
|
import EyeOff from 'lucide-svelte/icons/eye-off';
|
|
import ExternalLink from 'lucide-svelte/icons/external-link';
|
|
import type { PageProps } from './$types';
|
|
|
|
let { data }: PageProps = $props();
|
|
let viewing = $state<string | null>(null);
|
|
|
|
const dateFormat = new Intl.DateTimeFormat('en-NL', { dateStyle: 'medium' });
|
|
function dateLabel(date: string) {
|
|
return dateFormat.format(new Date(date + 'T12:00:00'));
|
|
}
|
|
function fileHref(id: string, download = false) {
|
|
const href = resolve('/trackers/money/documents/[id]/file', { id });
|
|
return download ? href + '?download' : href;
|
|
}
|
|
</script>
|
|
|
|
<svelte:head>
|
|
<title>Documents · Money</title>
|
|
<meta name="robots" content="noindex, nofollow" />
|
|
</svelte:head>
|
|
|
|
<div class="mx-auto max-w-screen-lg space-y-6 px-4 py-8 sm:px-6 sm:py-10">
|
|
<header>
|
|
<a
|
|
class="inline-flex items-center gap-1 text-sm opacity-70 hover:opacity-100"
|
|
href={resolve('/trackers/money')}><ArrowLeft size={14} /> Money</a
|
|
>
|
|
<h1 class="text-2xl font-bold">Documents</h1>
|
|
<p class="text-sm opacity-70">
|
|
Contracts and other paperwork, each with a summary of what matters for money. Private: only
|
|
visible to you.
|
|
</p>
|
|
</header>
|
|
|
|
{#if data.documents.length === 0}
|
|
<p class="rounded-lg border p-6 text-sm opacity-70">No documents yet.</p>
|
|
{/if}
|
|
|
|
{#each data.documents as document (document.id)}
|
|
<article class="space-y-4 rounded-lg border p-5" id={document.id}>
|
|
<div class="flex flex-wrap items-start justify-between gap-3">
|
|
<div class="space-y-1">
|
|
<h2 class="text-lg font-semibold">{document.title}</h2>
|
|
<p class="flex flex-wrap items-center gap-2 text-sm opacity-70">
|
|
<Badge variant="secondary">{document.category}</Badge>
|
|
{#if document.date}<span>{dateLabel(document.date)}</span>{/if}
|
|
{#if document.pages}<span>· {document.pages} pages</span>{/if}
|
|
<span>· {document.fileName}</span>
|
|
</p>
|
|
</div>
|
|
<div class="flex flex-wrap gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
aria-expanded={viewing === document.id}
|
|
onclick={() => (viewing = viewing === document.id ? null : document.id)}
|
|
>
|
|
{#if viewing === document.id}<EyeOff size={14} /> Hide{:else}<Eye size={14} /> View{/if}
|
|
</Button>
|
|
<Button variant="outline" size="sm" href={fileHref(document.id)} target="_blank"
|
|
><ExternalLink size={14} /> Open</Button
|
|
>
|
|
<Button variant="outline" size="sm" href={fileHref(document.id, true)}
|
|
><Download size={14} /> Download</Button
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
{#if viewing === document.id}
|
|
<iframe
|
|
class="h-[75vh] w-full rounded border"
|
|
title={document.title}
|
|
src={fileHref(document.id)}
|
|
></iframe>
|
|
{/if}
|
|
|
|
<div class="document-summary text-sm leading-relaxed">
|
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -- owner-written manifest on the host -->
|
|
{@html document.summaryHtml}
|
|
</div>
|
|
</article>
|
|
{/each}
|
|
</div>
|
|
|
|
<style>
|
|
.document-summary :global {
|
|
h3 {
|
|
margin-top: 1rem;
|
|
font-weight: 600;
|
|
}
|
|
p,
|
|
ul,
|
|
ol,
|
|
table {
|
|
margin-top: 0.5rem;
|
|
}
|
|
ul {
|
|
list-style: disc;
|
|
padding-left: 1.25rem;
|
|
}
|
|
ol {
|
|
list-style: decimal;
|
|
padding-left: 1.25rem;
|
|
}
|
|
table {
|
|
border-collapse: collapse;
|
|
width: 100%;
|
|
}
|
|
th,
|
|
td {
|
|
border-bottom: 1px solid color-mix(in oklab, currentColor 15%, transparent);
|
|
padding: 0.3rem 0.5rem 0.3rem 0;
|
|
text-align: left;
|
|
vertical-align: top;
|
|
}
|
|
th {
|
|
font-weight: 600;
|
|
}
|
|
}
|
|
</style>
|