arnaucube
arnaucube/jellyfin-toolsPublic Self-hosted repository
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>