refactor character section components
- move series from taxonomy to metadata section - rename uncap label to "Uncap Level" - show all uncap flags in view mode
This commit is contained in:
parent
34c3dd6aa6
commit
32c4880180
3 changed files with 231 additions and 183 deletions
|
|
@ -7,9 +7,10 @@
|
||||||
import SuggestionDetailItem from '$lib/components/ui/SuggestionDetailItem.svelte'
|
import SuggestionDetailItem from '$lib/components/ui/SuggestionDetailItem.svelte'
|
||||||
import CopyableText from '$lib/components/ui/CopyableText.svelte'
|
import CopyableText from '$lib/components/ui/CopyableText.svelte'
|
||||||
import Select from '$lib/components/ui/Select.svelte'
|
import Select from '$lib/components/ui/Select.svelte'
|
||||||
|
import MultiSelect from '$lib/components/ui/MultiSelect.svelte'
|
||||||
import { getRarityLabel, getRarityOptions } from '$lib/utils/rarity'
|
import { getRarityLabel, getRarityOptions } from '$lib/utils/rarity'
|
||||||
import { getWeaponImage } from '$lib/utils/images'
|
import { getWeaponImage } from '$lib/utils/images'
|
||||||
import { CHARACTER_SEASON_NAMES, getSeasonName } from '$lib/types/enums'
|
import { CHARACTER_SEASON_NAMES, CHARACTER_SERIES_NAMES, getSeasonName, getSeriesNames } from '$lib/types/enums'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
character: any
|
character: any
|
||||||
|
|
@ -43,10 +44,36 @@
|
||||||
}))
|
}))
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// Series options for multiselect
|
||||||
|
const seriesOptions = Object.entries(CHARACTER_SERIES_NAMES).map(([value, label]) => ({
|
||||||
|
value: Number(value),
|
||||||
|
label
|
||||||
|
}))
|
||||||
|
|
||||||
function formatPromotions(promotionNames: string[] | undefined): string {
|
function formatPromotions(promotionNames: string[] | undefined): string {
|
||||||
if (!promotionNames || promotionNames.length === 0) return '—'
|
if (!promotionNames || promotionNames.length === 0) return '—'
|
||||||
return promotionNames.join(', ')
|
return promotionNames.join(', ')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Format series for display - use API-provided seriesNames if available
|
||||||
|
function formatSeriesDisplay(): string {
|
||||||
|
// Use pre-computed seriesNames from API if available
|
||||||
|
if (character.seriesNames && character.seriesNames.length > 0) {
|
||||||
|
return character.seriesNames.join(', ')
|
||||||
|
}
|
||||||
|
// Fallback for legacy integer array
|
||||||
|
if (Array.isArray(character.series) && character.series.length > 0) {
|
||||||
|
const first = character.series[0]
|
||||||
|
if (typeof first === 'number') {
|
||||||
|
return getSeriesNames(character.series as number[]).join(', ')
|
||||||
|
}
|
||||||
|
// CharacterSeriesRef[] - extract names
|
||||||
|
return (character.series as Array<{ name: { en?: string } }>)
|
||||||
|
.map((s) => s.name?.en || 'Unknown')
|
||||||
|
.join(', ')
|
||||||
|
}
|
||||||
|
return '—'
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DetailsContainer title="Metadata">
|
<DetailsContainer title="Metadata">
|
||||||
|
|
@ -62,6 +89,47 @@
|
||||||
onAcceptSuggestion={() => onAcceptSuggestion?.('rarity', suggestions?.rarity)}
|
onAcceptSuggestion={() => onAcceptSuggestion?.('rarity', suggestions?.rarity)}
|
||||||
onDismissSuggestion={() => onDismissSuggestion?.('rarity')}
|
onDismissSuggestion={() => onDismissSuggestion?.('rarity')}
|
||||||
/>
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Granblue ID"
|
||||||
|
bind:value={editData.granblueId}
|
||||||
|
editable={true}
|
||||||
|
type="text"
|
||||||
|
placeholder="Granblue ID"
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Character ID"
|
||||||
|
sublabel="Separate multiple IDs with commas"
|
||||||
|
bind:value={editData.characterId}
|
||||||
|
editable={true}
|
||||||
|
type="text"
|
||||||
|
placeholder="Character IDs"
|
||||||
|
/>
|
||||||
|
{#if character.recruitedBy}
|
||||||
|
<DetailItem label="Recruited By">
|
||||||
|
<a href="/database/weapons/{character.recruitedBy.granblueId}" class="recruited-by-link">
|
||||||
|
<img
|
||||||
|
src={getWeaponImage(character.recruitedBy.granblueId, 'square')}
|
||||||
|
alt={character.recruitedBy.name.en || 'Recruiting weapon'}
|
||||||
|
class="recruited-by-image"
|
||||||
|
/>
|
||||||
|
<span class="recruited-by-name">{character.recruitedBy.name.en}</span>
|
||||||
|
</a>
|
||||||
|
</DetailItem>
|
||||||
|
<DetailItem
|
||||||
|
label="Promotions"
|
||||||
|
sublabel="Gacha pools from recruiting weapon"
|
||||||
|
value={formatPromotions(character.recruitedBy.promotionNames)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<DetailItem label="Series" editable={true}>
|
||||||
|
<MultiSelect
|
||||||
|
size="medium"
|
||||||
|
options={seriesOptions}
|
||||||
|
bind:value={editData.series}
|
||||||
|
placeholder="Select series"
|
||||||
|
contained
|
||||||
|
/>
|
||||||
|
</DetailItem>
|
||||||
<DetailItem
|
<DetailItem
|
||||||
label="Season"
|
label="Season"
|
||||||
sublabel="Used to disambiguate characters with the same name"
|
sublabel="Used to disambiguate characters with the same name"
|
||||||
|
|
@ -74,19 +142,8 @@
|
||||||
contained
|
contained
|
||||||
/>
|
/>
|
||||||
</DetailItem>
|
</DetailItem>
|
||||||
<DetailItem
|
|
||||||
label="Character ID"
|
|
||||||
sublabel="Separate multiple IDs with commas"
|
|
||||||
bind:value={editData.characterId}
|
|
||||||
editable={true}
|
|
||||||
type="text"
|
|
||||||
placeholder="Character IDs"
|
|
||||||
/>
|
|
||||||
{:else}
|
{:else}
|
||||||
<DetailItem label="Rarity" value={getRarityLabel(character.rarity)} />
|
<DetailItem label="Rarity" value={getRarityLabel(character.rarity)} />
|
||||||
{#if character.season}
|
|
||||||
<DetailItem label="Season" value={getSeasonName(character.season) || '—'} />
|
|
||||||
{/if}
|
|
||||||
<DetailItem label="Granblue ID">
|
<DetailItem label="Granblue ID">
|
||||||
{#if character.granblueId}
|
{#if character.granblueId}
|
||||||
<CopyableText value={character.granblueId} />
|
<CopyableText value={character.granblueId} />
|
||||||
|
|
@ -116,6 +173,8 @@
|
||||||
value={formatPromotions(character.recruitedBy.promotionNames)}
|
value={formatPromotions(character.recruitedBy.promotionNames)}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
<DetailItem label="Series" value={formatSeriesDisplay()} />
|
||||||
|
<DetailItem label="Season" value={getSeasonName(character.season) || '—'} />
|
||||||
{/if}
|
{/if}
|
||||||
</DetailsContainer>
|
</DetailsContainer>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,12 @@
|
||||||
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 SuggestionDetailItem from '$lib/components/ui/SuggestionDetailItem.svelte'
|
import SuggestionDetailItem from '$lib/components/ui/SuggestionDetailItem.svelte'
|
||||||
import MultiSelect from '$lib/components/ui/MultiSelect.svelte'
|
|
||||||
import ElementLabel from '$lib/components/labels/ElementLabel.svelte'
|
import ElementLabel from '$lib/components/labels/ElementLabel.svelte'
|
||||||
import ProficiencyLabel from '$lib/components/labels/ProficiencyLabel.svelte'
|
import ProficiencyLabel from '$lib/components/labels/ProficiencyLabel.svelte'
|
||||||
import { getElementOptions } from '$lib/utils/element'
|
import { getElementOptions } from '$lib/utils/element'
|
||||||
import { getRaceLabel, getRaceOptions } from '$lib/utils/race'
|
import { getRaceLabel, getRaceOptions } from '$lib/utils/race'
|
||||||
import { getGenderLabel, getGenderOptions } from '$lib/utils/gender'
|
import { getGenderLabel, getGenderOptions } from '$lib/utils/gender'
|
||||||
import { getProficiencyOptions } from '$lib/utils/proficiency'
|
import { getProficiencyOptions } from '$lib/utils/proficiency'
|
||||||
import { CHARACTER_SERIES_NAMES, getSeriesNames } from '$lib/types/enums'
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
character: any
|
character: any
|
||||||
|
|
@ -39,18 +37,6 @@
|
||||||
const raceOptions = getRaceOptions()
|
const raceOptions = getRaceOptions()
|
||||||
const genderOptions = getGenderOptions()
|
const genderOptions = getGenderOptions()
|
||||||
const proficiencyOptions = getProficiencyOptions()
|
const proficiencyOptions = getProficiencyOptions()
|
||||||
|
|
||||||
// Series options for multiselect
|
|
||||||
const seriesOptions = Object.entries(CHARACTER_SERIES_NAMES).map(([value, label]) => ({
|
|
||||||
value: Number(value),
|
|
||||||
label
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Format series for display
|
|
||||||
function formatSeriesDisplay(series: number[]): string {
|
|
||||||
if (!series || series.length === 0) return '—'
|
|
||||||
return getSeriesNames(series).join(', ')
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DetailsContainer title="Details">
|
<DetailsContainer title="Details">
|
||||||
|
|
@ -121,18 +107,6 @@
|
||||||
onAcceptSuggestion={() => onAcceptSuggestion?.('proficiency2', suggestions?.proficiency2)}
|
onAcceptSuggestion={() => onAcceptSuggestion?.('proficiency2', suggestions?.proficiency2)}
|
||||||
onDismissSuggestion={() => onDismissSuggestion?.('proficiency2')}
|
onDismissSuggestion={() => onDismissSuggestion?.('proficiency2')}
|
||||||
/>
|
/>
|
||||||
<DetailItem
|
|
||||||
label="Series"
|
|
||||||
editable={true}
|
|
||||||
>
|
|
||||||
<MultiSelect
|
|
||||||
size="medium"
|
|
||||||
options={seriesOptions}
|
|
||||||
bind:value={editData.series}
|
|
||||||
placeholder="Select series"
|
|
||||||
contained
|
|
||||||
/>
|
|
||||||
</DetailItem>
|
|
||||||
{:else}
|
{:else}
|
||||||
<DetailItem label="Element">
|
<DetailItem label="Element">
|
||||||
<ElementLabel element={character.element} size="medium" />
|
<ElementLabel element={character.element} size="medium" />
|
||||||
|
|
@ -146,6 +120,5 @@
|
||||||
<DetailItem label="Proficiency 2">
|
<DetailItem label="Proficiency 2">
|
||||||
<ProficiencyLabel proficiency={character.proficiency?.[1] ?? 0} size="medium" />
|
<ProficiencyLabel proficiency={character.proficiency?.[1] ?? 0} size="medium" />
|
||||||
</DetailItem>
|
</DetailItem>
|
||||||
<DetailItem label="Series" value={formatSeriesDisplay(character.series)} />
|
|
||||||
{/if}
|
{/if}
|
||||||
</DetailsContainer>
|
</DetailsContainer>
|
||||||
|
|
|
||||||
|
|
@ -51,9 +51,7 @@
|
||||||
const elementName = $derived.by((): ElementName | undefined => {
|
const elementName = $derived.by((): ElementName | undefined => {
|
||||||
const el = editMode ? editData.element : character?.element
|
const el = editMode ? editData.element : character?.element
|
||||||
const label = getElementLabel(el)
|
const label = getElementLabel(el)
|
||||||
return label !== '—' && label !== 'Null'
|
return label !== '—' && label !== 'Null' ? (label.toLowerCase() as ElementName) : undefined
|
||||||
? (label.toLowerCase() as ElementName)
|
|
||||||
: undefined
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Auto-check/uncheck uncap levels in hierarchy: Transcendence > ULB > FLB
|
// Auto-check/uncheck uncap levels in hierarchy: Transcendence > ULB > FLB
|
||||||
|
|
@ -98,8 +96,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DetailsContainer title="Uncap">
|
<DetailsContainer title="Uncap">
|
||||||
{#if character?.uncap || editMode}
|
<DetailItem label="Uncap Level">
|
||||||
<DetailItem label="Uncap">
|
|
||||||
<UncapIndicator
|
<UncapIndicator
|
||||||
type="character"
|
type="character"
|
||||||
{uncapLevel}
|
{uncapLevel}
|
||||||
|
|
@ -111,9 +108,13 @@
|
||||||
editable={false}
|
editable={false}
|
||||||
/>
|
/>
|
||||||
</DetailItem>
|
</DetailItem>
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if editMode}
|
{#if !editMode}
|
||||||
|
<DetailItem label="FLB" value={flb ? 'Yes' : 'No'} />
|
||||||
|
<DetailItem label="ULB" value={ulb ? 'Yes' : 'No'} />
|
||||||
|
<DetailItem label="Transcendence" value={transcendence ? 'Yes' : 'No'} />
|
||||||
|
<DetailItem label="Special" value={special ? 'Yes' : 'No'} />
|
||||||
|
{:else}
|
||||||
<SuggestionDetailItem
|
<SuggestionDetailItem
|
||||||
label="FLB"
|
label="FLB"
|
||||||
bind:value={editData.flb}
|
bind:value={editData.flb}
|
||||||
|
|
@ -138,10 +139,26 @@
|
||||||
onAcceptSuggestion={() => onAcceptSuggestion?.('ulb', suggestions?.ulb)}
|
onAcceptSuggestion={() => onAcceptSuggestion?.('ulb', suggestions?.ulb)}
|
||||||
onDismissSuggestion={() => onDismissSuggestion?.('ulb')}
|
onDismissSuggestion={() => onDismissSuggestion?.('ulb')}
|
||||||
/>
|
/>
|
||||||
<DetailItem label="Transcendence" bind:value={editData.transcendence} editable={true} type="checkbox" element={elementName} onchange={handleTranscendenceChange} />
|
<DetailItem
|
||||||
|
label="Transcendence"
|
||||||
|
bind:value={editData.transcendence}
|
||||||
|
editable={true}
|
||||||
|
type="checkbox"
|
||||||
|
element={elementName}
|
||||||
|
onchange={handleTranscendenceChange}
|
||||||
|
/>
|
||||||
<div class="special-field">
|
<div class="special-field">
|
||||||
<DetailItem label="Special" bind:value={editData.special} editable={true} type="checkbox" element={elementName} onchange={handleSpecialChange} />
|
<DetailItem
|
||||||
<p class="special-note">This is for Story SRs. Don't check this unless something really weird happens.</p>
|
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>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</DetailsContainer>
|
</DetailsContainer>
|
||||||
|
|
@ -160,7 +177,6 @@
|
||||||
font-size: typography.$font-small;
|
font-size: typography.$font-small;
|
||||||
color: colors.$grey-50;
|
color: colors.$grey-50;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 spacing.$unit spacing.$unit;
|
padding-bottom: spacing.$unit;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue