reconcile character detail and edit pages

- same section order on both pages
- show all fields (empty shows dash)
- add editable nicknames/links to edit page
- handle CharacterSeriesRef[] -> number[] conversion
This commit is contained in:
Justin Edmund 2025-12-15 12:49:00 -08:00
parent 32c4880180
commit 9243d133cd
2 changed files with 222 additions and 75 deletions

View file

@ -21,7 +21,6 @@
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 CharacterGachaSection from '$lib/features/database/characters/sections/CharacterGachaSection.svelte'
import CharacterStatsSection from '$lib/features/database/characters/sections/CharacterStatsSection.svelte'
import EntityImagesTab from '$lib/features/database/detail/tabs/EntityImagesTab.svelte'
import EntityRawDataTab from '$lib/features/database/detail/tabs/EntityRawDataTab.svelte'
@ -180,81 +179,83 @@
{#if currentTab === 'info'}
<section class="details">
<CharacterMetadataSection {character} />
{#if character.nicknames?.en?.length || character.nicknames?.ja?.length}
<DetailsContainer title="Nicknames">
{#if character.nicknames?.en?.length}
<DetailItem label="English">
<div class="nickname-tags">
{#each character.nicknames.en as nickname}
<span class="nickname-tag">{nickname}</span>
{/each}
</div>
</DetailItem>
{/if}
{#if character.nicknames?.ja?.length}
<DetailItem label="Japanese">
<div class="nickname-tags">
{#each character.nicknames.ja as nickname}
<span class="nickname-tag">{nickname}</span>
{/each}
</div>
</DetailItem>
{/if}
</DetailsContainer>
{/if}
<CharacterUncapSection {character} />
<CharacterTaxonomySection {character} />
<CharacterGachaSection {character} />
<CharacterStatsSection {character} />
{#if character.releaseDate || character.flbDate || character.ulbDate}
<DetailsContainer title="Dates">
{#if character.releaseDate}
<DetailItem label="Release Date" value={character.releaseDate} />
<DetailsContainer title="Nicknames">
<DetailItem label="Nicknames (EN)">
{#if character.nicknames?.en?.length}
<div class="nickname-tags">
{#each character.nicknames.en as nickname}
<span class="nickname-tag">{nickname}</span>
{/each}
</div>
{:else}
<span class="empty-value"></span>
{/if}
{#if character.flbDate}
<DetailItem label="FLB Date" value={character.flbDate} />
</DetailItem>
<DetailItem label="Nicknames (JP)">
{#if character.nicknames?.ja?.length}
<div class="nickname-tags">
{#each character.nicknames.ja as nickname}
<span class="nickname-tag">{nickname}</span>
{/each}
</div>
{:else}
<span class="empty-value"></span>
{/if}
{#if character.ulbDate}
<DetailItem label="ULB Date" value={character.ulbDate} />
{/if}
</DetailsContainer>
{/if}
</DetailItem>
</DetailsContainer>
{#if character.links?.wikiEn || character.links?.wikiJa || character.links?.gamewith || character.links?.kamigame}
<DetailsContainer title="Links">
{#if character.links?.wikiEn}
<DetailItem label="Wiki (EN)">
<a href={character.links.wikiEn} target="_blank" rel="noopener noreferrer" class="external-link">
{character.links.wikiEn}
</a>
</DetailItem>
<DetailsContainer title="Dates">
<DetailItem label="Release Date" value={character.releaseDate || '—'} />
{#if character.uncap?.flb}
<DetailItem label="FLB Date" value={character.flbDate || '—'} />
{/if}
{#if character.uncap?.ulb}
<DetailItem label="ULB Date" value={character.ulbDate || '—'} />
{/if}
</DetailsContainer>
<DetailsContainer title="Links">
<DetailItem label="Wiki (EN)">
{#if character.wiki?.en}
<a href={character.wiki.en} target="_blank" rel="noopener noreferrer" class="external-link">
{character.wiki.en}
</a>
{:else}
<span class="empty-value"></span>
{/if}
{#if character.links?.wikiJa}
<DetailItem label="Wiki (JP)">
<a href={character.links.wikiJa} target="_blank" rel="noopener noreferrer" class="external-link">
{character.links.wikiJa}
</a>
</DetailItem>
</DetailItem>
<DetailItem label="Wiki (JP)">
{#if character.wiki?.ja}
<a href={character.wiki.ja} target="_blank" rel="noopener noreferrer" class="external-link">
{character.wiki.ja}
</a>
{:else}
<span class="empty-value"></span>
{/if}
{#if character.links?.gamewith}
<DetailItem label="Gamewith">
<a href={character.links.gamewith} target="_blank" rel="noopener noreferrer" class="external-link">
{character.links.gamewith}
</a>
</DetailItem>
</DetailItem>
<DetailItem label="Gamewith">
{#if character.gamewith}
<a href={character.gamewith} target="_blank" rel="noopener noreferrer" class="external-link">
{character.gamewith}
</a>
{:else}
<span class="empty-value"></span>
{/if}
{#if character.links?.kamigame}
<DetailItem label="Kamigame">
<a href={character.links.kamigame} target="_blank" rel="noopener noreferrer" class="external-link">
{character.links.kamigame}
</a>
</DetailItem>
</DetailItem>
<DetailItem label="Kamigame">
{#if character.kamigame}
<a href={character.kamigame} target="_blank" rel="noopener noreferrer" class="external-link">
{character.kamigame}
</a>
{:else}
<span class="empty-value"></span>
{/if}
</DetailsContainer>
{/if}
</DetailItem>
</DetailsContainer>
{#if relatedQuery.data?.length}
<DetailsContainer title="Related Units">
@ -402,4 +403,8 @@
text-decoration: underline;
}
}
.empty-value {
color: colors.$grey-50;
}
</style>

View file

@ -19,15 +19,21 @@
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 CharacterGachaSection from '$lib/features/database/characters/sections/CharacterGachaSection.svelte'
import CharacterStatsSection from '$lib/features/database/characters/sections/CharacterStatsSection.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 { getCharacterImage } from '$lib/utils/images'
import { CHARACTER_SERIES_NAMES } from '$lib/types/enums'
// Types
import type { PageData } from './$types'
// Create reverse mapping from series name to integer
const SERIES_NAME_TO_INT: Record<string, number> = Object.fromEntries(
Object.entries(CHARACTER_SERIES_NAMES).map(([key, name]) => [name, Number(key)])
)
let { data }: { data: PageData } = $props()
const queryClient = useQueryClient()
@ -52,7 +58,7 @@
let editData = $state({
name: '',
granblueId: '',
characterId: null as number | null,
characterId: '', // Comma-separated string for dual/trio units
rarity: 1,
element: 0,
race1: null as number | null,
@ -60,28 +66,68 @@
gender: 0,
proficiency1: 0,
proficiency2: 0,
season: 0,
series: [] as number[],
// HP stats
minHp: 0,
maxHp: 0,
maxHpFlb: 0,
maxHpUlb: 0,
// Attack stats
minAtk: 0,
maxAtk: 0,
maxAtkFlb: 0,
maxAtkUlb: 0,
// Other stats
baseDa: 0,
baseTa: 0,
ougiRatio: 0,
ougiRatioFlb: 0,
// Uncap flags
flb: false,
ulb: false,
transcendence: false,
special: false,
// Dates
releaseDate: '',
flbDate: '',
ulbDate: ''
ulbDate: '',
// Nicknames
nicknamesEn: [] as string[],
nicknamesJp: [] as string[],
// Links
wikiEn: '',
wikiJa: '',
gamewith: '',
kamigame: ''
})
// Helper to convert series to number array (handles both legacy integer and object formats)
function seriesAsNumbers(
series: number[] | Array<{ id: string; name?: { en?: string } }> | undefined
): number[] {
if (!series || series.length === 0) return []
// Check if first element is an object (CharacterSeriesRef) or number
const first = series[0]
if (typeof first === 'object' && first !== null && 'id' in first) {
// It's CharacterSeriesRef[] - convert using name.en to look up enum value
return (series as Array<{ id: string; name?: { en?: string } }>)
.map((s) => {
const name = s.name?.en
return name ? SERIES_NAME_TO_INT[name] : undefined
})
.filter((n): n is number => n !== undefined)
}
return series as number[]
}
// Populate edit data when character loads
$effect(() => {
if (character) {
editData = {
name: character.name?.en || '',
granblueId: character.granblueId || '',
characterId: character.characterId ?? null,
characterId: character.characterId?.join(', ') || '',
rarity: character.rarity || 1,
element: character.element || 0,
race1: character.race?.[0] ?? null,
@ -89,19 +135,40 @@
gender: character.gender || 0,
proficiency1: character.proficiency?.[0] || 0,
proficiency2: character.proficiency?.[1] || 0,
season: character.season || 0,
series: seriesAsNumbers(character.series),
// HP stats
minHp: character.hp?.minHp || 0,
maxHp: character.hp?.maxHp || 0,
maxHpFlb: character.hp?.maxHpFlb || 0,
maxHpUlb: character.hp?.maxHpUlb || 0,
// Attack stats
minAtk: character.atk?.minAtk || 0,
maxAtk: character.atk?.maxAtk || 0,
maxAtkFlb: character.atk?.maxAtkFlb || 0,
maxAtkUlb: character.atk?.maxAtkUlb || 0,
// Other stats
baseDa: character.baseDa || 0,
baseTa: character.baseTa || 0,
ougiRatio: character.ougiRatio?.ougiRatio || 0,
ougiRatioFlb: character.ougiRatio?.ougiRatioFlb || 0,
// Uncap flags
flb: character.uncap?.flb || false,
ulb: character.uncap?.ulb || false,
transcendence: character.uncap?.transcendence || false,
special: character.special || false,
// Dates
releaseDate: character.releaseDate || '',
flbDate: character.flbDate || '',
ulbDate: character.ulbDate || ''
ulbDate: character.ulbDate || '',
// Nicknames
nicknamesEn: character.nicknames?.en || [],
nicknamesJp: character.nicknames?.ja || [],
// Links
wikiEn: character.wiki?.en || '',
wikiJa: character.wiki?.ja || '',
gamewith: character.gamewith || '',
kamigame: character.kamigame || ''
}
}
})
@ -117,7 +184,13 @@
const payload = {
name_en: editData.name,
granblue_id: editData.granblueId,
character_id: editData.characterId ? [editData.characterId] : [],
character_id:
editData.characterId.trim() === ''
? []
: editData.characterId
.split(',')
.map((id) => Number(id.trim()))
.filter((id) => !isNaN(id)),
rarity: editData.rarity,
element: editData.element,
race1: editData.race1,
@ -125,18 +198,40 @@
gender: editData.gender,
proficiency1: editData.proficiency1,
proficiency2: editData.proficiency2,
season: editData.season || undefined,
series: editData.series,
// HP stats
min_hp: editData.minHp,
max_hp: editData.maxHp,
max_hp_flb: editData.maxHpFlb,
max_hp_ulb: editData.maxHpUlb,
// Attack stats
min_atk: editData.minAtk,
max_atk: editData.maxAtk,
max_atk_flb: editData.maxAtkFlb,
max_atk_ulb: editData.maxAtkUlb,
// Other stats
base_da: editData.baseDa,
base_ta: editData.baseTa,
ougi_ratio: editData.ougiRatio,
ougi_ratio_flb: editData.ougiRatioFlb,
// Uncap flags
flb: editData.flb,
ulb: editData.ulb,
transcendence: editData.transcendence,
special: editData.special,
release_date: editData.releaseDate || null,
flb_date: editData.flbDate || null,
ulb_date: editData.ulbDate || null
// Dates
release_date: editData.releaseDate || undefined,
flb_date: editData.flbDate || undefined,
ulb_date: editData.ulbDate || undefined,
// Nicknames
nicknames_en: editData.nicknamesEn,
nicknames_jp: editData.nicknamesJp,
// Links
wiki_en: editData.wikiEn || undefined,
wiki_ja: editData.wikiJa || undefined,
gamewith: editData.gamewith || undefined,
kamigame: editData.kamigame || undefined
}
await entityAdapter.updateCharacter(character.id, payload)
@ -186,9 +281,21 @@
<CharacterMetadataSection {character} {editMode} bind:editData />
<CharacterUncapSection {character} {editMode} bind:editData />
<CharacterTaxonomySection {character} {editMode} bind:editData />
<CharacterGachaSection {character} {editMode} bind:editData />
<CharacterStatsSection {character} {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"
@ -213,6 +320,41 @@
/>
{/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}