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  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>