add utility functions for party component

- extractErrorMessage: handle nested api error structures
- transformSkillsToArray: convert job skills to api format
- findNextEmptySlot: find available positions in grids
This commit is contained in:
Justin Edmund 2025-11-29 02:41:01 -08:00
parent f9bb43f214
commit 112e8c39a9
3 changed files with 229 additions and 0 deletions

64
src/lib/utils/errors.ts Normal file
View file

@ -0,0 +1,64 @@
/**
* Error message extraction utilities
* Handles complex nested error structures from API responses
*/
export interface NestedErrorDetails {
details?: NestedErrorDetails
errors?: { message?: string; [key: string]: any }
message?: string
}
/**
* Extracts user-friendly error message from nested API error structures
* Handles the pattern: error.details.details.errors.message
*
* @param error - The error object to extract from
* @param fallbackMessage - Message to return if extraction fails
* @returns Extracted error message or fallback
*
* @example
* ```typescript
* try {
* await api.updateParty(...)
* } catch (e) {
* error = extractErrorMessage(e, 'Failed to update party')
* }
* ```
*/
export function extractErrorMessage(
error: any,
fallbackMessage: string = 'An error occurred'
): string {
if (!error) return fallbackMessage
// Navigate through nested details structure
let errorDetails: NestedErrorDetails | undefined = error?.details
while (errorDetails?.details) {
errorDetails = errorDetails.details
}
// Try to extract message from various formats
if (errorDetails?.errors) {
// Simple message format
if (errorDetails.errors.message) {
return errorDetails.errors.message
}
// Field-based errors - combine all messages
const errorMessages = Object.entries(errorDetails.errors)
.map(([field, messages]) => {
if (Array.isArray(messages)) {
return messages.join(', ')
}
return String(messages)
})
.filter((msg) => msg && msg !== 'undefined')
.join('; ')
if (errorMessages) return errorMessages
}
// Fallback to error.message
return error?.message || fallbackMessage
}

View file

@ -0,0 +1,99 @@
/**
* Grid slot finding and helper utilities
* Handles finding available positions in weapon, summon, and character grids
*/
import type { Party } from '$lib/types/api/party'
import { GridType } from '$lib/types/enums'
/** Sentinel value indicating no empty slot was found */
export const SLOT_NOT_FOUND = -999
export interface SlotRange {
start: number
end: number
specialSlots?: number[] // e.g., mainhand (-1), friend summon (6)
}
/** Grid slot configuration for each grid type */
const GRID_CONFIGS: Record<GridType, SlotRange> = {
[GridType.Weapon]: { start: 0, end: 8, specialSlots: [-1] }, // mainhand + 9 grid slots
[GridType.Summon]: { start: 0, end: 5, specialSlots: [-1, 6] }, // main + 6 grid + friend
[GridType.Character]: { start: 0, end: 4, specialSlots: [] } // 5 slots (0-4)
}
/**
* Finds the next empty slot in a grid
*
* @param party - Current party state
* @param gridType - Type of grid to search (weapon, summon, or character)
* @returns Position number of next empty slot, or SLOT_NOT_FOUND if grid is full
*
* @example
* ```typescript
* const nextSlot = findNextEmptySlot(party, GridType.Weapon)
* if (nextSlot !== SLOT_NOT_FOUND) {
* selectedSlot = nextSlot
* }
* ```
*/
export function findNextEmptySlot(party: Party, gridType: GridType): number {
const config = GRID_CONFIGS[gridType]
const collection = getCollectionForType(party, gridType)
// Check special slots first (e.g., mainhand, main summon)
for (const specialSlot of config.specialSlots || []) {
if (!isSlotOccupied(collection, specialSlot, gridType)) {
return specialSlot
}
}
// Check regular grid slots
for (let i = config.start; i <= config.end; i++) {
if (!isSlotOccupied(collection, i, gridType)) {
return i
}
}
return SLOT_NOT_FOUND
}
/**
* Gets the appropriate collection array for a grid type
*/
function getCollectionForType(party: Party, gridType: GridType) {
switch (gridType) {
case GridType.Weapon:
return party.weapons
case GridType.Summon:
return party.summons
case GridType.Character:
return party.characters
}
}
/**
* Checks if a specific slot position is occupied
* Handles special cases for mainhand weapons, main/friend summons
*/
function isSlotOccupied(collection: any[], position: number, gridType: GridType): boolean {
// For weapons, check both position and mainhand flag
if (gridType === GridType.Weapon) {
return collection.some(
(item) => item.position === position || (position === -1 && item.mainhand)
)
}
// For summons, check position, main, and friend flags
if (gridType === GridType.Summon) {
return collection.some(
(item) =>
item.position === position ||
(position === -1 && item.main) ||
(position === 6 && item.friend)
)
}
// For characters, simple position check
return collection.some((item) => item.position === position)
}

View file

@ -0,0 +1,66 @@
/**
* Job skills transformation utilities
* Handles converting between object and array formats for job skills
*/
import type { JobSkill } from '$lib/types/api/entities'
export interface JobSkillsMap {
[slot: string]: JobSkill | null | undefined
}
export interface JobSkillPayload {
id: string
slot: number
}
/**
* Converts job skills object to API array format
* Filters out null/undefined values and adds slot numbers
*
* @param skillsMap - Object mapping slot numbers to job skills
* @returns Array of job skill payloads ready for API submission
*
* @example
* ```typescript
* const skillsMap = { '0': skill1, '1': skill2, '2': null }
* const payload = transformSkillsToArray(skillsMap)
* // Returns: [{ id: 'skill1-id', slot: 0 }, { id: 'skill2-id', slot: 1 }]
* ```
*/
export function transformSkillsToArray(skillsMap: JobSkillsMap): JobSkillPayload[] {
return Object.entries(skillsMap)
.filter(([_, skill]) => skill !== null && skill !== undefined)
.map(([slotKey, skill]) => ({
id: skill!.id,
slot: parseInt(slotKey)
}))
}
/**
* Updates a skill in a specific slot (returns new object, immutable)
*
* @param currentSkills - Current job skills map
* @param slot - Slot number to update
* @param skill - Job skill to set, or null to remove
* @returns New skills map with the update applied
*
* @example
* ```typescript
* const updated = updateSkillInSlot(currentSkills, 0, newSkill)
* const removed = updateSkillInSlot(currentSkills, 1, null)
* ```
*/
export function updateSkillInSlot(
currentSkills: JobSkillsMap,
slot: number,
skill: JobSkill | null
): JobSkillsMap {
const updated = { ...currentSkills }
if (skill === null) {
delete updated[String(slot)]
} else {
updated[String(slot)] = skill
}
return updated
}