jellyfin-tools/jellyfin-year-review/web/src/lib/ViewingTimeline.svelte
jellyfin-year-review/web/src/lib/ViewingTimeline.svelteRaw
1<script>
2 import { t, locale } from './i18n.js';
3 import Poster from './Poster.svelte';
4
5 let { items, year, kind } = $props();
6 const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'].map(value => t(value));
7 let titles = $derived([...items].sort((a, b) => a.title.localeCompare(b.title, locale) || a.id.localeCompare(b.id)));
8 let groups = $derived(monthNames.map((name, i) => ({
9 month: i + 1, name, items: titles.filter(item => item.months?.includes(i + 1))
10 })).filter(group => group.items.length));
11 // Keep older exports usable without inventing viewing dates for their titles.
12 let undated = $derived(titles.filter(item => !item.months?.length));
13</script>
14
15{#if items.length}
16 <div class="viewing-timeline" aria-label={t(kind === 'movies' ? "Movies watched by month" : "TV shows watched by month")}>
17 {#each groups as group (group.month)}
18 <section class="timeline-month" aria-label={t("{month} {year}", { month: group.name, year })}>
19 <h3><time datetime="{year}-{String(group.month).padStart(2, '0')}">{t("{month} {year}", { month: group.name, year })}</time></h3>
20 <ul class="timeline-titles">
21 {#each group.items as item (item.id)}
22 <li><Poster {item} /><h4>{item.title}</h4></li>
23 {/each}
24 </ul>
25 </section>
26 {/each}
27 {#if undated.length}
28 <section class="timeline-month" aria-label={t("Viewing dates unavailable")}>
29 <h3>{t("Viewing dates unavailable")}</h3>
30 <ul class="timeline-titles">
31 {#each undated as item (item.id)}
32 <li><Poster {item} /><h4>{item.title}</h4></li>
33 {/each}
34 </ul>
35 </section>
36 {/if}
37 </div>
38{:else}
39 <p class="story-lede">{t(kind === 'movies' ? "No movies watched in {year}." : "No tv shows watched in {year}.", { year })}</p>
40{/if}
41
42<style>
43 .viewing-timeline { margin-top: 32px; padding-left: 24px; border-left: 2px solid rgba(var(--accent-rgb), .25); }
44 .timeline-month { position: relative; padding-bottom: 28px; }
45 .timeline-month:last-child { padding-bottom: 0; }
46 .timeline-month::before { content: ''; position: absolute; width: 10px; height: 10px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 14px rgba(var(--accent-rgb), .6); left: -30px; top: 6px; }
47 h3 { color: var(--accent); font-size: 1.2rem; margin-bottom: 16px; }
48 .timeline-titles { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr)); gap: 16px 24px; }
49 li { display: flex; align-items: center; gap: 14px; min-width: 0; }
50 h4 { margin: 0; font-size: 1rem; line-height: 1.5; overflow-wrap: anywhere; }
51 @media (max-width: 760px) { .viewing-timeline { padding-left: 18px; } .timeline-month::before { left: -24px; } }
52</style>