add weapon series detail/edit pages, fix navigation
- add detail page at /database/series/weapons/[slug] - add edit page with form for series properties - fix series table to navigate using slug instead of id - add ?view=series URL param for bookmarkable series view - remove redundant /database/series page - back buttons now go to /database/weapons?view=series
This commit is contained in:
parent
839365a5a1
commit
adc7a6e2fe
6 changed files with 443 additions and 250 deletions
|
|
@ -1,241 +0,0 @@
|
||||||
<svelte:options runes={true} />
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import PageMeta from '$lib/components/PageMeta.svelte'
|
|
||||||
import * as m from '$lib/paraglide/messages'
|
|
||||||
import { createQuery } from '@tanstack/svelte-query'
|
|
||||||
import { entityQueries } from '$lib/api/queries/entity.queries'
|
|
||||||
import SegmentedControl from '$lib/components/ui/segmented-control/SegmentedControl.svelte'
|
|
||||||
import Segment from '$lib/components/ui/segmented-control/Segment.svelte'
|
|
||||||
|
|
||||||
type SeriesType = 'weapons' | 'characters' | 'summons'
|
|
||||||
|
|
||||||
let activeType = $state<SeriesType>('weapons')
|
|
||||||
|
|
||||||
// Fetch all series lists
|
|
||||||
const weaponSeriesQuery = createQuery(() => entityQueries.weaponSeriesList())
|
|
||||||
const characterSeriesQuery = createQuery(() => entityQueries.characterSeriesList())
|
|
||||||
const summonSeriesQuery = createQuery(() => entityQueries.summonSeriesList())
|
|
||||||
|
|
||||||
// Get active query based on selected type
|
|
||||||
const activeQuery = $derived.by(() => {
|
|
||||||
switch (activeType) {
|
|
||||||
case 'weapons':
|
|
||||||
return weaponSeriesQuery
|
|
||||||
case 'characters':
|
|
||||||
return characterSeriesQuery
|
|
||||||
case 'summons':
|
|
||||||
return summonSeriesQuery
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Get sorted data
|
|
||||||
const sortedData = $derived.by(() => {
|
|
||||||
if (!activeQuery.data) return []
|
|
||||||
return [...activeQuery.data].sort((a, b) => a.order - b.order)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Check if the current type has flags (only weapons)
|
|
||||||
const hasFlags = $derived(activeType === 'weapons')
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<PageMeta title={m.page_title_db_series()} description={m.page_desc_home()} />
|
|
||||||
|
|
||||||
<div class="database-page">
|
|
||||||
<div class="grid-container">
|
|
||||||
<nav class="series-nav" aria-label="Series type">
|
|
||||||
<SegmentedControl bind:value={activeType} variant="blended" size="small">
|
|
||||||
<Segment value="weapons">Weapons</Segment>
|
|
||||||
<Segment value="characters">Characters</Segment>
|
|
||||||
<Segment value="summons">Summons</Segment>
|
|
||||||
</SegmentedControl>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
{#if activeQuery.isPending}
|
|
||||||
<div class="loading">Loading {activeType} series...</div>
|
|
||||||
{:else if activeQuery.error}
|
|
||||||
<div class="error">Failed to load {activeType} series</div>
|
|
||||||
{:else if sortedData.length > 0}
|
|
||||||
<div class="series-table">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="order">Order</th>
|
|
||||||
<th class="name">Name (EN)</th>
|
|
||||||
<th class="name-ja">Name (JA)</th>
|
|
||||||
<th class="slug">Slug</th>
|
|
||||||
{#if hasFlags}
|
|
||||||
<th class="flags">Flags</th>
|
|
||||||
{/if}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{#each sortedData as series (series.id)}
|
|
||||||
<tr>
|
|
||||||
<td class="order">{series.order}</td>
|
|
||||||
<td class="name">{series.name.en}</td>
|
|
||||||
<td class="name-ja">{series.name.ja}</td>
|
|
||||||
<td class="slug"><code>{series.slug}</code></td>
|
|
||||||
{#if hasFlags && 'extra' in series}
|
|
||||||
<td class="flags">
|
|
||||||
{#if series.extra}<span class="flag extra">Extra</span>{/if}
|
|
||||||
{#if series.elementChangeable}<span class="flag element">Element</span>{/if}
|
|
||||||
{#if series.hasWeaponKeys}<span class="flag keys">Keys</span>{/if}
|
|
||||||
{#if series.hasAwakening}<span class="flag awaken">Awaken</span>{/if}
|
|
||||||
{#if series.hasAxSkills}<span class="flag ax">AX</span>{/if}
|
|
||||||
</td>
|
|
||||||
{/if}
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="empty">No {activeType} series found</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
@use '$src/themes/colors' as colors;
|
|
||||||
@use '$src/themes/effects' as effects;
|
|
||||||
@use '$src/themes/layout' as layout;
|
|
||||||
@use '$src/themes/spacing' as spacing;
|
|
||||||
@use '$src/themes/typography' as typography;
|
|
||||||
|
|
||||||
.database-page {
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-container {
|
|
||||||
background: var(--card-bg);
|
|
||||||
border: 0.5px solid rgba(0, 0, 0, 0.18);
|
|
||||||
border-radius: layout.$page-corner;
|
|
||||||
box-shadow: effects.$page-elevation;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.series-nav {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: spacing.$unit-2x;
|
|
||||||
padding: spacing.$unit-2x;
|
|
||||||
border-bottom: 1px solid var(--border-subtle);
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading,
|
|
||||||
.error,
|
|
||||||
.empty {
|
|
||||||
padding: spacing.$unit-4x;
|
|
||||||
text-align: center;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
color: var(--text-error, #ef4444);
|
|
||||||
}
|
|
||||||
|
|
||||||
.series-table {
|
|
||||||
overflow-x: auto;
|
|
||||||
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
th,
|
|
||||||
td {
|
|
||||||
padding: spacing.$unit-2x spacing.$unit-3x;
|
|
||||||
text-align: left;
|
|
||||||
border-bottom: 1px solid var(--border-secondary, #e5e5e5);
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
background: var(--table-header-bg, #f9f9f9);
|
|
||||||
font-weight: typography.$medium;
|
|
||||||
font-size: typography.$font-small;
|
|
||||||
color: colors.$grey-40;
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
font-size: typography.$font-regular;
|
|
||||||
}
|
|
||||||
|
|
||||||
.order {
|
|
||||||
width: 80px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
|
||||||
min-width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name-ja {
|
|
||||||
min-width: 180px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slug code {
|
|
||||||
font-family: 'SF Mono', Monaco, monospace;
|
|
||||||
font-size: typography.$font-small;
|
|
||||||
background: var(--code-bg, #f0f0f0);
|
|
||||||
padding: spacing.$unit-fourth spacing.$unit-half;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flags {
|
|
||||||
.flag {
|
|
||||||
display: inline-block;
|
|
||||||
font-size: typography.$font-tiny;
|
|
||||||
padding: spacing.$unit-fourth spacing.$unit-half;
|
|
||||||
margin-right: spacing.$unit-half;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-weight: typography.$medium;
|
|
||||||
|
|
||||||
&.extra {
|
|
||||||
background: #f3e8ff;
|
|
||||||
color: #6b21a8;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.element {
|
|
||||||
background: linear-gradient(
|
|
||||||
to right,
|
|
||||||
#fecaca,
|
|
||||||
#fef08a,
|
|
||||||
#bbf7d0,
|
|
||||||
#bfdbfe,
|
|
||||||
#e9d5ff,
|
|
||||||
#fbcfe8
|
|
||||||
);
|
|
||||||
color: #374151;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.keys {
|
|
||||||
background: #fef3c7;
|
|
||||||
color: #92400e;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.awaken {
|
|
||||||
background: #dcfce7;
|
|
||||||
color: #166534;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.ax {
|
|
||||||
background: #ffe4e6;
|
|
||||||
color: #9f1239;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody tr:nth-child(odd) {
|
|
||||||
background: var(--table-row-alt, #fafafa);
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody tr:hover {
|
|
||||||
background: var(--table-row-hover, #f0f0f0);
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody tr:last-child td {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import type { PageServerLoad } from './$types'
|
||||||
|
import { entityAdapter } from '$lib/api/adapters/entity.adapter'
|
||||||
|
import { error } from '@sveltejs/kit'
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ params, parent }) => {
|
||||||
|
const parentData = await parent()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const series = await entityAdapter.getWeaponSeries(params.slug)
|
||||||
|
|
||||||
|
if (!series) {
|
||||||
|
throw error(404, 'Weapon series not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
series,
|
||||||
|
role: parentData.role
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load weapon series:', err)
|
||||||
|
|
||||||
|
if (err instanceof Error && 'status' in err && err.status === 404) {
|
||||||
|
throw error(404, 'Weapon series not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error(500, 'Failed to load weapon series')
|
||||||
|
}
|
||||||
|
}
|
||||||
110
src/routes/(app)/database/series/weapons/[slug]/+page.svelte
Normal file
110
src/routes/(app)/database/series/weapons/[slug]/+page.svelte
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation'
|
||||||
|
import { createQuery } from '@tanstack/svelte-query'
|
||||||
|
import { entityQueries } from '$lib/api/queries/entity.queries'
|
||||||
|
import { withInitialData } from '$lib/query/ssr'
|
||||||
|
import PageMeta from '$lib/components/PageMeta.svelte'
|
||||||
|
import * as m from '$lib/paraglide/messages'
|
||||||
|
import DatabasePageHeader from '$lib/components/database/DatabasePageHeader.svelte'
|
||||||
|
import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte'
|
||||||
|
import DetailItem from '$lib/components/ui/DetailItem.svelte'
|
||||||
|
import Button from '$lib/components/ui/Button.svelte'
|
||||||
|
import { getAugmentTypeLabel } from '$lib/utils/augmentType'
|
||||||
|
import type { PageData } from './$types'
|
||||||
|
|
||||||
|
let { data }: { data: PageData } = $props()
|
||||||
|
|
||||||
|
const seriesQuery = createQuery(() => ({
|
||||||
|
...entityQueries.weaponSeries(data.series?.slug ?? ''),
|
||||||
|
...withInitialData(data.series)
|
||||||
|
}))
|
||||||
|
|
||||||
|
const series = $derived(seriesQuery.data)
|
||||||
|
const userRole = $derived(data.role || 0)
|
||||||
|
const canEdit = $derived(userRole >= 7)
|
||||||
|
const editUrl = $derived(series?.slug ? `/database/series/weapons/${series.slug}/edit` : undefined)
|
||||||
|
|
||||||
|
const pageTitle = $derived(series?.name?.en ? `${series.name.en} Series` : 'Weapon Series')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<PageMeta title={pageTitle} description={m.page_desc_home()} />
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
<DatabasePageHeader title="Weapon Series" backHref="/database/weapons?view=series">
|
||||||
|
{#snippet rightAction()}
|
||||||
|
{#if canEdit && editUrl}
|
||||||
|
<Button variant="ghost" size="small" href={editUrl}>Edit</Button>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
</DatabasePageHeader>
|
||||||
|
|
||||||
|
{#if series}
|
||||||
|
<div class="content">
|
||||||
|
<DetailsContainer title="Basic Info">
|
||||||
|
<DetailItem label="Name (EN)" value={series.name.en} />
|
||||||
|
<DetailItem label="Name (JA)" value={series.name.ja} />
|
||||||
|
<DetailItem label="Slug" value={series.slug} />
|
||||||
|
<DetailItem label="Order" value={series.order} />
|
||||||
|
</DetailsContainer>
|
||||||
|
|
||||||
|
<DetailsContainer title="Flags">
|
||||||
|
<DetailItem label="Extra Grid" value={series.extra ? 'Yes' : 'No'} />
|
||||||
|
<DetailItem label="Element Changeable" value={series.elementChangeable ? 'Yes' : 'No'} />
|
||||||
|
<DetailItem label="Has Weapon Keys" value={series.hasWeaponKeys ? 'Yes' : 'No'} />
|
||||||
|
<DetailItem label="Has Awakening" value={series.hasAwakening ? 'Yes' : 'No'} />
|
||||||
|
<DetailItem label="Augment Type" value={getAugmentTypeLabel(series.augmentType)} />
|
||||||
|
</DetailsContainer>
|
||||||
|
|
||||||
|
{#if series.weaponCount !== undefined}
|
||||||
|
<DetailsContainer title="Statistics">
|
||||||
|
<DetailItem label="Weapon Count" value={series.weaponCount} />
|
||||||
|
</DetailsContainer>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="not-found">
|
||||||
|
<h2>Series Not Found</h2>
|
||||||
|
<p>The weapon series you're looking for could not be found.</p>
|
||||||
|
<button onclick={() => goto('/database/weapons?view=series')}>Back to Series</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use '$src/themes/colors' as colors;
|
||||||
|
@use '$src/themes/layout' as layout;
|
||||||
|
@use '$src/themes/spacing' as spacing;
|
||||||
|
@use '$src/themes/typography' as typography;
|
||||||
|
|
||||||
|
.page {
|
||||||
|
background: white;
|
||||||
|
border-radius: layout.$page-corner;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found {
|
||||||
|
text-align: center;
|
||||||
|
padding: spacing.$unit * 4;
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: spacing.$unit-half spacing.$unit;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: spacing.$unit;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #0056b3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import type { PageServerLoad } from './$types'
|
||||||
|
import { entityAdapter } from '$lib/api/adapters/entity.adapter'
|
||||||
|
import { error, redirect } from '@sveltejs/kit'
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ params, parent }) => {
|
||||||
|
const parentData = await parent()
|
||||||
|
|
||||||
|
// Role check - must be editor level (>= 7) to edit
|
||||||
|
if (!parentData.role || parentData.role < 7) {
|
||||||
|
throw redirect(303, `/database/series/weapons/${params.slug}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const series = await entityAdapter.getWeaponSeries(params.slug)
|
||||||
|
|
||||||
|
if (!series) {
|
||||||
|
throw error(404, 'Weapon series not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
series,
|
||||||
|
role: parentData.role
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load weapon series:', err)
|
||||||
|
|
||||||
|
if (err instanceof Error && 'status' in err && err.status === 404) {
|
||||||
|
throw error(404, 'Weapon series not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error(500, 'Failed to load weapon series')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,252 @@
|
||||||
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation'
|
||||||
|
import { createQuery, useQueryClient } from '@tanstack/svelte-query'
|
||||||
|
import { entityQueries } from '$lib/api/queries/entity.queries'
|
||||||
|
import { entityAdapter } from '$lib/api/adapters/entity.adapter'
|
||||||
|
import { withInitialData } from '$lib/query/ssr'
|
||||||
|
import PageMeta from '$lib/components/PageMeta.svelte'
|
||||||
|
import * as m from '$lib/paraglide/messages'
|
||||||
|
import DatabasePageHeader from '$lib/components/database/DatabasePageHeader.svelte'
|
||||||
|
import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte'
|
||||||
|
import DetailItem from '$lib/components/ui/DetailItem.svelte'
|
||||||
|
import Button from '$lib/components/ui/Button.svelte'
|
||||||
|
import { getAugmentTypeOptions } from '$lib/utils/augmentType'
|
||||||
|
import type { AugmentType } from '$lib/types/api/weaponStatModifier'
|
||||||
|
import type { PageData } from './$types'
|
||||||
|
|
||||||
|
let { data }: { data: PageData } = $props()
|
||||||
|
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
const seriesQuery = createQuery(() => ({
|
||||||
|
...entityQueries.weaponSeries(data.series?.slug ?? ''),
|
||||||
|
...withInitialData(data.series)
|
||||||
|
}))
|
||||||
|
|
||||||
|
const series = $derived(seriesQuery.data)
|
||||||
|
const pageTitle = $derived(series?.name?.en ? `Edit ${series.name.en}` : 'Edit Weapon Series')
|
||||||
|
|
||||||
|
// Save state
|
||||||
|
let isSaving = $state(false)
|
||||||
|
let saveError = $state<string | null>(null)
|
||||||
|
|
||||||
|
// Editable fields
|
||||||
|
let editData = $state({
|
||||||
|
nameEn: '',
|
||||||
|
nameJa: '',
|
||||||
|
slug: '',
|
||||||
|
order: 0,
|
||||||
|
extra: false,
|
||||||
|
elementChangeable: false,
|
||||||
|
hasWeaponKeys: false,
|
||||||
|
hasAwakening: false,
|
||||||
|
augmentType: 'no_augment' as AugmentType
|
||||||
|
})
|
||||||
|
|
||||||
|
// Populate edit data when series loads
|
||||||
|
$effect(() => {
|
||||||
|
if (series) {
|
||||||
|
editData = {
|
||||||
|
nameEn: series.name.en || '',
|
||||||
|
nameJa: series.name.ja || '',
|
||||||
|
slug: series.slug || '',
|
||||||
|
order: series.order || 0,
|
||||||
|
extra: series.extra || false,
|
||||||
|
elementChangeable: series.elementChangeable || false,
|
||||||
|
hasWeaponKeys: series.hasWeaponKeys || false,
|
||||||
|
hasAwakening: series.hasAwakening || false,
|
||||||
|
augmentType: series.augmentType || 'no_augment'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Augment type options for dropdown
|
||||||
|
const augmentTypeOptions = getAugmentTypeOptions().map((opt) => ({
|
||||||
|
value: opt.value,
|
||||||
|
label: opt.label
|
||||||
|
}))
|
||||||
|
|
||||||
|
async function saveChanges() {
|
||||||
|
if (!series?.id) return
|
||||||
|
|
||||||
|
isSaving = true
|
||||||
|
saveError = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
name_en: editData.nameEn,
|
||||||
|
name_jp: editData.nameJa,
|
||||||
|
slug: editData.slug,
|
||||||
|
order: editData.order,
|
||||||
|
extra: editData.extra,
|
||||||
|
element_changeable: editData.elementChangeable,
|
||||||
|
has_weapon_keys: editData.hasWeaponKeys,
|
||||||
|
has_awakening: editData.hasAwakening,
|
||||||
|
augment_type: editData.augmentType
|
||||||
|
}
|
||||||
|
|
||||||
|
await entityAdapter.updateWeaponSeries(series.id, payload)
|
||||||
|
|
||||||
|
// Invalidate cache
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: ['weaponSeries'],
|
||||||
|
refetchType: 'all'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Navigate back to detail page (use new slug if changed)
|
||||||
|
goto(`/database/series/weapons/${editData.slug}`)
|
||||||
|
} catch (error) {
|
||||||
|
saveError = 'Failed to save changes. Please try again.'
|
||||||
|
console.error('Save error:', error)
|
||||||
|
} finally {
|
||||||
|
isSaving = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<PageMeta title={pageTitle} description={m.page_desc_home()} />
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
<DatabasePageHeader title="Edit Weapon Series" backHref={`/database/series/weapons/${series?.slug}`}>
|
||||||
|
{#snippet rightAction()}
|
||||||
|
<Button variant="ghost" size="small" onclick={saveChanges} disabled={isSaving}>
|
||||||
|
{isSaving ? 'Saving...' : 'Save'}
|
||||||
|
</Button>
|
||||||
|
{/snippet}
|
||||||
|
</DatabasePageHeader>
|
||||||
|
|
||||||
|
{#if series}
|
||||||
|
<div class="content">
|
||||||
|
{#if saveError}
|
||||||
|
<div class="error-banner">{saveError}</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<DetailsContainer title="Basic Info">
|
||||||
|
<DetailItem
|
||||||
|
label="Name (EN)"
|
||||||
|
bind:value={editData.nameEn}
|
||||||
|
editable={true}
|
||||||
|
type="text"
|
||||||
|
placeholder="English name"
|
||||||
|
width="320px"
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Name (JA)"
|
||||||
|
bind:value={editData.nameJa}
|
||||||
|
editable={true}
|
||||||
|
type="text"
|
||||||
|
placeholder="Japanese name"
|
||||||
|
width="320px"
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Slug"
|
||||||
|
bind:value={editData.slug}
|
||||||
|
editable={true}
|
||||||
|
type="text"
|
||||||
|
placeholder="url-friendly-slug"
|
||||||
|
width="240px"
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Order"
|
||||||
|
bind:value={editData.order}
|
||||||
|
editable={true}
|
||||||
|
type="number"
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
</DetailsContainer>
|
||||||
|
|
||||||
|
<DetailsContainer title="Flags">
|
||||||
|
<DetailItem
|
||||||
|
label="Extra Grid"
|
||||||
|
sublabel="Weapon can be placed in Extra grid slots"
|
||||||
|
bind:value={editData.extra}
|
||||||
|
editable={true}
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Element Changeable"
|
||||||
|
sublabel="Weapon element can be changed by player"
|
||||||
|
bind:value={editData.elementChangeable}
|
||||||
|
editable={true}
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Has Weapon Keys"
|
||||||
|
sublabel="Weapon supports Pendulum/Teluma keys"
|
||||||
|
bind:value={editData.hasWeaponKeys}
|
||||||
|
editable={true}
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Has Awakening"
|
||||||
|
sublabel="Weapon can be awakened"
|
||||||
|
bind:value={editData.hasAwakening}
|
||||||
|
editable={true}
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Augment Type"
|
||||||
|
sublabel="Type of stat augments this series supports"
|
||||||
|
bind:value={editData.augmentType}
|
||||||
|
editable={true}
|
||||||
|
type="select"
|
||||||
|
options={augmentTypeOptions}
|
||||||
|
/>
|
||||||
|
</DetailsContainer>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="not-found">
|
||||||
|
<h2>Series Not Found</h2>
|
||||||
|
<p>The weapon series you're looking for could not be found.</p>
|
||||||
|
<button onclick={() => goto('/database/weapons?view=series')}>Back to Series</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use '$src/themes/colors' as colors;
|
||||||
|
@use '$src/themes/layout' as layout;
|
||||||
|
@use '$src/themes/spacing' as spacing;
|
||||||
|
@use '$src/themes/typography' as typography;
|
||||||
|
|
||||||
|
.page {
|
||||||
|
background: white;
|
||||||
|
border-radius: layout.$page-corner;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-banner {
|
||||||
|
background: #fef2f2;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
color: #dc2626;
|
||||||
|
padding: spacing.$unit-2x;
|
||||||
|
margin: spacing.$unit-2x;
|
||||||
|
border-radius: layout.$item-corner;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found {
|
||||||
|
text-align: center;
|
||||||
|
padding: spacing.$unit * 4;
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: spacing.$unit-half spacing.$unit;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: spacing.$unit;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #0056b3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
import PageMeta from '$lib/components/PageMeta.svelte'
|
import PageMeta from '$lib/components/PageMeta.svelte'
|
||||||
import * as m from '$lib/paraglide/messages'
|
import * as m from '$lib/paraglide/messages'
|
||||||
import { goto } from '$app/navigation'
|
import { goto } from '$app/navigation'
|
||||||
|
import { page } from '$app/stores'
|
||||||
import { createQuery } 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 DatabaseGridWithProvider from '$lib/components/database/DatabaseGridWithProvider.svelte'
|
import DatabaseGridWithProvider from '$lib/components/database/DatabaseGridWithProvider.svelte'
|
||||||
|
|
@ -17,8 +18,19 @@
|
||||||
import LastUpdatedCell from '$lib/components/database/cells/LastUpdatedCell.svelte'
|
import LastUpdatedCell from '$lib/components/database/cells/LastUpdatedCell.svelte'
|
||||||
import { getRarityLabel } from '$lib/utils/rarity'
|
import { getRarityLabel } from '$lib/utils/rarity'
|
||||||
|
|
||||||
// View mode state
|
// View mode state - read initial value from URL
|
||||||
let viewMode = $state<'weapons' | 'series'>('weapons')
|
const initialView = $page.url.searchParams.get('view')
|
||||||
|
let viewMode = $state<'weapons' | 'series'>(initialView === 'series' ? 'series' : 'weapons')
|
||||||
|
|
||||||
|
// Sync viewMode changes to URL
|
||||||
|
$effect(() => {
|
||||||
|
const currentView = $page.url.searchParams.get('view')
|
||||||
|
if (viewMode === 'series' && currentView !== 'series') {
|
||||||
|
goto('?view=series', { replaceState: true, noScroll: true })
|
||||||
|
} else if (viewMode === 'weapons' && currentView === 'series') {
|
||||||
|
goto('/database/weapons', { replaceState: true, noScroll: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Query for weapon series
|
// Query for weapon series
|
||||||
const weaponSeriesQuery = createQuery(() => entityQueries.weaponSeriesList())
|
const weaponSeriesQuery = createQuery(() => entityQueries.weaponSeriesList())
|
||||||
|
|
@ -30,8 +42,8 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
// Navigate to series detail
|
// Navigate to series detail
|
||||||
function handleSeriesClick(seriesId: string) {
|
function handleSeriesClick(slug: string) {
|
||||||
goto(`/database/series/${seriesId}`)
|
goto(`/database/series/weapons/${slug}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Column configuration for weapons
|
// Column configuration for weapons
|
||||||
|
|
@ -130,7 +142,7 @@
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each sortedSeries as series (series.id)}
|
{#each sortedSeries as series (series.id)}
|
||||||
<tr onclick={() => handleSeriesClick(series.id)} class="clickable">
|
<tr onclick={() => handleSeriesClick(series.slug)} class="clickable">
|
||||||
<td class="col-order">{series.order}</td>
|
<td class="col-order">{series.order}</td>
|
||||||
<td class="col-name">
|
<td class="col-name">
|
||||||
<span class="series-name">{series.name.en}</span>
|
<span class="series-name">{series.name.en}</span>
|
||||||
|
|
@ -188,8 +200,7 @@
|
||||||
.controls {
|
.controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: spacing.$unit;
|
padding: spacing.$unit-2x;
|
||||||
border-bottom: 1px solid #e5e5e5;
|
|
||||||
gap: spacing.$unit;
|
gap: spacing.$unit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -216,7 +227,7 @@
|
||||||
|
|
||||||
th,
|
th,
|
||||||
td {
|
td {
|
||||||
padding: spacing.$unit spacing.$unit-2x;
|
padding: spacing.$unit-2x spacing.$unit-2x;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border-bottom: 1px solid #e5e5e5;
|
border-bottom: 1px solid #e5e5e5;
|
||||||
}
|
}
|
||||||
|
|
@ -262,7 +273,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.series-name {
|
.series-name {
|
||||||
font-weight: typography.$bold;
|
font-weight: typography.$normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-flags {
|
.no-flags {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue