|
| 1 | +<script lang="ts"> |
| 2 | + import { |
| 3 | + Heading, |
| 4 | + ButtonGroup, |
| 5 | + Button, |
| 6 | + Table, |
| 7 | + TableBody, |
| 8 | + TableBodyCell, |
| 9 | + TableBodyRow, |
| 10 | + TableHead, |
| 11 | + TableHeadCell, |
| 12 | + } from 'flowbite-svelte'; |
| 13 | +
|
| 14 | + import type { TaskResults, TaskResult } from '$lib/types/task'; |
| 15 | + import { ContestType } from '$lib/types/contest'; |
| 16 | +
|
| 17 | + import UpdatingModal from '$lib/components/SubmissionStatus/UpdatingModal.svelte'; |
| 18 | + import TaskTableBodyCell from '$lib/components/TaskTables/TaskTableBodyCell.svelte'; |
| 19 | +
|
| 20 | + import { classifyContest, getContestNameLabel } from '$lib/utils/contest'; |
| 21 | + import { getTaskTableHeaderName } from '$lib/utils/task'; |
| 22 | + import { getBackgroundColorFrom } from '$lib/services/submission_status'; |
| 23 | +
|
| 24 | + export let taskResults: TaskResults; |
| 25 | + export let isLoggedIn: boolean; |
| 26 | +
|
| 27 | + let selectedTaskResults: TaskResults; |
| 28 | + let contestIds: Array<string>; |
| 29 | + let taskTableIndices: Array<string>; |
| 30 | + let taskTable: Record<string, Record<string, TaskResult>>; |
| 31 | + let updatingModal: UpdatingModal; |
| 32 | +
|
| 33 | + // TODO: 任意のコンテスト種別に拡張 |
| 34 | + $: selectedTaskResults = filterTaskResultsByContestType(taskResults, fromABC212_Onwards); |
| 35 | + $: contestIds = getContestIds(selectedTaskResults); |
| 36 | + $: taskTableIndices = getTaskTableIndices(selectedTaskResults, ContestType.ABC); |
| 37 | + $: taskTable = prepareTaskTable(selectedTaskResults, ContestType.ABC); |
| 38 | +
|
| 39 | + function filterTaskResultsByContestType( |
| 40 | + taskResults: TaskResults, |
| 41 | + condition: (taskResult: TaskResult) => boolean, |
| 42 | + ): TaskResults { |
| 43 | + return taskResults.filter(condition); |
| 44 | + } |
| 45 | +
|
| 46 | + // Note: |
| 47 | + // Before and from ABC212 onwards, the number and tendency of tasks are very different. |
| 48 | + const fromABC212_Onwards = (taskResult: TaskResult) => |
| 49 | + classifyContest(taskResult.contest_id) === ContestType.ABC && taskResult.contest_id >= 'abc212'; |
| 50 | +
|
| 51 | + function getContestIds(selectedTaskResults: TaskResults): Array<string> { |
| 52 | + const contestList = selectedTaskResults.map((taskResult: TaskResult) => taskResult.contest_id); |
| 53 | + return Array.from(new Set(contestList)).sort().reverse(); |
| 54 | + } |
| 55 | +
|
| 56 | + function getTaskTableIndices( |
| 57 | + selectedTaskResults: TaskResults, |
| 58 | + selectedContestType: ContestType, |
| 59 | + ): Array<string> { |
| 60 | + const headerList = selectedTaskResults.map((taskResult: TaskResult) => |
| 61 | + getTaskTableHeaderName(selectedContestType, taskResult), |
| 62 | + ); |
| 63 | + return Array.from(new Set(headerList)).sort(); |
| 64 | + } |
| 65 | +
|
| 66 | + /** |
| 67 | + * Prepare a table for task and submission statuses. |
| 68 | + * |
| 69 | + * Computational complexity of preparation table: O(N), where N is the number of task results. |
| 70 | + * Computational complexity of accessing table: O(1). |
| 71 | + * |
| 72 | + * @param selectedTaskResults Task results to be shown in the table. |
| 73 | + * @param selectedContestType Contest type of the task results. |
| 74 | + * @returns A table for task and submission statuses. |
| 75 | + */ |
| 76 | + function prepareTaskTable( |
| 77 | + selectedTaskResults: TaskResults, |
| 78 | + selectedContestType: ContestType, |
| 79 | + ): Record<string, Record<string, TaskResult>> { |
| 80 | + const table: Record<string, Record<string, TaskResult>> = {}; |
| 81 | +
|
| 82 | + selectedTaskResults.forEach((taskResult: TaskResult) => { |
| 83 | + const contestId = taskResult.contest_id; |
| 84 | + const taskTableIndex = getTaskTableHeaderName(selectedContestType, taskResult); |
| 85 | +
|
| 86 | + if (!table[contestId]) { |
| 87 | + table[contestId] = {}; |
| 88 | + } |
| 89 | +
|
| 90 | + table[contestId][taskTableIndex] = taskResult; |
| 91 | + }); |
| 92 | +
|
| 93 | + return table; |
| 94 | + } |
| 95 | +
|
| 96 | + function getContestNameLabelForTaskTable(contestId: string): string { |
| 97 | + let contestNameLabel = getContestNameLabel(contestId); |
| 98 | + const contestType = classifyContest(contestId); |
| 99 | +
|
| 100 | + switch (contestType) { |
| 101 | + case ContestType.ABC: |
| 102 | + return contestNameLabel.replace('ABC ', ''); |
| 103 | + // TODO: Add cases for other contest types. |
| 104 | + default: |
| 105 | + return contestNameLabel; |
| 106 | + } |
| 107 | + } |
| 108 | +
|
| 109 | + function getBodyCellClasses(contestId: string, taskIndex: string): string { |
| 110 | + 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 border'; |
| 111 | + const backgroundColor = getBackgroundColor(taskTable[contestId][taskIndex]); |
| 112 | +
|
| 113 | + return `${baseClasses} ${backgroundColor}`; |
| 114 | + } |
| 115 | +
|
| 116 | + function getBackgroundColor(taskResult: TaskResult): string { |
| 117 | + const statusName = taskResult?.status_name; |
| 118 | +
|
| 119 | + if (taskResult && statusName !== 'ns') { |
| 120 | + return getBackgroundColorFrom(statusName); |
| 121 | + } |
| 122 | +
|
| 123 | + return ''; |
| 124 | + } |
| 125 | +</script> |
| 126 | + |
| 127 | +<!-- TODO: コンテスト種別のボタンの並び順を決める --> |
| 128 | +<!-- See: --> |
| 129 | +<!-- https://flowbite-svelte.com/docs/components/button-group --> |
| 130 | +<ButtonGroup class="m-4 contents-center" aria-label="Contest filter options"> |
| 131 | + <Button |
| 132 | + on:click={() => filterTaskResultsByContestType(taskResults, fromABC212_Onwards)} |
| 133 | + aria-label="Filter contests from ABC212 onwards" |
| 134 | + > |
| 135 | + ABC212〜 |
| 136 | + </Button> |
| 137 | +</ButtonGroup> |
| 138 | + |
| 139 | +<!-- TODO: コンテスト種別に応じて動的に変更できるようにする --> |
| 140 | +<Heading tag="h2" class="text-2xl pb-3 text-gray-900 dark:text-white"> |
| 141 | + {'AtCoder Beginners Contest 212 〜'} |
| 142 | +</Heading> |
| 143 | + |
| 144 | +<!-- TODO: ページネーションを実装 --> |
| 145 | +<!-- TODO: ページネーションライブラリを導入するには、Svelte v4 から v5 へのアップデートが必要 --> |
| 146 | +<!-- See: --> |
| 147 | +<!-- https://github.com/kenkoooo/AtCoderProblems/blob/master/atcoder-problems-frontend/src/pages/TablePage/AtCoderRegularTable.tsx --> |
| 148 | +<!-- https://github.com/birdou/atcoder-blogs/blob/main/app/atcoder-blogs-frontend/src/pages/BlogTablePage/BlogTablePage.tsx --> |
| 149 | +<div class="container w-full overflow-auto border rounded-md"> |
| 150 | + <Table shadow id="task-table" class="text-md table-fixed" aria-label="Task table"> |
| 151 | + <TableHead class="text-sm bg-gray-100"> |
| 152 | + <TableHeadCell class="w-full xl:w-16 px-2 text-center border" scope="col">Round</TableHeadCell |
| 153 | + > |
| 154 | + |
| 155 | + {#if taskTableIndices.length} |
| 156 | + {#each taskTableIndices as taskTableIndex} |
| 157 | + <TableHeadCell class="text-center border" scope="col">{taskTableIndex}</TableHeadCell> |
| 158 | + {/each} |
| 159 | + {/if} |
| 160 | + </TableHead> |
| 161 | + |
| 162 | + <TableBody tableBodyClass="divide-y"> |
| 163 | + {#if contestIds.length && taskTableIndices.length} |
| 164 | + {#each contestIds as contestId} |
| 165 | + <TableBodyRow class="flex flex-wrap xl:table-row"> |
| 166 | + <TableBodyCell class="w-full xl:w-16 truncate px-2 py-2 text-center border"> |
| 167 | + <!-- FIXME: コンテスト種別に合わせて修正できるようにする --> |
| 168 | + {getContestNameLabelForTaskTable(contestId)} |
| 169 | + </TableBodyCell> |
| 170 | + |
| 171 | + {#each taskTableIndices as taskIndex} |
| 172 | + <TableBodyCell |
| 173 | + key={contestId + '-' + taskIndex} |
| 174 | + class={getBodyCellClasses(contestId, taskIndex)} |
| 175 | + > |
| 176 | + {#if taskTable[contestId][taskIndex]} |
| 177 | + <TaskTableBodyCell |
| 178 | + taskResult={taskTable[contestId][taskIndex]} |
| 179 | + {isLoggedIn} |
| 180 | + {updatingModal} |
| 181 | + /> |
| 182 | + {/if} |
| 183 | + </TableBodyCell> |
| 184 | + {/each} |
| 185 | + </TableBodyRow> |
| 186 | + {/each} |
| 187 | + {/if} |
| 188 | + </TableBody> |
| 189 | + </Table> |
| 190 | +</div> |
| 191 | + |
| 192 | +<UpdatingModal bind:this={updatingModal} {isLoggedIn} /> |
0 commit comments