refactor SearchContent filters layout with inline clear links
This commit is contained in:
parent
243ab3e5b5
commit
34db5dd6aa
1 changed files with 134 additions and 81 deletions
|
|
@ -6,11 +6,15 @@
|
||||||
import type { SearchResult } from '$lib/api/adapters/search.adapter'
|
import type { SearchResult } from '$lib/api/adapters/search.adapter'
|
||||||
import { searchQueries, type SearchFilters } from '$lib/api/queries/search.queries'
|
import { searchQueries, type SearchFilters } from '$lib/api/queries/search.queries'
|
||||||
import { collectionQueries } from '$lib/api/queries/collection.queries'
|
import { collectionQueries } from '$lib/api/queries/collection.queries'
|
||||||
|
import { entityQueries } from '$lib/api/queries/entity.queries'
|
||||||
import Button from '../ui/Button.svelte'
|
import Button from '../ui/Button.svelte'
|
||||||
|
import Select from '../ui/Select.svelte'
|
||||||
import Icon from '../Icon.svelte'
|
import Icon from '../Icon.svelte'
|
||||||
import Input from '../ui/Input.svelte'
|
import Input from '../ui/Input.svelte'
|
||||||
import CharacterTags from '$lib/components/tags/CharacterTags.svelte'
|
import CharacterTags from '$lib/components/tags/CharacterTags.svelte'
|
||||||
import ElementPicker from '../ui/element-picker/ElementPicker.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 { useInfiniteLoader } from '$lib/stores/loaderState.svelte'
|
||||||
import { getCharacterImage, getWeaponImage, getSummonImage } from '$lib/features/database/detail/image'
|
import { getCharacterImage, getWeaponImage, getSummonImage } from '$lib/features/database/detail/image'
|
||||||
import type { AddItemResult, SearchMode } from '$lib/types/api/search'
|
import type { AddItemResult, SearchMode } from '$lib/types/api/search'
|
||||||
|
|
@ -43,6 +47,7 @@
|
||||||
let elementFilters = $state<number[]>([])
|
let elementFilters = $state<number[]>([])
|
||||||
let rarityFilters = $state<number[]>([])
|
let rarityFilters = $state<number[]>([])
|
||||||
let proficiencyFilters = $state<number[]>([])
|
let proficiencyFilters = $state<number[]>([])
|
||||||
|
let seriesFilter = $state<string | undefined>(undefined)
|
||||||
|
|
||||||
// Search mode state (only available when authUserId is provided)
|
// Search mode state (only available when authUserId is provided)
|
||||||
let searchMode = $state<SearchMode>('all')
|
let searchMode = $state<SearchMode>('all')
|
||||||
|
|
@ -61,25 +66,7 @@
|
||||||
{ value: 6, label: 'Light', color: 'var(--light-bg)' }
|
{ 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
|
// Debounce search query changes
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const query = searchQuery
|
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
|
// Build filters object for query
|
||||||
// Use requiredProficiencies for mainhand selection if set, otherwise use user-selected filters
|
// Use requiredProficiencies for mainhand selection if set, otherwise use user-selected filters
|
||||||
const effectiveProficiencies = $derived(
|
const effectiveProficiencies = $derived(
|
||||||
|
|
@ -108,7 +117,8 @@
|
||||||
const filters = $derived<SearchFilters>({
|
const filters = $derived<SearchFilters>({
|
||||||
element: elementFilters.length > 0 ? elementFilters : undefined,
|
element: elementFilters.length > 0 ? elementFilters : undefined,
|
||||||
rarity: rarityFilters.length > 0 ? rarityFilters : 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
|
// Helper to map collection items to search result format with collectionId
|
||||||
|
|
@ -282,20 +292,16 @@
|
||||||
elementFilters = Array.isArray(value) ? value : value !== undefined ? [value] : []
|
elementFilters = Array.isArray(value) ? value : value !== undefined ? [value] : []
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleRarityFilter(rarity: number) {
|
function handleRarityChange(value: number | number[]) {
|
||||||
if (rarityFilters.includes(rarity)) {
|
rarityFilters = Array.isArray(value) ? value : value !== undefined ? [value] : []
|
||||||
rarityFilters = rarityFilters.filter(r => r !== rarity)
|
|
||||||
} else {
|
|
||||||
rarityFilters = [...rarityFilters, rarity]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleProficiencyFilter(prof: number) {
|
function handleSeriesChange(value: string | undefined) {
|
||||||
if (proficiencyFilters.includes(prof)) {
|
seriesFilter = value
|
||||||
proficiencyFilters = proficiencyFilters.filter(p => p !== prof)
|
}
|
||||||
} else {
|
|
||||||
proficiencyFilters = [...proficiencyFilters, prof]
|
function handleProficiencyChange(value: number | number[]) {
|
||||||
}
|
proficiencyFilters = Array.isArray(value) ? value : value !== undefined ? [value] : []
|
||||||
}
|
}
|
||||||
|
|
||||||
function getImageUrl(item: AddItemResult): string {
|
function getImageUrl(item: AddItemResult): string {
|
||||||
|
|
@ -354,54 +360,78 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="filters-section">
|
<div class="filters-section">
|
||||||
<!-- Element filters -->
|
<!-- Rarity and Element filters (side by side) -->
|
||||||
<div class="filter-group">
|
<div class="filter-row">
|
||||||
<label class="filter-label">Element</label>
|
<div class="filter-group">
|
||||||
<ElementPicker
|
<div class="filter-header">
|
||||||
value={elementFilters}
|
<label class="filter-label">Rarity</label>
|
||||||
onValueChange={handleElementChange}
|
{#if rarityFilters.length > 0}
|
||||||
multiple={true}
|
<a href="#" class="clear-link" onclick={(e) => { e.preventDefault(); rarityFilters = [] }}>Clear</a>
|
||||||
includeAny={true}
|
{/if}
|
||||||
contained={true}
|
</div>
|
||||||
showClear={true}
|
<RarityPicker
|
||||||
/>
|
value={rarityFilters}
|
||||||
</div>
|
onValueChange={handleRarityChange}
|
||||||
|
multiple={true}
|
||||||
|
contained={true}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Rarity filters -->
|
<div class="filter-group">
|
||||||
<div class="filter-group">
|
<div class="filter-header">
|
||||||
<label class="filter-label">Rarity</label>
|
<label class="filter-label">Element</label>
|
||||||
<div class="filter-buttons">
|
{#if elementFilters.length > 0}
|
||||||
{#each rarities as rarity}
|
<a href="#" class="clear-link" onclick={(e) => { e.preventDefault(); elementFilters = [] }}>Clear</a>
|
||||||
<button
|
{/if}
|
||||||
class="filter-btn rarity-btn"
|
</div>
|
||||||
class:active={rarityFilters.includes(rarity.value)}
|
<ElementPicker
|
||||||
onclick={() => toggleRarityFilter(rarity.value)}
|
value={elementFilters}
|
||||||
aria-pressed={rarityFilters.includes(rarity.value)}
|
onValueChange={handleElementChange}
|
||||||
>
|
multiple={true}
|
||||||
{rarity.label}
|
includeAny={true}
|
||||||
</button>
|
contained={true}
|
||||||
{/each}
|
size="small"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Proficiency filters (weapons only, hidden when required proficiencies set for mainhand) -->
|
<!-- Proficiency filters (weapons and characters, hidden when required proficiencies set for mainhand) -->
|
||||||
{#if type === 'weapon' && !requiredProficiencies}
|
{#if (type === 'weapon' || type === 'character') && !requiredProficiencies}
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
<label class="filter-label">Proficiency</label>
|
<div class="filter-header">
|
||||||
<div class="filter-buttons proficiency-grid">
|
<label class="filter-label">Proficiency</label>
|
||||||
{#each proficiencies as prof}
|
{#if proficiencyFilters.length > 0}
|
||||||
<button
|
<a href="#" class="clear-link" onclick={(e) => { e.preventDefault(); proficiencyFilters = [] }}>Clear</a>
|
||||||
class="filter-btn prof-btn"
|
{/if}
|
||||||
class:active={proficiencyFilters.includes(prof.value)}
|
|
||||||
onclick={() => toggleProficiencyFilter(prof.value)}
|
|
||||||
aria-pressed={proficiencyFilters.includes(prof.value)}
|
|
||||||
>
|
|
||||||
{prof.label}
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
</div>
|
||||||
|
<ProficiencyPicker
|
||||||
|
value={proficiencyFilters}
|
||||||
|
onValueChange={handleProficiencyChange}
|
||||||
|
multiple={true}
|
||||||
|
contained={true}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/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>
|
</div>
|
||||||
|
|
||||||
<!-- Results -->
|
<!-- Results -->
|
||||||
|
|
@ -543,26 +573,49 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.filters-section {
|
.filters-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: calc($unit * 1.5);
|
||||||
padding: 0 $unit-2x $unit-2x $unit-2x;
|
padding: 0 $unit-2x $unit-2x $unit-2x;
|
||||||
border-bottom: 1px solid var(--border-primary);
|
border-bottom: 1px solid var(--border-primary);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
.filter-group {
|
.filter-row {
|
||||||
margin-bottom: calc($unit * 1.5);
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: $unit;
|
||||||
|
}
|
||||||
|
|
||||||
&:last-child {
|
.filter-group {
|
||||||
margin-bottom: 0;
|
display: flex;
|
||||||
}
|
flex-direction: column;
|
||||||
|
gap: $unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 $unit-half;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-label {
|
.filter-label {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: $font-tiny;
|
font-size: $font-small;
|
||||||
font-weight: $bold;
|
font-weight: $bold;
|
||||||
text-transform: uppercase;
|
|
||||||
color: var(--text-secondary);
|
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 {
|
.filter-buttons {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue