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, +};