database: add entity edit page scaffolds
This commit is contained in:
parent
1cbcd91f94
commit
7a639effaa
6 changed files with 1024 additions and 0 deletions
|
|
@ -0,0 +1,34 @@
|
|||
import type { PageServerLoad } from './$types'
|
||||
import { entityAdapter } from '$lib/api/adapters/entity.adapter'
|
||||
import { error, redirect } from '@sveltejs/kit'
|
||||
|
||||
export const load: PageServerLoad = async ({ params, parent }) => {
|
||||
// Get parent data to access role
|
||||
const parentData = await parent()
|
||||
|
||||
// Role check - must be editor level (>= 7) to edit
|
||||
if (!parentData.role || parentData.role < 7) {
|
||||
throw redirect(303, `/database/characters/${params.id}`)
|
||||
}
|
||||
|
||||
try {
|
||||
const character = await entityAdapter.getCharacter(params.id)
|
||||
|
||||
if (!character) {
|
||||
throw error(404, 'Character not found')
|
||||
}
|
||||
|
||||
return {
|
||||
character,
|
||||
role: parentData.role
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load character:', err)
|
||||
|
||||
if (err instanceof Error && 'status' in err && err.status === 404) {
|
||||
throw error(404, 'Character not found')
|
||||
}
|
||||
|
||||
throw error(500, 'Failed to load character')
|
||||
}
|
||||
}
|
||||
224
src/routes/(app)/database/characters/[id]/edit/+page.svelte
Normal file
224
src/routes/(app)/database/characters/[id]/edit/+page.svelte
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
<svelte:options runes={true} />
|
||||
|
||||
<script lang="ts">
|
||||
// SvelteKit imports
|
||||
import { goto } from '$app/navigation'
|
||||
|
||||
// TanStack Query
|
||||
import { createQuery, useQueryClient } from '@tanstack/svelte-query'
|
||||
import { entityQueries } from '$lib/api/queries/entity.queries'
|
||||
import { entityAdapter } from '$lib/api/adapters/entity.adapter'
|
||||
import { withInitialData } from '$lib/query/ssr'
|
||||
|
||||
// Components
|
||||
import DetailScaffold from '$lib/features/database/detail/DetailScaffold.svelte'
|
||||
import CharacterMetadataSection from '$lib/features/database/characters/sections/CharacterMetadataSection.svelte'
|
||||
import CharacterUncapSection from '$lib/features/database/characters/sections/CharacterUncapSection.svelte'
|
||||
import CharacterTaxonomySection from '$lib/features/database/characters/sections/CharacterTaxonomySection.svelte'
|
||||
import CharacterStatsSection from '$lib/features/database/characters/sections/CharacterStatsSection.svelte'
|
||||
import CharacterImagesSection from '$lib/features/database/characters/sections/CharacterImagesSection.svelte'
|
||||
import { getCharacterImage } from '$lib/utils/images'
|
||||
|
||||
// Types
|
||||
import type { PageData } from './$types'
|
||||
|
||||
let { data }: { data: PageData } = $props()
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
// Use TanStack Query with SSR initial data
|
||||
const characterQuery = createQuery(() => ({
|
||||
...entityQueries.character(data.character?.id ?? ''),
|
||||
...withInitialData(data.character)
|
||||
}))
|
||||
|
||||
// Get character from query
|
||||
const character = $derived(characterQuery.data)
|
||||
|
||||
// Always in edit mode
|
||||
const editMode = true
|
||||
|
||||
// Save state
|
||||
let isSaving = $state(false)
|
||||
let saveError = $state<string | null>(null)
|
||||
|
||||
// Editable fields - initialized from character data
|
||||
let editData = $state({
|
||||
name: '',
|
||||
granblueId: '',
|
||||
characterId: null as number | null,
|
||||
rarity: 1,
|
||||
element: 0,
|
||||
race1: null as number | null,
|
||||
race2: null as number | null,
|
||||
gender: 0,
|
||||
proficiency1: 0,
|
||||
proficiency2: 0,
|
||||
minHp: 0,
|
||||
maxHp: 0,
|
||||
maxHpFlb: 0,
|
||||
minAtk: 0,
|
||||
maxAtk: 0,
|
||||
maxAtkFlb: 0,
|
||||
flb: false,
|
||||
ulb: false,
|
||||
transcendence: false,
|
||||
special: false
|
||||
})
|
||||
|
||||
// Populate edit data when character loads
|
||||
$effect(() => {
|
||||
if (character) {
|
||||
editData = {
|
||||
name: character.name || '',
|
||||
granblueId: character.granblueId || '',
|
||||
characterId: character.characterId ?? null,
|
||||
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,
|
||||
minHp: character.hp?.minHp || 0,
|
||||
maxHp: character.hp?.maxHp || 0,
|
||||
maxHpFlb: character.hp?.maxHpFlb || 0,
|
||||
minAtk: character.atk?.minAtk || 0,
|
||||
maxAtk: character.atk?.maxAtk || 0,
|
||||
maxAtkFlb: character.atk?.maxAtkFlb || 0,
|
||||
flb: character.uncap?.flb || false,
|
||||
ulb: character.uncap?.ulb || false,
|
||||
transcendence: character.uncap?.transcendence || false,
|
||||
special: character.special || false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
async function saveChanges() {
|
||||
if (!character?.id) return
|
||||
|
||||
isSaving = true
|
||||
saveError = null
|
||||
|
||||
try {
|
||||
// Prepare the data for API (flat snake_case format)
|
||||
const payload = {
|
||||
name_en: editData.name,
|
||||
granblue_id: editData.granblueId,
|
||||
character_id: editData.characterId ? [editData.characterId] : [],
|
||||
rarity: editData.rarity,
|
||||
element: editData.element,
|
||||
race1: editData.race1,
|
||||
race2: editData.race2,
|
||||
gender: editData.gender,
|
||||
proficiency1: editData.proficiency1,
|
||||
proficiency2: editData.proficiency2,
|
||||
min_hp: editData.minHp,
|
||||
max_hp: editData.maxHp,
|
||||
max_hp_flb: editData.maxHpFlb,
|
||||
min_atk: editData.minAtk,
|
||||
max_atk: editData.maxAtk,
|
||||
max_atk_flb: editData.maxAtkFlb,
|
||||
flb: editData.flb,
|
||||
ulb: editData.ulb,
|
||||
special: editData.special
|
||||
}
|
||||
|
||||
await entityAdapter.updateCharacter(character.id, payload)
|
||||
|
||||
// Invalidate TanStack Query cache to refetch fresh data
|
||||
await queryClient.invalidateQueries({ queryKey: ['character', character.id] })
|
||||
|
||||
// Navigate back to detail page
|
||||
goto(`/database/characters/${character.id}`)
|
||||
} catch (error) {
|
||||
saveError = 'Failed to save changes. Please try again.'
|
||||
console.error('Save error:', error)
|
||||
} finally {
|
||||
isSaving = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
goto(`/database/characters/${character?.id}`)
|
||||
}
|
||||
|
||||
// Helper function for character grid image
|
||||
function getCharacterGridImage(character: any): string {
|
||||
return getCharacterImage(character?.granblueId, 'grid', '01')
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="page">
|
||||
{#if character}
|
||||
<DetailScaffold
|
||||
type="character"
|
||||
item={character}
|
||||
image={getCharacterGridImage(character)}
|
||||
showEdit={true}
|
||||
{editMode}
|
||||
{isSaving}
|
||||
{saveError}
|
||||
onSave={saveChanges}
|
||||
onCancel={handleCancel}
|
||||
>
|
||||
<section class="details">
|
||||
<CharacterMetadataSection {character} {editMode} bind:editData />
|
||||
<CharacterUncapSection {character} {editMode} bind:editData />
|
||||
<CharacterTaxonomySection {character} {editMode} bind:editData />
|
||||
<CharacterStatsSection {character} {editMode} bind:editData />
|
||||
|
||||
{#if character?.id && character?.granblueId}
|
||||
<CharacterImagesSection
|
||||
characterId={character.id}
|
||||
granblueId={character.granblueId}
|
||||
canEdit={true}
|
||||
/>
|
||||
{/if}
|
||||
</section>
|
||||
</DetailScaffold>
|
||||
{:else}
|
||||
<div class="not-found">
|
||||
<h2>Character Not Found</h2>
|
||||
<p>The character you're looking for could not be found.</p>
|
||||
<button onclick={() => goto('/database/characters')}>Back to Characters</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use '$src/themes/colors' as colors;
|
||||
@use '$src/themes/layout' as layout;
|
||||
@use '$src/themes/spacing' as spacing;
|
||||
@use '$src/themes/typography' as typography;
|
||||
|
||||
.page {
|
||||
background: white;
|
||||
border-radius: layout.$card-corner;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.not-found {
|
||||
text-align: center;
|
||||
padding: spacing.$unit * 4;
|
||||
|
||||
button {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: spacing.$unit * 0.5 spacing.$unit;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-top: spacing.$unit;
|
||||
|
||||
&:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
34
src/routes/(app)/database/summons/[id]/edit/+page.server.ts
Normal file
34
src/routes/(app)/database/summons/[id]/edit/+page.server.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import type { PageServerLoad } from './$types'
|
||||
import { entityAdapter } from '$lib/api/adapters/entity.adapter'
|
||||
import { error, redirect } from '@sveltejs/kit'
|
||||
|
||||
export const load: PageServerLoad = async ({ params, parent }) => {
|
||||
// Get parent data to access role
|
||||
const parentData = await parent()
|
||||
|
||||
// Role check - must be editor level (>= 7) to edit
|
||||
if (!parentData.role || parentData.role < 7) {
|
||||
throw redirect(303, `/database/summons/${params.id}`)
|
||||
}
|
||||
|
||||
try {
|
||||
const summon = await entityAdapter.getSummon(params.id)
|
||||
|
||||
if (!summon) {
|
||||
throw error(404, 'Summon not found')
|
||||
}
|
||||
|
||||
return {
|
||||
summon,
|
||||
role: parentData.role
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load summon:', err)
|
||||
|
||||
if (err instanceof Error && 'status' in err && err.status === 404) {
|
||||
throw error(404, 'Summon not found')
|
||||
}
|
||||
|
||||
throw error(500, 'Failed to load summon')
|
||||
}
|
||||
}
|
||||
339
src/routes/(app)/database/summons/[id]/edit/+page.svelte
Normal file
339
src/routes/(app)/database/summons/[id]/edit/+page.svelte
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
<svelte:options runes={true} />
|
||||
|
||||
<script lang="ts">
|
||||
// SvelteKit imports
|
||||
import { goto } from '$app/navigation'
|
||||
|
||||
// TanStack Query
|
||||
import { createQuery, useQueryClient } from '@tanstack/svelte-query'
|
||||
import { entityQueries } from '$lib/api/queries/entity.queries'
|
||||
import { entityAdapter } from '$lib/api/adapters/entity.adapter'
|
||||
import { withInitialData } from '$lib/query/ssr'
|
||||
|
||||
// Components
|
||||
import DetailScaffold from '$lib/features/database/detail/DetailScaffold.svelte'
|
||||
import SummonMetadataSection from '$lib/features/database/summons/sections/SummonMetadataSection.svelte'
|
||||
import SummonUncapSection from '$lib/features/database/summons/sections/SummonUncapSection.svelte'
|
||||
import SummonTaxonomySection from '$lib/features/database/summons/sections/SummonTaxonomySection.svelte'
|
||||
import SummonStatsSection from '$lib/features/database/summons/sections/SummonStatsSection.svelte'
|
||||
import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte'
|
||||
import DetailItem from '$lib/components/ui/DetailItem.svelte'
|
||||
import TagInput from '$lib/components/ui/TagInput.svelte'
|
||||
import { getSummonImage } from '$lib/utils/images'
|
||||
|
||||
// Types
|
||||
import type { PageData } from './$types'
|
||||
|
||||
let { data }: { data: PageData } = $props()
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
// Use TanStack Query with SSR initial data
|
||||
const summonQuery = createQuery(() => ({
|
||||
...entityQueries.summon(data.summon?.id ?? ''),
|
||||
...withInitialData(data.summon)
|
||||
}))
|
||||
|
||||
// Get summon from query
|
||||
const summon = $derived(summonQuery.data)
|
||||
|
||||
// Always in edit mode
|
||||
const editMode = true
|
||||
|
||||
// Save state
|
||||
let isSaving = $state(false)
|
||||
let saveError = $state<string | null>(null)
|
||||
|
||||
// Editable fields - initialized from summon data
|
||||
let editData = $state({
|
||||
name: '',
|
||||
nameJp: '',
|
||||
granblueId: '',
|
||||
rarity: 3,
|
||||
element: 0,
|
||||
series: '',
|
||||
minHp: 0,
|
||||
maxHp: 0,
|
||||
maxHpFlb: 0,
|
||||
maxHpUlb: 0,
|
||||
maxHpTranscendence: 0,
|
||||
minAtk: 0,
|
||||
maxAtk: 0,
|
||||
maxAtkFlb: 0,
|
||||
maxAtkUlb: 0,
|
||||
maxAtkTranscendence: 0,
|
||||
maxLevel: 100,
|
||||
flb: false,
|
||||
ulb: false,
|
||||
transcendence: false,
|
||||
subaura: false,
|
||||
limit: false,
|
||||
releaseDate: '',
|
||||
flbDate: '',
|
||||
ulbDate: '',
|
||||
transcendenceDate: '',
|
||||
wikiEn: '',
|
||||
wikiJa: '',
|
||||
gamewith: '',
|
||||
kamigame: '',
|
||||
nicknamesEn: [] as string[],
|
||||
nicknamesJp: [] as string[]
|
||||
})
|
||||
|
||||
// Populate edit data when summon loads
|
||||
$effect(() => {
|
||||
if (summon) {
|
||||
editData = {
|
||||
name: summon.name?.en || '',
|
||||
nameJp: summon.name?.ja || '',
|
||||
granblueId: summon.granblueId || '',
|
||||
rarity: summon.rarity || 3,
|
||||
element: summon.element || 0,
|
||||
series: summon.series || '',
|
||||
minHp: summon.hp?.minHp || 0,
|
||||
maxHp: summon.hp?.maxHp || 0,
|
||||
maxHpFlb: summon.hp?.maxHpFlb || 0,
|
||||
maxHpUlb: summon.hp?.maxHpUlb || 0,
|
||||
maxHpTranscendence: summon.hp?.maxHpXlb || 0,
|
||||
minAtk: summon.atk?.minAtk || 0,
|
||||
maxAtk: summon.atk?.maxAtk || 0,
|
||||
maxAtkFlb: summon.atk?.maxAtkFlb || 0,
|
||||
maxAtkUlb: summon.atk?.maxAtkUlb || 0,
|
||||
maxAtkTranscendence: summon.atk?.maxAtkXlb || 0,
|
||||
maxLevel: 100,
|
||||
flb: summon.uncap?.flb || false,
|
||||
ulb: summon.uncap?.ulb || false,
|
||||
transcendence: summon.uncap?.transcendence || false,
|
||||
subaura: summon.subaura || false,
|
||||
limit: false,
|
||||
releaseDate: '',
|
||||
flbDate: '',
|
||||
ulbDate: '',
|
||||
transcendenceDate: '',
|
||||
wikiEn: '',
|
||||
wikiJa: '',
|
||||
gamewith: '',
|
||||
kamigame: '',
|
||||
nicknamesEn: [],
|
||||
nicknamesJp: []
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
async function saveChanges() {
|
||||
if (!summon?.id) return
|
||||
|
||||
isSaving = true
|
||||
saveError = null
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
name_en: editData.name,
|
||||
name_jp: editData.nameJp || undefined,
|
||||
granblue_id: editData.granblueId,
|
||||
rarity: editData.rarity,
|
||||
element: editData.element,
|
||||
series: editData.series || undefined,
|
||||
min_hp: editData.minHp,
|
||||
max_hp: editData.maxHp,
|
||||
max_hp_flb: editData.maxHpFlb,
|
||||
max_hp_ulb: editData.maxHpUlb,
|
||||
max_hp_xlb: editData.maxHpTranscendence,
|
||||
min_atk: editData.minAtk,
|
||||
max_atk: editData.maxAtk,
|
||||
max_atk_flb: editData.maxAtkFlb,
|
||||
max_atk_ulb: editData.maxAtkUlb,
|
||||
max_atk_xlb: editData.maxAtkTranscendence,
|
||||
max_level: editData.maxLevel,
|
||||
flb: editData.flb,
|
||||
ulb: editData.ulb,
|
||||
transcendence: editData.transcendence,
|
||||
subaura: editData.subaura,
|
||||
limit: editData.limit,
|
||||
release_date: editData.releaseDate || null,
|
||||
flb_date: editData.flbDate || null,
|
||||
ulb_date: editData.ulbDate || null,
|
||||
transcendence_date: editData.transcendenceDate || null,
|
||||
wiki_en: editData.wikiEn,
|
||||
wiki_ja: editData.wikiJa,
|
||||
gamewith: editData.gamewith,
|
||||
kamigame: editData.kamigame,
|
||||
nicknames_en: editData.nicknamesEn,
|
||||
nicknames_jp: editData.nicknamesJp
|
||||
}
|
||||
|
||||
await entityAdapter.updateSummon(summon.id, payload)
|
||||
|
||||
// Invalidate TanStack Query cache to refetch fresh data
|
||||
await queryClient.invalidateQueries({ queryKey: ['summon', summon.id] })
|
||||
|
||||
// Navigate back to detail page
|
||||
goto(`/database/summons/${summon.id}`)
|
||||
} catch (error) {
|
||||
saveError = 'Failed to save changes. Please try again.'
|
||||
console.error('Save error:', error)
|
||||
} finally {
|
||||
isSaving = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
goto(`/database/summons/${summon?.id}`)
|
||||
}
|
||||
|
||||
// Helper function for summon grid image
|
||||
function getSummonGridImage(summon: any): string {
|
||||
return getSummonImage(summon?.granblueId, 'grid')
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="page">
|
||||
{#if summon}
|
||||
<DetailScaffold
|
||||
type="summon"
|
||||
item={summon}
|
||||
image={getSummonGridImage(summon)}
|
||||
showEdit={true}
|
||||
{editMode}
|
||||
{isSaving}
|
||||
{saveError}
|
||||
onSave={saveChanges}
|
||||
onCancel={handleCancel}
|
||||
>
|
||||
<section class="details">
|
||||
<SummonMetadataSection {summon} {editMode} bind:editData />
|
||||
<SummonUncapSection {summon} {editMode} bind:editData />
|
||||
<SummonTaxonomySection {summon} {editMode} bind:editData />
|
||||
<SummonStatsSection {summon} {editMode} bind:editData />
|
||||
|
||||
<DetailsContainer title="Nicknames">
|
||||
<DetailItem label="Nicknames (EN)">
|
||||
<TagInput bind:value={editData.nicknamesEn} placeholder="Add nickname..." contained />
|
||||
</DetailItem>
|
||||
<DetailItem label="Nicknames (JP)">
|
||||
<TagInput
|
||||
bind:value={editData.nicknamesJp}
|
||||
placeholder="ニックネームを入力"
|
||||
contained
|
||||
/>
|
||||
</DetailItem>
|
||||
</DetailsContainer>
|
||||
|
||||
<DetailsContainer title="Dates">
|
||||
<DetailItem
|
||||
label="Release Date"
|
||||
bind:value={editData.releaseDate}
|
||||
editable={true}
|
||||
type="text"
|
||||
placeholder="YYYY-MM-DD"
|
||||
/>
|
||||
{#if editData.flb}
|
||||
<DetailItem
|
||||
label="FLB Date"
|
||||
bind:value={editData.flbDate}
|
||||
editable={true}
|
||||
type="text"
|
||||
placeholder="YYYY-MM-DD"
|
||||
/>
|
||||
{/if}
|
||||
{#if editData.ulb}
|
||||
<DetailItem
|
||||
label="ULB Date"
|
||||
bind:value={editData.ulbDate}
|
||||
editable={true}
|
||||
type="text"
|
||||
placeholder="YYYY-MM-DD"
|
||||
/>
|
||||
{/if}
|
||||
{#if editData.transcendence}
|
||||
<DetailItem
|
||||
label="Transcendence Date"
|
||||
bind:value={editData.transcendenceDate}
|
||||
editable={true}
|
||||
type="text"
|
||||
placeholder="YYYY-MM-DD"
|
||||
/>
|
||||
{/if}
|
||||
</DetailsContainer>
|
||||
|
||||
<DetailsContainer title="Links">
|
||||
<DetailItem
|
||||
label="Wiki (EN)"
|
||||
bind:value={editData.wikiEn}
|
||||
editable={true}
|
||||
type="text"
|
||||
placeholder="https://gbf.wiki/..."
|
||||
width="480px"
|
||||
/>
|
||||
<DetailItem
|
||||
label="Wiki (JP)"
|
||||
bind:value={editData.wikiJa}
|
||||
editable={true}
|
||||
type="text"
|
||||
placeholder="https://gbf-wiki.com/..."
|
||||
width="480px"
|
||||
/>
|
||||
<DetailItem
|
||||
label="Gamewith"
|
||||
bind:value={editData.gamewith}
|
||||
editable={true}
|
||||
type="text"
|
||||
placeholder="https://xn--bck3aza1a2if6kra4ee0hf.gamewith.jp/..."
|
||||
width="480px"
|
||||
/>
|
||||
<DetailItem
|
||||
label="Kamigame"
|
||||
bind:value={editData.kamigame}
|
||||
editable={true}
|
||||
type="text"
|
||||
placeholder="https://kamigame.jp/..."
|
||||
width="480px"
|
||||
/>
|
||||
</DetailsContainer>
|
||||
</section>
|
||||
</DetailScaffold>
|
||||
{:else}
|
||||
<div class="not-found">
|
||||
<h2>Summon Not Found</h2>
|
||||
<p>The summon you're looking for could not be found.</p>
|
||||
<button onclick={() => goto('/database/summons')}>Back to Summons</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use '$src/themes/colors' as colors;
|
||||
@use '$src/themes/layout' as layout;
|
||||
@use '$src/themes/spacing' as spacing;
|
||||
@use '$src/themes/typography' as typography;
|
||||
|
||||
.page {
|
||||
background: white;
|
||||
border-radius: layout.$card-corner;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.not-found {
|
||||
text-align: center;
|
||||
padding: spacing.$unit * 4;
|
||||
|
||||
button {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: spacing.$unit-half spacing.$unit;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-top: spacing.$unit;
|
||||
|
||||
&:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
34
src/routes/(app)/database/weapons/[id]/edit/+page.server.ts
Normal file
34
src/routes/(app)/database/weapons/[id]/edit/+page.server.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import type { PageServerLoad } from './$types'
|
||||
import { entityAdapter } from '$lib/api/adapters/entity.adapter'
|
||||
import { error, redirect } from '@sveltejs/kit'
|
||||
|
||||
export const load: PageServerLoad = async ({ params, parent }) => {
|
||||
// Get parent data to access role
|
||||
const parentData = await parent()
|
||||
|
||||
// Role check - must be editor level (>= 7) to edit
|
||||
if (!parentData.role || parentData.role < 7) {
|
||||
throw redirect(303, `/database/weapons/${params.id}`)
|
||||
}
|
||||
|
||||
try {
|
||||
const weapon = await entityAdapter.getWeapon(params.id)
|
||||
|
||||
if (!weapon) {
|
||||
throw error(404, 'Weapon not found')
|
||||
}
|
||||
|
||||
return {
|
||||
weapon,
|
||||
role: parentData.role
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load weapon:', err)
|
||||
|
||||
if (err instanceof Error && 'status' in err && err.status === 404) {
|
||||
throw error(404, 'Weapon not found')
|
||||
}
|
||||
|
||||
throw error(500, 'Failed to load weapon')
|
||||
}
|
||||
}
|
||||
359
src/routes/(app)/database/weapons/[id]/edit/+page.svelte
Normal file
359
src/routes/(app)/database/weapons/[id]/edit/+page.svelte
Normal file
|
|
@ -0,0 +1,359 @@
|
|||
<svelte:options runes={true} />
|
||||
|
||||
<script lang="ts">
|
||||
// SvelteKit imports
|
||||
import { goto } from '$app/navigation'
|
||||
|
||||
// TanStack Query
|
||||
import { createQuery, useQueryClient } from '@tanstack/svelte-query'
|
||||
import { entityQueries } from '$lib/api/queries/entity.queries'
|
||||
import { entityAdapter } from '$lib/api/adapters/entity.adapter'
|
||||
import { withInitialData } from '$lib/query/ssr'
|
||||
|
||||
// Components
|
||||
import DetailScaffold from '$lib/features/database/detail/DetailScaffold.svelte'
|
||||
import WeaponMetadataSection from '$lib/features/database/weapons/sections/WeaponMetadataSection.svelte'
|
||||
import WeaponUncapSection from '$lib/features/database/weapons/sections/WeaponUncapSection.svelte'
|
||||
import WeaponTaxonomySection from '$lib/features/database/weapons/sections/WeaponTaxonomySection.svelte'
|
||||
import WeaponStatsSection from '$lib/features/database/weapons/sections/WeaponStatsSection.svelte'
|
||||
import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte'
|
||||
import DetailItem from '$lib/components/ui/DetailItem.svelte'
|
||||
import TagInput from '$lib/components/ui/TagInput.svelte'
|
||||
import { getWeaponGridImage } from '$lib/utils/images'
|
||||
|
||||
// Types
|
||||
import type { PageData } from './$types'
|
||||
|
||||
let { data }: { data: PageData } = $props()
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
// Use TanStack Query with SSR initial data
|
||||
const weaponQuery = createQuery(() => ({
|
||||
...entityQueries.weapon(data.weapon?.id ?? ''),
|
||||
...withInitialData(data.weapon)
|
||||
}))
|
||||
|
||||
// Get weapon from query
|
||||
const weapon = $derived(weaponQuery.data)
|
||||
|
||||
// Always in edit mode
|
||||
const editMode = true
|
||||
|
||||
// Save state
|
||||
let isSaving = $state(false)
|
||||
let saveError = $state<string | null>(null)
|
||||
|
||||
// Editable fields - initialized from weapon data
|
||||
let editData = $state({
|
||||
name: '',
|
||||
nameJp: '',
|
||||
granblue_id: '',
|
||||
rarity: 3,
|
||||
element: 0,
|
||||
proficiency: 0,
|
||||
series: 0,
|
||||
minHp: 0,
|
||||
maxHp: 0,
|
||||
maxHpFlb: 0,
|
||||
maxHpUlb: 0,
|
||||
minAtk: 0,
|
||||
maxAtk: 0,
|
||||
maxAtkFlb: 0,
|
||||
maxAtkUlb: 0,
|
||||
maxLevel: 100,
|
||||
maxSkillLevel: 10,
|
||||
maxAwakeningLevel: 0,
|
||||
flb: false,
|
||||
ulb: false,
|
||||
transcendence: false,
|
||||
extra: false,
|
||||
limit: false,
|
||||
ax: false,
|
||||
releaseDate: '',
|
||||
flbDate: '',
|
||||
ulbDate: '',
|
||||
transcendenceDate: '',
|
||||
wikiEn: '',
|
||||
wikiJa: '',
|
||||
gamewith: '',
|
||||
kamigame: '',
|
||||
nicknamesEn: [] as string[],
|
||||
nicknamesJp: [] as string[],
|
||||
recruits: ''
|
||||
})
|
||||
|
||||
// Populate edit data when weapon loads
|
||||
$effect(() => {
|
||||
if (weapon) {
|
||||
editData = {
|
||||
name: weapon.name?.en || '',
|
||||
nameJp: weapon.name?.ja || '',
|
||||
granblue_id: weapon.granblueId || '',
|
||||
rarity: weapon.rarity || 3,
|
||||
element: weapon.element || 0,
|
||||
proficiency: weapon.proficiency || 0,
|
||||
series: weapon.series || 0,
|
||||
minHp: weapon.hp?.minHp || 0,
|
||||
maxHp: weapon.hp?.maxHp || 0,
|
||||
maxHpFlb: weapon.hp?.maxHpFlb || 0,
|
||||
maxHpUlb: weapon.hp?.maxHpUlb || 0,
|
||||
minAtk: weapon.atk?.minAtk || 0,
|
||||
maxAtk: weapon.atk?.maxAtk || 0,
|
||||
maxAtkFlb: weapon.atk?.maxAtkFlb || 0,
|
||||
maxAtkUlb: weapon.atk?.maxAtkUlb || 0,
|
||||
maxLevel: weapon.maxLevel || 100,
|
||||
maxSkillLevel: weapon.maxSkillLevel || 10,
|
||||
maxAwakeningLevel: weapon.maxAwakeningLevel || 0,
|
||||
flb: weapon.uncap?.flb || false,
|
||||
ulb: weapon.uncap?.ulb || false,
|
||||
transcendence: weapon.uncap?.transcendence || false,
|
||||
extra: false,
|
||||
limit: false,
|
||||
ax: weapon.ax || false,
|
||||
releaseDate: '',
|
||||
flbDate: '',
|
||||
ulbDate: '',
|
||||
transcendenceDate: '',
|
||||
wikiEn: '',
|
||||
wikiJa: '',
|
||||
gamewith: '',
|
||||
kamigame: '',
|
||||
nicknamesEn: [],
|
||||
nicknamesJp: [],
|
||||
recruits: ''
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
async function saveChanges() {
|
||||
if (!weapon?.id) return
|
||||
|
||||
isSaving = true
|
||||
saveError = null
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
name_en: editData.name,
|
||||
name_jp: editData.nameJp || undefined,
|
||||
granblue_id: editData.granblue_id,
|
||||
rarity: editData.rarity,
|
||||
element: editData.element,
|
||||
proficiency: editData.proficiency,
|
||||
series: editData.series || undefined,
|
||||
min_hp: editData.minHp,
|
||||
max_hp: editData.maxHp,
|
||||
max_hp_flb: editData.maxHpFlb,
|
||||
max_hp_ulb: editData.maxHpUlb,
|
||||
min_atk: editData.minAtk,
|
||||
max_atk: editData.maxAtk,
|
||||
max_atk_flb: editData.maxAtkFlb,
|
||||
max_atk_ulb: editData.maxAtkUlb,
|
||||
max_level: editData.maxLevel,
|
||||
max_skill_level: editData.maxSkillLevel,
|
||||
max_awakening_level: editData.maxAwakeningLevel,
|
||||
flb: editData.flb,
|
||||
ulb: editData.ulb,
|
||||
transcendence: editData.transcendence,
|
||||
extra: editData.extra,
|
||||
limit: editData.limit,
|
||||
ax: editData.ax,
|
||||
release_date: editData.releaseDate || null,
|
||||
flb_date: editData.flbDate || null,
|
||||
ulb_date: editData.ulbDate || null,
|
||||
transcendence_date: editData.transcendenceDate || null,
|
||||
wiki_en: editData.wikiEn,
|
||||
wiki_ja: editData.wikiJa,
|
||||
gamewith: editData.gamewith,
|
||||
kamigame: editData.kamigame,
|
||||
nicknames_en: editData.nicknamesEn,
|
||||
nicknames_jp: editData.nicknamesJp,
|
||||
recruits: editData.recruits || undefined
|
||||
}
|
||||
|
||||
await entityAdapter.updateWeapon(weapon.id, payload)
|
||||
|
||||
// Invalidate TanStack Query cache to refetch fresh data
|
||||
await queryClient.invalidateQueries({ queryKey: ['weapon', weapon.id] })
|
||||
|
||||
// Navigate back to detail page
|
||||
goto(`/database/weapons/${weapon.id}`)
|
||||
} catch (error) {
|
||||
saveError = 'Failed to save changes. Please try again.'
|
||||
console.error('Save error:', error)
|
||||
} finally {
|
||||
isSaving = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
goto(`/database/weapons/${weapon?.id}`)
|
||||
}
|
||||
|
||||
// Helper function for weapon grid image
|
||||
function getWeaponImage(weapon: any): string {
|
||||
return getWeaponGridImage(weapon?.granblueId, weapon?.element, weapon?.instanceElement)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="page">
|
||||
{#if weapon}
|
||||
<DetailScaffold
|
||||
type="weapon"
|
||||
item={weapon}
|
||||
image={getWeaponImage(weapon)}
|
||||
showEdit={true}
|
||||
{editMode}
|
||||
{isSaving}
|
||||
{saveError}
|
||||
onSave={saveChanges}
|
||||
onCancel={handleCancel}
|
||||
>
|
||||
<section class="details">
|
||||
<WeaponMetadataSection {weapon} {editMode} bind:editData />
|
||||
<WeaponUncapSection {weapon} {editMode} bind:editData />
|
||||
<WeaponTaxonomySection {weapon} {editMode} bind:editData />
|
||||
<WeaponStatsSection {weapon} {editMode} bind:editData />
|
||||
|
||||
<DetailsContainer title="Nicknames">
|
||||
<DetailItem label="Nicknames (EN)">
|
||||
<TagInput bind:value={editData.nicknamesEn} placeholder="Add nickname..." contained />
|
||||
</DetailItem>
|
||||
<DetailItem label="Nicknames (JP)">
|
||||
<TagInput
|
||||
bind:value={editData.nicknamesJp}
|
||||
placeholder="ニックネームを入力"
|
||||
contained
|
||||
/>
|
||||
</DetailItem>
|
||||
</DetailsContainer>
|
||||
|
||||
<DetailsContainer title="Dates">
|
||||
<DetailItem
|
||||
label="Release Date"
|
||||
bind:value={editData.releaseDate}
|
||||
editable={true}
|
||||
type="text"
|
||||
placeholder="YYYY-MM-DD"
|
||||
/>
|
||||
{#if editData.flb}
|
||||
<DetailItem
|
||||
label="FLB Date"
|
||||
bind:value={editData.flbDate}
|
||||
editable={true}
|
||||
type="text"
|
||||
placeholder="YYYY-MM-DD"
|
||||
/>
|
||||
{/if}
|
||||
{#if editData.ulb}
|
||||
<DetailItem
|
||||
label="ULB Date"
|
||||
bind:value={editData.ulbDate}
|
||||
editable={true}
|
||||
type="text"
|
||||
placeholder="YYYY-MM-DD"
|
||||
/>
|
||||
{/if}
|
||||
{#if editData.transcendence}
|
||||
<DetailItem
|
||||
label="Transcendence Date"
|
||||
bind:value={editData.transcendenceDate}
|
||||
editable={true}
|
||||
type="text"
|
||||
placeholder="YYYY-MM-DD"
|
||||
/>
|
||||
{/if}
|
||||
</DetailsContainer>
|
||||
|
||||
<DetailsContainer title="Links">
|
||||
<DetailItem
|
||||
label="Wiki (EN)"
|
||||
bind:value={editData.wikiEn}
|
||||
editable={true}
|
||||
type="text"
|
||||
placeholder="https://gbf.wiki/..."
|
||||
width="480px"
|
||||
/>
|
||||
<DetailItem
|
||||
label="Wiki (JP)"
|
||||
bind:value={editData.wikiJa}
|
||||
editable={true}
|
||||
type="text"
|
||||
placeholder="https://gbf-wiki.com/..."
|
||||
width="480px"
|
||||
/>
|
||||
<DetailItem
|
||||
label="Gamewith"
|
||||
bind:value={editData.gamewith}
|
||||
editable={true}
|
||||
type="text"
|
||||
placeholder="https://xn--bck3aza1a2if6kra4ee0hf.gamewith.jp/..."
|
||||
width="480px"
|
||||
/>
|
||||
<DetailItem
|
||||
label="Kamigame"
|
||||
bind:value={editData.kamigame}
|
||||
editable={true}
|
||||
type="text"
|
||||
placeholder="https://kamigame.jp/..."
|
||||
width="480px"
|
||||
/>
|
||||
</DetailsContainer>
|
||||
|
||||
<DetailsContainer title="Character">
|
||||
<DetailItem
|
||||
label="Recruits"
|
||||
sublabel="Character ID this weapon recruits"
|
||||
bind:value={editData.recruits}
|
||||
editable={true}
|
||||
type="text"
|
||||
placeholder="Character ID..."
|
||||
/>
|
||||
</DetailsContainer>
|
||||
</section>
|
||||
</DetailScaffold>
|
||||
{:else}
|
||||
<div class="not-found">
|
||||
<h2>Weapon Not Found</h2>
|
||||
<p>The weapon you're looking for could not be found.</p>
|
||||
<button onclick={() => goto('/database/weapons')}>Back to Weapons</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use '$src/themes/colors' as colors;
|
||||
@use '$src/themes/layout' as layout;
|
||||
@use '$src/themes/spacing' as spacing;
|
||||
@use '$src/themes/typography' as typography;
|
||||
|
||||
.page {
|
||||
background: white;
|
||||
border-radius: layout.$card-corner;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.not-found {
|
||||
text-align: center;
|
||||
padding: spacing.$unit * 4;
|
||||
|
||||
button {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: spacing.$unit-half spacing.$unit;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-top: spacing.$unit;
|
||||
|
||||
&:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in a new issue