Skip to content

Commit fc548de

Browse files
authored
Merge pull request #1855 from AtCoder-NoviSteps/#1854
🎨 Save active problem list or table (#1854)
2 parents 896ebc4 + 2214dd0 commit fc548de

File tree

4 files changed

+164
-21
lines changed

4 files changed

+164
-21
lines changed

src/lib/components/TabItemWrapper.svelte

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,16 @@
66
77
import { WorkBookType } from '$lib/types/workbook';
88
import { activeWorkbookTabStore } from '$lib/stores/active_workbook_tab';
9+
import {
10+
activeProblemListTabStore,
11+
type ActiveProblemListTab,
12+
} from '$lib/stores/active_problem_list_tab.svelte';
913
1014
import { TOOLTIP_CLASS_BASE } from '$lib/constants/tailwind-helper';
1115
1216
interface Props {
13-
workbookType: WorkBookType | null;
17+
workbookType?: WorkBookType | null;
18+
activeProblemList?: ActiveProblemListTab | null;
1419
isOpen?: boolean;
1520
title: string;
1621
tooltipContent?: string;
@@ -19,6 +24,7 @@
1924
2025
let {
2126
workbookType = null,
27+
activeProblemList = null,
2228
isOpen = false,
2329
title,
2430
tooltipContent = '',
@@ -31,10 +37,17 @@
3137
titleId = `title-${Math.floor(Math.random() * 10000)}`;
3238
});
3339
34-
function handleClick(workBookType: WorkBookType | null): void {
35-
if (workBookType === null) return;
40+
function handleClick(
41+
workBookType: WorkBookType | null,
42+
activeProblemList: ActiveProblemListTab | null,
43+
): void {
44+
if (workBookType !== null) {
45+
activeWorkbookTabStore.setActiveWorkbookTab(workBookType);
46+
}
3647
37-
activeWorkbookTabStore.setActiveWorkbookTab(workBookType);
48+
if (activeProblemList !== null) {
49+
activeProblemListTabStore.set(activeProblemList);
50+
}
3851
}
3952
</script>
4053

@@ -54,7 +67,7 @@
5467

5568
<!-- See: -->
5669
<!-- https://svelte-5-ui-lib.codewithshin.com/components/tabs -->
57-
<TabItem open={isOpen} onclick={() => handleClick(workbookType)}>
70+
<TabItem open={isOpen} onclick={() => handleClick(workbookType, activeProblemList)}>
5871
{#snippet titleSlot()}
5972
<span class="text-lg" id={titleId}>
6073
<div class="flex items-center space-x-2">
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
export type ActiveProblemListTab = 'contestTable' | 'listByGrade' | 'gradeGuidelineTable';
2+
3+
export class ActiveProblemListTabStore {
4+
value = $state<ActiveProblemListTab>('listByGrade');
5+
6+
/**
7+
* Creates an instance with the specified problem list tab.
8+
*
9+
* @param activeTab - The default problem list tab to initialize.
10+
* Defaults to 'listByGrade'.
11+
*/
12+
constructor(activeTab: ActiveProblemListTab = 'listByGrade') {
13+
this.value = activeTab;
14+
}
15+
16+
/**
17+
* Gets the current active tab.
18+
*
19+
* @returns The current active tab.
20+
*/
21+
get(): ActiveProblemListTab {
22+
return this.value;
23+
}
24+
25+
/**
26+
* Sets the current tab to the specified value.
27+
*
28+
* @param activeTab - The active tab to set as the current value
29+
*/
30+
set(activeTab: ActiveProblemListTab): void {
31+
this.value = activeTab;
32+
}
33+
34+
/**
35+
* Validates if the current tab matches the task list.
36+
* @param activeTab - The active tab to compare against
37+
* @returns `true` if the active tab matches the task list, `false` otherwise
38+
*/
39+
isSame(activeTab: ActiveProblemListTab): boolean {
40+
return this.value === activeTab;
41+
}
42+
43+
/**
44+
* Resets the active tab to the default value.
45+
* Sets the internal value to 'listByGrade'.
46+
*/
47+
reset(): void {
48+
this.value = 'listByGrade';
49+
}
50+
}
51+
52+
export const activeProblemListTabStore = new ActiveProblemListTabStore();

src/routes/problems/+page.svelte

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
<script lang="ts">
2+
import type { Snippet } from 'svelte';
3+
24
import { Tabs } from 'svelte-5-ui-lib';
35
46
import type { TaskResults } from '$lib/types/task';
@@ -11,12 +13,21 @@
1113
import TaskGradeList from '$lib/components/TaskGradeList.svelte';
1214
import GradeGuidelineTable from '$lib/components/TaskGrades/GradeGuidelineTable.svelte';
1315
16+
import {
17+
activeProblemListTabStore,
18+
type ActiveProblemListTab,
19+
} from '$lib/stores/active_problem_list_tab.svelte';
20+
1421
let { data } = $props();
1522
1623
let taskResults: TaskResults = $derived(data.taskResults.sort(compareByContestIdAndTaskId));
1724
1825
let isAdmin: boolean = data.isAdmin;
1926
let isLoggedIn: boolean = data.isLoggedIn;
27+
28+
function isActiveTab(currentTab: ActiveProblemListTab): boolean {
29+
return currentTab === activeProblemListTabStore.get();
30+
}
2031
</script>
2132

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

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

4350
<!-- Grade guidelines -->
44-
<TabItemWrapper workbookType={null} title="グレードの目安">
45-
<GradeGuidelineTable />
46-
</TabItemWrapper>
47-
48-
<!-- HACK: 以下、各テーブルを実装するまで非表示 -->
49-
<!-- Tags -->
50-
<!-- <TabItemWrapper title="Tags">
51-
<div class="m-4">Comming Soon.</div>
52-
</TabItemWrapper> -->
51+
{@render problemListTab('グレードの目安', 'gradeGuidelineTable', gradeGuidelineTable)}
5352
</Tabs>
5453
</div>
54+
55+
{#snippet problemListTab(title: string, tab: ActiveProblemListTab, children: Snippet)}
56+
<TabItemWrapper {title} activeProblemList={tab} isOpen={isActiveTab(tab)}>
57+
{@render children()}
58+
</TabItemWrapper>
59+
{/snippet}
60+
61+
{#snippet contestTable()}
62+
<TaskTable {taskResults} {isLoggedIn} />
63+
{/snippet}
64+
65+
{#snippet listByGrade()}
66+
<TaskGradeList {taskResults} {isAdmin} {isLoggedIn}></TaskGradeList>
67+
{/snippet}
68+
69+
{#snippet gradeGuidelineTable()}
70+
<GradeGuidelineTable />
71+
{/snippet}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { describe, test, expect, beforeEach } from 'vitest';
2+
3+
import { ActiveProblemListTabStore } from '$lib/stores/active_problem_list_tab.svelte';
4+
5+
describe('ActiveProblemListTabStore', () => {
6+
let store: ActiveProblemListTabStore;
7+
8+
beforeEach(() => {
9+
store = new ActiveProblemListTabStore();
10+
});
11+
12+
describe('constructor', () => {
13+
test('expects to initialize with default value', () => {
14+
expect(store.get()).toBe('listByGrade');
15+
});
16+
17+
test('expects to initialize with provided value', () => {
18+
const customStore = new ActiveProblemListTabStore('contestTable');
19+
expect(customStore.get()).toBe('contestTable');
20+
});
21+
});
22+
23+
describe('get', () => {
24+
test('expects to return the current active tab', () => {
25+
expect(store.get()).toBe('listByGrade');
26+
});
27+
});
28+
29+
describe('set', () => {
30+
test('expects to update the active tab value', () => {
31+
store.set('contestTable');
32+
expect(store.get()).toBe('contestTable');
33+
34+
store.set('gradeGuidelineTable');
35+
expect(store.get()).toBe('gradeGuidelineTable');
36+
});
37+
});
38+
39+
describe('isSame', () => {
40+
test('expects to return true when active tab matches the argument', () => {
41+
store.set('contestTable');
42+
expect(store.isSame('contestTable')).toBe(true);
43+
});
44+
45+
test('expects to return false when active tab does not match the argument', () => {
46+
store.set('contestTable');
47+
expect(store.isSame('listByGrade')).toBe(false);
48+
expect(store.isSame('gradeGuidelineTable')).toBe(false);
49+
});
50+
});
51+
52+
describe('reset', () => {
53+
test('expects to reset the active tab to the default value', () => {
54+
store.set('contestTable');
55+
expect(store.get()).toBe('contestTable');
56+
57+
store.reset();
58+
expect(store.get()).toBe('listByGrade');
59+
});
60+
});
61+
});

0 commit comments

Comments
 (0)