Initial public scoreboard
This commit is contained in:
parent
46974efdc1
commit
366fe93405
@ -1,16 +1,16 @@
|
|||||||
version: '3'
|
version: '3'
|
||||||
services:
|
services:
|
||||||
db:
|
db:
|
||||||
image: "postgres:latest"
|
image: 'postgres:latest'
|
||||||
container_name: 'bwcontest-postgres'
|
container_name: 'bwcontest-postgres'
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: 'bwcontest'
|
POSTGRES_DB: 'bwcontest'
|
||||||
POSTGRES_USER: 'bwcontest'
|
POSTGRES_USER: 'bwcontest'
|
||||||
POSTGRES_PASSWORD: 'password'
|
POSTGRES_PASSWORD: 'pass123'
|
||||||
TZ: America/New_York
|
TZ: America/New_York
|
||||||
ports:
|
ports:
|
||||||
- '5432:5432'
|
- '5433:5432'
|
||||||
volumes:
|
volumes:
|
||||||
- bwcontest-postgres-data:/var/lib/postgresql/data
|
- bwcontest-postgres-data:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
7
web/src/routes/public/scoreboard/+layout.server.ts
Normal file
7
web/src/routes/public/scoreboard/+layout.server.ts
Normal 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;
|
44
web/src/routes/public/scoreboard/+layout.svelte
Normal file
44
web/src/routes/public/scoreboard/+layout.svelte
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { theme } from '../../stores';
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
import { contestId } from './stores';
|
||||||
|
export let data: PageData;
|
||||||
|
|
||||||
|
function onContestSelect() {
|
||||||
|
if ($contestId != null) {
|
||||||
|
goto(`/public/scoreboard/${$contestId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="row mt-2">
|
||||||
|
<div class="col d-flex flex-row-reverse gap-3">
|
||||||
|
<button
|
||||||
|
on:click={() => {
|
||||||
|
$theme = $theme === 'light' ? 'dark' : 'light';
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
aria-label="theme"
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
><i class={`bi bi-${$theme == 'light' ? 'sun' : 'moon'}`} /></button
|
||||||
|
>
|
||||||
|
<div class="d-flex flex-row gap-2">
|
||||||
|
<label class="form-label mt-auto mb-auto" for="scoreboardSelect">Scoreboard</label>
|
||||||
|
<select
|
||||||
|
bind:value={$contestId}
|
||||||
|
on:change={onContestSelect}
|
||||||
|
id="scoreboardSelect"
|
||||||
|
class="form-control form-select w-auto"
|
||||||
|
>
|
||||||
|
{#each data.contests as contest}
|
||||||
|
{#if $contestId === null}
|
||||||
|
<option value={null}>None</option>
|
||||||
|
{/if}
|
||||||
|
<option selected={contest.id === $contestId} value={contest.id}>{contest.name}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<slot />
|
5
web/src/routes/public/scoreboard/+page.svelte
Normal file
5
web/src/routes/public/scoreboard/+page.svelte
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { contestId } from './stores';
|
||||||
|
|
||||||
|
$contestId = null;
|
||||||
|
</script>
|
113
web/src/routes/public/scoreboard/[contestId]/+page.server.ts
Normal file
113
web/src/routes/public/scoreboard/[contestId]/+page.server.ts
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import { db } from '$lib/server/prisma';
|
||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
export const load = (async ({ params }) => {
|
||||||
|
const contestId = parseInt(params.contestId);
|
||||||
|
if (isNaN(contestId)) {
|
||||||
|
throw redirect(302, '/public/scoreboard');
|
||||||
|
}
|
||||||
|
const timestamp = new Date();
|
||||||
|
const contest = await db.contest.findUnique({
|
||||||
|
where: { id: contestId },
|
||||||
|
include: { problems: true, teams: { include: { submissions: true } } }
|
||||||
|
});
|
||||||
|
if (contest === null) {
|
||||||
|
throw redirect(302, '/public/scoreboard');
|
||||||
|
}
|
||||||
|
const data = {
|
||||||
|
timestamp: timestamp,
|
||||||
|
selected: {
|
||||||
|
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;
|
@ -0,0 +1,8 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
import { contestId } from '../stores';
|
||||||
|
|
||||||
|
$contestId = parseInt($page.params.contestId);
|
||||||
|
let data: PageData;
|
||||||
|
</script>
|
3
web/src/routes/public/scoreboard/stores.ts
Normal file
3
web/src/routes/public/scoreboard/stores.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
|
export const contestId = writable<number | null>(null);
|
Loading…
Reference in New Issue
Block a user