new character page: add all fields, uncap cascade logic, validation
This commit is contained in:
parent
28ad2fb37e
commit
754d5a633c
3 changed files with 335 additions and 189 deletions
|
|
@ -13,19 +13,28 @@
|
||||||
let { character, editMode = false, editData = $bindable() }: Props = $props()
|
let { character, editMode = false, editData = $bindable() }: Props = $props()
|
||||||
|
|
||||||
const flb = $derived(editMode ? Boolean(editData.flb) : Boolean(character?.uncap?.flb))
|
const flb = $derived(editMode ? Boolean(editData.flb) : Boolean(character?.uncap?.flb))
|
||||||
|
const ulb = $derived(editMode ? Boolean(editData.ulb) : Boolean(character?.uncap?.ulb))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DetailsContainer title="HP Stats">
|
<DetailsContainer title="HP Stats">
|
||||||
{#if editMode}
|
{#if editMode}
|
||||||
<DetailItem label="Base HP" bind:value={editData.minHp} editable={true} type="number" placeholder="0" />
|
<DetailItem label="Base HP" bind:value={editData.minHp} editable={true} type="number" placeholder="0" />
|
||||||
<DetailItem label="Max HP" bind:value={editData.maxHp} editable={true} type="number" placeholder="0" />
|
<DetailItem label="Max HP" 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" />
|
<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}
|
||||||
{:else}
|
{:else}
|
||||||
<DetailItem label="Base HP" value={character.hp?.minHp} />
|
<DetailItem label="Base HP" value={character.hp?.minHp} />
|
||||||
<DetailItem label="Max HP" value={character.hp?.maxHp} />
|
<DetailItem label="Max HP" value={character.hp?.maxHp} />
|
||||||
{#if flb}
|
{#if flb}
|
||||||
<DetailItem label="Max HP (FLB)" value={character.hp?.maxHpFlb} />
|
<DetailItem label="Max HP (FLB)" value={character.hp?.maxHpFlb} />
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if ulb}
|
||||||
|
<DetailItem label="Max HP (ULB)" value={character.hp?.maxHpUlb} />
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</DetailsContainer>
|
</DetailsContainer>
|
||||||
|
|
||||||
|
|
@ -33,13 +42,45 @@
|
||||||
{#if editMode}
|
{#if editMode}
|
||||||
<DetailItem label="Base Attack" bind:value={editData.minAtk} editable={true} type="number" placeholder="0" />
|
<DetailItem label="Base Attack" bind:value={editData.minAtk} editable={true} type="number" placeholder="0" />
|
||||||
<DetailItem label="Max Attack" bind:value={editData.maxAtk} editable={true} type="number" placeholder="0" />
|
<DetailItem label="Max Attack" 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" />
|
<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}
|
||||||
{:else}
|
{:else}
|
||||||
<DetailItem label="Base Attack" value={character.atk?.minAtk} />
|
<DetailItem label="Base Attack" value={character.atk?.minAtk} />
|
||||||
<DetailItem label="Max Attack" value={character.atk?.maxAtk} />
|
<DetailItem label="Max Attack" value={character.atk?.maxAtk} />
|
||||||
{#if flb}
|
{#if flb}
|
||||||
<DetailItem label="Max Attack (FLB)" value={character.atk?.maxAtkFlb} />
|
<DetailItem label="Max Attack (FLB)" value={character.atk?.maxAtkFlb} />
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if ulb}
|
||||||
|
<DetailItem label="Max Attack (ULB)" value={character.atk?.maxAtkUlb} />
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</DetailsContainer>
|
||||||
|
|
||||||
|
<DetailsContainer title="Other Stats">
|
||||||
|
{#if editMode}
|
||||||
|
<DetailItem label="Base DA" bind:value={editData.baseDa} editable={true} type="number" placeholder="0" />
|
||||||
|
<DetailItem label="Base TA" bind:value={editData.baseTa} editable={true} type="number" placeholder="0" />
|
||||||
|
<DetailItem label="Ougi Ratio" bind:value={editData.ougiRatio} editable={true} type="number" placeholder="0" />
|
||||||
|
{#if flb}
|
||||||
|
<DetailItem label="Ougi Ratio (FLB)" bind:value={editData.ougiRatioFlb} editable={true} type="number" placeholder="0" />
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
{#if character.baseDa}
|
||||||
|
<DetailItem label="Base DA" value={character.baseDa} />
|
||||||
|
{/if}
|
||||||
|
{#if character.baseTa}
|
||||||
|
<DetailItem label="Base TA" value={character.baseTa} />
|
||||||
|
{/if}
|
||||||
|
{#if character.ougiRatio}
|
||||||
|
<DetailItem label="Ougi Ratio" value={character.ougiRatio} />
|
||||||
|
{/if}
|
||||||
|
{#if character.ougiRatioFlb}
|
||||||
|
<DetailItem label="Ougi Ratio (FLB)" value={character.ougiRatioFlb} />
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</DetailsContainer>
|
</DetailsContainer>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,42 @@
|
||||||
? (label.toLowerCase() as ElementName)
|
? (label.toLowerCase() as ElementName)
|
||||||
: undefined
|
: 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSpecialChange(checked: boolean) {
|
||||||
|
if (checked) {
|
||||||
|
// Special characters (Story SRs) don't have standard uncap levels
|
||||||
|
editData.flb = false
|
||||||
|
editData.ulb = false
|
||||||
|
editData.transcendence = false
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DetailsContainer title="Uncap">
|
<DetailsContainer title="Uncap">
|
||||||
|
|
@ -56,10 +92,31 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if editMode}
|
{#if editMode}
|
||||||
<DetailItem label="FLB" bind:value={editData.flb} editable={true} type="checkbox" element={elementName} />
|
<DetailItem label="FLB" bind:value={editData.flb} editable={true} type="checkbox" element={elementName} onchange={handleFlbChange} />
|
||||||
<DetailItem label="ULB" bind:value={editData.ulb} editable={true} type="checkbox" element={elementName} />
|
<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} />
|
<DetailItem label="Transcendence" bind:value={editData.transcendence} editable={true} type="checkbox" element={elementName} onchange={handleTranscendenceChange} />
|
||||||
<DetailItem label="Special" bind:value={editData.special} editable={true} type="checkbox" element={elementName} />
|
<div class="special-field">
|
||||||
|
<DetailItem label="Special" bind:value={editData.special} editable={true} type="checkbox" element={elementName} onchange={handleSpecialChange} />
|
||||||
|
<p class="special-note">This is for Story SRs. Don't check this unless something really weird happens.</p>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</DetailsContainer>
|
</DetailsContainer>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use '$src/themes/colors' as colors;
|
||||||
|
@use '$src/themes/spacing' as spacing;
|
||||||
|
@use '$src/themes/typography' as typography;
|
||||||
|
|
||||||
|
.special-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.special-note {
|
||||||
|
font-size: typography.$font-small;
|
||||||
|
color: colors.$grey-50;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 spacing.$unit spacing.$unit;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,16 +5,16 @@
|
||||||
import { goto } from '$app/navigation'
|
import { goto } from '$app/navigation'
|
||||||
|
|
||||||
// Components
|
// 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 CharacterUncapSection from '$lib/features/database/characters/sections/CharacterUncapSection.svelte'
|
||||||
import CharacterTaxonomySection from '$lib/features/database/characters/sections/CharacterTaxonomySection.svelte'
|
import CharacterTaxonomySection from '$lib/features/database/characters/sections/CharacterTaxonomySection.svelte'
|
||||||
import CharacterStatsSection from '$lib/features/database/characters/sections/CharacterStatsSection.svelte'
|
import CharacterStatsSection from '$lib/features/database/characters/sections/CharacterStatsSection.svelte'
|
||||||
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 SidebarHeader from '$lib/components/ui/SidebarHeader.svelte'
|
||||||
import Button from '$lib/components/ui/Button.svelte'
|
import Button from '$lib/components/ui/Button.svelte'
|
||||||
import { getCharacterImage } from '$lib/utils/images'
|
import ValidatedInput from '$lib/components/ui/ValidatedInput.svelte'
|
||||||
import { entityAdapter } from '$lib/api/adapters/entity.adapter'
|
import { entityAdapter } from '$lib/api/adapters/entity.adapter'
|
||||||
|
import { getRarityOptions } from '$lib/utils/rarity'
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import type { PageData } from './$types'
|
import type { PageData } from './$types'
|
||||||
|
|
@ -23,38 +23,20 @@
|
||||||
|
|
||||||
// Always in edit mode for new character
|
// Always in edit mode for new character
|
||||||
const editMode = true
|
const editMode = true
|
||||||
const canEdit = true
|
|
||||||
|
|
||||||
let isSaving = $state(false)
|
let isSaving = $state(false)
|
||||||
let saveError = $state<string | null>(null)
|
let saveError = $state<string | null>(null)
|
||||||
let saveSuccess = $state(false)
|
|
||||||
|
|
||||||
// Validation state
|
// Validation state for canCreate check
|
||||||
let isValidating = $state(false)
|
let granblueIdValid = $state(false)
|
||||||
let validationError = $state<string | null>(null)
|
let granblueIdExistsInDb = $state(false)
|
||||||
let validationResult = $state<{
|
|
||||||
valid: boolean
|
|
||||||
existsInDb: boolean
|
|
||||||
imageUrls?: { main?: string; grid?: string; square?: string }
|
|
||||||
} | null>(null)
|
|
||||||
|
|
||||||
// Download state
|
|
||||||
let isDownloading = $state(false)
|
|
||||||
let downloadStatus = $state<{
|
|
||||||
status: string
|
|
||||||
progress: number
|
|
||||||
imagesDownloaded?: number
|
|
||||||
imagesTotal?: number
|
|
||||||
error?: string
|
|
||||||
} | null>(null)
|
|
||||||
let downloadPollingInterval = $state<ReturnType<typeof setInterval> | null>(null)
|
|
||||||
|
|
||||||
// Empty character for new creation
|
// Empty character for new creation
|
||||||
const emptyCharacter = {
|
const emptyCharacter = {
|
||||||
id: '',
|
id: '',
|
||||||
name: { en: '', jp: '' },
|
name: { en: '', jp: '' },
|
||||||
granblueId: '',
|
granblueId: '',
|
||||||
characterId: null,
|
characterId: '',
|
||||||
rarity: 3,
|
rarity: 3,
|
||||||
element: 0,
|
element: 0,
|
||||||
race: [],
|
race: [],
|
||||||
|
|
@ -68,70 +50,97 @@
|
||||||
|
|
||||||
// Editable fields
|
// Editable fields
|
||||||
let editData = $state({
|
let editData = $state({
|
||||||
|
// Basic Info
|
||||||
name: '',
|
name: '',
|
||||||
|
nameJp: '',
|
||||||
granblueId: '',
|
granblueId: '',
|
||||||
characterId: null as number | null,
|
characterId: '', // Comma-separated for dual/trio units (e.g., "123, 456")
|
||||||
rarity: 3,
|
rarity: 3,
|
||||||
|
|
||||||
|
// Taxonomy
|
||||||
element: 0,
|
element: 0,
|
||||||
race1: null as number | null,
|
race1: null as number | null,
|
||||||
race2: null as number | null,
|
race2: null as number | null,
|
||||||
gender: 0,
|
gender: 0,
|
||||||
proficiency1: 0,
|
proficiency1: 0,
|
||||||
proficiency2: 0,
|
proficiency2: 0,
|
||||||
|
|
||||||
|
// Stats
|
||||||
minHp: 0,
|
minHp: 0,
|
||||||
maxHp: 0,
|
maxHp: 0,
|
||||||
maxHpFlb: 0,
|
maxHpFlb: 0,
|
||||||
|
maxHpUlb: 0,
|
||||||
minAtk: 0,
|
minAtk: 0,
|
||||||
maxAtk: 0,
|
maxAtk: 0,
|
||||||
maxAtkFlb: 0,
|
maxAtkFlb: 0,
|
||||||
|
maxAtkUlb: 0,
|
||||||
|
baseDa: 0,
|
||||||
|
baseTa: 0,
|
||||||
|
ougiRatio: 0,
|
||||||
|
ougiRatioFlb: 0,
|
||||||
|
|
||||||
|
// Uncap
|
||||||
flb: false,
|
flb: false,
|
||||||
ulb: false,
|
ulb: false,
|
||||||
transcendence: false,
|
special: false,
|
||||||
special: false
|
|
||||||
|
// Dates
|
||||||
|
releaseDate: '',
|
||||||
|
flbDate: '',
|
||||||
|
ulbDate: '',
|
||||||
|
|
||||||
|
// Links
|
||||||
|
wikiEn: '',
|
||||||
|
wikiJa: '',
|
||||||
|
gamewith: '',
|
||||||
|
kamigame: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const rarityOptions = getRarityOptions()
|
||||||
|
|
||||||
// Validation is required before create
|
// Validation is required before create
|
||||||
const canCreate = $derived(
|
const canCreate = $derived(
|
||||||
validationResult?.valid === true &&
|
granblueIdValid &&
|
||||||
!validationResult?.existsInDb &&
|
!granblueIdExistsInDb &&
|
||||||
editData.name.trim() !== '' &&
|
editData.name.trim() !== '' &&
|
||||||
editData.granblueId.trim() !== ''
|
editData.granblueId.trim() !== ''
|
||||||
)
|
)
|
||||||
|
|
||||||
// Get preview image from validation or placeholder
|
async function validateGranblueId(value: string): Promise<{ valid: boolean; message: string }> {
|
||||||
const previewImage = $derived(
|
console.log('[+page] validateGranblueId called with:', value)
|
||||||
validationResult?.imageUrls?.grid || getCharacterImage(editData.granblueId, 'grid', '01')
|
|
||||||
)
|
|
||||||
|
|
||||||
async function validateGranblueId() {
|
if (!value || value.length !== 10) {
|
||||||
if (!editData.granblueId || editData.granblueId.length !== 10) {
|
console.log('[+page] Invalid length, returning early')
|
||||||
validationError = 'Granblue ID must be exactly 10 digits'
|
granblueIdValid = false
|
||||||
validationResult = null
|
granblueIdExistsInDb = false
|
||||||
return
|
return { valid: false, message: 'Granblue ID must be exactly 10 digits' }
|
||||||
}
|
}
|
||||||
|
|
||||||
isValidating = true
|
|
||||||
validationError = null
|
|
||||||
validationResult = null
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await entityAdapter.validateCharacterGranblueId(editData.granblueId)
|
console.log('[+page] Calling entityAdapter.validateCharacterGranblueId...')
|
||||||
validationResult = {
|
const result = await entityAdapter.validateCharacterGranblueId(value)
|
||||||
valid: result.valid,
|
console.log('[+page] API result:', result)
|
||||||
existsInDb: result.existsInDb,
|
|
||||||
imageUrls: result.imageUrls
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!result.valid) {
|
if (!result.valid) {
|
||||||
validationError = result.error || 'Invalid Granblue ID'
|
granblueIdValid = false
|
||||||
} else if (result.existsInDb) {
|
granblueIdExistsInDb = false
|
||||||
validationError = 'A character with this Granblue ID already exists'
|
return { valid: false, message: result.error || 'Invalid Granblue ID' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (result.existsInDb) {
|
||||||
|
granblueIdValid = true
|
||||||
|
granblueIdExistsInDb = true
|
||||||
|
return { valid: false, message: 'A character with this Granblue ID already exists' }
|
||||||
|
}
|
||||||
|
|
||||||
|
granblueIdValid = true
|
||||||
|
granblueIdExistsInDb = false
|
||||||
|
return { valid: true, message: 'Valid Granblue ID - images found on server' }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
validationError = 'Failed to validate Granblue ID'
|
granblueIdValid = false
|
||||||
console.error('Validation error:', error)
|
granblueIdExistsInDb = false
|
||||||
} finally {
|
console.error('[+page] Validation error:', error)
|
||||||
isValidating = false
|
return { valid: false, message: 'Failed to validate Granblue ID' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -140,37 +149,62 @@
|
||||||
|
|
||||||
isSaving = true
|
isSaving = true
|
||||||
saveError = null
|
saveError = null
|
||||||
saveSuccess = false
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Prepare the data for API
|
|
||||||
const payload = {
|
const payload = {
|
||||||
|
// Basic Info
|
||||||
granblue_id: editData.granblueId,
|
granblue_id: editData.granblueId,
|
||||||
name_en: editData.name,
|
name_en: editData.name,
|
||||||
name_jp: '', // Can be added later
|
name_jp: editData.nameJp,
|
||||||
|
character_id:
|
||||||
|
editData.characterId.trim() === ''
|
||||||
|
? []
|
||||||
|
: editData.characterId
|
||||||
|
.split(',')
|
||||||
|
.map((id) => Number(id.trim()))
|
||||||
|
.filter((id) => !isNaN(id)),
|
||||||
rarity: editData.rarity,
|
rarity: editData.rarity,
|
||||||
|
|
||||||
|
// Taxonomy
|
||||||
element: editData.element,
|
element: editData.element,
|
||||||
race1: editData.race1,
|
race1: editData.race1,
|
||||||
race2: editData.race2,
|
race2: editData.race2,
|
||||||
gender: editData.gender,
|
gender: editData.gender,
|
||||||
proficiency1: editData.proficiency1,
|
proficiency1: editData.proficiency1,
|
||||||
proficiency2: editData.proficiency2,
|
proficiency2: editData.proficiency2,
|
||||||
|
|
||||||
|
// Stats
|
||||||
min_hp: editData.minHp,
|
min_hp: editData.minHp,
|
||||||
max_hp: editData.maxHp,
|
max_hp: editData.maxHp,
|
||||||
max_hp_flb: editData.maxHpFlb,
|
max_hp_flb: editData.maxHpFlb,
|
||||||
|
max_hp_ulb: editData.maxHpUlb,
|
||||||
min_atk: editData.minAtk,
|
min_atk: editData.minAtk,
|
||||||
max_atk: editData.maxAtk,
|
max_atk: editData.maxAtk,
|
||||||
max_atk_flb: editData.maxAtkFlb,
|
max_atk_flb: editData.maxAtkFlb,
|
||||||
|
max_atk_ulb: editData.maxAtkUlb,
|
||||||
|
base_da: editData.baseDa,
|
||||||
|
base_ta: editData.baseTa,
|
||||||
|
ougi_ratio: editData.ougiRatio,
|
||||||
|
ougi_ratio_flb: editData.ougiRatioFlb,
|
||||||
|
|
||||||
|
// Uncap
|
||||||
flb: editData.flb,
|
flb: editData.flb,
|
||||||
ulb: editData.ulb,
|
ulb: editData.ulb,
|
||||||
special: editData.special
|
special: editData.special,
|
||||||
|
|
||||||
|
// Dates
|
||||||
|
release_date: editData.releaseDate || null,
|
||||||
|
flb_date: editData.flbDate || null,
|
||||||
|
ulb_date: editData.ulbDate || null,
|
||||||
|
|
||||||
|
// Links
|
||||||
|
wiki_en: editData.wikiEn,
|
||||||
|
wiki_ja: editData.wikiJa,
|
||||||
|
gamewith: editData.gamewith,
|
||||||
|
kamigame: editData.kamigame
|
||||||
}
|
}
|
||||||
|
|
||||||
const newCharacter = await entityAdapter.createCharacter(payload)
|
const newCharacter = await entityAdapter.createCharacter(payload)
|
||||||
|
|
||||||
saveSuccess = true
|
|
||||||
|
|
||||||
// Redirect to the new character's page
|
|
||||||
await goto(`/database/characters/${newCharacter.id}`)
|
await goto(`/database/characters/${newCharacter.id}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
saveError = 'Failed to create character. Please try again.'
|
saveError = 'Failed to create character. Please try again.'
|
||||||
|
|
@ -180,74 +214,44 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startImageDownload() {
|
|
||||||
if (!validationResult?.valid || validationResult.existsInDb) return
|
|
||||||
|
|
||||||
// Note: This would need a character ID, so it would happen after creation
|
|
||||||
// For now, we'll skip this and implement it in the edit page
|
|
||||||
// The user can download images after creating the character
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCancel() {
|
function handleCancel() {
|
||||||
goto('/database/characters')
|
goto('/database/characters')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div class="page">
|
||||||
<DetailScaffold
|
<SidebarHeader title="New Character">
|
||||||
type="character"
|
{#snippet leftAccessory()}
|
||||||
item={{ ...emptyCharacter, name: { en: editData.name || 'New Character', jp: '' } }}
|
<Button variant="secondary" size="small" onclick={handleCancel}>Cancel</Button>
|
||||||
image={previewImage}
|
{/snippet}
|
||||||
showEdit={false}
|
{#snippet rightAccessory()}
|
||||||
editMode={true}
|
|
||||||
{isSaving}
|
|
||||||
{saveSuccess}
|
|
||||||
{saveError}
|
|
||||||
onSave={createCharacter}
|
|
||||||
onCancel={handleCancel}
|
|
||||||
>
|
|
||||||
<section class="details">
|
|
||||||
<!-- Granblue ID Validation Section -->
|
|
||||||
<DetailsContainer title="Granblue ID Validation">
|
|
||||||
<div class="validation-section">
|
|
||||||
<div class="validation-input">
|
|
||||||
<DetailItem
|
|
||||||
label="Granblue ID"
|
|
||||||
bind:value={editData.granblueId}
|
|
||||||
editable={true}
|
|
||||||
type="text"
|
|
||||||
placeholder="e.g., 3040001000"
|
|
||||||
/>
|
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="small"
|
size="small"
|
||||||
onclick={validateGranblueId}
|
onclick={createCharacter}
|
||||||
disabled={isValidating || !editData.granblueId}
|
disabled={!canCreate || isSaving}
|
||||||
>
|
>
|
||||||
{isValidating ? 'Validating...' : 'Validate'}
|
{isSaving ? 'Saving...' : 'Save'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
{/snippet}
|
||||||
|
</SidebarHeader>
|
||||||
|
|
||||||
{#if validationError}
|
{#if saveError}
|
||||||
<div class="validation-error">{validationError}</div>
|
<div class="error-banner">{saveError}</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if validationResult?.valid && !validationResult.existsInDb}
|
<section class="details">
|
||||||
<div class="validation-success">
|
|
||||||
Valid Granblue ID - images found on server
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if validationResult?.existsInDb}
|
|
||||||
<div class="validation-warning">
|
|
||||||
A character with this ID already exists in the database
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</DetailsContainer>
|
|
||||||
|
|
||||||
<!-- Basic Info -->
|
|
||||||
<DetailsContainer title="Basic Info">
|
<DetailsContainer title="Basic Info">
|
||||||
|
<DetailItem label="Granblue ID">
|
||||||
|
<ValidatedInput
|
||||||
|
bind:value={editData.granblueId}
|
||||||
|
placeholder="3040001000"
|
||||||
|
onValidate={validateGranblueId}
|
||||||
|
minLength={10}
|
||||||
|
contained
|
||||||
|
alignRight={false}
|
||||||
|
/>
|
||||||
|
</DetailItem>
|
||||||
<DetailItem
|
<DetailItem
|
||||||
label="Name (EN)"
|
label="Name (EN)"
|
||||||
bind:value={editData.name}
|
bind:value={editData.name}
|
||||||
|
|
@ -255,80 +259,124 @@
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Character name"
|
placeholder="Character 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="Character ID"
|
||||||
|
sublabel="Separate multiple IDs with commas (e.g. 123, 456)"
|
||||||
|
bind:value={editData.characterId}
|
||||||
|
editable={true}
|
||||||
|
type="text"
|
||||||
|
placeholder="Character IDs"
|
||||||
|
/>
|
||||||
</DetailsContainer>
|
</DetailsContainer>
|
||||||
|
|
||||||
<CharacterMetadataSection character={emptyCharacter} {editMode} bind:editData />
|
|
||||||
<CharacterUncapSection character={emptyCharacter} {editMode} bind:editData />
|
<CharacterUncapSection character={emptyCharacter} {editMode} bind:editData />
|
||||||
<CharacterTaxonomySection character={emptyCharacter} {editMode} bind:editData />
|
<CharacterTaxonomySection character={emptyCharacter} {editMode} bind:editData />
|
||||||
<CharacterStatsSection character={emptyCharacter} {editMode} bind:editData />
|
<CharacterStatsSection character={emptyCharacter} {editMode} bind:editData />
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
<DetailsContainer title="Dates">
|
||||||
<div class="action-buttons">
|
<DetailItem
|
||||||
<Button variant="secondary" onclick={handleCancel}>Cancel</Button>
|
label="Release Date"
|
||||||
<Button variant="primary" onclick={createCharacter} disabled={!canCreate || isSaving}>
|
bind:value={editData.releaseDate}
|
||||||
{isSaving ? 'Creating...' : 'Create Character'}
|
editable={true}
|
||||||
</Button>
|
type="text"
|
||||||
</div>
|
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}
|
||||||
|
</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>
|
</section>
|
||||||
</DetailScaffold>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@use '$src/themes/colors' as colors;
|
@use '$src/themes/colors' as colors;
|
||||||
@use '$src/themes/spacing' as spacing;
|
@use '$src/themes/spacing' as spacing;
|
||||||
@use '$src/themes/typography' as typography;
|
@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 {
|
.details {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
|
||||||
|
|
||||||
.validation-section {
|
:global(.container) {
|
||||||
display: flex;
|
border-bottom: none;
|
||||||
flex-direction: column;
|
|
||||||
gap: spacing.$unit;
|
|
||||||
padding: spacing.$unit-2x;
|
|
||||||
}
|
|
||||||
|
|
||||||
.validation-input {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
gap: spacing.$unit-2x;
|
|
||||||
|
|
||||||
:global(.detail-item) {
|
|
||||||
flex: 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.validation-error {
|
.error-banner {
|
||||||
color: colors.$error;
|
color: colors.$error;
|
||||||
font-size: typography.$font-small;
|
font-size: typography.$font-small;
|
||||||
padding: spacing.$unit;
|
|
||||||
background: colors.$error--bg--light;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.validation-success {
|
|
||||||
color: colors.$wind-text-20;
|
|
||||||
font-size: typography.$font-small;
|
|
||||||
padding: spacing.$unit;
|
|
||||||
background: colors.$wind-bg-20;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.validation-warning {
|
|
||||||
color: colors.$orange-40;
|
|
||||||
font-size: typography.$font-small;
|
|
||||||
padding: spacing.$unit;
|
|
||||||
background: colors.$orange-90;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-buttons {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: spacing.$unit-2x;
|
|
||||||
padding: spacing.$unit-2x;
|
padding: spacing.$unit-2x;
|
||||||
border-top: 1px solid colors.$grey-80;
|
background: colors.$error--bg--light;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue