hensei-web/src/lib/components/grids/CharacterGrid.svelte
Justin Edmund f457343e26
Complete TanStack Query v6 migration (#445)
## 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>
2025-11-29 22:32:15 -08:00

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>