Skip to content

Commit

Permalink
Add leaderboard to team page (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
Rabrennie committed May 27, 2023
1 parent 4ca26c4 commit 3c7644b
Show file tree
Hide file tree
Showing 10 changed files with 431 additions and 50 deletions.
307 changes: 269 additions & 38 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Expand Up @@ -32,6 +32,7 @@
"@sveltejs/adapter-auto": "^1.0.0",
"@sveltejs/adapter-node": "^1.2.4",
"@sveltejs/kit": "^1.0.0",
"@types/d3-scale": "^4.0.3",
"@types/spotify-api": "^0.0.21",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
Expand All @@ -54,7 +55,7 @@
"tailwindcss": "^3.2.4",
"ts-node": "^10.9.1",
"tslib": "^2.4.1",
"typescript": "^4.9.3",
"typescript": "^5.0.4",
"vite": "^4.0.0",
"vitest": "^0.25.3"
},
Expand All @@ -64,6 +65,7 @@
"@rabrennie/sveltekit-auth": "^0.0.8",
"animejs": "^3.2.1",
"canvas-confetti": "^1.6.0",
"layercake": "^7.4.0",
"zod": "^3.21.4",
"zodkit": "^0.2.0"
}
Expand Down
1 change: 1 addition & 0 deletions prisma/schema.prisma
@@ -1,5 +1,6 @@
generator client {
provider = "prisma-client-js"
previewFeatures = ["filteredRelationCount"]
}

datasource db {
Expand Down
9 changes: 9 additions & 0 deletions src/lib/Utils/stringToHslColor.ts
@@ -0,0 +1,9 @@
export function stringToHslColor(str: string, s: number, l: number) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}

const h = hash % 360;
return `hsl(${h}, ${s}%, ${l}%)`;
}
42 changes: 42 additions & 0 deletions src/lib/components/Leaderboard/Column.svelte
@@ -0,0 +1,42 @@
<!--
@component
Generates an SVG column chart.
-->
<script lang="ts">
import { stringToHslColor } from '$lib/Utils/stringToHslColor';
import { getContext } from 'svelte';
// @ts-ignore
const { data, xGet, yGet, x, yRange, xScale, y } = getContext('LayerCake');
$: columnWidth = (d: any) => {
const vals = $xGet(d);
return Math.abs(vals[1] - vals[0]);
};
$: columnHeight = (d: any) => {
return $yRange[0] - $yGet(d);
};
</script>

