From 5c8e23a38ed8d689f6273153a3ac484861d97ca3 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Wed, 17 Sep 2025 22:15:24 -0700 Subject: [PATCH] Added editable mode to character detail --- .../database/characters/[id]/+page.server.ts | 8 +- .../database/characters/[id]/+page.svelte | 389 ++++++++++++++++-- 2 files changed, 371 insertions(+), 26 deletions(-) diff --git a/src/routes/database/characters/[id]/+page.server.ts b/src/routes/database/characters/[id]/+page.server.ts index 3a650851..8cc8ecb4 100644 --- a/src/routes/database/characters/[id]/+page.server.ts +++ b/src/routes/database/characters/[id]/+page.server.ts @@ -2,8 +2,11 @@ import type { PageServerLoad } from './$types' import { get } from '$lib/api/core' import { error } from '@sveltejs/kit' -export const load: PageServerLoad = async ({ params, fetch }) => { +export const load: PageServerLoad = async ({ params, fetch, parent }) => { try { + // Get parent data to access role + const parentData = await parent() + const character = await get(fetch, `/characters/${params.id}`) if (!character) { @@ -11,7 +14,8 @@ export const load: PageServerLoad = async ({ params, fetch }) => { } return { - character + character, + role: parentData.role } } catch (err) { console.error('Failed to load character:', err) diff --git a/src/routes/database/characters/[id]/+page.svelte b/src/routes/database/characters/[id]/+page.svelte index 0f696b30..9b8d7912 100644 --- a/src/routes/database/characters/[id]/+page.svelte +++ b/src/routes/database/characters/[id]/+page.svelte @@ -5,11 +5,11 @@ import { goto } from '$app/navigation' // Utility functions - import { getRarityLabel } from '$lib/utils/rarity' - import { getElementLabel } from '$lib/utils/element' - import { getProficiencyLabel } from '$lib/utils/proficiency' - import { getRaceLabel } from '$lib/utils/race' - import { getGenderLabel } from '$lib/utils/gender' + import { getRarityLabel, getRarityOptions } from '$lib/utils/rarity' + import { getElementLabel, getElementOptions } from '$lib/utils/element' + import { getProficiencyLabel, getProficiencyOptions } from '$lib/utils/proficiency' + import { getRaceLabel, getRaceOptions } from '$lib/utils/race' + import { getGenderLabel, getGenderOptions } from '$lib/utils/gender' import { getCharacterMaxUncapLevel } from '$lib/utils/uncap' // Components @@ -17,6 +17,7 @@ import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte' import DetailItem from '$lib/components/ui/DetailItem.svelte' import DetailsHeader from '$lib/components/ui/DetailsHeader.svelte' + import Button from '$lib/components/ui/Button.svelte' // Types import type { PageData } from './$types' @@ -25,6 +26,160 @@ // Get character from server data const character = $derived(data.character) + const userRole = $derived(data.role || 0) + const canEdit = $derived(userRole >= 7) + + // Edit mode state + let editMode = $state(false) + let isSaving = $state(false) + let saveError = $state(null) + let saveSuccess = $state(false) + + // Editable fields - create reactive state for each field + let editData = $state({ + name: character?.name || '', + granblue_id: character?.granblue_id || '', + rarity: character?.rarity || 1, + element: character?.element || 0, + race1: character?.race?.[0] ?? null, + race2: character?.race?.[1] ?? null, + gender: character?.gender || 0, + proficiency1: character?.proficiency?.[0] || 0, + proficiency2: character?.proficiency?.[1] || 0, + min_hp: character?.hp?.min_hp || 0, + max_hp: character?.hp?.max_hp || 0, + max_hp_flb: character?.hp?.max_hp_flb || 0, + min_atk: character?.atk?.min_atk || 0, + max_atk: character?.atk?.max_atk || 0, + max_atk_flb: character?.atk?.max_atk_flb || 0, + flb: character?.uncap?.flb || false, + ulb: character?.uncap?.ulb || false, + transcendence: character?.uncap?.transcendence || false, + special: character?.special || false + }) + + // Reset edit data when character changes + $effect(() => { + if (character) { + editData = { + name: character.name || '', + granblue_id: character.granblue_id || '', + rarity: character.rarity || 1, + element: character.element || 0, + race1: character.race?.[0] ?? null, + race2: character.race?.[1] ?? null, + gender: character.gender || 0, + proficiency1: character.proficiency?.[0] || 0, + proficiency2: character.proficiency?.[1] || 0, + min_hp: character.hp?.min_hp || 0, + max_hp: character.hp?.max_hp || 0, + max_hp_flb: character.hp?.max_hp_flb || 0, + min_atk: character.atk?.min_atk || 0, + max_atk: character.atk?.max_atk || 0, + max_atk_flb: character.atk?.max_atk_flb || 0, + flb: character.uncap?.flb || false, + ulb: character.uncap?.ulb || false, + transcendence: character.uncap?.transcendence || false, + special: character.special || false + } + } + }) + + // Options for select dropdowns - using centralized utilities + const rarityOptions = getRarityOptions() + const elementOptions = getElementOptions() + const raceOptions = getRaceOptions() + const genderOptions = getGenderOptions() + const proficiencyOptions = getProficiencyOptions() + + function toggleEditMode() { + editMode = !editMode + saveError = null + saveSuccess = false + + // Reset data when canceling + if (!editMode && character) { + editData = { + name: character.name || '', + granblue_id: character.granblue_id || '', + rarity: character.rarity || 1, + element: character.element || 0, + race1: character.race?.[0] ?? null, + race2: character.race?.[1] ?? null, + gender: character.gender || 0, + proficiency1: character.proficiency?.[0] || 0, + proficiency2: character.proficiency?.[1] || 0, + min_hp: character.hp?.min_hp || 0, + max_hp: character.hp?.max_hp || 0, + max_hp_flb: character.hp?.max_hp_flb || 0, + min_atk: character.atk?.min_atk || 0, + max_atk: character.atk?.max_atk || 0, + max_atk_flb: character.atk?.max_atk_flb || 0, + flb: character.uncap?.flb || false, + ulb: character.uncap?.ulb || false, + transcendence: character.uncap?.transcendence || false, + special: character.special || false + } + } + } + + async function saveChanges() { + isSaving = true + saveError = null + saveSuccess = false + + try { + // Prepare the data for API + const payload = { + name: editData.name, + granblue_id: editData.granblue_id, + rarity: editData.rarity, + element: editData.element, + race: [editData.race1, editData.race2].filter(r => r !== null && r !== undefined), + gender: editData.gender, + proficiency: [editData.proficiency1, editData.proficiency2], + hp: { + min_hp: editData.min_hp, + max_hp: editData.max_hp, + max_hp_flb: editData.max_hp_flb + }, + atk: { + min_atk: editData.min_atk, + max_atk: editData.max_atk, + max_atk_flb: editData.max_atk_flb + }, + uncap: { + flb: editData.flb, + ulb: editData.ulb, + transcendence: editData.transcendence + }, + special: editData.special + } + + // TODO: When backend endpoint is ready, make the API call here + // const response = await fetch(`/api/v1/characters/${character.id}`, { + // method: 'PUT', + // headers: { 'Content-Type': 'application/json' }, + // body: JSON.stringify(payload) + // }) + + // For now, just simulate success + await new Promise((resolve) => setTimeout(resolve, 1000)) + + saveSuccess = true + editMode = false + + // Show success message for 3 seconds + setTimeout(() => { + saveSuccess = false + }, 3000) + } catch (error) { + saveError = 'Failed to save changes. Please try again.' + console.error('Save error:', error) + } finally { + isSaving = false + } + } // Helper function to get character image function getCharacterImage(character: any): string { @@ -33,11 +188,15 @@ } // Calculate uncap properties for the indicator - const uncap = $derived(character?.uncap ?? {}) + const uncap = $derived( + editMode + ? { flb: editData.flb, ulb: editData.ulb, transcendence: editData.transcendence } + : (character?.uncap ?? {}) + ) const flb = $derived(uncap.flb ?? false) const ulb = $derived(uncap.ulb ?? false) const transcendence = $derived(uncap.transcendence ?? false) - const special = $derived(character?.special ?? false) + const special = $derived(editMode ? editData.special : (character?.special ?? false)) const uncapLevel = $derived(getCharacterMaxUncapLevel({ special, uncap })) const transcendenceStage = $derived(transcendence ? 5 : 0) @@ -46,11 +205,49 @@
{#if character}
- + + + {#if saveSuccess || saveError} +
+ {#if saveSuccess} + Changes saved successfully! + {/if} + + {#if saveError} + {saveError} + {/if} +
+ {/if} - - + {#if editMode} + + + {:else} + + + {/if} {#if character.uncap} @@ -67,27 +264,140 @@ /> {/if} - - - - - + {#if editMode} + + + + + {/if} + + {#if editMode} + + + + + + + {:else} + + + {#if character.race?.[1]} + + {/if} + + + + {/if} - - - {#if flb} - + {#if editMode} + + + + {:else} + + + {#if flb} + + {/if} {/if} - - - {#if flb} - + {#if editMode} + + + + {:else} + + + {#if flb} + + {/if} {/if}
@@ -105,6 +415,7 @@ @use '$src/themes/layout' as layout; @use '$src/themes/spacing' as spacing; @use '$src/themes/typography' as typography; + @use '$src/themes/effects' as effects; .not-found { text-align: center; @@ -129,7 +440,37 @@ background: white; border-radius: layout.$card-corner; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - overflow: hidden; + overflow: visible; // Changed from hidden to allow sticky header margin-top: spacing.$unit-2x; + position: relative; + } + + .edit-controls { + padding: spacing.$unit-2x; + border-bottom: 1px solid colors.$grey-80; + display: flex; + gap: spacing.$unit; + align-items: center; + + .success-message { + color: colors.$grey-30; + font-size: typography.$font-small; + animation: fadeIn effects.$duration-opacity-fade ease-in; + } + + .error-message { + color: colors.$error; + font-size: typography.$font-small; + animation: fadeIn effects.$duration-opacity-fade ease-in; + } + } + + @keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } }