-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #34 from Shtian/feat/stats-page
Add a stats page
- Loading branch information
Showing
7 changed files
with
186 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"authenticlash": minor | ||
--- | ||
|
||
Add stats page |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import { fail, redirect } from '@sveltejs/kit'; | ||
import type { PageServerLoad } from './$types'; | ||
|
||
export const load: PageServerLoad = async ({ locals: { getSession, supabase } }) => { | ||
const session = await getSession(); | ||
if (!session) { | ||
redirect(303, '/auth/login'); | ||
} | ||
|
||
const userId = session.user.id; | ||
if (!userId) return fail(401, { message: 'User not found' }); | ||
|
||
const { data: games, error } = await supabase | ||
.from('games') | ||
.select( | ||
'id, creator, code, end_at, name, participation ( profile_id, score, total_score, nickname, nickname_image_url )' | ||
); | ||
|
||
if (error) { | ||
return fail(500, { message: error }); | ||
} | ||
|
||
const participatedGames = games | ||
.filter((game) => game.participation.some((p) => p.profile_id === userId)) | ||
.sort((a, b) => new Date(b.end_at).getTime() - new Date(a.end_at).getTime()); | ||
|
||
const allParticipations = participatedGames.flatMap((game) => | ||
game.participation.filter((p) => p.profile_id === userId) | ||
); | ||
|
||
const allScores = allParticipations.flatMap((p) => p.score); | ||
|
||
const totalScoreAcrossGames = allScores.reduce((acc, score) => acc + score, 0); | ||
|
||
const average2FAScore = totalScoreAcrossGames / allScores.length; | ||
|
||
const averageTotalScore = | ||
allParticipations.reduce((acc, p) => acc + p.total_score, 0) / allParticipations.length; | ||
|
||
const median2FAscore = allScores.sort((a, b) => a - b)[Math.floor(allScores.length / 2)]; | ||
|
||
const wins = participatedGames | ||
.map((game) => { | ||
const highscoreList = game.participation.sort((a, b) => b.total_score - a.total_score); | ||
return highscoreList[0].profile_id === userId; | ||
}) | ||
.filter(Boolean).length; | ||
|
||
return { | ||
stats: { | ||
numberOfGames: participatedGames.length, | ||
allScores, | ||
totalScoreAcrossGames, | ||
average2FAScore, | ||
averageTotalScore, | ||
median2FAscore, | ||
wins | ||
}, | ||
title: 'Stats' | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
<script lang="ts"> | ||
import { RocketIcon } from 'lucide-svelte'; | ||
import StatsCard from './StatsCard.svelte'; | ||
import StatsNumber from './StatsNumber.svelte'; | ||
import ScoreGraph from '$lib/components/ScoreGraph.svelte'; | ||
export let data; | ||
const { | ||
numberOfGames, | ||
allScores, | ||
totalScoreAcrossGames, | ||
average2FAScore, | ||
averageTotalScore, | ||
median2FAscore, | ||
wins | ||
} = data.stats; | ||
</script> | ||
|
||
<div class="mx-auto max-w-[1200px] px-6 lg:px-8 lg:py-10"> | ||
{#if numberOfGames} | ||
<div class="grid grid-cols-12 gap-4"> | ||
<StatsCard title="Number of Games"> | ||
<p class="text-center text-4xl font-bold"> | ||
<StatsNumber value={numberOfGames} /> | ||
</p> | ||
</StatsCard> | ||
<StatsCard title="Number of Wins"> | ||
<p class="text-center text-4xl font-bold"> | ||
<StatsNumber value={wins} /> | ||
</p> | ||
</StatsCard> | ||
<StatsCard title="Average Total Score"> | ||
<p class="text-center text-4xl font-bold"> | ||
<StatsNumber value={averageTotalScore} decimals={2} /> | ||
</p> | ||
</StatsCard> | ||
<StatsCard title="Total accumulated score"> | ||
<p class="text-center text-4xl font-bold"> | ||
<StatsNumber value={totalScoreAcrossGames} /> | ||
</p> | ||
</StatsCard> | ||
<StatsCard title="2FA value history" cols="full"> | ||
<ScoreGraph scores={allScores} height={300} /> | ||
</StatsCard> | ||
<StatsCard title="Median 2FA Value"> | ||
<p class="text-center text-4xl font-bold"> | ||
<StatsNumber value={median2FAscore} /> | ||
</p> | ||
</StatsCard> | ||
<StatsCard title="Average 2FA Value"> | ||
<p class="text-center text-4xl font-bold"> | ||
<StatsNumber value={average2FAScore} decimals={2} /> | ||
</p> | ||
</StatsCard> | ||
</div> | ||
{:else} | ||
<div class="mx-auto max-w-7xl py-6 text-center sm:px-6 lg:px-8 lg:py-10"> | ||
<RocketIcon class="mx-auto h-12 w-12 text-gray-400" /> | ||
<h3 class="mt-2 text-sm font-semibold text-white">No past games found</h3> | ||
<p class="mt-1 text-sm text-white">Get started by creating a new AuthentiClash session.</p> | ||
<div class="mt-6"> | ||
<a | ||
href="/games/create" | ||
type="button" | ||
class="inline-flex items-center rounded-md bg-clash-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-clash-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-clash-600" | ||
> | ||
<svg | ||
class="-ml-0.5 mr-1.5 h-5 w-5" | ||
viewBox="0 0 20 20" | ||
fill="currentColor" | ||
aria-hidden="true" | ||
> | ||
<path | ||
d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z" | ||
/> | ||
</svg> | ||
New AuthentiClash | ||
</a> | ||
</div> | ||
</div> | ||
{/if} | ||
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
<script lang="ts"> | ||
export let title: string; | ||
export let cols: 'single' | 'half' | 'full' = 'half'; | ||
let colClass = 'sm:col-span-6'; | ||
switch (cols) { | ||
case 'half': | ||
colClass = 'sm:col-span-6'; | ||
break; | ||
case 'full': | ||
colClass = 'sm:col-span-12'; | ||
break; | ||
case 'single': | ||
colClass = 'sm:col-span-1'; | ||
break; | ||
} | ||
</script> | ||
|
||
<div | ||
class={`col-span-12 ${colClass} flex flex-col gap-4 rounded-lg border-[1px] border-gray-700 p-6 shadow-sm`} | ||
> | ||
<h2 class="text-xl text-gray-300">{title}</h2> | ||
<slot /> | ||
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
<script lang="ts"> | ||
import { tweened } from 'svelte/motion'; | ||
export let value: number; | ||
export let decimals = 0; | ||
export let duration = 300; | ||
const progress = tweened(0, { duration }); | ||
progress.set(100); | ||
$: currentNum = ($progress * value) / 100; | ||
</script> | ||
|
||
<span class="tabular-nums">{currentNum.toFixed(decimals)}</span> |