## Overview Complete migration from service layer to TanStack Query v6 with mutations, queries, and automatic cache management. ## What Was Completed ### Phase 1: Entity Queries & Database Pages ✅ - Created `entity.queries.ts` with query options for weapons, characters, and summons - Migrated all database detail pages to use TanStack Query with `withInitialData()` pattern - SSR with client-side hydration working correctly ### Phase 2: Server Load Cleanup ✅ - Removed PartyService dependency from teams detail server load - Server loads now use adapters directly instead of service layer - Cleaner separation of concerns ### Phase 3: Party.svelte Refactoring ✅ - Removed all PartyService, GridService, and ConflictService dependencies - Migrated to TanStack Query mutations for all operations: - Grid operations: create, update, delete, swap, move - Party operations: update, delete, remix, favorite, unfavorite - Added swap/move mutations for drag-and-drop operations - Automatic cache invalidation and query refetching ### Phase 4: Service Layer Removal ✅ - Deleted all service layer files (~1,345 lines removed): - `party.service.ts` (620 lines) - `grid.service.ts` (450 lines) - `conflict.service.ts` (120 lines) - `gridOperations.ts` (unused utility) - Deleted empty `services/` directory - Created utility functions for cross-cutting concerns: - `localId.ts`: Anonymous user local ID management - `editKeys.ts`: Edit key management for anonymous editing - `party-context.ts`: Extracted PartyContext type ### Phase 5: /teams/new Migration ✅ - Migrated party creation wizard to use TanStack Query mutations - Replaced all direct adapter calls with mutations (7 locations) - Maintains existing behavior and flow - Automatic cache invalidation for newly created parties ## Benefits ### Performance - Automatic request deduplication - Better cache utilization across pages - Background refetching for fresh data - Optimistic updates for instant UI feedback ### Developer Experience - Single source of truth for data fetching - Consistent patterns across entire app - Query devtools for debugging - Less boilerplate code ### Code Quality - ~1,088 net lines removed - Simpler mental model (no service layer) - Better TypeScript inference - Easier to test ### Architecture - **100% TanStack Query coverage** - no service layer, no direct adapter calls - Clear separation: UI ← Queries/Mutations ← Adapters ← API - Automatic cache management - Consistent mutation patterns everywhere ## Testing All features verified: - Party creation (anonymous & authenticated) - Grid operations (add, remove, update, swap, move) - Party operations (update, delete, remix, favorite) - Cache invalidation across tabs - Error handling and rollback - SSR with hydration ## Files Modified ### Created (3) - `src/lib/types/party-context.ts` - `src/lib/utils/editKeys.ts` - `src/lib/utils/localId.ts` ### Deleted (4) - `src/lib/services/party.service.ts` - `src/lib/services/grid.service.ts` - `src/lib/services/conflict.service.ts` - `src/lib/utils/gridOperations.ts` ### Modified (13) - `src/lib/api/mutations/grid.mutations.ts` - `src/lib/components/grids/CharacterGrid.svelte` - `src/lib/components/grids/SummonGrid.svelte` - `src/lib/components/grids/WeaponGrid.svelte` - `src/lib/components/party/Party.svelte` - `src/routes/teams/new/+page.svelte` - Database entity pages (characters, weapons, summons) - Other supporting files ## Migration Complete This PR completes the TanStack Query migration. The entire application now uses TanStack Query v6 for all data fetching and mutations, with zero remaining service layer code. --------- Co-authored-by: Claude <noreply@anthropic.com>
130 lines
2.9 KiB
Svelte
130 lines
2.9 KiB
Svelte
<svelte:options runes={true} />
|
|
|
|
<script lang="ts">
|
|
import type { GridCharacter } from '$lib/types/api/party'
|
|
import type { Job } from '$lib/types/api/entities'
|
|
import { getContext } from 'svelte'
|
|
import type { PartyContext } from '$lib/types/party-context'
|
|
import type { DragDropContext } from '$lib/composables/drag-drop.svelte'
|
|
import DraggableItem from '$lib/components/dnd/DraggableItem.svelte'
|
|
import DropZone from '$lib/components/dnd/DropZone.svelte'
|
|
|
|
interface Props {
|
|
characters?: GridCharacter[] | undefined
|
|
mainWeaponElement?: number | null | undefined
|
|
partyElement?: number | null | undefined
|
|
job?: Job | undefined
|
|
container?: string | undefined
|
|
}
|
|
|
|
let {
|
|
characters = [],
|
|
mainWeaponElement = undefined,
|
|
partyElement = undefined,
|
|
job = undefined,
|
|
container = 'main-characters'
|
|
}: Props = $props()
|
|
|
|
import CharacterUnit from '$lib/components/units/CharacterUnit.svelte'
|
|
|
|
const ctx = getContext<PartyContext>('party')
|
|
const dragContext = getContext<DragDropContext | undefined>('drag-drop')
|
|
|
|
// Create array with proper empty slots
|
|
let characterSlots = $derived.by(() => {
|
|
const slots: (GridCharacter | undefined)[] = Array(5).fill(undefined)
|
|
characters.forEach(char => {
|
|
if (char.position >= 0 && char.position < 5) {
|
|
slots[char.position] = char
|
|
}
|
|
})
|
|
return slots
|
|
})
|
|
</script>
|
|
|
|
<div class="wrapper">
|
|
<ul
|
|
class="characters"
|
|
aria-label="Character Grid"
|
|
>
|
|
{#each characterSlots as character, i}
|
|
<li
|
|
aria-label={`Character slot ${i}`}
|
|
class:main-character={i === 0}
|
|
class:Empty={!character}
|
|
>
|
|
{#if dragContext}
|
|
<DropZone
|
|
{container}
|
|
position={i}
|
|
type="character"
|
|
item={character}
|
|
canDrop={ctx?.canEdit() ?? false}
|
|
>
|
|
<DraggableItem
|
|
item={character}
|
|
{container}
|
|
position={i}
|
|
type="character"
|
|
canDrag={!!character && (ctx?.canEdit() ?? false)}
|
|
>
|
|
<CharacterUnit
|
|
item={character}
|
|
position={i}
|
|
{mainWeaponElement}
|
|
{partyElement}
|
|
job={i === 0 ? job : undefined}
|
|
/>
|
|
</DraggableItem>
|
|
</DropZone>
|
|
{:else}
|
|
<CharacterUnit
|
|
item={character}
|
|
position={i}
|
|
{mainWeaponElement}
|
|
{partyElement}
|
|
job={i === 0 ? job : undefined}
|
|
/>
|
|
{/if}
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
</div>
|
|
|
|
<style lang="scss">
|
|
@use '$src/themes/colors' as *;
|
|
@use '$src/themes/typography' as *;
|
|
@use '$src/themes/spacing' as *;
|
|
|
|
.characters {
|
|
display: grid;
|
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
|
gap: $unit-3x;
|
|
|
|
& > li {
|
|
list-style: none;
|
|
}
|
|
}
|
|
|
|
.unit {
|
|
width: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: $unit;
|
|
}
|
|
|
|
.image {
|
|
width: 100%;
|
|
height: auto;
|
|
border: 1px solid $grey-75;
|
|
border-radius: 8px;
|
|
display: block;
|
|
}
|
|
|
|
.name {
|
|
font-size: $font-small;
|
|
text-align: center;
|
|
color: $grey-50;
|
|
}
|
|
</style>
|