diff --git a/src/__tests__/sorting.test.ts b/src/__tests__/sorting.test.ts new file mode 100644 index 0000000..23673bd --- /dev/null +++ b/src/__tests__/sorting.test.ts @@ -0,0 +1,57 @@ +import { describe, it, expect } from 'vitest' +import type { SelectedPotential, PotentialPriority } from '../types' + +const PRIORITY_ORDER: Record = { + Core: 0, + Medium: 1, + Optional: 2, +} + +function sortPotentials(potentials: SelectedPotential[]): SelectedPotential[] { + return [...potentials].sort((a, b) => { + if (a.priority !== b.priority) { + return PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority] + } + return a.selectionTimestamp - b.selectionTimestamp + }) +} + +describe('Potential Sorting Logic', () => { + it('should sort by priority (Core > Medium > Optional)', () => { + const potentials: any[] = [ + { id: 1, priority: 'Optional', selectionTimestamp: 100 }, + { id: 2, priority: 'Medium', selectionTimestamp: 200 }, + { id: 3, priority: 'Core', selectionTimestamp: 300 }, + ] + const sorted = sortPotentials(potentials) + expect(sorted[0].id).toBe(3) // Core + expect(sorted[1].id).toBe(2) // Medium + expect(sorted[2].id).toBe(1) // Optional + }) + + it('should sort by selection order within the same priority', () => { + const potentials: any[] = [ + { id: 1, priority: 'Medium', selectionTimestamp: 500 }, + { id: 2, priority: 'Medium', selectionTimestamp: 100 }, + { id: 3, priority: 'Medium', selectionTimestamp: 300 }, + ] + const sorted = sortPotentials(potentials) + expect(sorted[0].id).toBe(2) // Earliest + expect(sorted[1].id).toBe(3) // Middle + expect(sorted[2].id).toBe(1) // Latest + }) + + it('should correctly handle mixed priorities and timestamps', () => { + const potentials: any[] = [ + { id: 1, priority: 'Optional', selectionTimestamp: 10 }, + { id: 2, priority: 'Core', selectionTimestamp: 100 }, + { id: 3, priority: 'Medium', selectionTimestamp: 5 }, + { id: 4, priority: 'Medium', selectionTimestamp: 50 }, + ] + const sorted = sortPotentials(potentials) + expect(sorted[0].id).toBe(2) // Core + expect(sorted[1].id).toBe(3) // Medium (earlier) + expect(sorted[2].id).toBe(4) // Medium (later) + expect(sorted[3].id).toBe(1) // Optional + }) +}) diff --git a/src/components/single-selected.tsx b/src/components/single-selected.tsx new file mode 100644 index 0000000..946a142 --- /dev/null +++ b/src/components/single-selected.tsx @@ -0,0 +1,97 @@ +import { InfoIcon, X } from 'lucide-react' +import { Slider } from './ui/slider' +import { Button } from './ui/button' +import { + HybridTooltip, + HybridTooltipContent, + HybridTooltipTrigger, +} from './ui/hybrid-tooltip' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from './ui/select' +import type { PotentialPriority, Slot } from '@/types' +import { useTrekkerStore } from '@/lib/trekker-store' +import ResponsivePotential from './responsive-potential' + +export function SingleSelected({ slot, id }: { slot: Slot; id: number }) { + const s = useTrekkerStore((state) => state.potentials[slot][id]) + const updateLevel = useTrekkerStore((sel) => sel.updateLevel) + const removePotential = useTrekkerStore((sel) => sel.removePotential) + const updatePriority = useTrekkerStore((sel) => sel.updatePriority) + + if (!s) return null + + return ( +
+ + +
+ + {s.rarity !== 0 && ( +
+ {s.level} +
+ )} + + +
+
+ +

{s.briefDesc}

+
+
+ +
+ ) => + updateLevel(slot, s.id, newValue[0]) + } + > + +
+
+ ) +} diff --git a/src/components/ss-potentials.tsx b/src/components/ss-potentials.tsx index e0b3c7e..31626bf 100644 --- a/src/components/ss-potentials.tsx +++ b/src/components/ss-potentials.tsx @@ -1,8 +1,7 @@ import { memo } from 'react' -import { InfoIcon, PlusIcon, X } from 'lucide-react' +import { InfoIcon, PlusIcon } from 'lucide-react' import { useShallow } from 'zustand/shallow' import ResponsivePotential from './responsive-potential' -import { Slider } from './ui/slider' import { Button } from './ui/button' import { ScrollArea, ScrollBar } from './ui/scroll-area' import { @@ -11,13 +10,6 @@ import { HybridTooltipProvider, HybridTooltipTrigger, } from './ui/hybrid-tooltip' -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from './ui/select' import type { PotentialPriority, Slot } from '@/types' import { Popover, @@ -25,94 +17,30 @@ import { PopoverTrigger, } from '@/components/ui/popover' import { useTrekkerStore } from '@/lib/trekker-store' +import { SingleSelected } from './single-selected' + +const PRIORITY_ORDER: Record = { + Core: 0, + Medium: 1, + Optional: 2, +} type SSPotentialsProps = { slot: Slot type: 'main' | 'support' } -function SingleSelected({ slot, id }: { slot: Slot; id: number }) { - const s = useTrekkerStore((state) => state.potentials[slot][id]) - const updateLevel = useTrekkerStore((sel) => sel.updateLevel) - const removePotential = useTrekkerStore((sel) => sel.removePotential) - const updatePriority = useTrekkerStore((sel) => sel.updatePriority) - return ( -
- - -
- - {s.rarity !== 0 && ( -
- {s.level} -
- )} - - -
-
- -

{s.briefDesc}

-
-
- -
- ) => - updateLevel(slot, s.id, newValue[0]) - } - > - -
-
- ) -} - function SSPotentials({ slot, type }: SSPotentialsProps) { const potentials = useTrekkerStore((s) => s.trekkers[slot]?.potential) - const selected = useTrekkerStore( + const selectedIds = useTrekkerStore( useShallow((s) => Object.values(s.potentials[slot]) - .sort((a, b) => a.rarity - b.rarity) + .sort((a, b) => { + if (a.priority !== b.priority) { + return PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority] + } + return a.selectionTimestamp - b.selectionTimestamp + }) .map((p) => p.id), ), ) @@ -133,7 +61,7 @@ function SSPotentials({ slot, type }: SSPotentialsProps) { const filteredPotentials = potentials .filter( (p) => - (p.type === type || p.type === 'common') && !selected.includes(p.id), + (p.type === type || p.type === 'common') && !selectedIds.includes(p.id), ) .sort((a, b) => a.rarity - b.rarity) @@ -199,7 +127,7 @@ function SSPotentials({ slot, type }: SSPotentialsProps) {
- {selected.length === 0 ? ( + {selectedIds.length === 0 ? (
) : ( - selected.map((s) => ( - + selectedIds.map((id) => ( + )) )}
diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..0c9ebb0 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,3 @@ +export const CONFIG = { + DATA_URL: 'https://raw.githubusercontent.com/maj-rf/StellaSoraData/refs/heads/main/character.json', +} as const; diff --git a/src/lib/trekker-store.ts b/src/lib/trekker-store.ts index 3929036..837cb70 100644 --- a/src/lib/trekker-store.ts +++ b/src/lib/trekker-store.ts @@ -46,8 +46,8 @@ export const useTrekkerStore = create()((set) => ({ ...state.potentials[slot], [p.id]: p.rarity === 0 - ? { ...p, rarity: 0, priority: 'Core' } - : { ...p, level: 1, priority: 'Medium' }, + ? { ...p, rarity: 0, priority: 'Core', selectionTimestamp: Date.now() } + : { ...p, level: 1, priority: 'Medium', selectionTimestamp: Date.now() }, }, }, })), diff --git a/src/lib/utils.ts b/src/lib/utils.ts index de87752..e8445f5 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -2,6 +2,7 @@ import { clsx } from 'clsx' import { twMerge } from 'tailwind-merge' import { snapdom } from '@zumer/snapdom' import { useTrekkerStore } from './trekker-store' +import { CONFIG } from '@/config' import type { SnapdomPlugin } from '@zumer/snapdom' import type { ClassValue } from 'clsx' import type { SSCharacter, TAvatar, Trekkers } from '@/types' @@ -11,14 +12,14 @@ export function cn(...inputs: Array) { } export async function fetchCharacters(): Promise> { - const response = await fetch( - 'https://raw.githubusercontent.com/maj-rf/StellaSoraData/refs/heads/main/character.json', - ) + const response = await fetch(CONFIG.DATA_URL) if (!response.ok) { throw new Error('Failed to fetch characters') } - const characters = await response.json() + return response.json() +} +export function initializeDefaultTrekkers(characters: Record) { useTrekkerStore.setState({ trekkers: { main: characters[103], @@ -26,7 +27,6 @@ export async function fetchCharacters(): Promise> { sub2: characters[111], }, }) - return characters } export function getTrekkersWithoutPotentials(trekkers: Trekkers) { diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 2e5a114..4906f1d 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -3,7 +3,7 @@ import { useRef, useTransition } from 'react' import { ResponsiveModal } from '@/components/responsive-modal' import SSPotentials from '@/components/ss-potentials' import { AvatarSelection } from '@/components/avatar-selection' -import { downloadImage, fetchCharacters } from '@/lib/utils' +import { downloadImage, fetchCharacters, initializeDefaultTrekkers } from '@/lib/utils' import { Preview } from '@/components/preview' import { Button } from '@/components/ui/button' import { Loading } from '@/components/loading' @@ -29,7 +29,11 @@ function AvatarPlaceholder() { export const Route = createFileRoute('/')({ component: App, - loader: fetchCharacters, + loader: async () => { + const data = await fetchCharacters() + initializeDefaultTrekkers(data) + return data + }, pendingComponent: Loading, }) diff --git a/src/types.ts b/src/types.ts index 4402c3d..e4b23dc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -29,14 +29,16 @@ export type PotentialPriority = 'Core' | 'Medium' | 'Optional' export type SelectedPotential = | (SSPotential & { - rarity: 0 - level?: never - priority: Extract - }) + rarity: 0 + level?: never + priority: Extract + selectionTimestamp: number + }) | (SSPotential & { - rarity: 1 | 2 - level: number - priority: PotentialPriority - }) + rarity: 1 | 2 + level: number + priority: PotentialPriority + selectionTimestamp: number + }) export type SelectedPotentialMap = Record