diff --git a/src/lib/api/adapters/collection.adapter.ts b/src/lib/api/adapters/collection.adapter.ts new file mode 100644 index 00000000..11adebe7 --- /dev/null +++ b/src/lib/api/adapters/collection.adapter.ts @@ -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> { + const response = await this.request('/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 { + return this.request(`/collection/characters/${id}`, { + method: 'GET' + }) + } + + /** + * Adds a character to the collection + */ + async addCharacter(input: CollectionCharacterInput): Promise { + return this.request('/collection/characters', { + method: 'POST', + body: { + collectionCharacter: input + } + }) + } + + /** + * Adds multiple characters to the collection + * Makes parallel requests for each character + */ + async addCharacters(inputs: CollectionCharacterInput[]): Promise { + const results = await Promise.all(inputs.map((input) => this.addCharacter(input))) + return results + } + + /** + * Updates a collection character + */ + async updateCharacter( + id: string, + input: Partial + ): Promise { + return this.request(`/collection/characters/${id}`, { + method: 'PATCH', + body: { + collectionCharacter: input + } + }) + } + + /** + * Removes a character from the collection + */ + async removeCharacter(id: string): Promise { + return this.request(`/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 { + // 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> { + 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 { + return this.request('/collection/weapons', { + method: 'POST', + body: { + collectionWeapon: input + } + }) + } + + /** + * Updates a collection weapon + */ + async updateWeapon(id: string, input: Partial): Promise { + return this.request(`/collection/weapons/${id}`, { + method: 'PATCH', + body: { + collectionWeapon: input + } + }) + } + + /** + * Removes a weapon from the collection + */ + async removeWeapon(id: string): Promise { + return this.request(`/collection/weapons/${id}`, { + method: 'DELETE' + }) + } + + // ============================================ + // Collection Summons + // ============================================ + + /** + * Lists the current user's collection summons with optional filters + */ + async listSummons(params: CollectionListParams = {}): Promise> { + 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 { + return this.request('/collection/summons', { + method: 'POST', + body: { + collectionSummon: input + } + }) + } + + /** + * Updates a collection summon + */ + async updateSummon(id: string, input: Partial): Promise { + return this.request(`/collection/summons/${id}`, { + method: 'PATCH', + body: { + collectionSummon: input + } + }) + } + + /** + * Removes a summon from the collection + */ + async removeSummon(id: string): Promise { + return this.request(`/collection/summons/${id}`, { + method: 'DELETE' + }) + } + + // ============================================ + // Collection Job Accessories + // ============================================ + + /** + * Lists the current user's collection job accessories + */ + async listJobAccessories(): Promise { + 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 { + return this.request('/collection/job_accessories', { + method: 'POST', + body: { + collectionJobAccessory: input + } + }) + } + + /** + * Removes a job accessory from the collection + */ + async removeJobAccessory(id: string): Promise { + return this.request(`/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 { + return this.request(`/users/${userId}/collection`, { + method: 'GET', + query: type ? { type } : undefined + }) + } + + /** + * Gets a user's public character collection + */ + async getPublicCharacters(userId: string): Promise { + const response = await this.getPublicCollection(userId, 'characters') + return response.characters || [] + } + + /** + * Gets a user's public weapon collection + */ + async getPublicWeapons(userId: string): Promise { + const response = await this.getPublicCollection(userId, 'weapons') + return response.weapons || [] + } + + /** + * Gets a user's public summon collection + */ + async getPublicSummons(userId: string): Promise { + 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) diff --git a/src/lib/api/mutations/collection.mutations.ts b/src/lib/api/mutations/collection.mutations.ts new file mode 100644 index 00000000..e40fa070 --- /dev/null +++ b/src/lib/api/mutations/collection.mutations.ts @@ -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 + * + * ``` + */ +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 }) => + 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( + 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 }) => + 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 }) => + 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 }) + } + })) +} diff --git a/src/lib/api/queries/collection.queries.ts b/src/lib/api/queries/collection.queries.ts new file mode 100644 index 00000000..5e9fb4d1 --- /dev/null +++ b/src/lib/api/queries/collection.queries.ts @@ -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 { + 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> => { + 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> => { + 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> => { + 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 +} diff --git a/src/lib/types/api/collection.ts b/src/lib/types/api/collection.ts new file mode 100644 index 00000000..871cf233 --- /dev/null +++ b/src/lib/types/api/collection.ts @@ -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 +}