[Web] Add dropdown for admin scoreboards

This commit is contained in:
orosmatthew 2024-03-15 10:08:10 -04:00
parent 23c6711ad6
commit 27dc266353
7 changed files with 271 additions and 215 deletions

View File

@ -0,0 +1,7 @@
import { db } from '$lib/server/prisma';
import type { LayoutServerLoad } from './$types';
export const load = (async () => {
const contests = await db.contest.findMany({ select: { id: true, name: true } });
return { contests };
}) satisfies LayoutServerLoad;

View File

@ -0,0 +1,41 @@
<script lang="ts">
import { goto } from '$app/navigation';
import type { LayoutData } from './$types';
import { selectedScoreboard } from './stores';
export let data: LayoutData;
let scoreboardSelectValue: number | null = null;
function onScoreboardSelect() {
console.log(scoreboardSelectValue);
if (scoreboardSelectValue !== null) {
goto(`/admin/scoreboard/${scoreboardSelectValue}`);
}
}
</script>
<svelte:head>
<title>Admin Scoreboards</title>
</svelte:head>
<h1 style="text-align:center" class="mb-1"><i class="bi bi-trophy"></i> Admin Scoreboards</h1>
<div class="d-flex flex-row-reverse gap-3 pb-2">
<select
class="form-select w-auto"
bind:value={scoreboardSelectValue}
on:change={onScoreboardSelect}
>
{#if $selectedScoreboard === null}
<option value={null} selected>Select</option>
{/if}
{#each data.contests as contest}
<option value={contest.id} selected={$selectedScoreboard === contest.id}
>{contest.name}</option
>
{/each}
</select>
</div>
<slot />

View File

@ -1,108 +0,0 @@
import { db } from '$lib/server/prisma';
import type { PageServerLoad } from './$types';
export const load = (async () => {
const timestamp = new Date();
const contests = await db.contest.findMany({
include: { problems: true, teams: { include: { submissions: true } } }
});
const data = {
timestamp: timestamp,
contests: contests.map((contest) => {
return {
name: contest.name,
problems: contest.problems.map((problem) => {
return { id: problem.id, friendlyName: problem.friendlyName };
}),
teams: contest.teams
.map((team) => {
return {
name: team.name,
solves: team.submissions.filter((submission) => {
return submission.contestId === contest.id && submission.state === 'Correct';
}).length,
time: (() => {
const correctSubmissions = team.submissions.filter((submission) => {
return submission.contestId === contest.id && submission.state === 'Correct';
});
const penaltyTime =
team.submissions.filter((submission) => {
return (
submission.contestId === contest.id &&
submission.state === 'Incorrect' &&
correctSubmissions.find((correct) => {
return correct.problemId === submission.problemId;
})
);
}).length * 10;
let time = penaltyTime;
correctSubmissions.forEach((correctSubmission) => {
const gradedAt = correctSubmission.gradedAt!.valueOf();
const min = (gradedAt - contest.startTime!.valueOf()) / 60000;
time += min;
});
return time;
})(),
problems: contest.problems.map((problem) => {
return {
id: problem.id,
attempts: team.submissions.filter((submission) => {
return (
submission.contestId === contest.id &&
submission.problemId === problem.id &&
(submission.state === 'Correct' || submission.state === 'Incorrect')
);
}).length,
graphic: team.submissions.find((submission) => {
return (
submission.contestId === contest.id &&
submission.problemId === problem.id &&
(submission.state === 'Correct' || submission.state === 'Incorrect')
);
})
? team.submissions.find((submission) => {
return (
submission.problemId === problem.id && submission.state === 'Correct'
);
})
? 'correct'
: 'incorrect'
: null,
min: (() => {
const correctSubmission = team.submissions.find((submission) => {
return (
submission.contestId === contest.id &&
submission.problemId === problem.id &&
submission.state === 'Correct'
);
});
if (correctSubmission) {
const gradedAt = correctSubmission.gradedAt!.valueOf();
return (gradedAt - contest.startTime!.valueOf()) / 60000;
}
return undefined;
})()
};
})
};
})
.sort((a, b) => {
if (a.solves > b.solves) {
return -1;
} else if (a.solves < b.solves) {
return 1;
} else {
if (a.time < b.time) {
return -1;
} else if (a.time > b.time) {
return 1;
} else {
return 0;
}
}
})
};
})
};
return data;
}) satisfies PageServerLoad;

View File

@ -1,109 +1,4 @@
<script lang="ts"> <script lang="ts">
import { onDestroy, onMount } from 'svelte'; import { selectedScoreboard } from './stores';
import type { PageData } from './$types'; $selectedScoreboard = null;
import { invalidateAll } from '$app/navigation';
export let data: PageData;
let updateInterval: ReturnType<typeof setInterval>;
let updating = false;
onMount(() => {
updateInterval = setInterval(async () => {
updating = true;
await invalidateAll();
updating = false;
}, 10000);
});
onDestroy(() => {
clearInterval(updateInterval);
});
</script> </script>
<svelte:head>
<title>Admin Scoreboards</title>
</svelte:head>
<h1 style="text-align:center" class="mb-1"><i class="bi bi-trophy"></i> Admin Scoreboards</h1>
<div class="text-end">
{#if updating}
<div class="spinner-border spinner-border-sm text-secondary" />
{/if}
<strong>Last Updated: </strong>{data.timestamp.toLocaleTimeString()}
</div>
{#each data.contests as contest}
<h2 style="text-align:center">{contest.name}</h2>
<div class="mb-3 row">
<div class="text-end" />
</div>
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>Place</th>
<th>Team Name</th>
<th>Solves</th>
<th>Time</th>
{#each contest.problems as problem}
<th>{problem.friendlyName}</th>
{/each}
</tr>
</thead>
<tbody>
{#each contest.teams as team, i}
<tr>
<td style="text-align:center; font-size:24px;"><strong>{i + 1}</strong></td>
<td style="font-size:18px">{team.name}</td>
<td style="font-size:18px">{team.solves}</td>
<td style="font-size:18px">{team.time.toFixed(0)}</td>
{#each contest.problems as problem}
<td>
<div class="row">
<div class="col-3">
{#if team.problems.find((p) => {
return p.id === problem.id;
})?.graphic !== null}
<img
src={team.problems.find((p) => {
return p.id === problem.id;
})?.graphic === 'correct'
? '/correct.png'
: '/incorrect.png'}
alt="check or X"
width="30px"
/>
{/if}
</div>
<div class="col-9">
{#if team.problems.find((p) => {
return p.id === problem.id;
})?.attempts !== 0}
{team.problems.find((p) => {
return p.id === problem.id;
})?.attempts}
{team.problems.find((p) => {
return p.id === problem.id;
})?.attempts === 1
? 'Attempt'
: 'Attempts'}<br />{#if team.problems.find((p) => {
return p.id === problem.id;
})?.min}<span style="color:rgb(102,102,102)"
>{team.problems
.find((p) => {
return p.id === problem.id;
})
?.min?.toFixed(0)} min</span
>{/if}
{/if}
</div>
</div>
</td>
{/each}
</tr>
{/each}
</tbody>
</table>
{/each}

View File

@ -0,0 +1,109 @@
import { db } from '$lib/server/prisma';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ params }) => {
const timestamp = new Date();
const contest = await db.contest.findUnique({
where: { id: parseInt(params.contestId) },
include: { problems: true, teams: { include: { submissions: true } } }
});
if (contest === null) {
throw redirect(302, '/admin/scoreboard');
}
const data = {
timestamp: timestamp,
contest: {
name: contest.name,
problems: contest.problems.map((problem) => {
return { id: problem.id, friendlyName: problem.friendlyName };
}),
teams: contest.teams
.map((team) => {
return {
name: team.name,
solves: team.submissions.filter((submission) => {
return submission.contestId === contest.id && submission.state === 'Correct';
}).length,
time: (() => {
const correctSubmissions = team.submissions.filter((submission) => {
return submission.contestId === contest.id && submission.state === 'Correct';
});
const penaltyTime =
team.submissions.filter((submission) => {
return (
submission.contestId === contest.id &&
submission.state === 'Incorrect' &&
correctSubmissions.find((correct) => {
return correct.problemId === submission.problemId;
})
);
}).length * 10;
let time = penaltyTime;
correctSubmissions.forEach((correctSubmission) => {
const gradedAt = correctSubmission.gradedAt!.valueOf();
const min = (gradedAt - contest.startTime!.valueOf()) / 60000;
time += min;
});
return time;
})(),
problems: contest.problems.map((problem) => {
return {
id: problem.id,
attempts: team.submissions.filter((submission) => {
return (
submission.contestId === contest.id &&
submission.problemId === problem.id &&
(submission.state === 'Correct' || submission.state === 'Incorrect')
);
}).length,
graphic: team.submissions.find((submission) => {
return (
submission.contestId === contest.id &&
submission.problemId === problem.id &&
(submission.state === 'Correct' || submission.state === 'Incorrect')
);
})
? team.submissions.find((submission) => {
return submission.problemId === problem.id && submission.state === 'Correct';
})
? 'correct'
: 'incorrect'
: null,
min: (() => {
const correctSubmission = team.submissions.find((submission) => {
return (
submission.contestId === contest.id &&
submission.problemId === problem.id &&
submission.state === 'Correct'
);
});
if (correctSubmission) {
const gradedAt = correctSubmission.gradedAt!.valueOf();
return (gradedAt - contest.startTime!.valueOf()) / 60000;
}
return undefined;
})()
};
})
};
})
.sort((a, b) => {
if (a.solves > b.solves) {
return -1;
} else if (a.solves < b.solves) {
return 1;
} else {
if (a.time < b.time) {
return -1;
} else if (a.time > b.time) {
return 1;
} else {
return 0;
}
}
})
}
};
return data;
}) satisfies PageServerLoad;

View File

@ -0,0 +1,109 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import type { PageData } from './$types';
import { invalidateAll } from '$app/navigation';
import { selectedScoreboard } from '../stores';
import { page } from '$app/stores';
export let data: PageData;
let updateInterval: ReturnType<typeof setInterval>;
let updating = false;
$selectedScoreboard = parseInt($page.params.contestId);
onMount(() => {
updateInterval = setInterval(async () => {
updating = true;
await invalidateAll();
updating = false;
}, 10000);
});
onDestroy(() => {
clearInterval(updateInterval);
});
</script>
<svelte:head>
<title>Admin Scoreboards</title>
</svelte:head>
<div class="text-end">
{#if updating}
<div class="spinner-border spinner-border-sm text-secondary" />
{/if}
<strong>Last Updated: </strong>{data.timestamp.toLocaleTimeString()}
</div>
<h2 style="text-align:center">{data.contest.name}</h2>
<div class="mb-3 row">
<div class="text-end" />
</div>
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>Place</th>
<th>Team Name</th>
<th>Solves</th>
<th>Time</th>
{#each data.contest.problems as problem}
<th>{problem.friendlyName}</th>
{/each}
</tr>
</thead>
<tbody>
{#each data.contest.teams as team, i}
<tr>
<td style="text-align:center; font-size:24px;"><strong>{i + 1}</strong></td>
<td style="font-size:18px">{team.name}</td>
<td style="font-size:18px">{team.solves}</td>
<td style="font-size:18px">{team.time.toFixed(0)}</td>
{#each data.contest.problems as problem}
<td>
<div class="row">
<div class="col-3">
{#if team.problems.find((p) => {
return p.id === problem.id;
})?.graphic !== null}
<img
src={team.problems.find((p) => {
return p.id === problem.id;
})?.graphic === 'correct'
? '/correct.png'
: '/incorrect.png'}
alt="check or X"
width="30px"
/>
{/if}
</div>
<div class="col-9">
{#if team.problems.find((p) => {
return p.id === problem.id;
})?.attempts !== 0}
{team.problems.find((p) => {
return p.id === problem.id;
})?.attempts}
{team.problems.find((p) => {
return p.id === problem.id;
})?.attempts === 1
? 'Attempt'
: 'Attempts'}<br />{#if team.problems.find((p) => {
return p.id === problem.id;
})?.min}<span style="color:rgb(102,102,102)"
>{team.problems
.find((p) => {
return p.id === problem.id;
})
?.min?.toFixed(0)} min</span
>{/if}
{/if}
</div>
</div>
</td>
{/each}
</tr>
{/each}
</tbody>
</table>

View File

@ -0,0 +1,3 @@
import { writable } from 'svelte/store';
export const selectedScoreboard = writable<number | null>(null);