summons: add creation page and section components
- /database/summons/new route with full form - UncapSection with FLB/ULB/Transcendence cascade - TaxonomySection with element and series - StatsSection with HP/ATK at all uncap levels - Nicknames via TagInput component - getSummonMaxUncapLevel() utility function 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
4973643ee6
commit
1fa6429749
5 changed files with 695 additions and 56 deletions
|
|
@ -1,40 +1,150 @@
|
||||||
<svelte:options runes={true} />
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte'
|
import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte'
|
||||||
import DetailItem from '$lib/components/ui/DetailItem.svelte'
|
import DetailItem from '$lib/components/ui/DetailItem.svelte'
|
||||||
|
|
||||||
let { summon, editMode = false, editData = $bindable<any>() }:
|
interface Props {
|
||||||
{ summon: any; editMode?: boolean; editData?: any } = $props()
|
summon: any
|
||||||
|
editMode?: boolean
|
||||||
|
editData?: any
|
||||||
|
}
|
||||||
|
|
||||||
const flb = $derived(editMode ? Boolean(editData.flb) : Boolean(summon?.uncap?.flb))
|
let { summon, editMode = false, editData = $bindable() }: Props = $props()
|
||||||
|
|
||||||
|
const flb = $derived(editMode ? Boolean(editData.flb) : Boolean(summon?.uncap?.flb))
|
||||||
|
const ulb = $derived(editMode ? Boolean(editData.ulb) : Boolean(summon?.uncap?.ulb))
|
||||||
|
const transcendence = $derived(
|
||||||
|
editMode ? Boolean(editData.transcendence) : Boolean(summon?.uncap?.transcendence)
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DetailsContainer title="HP Stats">
|
<DetailsContainer title="HP Stats">
|
||||||
{#if editMode}
|
{#if editMode}
|
||||||
<DetailItem label="Base HP" bind:value={editData.min_hp} editable={true} type="number" placeholder="0" />
|
<DetailItem
|
||||||
<DetailItem label="Max HP" bind:value={editData.max_hp} editable={true} type="number" placeholder="0" />
|
label="Base HP"
|
||||||
<DetailItem label="Max HP (FLB)" bind:value={editData.max_hp_flb} editable={true} type="number" placeholder="0" />
|
bind:value={editData.minHp}
|
||||||
{:else}
|
editable={true}
|
||||||
<DetailItem label="Base HP" value={summon.hp?.min_hp} />
|
type="number"
|
||||||
<DetailItem label="Max HP" value={summon.hp?.max_hp} />
|
placeholder="0"
|
||||||
{#if flb}
|
/>
|
||||||
<DetailItem label="Max HP (FLB)" value={summon.hp?.max_hp_flb} />
|
<DetailItem
|
||||||
{/if}
|
label="Max HP"
|
||||||
{/if}
|
bind:value={editData.maxHp}
|
||||||
|
editable={true}
|
||||||
|
type="number"
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
{#if flb}
|
||||||
|
<DetailItem
|
||||||
|
label="Max HP (FLB)"
|
||||||
|
bind:value={editData.maxHpFlb}
|
||||||
|
editable={true}
|
||||||
|
type="number"
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{#if ulb}
|
||||||
|
<DetailItem
|
||||||
|
label="Max HP (ULB)"
|
||||||
|
bind:value={editData.maxHpUlb}
|
||||||
|
editable={true}
|
||||||
|
type="number"
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{#if transcendence}
|
||||||
|
<DetailItem
|
||||||
|
label="Max HP (Transcendence)"
|
||||||
|
bind:value={editData.maxHpTranscendence}
|
||||||
|
editable={true}
|
||||||
|
type="number"
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<DetailItem label="Base HP" value={summon.hp?.minHp} />
|
||||||
|
<DetailItem label="Max HP" value={summon.hp?.maxHp} />
|
||||||
|
{#if flb}
|
||||||
|
<DetailItem label="Max HP (FLB)" value={summon.hp?.maxHpFlb} />
|
||||||
|
{/if}
|
||||||
|
{#if ulb}
|
||||||
|
<DetailItem label="Max HP (ULB)" value={summon.hp?.maxHpUlb} />
|
||||||
|
{/if}
|
||||||
|
{#if transcendence}
|
||||||
|
<DetailItem label="Max HP (Transcendence)" value={summon.hp?.maxHpXlb} />
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
</DetailsContainer>
|
</DetailsContainer>
|
||||||
|
|
||||||
<DetailsContainer title="Attack Stats">
|
<DetailsContainer title="Attack Stats">
|
||||||
{#if editMode}
|
{#if editMode}
|
||||||
<DetailItem label="Base Attack" bind:value={editData.min_atk} editable={true} type="number" placeholder="0" />
|
<DetailItem
|
||||||
<DetailItem label="Max Attack" bind:value={editData.max_atk} editable={true} type="number" placeholder="0" />
|
label="Base Attack"
|
||||||
<DetailItem label="Max Attack (FLB)" bind:value={editData.max_atk_flb} editable={true} type="number" placeholder="0" />
|
bind:value={editData.minAtk}
|
||||||
{:else}
|
editable={true}
|
||||||
<DetailItem label="Base Attack" value={summon.atk?.min_atk} />
|
type="number"
|
||||||
<DetailItem label="Max Attack" value={summon.atk?.max_atk} />
|
placeholder="0"
|
||||||
{#if flb}
|
/>
|
||||||
<DetailItem label="Max Attack (FLB)" value={summon.atk?.max_atk_flb} />
|
<DetailItem
|
||||||
{/if}
|
label="Max Attack"
|
||||||
{/if}
|
bind:value={editData.maxAtk}
|
||||||
|
editable={true}
|
||||||
|
type="number"
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
{#if flb}
|
||||||
|
<DetailItem
|
||||||
|
label="Max Attack (FLB)"
|
||||||
|
bind:value={editData.maxAtkFlb}
|
||||||
|
editable={true}
|
||||||
|
type="number"
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{#if ulb}
|
||||||
|
<DetailItem
|
||||||
|
label="Max Attack (ULB)"
|
||||||
|
bind:value={editData.maxAtkUlb}
|
||||||
|
editable={true}
|
||||||
|
type="number"
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{#if transcendence}
|
||||||
|
<DetailItem
|
||||||
|
label="Max Attack (Transcendence)"
|
||||||
|
bind:value={editData.maxAtkTranscendence}
|
||||||
|
editable={true}
|
||||||
|
type="number"
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<DetailItem label="Base Attack" value={summon.atk?.minAtk} />
|
||||||
|
<DetailItem label="Max Attack" value={summon.atk?.maxAtk} />
|
||||||
|
{#if flb}
|
||||||
|
<DetailItem label="Max Attack (FLB)" value={summon.atk?.maxAtkFlb} />
|
||||||
|
{/if}
|
||||||
|
{#if ulb}
|
||||||
|
<DetailItem label="Max Attack (ULB)" value={summon.atk?.maxAtkUlb} />
|
||||||
|
{/if}
|
||||||
|
{#if transcendence}
|
||||||
|
<DetailItem label="Max Attack (Transcendence)" value={summon.atk?.maxAtkXlb} />
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
</DetailsContainer>
|
</DetailsContainer>
|
||||||
|
|
||||||
|
<DetailsContainer title="Caps">
|
||||||
|
{#if editMode}
|
||||||
|
<DetailItem
|
||||||
|
label="Max Level"
|
||||||
|
bind:value={editData.maxLevel}
|
||||||
|
editable={true}
|
||||||
|
type="number"
|
||||||
|
placeholder="100"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<DetailItem label="Max Level" value={summon.maxLevel} />
|
||||||
|
{/if}
|
||||||
|
</DetailsContainer>
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,41 @@
|
||||||
<svelte:options runes={true} />
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte'
|
import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte'
|
||||||
import DetailItem from '$lib/components/ui/DetailItem.svelte'
|
import DetailItem from '$lib/components/ui/DetailItem.svelte'
|
||||||
import { getElementLabel, getElementOptions } from '$lib/utils/element'
|
import { getElementLabel, getElementOptions } from '$lib/utils/element'
|
||||||
|
|
||||||
let { summon, editMode = false, editData = $bindable<any>() }:
|
interface Props {
|
||||||
{ summon: any; editMode?: boolean; editData?: any } = $props()
|
summon: any
|
||||||
|
editMode?: boolean
|
||||||
|
editData?: any
|
||||||
|
}
|
||||||
|
|
||||||
const elementOptions = getElementOptions()
|
let { summon, editMode = false, editData = $bindable() }: Props = $props()
|
||||||
|
|
||||||
|
const elementOptions = getElementOptions()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DetailsContainer title="Details">
|
<DetailsContainer title="Details">
|
||||||
{#if editMode}
|
{#if editMode}
|
||||||
<DetailItem label="Element" bind:value={editData.element} editable={true} type="select" options={elementOptions} />
|
<DetailItem
|
||||||
{:else}
|
label="Element"
|
||||||
<DetailItem label="Element" value={getElementLabel(summon.element)} />
|
bind:value={editData.element}
|
||||||
{/if}
|
editable={true}
|
||||||
|
type="select"
|
||||||
|
options={elementOptions}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Series"
|
||||||
|
bind:value={editData.series}
|
||||||
|
editable={true}
|
||||||
|
type="text"
|
||||||
|
placeholder="Series name"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<DetailItem label="Element" value={getElementLabel(summon.element)} />
|
||||||
|
{#if summon.series}
|
||||||
|
<DetailItem label="Series" value={summon.series} />
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
</DetailsContainer>
|
</DetailsContainer>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,123 @@
|
||||||
<svelte:options runes={true} />
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte'
|
import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte'
|
||||||
import DetailItem from '$lib/components/ui/DetailItem.svelte'
|
import DetailItem from '$lib/components/ui/DetailItem.svelte'
|
||||||
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
|
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
|
||||||
|
import { getSummonMaxUncapLevel } from '$lib/utils/uncap'
|
||||||
|
import { getElementLabel } from '$lib/utils/element'
|
||||||
|
|
||||||
let { summon, editMode = false, editData = $bindable<any>() }:
|
type ElementName = 'wind' | 'fire' | 'water' | 'earth' | 'dark' | 'light'
|
||||||
{ summon: any; editMode?: boolean; editData?: any } = $props()
|
|
||||||
|
|
||||||
const flb = $derived(editMode ? Boolean(editData.flb) : Boolean(summon?.uncap?.flb))
|
interface Props {
|
||||||
const ulb = $derived(editMode ? Boolean(editData.ulb) : Boolean(summon?.uncap?.ulb))
|
summon: any
|
||||||
const transcendence = $derived(editMode ? Boolean(editData.transcendence) : Boolean(summon?.uncap?.transcendence))
|
editMode?: boolean
|
||||||
|
editData?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
let { summon, editMode = false, editData = $bindable() }: Props = $props()
|
||||||
|
|
||||||
|
const uncap = $derived(
|
||||||
|
editMode
|
||||||
|
? { flb: editData.flb, ulb: editData.ulb, transcendence: editData.transcendence }
|
||||||
|
: (summon?.uncap ?? {})
|
||||||
|
)
|
||||||
|
const flb = $derived(uncap.flb ?? false)
|
||||||
|
const ulb = $derived(uncap.ulb ?? false)
|
||||||
|
const transcendence = $derived(uncap.transcendence ?? false)
|
||||||
|
const subaura = $derived(editMode ? editData.subaura : (summon?.subaura ?? false))
|
||||||
|
const uncapLevel = $derived(getSummonMaxUncapLevel({ uncap }))
|
||||||
|
const transcendenceStage = $derived(transcendence ? 5 : 0)
|
||||||
|
|
||||||
|
// Get element name for checkbox theming
|
||||||
|
const elementName = $derived.by((): ElementName | undefined => {
|
||||||
|
const el = editMode ? editData.element : summon?.element
|
||||||
|
const label = getElementLabel(el)
|
||||||
|
return label !== '—' && label !== 'Null' ? (label.toLowerCase() as ElementName) : undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
// Auto-check/uncheck uncap levels in hierarchy: Transcendence > ULB > FLB
|
||||||
|
function handleFlbChange(checked: boolean) {
|
||||||
|
if (!checked) {
|
||||||
|
// Unchecking FLB should also uncheck ULB and Transcendence
|
||||||
|
editData.ulb = false
|
||||||
|
editData.transcendence = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUlbChange(checked: boolean) {
|
||||||
|
if (checked && !editData.flb) {
|
||||||
|
// Checking ULB should also check FLB
|
||||||
|
editData.flb = true
|
||||||
|
} else if (!checked) {
|
||||||
|
// Unchecking ULB should also uncheck Transcendence
|
||||||
|
editData.transcendence = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTranscendenceChange(checked: boolean) {
|
||||||
|
if (checked) {
|
||||||
|
// Checking Transcendence should also check ULB and FLB
|
||||||
|
if (!editData.ulb) editData.ulb = true
|
||||||
|
if (!editData.flb) editData.flb = true
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DetailsContainer title="Uncap">
|
<DetailsContainer title="Uncap">
|
||||||
<DetailItem label="Uncap">
|
{#if summon?.uncap || editMode}
|
||||||
<UncapIndicator type="summon" {flb} {ulb} {transcendence} editable={false} />
|
<DetailItem label="Uncap">
|
||||||
</DetailItem>
|
<UncapIndicator
|
||||||
|
type="summon"
|
||||||
|
{uncapLevel}
|
||||||
|
{transcendenceStage}
|
||||||
|
{flb}
|
||||||
|
{ulb}
|
||||||
|
{transcendence}
|
||||||
|
{subaura}
|
||||||
|
editable={false}
|
||||||
|
/>
|
||||||
|
</DetailItem>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if editMode}
|
{#if editMode}
|
||||||
<DetailItem label="FLB" bind:value={editData.flb} editable={true} type="checkbox" />
|
<DetailItem
|
||||||
<DetailItem label="ULB" bind:value={editData.ulb} editable={true} type="checkbox" />
|
label="FLB"
|
||||||
<DetailItem label="Transcendence" bind:value={editData.transcendence} editable={true} type="checkbox" />
|
bind:value={editData.flb}
|
||||||
{/if}
|
editable={true}
|
||||||
|
type="checkbox"
|
||||||
|
element={elementName}
|
||||||
|
onchange={handleFlbChange}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="ULB"
|
||||||
|
bind:value={editData.ulb}
|
||||||
|
editable={true}
|
||||||
|
type="checkbox"
|
||||||
|
element={elementName}
|
||||||
|
onchange={handleUlbChange}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Transcendence"
|
||||||
|
bind:value={editData.transcendence}
|
||||||
|
editable={true}
|
||||||
|
type="checkbox"
|
||||||
|
element={elementName}
|
||||||
|
onchange={handleTranscendenceChange}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Subaura"
|
||||||
|
bind:value={editData.subaura}
|
||||||
|
editable={true}
|
||||||
|
type="checkbox"
|
||||||
|
element={elementName}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Limit"
|
||||||
|
bind:value={editData.limit}
|
||||||
|
editable={true}
|
||||||
|
type="checkbox"
|
||||||
|
element={elementName}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</DetailsContainer>
|
</DetailsContainer>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,10 @@ export interface CharacterUncapData {
|
||||||
uncap: UncapData
|
uncap: UncapData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SummonUncapData {
|
||||||
|
uncap: UncapData
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate the maximum uncap level for a character based on their uncap data
|
* Calculate the maximum uncap level for a character based on their uncap data
|
||||||
* @param special - Whether the character is special (limited/seasonal)
|
* @param special - Whether the character is special (limited/seasonal)
|
||||||
|
|
@ -40,6 +44,19 @@ export function getCharacterMaxUncapLevel(character: CharacterUncapData): number
|
||||||
return getMaxUncapLevel(special, uncap.flb, uncap.ulb)
|
return getMaxUncapLevel(special, uncap.flb, uncap.ulb)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the maximum uncap level from summon uncap data
|
||||||
|
* Summons: 3 base, +1 for FLB (4), +1 for ULB (5), transcendence stage tracked separately
|
||||||
|
* @param summon - Summon data with uncap information
|
||||||
|
* @returns The maximum uncap level
|
||||||
|
*/
|
||||||
|
export function getSummonMaxUncapLevel(summon: SummonUncapData): number {
|
||||||
|
const { uncap } = summon
|
||||||
|
if (uncap.ulb) return 5
|
||||||
|
if (uncap.flb) return 4
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the default max uncap level for an item type (without transcendence)
|
* Get the default max uncap level for an item type (without transcendence)
|
||||||
* @param type - The type of item (character, weapon, or summon)
|
* @param type - The type of item (character, weapon, or summon)
|
||||||
|
|
|
||||||
396
src/routes/(app)/database/summons/new/+page.svelte
Normal file
396
src/routes/(app)/database/summons/new/+page.svelte
Normal file
|
|
@ -0,0 +1,396 @@
|
||||||
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
// SvelteKit imports
|
||||||
|
import { goto } from '$app/navigation'
|
||||||
|
|
||||||
|
// Components
|
||||||
|
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 SidebarHeader from '$lib/components/ui/SidebarHeader.svelte'
|
||||||
|
import Button from '$lib/components/ui/Button.svelte'
|
||||||
|
import ValidatedInput from '$lib/components/ui/ValidatedInput.svelte'
|
||||||
|
import TagInput from '$lib/components/ui/TagInput.svelte'
|
||||||
|
import { entityAdapter } from '$lib/api/adapters/entity.adapter'
|
||||||
|
import { getRarityOptions } from '$lib/utils/rarity'
|
||||||
|
|
||||||
|
// Always in edit mode for new summon
|
||||||
|
const editMode = true
|
||||||
|
|
||||||
|
let isSaving = $state(false)
|
||||||
|
let saveError = $state<string | null>(null)
|
||||||
|
|
||||||
|
// Validation state for canCreate check
|
||||||
|
let granblueIdValid = $state(false)
|
||||||
|
let granblueIdExistsInDb = $state(false)
|
||||||
|
|
||||||
|
// Empty summon for new creation
|
||||||
|
const emptySummon = {
|
||||||
|
id: '',
|
||||||
|
name: { en: '', jp: '' },
|
||||||
|
granblueId: '',
|
||||||
|
summonId: '',
|
||||||
|
rarity: 3,
|
||||||
|
element: 0,
|
||||||
|
series: '',
|
||||||
|
hp: { minHp: 0, maxHp: 0, maxHpFlb: 0, maxHpUlb: 0, maxHpXlb: 0 },
|
||||||
|
atk: { minAtk: 0, maxAtk: 0, maxAtkFlb: 0, maxAtkUlb: 0, maxAtkXlb: 0 },
|
||||||
|
uncap: { flb: false, ulb: false, transcendence: false },
|
||||||
|
subaura: false,
|
||||||
|
limit: false,
|
||||||
|
maxLevel: 100
|
||||||
|
}
|
||||||
|
|
||||||
|
// Editable fields
|
||||||
|
let editData = $state({
|
||||||
|
// Basic Info
|
||||||
|
name: '',
|
||||||
|
nameJp: '',
|
||||||
|
granblueId: '',
|
||||||
|
summonId: '',
|
||||||
|
rarity: 3,
|
||||||
|
|
||||||
|
// Taxonomy
|
||||||
|
element: 0,
|
||||||
|
series: '',
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
minHp: 0,
|
||||||
|
maxHp: 0,
|
||||||
|
maxHpFlb: 0,
|
||||||
|
maxHpUlb: 0,
|
||||||
|
maxHpTranscendence: 0,
|
||||||
|
minAtk: 0,
|
||||||
|
maxAtk: 0,
|
||||||
|
maxAtkFlb: 0,
|
||||||
|
maxAtkUlb: 0,
|
||||||
|
maxAtkTranscendence: 0,
|
||||||
|
maxLevel: 100,
|
||||||
|
|
||||||
|
// Uncap
|
||||||
|
flb: false,
|
||||||
|
ulb: false,
|
||||||
|
transcendence: false,
|
||||||
|
subaura: false,
|
||||||
|
limit: false,
|
||||||
|
|
||||||
|
// Dates
|
||||||
|
releaseDate: '',
|
||||||
|
flbDate: '',
|
||||||
|
ulbDate: '',
|
||||||
|
transcendenceDate: '',
|
||||||
|
|
||||||
|
// Links
|
||||||
|
wikiEn: '',
|
||||||
|
wikiJa: '',
|
||||||
|
gamewith: '',
|
||||||
|
kamigame: '',
|
||||||
|
|
||||||
|
// Nicknames
|
||||||
|
nicknamesEn: [] as string[],
|
||||||
|
nicknamesJp: [] as string[]
|
||||||
|
})
|
||||||
|
|
||||||
|
const rarityOptions = getRarityOptions()
|
||||||
|
|
||||||
|
// Validation is required before create
|
||||||
|
const canCreate = $derived(
|
||||||
|
granblueIdValid &&
|
||||||
|
!granblueIdExistsInDb &&
|
||||||
|
editData.name.trim() !== '' &&
|
||||||
|
editData.granblueId.trim() !== ''
|
||||||
|
)
|
||||||
|
|
||||||
|
async function validateGranblueId(value: string): Promise<{ valid: boolean; message: string }> {
|
||||||
|
if (!value || value.length !== 10) {
|
||||||
|
granblueIdValid = false
|
||||||
|
granblueIdExistsInDb = false
|
||||||
|
return { valid: false, message: 'Granblue ID must be exactly 10 digits' }
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await entityAdapter.validateSummonGranblueId(value)
|
||||||
|
|
||||||
|
if (!result.valid) {
|
||||||
|
granblueIdValid = false
|
||||||
|
granblueIdExistsInDb = false
|
||||||
|
return { valid: false, message: result.error || 'Invalid Granblue ID' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.existsInDb) {
|
||||||
|
granblueIdValid = true
|
||||||
|
granblueIdExistsInDb = true
|
||||||
|
return { valid: false, message: 'A summon with this Granblue ID already exists' }
|
||||||
|
}
|
||||||
|
|
||||||
|
granblueIdValid = true
|
||||||
|
granblueIdExistsInDb = false
|
||||||
|
return { valid: true, message: 'Valid Granblue ID - images found on server' }
|
||||||
|
} catch (error) {
|
||||||
|
granblueIdValid = false
|
||||||
|
granblueIdExistsInDb = false
|
||||||
|
console.error('Validation error:', error)
|
||||||
|
return { valid: false, message: 'Failed to validate Granblue ID' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createSummon() {
|
||||||
|
if (!canCreate) return
|
||||||
|
|
||||||
|
isSaving = true
|
||||||
|
saveError = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Map transcendence stats to xlb for API
|
||||||
|
const payload = {
|
||||||
|
// Basic Info
|
||||||
|
granblue_id: editData.granblueId,
|
||||||
|
name_en: editData.name,
|
||||||
|
name_jp: editData.nameJp,
|
||||||
|
summon_id: editData.summonId || undefined,
|
||||||
|
rarity: editData.rarity,
|
||||||
|
|
||||||
|
// Taxonomy
|
||||||
|
element: editData.element,
|
||||||
|
series: editData.series || undefined,
|
||||||
|
|
||||||
|
// Stats - note: transcendence maps to xlb
|
||||||
|
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,
|
||||||
|
|
||||||
|
// Uncap
|
||||||
|
flb: editData.flb,
|
||||||
|
ulb: editData.ulb,
|
||||||
|
transcendence: editData.transcendence,
|
||||||
|
subaura: editData.subaura,
|
||||||
|
limit: editData.limit,
|
||||||
|
|
||||||
|
// Dates
|
||||||
|
release_date: editData.releaseDate || null,
|
||||||
|
flb_date: editData.flbDate || null,
|
||||||
|
ulb_date: editData.ulbDate || null,
|
||||||
|
transcendence_date: editData.transcendenceDate || null,
|
||||||
|
|
||||||
|
// Links
|
||||||
|
wiki_en: editData.wikiEn,
|
||||||
|
wiki_ja: editData.wikiJa,
|
||||||
|
gamewith: editData.gamewith,
|
||||||
|
kamigame: editData.kamigame,
|
||||||
|
|
||||||
|
// Nicknames
|
||||||
|
nicknames_en: editData.nicknamesEn,
|
||||||
|
nicknames_jp: editData.nicknamesJp
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSummon = await entityAdapter.createSummon(payload)
|
||||||
|
await goto(`/database/summons/${newSummon.id}`)
|
||||||
|
} catch (error) {
|
||||||
|
saveError = 'Failed to create summon. Please try again.'
|
||||||
|
console.error('Create error:', error)
|
||||||
|
} finally {
|
||||||
|
isSaving = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancel() {
|
||||||
|
goto('/database/summons')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
<SidebarHeader title="New Summon">
|
||||||
|
{#snippet leftAccessory()}
|
||||||
|
<Button variant="secondary" size="small" onclick={handleCancel}>Cancel</Button>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet rightAccessory()}
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="small"
|
||||||
|
onclick={createSummon}
|
||||||
|
disabled={!canCreate || isSaving}
|
||||||
|
>
|
||||||
|
{isSaving ? 'Saving...' : 'Save'}
|
||||||
|
</Button>
|
||||||
|
{/snippet}
|
||||||
|
</SidebarHeader>
|
||||||
|
|
||||||
|
{#if saveError}
|
||||||
|
<div class="error-banner">{saveError}</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<section class="details">
|
||||||
|
<DetailsContainer title="Basic Info">
|
||||||
|
<DetailItem label="Granblue ID">
|
||||||
|
<ValidatedInput
|
||||||
|
bind:value={editData.granblueId}
|
||||||
|
placeholder="2040001000"
|
||||||
|
onValidate={validateGranblueId}
|
||||||
|
minLength={10}
|
||||||
|
contained
|
||||||
|
alignRight={false}
|
||||||
|
/>
|
||||||
|
</DetailItem>
|
||||||
|
<DetailItem
|
||||||
|
label="Name (EN)"
|
||||||
|
bind:value={editData.name}
|
||||||
|
editable={true}
|
||||||
|
type="text"
|
||||||
|
placeholder="Summon name"
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Name (JP)"
|
||||||
|
bind:value={editData.nameJp}
|
||||||
|
editable={true}
|
||||||
|
type="text"
|
||||||
|
placeholder="ìšó
"
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Rarity"
|
||||||
|
bind:value={editData.rarity}
|
||||||
|
editable={true}
|
||||||
|
type="select"
|
||||||
|
options={rarityOptions}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Summon ID"
|
||||||
|
sublabel="Internal game identifier (if known)"
|
||||||
|
bind:value={editData.summonId}
|
||||||
|
editable={true}
|
||||||
|
type="text"
|
||||||
|
placeholder="Optional"
|
||||||
|
/>
|
||||||
|
</DetailsContainer>
|
||||||
|
|
||||||
|
<SummonUncapSection summon={emptySummon} {editMode} bind:editData />
|
||||||
|
<SummonTaxonomySection summon={emptySummon} {editMode} bind:editData />
|
||||||
|
<SummonStatsSection summon={emptySummon} {editMode} bind:editData />
|
||||||
|
|
||||||
|
<DetailsContainer title="Nicknames">
|
||||||
|
<DetailItem label="Nicknames (EN)">
|
||||||
|
<TagInput
|
||||||
|
bind:value={editData.nicknamesEn}
|
||||||
|
placeholder="Add nickname..."
|
||||||
|
/>
|
||||||
|
</DetailItem>
|
||||||
|
<DetailItem label="Nicknames (JP)">
|
||||||
|
<TagInput
|
||||||
|
bind:value={editData.nicknamesJp}
|
||||||
|
placeholder="ËïÍüà’ý ..."
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use '$src/themes/colors' as colors;
|
||||||
|
@use '$src/themes/spacing' as spacing;
|
||||||
|
@use '$src/themes/typography' as typography;
|
||||||
|
@use '$src/themes/layout' as layout;
|
||||||
|
|
||||||
|
.page {
|
||||||
|
background: white;
|
||||||
|
border-radius: layout.$card-corner;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
:global(.container) {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-banner {
|
||||||
|
color: colors.$error;
|
||||||
|
font-size: typography.$font-small;
|
||||||
|
padding: spacing.$unit-2x;
|
||||||
|
background: colors.$error--bg--light;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Reference in a new issue