diff --git a/src/lib/components/TabItemWrapper.svelte b/src/lib/components/TabItemWrapper.svelte index 7b2f284ec..1dfbbed1a 100644 --- a/src/lib/components/TabItemWrapper.svelte +++ b/src/lib/components/TabItemWrapper.svelte @@ -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; @@ -19,6 +24,7 @@ let { workbookType = null, + activeProblemList = null, isOpen = false, title, tooltipContent = '', @@ -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); + } } @@ -54,7 +67,7 @@ - handleClick(workbookType)}> + handleClick(workbookType, activeProblemList)}> {#snippet titleSlot()}
diff --git a/src/lib/stores/active_problem_list_tab.svelte.ts b/src/lib/stores/active_problem_list_tab.svelte.ts new file mode 100644 index 000000000..e40f1b125 --- /dev/null +++ b/src/lib/stores/active_problem_list_tab.svelte.ts @@ -0,0 +1,52 @@ +export type ActiveProblemListTab = 'contestTable' | 'listByGrade' | 'gradeGuidelineTable'; + +export class ActiveProblemListTabStore { + value = $state('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(); diff --git a/src/routes/problems/+page.svelte b/src/routes/problems/+page.svelte index 4c0ce8bd1..04d403b37 100644 --- a/src/routes/problems/+page.svelte +++ b/src/routes/problems/+page.svelte @@ -1,4 +1,6 @@ @@ -26,29 +37,35 @@ - + {#if isAdmin} - - - + {@render problemListTab('テーブル', 'contestTable', contestTable)} {/if} - - - + {@render problemListTab('グレード', 'listByGrade', listByGrade)} - - - - - - - + {@render problemListTab('グレードの目安', 'gradeGuidelineTable', gradeGuidelineTable)}
+ +{#snippet problemListTab(title: string, tab: ActiveProblemListTab, children: Snippet)} + + {@render children()} + +{/snippet} + +{#snippet contestTable()} + +{/snippet} + +{#snippet listByGrade()} + +{/snippet} + +{#snippet gradeGuidelineTable()} + +{/snippet} diff --git a/src/test/lib/stores/active_problem_list_tab.svelte.test.ts b/src/test/lib/stores/active_problem_list_tab.svelte.test.ts new file mode 100644 index 000000000..a1a09dd1f --- /dev/null +++ b/src/test/lib/stores/active_problem_list_tab.svelte.test.ts @@ -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'); + }); + }); +});