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
23 changes: 18 additions & 5 deletions src/lib/components/TabItemWrapper.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@

import { WorkBookType } from '$lib/types/workbook';
import { activeWorkbookTabStore } from '$lib/stores/active_workbook_tab';
import {
activeProblemListTabStore,
type ActiveProblemListTab,
} from '$lib/stores/active_problem_list_tab.svelte';

import { TOOLTIP_CLASS_BASE } from '$lib/constants/tailwind-helper';

interface Props {
workbookType: WorkBookType | null;
workbookType?: WorkBookType | null;
activeProblemList?: ActiveProblemListTab | null;
isOpen?: boolean;
title: string;
tooltipContent?: string;
Expand All @@ -19,6 +24,7 @@

let {
workbookType = null,
activeProblemList = null,
isOpen = false,
title,
tooltipContent = '',
Expand All @@ -31,10 +37,17 @@
titleId = `title-${Math.floor(Math.random() * 10000)}`;
});

function handleClick(workBookType: WorkBookType | null): void {
if (workBookType === null) return;
function handleClick(
workBookType: WorkBookType | null,
activeProblemList: ActiveProblemListTab | null,
): void {
if (workBookType !== null) {
activeWorkbookTabStore.setActiveWorkbookTab(workBookType);
}

activeWorkbookTabStore.setActiveWorkbookTab(workBookType);
if (activeProblemList !== null) {
activeProblemListTabStore.set(activeProblemList);
}
}
</script>

Expand All @@ -54,7 +67,7 @@

<!-- See: -->
<!-- https://svelte-5-ui-lib.codewithshin.com/components/tabs -->
<TabItem open={isOpen} onclick={() => handleClick(workbookType)}>
<TabItem open={isOpen} onclick={() => handleClick(workbookType, activeProblemList)}>
{#snippet titleSlot()}
<span class="text-lg" id={titleId}>
<div class="flex items-center space-x-2">
Expand Down
52 changes: 52 additions & 0 deletions src/lib/stores/active_problem_list_tab.svelte.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
export type ActiveProblemListTab = 'contestTable' | 'listByGrade' | 'gradeGuidelineTable';

export class ActiveProblemListTabStore {
value = $state<ActiveProblemListTab>('listByGrade');

/**
* Creates an instance with the specified problem list tab.
*
* @param activeTab - The default problem list tab to initialize.
* Defaults to 'listByGrade'.
*/
constructor(activeTab: ActiveProblemListTab = 'listByGrade') {
this.value = activeTab;
}

/**
* Gets the current active tab.
*
* @returns The current active tab.
*/
get(): ActiveProblemListTab {
return this.value;
}

/**
* Sets the current tab to the specified value.
*
* @param activeTab - The active tab to set as the current value
*/
set(activeTab: ActiveProblemListTab): void {
this.value = activeTab;
}

/**
* Validates if the current tab matches the task list.
* @param activeTab - The active tab to compare against
* @returns `true` if the active tab matches the task list, `false` otherwise
*/
isSame(activeTab: ActiveProblemListTab): boolean {
return this.value === activeTab;
}

/**
* Resets the active tab to the default value.
* Sets the internal value to 'listByGrade'.
*/
reset(): void {
this.value = 'listByGrade';
}
}

export const activeProblemListTabStore = new ActiveProblemListTabStore();
49 changes: 33 additions & 16 deletions src/routes/problems/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<script lang="ts">
import type { Snippet } from 'svelte';

import { Tabs } from 'svelte-5-ui-lib';

import type { TaskResults } from '$lib/types/task';
Expand All @@ -11,12 +13,21 @@
import TaskGradeList from '$lib/components/TaskGradeList.svelte';
import GradeGuidelineTable from '$lib/components/TaskGrades/GradeGuidelineTable.svelte';

import {
activeProblemListTabStore,
type ActiveProblemListTab,
} from '$lib/stores/active_problem_list_tab.svelte';

let { data } = $props();

let taskResults: TaskResults = $derived(data.taskResults.sort(compareByContestIdAndTaskId));

let isAdmin: boolean = data.isAdmin;
let isLoggedIn: boolean = data.isLoggedIn;

function isActiveTab(currentTab: ActiveProblemListTab): boolean {
return currentTab === activeProblemListTabStore.get();
}
</script>

<!-- TODO: Searchを追加 -->
Expand All @@ -26,29 +37,35 @@
<!-- See: -->
<!-- https://flowbite-svelte.com/docs/components/tabs -->
<Tabs tabStyle="underline" contentClass="bg-white dark:bg-gray-800 mt-0 p-0">
<!-- Task table -->
<!-- Contest table -->
<!-- WIP: UIのデザインが試行錯誤の段階であるため、管理者のみ閲覧可能 -->
<!-- TODO: 一般公開するときに、デフォルトで開くタブにする -->
{#if isAdmin}
<TabItemWrapper workbookType={null} title="テーブル">
<TaskTable {taskResults} {isLoggedIn} />
</TabItemWrapper>
{@render problemListTab('テーブル', 'contestTable', contestTable)}
{/if}

<!-- Grades -->
<TabItemWrapper workbookType={null} isOpen={true} title="グレード">
<TaskGradeList {taskResults} {isAdmin} {isLoggedIn}></TaskGradeList>
</TabItemWrapper>
{@render problemListTab('グレード', 'listByGrade', listByGrade)}

<!-- Grade guidelines -->
<TabItemWrapper workbookType={null} title="グレードの目安">
<GradeGuidelineTable />
</TabItemWrapper>

<!-- HACK: 以下、各テーブルを実装するまで非表示 -->
<!-- Tags -->
<!-- <TabItemWrapper title="Tags">
<div class="m-4">Comming Soon.</div>
</TabItemWrapper> -->
{@render problemListTab('グレードの目安', 'gradeGuidelineTable', gradeGuidelineTable)}
</Tabs>
</div>

{#snippet problemListTab(title: string, tab: ActiveProblemListTab, children: Snippet)}
<TabItemWrapper {title} activeProblemList={tab} isOpen={isActiveTab(tab)}>
{@render children()}
</TabItemWrapper>
{/snippet}

{#snippet contestTable()}
<TaskTable {taskResults} {isLoggedIn} />
{/snippet}

{#snippet listByGrade()}
<TaskGradeList {taskResults} {isAdmin} {isLoggedIn}></TaskGradeList>
{/snippet}

{#snippet gradeGuidelineTable()}
<GradeGuidelineTable />
{/snippet}
61 changes: 61 additions & 0 deletions src/test/lib/stores/active_problem_list_tab.svelte.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { describe, test, expect, beforeEach } from 'vitest';

import { ActiveProblemListTabStore } from '$lib/stores/active_problem_list_tab.svelte';

describe('ActiveProblemListTabStore', () => {
let store: ActiveProblemListTabStore;

beforeEach(() => {
store = new ActiveProblemListTabStore();
});

describe('constructor', () => {
test('expects to initialize with default value', () => {
expect(store.get()).toBe('listByGrade');
});

test('expects to initialize with provided value', () => {
const customStore = new ActiveProblemListTabStore('contestTable');
expect(customStore.get()).toBe('contestTable');
});
});

describe('get', () => {
test('expects to return the current active tab', () => {
expect(store.get()).toBe('listByGrade');
});
});

describe('set', () => {
test('expects to update the active tab value', () => {
store.set('contestTable');
expect(store.get()).toBe('contestTable');

store.set('gradeGuidelineTable');
expect(store.get()).toBe('gradeGuidelineTable');
});
});

describe('isSame', () => {
test('expects to return true when active tab matches the argument', () => {
store.set('contestTable');
expect(store.isSame('contestTable')).toBe(true);
});

test('expects to return false when active tab does not match the argument', () => {
store.set('contestTable');
expect(store.isSame('listByGrade')).toBe(false);
expect(store.isSame('gradeGuidelineTable')).toBe(false);
});
});

describe('reset', () => {
test('expects to reset the active tab to the default value', () => {
store.set('contestTable');
expect(store.get()).toBe('contestTable');

store.reset();
expect(store.get()).toBe('listByGrade');
});
});
});
Loading