jellyfin-tools/jellyfin-year-review/web/src/lib/RankingPosition.svelte
jellyfin-year-review/web/src/lib/RankingPosition.svelteRaw
1<script>
2 let { ranking } = $props();
3 let above = $derived(ranking.rank - 1);
4 // The percentile counts strictly lower totals, excluding the current viewer.
5 let below = $derived(Math.round(ranking.percentile * (ranking.cohort - 1) / 100));
6 let tied = $derived(ranking.cohort - above - below - 1);
7 let viewers = $derived([
8 ...Array.from({ length: above }, () => 'above'),
9 'you',
10 ...Array.from({ length: tied }, () => 'tied'),
11 ...Array.from({ length: below }, () => 'below')
12 ]);
13 const labels = { above: 'Viewer with more watch time', tied: 'Viewer with the same watch time', below: 'Viewer with less watch time' };
14</script>
15
16<figure class="ranking-position">
17 <figcaption>Your position among {ranking.cohort} {ranking.cohort === 1 ? 'viewer' : 'viewers'}</figcaption>
18 {#if ranking.cohort > 1}<div class="ranking-direction" aria-hidden="true"><span>Most watch time</span><span>Least watch time →</span></div>{/if}
19 <ol aria-label="Viewers ordered from most to least watch time">
20 {#each viewers as kind, i (i)}
21 <li class={kind} aria-label={kind === 'you' ? `You: rank ${ranking.rank} of ${ranking.cohort}` : labels[kind]} aria-current={kind === 'you' ? 'true' : undefined}>
22 <span class="viewer-icon">
23 <svg viewBox="0 0 32 40" aria-hidden="true"><circle cx="16" cy="10" r="7" /><path d="M3 36v-6a13 13 0 0 1 26 0v6Z" /></svg>
24 </span>
25 {#if kind === 'you'}<strong>You</strong>{/if}
26 </li>
27 {/each}
28 </ol>
29 {#if ranking.cohort === 1}
30 <p>You are the only eligible viewer.</p>
31 {:else}
32 <p>{above} above you · {tied} tied with you · {below} below you</p>
33 {/if}
34</figure>
35
36<style>
37 .ranking-position { margin: 28px 0; max-width: 850px; }
38 figcaption { font-weight: 700; font-size: 1rem; margin-bottom: 16px; }
39 .ranking-direction { display: flex; justify-content: space-between; gap: 12px; font-size: 11px; margin-bottom: 10px; }
40 ol { display: grid; grid-template-columns: repeat(auto-fit, minmax(42px, 1fr)); gap: 12px 8px; list-style: none; margin: 0; padding: 0; }
41 li { display: flex; flex-direction: column; align-items: center; gap: 5px; min-height: 82px; font-size: 12px; }
42 .viewer-icon { display: grid; place-items: center; width: 42px; height: 56px; border: 2px solid transparent; border-radius: 12px; }
43 svg { width: 28px; height: 36px; fill: currentColor; }
44 .above .viewer-icon { opacity: .65; }
45 .below .viewer-icon { opacity: .3; }
46 .tied .viewer-icon { border-color: currentColor; border-style: dashed; }
47 .you .viewer-icon { background: #202436; color: white; box-shadow: 0 0 0 3px #fff9; }
48 strong { font-weight: 800; }
49 p { font-size: 12px; margin: 12px 0 0; }
50</style>