diff --git a/src/lib/api/adapters/resources/party.resource.svelte.ts b/src/lib/api/adapters/resources/party.resource.svelte.ts
index f1141f4e..cf4af750 100644
--- a/src/lib/api/adapters/resources/party.resource.svelte.ts
+++ b/src/lib/api/adapters/resources/party.resource.svelte.ts
@@ -4,6 +4,27 @@
* 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
*/
diff --git a/src/lib/api/adapters/resources/search.resource.svelte.ts b/src/lib/api/adapters/resources/search.resource.svelte.ts
index 68c8d1a1..180d31e4 100644
--- a/src/lib/api/adapters/resources/search.resource.svelte.ts
+++ b/src/lib/api/adapters/resources/search.resource.svelte.ts
@@ -4,6 +4,23 @@
* 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
*/
@@ -227,4 +244,4 @@ export class SearchResource {
*/
export function createSearchResource(options?: SearchResourceOptions): SearchResource {
return new SearchResource(options)
-}
\ No newline at end of file
+}
diff --git a/src/lib/api/mutations/grid.mutations.ts b/src/lib/api/mutations/grid.mutations.ts
new file mode 100644
index 00000000..edb38f78
--- /dev/null
+++ b/src/lib/api/mutations/grid.mutations.ts
@@ -0,0 +1,568 @@
+/**
+ * Grid Mutation Configurations
+ *
+ * Provides mutation configurations for grid item operations (weapons, characters, summons)
+ * with cache invalidation and optimistic updates using TanStack Query v6.
+ *
+ * @module api/mutations/grid
+ */
+
+import { useQueryClient, createMutation } from '@tanstack/svelte-query'
+import {
+ gridAdapter,
+ type CreateGridWeaponParams,
+ type CreateGridCharacterParams,
+ type CreateGridSummonParams,
+ type UpdateUncapParams,
+ type ResolveConflictParams
+} from '$lib/api/adapters/grid.adapter'
+import { partyKeys } from '$lib/api/queries/party.queries'
+import type { Party, GridWeapon, GridCharacter, GridSummon } from '$lib/types/api/party'
+
+// ============================================================================
+// Weapon Mutations
+// ============================================================================
+
+/**
+ * Create grid weapon mutation
+ *
+ * Adds a weapon to a party's grid.
+ *
+ * @example
+ * ```svelte
+ *
+ * ```
+ */
+export function useCreateGridWeapon() {
+ const queryClient = useQueryClient()
+
+ return createMutation(() => ({
+ mutationFn: (params: CreateGridWeaponParams) => gridAdapter.createWeapon(params),
+ onSuccess: (_data, params) => {
+ // Invalidate the party to refetch with new weapon
+ queryClient.invalidateQueries({ queryKey: partyKeys.detail(params.partyId) })
+ }
+ }))
+}
+
+/**
+ * Update grid weapon mutation
+ *
+ * Updates a weapon in a party's grid with optimistic updates.
+ *
+ * @example
+ * ```svelte
+ *
+ * ```
+ */
+export function useUpdateGridWeapon() {
+ const queryClient = useQueryClient()
+
+ return createMutation(() => ({
+ mutationFn: ({ id, updates }: { id: string; partyShortcode: string; updates: Partial }) =>
+ gridAdapter.updateWeapon(id, updates),
+ onMutate: async ({ id, partyShortcode, updates }) => {
+ await queryClient.cancelQueries({ queryKey: partyKeys.detail(partyShortcode) })
+
+ const previousParty = queryClient.getQueryData(partyKeys.detail(partyShortcode))
+
+ if (previousParty?.weapons) {
+ const updatedWeapons = previousParty.weapons.map((w) =>
+ w.id === id ? { ...w, ...updates } : w
+ )
+ queryClient.setQueryData(partyKeys.detail(partyShortcode), {
+ ...previousParty,
+ weapons: updatedWeapons
+ })
+ }
+
+ return { previousParty }
+ },
+ onError: (_err, { partyShortcode }, context) => {
+ if (context?.previousParty) {
+ queryClient.setQueryData(partyKeys.detail(partyShortcode), context.previousParty)
+ }
+ },
+ onSettled: (_data, _err, { partyShortcode }) => {
+ queryClient.invalidateQueries({ queryKey: partyKeys.detail(partyShortcode) })
+ }
+ }))
+}
+
+/**
+ * Delete grid weapon mutation
+ *
+ * Removes a weapon from a party's grid.
+ */
+export function useDeleteGridWeapon() {
+ const queryClient = useQueryClient()
+
+ return createMutation(() => ({
+ mutationFn: (params: { id?: string; partyId: string; partyShortcode: string; position?: number }) =>
+ gridAdapter.deleteWeapon({ id: params.id, partyId: params.partyId, position: params.position }),
+ onMutate: async ({ partyShortcode, id, position }) => {
+ await queryClient.cancelQueries({ queryKey: partyKeys.detail(partyShortcode) })
+
+ const previousParty = queryClient.getQueryData(partyKeys.detail(partyShortcode))
+
+ if (previousParty?.weapons) {
+ const updatedWeapons = previousParty.weapons.filter((w) =>
+ id ? w.id !== id : w.position !== position
+ )
+ queryClient.setQueryData(partyKeys.detail(partyShortcode), {
+ ...previousParty,
+ weapons: updatedWeapons
+ })
+ }
+
+ return { previousParty }
+ },
+ onError: (_err, { partyShortcode }, context) => {
+ if (context?.previousParty) {
+ queryClient.setQueryData(partyKeys.detail(partyShortcode), context.previousParty)
+ }
+ },
+ onSettled: (_data, _err, { partyShortcode }) => {
+ queryClient.invalidateQueries({ queryKey: partyKeys.detail(partyShortcode) })
+ }
+ }))
+}
+
+/**
+ * Update weapon uncap mutation
+ *
+ * Updates a weapon's uncap level with optimistic updates.
+ */
+export function useUpdateWeaponUncap() {
+ const queryClient = useQueryClient()
+
+ return createMutation(() => ({
+ mutationFn: (params: UpdateUncapParams & { partyShortcode: string }) =>
+ gridAdapter.updateWeaponUncap(params),
+ onMutate: async ({ partyShortcode, id, uncapLevel, transcendenceStep }) => {
+ await queryClient.cancelQueries({ queryKey: partyKeys.detail(partyShortcode) })
+
+ const previousParty = queryClient.getQueryData(partyKeys.detail(partyShortcode))
+
+ if (previousParty?.weapons) {
+ const updatedWeapons = previousParty.weapons.map((w) =>
+ w.id === id
+ ? {
+ ...w,
+ uncapLevel,
+ ...(transcendenceStep !== undefined && { transcendenceStep })
+ }
+ : w
+ )
+ queryClient.setQueryData(partyKeys.detail(partyShortcode), {
+ ...previousParty,
+ weapons: updatedWeapons
+ })
+ }
+
+ return { previousParty }
+ },
+ onError: (_err, { partyShortcode }, context) => {
+ if (context?.previousParty) {
+ queryClient.setQueryData(partyKeys.detail(partyShortcode), context.previousParty)
+ }
+ },
+ onSettled: (_data, _err, { partyShortcode }) => {
+ queryClient.invalidateQueries({ queryKey: partyKeys.detail(partyShortcode) })
+ }
+ }))
+}
+
+/**
+ * Resolve weapon conflict mutation
+ *
+ * Resolves conflicts when adding a weapon that conflicts with existing weapons.
+ */
+export function useResolveWeaponConflict() {
+ const queryClient = useQueryClient()
+
+ return createMutation(() => ({
+ mutationFn: (params: ResolveConflictParams & { partyShortcode: string }) =>
+ gridAdapter.resolveWeaponConflict(params),
+ onSuccess: (_data, { partyShortcode }) => {
+ queryClient.invalidateQueries({ queryKey: partyKeys.detail(partyShortcode) })
+ }
+ }))
+}
+
+// ============================================================================
+// Character Mutations
+// ============================================================================
+
+/**
+ * Create grid character mutation
+ *
+ * Adds a character to a party's grid.
+ */
+export function useCreateGridCharacter() {
+ const queryClient = useQueryClient()
+
+ return createMutation(() => ({
+ mutationFn: (params: CreateGridCharacterParams) => gridAdapter.createCharacter(params),
+ onSuccess: (_data, params) => {
+ queryClient.invalidateQueries({ queryKey: partyKeys.detail(params.partyId) })
+ }
+ }))
+}
+
+/**
+ * Update grid character mutation
+ *
+ * Updates a character in a party's grid with optimistic updates.
+ */
+export function useUpdateGridCharacter() {
+ const queryClient = useQueryClient()
+
+ return createMutation(() => ({
+ mutationFn: ({ id, updates }: { id: string; partyShortcode: string; updates: Partial }) =>
+ gridAdapter.updateCharacter(id, updates),
+ onMutate: async ({ id, partyShortcode, updates }) => {
+ await queryClient.cancelQueries({ queryKey: partyKeys.detail(partyShortcode) })
+
+ const previousParty = queryClient.getQueryData(partyKeys.detail(partyShortcode))
+
+ if (previousParty?.characters) {
+ const updatedCharacters = previousParty.characters.map((c) =>
+ c.id === id ? { ...c, ...updates } : c
+ )
+ queryClient.setQueryData(partyKeys.detail(partyShortcode), {
+ ...previousParty,
+ characters: updatedCharacters
+ })
+ }
+
+ return { previousParty }
+ },
+ onError: (_err, { partyShortcode }, context) => {
+ if (context?.previousParty) {
+ queryClient.setQueryData(partyKeys.detail(partyShortcode), context.previousParty)
+ }
+ },
+ onSettled: (_data, _err, { partyShortcode }) => {
+ queryClient.invalidateQueries({ queryKey: partyKeys.detail(partyShortcode) })
+ }
+ }))
+}
+
+/**
+ * Delete grid character mutation
+ *
+ * Removes a character from a party's grid.
+ */
+export function useDeleteGridCharacter() {
+ const queryClient = useQueryClient()
+
+ return createMutation(() => ({
+ mutationFn: (params: { id?: string; partyId: string; partyShortcode: string; position?: number }) =>
+ gridAdapter.deleteCharacter({ id: params.id, partyId: params.partyId, position: params.position }),
+ onMutate: async ({ partyShortcode, id, position }) => {
+ await queryClient.cancelQueries({ queryKey: partyKeys.detail(partyShortcode) })
+
+ const previousParty = queryClient.getQueryData(partyKeys.detail(partyShortcode))
+
+ if (previousParty?.characters) {
+ const updatedCharacters = previousParty.characters.filter((c) =>
+ id ? c.id !== id : c.position !== position
+ )
+ queryClient.setQueryData(partyKeys.detail(partyShortcode), {
+ ...previousParty,
+ characters: updatedCharacters
+ })
+ }
+
+ return { previousParty }
+ },
+ onError: (_err, { partyShortcode }, context) => {
+ if (context?.previousParty) {
+ queryClient.setQueryData(partyKeys.detail(partyShortcode), context.previousParty)
+ }
+ },
+ onSettled: (_data, _err, { partyShortcode }) => {
+ queryClient.invalidateQueries({ queryKey: partyKeys.detail(partyShortcode) })
+ }
+ }))
+}
+
+/**
+ * Update character uncap mutation
+ *
+ * Updates a character's uncap level with optimistic updates.
+ */
+export function useUpdateCharacterUncap() {
+ const queryClient = useQueryClient()
+
+ return createMutation(() => ({
+ mutationFn: (params: UpdateUncapParams & { partyShortcode: string }) =>
+ gridAdapter.updateCharacterUncap(params),
+ onMutate: async ({ partyShortcode, id, uncapLevel, transcendenceStep }) => {
+ await queryClient.cancelQueries({ queryKey: partyKeys.detail(partyShortcode) })
+
+ const previousParty = queryClient.getQueryData(partyKeys.detail(partyShortcode))
+
+ if (previousParty?.characters) {
+ const updatedCharacters = previousParty.characters.map((c) =>
+ c.id === id
+ ? {
+ ...c,
+ uncapLevel,
+ ...(transcendenceStep !== undefined && { transcendenceStep })
+ }
+ : c
+ )
+ queryClient.setQueryData(partyKeys.detail(partyShortcode), {
+ ...previousParty,
+ characters: updatedCharacters
+ })
+ }
+
+ return { previousParty }
+ },
+ onError: (_err, { partyShortcode }, context) => {
+ if (context?.previousParty) {
+ queryClient.setQueryData(partyKeys.detail(partyShortcode), context.previousParty)
+ }
+ },
+ onSettled: (_data, _err, { partyShortcode }) => {
+ queryClient.invalidateQueries({ queryKey: partyKeys.detail(partyShortcode) })
+ }
+ }))
+}
+
+/**
+ * Resolve character conflict mutation
+ *
+ * Resolves conflicts when adding a character that conflicts with existing characters.
+ */
+export function useResolveCharacterConflict() {
+ const queryClient = useQueryClient()
+
+ return createMutation(() => ({
+ mutationFn: (params: ResolveConflictParams & { partyShortcode: string }) =>
+ gridAdapter.resolveCharacterConflict(params),
+ onSuccess: (_data, { partyShortcode }) => {
+ queryClient.invalidateQueries({ queryKey: partyKeys.detail(partyShortcode) })
+ }
+ }))
+}
+
+// ============================================================================
+// Summon Mutations
+// ============================================================================
+
+/**
+ * Create grid summon mutation
+ *
+ * Adds a summon to a party's grid.
+ */
+export function useCreateGridSummon() {
+ const queryClient = useQueryClient()
+
+ return createMutation(() => ({
+ mutationFn: (params: CreateGridSummonParams) => gridAdapter.createSummon(params),
+ onSuccess: (_data, params) => {
+ queryClient.invalidateQueries({ queryKey: partyKeys.detail(params.partyId) })
+ }
+ }))
+}
+
+/**
+ * Update grid summon mutation
+ *
+ * Updates a summon in a party's grid with optimistic updates.
+ */
+export function useUpdateGridSummon() {
+ const queryClient = useQueryClient()
+
+ return createMutation(() => ({
+ mutationFn: ({ id, updates }: { id: string; partyShortcode: string; updates: Partial }) =>
+ gridAdapter.updateSummon(id, updates),
+ onMutate: async ({ id, partyShortcode, updates }) => {
+ await queryClient.cancelQueries({ queryKey: partyKeys.detail(partyShortcode) })
+
+ const previousParty = queryClient.getQueryData(partyKeys.detail(partyShortcode))
+
+ if (previousParty?.summons) {
+ const updatedSummons = previousParty.summons.map((s) =>
+ s.id === id ? { ...s, ...updates } : s
+ )
+ queryClient.setQueryData(partyKeys.detail(partyShortcode), {
+ ...previousParty,
+ summons: updatedSummons
+ })
+ }
+
+ return { previousParty }
+ },
+ onError: (_err, { partyShortcode }, context) => {
+ if (context?.previousParty) {
+ queryClient.setQueryData(partyKeys.detail(partyShortcode), context.previousParty)
+ }
+ },
+ onSettled: (_data, _err, { partyShortcode }) => {
+ queryClient.invalidateQueries({ queryKey: partyKeys.detail(partyShortcode) })
+ }
+ }))
+}
+
+/**
+ * Delete grid summon mutation
+ *
+ * Removes a summon from a party's grid.
+ */
+export function useDeleteGridSummon() {
+ const queryClient = useQueryClient()
+
+ return createMutation(() => ({
+ mutationFn: (params: { id?: string; partyId: string; partyShortcode: string; position?: number }) =>
+ gridAdapter.deleteSummon({ id: params.id, partyId: params.partyId, position: params.position }),
+ onMutate: async ({ partyShortcode, id, position }) => {
+ await queryClient.cancelQueries({ queryKey: partyKeys.detail(partyShortcode) })
+
+ const previousParty = queryClient.getQueryData(partyKeys.detail(partyShortcode))
+
+ if (previousParty?.summons) {
+ const updatedSummons = previousParty.summons.filter((s) =>
+ id ? s.id !== id : s.position !== position
+ )
+ queryClient.setQueryData(partyKeys.detail(partyShortcode), {
+ ...previousParty,
+ summons: updatedSummons
+ })
+ }
+
+ return { previousParty }
+ },
+ onError: (_err, { partyShortcode }, context) => {
+ if (context?.previousParty) {
+ queryClient.setQueryData(partyKeys.detail(partyShortcode), context.previousParty)
+ }
+ },
+ onSettled: (_data, _err, { partyShortcode }) => {
+ queryClient.invalidateQueries({ queryKey: partyKeys.detail(partyShortcode) })
+ }
+ }))
+}
+
+/**
+ * Update summon uncap mutation
+ *
+ * Updates a summon's uncap level with optimistic updates.
+ */
+export function useUpdateSummonUncap() {
+ const queryClient = useQueryClient()
+
+ return createMutation(() => ({
+ mutationFn: (params: UpdateUncapParams & { partyShortcode: string }) =>
+ gridAdapter.updateSummonUncap(params),
+ onMutate: async ({ partyShortcode, id, uncapLevel, transcendenceStep }) => {
+ await queryClient.cancelQueries({ queryKey: partyKeys.detail(partyShortcode) })
+
+ const previousParty = queryClient.getQueryData(partyKeys.detail(partyShortcode))
+
+ if (previousParty?.summons) {
+ const updatedSummons = previousParty.summons.map((s) =>
+ s.id === id
+ ? {
+ ...s,
+ uncapLevel,
+ ...(transcendenceStep !== undefined && { transcendenceStep })
+ }
+ : s
+ )
+ queryClient.setQueryData(partyKeys.detail(partyShortcode), {
+ ...previousParty,
+ summons: updatedSummons
+ })
+ }
+
+ return { previousParty }
+ },
+ onError: (_err, { partyShortcode }, context) => {
+ if (context?.previousParty) {
+ queryClient.setQueryData(partyKeys.detail(partyShortcode), context.previousParty)
+ }
+ },
+ onSettled: (_data, _err, { partyShortcode }) => {
+ queryClient.invalidateQueries({ queryKey: partyKeys.detail(partyShortcode) })
+ }
+ }))
+}
+
+/**
+ * Update quick summon mutation
+ *
+ * Updates a summon's quick summon setting with optimistic updates.
+ */
+export function useUpdateQuickSummon() {
+ const queryClient = useQueryClient()
+
+ return createMutation(() => ({
+ mutationFn: (params: {
+ id?: string
+ partyId: string
+ partyShortcode: string
+ position?: number
+ quickSummon: boolean
+ }) =>
+ gridAdapter.updateQuickSummon({
+ id: params.id,
+ partyId: params.partyId,
+ position: params.position,
+ quickSummon: params.quickSummon
+ }),
+ onMutate: async ({ partyShortcode, id, quickSummon }) => {
+ await queryClient.cancelQueries({ queryKey: partyKeys.detail(partyShortcode) })
+
+ const previousParty = queryClient.getQueryData(partyKeys.detail(partyShortcode))
+
+ if (previousParty?.summons) {
+ const updatedSummons = previousParty.summons.map((s) =>
+ s.id === id ? { ...s, quickSummon } : s
+ )
+ queryClient.setQueryData(partyKeys.detail(partyShortcode), {
+ ...previousParty,
+ summons: updatedSummons
+ })
+ }
+
+ return { previousParty }
+ },
+ onError: (_err, { partyShortcode }, context) => {
+ if (context?.previousParty) {
+ queryClient.setQueryData(partyKeys.detail(partyShortcode), context.previousParty)
+ }
+ },
+ onSettled: (_data, _err, { partyShortcode }) => {
+ queryClient.invalidateQueries({ queryKey: partyKeys.detail(partyShortcode) })
+ }
+ }))
+}
diff --git a/src/lib/api/mutations/job.mutations.ts b/src/lib/api/mutations/job.mutations.ts
new file mode 100644
index 00000000..1b6cd7db
--- /dev/null
+++ b/src/lib/api/mutations/job.mutations.ts
@@ -0,0 +1,182 @@
+/**
+ * Job Mutation Configurations
+ *
+ * Provides mutation configurations for job-related operations
+ * with cache invalidation using TanStack Query v6.
+ *
+ * @module api/mutations/job
+ */
+
+import { useQueryClient, createMutation } from '@tanstack/svelte-query'
+import { partyAdapter } from '$lib/api/adapters/party.adapter'
+import { partyKeys } from '$lib/api/queries/party.queries'
+import type { Party } from '$lib/types/api/party'
+
+/**
+ * Update party job mutation
+ *
+ * Updates the job for a party with optimistic updates.
+ *
+ * @example
+ * ```svelte
+ *
+ * ```
+ */
+export function useUpdatePartyJob() {
+ const queryClient = useQueryClient()
+
+ return createMutation(() => ({
+ mutationFn: ({ shortcode, jobId }: { shortcode: string; jobId: string }) =>
+ partyAdapter.updateJob(shortcode, jobId),
+ onMutate: async ({ shortcode, jobId }) => {
+ await queryClient.cancelQueries({ queryKey: partyKeys.detail(shortcode) })
+
+ const previousParty = queryClient.getQueryData(partyKeys.detail(shortcode))
+
+ // Optimistically update the job ID
+ // Note: We don't have the full job object here, so we just update the ID
+ // The full job will be fetched when the query is invalidated
+ if (previousParty) {
+ queryClient.setQueryData(partyKeys.detail(shortcode), {
+ ...previousParty,
+ job: previousParty.job ? { ...previousParty.job, id: jobId } : undefined
+ })
+ }
+
+ return { previousParty }
+ },
+ onError: (_err, { shortcode }, context) => {
+ if (context?.previousParty) {
+ queryClient.setQueryData(partyKeys.detail(shortcode), context.previousParty)
+ }
+ },
+ onSettled: (_data, _err, { shortcode }) => {
+ queryClient.invalidateQueries({ queryKey: partyKeys.detail(shortcode) })
+ }
+ }))
+}
+
+/**
+ * Update party job skills mutation
+ *
+ * Updates the job skills for a party.
+ *
+ * @example
+ * ```svelte
+ *
+ * ```
+ */
+export function useUpdatePartyJobSkills() {
+ const queryClient = useQueryClient()
+
+ return createMutation(() => ({
+ mutationFn: ({
+ shortcode,
+ skills
+ }: {
+ shortcode: string
+ skills: Array<{ id: string; slot: number }>
+ }) => partyAdapter.updateJobSkills(shortcode, skills),
+ onSuccess: (_data, { shortcode }) => {
+ queryClient.invalidateQueries({ queryKey: partyKeys.detail(shortcode) })
+ }
+ }))
+}
+
+/**
+ * Remove party job skill mutation
+ *
+ * Removes a job skill from a party.
+ *
+ * @example
+ * ```svelte
+ *
+ * ```
+ */
+export function useRemovePartyJobSkill() {
+ const queryClient = useQueryClient()
+
+ return createMutation(() => ({
+ mutationFn: ({ shortcode, slot }: { shortcode: string; slot: number }) =>
+ partyAdapter.removeJobSkill(shortcode, slot),
+ onMutate: async ({ shortcode, slot }) => {
+ await queryClient.cancelQueries({ queryKey: partyKeys.detail(shortcode) })
+
+ const previousParty = queryClient.getQueryData(partyKeys.detail(shortcode))
+
+ // Optimistically remove the skill from the slot
+ if (previousParty?.jobSkills) {
+ const updatedSkills = { ...previousParty.jobSkills }
+ delete updatedSkills[slot]
+ queryClient.setQueryData(partyKeys.detail(shortcode), {
+ ...previousParty,
+ jobSkills: updatedSkills
+ })
+ }
+
+ return { previousParty }
+ },
+ onError: (_err, { shortcode }, context) => {
+ if (context?.previousParty) {
+ queryClient.setQueryData(partyKeys.detail(shortcode), context.previousParty)
+ }
+ },
+ onSettled: (_data, _err, { shortcode }) => {
+ queryClient.invalidateQueries({ queryKey: partyKeys.detail(shortcode) })
+ }
+ }))
+}
+
+/**
+ * Update party accessory mutation
+ *
+ * Updates the accessory for a party.
+ *
+ * @example
+ * ```svelte
+ *
+ * ```
+ */
+export function useUpdatePartyAccessory() {
+ const queryClient = useQueryClient()
+
+ return createMutation(() => ({
+ mutationFn: ({ shortcode, accessoryId }: { shortcode: string; accessoryId: string }) =>
+ partyAdapter.updateAccessory(shortcode, accessoryId),
+ onSuccess: (_data, { shortcode }) => {
+ queryClient.invalidateQueries({ queryKey: partyKeys.detail(shortcode) })
+ }
+ }))
+}
diff --git a/src/lib/api/mutations/party.mutations.ts b/src/lib/api/mutations/party.mutations.ts
new file mode 100644
index 00000000..0a035273
--- /dev/null
+++ b/src/lib/api/mutations/party.mutations.ts
@@ -0,0 +1,313 @@
+/**
+ * Party Mutation Configurations
+ *
+ * Provides mutation configurations for party CRUD operations
+ * with cache invalidation and optimistic updates using TanStack Query v6.
+ *
+ * @module api/mutations/party
+ */
+
+import { useQueryClient, createMutation } from '@tanstack/svelte-query'
+import {
+ partyAdapter,
+ type CreatePartyParams,
+ type UpdatePartyParams
+} from '$lib/api/adapters/party.adapter'
+import { partyKeys } from '$lib/api/queries/party.queries'
+import { userKeys } from '$lib/api/queries/user.queries'
+import type { Party } from '$lib/types/api/party'
+
+/**
+ * Create party mutation
+ *
+ * Creates a new party and invalidates relevant caches.
+ *
+ * @example
+ * ```svelte
+ *
+ * ```
+ */
+export function useCreateParty() {
+ const queryClient = useQueryClient()
+
+ return createMutation(() => ({
+ mutationFn: (params: CreatePartyParams) => partyAdapter.create(params),
+ onSuccess: (party) => {
+ // Set the new party in cache
+ queryClient.setQueryData(partyKeys.detail(party.shortcode), party)
+ // Invalidate party lists to include the new party
+ queryClient.invalidateQueries({ queryKey: partyKeys.lists() })
+ // Invalidate user's party lists
+ queryClient.invalidateQueries({ queryKey: userKeys.all })
+ }
+ }))
+}
+
+/**
+ * Update party mutation
+ *
+ * Updates an existing party with optimistic updates.
+ *
+ * @example
+ * ```svelte
+ *
+ * ```
+ */
+export function useUpdateParty() {
+ const queryClient = useQueryClient()
+
+ return createMutation(() => ({
+ mutationFn: (params: UpdatePartyParams) => partyAdapter.update(params),
+ onMutate: async (params) => {
+ // Cancel any outgoing refetches
+ await queryClient.cancelQueries({ queryKey: partyKeys.detail(params.shortcode) })
+
+ // Snapshot the previous value
+ const previousParty = queryClient.getQueryData(partyKeys.detail(params.shortcode))
+
+ // Optimistically update the cache
+ if (previousParty) {
+ queryClient.setQueryData(partyKeys.detail(params.shortcode), {
+ ...previousParty,
+ ...params
+ })
+ }
+
+ return { previousParty }
+ },
+ onError: (_err, params, context) => {
+ // Rollback on error
+ if (context?.previousParty) {
+ queryClient.setQueryData(partyKeys.detail(params.shortcode), context.previousParty)
+ }
+ },
+ onSettled: (_data, _err, params) => {
+ // Always refetch after error or success
+ queryClient.invalidateQueries({ queryKey: partyKeys.detail(params.shortcode) })
+ }
+ }))
+}
+
+/**
+ * Delete party mutation
+ *
+ * Deletes a party and removes it from all caches.
+ *
+ * @example
+ * ```svelte
+ *
+ * ```
+ */
+export function useDeleteParty() {
+ const queryClient = useQueryClient()
+
+ return createMutation(() => ({
+ mutationFn: (shortcode: string) => partyAdapter.delete(shortcode),
+ onSuccess: (_data, shortcode) => {
+ // Remove the party from cache
+ queryClient.removeQueries({ queryKey: partyKeys.detail(shortcode) })
+ // Invalidate party lists
+ queryClient.invalidateQueries({ queryKey: partyKeys.lists() })
+ // Invalidate user's party lists
+ queryClient.invalidateQueries({ queryKey: userKeys.all })
+ }
+ }))
+}
+
+/**
+ * Remix party mutation
+ *
+ * Creates a copy of an existing party.
+ *
+ * @example
+ * ```svelte
+ *
+ * ```
+ */
+export function useRemixParty() {
+ const queryClient = useQueryClient()
+
+ return createMutation(() => ({
+ mutationFn: (shortcode: string) => partyAdapter.remix(shortcode),
+ onSuccess: (newParty) => {
+ // Set the new party in cache
+ queryClient.setQueryData(partyKeys.detail(newParty.shortcode), newParty)
+ // Invalidate party lists to include the new party
+ queryClient.invalidateQueries({ queryKey: partyKeys.lists() })
+ // Invalidate user's party lists
+ queryClient.invalidateQueries({ queryKey: userKeys.all })
+ }
+ }))
+}
+
+/**
+ * Favorite party mutation
+ *
+ * Adds a party to the user's favorites.
+ *
+ * @example
+ * ```svelte
+ *
+ * ```
+ */
+export function useFavoriteParty() {
+ const queryClient = useQueryClient()
+
+ return createMutation(() => ({
+ mutationFn: (shortcode: string) => partyAdapter.favorite(shortcode),
+ onMutate: async (shortcode) => {
+ // Cancel any outgoing refetches
+ await queryClient.cancelQueries({ queryKey: partyKeys.detail(shortcode) })
+
+ // Snapshot the previous value
+ const previousParty = queryClient.getQueryData(partyKeys.detail(shortcode))
+
+ // Optimistically update the cache
+ if (previousParty) {
+ queryClient.setQueryData(partyKeys.detail(shortcode), {
+ ...previousParty,
+ favorited: true
+ })
+ }
+
+ return { previousParty }
+ },
+ onError: (_err, shortcode, context) => {
+ // Rollback on error
+ if (context?.previousParty) {
+ queryClient.setQueryData(partyKeys.detail(shortcode), context.previousParty)
+ }
+ },
+ onSettled: (_data, _err, shortcode) => {
+ // Invalidate favorites list
+ queryClient.invalidateQueries({ queryKey: userKeys.favorites() })
+ // Refetch the party to get accurate state
+ queryClient.invalidateQueries({ queryKey: partyKeys.detail(shortcode) })
+ }
+ }))
+}
+
+/**
+ * Unfavorite party mutation
+ *
+ * Removes a party from the user's favorites.
+ *
+ * @example
+ * ```svelte
+ *
+ * ```
+ */
+export function useUnfavoriteParty() {
+ const queryClient = useQueryClient()
+
+ return createMutation(() => ({
+ mutationFn: (shortcode: string) => partyAdapter.unfavorite(shortcode),
+ onMutate: async (shortcode) => {
+ // Cancel any outgoing refetches
+ await queryClient.cancelQueries({ queryKey: partyKeys.detail(shortcode) })
+
+ // Snapshot the previous value
+ const previousParty = queryClient.getQueryData(partyKeys.detail(shortcode))
+
+ // Optimistically update the cache
+ if (previousParty) {
+ queryClient.setQueryData(partyKeys.detail(shortcode), {
+ ...previousParty,
+ favorited: false
+ })
+ }
+
+ return { previousParty }
+ },
+ onError: (_err, shortcode, context) => {
+ // Rollback on error
+ if (context?.previousParty) {
+ queryClient.setQueryData(partyKeys.detail(shortcode), context.previousParty)
+ }
+ },
+ onSettled: (_data, _err, shortcode) => {
+ // Invalidate favorites list
+ queryClient.invalidateQueries({ queryKey: userKeys.favorites() })
+ // Refetch the party to get accurate state
+ queryClient.invalidateQueries({ queryKey: partyKeys.detail(shortcode) })
+ }
+ }))
+}
+
+/**
+ * Regenerate preview mutation
+ *
+ * Triggers regeneration of a party's preview image.
+ *
+ * @example
+ * ```svelte
+ *
+ * ```
+ */
+export function useRegeneratePreview() {
+ const queryClient = useQueryClient()
+
+ return createMutation(() => ({
+ mutationFn: (shortcode: string) => partyAdapter.regeneratePreview(shortcode),
+ onSuccess: (_data, shortcode) => {
+ // Invalidate preview status to trigger refetch
+ queryClient.invalidateQueries({ queryKey: partyKeys.preview(shortcode) })
+ }
+ }))
+}
diff --git a/src/lib/api/queries/job.queries.ts b/src/lib/api/queries/job.queries.ts
new file mode 100644
index 00000000..fb438513
--- /dev/null
+++ b/src/lib/api/queries/job.queries.ts
@@ -0,0 +1,182 @@
+/**
+ * Job Query Options Factory
+ *
+ * Provides type-safe, reusable query configurations for job operations
+ * using TanStack Query v6 patterns.
+ *
+ * @module api/queries/job
+ */
+
+import { queryOptions, infiniteQueryOptions } from '@tanstack/svelte-query'
+import {
+ jobAdapter,
+ type SearchJobSkillsParams
+} from '$lib/api/adapters/job.adapter'
+import type { Job, JobSkill, JobAccessory } from '$lib/types/api/entities'
+
+/**
+ * Standard page result format for job skill infinite queries
+ */
+export interface JobSkillPageResult {
+ results: JobSkill[]
+ page: number
+ totalPages: number
+ total: number
+}
+
+/**
+ * Job query options factory
+ *
+ * Provides query configurations for all job-related operations.
+ * These can be used with `createQuery`, `createInfiniteQuery`, or for prefetching.
+ *
+ * @example
+ * ```typescript
+ * import { createQuery, createInfiniteQuery } from '@tanstack/svelte-query'
+ * import { jobQueries } from '$lib/api/queries/job.queries'
+ *
+ * // All jobs
+ * const jobs = createQuery(() => jobQueries.list())
+ *
+ * // Skills for a specific job with infinite scroll
+ * const skills = createInfiniteQuery(() => jobQueries.skills(jobId, { query: searchTerm }))
+ * ```
+ */
+export const jobQueries = {
+ /**
+ * All jobs list query options
+ *
+ * @returns Query options for fetching all jobs
+ */
+ list: () =>
+ queryOptions({
+ queryKey: ['jobs'] as const,
+ queryFn: () => jobAdapter.getAll(),
+ staleTime: 1000 * 60 * 30, // 30 minutes - jobs rarely change
+ gcTime: 1000 * 60 * 60 // 1 hour
+ }),
+
+ /**
+ * Single job query options
+ *
+ * @param id - Job ID
+ * @returns Query options for fetching a single job
+ */
+ byId: (id: string) =>
+ queryOptions({
+ queryKey: ['jobs', id] as const,
+ queryFn: () => jobAdapter.getById(id),
+ enabled: !!id,
+ staleTime: 1000 * 60 * 30, // 30 minutes
+ gcTime: 1000 * 60 * 60 // 1 hour
+ }),
+
+ /**
+ * Job skills query options (non-paginated)
+ *
+ * @param jobId - Job ID to fetch skills for
+ * @returns Query options for fetching all skills for a job
+ */
+ skillsByJob: (jobId: string) =>
+ queryOptions({
+ queryKey: ['jobs', jobId, 'skills'] as const,
+ queryFn: () => jobAdapter.getSkills(jobId),
+ enabled: !!jobId,
+ staleTime: 1000 * 60 * 30, // 30 minutes
+ gcTime: 1000 * 60 * 60 // 1 hour
+ }),
+
+ /**
+ * Job skills search infinite query options
+ *
+ * @param jobId - Job ID to search skills for
+ * @param params - Optional search parameters (query, filters, locale)
+ * @returns Infinite query options for searching job skills
+ */
+ skills: (
+ jobId: string,
+ params?: Omit
+ ) =>
+ infiniteQueryOptions({
+ queryKey: ['jobs', jobId, 'skills', 'search', params] as const,
+ queryFn: async ({ pageParam }): Promise => {
+ const response = await jobAdapter.searchSkills({
+ jobId,
+ ...params,
+ page: pageParam
+ })
+ return {
+ results: response.results,
+ page: response.page,
+ totalPages: response.totalPages,
+ total: response.total
+ }
+ },
+ initialPageParam: 1,
+ getNextPageParam: (lastPage) => {
+ if (lastPage.page < lastPage.totalPages) {
+ return lastPage.page + 1
+ }
+ return undefined
+ },
+ enabled: !!jobId,
+ staleTime: 1000 * 60 * 5, // 5 minutes - search results can change
+ gcTime: 1000 * 60 * 15 // 15 minutes
+ }),
+
+ /**
+ * Job accessories query options
+ *
+ * @param jobId - Job ID to fetch accessories for
+ * @returns Query options for fetching job accessories
+ */
+ accessories: (jobId: string) =>
+ queryOptions({
+ queryKey: ['jobs', jobId, 'accessories'] as const,
+ queryFn: () => jobAdapter.getAccessories(jobId),
+ enabled: !!jobId,
+ staleTime: 1000 * 60 * 30, // 30 minutes
+ gcTime: 1000 * 60 * 60 // 1 hour
+ }),
+
+ /**
+ * All job skills query options (not filtered by job)
+ *
+ * @returns Query options for fetching all job skills
+ */
+ allSkills: () =>
+ queryOptions({
+ queryKey: ['jobs', 'skills', 'all'] as const,
+ queryFn: () => jobAdapter.getAllSkills(),
+ staleTime: 1000 * 60 * 30, // 30 minutes
+ gcTime: 1000 * 60 * 60 // 1 hour
+ })
+}
+
+/**
+ * Query key helpers for cache invalidation
+ *
+ * @example
+ * ```typescript
+ * import { useQueryClient } from '@tanstack/svelte-query'
+ * import { jobKeys } from '$lib/api/queries/job.queries'
+ *
+ * const queryClient = useQueryClient()
+ *
+ * // Invalidate all jobs
+ * queryClient.invalidateQueries({ queryKey: jobKeys.all })
+ *
+ * // Invalidate skills for a specific job
+ * queryClient.invalidateQueries({ queryKey: jobKeys.skills('job-id') })
+ * ```
+ */
+export const jobKeys = {
+ all: ['jobs'] as const,
+ lists: () => [...jobKeys.all] as const,
+ detail: (id: string) => [...jobKeys.all, id] as const,
+ skills: (jobId: string) => [...jobKeys.all, jobId, 'skills'] as const,
+ skillsSearch: (jobId: string, params?: Omit) =>
+ [...jobKeys.skills(jobId), 'search', params] as const,
+ accessories: (jobId: string) => [...jobKeys.all, jobId, 'accessories'] as const,
+ allSkills: () => [...jobKeys.all, 'skills', 'all'] as const
+}
diff --git a/src/lib/api/queries/party.queries.ts b/src/lib/api/queries/party.queries.ts
new file mode 100644
index 00000000..b8ae67a2
--- /dev/null
+++ b/src/lib/api/queries/party.queries.ts
@@ -0,0 +1,185 @@
+/**
+ * Party Query Options Factory
+ *
+ * Provides type-safe, reusable query configurations for party operations
+ * using TanStack Query v6 patterns.
+ *
+ * @module api/queries/party
+ */
+
+import { queryOptions, infiniteQueryOptions } from '@tanstack/svelte-query'
+import {
+ partyAdapter,
+ type ListUserPartiesParams
+} from '$lib/api/adapters/party.adapter'
+import type { Party } from '$lib/types/api/party'
+
+/**
+ * Standard page result format for infinite queries
+ */
+export interface PartyPageResult {
+ results: Party[]
+ page: number
+ totalPages: number
+ total: number
+ perPage: number
+}
+
+/**
+ * Parameters for listing parties
+ */
+export interface ListPartiesParams {
+ page?: number
+ per?: number
+}
+
+/**
+ * Party query options factory
+ *
+ * Provides query configurations for all party-related operations.
+ * These can be used with `createQuery`, `createInfiniteQuery`, or for prefetching.
+ *
+ * @example
+ * ```typescript
+ * import { createQuery, createInfiniteQuery } from '@tanstack/svelte-query'
+ * import { partyQueries } from '$lib/api/queries/party.queries'
+ *
+ * // Single party by shortcode
+ * const party = createQuery(() => partyQueries.byShortcode(shortcode))
+ *
+ * // Infinite list of parties
+ * const parties = createInfiniteQuery(() => partyQueries.list())
+ * ```
+ */
+export const partyQueries = {
+ /**
+ * Single party query options
+ *
+ * @param shortcode - Party shortcode identifier
+ * @returns Query options for fetching a single party
+ */
+ byShortcode: (shortcode: string) =>
+ queryOptions({
+ queryKey: ['party', shortcode] as const,
+ queryFn: () => partyAdapter.getByShortcode(shortcode),
+ enabled: !!shortcode,
+ staleTime: 1000 * 60 * 5, // 5 minutes
+ gcTime: 1000 * 60 * 30 // 30 minutes
+ }),
+
+ /**
+ * Public parties list (explore page) infinite query options
+ *
+ * @param params - Optional pagination parameters
+ * @returns Infinite query options for listing public parties
+ */
+ list: (params?: Omit) =>
+ infiniteQueryOptions({
+ queryKey: ['parties', 'list', params] as const,
+ queryFn: async ({ pageParam }): Promise => {
+ const response = await partyAdapter.list({
+ ...params,
+ page: pageParam
+ })
+ return {
+ results: response.results,
+ page: response.page,
+ totalPages: response.totalPages,
+ total: response.total,
+ perPage: response.perPage
+ }
+ },
+ initialPageParam: 1,
+ getNextPageParam: (lastPage) => {
+ if (lastPage.page < lastPage.totalPages) {
+ return lastPage.page + 1
+ }
+ return undefined
+ },
+ staleTime: 1000 * 60 * 2, // 2 minutes - parties change more frequently
+ gcTime: 1000 * 60 * 15 // 15 minutes
+ }),
+
+ /**
+ * User parties list infinite query options
+ *
+ * @param username - Username to fetch parties for
+ * @param params - Optional filter parameters
+ * @returns Infinite query options for listing user's parties
+ */
+ userParties: (
+ username: string,
+ params?: Omit
+ ) =>
+ infiniteQueryOptions({
+ queryKey: ['parties', 'user', username, params] as const,
+ queryFn: async ({ pageParam }): Promise => {
+ const response = await partyAdapter.listUserParties({
+ username,
+ ...params,
+ page: pageParam
+ })
+ return {
+ results: response.results,
+ page: response.page,
+ totalPages: response.totalPages,
+ total: response.total,
+ perPage: response.perPage
+ }
+ },
+ initialPageParam: 1,
+ getNextPageParam: (lastPage) => {
+ if (lastPage.page < lastPage.totalPages) {
+ return lastPage.page + 1
+ }
+ return undefined
+ },
+ enabled: !!username,
+ staleTime: 1000 * 60 * 2, // 2 minutes
+ gcTime: 1000 * 60 * 15 // 15 minutes
+ }),
+
+ /**
+ * Party preview status query options
+ *
+ * @param shortcode - Party shortcode identifier
+ * @returns Query options for fetching party preview status
+ */
+ previewStatus: (shortcode: string) =>
+ queryOptions({
+ queryKey: ['party', shortcode, 'preview'] as const,
+ queryFn: () => partyAdapter.getPreviewStatus(shortcode),
+ enabled: !!shortcode,
+ staleTime: 1000 * 30, // 30 seconds - preview status changes
+ gcTime: 1000 * 60 * 5 // 5 minutes
+ })
+}
+
+/**
+ * Query key helpers for cache invalidation
+ *
+ * @example
+ * ```typescript
+ * import { useQueryClient } from '@tanstack/svelte-query'
+ * import { partyKeys } from '$lib/api/queries/party.queries'
+ *
+ * const queryClient = useQueryClient()
+ *
+ * // Invalidate a specific party
+ * queryClient.invalidateQueries({ queryKey: partyKeys.detail('abc123') })
+ *
+ * // Invalidate all party lists
+ * queryClient.invalidateQueries({ queryKey: partyKeys.lists() })
+ * ```
+ */
+export const partyKeys = {
+ all: ['parties'] as const,
+ lists: () => [...partyKeys.all, 'list'] as const,
+ list: (params?: ListPartiesParams) => [...partyKeys.lists(), params] as const,
+ userLists: () => [...partyKeys.all, 'user'] as const,
+ userList: (username: string, params?: Omit) =>
+ [...partyKeys.userLists(), username, params] as const,
+ details: () => ['party'] as const,
+ detail: (shortcode: string) => [...partyKeys.details(), shortcode] as const,
+ preview: (shortcode: string) => [...partyKeys.detail(shortcode), 'preview'] as const
+}
diff --git a/src/lib/api/queries/user.queries.ts b/src/lib/api/queries/user.queries.ts
new file mode 100644
index 00000000..3130ec8d
--- /dev/null
+++ b/src/lib/api/queries/user.queries.ts
@@ -0,0 +1,218 @@
+/**
+ * User Query Options Factory
+ *
+ * Provides type-safe, reusable query configurations for user operations
+ * using TanStack Query v6 patterns.
+ *
+ * @module api/queries/user
+ */
+
+import { queryOptions, infiniteQueryOptions } from '@tanstack/svelte-query'
+import { userAdapter, type UserInfo, type UserProfile } from '$lib/api/adapters/user.adapter'
+import type { Party } from '$lib/types/api/party'
+
+/**
+ * Standard page result format for user parties infinite queries
+ */
+export interface UserPartiesPageResult {
+ results: Party[]
+ page: number
+ totalPages: number
+ total: number
+ perPage: number
+}
+
+/**
+ * Standard page result format for favorites infinite queries
+ */
+export interface FavoritesPageResult {
+ items: Party[]
+ page: number
+ totalPages: number
+ total: number
+ perPage: number
+}
+
+/**
+ * User query options factory
+ *
+ * Provides query configurations for all user-related operations.
+ * These can be used with `createQuery`, `createInfiniteQuery`, or for prefetching.
+ *
+ * @example
+ * ```typescript
+ * import { createQuery, createInfiniteQuery } from '@tanstack/svelte-query'
+ * import { userQueries } from '$lib/api/queries/user.queries'
+ *
+ * // Current user
+ * const currentUser = createQuery(() => userQueries.me())
+ *
+ * // User profile with parties
+ * const profile = createQuery(() => userQueries.profile(username))
+ *
+ * // User's parties with infinite scroll
+ * const parties = createInfiniteQuery(() => userQueries.parties(username))
+ * ```
+ */
+export const userQueries = {
+ /**
+ * Current user query options
+ *
+ * @returns Query options for fetching the current authenticated user
+ */
+ me: () =>
+ queryOptions({
+ queryKey: ['user', 'me'] as const,
+ queryFn: () => userAdapter.getCurrentUser(),
+ staleTime: 1000 * 60 * 5, // 5 minutes
+ gcTime: 1000 * 60 * 30 // 30 minutes
+ }),
+
+ /**
+ * User info query options
+ *
+ * @param username - Username to fetch info for
+ * @returns Query options for fetching user info
+ */
+ info: (username: string) =>
+ queryOptions({
+ queryKey: ['user', username, 'info'] as const,
+ queryFn: () => userAdapter.getInfo(username),
+ enabled: !!username,
+ staleTime: 1000 * 60 * 5, // 5 minutes
+ gcTime: 1000 * 60 * 30 // 30 minutes
+ }),
+
+ /**
+ * User profile query options (includes first page of parties)
+ *
+ * @param username - Username to fetch profile for
+ * @returns Query options for fetching user profile
+ */
+ profile: (username: string) =>
+ queryOptions({
+ queryKey: ['user', username, 'profile'] as const,
+ queryFn: () => userAdapter.getProfile(username),
+ enabled: !!username,
+ staleTime: 1000 * 60 * 2, // 2 minutes - profile data changes
+ gcTime: 1000 * 60 * 15 // 15 minutes
+ }),
+
+ /**
+ * User parties infinite query options
+ *
+ * @param username - Username to fetch parties for
+ * @returns Infinite query options for fetching user's parties
+ */
+ parties: (username: string) =>
+ infiniteQueryOptions({
+ queryKey: ['user', username, 'parties'] as const,
+ queryFn: async ({ pageParam }): Promise => {
+ const response = await userAdapter.getProfileParties(username, pageParam)
+ return {
+ results: response.results,
+ page: response.page,
+ totalPages: response.totalPages,
+ total: response.total,
+ perPage: response.perPage
+ }
+ },
+ initialPageParam: 1,
+ getNextPageParam: (lastPage) => {
+ if (lastPage.page < lastPage.totalPages) {
+ return lastPage.page + 1
+ }
+ return undefined
+ },
+ enabled: !!username,
+ staleTime: 1000 * 60 * 2, // 2 minutes
+ gcTime: 1000 * 60 * 15 // 15 minutes
+ }),
+
+ /**
+ * User favorites infinite query options
+ *
+ * @returns Infinite query options for fetching user's favorite parties
+ */
+ favorites: () =>
+ infiniteQueryOptions({
+ queryKey: ['user', 'favorites'] as const,
+ queryFn: async ({ pageParam }): Promise => {
+ const response = await userAdapter.getFavorites({ page: pageParam })
+ return {
+ items: response.items,
+ page: response.page,
+ totalPages: response.totalPages,
+ total: response.total,
+ perPage: response.perPage
+ }
+ },
+ initialPageParam: 1,
+ getNextPageParam: (lastPage) => {
+ if (lastPage.page < lastPage.totalPages) {
+ return lastPage.page + 1
+ }
+ return undefined
+ },
+ staleTime: 1000 * 60 * 2, // 2 minutes
+ gcTime: 1000 * 60 * 15 // 15 minutes
+ }),
+
+ /**
+ * Username availability check query options
+ *
+ * @param username - Username to check availability for
+ * @returns Query options for checking username availability
+ */
+ checkUsername: (username: string) =>
+ queryOptions({
+ queryKey: ['user', 'check', 'username', username] as const,
+ queryFn: () => userAdapter.checkUsernameAvailability(username),
+ enabled: !!username && username.length >= 3,
+ staleTime: 1000 * 30, // 30 seconds - availability can change
+ gcTime: 1000 * 60 * 5 // 5 minutes
+ }),
+
+ /**
+ * Email availability check query options
+ *
+ * @param email - Email to check availability for
+ * @returns Query options for checking email availability
+ */
+ checkEmail: (email: string) =>
+ queryOptions({
+ queryKey: ['user', 'check', 'email', email] as const,
+ queryFn: () => userAdapter.checkEmailAvailability(email),
+ enabled: !!email && email.includes('@'),
+ staleTime: 1000 * 30, // 30 seconds - availability can change
+ gcTime: 1000 * 60 * 5 // 5 minutes
+ })
+}
+
+/**
+ * Query key helpers for cache invalidation
+ *
+ * @example
+ * ```typescript
+ * import { useQueryClient } from '@tanstack/svelte-query'
+ * import { userKeys } from '$lib/api/queries/user.queries'
+ *
+ * const queryClient = useQueryClient()
+ *
+ * // Invalidate current user
+ * queryClient.invalidateQueries({ queryKey: userKeys.me() })
+ *
+ * // Invalidate a user's profile
+ * queryClient.invalidateQueries({ queryKey: userKeys.profile('username') })
+ * ```
+ */
+export const userKeys = {
+ all: ['user'] as const,
+ me: () => [...userKeys.all, 'me'] as const,
+ info: (username: string) => [...userKeys.all, username, 'info'] as const,
+ profile: (username: string) => [...userKeys.all, username, 'profile'] as const,
+ parties: (username: string) => [...userKeys.all, username, 'parties'] as const,
+ favorites: () => [...userKeys.all, 'favorites'] as const,
+ checkUsername: (username: string) => [...userKeys.all, 'check', 'username', username] as const,
+ checkEmail: (email: string) => [...userKeys.all, 'check', 'email', email] as const
+}