Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 26 additions & 66 deletions src/lib/components/TaskTables/TaskTable.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,15 @@
} from 'svelte-5-ui-lib';

import type { TaskResults, TaskResult } from '$lib/types/task';
import { ContestType } from '$lib/types/contest';
import type { ContestTableProvider } from '$lib/types/contest_table_provider';

import UpdatingModal from '$lib/components/SubmissionStatus/UpdatingModal.svelte';
import TaskTableBodyCell from '$lib/components/TaskTables/TaskTableBodyCell.svelte';

import {
type TaskResultsFilter,
taskResultsForABCLatest20,
taskResultsFromABC319Onwards,
taskResultsFromABC212ToABC318,
} from '$lib/utils/task_results_filter';

import {
type TaskTableGenerator,
taskTableGeneratorForABCLatest20,
taskTableGeneratorFromABC319Onwards,
taskTableGeneratorFromABC212ToABC318,
} from '$lib/utils/task_table_generator';
contestTableProviders,
type ContestTableProviders,
} from '$lib/utils/contest_table_provider';

import { getBackgroundColorFrom } from '$lib/services/submission_status';

Expand All @@ -40,58 +31,27 @@

let { taskResults, isLoggedIn }: Props = $props();

// TODO: 任意のコンテスト種別を追加
// TODO: コンテスト種別の並び順を決める
const contestFilterConfigs = {
abcLatest20Rounds: {
filter: () => taskResultsForABCLatest20(taskResults),
table: (results: TaskResults) => taskTableGeneratorForABCLatest20(results, ContestType.ABC),
buttonLabel: 'ABC 最新 20 回',
ariaLabel: 'Filter ABC latest 20 rounds',
},
abc319Onwards: {
filter: () => taskResultsFromABC319Onwards(taskResults),
table: (results: TaskResults) =>
taskTableGeneratorFromABC319Onwards(results, ContestType.ABC),
buttonLabel: 'ABC319 〜',
ariaLabel: 'Filter contests from ABC 319 onwards',
},
abc212To318: {
filter: () => taskResultsFromABC212ToABC318(taskResults),
table: (results: TaskResults) =>
taskTableGeneratorFromABC212ToABC318(results, ContestType.ABC),
buttonLabel: 'ABC212 〜 318',
ariaLabel: 'Filter contests from ABC 212 to ABC 318',
},
};

type ContestTypeFilter = 'abcLatest20Rounds' | 'abc319Onwards' | 'abc212To318';

let activeContestType = $state<ContestTypeFilter>('abcLatest20Rounds');

// Select the task results based on the active contest type.
let taskResultsFilter: TaskResultsFilter = $derived(
contestFilterConfigs[activeContestType].filter(),
);
let selectedTaskResults: TaskResults = $derived(taskResultsFilter.run());
let activeContestType = $state<ContestTableProviders>('abcLatest20Rounds');

// Generate the task table based on the selected task results.
let taskTableGenerator: TaskTableGenerator = $derived(
contestFilterConfigs[activeContestType].table(selectedTaskResults),
let provider: ContestTableProvider = $derived(
contestTableProviders[activeContestType as ContestTableProviders],
);
let taskTable: Record<string, Record<string, TaskResult>> = $derived(taskTableGenerator.run());
let taskTableHeaderIds: Array<string> = $derived(taskTableGenerator.getHeaderIdsForTask());
let contestIds: Array<string> = $derived(taskTableGenerator.getContestRoundIds());

function getTaskTableTitle(taskTableGenerator: TaskTableGenerator): string {
return taskTableGenerator.getTitle() ?? '';
}
// 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<string, Record<string, TaskResult>> = $derived(
provider.generateTable(filteredTaskResults),
);
let taskTableHeaderIds: Array<string> = $derived(
provider.getHeaderIdsForTask(filteredTaskResults),
);
let contestIds: Array<string> = $derived(provider.getContestRoundIds(filteredTaskResults));
let title = $derived(provider.getMetadata().title);

