database detail pages: add Info/Images/Raw tabs

This commit is contained in:
Justin Edmund 2025-12-01 09:54:35 -08:00
parent 395a5c166f
commit be75fcbcbd
7 changed files with 561 additions and 881 deletions

View file

@ -11,7 +11,7 @@
type: 'character' | 'summon' | 'weapon' type: 'character' | 'summon' | 'weapon'
item: any // The character/summon/weapon object item: any // The character/summon/weapon object
image: string image: string
onEdit?: () => void // Optional edit handler editUrl?: string // URL to navigate to for editing (view mode)
showEdit?: boolean // Whether to show the edit button showEdit?: boolean // Whether to show the edit button
editMode?: boolean // Whether currently in edit mode editMode?: boolean // Whether currently in edit mode
onSave?: () => void // Save handler onSave?: () => void // Save handler
@ -23,7 +23,7 @@
type, type,
item, item,
image, image,
onEdit, editUrl,
showEdit = false, showEdit = false,
editMode = false, editMode = false,
onSave, onSave,
@ -124,8 +124,8 @@
> >
{isSaving ? 'Saving...' : 'Save'} {isSaving ? 'Saving...' : 'Save'}
</Button> </Button>
{:else} {:else if editUrl}
<Button variant="secondary" size="medium" onclick={onEdit}>Edit</Button> <Button variant="secondary" size="medium" href={editUrl}>Edit</Button>
{/if} {/if}
</div> </div>
{/if} {/if}

View file

