diff --git a/src/lib/api/adapters/index.ts b/src/lib/api/adapters/index.ts index 203c230d..d6392cef 100644 --- a/src/lib/api/adapters/index.ts +++ b/src/lib/api/adapters/index.ts @@ -51,5 +51,12 @@ export type { Summon } from './entity.adapter' +export { UserAdapter, userAdapter } from './user.adapter' +export type { + UserInfo, + UserProfile, + UserProfileResponse +} from './user.adapter' + // Reactive resources using Svelte 5 runes export * from './resources' \ No newline at end of file diff --git a/src/lib/api/adapters/user.adapter.ts b/src/lib/api/adapters/user.adapter.ts new file mode 100644 index 00000000..f74f82f9 --- /dev/null +++ b/src/lib/api/adapters/user.adapter.ts @@ -0,0 +1,131 @@ +import { BaseAdapter } from './base.adapter' +import type { Party } from '$lib/types/api/party' + +export interface UserInfo { + id: string + username: string + language: string + private: boolean + gender: number + theme: string + role: number + avatar: { + picture: string + element: string + } +} + +export interface UserProfile extends UserInfo { + parties?: Party[] +} + +export interface UserProfileResponse { + user: UserProfile + items: Party[] + page: number + total?: number + totalPages?: number + perPage?: number +} + +/** + * Adapter for user-related API operations + */ +export class UserAdapter extends BaseAdapter { + /** + * Get user information + */ + async getInfo(username: string): Promise { + return this.request(`/users/info/${encodeURIComponent(username)}`) + } + + /** + * Get user profile with their parties + */ + async getProfile(username: string, page = 1): Promise { + const params = page > 1 ? { page } : undefined + const response = await this.request<{ + profile: UserProfile + meta?: { count?: number; total_pages?: number; per_page?: number } + }>(`/users/${encodeURIComponent(username)}`, { params }) + + const items = Array.isArray(response.profile?.parties) ? response.profile.parties : [] + + return { + user: response.profile, + items, + page, + total: response.meta?.count, + totalPages: response.meta?.total_pages, + perPage: response.meta?.per_page + } + } + + /** + * Get user's favorite parties + */ + async getFavorites(options: { page?: number } = {}): Promise<{ + items: Party[] + page: number + total: number + totalPages: number + perPage: number + }> { + const { page = 1 } = options + const params = page > 1 ? { page } : undefined + + const response = await this.request<{ + results: Party[] + total: number + total_pages: number + per?: number + }>('/parties/favorites', { params }) + + return { + items: response.results, + page, + total: response.total, + totalPages: response.total_pages, + perPage: response.per || 20 + } + } + + /** + * Check username availability + */ + async checkUsernameAvailability(username: string): Promise<{ available: boolean }> { + return this.request<{ available: boolean }>(`/users/check-username`, { + method: 'POST', + body: JSON.stringify({ username }) + }) + } + + /** + * Check email availability + */ + async checkEmailAvailability(email: string): Promise<{ available: boolean }> { + return this.request<{ available: boolean }>(`/users/check-email`, { + method: 'POST', + body: JSON.stringify({ email }) + }) + } + + /** + * Update user profile + */ + async updateProfile(updates: Partial): Promise { + return this.request('/users/me', { + method: 'PUT', + body: JSON.stringify(updates) + }) + } + + /** + * Get current user + */ + async getCurrentUser(): Promise { + return this.request('/users/me') + } +} + +export const userAdapter = new UserAdapter() \ No newline at end of file diff --git a/src/lib/api/resources/grid.ts b/src/lib/api/resources/grid.ts deleted file mode 100644 index 3575d39d..00000000 --- a/src/lib/api/resources/grid.ts +++ /dev/null @@ -1,232 +0,0 @@ -/** - * Grid API resource functions - Facade layer for migration - * - * This module provides backward compatibility during the migration - * from api/core to the adapter pattern. Services can continue using - * these functions while we migrate them incrementally. - */ - -import { gridAdapter } from '$lib/api/adapters' -import type { - GridWeapon, - GridCharacter, - GridSummon -} from '$lib/api/adapters' - -// FetchLike type for backward compatibility -export type FetchLike = typeof fetch - -// Weapon grid operations -export async function addWeapon( - fetch: FetchLike, - partyId: string, - weaponId: string, // Granblue ID - position: number, - options?: { - mainhand?: boolean - uncapLevel?: number - transcendenceStep?: number - element?: number - }, - headers?: Record -): Promise { - return gridAdapter.createWeapon({ - partyId, - weaponId, - position, - mainhand: position === -1 || options?.mainhand, - uncapLevel: options?.uncapLevel ?? 3, - transcendenceStage: options?.transcendenceStep ?? 0 - }) -} - -export async function updateWeapon( - fetch: FetchLike, - partyId: string, - gridWeaponId: string, - updates: { - position?: number - uncapLevel?: number - transcendenceStep?: number - element?: number - }, - headers?: Record -): Promise { - return gridAdapter.updateWeapon(gridWeaponId, { - position: updates.position, - uncapLevel: updates.uncapLevel, - transcendenceStage: updates.transcendenceStep, - element: updates.element - }) -} - -export async function removeWeapon( - fetch: FetchLike, - partyId: string, - gridWeaponId: string, - headers?: Record -): Promise { - return gridAdapter.deleteWeapon({ - id: gridWeaponId, - partyId - }) -} - -// Summon grid operations -export async function addSummon( - fetch: FetchLike, - partyId: string, - summonId: string, // Granblue ID - position: number, - options?: { - main?: boolean - friend?: boolean - quickSummon?: boolean - uncapLevel?: number - transcendenceStep?: number - }, - headers?: Record -): Promise { - return gridAdapter.createSummon({ - partyId, - summonId, - position, - main: position === -1 || options?.main, - friend: position === 6 || options?.friend, - quickSummon: options?.quickSummon ?? false, - uncapLevel: options?.uncapLevel ?? 3, - transcendenceStage: options?.transcendenceStep ?? 0 - }) -} - -export async function updateSummon( - fetch: FetchLike, - partyId: string, - gridSummonId: string, - updates: { - position?: number - quickSummon?: boolean - uncapLevel?: number - transcendenceStep?: number - }, - headers?: Record -): Promise { - return gridAdapter.updateSummon(gridSummonId, { - position: updates.position, - quickSummon: updates.quickSummon, - uncapLevel: updates.uncapLevel, - transcendenceStage: updates.transcendenceStep - }) -} - -export async function removeSummon( - fetch: FetchLike, - partyId: string, - gridSummonId: string, - headers?: Record -): Promise { - return gridAdapter.deleteSummon({ - id: gridSummonId, - partyId - }) -} - -// Character grid operations -export async function addCharacter( - fetch: FetchLike, - partyId: string, - characterId: string, // Granblue ID - position: number, - options?: { - uncapLevel?: number - transcendenceStep?: number - perpetuity?: boolean - }, - headers?: Record -): Promise { - return gridAdapter.createCharacter({ - partyId, - characterId, - position, - uncapLevel: options?.uncapLevel ?? 3, - transcendenceStage: options?.transcendenceStep ?? 0 - }) -} - -export async function updateCharacter( - fetch: FetchLike, - partyId: string, - gridCharacterId: string, - updates: { - position?: number - uncapLevel?: number - transcendenceStep?: number - perpetuity?: boolean - }, - headers?: Record -): Promise { - return gridAdapter.updateCharacter(gridCharacterId, { - position: updates.position, - uncapLevel: updates.uncapLevel, - transcendenceStage: updates.transcendenceStep, - perpetualModifiers: updates.perpetuity ? {} : undefined - }) -} - -export async function removeCharacter( - fetch: FetchLike, - partyId: string, - gridCharacterId: string, - headers?: Record -): Promise { - return gridAdapter.deleteCharacter({ - id: gridCharacterId, - partyId - }) -} - -// Uncap update methods - these use special endpoints -export async function updateCharacterUncap( - gridCharacterId: string, - uncapLevel?: number, - transcendenceStep?: number, - headers?: Record -): Promise { - // For uncap updates, we need the partyId which isn't passed here - // This is a limitation of the current API design - // For now, we'll use the update method with a fake partyId - return gridAdapter.updateCharacterUncap({ - id: gridCharacterId, - partyId: 'unknown', // This is a hack - the API should be redesigned - uncapLevel: uncapLevel ?? 3, - transcendenceStep - }) -} - -export async function updateWeaponUncap( - gridWeaponId: string, - uncapLevel?: number, - transcendenceStep?: number, - headers?: Record -): Promise { - return gridAdapter.updateWeaponUncap({ - id: gridWeaponId, - partyId: 'unknown', // This is a hack - the API should be redesigned - uncapLevel: uncapLevel ?? 3, - transcendenceStep - }) -} - -export async function updateSummonUncap( - gridSummonId: string, - uncapLevel?: number, - transcendenceStep?: number, - headers?: Record -): Promise { - return gridAdapter.updateSummonUncap({ - id: gridSummonId, - partyId: 'unknown', // This is a hack - the API should be redesigned - uncapLevel: uncapLevel ?? 3, - transcendenceStep - }) -} \ No newline at end of file diff --git a/src/lib/api/resources/parties.ts b/src/lib/api/resources/parties.ts deleted file mode 100644 index 328ade69..00000000 --- a/src/lib/api/resources/parties.ts +++ /dev/null @@ -1,214 +0,0 @@ -/** - * Party API resource functions - Facade layer for migration - * - * This module provides backward compatibility during the migration - * from api/core to the adapter pattern. Services can continue using - * these functions while we migrate them incrementally. - */ - -import { partyAdapter } from '$lib/api/adapters' -import type { Party } from '$lib/types/api/party' -import { z } from 'zod' - -// FetchLike type for backward compatibility -export type FetchLike = typeof fetch - -// API functions - Now using PartyAdapter -export async function getByShortcode(fetch: FetchLike, shortcode: string): Promise { - // Ignore fetch parameter - adapter handles its own fetching - return partyAdapter.getByShortcode(shortcode) -} - -export async function create( - fetch: FetchLike, - payload: Partial, - headers?: Record -): Promise<{ party: Party; editKey?: string }> { - // The adapter returns the party directly, we need to wrap it - // to maintain backward compatibility with editKey - const party = await partyAdapter.create(payload, headers) - - // Note: editKey is returned in headers by the adapter if present - // For now, we'll return just the party - return { - party, - editKey: undefined // Edit key handling may need adjustment - } -} - -export async function update( - fetch: FetchLike, - id: string, - payload: Partial, - headers?: Record -): Promise { - return partyAdapter.update({ shortcode: id, ...payload }, headers) -} - -export async function remix( - fetch: FetchLike, - shortcode: string, - localId?: string, - headers?: Record -): Promise<{ party: Party; editKey?: string }> { - const party = await partyAdapter.remix(shortcode, headers) - - return { - party, - editKey: undefined // Edit key handling may need adjustment - } -} - -export async function deleteParty( - fetch: FetchLike, - id: string, - headers?: Record -): Promise { - return partyAdapter.delete(id, headers) -} - -/** - * List public parties for explore page - */ -export async function list( - fetch: FetchLike, - params?: { - page?: number - per_page?: number - raid_id?: string - element?: number - } -): Promise<{ - items: Party[] - total: number - totalPages: number - perPage: number -}> { - // Map parameters to adapter format - const adapterParams = { - page: params?.page, - per: params?.per_page, - raidId: params?.raid_id, - element: params?.element - } - - const response = await partyAdapter.list(adapterParams) - - // Map adapter response to expected format - return { - items: response.results, - total: response.total, - totalPages: response.totalPages, - perPage: response.per || 20 - } -} - -export async function getUserParties( - fetch: FetchLike, - username: string, - filters?: { - raid?: string - element?: number - recency?: number - page?: number - } -): Promise<{ - parties: Party[] - meta?: { - count?: number - totalPages?: number - perPage?: number - } -}> { - // Map parameters to adapter format - const adapterParams = { - username, - page: filters?.page, - per: 20, // Default page size - visibility: undefined, // Not specified in original - raidId: filters?.raid, - element: filters?.element, - recency: filters?.recency - } - - const response = await partyAdapter.listUserParties(adapterParams) - - // Map adapter response to expected format - return { - parties: response.results, - meta: { - count: response.total, - totalPages: response.totalPages, - perPage: response.per || 20 - } - } -} - -// Grid operations - These should eventually move to GridAdapter -export async function updateWeaponGrid( - fetch: FetchLike, - partyId: string, - payload: any, - headers?: Record -): Promise { - // For now, use gridUpdate with a single operation - // This is a temporary implementation until GridAdapter is fully integrated - const operation = { - type: 'add' as const, - entity: 'weapon' as const, - ...payload - } - - const response = await partyAdapter.gridUpdate(partyId, [operation]) - - // Check for conflicts - if ('conflicts' in response && response.conflicts) { - const error = new Error('Weapon conflict') as any - error.conflicts = response - throw error - } - - return response.party -} - -export async function updateSummonGrid( - fetch: FetchLike, - partyId: string, - payload: any, - headers?: Record -): Promise { - // For now, use gridUpdate with a single operation - const operation = { - type: 'add' as const, - entity: 'summon' as const, - ...payload - } - - const response = await partyAdapter.gridUpdate(partyId, [operation]) - return response.party -} - -export async function updateCharacterGrid( - fetch: FetchLike, - partyId: string, - payload: any, - headers?: Record -): Promise { - // For now, use gridUpdate with a single operation - const operation = { - type: 'add' as const, - entity: 'character' as const, - ...payload - } - - const response = await partyAdapter.gridUpdate(partyId, [operation]) - - // Check for conflicts - if ('conflicts' in response && response.conflicts) { - const error = new Error('Character conflict') as any - error.conflicts = response - throw error - } - - return response.party -} \ No newline at end of file diff --git a/src/lib/api/resources/users.ts b/src/lib/api/resources/users.ts deleted file mode 100644 index bcc96d4d..00000000 --- a/src/lib/api/resources/users.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { FetchLike } from '../core' -import { get, buildUrl } from '../core' - -export interface UserInfoResponse { - id: string - username: string - language: string - private: boolean - gender: number - theme: string - role: number - avatar: { - picture: string - element: string - } -} - -export const users = { - info: (f: FetchLike, username: string, init?: RequestInit) => - get(f, `/users/info/${encodeURIComponent(username)}`, undefined, init) -} - -export interface UserProfileResponse { - profile: UserInfoResponse & { parties?: any[] } - meta?: { count?: number; total_pages?: number; per_page?: number } -} - -export async function profile( - f: FetchLike, - username: string, - page?: number -): Promise<{ user: UserInfoResponse; items: any[]; page: number; total?: number; totalPages?: number; perPage?: number }> { - const qs = page && page > 1 ? { page } : undefined - const url = buildUrl(`/users/${encodeURIComponent(username)}`, qs as any) - const resp = await f(url, { credentials: 'include' }) - if (!resp.ok) throw new Error(resp.statusText || 'Failed to load profile') - const json = (await resp.json()) as UserProfileResponse - const items = Array.isArray(json.profile?.parties) ? json.profile.parties : [] - return { - user: json.profile as any, - items, - page: page || 1, - total: json.meta?.count, - totalPages: json.meta?.total_pages, - perPage: json.meta?.per_page - } -} diff --git a/src/routes/[username]/+page.server.ts b/src/routes/[username]/+page.server.ts index 67533914..9b60337b 100644 --- a/src/routes/[username]/+page.server.ts +++ b/src/routes/[username]/+page.server.ts @@ -1,10 +1,9 @@ import type { PageServerLoad } from './$types' import { error } from '@sveltejs/kit' -import { profile } from '$lib/api/resources/users' +import { userAdapter } from '$lib/api/adapters' import { parseParty } from '$lib/api/schemas/party' -import * as partiesApi from '$lib/api/resources/parties' -export const load: PageServerLoad = async ({ fetch, params, url, depends, locals }) => { +export const load: PageServerLoad = async ({ params, url, depends, locals }) => { depends('app:profile') const username = params.username const pageParam = url.searchParams.get('page') @@ -14,11 +13,20 @@ export const load: PageServerLoad = async ({ fetch, params, url, depends, locals try { if (tab === 'favorites' && isOwner) { - const fav = await partiesApi.favorites(fetch as any, { page }) - return { user: { username } as any, items: fav.items, page: fav.page, total: fav.total, totalPages: fav.totalPages, perPage: fav.perPage, tab, isOwner } + const fav = await userAdapter.getFavorites({ page }) + return { + user: { username } as any, + items: fav.items, + page: fav.page, + total: fav.total, + totalPages: fav.totalPages, + perPage: fav.perPage, + tab, + isOwner + } } - const { user, items, total, totalPages, perPage } = await profile(fetch as any, username, page) + const { user, items, total, totalPages, perPage } = await userAdapter.getProfile(username, page) const parties = items.map((p) => parseParty(p)) return { user, items: parties, page, total, totalPages, perPage, tab, isOwner } } catch (e: any) { diff --git a/src/routes/teams/explore/+page.server.ts b/src/routes/teams/explore/+page.server.ts index ad40633f..e790f1b2 100644 --- a/src/routes/teams/explore/+page.server.ts +++ b/src/routes/teams/explore/+page.server.ts @@ -1,8 +1,8 @@ import type { PageServerLoad } from './$types' import { error } from '@sveltejs/kit' -import * as parties from '$lib/api/resources/parties' +import { partyAdapter } from '$lib/api/adapters' -export const load: PageServerLoad = async ({ fetch, url, depends }) => { +export const load: PageServerLoad = async ({ url, depends }) => { depends('app:parties:list') const pageParam = url.searchParams.get('page') @@ -12,9 +12,16 @@ export const load: PageServerLoad = async ({ fetch, url, depends }) => { console.log('[explore/+page.server.ts] Full URL:', url.toString()) try { - const { items, total, totalPages, perPage } = await parties.list(fetch, { page }) - console.log('[explore/+page.server.ts] Successfully loaded', items.length, 'parties') - return { items, page, total, totalPages, perPage } + const response = await partyAdapter.list({ page }) + console.log('[explore/+page.server.ts] Successfully loaded', response.results.length, 'parties') + + return { + items: response.results, + page, + total: response.total, + totalPages: response.totalPages, + perPage: response.per || 20 + } } catch (e: any) { console.error('[explore/+page.server.ts] Failed to load teams:', { error: e,