function getContestRoundLabel(taskTableGenerator: TaskTableGenerator, contestId: string): string {
return taskTableGenerator.getContestRoundLabel(contestId);
function getContestRoundLabel(provider: ContestTableProvider, contestId: string): string {
return provider.getContestRoundLabel(contestId);
}

// FIXME: 他のコンポーネントと完全に重複しているので、コンポーネントとして切り出す。
let updatingModal: UpdatingModal | null = null;

// WHY: () => updatingModal.openModal(taskResult) だけだと、updatingModalがnullの可能性があるため。
Expand Down Expand Up @@ -124,19 +84,19 @@
<!-- See: -->
<!-- https://flowbite-svelte.com/docs/components/button-group -->
<ButtonGroup class="m-4 contents-center">
{#each Object.entries(contestFilterConfigs) as [type, config]}
{#each Object.entries(contestTableProviders) as [type, config]}
<Button
onclick={() => (activeContestType = type as ContestTypeFilter)}
onclick={() => (activeContestType = type as ContestTableProviders)}
class={activeContestType === type ? 'active-button-class' : ''}
aria-label={config.ariaLabel}
aria-label={config.getMetadata().ariaLabel}
>
{config.buttonLabel}
{config.getMetadata().buttonLabel}
</Button>
{/each}
</ButtonGroup>

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

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

{#each taskTableHeaderIds as taskTableHeaderId}
Expand Down
105 changes: 105 additions & 0 deletions src/lib/types/contest_table_provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import type { TaskResults, TaskResult } from '$lib/types/task';

/**
* Provider interface for building and managing contest tables.
*
* This interface defines the contract for components that create, filter, and
* generate contest tables from task results.
*
*/
export interface ContestTableProvider {
/**
* Filters the provided task results according to implementation-specific criteria.
*
* @param {TaskResults} taskResults - The original task results to be filtered
* @returns {TaskResults} The filtered task results
*/
filter(taskResults: TaskResults): TaskResults;

/**
* Generates a contest table based on the provided filtered task results.
*
* @param {TaskResults} filteredTaskResults - The filtered task results to use for table generation
* @returns {ContestTable} The generated contest table
*/
generateTable(filteredTaskResults: TaskResults): ContestTable;

/**
* Retrieves the unique identifiers for all contest rounds.
*
* @param {TaskResults} filteredTaskResults - The filtered task results to use for table generation
* @returns {Array<string>} An array of contest round identifiers.
*/
getContestRoundIds(filteredTaskResults: TaskResults): Array<string>;

/**
* Retrieves an array of header IDs associated with the current contest tasks.
* These IDs are used to identify and display the relevant columns in the task table.
*
* @param {TaskResults} filteredTaskResults - The filtered task results to use for table generation
* @returns {Array<string>} An array of string IDs corresponding to the header columns for the task.
*/
getHeaderIdsForTask(filteredTaskResults: TaskResults): Array<string>;

/**
* Retrieves metadata associated with the contest table.
*
* @returns {ContestTableMetaData} Metadata for the contest table
*/
getMetadata(): ContestTableMetaData;

/**
* Returns a formatted label for the contest round.
*
* This abstract method must be implemented by subclasses to provide
* a string representation of the contest round that can be displayed
* in the task table.
*
* @param contestId - The ID of the contest.
*
* @returns {string} The formatted label string for the contest round.
*/
getContestRoundLabel(contestId: string): string;
}

/**
* Represents a two-dimensional table of contest results.
*
* The structure is organized as a nested record:
* - The outer keys represent contest id
* - The inner keys represent task id
* - The values are the results for each task
*
* @example
* {
* "abc396": {
* "abc396_a": {contest_id: "abc396", task_id: "abc396_a", status_name: "ac", ...},
* "abc396_b": {contest_id: "abc396", task_id: "abc396_b", status_name: "ac", ...},
* "abc396_c": {contest_id: "abc396", task_id: "abc396_c", status_name: "ac_with_editorial", ...},
* ...,
* "abc396_g": {contest_id: "abc396", task_id: "abc396_g", status_name: "wa", ...},
* },
* "abc395": {
* "abc395_a": {contest_id: "abc395", task_id: "abc395_a", status_name: "ac", ...},
* "abc395_b": {contest_id: "abc395", task_id: "abc395_b", status_name: "ac", ...},
* "abc395_c": {contest_id: "abc395", task_id: "abc395_c", status_name: "ac", ...},
* ...,
* "abc395_g": {contest_id: "abc395", task_id: "abc395_g", status_name: "wa", ...},
* },
* }
*/
export type ContestTable = Record<string, Record<string, TaskResult>>;

/**
* Metadata for configuring a contest table's display properties.
*
* @typedef {Object} ContestTableMetaData
* @property {string} title - The title text to display for the contest table.
* @property {string} buttonLabel - The text to display on the contest table's primary action button.
* @property {string} ariaLabel - Accessibility label for screen readers describing the contest table.
*/
export type ContestTableMetaData = {
title: string;
buttonLabel: string;
ariaLabel: string;
};
Loading
Loading