refactor SearchContent filters layout with inline clear links

This commit is contained in:
Justin Edmund 2026-01-04 19:53:36 -08:00
parent 243ab3e5b5
commit 34db5dd6aa

View file

@ -6,11 +6,15 @@
import type { SearchResult } from '$lib/api/adapters/search.adapter'
import { searchQueries, type SearchFilters } from '$lib/api/queries/search.queries'
import { collectionQueries } from '$lib/api/queries/collection.queries'
import { entityQueries } from '$lib/api/queries/entity.queries'
import Button from '../ui/Button.svelte'
import Select from '../ui/Select.svelte'
import Icon from '../Icon.svelte'
import Input from '../ui/Input.svelte'
import CharacterTags from '$lib/components/tags/CharacterTags.svelte'
import ElementPicker from '../ui/element-picker/ElementPicker.svelte'
import RarityPicker from '../ui/rarity-picker/RarityPicker.svelte'
import ProficiencyPicker from '../ui/proficiency-picker/ProficiencyPicker.svelte'
import { useInfiniteLoader } from '$lib/stores/loaderState.svelte'
import { getCharacterImage, getWeaponImage, getSummonImage } from '$lib/features/database/detail/image'
import type { AddItemResult, SearchMode } from '$lib/types/api/search'
@ -43,6 +47,7 @@
let elementFilters = $state<number[]>([])
let rarityFilters = $state<number[]>([])
let proficiencyFilters = $state<number[]>([])
let seriesFilter = $state<string | undefined>(undefined)
// Search mode state (only available when authUserId is provided)
let searchMode = $state<SearchMode>('all')
@ -61,25 +66,7 @@
{ value: 6, label: 'Light', color: 'var(--light-bg)' }
]
const rarities = [
{ value: 1, label: 'R' },
{ value: 2, label: 'SR' },
{ value: 3, label: 'SSR' }
]
const proficiencies = [
{ value: 1, label: 'Sabre' },
{ value: 2, label: 'Dagger' },
{ value: 3, label: 'Spear' },
{ value: 4, label: 'Axe' },
{ value: 5, label: 'Staff' },
{ value: 6, label: 'Gun' },
{ value: 7, label: 'Melee' },
{ value: 8, label: 'Bow' },
{ value: 9, label: 'Harp' },
{ value: 10, label: 'Katana' }
]
// Debounce search query changes
$effect(() => {
const query = searchQuery
@ -99,6 +86,28 @@
}
})
// Series query - fetch list based on current type
const seriesQuery = createQuery(() => {
switch (type) {
case 'weapon':
return entityQueries.weaponSeriesList()
case 'character':
return entityQueries.characterSeriesList()
case 'summon':
return entityQueries.summonSeriesList()
}
})
// Build series options for dropdown (use ID for API filtering)
const seriesOptions = $derived.by(() => {
const data = seriesQuery.data
if (!data) return []
return data.map((s) => ({
value: s.id,
label: s.name.en
}))
})
// Build filters object for query
// Use requiredProficiencies for mainhand selection if set, otherwise use user-selected filters
const effectiveProficiencies = $derived(
@ -108,7 +117,8 @@
const filters = $derived<SearchFilters>({
element: elementFilters.length > 0 ? elementFilters : undefined,
rarity: rarityFilters.length > 0 ? rarityFilters : undefined,
proficiency: type === 'weapon' && effectiveProficiencies ? effectiveProficiencies : undefined
proficiency: type === 'weapon' && effectiveProficiencies ? effectiveProficiencies : undefined,
series: seriesFilter ? [seriesFilter] : undefined
})
// Helper to map collection items to search result format with collectionId
@ -282,20 +292,16 @@
elementFilters = Array.isArray(value) ? value : value !== undefined ? [value] : []
}
function toggleRarityFilter(rarity: number) {
if (rarityFilters.includes(rarity)) {
rarityFilters = rarityFilters.filter(r => r !== rarity)
} else {
rarityFilters = [...rarityFilters, rarity]
}
function handleRarityChange(value: number | number[]) {
rarityFilters = Array.isArray(value) ? value : value !== undefined ? [value] : []
}
function toggleProficiencyFilter(prof: number) {
if (proficiencyFilters.includes(prof)) {
proficiencyFilters = proficiencyFilters.filter(p => p !== prof)
} else {
proficiencyFilters = [...proficiencyFilters, prof]
}
function handleSeriesChange(value: string | undefined) {
seriesFilter = value
}
function handleProficiencyChange(value: number | number[]) {
proficiencyFilters = Array.isArray(value) ? value : value !== undefined ? [value] : []
}
function getImageUrl(item: AddItemResult): string {
@ -354,54 +360,78 @@
{/if}
<div class="filters-section">
<!-- Element filters -->
<div class="filter-group">
<label class="filter-label">Element</label>
<ElementPicker
value={elementFilters}
onValueChange={handleElementChange}
multiple={true}
includeAny={true}
contained={true}
showClear={true}
/>
</div>
<!-- Rarity and Element filters (side by side) -->
<div class="filter-row">
<div class="filter-group">
<div class="filter-header">
<label class="filter-label">Rarity</label>
{#if rarityFilters.length > 0}
<a href="#" class="clear-link" onclick={(e) => { e.preventDefault(); rarityFilters = [] }}>Clear</a>
{/if}
</div>
<RarityPicker
value={rarityFilters}
onValueChange={handleRarityChange}
multiple={true}
contained={true}
size="small"
/>
</div>
<!-- Rarity filters -->
<div class="filter-group">
<label class="filter-label">Rarity</label>
<div class="filter-buttons">
{#each rarities as rarity}
<button
class="filter-btn rarity-btn"
class:active={rarityFilters.includes(rarity.value)}
onclick={() => toggleRarityFilter(rarity.value)}
aria-pressed={rarityFilters.includes(rarity.value)}
>
{rarity.label}
</button>
{/each}
<div class="filter-group">
<div class="filter-header">
<label class="filter-label">Element</label>
{#if elementFilters.length > 0}
<a href="#" class="clear-link" onclick={(e) => { e.preventDefault(); elementFilters = [] }}>Clear</a>
{/if}
</div>
<ElementPicker
value={elementFilters}
onValueChange={handleElementChange}
multiple={true}
includeAny={true}
contained={true}
size="small"
/>
</div>
</div>
<!-- Proficiency filters (weapons only, hidden when required proficiencies set for mainhand) -->
{#if type === 'weapon' && !requiredProficiencies}
<!-- Proficiency filters (weapons and characters, hidden when required proficiencies set for mainhand) -->
{#if (type === 'weapon' || type === 'character') && !requiredProficiencies}
<div class="filter-group">
<label class="filter-label">Proficiency</label>
<div class="filter-buttons proficiency-grid">
{#each proficiencies as prof}
<button
class="filter-btn prof-btn"
class:active={proficiencyFilters.includes(prof.value)}
onclick={() => toggleProficiencyFilter(prof.value)}
aria-pressed={proficiencyFilters.includes(prof.value)}
>
{prof.label}
</button>
{/each}
<div class="filter-header">
<label class="filter-label">Proficiency</label>
{#if proficiencyFilters.length > 0}
<a href="#" class="clear-link" onclick={(e) => { e.preventDefault(); proficiencyFilters = [] }}>Clear</a>
{/if}
</div>
<ProficiencyPicker
value={proficiencyFilters}
onValueChange={handleProficiencyChange}
multiple={true}
contained={true}
size="small"
/>
</div>
{/if}
<!-- Series filter -->
<div class="filter-group">
<div class="filter-header">
<label class="filter-label">Series</label>
{#if seriesFilter}
<a href="#" class="clear-link" onclick={(e) => { e.preventDefault(); seriesFilter = undefined }}>Clear</a>
{/if}
</div>
<Select
options={seriesOptions}
value={seriesFilter}
onValueChange={handleSeriesChange}
placeholder="All series"
contained={true}
fullWidth={true}
/>
</div>
</div>
<!-- Results -->
@ -543,26 +573,49 @@
}
.filters-section {
display: flex;
flex-direction: column;
gap: calc($unit * 1.5);
padding: 0 $unit-2x $unit-2x $unit-2x;
border-bottom: 1px solid var(--border-primary);
flex-shrink: 0;
.filter-group {
margin-bottom: calc($unit * 1.5);
.filter-row {
display: flex;
justify-content: space-between;
gap: $unit;
}
&:last-child {
margin-bottom: 0;
}
.filter-group {
display: flex;
flex-direction: column;
gap: $unit;
}
.filter-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 $unit-half;
}
.filter-label {
display: block;
font-size: $font-tiny;
font-size: $font-small;
font-weight: $bold;
text-transform: uppercase;
color: var(--text-secondary);
margin-bottom: $unit;
letter-spacing: 0.5px;
}
.clear-link {
font-size: $font-small;
color: var(--text-secondary);
text-decoration: none;
cursor: pointer;
transition: 0.15s color ease-out;
&:hover {
color: var(--text-primary);
}
}
.filter-buttons {