From 112e8c39a9374dac575cab62c7bddfbb239536fa Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Sat, 29 Nov 2025 02:41:01 -0800 Subject: [PATCH] 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 --- src/lib/utils/errors.ts | 64 +++++++++++++++++++++++ src/lib/utils/gridHelpers.ts | 99 ++++++++++++++++++++++++++++++++++++ src/lib/utils/jobSkills.ts | 66 ++++++++++++++++++++++++ 3 files changed, 229 insertions(+) create mode 100644 src/lib/utils/errors.ts create mode 100644 src/lib/utils/gridHelpers.ts create mode 100644 src/lib/utils/jobSkills.ts diff --git a/src/lib/utils/errors.ts b/src/lib/utils/errors.ts new file mode 100644 index 00000000..e557f6ff --- /dev/null +++ b/src/lib/utils/errors.ts @@ -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 +} diff --git a/src/lib/utils/gridHelpers.ts b/src/lib/utils/gridHelpers.ts new file mode 100644 index 00000000..570e2bf9 --- /dev/null +++ b/src/lib/utils/gridHelpers.ts @@ -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.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) +} diff --git a/src/lib/utils/jobSkills.ts b/src/lib/utils/jobSkills.ts new file mode 100644 index 00000000..9dc92431 --- /dev/null +++ b/src/lib/utils/jobSkills.ts @@ -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 +}