database detail pages: add Info/Images/Raw tabs
This commit is contained in:
parent
395a5c166f
commit
be75fcbcbd
7 changed files with 561 additions and 881 deletions
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
77
src/lib/features/database/detail/tabs/EntityImagesTab.svelte
Normal file
77
src/lib/features/database/detail/tabs/EntityImagesTab.svelte
Normal 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>
|
||||||
113
src/lib/features/database/detail/tabs/EntityRawDataTab.svelte
Normal file
113
src/lib/features/database/detail/tabs/EntityRawDataTab.svelte
Normal 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>
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue