From f66b31fea8b6bac873becc2617cc8709c626bbe4 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Sat, 29 Nov 2025 17:50:11 -0800 Subject: [PATCH] migrate /teams/new to query-based architecture --- src/routes/teams/new/+page.svelte | 257 +++++++++++++----------------- 1 file changed, 108 insertions(+), 149 deletions(-) diff --git a/src/routes/teams/new/+page.svelte b/src/routes/teams/new/+page.svelte index 0c1661a0..219d3f77 100644 --- a/src/routes/teams/new/+page.svelte +++ b/src/routes/teams/new/+page.svelte @@ -13,13 +13,22 @@ import { partyAdapter, gridAdapter } from '$lib/api/adapters' import { getLocalId } from '$lib/utils/localId' import { storeEditKey } from '$lib/utils/editKeys' + import type { Party } from '$lib/types/api/party' + + // TanStack Query + import { createQuery, useQueryClient } from '@tanstack/svelte-query' + import { partyQueries } from '$lib/api/queries/party.queries' + import { partyKeys } from '$lib/api/queries/party.queries' // TanStack Query mutations import { useCreateParty } from '$lib/api/mutations/party.mutations' import { useCreateGridWeapon, useCreateGridCharacter, - useCreateGridSummon + useCreateGridSummon, + useDeleteGridWeapon, + useDeleteGridCharacter, + useDeleteGridSummon } from '$lib/api/mutations/grid.mutations' import { Dialog } from 'bits-ui' import { replaceState } from '$app/navigation' @@ -73,19 +82,42 @@ return characters.length >= 5 } - // Grid state - let weapons = $state([]) - let summons = $state([]) - let characters = $state([]) - let selectedSlot = $state(null) - let isFirstItemForSlot = false // Track if this is the first item after clicking empty cell - // Party state let partyId = $state(null) let shortcode = $state(null) let editKey = $state(null) let isCreatingParty = $state(false) + // Placeholder party for 'new' route + const placeholderParty: Party = { + id: 'new', + shortcode: 'new', + name: 'New Team', + description: '', + weapons: [], + summons: [], + characters: [], + element: 0, + visibility: 1 + } + + // Create query with placeholder data + const queryClient = useQueryClient() + const partyQuery = createQuery(() => ({ + ...partyQueries.byShortcode(shortcode || 'new'), + initialData: placeholderParty, + enabled: false // Disable automatic fetching for 'new' party + })) + + // Derive state from query + const party = $derived(partyQuery.data ?? placeholderParty) + const weapons = $derived(party.weapons ?? []) + const summons = $derived(party.summons ?? []) + const characters = $derived(party.characters ?? []) + + let selectedSlot = $state(null) + let isFirstItemForSlot = false // Track if this is the first item after clicking empty cell + // Error dialog state let errorDialogOpen = $state(false) let errorMessage = $state('') @@ -96,6 +128,22 @@ const createWeaponMutation = useCreateGridWeapon() const createCharacterMutation = useCreateGridCharacter() const createSummonMutation = useCreateGridSummon() + const deleteWeapon = useDeleteGridWeapon() + const deleteCharacter = useDeleteGridCharacter() + const deleteSummon = useDeleteGridSummon() + + // Helper to add item to cache + function addItemToCache(itemType: 'weapons' | 'summons' | 'characters', item: any) { + const cacheKey = partyKeys.detail(shortcode || 'new') + + queryClient.setQueryData(cacheKey, (old: Party | undefined) => { + if (!old) return placeholderParty + return { + ...old, + [itemType]: [...(old[itemType] ?? []), item] + } + }) + } // Calculate if grids are full let isWeaponGridFull = $derived(weapons.length >= 10) // 1 mainhand + 9 grid slots @@ -156,6 +204,12 @@ throw new Error('Party creation did not return ID or shortcode') } + // Update the query cache with the created party + queryClient.setQueryData( + partyKeys.detail(createdParty.shortcode), + createdParty + ) + // Step 2: Add the first item to the party let position = selectedSlot !== null ? selectedSlot : -1 // Use selectedSlot if available let itemAdded = false @@ -174,8 +228,8 @@ console.log('Weapon added:', addResult) itemAdded = true - // Update local state with the added weapon - weapons = [...weapons, addResult] + // Update cache with the added weapon + addItemToCache('weapons', addResult) } else if (activeTab === GridType.Summon) { // Use selectedSlot if available, otherwise default to main summon if (selectedSlot === null) position = -1 @@ -189,8 +243,8 @@ console.log('Summon added:', addResult) itemAdded = true - // Update local state with the added summon - summons = [...summons, addResult] + // Update cache with the added summon + addItemToCache('summons', addResult) } else if (activeTab === GridType.Character) { // Use selectedSlot if available, otherwise default to first slot if (selectedSlot === null) position = 0 @@ -202,8 +256,8 @@ console.log('Character added:', addResult) itemAdded = true - // Update local state with the added character - characters = [...characters, addResult] + // Update cache with the added character + addItemToCache('characters', addResult) } selectedSlot = null // Reset after using @@ -290,8 +344,8 @@ mainhand: position === -1 }) - // Add to local state - weapons = [...weapons, response] + // Add to cache + addItemToCache('weapons', response) } else if (activeTab === GridType.Summon) { // Use selectedSlot for first item if available if (i === 0 && selectedSlot !== null && !summons.find(s => s.position === selectedSlot)) { @@ -314,8 +368,8 @@ friend: position === 6 }) - // Add to local state - summons = [...summons, response] + // Add to cache + addItemToCache('summons', response) } else if (activeTab === GridType.Character) { // Use selectedSlot for first item if available if (i === 0 && selectedSlot !== null && !characters.find(c => c.position === selectedSlot)) { @@ -336,8 +390,8 @@ position }) - // Add to local state - characters = [...characters, response] + // Add to cache + addItemToCache('characters', response) } } } catch (error: any) { @@ -348,143 +402,48 @@ } return } - - // Original local-only adding logic (before party creation) - if (activeTab === GridType.Weapon) { - // Add weapons to empty slots - const emptySlots = Array.from({ length: 10 }, (_, i) => i - 1) // -1 for mainhand, 0-8 for grid - .filter(i => !weapons.find(w => w.position === i)) - - items.forEach((item, index) => { - let position: number - // Use selectedSlot for first item if available - if (index === 0 && selectedSlot !== null && !weapons.find(w => w.position === selectedSlot)) { - position = selectedSlot - selectedSlot = null // Reset after using - } else { - // Find next empty slot - const availableSlots = emptySlots.filter(s => !weapons.find(w => w.position === s)) - if (availableSlots.length === 0) return - position = availableSlots[0]! - } - - const newWeapon = { - id: `temp-${Date.now()}-${index}`, - position, - object: { - granblueId: item.granblueId, - name: item.name, - element: item.element - }, - mainhand: position === -1 - } - console.log('Adding weapon:', newWeapon) - weapons = [...weapons, newWeapon] - }) - console.log('Updated weapons array:', weapons) - } else if (activeTab === GridType.Summon) { - // Add summons to empty slots - const emptySlots = [-1, 0, 1, 2, 3, 6] // main, 4 grid slots, friend - .filter(i => !summons.find(s => s.position === i)) - - items.forEach((item, index) => { - let position: number - // Use selectedSlot for first item if available - if (index === 0 && selectedSlot !== null && !summons.find(s => s.position === selectedSlot)) { - position = selectedSlot - selectedSlot = null // Reset after using - } else { - // Find next empty slot - const availableSlots = emptySlots.filter(s => !summons.find(sum => sum.position === s)) - if (availableSlots.length === 0) return - position = availableSlots[0]! - } - - summons = [...summons, { - id: `temp-${Date.now()}-${index}`, - position, - object: { - granblueId: item.granblueId, - name: item.name, - element: item.element - }, - main: position === -1, - friend: position === 6 - }] - }) - } else if (activeTab === GridType.Character) { - // Add characters to empty slots - const emptySlots = Array.from({ length: 5 }, (_, i) => i) - .filter(i => !characters.find(c => c.position === i)) - - items.forEach((item, index) => { - let position: number - // Use selectedSlot for first item if available - if (index === 0 && selectedSlot !== null && !characters.find(c => c.position === selectedSlot)) { - position = selectedSlot - selectedSlot = null // Reset after using - } else { - // Find next empty slot - const availableSlots = emptySlots.filter(s => !characters.find(c => c.position === s)) - if (availableSlots.length === 0) return - position = availableSlots[0]! - } - - characters = [...characters, { - id: `temp-${Date.now()}-${index}`, - position, - object: { - granblueId: item.granblueId, - name: item.name, - element: item.element - } - }] - }) - } } - // Remove functions - function removeWeapon(itemId: string) { - console.log('Removing weapon:', itemId) - weapons = weapons.filter(w => w.id !== itemId) - return Promise.resolve({ id: 'new', shortcode: 'new', weapons, summons, characters }) - } - function removeSummon(itemId: string) { - console.log('Removing summon:', itemId) - summons = summons.filter(s => s.id !== itemId) - return Promise.resolve({ id: 'new', shortcode: 'new', weapons, summons, characters }) - } - - function removeCharacter(itemId: string) { - console.log('Removing character:', itemId) - characters = characters.filter(c => c.id !== itemId) - return Promise.resolve({ id: 'new', shortcode: 'new', weapons, summons, characters }) - } - - // Provide a minimal party context so Unit components can render safely. + // Provide party context using query data setContext('party', { - getParty: () => ({ id: 'new', shortcode: 'new', weapons, summons, characters }), - updateParty: (updatedParty: any) => { - // Update the local state when party is updated - if (updatedParty.weapons) weapons = updatedParty.weapons - if (updatedParty.summons) summons = updatedParty.summons - if (updatedParty.characters) characters = updatedParty.characters + getParty: () => party, + updateParty: (p: Party) => { + // Update cache instead of local state + queryClient.setQueryData(partyKeys.detail(shortcode || 'new'), p) }, canEdit: () => true, + getEditKey: () => editKey, services: { gridService: { - removeWeapon: (partyId: string, itemId: string) => removeWeapon(itemId), - removeSummon: (partyId: string, itemId: string) => removeSummon(itemId), - removeCharacter: (partyId: string, itemId: string) => removeCharacter(itemId), - addWeapon: () => Promise.resolve({ party: { id: 'new', shortcode: 'new', weapons, summons, characters } }), - addSummon: () => Promise.resolve({ party: { id: 'new', shortcode: 'new', weapons, summons, characters } }), - addCharacter: () => Promise.resolve({ party: { id: 'new', shortcode: 'new', weapons, summons, characters } }), - replaceWeapon: () => Promise.resolve({ party: { id: 'new', shortcode: 'new', weapons, summons, characters } }), - replaceSummon: () => Promise.resolve({ party: { id: 'new', shortcode: 'new', weapons, summons, characters } }), - replaceCharacter: () => Promise.resolve({ party: { id: 'new', shortcode: 'new', weapons, summons, characters } }) - }, - partyService: { getEditKey: () => null } + removeWeapon: async (partyId: string, itemId: string) => { + if (!partyId || partyId === 'new') return party + await deleteWeapon.mutateAsync({ + id: itemId, + partyId, + partyShortcode: shortcode || 'new' + }) + return party + }, + removeSummon: async (partyId: string, itemId: string) => { + if (!partyId || partyId === 'new') return party + await deleteSummon.mutateAsync({ + id: itemId, + partyId, + partyShortcode: shortcode || 'new' + }) + return party + }, + removeCharacter: async (partyId: string, itemId: string) => { + if (!partyId || partyId === 'new') return party + await deleteCharacter.mutateAsync({ + id: itemId, + partyId, + partyShortcode: shortcode || 'new' + }) + return party + } + } }, openPicker: (opts: { type: 'weapon' | 'summon' | 'character'; position: number; item?: any }) => { selectedSlot = opts.position