From 29142ab901362dc555d4ad131398f4e97108fee3 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Sat, 20 Dec 2025 01:39:57 -0800 Subject: [PATCH] use svar grid for raid groups, improve grid header styling --- .../database/DatabaseGridWithProvider.svelte | 13 +- src/routes/(app)/database/raids/+page.svelte | 301 +++++++++++++----- 2 files changed, 237 insertions(+), 77 deletions(-) diff --git a/src/lib/components/database/DatabaseGridWithProvider.svelte b/src/lib/components/database/DatabaseGridWithProvider.svelte index dd455783..865bdf54 100644 --- a/src/lib/components/database/DatabaseGridWithProvider.svelte +++ b/src/lib/components/database/DatabaseGridWithProvider.svelte @@ -459,11 +459,16 @@ } :global(.wx-grid .wx-header) { - background: transparent; + background: #f8f9fa; + } + + :global(.wx-grid .wx-h-row) { + background: #f8f9fa; + border-bottom: 1px solid #e5e5e5; } :global(.wx-grid .wx-header-cell) { - background: transparent; + background: #f8f9fa; font-weight: typography.$bold; color: #495057; border-bottom: 2px solid #dee2e6; @@ -486,6 +491,10 @@ --wx-table-cell-border: none; } + :global(.wx-grid .wx-cell:first-child) { + padding-left: spacing.$unit-2x; + } + :global(.wx-grid .wx-cell:not(:last-child)) { border-right: none; } diff --git a/src/routes/(app)/database/raids/+page.svelte b/src/routes/(app)/database/raids/+page.svelte index ad5f13c4..d346e823 100644 --- a/src/routes/(app)/database/raids/+page.svelte +++ b/src/routes/(app)/database/raids/+page.svelte @@ -4,15 +4,22 @@ import PageMeta from '$lib/components/PageMeta.svelte' import * as m from '$lib/paraglide/messages' import { goto } from '$app/navigation' + import { page } from '$app/stores' + import { onMount } from 'svelte' import { createQuery } from '@tanstack/svelte-query' + import { Grid } from 'wx-svelte-grid' + import type { IColumn } from 'wx-svelte-grid' import { raidAdapter } from '$lib/api/adapters/raid.adapter' import ElementBadge from '$lib/components/ui/ElementBadge.svelte' import MultiSelect from '$lib/components/ui/MultiSelect.svelte' import Select from '$lib/components/ui/Select.svelte' import SegmentedControl from '$lib/components/ui/segmented-control/SegmentedControl.svelte' import Segment from '$lib/components/ui/segmented-control/Segment.svelte' + import RaidGroupNameCell from '$lib/components/database/cells/RaidGroupNameCell.svelte' + import RaidGroupFlagsCell from '$lib/components/database/cells/RaidGroupFlagsCell.svelte' import type { Raid, RaidGroup } from '$lib/types/api/entities' import type { RaidGroupFull } from '$lib/types/api/raid' + import { getRaidSectionLabel } from '$lib/utils/raidSection' function displayName(input: any): string { if (!input) return '—' @@ -30,6 +37,31 @@ let hlFilter = $state(undefined) let extraFilter = $state(undefined) + // Read initial view mode from URL + onMount(() => { + const viewParam = $page.url.searchParams.get('view') + if (viewParam === 'groups') { + viewMode = 'groups' + } + }) + + // Update URL when view mode changes + function updateViewUrl(mode: 'raids' | 'groups') { + const url = new URL($page.url) + if (mode === 'groups') { + url.searchParams.set('view', 'groups') + } else { + url.searchParams.delete('view') + } + goto(url.pathname + url.search, { replaceState: true, noScroll: true, keepFocus: true }) + } + + // Handle view mode change from segmented control + function handleViewModeChange(newMode: string) { + viewMode = newMode as 'raids' | 'groups' + updateViewUrl(viewMode) + } + // Query for raids const raidsQuery = createQuery(() => ({ queryKey: ['raids', 'list'], @@ -52,7 +84,7 @@ })) ) - // Filter raids + // Filter and sort raids const filteredRaids = $derived.by(() => { let raids = raidsQuery.data ?? [] @@ -89,7 +121,23 @@ raids = raids.filter((r) => r.group?.extra === extraBool) } - return raids + // Sort by group section, then group order, then element + return [...raids].sort((a, b) => { + // Section first (may be string or number) + const sectionA = Number(a.group?.section) || 999 + const sectionB = Number(b.group?.section) || 999 + if (sectionA !== sectionB) { + return sectionA - sectionB + } + // Then group order + const groupOrderA = a.group?.order ?? 999 + const groupOrderB = b.group?.order ?? 999 + if (groupOrderA !== groupOrderB) { + return groupOrderA - groupOrderB + } + // Then element + return (a.element ?? 999) - (b.element ?? 999) + }) }) // Navigate to raid detail @@ -134,14 +182,132 @@ { value: 1, label: 'Yes' }, { value: 0, label: 'No' } ] + + // ==================== Groups Grid Configuration ==================== + + // Sort state for groups grid + let groupsSortMarks = $state>({}) + + // Groups grid columns + const groupsColumns: IColumn[] = [ + { + id: 'name', + header: 'Name', + flexgrow: 1, + sort: true, + cell: RaidGroupNameCell + }, + { + id: 'section', + header: 'Section', + width: 100, + sort: true, + template: (val: any) => getRaidSectionLabel(val) + }, + { + id: 'order', + header: 'Order', + width: 80, + sort: true, + template: (val: any) => val?.toString() ?? '-' + }, + { + id: 'difficulty', + header: 'Difficulty', + width: 100, + sort: true, + template: (val: any) => val?.toString() ?? '-' + }, + { + id: 'flags', + header: 'Flags', + width: 180, + cell: RaidGroupFlagsCell + }, + { + id: 'raids', + header: 'Raids', + width: 80, + template: (_val: any, row: any) => row.raids?.length?.toString() ?? '0' + } + ] + + // Sorted groups data + const sortedGroupsData = $derived.by(() => { + const groups = groupsQuery.data ?? [] + const sortKey = Object.keys(groupsSortMarks)[0] + if (!sortKey) return groups + + const order = groupsSortMarks[sortKey]?.order + return [...groups].sort((a: any, b: any) => { + let valA = a[sortKey] + let valB = b[sortKey] + + // Handle name sorting (use English name) + if (sortKey === 'name') { + valA = a.name?.en ?? '' + valB = b.name?.en ?? '' + } + + // Handle numeric values + if (typeof valA === 'number' && typeof valB === 'number') { + return order === 'asc' ? valA - valB : valB - valA + } + + // Handle string values + const strA = String(valA ?? '') + const strB = String(valB ?? '') + return order === 'asc' ? strA.localeCompare(strB) : strB.localeCompare(strA) + }) + }) + + // Groups grid API reference + let groupsGridApi: any + + // Initialize groups grid + const initGroupsGrid = (apiRef: any) => { + groupsGridApi = apiRef + + // Intercept sort-rows for client-side sorting + groupsGridApi.intercept('sort-rows', (ev: { key: string; add: boolean }) => { + const { key } = ev + const currentOrder = groupsSortMarks[key]?.order + + // Toggle: asc -> desc -> clear + if (currentOrder === 'asc') { + groupsSortMarks = { [key]: { order: 'desc' } } + } else if (currentOrder === 'desc') { + groupsSortMarks = {} + } else { + groupsSortMarks = { [key]: { order: 'asc' } } + } + + return false // Prevent default sorting + }) + + // Row click handler + groupsGridApi.on('select-row', (ev: any) => { + const rowId = ev.id + if (rowId) { + const group = (groupsQuery.data ?? []).find((g: any) => g.id === rowId) + if (group) { + handleGroupClick(group) + } + } + }) + } + + + +
- + Raids Groups @@ -256,56 +422,19 @@
{/if} - - - - - - - - - - - - {#if (groupsQuery.data ?? []).length === 0 && !groupsQuery.isLoading} - - - - {:else} - {#each (groupsQuery.data ?? []) as group} - handleGroupClick(group)} class="clickable"> - - - - - - - {/each} - {/if} - -
NameSectionDifficultyFlagsRaids
No raid groups yet
- {displayName(group)} - - {group.section ?? '-'} - - {group.difficulty ?? '-'} - -
- {#if group.hl}HL{/if} - {#if group.extra}Extra{/if} - {#if group.guidebooks}Guidebooks{/if} - {#if !group.hl && !group.extra && !group.guidebooks} - - - {/if} -
-
- {group.raids?.length ?? 0} -
+
{/if} @@ -472,41 +601,16 @@ } } - .col-section, - .col-difficulty, - .col-raids { - width: 100px; - } - - .col-flags { - min-width: 150px; - } } .raid-name { font-weight: typography.$bold; } - .no-element, - .no-flags { + .no-element { color: #999; } - .flags { - display: flex; - flex-wrap: wrap; - gap: spacing.$unit-half; - } - - .flag { - display: inline-block; - padding: 2px 8px; - border-radius: 4px; - font-size: typography.$font-tiny; - background: #e9ecef; - color: #495057; - } - .empty-state { text-align: center; color: #666; @@ -526,4 +630,51 @@ color: #6c757d; } } + + // SVAR Grid styles + :global(.database-grid-theme) { + font-size: typography.$font-small; + width: 100%; + } + + :global(.wx-grid .wx-header) { + background: #f8f9fa; + } + + :global(.wx-grid .wx-h-row) { + background: #f8f9fa; + border-bottom: 1px solid #e5e5e5; + } + + :global(.wx-grid .wx-header-cell) { + background: #f8f9fa; + font-weight: typography.$bold; + color: #495057; + border-bottom: 2px solid #dee2e6; + border-radius: layout.$item-corner; + transition: background-color 0.15s ease; + cursor: pointer; + + &:hover { + background: #e9ecef; + } + } + + :global(.wx-grid .wx-cell) { + padding: spacing.$unit * 0.5; + vertical-align: middle; + display: flex; + align-items: center; + border: none; + --wx-table-cell-border: none; + } + + :global(.wx-grid .wx-cell:first-child) { + padding-left: spacing.$unit-2x; + } + + :global(.wx-grid .wx-row:hover) { + background: #f8f9fa; + cursor: pointer; + }