improve import form components
- fix Select reactivity with external value changes - add series/promotions to taxonomy sections - add onDataChange callbacks to uncap sections - add game CDN image helpers for batch imports - expand suggestion type definitions
This commit is contained in:
parent
37f2178d4a
commit
e1ba34048a
11 changed files with 359 additions and 211 deletions
|
|
@ -550,6 +550,8 @@ export interface CharacterSuggestions {
|
|||
maxAtkFlb?: number
|
||||
flb?: boolean
|
||||
ulb?: boolean
|
||||
/** Series array (e.g., [2] for Grand, [3] for Zodiac) */
|
||||
series?: number[]
|
||||
releaseDate?: string
|
||||
flbDate?: string
|
||||
ulbDate?: string
|
||||
|
|
@ -570,11 +572,15 @@ export interface WeaponSuggestions {
|
|||
minHp?: number
|
||||
maxHp?: number
|
||||
maxHpFlb?: number
|
||||
maxHpUlb?: number
|
||||
minAtk?: number
|
||||
maxAtk?: number
|
||||
maxAtkFlb?: number
|
||||
maxAtkUlb?: number
|
||||
flb?: boolean
|
||||
ulb?: boolean
|
||||
/** Series name (e.g., "Revenant", "Optimus") */
|
||||
series?: string
|
||||
releaseDate?: string
|
||||
flbDate?: string
|
||||
ulbDate?: string
|
||||
|
|
@ -596,11 +602,16 @@ export interface SummonSuggestions {
|
|||
minHp?: number
|
||||
maxHp?: number
|
||||
maxHpFlb?: number
|
||||
maxHpUlb?: number
|
||||
minAtk?: number
|
||||
maxAtk?: number
|
||||
maxAtkFlb?: number
|
||||
maxAtkUlb?: number
|
||||
flb?: boolean
|
||||
ulb?: boolean
|
||||
transcendence?: boolean
|
||||
/** Series name (e.g., "Optimus", "Arcarum") */
|
||||
series?: string
|
||||
subaura?: boolean
|
||||
releaseDate?: string
|
||||
flbDate?: string
|
||||
|
|
|
|||
|
|
@ -64,11 +64,16 @@
|
|||
|
||||
const selected = $derived(options.find((opt) => opt.value === value))
|
||||
|
||||
// Local string value for Bits UI (which manages its own internal state)
|
||||
let internalValue = $state<string | undefined>(
|
||||
value !== undefined && value !== null ? String(value) : undefined
|
||||
)
|
||||
|
||||
// Sync external value changes to internal state
|
||||
$effect(() => {
|
||||
console.log('[Select] value:', value, typeof value)
|
||||
console.log('[Select] options:', options.map(o => ({ value: o.value, type: typeof o.value })))
|
||||
console.log('[Select] selected:', selected)
|
||||
internalValue = value !== undefined && value !== null ? String(value) : undefined
|
||||
})
|
||||
|
||||
const hasWrapper = $derived(label || error)
|
||||
|
||||
const fieldsetClasses = $derived(
|
||||
|
|
@ -106,7 +111,7 @@
|
|||
|
||||
<SelectPrimitive.Root
|
||||
type="single"
|
||||
{...value !== undefined && value !== null ? { value: String(value) } : {}}
|
||||
bind:value={internalValue}
|
||||
onValueChange={handleValueChange}
|
||||
{disabled}
|
||||
items={stringOptions}
|
||||
|
|
@ -191,7 +196,7 @@
|
|||
{:else}
|
||||
<SelectPrimitive.Root
|
||||
type="single"
|
||||
{...value !== undefined && value !== null ? { value: String(value) } : {}}
|
||||
bind:value={internalValue}
|
||||
onValueChange={handleValueChange}
|
||||
{disabled}
|
||||
items={stringOptions}
|
||||
|
|
|
|||
|
|
@ -1,189 +1,221 @@
|
|||
<svelte:options runes={true} />
|
||||
|
||||
<script lang="ts">
|
||||
import type { CharacterSuggestions } from '$lib/api/adapters/entity.adapter'
|
||||
import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte'
|
||||
import DetailItem from '$lib/components/ui/DetailItem.svelte'
|
||||
import SuggestionDetailItem from '$lib/components/ui/SuggestionDetailItem.svelte'
|
||||
import ElementLabel from '$lib/components/labels/ElementLabel.svelte'
|
||||
import ProficiencyLabel from '$lib/components/labels/ProficiencyLabel.svelte'
|
||||
import { getElementLabel, getElementOptions } from '$lib/utils/element'
|
||||
import { getRaceLabel, getRaceOptions } from '$lib/utils/race'
|
||||
import { getGenderLabel, getGenderOptions } from '$lib/utils/gender'
|
||||
import { getProficiencyOptions } from '$lib/utils/proficiency'
|
||||
import {
|
||||
CharacterSeason,
|
||||
CharacterSeries,
|
||||
CHARACTER_SEASON_NAMES,
|
||||
CHARACTER_SERIES_NAMES,
|
||||
getSeasonName,
|
||||
getSeriesNames
|
||||
} from '$lib/types/enums'
|
||||
import type { CharacterSuggestions } from '$lib/api/adapters/entity.adapter'
|
||||
import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte'
|
||||
import DetailItem from '$lib/components/ui/DetailItem.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 ProficiencyLabel from '$lib/components/labels/ProficiencyLabel.svelte'
|
||||
import { getElementLabel, getElementOptions } from '$lib/utils/element'
|
||||
import { getRaceLabel, getRaceOptions } from '$lib/utils/race'
|
||||
import { getGenderLabel, getGenderOptions } from '$lib/utils/gender'
|
||||
import { getProficiencyOptions } from '$lib/utils/proficiency'
|
||||
import {
|
||||
CharacterSeason,
|
||||
CharacterSeries,
|
||||
CHARACTER_SEASON_NAMES,
|
||||
CHARACTER_SERIES_NAMES,
|
||||
PROMOTION_NAMES,
|
||||
getSeasonName,
|
||||
getSeriesNames,
|
||||
getPromotionNames
|
||||
} from '$lib/types/enums'
|
||||
|
||||
type ElementName = 'wind' | 'fire' | 'water' | 'earth' | 'dark' | 'light'
|
||||
type ElementName = 'wind' | 'fire' | 'water' | 'earth' | 'dark' | 'light'
|
||||
|
||||
interface Props {
|
||||
character: any
|
||||
editMode?: boolean
|
||||
editData?: any
|
||||
// Suggestion support for batch import
|
||||
suggestions?: CharacterSuggestions
|
||||
dismissedSuggestions?: Set<string>
|
||||
onAcceptSuggestion?: (field: string, value: any) => void
|
||||
onDismissSuggestion?: (field: string) => void
|
||||
}
|
||||
interface Props {
|
||||
character: any
|
||||
editMode?: boolean
|
||||
editData?: any
|
||||
// Suggestion support for batch import
|
||||
suggestions?: CharacterSuggestions
|
||||
dismissedSuggestions?: Set<string>
|
||||
onAcceptSuggestion?: (field: string, value: any) => void
|
||||
onDismissSuggestion?: (field: string) => void
|
||||
}
|
||||
|
||||
let {
|
||||
character,
|
||||
editMode = false,
|
||||
editData = $bindable(),
|
||||
suggestions,
|
||||
dismissedSuggestions,
|
||||
onAcceptSuggestion,
|
||||
onDismissSuggestion
|
||||
}: Props = $props()
|
||||
let {
|
||||
character,
|
||||
editMode = false,
|
||||
editData = $bindable(),
|
||||
suggestions,
|
||||
dismissedSuggestions,
|
||||
onAcceptSuggestion,
|
||||
onDismissSuggestion
|
||||
}: Props = $props()
|
||||
|
||||
const elementOptions = getElementOptions()
|
||||
const raceOptions = getRaceOptions()
|
||||
const genderOptions = getGenderOptions()
|
||||
const proficiencyOptions = getProficiencyOptions()
|
||||
const elementOptions = getElementOptions()
|
||||
const raceOptions = getRaceOptions()
|
||||
const genderOptions = getGenderOptions()
|
||||
const proficiencyOptions = getProficiencyOptions()
|
||||
|
||||
// Season options (nullable, so include a "None" option)
|
||||
const seasonOptions = [
|
||||
{ value: 0, label: 'None' },
|
||||
...Object.entries(CHARACTER_SEASON_NAMES).map(([value, label]) => ({
|
||||
value: Number(value),
|
||||
label
|
||||
}))
|
||||
]
|
||||
// Season options (nullable, so include a "None" option)
|
||||
const seasonOptions = [
|
||||
{ value: 0, label: 'None' },
|
||||
...Object.entries(CHARACTER_SEASON_NAMES).map(([value, label]) => ({
|
||||
value: Number(value),
|
||||
label
|
||||
}))
|
||||
]
|
||||
|
||||
// Series options for multiselect
|
||||
const seriesOptions = Object.entries(CHARACTER_SERIES_NAMES).map(([value, label]) => ({
|
||||
value: Number(value),
|
||||
label
|
||||
}))
|
||||
// Series options for multiselect
|
||||
const seriesOptions = Object.entries(CHARACTER_SERIES_NAMES).map(([value, label]) => ({
|
||||
value: Number(value),
|
||||
label
|
||||
}))
|
||||
|
||||
// Get element name for checkbox theming
|
||||
const elementName = $derived.by((): ElementName | undefined => {
|
||||
const el = editMode ? editData?.element : character?.element
|
||||
const label = getElementLabel(el)
|
||||
return label !== '—' && label !== 'Null' ? (label.toLowerCase() as ElementName) : undefined
|
||||
})
|
||||
// Promotion options for multiselect
|
||||
const promotionOptions = Object.entries(PROMOTION_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(', ')
|
||||
}
|
||||
// Get element name for checkbox theming
|
||||
const elementName = $derived.by((): ElementName | undefined => {
|
||||
const el = editMode ? editData?.element : character?.element
|
||||
const label = getElementLabel(el)
|
||||
return label !== '—' && label !== 'Null' ? (label.toLowerCase() as ElementName) : undefined
|
||||
})
|
||||
|
||||
// Format series for display
|
||||
function formatSeriesDisplay(series: number[]): string {
|
||||
if (!series || series.length === 0) return '—'
|
||||
return getSeriesNames(series).join(', ')
|
||||
}
|
||||
|
||||
// Format promotions for display
|
||||
function formatPromotionsDisplay(promotions: number[]): string {
|
||||
if (!promotions || promotions.length === 0) return '—'
|
||||
return getPromotionNames(promotions).join(', ')
|
||||
}
|
||||
</script>
|
||||
|
||||
<DetailsContainer title="Details">
|
||||
{#if editMode}
|
||||
<SuggestionDetailItem
|
||||
label="Element"
|
||||
bind:value={editData.element}
|
||||
editable={true}
|
||||
type="select"
|
||||
options={elementOptions}
|
||||
suggestion={suggestions?.element}
|
||||
dismissedSuggestion={dismissedSuggestions?.has('element')}
|
||||
onAcceptSuggestion={() => onAcceptSuggestion?.('element', suggestions?.element)}
|
||||
onDismissSuggestion={() => onDismissSuggestion?.('element')}
|
||||
/>
|
||||
<SuggestionDetailItem
|
||||
label="Race 1"
|
||||
bind:value={editData.race1}
|
||||
editable={true}
|
||||
type="select"
|
||||
options={raceOptions}
|
||||
suggestion={suggestions?.race1}
|
||||
dismissedSuggestion={dismissedSuggestions?.has('race1')}
|
||||
onAcceptSuggestion={() => onAcceptSuggestion?.('race1', suggestions?.race1)}
|
||||
onDismissSuggestion={() => onDismissSuggestion?.('race1')}
|
||||
/>
|
||||
<SuggestionDetailItem
|
||||
label="Race 2"
|
||||
bind:value={editData.race2}
|
||||
editable={true}
|
||||
type="select"
|
||||
options={raceOptions}
|
||||
suggestion={suggestions?.race2}
|
||||
dismissedSuggestion={dismissedSuggestions?.has('race2')}
|
||||
onAcceptSuggestion={() => onAcceptSuggestion?.('race2', suggestions?.race2)}
|
||||
onDismissSuggestion={() => onDismissSuggestion?.('race2')}
|
||||
/>
|
||||
<SuggestionDetailItem
|
||||
label="Gender"
|
||||
bind:value={editData.gender}
|
||||
editable={true}
|
||||
type="select"
|
||||
options={genderOptions}
|
||||
suggestion={suggestions?.gender}
|
||||
dismissedSuggestion={dismissedSuggestions?.has('gender')}
|
||||
onAcceptSuggestion={() => onAcceptSuggestion?.('gender', suggestions?.gender)}
|
||||
onDismissSuggestion={() => onDismissSuggestion?.('gender')}
|
||||
/>
|
||||
<SuggestionDetailItem
|
||||
label="Proficiency 1"
|
||||
bind:value={editData.proficiency1}
|
||||
editable={true}
|
||||
type="select"
|
||||
options={proficiencyOptions}
|
||||
suggestion={suggestions?.proficiency1}
|
||||
dismissedSuggestion={dismissedSuggestions?.has('proficiency1')}
|
||||
onAcceptSuggestion={() => onAcceptSuggestion?.('proficiency1', suggestions?.proficiency1)}
|
||||
onDismissSuggestion={() => onDismissSuggestion?.('proficiency1')}
|
||||
/>
|
||||
<SuggestionDetailItem
|
||||
label="Proficiency 2"
|
||||
bind:value={editData.proficiency2}
|
||||
editable={true}
|
||||
type="select"
|
||||
options={proficiencyOptions}
|
||||
suggestion={suggestions?.proficiency2}
|
||||
dismissedSuggestion={dismissedSuggestions?.has('proficiency2')}
|
||||
onAcceptSuggestion={() => onAcceptSuggestion?.('proficiency2', suggestions?.proficiency2)}
|
||||
onDismissSuggestion={() => onDismissSuggestion?.('proficiency2')}
|
||||
/>
|
||||
<DetailItem
|
||||
label="Season"
|
||||
bind:value={editData.season}
|
||||
editable={true}
|
||||
type="select"
|
||||
options={seasonOptions}
|
||||
/>
|
||||
<DetailItem
|
||||
label="Series"
|
||||
bind:value={editData.series}
|
||||
editable={true}
|
||||
type="multiselect"
|
||||
options={seriesOptions}
|
||||
element={elementName}
|
||||
/>
|
||||
<DetailItem
|
||||
label="Gacha Available"
|
||||
sublabel="Can be pulled from gacha"
|
||||
bind:value={editData.gacha_available}
|
||||
editable={true}
|
||||
type="checkbox"
|
||||
element={elementName}
|
||||
/>
|
||||
{:else}
|
||||
<DetailItem label="Element">
|
||||
<ElementLabel element={character.element} size="medium" />
|
||||
</DetailItem>
|
||||
<DetailItem label="Race 1" value={getRaceLabel(character.race?.[0])} />
|
||||
<DetailItem label="Race 2" value={getRaceLabel(character.race?.[1])} />
|
||||
<DetailItem label="Gender" value={getGenderLabel(character.gender)} />
|
||||
<DetailItem label="Proficiency 1">
|
||||
<ProficiencyLabel proficiency={character.proficiency?.[0] ?? 0} size="medium" />
|
||||
</DetailItem>
|
||||
<DetailItem label="Proficiency 2">
|
||||
<ProficiencyLabel proficiency={character.proficiency?.[1] ?? 0} size="medium" />
|
||||
</DetailItem>
|
||||
<DetailItem label="Season" value={getSeasonName(character.season) || '—'} />
|
||||
<DetailItem label="Series" value={formatSeriesDisplay(character.series)} />
|
||||
<DetailItem label="Gacha Available" value={character.gachaAvailable ? 'Yes' : 'No'} />
|
||||
{/if}
|
||||
{#if editMode}
|
||||
<SuggestionDetailItem
|
||||
label="Element"
|
||||
bind:value={editData.element}
|
||||
editable={true}
|
||||
type="select"
|
||||
options={elementOptions}
|
||||
suggestion={suggestions?.element}
|
||||
dismissedSuggestion={dismissedSuggestions?.has('element')}
|
||||
onAcceptSuggestion={() => onAcceptSuggestion?.('element', suggestions?.element)}
|
||||
onDismissSuggestion={() => onDismissSuggestion?.('element')}
|
||||
/>
|
||||
<SuggestionDetailItem
|
||||
label="Race 1"
|
||||
bind:value={editData.race1}
|
||||
editable={true}
|
||||
type="select"
|
||||
options={raceOptions}
|
||||
suggestion={suggestions?.race1}
|
||||
dismissedSuggestion={dismissedSuggestions?.has('race1')}
|
||||
onAcceptSuggestion={() => onAcceptSuggestion?.('race1', suggestions?.race1)}
|
||||
onDismissSuggestion={() => onDismissSuggestion?.('race1')}
|
||||
/>
|
||||
<SuggestionDetailItem
|
||||
label="Race 2"
|
||||
bind:value={editData.race2}
|
||||
editable={true}
|
||||
type="select"
|
||||
options={raceOptions}
|
||||
suggestion={suggestions?.race2}
|
||||
dismissedSuggestion={dismissedSuggestions?.has('race2')}
|
||||
onAcceptSuggestion={() => onAcceptSuggestion?.('race2', suggestions?.race2)}
|
||||
onDismissSuggestion={() => onDismissSuggestion?.('race2')}
|
||||
/>
|
||||
<SuggestionDetailItem
|
||||
label="Gender"
|
||||
bind:value={editData.gender}
|
||||
editable={true}
|
||||
type="select"
|
||||
options={genderOptions}
|
||||
suggestion={suggestions?.gender}
|
||||
dismissedSuggestion={dismissedSuggestions?.has('gender')}
|
||||
onAcceptSuggestion={() => onAcceptSuggestion?.('gender', suggestions?.gender)}
|
||||
onDismissSuggestion={() => onDismissSuggestion?.('gender')}
|
||||
/>
|
||||
<SuggestionDetailItem
|
||||
label="Proficiency 1"
|
||||
bind:value={editData.proficiency1}
|
||||
editable={true}
|
||||
type="select"
|
||||
options={proficiencyOptions}
|
||||
suggestion={suggestions?.proficiency1}
|
||||
dismissedSuggestion={dismissedSuggestions?.has('proficiency1')}
|
||||
onAcceptSuggestion={() => onAcceptSuggestion?.('proficiency1', suggestions?.proficiency1)}
|
||||
onDismissSuggestion={() => onDismissSuggestion?.('proficiency1')}
|
||||
/>
|
||||
<SuggestionDetailItem
|
||||
label="Proficiency 2"
|
||||
bind:value={editData.proficiency2}
|
||||
editable={true}
|
||||
type="select"
|
||||
options={proficiencyOptions}
|
||||
suggestion={suggestions?.proficiency2}
|
||||
dismissedSuggestion={dismissedSuggestions?.has('proficiency2')}
|
||||
onAcceptSuggestion={() => onAcceptSuggestion?.('proficiency2', suggestions?.proficiency2)}
|
||||
onDismissSuggestion={() => onDismissSuggestion?.('proficiency2')}
|
||||
/>
|
||||
<DetailItem
|
||||
label="Season"
|
||||
bind:value={editData.season}
|
||||
editable={true}
|
||||
type="select"
|
||||
options={seasonOptions}
|
||||
/>
|
||||
<DetailItem
|
||||
label="Series"
|
||||
bind:value={editData.series}
|
||||
editable={true}
|
||||
type="multiselect"
|
||||
options={seriesOptions}
|
||||
element={elementName}
|
||||
/>
|
||||
<DetailItem
|
||||
label="Gacha Available"
|
||||
sublabel="Can be pulled from gacha"
|
||||
bind:value={editData.gacha_available}
|
||||
editable={true}
|
||||
type="checkbox"
|
||||
element={elementName}
|
||||
/>
|
||||
<DetailItem
|
||||
label="Promotions"
|
||||
sublabel="Gacha pools where this character appears"
|
||||
editable={true}
|
||||
>
|
||||
<MultiSelect
|
||||
size="medium"
|
||||
options={promotionOptions}
|
||||
bind:value={editData.promotions}
|
||||
placeholder="Select promotions"
|
||||
contained
|
||||
/>
|
||||
</DetailItem>
|
||||
{:else}
|
||||
<DetailItem label="Element">
|
||||
<ElementLabel element={character.element} size="medium" />
|
||||
</DetailItem>
|
||||
<DetailItem label="Race 1" value={getRaceLabel(character.race?.[0])} />
|
||||
<DetailItem label="Race 2" value={getRaceLabel(character.race?.[1])} />
|
||||
<DetailItem label="Gender" value={getGenderLabel(character.gender)} />
|
||||
<DetailItem label="Proficiency 1">
|
||||
<ProficiencyLabel proficiency={character.proficiency?.[0] ?? 0} size="medium" />
|
||||
</DetailItem>
|
||||
<DetailItem label="Proficiency 2">
|
||||
<ProficiencyLabel proficiency={character.proficiency?.[1] ?? 0} size="medium" />
|
||||
</DetailItem>
|
||||
<DetailItem label="Season" value={getSeasonName(character.season) || '—'} />
|
||||
<DetailItem label="Series" value={formatSeriesDisplay(character.series)} />
|
||||
<DetailItem label="Gacha Available" value={character.gachaAvailable ? 'Yes' : 'No'} />
|
||||
<DetailItem
|
||||
label="Promotions"
|
||||
sublabel="Gacha pools where this character appears"
|
||||
value={formatPromotionsDisplay(character.promotions)}
|
||||
/>
|
||||
{/if}
|
||||
</DetailsContainer>
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@
|
|||
dismissedSuggestions?: Set<string>
|
||||
onAcceptSuggestion?: (field: string, value: any) => void
|
||||
onDismissSuggestion?: (field: string) => void
|
||||
// Callback when editData is modified (for triggering reactivity in parent)
|
||||
onDataChange?: () => void
|
||||
}
|
||||
|
||||
let {
|
||||
|
|
@ -29,7 +31,8 @@
|
|||
suggestions,
|
||||
dismissedSuggestions,
|
||||
onAcceptSuggestion,
|
||||
onDismissSuggestion
|
||||
onDismissSuggestion,
|
||||
onDataChange
|
||||
}: Props = $props()
|
||||
|
||||
const uncap = $derived(
|
||||
|
|
@ -60,6 +63,7 @@
|
|||
editData.ulb = false
|
||||
editData.transcendence = false
|
||||
}
|
||||
onDataChange?.()
|
||||
}
|
||||
|
||||
function handleUlbChange(checked: boolean) {
|
||||
|
|
@ -70,6 +74,7 @@
|
|||
// Unchecking ULB should also uncheck Transcendence
|
||||
editData.transcendence = false
|
||||
}
|
||||
onDataChange?.()
|
||||
}
|
||||
|
||||
function handleTranscendenceChange(checked: boolean) {
|
||||
|
|
@ -78,6 +83,7 @@
|
|||
if (!editData.ulb) editData.ulb = true
|
||||
if (!editData.flb) editData.flb = true
|
||||
}
|
||||
onDataChange?.()
|
||||
}
|
||||
|
||||
function handleSpecialChange(checked: boolean) {
|
||||
|
|
@ -87,6 +93,7 @@
|
|||
editData.ulb = false
|
||||
editData.transcendence = false
|
||||
}
|
||||
onDataChange?.()
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
<svelte:options runes={true} />
|
||||
|
||||
<script lang="ts">
|
||||
import { createQuery } from '@tanstack/svelte-query'
|
||||
import type { SummonSuggestions } from '$lib/api/adapters/entity.adapter'
|
||||
import { entityQueries } from '$lib/api/queries/entity.queries'
|
||||
import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte'
|
||||
import DetailItem from '$lib/components/ui/DetailItem.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 { getElementLabel, getElementOptions } from '$lib/utils/element'
|
||||
import type { SummonSeriesRef } from '$lib/types/api/summonSeries'
|
||||
import { PROMOTION_NAMES, getPromotionNames } from '$lib/types/enums'
|
||||
|
||||
type ElementName = 'wind' | 'fire' | 'water' | 'earth' | 'dark' | 'light'
|
||||
|
|
@ -32,8 +36,23 @@
|
|||
onDismissSuggestion
|
||||
}: Props = $props()
|
||||
|
||||
// Fetch summon series list from API
|
||||
const summonSeriesQuery = createQuery(() => entityQueries.summonSeriesList())
|
||||
|
||||
const elementOptions = getElementOptions()
|
||||
|
||||
// Build series options from fetched data
|
||||
const seriesOptions = $derived.by(() => {
|
||||
const series = summonSeriesQuery.data ?? []
|
||||
return [
|
||||
{ value: '', label: 'None' },
|
||||
...series.map((s) => ({
|
||||
value: s.id,
|
||||
label: s.name.en
|
||||
}))
|
||||
]
|
||||
})
|
||||
|
||||
// Promotion options for multiselect
|
||||
const promotionOptions = Object.entries(PROMOTION_NAMES).map(([value, label]) => ({
|
||||
value: Number(value),
|
||||
|
|
@ -47,6 +66,12 @@
|
|||
return label !== '—' && label !== 'Null' ? (label.toLowerCase() as ElementName) : undefined
|
||||
})
|
||||
|
||||
// Format series label for display mode
|
||||
function formatSeriesLabel(series: SummonSeriesRef | null | undefined): string {
|
||||
if (!series) return '—'
|
||||
return series.name?.en || '—'
|
||||
}
|
||||
|
||||
// Format promotions for display
|
||||
function formatPromotionsDisplay(promotions: number[]): string {
|
||||
if (!promotions || promotions.length === 0) return '—'
|
||||
|
|
@ -71,23 +96,27 @@
|
|||
label="Series"
|
||||
bind:value={editData.series}
|
||||
editable={true}
|
||||
type="text"
|
||||
placeholder="Series name"
|
||||
/>
|
||||
<DetailItem
|
||||
label="Promotions"
|
||||
sublabel="Gacha pools where this summon appears"
|
||||
bind:value={editData.promotions}
|
||||
editable={true}
|
||||
type="multiselect"
|
||||
options={promotionOptions}
|
||||
element={elementName}
|
||||
type="select"
|
||||
options={seriesOptions}
|
||||
/>
|
||||
<DetailItem label="Promotions" sublabel="Gacha pools where this summon appears" editable={true}>
|
||||
<MultiSelect
|
||||
size="medium"
|
||||
options={promotionOptions}
|
||||
bind:value={editData.promotions}
|
||||
placeholder="Select promotions"
|
||||
contained
|
||||
/>
|
||||
</DetailItem>
|
||||
{:else}
|
||||
<DetailItem label="Element">
|
||||
<ElementLabel element={summon.element} size="medium" />
|
||||
</DetailItem>
|
||||
<DetailItem label="Series" value={summon.series || '—'} />
|
||||
<DetailItem label="Promotions" sublabel="Gacha pools where this summon appears" value={formatPromotionsDisplay(summon.promotions)} />
|
||||
<DetailItem label="Series" value={formatSeriesLabel(summon.series)} />
|
||||
<DetailItem
|
||||
label="Promotions"
|
||||
sublabel="Gacha pools where this summon appears"
|
||||
value={formatPromotionsDisplay(summon.promotions)}
|
||||
/>
|
||||
{/if}
|
||||
</DetailsContainer>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@
|
|||
dismissedSuggestions?: Set<string>
|
||||
onAcceptSuggestion?: (field: string, value: any) => void
|
||||
onDismissSuggestion?: (field: string) => void
|
||||
// Callback when editData is modified (for triggering reactivity in parent)
|
||||
onDataChange?: () => void
|
||||
}
|
||||
|
||||
let {
|
||||
|
|
@ -29,7 +31,8 @@
|
|||
suggestions,
|
||||
dismissedSuggestions,
|
||||
onAcceptSuggestion,
|
||||
onDismissSuggestion
|
||||
onDismissSuggestion,
|
||||
onDataChange
|
||||
}: Props = $props()
|
||||
|
||||
const uncap = $derived(
|
||||
|
|
@ -58,6 +61,7 @@
|
|||
editData.ulb = false
|
||||
editData.transcendence = false
|
||||
}
|
||||
onDataChange?.()
|
||||
}
|
||||
|
||||
function handleUlbChange(checked: boolean) {
|
||||
|
|
@ -68,6 +72,7 @@
|
|||
// Unchecking ULB should also uncheck Transcendence
|
||||
editData.transcendence = false
|
||||
}
|
||||
onDataChange?.()
|
||||
}
|
||||
|
||||
function handleTranscendenceChange(checked: boolean) {
|
||||
|
|
@ -76,6 +81,7 @@
|
|||
if (!editData.ulb) editData.ulb = true
|
||||
if (!editData.flb) editData.flb = true
|
||||
}
|
||||
onDataChange?.()
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte'
|
||||
import DetailItem from '$lib/components/ui/DetailItem.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 ProficiencyLabel from '$lib/components/labels/ProficiencyLabel.svelte'
|
||||
import { getElementLabel, getElementOptions } from '$lib/utils/element'
|
||||
|
|
@ -138,15 +139,15 @@
|
|||
type="checkbox"
|
||||
element={elementName}
|
||||
/>
|
||||
<DetailItem
|
||||
label="Promotions"
|
||||
sublabel="Gacha pools where this weapon appears"
|
||||
bind:value={editData.promotions}
|
||||
editable={true}
|
||||
type="multiselect"
|
||||
options={promotionOptions}
|
||||
element={elementName}
|
||||
/>
|
||||
<DetailItem label="Promotions" sublabel="Gacha pools where this weapon appears" editable={true}>
|
||||
<MultiSelect
|
||||
size="medium"
|
||||
options={promotionOptions}
|
||||
bind:value={editData.promotions}
|
||||
placeholder="Select promotions"
|
||||
contained
|
||||
/>
|
||||
</DetailItem>
|
||||
{:else}
|
||||
<DetailItem label="Element">
|
||||
<ElementLabel element={weapon.element} size="medium" />
|
||||
|
|
@ -158,9 +159,21 @@
|
|||
/>
|
||||
</DetailItem>
|
||||
<DetailItem label="Series" value={formatSeriesLabel(weapon.series)} />
|
||||
<DetailItem label="Extra" sublabel="Can be placed in Additional Weapons" value={weapon.extra ? 'Yes' : 'No'} />
|
||||
<DetailItem label="Limit" sublabel="Only one copy can be placed in a team" value={weapon.limit ? 'Yes' : 'No'} />
|
||||
<DetailItem
|
||||
label="Extra"
|
||||
sublabel="Can be placed in Additional Weapons"
|
||||
value={weapon.extra ? 'Yes' : 'No'}
|
||||
/>
|
||||
<DetailItem
|
||||
label="Limit"
|
||||
sublabel="Only one copy can be placed in a team"
|
||||
value={weapon.limit ? 'Yes' : 'No'}
|
||||
/>
|
||||
<DetailItem label="AX Skills" sublabel="Can have AX Skills" value={weapon.ax ? 'Yes' : 'No'} />
|
||||
<DetailItem label="Promotions" sublabel="Gacha pools where this weapon appears" value={formatPromotionsDisplay(weapon.promotions)} />
|
||||
<DetailItem
|
||||
label="Promotions"
|
||||
sublabel="Gacha pools where this weapon appears"
|
||||
value={formatPromotionsDisplay(weapon.promotions)}
|
||||
/>
|
||||
{/if}
|
||||
</DetailsContainer>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@
|
|||
dismissedSuggestions?: Set<string>
|
||||
onAcceptSuggestion?: (field: string, value: any) => void
|
||||
onDismissSuggestion?: (field: string) => void
|
||||
// Callback when editData is modified (for triggering reactivity in parent)
|
||||
onDataChange?: () => void
|
||||
}
|
||||
|
||||
let {
|
||||
|
|
@ -28,7 +30,8 @@
|
|||
suggestions,
|
||||
dismissedSuggestions,
|
||||
onAcceptSuggestion,
|
||||
onDismissSuggestion
|
||||
onDismissSuggestion,
|
||||
onDataChange
|
||||
}: Props = $props()
|
||||
|
||||
const uncap = $derived(
|
||||
|
|
@ -57,6 +60,7 @@
|
|||
editData.ulb = false
|
||||
editData.transcendence = false
|
||||
}
|
||||
onDataChange?.()
|
||||
}
|
||||
|
||||
function handleUlbChange(checked: boolean) {
|
||||
|
|
@ -67,6 +71,7 @@
|
|||
// Unchecking ULB should also uncheck Transcendence
|
||||
editData.transcendence = false
|
||||
}
|
||||
onDataChange?.()
|
||||
}
|
||||
|
||||
function handleTranscendenceChange(checked: boolean) {
|
||||
|
|
@ -75,6 +80,7 @@
|
|||
if (!editData.ulb) editData.ulb = true
|
||||
if (!editData.flb) editData.flb = true
|
||||
}
|
||||
onDataChange?.()
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -389,6 +389,41 @@ export function getArtifactImage(
|
|||
return `${getBasePath()}/${directory}/${granblueId}.jpg`
|
||||
}
|
||||
|
||||
// ===== Game CDN Images =====
|
||||
// For new items not yet in our AWS CDN (used in batch import)
|
||||
|
||||
const GAME_CDN_BASE = 'https://prd-game-a-granbluefantasy.akamaized.net/assets_en/img/sp/assets'
|
||||
|
||||
/**
|
||||
* Get character image from the game CDN
|
||||
* Used for batch imports where images aren't yet in AWS
|
||||
*/
|
||||
export function getGameCdnCharacterImage(
|
||||
id: string | number | null | undefined,
|
||||
pose: string = '01'
|
||||
): string {
|
||||
if (!id) return getPlaceholderImage('character', 'square')
|
||||
return `${GAME_CDN_BASE}/npc/s/${id}_${pose}.jpg`
|
||||
}
|
||||
|
||||
/**
|
||||
* Get weapon image from the game CDN
|
||||
* Used for batch imports where images aren't yet in AWS
|
||||
*/
|
||||
export function getGameCdnWeaponImage(id: string | number | null | undefined): string {
|
||||
if (!id) return getPlaceholderImage('weapon', 'square')
|
||||
return `${GAME_CDN_BASE}/weapon/s/${id}.jpg`
|
||||
}
|
||||
|
||||
/**
|
||||
* Get summon image from the game CDN
|
||||
* Used for batch imports where images aren't yet in AWS
|
||||
*/
|
||||
export function getGameCdnSummonImage(id: string | number | null | undefined): string {
|
||||
if (!id) return getPlaceholderImage('summon', 'square')
|
||||
return `${GAME_CDN_BASE}/summon/s/${id}.jpg`
|
||||
}
|
||||
|
||||
// ===== Other Game Images =====
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -77,7 +77,8 @@
|
|||
gamewith: '',
|
||||
kamigame: '',
|
||||
nicknamesEn: [] as string[],
|
||||
nicknamesJp: [] as string[]
|
||||
nicknamesJp: [] as string[],
|
||||
promotions: [] as number[]
|
||||
})
|
||||
|
||||
// Populate edit data when summon loads
|
||||
|
|
@ -89,7 +90,7 @@
|
|||
granblueId: summon.granblueId || '',
|
||||
rarity: summon.rarity || 3,
|
||||
element: summon.element || 0,
|
||||
series: summon.series?.toString() ?? '',
|
||||
series: summon.series?.id || '',
|
||||
minHp: summon.hp?.minHp || 0,
|
||||
maxHp: summon.hp?.maxHp || 0,
|
||||
maxHpFlb: summon.hp?.maxHpFlb || 0,
|
||||
|
|
@ -115,7 +116,8 @@
|
|||
gamewith: summon.gamewith || '',
|
||||
kamigame: summon.kamigame || '',
|
||||
nicknamesEn: summon.nicknames?.en || [],
|
||||
nicknamesJp: summon.nicknames?.ja || []
|
||||
nicknamesJp: summon.nicknames?.ja || [],
|
||||
promotions: summon.promotions || []
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -159,7 +161,8 @@
|
|||
gamewith: editData.gamewith,
|
||||
kamigame: editData.kamigame,
|
||||
nicknames_en: editData.nicknamesEn,
|
||||
nicknames_jp: editData.nicknamesJp
|
||||
nicknames_jp: editData.nicknamesJp,
|
||||
promotions: editData.promotions
|
||||
}
|
||||
|
||||
await entityAdapter.updateSummon(summon.id, payload)
|
||||
|
|
|
|||
|
|
@ -157,6 +157,7 @@
|
|||
// Taxonomy
|
||||
element: editData.element,
|
||||
series: editData.series || undefined,
|
||||
promotions: editData.promotions,
|
||||
|
||||
// Stats - note: transcendence maps to xlb
|
||||
min_hp: editData.minHp,
|
||||
|
|
|
|||
Loading…
Reference in a new issue