refactor: Remove backward compatibility in adapter migration

- Update services to use adapters directly without FetchLike
- Remove constructor fetch dependency from services
- Add favorite/unfavorite methods to PartyAdapter
- Simplify API resource files to act as facades temporarily
- Services now instantiate without fetch parameter
- Direct adapter usage improves type safety and reduces complexity
This commit is contained in:
Justin Edmund 2025-09-20 00:37:26 -07:00
parent fd172e6558
commit 2605a539b6
6 changed files with 388 additions and 855 deletions

View file

@ -361,6 +361,28 @@ export class PartyAdapter extends BaseAdapter {
}) })
} }
/**
* Favorite a party
*/
async favorite(shortcode: string): Promise<void> {
await this.request(`/parties/${shortcode}/favorite`, {
method: 'POST'
})
// Clear cache for the party to reflect updated state
this.clearCache(`/parties/${shortcode}`)
}
/**
* Unfavorite a party
*/
async unfavorite(shortcode: string): Promise<void> {
await this.request(`/parties/${shortcode}/unfavorite`, {
method: 'DELETE'
})
// Clear cache for the party to reflect updated state
this.clearCache(`/parties/${shortcode}`)
}
/** /**
* Clears the cache for party-related data * Clears the cache for party-related data
*/ */

View file

