aboutsummaryrefslogtreecommitdiffhomepage
path: root/assets
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2025-12-05 04:08:22 +0900
committernsfisis <nsfisis@gmail.com>2025-12-05 04:08:22 +0900
commitfbd4f2129ce8fe106391302896dd86e05b2f331b (patch)
tree3be97d86c89a4a8a497e1b8a2d7d032eb153438e /assets
parentca3f1c4b0bd08ab6e836923601cf699d1fd894fb (diff)
downloadphperkaigi-2024-albatross-archive-fbd4f2129ce8fe106391302896dd86e05b2f331b.tar.gz
phperkaigi-2024-albatross-archive-fbd4f2129ce8fe106391302896dd86e05b2f331b.tar.zst
phperkaigi-2024-albatross-archive-fbd4f2129ce8fe106391302896dd86e05b2f331b.zip
add files
Diffstat (limited to 'assets')
-rw-r--r--assets/chart.js146
-rw-r--r--assets/favicon.svg21
-rw-r--r--assets/index.js13
-rw-r--r--assets/loading.js60
4 files changed, 240 insertions, 0 deletions
diff --git a/assets/chart.js b/assets/chart.js
new file mode 100644
index 0000000..9ff08e4
--- /dev/null
+++ b/assets/chart.js
@@ -0,0 +1,146 @@
+import {
+ Chart,
+ Colors,
+ LineController,
+ LineElement,
+ LinearScale,
+ PointElement,
+ TimeScale,
+ Tooltip,
+} from 'chart.js'
+import 'chartjs-adapter-date-fns';
+
+Chart.register(
+ Colors,
+ LineController,
+ LineElement,
+ LinearScale,
+ PointElement,
+ TimeScale,
+ Tooltip,
+);
+
+document.addEventListener('DOMContentLoaded', async () => {
+ const chartCanvas = document.getElementById('chart');
+ const quizId = chartCanvas.dataset.quizId;
+
+ const apiUrl = `${process.env.ALBATROSS_BASE_PATH}/api/quizzes/${quizId}/chart`;
+ const apiResult = await fetch(apiUrl).then(res => res.json());
+ if (apiResult.error) {
+ return;
+ }
+ const stats = apiResult.stats;
+
+ // Filter best scores.
+ for (const s of stats) {
+ const bestScores = [];
+ for (const score of s.scores) {
+ if (bestScores.length === 0 || bestScores[bestScores.length - 1].code_size > score.code_size) {
+ bestScores.push(score);
+ }
+ }
+ s.scores = bestScores;
+ }
+
+ const scoresInChronologicalOrder = stats
+ .flatMap(s => s.scores.map(score => ({ ...score, user: s.user })))
+ .toSorted((a, b) => a.submitted_at - b.submitted_at);
+
+ const scoresAndRanksAtEachTime = (() => {
+ const result = [];
+ const currentScoresForUser = new Map();
+ for (const { user, submitted_at, code_size } of scoresInChronologicalOrder) {
+ currentScoresForUser.set(user.name, { user, submitted_at, code_size });
+ const ranking = currentScoresForUser
+ .values()
+ .toArray()
+ .toSorted(
+ (a, b) => a.code_size === b.code_size ? a.submitted_at - b.submitted_at : a.code_size - b.code_size,
+ );
+ const scores = new Map();
+ for (const [i, { user, code_size }] of ranking.entries()) {
+ scores.set(user.name, {
+ user,
+ code_size,
+ rank: i + 1,
+ });
+ }
+ result.push({ submitted_at: submitted_at, scores });
+ }
+ return result;
+ })();
+
+ const rankingHistory = (() => {
+ const result = new Map();
+ for (const { submitted_at, scores } of scoresAndRanksAtEachTime) {
+ for (const [username, { user, code_size, rank }] of scores.entries()) {
+ if (!result.has(username)) {
+ result.set(username, []);
+ }
+ const scores = result.get(username);
+ scores.push({ user, code_size, rank, submitted_at });
+ }
+ }
+ return result
+ .values()
+ .toArray()
+ .toSorted((a, b) => {
+ const finalRankA = a[a.length - 1].rank;
+ const finalRankB = b[b.length - 1].rank;
+ return finalRankA - finalRankB;
+ });
+ })();
+
+ new Chart(
+ chartCanvas,
+ {
+ type: 'line',
+ data: {
+ datasets: rankingHistory.map(s => ({
+ label: `${s[0].user.name}${s[0].user.is_admin ? ' (staff)' : ''}`,
+ data: s.map(row => ({ x: row.submitted_at * 1000, y: row.rank, code_size: row.code_size })),
+ }))
+ },
+ options: {
+ scales: {
+ x: {
+ type: 'time',
+ time: {
+ parsing: false,
+ display: false,
+ unit: 'day',
+ tooltipFormat: 'yyyy-MM-dd HH:mm:ss',
+ displayFormats: {
+ day: 'yyyy-MM-dd',
+ },
+ },
+ title: {
+ display: true,
+ text: '日時',
+ },
+ },
+ y: {
+ title: {
+ display: true,
+ text: '順位',
+ },
+ reverse: true,
+ min: 1,
+ offset: true,
+ },
+ },
+ plugins: {
+ tooltip: {
+ callbacks: {
+ label: (context) => {
+ const label = context.dataset.label;
+ const code_size = context.raw.code_size;
+ return `${label} (${code_size} byte)`;
+ },
+ },
+ },
+ },
+ },
+ },
+ );
+});
diff --git a/assets/favicon.svg b/assets/favicon.svg
new file mode 100644
index 0000000..931fc7b
--- /dev/null
+++ b/assets/favicon.svg
@@ -0,0 +1,21 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="130" height="130" viewBox="0 0 130 130">
+ <defs>
+ <style>
+ .cls-1 {
+ fill: none;
+ }
+ .cls-2 {
+ fill: #e60082;
+ }
+ @media (prefers-color-scheme: dark) {
+ .cls-2 {
+ fill: #fff;
+ }
+ }
+ </style>
+ </defs>
+ <g transform="translate(627 -2695)">
+ <rect class="cls-1" width="130" height="130" rx="5" transform="translate(-627 2695)"/>
+ <path id="ttl_logo" class="cls-2" d="M109.44,46.755c-.044-.422-.184-1.635-.229-2.031a33.3,33.3,0,0,0-2.493-8.4C99.059,19.619,80.376,18.662,64.9,23.4A81.749,81.749,0,0,0,52.789,28.1a4.476,4.476,0,0,1-6.2-2.578C44.424,19.075,44.146,10.741,45.347,0,41.894,30.908,30.908,39.686,0,36.231c8.49.949,15.308,2.466,20.663,4.808a7.064,7.064,0,0,1,2.713,10.813q-1.432,1.849-2.762,3.781C7.53,74.662,3.576,99.531,9.39,121.736c.008-.4.02-1.02.047-1.771.194-5.34,1.6-39.572,25.766-63.228,1.965,6.352,2.186,14.475,1.029,24.842C39.686,50.67,50.669,41.894,81.578,45.347c-8.792-.983-15.791-2.576-21.228-5.062A70.955,70.955,0,0,1,72.492,36.52a37.193,37.193,0,0,1,8.647-.852,18.06,18.06,0,0,1,7.23,1.463c3.974,1.807,5.926,5.8,6.8,9.936.028.208.131.893.164,1.1.081.4.074,1.13.134,1.54.557,15.945-13.589,39.72-29.5,44.04A21.9,21.9,0,0,1,50.882,92.5a15.726,15.726,0,0,1-7.218-8.4,10.8,10.8,0,0,1-.124-7.89l-.612.876c-2.992,4.969-1.251,11.5,1.872,16.031,5.132,7.174,15.394,9.676,23.777,8.412,21.848-3.465,40.913-30.683,40.983-52.124-.029-.775-.039-1.894-.12-2.654" transform="translate(-617.883 2701.132)"/>
+ </g>
+</svg>
diff --git a/assets/index.js b/assets/index.js
new file mode 100644
index 0000000..1125516
--- /dev/null
+++ b/assets/index.js
@@ -0,0 +1,13 @@
+import hljs from 'highlight.js/lib/core';
+import php from 'highlight.js/lib/languages/php';
+import plaintext from 'highlight.js/lib/languages/plaintext';
+import 'highlight.js/styles/github.css';
+import 'bootstrap/dist/css/bootstrap.css';
+
+document.addEventListener('DOMContentLoaded', () => {
+ hljs.registerLanguage('php', php);
+ hljs.registerLanguage('plaintext', plaintext);
+ hljs.highlightAll();
+});
+
+console.log(`#Hooray!Albatross!`);
diff --git a/assets/loading.js b/assets/loading.js
new file mode 100644
index 0000000..49d8b48
--- /dev/null
+++ b/assets/loading.js
@@ -0,0 +1,60 @@
+document.addEventListener('DOMContentLoaded', () => {
+ const phperTokenElem = document.getElementsByClassName('js-phper-token')[0];
+
+ const aggregatedStatusElem = document.getElementsByClassName('js-aggregated-execution-status')[0];
+ const aggregatedStatusLoadingIndicatorElem = document.getElementsByClassName('js-aggregated-execution-status-loading-indicator')[0];
+ const answerId = aggregatedStatusElem.dataset.answerId;
+
+ const getElemsMap = cls => new Map(
+ Array.from(document.getElementsByClassName(cls) ?? [])
+ .map(e => [parseInt(e.dataset.testcaseExecutionId), e])
+ );
+ const statusElemsMap = getElemsMap('js-testcase-execution-status');
+ const statusLoadingIndicatorElemsMap = getElemsMap('js-testcase-execution-status-loading-indicator');
+ const stdoutElemsMap = getElemsMap('js-testcase-execution-stdout');
+ const stderrElemsMap = getElemsMap('js-testcase-execution-stderr');
+
+ if (!aggregatedStatusLoadingIndicatorElem) {
+ return;
+ }
+
+ const apiUrl = `${process.env.ALBATROSS_BASE_PATH}/api/answers/${answerId}/statuses`;
+
+ let timerId;
+ timerId = setInterval(() => {
+ fetch(apiUrl)
+ .then(response => response.json())
+ .then(({ aggregated_status, testcase_executions, phper_token }) => {
+ if (phper_token) {
+ phperTokenElem.innerHTML = `<div class="alert alert-success">バーディー! ${phper_token}</div>`;
+ }
+
+ for (const ex of testcase_executions) {
+ const statusElem = statusElemsMap.get(ex.id);
+ const loadingIndicatorElem = statusLoadingIndicatorElemsMap.get(ex.id);
+ const stdoutElem = stdoutElemsMap.get(ex.id);
+ const stderrElem = stderrElemsMap.get(ex.id);
+
+ const { status, stdout, stderr } = ex;
+ if (status.label === statusElem.textContent) {
+ continue;
+ }
+ statusElem.textContent = status.label;
+ stdoutElem.textContent = stdout;
+ stderrElem.textContent = stderr;
+ if (loadingIndicatorElem && !status.show_loading_indicator) {
+ loadingIndicatorElem.remove();
+ }
+ }
+
+ if (aggregated_status.label === aggregatedStatusElem.textContent) {
+ return;
+ }
+ aggregatedStatusElem.textContent = aggregated_status.label;
+ if (!aggregated_status.show_loading_indicator) {
+ aggregatedStatusLoadingIndicatorElem.remove();
+ clearInterval(timerId);
+ }
+ });
+ }, 5 * 1000);
+});