|
17 | 17 | import UpdatingModal from '$lib/components/SubmissionStatus/UpdatingModal.svelte'; |
18 | 18 | import TaskTableBodyCell from '$lib/components/TaskTables/TaskTableBodyCell.svelte'; |
19 | 19 |
|
20 | | - import { classifyContest, getContestNameLabel } from '$lib/utils/contest'; |
21 | | - import { getTaskTableHeaderName } from '$lib/utils/task'; |
| 20 | + import { |
| 21 | + type TaskResultsFilter, |
| 22 | + taskResultsForABCLatest20, |
| 23 | + taskResultsFromABC319Onwards, |
| 24 | + taskResultsFromABC212ToABC318, |
| 25 | + } from '$lib/utils/task_results_filter'; |
| 26 | +
|
| 27 | + import { |
| 28 | + type TaskTableGenerator, |
| 29 | + taskTableGeneratorForABCLatest20, |
| 30 | + taskTableGeneratorFromABC319Onwards, |
| 31 | + taskTableGeneratorFromABC212ToABC318, |
| 32 | + } from '$lib/utils/task_table_generator'; |
| 33 | +
|
22 | 34 | import { getBackgroundColorFrom } from '$lib/services/submission_status'; |
23 | 35 |
|
24 | 36 | interface Props { |
|
28 | 40 |
|
29 | 41 | let { taskResults, isLoggedIn }: Props = $props(); |
30 | 42 |
|
31 | | - // TODO: 任意のコンテスト種別に拡張 |
32 | | - // Note: |
33 | | - // Before and from ABC212 onwards, the number and tendency of tasks are very different. |
34 | | - const fromABC212_Onwards = (taskResult: TaskResult) => |
35 | | - classifyContest(taskResult.contest_id) === ContestType.ABC && taskResult.contest_id >= 'abc212'; |
36 | | -
|
37 | | - let selectedTaskResults: TaskResults = $derived( |
38 | | - filterTaskResultsByContestType(taskResults, fromABC212_Onwards), |
39 | | - ); |
40 | | - let contestIds: Array<string> = $derived(getContestIds(selectedTaskResults)); |
41 | | - let taskTableIndices: Array<string> = $derived( |
42 | | - getTaskTableIndices(selectedTaskResults, ContestType.ABC), |
| 43 | + // TODO: 任意のコンテスト種別を追加 |
| 44 | + // TODO: コンテスト種別の並び順を決める |
| 45 | + const contestFilterConfigs = { |
| 46 | + abcLatest20Rounds: { |
| 47 | + filter: () => taskResultsForABCLatest20(taskResults), |
| 48 | + table: (results: TaskResults) => taskTableGeneratorForABCLatest20(results, ContestType.ABC), |
| 49 | + buttonLabel: 'ABC 最新 20 回', |
| 50 | + ariaLabel: 'Filter ABC latest 20 rounds', |
| 51 | + }, |
| 52 | + abc319Onwards: { |
| 53 | + filter: () => taskResultsFromABC319Onwards(taskResults), |
| 54 | + table: (results: TaskResults) => |
| 55 | + taskTableGeneratorFromABC319Onwards(results, ContestType.ABC), |
| 56 | + buttonLabel: 'ABC319 〜', |
| 57 | + ariaLabel: 'Filter contests from ABC 319 onwards', |
| 58 | + }, |
| 59 | + abc212To318: { |
| 60 | + filter: () => taskResultsFromABC212ToABC318(taskResults), |
| 61 | + table: (results: TaskResults) => |
| 62 | + taskTableGeneratorFromABC212ToABC318(results, ContestType.ABC), |
| 63 | + buttonLabel: 'ABC212 〜 318', |
| 64 | + ariaLabel: 'Filter contests from ABC 212 to ABC 318', |
| 65 | + }, |
| 66 | + }; |
| 67 | +
|
| 68 | + type ContestTypeFilter = 'abcLatest20Rounds' | 'abc319Onwards' | 'abc212To318'; |
| 69 | +
|
| 70 | + let activeContestType = $state<ContestTypeFilter>('abcLatest20Rounds'); |
| 71 | +
|
| 72 | + // Select the task results based on the active contest type. |
| 73 | + let taskResultsFilter: TaskResultsFilter = $derived( |
| 74 | + contestFilterConfigs[activeContestType].filter(), |
43 | 75 | ); |
44 | | - let taskTable: Record<string, Record<string, TaskResult>> = $derived( |
45 | | - prepareTaskTable(selectedTaskResults, ContestType.ABC), |
| 76 | + let selectedTaskResults: TaskResults = $derived(taskResultsFilter.run()); |
| 77 | +
|
| 78 | + // Generate the task table based on the selected task results. |
| 79 | + let taskTableGenerator: TaskTableGenerator = $derived( |
| 80 | + contestFilterConfigs[activeContestType].table(selectedTaskResults), |
46 | 81 | ); |
| 82 | + let taskTable: Record<string, Record<string, TaskResult>> = $derived(taskTableGenerator.run()); |
| 83 | + let taskTableHeaderIds: Array<string> = $derived(taskTableGenerator.getHeaderIdsForTask()); |
| 84 | + let contestIds: Array<string> = $derived(taskTableGenerator.getContestRoundIds()); |
| 85 | +
|
| 86 | + function getTaskTableTitle(taskTableGenerator: TaskTableGenerator): string { |
| 87 | + return taskTableGenerator.getTitle() ?? ''; |
| 88 | + } |
| 89 | +
|
| 90 | + function getContestRoundLabel(taskTableGenerator: TaskTableGenerator, contestId: string): string { |
| 91 | + return taskTableGenerator.getContestRoundLabel(contestId); |
| 92 | + } |
| 93 | +
|
47 | 94 | // FIXME: 他のコンポーネントと完全に重複しているので、コンポーネントとして切り出す。 |
48 | 95 | let updatingModal: UpdatingModal | null = null; |
49 | 96 |
|
|
56 | 103 | } |
57 | 104 | } |
58 | 105 |
|
59 | | - function filterTaskResultsByContestType( |
60 | | - taskResults: TaskResults, |
61 | | - condition: (taskResult: TaskResult) => boolean, |
62 | | - ): TaskResults { |
63 | | - return taskResults.filter(condition); |
64 | | - } |
65 | | -
|
66 | | - function getContestIds(selectedTaskResults: TaskResults): Array<string> { |
67 | | - const contestList = selectedTaskResults.map((taskResult: TaskResult) => taskResult.contest_id); |
68 | | - return Array.from(new Set(contestList)).sort().reverse(); |
69 | | - } |
70 | | -
|
71 | | - function getTaskTableIndices( |
72 | | - selectedTaskResults: TaskResults, |
73 | | - selectedContestType: ContestType, |
74 | | - ): Array<string> { |
75 | | - const headerList = selectedTaskResults.map((taskResult: TaskResult) => |
76 | | - getTaskTableHeaderName(selectedContestType, taskResult), |
77 | | - ); |
78 | | - return Array.from(new Set(headerList)).sort(); |
79 | | - } |
80 | | -
|
81 | | - /** |
82 | | - * Prepare a table for task and submission statuses. |
83 | | - * |
84 | | - * Computational complexity of preparation table: O(N), where N is the number of task results. |
85 | | - * Computational complexity of accessing table: O(1). |
86 | | - * |
87 | | - * @param selectedTaskResults Task results to be shown in the table. |
88 | | - * @param selectedContestType Contest type of the task results. |
89 | | - * @returns A table for task and submission statuses. |
90 | | - */ |
91 | | - function prepareTaskTable( |
92 | | - selectedTaskResults: TaskResults, |
93 | | - selectedContestType: ContestType, |
94 | | - ): Record<string, Record<string, TaskResult>> { |
95 | | - const table: Record<string, Record<string, TaskResult>> = {}; |
96 | | -
|
97 | | - selectedTaskResults.forEach((taskResult: TaskResult) => { |
98 | | - const contestId = taskResult.contest_id; |
99 | | - const taskTableIndex = getTaskTableHeaderName(selectedContestType, taskResult); |
100 | | -
|
101 | | - if (!table[contestId]) { |
102 | | - table[contestId] = {}; |
103 | | - } |
104 | | -
|
105 | | - table[contestId][taskTableIndex] = taskResult; |
106 | | - }); |
107 | | -
|
108 | | - return table; |
109 | | - } |
110 | | -
|
111 | | - function getContestNameLabelForTaskTable(contestId: string): string { |
112 | | - let contestNameLabel = getContestNameLabel(contestId); |
113 | | - const contestType = classifyContest(contestId); |
114 | | -
|
115 | | - switch (contestType) { |
116 | | - case ContestType.ABC: |
117 | | - return contestNameLabel.replace('ABC ', ''); |
118 | | - // TODO: Add cases for other contest types. |
119 | | - default: |
120 | | - return contestNameLabel; |
121 | | - } |
122 | | - } |
123 | | -
|
124 | 106 | function getBodyCellClasses(contestId: string, taskIndex: string): string { |
125 | 107 | 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'; |
126 | 108 | const backgroundColor = getBackgroundColor(taskTable[contestId][taskIndex]); |
|
139 | 121 | } |
140 | 122 | </script> |
141 | 123 |
|
142 | | -<!-- TODO: コンテスト種別のボタンの並び順を決める --> |
143 | 124 | <!-- See: --> |
144 | 125 | <!-- https://flowbite-svelte.com/docs/components/button-group --> |
145 | 126 | <ButtonGroup class="m-4 contents-center"> |
146 | | - <Button |
147 | | - onclick={() => filterTaskResultsByContestType(taskResults, fromABC212_Onwards)} |
148 | | - aria-label="Filter contests from ABC212 onwards" |
149 | | - > |
150 | | - ABC212〜 |
151 | | - </Button> |
| 127 | + {#each Object.entries(contestFilterConfigs) as [type, config]} |
| 128 | + <Button |
| 129 | + onclick={() => (activeContestType = type as ContestTypeFilter)} |
| 130 | + class={activeContestType === type ? 'active-button-class' : ''} |
| 131 | + aria-label={config.ariaLabel} |
| 132 | + > |
| 133 | + {config.buttonLabel} |
| 134 | + </Button> |
| 135 | + {/each} |
152 | 136 | </ButtonGroup> |
153 | 137 |
|
154 | | -<!-- TODO: コンテスト種別に応じて動的に変更できるようにする --> |
155 | 138 | <Heading tag="h2" class="text-2xl pb-3 text-gray-900 dark:text-white"> |
156 | | - {'AtCoder Beginners Contest 212 〜'} |
| 139 | + {getTaskTableTitle(taskTableGenerator)} |
157 | 140 | </Heading> |
158 | 141 |
|
159 | 142 | <!-- TODO: ページネーションを実装 --> |
|
163 | 146 | <div class="container w-full overflow-auto border rounded-md"> |
164 | 147 | <Table shadow id="task-table" class="text-md table-fixed" aria-label="Task table"> |
165 | 148 | <TableHead class="text-sm bg-gray-100"> |
166 | | - <TableHeadCell class="w-full xl:w-16 px-2 text-center border" scope="col">Round</TableHeadCell |
167 | | - > |
| 149 | + <TableHeadCell class="w-full xl:w-16 px-2 text-center border" scope="col"> |
| 150 | + Round |
| 151 | + </TableHeadCell> |
168 | 152 |
|
169 | | - {#if taskTableIndices.length} |
170 | | - {#each taskTableIndices as taskTableIndex} |
171 | | - <TableHeadCell class="text-center border" scope="col">{taskTableIndex}</TableHeadCell> |
| 153 | + {#if taskTableHeaderIds.length} |
| 154 | + {#each taskTableHeaderIds as taskTableHeaderId} |
| 155 | + <TableHeadCell class="text-center border" scope="col">{taskTableHeaderId}</TableHeadCell> |
172 | 156 | {/each} |
173 | 157 | {/if} |
174 | 158 | </TableHead> |
175 | 159 |
|
176 | 160 | <TableBody class="divide-y"> |
177 | | - {#if contestIds.length && taskTableIndices.length} |
| 161 | + {#if contestIds.length && taskTableHeaderIds.length} |
178 | 162 | {#each contestIds as contestId} |
179 | 163 | <TableBodyRow class="flex flex-wrap xl:table-row"> |
180 | 164 | <TableBodyCell class="w-full xl:w-16 truncate px-2 py-2 text-center border"> |
181 | | - <!-- FIXME: コンテスト種別に合わせて修正できるようにする --> |
182 | | - {getContestNameLabelForTaskTable(contestId)} |
| 165 | + {getContestRoundLabel(taskTableGenerator, contestId)} |
183 | 166 | </TableBodyCell> |
184 | 167 |
|
185 | | - {#each taskTableIndices as taskIndex} |
| 168 | + {#each taskTableHeaderIds as taskTableHeaderId} |
186 | 169 | <TableBodyCell |
187 | | - id={contestId + '-' + taskIndex} |
188 | | - class={getBodyCellClasses(contestId, taskIndex)} |
| 170 | + id={contestId + '-' + taskTableHeaderId} |
| 171 | + class={getBodyCellClasses(contestId, taskTableHeaderId)} |
189 | 172 | > |
190 | | - {#if taskTable[contestId][taskIndex]} |
| 173 | + {#if taskTable[contestId][taskTableHeaderId]} |
191 | 174 | <TaskTableBodyCell |
192 | | - taskResult={taskTable[contestId][taskIndex]} |
| 175 | + taskResult={taskTable[contestId][taskTableHeaderId]} |
193 | 176 | {isLoggedIn} |
194 | | - onClick={() => openModal(taskTable[contestId][taskIndex])} |
| 177 | + onClick={() => openModal(taskTable[contestId][taskTableHeaderId])} |
195 | 178 | /> |
196 | 179 | {/if} |
197 | 180 | </TableBodyCell> |
|
0 commit comments