@ -1,104 +1,142 @@
<svelte:options runes={true} /> <svelte:options runes={true} />
<script lang="ts"> <script lang="ts">
import DetailsHeader from '$lib/components/ui/DetailsHeader.svelte' import DetailsHeader from '$lib/components/ui/DetailsHeader.svelte'
import type { Snippet } from 'svelte' import SegmentedControl from '$lib/components/ui/segmented-control/SegmentedControl.svelte'
import Segment from '$lib/components/ui/segmented-control/Segment.svelte'
import type { Snippet } from 'svelte'
interface Props { export type DetailTab = 'info' | 'images' | 'raw'
type: 'character' | 'summon' | 'weapon'
item: any
image: string
showEdit?: boolean
editMode?: boolean
isSaving?: boolean
saveSuccess?: boolean
saveError?: string | null
onEdit?: () => void
onSave?: () => void
onCancel?: () => void
}
let { interface Props {
type, type: 'character' | 'summon' | 'weapon'
item, item: any
image, image: string
showEdit = false, showEdit?: boolean
editMode = false, editUrl?: string
isSaving = false, editMode?: boolean
saveSuccess = false, isSaving?: boolean
saveError = null, saveSuccess?: boolean
onEdit, saveError?: string | null
onSave, onSave?: () => void
onCancel, onCancel?: () => void
children // Tab navigation
}: Props & { children: Snippet } = $props() currentTab?: DetailTab
onTabChange?: (tab: DetailTab) => void
showTabs?: boolean
}
let {
type,
item,
image,
showEdit = false,
editUrl,
editMode = false,
isSaving = false,
saveSuccess = false,
saveError = null,
onSave,
onCancel,
currentTab = 'info',
onTabChange,
showTabs = true,
children
}: Props & { children: Snippet } = $props()
function handleTabChange(value: string) {
onTabChange?.(value as DetailTab)
}
</script> </script>
<div class="content"> <div class="content">
<DetailsHeader <DetailsHeader
{type} {type}
{item} {item}
{image} {image}
{editMode} {editMode}
showEdit={showEdit} {showEdit}
onEdit={onEdit ?? (() => {})} {editUrl}
onSave={onSave ?? (() => {})} onSave={onSave ?? (() => {})}
onCancel={onCancel ?? (() => {})} onCancel={onCancel ?? (() => {})}
{isSaving} {isSaving}
/> />
{#if saveSuccess || saveError} {#if showTabs && !editMode}
<div class="edit-controls"> <div class="tab-navigation">
{#if saveSuccess} <SegmentedControl
<span class="success-message">Changes saved successfully!</span> value={currentTab}
{/if} onValueChange={handleTabChange}
{#if saveError} variant="background"
<span class="error-message">{saveError}</span> size="small"
{/if} >
</div> <Segment value="info">Info</Segment>
{/if} <Segment value="images">Images</Segment>
<Segment value="raw">Raw Data</Segment>
</SegmentedControl>
</div>
{/if}
{@render children?.()} {#if saveSuccess || saveError}
<div class="edit-controls">
{#if saveSuccess}
<span class="success-message">Changes saved successfully!</span>
{/if}
{#if saveError}
<span class="error-message">{saveError}</span>
{/if}
</div>
{/if}
{@render children?.()}
</div> </div>
<style lang="scss"> <style lang="scss">
@use '$src/themes/colors' as colors; @use '$src/themes/colors' as colors;
@use '$src/themes/layout' as layout; @use '$src/themes/layout' as layout;
@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/effects' as effects; @use '$src/themes/effects' as effects;
.content { .content {
background: white; background: white;
border-radius: layout.$card-corner; border-radius: layout.$card-corner;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
overflow: visible; overflow: visible;
margin-top: spacing.$unit-2x; margin-top: spacing.$unit-2x;
position: relative; position: relative;
} }
.edit-controls { .tab-navigation {
padding: spacing.$unit-2x; padding: spacing.$unit-2x;
border-bottom: 1px solid colors.$grey-80; }
display: flex;
gap: spacing.$unit;
align-items: center;
.success-message { .edit-controls {
color: colors.$grey-30; padding: spacing.$unit-2x;
font-size: typography.$font-small; border-bottom: 1px solid colors.$grey-80;
animation: fadeIn effects.$duration-opacity-fade ease-in; display: flex;
} gap: spacing.$unit;
align-items: center;
.error-message { .success-message {
color: colors.$error; color: colors.$grey-30;
font-size: typography.$font-small; font-size: typography.$font-small;
animation: fadeIn effects.$duration-opacity-fade ease-in; animation: fadeIn effects.$duration-opacity-fade ease-in;
} }
}
@keyframes fadeIn { .error-message {
from { opacity: 0; } color: colors.$error;
to { opacity: 1; } font-size: typography.$font-small;
} animation: fadeIn effects.$duration-opacity-fade ease-in;
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
</style> </style>

View file

@ -0,0 +1,77 @@
<svelte:options runes={true} />
<script lang="ts">
export interface ImageItem {
url: string
label: string
variant: string
pose?: string
}
interface Props {
images: ImageItem[]
}
let { images }: Props = $props()
</script>
<div class="images-tab">
<div class="images-grid">
{#each images as image}
<div class="image-item">
<a href={image.url} target="_blank" rel="noopener noreferrer">
<img src={image.url} alt={image.label} loading="lazy" />
</a>
<span class="image-label">{image.label}</span>
</div>
{/each}
</div>
</div>
<style lang="scss">
@use '$src/themes/colors' as colors;
@use '$src/themes/spacing' as spacing;
@use '$src/themes/typography' as typography;
@use '$src/themes/layout' as layout;
.images-tab {
padding: spacing.$unit-2x;
}
.images-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: spacing.$unit-2x;
}
.image-item {
display: flex;
flex-direction: column;
align-items: center;
gap: spacing.$unit;
a {
display: block;
border-radius: layout.$item-corner;
overflow: hidden;
transition: transform 0.2s ease;
&:hover {
transform: scale(1.02);
}
}
img {
display: block;
max-width: 100%;
height: auto;
background: colors.$grey-90;
}
}
.image-label {
font-size: typography.$font-small;
color: colors.$grey-40;
text-align: center;
}
</style>

View file

@ -0,0 +1,113 @@
<svelte:options runes={true} />
<script lang="ts">
import SegmentedControl from '$lib/components/ui/segmented-control/SegmentedControl.svelte'
import Segment from '$lib/components/ui/segmented-control/Segment.svelte'
interface Props {
wikiRaw?: string | null
gameRawEn?: Record<string, unknown> | null
gameRawJp?: Record<string, unknown> | null
isLoading?: boolean
}
let { wikiRaw, gameRawEn, gameRawJp, isLoading = false }: Props = $props()
let selectedLang = $state('en')
const currentGameRaw = $derived(selectedLang === 'en' ? gameRawEn : gameRawJp)
const formattedGameRaw = $derived(
currentGameRaw ? JSON.stringify(currentGameRaw, null, 2) : null
)
</script>
<div class="raw-data-tab">
{#if isLoading}
<p class="loading">Loading raw data...</p>
{:else}
{#if wikiRaw}
<section class="raw-section">
<h3>Wiki Raw</h3>
<pre class="raw-content">{wikiRaw}</pre>
</section>
{/if}
{#if gameRawEn || gameRawJp}
<section class="raw-section">
<div class="section-header">
<h3>Game Raw</h3>
<SegmentedControl bind:value={selectedLang} variant="background" size="small">
<Segment value="en">EN</Segment>
<Segment value="jp">JP</Segment>
</SegmentedControl>
</div>
{#if formattedGameRaw}
<pre class="raw-content">{formattedGameRaw}</pre>
{:else}
<p class="no-data">No {selectedLang.toUpperCase()} data available</p>
{/if}
</section>
{/if}
{#if !wikiRaw && !gameRawEn && !gameRawJp}
<p class="no-data">No raw data available</p>
{/if}
{/if}
</div>
<style lang="scss">
@use '$src/themes/colors' as colors;
@use '$src/themes/spacing' as spacing;
@use '$src/themes/typography' as typography;
@use '$src/themes/layout' as layout;
.raw-data-tab {
padding: spacing.$unit-2x;
display: flex;
flex-direction: column;
gap: spacing.$unit-3x;
}
.raw-section {
h3 {
font-size: typography.$font-regular;
font-weight: 600;
color: colors.$grey-20;
margin: 0 0 spacing.$unit 0;
}
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: spacing.$unit-2x;
margin-bottom: spacing.$unit;
}
.raw-content {
background: colors.$grey-95;
border: 1px solid colors.$grey-80;
border-radius: layout.$item-corner;
padding: spacing.$unit-2x;
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Mono', 'Droid Sans Mono', monospace;
font-size: typography.$font-small;
line-height: 1.5;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-word;
max-height: 500px;
overflow-y: auto;
margin: 0;
}
.no-data {
color: colors.$grey-50;
font-style: italic;
}
.loading {
color: colors.$grey-50;
font-style: italic;
}
</style>

View file

@ -3,29 +3,44 @@
<script lang="ts"> <script lang="ts">
// SvelteKit imports // SvelteKit imports
import { goto } from '$app/navigation' import { goto } from '$app/navigation'
import { page } from '$app/stores'
// TanStack Query // TanStack Query
import { createQuery, useQueryClient } from '@tanstack/svelte-query' import { createQuery } from '@tanstack/svelte-query'
import { entityQueries } from '$lib/api/queries/entity.queries' import { entityQueries } from '$lib/api/queries/entity.queries'
import { entityAdapter } from '$lib/api/adapters/entity.adapter' import { entityAdapter } from '$lib/api/adapters/entity.adapter'
import { withInitialData } from '$lib/query/ssr' import { withInitialData } from '$lib/query/ssr'
// Components // Components
import DetailScaffold from '$lib/features/database/detail/DetailScaffold.svelte' import DetailScaffold, { type DetailTab } from '$lib/features/database/detail/DetailScaffold.svelte'
import CharacterMetadataSection from '$lib/features/database/characters/sections/CharacterMetadataSection.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 CharacterImagesSection from '$lib/features/database/characters/sections/CharacterImagesSection.svelte' import CharacterImagesSection from '$lib/features/database/characters/sections/CharacterImagesSection.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' import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte'
import { getCharacterImage } from '$lib/utils/images' import { getCharacterImage } from '$lib/utils/images'
// Types // Types
import type { PageData } from './$types' import type { PageData } from './$types'
import type { ImageItem } from '$lib/features/database/detail/tabs/EntityImagesTab.svelte'
let { data }: { data: PageData } = $props() let { data }: { data: PageData } = $props()
const queryClient = useQueryClient() // Tab state from URL
const currentTab = $derived(($page.url.searchParams.get('tab') as DetailTab) || 'info')
function handleTabChange(tab: DetailTab) {
const url = new URL($page.url)
if (tab === 'info') {
url.searchParams.delete('tab')
} else {
url.searchParams.set('tab', tab)
}
goto(url.toString(), { replaceState: true })
}
// Use TanStack Query with SSR initial data // Use TanStack Query with SSR initial data
const characterQuery = createQuery(() => ({ const characterQuery = createQuery(() => ({
@ -38,8 +53,8 @@
const userRole = $derived(data.role || 0) const userRole = $derived(data.role || 0)
const canEdit = $derived(userRole >= 7) const canEdit = $derived(userRole >= 7)
// Edit mode state // Edit URL for navigation
let editMode = $state(false) const editUrl = $derived(character?.id ? `/database/characters/${character.id}/edit` : undefined)
// Query for related characters (same character_id) // Query for related characters (same character_id)
const relatedQuery = createQuery(() => ({ const relatedQuery = createQuery(() => ({
@ -48,152 +63,59 @@
if (!character?.id) return [] if (!character?.id) return []
return entityAdapter.getRelatedCharacters(character.id) return entityAdapter.getRelatedCharacters(character.id)
}, },
enabled: !!character?.characterId && !editMode enabled: !!character?.characterId
})) }))
let isSaving = $state(false) // Query for raw data (only when on raw tab)
let saveError = $state<string | null>(null) const rawDataQuery = createQuery(() => ({
let saveSuccess = $state(false) queryKey: ['characters', 'raw', character?.id],
queryFn: async () => {
// Editable fields - create reactive state for each field if (!character?.id) return null
let editData = $state({ return entityAdapter.getCharacterRawData(character.id)
name: character?.name || '', },
granblueId: character?.granblueId || '', enabled: currentTab === 'raw' && !!character?.id
characterId: character?.characterId ?? (null as number | null), }))
rarity: character?.rarity || 1,
element: character?.element || 0,
race1: character?.race?.[0] ?? null,
race2: character?.race?.[1] ?? null,
gender: character?.gender || 0,
proficiency1: character?.proficiency?.[0] || 0,
proficiency2: character?.proficiency?.[1] || 0,
minHp: character?.hp?.minHp || 0,
maxHp: character?.hp?.maxHp || 0,
maxHpFlb: character?.hp?.maxHpFlb || 0,
minAtk: character?.atk?.minAtk || 0,
maxAtk: character?.atk?.maxAtk || 0,
maxAtkFlb: character?.atk?.maxAtkFlb || 0,
flb: character?.uncap?.flb || false,
ulb: character?.uncap?.ulb || false,
transcendence: character?.uncap?.transcendence || false,
special: character?.special || false
})
// Reset edit data when character changes
$effect(() => {
if (character) {
editData = {
name: character.name || '',
granblueId: character.granblueId || '',
characterId: character.characterId ?? null,
rarity: character.rarity || 1,
element: character.element || 0,
race1: character.race?.[0] ?? null,
race2: character.race?.[1] ?? null,
gender: character.gender || 0,
proficiency1: character.proficiency?.[0] || 0,
proficiency2: character.proficiency?.[1] || 0,
minHp: character.hp?.minHp || 0,
maxHp: character.hp?.maxHp || 0,
maxHpFlb: character.hp?.maxHpFlb || 0,
minAtk: character.atk?.minAtk || 0,
maxAtk: character.atk?.maxAtk || 0,
maxAtkFlb: character.atk?.maxAtkFlb || 0,
flb: character.uncap?.flb || false,
ulb: character.uncap?.ulb || false,
transcendence: character.uncap?.transcendence || false,
special: character.special || false
}
}
})
function toggleEditMode() {
editMode = !editMode
saveError = null
saveSuccess = false
// Reset data when canceling
if (!editMode && character) {
editData = {
name: character.name || '',
granblueId: character.granblueId || '',
characterId: character.characterId ?? null,
rarity: character.rarity || 1,
element: character.element || 0,
race1: character.race?.[0] ?? null,
race2: character.race?.[1] ?? null,
gender: character.gender || 0,
proficiency1: character.proficiency?.[0] || 0,
proficiency2: character.proficiency?.[1] || 0,
minHp: character.hp?.minHp || 0,
maxHp: character.hp?.maxHp || 0,
maxHpFlb: character.hp?.maxHpFlb || 0,
minAtk: character.atk?.minAtk || 0,
maxAtk: character.atk?.maxAtk || 0,
maxAtkFlb: character.atk?.maxAtkFlb || 0,
flb: character.uncap?.flb || false,
ulb: character.uncap?.ulb || false,
transcendence: character.uncap?.transcendence || false,
special: character.special || false
}
}
}
async function saveChanges() {
if (!character?.id) return
isSaving = true
saveError = null
saveSuccess = false
try {
// Prepare the data for API (flat snake_case format)
const payload = {
name_en: editData.name,
granblue_id: editData.granblueId,
character_id: editData.characterId ? [editData.characterId] : [],
rarity: editData.rarity,
element: editData.element,
race1: editData.race1,
race2: editData.race2,
gender: editData.gender,
proficiency1: editData.proficiency1,
proficiency2: editData.proficiency2,
min_hp: editData.minHp,
max_hp: editData.maxHp,
max_hp_flb: editData.maxHpFlb,
min_atk: editData.minAtk,
max_atk: editData.maxAtk,
max_atk_flb: editData.maxAtkFlb,
flb: editData.flb,
ulb: editData.ulb,
special: editData.special
}
await entityAdapter.updateCharacter(character.id, payload)
// Invalidate TanStack Query cache to refetch fresh data
await queryClient.invalidateQueries({ queryKey: ['character', character.id] })
saveSuccess = true
editMode = false
// Show success message for 3 seconds
setTimeout(() => {
saveSuccess = false
}, 3000)
} catch (error) {
saveError = 'Failed to save changes. Please try again.'
console.error('Save error:', error)
} finally {
isSaving = false
}
}
// Helper function for character grid image // Helper function for character grid image
function getCharacterGridImage(character: any): string { function getCharacterGridImage(character: any): string {
return getCharacterImage(character?.granblueId, 'grid', '01') return getCharacterImage(character?.granblueId, 'grid', '01')
} }
// Generate image items for character (variants and poses based on uncap level)
const characterImages = $derived.by((): ImageItem[] => {
if (!character?.granblueId) return []
const variants = ['detail', 'grid', 'main', 'square'] as const
const images: ImageItem[] = []
// Determine available poses based on uncap level
// _01 = Base, _02 = MLB (3*), _03 = FLB (5*), _04 = Transcendence
const poses: { id: string; label: string }[] = [
{ id: '01', label: 'Base' },
{ id: '02', label: 'MLB' }
]
if (character.uncap?.flb) {
poses.push({ id: '03', label: 'FLB' })
}
if (character.uncap?.transcendence) {
poses.push({ id: '04', label: 'Transcendence' })
}
for (const variant of variants) {
for (const pose of poses) {
images.push({
url: getCharacterImage(character.granblueId, variant, pose.id),
label: `${variant} (${pose.label})`,
variant,
pose: pose.id
})
}
}
return images
})
</script> </script>
<div class="page"> <div class="page">
@ -203,45 +125,52 @@
item={character} item={character}
image={getCharacterGridImage(character)} image={getCharacterGridImage(character)}
showEdit={canEdit} showEdit={canEdit}
{editMode} editUrl={canEdit ? editUrl : undefined}
{isSaving} {currentTab}
{saveSuccess} onTabChange={handleTabChange}
{saveError}
onEdit={toggleEditMode}
onSave={saveChanges}
onCancel={toggleEditMode}
> >
<section class="details"> {#if currentTab === 'info'}
<CharacterMetadataSection {character} {editMode} bind:editData /> <section class="details">
<CharacterUncapSection {character} {editMode} bind:editData /> <CharacterMetadataSection {character} />
<CharacterTaxonomySection {character} {editMode} bind:editData /> <CharacterUncapSection {character} />
<CharacterStatsSection {character} {editMode} bind:editData /> <CharacterTaxonomySection {character} />
<CharacterStatsSection {character} />
{#if character?.id && character?.granblueId} {#if character?.id && character?.granblueId}
<CharacterImagesSection <CharacterImagesSection
characterId={character.id} characterId={character.id}
granblueId={character.granblueId} granblueId={character.granblueId}
{canEdit} {canEdit}
/> />
{/if} {/if}
{#if !editMode && relatedQuery.data?.length} {#if relatedQuery.data?.length}
<DetailsContainer title="Related Units"> <DetailsContainer title="Related Units">
<div class="related-units"> <div class="related-units">
{#each relatedQuery.data as related} {#each relatedQuery.data as related}
<a href="/database/characters/{related.id}" class="related-unit"> <a href="/database/characters/{related.id}" class="related-unit">
<img <img
src={getCharacterImage(related.granblueId, 'grid', '01')} src={getCharacterImage(related.granblueId, 'grid', '01')}
alt={related.name.en} alt={related.name.en}
class="related-image" class="related-image"
/> />
<span class="related-name">{related.name.en}</span> <span class="related-name">{related.name.en}</span>
</a> </a>
{/each} {/each}
</div> </div>
</DetailsContainer> </DetailsContainer>
{/if} {/if}
</section> </section>
{:else if currentTab === 'images'}
<EntityImagesTab images={characterImages} />
{:else if currentTab === 'raw'}
<EntityRawDataTab
wikiRaw={rawDataQuery.data?.wikiRaw}
gameRawEn={rawDataQuery.data?.gameRawEn}
gameRawJp={rawDataQuery.data?.gameRawJp}
isLoading={rawDataQuery.isLoading}
/>
{/if}
</DetailScaffold> </DetailScaffold>
{:else} {:else}
<div class="not-found"> <div class="not-found">

View file

@ -3,30 +3,42 @@
<script lang="ts"> <script lang="ts">
// SvelteKit imports // SvelteKit imports
import { goto } from '$app/navigation' import { goto } from '$app/navigation'
import { page } from '$app/stores'
// TanStack Query // TanStack Query
import { createQuery, useQueryClient } from '@tanstack/svelte-query' import { createQuery } from '@tanstack/svelte-query'
import { entityQueries } from '$lib/api/queries/entity.queries' import { entityQueries } from '$lib/api/queries/entity.queries'
import { entityAdapter } from '$lib/api/adapters/entity.adapter' import { entityAdapter } from '$lib/api/adapters/entity.adapter'
import { withInitialData } from '$lib/query/ssr' import { withInitialData } from '$lib/query/ssr'
// Components // Components
import DetailScaffold from '$lib/features/database/detail/DetailScaffold.svelte' import DetailScaffold, { type DetailTab } from '$lib/features/database/detail/DetailScaffold.svelte'
import SummonMetadataSection from '$lib/features/database/summons/sections/SummonMetadataSection.svelte' import SummonMetadataSection from '$lib/features/database/summons/sections/SummonMetadataSection.svelte'
import SummonUncapSection from '$lib/features/database/summons/sections/SummonUncapSection.svelte' import SummonUncapSection from '$lib/features/database/summons/sections/SummonUncapSection.svelte'
import SummonTaxonomySection from '$lib/features/database/summons/sections/SummonTaxonomySection.svelte' import SummonTaxonomySection from '$lib/features/database/summons/sections/SummonTaxonomySection.svelte'
import SummonStatsSection from '$lib/features/database/summons/sections/SummonStatsSection.svelte' import SummonStatsSection from '$lib/features/database/summons/sections/SummonStatsSection.svelte'
import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte' import EntityImagesTab from '$lib/features/database/detail/tabs/EntityImagesTab.svelte'
import DetailItem from '$lib/components/ui/DetailItem.svelte' import EntityRawDataTab from '$lib/features/database/detail/tabs/EntityRawDataTab.svelte'
import TagInput from '$lib/components/ui/TagInput.svelte'
import { getSummonImage } from '$lib/utils/images' import { getSummonImage } from '$lib/utils/images'
// Types // Types
import type { PageData } from './$types' import type { PageData } from './$types'
import type { ImageItem } from '$lib/features/database/detail/tabs/EntityImagesTab.svelte'
let { data }: { data: PageData } = $props() let { data }: { data: PageData } = $props()
const queryClient = useQueryClient() // Tab state from URL
const currentTab = $derived(($page.url.searchParams.get('tab') as DetailTab) || 'info')
function handleTabChange(tab: DetailTab) {
const url = new URL($page.url)
if (tab === 'info') {
url.searchParams.delete('tab')
} else {
url.searchParams.set('tab', tab)
}
goto(url.toString(), { replaceState: true })
}
// Use TanStack Query with SSR initial data // Use TanStack Query with SSR initial data
const summonQuery = createQuery(() => ({ const summonQuery = createQuery(() => ({
@ -39,198 +51,41 @@
const userRole = $derived(data.role || 0) const userRole = $derived(data.role || 0)
const canEdit = $derived(userRole >= 7) const canEdit = $derived(userRole >= 7)
// Edit mode state // Edit URL for navigation
let editMode = $state(false) const editUrl = $derived(summon?.id ? `/database/summons/${summon.id}/edit` : undefined)
let isSaving = $state(false)
let saveError = $state<string | null>(null)
let saveSuccess = $state(false)
// Editable fields // Query for raw data (only when on raw tab)
let editData = $state({ const rawDataQuery = createQuery(() => ({
name: summon?.name?.en || '', queryKey: ['summons', 'raw', summon?.id],
nameJp: summon?.name?.ja || '', queryFn: async () => {
granblueId: summon?.granblueId || '', if (!summon?.id) return null
rarity: summon?.rarity || 3, return entityAdapter.getSummonRawData(summon.id)
element: summon?.element || 0, },
series: summon?.series || '', enabled: currentTab === 'raw' && !!summon?.id
minHp: summon?.hp?.minHp || 0, }))
maxHp: summon?.hp?.maxHp || 0,
maxHpFlb: summon?.hp?.maxHpFlb || 0,
maxHpUlb: summon?.hp?.maxHpUlb || 0,
maxHpTranscendence: summon?.hp?.maxHpXlb || 0,
minAtk: summon?.atk?.minAtk || 0,
maxAtk: summon?.atk?.maxAtk || 0,
maxAtkFlb: summon?.atk?.maxAtkFlb || 0,
maxAtkUlb: summon?.atk?.maxAtkUlb || 0,
maxAtkTranscendence: summon?.atk?.maxAtkXlb || 0,
maxLevel: 100,
flb: summon?.uncap?.flb || false,
ulb: summon?.uncap?.ulb || false,
transcendence: summon?.uncap?.transcendence || false,
subaura: summon?.subaura || false,
limit: false,
releaseDate: '',
flbDate: '',
ulbDate: '',
transcendenceDate: '',
wikiEn: '',
wikiJa: '',
gamewith: '',
kamigame: '',
nicknamesEn: [] as string[],
nicknamesJp: [] as string[]
})
// Reset edit data when summon changes
$effect(() => {
if (summon) {
editData = {
name: summon.name?.en || '',
nameJp: summon.name?.ja || '',
granblueId: summon.granblueId || '',
rarity: summon.rarity || 3,
element: summon.element || 0,
series: summon.series || '',
minHp: summon.hp?.minHp || 0,
maxHp: summon.hp?.maxHp || 0,
maxHpFlb: summon.hp?.maxHpFlb || 0,
maxHpUlb: summon.hp?.maxHpUlb || 0,
maxHpTranscendence: summon.hp?.maxHpXlb || 0,
minAtk: summon.atk?.minAtk || 0,
maxAtk: summon.atk?.maxAtk || 0,
maxAtkFlb: summon.atk?.maxAtkFlb || 0,
maxAtkUlb: summon.atk?.maxAtkUlb || 0,
maxAtkTranscendence: summon.atk?.maxAtkXlb || 0,
maxLevel: 100,
flb: summon.uncap?.flb || false,
ulb: summon.uncap?.ulb || false,
transcendence: summon.uncap?.transcendence || false,
subaura: summon.subaura || false,
limit: false,
releaseDate: '',
flbDate: '',
ulbDate: '',
transcendenceDate: '',
wikiEn: '',
wikiJa: '',
gamewith: '',
kamigame: '',
nicknamesEn: [],
nicknamesJp: []
}
}
})
function toggleEditMode() {
editMode = !editMode
saveError = null
saveSuccess = false
// Reset data when canceling
if (!editMode && summon) {
editData = {
name: summon.name?.en || '',
nameJp: summon.name?.ja || '',
granblueId: summon.granblueId || '',
rarity: summon.rarity || 3,
element: summon.element || 0,
series: summon.series || '',
minHp: summon.hp?.minHp || 0,
maxHp: summon.hp?.maxHp || 0,
maxHpFlb: summon.hp?.maxHpFlb || 0,
maxHpUlb: summon.hp?.maxHpUlb || 0,
maxHpTranscendence: summon.hp?.maxHpXlb || 0,
minAtk: summon.atk?.minAtk || 0,
maxAtk: summon.atk?.maxAtk || 0,
maxAtkFlb: summon.atk?.maxAtkFlb || 0,
maxAtkUlb: summon.atk?.maxAtkUlb || 0,
maxAtkTranscendence: summon.atk?.maxAtkXlb || 0,
maxLevel: 100,
flb: summon.uncap?.flb || false,
ulb: summon.uncap?.ulb || false,
transcendence: summon.uncap?.transcendence || false,
subaura: summon.subaura || false,
limit: false,
releaseDate: '',
flbDate: '',
ulbDate: '',
transcendenceDate: '',
wikiEn: '',
wikiJa: '',
gamewith: '',
kamigame: '',
nicknamesEn: [],
nicknamesJp: []
}
}
}
async function saveChanges() {
if (!summon?.id) return
isSaving = true
saveError = null
saveSuccess = false
try {
const payload = {
name_en: editData.name,
name_jp: editData.nameJp || undefined,
granblue_id: editData.granblueId,
rarity: editData.rarity,
element: editData.element,
series: editData.series || undefined,
min_hp: editData.minHp,
max_hp: editData.maxHp,
max_hp_flb: editData.maxHpFlb,
max_hp_ulb: editData.maxHpUlb,
max_hp_xlb: editData.maxHpTranscendence,
min_atk: editData.minAtk,
max_atk: editData.maxAtk,
max_atk_flb: editData.maxAtkFlb,
max_atk_ulb: editData.maxAtkUlb,
max_atk_xlb: editData.maxAtkTranscendence,
max_level: editData.maxLevel,
flb: editData.flb,
ulb: editData.ulb,
transcendence: editData.transcendence,
subaura: editData.subaura,
limit: editData.limit,
release_date: editData.releaseDate || null,
flb_date: editData.flbDate || null,
ulb_date: editData.ulbDate || null,
transcendence_date: editData.transcendenceDate || null,
wiki_en: editData.wikiEn,
wiki_ja: editData.wikiJa,
gamewith: editData.gamewith,
kamigame: editData.kamigame,
nicknames_en: editData.nicknamesEn,
nicknames_jp: editData.nicknamesJp
}
await entityAdapter.updateSummon(summon.id, payload)
// Invalidate TanStack Query cache to refetch fresh data
await queryClient.invalidateQueries({ queryKey: ['summon', summon.id] })
saveSuccess = true
editMode = false
setTimeout(() => {
saveSuccess = false
}, 3000)
} catch (error) {
saveError = 'Failed to save changes. Please try again.'
console.error('Save error:', error)
} finally {
isSaving = false
}
}
// Helper function for summon grid image // Helper function for summon grid image
function getSummonGridImage(summon: any): string { function getSummonGridImage(summon: any): string {
return getSummonImage(summon?.granblueId, 'grid') return getSummonImage(summon?.granblueId, 'grid')
} }
// Generate image items for summon (detail, grid, main, square, wide variants)
const summonImages = $derived.by((): ImageItem[] => {
if (!summon?.granblueId) return []
const variants = ['detail', 'grid', 'main', 'square', 'wide'] as const
const images: ImageItem[] = []
for (const variant of variants) {
images.push({
url: getSummonImage(summon.granblueId, variant),
label: variant,
variant
})
}
return images
})
</script> </script>
<div class="page"> <div class="page">
@ -240,108 +95,17 @@
item={summon} item={summon}
image={getSummonGridImage(summon)} image={getSummonGridImage(summon)}
showEdit={canEdit} showEdit={canEdit}
{editMode} editUrl={canEdit ? editUrl : undefined}
{isSaving} {currentTab}
{saveSuccess} onTabChange={handleTabChange}
{saveError}
onEdit={toggleEditMode}
onSave={saveChanges}
onCancel={toggleEditMode}
> >
<section class="details"> {#if currentTab === 'info'}
<SummonMetadataSection {summon} {editMode} bind:editData /> <section class="details">
<SummonUncapSection {summon} {editMode} bind:editData /> <SummonMetadataSection {summon} />
<SummonTaxonomySection {summon} {editMode} bind:editData /> <SummonUncapSection {summon} />
<SummonStatsSection {summon} {editMode} bind:editData /> <SummonTaxonomySection {summon} />
<SummonStatsSection {summon} />
{#if editMode}
<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"
bind:value={editData.releaseDate}
editable={true}
type="text"
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}
{#if editData.transcendence}
<DetailItem
label="Transcendence Date"
bind:value={editData.transcendenceDate}
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>
{/if}
{#if !editMode}
<div class="summon-abilities"> <div class="summon-abilities">
<h3>Call Effect</h3> <h3>Call Effect</h3>
<div class="abilities-section"> <div class="abilities-section">
@ -383,8 +147,17 @@
</div> </div>
{/if} {/if}
</div> </div>
{/if} </section>
</section> {:else if currentTab === 'images'}
<EntityImagesTab images={summonImages} />
{:else if currentTab === 'raw'}
<EntityRawDataTab
wikiRaw={rawDataQuery.data?.wikiRaw}
gameRawEn={rawDataQuery.data?.gameRawEn}
gameRawJp={rawDataQuery.data?.gameRawJp}
isLoading={rawDataQuery.isLoading}
/>
{/if}
</DetailScaffold> </DetailScaffold>
{:else} {:else}
<div class="not-found"> <div class="not-found">

View file

@ -3,30 +3,42 @@
<script lang="ts"> <script lang="ts">
// SvelteKit imports // SvelteKit imports
import { goto } from '$app/navigation' import { goto } from '$app/navigation'
import { page } from '$app/stores'
// TanStack Query // TanStack Query
import { createQuery, useQueryClient } from '@tanstack/svelte-query' import { createQuery } from '@tanstack/svelte-query'
import { entityQueries } from '$lib/api/queries/entity.queries' import { entityQueries } from '$lib/api/queries/entity.queries'
import { entityAdapter } from '$lib/api/adapters/entity.adapter' import { entityAdapter } from '$lib/api/adapters/entity.adapter'
import { withInitialData } from '$lib/query/ssr' import { withInitialData } from '$lib/query/ssr'
// Components // Components
import DetailScaffold from '$lib/features/database/detail/DetailScaffold.svelte' import DetailScaffold, { type DetailTab } from '$lib/features/database/detail/DetailScaffold.svelte'
import WeaponMetadataSection from '$lib/features/database/weapons/sections/WeaponMetadataSection.svelte' import WeaponMetadataSection from '$lib/features/database/weapons/sections/WeaponMetadataSection.svelte'
import WeaponUncapSection from '$lib/features/database/weapons/sections/WeaponUncapSection.svelte' import WeaponUncapSection from '$lib/features/database/weapons/sections/WeaponUncapSection.svelte'
import WeaponTaxonomySection from '$lib/features/database/weapons/sections/WeaponTaxonomySection.svelte' import WeaponTaxonomySection from '$lib/features/database/weapons/sections/WeaponTaxonomySection.svelte'
import WeaponStatsSection from '$lib/features/database/weapons/sections/WeaponStatsSection.svelte' import WeaponStatsSection from '$lib/features/database/weapons/sections/WeaponStatsSection.svelte'
import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte' import EntityImagesTab from '$lib/features/database/detail/tabs/EntityImagesTab.svelte'
import DetailItem from '$lib/components/ui/DetailItem.svelte' import EntityRawDataTab from '$lib/features/database/detail/tabs/EntityRawDataTab.svelte'
import TagInput from '$lib/components/ui/TagInput.svelte' import { getWeaponGridImage, getWeaponImage as getWeaponImageUrl } from '$lib/utils/images'
import { getWeaponGridImage } from '$lib/utils/images'
// Types // Types
import type { PageData } from './$types' import type { PageData } from './$types'
import type { ImageItem } from '$lib/features/database/detail/tabs/EntityImagesTab.svelte'
let { data }: { data: PageData } = $props() let { data }: { data: PageData } = $props()
const queryClient = useQueryClient() // Tab state from URL
const currentTab = $derived(($page.url.searchParams.get('tab') as DetailTab) || 'info')
function handleTabChange(tab: DetailTab) {
const url = new URL($page.url)
if (tab === 'info') {
url.searchParams.delete('tab')
} else {
url.searchParams.set('tab', tab)
}
goto(url.toString(), { replaceState: true })
}
// Use TanStack Query with SSR initial data // Use TanStack Query with SSR initial data
const weaponQuery = createQuery(() => ({ const weaponQuery = createQuery(() => ({
@ -39,210 +51,41 @@
const userRole = $derived(data.role || 0) const userRole = $derived(data.role || 0)
const canEdit = $derived(userRole >= 7) const canEdit = $derived(userRole >= 7)
// Edit mode state // Edit URL for navigation
let editMode = $state(false) const editUrl = $derived(weapon?.id ? `/database/weapons/${weapon.id}/edit` : undefined)
let isSaving = $state(false)
let saveError = $state<string | null>(null)
let saveSuccess = $state(false)
// Editable fields // Query for raw data (only when on raw tab)
let editData = $state({ const rawDataQuery = createQuery(() => ({
name: weapon?.name?.en || '', queryKey: ['weapons', 'raw', weapon?.id],
nameJp: weapon?.name?.ja || '', queryFn: async () => {
granblue_id: weapon?.granblueId || '', if (!weapon?.id) return null
rarity: weapon?.rarity || 3, return entityAdapter.getWeaponRawData(weapon.id)
element: weapon?.element || 0, },
proficiency: weapon?.proficiency || 0, enabled: currentTab === 'raw' && !!weapon?.id
series: weapon?.series || 0, }))
minHp: weapon?.hp?.minHp || 0,
maxHp: weapon?.hp?.maxHp || 0,
maxHpFlb: weapon?.hp?.maxHpFlb || 0,
maxHpUlb: weapon?.hp?.maxHpUlb || 0,
minAtk: weapon?.atk?.minAtk || 0,
maxAtk: weapon?.atk?.maxAtk || 0,
maxAtkFlb: weapon?.atk?.maxAtkFlb || 0,
maxAtkUlb: weapon?.atk?.maxAtkUlb || 0,
maxLevel: weapon?.maxLevel || 100,
maxSkillLevel: weapon?.maxSkillLevel || 10,
maxAwakeningLevel: weapon?.maxAwakeningLevel || 0,
flb: weapon?.uncap?.flb || false,
ulb: weapon?.uncap?.ulb || false,
transcendence: weapon?.uncap?.transcendence || false,
extra: false,
limit: false,
ax: weapon?.ax || false,
releaseDate: '',
flbDate: '',
ulbDate: '',
transcendenceDate: '',
wikiEn: '',
wikiJa: '',
gamewith: '',
kamigame: '',
nicknamesEn: [] as string[],
nicknamesJp: [] as string[],
recruits: ''
})
// Reset edit data when weapon changes
$effect(() => {
if (weapon) {
editData = {
name: weapon.name?.en || '',
nameJp: weapon.name?.ja || '',
granblue_id: weapon.granblueId || '',
rarity: weapon.rarity || 3,
element: weapon.element || 0,
proficiency: weapon.proficiency || 0,
series: weapon.series || 0,
minHp: weapon.hp?.minHp || 0,
maxHp: weapon.hp?.maxHp || 0,
maxHpFlb: weapon.hp?.maxHpFlb || 0,
maxHpUlb: weapon.hp?.maxHpUlb || 0,
minAtk: weapon.atk?.minAtk || 0,
maxAtk: weapon.atk?.maxAtk || 0,
maxAtkFlb: weapon.atk?.maxAtkFlb || 0,
maxAtkUlb: weapon.atk?.maxAtkUlb || 0,
maxLevel: weapon.maxLevel || 100,
maxSkillLevel: weapon.maxSkillLevel || 10,
maxAwakeningLevel: weapon.maxAwakeningLevel || 0,
flb: weapon.uncap?.flb || false,
ulb: weapon.uncap?.ulb || false,
transcendence: weapon.uncap?.transcendence || false,
extra: false,
limit: false,
ax: weapon.ax || false,
releaseDate: '',
flbDate: '',
ulbDate: '',
transcendenceDate: '',
wikiEn: '',
wikiJa: '',
gamewith: '',
kamigame: '',
nicknamesEn: [],
nicknamesJp: [],
recruits: ''
}
}
})
function toggleEditMode() {
editMode = !editMode
saveError = null
saveSuccess = false
// Reset data when canceling
if (!editMode && weapon) {
editData = {
name: weapon.name?.en || '',
nameJp: weapon.name?.ja || '',
granblue_id: weapon.granblueId || '',
rarity: weapon.rarity || 3,
element: weapon.element || 0,
proficiency: weapon.proficiency || 0,
series: weapon.series || 0,
minHp: weapon.hp?.minHp || 0,
maxHp: weapon.hp?.maxHp || 0,
maxHpFlb: weapon.hp?.maxHpFlb || 0,
maxHpUlb: weapon.hp?.maxHpUlb || 0,
minAtk: weapon.atk?.minAtk || 0,
maxAtk: weapon.atk?.maxAtk || 0,
maxAtkFlb: weapon.atk?.maxAtkFlb || 0,
maxAtkUlb: weapon.atk?.maxAtkUlb || 0,
maxLevel: weapon.maxLevel || 100,
maxSkillLevel: weapon.maxSkillLevel || 10,
maxAwakeningLevel: weapon.maxAwakeningLevel || 0,
flb: weapon.uncap?.flb || false,
ulb: weapon.uncap?.ulb || false,
transcendence: weapon.uncap?.transcendence || false,
extra: false,
limit: false,
ax: weapon.ax || false,
releaseDate: '',
flbDate: '',
ulbDate: '',
transcendenceDate: '',
wikiEn: '',
wikiJa: '',
gamewith: '',
kamigame: '',
nicknamesEn: [],
nicknamesJp: [],
recruits: ''
}
}
}
async function saveChanges() {
if (!weapon?.id) return
isSaving = true
saveError = null
saveSuccess = false
try {
const payload = {
name_en: editData.name,
name_jp: editData.nameJp || undefined,
granblue_id: editData.granblue_id,
rarity: editData.rarity,
element: editData.element,
proficiency: editData.proficiency,
series: editData.series || undefined,
min_hp: editData.minHp,
max_hp: editData.maxHp,
max_hp_flb: editData.maxHpFlb,
max_hp_ulb: editData.maxHpUlb,
min_atk: editData.minAtk,
max_atk: editData.maxAtk,
max_atk_flb: editData.maxAtkFlb,
max_atk_ulb: editData.maxAtkUlb,
max_level: editData.maxLevel,
max_skill_level: editData.maxSkillLevel,
max_awakening_level: editData.maxAwakeningLevel,
flb: editData.flb,
ulb: editData.ulb,
transcendence: editData.transcendence,
extra: editData.extra,
limit: editData.limit,
ax: editData.ax,
release_date: editData.releaseDate || null,
flb_date: editData.flbDate || null,
ulb_date: editData.ulbDate || null,
transcendence_date: editData.transcendenceDate || null,
wiki_en: editData.wikiEn,
wiki_ja: editData.wikiJa,
gamewith: editData.gamewith,
kamigame: editData.kamigame,
nicknames_en: editData.nicknamesEn,
nicknames_jp: editData.nicknamesJp,
recruits: editData.recruits || undefined
}
await entityAdapter.updateWeapon(weapon.id, payload)
// Invalidate TanStack Query cache to refetch fresh data
await queryClient.invalidateQueries({ queryKey: ['weapon', weapon.id] })
saveSuccess = true
editMode = false
setTimeout(() => {
saveSuccess = false
}, 3000)
} catch (error) {
saveError = 'Failed to save changes. Please try again.'
console.error('Save error:', error)
} finally {
isSaving = false
}
}
// Helper function for weapon grid image // Helper function for weapon grid image
function getWeaponImage(weapon: any): string { function getWeaponImage(weapon: any): string {
return getWeaponGridImage(weapon?.granblueId, weapon?.element, weapon?.instanceElement) return getWeaponGridImage(weapon?.granblueId, weapon?.element, weapon?.instanceElement)
} }
// Generate image items for weapon (base, grid, main, square variants)
const weaponImages = $derived.by((): ImageItem[] => {
if (!weapon?.granblueId) return []
const variants = ['base', 'grid', 'main', 'square'] as const
const images: ImageItem[] = []
for (const variant of variants) {
images.push({
url: getWeaponImageUrl(weapon.granblueId, variant),
label: variant,
variant
})
}
return images
})
</script> </script>
<div class="page"> <div class="page">
@ -252,119 +95,17 @@
item={weapon} item={weapon}
image={getWeaponImage(weapon)} image={getWeaponImage(weapon)}
showEdit={canEdit} showEdit={canEdit}
{editMode} editUrl={canEdit ? editUrl : undefined}
{isSaving} {currentTab}
{saveSuccess} onTabChange={handleTabChange}
{saveError}
onEdit={toggleEditMode}
onSave={saveChanges}
onCancel={toggleEditMode}
> >
<section class="details"> {#if currentTab === 'info'}
<WeaponMetadataSection {weapon} {editMode} bind:editData /> <section class="details">
<WeaponUncapSection {weapon} {editMode} bind:editData /> <WeaponMetadataSection {weapon} />
<WeaponTaxonomySection {weapon} {editMode} bind:editData /> <WeaponUncapSection {weapon} />
<WeaponStatsSection {weapon} {editMode} bind:editData /> <WeaponTaxonomySection {weapon} />
<WeaponStatsSection {weapon} />
{#if editMode}
<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"
bind:value={editData.releaseDate}
editable={true}
type="text"
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}
{#if editData.transcendence}
<DetailItem
label="Transcendence Date"
bind:value={editData.transcendenceDate}
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>
<DetailsContainer title="Character">
<DetailItem
label="Recruits"
sublabel="Character ID this weapon recruits"
bind:value={editData.recruits}
editable={true}
type="text"
placeholder="Character ID..."
/>
</DetailsContainer>
{/if}
{#if !editMode}
<div class="weapon-skills"> <div class="weapon-skills">
<h3>Skills</h3> <h3>Skills</h3>
<div class="skills-grid"> <div class="skills-grid">
@ -382,8 +123,17 @@
{/if} {/if}
</div> </div>
</div> </div>
{/if} </section>
</section> {:else if currentTab === 'images'}
<EntityImagesTab images={weaponImages} />
{:else if currentTab === 'raw'}
<EntityRawDataTab
wikiRaw={rawDataQuery.data?.wikiRaw}
gameRawEn={rawDataQuery.data?.gameRawEn}
gameRawJp={rawDataQuery.data?.gameRawJp}
isLoading={rawDataQuery.isLoading}
/>
{/if}
</DetailScaffold> </DetailScaffold>
{:else} {:else}
<div class="not-found"> <div class="not-found">