add collection API layer
Types, adapter, queries, and mutations for managing user collections (characters, weapons, summons, job accessories). Supports both private collection management and public collection viewing with privacy.
This commit is contained in:
parent
51db7f7604
commit
60ac5d4ab2
4 changed files with 1019 additions and 0 deletions
366
src/lib/api/adapters/collection.adapter.ts
Normal file
366
src/lib/api/adapters/collection.adapter.ts
Normal file
|
|
@ -0,0 +1,366 @@
|
|||
/**
|
||||
* Collection Adapter
|
||||
*
|
||||
* Handles all collection-related API operations including CRUD for
|
||||
* characters, weapons, summons, and job accessories in a user's collection.
|
||||
*
|
||||
* @module adapters/collection
|
||||
*/
|
||||
|
||||
import { BaseAdapter } from './base.adapter'
|
||||
import type { AdapterOptions, PaginatedResponse } from './types'
|
||||
import { DEFAULT_ADAPTER_CONFIG } from './config'
|
||||
import type {
|
||||
CollectionCharacter,
|
||||
CollectionWeapon,
|
||||
CollectionSummon,
|
||||
CollectionJobAccessory,
|
||||
CollectionCharacterInput,
|
||||
CollectionWeaponInput,
|
||||
CollectionSummonInput,
|
||||
CollectionJobAccessoryInput,
|
||||
CollectionFilters,
|
||||
CollectionResponse
|
||||
} from '$lib/types/api/collection'
|
||||
|
||||
/**
|
||||
* Parameters for listing collection items with pagination
|
||||
*/
|
||||
export interface CollectionListParams extends CollectionFilters {
|
||||
page?: number
|
||||
limit?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Response structure for paginated collection list
|
||||
*/
|
||||
export interface CollectionCharacterListResponse {
|
||||
characters: CollectionCharacter[]
|
||||
meta: {
|
||||
count: number
|
||||
totalPages: number
|
||||
perPage: number
|
||||
currentPage: number
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collection adapter for managing user collections
|
||||
*/
|
||||
export class CollectionAdapter extends BaseAdapter {
|
||||
constructor(options?: AdapterOptions) {
|
||||
super(options)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Collection Characters
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Lists the current user's collection characters with optional filters
|
||||
*/
|
||||
async listCharacters(
|
||||
params: CollectionListParams = {}
|
||||
): Promise<PaginatedResponse<CollectionCharacter>> {
|
||||
const response = await this.request<CollectionCharacterListResponse>('/collection/characters', {
|
||||
method: 'GET',
|
||||
query: params
|
||||
})
|
||||
|
||||
return {
|
||||
results: response.characters,
|
||||
page: response.meta.currentPage,
|
||||
total: response.meta.count,
|
||||
totalPages: response.meta.totalPages,
|
||||
perPage: response.meta.perPage
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a single collection character by ID
|
||||
*/
|
||||
async getCharacter(id: string): Promise<CollectionCharacter> {
|
||||
return this.request<CollectionCharacter>(`/collection/characters/${id}`, {
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a character to the collection
|
||||
*/
|
||||
async addCharacter(input: CollectionCharacterInput): Promise<CollectionCharacter> {
|
||||
return this.request<CollectionCharacter>('/collection/characters', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
collectionCharacter: input
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds multiple characters to the collection
|
||||
* Makes parallel requests for each character
|
||||
*/
|
||||
async addCharacters(inputs: CollectionCharacterInput[]): Promise<CollectionCharacter[]> {
|
||||
const results = await Promise.all(inputs.map((input) => this.addCharacter(input)))
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a collection character
|
||||
*/
|
||||
async updateCharacter(
|
||||
id: string,
|
||||
input: Partial<CollectionCharacterInput>
|
||||
): Promise<CollectionCharacter> {
|
||||
return this.request<CollectionCharacter>(`/collection/characters/${id}`, {
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
collectionCharacter: input
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a character from the collection
|
||||
*/
|
||||
async removeCharacter(id: string): Promise<void> {
|
||||
return this.request<void>(`/collection/characters/${id}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the IDs of all characters in the current user's collection
|
||||
* Useful for filtering out already-owned characters in the add modal
|
||||
*/
|
||||
async getCollectedCharacterIds(): Promise<string[]> {
|
||||
// Fetch all pages to get complete list
|
||||
const allIds: string[] = []
|
||||
let page = 1
|
||||
let hasMore = true
|
||||
|
||||
while (hasMore) {
|
||||
const response = await this.listCharacters({ page, limit: 100 })
|
||||
allIds.push(...response.results.map((c) => c.character.id))
|
||||
hasMore = page < response.totalPages
|
||||
page++
|
||||
}
|
||||
|
||||
return allIds
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Collection Weapons
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Lists the current user's collection weapons with optional filters
|
||||
*/
|
||||
async listWeapons(params: CollectionListParams = {}): Promise<PaginatedResponse<CollectionWeapon>> {
|
||||
const response = await this.request<{
|
||||
weapons: CollectionWeapon[]
|
||||
meta: { count: number; totalPages: number; perPage: number; currentPage: number }
|
||||
}>('/collection/weapons', {
|
||||
method: 'GET',
|
||||
query: params
|
||||
})
|
||||
|
||||
return {
|
||||
results: response.weapons,
|
||||
page: response.meta.currentPage,
|
||||
total: response.meta.count,
|
||||
totalPages: response.meta.totalPages,
|
||||
perPage: response.meta.perPage
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a weapon to the collection
|
||||
*/
|
||||
async addWeapon(input: CollectionWeaponInput): Promise<CollectionWeapon> {
|
||||
return this.request<CollectionWeapon>('/collection/weapons', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
collectionWeapon: input
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a collection weapon
|
||||
*/
|
||||
async updateWeapon(id: string, input: Partial<CollectionWeaponInput>): Promise<CollectionWeapon> {
|
||||
return this.request<CollectionWeapon>(`/collection/weapons/${id}`, {
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
collectionWeapon: input
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a weapon from the collection
|
||||
*/
|
||||
async removeWeapon(id: string): Promise<void> {
|
||||
return this.request<void>(`/collection/weapons/${id}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Collection Summons
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Lists the current user's collection summons with optional filters
|
||||
*/
|
||||
async listSummons(params: CollectionListParams = {}): Promise<PaginatedResponse<CollectionSummon>> {
|
||||
const response = await this.request<{
|
||||
summons: CollectionSummon[]
|
||||
meta: { count: number; totalPages: number; perPage: number; currentPage: number }
|
||||
}>('/collection/summons', {
|
||||
method: 'GET',
|
||||
query: params
|
||||
})
|
||||
|
||||
return {
|
||||
results: response.summons,
|
||||
page: response.meta.currentPage,
|
||||
total: response.meta.count,
|
||||
totalPages: response.meta.totalPages,
|
||||
perPage: response.meta.perPage
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a summon to the collection
|
||||
*/
|
||||
async addSummon(input: CollectionSummonInput): Promise<CollectionSummon> {
|
||||
return this.request<CollectionSummon>('/collection/summons', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
collectionSummon: input
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a collection summon
|
||||
*/
|
||||
async updateSummon(id: string, input: Partial<CollectionSummonInput>): Promise<CollectionSummon> {
|
||||
return this.request<CollectionSummon>(`/collection/summons/${id}`, {
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
collectionSummon: input
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a summon from the collection
|
||||
*/
|
||||
async removeSummon(id: string): Promise<void> {
|
||||
return this.request<void>(`/collection/summons/${id}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Collection Job Accessories
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Lists the current user's collection job accessories
|
||||
*/
|
||||
async listJobAccessories(): Promise<CollectionJobAccessory[]> {
|
||||
const response = await this.request<{ jobAccessories: CollectionJobAccessory[] }>(
|
||||
'/collection/job_accessories',
|
||||
{
|
||||
method: 'GET'
|
||||
}
|
||||
)
|
||||
return response.jobAccessories
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a job accessory to the collection
|
||||
*/
|
||||
async addJobAccessory(input: CollectionJobAccessoryInput): Promise<CollectionJobAccessory> {
|
||||
return this.request<CollectionJobAccessory>('/collection/job_accessories', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
collectionJobAccessory: input
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a job accessory from the collection
|
||||
*/
|
||||
async removeJobAccessory(id: string): Promise<void> {
|
||||
return this.request<void>(`/collection/job_accessories/${id}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Public Collection (viewing other users)
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Gets a user's public collection (respects privacy settings)
|
||||
* @param userId - The user's ID
|
||||
* @param type - Optional type filter: 'characters', 'weapons', 'summons', 'job_accessories'
|
||||
*/
|
||||
async getPublicCollection(
|
||||
userId: string,
|
||||
type?: 'characters' | 'weapons' | 'summons' | 'job_accessories'
|
||||
): Promise<CollectionResponse> {
|
||||
return this.request<CollectionResponse>(`/users/${userId}/collection`, {
|
||||
method: 'GET',
|
||||
query: type ? { type } : undefined
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a user's public character collection
|
||||
*/
|
||||
async getPublicCharacters(userId: string): Promise<CollectionCharacter[]> {
|
||||
const response = await this.getPublicCollection(userId, 'characters')
|
||||
return response.characters || []
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a user's public weapon collection
|
||||
*/
|
||||
async getPublicWeapons(userId: string): Promise<CollectionWeapon[]> {
|
||||
const response = await this.getPublicCollection(userId, 'weapons')
|
||||
return response.weapons || []
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a user's public summon collection
|
||||
*/
|
||||
async getPublicSummons(userId: string): Promise<CollectionSummon[]> {
|
||||
const response = await this.getPublicCollection(userId, 'summons')
|
||||
return response.summons || []
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Cache Management
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Clears collection-related cache
|
||||
*/
|
||||
clearCollectionCache() {
|
||||
this.clearCache('/collection')
|
||||
this.clearCache('/users')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default collection adapter instance
|
||||
*/
|
||||
export const collectionAdapter = new CollectionAdapter(DEFAULT_ADAPTER_CONFIG)
|
||||
256
src/lib/api/mutations/collection.mutations.ts
Normal file
256
src/lib/api/mutations/collection.mutations.ts
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
/**
|
||||
* Collection Mutation Configurations
|
||||
*
|
||||
* Provides mutation configurations for collection operations
|
||||
* with cache invalidation using TanStack Query v6.
|
||||
*
|
||||
* @module api/mutations/collection
|
||||
*/
|
||||
|
||||
import { useQueryClient, createMutation } from '@tanstack/svelte-query'
|
||||
import { collectionAdapter } from '$lib/api/adapters/collection.adapter'
|
||||
import { collectionKeys } from '$lib/api/queries/collection.queries'
|
||||
import type {
|
||||
CollectionCharacter,
|
||||
CollectionCharacterInput,
|
||||
CollectionWeaponInput,
|
||||
CollectionSummonInput,
|
||||
CollectionJobAccessoryInput
|
||||
} from '$lib/types/api/collection'
|
||||
|
||||
// ============================================================================
|
||||
// Character Mutations
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Add characters to collection mutation
|
||||
*
|
||||
* Adds one or more characters to the user's collection.
|
||||
*
|
||||
* @example
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { useAddToCollection } from '$lib/api/mutations/collection.mutations'
|
||||
*
|
||||
* const addToCollection = useAddToCollection()
|
||||
*
|
||||
* function handleAdd(characterIds: string[]) {
|
||||
* addToCollection.mutate(
|
||||
* characterIds.map(id => ({ characterId: id }))
|
||||
* )
|
||||
* }
|
||||
* </script>
|
||||
* ```
|
||||
*/
|
||||
export function useAddCharactersToCollection() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: (inputs: CollectionCharacterInput[]) => collectionAdapter.addCharacters(inputs),
|
||||
onSuccess: () => {
|
||||
// Invalidate all character-related collection queries
|
||||
queryClient.invalidateQueries({ queryKey: collectionKeys.characters() })
|
||||
queryClient.invalidateQueries({ queryKey: collectionKeys.characterIds() })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Add single character to collection mutation
|
||||
*/
|
||||
export function useAddCharacterToCollection() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: (input: CollectionCharacterInput) => collectionAdapter.addCharacter(input),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: collectionKeys.characters() })
|
||||
queryClient.invalidateQueries({ queryKey: collectionKeys.characterIds() })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Update collection character mutation
|
||||
*
|
||||
* Updates a character's customizations (uncap, rings, etc.) in the collection.
|
||||
*/
|
||||
export function useUpdateCollectionCharacter() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: ({ id, input }: { id: string; input: Partial<CollectionCharacterInput> }) =>
|
||||
collectionAdapter.updateCharacter(id, input),
|
||||
onMutate: async ({ id, input }) => {
|
||||
// Cancel any outgoing refetches
|
||||
await queryClient.cancelQueries({ queryKey: collectionKeys.character(id) })
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousCharacter = queryClient.getQueryData<CollectionCharacter>(
|
||||
collectionKeys.character(id)
|
||||
)
|
||||
|
||||
// Optimistically update the cache
|
||||
if (previousCharacter) {
|
||||
queryClient.setQueryData(collectionKeys.character(id), {
|
||||
...previousCharacter,
|
||||
...input
|
||||
})
|
||||
}
|
||||
|
||||
return { previousCharacter }
|
||||
},
|
||||
onError: (_err, { id }, context) => {
|
||||
// Rollback on error
|
||||
if (context?.previousCharacter) {
|
||||
queryClient.setQueryData(collectionKeys.character(id), context.previousCharacter)
|
||||
}
|
||||
},
|
||||
onSettled: (_data, _err, { id }) => {
|
||||
// Always refetch after mutation
|
||||
queryClient.invalidateQueries({ queryKey: collectionKeys.character(id) })
|
||||
queryClient.invalidateQueries({ queryKey: collectionKeys.characters() })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove character from collection mutation
|
||||
*/
|
||||
export function useRemoveCharacterFromCollection() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: (id: string) => collectionAdapter.removeCharacter(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: collectionKeys.characters() })
|
||||
queryClient.invalidateQueries({ queryKey: collectionKeys.characterIds() })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Weapon Mutations
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Add weapon to collection mutation
|
||||
*/
|
||||
export function useAddWeaponToCollection() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: (input: CollectionWeaponInput) => collectionAdapter.addWeapon(input),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: collectionKeys.weapons() })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Update collection weapon mutation
|
||||
*/
|
||||
export function useUpdateCollectionWeapon() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: ({ id, input }: { id: string; input: Partial<CollectionWeaponInput> }) =>
|
||||
collectionAdapter.updateWeapon(id, input),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: collectionKeys.weapons() })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove weapon from collection mutation
|
||||
*/
|
||||
export function useRemoveWeaponFromCollection() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: (id: string) => collectionAdapter.removeWeapon(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: collectionKeys.weapons() })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Summon Mutations
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Add summon to collection mutation
|
||||
*/
|
||||
export function useAddSummonToCollection() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: (input: CollectionSummonInput) => collectionAdapter.addSummon(input),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: collectionKeys.summons() })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Update collection summon mutation
|
||||
*/
|
||||
export function useUpdateCollectionSummon() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: ({ id, input }: { id: string; input: Partial<CollectionSummonInput> }) =>
|
||||
collectionAdapter.updateSummon(id, input),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: collectionKeys.summons() })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove summon from collection mutation
|
||||
*/
|
||||
export function useRemoveSummonFromCollection() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: (id: string) => collectionAdapter.removeSummon(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: collectionKeys.summons() })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Job Accessory Mutations
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Add job accessory to collection mutation
|
||||
*/
|
||||
export function useAddJobAccessoryToCollection() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: (input: CollectionJobAccessoryInput) => collectionAdapter.addJobAccessory(input),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: collectionKeys.all })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove job accessory from collection mutation
|
||||
*/
|
||||
export function useRemoveJobAccessoryFromCollection() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: (id: string) => collectionAdapter.removeJobAccessory(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: collectionKeys.all })
|
||||
}
|
||||
}))
|
||||
}
|
||||
233
src/lib/api/queries/collection.queries.ts
Normal file
233
src/lib/api/queries/collection.queries.ts
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
/**
|
||||
* Collection Query Options Factory
|
||||
*
|
||||
* Provides type-safe, reusable query configurations for collection operations
|
||||
* using TanStack Query v6 patterns.
|
||||
*
|
||||
* @module api/queries/collection
|
||||
*/
|
||||
|
||||
import { queryOptions, infiniteQueryOptions } from '@tanstack/svelte-query'
|
||||
import { collectionAdapter } from '$lib/api/adapters/collection.adapter'
|
||||
import type {
|
||||
CollectionCharacter,
|
||||
CollectionWeapon,
|
||||
CollectionSummon,
|
||||
CollectionFilters
|
||||
} from '$lib/types/api/collection'
|
||||
|
||||
/**
|
||||
* Page result format for collection infinite queries
|
||||
*/
|
||||
export interface CollectionPageResult<T> {
|
||||
results: T[]
|
||||
page: number
|
||||
totalPages: number
|
||||
total: number
|
||||
perPage: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Collection query options factory
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { createQuery, createInfiniteQuery } from '@tanstack/svelte-query'
|
||||
* import { collectionQueries } from '$lib/api/queries/collection.queries'
|
||||
*
|
||||
* // Own collection characters
|
||||
* const characters = createInfiniteQuery(() => collectionQueries.characters())
|
||||
*
|
||||
* // Public collection
|
||||
* const publicChars = createQuery(() => collectionQueries.publicCharacters(userId))
|
||||
*
|
||||
* // Collected character IDs (for filtering add modal)
|
||||
* const ownedIds = createQuery(() => collectionQueries.collectedCharacterIds())
|
||||
* ```
|
||||
*/
|
||||
export const collectionQueries = {
|
||||
/**
|
||||
* Current user's collection characters with infinite scroll
|
||||
*/
|
||||
characters: (filters?: CollectionFilters) =>
|
||||
infiniteQueryOptions({
|
||||
queryKey: ['collection', 'characters', filters] as const,
|
||||
queryFn: async ({ pageParam }): Promise<CollectionPageResult<CollectionCharacter>> => {
|
||||
const response = await collectionAdapter.listCharacters({
|
||||
...filters,
|
||||
page: pageParam
|
||||
})
|
||||
return {
|
||||
results: response.results,
|
||||
page: response.page,
|
||||
totalPages: response.totalPages,
|
||||
total: response.total,
|
||||
perPage: response.perPage ?? 50
|
||||
}
|
||||
},
|
||||
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
|
||||
}),
|
||||
|
||||
/**
|
||||
* Current user's collection weapons with infinite scroll
|
||||
*/
|
||||
weapons: (filters?: CollectionFilters) =>
|
||||
infiniteQueryOptions({
|
||||
queryKey: ['collection', 'weapons', filters] as const,
|
||||
queryFn: async ({ pageParam }): Promise<CollectionPageResult<CollectionWeapon>> => {
|
||||
const response = await collectionAdapter.listWeapons({
|
||||
...filters,
|
||||
page: pageParam
|
||||
})
|
||||
return {
|
||||
results: response.results,
|
||||
page: response.page,
|
||||
totalPages: response.totalPages,
|
||||
total: response.total,
|
||||
perPage: response.perPage ?? 50
|
||||
}
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: (lastPage) => {
|
||||
if (lastPage.page < lastPage.totalPages) {
|
||||
return lastPage.page + 1
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
staleTime: 1000 * 60 * 2,
|
||||
gcTime: 1000 * 60 * 15
|
||||
}),
|
||||
|
||||
/**
|
||||
* Current user's collection summons with infinite scroll
|
||||
*/
|
||||
summons: (filters?: CollectionFilters) =>
|
||||
infiniteQueryOptions({
|
||||
queryKey: ['collection', 'summons', filters] as const,
|
||||
queryFn: async ({ pageParam }): Promise<CollectionPageResult<CollectionSummon>> => {
|
||||
const response = await collectionAdapter.listSummons({
|
||||
...filters,
|
||||
page: pageParam
|
||||
})
|
||||
return {
|
||||
results: response.results,
|
||||
page: response.page,
|
||||
totalPages: response.totalPages,
|
||||
total: response.total,
|
||||
perPage: response.perPage ?? 50
|
||||
}
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: (lastPage) => {
|
||||
if (lastPage.page < lastPage.totalPages) {
|
||||
return lastPage.page + 1
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
staleTime: 1000 * 60 * 2,
|
||||
gcTime: 1000 * 60 * 15
|
||||
}),
|
||||
|
||||
/**
|
||||
* Get IDs of characters already in the user's collection
|
||||
* Used to filter out owned characters in the add modal
|
||||
*/
|
||||
collectedCharacterIds: () =>
|
||||
queryOptions({
|
||||
queryKey: ['collection', 'characters', 'ids'] as const,
|
||||
queryFn: () => collectionAdapter.getCollectedCharacterIds(),
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 30 // 30 minutes
|
||||
}),
|
||||
|
||||
/**
|
||||
* Single collection character by ID
|
||||
*/
|
||||
character: (id: string) =>
|
||||
queryOptions({
|
||||
queryKey: ['collection', 'character', id] as const,
|
||||
queryFn: () => collectionAdapter.getCharacter(id),
|
||||
enabled: !!id,
|
||||
staleTime: 1000 * 60 * 5,
|
||||
gcTime: 1000 * 60 * 30
|
||||
}),
|
||||
|
||||
/**
|
||||
* Public collection for a user (respects privacy)
|
||||
*/
|
||||
publicCharacters: (userId: string) =>
|
||||
queryOptions({
|
||||
queryKey: ['collection', 'public', userId, 'characters'] as const,
|
||||
queryFn: () => collectionAdapter.getPublicCharacters(userId),
|
||||
enabled: !!userId,
|
||||
staleTime: 1000 * 60 * 5,
|
||||
gcTime: 1000 * 60 * 30
|
||||
}),
|
||||
|
||||
/**
|
||||
* Public weapon collection for a user
|
||||
*/
|
||||
publicWeapons: (userId: string) =>
|
||||
queryOptions({
|
||||
queryKey: ['collection', 'public', userId, 'weapons'] as const,
|
||||
queryFn: () => collectionAdapter.getPublicWeapons(userId),
|
||||
enabled: !!userId,
|
||||
staleTime: 1000 * 60 * 5,
|
||||
gcTime: 1000 * 60 * 30
|
||||
}),
|
||||
|
||||
/**
|
||||
* Public summon collection for a user
|
||||
*/
|
||||
publicSummons: (userId: string) =>
|
||||
queryOptions({
|
||||
queryKey: ['collection', 'public', userId, 'summons'] as const,
|
||||
queryFn: () => collectionAdapter.getPublicSummons(userId),
|
||||
enabled: !!userId,
|
||||
staleTime: 1000 * 60 * 5,
|
||||
gcTime: 1000 * 60 * 30
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Query key helpers for cache invalidation
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { useQueryClient } from '@tanstack/svelte-query'
|
||||
* import { collectionKeys } from '$lib/api/queries/collection.queries'
|
||||
*
|
||||
* const queryClient = useQueryClient()
|
||||
*
|
||||
* // Invalidate all collection data
|
||||
* queryClient.invalidateQueries({ queryKey: collectionKeys.all })
|
||||
*
|
||||
* // Invalidate only characters
|
||||
* queryClient.invalidateQueries({ queryKey: collectionKeys.characters() })
|
||||
* ```
|
||||
*/
|
||||
export const collectionKeys = {
|
||||
all: ['collection'] as const,
|
||||
characters: () => [...collectionKeys.all, 'characters'] as const,
|
||||
characterList: (filters?: CollectionFilters) =>
|
||||
[...collectionKeys.characters(), filters] as const,
|
||||
character: (id: string) => [...collectionKeys.all, 'character', id] as const,
|
||||
characterIds: () => [...collectionKeys.characters(), 'ids'] as const,
|
||||
weapons: () => [...collectionKeys.all, 'weapons'] as const,
|
||||
weaponList: (filters?: CollectionFilters) => [...collectionKeys.weapons(), filters] as const,
|
||||
summons: () => [...collectionKeys.all, 'summons'] as const,
|
||||
summonList: (filters?: CollectionFilters) => [...collectionKeys.summons(), filters] as const,
|
||||
public: (userId: string) => [...collectionKeys.all, 'public', userId] as const,
|
||||
publicCharacters: (userId: string) =>
|
||||
[...collectionKeys.public(userId), 'characters'] as const,
|
||||
publicWeapons: (userId: string) => [...collectionKeys.public(userId), 'weapons'] as const,
|
||||
publicSummons: (userId: string) => [...collectionKeys.public(userId), 'summons'] as const
|
||||
}
|
||||
164
src/lib/types/api/collection.ts
Normal file
164
src/lib/types/api/collection.ts
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
// Collection types based on Rails CollectionCharacter/Weapon/Summon blueprints
|
||||
// These define user-owned items with customizations
|
||||
|
||||
import type { Character, Weapon, Summon, JobAccessory, Awakening } from './entities'
|
||||
|
||||
/**
|
||||
* Extended mastery modifier (used for rings and earrings)
|
||||
*/
|
||||
export interface ExtendedMastery {
|
||||
modifier: number
|
||||
strength: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Collection character from CollectionCharacterBlueprint
|
||||
* Represents a user's owned character with customizations
|
||||
*/
|
||||
export interface CollectionCharacter {
|
||||
id: string
|
||||
uncapLevel: number
|
||||
transcendenceStep: number
|
||||
perpetuity: boolean
|
||||
ring1: ExtendedMastery | null
|
||||
ring2: ExtendedMastery | null
|
||||
ring3: ExtendedMastery | null
|
||||
ring4: ExtendedMastery | null
|
||||
earring: ExtendedMastery | null
|
||||
awakening: {
|
||||
type: Awakening
|
||||
level: number
|
||||
} | null
|
||||
character: Character
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Collection weapon from CollectionWeaponBlueprint
|
||||
* Represents a user's owned weapon with customizations
|
||||
*/
|
||||
export interface CollectionWeapon {
|
||||
id: string
|
||||
uncapLevel: number
|
||||
transcendenceStep: number
|
||||
element?: number // For element-changeable weapons
|
||||
ax?: Array<{ modifier: number; strength: number }>
|
||||
awakening: {
|
||||
type: Awakening
|
||||
level: number
|
||||
} | null
|
||||
weaponKeys?: Array<{
|
||||
id: string
|
||||
name: { en: string; ja: string }
|
||||
slot: number
|
||||
}>
|
||||
weapon: Weapon
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Collection summon from CollectionSummonBlueprint
|
||||
* Represents a user's owned summon with customizations
|
||||
*/
|
||||
export interface CollectionSummon {
|
||||
id: string
|
||||
uncapLevel: number
|
||||
transcendenceStep: number
|
||||
summon: Summon
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Collection job accessory from CollectionJobAccessoryBlueprint
|
||||
*/
|
||||
export interface CollectionJobAccessory {
|
||||
id: string
|
||||
jobAccessory: JobAccessory
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Full collection response (when no type filter is applied)
|
||||
*/
|
||||
export interface CollectionResponse {
|
||||
characters?: CollectionCharacter[]
|
||||
weapons?: CollectionWeapon[]
|
||||
summons?: CollectionSummon[]
|
||||
jobAccessories?: CollectionJobAccessory[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Input for creating a collection character
|
||||
*/
|
||||
export interface CollectionCharacterInput {
|
||||
characterId: string
|
||||
uncapLevel?: number
|
||||
transcendenceStep?: number
|
||||
perpetuity?: boolean
|
||||
awakeningId?: string
|
||||
awakeningLevel?: number
|
||||
ring1?: ExtendedMastery
|
||||
ring2?: ExtendedMastery
|
||||
ring3?: ExtendedMastery
|
||||
ring4?: ExtendedMastery
|
||||
earring?: ExtendedMastery
|
||||
}
|
||||
|
||||
/**
|
||||
* Input for creating a collection weapon
|
||||
*/
|
||||
export interface CollectionWeaponInput {
|
||||
weaponId: string
|
||||
uncapLevel?: number
|
||||
transcendenceStep?: number
|
||||
element?: number
|
||||
weaponKey1Id?: string
|
||||
weaponKey2Id?: string
|
||||
weaponKey3Id?: string
|
||||
weaponKey4Id?: string
|
||||
awakeningId?: string
|
||||
awakeningLevel?: number
|
||||
axModifier1?: number
|
||||
axStrength1?: number
|
||||
axModifier2?: number
|
||||
axStrength2?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Input for creating a collection summon
|
||||
*/
|
||||
export interface CollectionSummonInput {
|
||||
summonId: string
|
||||
uncapLevel?: number
|
||||
transcendenceStep?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Input for creating a collection job accessory
|
||||
*/
|
||||
export interface CollectionJobAccessoryInput {
|
||||
jobAccessoryId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters for listing collection items
|
||||
*/
|
||||
export interface CollectionFilters {
|
||||
element?: number[]
|
||||
rarity?: number[]
|
||||
page?: number
|
||||
limit?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Collection privacy levels (matches Rails enum)
|
||||
*/
|
||||
export enum CollectionPrivacy {
|
||||
Everyone = 0,
|
||||
CrewOnly = 1,
|
||||
Private = 2
|
||||
}
|
||||
Loading…
Reference in a new issue