hensei-web/src/routes/(app)/database/gw-events/+page.svelte

325 lines
6.4 KiB
Svelte

<svelte:options runes={true} />
<script lang="ts">
import PageMeta from '$lib/components/PageMeta.svelte'
import * as m from '$lib/paraglide/messages'
import { goto } from '$app/navigation'
import { createQuery } from '@tanstack/svelte-query'
import { gwAdapter } from '$lib/api/adapters/gw.adapter'
import Button from '$lib/components/ui/Button.svelte'
import { formatDateJST } from '$lib/utils/date'
import type { GwEvent } from '$lib/types/api/gw'
import type { PageData } from './$types'
interface Props {
data: PageData
}
let { data }: Props = $props()
// State
let searchTerm = $state('')
// Query for GW events
const eventsQuery = createQuery(() => ({
queryKey: ['gw', 'events', 'admin'],
queryFn: () => gwAdapter.getEvents(),
staleTime: 1000 * 60 * 5
}))
// Element labels (matches GranblueEnums::ELEMENTS)
const elementLabels: Record<number, string> = {
0: 'Null',
1: 'Wind',
2: 'Fire',
3: 'Water',
4: 'Earth',
5: 'Dark',
6: 'Light'
}
// Element colors for badges
const elementColors: Record<number, string> = {
0: 'null',
1: 'wind',
2: 'fire',
3: 'water',
4: 'earth',
5: 'dark',
6: 'light'
}
// Filter events by search
const filteredEvents = $derived.by(() => {
const events = eventsQuery.data ?? []
if (!searchTerm.trim()) return events
const term = searchTerm.toLowerCase()
return events.filter(
(e) =>
String(e.eventNumber).includes(term) ||
elementLabels[e.element]?.toLowerCase().includes(term)
)
})
// Navigate to event detail/edit
function handleRowClick(event: GwEvent) {
goto(`/database/gw-events/${event.id}`)
}
</script>
<PageMeta title={m.page_title_db_gw()} description={m.page_desc_home()} />
<div class="page">
<div class="grid">
<div class="controls">
<input type="text" placeholder="Search events..." bind:value={searchTerm} />
<div class="controls-right">
<Button variant="primary" size="small" onclick={() => goto('/database/gw-events/new')}>
New Event
</Button>
</div>
</div>
<div class="grid-wrapper" class:loading={eventsQuery.isLoading}>
{#if eventsQuery.isLoading}
<div class="loading-overlay">
<div class="loading-spinner">Loading...</div>
</div>
{/if}
<table class="events-table">
<thead>
<tr>
<th class="col-number">#</th>
<th class="col-element">Element</th>
<th class="col-dates">Dates</th>
</tr>
</thead>
<tbody>
{#if filteredEvents.length === 0 && !eventsQuery.isLoading}
<tr>
<td colspan="3" class="empty-state">
{searchTerm ? 'No events match your search' : 'No GW events yet'}
</td>
</tr>
{:else}
{#each filteredEvents as event}
<tr onclick={() => handleRowClick(event)} class="clickable">
<td class="col-number">
<span class="event-number">{event.eventNumber}</span>
</td>
<td class="col-element">
<span class="element-badge element-{elementColors[event.element]}">
{elementLabels[event.element] ?? 'Unknown'}
</span>
</td>
<td class="col-dates">
<span class="dates">
{formatDateJST(event.startDate)} - {formatDateJST(event.endDate)}
</span>
</td>
</tr>
{/each}
{/if}
</tbody>
</table>
</div>
<div class="grid-footer">
<div class="pagination-info">
{filteredEvents.length} event{filteredEvents.length === 1 ? '' : 's'}
</div>
</div>
</div>
</div>
<style lang="scss">
@use '$src/themes/effects' as effects;
@use '$src/themes/layout' as layout;
@use '$src/themes/spacing' as spacing;
@use '$src/themes/typography' as typography;
.page {
padding: 0;
margin: 0 auto;
}
.grid {
width: 100%;
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;
}
.controls {
display: flex;
justify-content: space-between;
align-items: center;
padding: spacing.$unit;
border-bottom: 1px solid #e5e5e5;
gap: spacing.$unit;
input {
padding: spacing.$unit spacing.$unit-2x;
background: var(--input-bound-bg);
border: none;
border-radius: layout.$item-corner;
font-size: typography.$font-medium;
width: 100%;
max-width: 300px;
&:hover {
background: var(--input-bound-bg-hover);
}
&:focus {
outline: none;
border-color: #007bff;
}
}
.controls-right {
display: flex;
align-items: center;
gap: spacing.$unit;
}
}
.grid-wrapper {
position: relative;
overflow-x: auto;
min-height: 200px;
&.loading {
opacity: 0.6;
}
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.9);
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
.loading-spinner {
font-size: typography.$font-medium;
color: #666;
}
}
.events-table {
width: 100%;
border-collapse: collapse;
th,
td {
padding: spacing.$unit spacing.$unit-2x;
text-align: left;
border-bottom: 1px solid #e5e5e5;
}
th {
background: #f8f9fa;
font-weight: typography.$bold;
color: #495057;
font-size: typography.$font-small;
}
tr.clickable {
cursor: pointer;
&:hover {
background: #f8f9fa;
}
}
.col-number {
width: 80px;
}
.col-element {
width: 120px;
}
.col-dates {
min-width: 200px;
}
}
.event-number {
font-weight: typography.$bold;
color: #666;
}
.element-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: typography.$font-small;
font-weight: 500;
&.element-fire {
background: #fee2e2;
color: #dc2626;
}
&.element-water {
background: #dbeafe;
color: #2563eb;
}
&.element-earth {
background: #fef3c7;
color: #d97706;
}
&.element-wind {
background: #d1fae5;
color: #059669;
}
&.element-light {
background: #fef9c3;
color: #ca8a04;
}
&.element-dark {
background: #ede9fe;
color: #7c3aed;
}
}
.dates {
font-size: typography.$font-small;
color: #666;
}
.empty-state {
text-align: center;
color: #666;
padding: spacing.$unit-4x !important;
}
.grid-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: spacing.$unit;
border-top: 1px solid #e5e5e5;
background: #f8f9fa;
.pagination-info {
font-size: typography.$font-small;
color: #6c757d;
}
}
</style>