Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
ca6c8ac
✨ Add task table tab for ABC212 - (#1611)
KATO-Hiro Dec 30, 2024
15f9e7d
✨ Select ABC212 - tasks (#1611)
KATO-Hiro Dec 30, 2024
abd8767
♻️ Rename (#1611)
KATO-Hiro Dec 30, 2024
4416a23
:bug: Fix conflict (#1611)
KATO-Hiro Dec 30, 2024
630892e
:books: Add future tasks to comment (#1611)
KATO-Hiro Dec 30, 2024
04f6fe7
:books: Add future tasks to comment (#1611)
KATO-Hiro Dec 30, 2024
013db18
✨ Show task title in task table (#1611)
KATO-Hiro Dec 30, 2024
9f14afe
:art: Show external links and add styles (#1611)
KATO-Hiro Dec 30, 2024
2063e77
:art: Add components and styles (#1611)
KATO-Hiro Dec 30, 2024
09b753b
Merge branch 'staging' of github.com:KATO-Hiro/AtCoderNoviceProblemsS…
KATO-Hiro Dec 30, 2024
9403c08
✨ Add textOverflow and iconSize (#1611)
KATO-Hiro Dec 31, 2024
dec29c5
:art: Add and update styles (#1611)
KATO-Hiro Dec 31, 2024
fd9d7f0
:art: Enable to hide icon (#1611)
KATO-Hiro Dec 31, 2024
2f36609
:art: Add submission status icon and colors (#1611)
KATO-Hiro Dec 31, 2024
a7218b9
✨ Add helper method (#1611)
KATO-Hiro Dec 31, 2024
2969d75
:art: Add and update styles (#1611)
KATO-Hiro Dec 31, 2024
1213b78
Merge branch 'staging' of github.com:KATO-Hiro/AtCoderNoviceProblemsS…
KATO-Hiro Jan 1, 2025
f654aea
:art: Fix title position and size (#1611)
KATO-Hiro Jan 1, 2025
026409a
:art: Fix icon size (#1611)
KATO-Hiro Jan 1, 2025
4f96b9d
♻️ Split image and text (#1611)
KATO-Hiro Jan 1, 2025
47aff4b
:art: Fix layout (#1611)
KATO-Hiro Jan 1, 2025
6923c67
✨ Enable to update submission status from task table (#1611)
KATO-Hiro Jan 1, 2025
a34c7d5
♻️ Extract component (#1611)
KATO-Hiro Jan 2, 2025
dd7c19a
:art: Improve layout (#1611)
KATO-Hiro Jan 3, 2025
0fb55e0
:bug: Fix lint error (#1611)
KATO-Hiro Jan 3, 2025
c05abd8
:art: Improve accessibility (#1611)
KATO-Hiro Jan 3, 2025
a40715d
♻️ Extract method (#1611)
KATO-Hiro Jan 3, 2025
d45bd4d
:art: Improve styles (#1611)
KATO-Hiro Jan 3, 2025
a88893a
📖 Add docs (#1611)
KATO-Hiro Jan 3, 2025
0ea88fb
🚨 Add tests for task table header name (#1611)
KATO-Hiro Jan 3, 2025
cc8845c
📖 Add and update docs (#1611)
KATO-Hiro Jan 3, 2025
37c1018
♻️ Extract method (#1611)
KATO-Hiro Jan 3, 2025
da519fa
📖 Update docs (#1611)
KATO-Hiro Jan 3, 2025
f6f55ba
:art: Add accessibility attributes (#1611)
KATO-Hiro Jan 3, 2025
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
8 changes: 6 additions & 2 deletions src/lib/components/ExternalLinkWrapper.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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;
</script>

<a
Expand All @@ -13,10 +15,12 @@
target="_blank"
rel="noreferrer"
>
{description}
<div class="{textOverflow} truncate">
{description}
</div>

<div class="ml-1.5">
<ExternalLinkIcon size="w-4 h-4" />
<ExternalLinkIcon size="w-{iconSize} h-{iconSize}" />
</div>
</a>

Expand Down
4 changes: 3 additions & 1 deletion src/lib/components/GradeLabel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@
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);
</script>

<div class="rounded-lg border-2 border-white">
<div
class="p-1 w-8 xs:w-10 text-sm xs:text-md text-center rounded-md {toWhiteTextIfNeeds(
class="p-{defaultPadding} w-8 xs:w-{defaultWidth} text-sm xs:text-md text-center rounded-md {toWhiteTextIfNeeds(
grade,
)} {gradeColor}"
>
Expand Down
18 changes: 18 additions & 0 deletions src/lib/components/SubmissionStatus/IconForUpdating.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script lang="ts">
// @ts-ignore
import ChevronDownOutline from 'flowbite-svelte-icons/ChevronDownOutline.svelte';

export let isLoggedIn: boolean;
</script>

<!-- HACK: 以下のコンポーネントと類似しているが、差分が大きいため別コンポーネントとして用意 -->
<!-- src/lib/components/SubmissionStatus/SubmissionStatusImage.svelte -->
{#if isLoggedIn}
<div class="flex items-center justify-center text-sm">
<div class="dark:text-gray-300">
{'更新'}
</div>

<ChevronDownOutline class="w-4 h-4 text-primary-600 dark:text-gray-300 inline" />
</div>
{/if}
192 changes: 192 additions & 0 deletions src/lib/components/TaskTables/TaskTable.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
<script lang="ts">
import {
Heading,
ButtonGroup,
Button,
Table,
TableBody,
TableBodyCell,
TableBodyRow,
TableHead,
TableHeadCell,
} from 'flowbite-svelte';

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

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

import { classifyContest, getContestNameLabel } from '$lib/utils/contest';
import { getTaskTableHeaderName } from '$lib/utils/task';
import { getBackgroundColorFrom } from '$lib/services/submission_status';

export let taskResults: TaskResults;
export let isLoggedIn: boolean;

let selectedTaskResults: TaskResults;
let contestIds: Array<string>;
let taskTableIndices: Array<string>;
let taskTable: Record<string, Record<string, TaskResult>>;
let updatingModal: UpdatingModal;

// TODO: 任意のコンテスト種別に拡張
$: selectedTaskResults = filterTaskResultsByContestType(taskResults, fromABC212_Onwards);
$: contestIds = getContestIds(selectedTaskResults);
$: taskTableIndices = getTaskTableIndices(selectedTaskResults, ContestType.ABC);
$: taskTable = prepareTaskTable(selectedTaskResults, ContestType.ABC);

function filterTaskResultsByContestType(
taskResults: TaskResults,
condition: (taskResult: TaskResult) => boolean,
): TaskResults {
return taskResults.filter(condition);
}

// Note:
// Before and from ABC212 onwards, the number and tendency of tasks are very different.
const fromABC212_Onwards = (taskResult: TaskResult) =>
classifyContest(taskResult.contest_id) === ContestType.ABC && taskResult.contest_id >= 'abc212';

function getContestIds(selectedTaskResults: TaskResults): Array<string> {
const contestList = selectedTaskResults.map((taskResult: TaskResult) => taskResult.contest_id);
return Array.from(new Set(contestList)).sort().reverse();
}

function getTaskTableIndices(
selectedTaskResults: TaskResults,
selectedContestType: ContestType,
): Array<string> {
const headerList = selectedTaskResults.map((taskResult: TaskResult) =>
getTaskTableHeaderName(selectedContestType, taskResult),
);
return Array.from(new Set(headerList)).sort();
}

/**
* Prepare a table for task and submission statuses.
*
* Computational complexity of preparation table: O(N), where N is the number of task results.
* Computational complexity of accessing table: O(1).
*
* @param selectedTaskResults Task results to be shown in the table.
* @param selectedContestType Contest type of the task results.
* @returns A table for task and submission statuses.
*/
function prepareTaskTable(
selectedTaskResults: TaskResults,
selectedContestType: ContestType,
): Record<string, Record<string, TaskResult>> {
const table: Record<string, Record<string, TaskResult>> = {};

selectedTaskResults.forEach((taskResult: TaskResult) => {
const contestId = taskResult.contest_id;
const taskTableIndex = getTaskTableHeaderName(selectedContestType, taskResult);

if (!table[contestId]) {
table[contestId] = {};
}

table[contestId][taskTableIndex] = taskResult;
});

return table;
}

function getContestNameLabelForTaskTable(contestId: string): string {
let contestNameLabel = getContestNameLabel(contestId);
const contestType = classifyContest(contestId);

switch (contestType) {
case ContestType.ABC:
return contestNameLabel.replace('ABC ', '');
// TODO: Add cases for other contest types.
default:
return contestNameLabel;
}
}

function getBodyCellClasses(contestId: string, taskIndex: string): string {
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';
const backgroundColor = getBackgroundColor(taskTable[contestId][taskIndex]);

return `${baseClasses} ${backgroundColor}`;
}

function getBackgroundColor(taskResult: TaskResult): string {
const statusName = taskResult?.status_name;

if (taskResult && statusName !== 'ns') {
return getBackgroundColorFrom(statusName);
}

return '';
}
</script>

<!-- TODO: コンテスト種別のボタンの並び順を決める -->
<!-- See: -->
<!-- https://flowbite-svelte.com/docs/components/button-group -->
<ButtonGroup class="m-4 contents-center" aria-label="Contest filter options">
<Button
on:click={() => filterTaskResultsByContestType(taskResults, fromABC212_Onwards)}
aria-label="Filter contests from ABC212 onwards"
>
ABC212〜
</Button>
</ButtonGroup>

<!-- TODO: コンテスト種別に応じて動的に変更できるようにする -->
<Heading tag="h2" class="text-2xl pb-3 text-gray-900 dark:text-white">
{'AtCoder Beginners Contest 212 〜'}
</Heading>

<!-- TODO: ページネーションを実装 -->
<!-- TODO: ページネーションライブラリを導入するには、Svelte v4 から v5 へのアップデートが必要 -->
<!-- See: -->
<!-- https://github.com/kenkoooo/AtCoderProblems/blob/master/atcoder-problems-frontend/src/pages/TablePage/AtCoderRegularTable.tsx -->
<!-- https://github.com/birdou/atcoder-blogs/blob/main/app/atcoder-blogs-frontend/src/pages/BlogTablePage/BlogTablePage.tsx -->
<div class="container w-full overflow-auto border rounded-md">
<Table shadow id="task-table" class="text-md table-fixed" aria-label="Task table">
<TableHead class="text-sm bg-gray-100">
<TableHeadCell class="w-full xl:w-16 px-2 text-center border" scope="col">Round</TableHeadCell
>

{#if taskTableIndices.length}
{#each taskTableIndices as taskTableIndex}
<TableHeadCell class="text-center border" scope="col">{taskTableIndex}</TableHeadCell>
{/each}
{/if}
</TableHead>

<TableBody tableBodyClass="divide-y">
{#if contestIds.length && taskTableIndices.length}
{#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">
<!-- FIXME: コンテスト種別に合わせて修正できるようにする -->
{getContestNameLabelForTaskTable(contestId)}
</TableBodyCell>

{#each taskTableIndices as taskIndex}
<TableBodyCell
key={contestId + '-' + taskIndex}
class={getBodyCellClasses(contestId, taskIndex)}
>
{#if taskTable[contestId][taskIndex]}
<TaskTableBodyCell
taskResult={taskTable[contestId][taskIndex]}
{isLoggedIn}
{updatingModal}
/>
{/if}
</TableBodyCell>
{/each}
</TableBodyRow>
{/each}
{/if}
</TableBody>
</Table>
</div>

<UpdatingModal bind:this={updatingModal} {isLoggedIn} />
46 changes: 46 additions & 0 deletions src/lib/components/TaskTables/TaskTableBodyCell.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<script lang="ts">
import type { TaskResult } from '$lib/types/task';

import ExternalLinkWrapper from '$lib/components/ExternalLinkWrapper.svelte';
import GradeLabel from '$lib/components/GradeLabel.svelte';
import IconForUpdating from '$lib/components/SubmissionStatus/IconForUpdating.svelte';
import UpdatingModal from '$lib/components/SubmissionStatus/UpdatingModal.svelte';

import { getTaskUrl } from '$lib/utils/task';

export let taskResult: TaskResult;
export let isLoggedIn: boolean;
export let updatingModal: UpdatingModal;
</script>

<!-- Task title and an external link -->
<div class="text-left text-md sm:text-lg">
<ExternalLinkWrapper
url={getTaskUrl(taskResult.contest_id, taskResult.task_id)}
description={taskResult.title}
textSize="xs:text-md"
textColorInDarkMode="dark:text-gray-300"
textOverflow="min-w-[60px] max-w-[120px]"
iconSize={0}
/>
</div>

<div class="flex items-center justify-between py-1">
<!-- Task grade -->
<GradeLabel taskGrade={taskResult.grade} defaultPadding={0.25} defaultWidth={8} />

<!-- Submission updater and links of task detail page -->
<button
type="button"
class="mx-2 w-8 text-center"
on:click={() => updatingModal.openModal(taskResult)}
aria-label="Update submission for {taskResult.title}"
>
<IconForUpdating {isLoggedIn} />
</button>

<!-- TODO: Add link of detailed page. -->
<div class="flex-1 text-center text-sm dark:text-gray-300 max-w-[32px]">
{'詳細'}
</div>
</div>
13 changes: 13 additions & 0 deletions src/lib/utils/task.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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. など)を非表示にする
// 理由: 問題を解くときに、プレフィックスからの先入観を受けないようにするため
// その他: プレフィックスは、同じテーブルの出典に記載する
Expand Down
18 changes: 10 additions & 8 deletions src/routes/problems/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -25,8 +26,16 @@
<!-- See: -->
<!-- https://flowbite-svelte.com/docs/components/tabs -->
<Tabs tabStyle="underline" contentClass="bg-white dark:bg-gray-800">
<!-- Task table -->
<!-- WIP: UIのデザインが試行錯誤の段階であるため、管理者のみ閲覧可能 -->
{#if isAdmin}
<TabItemWrapper workbookType={null} isOpen={true} title="テーブル">
<TaskTable {taskResults} {isLoggedIn} />
</TabItemWrapper>
{/if}

<!-- Grades -->
<TabItemWrapper workbookType={null} isOpen={true} title="グレード">
<TabItemWrapper workbookType={null} title="グレード">
<TaskGradeList {taskResults} {isAdmin} {isLoggedIn}></TaskGradeList>
</TabItemWrapper>

Expand All @@ -45,12 +54,5 @@
<!-- <TabItemWrapper title="Latest">
<div class="m-4">Comming Soon.</div>
</TabItemWrapper> -->

<!-- Table -->
<!-- TODO: コンテスト種類をトグルボタンで切り替えられるようにする -->
<!-- <TabItemWrapper title="Table"> -->
<!-- <TaskTable {taskResults} /> -->
<!-- <div class="m-4">Comming Soon.</div>
</TabItemWrapper> -->
</Tabs>
</div>
Loading
Loading