Add API client and resource modules
This commit is contained in:
parent
65411ad2ae
commit
1b6da60aa3
10 changed files with 1454 additions and 396 deletions
444
src/lib/api/client.ts
Normal file
444
src/lib/api/client.ts
Normal file
|
|
@ -0,0 +1,444 @@
|
||||||
|
/**
|
||||||
|
* Unified API Client for client-side use
|
||||||
|
* All API calls go through our SvelteKit proxy endpoints
|
||||||
|
* Automatically handles edit keys from localStorage
|
||||||
|
* Automatically transforms data between API format and clean types
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { snakeToCamel, camelToSnake } from './schemas/transforms'
|
||||||
|
|
||||||
|
export interface PartyPayload {
|
||||||
|
name?: string
|
||||||
|
description?: string | null
|
||||||
|
element?: number
|
||||||
|
visibility?: number
|
||||||
|
localId?: string
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GridItemOptions {
|
||||||
|
uncapLevel?: number
|
||||||
|
transcendenceStep?: number
|
||||||
|
element?: number
|
||||||
|
mainhand?: boolean
|
||||||
|
main?: boolean
|
||||||
|
friend?: boolean
|
||||||
|
quickSummon?: boolean
|
||||||
|
perpetuity?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms API response data to match our clean type definitions
|
||||||
|
* - Converts snake_case to camelCase
|
||||||
|
* - Renames "object" to proper entity names (weapon, character, summon)
|
||||||
|
*/
|
||||||
|
export function transformResponse<T>(data: any): T {
|
||||||
|
if (data === null || data === undefined) return data
|
||||||
|
|
||||||
|
// First convert snake_case to camelCase
|
||||||
|
const camelCased = snakeToCamel(data)
|
||||||
|
|
||||||
|
// Then rename "object" fields to proper entity names
|
||||||
|
return renameObjectFields(camelCased) as T
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms request data to match API expectations
|
||||||
|
* - Converts camelCase to snake_case
|
||||||
|
* - Renames entity names back to "object" for API
|
||||||
|
*/
|
||||||
|
export function transformRequest<T>(data: T): any {
|
||||||
|
if (data === null || data === undefined) return data
|
||||||
|
|
||||||
|
// First rename entity fields back to "object"
|
||||||
|
const withObjectFields = renameEntityFields(data)
|
||||||
|
|
||||||
|
// Then convert camelCase to snake_case
|
||||||
|
return camelToSnake(withObjectFields)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renames "object" fields to proper entity names in response data
|
||||||
|
*/
|
||||||
|
function renameObjectFields(obj: any): any {
|
||||||
|
if (obj === null || obj === undefined) return obj
|
||||||
|
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
return obj.map(renameObjectFields)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj === 'object') {
|
||||||
|
const result: any = {}
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
|
// Handle weapons array
|
||||||
|
if (key === 'weapons' && Array.isArray(value)) {
|
||||||
|
result.weapons = value.map((item: any) => {
|
||||||
|
if (item && typeof item === 'object' && 'object' in item) {
|
||||||
|
const { object, ...rest } = item
|
||||||
|
return { ...rest, weapon: renameObjectFields(object) }
|
||||||
|
}
|
||||||
|
return renameObjectFields(item)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Handle characters array
|
||||||
|
else if (key === 'characters' && Array.isArray(value)) {
|
||||||
|
result.characters = value.map((item: any) => {
|
||||||
|
if (item && typeof item === 'object' && 'object' in item) {
|
||||||
|
const { object, ...rest } = item
|
||||||
|
return { ...rest, character: renameObjectFields(object) }
|
||||||
|
}
|
||||||
|
return renameObjectFields(item)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Handle summons array
|
||||||
|
else if (key === 'summons' && Array.isArray(value)) {
|
||||||
|
result.summons = value.map((item: any) => {
|
||||||
|
if (item && typeof item === 'object' && 'object' in item) {
|
||||||
|
const { object, ...rest } = item
|
||||||
|
return { ...rest, summon: renameObjectFields(object) }
|
||||||
|
}
|
||||||
|
return renameObjectFields(item)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Recursively process other fields
|
||||||
|
else {
|
||||||
|
result[key] = renameObjectFields(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renames entity fields back to "object" for API requests
|
||||||
|
*/
|
||||||
|
function renameEntityFields(obj: any): any {
|
||||||
|
if (obj === null || obj === undefined) return obj
|
||||||
|
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
return obj.map(renameEntityFields)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj === 'object') {
|
||||||
|
const result: any = {}
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
|
// Handle weapons array
|
||||||
|
if (key === 'weapons' && Array.isArray(value)) {
|
||||||
|
result.weapons = value.map((item: any) => {
|
||||||
|
if (item && typeof item === 'object' && 'weapon' in item) {
|
||||||
|
const { weapon, ...rest } = item
|
||||||
|
return { ...rest, object: renameEntityFields(weapon) }
|
||||||
|
}
|
||||||
|
return renameEntityFields(item)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Handle characters array
|
||||||
|
else if (key === 'characters' && Array.isArray(value)) {
|
||||||
|
result.characters = value.map((item: any) => {
|
||||||
|
if (item && typeof item === 'object' && 'character' in item) {
|
||||||
|
const { character, ...rest } = item
|
||||||
|
return { ...rest, object: renameEntityFields(character) }
|
||||||
|
}
|
||||||
|
return renameEntityFields(item)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Handle summons array
|
||||||
|
else if (key === 'summons' && Array.isArray(value)) {
|
||||||
|
result.summons = value.map((item: any) => {
|
||||||
|
if (item && typeof item === 'object' && 'summon' in item) {
|
||||||
|
const { summon, ...rest } = item
|
||||||
|
return { ...rest, object: renameEntityFields(summon) }
|
||||||
|
}
|
||||||
|
return renameEntityFields(item)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Recursively process other fields
|
||||||
|
else {
|
||||||
|
result[key] = renameEntityFields(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
export class APIClient {
|
||||||
|
/**
|
||||||
|
* Get edit key for a party from localStorage
|
||||||
|
*/
|
||||||
|
private getEditKey(partyIdOrShortcode: string): string | null {
|
||||||
|
if (typeof window === 'undefined') return null
|
||||||
|
|
||||||
|
// Try both formats - with party ID and shortcode
|
||||||
|
const keyById = localStorage.getItem(`edit_key_${partyIdOrShortcode}`)
|
||||||
|
if (keyById) return keyById
|
||||||
|
|
||||||
|
// Also check if it's stored by shortcode
|
||||||
|
return localStorage.getItem(`edit_key_${partyIdOrShortcode}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store edit key for a party in localStorage
|
||||||
|
*/
|
||||||
|
storeEditKey(partyShortcode: string, editKey: string): void {
|
||||||
|
if (typeof window !== 'undefined' && editKey) {
|
||||||
|
localStorage.setItem(`edit_key_${partyShortcode}`, editKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new party
|
||||||
|
*/
|
||||||
|
async createParty(payload: PartyPayload): Promise<{ party: any; editKey?: string }> {
|
||||||
|
const response = await fetch('/api/parties', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json()
|
||||||
|
throw new Error(error.error || `Failed to create party: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
// Store edit key if present
|
||||||
|
if (data.edit_key && data.party?.shortcode) {
|
||||||
|
this.storeEditKey(data.party.shortcode, data.edit_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
party: data.party,
|
||||||
|
editKey: data.edit_key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a party
|
||||||
|
*/
|
||||||
|
async updateParty(partyId: string, payload: Partial<PartyPayload>): Promise<any> {
|
||||||
|
const editKey = this.getEditKey(partyId)
|
||||||
|
|
||||||
|
const response = await fetch(`/api/parties/${partyId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json()
|
||||||
|
throw new Error(error.error || `Failed to update party: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a party
|
||||||
|
*/
|
||||||
|
async deleteParty(partyId: string): Promise<void> {
|
||||||
|
const editKey = this.getEditKey(partyId)
|
||||||
|
|
||||||
|
const response = await fetch(`/api/parties/${partyId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json()
|
||||||
|
throw new Error(error.error || `Failed to delete party: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a weapon to a party
|
||||||
|
*/
|
||||||
|
async addWeapon(
|
||||||
|
partyId: string,
|
||||||
|
weaponId: string,
|
||||||
|
position: number,
|
||||||
|
options?: GridItemOptions
|
||||||
|
): Promise<any> {
|
||||||
|
const editKey = this.getEditKey(partyId)
|
||||||
|
|
||||||
|
const response = await fetch(`/api/parties/${partyId}/weapons`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
weaponId,
|
||||||
|
position,
|
||||||
|
...options
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json()
|
||||||
|
throw new Error(error.error || `Failed to add weapon: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a weapon from a party
|
||||||
|
*/
|
||||||
|
async removeWeapon(partyId: string, gridWeaponId: string): Promise<void> {
|
||||||
|
const editKey = this.getEditKey(partyId)
|
||||||
|
|
||||||
|
const response = await fetch(`/api/parties/${partyId}/weapons`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ gridWeaponId })
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json()
|
||||||
|
throw new Error(error.error || `Failed to remove weapon: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a summon to a party
|
||||||
|
*/
|
||||||
|
async addSummon(
|
||||||
|
partyId: string,
|
||||||
|
summonId: string,
|
||||||
|
position: number,
|
||||||
|
options?: GridItemOptions
|
||||||
|
): Promise<any> {
|
||||||
|
const editKey = this.getEditKey(partyId)
|
||||||
|
|
||||||
|
const response = await fetch(`/api/parties/${partyId}/summons`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
summonId,
|
||||||
|
position,
|
||||||
|
...options
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json()
|
||||||
|
throw new Error(error.error || `Failed to add summon: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a summon from a party
|
||||||
|
*/
|
||||||
|
async removeSummon(partyId: string, gridSummonId: string): Promise<void> {
|
||||||
|
const editKey = this.getEditKey(partyId)
|
||||||
|
|
||||||
|
const response = await fetch(`/api/parties/${partyId}/summons`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ gridSummonId })
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json()
|
||||||
|
throw new Error(error.error || `Failed to remove summon: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a character to a party
|
||||||
|
*/
|
||||||
|
async addCharacter(
|
||||||
|
partyId: string,
|
||||||
|
characterId: string,
|
||||||
|
position: number,
|
||||||
|
options?: GridItemOptions
|
||||||
|
): Promise<any> {
|
||||||
|
const editKey = this.getEditKey(partyId)
|
||||||
|
|
||||||
|
const response = await fetch(`/api/parties/${partyId}/characters`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
characterId,
|
||||||
|
position,
|
||||||
|
...options
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json()
|
||||||
|
throw new Error(error.error || `Failed to add character: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a character from a party
|
||||||
|
*/
|
||||||
|
async removeCharacter(partyId: string, gridCharacterId: string): Promise<void> {
|
||||||
|
const editKey = this.getEditKey(partyId)
|
||||||
|
|
||||||
|
const response = await fetch(`/api/parties/${partyId}/characters`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ gridCharacterId })
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json()
|
||||||
|
throw new Error(error.error || `Failed to remove character: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get local ID for anonymous users
|
||||||
|
*/
|
||||||
|
getLocalId(): string {
|
||||||
|
if (typeof window === 'undefined') return ''
|
||||||
|
|
||||||
|
let localId = localStorage.getItem('local_id')
|
||||||
|
if (!localId) {
|
||||||
|
localId = crypto.randomUUID()
|
||||||
|
localStorage.setItem('local_id', localId)
|
||||||
|
}
|
||||||
|
return localId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export a singleton instance for convenience
|
||||||
|
export const apiClient = new APIClient()
|
||||||
197
src/lib/api/resources/grid.ts
Normal file
197
src/lib/api/resources/grid.ts
Normal file
|
|
@ -0,0 +1,197 @@
|
||||||
|
import { buildUrl, type FetchLike } from '$lib/api/core'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grid API resource functions for managing party items
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 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<string, string>
|
||||||
|
): Promise<any> {
|
||||||
|
const body = {
|
||||||
|
weapon: {
|
||||||
|
party_id: partyId,
|
||||||
|
weapon_id: weaponId,
|
||||||
|
position,
|
||||||
|
mainhand: position === -1 || options?.mainhand,
|
||||||
|
uncap_level: options?.uncapLevel ?? 3,
|
||||||
|
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 removeWeapon(
|
||||||
|
fetch: FetchLike,
|
||||||
|
partyId: string,
|
||||||
|
gridWeaponId: string,
|
||||||
|
headers?: Record<string, string>
|
||||||
|
): Promise<void> {
|
||||||
|
const res = await fetch(buildUrl('/weapons'), {
|
||||||
|
method: 'DELETE',
|
||||||
|
credentials: 'include',
|
||||||
|
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
|
||||||
|
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<string, string>
|
||||||
|
): Promise<any> {
|
||||||
|
const body = {
|
||||||
|
summon: {
|
||||||
|
party_id: partyId,
|
||||||
|
summon_id: summonId,
|
||||||
|
position,
|
||||||
|
main: position === -1 || options?.main,
|
||||||
|
friend: position === 6 || options?.friend,
|
||||||
|
quick_summon: options?.quickSummon ?? false,
|
||||||
|
uncap_level: options?.uncapLevel ?? 3,
|
||||||
|
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 removeSummon(
|
||||||
|
fetch: FetchLike,
|
||||||
|
partyId: string,
|
||||||
|
gridSummonId: string,
|
||||||
|
headers?: Record<string, string>
|
||||||
|
): Promise<void> {
|
||||||
|
const res = await fetch(buildUrl('/summons'), {
|
||||||
|
method: 'DELETE',
|
||||||
|
credentials: 'include',
|
||||||
|
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
|
||||||
|
export async function addCharacter(
|
||||||
|
fetch: FetchLike,
|
||||||
|
partyId: string,
|
||||||
|
characterId: string, // Granblue ID
|
||||||
|
position: number,
|
||||||
|
options?: {
|
||||||
|
uncapLevel?: number
|
||||||
|
transcendenceStep?: number
|
||||||
|
perpetuity?: boolean
|
||||||
|
},
|
||||||
|
headers?: Record<string, string>
|
||||||
|
): Promise<any> {
|
||||||
|
const body = {
|
||||||
|
character: {
|
||||||
|
party_id: partyId,
|
||||||
|
character_id: characterId,
|
||||||
|
position,
|
||||||
|
uncap_level: options?.uncapLevel ?? 3,
|
||||||
|
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 removeCharacter(
|
||||||
|
fetch: FetchLike,
|
||||||
|
partyId: string,
|
||||||
|
gridCharacterId: string,
|
||||||
|
headers?: Record<string, string>
|
||||||
|
): Promise<void> {
|
||||||
|
const res = await fetch(buildUrl('/characters'), {
|
||||||
|
method: 'DELETE',
|
||||||
|
credentials: 'include',
|
||||||
|
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}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { buildUrl, get, post, put, del, type FetchLike } from '$lib/api/core'
|
import { buildUrl, get, post, put, del, type FetchLike } from '$lib/api/core'
|
||||||
import { parseParty, type Party } from '$lib/api/schemas/party'
|
import { parseParty } from '$lib/api/schemas/party'
|
||||||
|
import type { Party } from '$lib/types/api/party'
|
||||||
import { camelToSnake } from '$lib/api/schemas/transforms'
|
import { camelToSnake } from '$lib/api/schemas/transforms'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
|
@ -52,6 +53,7 @@ export async function getByShortcode(fetch: FetchLike, shortcode: string): Promi
|
||||||
|
|
||||||
// Validate and transform snake_case to camelCase
|
// Validate and transform snake_case to camelCase
|
||||||
const parsed = parseParty(partyData)
|
const parsed = parseParty(partyData)
|
||||||
|
|
||||||
return parsed
|
return parsed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,16 +61,16 @@ export async function create(
|
||||||
fetch: FetchLike,
|
fetch: FetchLike,
|
||||||
payload: Partial<Party>,
|
payload: Partial<Party>,
|
||||||
headers?: Record<string, string>
|
headers?: Record<string, string>
|
||||||
): Promise<Party> {
|
): Promise<{ party: Party; editKey?: string }> {
|
||||||
const body = camelToSnake(payload)
|
const url = buildUrl('/parties')
|
||||||
const res = await fetch(buildUrl('/parties'), {
|
const res = await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...headers
|
...headers
|
||||||
},
|
},
|
||||||
body: JSON.stringify(body)
|
body: JSON.stringify(camelToSnake(payload)),
|
||||||
|
credentials: 'include'
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
|
@ -77,58 +79,11 @@ export async function create(
|
||||||
}
|
}
|
||||||
|
|
||||||
const json = await res.json()
|
const json = await res.json()
|
||||||
const parsed = PartyResponseSchema.parse(json)
|
const party = parseParty(json.party)
|
||||||
return parseParty(parsed.party)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function list(
|
|
||||||
fetch: FetchLike,
|
|
||||||
params?: { page?: number }
|
|
||||||
): Promise<{ items: Party[]; total?: number; totalPages?: number; perPage?: number; page: number }> {
|
|
||||||
const page = params?.page && params.page > 0 ? params.page : 1
|
|
||||||
const url = buildUrl('/parties', { page })
|
|
||||||
const res = await fetch(url, { credentials: 'include' })
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
const error = await parseError(res)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
const json = await res.json()
|
|
||||||
// Controller returns { results: [...], meta: { count, total_pages, per_page } }
|
|
||||||
const parsed = PaginatedPartiesSchema.parse(json)
|
|
||||||
const items = (parsed.results || []).map(parseParty)
|
|
||||||
return {
|
return {
|
||||||
items,
|
party,
|
||||||
total: parsed.meta?.count,
|
editKey: json.edit_key
|
||||||
totalPages: parsed.meta?.total_pages,
|
|
||||||
perPage: parsed.meta?.per_page,
|
|
||||||
page
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function favorites(
|
|
||||||
fetch: FetchLike,
|
|
||||||
params?: { page?: number }
|
|
||||||
): Promise<{ items: Party[]; total?: number; totalPages?: number; perPage?: number; page: number }> {
|
|
||||||
const page = params?.page && params.page > 0 ? params.page : 1
|
|
||||||
const url = buildUrl('/parties/favorites', { page })
|
|
||||||
const res = await fetch(url, { credentials: 'include' })
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
const error = await parseError(res)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
const json = await res.json()
|
|
||||||
const parsed = PaginatedPartiesSchema.parse(json)
|
|
||||||
const items = (parsed.results || []).map(parseParty)
|
|
||||||
return {
|
|
||||||
items,
|
|
||||||
total: parsed.meta?.count,
|
|
||||||
totalPages: parsed.meta?.total_pages,
|
|
||||||
perPage: parsed.meta?.per_page,
|
|
||||||
page
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -138,15 +93,15 @@ export async function update(
|
||||||
payload: Partial<Party>,
|
payload: Partial<Party>,
|
||||||
headers?: Record<string, string>
|
headers?: Record<string, string>
|
||||||
): Promise<Party> {
|
): Promise<Party> {
|
||||||
const body = camelToSnake(payload)
|
const url = buildUrl(`/parties/${encodeURIComponent(id)}`)
|
||||||
const res = await fetch(buildUrl(`/parties/${id}`), {
|
const res = await fetch(url, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
credentials: 'include',
|
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...headers
|
...headers
|
||||||
},
|
},
|
||||||
body: JSON.stringify(body)
|
body: JSON.stringify(camelToSnake(payload)),
|
||||||
|
credentials: 'include'
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
|
@ -155,18 +110,7 @@ export async function update(
|
||||||
}
|
}
|
||||||
|
|
||||||
const json = await res.json()
|
const json = await res.json()
|
||||||
|
return parseParty(json.party || json)
|
||||||
// Handle conflict response
|
|
||||||
if ('conflicts' in json) {
|
|
||||||
const conflict = ConflictResponseSchema.parse(json)
|
|
||||||
throw {
|
|
||||||
type: 'conflict',
|
|
||||||
...conflict
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsed = PartyResponseSchema.parse(json)
|
|
||||||
return parseParty(parsed.party)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function remix(
|
export async function remix(
|
||||||
|
|
@ -175,15 +119,17 @@ 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 body = localId ? { local_id: localId } : {}
|
const url = buildUrl(`/parties/${encodeURIComponent(shortcode)}/remix`)
|
||||||
const res = await fetch(buildUrl(`/parties/${encodeURIComponent(shortcode)}/remix`), {
|
const payload = localId ? { local_id: localId } : {}
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...headers
|
...headers
|
||||||
},
|
},
|
||||||
body: JSON.stringify(body)
|
body: JSON.stringify(payload),
|
||||||
|
credentials: 'include'
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
|
@ -192,54 +138,11 @@ export async function remix(
|
||||||
}
|
}
|
||||||
|
|
||||||
const json = await res.json()
|
const json = await res.json()
|
||||||
const parsed = PartyResponseSchema.parse(json)
|
const party = parseParty(json.party)
|
||||||
|
|
||||||
// Check for edit_key in response
|
|
||||||
const editKey = (json as any).edit_key
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
party: parseParty(parsed.party),
|
party,
|
||||||
editKey
|
editKey: json.edit_key
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function favorite(
|
|
||||||
fetch: FetchLike,
|
|
||||||
id: string,
|
|
||||||
headers?: Record<string, string>
|
|
||||||
): Promise<void> {
|
|
||||||
const res = await fetch(buildUrl(`/parties/${id}/favorite`), {
|
|
||||||
method: 'POST',
|
|
||||||
credentials: 'include',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...headers
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
const error = await parseError(res)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function unfavorite(
|
|
||||||
fetch: FetchLike,
|
|
||||||
id: string,
|
|
||||||
headers?: Record<string, string>
|
|
||||||
): Promise<void> {
|
|
||||||
const res = await fetch(buildUrl(`/parties/${id}/unfavorite`), {
|
|
||||||
method: 'DELETE',
|
|
||||||
credentials: 'include',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...headers
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
const error = await parseError(res)
|
|
||||||
throw error
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -248,13 +151,13 @@ export async function deleteParty(
|
||||||
id: string,
|
id: string,
|
||||||
headers?: Record<string, string>
|
headers?: Record<string, string>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const res = await fetch(buildUrl(`/parties/${id}`), {
|
const url = buildUrl(`/parties/${encodeURIComponent(id)}`)
|
||||||
|
const res = await fetch(url, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
credentials: 'include',
|
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...headers
|
...headers
|
||||||
}
|
},
|
||||||
|
credentials: 'include'
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
|
@ -263,22 +166,81 @@ export async function deleteParty(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grid update functions
|
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
|
||||||
|
}
|
||||||
|
}> {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (filters?.raid) params.set('raid', filters.raid)
|
||||||
|
if (filters?.element !== undefined) params.set('element', filters.element.toString())
|
||||||
|
if (filters?.recency !== undefined) params.set('recency', filters.recency.toString())
|
||||||
|
if (filters?.page !== undefined) params.set('page', filters.page.toString())
|
||||||
|
|
||||||
|
const queryString = params.toString()
|
||||||
|
const url = buildUrl(`/users/${encodeURIComponent(username)}/parties${queryString ? `?${queryString}` : ''}`)
|
||||||
|
|
||||||
|
const res = await fetch(url, { credentials: 'include' })
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const error = await parseError(res)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = await res.json()
|
||||||
|
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')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
parties: parsed.data.results.map(parseParty),
|
||||||
|
meta: parsed.data.meta
|
||||||
|
? {
|
||||||
|
count: parsed.data.meta.count,
|
||||||
|
totalPages: parsed.data.meta.total_pages,
|
||||||
|
perPage: parsed.data.meta.per_page
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grid operations
|
||||||
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 body = camelToSnake(payload)
|
const url = buildUrl(`/parties/${encodeURIComponent(partyId)}/grid_weapons`)
|
||||||
const res = await fetch(buildUrl(`/parties/${partyId}/grid_weapons`), {
|
const res = await fetch(url, {
|
||||||
method: 'PUT',
|
method: 'POST',
|
||||||
credentials: 'include',
|
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...headers
|
...headers
|
||||||
},
|
},
|
||||||
body: JSON.stringify(body)
|
body: JSON.stringify(camelToSnake(payload)),
|
||||||
|
credentials: 'include'
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
|
@ -288,17 +250,14 @@ export async function updateWeaponGrid(
|
||||||
|
|
||||||
const json = await res.json()
|
const json = await res.json()
|
||||||
|
|
||||||
// Handle conflict response
|
// Check for conflicts
|
||||||
if ('conflicts' in json) {
|
if (json.conflicts) {
|
||||||
const conflict = ConflictResponseSchema.parse(json)
|
const error = new Error('Weapon conflict') as any
|
||||||
throw {
|
error.conflicts = json
|
||||||
type: 'conflict',
|
throw error
|
||||||
...conflict
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsed = PartyResponseSchema.parse(json)
|
return parseParty(json.party || json)
|
||||||
return parseParty(parsed.party)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateSummonGrid(
|
export async function updateSummonGrid(
|
||||||
|
|
@ -307,15 +266,15 @@ export async function updateSummonGrid(
|
||||||
payload: any,
|
payload: any,
|
||||||
headers?: Record<string, string>
|
headers?: Record<string, string>
|
||||||
): Promise<Party> {
|
): Promise<Party> {
|
||||||
const body = camelToSnake(payload)
|
const url = buildUrl(`/parties/${encodeURIComponent(partyId)}/grid_summons`)
|
||||||
const res = await fetch(buildUrl(`/parties/${partyId}/grid_summons`), {
|
const res = await fetch(url, {
|
||||||
method: 'PUT',
|
method: 'POST',
|
||||||
credentials: 'include',
|
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...headers
|
...headers
|
||||||
},
|
},
|
||||||
body: JSON.stringify(body)
|
body: JSON.stringify(camelToSnake(payload)),
|
||||||
|
credentials: 'include'
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
|
@ -324,8 +283,7 @@ export async function updateSummonGrid(
|
||||||
}
|
}
|
||||||
|
|
||||||
const json = await res.json()
|
const json = await res.json()
|
||||||
const parsed = PartyResponseSchema.parse(json)
|
return parseParty(json.party || json)
|
||||||
return parseParty(parsed.party)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateCharacterGrid(
|
export async function updateCharacterGrid(
|
||||||
|
|
@ -334,15 +292,15 @@ export async function updateCharacterGrid(
|
||||||
payload: any,
|
payload: any,
|
||||||
headers?: Record<string, string>
|
headers?: Record<string, string>
|
||||||
): Promise<Party> {
|
): Promise<Party> {
|
||||||
const body = camelToSnake(payload)
|
const url = buildUrl(`/parties/${encodeURIComponent(partyId)}/grid_characters`)
|
||||||
const res = await fetch(buildUrl(`/parties/${partyId}/grid_characters`), {
|
const res = await fetch(url, {
|
||||||
method: 'PUT',
|
method: 'POST',
|
||||||
credentials: 'include',
|
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...headers
|
...headers
|
||||||
},
|
},
|
||||||
body: JSON.stringify(body)
|
body: JSON.stringify(camelToSnake(payload)),
|
||||||
|
credentials: 'include'
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
|
@ -352,31 +310,49 @@ export async function updateCharacterGrid(
|
||||||
|
|
||||||
const json = await res.json()
|
const json = await res.json()
|
||||||
|
|
||||||
// Handle conflict response
|
// Check for conflicts
|
||||||
if ('conflicts' in json) {
|
if (json.conflicts) {
|
||||||
const conflict = ConflictResponseSchema.parse(json)
|
const error = new Error('Character conflict') as any
|
||||||
throw {
|
error.conflicts = json
|
||||||
type: 'conflict',
|
throw error
|
||||||
...conflict
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsed = PartyResponseSchema.parse(json)
|
return parseParty(json.party || json)
|
||||||
return parseParty(parsed.party)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to parse API errors
|
// Error parsing
|
||||||
async function parseError(res: Response): Promise<Error> {
|
async function parseError(res: Response): Promise<Error & { status: number; details?: any[] }> {
|
||||||
let message = res.statusText || 'Request failed'
|
let message = 'Request failed'
|
||||||
|
let details: any[] = []
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await res.clone().json()
|
const errorData = await res.json()
|
||||||
if (typeof data?.error === 'string') message = data.error
|
if (errorData.error) {
|
||||||
else if (typeof data?.message === 'string') message = data.message
|
message = errorData.error
|
||||||
else if (Array.isArray(data?.errors)) message = data.errors.join(', ')
|
} else if (errorData.errors) {
|
||||||
} catch {}
|
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 {
|
||||||
|
// If JSON parsing fails, use status text
|
||||||
|
message = res.statusText || message
|
||||||
|
}
|
||||||
|
|
||||||
const error = new Error(message) as any
|
const error = new Error(message) as Error & { status: number; details?: any[] }
|
||||||
error.status = res.status
|
error.status = res.status
|
||||||
|
if (details.length > 0) {
|
||||||
|
error.details = details
|
||||||
|
}
|
||||||
return error
|
return error
|
||||||
}
|
}
|
||||||
155
src/lib/api/resources/search.ts
Normal file
155
src/lib/api/resources/search.ts
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
import type { FetchLike, Dict } from '../core'
|
||||||
|
import { buildUrl, API_BASE } from '../core'
|
||||||
|
|
||||||
|
// Custom JSON fetch without credentials for search endpoints to avoid CORS issues
|
||||||
|
async function searchJson<T>(fetchFn: FetchLike, url: string, body?: unknown): Promise<T> {
|
||||||
|
const res = await fetchFn(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status} ${url}`)
|
||||||
|
return res.json() as Promise<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchParams {
|
||||||
|
query?: string
|
||||||
|
locale?: 'en' | 'ja'
|
||||||
|
exclude?: string[]
|
||||||
|
page?: number
|
||||||
|
filters?: {
|
||||||
|
element?: number[]
|
||||||
|
rarity?: number[]
|
||||||
|
proficiency1?: number[] // For weapons and characters
|
||||||
|
proficiency2?: number[] // For characters only
|
||||||
|
series?: number[]
|
||||||
|
extra?: boolean
|
||||||
|
subaura?: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchResult {
|
||||||
|
id: string
|
||||||
|
granblue_id: string
|
||||||
|
name: { en?: string; ja?: string }
|
||||||
|
element?: number
|
||||||
|
rarity?: number
|
||||||
|
proficiency?: number
|
||||||
|
series?: number
|
||||||
|
image_url?: string
|
||||||
|
searchable_type: 'Weapon' | 'Character' | 'Summon'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchResponse {
|
||||||
|
results: SearchResult[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
total_pages: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function searchAll(
|
||||||
|
params: SearchParams,
|
||||||
|
init?: RequestInit,
|
||||||
|
fetchFn: FetchLike = fetch
|
||||||
|
): Promise<SearchResponse> {
|
||||||
|
const body = {
|
||||||
|
query: params.query || '',
|
||||||
|
locale: params.locale || 'en',
|
||||||
|
page: params.page || 1,
|
||||||
|
exclude: params.exclude || [],
|
||||||
|
filters: params.filters || {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${API_BASE}/search/all`
|
||||||
|
return searchJson(fetchFn, url, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function searchWeapons(
|
||||||
|
params: SearchParams,
|
||||||
|
init?: RequestInit,
|
||||||
|
fetchFn: FetchLike = fetch
|
||||||
|
): Promise<SearchResponse> {
|
||||||
|
const body: any = {
|
||||||
|
locale: params.locale || 'en',
|
||||||
|
page: params.page || 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only include query if it's provided and not empty
|
||||||
|
if (params.query) {
|
||||||
|
body.query = params.query
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only include filters if they have values
|
||||||
|
const filters: any = {}
|
||||||
|
if (params.filters?.element?.length) filters.element = params.filters.element
|
||||||
|
if (params.filters?.rarity?.length) filters.rarity = params.filters.rarity
|
||||||
|
if (params.filters?.proficiency1?.length) filters.proficiency1 = params.filters.proficiency1
|
||||||
|
if (params.filters?.extra !== undefined) filters.extra = params.filters.extra
|
||||||
|
|
||||||
|
if (Object.keys(filters).length > 0) {
|
||||||
|
body.filters = filters
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${API_BASE}/search/weapons`
|
||||||
|
return searchJson(fetchFn, url, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function searchCharacters(
|
||||||
|
params: SearchParams,
|
||||||
|
init?: RequestInit,
|
||||||
|
fetchFn: FetchLike = fetch
|
||||||
|
): Promise<SearchResponse> {
|
||||||
|
const body: any = {
|
||||||
|
locale: params.locale || 'en',
|
||||||
|
page: params.page || 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only include query if it's provided and not empty
|
||||||
|
if (params.query) {
|
||||||
|
body.query = params.query
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only include filters if they have values
|
||||||
|
const filters: any = {}
|
||||||
|
if (params.filters?.element?.length) filters.element = params.filters.element
|
||||||
|
if (params.filters?.rarity?.length) filters.rarity = params.filters.rarity
|
||||||
|
if (params.filters?.proficiency1?.length) filters.proficiency1 = params.filters.proficiency1
|
||||||
|
if (params.filters?.proficiency2?.length) filters.proficiency2 = params.filters.proficiency2
|
||||||
|
|
||||||
|
if (Object.keys(filters).length > 0) {
|
||||||
|
body.filters = filters
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${API_BASE}/search/characters`
|
||||||
|
return searchJson(fetchFn, url, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function searchSummons(
|
||||||
|
params: SearchParams,
|
||||||
|
init?: RequestInit,
|
||||||
|
fetchFn: FetchLike = fetch
|
||||||
|
): Promise<SearchResponse> {
|
||||||
|
const body: any = {
|
||||||
|
locale: params.locale || 'en',
|
||||||
|
page: params.page || 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only include query if it's provided and not empty
|
||||||
|
if (params.query) {
|
||||||
|
body.query = params.query
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only include filters if they have values
|
||||||
|
const filters: any = {}
|
||||||
|
if (params.filters?.element?.length) filters.element = params.filters.element
|
||||||
|
if (params.filters?.rarity?.length) filters.rarity = params.filters.rarity
|
||||||
|
if (params.filters?.subaura !== undefined) filters.subaura = params.filters.subaura
|
||||||
|
|
||||||
|
if (Object.keys(filters).length > 0) {
|
||||||
|
body.filters = filters
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${API_BASE}/search/summons`
|
||||||
|
return searchJson(fetchFn, url, body)
|
||||||
|
}
|
||||||
|
|
@ -14,37 +14,8 @@ const MinimalCamelPartySchema = z
|
||||||
})
|
})
|
||||||
.passthrough()
|
.passthrough()
|
||||||
|
|
||||||
// Minimal TS types for UI (grid view)
|
// NOTE: These old types are deprecated - use types from $lib/types/api/party instead
|
||||||
export type LocalizedName = string | { en?: string | null; ja?: string | null }
|
// Keeping minimal exports for backward compatibility during migration
|
||||||
export interface NamedObject { name?: LocalizedName | null; [k: string]: unknown }
|
|
||||||
export interface GridWeaponItemView { position: number; mainhand?: boolean | null; object?: NamedObject | null; [k: string]: unknown }
|
|
||||||
export interface GridSummonItemView { position: number; main?: boolean | null; friend?: boolean | null; quickSummon?: boolean | null; object?: NamedObject | null; [k: string]: unknown }
|
|
||||||
export interface GridCharacterItemView { position: number; perpetuity?: boolean | null; transcendenceStep?: number | null; object?: NamedObject | null; [k: string]: unknown }
|
|
||||||
|
|
||||||
export interface PartyView {
|
|
||||||
id: string
|
|
||||||
shortcode: string
|
|
||||||
name?: string | null
|
|
||||||
description?: string | null
|
|
||||||
user?: { id?: string | null } | null
|
|
||||||
localId?: string | null
|
|
||||||
favorited?: boolean
|
|
||||||
fullAuto?: boolean
|
|
||||||
autoGuard?: boolean
|
|
||||||
autoSummon?: boolean
|
|
||||||
chargeAttack?: boolean
|
|
||||||
clearTime?: number
|
|
||||||
buttonCount?: number
|
|
||||||
chainCount?: number
|
|
||||||
turnCount?: number
|
|
||||||
visibility?: number
|
|
||||||
raid?: { name?: LocalizedName | null; group?: { difficulty?: number | null; extra?: boolean | null; guidebooks?: boolean | null; [k: string]: unknown } | null; [k: string]: unknown } | null
|
|
||||||
job?: { name?: LocalizedName | null; [k: string]: unknown } | null
|
|
||||||
weapons: GridWeaponItemView[]
|
|
||||||
summons: GridSummonItemView[]
|
|
||||||
characters: GridCharacterItemView[]
|
|
||||||
[k: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper for localized names
|
// Helper for localized names
|
||||||
const LocalizedNameSchema = z.union([
|
const LocalizedNameSchema = z.union([
|
||||||
|
|
@ -493,47 +464,13 @@ export type RaidGroup = CamelCasedKeysDeep<z.infer<typeof RaidGroupSchema>>
|
||||||
export type User = CamelCasedKeysDeep<z.infer<typeof UserSchema>>
|
export type User = CamelCasedKeysDeep<z.infer<typeof UserSchema>>
|
||||||
export type Guidebook = CamelCasedKeysDeep<z.infer<typeof GuidebookSchema>>
|
export type Guidebook = CamelCasedKeysDeep<z.infer<typeof GuidebookSchema>>
|
||||||
|
|
||||||
// Helper: parse raw API party (snake_case) and convert to camelCase
|
// Import transformation from client
|
||||||
export function parseParty(input: unknown): Party {
|
import { transformResponse } from '../client'
|
||||||
// Step 1: convert server snake_case to client camelCase
|
import type { Party as CleanParty } from '$lib/types/api/party'
|
||||||
const camel = snakeToCamel(input) as Record<string, unknown>
|
|
||||||
// Step 2: validate only minimal, core fields and pass through the rest
|
|
||||||
const parsed = MinimalCamelPartySchema.parse(camel) as any
|
|
||||||
|
|
||||||
// Step 3: minimally validate grids and coerce to arrays for UI safety
|
// Helper: parse raw API party (snake_case) and convert to clean types
|
||||||
const grids = MinimalGridsSchema.safeParse(camel)
|
export function parseParty(input: unknown): CleanParty {
|
||||||
if (grids.success) {
|
// Use the unified transformation from the API client
|
||||||
parsed.weapons = grids.data.weapons ?? []
|
// This handles both snake_case → camelCase and object → entity name mapping
|
||||||
parsed.summons = grids.data.summons ?? []
|
return transformResponse<CleanParty>(input)
|
||||||
parsed.characters = grids.data.characters ?? []
|
|
||||||
} else {
|
|
||||||
parsed.weapons = Array.isArray(parsed.weapons) ? parsed.weapons : []
|
|
||||||
parsed.summons = Array.isArray(parsed.summons) ? parsed.summons : []
|
|
||||||
parsed.characters = Array.isArray(parsed.characters) ? parsed.characters : []
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 4: minimally validate header associations (raid/job)
|
|
||||||
const hdr = MinimalHeaderSchema.safeParse(camel)
|
|
||||||
if (hdr.success) {
|
|
||||||
if (hdr.data.raid !== undefined) parsed.raid = hdr.data.raid
|
|
||||||
if (hdr.data.job !== undefined) parsed.job = hdr.data.job
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 5: core scalars with safe defaults for UI
|
|
||||||
const scal = MinimalScalarsSchema.safeParse(camel)
|
|
||||||
const pick = <T>(v: T | null | undefined, d: T) => (v ?? d)
|
|
||||||
if (scal.success) {
|
|
||||||
parsed.favorited = pick(scal.data.favorited ?? (parsed as any).favorited, false)
|
|
||||||
parsed.fullAuto = pick(scal.data.fullAuto ?? (parsed as any).fullAuto, false)
|
|
||||||
parsed.autoGuard = pick(scal.data.autoGuard ?? (parsed as any).autoGuard, false)
|
|
||||||
parsed.autoSummon = pick(scal.data.autoSummon ?? (parsed as any).autoSummon, false)
|
|
||||||
parsed.chargeAttack = pick(scal.data.chargeAttack ?? (parsed as any).chargeAttack, true)
|
|
||||||
parsed.clearTime = pick(scal.data.clearTime ?? (parsed as any).clearTime, 0)
|
|
||||||
parsed.buttonCount = pick(scal.data.buttonCount ?? (parsed as any).buttonCount, 0)
|
|
||||||
parsed.chainCount = pick(scal.data.chainCount ?? (parsed as any).chainCount, 0)
|
|
||||||
parsed.turnCount = pick(scal.data.turnCount ?? (parsed as any).turnCount, 0)
|
|
||||||
parsed.visibility = pick(scal.data.visibility ?? (parsed as any).visibility, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsed as Party
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
43
src/routes/api/parties/+server.ts
Normal file
43
src/routes/api/parties/+server.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { json, type RequestHandler } from '@sveltejs/kit'
|
||||||
|
import { buildUrl } from '$lib/api/core'
|
||||||
|
import { PUBLIC_SIERO_API_URL } from '$env/static/public'
|
||||||
|
|
||||||
|
const API_BASE = new URL(PUBLIC_SIERO_API_URL || 'http://localhost:3000').href
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/parties - Create a new party
|
||||||
|
* Proxies to Rails API with proper authentication
|
||||||
|
*/
|
||||||
|
export const POST: RequestHandler = async ({ request, fetch }) => {
|
||||||
|
try {
|
||||||
|
const body = await request.json()
|
||||||
|
const editKey = request.headers.get('X-Edit-Key')
|
||||||
|
|
||||||
|
// Forward to Rails API
|
||||||
|
// The server-side fetch will automatically add Bearer token if user is authenticated
|
||||||
|
const response = await fetch(buildUrl('/parties'), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
// If creation was successful and returned an edit key, include it in response
|
||||||
|
if (response.ok) {
|
||||||
|
return json(data, { status: response.status })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward error response
|
||||||
|
return json(data, { status: response.status })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating party:', error)
|
||||||
|
return json(
|
||||||
|
{ error: 'Failed to create party' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
66
src/routes/api/parties/[id]/+server.ts
Normal file
66
src/routes/api/parties/[id]/+server.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { json, type RequestHandler } from '@sveltejs/kit'
|
||||||
|
import { buildUrl } from '$lib/api/core'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT /api/parties/[id] - Update a party
|
||||||
|
* DELETE /api/parties/[id] - Delete a party
|
||||||
|
* Proxies to Rails API with proper authentication
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const PUT: RequestHandler = async ({ request, params, fetch }) => {
|
||||||
|
try {
|
||||||
|
const { id } = params
|
||||||
|
const body = await request.json()
|
||||||
|
const editKey = request.headers.get('X-Edit-Key')
|
||||||
|
|
||||||
|
// Forward to Rails API
|
||||||
|
const response = await fetch(buildUrl(`/parties/${id}`), {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
return json(data, { status: response.status })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating party:', error)
|
||||||
|
return json(
|
||||||
|
{ error: 'Failed to update party' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DELETE: RequestHandler = async ({ request, params, fetch }) => {
|
||||||
|
try {
|
||||||
|
const { id } = params
|
||||||
|
const editKey = request.headers.get('X-Edit-Key')
|
||||||
|
|
||||||
|
// Forward to Rails API
|
||||||
|
const response = await fetch(buildUrl(`/parties/${id}`), {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
return json(data, { status: response.status })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle error responses
|
||||||
|
const errorData = await response.json().catch(() => ({}))
|
||||||
|
return json(errorData, { status: response.status })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting party:', error)
|
||||||
|
return json(
|
||||||
|
{ error: 'Failed to delete party' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
79
src/routes/api/parties/[id]/characters/+server.ts
Normal file
79
src/routes/api/parties/[id]/characters/+server.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { json, type RequestHandler } from '@sveltejs/kit'
|
||||||
|
import { buildUrl } from '$lib/api/core'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/parties/[id]/characters - Add character to party
|
||||||
|
* DELETE /api/parties/[id]/characters - Remove character from party
|
||||||
|
* Proxies to Rails API with proper authentication
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const POST: RequestHandler = async ({ request, params, fetch }) => {
|
||||||
|
try {
|
||||||
|
const body = await request.json()
|
||||||
|
const editKey = request.headers.get('X-Edit-Key')
|
||||||
|
|
||||||
|
// Transform to Rails API format
|
||||||
|
const railsBody = {
|
||||||
|
character: {
|
||||||
|
party_id: params.id,
|
||||||
|
character_id: body.characterId,
|
||||||
|
position: body.position,
|
||||||
|
uncap_level: body.uncapLevel ?? 3,
|
||||||
|
transcendence_step: body.transcendenceStep ?? 0,
|
||||||
|
perpetuity: body.perpetuity ?? false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward to Rails API
|
||||||
|
const response = await fetch(buildUrl('/characters'), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||||
|
},
|
||||||
|
body: JSON.stringify(railsBody)
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
return json(data, { status: response.status })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding character:', error)
|
||||||
|
return json(
|
||||||
|
{ error: 'Failed to add character' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DELETE: RequestHandler = async ({ request, params, fetch }) => {
|
||||||
|
try {
|
||||||
|
const body = await request.json()
|
||||||
|
const editKey = request.headers.get('X-Edit-Key')
|
||||||
|
|
||||||
|
// Forward to Rails API
|
||||||
|
const response = await fetch(buildUrl('/characters'), {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ grid_character_id: body.gridCharacterId })
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
// DELETE might not return a body
|
||||||
|
const text = await response.text()
|
||||||
|
const data = text ? JSON.parse(text) : {}
|
||||||
|
return json(data, { status: response.status })
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorData = await response.json().catch(() => ({}))
|
||||||
|
return json(errorData, { status: response.status })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error removing character:', error)
|
||||||
|
return json(
|
||||||
|
{ error: 'Failed to remove character' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
81
src/routes/api/parties/[id]/summons/+server.ts
Normal file
81
src/routes/api/parties/[id]/summons/+server.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { json, type RequestHandler } from '@sveltejs/kit'
|
||||||
|
import { buildUrl } from '$lib/api/core'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/parties/[id]/summons - Add summon to party
|
||||||
|
* DELETE /api/parties/[id]/summons - Remove summon from party
|
||||||
|
* Proxies to Rails API with proper authentication
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const POST: RequestHandler = async ({ request, params, fetch }) => {
|
||||||
|
try {
|
||||||
|
const body = await request.json()
|
||||||
|
const editKey = request.headers.get('X-Edit-Key')
|
||||||
|
|
||||||
|
// Transform to Rails API format
|
||||||
|
const railsBody = {
|
||||||
|
summon: {
|
||||||
|
party_id: params.id,
|
||||||
|
summon_id: body.summonId,
|
||||||
|
position: body.position,
|
||||||
|
main: body.position === -1 || body.main,
|
||||||
|
friend: body.position === 6 || body.friend,
|
||||||
|
quick_summon: body.quickSummon ?? false,
|
||||||
|
uncap_level: body.uncapLevel ?? 3,
|
||||||
|
transcendence_step: body.transcendenceStep ?? 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward to Rails API
|
||||||
|
const response = await fetch(buildUrl('/summons'), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||||
|
},
|
||||||
|
body: JSON.stringify(railsBody)
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
return json(data, { status: response.status })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding summon:', error)
|
||||||
|
return json(
|
||||||
|
{ error: 'Failed to add summon' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DELETE: RequestHandler = async ({ request, params, fetch }) => {
|
||||||
|
try {
|
||||||
|
const body = await request.json()
|
||||||
|
const editKey = request.headers.get('X-Edit-Key')
|
||||||
|
|
||||||
|
// Forward to Rails API
|
||||||
|
const response = await fetch(buildUrl('/summons'), {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ grid_summon_id: body.gridSummonId })
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
// DELETE might not return a body
|
||||||
|
const text = await response.text()
|
||||||
|
const data = text ? JSON.parse(text) : {}
|
||||||
|
return json(data, { status: response.status })
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorData = await response.json().catch(() => ({}))
|
||||||
|
return json(errorData, { status: response.status })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error removing summon:', error)
|
||||||
|
return json(
|
||||||
|
{ error: 'Failed to remove summon' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
80
src/routes/api/parties/[id]/weapons/+server.ts
Normal file
80
src/routes/api/parties/[id]/weapons/+server.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
import { json, type RequestHandler } from '@sveltejs/kit'
|
||||||
|
import { buildUrl } from '$lib/api/core'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/parties/[id]/weapons - Add weapon to party
|
||||||
|
* DELETE /api/parties/[id]/weapons - Remove weapon from party
|
||||||
|
* Proxies to Rails API with proper authentication
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const POST: RequestHandler = async ({ request, params, fetch }) => {
|
||||||
|
try {
|
||||||
|
const body = await request.json()
|
||||||
|
const editKey = request.headers.get('X-Edit-Key')
|
||||||
|
|
||||||
|
// Transform to Rails API format
|
||||||
|
const railsBody = {
|
||||||
|
weapon: {
|
||||||
|
party_id: params.id,
|
||||||
|
weapon_id: body.weaponId,
|
||||||
|
position: body.position,
|
||||||
|
mainhand: body.position === -1 || body.mainhand,
|
||||||
|
uncap_level: body.uncapLevel ?? 3,
|
||||||
|
transcendence_step: body.transcendenceStep ?? 0,
|
||||||
|
element: body.element
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward to Rails API
|
||||||
|
const response = await fetch(buildUrl('/weapons'), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||||
|
},
|
||||||
|
body: JSON.stringify(railsBody)
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
return json(data, { status: response.status })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding weapon:', error)
|
||||||
|
return json(
|
||||||
|
{ error: 'Failed to add weapon' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DELETE: RequestHandler = async ({ request, params, fetch }) => {
|
||||||
|
try {
|
||||||
|
const body = await request.json()
|
||||||
|
const editKey = request.headers.get('X-Edit-Key')
|
||||||
|
|
||||||
|
// Forward to Rails API
|
||||||
|
const response = await fetch(buildUrl('/weapons'), {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ grid_weapon_id: body.gridWeaponId })
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
// DELETE might not return a body
|
||||||
|
const text = await response.text()
|
||||||
|
const data = text ? JSON.parse(text) : {}
|
||||||
|
return json(data, { status: response.status })
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorData = await response.json().catch(() => ({}))
|
||||||
|
return json(errorData, { status: response.status })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error removing weapon:', error)
|
||||||
|
return json(
|
||||||
|
{ error: 'Failed to remove weapon' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue