add extra_prerequisite and forge chain to weapon database pages
This commit is contained in:
parent
33b578ec21
commit
a01c6e8d31
8 changed files with 604 additions and 7 deletions
364
src/lib/components/ui/WeaponTypeahead.svelte
Normal file
364
src/lib/components/ui/WeaponTypeahead.svelte
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
<!-- WeaponTypeahead Component - Async weapon search with Svelecte -->
|
||||
<svelte:options runes={true} />
|
||||
|
||||
<script lang="ts">
|
||||
import Svelecte from 'svelecte'
|
||||
import Icon from '../Icon.svelte'
|
||||
import { searchAdapter, type SearchResult } from '$lib/api/adapters/search.adapter'
|
||||
import { getWeaponGridImage } from '$lib/utils/images'
|
||||
|
||||
interface WeaponOption {
|
||||
id: string
|
||||
label: string
|
||||
granblueId: string
|
||||
element?: number
|
||||
}
|
||||
|
||||
interface Props {
|
||||
/** Selected weapon granblue ID (e.g. "1040001000") */
|
||||
value?: string | null
|
||||
/** Initial weapon data for display (when loading existing value) */
|
||||
initialWeapon?: { id: string; name: string; granblueId: string } | null
|
||||
/** Callback when value changes */
|
||||
onValueChange?: (granblueId: string | null) => void
|
||||
/** Placeholder text */
|
||||
placeholder?: string
|
||||
/** Disabled state */
|
||||
disabled?: boolean
|
||||
/** Component size */
|
||||
size?: 'small' | 'medium' | 'large'
|
||||
/** Clear button visible */
|
||||
clearable?: boolean
|
||||
/** Minimum characters before search */
|
||||
minQuery?: number
|
||||
/** Use contained styling (for use inside containers) */
|
||||
contained?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
value = $bindable(null),
|
||||
initialWeapon = null,
|
||||
onValueChange,
|
||||
placeholder = 'Search weapons...',
|
||||
disabled = false,
|
||||
size = 'medium',
|
||||
clearable = true,
|
||||
minQuery = 2,
|
||||
contained = false
|
||||
}: Props = $props()
|
||||
|
||||
let searchResults = $state<WeaponOption[]>([])
|
||||
// Only used when user selects something NEW (different from initialWeapon)
|
||||
let userSelectedOption = $state<WeaponOption | null>(null)
|
||||
let isLoading = $state(false)
|
||||
let searchTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
// Clear userSelectedOption when value is cleared
|
||||
$effect(() => {
|
||||
if (!value) {
|
||||
userSelectedOption = null
|
||||
}
|
||||
})
|
||||
|
||||
// Derive options: include initialWeapon or userSelectedOption so Svelecte can find the value
|
||||
const options = $derived.by(() => {
|
||||
const results = [...searchResults]
|
||||
|
||||
// If user selected something new, prioritize that
|
||||
const userSelected = userSelectedOption
|
||||
if (userSelected && !results.find((o) => o.granblueId === userSelected.granblueId)) {
|
||||
return [userSelected, ...results]
|
||||
}
|
||||
|
||||
// Otherwise, include initialWeapon if we have a value matching it
|
||||
if (value && initialWeapon && initialWeapon.granblueId === value) {
|
||||
const initOption: WeaponOption = {
|
||||
id: initialWeapon.id,
|
||||
label: initialWeapon.name,
|
||||
granblueId: initialWeapon.granblueId
|
||||
}
|
||||
if (!results.find((o) => o.granblueId === initOption.granblueId)) {
|
||||
return [initOption, ...results]
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
})
|
||||
|
||||
const typeaheadClasses = $derived(
|
||||
['weapon-typeahead', size, contained && 'contained', disabled && 'disabled']
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
)
|
||||
|
||||
async function searchWeapons(query: string) {
|
||||
if (query.length < minQuery) {
|
||||
searchResults = []
|
||||
return
|
||||
}
|
||||
|
||||
isLoading = true
|
||||
try {
|
||||
const response = await searchAdapter.searchWeapons({
|
||||
query,
|
||||
per: 20,
|
||||
locale: 'en'
|
||||
})
|
||||
|
||||
searchResults = response.results.map((result: SearchResult) => ({
|
||||
id: result.id,
|
||||
label: result.name?.en || result.name?.ja || result.granblueId,
|
||||
granblueId: result.granblueId,
|
||||
element: result.element
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('Weapon search error:', error)
|
||||
searchResults = []
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleInput(event: Event) {
|
||||
const target = event.target as HTMLInputElement | null
|
||||
const query = target?.value ?? ''
|
||||
|
||||
// Debounce the search
|
||||
if (searchTimeout) {
|
||||
clearTimeout(searchTimeout)
|
||||
}
|
||||
|
||||
searchTimeout = setTimeout(() => {
|
||||
searchWeapons(query)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
function handleChange(selected: WeaponOption | null) {
|
||||
const newValue = selected?.granblueId || null
|
||||
value = newValue
|
||||
// Only track as userSelectedOption if it's different from initialWeapon
|
||||
if (selected && initialWeapon && selected.granblueId === initialWeapon.granblueId) {
|
||||
userSelectedOption = null // Use initialWeapon instead
|
||||
} else {
|
||||
userSelectedOption = selected
|
||||
}
|
||||
onValueChange?.(newValue)
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class={typeaheadClasses} oninput={handleInput}>
|
||||
<Svelecte
|
||||
{options}
|
||||
value={value}
|
||||
labelField="label"
|
||||
valueField="granblueId"
|
||||
searchable={true}
|
||||
{placeholder}
|
||||
{disabled}
|
||||
{clearable}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{#snippet toggleIcon(dropdownShow)}
|
||||
<Icon name="chevron-down-small" size={14} class="chevron" />
|
||||
{/snippet}
|
||||
{#snippet option(opt)}
|
||||
{@const weapon = opt as WeaponOption}
|
||||
<div class="option-item">
|
||||
<img
|
||||
src={getWeaponGridImage(weapon.granblueId, weapon.element)}
|
||||
alt=""
|
||||
class="option-image"
|
||||
/>
|
||||
<span class="option-label">{weapon.label}</span>
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet selection(sel)}
|
||||
{@const weapon = (sel as WeaponOption[])[0]}
|
||||
{#if weapon}
|
||||
<div class="selection-item">
|
||||
<img
|
||||
src={getWeaponGridImage(weapon.granblueId, weapon.element)}
|
||||
alt=""
|
||||
class="selection-image"
|
||||
/>
|
||||
<span class="selection-label">{weapon.label}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</Svelecte>
|
||||
{#if isLoading}
|
||||
<span class="loading-indicator">...</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use '$src/themes/spacing' as *;
|
||||
@use '$src/themes/colors' as *;
|
||||
@use '$src/themes/typography' as *;
|
||||
@use '$src/themes/layout' as *;
|
||||
@use '$src/themes/mixins' as *;
|
||||
@use '$src/themes/effects' as *;
|
||||
|
||||
.weapon-typeahead {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
// Svelecte CSS variable overrides
|
||||
--sv-bg: var(--input-bg);
|
||||
--sv-border-color: transparent;
|
||||
--sv-border: 1px solid var(--sv-border-color);
|
||||
--sv-active-border: 1px solid #{$blue};
|
||||
--sv-active-outline: none;
|
||||
--sv-border-radius: #{$input-corner};
|
||||
--sv-min-height: #{$unit-4x};
|
||||
--sv-placeholder-color: var(--text-tertiary);
|
||||
--sv-color: var(--text-primary);
|
||||
|
||||
--sv-dropdown-bg: var(--dialog-bg);
|
||||
--sv-dropdown-border-radius: #{$card-corner};
|
||||
--sv-dropdown-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
--sv-dropdown-offset: #{$unit-half};
|
||||
|
||||
--sv-item-color: var(--text-primary);
|
||||
--sv-item-active-bg: var(--option-bg-hover);
|
||||
--sv-item-selected-bg: var(--option-bg-hover);
|
||||
|
||||
--sv-icon-color: var(--text-tertiary);
|
||||
--sv-icon-hover-color: var(--text-primary);
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// Target Svelecte control for hover states
|
||||
:global(.sv-control) {
|
||||
padding: calc($unit-half + 1px) $unit calc($unit-half + 1px) $unit-half;
|
||||
@include smooth-transition($duration-quick, background-color, border-color);
|
||||
}
|
||||
|
||||
&:hover:not(.disabled) :global(.sv-control) {
|
||||
background-color: var(--input-bg-hover);
|
||||
}
|
||||
|
||||
// Contained variant
|
||||
&.contained {
|
||||
--sv-bg: var(--select-contained-bg);
|
||||
|
||||
&:hover:not(.disabled) :global(.sv-control) {
|
||||
background-color: var(--select-contained-bg-hover);
|
||||
}
|
||||
}
|
||||
|
||||
// Style the dropdown
|
||||
:global(.sv_dropdown) {
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
max-height: 40vh;
|
||||
z-index: 102;
|
||||
}
|
||||
|
||||
// Style dropdown items
|
||||
:global(.sv-item) {
|
||||
border-radius: $item-corner-small;
|
||||
padding: $unit $unit-2x;
|
||||
gap: $unit;
|
||||
@include smooth-transition($duration-quick, background-color);
|
||||
}
|
||||
|
||||
// Style the input text
|
||||
:global(.sv-input--text) {
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
// Style the indicator buttons
|
||||
:global(.sv-btn-indicator) {
|
||||
color: var(--text-tertiary);
|
||||
@include smooth-transition($duration-quick, color);
|
||||
|
||||
&:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
// Style our custom chevron icon
|
||||
:global(.chevron) {
|
||||
flex-shrink: 0;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
// Hide the separator bar between buttons
|
||||
:global(.sv-btn-separator) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Custom option item styling
|
||||
.option-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $unit;
|
||||
}
|
||||
|
||||
.option-image {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: $item-corner-small;
|
||||
flex-shrink: 0;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.option-label {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// Custom selection item styling (shown in input when value selected)
|
||||
.selection-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $unit-half;
|
||||
}
|
||||
|
||||
.selection-image {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: $item-corner-small;
|
||||
flex-shrink: 0;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.selection-label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
// Size variants
|
||||
.weapon-typeahead.small {
|
||||
--sv-min-height: #{$unit-3x};
|
||||
--sv-font-size: #{$font-small};
|
||||
}
|
||||
|
||||
.weapon-typeahead.medium {
|
||||
--sv-min-height: #{$unit-4x};
|
||||
--sv-font-size: #{$font-regular};
|
||||
}
|
||||
|
||||
.weapon-typeahead.large {
|
||||
--sv-min-height: calc(#{$unit} * 6);
|
||||
--sv-font-size: #{$font-large};
|
||||
}
|
||||
|
||||
.loading-indicator {
|
||||
position: absolute;
|
||||
right: $unit-3x;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--text-tertiary);
|
||||
font-size: $font-small;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
<svelte:options runes={true} />
|
||||
|
||||
<script lang="ts">
|
||||
import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte'
|
||||
import DetailItem from '$lib/components/ui/DetailItem.svelte'
|
||||
import WeaponTypeahead from '$lib/components/ui/WeaponTypeahead.svelte'
|
||||
import { getWeaponGridImage } from '$lib/utils/images'
|
||||
|
||||
interface Props {
|
||||
weapon: any
|
||||
editMode?: boolean
|
||||
editData?: any
|
||||
}
|
||||
|
||||
let { weapon, editMode = false, editData = $bindable() }: Props = $props()
|
||||
|
||||
// Get forge chain for display (only in view mode)
|
||||
const forgeChain = $derived(weapon?.forgeChain ?? [])
|
||||
const forgedFrom = $derived(weapon?.forgedFrom ?? null)
|
||||
const forgeOrder = $derived(editMode ? editData?.forgeOrder : weapon?.forgeOrder)
|
||||
|
||||
// Check if weapon has any forge data
|
||||
const hasForgeData = $derived(
|
||||
forgeChain.length > 0 || forgedFrom != null || forgeOrder != null || editMode
|
||||
)
|
||||
|
||||
// Get initial weapon data for typeahead
|
||||
const initialForgedFrom = $derived.by(() => {
|
||||
if (!forgedFrom) return null
|
||||
return {
|
||||
id: forgedFrom.id,
|
||||
name: forgedFrom.name?.en || forgedFrom.name?.ja || forgedFrom.granblueId,
|
||||
granblueId: forgedFrom.granblueId
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if hasForgeData}
|
||||
<DetailsContainer title="Forge Chain">
|
||||
{#if editMode}
|
||||
<DetailItem label="Forged From" sublabel="The weapon this was forged from">
|
||||
<WeaponTypeahead
|
||||
bind:value={editData.forgedFrom}
|
||||
initialWeapon={initialForgedFrom}
|
||||
placeholder="Search for base weapon..."
|
||||
contained
|
||||
/>
|
||||
</DetailItem>
|
||||
<DetailItem
|
||||
label="Forge Order"
|
||||
sublabel="Position in chain (0=base, 1=first forge, etc.)"
|
||||
bind:value={editData.forgeOrder}
|
||||
editable={true}
|
||||
type="number"
|
||||
/>
|
||||
{:else}
|
||||
{#if forgeChain.length > 0}
|
||||
<DetailItem label="Forge Chain">
|
||||
<div class="forge-chain">
|
||||
{#each forgeChain as chainWeapon, index}
|
||||
<a
|
||||
href="/database/weapons/{chainWeapon.granblueId}"
|
||||
class="chain-item"
|
||||
class:current={chainWeapon.granblueId === weapon.granblueId}
|
||||
>
|
||||
<img
|
||||
src={getWeaponGridImage(chainWeapon.granblueId, weapon.element)}
|
||||
alt=""
|
||||
class="chain-image"
|
||||
/>
|
||||
<span class="chain-name">{chainWeapon.name?.en || chainWeapon.name?.ja}</span>
|
||||
<span class="chain-order">({chainWeapon.forgeOrder})</span>
|
||||
</a>
|
||||
{#if index < forgeChain.length - 1}
|
||||
<span class="chain-arrow">→</span>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</DetailItem>
|
||||
{/if}
|
||||
|
||||
{#if forgedFrom && forgeChain.length === 0}
|
||||
<DetailItem label="Forged From">
|
||||
<a href="/database/weapons/{forgedFrom.granblueId}" class="forged-from-link">
|
||||
{forgedFrom.name?.en || forgedFrom.name?.ja}
|
||||
</a>
|
||||
</DetailItem>
|
||||
{/if}
|
||||
|
||||
{#if forgeOrder != null}
|
||||
<DetailItem label="Forge Order" value={forgeOrder.toString()} />
|
||||
{/if}
|
||||
{/if}
|
||||
</DetailsContainer>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
@use '$src/themes/spacing' as spacing;
|
||||
@use '$src/themes/colors' as colors;
|
||||
@use '$src/themes/typography' as typography;
|
||||
@use '$src/themes/layout' as layout;
|
||||
|
||||
.forge-chain {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: spacing.$unit;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.chain-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: spacing.$unit-half;
|
||||
padding: spacing.$unit-half spacing.$unit;
|
||||
background: var(--card-bg);
|
||||
border-radius: layout.$item-corner-small;
|
||||
text-decoration: none;
|
||||
color: var(--text-primary);
|
||||
transition: background-color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--card-bg-hover);
|
||||
}
|
||||
|
||||
&.current {
|
||||
background: colors.$blue--bg--light;
|
||||
outline: 1px solid colors.$blue;
|
||||
}
|
||||
}
|
||||
|
||||
.chain-image {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.chain-name {
|
||||
font-size: typography.$font-small;
|
||||
}
|
||||
|
||||
.chain-order {
|
||||
font-size: typography.$font-xs;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.chain-arrow {
|
||||
color: var(--text-tertiary);
|
||||
font-size: typography.$font-small;
|
||||
}
|
||||
|
||||
.forged-from-link {
|
||||
color: colors.$blue;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -46,6 +46,22 @@
|
|||
const uncapLevel = $derived(transcendence ? 6 : ulb ? 5 : flb ? 4 : 3)
|
||||
const transcendenceStage = $derived(transcendence ? 5 : 0)
|
||||
|
||||
// Extra prerequisite options for dropdown
|
||||
const extraPrerequisiteOptions = [
|
||||
{ value: '', label: 'None' },
|
||||
{ value: 3, label: 'MLB' },
|
||||
{ value: 4, label: 'FLB' },
|
||||
{ value: 5, label: 'ULB' },
|
||||
{ value: 6, label: 'Transcendence' }
|
||||
]
|
||||
|
||||
// Get label for extra prerequisite value
|
||||
function getExtraPrerequisiteLabel(value: number | null | undefined): string {
|
||||
if (value == null) return '—'
|
||||
const option = extraPrerequisiteOptions.find((o) => o.value === value)
|
||||
return option?.label ?? '—'
|
||||
}
|
||||
|
||||
// Get element name for checkbox theming
|
||||
const elementName = $derived.by((): ElementName | undefined => {
|
||||
const el = editMode ? editData.element : weapon?.element
|
||||
|
|
@ -132,5 +148,15 @@
|
|||
element={elementName}
|
||||
onchange={handleTranscendenceChange}
|
||||
/>
|
||||
<DetailItem
|
||||
label="Extra Prerequisite"
|
||||
sublabel="Min uncap for Additional Weapons"
|
||||
bind:value={editData.extraPrerequisite}
|
||||
editable={true}
|
||||
type="select"
|
||||
options={extraPrerequisiteOptions}
|
||||
/>
|
||||
{:else if weapon?.uncap?.extraPrerequisite != null}
|
||||
<DetailItem label="Extra Prerequisite" value={getExtraPrerequisiteLabel(weapon.uncap.extraPrerequisite)} />
|
||||
{/if}
|
||||
</DetailsContainer>
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ export interface Weapon {
|
|||
flb: boolean
|
||||
ulb: boolean
|
||||
transcendence: boolean
|
||||
extraPrerequisite?: number | null
|
||||
}
|
||||
transcendenceHp?: number
|
||||
transcendenceAtk?: number
|
||||
|
|
@ -57,6 +58,10 @@ export interface Weapon {
|
|||
kamigame?: string
|
||||
nicknames?: { en?: string[]; ja?: string[] }
|
||||
recruits?: string | { id: string; granblueId: string; name: LocalizedName }
|
||||
// Forge chain fields
|
||||
forgeOrder?: number | null
|
||||
forgedFrom?: { id: string; granblueId: string; name: LocalizedName } | null
|
||||
forgeChain?: Array<{ id: string; granblueId: string; name: LocalizedName; forgeOrder: number }> | null
|
||||
}
|
||||
|
||||
// Character entity from CharacterBlueprint
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
import WeaponTaxonomySection from '$lib/features/database/weapons/sections/WeaponTaxonomySection.svelte'
|
||||
import WeaponStatsSection from '$lib/features/database/weapons/sections/WeaponStatsSection.svelte'
|
||||
import WeaponGachaSection from '$lib/features/database/weapons/sections/WeaponGachaSection.svelte'
|
||||
import WeaponForgeSection from '$lib/features/database/weapons/sections/WeaponForgeSection.svelte'
|
||||
import EntityImagesTab from '$lib/features/database/detail/tabs/EntityImagesTab.svelte'
|
||||
import EntityRawDataTab from '$lib/features/database/detail/tabs/EntityRawDataTab.svelte'
|
||||
import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte'
|
||||
|
|
@ -205,6 +206,7 @@
|
|||
<WeaponUncapSection {weapon} />
|
||||
<WeaponTaxonomySection {weapon} />
|
||||
<WeaponStatsSection {weapon} />
|
||||
<WeaponForgeSection {weapon} />
|
||||
|
||||
<DetailsContainer title="Nicknames">
|
||||
<DetailItem label="English">
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
import WeaponTaxonomySection from '$lib/features/database/weapons/sections/WeaponTaxonomySection.svelte'
|
||||
import WeaponStatsSection from '$lib/features/database/weapons/sections/WeaponStatsSection.svelte'
|
||||
import WeaponGachaSection from '$lib/features/database/weapons/sections/WeaponGachaSection.svelte'
|
||||
import WeaponForgeSection from '$lib/features/database/weapons/sections/WeaponForgeSection.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'
|
||||
|
|
@ -74,6 +75,7 @@
|
|||
flb: false,
|
||||
ulb: false,
|
||||
transcendence: false,
|
||||
extraPrerequisite: '' as number | '',
|
||||
extra: false,
|
||||
limit: false,
|
||||
ax: false,
|
||||
|
|
@ -88,7 +90,10 @@
|
|||
kamigame: '',
|
||||
nicknamesEn: [] as string[],
|
||||
nicknamesJp: [] as string[],
|
||||
recruits: ''
|
||||
recruits: '',
|
||||
// Forge chain fields
|
||||
forgedFrom: '' as string | null,
|
||||
forgeOrder: null as number | null
|
||||
})
|
||||
|
||||
// Populate edit data when weapon loads
|
||||
|
|
@ -117,6 +122,7 @@
|
|||
flb: weapon.uncap?.flb || false,
|
||||
ulb: weapon.uncap?.ulb || false,
|
||||
transcendence: weapon.uncap?.transcendence || false,
|
||||
extraPrerequisite: weapon.uncap?.extraPrerequisite ?? '',
|
||||
extra: weapon.extra || false,
|
||||
limit: Boolean(weapon.limit),
|
||||
ax: weapon.ax || false,
|
||||
|
|
@ -131,7 +137,10 @@
|
|||
kamigame: weapon.kamigame || '',
|
||||
nicknamesEn: weapon.nicknames?.en || [],
|
||||
nicknamesJp: weapon.nicknames?.ja || [],
|
||||
recruits: typeof weapon.recruits === 'string' ? weapon.recruits : (weapon.recruits?.granblueId ?? '')
|
||||
recruits: typeof weapon.recruits === 'string' ? weapon.recruits : (weapon.recruits?.granblueId ?? ''),
|
||||
// Forge chain fields
|
||||
forgedFrom: weapon.forgedFrom?.granblueId || null,
|
||||
forgeOrder: weapon.forgeOrder ?? null
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -165,6 +174,7 @@
|
|||
flb: editData.flb,
|
||||
ulb: editData.ulb,
|
||||
transcendence: editData.transcendence,
|
||||
extra_prerequisite: editData.extraPrerequisite === '' ? null : editData.extraPrerequisite,
|
||||
extra: editData.extra,
|
||||
limit: editData.limit,
|
||||
ax: editData.ax,
|
||||
|
|
@ -179,7 +189,10 @@
|
|||
kamigame: editData.kamigame,
|
||||
nicknames_en: editData.nicknamesEn,
|
||||
nicknames_jp: editData.nicknamesJp,
|
||||
recruits: editData.recruits || undefined
|
||||
recruits: editData.recruits || undefined,
|
||||
// Forge chain fields
|
||||
forged_from: editData.forgedFrom || null,
|
||||
forge_order: editData.forgeOrder
|
||||
}
|
||||
|
||||
await entityAdapter.updateWeapon(weapon.id, payload)
|
||||
|
|
@ -226,6 +239,7 @@
|
|||
<WeaponUncapSection {weapon} {editMode} bind:editData />
|
||||
<WeaponTaxonomySection {weapon} {editMode} bind:editData />
|
||||
<WeaponStatsSection {weapon} {editMode} bind:editData />
|
||||
<WeaponForgeSection {weapon} {editMode} bind:editData />
|
||||
|
||||
<DetailsContainer title="Nicknames">
|
||||
<DetailItem label="Nicknames (EN)">
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
import WeaponStatsSection from '$lib/features/database/weapons/sections/WeaponStatsSection.svelte'
|
||||
import WeaponMetadataSection from '$lib/features/database/weapons/sections/WeaponMetadataSection.svelte'
|
||||
import WeaponGachaSection from '$lib/features/database/weapons/sections/WeaponGachaSection.svelte'
|
||||
import WeaponForgeSection from '$lib/features/database/weapons/sections/WeaponForgeSection.svelte'
|
||||
import TabbedEntitySelector from '$lib/features/database/import/TabbedEntitySelector.svelte'
|
||||
import type { EntityTab } from '$lib/features/database/import/TabbedEntitySelector.svelte'
|
||||
import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte'
|
||||
|
|
@ -123,6 +124,7 @@
|
|||
flb: suggestions?.flb ?? false,
|
||||
ulb: suggestions?.ulb ?? false,
|
||||
transcendence: suggestions?.transcendence ?? false,
|
||||
extraPrerequisite: '' as number | '',
|
||||
extra: false,
|
||||
limit: false,
|
||||
ax: false,
|
||||
|
|
@ -136,7 +138,10 @@
|
|||
kamigame: suggestions?.kamigame ?? '',
|
||||
nicknamesEn: [] as string[],
|
||||
nicknamesJp: [] as string[],
|
||||
recruits: suggestions?.recruits ?? null
|
||||
recruits: suggestions?.recruits ?? null,
|
||||
// Forge chain
|
||||
forgedFrom: null as string | null,
|
||||
forgeOrder: null as number | null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -279,6 +284,7 @@
|
|||
flb: formData.flb,
|
||||
ulb: formData.ulb,
|
||||
transcendence: formData.transcendence,
|
||||
extra_prerequisite: formData.extraPrerequisite === '' ? null : formData.extraPrerequisite,
|
||||
extra: formData.extra,
|
||||
limit: formData.limit,
|
||||
ax: formData.ax,
|
||||
|
|
@ -293,7 +299,10 @@
|
|||
nicknames_en: formData.nicknamesEn,
|
||||
nicknames_jp: formData.nicknamesJp,
|
||||
recruits: formData.recruits,
|
||||
wiki_raw: wikiRawByPage[selectedWikiPage] || undefined
|
||||
wiki_raw: wikiRawByPage[selectedWikiPage] || undefined,
|
||||
// Forge chain
|
||||
forged_from: formData.forgedFrom || null,
|
||||
forge_order: formData.forgeOrder
|
||||
}
|
||||
|
||||
const newWeapon = await entityAdapter.createWeapon(payload)
|
||||
|
|
@ -510,6 +519,12 @@
|
|||
onDismissSuggestion={handleDismissSuggestion}
|
||||
/>
|
||||
|
||||
<WeaponForgeSection
|
||||
weapon={emptyWeapon}
|
||||
editMode={true}
|
||||
bind:editData={formDataByPage[selectedWikiPage]}
|
||||
/>
|
||||
|
||||
<DetailsContainer title="Nicknames">
|
||||
<DetailItem label="Nicknames (EN)">
|
||||
<TagInput bind:value={formDataByPage[selectedWikiPage].nicknamesEn} placeholder="Add nickname..." contained />
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
import WeaponUncapSection from '$lib/features/database/weapons/sections/WeaponUncapSection.svelte'
|
||||
import WeaponTaxonomySection from '$lib/features/database/weapons/sections/WeaponTaxonomySection.svelte'
|
||||
import WeaponStatsSection from '$lib/features/database/weapons/sections/WeaponStatsSection.svelte'
|
||||
import WeaponForgeSection from '$lib/features/database/weapons/sections/WeaponForgeSection.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'
|
||||
|
|
@ -79,6 +80,7 @@
|
|||
flb: false,
|
||||
ulb: false,
|
||||
transcendence: false,
|
||||
extraPrerequisite: '' as number | '',
|
||||
extra: false,
|
||||
limit: false,
|
||||
ax: false,
|
||||
|
|
@ -100,7 +102,11 @@
|
|||
nicknamesJp: [] as string[],
|
||||
|
||||
// Recruits (Character ID)
|
||||
recruits: null as string | null
|
||||
recruits: null as string | null,
|
||||
|
||||
// Forge chain
|
||||
forgedFrom: null as string | null,
|
||||
forgeOrder: null as number | null
|
||||
})
|
||||
|
||||
const rarityOptions = getRarityOptions()
|
||||
|
|
@ -183,6 +189,7 @@
|
|||
flb: editData.flb,
|
||||
ulb: editData.ulb,
|
||||
transcendence: editData.transcendence,
|
||||
extra_prerequisite: editData.extraPrerequisite === '' ? null : editData.extraPrerequisite,
|
||||
extra: editData.extra,
|
||||
limit: editData.limit,
|
||||
ax: editData.ax,
|
||||
|
|
@ -204,7 +211,11 @@
|
|||
nicknames_jp: editData.nicknamesJp,
|
||||
|
||||
// Recruits
|
||||
recruits: editData.recruits
|
||||
recruits: editData.recruits,
|
||||
|
||||
// Forge chain
|
||||
forged_from: editData.forgedFrom || null,
|
||||
forge_order: editData.forgeOrder
|
||||
}
|
||||
|
||||
const newWeapon = await entityAdapter.createWeapon(payload)
|
||||
|
|
@ -285,6 +296,7 @@
|
|||
<WeaponUncapSection weapon={emptyWeapon} {editMode} bind:editData />
|
||||
<WeaponTaxonomySection weapon={emptyWeapon} {editMode} bind:editData />
|
||||
<WeaponStatsSection weapon={emptyWeapon} {editMode} bind:editData />
|
||||
<WeaponForgeSection weapon={emptyWeapon} {editMode} bind:editData />
|
||||
|
||||
<DetailsContainer title="Nicknames">
|
||||
<DetailItem label="Nicknames (EN)">
|
||||
|
|
|
|||
Loading…
Reference in a new issue