<g class="column-group">
{#each $data as d, i}
{@const colHeight = columnHeight(d)}
{@const xGot = $xGet(d)}
{@const xPos = Array.isArray(xGot) ? xGot[0] : xGot}
{@const colWidth = $xScale.bandwidth ? $xScale.bandwidth() : columnWidth(d)}
{@const yValue = $y(d)}
<rect
class="group-rect"
data-id={i}
data-range={$x(d)}
data-count={yValue}
x={xPos}
y={$yGet(d)}
width={colWidth}
height={colHeight}
fill={stringToHslColor(d.name, 55, 60)}
stroke-width="0"
/>
{/each}
</g>
35 changes: 35 additions & 0 deletions src/lib/components/Leaderboard/Labels.svelte
@@ -0,0 +1,35 @@
<!--
@component
Generates an SVG column chart.
-->
<script lang="ts">
import { getContext } from 'svelte';
// @ts-ignore
const { data, xGet, xScale, containerHeight } = getContext('LayerCake');
$: columnWidth = (d: any) => {
const vals = $xGet(d);
return Math.abs(vals[1] - vals[0]);
};
</script>

<div>
{#each $data as d, i}
{@const xGot = $xGet(d)}
{@const xPos = Array.isArray(xGot) ? xGot[0] : xGot}
{@const colWidth = $xScale.bandwidth ? $xScale.bandwidth() : columnWidth(d)}
<div
class="absolute text-center"
style={`left: ${xPos}px; width: ${colWidth}px; top: ${$containerHeight - 32}px`}
>
<div class="tooltip tooltip-bottom" data-tip={`${d.name} - ${d.count}`}>
<div class="avatar">
<div class="w-8 rounded-full">
<img src={d.image} alt={d.name} />
</div>
</div>
</div>
</div>
{/each}
</div>
35 changes: 35 additions & 0 deletions src/lib/components/Leaderboard/Leaderboard.svelte
@@ -0,0 +1,35 @@
<script lang="ts">
import Column from '$lib/components/Leaderboard/Column.svelte';
import { LayerCake, Svg, Html } from 'layercake';
import { scaleBand } from 'd3-scale';
import Labels from '$lib/components/Leaderboard/Labels.svelte';
export let data: { name: string; image: string; count: number }[];
</script>

<div class="chart-container">
<LayerCake
padding={{ top: 0, right: 0, bottom: 45, left: 0 }}
{data}
x="name"
y="count"
xScale={scaleBand().paddingInner(0.2).round(true)}
xDomain={data.map((d) => d.name)}
yDomain={[0, data[0].count]}
>
<Svg>
<Column />
</Svg>

<Html>
<Labels />
</Html>
</LayerCake>
</div>

<style>
.chart-container {
width: 100%;
height: 150px;
}
</style>
11 changes: 1 addition & 10 deletions src/lib/components/SelectedAlbum/SelectedAlbum.svelte
@@ -1,4 +1,5 @@
<script lang="ts">
import { stringToHslColor } from '$lib/Utils/stringToHslColor';
import Confetti from '$lib/components/Confetti/Confetti.svelte';
import { tick } from 'svelte';
Expand All @@ -13,16 +14,6 @@
const emojis = ['', '👎', '🙅‍♀️', '⛔️', '🙅', '🙅‍♂️', '😭', '🚫', '💀'];
const eliminatedEmoji = emojis[Math.floor(Math.random() * emojis.length)];
function stringToHslColor(str: string, s: number, l: number) {
var hash = 0;
for (var i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
var h = hash % 360;
return `hsl(${h}, ${s}%, ${l}%)`;
}
async function copy() {
await navigator.clipboard.writeText(albumName);
}
Expand Down
19 changes: 18 additions & 1 deletion src/routes/team/[id]/+page.server.ts
Expand Up @@ -19,7 +19,24 @@ const getTeamOrError = async (event: RequestEvent) => {
const team = await db.team.findFirst({
where: { id: result.data.id, users: { some: { id: event.locals.user.id } } },
include: {
users: { select: { name: true, image: true, id: true } },
users: {
select: {
name: true,
image: true,
id: true,
_count: {
select: {
choices: {
where: {
eliminated: false,
room: { step: 'finished', teamId: result.data.id }
}
}
}
}
},
orderBy: { createdAt: 'desc' }
},
rooms: { include: { choices: true }, orderBy: { createdAt: 'desc' } }
}
});
Expand Down
18 changes: 18 additions & 0 deletions src/routes/team/[id]/+page.svelte
@@ -1,6 +1,7 @@
<script lang="ts">
import { page } from '$app/stores';
import Button from '$lib/components/Button/Button.svelte';
import Leaderboard from '$lib/components/Leaderboard/Leaderboard.svelte';
import SelectedAlbum from '$lib/components/SelectedAlbum/SelectedAlbum.svelte';
import { RoomState } from '../../../types/Room';
import type { PageData } from './$types';
Expand All @@ -11,6 +12,12 @@
let inviteModal: InviteModal;
export let data: PageData;
let leaderboardData: { name: string; image: string; count: number }[];
$: leaderboardData = data.team.users
.map((u) => ({ image: u.image, name: u.name, count: u._count.choices }))
.sort((a, b) => b.count - a.count);
</script>

<svelte:head>
Expand Down Expand Up @@ -44,6 +51,17 @@
</div>
</div>

<div>
<h2 class="text-2xl text-slate-100">Leaderboard</h2>
<div class="pt-8">
{#if !leaderboardData || leaderboardData[0].count === 0}
<div>There's no data 😢</div>
{:else}
<Leaderboard data={leaderboardData} />
{/if}
</div>
</div>

<div class="flex justify-between items-center">
<div class="flex gap-4 items-center">
<h2 class="text-2xl text-slate-100">Rooms</h2>
Expand Down

0 comments on commit 3c7644b

Please sign in to comment.