Added editable mode to character detail
This commit is contained in:
parent
0e92b9baf5
commit
5c8e23a38e
2 changed files with 371 additions and 26 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<string | null>(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 @@
|
|||
<div>
|
||||
{#if character}
|
||||
<div class="content">
|
||||
<DetailsHeader type="character" item={character} image={getCharacterImage(character)} />
|
||||
<DetailsHeader
|
||||
type="character"
|
||||
item={character}
|
||||
image={getCharacterImage(character)}
|
||||
onEdit={toggleEditMode}
|
||||
showEdit={canEdit}
|
||||
{editMode}
|
||||
onSave={saveChanges}
|
||||
onCancel={toggleEditMode}
|
||||
{isSaving}
|
||||
/>
|
||||
|
||||
{#if saveSuccess || saveError}
|
||||
<div class="edit-controls">
|
||||
{#if saveSuccess}
|
||||
<span class="success-message">Changes saved successfully!</span>
|
||||
{/if}
|
||||
|
||||
{#if saveError}
|
||||
<span class="error-message">{saveError}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<DetailsContainer title="Metadata">
|
||||
<DetailItem label="Rarity" value={getRarityLabel(character.rarity)} />
|
||||
<DetailItem label="Granblue ID" value={character.granblue_id} />
|
||||
{#if editMode}
|
||||
<DetailItem
|
||||
label="Rarity"
|
||||
bind:value={editData.rarity}
|
||||
editable={true}
|
||||
type="select"
|
||||
options={rarityOptions}
|
||||
/>
|
||||
<DetailItem
|
||||
label="Granblue ID"
|
||||
bind:value={editData.granblue_id}
|
||||
editable={true}
|
||||
type="text"
|
||||
/>
|
||||
{:else}
|
||||
<DetailItem label="Rarity" value={getRarityLabel(character.rarity)} />
|
||||
<DetailItem label="Granblue ID" value={character.granblue_id} />
|
||||
{/if}
|
||||
</DetailsContainer>
|
||||
<DetailsContainer title="Details">
|
||||
{#if character.uncap}
|
||||
|
|
@ -67,27 +264,140 @@
|
|||
/>
|
||||
</DetailItem>
|
||||
{/if}
|
||||
<DetailItem label="Element" value={getElementLabel(character.element)} />
|
||||
<DetailItem label="Race" value={getRaceLabel(character.race)} />
|
||||
<DetailItem label="Gender" value={getGenderLabel(character.gender)} />
|
||||
|
||||
<DetailItem label="Proficiency 1" value={getProficiencyLabel(character.proficiency[0])} />
|
||||
<DetailItem label="Proficiency 2" value={getProficiencyLabel(character.proficiency[1])} />
|
||||
{#if editMode}
|
||||
<DetailItem label="FLB" bind:value={editData.flb} editable={true} type="checkbox" />
|
||||
<DetailItem label="ULB" bind:value={editData.ulb} editable={true} type="checkbox" />
|
||||
<DetailItem
|
||||
label="Transcendence"
|
||||
bind:value={editData.transcendence}
|
||||
editable={true}
|
||||
type="checkbox"
|
||||
/>
|
||||
<DetailItem
|
||||
label="Special"
|
||||
bind:value={editData.special}
|
||||
editable={true}
|
||||
type="checkbox"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if editMode}
|
||||
<DetailItem
|
||||
label="Element"
|
||||
bind:value={editData.element}
|
||||
editable={true}
|
||||
type="select"
|
||||
options={elementOptions}
|
||||
/>
|
||||
<DetailItem
|
||||
label="Race 1"
|
||||
bind:value={editData.race1}
|
||||
editable={true}
|
||||
type="select"
|
||||
options={raceOptions}
|
||||
/>
|
||||
<DetailItem
|
||||
label="Race 2"
|
||||
bind:value={editData.race2}
|
||||
editable={true}
|
||||
type="select"
|
||||
options={raceOptions}
|
||||
/>
|
||||
<DetailItem
|
||||
label="Gender"
|
||||
bind:value={editData.gender}
|
||||
editable={true}
|
||||
type="select"
|
||||
options={genderOptions}
|
||||
/>
|
||||
<DetailItem
|
||||
label="Proficiency 1"
|
||||
bind:value={editData.proficiency1}
|
||||
editable={true}
|
||||
type="select"
|
||||
options={proficiencyOptions}
|
||||
/>
|
||||
<DetailItem
|
||||
label="Proficiency 2"
|
||||
bind:value={editData.proficiency2}
|
||||
editable={true}
|
||||
type="select"
|
||||
options={proficiencyOptions}
|
||||
/>
|
||||
{:else}
|
||||
<DetailItem label="Element" value={getElementLabel(character.element)} />
|
||||
<DetailItem label="Race 1" value={getRaceLabel(character.race?.[0])} />
|
||||
{#if character.race?.[1]}
|
||||
<DetailItem label="Race 2" value={getRaceLabel(character.race?.[1])} />
|
||||
{/if}
|
||||
<DetailItem label="Gender" value={getGenderLabel(character.gender)} />
|
||||
<DetailItem label="Proficiency 1" value={getProficiencyLabel(character.proficiency[0])} />
|
||||
<DetailItem label="Proficiency 2" value={getProficiencyLabel(character.proficiency[1])} />
|
||||
{/if}
|
||||
</DetailsContainer>
|
||||
|
||||
<DetailsContainer title="HP Stats">
|
||||
<DetailItem label="Base HP" value={character.hp?.min_hp} />
|
||||
<DetailItem label="Max HP" value={character.hp?.max_hp} />
|
||||
{#if flb}
|
||||
<DetailItem label="Max HP (FLB)" value={character.hp?.max_hp_flb} />
|
||||
{#if editMode}
|
||||
<DetailItem
|
||||
label="Base HP"
|
||||
bind:value={editData.min_hp}
|
||||
editable={true}
|
||||
type="number"
|
||||
placeholder="0"
|
||||
/>
|
||||
<DetailItem
|
||||
label="Max HP"
|
||||
bind:value={editData.max_hp}
|
||||
editable={true}
|
||||
type="number"
|
||||
placeholder="0"
|
||||
/>
|
||||
<DetailItem
|
||||
label="Max HP (FLB)"
|
||||
bind:value={editData.max_hp_flb}
|
||||
editable={true}
|
||||
type="number"
|
||||
placeholder="0"
|
||||
/>
|
||||
{:else}
|
||||
<DetailItem label="Base HP" value={character.hp?.min_hp} />
|
||||
<DetailItem label="Max HP" value={character.hp?.max_hp} />
|
||||
{#if flb}
|
||||
<DetailItem label="Max HP (FLB)" value={character.hp?.max_hp_flb} />
|
||||
{/if}
|
||||
{/if}
|
||||
</DetailsContainer>
|
||||
|
||||
<DetailsContainer title="Attack Stats">
|
||||
<DetailItem label="Base Attack" value={character.atk?.min_atk} />
|
||||
<DetailItem label="Max Attack" value={character.atk?.max_atk} />
|
||||
{#if flb}
|
||||
<DetailItem label="Max Attack (FLB)" value={character.atk?.max_atk_flb} />
|
||||
{#if editMode}
|
||||
<DetailItem
|
||||
label="Base Attack"
|
||||
bind:value={editData.min_atk}
|
||||
editable={true}
|
||||
type="number"
|
||||
placeholder="0"
|
||||
/>
|
||||
<DetailItem
|
||||
label="Max Attack"
|
||||
bind:value={editData.max_atk}
|
||||
editable={true}
|
||||
type="number"
|
||||
placeholder="0"
|
||||
/>
|
||||
<DetailItem
|
||||
label="Max Attack (FLB)"
|
||||
bind:value={editData.max_atk_flb}
|
||||
editable={true}
|
||||
type="number"
|
||||
placeholder="0"
|
||||
/>
|
||||
{:else}
|
||||
<DetailItem label="Base Attack" value={character.atk?.min_atk} />
|
||||
<DetailItem label="Max Attack" value={character.atk?.max_atk} />
|
||||
{#if flb}
|
||||
<DetailItem label="Max Attack (FLB)" value={character.atk?.max_atk_flb} />
|
||||
{/if}
|
||||
{/if}
|
||||
</DetailsContainer>
|
||||
</div>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in a new issue