Skip to content

Commit 41aa06e

Browse files
authored
Merge pull request #1820 from AtCoder-NoviSteps/#1807
♻️ Adhere to principle of single responsibility through class redesign (#1807)
2 parents cdd0749 + a481e46 commit 41aa06e

File tree

5 files changed

+301
-296
lines changed

5 files changed

+301
-296
lines changed

src/lib/components/TaskTables/TaskTable.svelte

Lines changed: 26 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,15 @@
1212
} from 'svelte-5-ui-lib';
1313
1414
import type { TaskResults, TaskResult } from '$lib/types/task';
15-
import { ContestType } from '$lib/types/contest';
15+
import type { ContestTableProvider } from '$lib/types/contest_table_provider';
1616
1717
import UpdatingModal from '$lib/components/SubmissionStatus/UpdatingModal.svelte';
1818
import TaskTableBodyCell from '$lib/components/TaskTables/TaskTableBodyCell.svelte';
1919
2020
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';
21+
contestTableProviders,
22+
type ContestTableProviders,
23+
} from '$lib/utils/contest_table_provider';
3324
3425
import { getBackgroundColorFrom } from '$lib/services/submission_status';
3526
@@ -40,58 +31,27 @@
4031
4132
let { taskResults, isLoggedIn }: Props = $props();
4233
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(),
75-
);
76-
let selectedTaskResults: TaskResults = $derived(taskResultsFilter.run());
34+
let activeContestType = $state<ContestTableProviders>('abcLatest20Rounds');
7735
78-
// Generate the task table based on the selected task results.
79-
let taskTableGenerator: TaskTableGenerator = $derived(
80-
contestFilterConfigs[activeContestType].table(selectedTaskResults),
36+
let provider: ContestTableProvider = $derived(
37+
contestTableProviders[activeContestType as ContestTableProviders],
8138
);
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-
}
39+
// Filter the task results based on the active contest type.
40+
let filteredTaskResults = $derived(provider.filter(taskResults));
41+
// Generate the task table based on the filtered task results.
42+
let taskTable: Record<string, Record<string, TaskResult>> = $derived(
43+
provider.generateTable(filteredTaskResults),
44+
);
45+
let taskTableHeaderIds: Array<string> = $derived(
46+
provider.getHeaderIdsForTask(filteredTaskResults),
47+
);
48+
let contestIds: Array<string> = $derived(provider.getContestRoundIds(filteredTaskResults));
49+
let title = $derived(provider.getMetadata().title);
8950
90-
function getContestRoundLabel(taskTableGenerator: TaskTableGenerator, contestId: string): string {
91-
return taskTableGenerator.getContestRoundLabel(contestId);
51+
function getContestRoundLabel(provider: ContestTableProvider, contestId: string): string {
52+
return provider.getContestRoundLabel(contestId);
9253
}
9354
94-
// FIXME: 他のコンポーネントと完全に重複しているので、コンポーネントとして切り出す。
9555
let updatingModal: UpdatingModal | null = null;
9656
9757
// WHY: () => updatingModal.openModal(taskResult) だけだと、updatingModalがnullの可能性があるため。
@@ -124,19 +84,19 @@
12484
<!-- See: -->
12585
<!-- https://flowbite-svelte.com/docs/components/button-group -->
12686
<ButtonGroup class="m-4 contents-center">
127-
{#each Object.entries(contestFilterConfigs) as [type, config]}
87+
{#each Object.entries(contestTableProviders) as [type, config]}
12888
<Button
129-
onclick={() => (activeContestType = type as ContestTypeFilter)}
89+
onclick={() => (activeContestType = type as ContestTableProviders)}
13090
class={activeContestType === type ? 'active-button-class' : ''}
131-
aria-label={config.ariaLabel}
91+
aria-label={config.getMetadata().ariaLabel}
13292
>
133-
{config.buttonLabel}
93+
{config.getMetadata().buttonLabel}
13494
</Button>
13595
{/each}
13696
</ButtonGroup>
13797

13898
<Heading tag="h2" class="text-2xl pb-3 text-gray-900 dark:text-white">
139-
{getTaskTableTitle(taskTableGenerator)}
99+
{title}
140100
</Heading>
141101

142102
<!-- TODO: ページネーションを実装 -->
@@ -162,7 +122,7 @@
162122
{#each contestIds as contestId}
163123
<TableBodyRow class="flex flex-wrap xl:table-row">
164124
<TableBodyCell class="w-full xl:w-16 truncate px-2 py-2 text-center border">
165-
{getContestRoundLabel(taskTableGenerator, contestId)}
125+
{getContestRoundLabel(provider, contestId)}
166126
</TableBodyCell>
167127

168128
{#each taskTableHeaderIds as taskTableHeaderId}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import type { TaskResults, TaskResult } from '$lib/types/task';
2+
3+
/**
4+
* Provider interface for building and managing contest tables.
5+
*
6+
* This interface defines the contract for components that create, filter, and
7+
* generate contest tables from task results.
8+
*
9+
*/
10+
export interface ContestTableProvider {
11+
/**
12+
* Filters the provided task results according to implementation-specific criteria.
13+
*
14+
* @param {TaskResults} taskResults - The original task results to be filtered
15+
* @returns {TaskResults} The filtered task results
16+
*/
17+
filter(taskResults: TaskResults): TaskResults;
18+
19+
/**
20+
* Generates a contest table based on the provided filtered task results.
21+
*
22+
* @param {TaskResults} filteredTaskResults - The filtered task results to use for table generation
23+
* @returns {ContestTable} The generated contest table
24+
*/
25+
generateTable(filteredTaskResults: TaskResults): ContestTable;
26+
27+
/**
28+
* Retrieves the unique identifiers for all contest rounds.
29+
*
30+
* @param {TaskResults} filteredTaskResults - The filtered task results to use for table generation
31+
* @returns {Array<string>} An array of contest round identifiers.
32+
*/
33+
getContestRoundIds(filteredTaskResults: TaskResults): Array<string>;
34+
35+
/**
36+
* Retrieves an array of header IDs associated with the current contest tasks.
37+
* These IDs are used to identify and display the relevant columns in the task table.
38+
*
39+
* @param {TaskResults} filteredTaskResults - The filtered task results to use for table generation
40+
* @returns {Array<string>} An array of string IDs corresponding to the header columns for the task.
41+
*/
42+
getHeaderIdsForTask(filteredTaskResults: TaskResults): Array<string>;
43+
44+
/**
45+
* Retrieves metadata associated with the contest table.
46+
*
47+
* @returns {ContestTableMetaData} Metadata for the contest table
48+
*/
49+
getMetadata(): ContestTableMetaData;
50+
51+
/**
52+
* Returns a formatted label for the contest round.
53+
*
54+
* This abstract method must be implemented by subclasses to provide
55+
* a string representation of the contest round that can be displayed
56+
* in the task table.
57+
*
58+
* @param contestId - The ID of the contest.
59+
*
60+
* @returns {string} The formatted label string for the contest round.
61+
*/
62+
getContestRoundLabel(contestId: string): string;
63+
}
64+
65+
/**
66+
* Represents a two-dimensional table of contest results.
67+
*
68+
* The structure is organized as a nested record:
69+
* - The outer keys represent contest id
70+
* - The inner keys represent task id
71+
* - The values are the results for each task
72+
*
73+
* @example
74+
* {
75+
* "abc396": {
76+
* "abc396_a": {contest_id: "abc396", task_id: "abc396_a", status_name: "ac", ...},
77+
* "abc396_b": {contest_id: "abc396", task_id: "abc396_b", status_name: "ac", ...},
78+
* "abc396_c": {contest_id: "abc396", task_id: "abc396_c", status_name: "ac_with_editorial", ...},
79+
* ...,
80+
* "abc396_g": {contest_id: "abc396", task_id: "abc396_g", status_name: "wa", ...},
81+
* },
82+
* "abc395": {
83+
* "abc395_a": {contest_id: "abc395", task_id: "abc395_a", status_name: "ac", ...},
84+
* "abc395_b": {contest_id: "abc395", task_id: "abc395_b", status_name: "ac", ...},
85+
* "abc395_c": {contest_id: "abc395", task_id: "abc395_c", status_name: "ac", ...},
86+
* ...,
87+
* "abc395_g": {contest_id: "abc395", task_id: "abc395_g", status_name: "wa", ...},
88+
* },
89+
* }
90+
*/
91+
export type ContestTable = Record<string, Record<string, TaskResult>>;
92+
93+
/**
94+
* Metadata for configuring a contest table's display properties.
95+
*
96+
* @typedef {Object} ContestTableMetaData
97+
* @property {string} title - The title text to display for the contest table.
98+
* @property {string} buttonLabel - The text to display on the contest table's primary action button.
99+
* @property {string} ariaLabel - Accessibility label for screen readers describing the contest table.
100+
*/
101+
export type ContestTableMetaData = {
102+
title: string;
103+
buttonLabel: string;
104+
ariaLabel: string;
105+
};

0 commit comments

Comments
 (0)