arnaucube
arnaucube/jellyfin-toolsPublic Self-hosted repository
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: #202436; color: white; box-shadow: 0 0 0 3px #fff9; }
49  strong { font-weight: 800; }
50  p { font-size: 12px; margin: 12px 0 0; }
51</style>