diff --git a/package.json b/package.json index 86bd3a786..223b34320 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "@tailwindcss/forms": "0.5.10", "@testing-library/jest-dom": "6.6.3", "@types/gtag.js": "0.0.20", + "@types/jsdom": "21.1.7", "@typescript-eslint/eslint-plugin": "8.36.0", "@typescript-eslint/parser": "8.36.0", "@vitest/coverage-v8": "3.2.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 318af2a74..2d5049a4c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -108,6 +108,9 @@ importers: '@types/gtag.js': specifier: 0.0.20 version: 0.0.20 + '@types/jsdom': + specifier: 21.1.7 + version: 21.1.7 '@typescript-eslint/eslint-plugin': specifier: 8.36.0 version: 8.36.0(@typescript-eslint/parser@8.36.0(eslint@9.30.1(jiti@1.21.6))(typescript@5.8.3))(eslint@9.30.1(jiti@1.21.6))(typescript@5.8.3) @@ -1532,6 +1535,9 @@ packages: '@types/jest@30.0.0': resolution: {integrity: sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==} + '@types/jsdom@21.1.7': + resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1547,6 +1553,9 @@ packages: '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -6088,6 +6097,12 @@ snapshots: expect: 30.0.0 pretty-format: 30.0.0 + '@types/jsdom@21.1.7': + dependencies: + '@types/node': 24.0.11 + '@types/tough-cookie': 4.0.5 + parse5: 7.2.1 + '@types/json-schema@7.0.15': {} '@types/node@16.18.11': {} @@ -6100,6 +6115,8 @@ snapshots: '@types/stack-utils@2.0.3': {} + '@types/tough-cookie@4.0.5': {} + '@types/trusted-types@2.0.7': optional: true diff --git a/prisma/tasks.ts b/prisma/tasks.ts index a72761eec..4a9499b21 100755 --- a/prisma/tasks.ts +++ b/prisma/tasks.ts @@ -4548,6 +4548,166 @@ export const tasks = [ title: 'A. Frog 1', grade: 'Q5', }, + { + id: 'tdpc_fibonacci', + contest_id: 'tdpc', + problem_index: 'T', + name: 'フィボナッチ', + title: 'T. フィボナッチ', + grade: 'D4', + }, + { + id: 'tdpc_grid', + contest_id: 'tdpc', + problem_index: 'S', + name: 'マス目', + title: 'S. マス目', + grade: 'D3', + }, + { + id: 'tdpc_graph', + contest_id: 'tdpc', + problem_index: 'R', + name: 'グラフ', + title: 'R. グラフ', + grade: 'D3', + }, + { + id: 'tdpc_concatenation', + contest_id: 'tdpc', + problem_index: 'Q', + name: '連結', + title: 'Q. 連結', + grade: 'D3', + }, + { + id: 'tdpc_eel', + contest_id: 'tdpc', + problem_index: 'P', + name: 'うなぎ', + title: 'P. うなぎ', + grade: 'D3', + }, + { + id: 'tdpc_string', + contest_id: 'tdpc', + problem_index: 'O', + name: '文字列', + title: 'O. 文字列', + grade: 'D2', + }, + { + id: 'tdpc_tree', + contest_id: 'tdpc', + problem_index: 'N', + name: '木', + title: 'N. 木', + grade: 'D2', + }, + { + id: 'tdpc_house', + contest_id: 'tdpc', + problem_index: 'M', + name: '家', + title: 'M. 家', + grade: 'D1', + }, + { + id: 'tdpc_cat', + contest_id: 'tdpc', + problem_index: 'L', + name: '猫', + title: 'L. 猫', + grade: 'D1', + }, + { + id: 'tdpc_target', + contest_id: 'tdpc', + problem_index: 'K', + name: 'ターゲット', + title: 'K. ターゲット', + grade: 'Q1', + }, + { + id: 'tdpc_ball', + contest_id: 'tdpc', + problem_index: 'J', + name: 'ボール', + title: 'J. ボール', + grade: 'D1', + }, + { + id: 'tdpc_iwi', + contest_id: 'tdpc', + problem_index: 'I', + name: 'イウィ', + title: 'I. イウィ', + grade: 'Q1', + }, + { + id: 'tdpc_knapsack', + contest_id: 'tdpc', + problem_index: 'H', + name: 'ナップザック', + title: 'H. ナップザック', + grade: 'Q1', + }, + { + id: 'tdpc_lexicographical', + contest_id: 'tdpc', + problem_index: 'G', + name: '辞書順', + title: 'G. 辞書順', + grade: 'D2', + }, + { + id: 'tdpc_semiexp', + contest_id: 'tdpc', + problem_index: 'F', + name: '準急', + title: 'F. 準急', + grade: 'Q1', + }, + { + id: 'tdpc_number', + contest_id: 'tdpc', + problem_index: 'E', + name: '数', + title: 'E. 数', + grade: 'Q1', + }, + { + id: 'tdpc_dice', + contest_id: 'tdpc', + problem_index: 'D', + name: 'サイコロ', + title: 'D. サイコロ', + grade: 'Q1', + }, + { + id: 'tdpc_tournament', + contest_id: 'tdpc', + problem_index: 'C', + name: 'トーナメント', + title: 'C. トーナメント', + grade: 'Q1', + }, + { + id: 'tdpc_game', + contest_id: 'tdpc', + problem_index: 'B', + name: 'ゲーム', + title: 'B. ゲーム', + grade: 'Q1', + }, + { + id: 'tdpc_contest', + contest_id: 'tdpc', + problem_index: 'A', + name: 'コンテスト', + title: 'A. コンテスト', + grade: 'Q2', + }, { id: 'math_and_algorithm_af', contest_id: 'math-and-algorithm', diff --git a/src/lib/components/TaskTables/TaskTable.svelte b/src/lib/components/TaskTables/TaskTable.svelte index 52be9b37f..8428f9b4c 100644 --- a/src/lib/components/TaskTables/TaskTable.svelte +++ b/src/lib/components/TaskTables/TaskTable.svelte @@ -12,14 +12,18 @@ } from 'svelte-5-ui-lib'; import type { TaskResults, TaskResult } from '$lib/types/task'; - import type { ContestTableProvider } from '$lib/types/contest_table_provider'; + import type { + ContestTableProvider, + ContestTableDisplayConfig, + } from '$lib/types/contest_table_provider'; import TaskTableBodyCell from '$lib/components/TaskTables/TaskTableBodyCell.svelte'; import { activeContestTypeStore } from '$lib/stores/active_contest_type.svelte'; import { - contestTableProviders, - type ContestTableProviders, + contestTableProviderGroups, + type ContestTableProviderGroup, + type ContestTableProviderGroups, } from '$lib/utils/contest_table_provider'; import { getBackgroundColorFrom } from '$lib/services/submission_status'; @@ -32,38 +36,81 @@ let { taskResults, isLoggedIn }: Props = $props(); // Prepare contest table provider based on the active contest type. - let activeContestType = $derived(activeContestTypeStore.get()); + let activeContestType: ContestTableProviderGroups = $derived(activeContestTypeStore.get()); // Note: This is necessary to ensure that the active contest type is updated correctly. - function updateActiveContestType(type: ContestTableProviders): void { + function updateActiveContestType(type: ContestTableProviderGroups): void { activeContestType = type; activeContestTypeStore.set(type); } - let provider: ContestTableProvider = $derived( - contestTableProviders[activeContestType as ContestTableProviders], + let providerGroups: ContestTableProviderGroup = $derived( + contestTableProviderGroups[activeContestType as ContestTableProviderGroups], ); - // Filter the task results based on the active contest type. - let filteredTaskResults = $derived(provider.filter(taskResults)); - // Generate the task table based on the filtered task results. - let taskTable: Record> = $derived( - provider.generateTable(filteredTaskResults), - ); - let taskTableHeaderIds: Array = $derived( - provider.getHeaderIdsForTask(filteredTaskResults), - ); - let contestIds: Array = $derived(provider.getContestRoundIds(filteredTaskResults)); - let title = $derived(provider.getMetadata().title); + let providers = $derived(providerGroups.getAllProviders()); + + interface ProviderData { + filteredTaskResults: TaskResults; + innerTaskTable: Record>; + headerIds: Array; + contestIds: Array; + metadata: any; + displayConfig: ContestTableDisplayConfig; + } + + let contestTableMaps = $derived(() => prepareContestTablesMap(providers)); + + function prepareContestTablesMap(providers: ContestTableProvider[]): Map { + const map = new Map(); + + for (const provider of providers) { + const abbreviationName = provider.getMetadata().abbreviationName; + const contestTable = prepareContestTable(provider); + + if (contestTable) { + map.set(abbreviationName, contestTable); + } + } + + return map; + } + + function prepareContestTable(provider: ContestTableProvider): ProviderData | null { + const filteredTaskResults = provider.filter(taskResults); + + if (filteredTaskResults.length === 0) { + return null; + } + + return { + filteredTaskResults: filteredTaskResults, + innerTaskTable: provider.generateTable(filteredTaskResults), + headerIds: provider.getHeaderIdsForTask(filteredTaskResults), + contestIds: provider.getContestRoundIds(filteredTaskResults), + metadata: provider.getMetadata(), + displayConfig: provider.getDisplayConfig(), + }; + } + + function getTaskTable(abbreviationName: string) { + return contestTableMaps().get(abbreviationName); + } function getContestRoundLabel(provider: ContestTableProvider, contestId: string): string { return provider.getContestRoundLabel(contestId); } - function getBodyCellClasses(contestId: string, taskIndex: string): string { + // More than 8 columns will wrap to the next line to align with ABC212 〜 ABC318 (8 tasks per contest). + function getBodyRowClasses(totalColumns: number): string { + return totalColumns > 8 ? 'flex flex-wrap' : 'flex flex-wrap xl:table-row'; + } + + function getBodyCellClasses(taskResult: TaskResult, totalColumns: number): string { const baseClasses = 'w-1/2 xs:w-1/3 sm:w-1/4 md:w-1/5 lg:w-1/6 px-1 py-1'; - const backgroundColor = getBackgroundColor(taskTable[contestId][taskIndex]); + const additionalClasses = totalColumns > 8 ? '2xl:w-1/7 py-2' : ''; + const backgroundColor = getBackgroundColor(taskResult); - return `${baseClasses} ${backgroundColor}`; + return `${baseClasses} ${additionalClasses} ${backgroundColor}`; } function getBackgroundColor(taskResult: TaskResult): string { @@ -117,10 +164,10 @@ - {#each Object.entries(contestTableProviders) as [type, config]} + {#each Object.entries(contestTableProviderGroups) as [type, config]}