fix: adapter initialization and image loading
- Add centralized adapter config with proper baseURL - Fix API response transformation (object -> weapon/character/summon) - Update components to use image service and correct property names - Fix Svelte 5 $derived syntax and missing PartyAdapter.list method Images now display correctly in grids.
This commit is contained in:
parent
842321efd2
commit
1a6a112efd
15 changed files with 101 additions and 86 deletions
|
|
@ -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<T>(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<T>(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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
24
src/lib/api/adapters/config.ts
Normal file
24
src/lib/api/adapters/config.ts
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
export const entityAdapter = new EntityAdapter(DEFAULT_ADAPTER_CONFIG)
|
||||
|
|
@ -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()
|
||||
export const gridAdapter = new GridAdapter(DEFAULT_ADAPTER_CONFIG)
|
||||
|
|
@ -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<PaginatedResponse<Party>> {
|
||||
return this.request<PaginatedResponse<Party>>('/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()
|
||||
export const partyAdapter = new PartyAdapter(DEFAULT_ADAPTER_CONFIG)
|
||||
|
|
@ -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<string, AbortController>()
|
||||
|
||||
constructor(options: PartyResourceOptions = {}) {
|
||||
this.adapter = options.adapter || new PartyAdapter()
|
||||
this.adapter = options.adapter || partyAdapter
|
||||
this.optimistic = options.optimistic ?? true
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string, AbortController>()
|
||||
|
||||
constructor(options: SearchResourceOptions = {}) {
|
||||
this.adapter = options.adapter || new SearchAdapter()
|
||||
this.adapter = options.adapter || searchAdapter
|
||||
this.debounceMs = options.debounceMs || 300
|
||||
this.baseParams = options.initialParams || {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
export const searchAdapter = new SearchAdapter(DEFAULT_ADAPTER_CONFIG)
|
||||
|
|
@ -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()
|
||||
export const userAdapter = new UserAdapter(DEFAULT_ADAPTER_CONFIG)
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import type { Party, GridWeapon, GridCharacter } from '$lib/types/api/party'
|
||||
import { getElementClass } from '$lib/types/enums'
|
||||
import { getCharacterImage } from '$lib/features/database/detail/image'
|
||||
|
||||
interface Props {
|
||||
party?: Party
|
||||
|
|
@ -12,8 +13,8 @@
|
|||
|
||||
let { party, characters: directCharacters, jobId, element, gender }: Props = $props()
|
||||
|
||||
// Use direct characters if provided, otherwise get from party
|
||||
const characters = $derived(directCharacters || party?.characters || [])
|
||||
// Use direct characters if provided, otherwise get from party (note: API returns gridCharacters)
|
||||
const characters = $derived(directCharacters || party?.gridCharacters || [])
|
||||
const grid = $derived(Array.from({ length: 3 }, (_, i) =>
|
||||
characters.find((c: GridCharacter) => c?.position === i)
|
||||
))
|
||||
|
|
@ -23,7 +24,7 @@
|
|||
element ? getElementClass(element) :
|
||||
// Otherwise try to get from party's mainhand weapon
|
||||
party ? (() => {
|
||||
const main: GridWeapon | undefined = (party.weapons || []).find(
|
||||
const main: GridWeapon | undefined = (party.gridWeapons || []).find(
|
||||
(w: GridWeapon) => w?.mainhand || w?.position === -1
|
||||
)
|
||||
const el = main?.element ?? main?.weapon?.element
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import type { Party, GridSummon } from '$lib/types/api/party'
|
||||
import { getSummonImage } from '$lib/features/database/detail/image'
|
||||
|
||||
interface Props {
|
||||
party?: Party
|
||||
|
|
@ -9,8 +10,8 @@
|
|||
|
||||
let { party, summons: directSummons, extendedView = false }: Props = $props()
|
||||
|
||||
// Use direct summons if provided, otherwise get from party
|
||||
const summons = $derived(directSummons || party?.summons || [])
|
||||
// Use direct summons if provided, otherwise get from party (note: API returns gridSummons)
|
||||
const summons = $derived(directSummons || party?.gridSummons || [])
|
||||
const main = $derived(summons.find((s: GridSummon) => s?.main || s?.position === -1))
|
||||
const friend = $derived(
|
||||
extendedView ? summons.find((s: GridSummon) => s?.friend || s?.position === -2) : undefined
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import type { Party, GridWeapon } from '$lib/types/api/party'
|
||||
import { getWeaponImage } from '$lib/features/database/detail/image'
|
||||
|
||||
interface Props {
|
||||
party?: Party
|
||||
|
|
@ -8,21 +9,19 @@
|
|||
|
||||
let { party, weapons: directWeapons }: Props = $props()
|
||||
|
||||
// Use direct weapons if provided, otherwise get from party
|
||||
// Use direct weapons if provided, otherwise get from party (note: API returns weapons after transformation)
|
||||
const weapons = $derived(directWeapons || party?.weapons || [])
|
||||
const mainhand = $derived(weapons.find((w: GridWeapon) => w?.mainhand || w?.position === -1))
|
||||
const grid = $derived(
|
||||
Array.from({ length: 9 }, (_, i) => weapons.find((w: GridWeapon) => w?.position === i))
|
||||
)
|
||||
|
||||
|
||||
function weaponImageUrl(w?: GridWeapon, isMain = false): string {
|
||||
const id = w?.weapon?.granblueId
|
||||
if (!id) return ''
|
||||
const folder = isMain ? 'weapon-main' : 'weapon-grid'
|
||||
const objElement = w?.weapon?.element
|
||||
const instElement = w?.element
|
||||
if (objElement === 0 && instElement) return `/images/${folder}/${id}_${instElement}.jpg`
|
||||
return `/images/${folder}/${id}.jpg`
|
||||
const variant = isMain ? 'main' : 'grid'
|
||||
// For weapons with null element that have an instance element, use it
|
||||
const element = (w?.weapon?.element === 0 && w?.element) ? w.element : undefined
|
||||
return getWeaponImage(w?.weapon?.granblueId, variant, element)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
<div class="actions">
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
<div class="actions">
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
<div class="actions">
|
||||
|
|
|
|||
Loading…
Reference in a new issue