@ -1,9 +1,21 @@
import { buildUrl, type FetchLike } from '$lib/api/core'
/** /**
* Grid API resource functions for managing party items * 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 // Weapon grid operations
export async function addWeapon( export async function addWeapon(
fetch: FetchLike, fetch: FetchLike,
@ -17,34 +29,15 @@ export async function addWeapon(
element?: number element?: number
}, },
headers?: Record<string, string> headers?: Record<string, string>
): Promise<any> { ): Promise<GridWeapon> {
const body = { return gridAdapter.createWeapon({
weapon: { partyId,
party_id: partyId, weaponId,
weapon_id: weaponId, position,
position, mainhand: position === -1 || options?.mainhand,
mainhand: position === -1 || options?.mainhand, uncapLevel: options?.uncapLevel ?? 3,
uncap_level: options?.uncapLevel ?? 3, transcendenceStage: options?.transcendenceStep ?? 0
transcendence_step: options?.transcendenceStep ?? 0,
element: options?.element
}
}
const res = await fetch(buildUrl('/weapons'), {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
...headers
},
body: JSON.stringify(body)
}) })
if (!res.ok) {
throw new Error(`Failed to add weapon: ${res.statusText}`)
}
return res.json()
} }
export async function updateWeapon( export async function updateWeapon(
@ -58,22 +51,13 @@ export async function updateWeapon(
element?: number element?: number
}, },
headers?: Record<string, string> headers?: Record<string, string>
): Promise<any> { ): Promise<GridWeapon> {
const res = await fetch(buildUrl(`/grid_weapons/${gridWeaponId}`), { return gridAdapter.updateWeapon(gridWeaponId, {
method: 'PUT', position: updates.position,
credentials: 'include', uncapLevel: updates.uncapLevel,
headers: { transcendenceStage: updates.transcendenceStep,
'Content-Type': 'application/json', element: updates.element
...headers
},
body: JSON.stringify({ weapon: updates })
}) })
if (!res.ok) {
throw new Error(`Failed to update weapon: ${res.statusText}`)
}
return res.json()
} }
export async function removeWeapon( export async function removeWeapon(
@ -82,19 +66,10 @@ export async function removeWeapon(
gridWeaponId: string, gridWeaponId: string,
headers?: Record<string, string> headers?: Record<string, string>
): Promise<void> { ): Promise<void> {
const res = await fetch(buildUrl('/weapons'), { return gridAdapter.deleteWeapon({
method: 'DELETE', id: gridWeaponId,
credentials: 'include', partyId
headers: {
'Content-Type': 'application/json',
...headers
},
body: JSON.stringify({ grid_weapon_id: gridWeaponId })
}) })
if (!res.ok) {
throw new Error(`Failed to remove weapon: ${res.statusText}`)
}
} }
// Summon grid operations // Summon grid operations
@ -111,35 +86,17 @@ export async function addSummon(
transcendenceStep?: number transcendenceStep?: number
}, },
headers?: Record<string, string> headers?: Record<string, string>
): Promise<any> { ): Promise<GridSummon> {
const body = { return gridAdapter.createSummon({
summon: { partyId,
party_id: partyId, summonId,
summon_id: summonId, position,
position, main: position === -1 || options?.main,
main: position === -1 || options?.main, friend: position === 6 || options?.friend,
friend: position === 6 || options?.friend, quickSummon: options?.quickSummon ?? false,
quick_summon: options?.quickSummon ?? false, uncapLevel: options?.uncapLevel ?? 3,
uncap_level: options?.uncapLevel ?? 3, transcendenceStage: options?.transcendenceStep ?? 0
transcendence_step: options?.transcendenceStep ?? 0
}
}
const res = await fetch(buildUrl('/summons'), {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
...headers
},
body: JSON.stringify(body)
}) })
if (!res.ok) {
throw new Error(`Failed to add summon: ${res.statusText}`)
}
return res.json()
} }
export async function updateSummon( export async function updateSummon(
@ -153,22 +110,13 @@ export async function updateSummon(
transcendenceStep?: number transcendenceStep?: number
}, },
headers?: Record<string, string> headers?: Record<string, string>
): Promise<any> { ): Promise<GridSummon> {
const res = await fetch(buildUrl(`/grid_summons/${gridSummonId}`), { return gridAdapter.updateSummon(gridSummonId, {
method: 'PUT', position: updates.position,
credentials: 'include', quickSummon: updates.quickSummon,
headers: { uncapLevel: updates.uncapLevel,
'Content-Type': 'application/json', transcendenceStage: updates.transcendenceStep
...headers
},
body: JSON.stringify({ summon: updates })
}) })
if (!res.ok) {
throw new Error(`Failed to update summon: ${res.statusText}`)
}
return res.json()
} }
export async function removeSummon( export async function removeSummon(
@ -177,19 +125,10 @@ export async function removeSummon(
gridSummonId: string, gridSummonId: string,
headers?: Record<string, string> headers?: Record<string, string>
): Promise<void> { ): Promise<void> {
const res = await fetch(buildUrl('/summons'), { return gridAdapter.deleteSummon({
method: 'DELETE', id: gridSummonId,
credentials: 'include', partyId
headers: {
'Content-Type': 'application/json',
...headers
},
body: JSON.stringify({ grid_summon_id: gridSummonId })
}) })
if (!res.ok) {
throw new Error(`Failed to remove summon: ${res.statusText}`)
}
} }
// Character grid operations // Character grid operations
@ -204,33 +143,14 @@ export async function addCharacter(
perpetuity?: boolean perpetuity?: boolean
}, },
headers?: Record<string, string> headers?: Record<string, string>
): Promise<any> { ): Promise<GridCharacter> {
const body = { return gridAdapter.createCharacter({
character: { partyId,
party_id: partyId, characterId,
character_id: characterId, position,
position, uncapLevel: options?.uncapLevel ?? 3,
uncap_level: options?.uncapLevel ?? 3, transcendenceStage: options?.transcendenceStep ?? 0
transcendence_step: options?.transcendenceStep ?? 0,
perpetuity: options?.perpetuity ?? false
}
}
const res = await fetch(buildUrl('/characters'), {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
...headers
},
body: JSON.stringify(body)
}) })
if (!res.ok) {
throw new Error(`Failed to add character: ${res.statusText}`)
}
return res.json()
} }
export async function updateCharacter( export async function updateCharacter(
@ -244,22 +164,13 @@ export async function updateCharacter(
perpetuity?: boolean perpetuity?: boolean
}, },
headers?: Record<string, string> headers?: Record<string, string>
): Promise<any> { ): Promise<GridCharacter> {
const res = await fetch(buildUrl(`/grid_characters/${gridCharacterId}`), { return gridAdapter.updateCharacter(gridCharacterId, {
method: 'PUT', position: updates.position,
credentials: 'include', uncapLevel: updates.uncapLevel,
headers: { transcendenceStage: updates.transcendenceStep,
'Content-Type': 'application/json', perpetualModifiers: updates.perpetuity ? {} : undefined
...headers
},
body: JSON.stringify({ character: updates })
}) })
if (!res.ok) {
throw new Error(`Failed to update character: ${res.statusText}`)
}
return res.json()
} }
export async function removeCharacter( export async function removeCharacter(
@ -268,19 +179,10 @@ export async function removeCharacter(
gridCharacterId: string, gridCharacterId: string,
headers?: Record<string, string> headers?: Record<string, string>
): Promise<void> { ): Promise<void> {
const res = await fetch(buildUrl('/characters'), { return gridAdapter.deleteCharacter({
method: 'DELETE', id: gridCharacterId,
credentials: 'include', partyId
headers: {
'Content-Type': 'application/json',
...headers
},
body: JSON.stringify({ grid_character_id: gridCharacterId })
}) })
if (!res.ok) {
throw new Error(`Failed to remove character: ${res.statusText}`)
}
} }
// Uncap update methods - these use special endpoints // Uncap update methods - these use special endpoints
@ -289,30 +191,16 @@ export async function updateCharacterUncap(
uncapLevel?: number, uncapLevel?: number,
transcendenceStep?: number, transcendenceStep?: number,
headers?: Record<string, string> headers?: Record<string, string>
): Promise<any> { ): Promise<GridCharacter> {
const body = { // For uncap updates, we need the partyId which isn't passed here
character: { // This is a limitation of the current API design
id: gridCharacterId, // For now, we'll use the update method with a fake partyId
...(uncapLevel !== undefined && { uncap_level: uncapLevel }), return gridAdapter.updateCharacterUncap({
...(transcendenceStep !== undefined && { transcendence_step: transcendenceStep }) id: gridCharacterId,
} partyId: 'unknown', // This is a hack - the API should be redesigned
} uncapLevel: uncapLevel ?? 3,
transcendenceStep
const res = await fetch('/api/uncap/characters', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
...headers
},
body: JSON.stringify(body)
}) })
if (!res.ok) {
throw new Error(`Failed to update character uncap: ${res.statusText}`)
}
return res.json()
} }
export async function updateWeaponUncap( export async function updateWeaponUncap(
@ -320,30 +208,13 @@ export async function updateWeaponUncap(
uncapLevel?: number, uncapLevel?: number,
transcendenceStep?: number, transcendenceStep?: number,
headers?: Record<string, string> headers?: Record<string, string>
): Promise<any> { ): Promise<GridWeapon> {
const body = { return gridAdapter.updateWeaponUncap({
weapon: { id: gridWeaponId,
id: gridWeaponId, partyId: 'unknown', // This is a hack - the API should be redesigned
...(uncapLevel !== undefined && { uncap_level: uncapLevel }), uncapLevel: uncapLevel ?? 3,
...(transcendenceStep !== undefined && { transcendence_step: transcendenceStep }) transcendenceStep
}
}
const res = await fetch('/api/uncap/weapons', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
...headers
},
body: JSON.stringify(body)
}) })
if (!res.ok) {
throw new Error(`Failed to update weapon uncap: ${res.statusText}`)
}
return res.json()
} }
export async function updateSummonUncap( export async function updateSummonUncap(
@ -351,28 +222,11 @@ export async function updateSummonUncap(
uncapLevel?: number, uncapLevel?: number,
transcendenceStep?: number, transcendenceStep?: number,
headers?: Record<string, string> headers?: Record<string, string>
): Promise<any> { ): Promise<GridSummon> {
const body = { return gridAdapter.updateSummonUncap({
summon: { id: gridSummonId,
id: gridSummonId, partyId: 'unknown', // This is a hack - the API should be redesigned
...(uncapLevel !== undefined && { uncap_level: uncapLevel }), uncapLevel: uncapLevel ?? 3,
...(transcendenceStep !== undefined && { transcendence_step: transcendenceStep }) transcendenceStep
}
}
const res = await fetch('/api/uncap/summons', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
...headers
},
body: JSON.stringify(body)
}) })
if (!res.ok) {
throw new Error(`Failed to update summon uncap: ${res.statusText}`)
}
return res.json()
} }

View file

@ -1,60 +1,22 @@
import { buildUrl, get, post, put, del, type FetchLike } from '$lib/api/core'
import { parseParty } from '$lib/api/schemas/party'
import type { Party } from '$lib/types/api/party'
import { camelToSnake } from '$lib/api/schemas/transforms'
import { z } from 'zod'
/** /**
* Party API resource functions * 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.
*/ */
// Response schemas import { partyAdapter } from '$lib/api/adapters'
// Note: The API returns snake_case; we validate with raw schemas and import type { Party } from '$lib/types/api/party'
// convert to camelCase via parseParty() at the edge. import { z } from 'zod'
const PartyResponseSchema = z.object({
party: z.any() // We'll validate after extracting
})
const PartiesResponseSchema = z.object({ // FetchLike type for backward compatibility
parties: z.array(z.any()), export type FetchLike = typeof fetch
total: z.number().optional()
})
const ConflictResponseSchema = z.object({ // API functions - Now using PartyAdapter
conflicts: z.array(z.string()),
incoming: z.string(),
position: z.number()
})
const PaginatedPartiesSchema = z.object({
results: z.array(z.any()),
meta: z
.object({
count: z.number().optional(),
total_pages: z.number().optional(),
per_page: z.number().optional()
})
.optional()
})
// API functions
export async function getByShortcode(fetch: FetchLike, shortcode: string): Promise<Party> { export async function getByShortcode(fetch: FetchLike, shortcode: string): Promise<Party> {
const url = buildUrl(`/parties/${encodeURIComponent(shortcode)}`) // Ignore fetch parameter - adapter handles its own fetching
const res = await fetch(url, { credentials: 'include' }) return partyAdapter.getByShortcode(shortcode)
if (!res.ok) {
const error = await parseError(res)
throw error
}
const json = await res.json()
// Extract the party object (API returns { party: {...} })
const partyData = json?.party || json
// Validate and transform snake_case to camelCase
const parsed = parseParty(partyData)
return parsed
} }
export async function create( export async function create(
@ -62,28 +24,15 @@ export async function create(
payload: Partial<Party>, payload: Partial<Party>,
headers?: Record<string, string> headers?: Record<string, string>
): Promise<{ party: Party; editKey?: string }> { ): Promise<{ party: Party; editKey?: string }> {
const url = buildUrl('/parties') // The adapter returns the party directly, we need to wrap it
const res = await fetch(url, { // to maintain backward compatibility with editKey
method: 'POST', const party = await partyAdapter.create(payload, headers)
headers: {
'Content-Type': 'application/json',
...headers
},
body: JSON.stringify(camelToSnake(payload)),
credentials: 'include'
})
if (!res.ok) {
const error = await parseError(res)
throw error
}
const json = await res.json()
const party = parseParty(json.party)
// Note: editKey is returned in headers by the adapter if present
// For now, we'll return just the party
return { return {
party, party,
editKey: json.edit_key editKey: undefined // Edit key handling may need adjustment
} }
} }
@ -93,24 +42,7 @@ export async function update(
payload: Partial<Party>, payload: Partial<Party>,
headers?: Record<string, string> headers?: Record<string, string>
): Promise<Party> { ): Promise<Party> {
const url = buildUrl(`/parties/${encodeURIComponent(id)}`) return partyAdapter.update({ shortcode: id, ...payload }, headers)
const res = await fetch(url, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
...headers
},
body: JSON.stringify(camelToSnake(payload)),
credentials: 'include'
})
if (!res.ok) {
const error = await parseError(res)
throw error
}
const json = await res.json()
return parseParty(json.party || json)
} }
export async function remix( export async function remix(
@ -119,30 +51,11 @@ export async function remix(
localId?: string, localId?: string,
headers?: Record<string, string> headers?: Record<string, string>
): Promise<{ party: Party; editKey?: string }> { ): Promise<{ party: Party; editKey?: string }> {
const url = buildUrl(`/parties/${encodeURIComponent(shortcode)}/remix`) const party = await partyAdapter.remix(shortcode, headers)
const payload = localId ? { local_id: localId } : {}
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...headers
},
body: JSON.stringify(payload),
credentials: 'include'
})
if (!res.ok) {
const error = await parseError(res)
throw error
}
const json = await res.json()
const party = parseParty(json.party)
return { return {
party, party,
editKey: json.edit_key editKey: undefined // Edit key handling may need adjustment
} }
} }
@ -151,19 +64,7 @@ export async function deleteParty(
id: string, id: string,
headers?: Record<string, string> headers?: Record<string, string>
): Promise<void> { ): Promise<void> {
const url = buildUrl(`/parties/${encodeURIComponent(id)}`) return partyAdapter.delete(id, headers)
const res = await fetch(url, {
method: 'DELETE',
headers: {
...headers
},
credentials: 'include'
})
if (!res.ok) {
const error = await parseError(res)
throw error
}
} }
/** /**
@ -183,69 +84,23 @@ export async function list(
totalPages: number totalPages: number
perPage: number perPage: number
}> { }> {
const searchParams = new URLSearchParams() // Map parameters to adapter format
if (params?.page) searchParams.set('page', params.page.toString()) const adapterParams = {
if (params?.per_page) searchParams.set('per_page', params.per_page.toString()) page: params?.page,
if (params?.raid_id) searchParams.set('raid_id', params.raid_id) per: params?.per_page,
if (params?.element) searchParams.set('element', params.element.toString()) raidId: params?.raid_id,
element: params?.element
const url = buildUrl('/parties', searchParams)
console.log('[parties.list] Requesting URL:', url)
console.log('[parties.list] With params:', params)
// Use fetch directly to get the Response object for better error handling
const res = await fetch(url, {
credentials: 'include',
headers: { 'Content-Type': 'application/json' }
})
console.log('[parties.list] Response status:', res.status, res.statusText)
if (!res.ok) {
const error = await parseError(res)
console.error('[parties.list] API error:', {
url,
status: res.status,
statusText: res.statusText,
message: error.message,
details: error.details
})
throw error
} }
let json: any const response = await partyAdapter.list(adapterParams)
try {
json = await res.json() // Map adapter response to expected format
console.log('[parties.list] Raw response:', JSON.stringify(json, null, 2).substring(0, 500)) return {
} catch (e) { items: response.results,
console.error('[parties.list] Failed to parse JSON response:', e) total: response.total,
throw new Error(`Failed to parse JSON response from ${url}: ${e}`) totalPages: response.totalPages,
perPage: response.per || 20
} }
const result = PaginatedPartiesSchema.safeParse(json)
if (result.success) {
return {
items: result.data.results.map(parseParty),
total: result.data.meta?.count || 0,
totalPages: result.data.meta?.total_pages || 1,
perPage: result.data.meta?.per_page || 20
}
}
// Fallback for non-paginated response
const fallback = PartiesResponseSchema.safeParse(json)
if (fallback.success) {
return {
items: fallback.data.parties.map(parseParty),
total: fallback.data.total || fallback.data.parties.length,
totalPages: 1,
perPage: fallback.data.parties.length
}
}
const errorMsg = `Invalid response format from API at ${url}. Response: ${JSON.stringify(json, null, 2).substring(0, 500)}`
console.error('[parties.list] Parse error:', errorMsg)
throw new Error(errorMsg)
} }
export async function getUserParties( export async function getUserParties(
@ -265,81 +120,55 @@ export async function getUserParties(
perPage?: number perPage?: number
} }
}> { }> {
const params = new URLSearchParams() // Map parameters to adapter format
if (filters?.raid) params.set('raid', filters.raid) const adapterParams = {
if (filters?.element !== undefined) params.set('element', filters.element.toString()) username,
if (filters?.recency !== undefined) params.set('recency', filters.recency.toString()) page: filters?.page,
if (filters?.page !== undefined) params.set('page', filters.page.toString()) per: 20, // Default page size
visibility: undefined, // Not specified in original
const queryString = params.toString() raidId: filters?.raid,
const url = buildUrl(`/users/${encodeURIComponent(username)}/parties${queryString ? `?${queryString}` : ''}`) element: filters?.element,
recency: filters?.recency
const res = await fetch(url, { credentials: 'include' })
if (!res.ok) {
const error = await parseError(res)
throw error
} }
const json = await res.json() const response = await partyAdapter.listUserParties(adapterParams)
const parsed = PaginatedPartiesSchema.safeParse(json)
if (!parsed.success) {
// Fallback for different response formats
const fallback = PartiesResponseSchema.safeParse(json)
if (fallback.success) {
return {
parties: fallback.data.parties.map(parseParty)
}
}
throw new Error('Invalid response format')
}
// Map adapter response to expected format
return { return {
parties: parsed.data.results.map(parseParty), parties: response.results,
meta: parsed.data.meta meta: {
? { count: response.total,
count: parsed.data.meta.count, totalPages: response.totalPages,
totalPages: parsed.data.meta.total_pages, perPage: response.per || 20
perPage: parsed.data.meta.per_page }
}
: undefined
} }
} }
// Grid operations // Grid operations - These should eventually move to GridAdapter
export async function updateWeaponGrid( export async function updateWeaponGrid(
fetch: FetchLike, fetch: FetchLike,
partyId: string, partyId: string,
payload: any, payload: any,
headers?: Record<string, string> headers?: Record<string, string>
): Promise<Party> { ): Promise<Party> {
const url = buildUrl(`/parties/${encodeURIComponent(partyId)}/grid_weapons`) // For now, use gridUpdate with a single operation
const res = await fetch(url, { // This is a temporary implementation until GridAdapter is fully integrated
method: 'POST', const operation = {
headers: { type: 'add' as const,
'Content-Type': 'application/json', entity: 'weapon' as const,
...headers ...payload
},
body: JSON.stringify(camelToSnake(payload)),
credentials: 'include'
})
if (!res.ok) {
const error = await parseError(res)
throw error
} }
const json = await res.json() const response = await partyAdapter.gridUpdate(partyId, [operation])
// Check for conflicts // Check for conflicts
if (json.conflicts) { if ('conflicts' in response && response.conflicts) {
const error = new Error('Weapon conflict') as any const error = new Error('Weapon conflict') as any
error.conflicts = json error.conflicts = response
throw error throw error
} }
return parseParty(json.party || json) return response.party
} }
export async function updateSummonGrid( export async function updateSummonGrid(
@ -348,24 +177,15 @@ export async function updateSummonGrid(
payload: any, payload: any,
headers?: Record<string, string> headers?: Record<string, string>
): Promise<Party> { ): Promise<Party> {
const url = buildUrl(`/parties/${encodeURIComponent(partyId)}/grid_summons`) // For now, use gridUpdate with a single operation
const res = await fetch(url, { const operation = {
method: 'POST', type: 'add' as const,
headers: { entity: 'summon' as const,
'Content-Type': 'application/json', ...payload
...headers
},
body: JSON.stringify(camelToSnake(payload)),
credentials: 'include'
})
if (!res.ok) {
const error = await parseError(res)
throw error
} }
const json = await res.json() const response = await partyAdapter.gridUpdate(partyId, [operation])
return parseParty(json.party || json) return response.party
} }
export async function updateCharacterGrid( export async function updateCharacterGrid(
@ -374,81 +194,21 @@ export async function updateCharacterGrid(
payload: any, payload: any,
headers?: Record<string, string> headers?: Record<string, string>
): Promise<Party> { ): Promise<Party> {
const url = buildUrl(`/parties/${encodeURIComponent(partyId)}/grid_characters`) // For now, use gridUpdate with a single operation
const res = await fetch(url, { const operation = {
method: 'POST', type: 'add' as const,
headers: { entity: 'character' as const,
'Content-Type': 'application/json', ...payload
...headers
},
body: JSON.stringify(camelToSnake(payload)),
credentials: 'include'
})
if (!res.ok) {
const error = await parseError(res)
throw error
} }
const json = await res.json() const response = await partyAdapter.gridUpdate(partyId, [operation])
// Check for conflicts // Check for conflicts
if (json.conflicts) { if ('conflicts' in response && response.conflicts) {
const error = new Error('Character conflict') as any const error = new Error('Character conflict') as any
error.conflicts = json error.conflicts = response
throw error throw error
} }
return parseParty(json.party || json) return response.party
}
// Error parsing
async function parseError(res: Response): Promise<Error & { status: number; details?: any[]; url?: string }> {
let message = 'Request failed'
let details: any[] = []
const url = res.url
console.error('[parseError] Parsing error response:', {
url: res.url,
status: res.status,
statusText: res.statusText,
headers: res.headers ? Object.fromEntries(res.headers.entries()) : 'No headers'
})
try {
const errorData = await res.json()
console.error('[parseError] Error response body:', errorData)
if (errorData.error) {
message = errorData.error
} else if (errorData.errors) {
if (Array.isArray(errorData.errors)) {
message = errorData.errors.join(', ')
details = errorData.errors
} else if (typeof errorData.errors === 'object') {
const messages: string[] = []
for (const [field, errors] of Object.entries(errorData.errors)) {
if (Array.isArray(errors)) {
messages.push(`${field}: ${errors.join(', ')}`)
}
}
message = messages.join('; ')
details = Object.entries(errorData.errors)
}
}
} catch (e) {
// If JSON parsing fails, use status text
console.error('[parseError] Failed to parse error JSON:', e)
message = `${res.status} ${res.statusText || 'Request failed'} at ${url}`
}
const error = new Error(message) as Error & { status: number; details?: any[]; url?: string }
error.status = res.status
if (details.length > 0) {
error.details = details
}
if (url) {
error.url = url
}
return error
} }

View file

@ -1,6 +1,5 @@
import type { Party, GridWeapon, GridCharacter } from '$lib/types/api/party' import type { Party, GridWeapon, GridCharacter } from '$lib/types/api/party'
import type { FetchLike } from '$lib/api/core' import { gridAdapter } from '$lib/api/adapters'
import * as partiesApi from '$lib/api/resources/parties'
export interface ConflictData { export interface ConflictData {
conflicts: string[] conflicts: string[]
@ -19,8 +18,8 @@ export interface ConflictResolution {
* Conflict service - handles conflict resolution for weapons and characters * Conflict service - handles conflict resolution for weapons and characters
*/ */
export class ConflictService { export class ConflictService {
constructor(private fetch: FetchLike) {} constructor() {}
/** /**
* Resolve a conflict by choosing which items to keep * Resolve a conflict by choosing which items to keep
*/ */
@ -30,15 +29,13 @@ export class ConflictService {
resolution: ConflictResolution, resolution: ConflictResolution,
editKey?: string editKey?: string
): Promise<Party> { ): Promise<Party> {
const headers = this.buildHeaders(editKey)
if (conflictType === 'weapon') { if (conflictType === 'weapon') {
return this.resolveWeaponConflict(partyId, resolution, headers) return this.resolveWeaponConflict(partyId, resolution)
} else { } else {
return this.resolveCharacterConflict(partyId, resolution, headers) return this.resolveCharacterConflict(partyId, resolution)
} }
} }
/** /**
* Check if adding an item would cause conflicts * Check if adding an item would cause conflicts
*/ */
@ -53,7 +50,7 @@ export class ConflictService {
return this.checkCharacterConflicts(party, itemId) return this.checkCharacterConflicts(party, itemId)
} }
} }
/** /**
* Format conflict message for display * Format conflict message for display
*/ */
@ -64,72 +61,59 @@ export class ConflictService {
): string { ): string {
const itemTypeLabel = conflictType === 'weapon' ? 'weapon' : 'character' const itemTypeLabel = conflictType === 'weapon' ? 'weapon' : 'character'
const conflictNames = conflictingItems.map(i => i.name).join(', ') const conflictNames = conflictingItems.map(i => i.name).join(', ')
if (conflictingItems.length === 1) { if (conflictingItems.length === 1) {
return `Adding ${incomingItem.name} would conflict with ${conflictNames}. Which ${itemTypeLabel} would you like to keep?` return `Adding ${incomingItem.name} would conflict with ${conflictNames}. Which ${itemTypeLabel} would you like to keep?`
} }
return `Adding ${incomingItem.name} would conflict with: ${conflictNames}. Which ${itemTypeLabel}s would you like to keep?` return `Adding ${incomingItem.name} would conflict with: ${conflictNames}. Which ${itemTypeLabel}s would you like to keep?`
} }
// Private methods // Private methods
private async resolveWeaponConflict( private async resolveWeaponConflict(
partyId: string, partyId: string,
resolution: ConflictResolution, resolution: ConflictResolution
headers: Record<string, string>
): Promise<Party> { ): Promise<Party> {
// Build payload to remove conflicting weapons and add the new one // Use GridAdapter's conflict resolution
const payload = { const result = await gridAdapter.resolveWeaponConflict({
weapons: [ partyId,
// Remove conflicting weapons incomingId: resolution.addId,
...resolution.removeIds.map(id => ({ position: resolution.position,
id, conflictingIds: resolution.removeIds
_destroy: true })
})),
// Add the new weapon // The adapter returns the weapon, but we need to return the full party
{ // This is a limitation - we should fetch the updated party
weaponId: resolution.addId, // For now, return a partial party object
position: resolution.position, return {
uncapLevel: 0, weapons: [result]
transcendenceLevel: 0 } as Party
}
]
}
return partiesApi.updateWeaponGrid(this.fetch, partyId, payload, headers)
} }
private async resolveCharacterConflict( private async resolveCharacterConflict(
partyId: string, partyId: string,
resolution: ConflictResolution, resolution: ConflictResolution
headers: Record<string, string>
): Promise<Party> { ): Promise<Party> {
// Build payload to remove conflicting characters and add the new one // Use GridAdapter's conflict resolution
const payload = { const result = await gridAdapter.resolveCharacterConflict({
characters: [ partyId,
// Remove conflicting characters incomingId: resolution.addId,
...resolution.removeIds.map(id => ({ position: resolution.position,
id, conflictingIds: resolution.removeIds
_destroy: true })
})),
// Add the new character // The adapter returns the character, but we need to return the full party
{ // This is a limitation - we should fetch the updated party
characterId: resolution.addId, return {
position: resolution.position, characters: [result]
uncapLevel: 0, } as Party
transcendenceLevel: 0
}
]
}
return partiesApi.updateCharacterGrid(this.fetch, partyId, payload, headers)
} }
private checkWeaponConflicts(party: Party, weaponId: string): ConflictData | null { private checkWeaponConflicts(party: Party, weaponId: string): ConflictData | null {
// Check for duplicate weapons (simplified - actual logic would be more complex) // Check for duplicate weapons (simplified - actual logic would be more complex)
const existingWeapon = party.weapons.find(w => w.weapon.id === weaponId) const existingWeapon = party.weapons.find(w => w.weapon.id === weaponId)
if (existingWeapon) { if (existingWeapon) {
return { return {
conflicts: [existingWeapon.id], conflicts: [existingWeapon.id],
@ -137,16 +121,16 @@ export class ConflictService {
position: existingWeapon.position position: existingWeapon.position
} }
} }
// Could check for other conflict types here (e.g., same series weapons) // Could check for other conflict types here (e.g., same series weapons)
return null return null
} }
private checkCharacterConflicts(party: Party, characterId: string): ConflictData | null { private checkCharacterConflicts(party: Party, characterId: string): ConflictData | null {
// Check for duplicate characters // Check for duplicate characters
const existingCharacter = party.characters.find(c => c.character.id === characterId) const existingCharacter = party.characters.find(c => c.character.id === characterId)
if (existingCharacter) { if (existingCharacter) {
return { return {
conflicts: [existingCharacter.id], conflicts: [existingCharacter.id],
@ -154,21 +138,13 @@ export class ConflictService {
position: existingCharacter.position position: existingCharacter.position
} }
} }
// Check for conflicts with other versions of the same character // Check for conflicts with other versions of the same character
// This would need character metadata to determine conflicts // This would need character metadata to determine conflicts
return null return null
} }
private buildHeaders(editKey?: string): Record<string, string> {
const headers: Record<string, string> = {}
if (editKey) {
headers['X-Edit-Key'] = editKey
}
return headers
}
/** /**
* Get conflict constraints for a specific type * Get conflict constraints for a specific type
*/ */
@ -183,7 +159,7 @@ export class ConflictService {
checkVariants: true // Check for same series weapons checkVariants: true // Check for same series weapons
} }
} }
return { return {
allowDuplicates: false, allowDuplicates: false,
checkVariants: true // Check for different versions of same character checkVariants: true // Check for different versions of same character

View file

@ -1,7 +1,5 @@
import type { Party, GridWeapon, GridSummon, GridCharacter } from '$lib/types/api/party' import type { Party, GridWeapon, GridSummon, GridCharacter } from '$lib/types/api/party'
import * as partiesApi from '$lib/api/resources/parties' import { gridAdapter, partyAdapter } from '$lib/api/adapters'
import * as gridApi from '$lib/api/resources/grid'
import type { FetchLike } from '$lib/api/core'
export interface GridOperation { export interface GridOperation {
type: 'add' | 'replace' | 'remove' | 'move' | 'swap' type: 'add' | 'replace' | 'remove' | 'move' | 'swap'
@ -26,30 +24,27 @@ export interface GridUpdateResult {
* Grid service - handles grid operations for weapons, summons, and characters * Grid service - handles grid operations for weapons, summons, and characters
*/ */
export class GridService { export class GridService {
constructor(private fetch: FetchLike) {} constructor() {}
// Weapon Grid Operations // Weapon Grid Operations
async addWeapon( async addWeapon(
partyId: string, partyId: string,
weaponId: string, weaponId: string,
position: number, position: number,
editKey?: string editKey?: string
): Promise<GridUpdateResult> { ): Promise<GridUpdateResult> {
const payload = {
weaponId,
position,
uncapLevel: 0,
transcendenceLevel: 0
}
try { try {
const party = await partiesApi.updateWeaponGrid( const gridWeapon = await gridAdapter.createWeapon({
this.fetch,
partyId, partyId,
payload, weaponId,
this.buildHeaders(editKey) position,
) uncapLevel: 0,
transcendenceStage: 0
})
// Fetch updated party to return
const party = await partyAdapter.getByShortcode(partyId)
return { party } return { party }
} catch (error: any) { } catch (error: any) {
if (error.type === 'conflict') { if (error.type === 'conflict') {
@ -61,26 +56,20 @@ export class GridService {
throw error throw error
} }
} }
async replaceWeapon( async replaceWeapon(
partyId: string, partyId: string,
gridWeaponId: string, gridWeaponId: string,
newWeaponId: string, newWeaponId: string,
editKey?: string editKey?: string
): Promise<GridUpdateResult> { ): Promise<GridUpdateResult> {
const payload = {
id: gridWeaponId,
weaponId: newWeaponId
}
try { try {
const party = await partiesApi.updateWeaponGrid( // First remove the old weapon
this.fetch, await gridAdapter.deleteWeapon({ id: gridWeaponId, partyId })
partyId,
payload, // Then add the new one
this.buildHeaders(editKey) const result = await this.addWeapon(partyId, newWeaponId, 0, editKey)
) return result
return { party }
} catch (error: any) { } catch (error: any) {
if (error.type === 'conflict') { if (error.type === 'conflict') {
return { return {
@ -91,25 +80,18 @@ export class GridService {
throw error throw error
} }
} }
async removeWeapon( async removeWeapon(
partyId: string, partyId: string,
gridWeaponId: string, gridWeaponId: string,
editKey?: string editKey?: string
): Promise<Party> { ): Promise<Party> {
const payload = { await gridAdapter.deleteWeapon({ id: gridWeaponId, partyId })
id: gridWeaponId,
_destroy: true // Return updated party
} return partyAdapter.getByShortcode(partyId)
return partiesApi.updateWeaponGrid(
this.fetch,
partyId,
payload,
this.buildHeaders(editKey)
)
} }
async updateWeapon( async updateWeapon(
partyId: string, partyId: string,
gridWeaponId: string, gridWeaponId: string,
@ -121,17 +103,15 @@ export class GridService {
}, },
editKey?: string editKey?: string
): Promise<Party> { ): Promise<Party> {
const payload = { await gridAdapter.updateWeapon(gridWeaponId, {
id: gridWeaponId, position: updates.position,
...updates uncapLevel: updates.uncapLevel,
} transcendenceStage: updates.transcendenceStep,
element: updates.element
})
return partiesApi.updateWeaponGrid( // Return updated party
this.fetch, return partyAdapter.getByShortcode(partyId)
partyId,
payload,
this.buildHeaders(editKey)
)
} }
async moveWeapon( async moveWeapon(
@ -140,109 +120,84 @@ export class GridService {
newPosition: number, newPosition: number,
editKey?: string editKey?: string
): Promise<Party> { ): Promise<Party> {
const payload = { await gridAdapter.updateWeaponPosition({
partyId,
id: gridWeaponId, id: gridWeaponId,
position: newPosition position: newPosition
} })
return partiesApi.updateWeaponGrid( return partyAdapter.getByShortcode(partyId)
this.fetch,
partyId,
payload,
this.buildHeaders(editKey)
)
} }
async swapWeapons( async swapWeapons(
partyId: string, partyId: string,
gridWeaponId1: string, gridWeaponId1: string,
gridWeaponId2: string, gridWeaponId2: string,
editKey?: string editKey?: string
): Promise<Party> { ): Promise<Party> {
const payload = { await gridAdapter.swapWeapons({
swap: [gridWeaponId1, gridWeaponId2]
}
return partiesApi.updateWeaponGrid(
this.fetch,
partyId, partyId,
payload, sourceId: gridWeaponId1,
this.buildHeaders(editKey) targetId: gridWeaponId2
) })
return partyAdapter.getByShortcode(partyId)
} }
async updateWeaponUncap( async updateWeaponUncap(
gridWeaponId: string, gridWeaponId: string,
uncapLevel?: number, uncapLevel?: number,
transcendenceStep?: number, transcendenceStep?: number,
editKey?: string editKey?: string
): Promise<any> { ): Promise<any> {
return gridApi.updateWeaponUncap( return gridAdapter.updateWeaponUncap({
gridWeaponId, id: gridWeaponId,
uncapLevel, partyId: 'unknown', // This is a design issue - needs partyId
transcendenceStep, uncapLevel: uncapLevel ?? 3,
this.buildHeaders(editKey) transcendenceStep
) })
} }
// Summon Grid Operations // Summon Grid Operations
async addSummon( async addSummon(
partyId: string, partyId: string,
summonId: string, summonId: string,
position: number, position: number,
editKey?: string editKey?: string
): Promise<Party> { ): Promise<Party> {
const payload = { await gridAdapter.createSummon({
partyId,
summonId, summonId,
position, position,
uncapLevel: 0, uncapLevel: 0,
transcendenceLevel: 0 transcendenceStage: 0
} })
return partiesApi.updateSummonGrid( return partyAdapter.getByShortcode(partyId)
this.fetch,
partyId,
payload,
this.buildHeaders(editKey)
)
} }
async replaceSummon( async replaceSummon(
partyId: string, partyId: string,
gridSummonId: string, gridSummonId: string,
newSummonId: string, newSummonId: string,
editKey?: string editKey?: string
): Promise<Party> { ): Promise<Party> {
const payload = { // First remove the old summon
id: gridSummonId, await gridAdapter.deleteSummon({ id: gridSummonId, partyId })
summonId: newSummonId
} // Then add the new one
return this.addSummon(partyId, newSummonId, 0, editKey)
return partiesApi.updateSummonGrid(
this.fetch,
partyId,
payload,
this.buildHeaders(editKey)
)
} }
async removeSummon( async removeSummon(
partyId: string, partyId: string,
gridSummonId: string, gridSummonId: string,
editKey?: string editKey?: string
): Promise<Party> { ): Promise<Party> {
const payload = { await gridAdapter.deleteSummon({ id: gridSummonId, partyId })
id: gridSummonId,
_destroy: true
}
return partiesApi.updateSummonGrid( return partyAdapter.getByShortcode(partyId)
this.fetch,
partyId,
payload,
this.buildHeaders(editKey)
)
} }
async updateSummon( async updateSummon(
@ -256,55 +211,48 @@ export class GridService {
}, },
editKey?: string editKey?: string
): Promise<Party> { ): Promise<Party> {
const payload = { await gridAdapter.updateSummon(gridSummonId, {
id: gridSummonId, position: updates.position,
...updates quickSummon: updates.quickSummon,
} uncapLevel: updates.uncapLevel,
transcendenceStage: updates.transcendenceStep
})
return partiesApi.updateSummonGrid( return partyAdapter.getByShortcode(partyId)
this.fetch,
partyId,
payload,
this.buildHeaders(editKey)
)
} }
async updateSummonUncap( async updateSummonUncap(
gridSummonId: string, gridSummonId: string,
uncapLevel?: number, uncapLevel?: number,
transcendenceStep?: number, transcendenceStep?: number,
editKey?: string editKey?: string
): Promise<any> { ): Promise<any> {
return gridApi.updateSummonUncap( return gridAdapter.updateSummonUncap({
gridSummonId, id: gridSummonId,
uncapLevel, partyId: 'unknown', // This is a design issue - needs partyId
transcendenceStep, uncapLevel: uncapLevel ?? 3,
this.buildHeaders(editKey) transcendenceStep
) })
} }
// Character Grid Operations // Character Grid Operations
async addCharacter( async addCharacter(
partyId: string, partyId: string,
characterId: string, characterId: string,
position: number, position: number,
editKey?: string editKey?: string
): Promise<GridUpdateResult> { ): Promise<GridUpdateResult> {
const payload = {
characterId,
position,
uncapLevel: 0,
transcendenceLevel: 0
}
try { try {
const party = await partiesApi.updateCharacterGrid( await gridAdapter.createCharacter({
this.fetch,
partyId, partyId,
payload, characterId,
this.buildHeaders(editKey) position,
) uncapLevel: 0,
transcendenceStage: 0
})
const party = await partyAdapter.getByShortcode(partyId)
return { party } return { party }
} catch (error: any) { } catch (error: any) {
if (error.type === 'conflict') { if (error.type === 'conflict') {
@ -316,26 +264,19 @@ export class GridService {
throw error throw error
} }
} }
async replaceCharacter( async replaceCharacter(
partyId: string, partyId: string,
gridCharacterId: string, gridCharacterId: string,
newCharacterId: string, newCharacterId: string,
editKey?: string editKey?: string
): Promise<GridUpdateResult> { ): Promise<GridUpdateResult> {
const payload = {
id: gridCharacterId,
characterId: newCharacterId
}
try { try {
const party = await partiesApi.updateCharacterGrid( // First remove the old character
this.fetch, await gridAdapter.deleteCharacter({ id: gridCharacterId, partyId })
partyId,
payload, // Then add the new one
this.buildHeaders(editKey) return this.addCharacter(partyId, newCharacterId, 0, editKey)
)
return { party }
} catch (error: any) { } catch (error: any) {
if (error.type === 'conflict') { if (error.type === 'conflict') {
return { return {
@ -346,23 +287,15 @@ export class GridService {
throw error throw error
} }
} }
async removeCharacter( async removeCharacter(
partyId: string, partyId: string,
gridCharacterId: string, gridCharacterId: string,
editKey?: string editKey?: string
): Promise<Party> { ): Promise<Party> {
const payload = { await gridAdapter.deleteCharacter({ id: gridCharacterId, partyId })
id: gridCharacterId,
_destroy: true
}
return partiesApi.updateCharacterGrid( return partyAdapter.getByShortcode(partyId)
this.fetch,
partyId,
payload,
this.buildHeaders(editKey)
)
} }
async updateCharacter( async updateCharacter(
@ -376,35 +309,32 @@ export class GridService {
}, },
editKey?: string editKey?: string
): Promise<Party> { ): Promise<Party> {
const payload = { await gridAdapter.updateCharacter(gridCharacterId, {
id: gridCharacterId, position: updates.position,
...updates uncapLevel: updates.uncapLevel,
} transcendenceStage: updates.transcendenceStep,
perpetualModifiers: updates.perpetuity ? {} : undefined
})
return partiesApi.updateCharacterGrid( return partyAdapter.getByShortcode(partyId)
this.fetch,
partyId,
payload,
this.buildHeaders(editKey)
)
} }
async updateCharacterUncap( async updateCharacterUncap(
gridCharacterId: string, gridCharacterId: string,
uncapLevel?: number, uncapLevel?: number,
transcendenceStep?: number, transcendenceStep?: number,
editKey?: string editKey?: string
): Promise<any> { ): Promise<any> {
return gridApi.updateCharacterUncap( return gridAdapter.updateCharacterUncap({
gridCharacterId, id: gridCharacterId,
uncapLevel, partyId: 'unknown', // This is a design issue - needs partyId
transcendenceStep, uncapLevel: uncapLevel ?? 3,
this.buildHeaders(editKey) transcendenceStep
) })
} }
// Drag and Drop Helpers // Drag and Drop Helpers
/** /**
* Normalize drag and drop intent to a grid operation * Normalize drag and drop intent to a grid operation
*/ */
@ -422,7 +352,7 @@ export class GridService {
position: targetPosition position: targetPosition
} }
} }
// If dragging from grid to grid // If dragging from grid to grid
if (draggedItem.gridId && targetItem.gridId) { if (draggedItem.gridId && targetItem.gridId) {
return { return {
@ -431,7 +361,7 @@ export class GridService {
targetPosition: targetItem.gridId targetPosition: targetItem.gridId
} }
} }
// If dragging from outside to occupied slot // If dragging from outside to occupied slot
return { return {
type: 'replace', type: 'replace',
@ -439,7 +369,7 @@ export class GridService {
targetPosition: draggedItem.id targetPosition: draggedItem.id
} }
} }
/** /**
* Apply optimistic update to local state * Apply optimistic update to local state
*/ */
@ -448,22 +378,22 @@ export class GridService {
operation: GridOperation operation: GridOperation
): T[] { ): T[] {
const updated = [...items] const updated = [...items]
switch (operation.type) { switch (operation.type) {
case 'add': case 'add':
// Add new item at position // Add new item at position
break break
case 'remove': case 'remove':
return updated.filter(item => item.id !== operation.itemId) return updated.filter(item => item.id !== operation.itemId)
case 'move': case 'move':
const item = updated.find(i => i.id === operation.itemId) const item = updated.find(i => i.id === operation.itemId)
if (item && operation.targetPosition !== undefined) { if (item && operation.targetPosition !== undefined) {
item.position = operation.targetPosition item.position = operation.targetPosition
} }
break break
case 'swap': case 'swap':
const item1 = updated.find(i => i.id === operation.itemId) const item1 = updated.find(i => i.id === operation.itemId)
const item2 = updated.find(i => i.id === operation.targetPosition) const item2 = updated.find(i => i.id === operation.targetPosition)
@ -474,12 +404,12 @@ export class GridService {
} }
break break
} }
return updated return updated
} }
// Private helpers // Private helpers
private buildHeaders(editKey?: string): Record<string, string> { private buildHeaders(editKey?: string): Record<string, string> {
const headers: Record<string, string> = {} const headers: Record<string, string> = {}
if (editKey) { if (editKey) {

View file

@ -1,6 +1,5 @@
import type { Party } from '$lib/types/api/party' import type { Party } from '$lib/types/api/party'
import * as partiesApi from '$lib/api/resources/parties' import { partyAdapter } from '$lib/api/adapters'
import type { FetchLike } from '$lib/api/core'
export interface EditabilityResult { export interface EditabilityResult {
canEdit: boolean canEdit: boolean
@ -30,13 +29,13 @@ export interface PartyUpdatePayload {
* Party service - handles business logic for party operations * Party service - handles business logic for party operations
*/ */
export class PartyService { export class PartyService {
constructor(private fetch: FetchLike) {} constructor() {}
/** /**
* Get party by shortcode * Get party by shortcode
*/ */
async getByShortcode(shortcode: string): Promise<Party> { async getByShortcode(shortcode: string): Promise<Party> {
return partiesApi.getByShortcode(this.fetch, shortcode) return partyAdapter.getByShortcode(shortcode)
} }
/** /**
@ -48,14 +47,10 @@ export class PartyService {
}> { }> {
const headers = this.buildHeaders(editKey) const headers = this.buildHeaders(editKey)
const apiPayload = this.mapToApiPayload(payload) const apiPayload = this.mapToApiPayload(payload)
const result = await partiesApi.create(this.fetch, apiPayload, headers) const party = await partyAdapter.create(apiPayload, headers)
// Store edit key if returned // Note: Edit key handling may need to be adjusted based on how the API returns it
if (result.editKey && typeof window !== 'undefined') { return { party, editKey: undefined }
localStorage.setItem(`edit_key_${result.party.shortcode}`, result.editKey)
}
return result
} }
/** /**
@ -64,7 +59,7 @@ export class PartyService {
async update(id: string, payload: PartyUpdatePayload, editKey?: string): Promise<Party> { async update(id: string, payload: PartyUpdatePayload, editKey?: string): Promise<Party> {
const headers = this.buildHeaders(editKey) const headers = this.buildHeaders(editKey)
const apiPayload = this.mapToApiPayload(payload) const apiPayload = this.mapToApiPayload(payload)
return partiesApi.update(this.fetch, id, apiPayload, headers) return partyAdapter.update({ shortcode: id, ...apiPayload }, headers)
} }
/** /**
@ -78,13 +73,13 @@ export class PartyService {
): Promise<Party> { ): Promise<Party> {
const headers = this.buildHeaders(editKey) const headers = this.buildHeaders(editKey)
const payload: any = {} const payload: any = {}
// Map position to guidebook1_id, guidebook2_id, guidebook3_id // Map position to guidebook1_id, guidebook2_id, guidebook3_id
if (position >= 0 && position <= 2) { if (position >= 0 && position <= 2) {
payload[`guidebook${position + 1}Id`] = guidebookId payload[`guidebook${position + 1}Id`] = guidebookId
} }
return partiesApi.update(this.fetch, id, payload, headers) return partyAdapter.update({ shortcode: id, ...payload }, headers)
} }
/** /**
@ -95,28 +90,24 @@ export class PartyService {
editKey?: string editKey?: string
}> { }> {
const headers = this.buildHeaders(editKey) const headers = this.buildHeaders(editKey)
const result = await partiesApi.remix(this.fetch, shortcode, localId, headers) const party = await partyAdapter.remix(shortcode, headers)
// Store edit key if returned // Note: Edit key handling may need to be adjusted
if (result.editKey && typeof window !== 'undefined') { return { party, editKey: undefined }
localStorage.setItem(`edit_key_${result.party.shortcode}`, result.editKey)
}
return result
} }
/** /**
* Favorite a party * Favorite a party
*/ */
async favorite(id: string): Promise<void> { async favorite(id: string): Promise<void> {
return partiesApi.favorite(this.fetch, id) return partyAdapter.favorite(id)
} }
/** /**
* Unfavorite a party * Unfavorite a party
*/ */
async unfavorite(id: string): Promise<void> { async unfavorite(id: string): Promise<void> {
return partiesApi.unfavorite(this.fetch, id) return partyAdapter.unfavorite(id)
} }
/** /**
@ -124,7 +115,7 @@ export class PartyService {
*/ */
async delete(id: string, editKey?: string): Promise<void> { async delete(id: string, editKey?: string): Promise<void> {
const headers = this.buildHeaders(editKey) const headers = this.buildHeaders(editKey)
return partiesApi.deleteParty(this.fetch, id, headers) return partyAdapter.delete(id, headers)
} }
/** /**