Skip to content

Commit 2c2f141

Browse files
authored
Merge pull request #1614 from AtCoder-NoviSteps/#1611
✨ Enable to show task table for ABC212 - (#1611)
2 parents 4d21cce + f6f55ba commit 2c2f141

File tree

9 files changed

+509
-11
lines changed

9 files changed

+509
-11
lines changed

src/lib/components/ExternalLinkWrapper.svelte

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
export let description: string;
66
export let textSize: string = '';
77
export let textColorInDarkMode = 'dark:text-primary-500';
8+
export let textOverflow = '';
9+
export let iconSize = 4;
810
</script>
911

1012
<a
@@ -13,10 +15,12 @@
1315
target="_blank"
1416
rel="noreferrer"
1517
>
16-
{description}
18+
<div class="{textOverflow} truncate">
19+
{description}
20+
</div>
1721

1822
<div class="ml-1.5">
19-
<ExternalLinkIcon size="w-4 h-4" />
23+
<ExternalLinkIcon size="w-{iconSize} h-{iconSize}" />
2024
</div>
2125
</a>
2226

src/lib/components/GradeLabel.svelte

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@
33
import { TaskGrade } from '$lib/types/task';
44
55
export let taskGrade: TaskGrade | string;
6+
export let defaultPadding: number = 1;
7+
export let defaultWidth: number = 10;
68
79
$: grade = getTaskGradeLabel(taskGrade);
810
$: gradeColor = getTaskGradeColor(taskGrade);
911
</script>
1012

1113
<div class="rounded-lg border-2 border-white">
1214
<div
13-
class="p-1 w-8 xs:w-10 text-sm xs:text-md text-center rounded-md {toWhiteTextIfNeeds(
15+
class="p-{defaultPadding} w-8 xs:w-{defaultWidth} text-sm xs:text-md text-center rounded-md {toWhiteTextIfNeeds(
1416
grade,
1517
)} {gradeColor}"
1618
>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<script lang="ts">
2+
// @ts-ignore
3+
import ChevronDownOutline from 'flowbite-svelte-icons/ChevronDownOutline.svelte';
4+
5+
export let isLoggedIn: boolean;
6+
</script>
7+
8+
<!-- HACK: 以下のコンポーネントと類似しているが、差分が大きいため別コンポーネントとして用意 -->
9+
<!-- src/lib/components/SubmissionStatus/SubmissionStatusImage.svelte -->
10+
{#if isLoggedIn}
11+
<div class="flex items-center justify-center text-sm">
12+
<div class="dark:text-gray-300">
13+
{'更新'}
14+
</div>
15+
16+
<ChevronDownOutline class="w-4 h-4 text-primary-600 dark:text-gray-300 inline" />
17+
</div>
18+
{/if}
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
<script lang="ts">
2+
import {
3+
Heading,
4+
ButtonGroup,
5+
Button,
6+
Table,
7+
TableBody,
8+
TableBodyCell,
9+
TableBodyRow,
10+
TableHead,
11+
TableHeadCell,
12+
} from 'flowbite-svelte';
13+
14+
import type { TaskResults, TaskResult } from '$lib/types/task';
15+
import { ContestType } from '$lib/types/contest';
16+
17+
import UpdatingModal from '$lib/components/SubmissionStatus/UpdatingModal.svelte';
18+
import TaskTableBodyCell from '$lib/components/TaskTables/TaskTableBodyCell.svelte';
19+
20+
import { classifyContest, getContestNameLabel } from '$lib/utils/contest';
21+
import { getTaskTableHeaderName } from '$lib/utils/task';
22+
import { getBackgroundColorFrom } from '$lib/services/submission_status';
23+
24+
export let taskResults: TaskResults;
25+
export let isLoggedIn: boolean;
26+
27+
let selectedTaskResults: TaskResults;
28+
let contestIds: Array<string>;
29+
let taskTableIndices: Array<string>;
30+
let taskTable: Record<string, Record<string, TaskResult>>;
31+
let updatingModal: UpdatingModal;
32+
33+
// TODO: 任意のコンテスト種別に拡張
34+
$: selectedTaskResults = filterTaskResultsByContestType(taskResults, fromABC212_Onwards);
35+
$: contestIds = getContestIds(selectedTaskResults);
36+
$: taskTableIndices = getTaskTableIndices(selectedTaskResults, ContestType.ABC);
37+
$: taskTable = prepareTaskTable(selectedTaskResults, ContestType.ABC);
38+
39+
function filterTaskResultsByContestType(
40+
taskResults: TaskResults,
41+
condition: (taskResult: TaskResult) => boolean,
42+
): TaskResults {
43+
return taskResults.filter(condition);
44+
}
45+
46+
// Note:
47+
// Before and from ABC212 onwards, the number and tendency of tasks are very different.
48+
const fromABC212_Onwards = (taskResult: TaskResult) =>
49+
classifyContest(taskResult.contest_id) === ContestType.ABC && taskResult.contest_id >= 'abc212';
50+
51+
function getContestIds(selectedTaskResults: TaskResults): Array<string> {
52+
const contestList = selectedTaskResults.map((taskResult: TaskResult) => taskResult.contest_id);
53+
return Array.from(new Set(contestList)).sort().reverse();
54+
}
55+
56+
function getTaskTableIndices(
57+
selectedTaskResults: TaskResults,
58+
selectedContestType: ContestType,
59+
): Array<string> {
60+
const headerList = selectedTaskResults.map((taskResult: TaskResult) =>
61+
getTaskTableHeaderName(selectedContestType, taskResult),
62+
);
63+
return Array.from(new Set(headerList)).sort();
64+
}
65+
66+
/**
67+
* Prepare a table for task and submission statuses.
68+
*
69+
* Computational complexity of preparation table: O(N), where N is the number of task results.
70+
* Computational complexity of accessing table: O(1).
71+
*
72+
* @param selectedTaskResults Task results to be shown in the table.
73+
* @param selectedContestType Contest type of the task results.
74+
* @returns A table for task and submission statuses.
75+
*/
76+
function prepareTaskTable(
77+
selectedTaskResults: TaskResults,
78+
selectedContestType: ContestType,
79+
): Record<string, Record<string, TaskResult>> {
80+
const table: Record<string, Record<string, TaskResult>> = {};
81+
82+
selectedTaskResults.forEach((taskResult: TaskResult) => {
83+
const contestId = taskResult.contest_id;
84+
const taskTableIndex = getTaskTableHeaderName(selectedContestType, taskResult);
85+
86+
if (!table[contestId]) {
87+
table[contestId] = {};
88+
}
89+
90+
table[contestId][taskTableIndex] = taskResult;
91+
});
92+
93+
return table;
94+
}
95+
96+
function getContestNameLabelForTaskTable(contestId: string): string {
97+
let contestNameLabel = getContestNameLabel(contestId);
98+
const contestType = classifyContest(contestId);
99+
100+
switch (contestType) {
101+
case ContestType.ABC:
102+
return contestNameLabel.replace('ABC ', '');
103+
// TODO: Add cases for other contest types.
104+
default:
105+
return contestNameLabel;
106+
}
107+
}
108+
109+
function getBodyCellClasses(contestId: string, taskIndex: string): string {
110+
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';
111+
const backgroundColor = getBackgroundColor(taskTable[contestId][taskIndex]);
112+
113+
return `${baseClasses} ${backgroundColor}`;
114+
}
115+
116+
function getBackgroundColor(taskResult: TaskResult): string {
117+
const statusName = taskResult?.status_name;
118+
119+
if (taskResult && statusName !== 'ns') {
120+
return getBackgroundColorFrom(statusName);
121+
}
122+
123+
return '';
124+
}
125+
</script>
126+
127+
<!-- TODO: コンテスト種別のボタンの並び順を決める -->
128+
<!-- See: -->
129+
<!-- https://flowbite-svelte.com/docs/components/button-group -->
130+
<ButtonGroup class="m-4 contents-center" aria-label="Contest filter options">
131+
<Button
132+
on:click={() => filterTaskResultsByContestType(taskResults, fromABC212_Onwards)}
133+
aria-label="Filter contests from ABC212 onwards"
134+
>
135+
ABC212〜
136+
</Button>
137+
</ButtonGroup>
138+
139+
<!-- TODO: コンテスト種別に応じて動的に変更できるようにする -->
140+
<Heading tag="h2" class="text-2xl pb-3 text-gray-900 dark:text-white">
141+
{'AtCoder Beginners Contest 212 〜'}
142+
</Heading>
143+
144+
<!-- TODO: ページネーションを実装 -->
145+
<!-- TODO: ページネーションライブラリを導入するには、Svelte v4 から v5 へのアップデートが必要 -->
146+
<!-- See: -->
147+
<!-- https://github.com/kenkoooo/AtCoderProblems/blob/master/atcoder-problems-frontend/src/pages/TablePage/AtCoderRegularTable.tsx -->
148+
<!-- https://github.com/birdou/atcoder-blogs/blob/main/app/atcoder-blogs-frontend/src/pages/BlogTablePage/BlogTablePage.tsx -->
149+
<div class="container w-full overflow-auto border rounded-md">
150+
<Table shadow id="task-table" class="text-md table-fixed" aria-label="Task table">
151+
<TableHead class="text-sm bg-gray-100">
152+
<TableHeadCell class="w-full xl:w-16 px-2 text-center border" scope="col">Round</TableHeadCell
153+
>
154+
155+
{#if taskTableIndices.length}
156+
{#each taskTableIndices as taskTableIndex}
157+
<TableHeadCell class="text-center border" scope="col">{taskTableIndex}</TableHeadCell>
158+
{/each}
159+
{/if}
160+
</TableHead>
161+
162+
<TableBody tableBodyClass="divide-y">
163+
{#if contestIds.length && taskTableIndices.length}
164+
{#each contestIds as contestId}
165+
<TableBodyRow class="flex flex-wrap xl:table-row">
166+
<TableBodyCell class="w-full xl:w-16 truncate px-2 py-2 text-center border">
167+
<!-- FIXME: コンテスト種別に合わせて修正できるようにする -->
168+
{getContestNameLabelForTaskTable(contestId)}
169+
</TableBodyCell>
170+
171+
{#each taskTableIndices as taskIndex}
172+
<TableBodyCell
173+
key={contestId + '-' + taskIndex}
174+
class={getBodyCellClasses(contestId, taskIndex)}
175+
>
176+
{#if taskTable[contestId][taskIndex]}
177+
<TaskTableBodyCell
178+
taskResult={taskTable[contestId][taskIndex]}
179+
{isLoggedIn}
180+
{updatingModal}
181+
/>
182+
{/if}
183+
</TableBodyCell>
184+
{/each}
185+
</TableBodyRow>
186+
{/each}
187+
{/if}
188+
</TableBody>
189+
</Table>
190+
</div>
191+
192+
<UpdatingModal bind:this={updatingModal} {isLoggedIn} />
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<script lang="ts">
2+
import type { TaskResult } from '$lib/types/task';
3+
4+
import ExternalLinkWrapper from '$lib/components/ExternalLinkWrapper.svelte';
5+
import GradeLabel from '$lib/components/GradeLabel.svelte';
6+
import IconForUpdating from '$lib/components/SubmissionStatus/IconForUpdating.svelte';
7+
import UpdatingModal from '$lib/components/SubmissionStatus/UpdatingModal.svelte';
8+
9+
import { getTaskUrl } from '$lib/utils/task';
10+
11+
export let taskResult: TaskResult;
12+
export let isLoggedIn: boolean;
13+
export let updatingModal: UpdatingModal;
14+
</script>
15+
16+
<!-- Task title and an external link -->
17+
<div class="text-left text-md sm:text-lg">
18+
<ExternalLinkWrapper
19+
url={getTaskUrl(taskResult.contest_id, taskResult.task_id)}
20+
description={taskResult.title}
21+
textSize="xs:text-md"
22+
textColorInDarkMode="dark:text-gray-300"
23+
textOverflow="min-w-[60px] max-w-[120px]"
24+
iconSize={0}
25+
/>
26+
</div>
27+
28+
<div class="flex items-center justify-between py-1">
29+
<!-- Task grade -->
30+
<GradeLabel taskGrade={taskResult.grade} defaultPadding={0.25} defaultWidth={8} />
31+
32+
<!-- Submission updater and links of task detail page -->
33+
<button
34+
type="button"
35+
class="mx-2 w-8 text-center"
36+
on:click={() => updatingModal.openModal(taskResult)}
37+
aria-label="Update submission for {taskResult.title}"
38+
>
39+
<IconForUpdating {isLoggedIn} />
40+
</button>
41+
42+
<!-- TODO: Add link of detailed page. -->
43+
<div class="flex-1 text-center text-sm dark:text-gray-300 max-w-[32px]">
44+
{'詳細'}
45+
</div>
46+
</div>

src/lib/utils/task.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { UrlGenerator, UrlGenerators } from '$lib/types/url';
2+
import { ContestType } from '$lib/types/contest';
23
import { type TaskResult, type TaskResults, TaskGrade, type TaskGrades } from '$lib/types/task';
34
import { type WorkBookTaskBase } from '$lib/types/workbook';
45
import { ATCODER_BASE_CONTEST_URL, AOJ_TASKS_URL } from '$lib/constants/urls';
@@ -92,6 +93,18 @@ export function compareByContestIdAndTaskId(first: TaskResult, second: TaskResul
9293
return first.task_table_index.localeCompare(second.task_table_index);
9394
}
9495

96+
// See:
97+
// https://github.com/kenkoooo/AtCoderProblems/blob/master/atcoder-problems-frontend/src/pages/TablePage/AtCoderRegularTable.tsx
98+
export const getTaskTableHeaderName = (contestType: ContestType, taskResult: TaskResult) => {
99+
if (contestType === ContestType.ABC && taskResult.task_table_index === 'H') {
100+
return 'H/Ex';
101+
} else if (taskResult.task_table_index === 'Ex') {
102+
return 'H/Ex';
103+
}
104+
105+
return taskResult.task_table_index;
106+
};
107+
95108
// 問題一覧や問題集の詳細ページでは、AtCoder ProblemsのAPIから取得したタイトルからプレフィックス(A., B., ..., G. など)を非表示にする
96109
// 理由: 問題を解くときに、プレフィックスからの先入観を受けないようにするため
97110
// その他: プレフィックスは、同じテーブルの出典に記載する

src/routes/problems/+page.svelte

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
88
import HeadingOne from '$lib/components/HeadingOne.svelte';
99
import TabItemWrapper from '$lib/components/TabItemWrapper.svelte';
10+
import TaskTable from '$lib/components/TaskTables/TaskTable.svelte';
1011
import TaskGradeList from '$lib/components/TaskGradeList.svelte';
1112
import GradeGuidelineTable from '$lib/components/TaskGrades/GradeGuidelineTable.svelte';
1213
@@ -25,8 +26,16 @@
2526
<!-- See: -->
2627
<!-- https://flowbite-svelte.com/docs/components/tabs -->
2728
<Tabs tabStyle="underline" contentClass="bg-white dark:bg-gray-800">
29+
<!-- Task table -->
30+
<!-- WIP: UIのデザインが試行錯誤の段階であるため、管理者のみ閲覧可能 -->
31+
{#if isAdmin}
32+
<TabItemWrapper workbookType={null} isOpen={true} title="テーブル">
33+
<TaskTable {taskResults} {isLoggedIn} />
34+
</TabItemWrapper>
35+
{/if}
36+
2837
<!-- Grades -->
29-
<TabItemWrapper workbookType={null} isOpen={true} title="グレード">
38+
<TabItemWrapper workbookType={null} title="グレード">
3039
<TaskGradeList {taskResults} {isAdmin} {isLoggedIn}></TaskGradeList>
3140
</TabItemWrapper>
3241

@@ -45,12 +54,5 @@
4554
<!-- <TabItemWrapper title="Latest">
4655
<div class="m-4">Comming Soon.</div>
4756
</TabItemWrapper> -->
48-
49-
<!-- Table -->
50-
<!-- TODO: コンテスト種類をトグルボタンで切り替えられるようにする -->
51-
<!-- <TabItemWrapper title="Table"> -->
52-
<!-- <TaskTable {taskResults} /> -->
53-
<!-- <div class="m-4">Comming Soon.</div>
54-
</TabItemWrapper> -->
5557
</Tabs>
5658
</div>

0 commit comments

Comments
 (0)