Adds filters and sorts for Hunt view

This commit is contained in:
2026-05-18 14:19:16 -05:00
parent 63d027ebb7
commit 3a0a0ae791

View File

@@ -173,6 +173,46 @@
const approved = $derived(Object.values(itemStatuses).filter(s => s.itemFoundStatus === 'APPROVED').length)
const submitted = $derived(Object.values(itemStatuses).filter(s => s.itemFoundStatus === 'SUBMITTED').length)
const rejected = $derived(Object.values(itemStatuses).filter(s => s.itemFoundStatus === 'REJECTED').length)
let searchQuery = $state('')
let filterStatus = $state<string | null>(null)
let filterPoints = $state<number | null>(null)
let sortBy = $state<'name' | 'points'>('points')
let sortDir = $state<'asc' | 'desc'>('asc')
const uniquePointValues = $derived([...new Set(items.map(i => i.points))].sort((a, b) => a - b))
const filteredItems = $derived(
[...items]
.filter(item => {
if (searchQuery && !item.name.toLowerCase().includes(searchQuery.toLowerCase())) return false
if (filterStatus && (itemStatuses[item.id]?.itemFoundStatus ?? 'NOT_FOUND') !== filterStatus) return false
if (filterPoints !== null && item.points !== filterPoints) return false
return true
})
.sort((a, b) => {
if (sortBy === 'name') {
const cmp = a.name.localeCompare(b.name)
return sortDir === 'asc' ? cmp : -cmp
}
const ptsCmp = a.points - b.points
if (ptsCmp !== 0) return sortDir === 'asc' ? ptsCmp : -ptsCmp
return a.name.localeCompare(b.name)
})
)
function toggleSort(field: 'name' | 'points') {
if (sortBy === field) sortDir = sortDir === 'asc' ? 'desc' : 'asc'
else { sortBy = field; sortDir = 'asc' }
}
const statusFilters: { label: string; value: string | null }[] = [
{ label: 'All', value: null },
{ label: 'Not Found', value: 'NOT_FOUND' },
{ label: 'Submitted', value: 'SUBMITTED' },
{ label: 'Approved', value: 'APPROVED' },
{ label: 'Rejected', value: 'REJECTED' },
]
</script>
<div class="pb-20">
@@ -248,8 +288,55 @@
<!-- On a team: item list -->
{:else}
<!-- Filter & sort controls -->
<div class="px-4 pt-3 pb-2 flex flex-col gap-2 border-b border-base-200">
<input
type="search"
placeholder="Search items…"
class="input input-bordered input-sm w-full"
bind:value={searchQuery}
/>
<div class="flex gap-2 overflow-x-auto pb-1 no-scrollbar">
{#each statusFilters as f}
<button
class="btn btn-xs shrink-0"
class:btn-primary={filterStatus === f.value}
class:btn-outline={filterStatus !== f.value}
onclick={() => filterStatus = f.value}
>{f.label}</button>
{/each}
</div>
<div class="flex items-center gap-2">
<select
class="select select-bordered select-sm flex-1"
onchange={(e) => filterPoints = (e.target as HTMLSelectElement).value === '' ? null : Number((e.target as HTMLSelectElement).value)}
>
<option value="">All pts</option>
{#each uniquePointValues as pts}
<option value={pts}>{pts} pts</option>
{/each}
</select>
<button
class="btn btn-sm gap-1"
class:btn-primary={sortBy === 'points'}
class:btn-outline={sortBy !== 'points'}
onclick={() => toggleSort('points')}
>
Pts {sortBy === 'points' ? (sortDir === 'asc' ? '↑' : '↓') : '↕'}
</button>
<button
class="btn btn-sm gap-1"
class:btn-primary={sortBy === 'name'}
class:btn-outline={sortBy !== 'name'}
onclick={() => toggleSort('name')}
>
Name {sortBy === 'name' ? (sortDir === 'asc' ? '↑' : '↓') : '↕'}
</button>
</div>
</div>
<div class="flex flex-col gap-2 p-4">
{#each items as item}
{#each filteredItems as item}
{@const status = itemStatuses[item.id]}
<button
class="card bg-base-100 shadow-sm border border-base-200 text-left w-full hover:border-primary transition-colors"
@@ -265,6 +352,8 @@
{/if}
</div>
</button>
{:else}
<p class="text-center text-base-content/50 py-8 text-sm">No items match your filters</p>
{/each}
</div>
{/if}