feat(person): show notable co-correspondents #22

Closed
opened 2026-03-19 21:14:05 +01:00 by marcel · 0 comments
Owner

User Journey

Klaus opens Marta's person page. He wants to understand who Marta's most important contacts were — but currently he has to read every document title and mentally track who appears most often.

With this feature, Klaus sees a small "Häufige Korrespondenten" section near the top showing the 5 people Marta wrote to or received from most: Hans (12), Maria (8), Peter (3). Each is a clickable chip linking to their person page. Klaus clicks Hans and immediately sees their shared correspondence.


High-Level Plan

Derive co-correspondent frequency from the already-loaded sent and received documents. Group by person, count, sort, take top 5. Render as clickable chips. Pure frontend — no backend changes or additional API calls.

Depends on: issue #1 (sent + received documents available on the page).


Detailed Plan

Frontend only

  1. Frequency computation in persons/[id]/+page.svelte:

    const coCorrespondents = $derived(() => {
        const freq = new Map<string, { id: string; name: string; count: number }>();
    
        // From sent documents: count each receiver
        for (const doc of data.sentDocuments) {
            for (const receiver of doc.receivers ?? []) {
                const key = receiver.id;
                const existing = freq.get(key);
                if (existing) existing.count++;
                else freq.set(key, { id: receiver.id, name: `${receiver.firstName} ${receiver.lastName}`, count: 1 });
            }
        }
    
        // From received documents: count the sender
        for (const doc of data.receivedDocuments) {
            if (doc.sender && doc.sender.id !== data.person.id) {
                const key = doc.sender.id;
                const existing = freq.get(key);
                if (existing) existing.count++;
                else freq.set(key, { id: doc.sender.id, name: `${doc.sender.firstName} ${doc.sender.lastName}`, count: 1 });
            }
        }
    
        return [...freq.values()].sort((a, b) => b.count - a.count).slice(0, 5);
    });
    
  2. Render — show section only when coCorrespondents.length > 0, placed between the header card and the statistics bar:

    {#if coCorrespondents.length > 0}
    <div class="mb-6">
        <h3 class="text-xs font-bold uppercase tracking-widest text-gray-400 mb-3">
            {m.person_co_correspondents_heading()}
        </h3>
        <div class="flex flex-wrap gap-2">
            {#each coCorrespondents as c}
                <a href="/persons/{c.id}"
                   class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full border border-brand-sand text-sm font-serif text-brand-navy hover:border-brand-navy transition-colors">
                    {c.name}
                    <span class="text-xs text-gray-400 font-sans">({c.count})</span>
                </a>
            {/each}
        </div>
    </div>
    {/if}
    
  3. i18n key person_co_correspondents_heading:

    • DE: "Häufige Korrespondenten"
    • EN: "Frequent correspondents"
    • ES: "Corresponsales frecuentes"

Acceptance Criteria

  • Shows at most 5 co-correspondents, ordered by frequency descending
  • Each chip links to the corresponding person page
  • Count in parentheses reflects combined sent + received
  • Section is hidden when no co-correspondents exist (e.g. person has no documents)
  • Depends on issue feat: show received documents on person detail page (#1)
  • No backend changes, no additional API calls
## User Journey Klaus opens Marta's person page. He wants to understand who Marta's most important contacts were — but currently he has to read every document title and mentally track who appears most often. With this feature, Klaus sees a small **"Häufige Korrespondenten"** section near the top showing the 5 people Marta wrote to or received from most: *Hans (12)*, *Maria (8)*, *Peter (3)*. Each is a clickable chip linking to their person page. Klaus clicks Hans and immediately sees their shared correspondence. --- ## High-Level Plan Derive co-correspondent frequency from the already-loaded sent and received documents. Group by person, count, sort, take top 5. Render as clickable chips. Pure frontend — no backend changes or additional API calls. **Depends on:** issue #1 (sent + received documents available on the page). --- ## Detailed Plan ### Frontend only 1. **Frequency computation** in `persons/[id]/+page.svelte`: ```typescript const coCorrespondents = $derived(() => { const freq = new Map<string, { id: string; name: string; count: number }>(); // From sent documents: count each receiver for (const doc of data.sentDocuments) { for (const receiver of doc.receivers ?? []) { const key = receiver.id; const existing = freq.get(key); if (existing) existing.count++; else freq.set(key, { id: receiver.id, name: `${receiver.firstName} ${receiver.lastName}`, count: 1 }); } } // From received documents: count the sender for (const doc of data.receivedDocuments) { if (doc.sender && doc.sender.id !== data.person.id) { const key = doc.sender.id; const existing = freq.get(key); if (existing) existing.count++; else freq.set(key, { id: doc.sender.id, name: `${doc.sender.firstName} ${doc.sender.lastName}`, count: 1 }); } } return [...freq.values()].sort((a, b) => b.count - a.count).slice(0, 5); }); ``` 2. **Render** — show section only when `coCorrespondents.length > 0`, placed between the header card and the statistics bar: ```svelte {#if coCorrespondents.length > 0} <div class="mb-6"> <h3 class="text-xs font-bold uppercase tracking-widest text-gray-400 mb-3"> {m.person_co_correspondents_heading()} </h3> <div class="flex flex-wrap gap-2"> {#each coCorrespondents as c} <a href="/persons/{c.id}" class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full border border-brand-sand text-sm font-serif text-brand-navy hover:border-brand-navy transition-colors"> {c.name} <span class="text-xs text-gray-400 font-sans">({c.count})</span> </a> {/each} </div> </div> {/if} ``` 3. **i18n key** `person_co_correspondents_heading`: - DE: `"Häufige Korrespondenten"` - EN: `"Frequent correspondents"` - ES: `"Corresponsales frecuentes"` ### Acceptance Criteria - [ ] Shows at most 5 co-correspondents, ordered by frequency descending - [ ] Each chip links to the corresponding person page - [ ] Count in parentheses reflects combined sent + received - [ ] Section is hidden when no co-correspondents exist (e.g. person has no documents) - [ ] Depends on issue #1 - [ ] No backend changes, no additional API calls
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#22