Initial public scoreboard

This commit is contained in:
orosmatthew 2024-02-26 13:52:55 -05:00
parent 46974efdc1
commit 366fe93405
9 changed files with 186 additions and 6 deletions

View File

@ -1,16 +1,16 @@
version: '3'
services:
db:
image: "postgres:latest"
image: 'postgres:latest'
container_name: 'bwcontest-postgres'
restart: unless-stopped
environment:
POSTGRES_DB: 'bwcontest'
POSTGRES_USER: 'bwcontest'
POSTGRES_PASSWORD: 'password'
POSTGRES_PASSWORD: 'pass123'
TZ: America/New_York
ports:
- '5432:5432'
- '5433:5432'
volumes:
- bwcontest-postgres-data:/var/lib/postgresql/data

View File

@ -123,7 +123,7 @@ export const actions = {
}
}
})
)
)
: []
}
}

View File

@ -64,7 +64,7 @@ export const load = (async () => {
return (
submission.problemId === problem.id && submission.state === 'Correct'
);
})
})
? 'correct'
: 'incorrect'
: null,

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

View File

@ -0,0 +1,5 @@
<script lang="ts">
import { contestId } from './stores';
$contestId = null;
</script>

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

View File

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

View File

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