diff --git a/src/lib/components/ExternalLinkWrapper.svelte b/src/lib/components/ExternalLinkWrapper.svelte
index 13bb32e8e..7edc80cf4 100644
--- a/src/lib/components/ExternalLinkWrapper.svelte
+++ b/src/lib/components/ExternalLinkWrapper.svelte
@@ -5,6 +5,8 @@
export let description: string;
export let textSize: string = '';
export let textColorInDarkMode = 'dark:text-primary-500';
+ export let textOverflow = '';
+ export let iconSize = 4;
- {description}
+
+ {description}
+
-
+
diff --git a/src/lib/components/GradeLabel.svelte b/src/lib/components/GradeLabel.svelte
index 9d0e6be27..4de0d01e6 100644
--- a/src/lib/components/GradeLabel.svelte
+++ b/src/lib/components/GradeLabel.svelte
@@ -3,6 +3,8 @@
import { TaskGrade } from '$lib/types/task';
export let taskGrade: TaskGrade | string;
+ export let defaultPadding: number = 1;
+ export let defaultWidth: number = 10;
$: grade = getTaskGradeLabel(taskGrade);
$: gradeColor = getTaskGradeColor(taskGrade);
@@ -10,7 +12,7 @@
diff --git a/src/lib/components/SubmissionStatus/IconForUpdating.svelte b/src/lib/components/SubmissionStatus/IconForUpdating.svelte
new file mode 100644
index 000000000..352775538
--- /dev/null
+++ b/src/lib/components/SubmissionStatus/IconForUpdating.svelte
@@ -0,0 +1,18 @@
+
+
+
+
+{#if isLoggedIn}
+
+{/if}
diff --git a/src/lib/components/TaskTables/TaskTable.svelte b/src/lib/components/TaskTables/TaskTable.svelte
new file mode 100644
index 000000000..275430fc6
--- /dev/null
+++ b/src/lib/components/TaskTables/TaskTable.svelte
@@ -0,0 +1,192 @@
+
+
+
+
+
+
+
+
+
+
+
+ {'AtCoder Beginners Contest 212 〜'}
+
+
+
+
+
+
+
+
+
+
+ Round
+
+ {#if taskTableIndices.length}
+ {#each taskTableIndices as taskTableIndex}
+ {taskTableIndex}
+ {/each}
+ {/if}
+
+
+
+ {#if contestIds.length && taskTableIndices.length}
+ {#each contestIds as contestId}
+
+
+
+ {getContestNameLabelForTaskTable(contestId)}
+
+
+ {#each taskTableIndices as taskIndex}
+
+ {#if taskTable[contestId][taskIndex]}
+
+ {/if}
+
+ {/each}
+
+ {/each}
+ {/if}
+
+
+
+
+
diff --git a/src/lib/components/TaskTables/TaskTableBodyCell.svelte b/src/lib/components/TaskTables/TaskTableBodyCell.svelte
new file mode 100644
index 000000000..830b64f08
--- /dev/null
+++ b/src/lib/components/TaskTables/TaskTableBodyCell.svelte
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {'詳細'}
+
+
diff --git a/src/lib/utils/task.ts b/src/lib/utils/task.ts
index 758fd5900..362649649 100644
--- a/src/lib/utils/task.ts
+++ b/src/lib/utils/task.ts
@@ -1,4 +1,5 @@
import type { UrlGenerator, UrlGenerators } from '$lib/types/url';
+import { ContestType } from '$lib/types/contest';
import { type TaskResult, type TaskResults, TaskGrade, type TaskGrades } from '$lib/types/task';
import { type WorkBookTaskBase } from '$lib/types/workbook';
import { ATCODER_BASE_CONTEST_URL, AOJ_TASKS_URL } from '$lib/constants/urls';
@@ -92,6 +93,18 @@ export function compareByContestIdAndTaskId(first: TaskResult, second: TaskResul
return first.task_table_index.localeCompare(second.task_table_index);
}
+// See:
+// https://github.com/kenkoooo/AtCoderProblems/blob/master/atcoder-problems-frontend/src/pages/TablePage/AtCoderRegularTable.tsx
+export const getTaskTableHeaderName = (contestType: ContestType, taskResult: TaskResult) => {
+ if (contestType === ContestType.ABC && taskResult.task_table_index === 'H') {
+ return 'H/Ex';
+ } else if (taskResult.task_table_index === 'Ex') {
+ return 'H/Ex';
+ }
+
+ return taskResult.task_table_index;
+};
+
// 問題一覧や問題集の詳細ページでは、AtCoder ProblemsのAPIから取得したタイトルからプレフィックス(A., B., ..., G. など)を非表示にする
// 理由: 問題を解くときに、プレフィックスからの先入観を受けないようにするため
// その他: プレフィックスは、同じテーブルの出典に記載する
diff --git a/src/routes/problems/+page.svelte b/src/routes/problems/+page.svelte
index 499b3b367..65e4fe22b 100644
--- a/src/routes/problems/+page.svelte
+++ b/src/routes/problems/+page.svelte
@@ -7,6 +7,7 @@
import HeadingOne from '$lib/components/HeadingOne.svelte';
import TabItemWrapper from '$lib/components/TabItemWrapper.svelte';
+ import TaskTable from '$lib/components/TaskTables/TaskTable.svelte';
import TaskGradeList from '$lib/components/TaskGradeList.svelte';
import GradeGuidelineTable from '$lib/components/TaskGrades/GradeGuidelineTable.svelte';
@@ -25,8 +26,16 @@
+
+
+ {#if isAdmin}
+
+
+
+ {/if}
+
-
+
@@ -45,12 +54,5 @@
-
-
-
-
-
-
diff --git a/src/test/lib/utils/task.test.ts b/src/test/lib/utils/task.test.ts
index d2b6adc51..5e2e46484 100644
--- a/src/test/lib/utils/task.test.ts
+++ b/src/test/lib/utils/task.test.ts
@@ -11,6 +11,7 @@ import {
threeWorkBookTasks,
tasksForVerificationOfOrder,
} from './test_cases/task_results';
+import { taskResultsForTaskTableHeaderName } from './test_cases/task_table_header_name';
import {
getTaskUrl,
countAcceptedTasks,
@@ -18,8 +19,10 @@ import {
areAllTasksAccepted,
compareByContestIdAndTaskId,
removeTaskIndexFromTitle,
+ getTaskTableHeaderName,
} from '$lib/utils/task';
import type { WorkBookTaskBase } from '$lib/types/workbook';
+import { ContestType } from '$lib/types/contest';
import { type TaskResult, type TaskResults } from '$lib/types/task';
type TestCaseForTaskResults = {
@@ -53,6 +56,14 @@ type TestCaseForNewTitle = {
type TestCasesForNewTitle = TestCaseForNewTitle[];
+type TestCaseForTaskTableHeaderName = {
+ contestType: ContestType;
+ taskResult: TaskResult;
+ expected: string;
+};
+
+type TestCasesForTaskTableHeaderName = TestCaseForTaskTableHeaderName[];
+
describe('Task', () => {
describe('task url', () => {
describe('when contest ids and task ids for AtCoder are given', () => {
@@ -450,6 +461,137 @@ describe('Task', () => {
});
});
+ describe('get task table header name', () => {
+ describe('when ABCxxx A, B, F and G are given', () => {
+ const testCases: TestCasesForTaskTableHeaderName = [
+ {
+ contestType: ContestType.ABC,
+ taskResult: taskResultsForTaskTableHeaderName.abc212_a,
+ expected: 'A',
+ },
+ {
+ contestType: ContestType.ABC,
+ taskResult: taskResultsForTaskTableHeaderName.abc212_b,
+ expected: 'B',
+ },
+ {
+ contestType: ContestType.ABC,
+ taskResult: taskResultsForTaskTableHeaderName.abc212_f,
+ expected: 'F',
+ },
+ {
+ contestType: ContestType.ABC,
+ taskResult: taskResultsForTaskTableHeaderName.abc212_g,
+ expected: 'G',
+ },
+ {
+ contestType: ContestType.ABC,
+ taskResult: taskResultsForTaskTableHeaderName.abc233_a,
+ expected: 'A',
+ },
+ {
+ contestType: ContestType.ABC,
+ taskResult: taskResultsForTaskTableHeaderName.abc233_b,
+ expected: 'B',
+ },
+ {
+ contestType: ContestType.ABC,
+ taskResult: taskResultsForTaskTableHeaderName.abc319_a,
+ expected: 'A',
+ },
+ {
+ contestType: ContestType.ABC,
+ taskResult: taskResultsForTaskTableHeaderName.abc319_b,
+ expected: 'B',
+ },
+ {
+ contestType: ContestType.ABC,
+ taskResult: taskResultsForTaskTableHeaderName.abc319_f,
+ expected: 'F',
+ },
+ {
+ contestType: ContestType.ABC,
+ taskResult: taskResultsForTaskTableHeaderName.abc319_g,
+ expected: 'G',
+ },
+ {
+ contestType: ContestType.ABC,
+ taskResult: taskResultsForTaskTableHeaderName.abc386_g,
+ expected: 'G',
+ },
+ ];
+
+ runTests(
+ 'getTaskTableHeaderName',
+ testCases,
+ ({ contestType, taskResult, expected }: TestCaseForTaskTableHeaderName) => {
+ expect(getTaskTableHeaderName(contestType, taskResult)).toBe(expected);
+ },
+ );
+ });
+
+ describe('when ABCxxx H are given', () => {
+ const testCases: TestCasesForTaskTableHeaderName = [
+ {
+ contestType: ContestType.ABC,
+ taskResult: taskResultsForTaskTableHeaderName.abc212_h,
+ expected: 'H/Ex',
+ },
+ {
+ contestType: ContestType.ABC,
+ taskResult: taskResultsForTaskTableHeaderName.abc213_h,
+ expected: 'H/Ex',
+ },
+ {
+ contestType: ContestType.ABC,
+ taskResult: taskResultsForTaskTableHeaderName.abc232_h,
+ expected: 'H/Ex',
+ },
+ ];
+
+ runTests(
+ 'getTaskTableHeaderName',
+ testCases,
+ ({ contestType, taskResult, expected }: TestCaseForTaskTableHeaderName) => {
+ expect(getTaskTableHeaderName(contestType, taskResult)).toBe(expected);
+ },
+ );
+ });
+
+ describe('when ABCxxx Ex are given', () => {
+ const testCases: TestCasesForTaskTableHeaderName = [
+ {
+ contestType: ContestType.ABC,
+ taskResult: taskResultsForTaskTableHeaderName.abc233_ex,
+ expected: 'H/Ex',
+ },
+ {
+ contestType: ContestType.ABC,
+ taskResult: taskResultsForTaskTableHeaderName.abc234_ex,
+ expected: 'H/Ex',
+ },
+ {
+ contestType: ContestType.ABC,
+ taskResult: taskResultsForTaskTableHeaderName.abc317_ex,
+ expected: 'H/Ex',
+ },
+ {
+ contestType: ContestType.ABC,
+ taskResult: taskResultsForTaskTableHeaderName.abc318_ex,
+ expected: 'H/Ex',
+ },
+ ];
+
+ runTests(
+ 'getTaskTableHeaderName',
+ testCases,
+ ({ contestType, taskResult, expected }: TestCaseForTaskTableHeaderName) => {
+ expect(getTaskTableHeaderName(contestType, taskResult)).toBe(expected);
+ },
+ );
+ });
+ });
+
describe('remove task index from title', () => {
describe('when ABC371 is given', () => {
const testCases: TestCasesForNewTitle = [
diff --git a/src/test/lib/utils/test_cases/task_table_header_name.ts b/src/test/lib/utils/test_cases/task_table_header_name.ts
new file mode 100644
index 000000000..382de06d3
--- /dev/null
+++ b/src/test/lib/utils/test_cases/task_table_header_name.ts
@@ -0,0 +1,79 @@
+import type { TaskResult } from '$lib/types/task';
+
+// Default task result with minimal initialization.
+// Most fields are empty strings as they're not relevant for these tests.
+// The updated_at field is set to Unix epoch as we only care about task_table_index
+// and task_id for header name testing.
+const defaultTaskResult: TaskResult = {
+ is_ac: true,
+ user_id: '',
+ status_name: '',
+ status_id: '',
+ submission_status_image_path: '',
+ submission_status_label_name: '',
+ contest_id: '',
+ task_table_index: '',
+ task_id: '',
+ title: '',
+ grade: '',
+ updated_at: new Date(0), // Use the Unix epoch as the default value.
+};
+
+/**
+ * Creates a new TaskResult using defaultTaskResult as a base, overriding the taskId and taskTableIndex.
+ * @param taskId - The ID of the task (e.g., 'abc212')
+ * @param taskTableIndex - The index of the task in the table (e.g., 'A', 'B', 'Ex')
+ * @returns A new TaskResult object with the specified taskId and taskTableIndex
+ */
+function createTaskResultWithTaskTableIndex(taskId: string, taskTableIndex: string): TaskResult {
+ return {
+ ...defaultTaskResult,
+ task_id: taskId,
+ task_table_index: taskTableIndex,
+ };
+}
+
+// ABC212 - ABC232: 8 tasks (A, B, C, D, E, F, G and H)
+const abc212_a = createTaskResultWithTaskTableIndex('abc212', 'A');
+const abc212_b = createTaskResultWithTaskTableIndex('abc212', 'B');
+const abc212_f = createTaskResultWithTaskTableIndex('abc212', 'F');
+const abc212_g = createTaskResultWithTaskTableIndex('abc212', 'G');
+const abc212_h = createTaskResultWithTaskTableIndex('abc212', 'H');
+const abc213_h = createTaskResultWithTaskTableIndex('abc213', 'H');
+const abc232_h = createTaskResultWithTaskTableIndex('abc232', 'H');
+
+// ABC233 - ABC318: 8 tasks (A, B, C, D, E, F, G and Ex)
+const abc233_a = createTaskResultWithTaskTableIndex('abc233', 'A');
+const abc233_b = createTaskResultWithTaskTableIndex('abc233', 'B');
+const abc233_ex = createTaskResultWithTaskTableIndex('abc233', 'Ex');
+const abc234_ex = createTaskResultWithTaskTableIndex('abc234', 'Ex');
+const abc317_ex = createTaskResultWithTaskTableIndex('abc317', 'Ex');
+const abc318_ex = createTaskResultWithTaskTableIndex('abc318', 'Ex');
+
+// ABC319 - : 7 tasks (A, B, C, D, E, F and G)
+const abc319_a = createTaskResultWithTaskTableIndex('abc319', 'A');
+const abc319_b = createTaskResultWithTaskTableIndex('abc319', 'B');
+const abc319_f = createTaskResultWithTaskTableIndex('abc319', 'F');
+const abc319_g = createTaskResultWithTaskTableIndex('abc319', 'G');
+const abc386_g = createTaskResultWithTaskTableIndex('abc386', 'G');
+
+export const taskResultsForTaskTableHeaderName = {
+ abc212_a,
+ abc212_b,
+ abc212_f,
+ abc212_g,
+ abc212_h,
+ abc213_h,
+ abc232_h,
+ abc233_a,
+ abc233_b,
+ abc233_ex,
+ abc234_ex,
+ abc317_ex,
+ abc318_ex,
+ abc319_a,
+ abc319_b,
+ abc319_f,
+ abc319_g,
+ abc386_g,
+};