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