Split conversations/+page.svelte (346 lines) into: - ConversationFilterBar.svelte: person A/B typeaheads, swap button, date range, sort toggle - ConversationTimeline.svelte: summary bar, chat bubbles, year dividers, new-doc link Page drops from 346 → ~70 lines; navigation logic and filter state stay in the page. Part of #75 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
165 lines
5.0 KiB
Svelte
165 lines
5.0 KiB
Svelte
<script lang="ts">
|
|
import { m } from '$lib/paraglide/messages.js';
|
|
import { formatDate } from '$lib/utils/date';
|
|
|
|
let {
|
|
documents,
|
|
senderId,
|
|
receiverId,
|
|
canWrite
|
|
}: {
|
|
documents: {
|
|
id: string;
|
|
title?: string;
|
|
originalFilename: string;
|
|
documentDate?: string;
|
|
location?: string;
|
|
status: string;
|
|
sender?: { id: string; firstName: string; lastName: string } | null;
|
|
}[];
|
|
senderId: string;
|
|
receiverId: string;
|
|
canWrite: boolean;
|
|
} = $props();
|
|
|
|
const documentYears = $derived(
|
|
documents
|
|
.map((doc) => (doc.documentDate ? new Date(doc.documentDate).getFullYear() : null))
|
|
.filter((y): y is number => y !== null)
|
|
);
|
|
const yearFrom = $derived(documentYears.length > 0 ? Math.min(...documentYears) : null);
|
|
const yearTo = $derived(documentYears.length > 0 ? Math.max(...documentYears) : null);
|
|
|
|
const enrichedDocuments = $derived(
|
|
documents.map((doc, i) => {
|
|
const year = doc.documentDate ? new Date(doc.documentDate).getFullYear() : null;
|
|
const prevYear =
|
|
i > 0 && documents[i - 1].documentDate
|
|
? new Date(documents[i - 1].documentDate!).getFullYear()
|
|
: null;
|
|
return { doc, year, showYearDivider: year !== null && year !== prevYear };
|
|
})
|
|
);
|
|
</script>
|
|
|
|
<!-- Summary bar -->
|
|
<div class="mb-4 flex items-center justify-between">
|
|
{#if yearFrom !== null && yearTo !== null}
|
|
<p data-testid="conv-summary" class="font-sans text-sm font-medium text-ink/70">
|
|
{m.conv_summary({ count: documents.length, yearFrom, yearTo })}
|
|
</p>
|
|
{:else}
|
|
<p data-testid="conv-summary" class="font-sans text-sm font-medium text-ink/70">
|
|
{documents.length}
|
|
</p>
|
|
{/if}
|
|
{#if canWrite}
|
|
<a
|
|
data-testid="conv-new-doc-link"
|
|
href="/documents/new?senderId={senderId}&receiverId={receiverId}"
|
|
class="inline-flex items-center gap-1 text-sm font-medium text-ink/60 transition-colors hover:text-ink"
|
|
>
|
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"
|
|
></path>
|
|
</svg>
|
|
{m.conv_new_doc_link()}
|
|
</a>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- CHAT CONTAINER -->
|
|
<div class="relative overflow-hidden rounded-sm border border-line bg-surface shadow-sm">
|
|
<!-- Decoration: Central Timeline Line -->
|
|
<div
|
|
class="absolute top-0 bottom-0 left-1/2 hidden w-px -translate-x-1/2 transform bg-muted md:block"
|
|
></div>
|
|
|
|
<div class="p-6 md:p-8">
|
|
<div class="relative z-10 flex flex-col gap-4">
|
|
{#each enrichedDocuments as { doc, year, showYearDivider } (doc.id)}
|
|
{#if showYearDivider}
|
|
<div data-testid="year-divider" class="relative flex items-center py-2 text-center">
|
|
<div class="flex-grow border-t border-line"></div>
|
|
<span class="mx-4 font-sans text-xs font-bold tracking-widest text-ink/40 uppercase"
|
|
>{year}</span
|
|
>
|
|
<div class="flex-grow border-t border-line"></div>
|
|
</div>
|
|
{/if}
|
|
{@const isRight = doc.sender?.id === senderId}
|
|
|
|
<!-- Message Row -->
|
|
<div class="flex w-full {isRight ? 'justify-end' : 'justify-start'}">
|
|
<!-- Bubble Group -->
|
|
<div
|
|
class="flex max-w-[90%] gap-3 md:max-w-[70%] {isRight
|
|
? 'flex-row-reverse'
|
|
: 'flex-row'}"
|
|
>
|
|
<!-- AVATAR -->
|
|
<div class="mt-auto mb-1 hidden flex-shrink-0 sm:block">
|
|
<div
|
|
class="flex h-8 w-8 items-center justify-center rounded-full border font-serif text-xs shadow-sm
|
|
{isRight
|
|
? 'border-primary bg-primary text-primary-fg'
|
|
: 'border-line bg-surface text-ink'}"
|
|
>
|
|
{#if doc.sender}
|
|
{doc.sender.firstName[0]}{doc.sender.lastName[0]}
|
|
{:else}
|
|
?
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- BUBBLE CARD -->
|
|
<a
|
|
href="/documents/{doc.id}"
|
|
class="group block transform rounded border p-4 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md
|
|
{isRight
|
|
? 'rounded-br-none border-primary bg-primary text-primary-fg'
|
|
: 'rounded-bl-none border-line bg-muted/50 text-ink'}"
|
|
>
|
|
<!-- Header -->
|
|
<div class="mb-2 flex items-start justify-between gap-4">
|
|
<h3
|
|
class="font-serif text-sm leading-snug font-medium {isRight
|
|
? 'text-primary-fg'
|
|
: 'text-ink'}"
|
|
>
|
|
{doc.title || doc.originalFilename}
|
|
</h3>
|
|
|
|
<!-- Status Dot -->
|
|
<span
|
|
class="mt-1.5 h-1.5 w-1.5 flex-shrink-0 rounded-full
|
|
{doc.status === 'UPLOADED' ? 'bg-accent' : 'bg-yellow-400'}"
|
|
title={doc.status}
|
|
>
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Metadata -->
|
|
<div
|
|
class="flex flex-wrap gap-3 font-sans text-[10px] tracking-wider uppercase opacity-80 {isRight
|
|
? 'text-primary-fg/70'
|
|
: 'text-ink-2'}"
|
|
>
|
|
<span class="flex items-center">
|
|
{doc.documentDate ? formatDate(doc.documentDate) : '—'}
|
|
</span>
|
|
{#if doc.location}
|
|
<span class="flex items-center">
|
|
• {doc.location}
|
|
</span>
|
|
{/if}
|
|
</div>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
</div>
|