diff --git a/src/lib/api/adapters/base.adapter.ts b/src/lib/api/adapters/base.adapter.ts index 133bb2bb..3c0fbad5 100644 --- a/src/lib/api/adapters/base.adapter.ts +++ b/src/lib/api/adapters/base.adapter.ts @@ -9,7 +9,7 @@ */ import { snakeToCamel, camelToSnake } from '../schemas/transforms' -import { API_BASE } from '../core' +import { transformResponse, transformRequest } from '../client' import type { AdapterOptions, RequestOptions, AdapterError } from './types' import { createErrorFromStatus, @@ -57,8 +57,10 @@ export abstract class BaseAdapter { * @param options.onError - Global error handler callback */ constructor(options: AdapterOptions = {}) { + // Default to localhost if no baseURL provided + const baseURL = options.baseURL ?? 'http://localhost:3000/api/v1' this.options = { - baseURL: options.baseURL ?? API_BASE, + baseURL, timeout: options.timeout ?? 30000, retries: options.retries ?? 3, cacheTime: options.cacheTime ?? 0, @@ -174,34 +176,34 @@ export abstract class BaseAdapter { } /** - * Transforms response data from snake_case to camelCase + * Transforms response data from snake_case to camelCase and object->entity * * @template T - The expected response type * @param data - Raw response data from the API - * @returns Transformed data with camelCase property names + * @returns Transformed data with camelCase property names and proper entity fields */ protected transformResponse(data: any): T { if (data === null || data === undefined) { return data } - // Apply snake_case to camelCase transformation - return snakeToCamel(data) as T + // Apply full transformation: snake_case->camelCase and object->entity + return transformResponse(data) } /** - * Transforms request data from camelCase to snake_case + * Transforms request data from camelCase to snake_case and entity->object * - * @param data - Request data with camelCase property names - * @returns Transformed data with snake_case property names + * @param data - Request data with camelCase property names and entity fields + * @returns Transformed data with snake_case property names and object fields */ protected transformRequest(data: any): any { if (data === null || data === undefined) { return data } - // Apply camelCase to snake_case transformation - return camelToSnake(data) + // Apply full transformation: entity->object and camelCase->snake_case + return transformRequest(data) } /** diff --git a/src/lib/api/adapters/config.ts b/src/lib/api/adapters/config.ts new file mode 100644 index 00000000..72e13ae2 --- /dev/null +++ b/src/lib/api/adapters/config.ts @@ -0,0 +1,24 @@ +/** + * Configuration for API adapters + */ +import { PUBLIC_SIERO_API_URL } from '$env/static/public' + +/** + * Get the base URL for API requests + * Handles both server and client environments + */ +export function getApiBaseUrl(): string { + // Use environment variable if available, otherwise default to localhost + const base = PUBLIC_SIERO_API_URL || 'http://localhost:3000' + return `${base}/api/v1` +} + +/** + * Default configuration for all adapters + */ +export const DEFAULT_ADAPTER_CONFIG = { + baseURL: getApiBaseUrl(), + timeout: 30000, + retries: 3, + cacheTime: 0 +} \ No newline at end of file diff --git a/src/lib/api/adapters/entity.adapter.ts b/src/lib/api/adapters/entity.adapter.ts index 801c024b..115987f3 100644 --- a/src/lib/api/adapters/entity.adapter.ts +++ b/src/lib/api/adapters/entity.adapter.ts @@ -10,6 +10,7 @@ import { BaseAdapter } from './base.adapter' import type { AdapterOptions } from './types' +import { DEFAULT_ADAPTER_CONFIG } from './config' /** * Canonical weapon data from the game @@ -108,14 +109,6 @@ export interface Summon { * Entity adapter for accessing canonical game data */ export class EntityAdapter extends BaseAdapter { - constructor(options?: AdapterOptions) { - super({ - ...options, - baseURL: options?.baseURL || '/api/v1', - // Cache entity data for longer since it rarely changes - cacheTime: options?.cacheTime || 300000 // 5 minutes default - }) - } /** * Gets canonical weapon data by ID @@ -190,4 +183,4 @@ export class EntityAdapter extends BaseAdapter { /** * Default entity adapter instance */ -export const entityAdapter = new EntityAdapter() \ No newline at end of file +export const entityAdapter = new EntityAdapter(DEFAULT_ADAPTER_CONFIG) \ No newline at end of file diff --git a/src/lib/api/adapters/grid.adapter.ts b/src/lib/api/adapters/grid.adapter.ts index 0b9cc969..a2cbe922 100644 --- a/src/lib/api/adapters/grid.adapter.ts +++ b/src/lib/api/adapters/grid.adapter.ts @@ -9,6 +9,7 @@ import { BaseAdapter } from './base.adapter' import type { AdapterOptions } from './types' +import { DEFAULT_ADAPTER_CONFIG } from './config' /** * Common grid item structure @@ -143,12 +144,6 @@ export interface ResolveConflictParams { * Grid adapter for managing user's grid item instances */ export class GridAdapter extends BaseAdapter { - constructor(options?: AdapterOptions) { - super({ - ...options, - baseURL: options?.baseURL || '/api/v1' - }) - } // Weapon operations @@ -401,4 +396,4 @@ export class GridAdapter extends BaseAdapter { /** * Default grid adapter instance */ -export const gridAdapter = new GridAdapter() \ No newline at end of file +export const gridAdapter = new GridAdapter(DEFAULT_ADAPTER_CONFIG) \ No newline at end of file diff --git a/src/lib/api/adapters/party.adapter.ts b/src/lib/api/adapters/party.adapter.ts index a450039d..7d67af63 100644 --- a/src/lib/api/adapters/party.adapter.ts +++ b/src/lib/api/adapters/party.adapter.ts @@ -10,6 +10,7 @@ import { BaseAdapter } from './base.adapter' import type { RequestOptions, AdapterOptions, PaginatedResponse } from './types' +import { DEFAULT_ADAPTER_CONFIG } from './config' /** * Party data structure @@ -252,6 +253,17 @@ export class PartyAdapter extends BaseAdapter { }) } + /** + * Lists all public parties (explore page) + */ + async list(params: { page?: number; per?: number } = {}): Promise> { + return this.request>('/parties', { + method: 'GET', + query: params, + cacheTTL: 30000 // Cache for 30 seconds + }) + } + /** * Lists parties for a specific user */ @@ -401,4 +413,4 @@ export class PartyAdapter extends BaseAdapter { /** * Default party adapter instance */ -export const partyAdapter = new PartyAdapter() \ No newline at end of file +export const partyAdapter = new PartyAdapter(DEFAULT_ADAPTER_CONFIG) \ No newline at end of file diff --git a/src/lib/api/adapters/resources/party.resource.svelte.ts b/src/lib/api/adapters/resources/party.resource.svelte.ts index 265c4a8f..e3ca6e84 100644 --- a/src/lib/api/adapters/resources/party.resource.svelte.ts +++ b/src/lib/api/adapters/resources/party.resource.svelte.ts @@ -7,7 +7,7 @@ * @module adapters/resources/party */ -import { PartyAdapter, type Party, type CreatePartyParams, type UpdatePartyParams } from '../party.adapter' +import { PartyAdapter, partyAdapter, type Party, type CreatePartyParams, type UpdatePartyParams } from '../party.adapter' import type { AdapterError } from '../types' /** @@ -88,7 +88,7 @@ export class PartyResource { private activeRequests = new Map() constructor(options: PartyResourceOptions = {}) { - this.adapter = options.adapter || new PartyAdapter() + this.adapter = options.adapter || partyAdapter this.optimistic = options.optimistic ?? true } diff --git a/src/lib/api/adapters/resources/search.resource.svelte.ts b/src/lib/api/adapters/resources/search.resource.svelte.ts index 36545ccb..1a1f11a7 100644 --- a/src/lib/api/adapters/resources/search.resource.svelte.ts +++ b/src/lib/api/adapters/resources/search.resource.svelte.ts @@ -8,7 +8,7 @@ */ import { debounced } from 'runed' -import { SearchAdapter, type SearchParams, type SearchResponse } from '../search.adapter' +import { SearchAdapter, searchAdapter, type SearchParams, type SearchResponse } from '../search.adapter' import type { AdapterError } from '../types' /** @@ -93,7 +93,7 @@ export class SearchResource { private activeRequests = new Map() constructor(options: SearchResourceOptions = {}) { - this.adapter = options.adapter || new SearchAdapter() + this.adapter = options.adapter || searchAdapter this.debounceMs = options.debounceMs || 300 this.baseParams = options.initialParams || {} } diff --git a/src/lib/api/adapters/search.adapter.ts b/src/lib/api/adapters/search.adapter.ts index 5ca8ac11..133c2205 100644 --- a/src/lib/api/adapters/search.adapter.ts +++ b/src/lib/api/adapters/search.adapter.ts @@ -9,6 +9,7 @@ import { BaseAdapter } from './base.adapter' import type { AdapterOptions, SearchFilters } from './types' +import { DEFAULT_ADAPTER_CONFIG } from './config' /** * Search parameters for entity queries @@ -293,4 +294,4 @@ export class SearchAdapter extends BaseAdapter { * Default singleton instance for search operations * Use this for most search needs unless you need custom configuration */ -export const searchAdapter = new SearchAdapter() \ No newline at end of file +export const searchAdapter = new SearchAdapter(DEFAULT_ADAPTER_CONFIG) \ No newline at end of file diff --git a/src/lib/api/adapters/user.adapter.ts b/src/lib/api/adapters/user.adapter.ts index 86372920..c8d1e772 100644 --- a/src/lib/api/adapters/user.adapter.ts +++ b/src/lib/api/adapters/user.adapter.ts @@ -1,5 +1,6 @@ import { BaseAdapter } from './base.adapter' import type { Party } from '$lib/types/api/party' +import { DEFAULT_ADAPTER_CONFIG } from './config' export interface UserInfo { id: string @@ -134,4 +135,4 @@ export class UserAdapter extends BaseAdapter { } } -export const userAdapter = new UserAdapter() \ No newline at end of file +export const userAdapter = new UserAdapter(DEFAULT_ADAPTER_CONFIG) \ No newline at end of file diff --git a/src/lib/components/reps/CharacterRep.svelte b/src/lib/components/reps/CharacterRep.svelte index 8525c3ae..8c31dad2 100644 --- a/src/lib/components/reps/CharacterRep.svelte +++ b/src/lib/components/reps/CharacterRep.svelte @@ -1,6 +1,7 @@ diff --git a/src/lib/components/units/CharacterUnit.svelte b/src/lib/components/units/CharacterUnit.svelte index b027be38..0b22f7d6 100644 --- a/src/lib/components/units/CharacterUnit.svelte +++ b/src/lib/components/units/CharacterUnit.svelte @@ -6,6 +6,7 @@ import ContextMenu from '$lib/components/ui/ContextMenu.svelte' import { ContextMenu as ContextMenuBase } from 'bits-ui' import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte' + import { getCharacterImage } from '$lib/features/database/detail/image' interface Props { item?: GridCharacter @@ -33,24 +34,27 @@ return '—' } // Use $derived to ensure consistent computation between server and client - let imageUrl = $derived(() => { + let imageUrl = $derived.by(() => { // If no item or no character with granblueId, return placeholder if (!item || !item.character?.granblueId) { - return '/images/placeholders/placeholder-weapon-grid.png' + return getCharacterImage(null, undefined, 'main') } const id = item.character.granblueId const uncap = item?.uncapLevel ?? 0 const transStep = item?.transcendenceStep ?? 0 - let suffix = '01' - if (transStep > 0) suffix = '04' - else if (uncap >= 5) suffix = '03' - else if (uncap > 2) suffix = '02' + let pose = '01' + if (transStep > 0) pose = '04' + else if (uncap >= 5) pose = '03' + else if (uncap > 2) pose = '02' + + // Special handling for Gran/Djeeta (3030182000) - element-specific poses if (String(id) === '3030182000') { let element = mainWeaponElement || partyElement || 1 - suffix = `${suffix}_0${element}` + pose = `${pose}_0${element}` } - return `/images/character-main/${id}_${suffix}.jpg` + + return getCharacterImage(id, pose, 'main') }) async function remove() { @@ -95,7 +99,7 @@ class="image" class:placeholder={!item?.character?.granblueId} alt={displayName(item?.character)} - src={imageUrl()} + src={imageUrl} /> {#if ctx?.canEdit() && item?.id}
diff --git a/src/lib/components/units/SummonUnit.svelte b/src/lib/components/units/SummonUnit.svelte index 3ba17c98..03308ac2 100644 --- a/src/lib/components/units/SummonUnit.svelte +++ b/src/lib/components/units/SummonUnit.svelte @@ -6,6 +6,7 @@ import ContextMenu from '$lib/components/ui/ContextMenu.svelte' import { ContextMenu as ContextMenuBase } from 'bits-ui' import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte' + import { getSummonImage } from '$lib/features/database/detail/image' interface Props { item?: GridSummon @@ -31,20 +32,12 @@ return '—' } // Use $derived to ensure consistent computation between server and client - let imageUrl = $derived(() => { + let imageUrl = $derived.by(() => { // Check position first for main/friend summon determination const isMain = position === -1 || position === 6 || item?.main || item?.friend + const variant = isMain ? 'main' : 'grid' - // If no item or no summon with granblueId, return placeholder - if (!item || !item.summon?.granblueId) { - return isMain - ? '/images/placeholders/placeholder-summon-main.png' - : '/images/placeholders/placeholder-summon-grid.png' - } - - const id = item.summon.granblueId - const folder = isMain ? 'summon-main' : 'summon-grid' - return `/images/${folder}/${id}.jpg` + return getSummonImage(item?.summon?.granblueId, variant) }) async function remove() { @@ -92,7 +85,7 @@ class="image" class:placeholder={!item?.summon?.granblueId} alt={displayName(item?.summon)} - src={imageUrl()} + src={imageUrl} /> {#if ctx?.canEdit() && item?.id}
diff --git a/src/lib/components/units/WeaponUnit.svelte b/src/lib/components/units/WeaponUnit.svelte index 4d9a2271..929ed301 100644 --- a/src/lib/components/units/WeaponUnit.svelte +++ b/src/lib/components/units/WeaponUnit.svelte @@ -6,6 +6,7 @@ import ContextMenu from '$lib/components/ui/ContextMenu.svelte' import { ContextMenu as ContextMenuBase } from 'bits-ui' import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte' + import { getWeaponImage } from '$lib/features/database/detail/image' interface Props { item?: GridWeapon @@ -33,26 +34,14 @@ } // Use $derived to ensure consistent computation between server and client - let imageUrl = $derived(() => { - // Check position first for main weapon determination + let imageUrl = $derived.by(() => { const isMain = position === -1 || item?.mainhand + const variant = isMain ? 'main' : 'grid' - // If no item or no weapon with granblueId, return placeholder - if (!item || !item.weapon?.granblueId) { - return isMain - ? '/images/placeholders/placeholder-weapon-main.png' - : '/images/placeholders/placeholder-weapon-grid.png' - } + // For weapons with null element that have an instance element, use it + const element = (item?.weapon?.element === 0 && item?.element) ? item.element : undefined - const id = item.weapon.granblueId - const folder = isMain ? 'weapon-main' : 'weapon-grid' - const objElement = item.weapon?.element - const instElement = item?.element - - if (objElement === 0 && instElement) { - return `/images/${folder}/${id}_${instElement}.jpg` - } - return `/images/${folder}/${id}.jpg` + return getWeaponImage(item?.weapon?.granblueId, variant, element) }) async function remove() { @@ -98,7 +87,7 @@ class="image" class:placeholder={!item?.weapon?.granblueId} alt={displayName(item?.weapon)} - src={imageUrl()} + src={imageUrl} /> {#if ctx?.canEdit() && item?.id}