diff --git a/CLEANUP_PLAN.md b/CLEANUP_PLAN.md deleted file mode 100644 index ebac2dcf..00000000 --- a/CLEANUP_PLAN.md +++ /dev/null @@ -1,147 +0,0 @@ -# Svelte-Main Branch Cleanup Plan - -## Overview -This document outlines the remaining work needed to clean up the `svelte-main` branch and get the build green. - -## Completed Fixes - -### 1. Environment/Generated Module Issues -- Ran Paraglide codegen to generate translation files in `src/lib/paraglide/` -- Added type declarations for `$env/static/public` module in `src/lib/types/declarations.d.ts` - -### 2. Broken Imports from Removed Legacy API Layer -- Updated `SearchSidebar.svelte` to use new adapter layer -- Updated `Party.svelte` to use new adapter layer -- Updated `teams/new/+page.svelte` to use new adapter layer - -### 3. Type Shims for External Libraries -- Added comprehensive type declarations for `wx-svelte-grid` in `src/lib/types/declarations.d.ts` - -### 4. RequestOptions Cache Type Incompatibility -- Fixed `RequestOptions` interface in `src/lib/api/adapters/types.ts` to exclude 'cache' from RequestInit extension -- Added both `cacheTime?: number` and `cache?: RequestCache` properties -- Updated `base.adapter.ts` to use `cacheTime` instead of `cache` for duration - -### 5. Users Resource Module -- Updated `src/lib/api/resources/users.ts` to use `userAdapter` instead of removed `../core` module -- Changed function signature from `update(fetch, userId, params)` to `update(userId, params)` - -### 6. UserSettingsModal.svelte Fixes -- Fixed Switch import path (case sensitivity: `switch.svelte` -> `Switch.svelte`) -- Fixed `users.update` call signature -- Removed invalid footer snippet definition -- Removed unused `Snippet` import - -### 7. Character Type in Entity Adapter -- Added missing properties to Character type in `entity.adapter.ts`: - - `gender?: number` - - `proficiency?: number[]` - - `race?: number[]` - - `hp?: { minHp, maxHp, maxHpFlb }` - - `atk?: { minAtk, maxAtk, maxAtkFlb }` - - `uncap?: { flb, ulb, transcendence }` - -### 8. Adapters Index File -- Created `src/lib/api/adapters/index.ts` to export all adapters and types - -### 9. Character Type in Entities -- Added missing properties to Character type in `src/lib/types/api/entities.ts`: - - `gender`, `race`, `proficiency`, `hp`, `atk` - -## Remaining Type Errors (~378 errors) - -### High Priority (Most Impactful) - -#### 1. 'firstItem' and 'item' Possibly Undefined (27 errors) -- **Location**: `src/routes/teams/new/+page.svelte` -- **Issue**: TypeScript strict null checks flagging array access without null guards -- **Fix**: Add null checks before accessing `items[0]` and in forEach loops - -#### 2. PartyCtx Missing openPicker Property (8 errors) -- **Location**: Various components using party context -- **Issue**: `PartyCtx` type doesn't include `openPicker` method -- **Fix**: Update `PartyCtx` type definition to include `openPicker` method - -#### 3. Missing Paraglide Translation Keys (18 errors) -- **Keys**: `context_view_details`, `context_replace`, `context_remove` -- **Location**: `src/lib/paraglide/messages` -- **Fix**: Add missing translation keys to `project.inlang/messages/en.json` and `ja.json` - -#### 4. Summon/Weapon Missing hp/atk Properties (18 errors) -- **Location**: Entity adapter types -- **Issue**: Summon and Weapon types in `entity.adapter.ts` need hp/atk properties -- **Fix**: Update Summon type to include `hp` and `atk` nested objects - -### Medium Priority - -#### 5. exactOptionalPropertyTypes Violations (~15 errors) -- **Issue**: Props with `undefined` values being passed to components that don't accept undefined -- **Fix**: Update component Props interfaces to accept `undefined` for optional properties - -#### 6. Select.svelte ItemIndicator Errors (4 errors) -- **Issue**: `Select.ItemIndicator` doesn't exist in bits-ui -- **Fix**: Check bits-ui documentation for correct component name or remove usage - -#### 7. Button.svelte Icon Type Issues (2 errors) -- **Issue**: `icon` prop is `string | undefined` but Icon component expects `string` -- **Fix**: Add conditional rendering or default value for icon prop - -#### 8. DropdownItem.svelte asChild Issue (2 errors) -- **Issue**: `asChild` prop doesn't exist on DropdownMenu.Item in bits-ui -- **Fix**: Use `child` snippet pattern instead of `asChild` prop - -### Lower Priority - -#### 9. maxLength vs maxlength (4 errors) -- **Issue**: HTML attribute should be lowercase `maxlength` -- **Fix**: Change `maxLength` to `maxlength` in input elements - -#### 10. Button Variant "outlined" (3 errors) -- **Issue**: "outlined" is not a valid Button variant -- **Fix**: Use correct variant name (check Button component for valid variants) - -#### 11. SearchResult Type Mismatch (5 errors) -- **Issue**: `SearchResult[]` vs `SearchResult[]` type mismatch -- **Fix**: Update function signatures to use consistent SearchResult type - -## Files Modified in This Session - -1. `src/lib/api/adapters/types.ts` - RequestOptions cache fix -2. `src/lib/api/adapters/base.adapter.ts` - cacheTime usage -3. `src/lib/api/adapters/entity.adapter.ts` - Character type properties -4. `src/lib/api/adapters/index.ts` - New file for exports -5. `src/lib/api/resources/users.ts` - Updated to use userAdapter -6. `src/lib/types/declarations.d.ts` - wx-svelte-grid and $env type shims -7. `src/lib/types/api/entities.ts` - Character type properties -8. `src/lib/components/UserSettingsModal.svelte` - Multiple fixes -9. `src/lib/components/panels/SearchSidebar.svelte` - Accepted upstream version -10. `src/lib/components/party/Party.svelte` - granblueId fix -11. `src/routes/teams/new/+page.svelte` - Accepted upstream version - -## Commands to Verify Progress - -```bash -# Count remaining errors -pnpm check 2>&1 | grep -c "Error:" - -# Analyze error patterns -pnpm check 2>&1 | grep "Error:" | sort | uniq -c | sort -rn | head -20 - -# Run lint -pnpm lint - -# Run build -pnpm build -``` - -## Next Steps - -1. Fix the 'firstItem'/'item' possibly undefined errors in teams/new/+page.svelte -2. Add missing Paraglide translation keys -3. Update PartyCtx type to include openPicker -4. Update Summon type in entity.adapter.ts to include hp/atk -5. Fix exactOptionalPropertyTypes violations -6. Fix bits-ui component usage (Select.ItemIndicator, DropdownItem asChild) -7. Run `pnpm check` to verify all errors are resolved -8. Run `pnpm lint` and `pnpm build` -9. Create PR with all fixes diff --git a/src/lib/api/adapters/resources/__tests__/search.resource.test.ts b/src/lib/api/adapters/resources/__tests__/search.resource.test.ts deleted file mode 100644 index f320f8fd..00000000 --- a/src/lib/api/adapters/resources/__tests__/search.resource.test.ts +++ /dev/null @@ -1,250 +0,0 @@ -/** - * Tests for SearchResource - * - * These tests verify the reactive search resource functionality - * including debouncing, state management, and cancellation. - */ - -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' -import { SearchResource } from '../search.resource.svelte' -import { SearchAdapter } from '../../search.adapter' -import type { SearchResponse } from '../../search.adapter' - -describe('SearchResource', () => { - let resource: SearchResource - let mockAdapter: SearchAdapter - let originalFetch: typeof global.fetch - - beforeEach(() => { - originalFetch = global.fetch - - // Create mock adapter - mockAdapter = new SearchAdapter({ - baseURL: 'https://api.example.com' - }) - - // Create resource with short debounce for testing - resource = new SearchResource({ - adapter: mockAdapter, - debounceMs: 10, - initialParams: { locale: 'en' } - }) - }) - - afterEach(() => { - global.fetch = originalFetch - resource.cancelAll() - vi.clearAllTimers() - }) - - describe('initialization', () => { - it('should initialize with empty state', () => { - expect(resource.weapons.loading).toBe(false) - expect(resource.weapons.data).toBeUndefined() - expect(resource.weapons.error).toBeUndefined() - - expect(resource.characters.loading).toBe(false) - expect(resource.summons.loading).toBe(false) - expect(resource.all.loading).toBe(false) - }) - - it('should accept initial parameters', () => { - const customResource = new SearchResource({ - initialParams: { - locale: 'ja', - page: 2 - } - }) - - // We'll verify these are used in the search tests - expect(customResource).toBeDefined() - }) - }) - - describe('search operations', () => { - it('should search weapons with debouncing', async () => { - const mockResponse: SearchResponse = { - results: [ - { - id: '1', - granblueId: 'weapon_1', - name: { en: 'Test Weapon' }, - element: 1, - rarity: 5, - searchableType: 'Weapon' - } - ], - total: 1 - } - - global.fetch = vi.fn().mockResolvedValue({ - ok: true, - json: async () => ({ - results: [ - { - id: '1', - granblue_id: 'weapon_1', - name: { en: 'Test Weapon' }, - element: 1, - rarity: 5, - searchable_type: 'Weapon' - } - ], - total: 1 - }) - }) - - // Start search - resource.searchWeapons({ query: 'test' }) - - // Should be loading immediately - expect(resource.weapons.loading).toBe(true) - - // Wait for debounce + response - await new Promise(resolve => setTimeout(resolve, 50)) - - // Should have results - expect(resource.weapons.loading).toBe(false) - expect(resource.weapons.data).toEqual(mockResponse) - expect(resource.weapons.error).toBeUndefined() - - // Verify API was called with merged params - expect(global.fetch).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - body: JSON.stringify({ - locale: 'en', // From initialParams - page: 1, - query: 'test' - }) - }) - ) - }) - - it('should handle search errors', async () => { - global.fetch = vi.fn().mockResolvedValue({ - ok: false, - status: 500, - statusText: 'Server Error', - json: async () => ({ error: 'Internal error' }) - }) - - resource.searchCharacters({ query: 'error' }) - - // Wait for debounce + response - await new Promise(resolve => setTimeout(resolve, 50)) - - expect(resource.characters.loading).toBe(false) - expect(resource.characters.data).toBeUndefined() - expect(resource.characters.error).toMatchObject({ - code: 'SERVER_ERROR', - status: 500 - }) - }) - - it('should cancel previous search when new one starts', async () => { - let callCount = 0 - global.fetch = vi.fn().mockImplementation(() => { - callCount++ - return new Promise(resolve => { - setTimeout(() => { - resolve({ - ok: true, - json: async () => ({ results: [] }) - }) - }, 100) // Slow response - }) - }) - - // Start first search - resource.searchAll({ query: 'first' }) - - // Start second search before first completes - await new Promise(resolve => setTimeout(resolve, 20)) - resource.searchAll({ query: 'second' }) - - // Wait for completion - await new Promise(resolve => setTimeout(resolve, 150)) - - // Should have made 2 calls but only the second one's result should be set - expect(callCount).toBe(2) - }) - }) - - describe('state management', () => { - it('should clear specific search type', () => { - // Set some mock data - resource.weapons = { - loading: false, - data: { results: [] } - } - - resource.characters = { - loading: false, - data: { results: [] } - } - - // Clear weapons only - resource.clear('weapons') - - expect(resource.weapons.data).toBeUndefined() - expect(resource.weapons.loading).toBe(false) - expect(resource.characters.data).toBeDefined() // Should remain - }) - - it('should clear all search results', () => { - // Set mock data for all types - resource.weapons = { loading: false, data: { results: [] } } - resource.characters = { loading: false, data: { results: [] } } - resource.summons = { loading: false, data: { results: [] } } - resource.all = { loading: false, data: { results: [] } } - - // Clear all - resource.clearAll() - - expect(resource.weapons.data).toBeUndefined() - expect(resource.characters.data).toBeUndefined() - expect(resource.summons.data).toBeUndefined() - expect(resource.all.data).toBeUndefined() - }) - - it('should update base parameters', () => { - resource.updateBaseParams({ locale: 'ja', per: 50 }) - - global.fetch = vi.fn().mockResolvedValue({ - ok: true, - json: async () => ({ results: [] }) - }) - - resource.searchWeapons({ query: 'test' }) - - // Wait for debounce - setTimeout(() => { - expect(global.fetch).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - body: expect.stringContaining('"locale":"ja"') - }) - ) - }, 50) - }) - }) - - describe('cancellation', () => { - it('should cancel specific search type', () => { - const cancelSpy = vi.spyOn(resource, 'cancelSearch') - - resource.clear('weapons') - - expect(cancelSpy).toHaveBeenCalledWith('weapons') - }) - - it('should cancel all searches', () => { - const cancelAllSpy = vi.spyOn(resource, 'cancelAll') - - resource.clearAll() - - expect(cancelAllSpy).toHaveBeenCalled() - }) - }) -}) \ No newline at end of file diff --git a/src/lib/api/adapters/resources/job.resource.svelte.ts b/src/lib/api/adapters/resources/job.resource.svelte.ts deleted file mode 100644 index ed3d6dac..00000000 --- a/src/lib/api/adapters/resources/job.resource.svelte.ts +++ /dev/null @@ -1,372 +0,0 @@ -/** - * Reactive Job Resource using Svelte 5 Runes and Runed - * - * Provides reactive state management for job-related operations with - * automatic loading states, error handling, and caching. - * - * @module adapters/resources/job - */ - -import { JobAdapter, jobAdapter } from '../job.adapter' -import type { Job, JobSkill, JobAccessory } from '$lib/types/api/entities' -import type { AdapterError, AdapterOptions } from '../types' - -/** - * Job resource configuration options - */ -export interface JobResourceOptions { - /** Job adapter instance to use */ - adapter?: JobAdapter - /** Cache duration in milliseconds */ - cacheDuration?: number -} - -/** - * State for job data - */ -interface JobState { - data: T | undefined - loading: boolean - error: AdapterError | undefined - lastFetch: number | undefined -} - -/** - * Creates a reactive job resource for job data management - * This is a Svelte 5 universal reactive state (works in both components and modules) - * - * @example - * ```svelte - * - * - * {#if jobResource.jobs.loading} - *

Loading jobs...

- * {:else if jobResource.jobs.error} - *

Error: {jobResource.jobs.error.message}

- * {:else if jobResource.jobs.data} - * {#each jobResource.jobs.data as job} - *
{job.name.en}
- * {/each} - * {/if} - * ``` - */ -export class JobResource { - // Private adapter instance - private adapter: JobAdapter - private cacheDuration: number - - // Reactive state for job data - jobs = $state>({ loading: false, data: undefined, error: undefined, lastFetch: undefined }) - currentJob = $state>({ loading: false, data: undefined, error: undefined, lastFetch: undefined }) - jobSkills = $state>({ loading: false, data: undefined, error: undefined, lastFetch: undefined }) - jobAccessories = $state>({ loading: false, data: undefined, error: undefined, lastFetch: undefined }) - allSkills = $state>({ loading: false, data: undefined, error: undefined, lastFetch: undefined }) - - // Track active requests - private activeRequests = new Map() - - constructor(options: JobResourceOptions = {}) { - this.adapter = options.adapter || jobAdapter - this.cacheDuration = options.cacheDuration || 5 * 60 * 1000 // 5 minutes default - } - - /** - * Check if cached data is still valid - */ - private isCacheValid(state: JobState): boolean { - if (!state.data || !state.lastFetch) return false - return Date.now() - state.lastFetch < this.cacheDuration - } - - /** - * Fetch all jobs - */ - async fetchJobs(force = false): Promise { - // Return cached data if valid and not forced - if (!force && this.isCacheValid(this.jobs) && this.jobs.data) { - return this.jobs.data - } - - // Cancel any existing request - this.cancelRequest('jobs') - - // Create new abort controller - const controller = new AbortController() - this.activeRequests.set('jobs', controller) - - // Update loading state - this.jobs = { ...this.jobs, loading: true, error: undefined } - - try { - const data = await this.adapter.getAll() - this.jobs = { data, loading: false, lastFetch: Date.now(), error: undefined } - return data - } catch (error: any) { - if (error.code !== 'CANCELLED') { - this.jobs = { - ...this.jobs, - loading: false, - error: error as AdapterError - } - } - throw error - } finally { - this.activeRequests.delete('jobs') - } - } - - /** - * Fetch a single job by ID - */ - async fetchJob(id: string, force = false): Promise { - // Check if this job is already loaded - if (!force && this.currentJob.data?.id === id && this.isCacheValid(this.currentJob)) { - return this.currentJob.data - } - - // Cancel any existing request - this.cancelRequest('currentJob') - - // Create new abort controller - const controller = new AbortController() - this.activeRequests.set('currentJob', controller) - - // Update loading state - this.currentJob = { ...this.currentJob, loading: true, error: undefined } - - try { - const data = await this.adapter.getById(id) - this.currentJob = { data, loading: false, lastFetch: Date.now(), error: undefined } - return data - } catch (error: any) { - if (error.code !== 'CANCELLED') { - this.currentJob = { - ...this.currentJob, - loading: false, - error: error as AdapterError - } - } - throw error - } finally { - this.activeRequests.delete('currentJob') - } - } - - /** - * Fetch skills for a specific job - */ - async fetchJobSkills(jobId: string, force = false): Promise { - // Cancel any existing request - this.cancelRequest('jobSkills') - - // Create new abort controller - const controller = new AbortController() - this.activeRequests.set('jobSkills', controller) - - // Update loading state - this.jobSkills = { ...this.jobSkills, loading: true, error: undefined } - - try { - const data = await this.adapter.getSkills(jobId) - this.jobSkills = { data, loading: false, lastFetch: Date.now(), error: undefined } - return data - } catch (error: any) { - if (error.code !== 'CANCELLED') { - this.jobSkills = { - ...this.jobSkills, - loading: false, - error: error as AdapterError - } - } - throw error - } finally { - this.activeRequests.delete('jobSkills') - } - } - - /** - * Fetch accessories for a specific job - */ - async fetchJobAccessories(jobId: string, force = false): Promise { - // Cancel any existing request - this.cancelRequest('jobAccessories') - - // Create new abort controller - const controller = new AbortController() - this.activeRequests.set('jobAccessories', controller) - - // Update loading state - this.jobAccessories = { ...this.jobAccessories, loading: true, error: undefined } - - try { - const data = await this.adapter.getAccessories(jobId) - this.jobAccessories = { data, loading: false, lastFetch: Date.now(), error: undefined } - return data - } catch (error: any) { - if (error.code !== 'CANCELLED') { - this.jobAccessories = { - ...this.jobAccessories, - loading: false, - error: error as AdapterError - } - } - throw error - } finally { - this.activeRequests.delete('jobAccessories') - } - } - - /** - * Fetch all available job skills - */ - async fetchAllSkills(force = false): Promise { - // Return cached data if valid and not forced - if (!force && this.isCacheValid(this.allSkills) && this.allSkills.data) { - return this.allSkills.data - } - - // Cancel any existing request - this.cancelRequest('allSkills') - - // Create new abort controller - const controller = new AbortController() - this.activeRequests.set('allSkills', controller) - - // Update loading state - this.allSkills = { ...this.allSkills, loading: true, error: undefined } - - try { - const data = await this.adapter.getAllSkills() - this.allSkills = { data, loading: false, lastFetch: Date.now(), error: undefined } - return data - } catch (error: any) { - if (error.code !== 'CANCELLED') { - this.allSkills = { - ...this.allSkills, - loading: false, - error: error as AdapterError - } - } - throw error - } finally { - this.activeRequests.delete('allSkills') - } - } - - /** - * Update party job - */ - async updatePartyJob(partyId: string, jobId: string): Promise { - await this.adapter.updatePartyJob(partyId, jobId) - } - - /** - * Update party job skills - */ - async updatePartyJobSkills( - partyId: string, - skills: Array<{ id: string; slot: number }> - ): Promise { - await this.adapter.updatePartyJobSkills(partyId, skills) - } - - /** - * Remove party job skill - */ - async removePartyJobSkill(partyId: string, slot: number): Promise { - await this.adapter.removePartyJobSkill(partyId, slot) - } - - /** - * Cancel an active request - */ - private cancelRequest(key: string) { - const controller = this.activeRequests.get(key) - if (controller) { - controller.abort() - this.activeRequests.delete(key) - } - } - - /** - * Cancel all active requests - */ - cancelAll() { - this.activeRequests.forEach(controller => controller.abort()) - this.activeRequests.clear() - } - - /** - * Clear cached data - */ - clearCache() { - this.jobs = { loading: false, data: undefined, error: undefined, lastFetch: undefined } - this.currentJob = { loading: false, data: undefined, error: undefined, lastFetch: undefined } - this.jobSkills = { loading: false, data: undefined, error: undefined, lastFetch: undefined } - this.jobAccessories = { loading: false, data: undefined, error: undefined, lastFetch: undefined } - this.allSkills = { loading: false, data: undefined, error: undefined, lastFetch: undefined } - } -} - -/** - * Create a new job resource instance - */ -export function createJobResource(options?: JobResourceOptions): JobResource { - return new JobResource(options) -} - -/** - * Helper to group jobs by tier (row) - */ -export function groupJobsByTier(jobs: Job[]): Record { - const tiers: Record = { - '1': [], - '2': [], - '3': [], - '4': [], - '5': [], - 'ex': [], - 'ex2': [] - } - - for (const job of jobs) { - const tier = job.row.toString().toLowerCase() - if (tier in tiers && tiers[tier]) { - tiers[tier].push(job) - } - } - - // Sort jobs within each tier by order - for (const tier in tiers) { - if (tiers[tier]) { - tiers[tier].sort((a, b) => a.order - b.order) - } - } - - return tiers -} - -/** - * Helper to get tier display name - */ -export function getTierDisplayName(tier: string): string { - const tierNames: Record = { - '1': 'Class I', - '2': 'Class II', - '3': 'Class III', - '4': 'Class IV', - '5': 'Class V', - 'ex': 'Extra', - 'ex2': 'Extra II' - } - return tierNames[tier] || tier -} diff --git a/src/lib/api/adapters/resources/party.resource.svelte.ts b/src/lib/api/adapters/resources/party.resource.svelte.ts deleted file mode 100644 index cf4af750..00000000 --- a/src/lib/api/adapters/resources/party.resource.svelte.ts +++ /dev/null @@ -1,427 +0,0 @@ -/** - * Reactive Party Resource using Svelte 5 Runes - * - * Provides reactive state management for party operations with - * automatic loading states, error handling, and optimistic updates. - * - * @deprecated This resource class is deprecated in favor of TanStack Query. - * Use `partyQueries` from `$lib/api/queries/party.queries` and - * mutation hooks from `$lib/api/mutations/party.mutations` instead. - * - * Migration example: - * ```typescript - * // Before (PartyResource) - * const party = createPartyResource() - * party.load('ABC123') - * party.update({ shortcode: 'ABC123', name: 'New Name' }) - * - * // After (TanStack Query) - * import { createQuery } from '@tanstack/svelte-query' - * import { partyQueries } from '$lib/api/queries/party.queries' - * import { useUpdateParty } from '$lib/api/mutations/party.mutations' - * - * const party = createQuery(() => partyQueries.byShortcode('ABC123')) - * const updateParty = useUpdateParty() - * updateParty.mutate({ shortcode: 'ABC123', name: 'New Name' }) - * ``` - * - * @module adapters/resources/party - */ - -import { SvelteDate, SvelteMap } from 'svelte/reactivity' -import { - PartyAdapter, - partyAdapter, - type CreatePartyParams, - type UpdatePartyParams -} from '../party.adapter' -import type { Party } from '$lib/types/api/party' -import type { AdapterError } from '../types' - -/** - * Party resource configuration options - */ -export interface PartyResourceOptions { - /** Party adapter instance to use */ - adapter?: PartyAdapter - /** Enable optimistic updates for mutations */ - optimistic?: boolean -} - -/** - * Resource state for a single party - */ -interface PartyState { - data?: Party - loading: boolean - error?: AdapterError - updating?: boolean -} - -/** - * Resource state for party lists - */ -interface PartyListState { - parties: Party[] - total?: number - page?: number - totalPages?: number - loading: boolean - error?: AdapterError -} - -/** - * Creates a reactive party resource for managing parties - * - * @example - * ```svelte - * - * - * {#if party.current.loading} - *

Loading party...

- * {:else if party.current.error} - *

Error: {party.current.error.message}

- * {:else if party.current.data} - *

{party.current.data.name}

- * {/if} - * ``` - */ -export class PartyResource { - private adapter: PartyAdapter - private optimistic: boolean - - // Reactive state for current party - current = $state({ loading: false }) - - // Reactive state for user parties list - userParties = $state({ - parties: [], - loading: false - }) - - // Track active requests for cancellation - private activeRequests = new SvelteMap() - - constructor(options: PartyResourceOptions = {}) { - this.adapter = options.adapter || partyAdapter - this.optimistic = options.optimistic ?? true - } - - /** - * Loads a party by shortcode - */ - async load(shortcode: string): Promise { - // Cancel any existing load request - this.cancelRequest('load') - - const controller = new AbortController() - this.activeRequests.set('load', controller) - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { error: _error, ...rest } = this.current - this.current = { ...rest, loading: true } - - try { - const party = await this.adapter.getByShortcode(shortcode) - this.current = { data: party, loading: false } - return party - } catch (error: unknown) { - if (error && typeof error === 'object' && 'code' in error && error.code !== 'CANCELLED') { - this.current = { - ...this.current, - loading: false, - error: error as AdapterError - } - } - } finally { - this.activeRequests.delete('load') - } - } - - /** - * Creates a new party - */ - async create(params: CreatePartyParams): Promise { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { error: _error, ...rest } = this.current - this.current = { ...rest, updating: true } - - try { - const party = await this.adapter.create(params) - this.current = { data: party, loading: false, updating: false } - - // Add to user parties if loaded - if (this.userParties.parties.length > 0) { - this.userParties.parties = [party, ...this.userParties.parties] - if (this.userParties.total !== undefined) { - this.userParties.total++ - } - } - - return party - } catch (error: unknown) { - this.current = { - ...this.current, - updating: false, - error: error as AdapterError - } - } - } - - /** - * Updates the current party - */ - async update(params: UpdatePartyParams): Promise { - // Optimistic update - if (this.optimistic && this.current.data) { - const optimisticData = { - ...this.current.data, - ...params, - updatedAt: new SvelteDate().toISOString() - } - this.current = { - ...this.current, - data: optimisticData as Party, - updating: true - } - } else { - this.current = { ...this.current, updating: true } - } - - try { - const party = await this.adapter.update(params) - this.current = { data: party, loading: false, updating: false } - - // Update in user parties list if present - const index = this.userParties.parties.findIndex((p) => p.shortcode === params.shortcode) - if (index !== -1) { - this.userParties.parties[index] = party - } - - return party - } catch (error: unknown) { - // Revert optimistic update on error - if (this.optimistic) { - await this.load(params.shortcode) - } - this.current = { - ...this.current, - updating: false, - error: error as AdapterError - } - } - } - - /** - * Deletes the current party - */ - async delete(shortcode: string): Promise { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { error: _error, ...rest } = this.current - this.current = { ...rest, updating: true } - - try { - await this.adapter.delete(shortcode) - - // Clear current party - this.current = { loading: false, updating: false } - - // Remove from user parties list - this.userParties.parties = this.userParties.parties.filter((p) => p.shortcode !== shortcode) - if (this.userParties.total !== undefined && this.userParties.total > 0) { - this.userParties.total-- - } - - return true - } catch (error: unknown) { - this.current = { - ...this.current, - updating: false, - error: error as AdapterError - } - return false - } - } - - /** - * Creates a remix (copy) of a party - */ - async remix(shortcode: string): Promise { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { error: _error, ...rest } = this.current - this.current = { ...rest, updating: true } - - try { - const party = await this.adapter.remix(shortcode) - this.current = { data: party, loading: false, updating: false } - - // Add to user parties if it's the current user's remix - if (this.userParties.parties.length > 0) { - this.userParties.parties = [party, ...this.userParties.parties] - if (this.userParties.total !== undefined) { - this.userParties.total++ - } - } - - return party - } catch (error: unknown) { - this.current = { - ...this.current, - updating: false, - error: error as AdapterError - } - } - } - - /** - * Loads parties for a specific user - */ - async loadUserParties( - username: string, - params: Omit[0], 'username'> = {} - ): Promise { - // Cancel any existing user parties request - this.cancelRequest('userParties') - - const controller = new AbortController() - this.activeRequests.set('userParties', controller) - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { error: _error, ...rest } = this.userParties - this.userParties = { ...rest, loading: true } - - try { - const response = await this.adapter.listUserParties({ - username, - ...params - }) - - this.userParties = { - parties: response.results, - total: response.total, - page: response.page, - totalPages: response.totalPages, - loading: false - } - } catch (error: unknown) { - if (error && typeof error === 'object' && 'code' in error && error.code !== 'CANCELLED') { - this.userParties = { - ...this.userParties, - loading: false, - error: error as AdapterError - } - } - } finally { - this.activeRequests.delete('userParties') - } - } - - /** - * Updates the job for the current party - */ - async updateJob( - partyId: string, - jobId: string, - skills?: Array<{ id: string; slot: number }>, - accessoryId?: string - ): Promise { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { error: _error, ...rest } = this.current - this.current = { ...rest, updating: true } - - try { - // Update job first - let party = await this.adapter.updateJob(partyId, jobId) - - // Update skills if provided - if (skills) { - party = await this.adapter.updateJobSkills(partyId, skills) - } - - // TODO: Handle accessory update when API supports it - if (accessoryId) { - party = await this.adapter.updateAccessory(partyId, accessoryId) - } - - this.current = { data: party, loading: false, updating: false } - return party - } catch (error: unknown) { - this.current = { - ...this.current, - updating: false, - error: error as AdapterError - } - } - } - - /** - * Cancels an active request - */ - private cancelRequest(key: string) { - const controller = this.activeRequests.get(key) - if (controller) { - controller.abort() - this.activeRequests.delete(key) - } - } - - /** - * Cancels all active requests - */ - cancelAll() { - this.activeRequests.forEach((controller) => controller.abort()) - this.activeRequests.clear() - } - - /** - * Clears the current party state - */ - clearCurrent() { - this.cancelRequest('load') - this.current = { loading: false } - } - - /** - * Clears the user parties state - */ - clearUserParties() { - this.cancelRequest('userParties') - this.userParties = { parties: [], loading: false } - } - - /** - * Clears all states - */ - clearAll() { - this.cancelAll() - this.current = { loading: false } - this.userParties = { parties: [], loading: false } - } - - /** - * Clears the adapter's cache - */ - clearCache(shortcode?: string) { - this.adapter.clearPartyCache(shortcode) - } -} - -/** - * Factory function for creating party resources - */ -export function createPartyResource(options?: PartyResourceOptions): PartyResource { - return new PartyResource(options) -} diff --git a/src/lib/api/adapters/resources/search.resource.svelte.ts b/src/lib/api/adapters/resources/search.resource.svelte.ts deleted file mode 100644 index 180d31e4..00000000 --- a/src/lib/api/adapters/resources/search.resource.svelte.ts +++ /dev/null @@ -1,247 +0,0 @@ -/** - * Reactive Search Resource using Svelte 5 Runes and Runed - * - * Provides reactive state management for search operations with - * automatic loading states, error handling, and debouncing. - * - * @deprecated This resource class is deprecated in favor of TanStack Query. - * Use `searchQueries` from `$lib/api/queries/search.queries` with `createInfiniteQuery` instead. - * - * Migration example: - * ```typescript - * // Before (SearchResource) - * const search = createSearchResource({ debounceMs: 300 }) - * search.searchWeapons({ query }) - * - * // After (TanStack Query) - * import { createInfiniteQuery } from '@tanstack/svelte-query' - * import { searchQueries } from '$lib/api/queries/search.queries' - * - * let debouncedQuery = $state('') - * const weapons = createInfiniteQuery(() => searchQueries.weapons(debouncedQuery)) - * ``` - * - * @module adapters/resources/search - */ - -import { useDebounce } from 'runed' -import { SearchAdapter, searchAdapter, type SearchParams, type SearchResponse } from '../search.adapter' -import type { AdapterError } from '../types' - -/** - * Search resource configuration options - */ -export interface SearchResourceOptions { - /** Search adapter instance to use */ - adapter?: SearchAdapter - /** Debounce delay in milliseconds for search queries */ - debounceMs?: number - /** Initial search parameters */ - initialParams?: SearchParams -} - -/** - * Search result state for a specific entity type - */ -interface SearchState { - data?: SearchResponse - loading: boolean - error?: AdapterError -} - -/** - * Creates a reactive search resource for entity searching - * This is a Svelte 5 universal reactive state (works in both components and modules) - * - * @example - * ```svelte - * - * - * - * - * {#if search.weapons.loading} - *

Searching...

- * {:else if search.weapons.error} - *

Error: {search.weapons.error.message}

- * {:else if search.weapons.data} - *
    - * {#each search.weapons.data.results as result} - *
  • {result.name.en}
  • - * {/each} - *
- * {/if} - * ``` - */ -export class SearchResource { - // Private adapter instance - private adapter: SearchAdapter - - // Base parameters for all searches - private baseParams: SearchParams - - // Debounce delay - private debounceMs: number - - // Reactive state for each search type - all = $state({ loading: false }) - weapons = $state({ loading: false }) - characters = $state({ loading: false }) - summons = $state({ loading: false }) - - // Track active requests for cancellation - private activeRequests = new Map() - - constructor(options: SearchResourceOptions = {}) { - this.adapter = options.adapter || searchAdapter - this.debounceMs = options.debounceMs || 300 - this.baseParams = options.initialParams || {} - } - - /** - * Creates a debounced search function for a specific entity type - */ - private createDebouncedSearch( - type: 'all' | 'weapons' | 'characters' | 'summons' - ) { - const searchFn = async (params: SearchParams) => { - // Cancel any existing request for this type - this.cancelSearch(type) - - // Create new abort controller - const controller = new AbortController() - this.activeRequests.set(type, controller) - - // Update loading state - this[type] = { ...this[type], loading: true } - - try { - // Merge base params with provided params - const mergedParams = { ...this.baseParams, ...params } - - // Call appropriate adapter method - let response: SearchResponse - switch (type) { - case 'all': - response = await this.adapter.searchAll(mergedParams) - break - case 'weapons': - response = await this.adapter.searchWeapons(mergedParams) - break - case 'characters': - response = await this.adapter.searchCharacters(mergedParams) - break - case 'summons': - response = await this.adapter.searchSummons(mergedParams) - break - } - - // Update state with results - this[type] = { data: response, loading: false } - } catch (error: any) { - // Don't update state if request was cancelled - if (error.code !== 'CANCELLED') { - this[type] = { - ...this[type], - loading: false, - error: error as AdapterError - } - } - } finally { - this.activeRequests.delete(type) - } - } - - // Create a debounced wrapper using useDebounce - const debouncedSearch = useDebounce( - (params: SearchParams) => searchFn(params), - () => this.debounceMs - ) - - // Return a function that calls the debounced search - return (params: SearchParams) => debouncedSearch(params) - } - - // Create debounced search methods - searchAll = this.createDebouncedSearch('all') - searchWeapons = this.createDebouncedSearch('weapons') - searchCharacters = this.createDebouncedSearch('characters') - searchSummons = this.createDebouncedSearch('summons') - - /** - * Cancels an active search request - */ - cancelSearch(type: 'all' | 'weapons' | 'characters' | 'summons') { - const controller = this.activeRequests.get(type) - if (controller) { - controller.abort() - this.activeRequests.delete(type) - } - } - - /** - * Cancels all active search requests - */ - cancelAll() { - this.activeRequests.forEach(controller => controller.abort()) - this.activeRequests.clear() - } - - /** - * Clears results for a specific search type - */ - clear(type: 'all' | 'weapons' | 'characters' | 'summons') { - this.cancelSearch(type) - this[type] = { loading: false } - } - - /** - * Clears all search results - */ - clearAll() { - this.cancelAll() - this.all = { loading: false } - this.weapons = { loading: false } - this.characters = { loading: false } - this.summons = { loading: false } - } - - /** - * Clears the adapter's cache - */ - clearCache() { - this.adapter.clearSearchCache() - } - - /** - * Updates base parameters for all searches - */ - updateBaseParams(params: SearchParams) { - this.baseParams = { ...this.baseParams, ...params } - } -} - -/** - * Factory function for creating search resources - * Provides a more functional API if preferred over class instantiation - */ -export function createSearchResource(options?: SearchResourceOptions): SearchResource { - return new SearchResource(options) -}