feat: add award category filter and remove sort dropdown

- Add category filter dropdown to awards list page
- Filters awards by category (dxcc, darc, was, etc.)
- Remove unused "Sort by" dropdown from award detail page
- Categories auto-extracted from award definitions

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-19 12:43:02 +01:00
parent 8d47e6e4ad
commit f09d96aa8c
2 changed files with 75 additions and 39 deletions

View File

@@ -2,7 +2,10 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { auth } from '$lib/stores.js'; import { auth } from '$lib/stores.js';
let allAwards = [];
let awards = []; let awards = [];
let categories = [];
let selectedCategory = 'all';
let loading = true; let loading = true;
let error = null; let error = null;
@@ -33,7 +36,7 @@
} }
// Load progress for each award // Load progress for each award
awards = await Promise.all( allAwards = await Promise.all(
data.awards.map(async (award) => { data.awards.map(async (award) => {
try { try {
const progressResponse = await fetch(`/api/awards/${award.id}/progress`, { const progressResponse = await fetch(`/api/awards/${award.id}/progress`, {
@@ -67,18 +70,50 @@
}; };
}) })
); );
// Extract unique categories
categories = ['all', ...new Set(allAwards.map(a => a.category).filter(Boolean))];
// Apply filter
applyFilter();
} catch (e) { } catch (e) {
error = e.message; error = e.message;
} finally { } finally {
loading = false; loading = false;
} }
} }
function applyFilter() {
if (selectedCategory === 'all') {
awards = allAwards;
} else {
awards = allAwards.filter(award => award.category === selectedCategory);
}
}
function onCategoryChange(event) {
selectedCategory = event.target.value;
applyFilter();
}
</script> </script>
<div class="container"> <div class="container">
<h1>Awards</h1> <h1>Awards</h1>
<p class="subtitle">Track your ham radio award progress</p> <p class="subtitle">Track your ham radio award progress</p>
{#if !loading && awards.length > 0}
<div class="filters">
<div class="filter-group">
<label for="category-filter">Category:</label>
<select id="category-filter" value={selectedCategory} on:change={onCategoryChange}>
{#each categories as category}
<option value={category}>{category === 'all' ? 'All Awards' : category.toUpperCase()}</option>
{/each}
</select>
</div>
</div>
{/if}
{#if loading} {#if loading}
<div class="loading">Loading awards...</div> <div class="loading">Loading awards...</div>
{:else if error} {:else if error}
@@ -162,6 +197,45 @@
margin-bottom: 2rem; margin-bottom: 2rem;
} }
.filters {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
align-items: center;
}
.filter-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.filter-group label {
font-weight: 600;
color: #333;
font-size: 0.9rem;
}
.filter-group select {
padding: 0.5rem 1rem;
border: 1px solid #ccc;
border-radius: 4px;
background-color: white;
font-size: 0.9rem;
cursor: pointer;
min-width: 150px;
}
.filter-group select:hover {
border-color: #4a90e2;
}
.filter-group select:focus {
outline: none;
border-color: #4a90e2;
box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2);
}
.loading, .loading,
.error, .error,
.empty { .empty {

View File

@@ -7,7 +7,6 @@
let entities = []; let entities = [];
let loading = true; let loading = true;
let error = null; let error = null;
let sort = 'name'; // name
let groupedData = []; let groupedData = [];
let bands = []; let bands = [];
@@ -175,15 +174,6 @@
<a href="/awards" class="back-link">← Back to Awards</a> <a href="/awards" class="back-link">← Back to Awards</a>
</div> </div>
<div class="controls">
<div class="sort-group">
<label>Sort by:</label>
<select bind:value={sort}>
<option value="name">Name</option>
</select>
</div>
</div>
<div class="summary"> <div class="summary">
{#if entities.length > 0 && entities[0].points !== undefined} {#if entities.length > 0 && entities[0].points !== undefined}
{@const earnedPoints = entities.reduce((sum, e) => sum + (e.confirmed ? e.points : 0), 0)} {@const earnedPoints = entities.reduce((sum, e) => sum + (e.confirmed ? e.points : 0), 0)}
@@ -345,34 +335,6 @@
text-decoration: underline; text-decoration: underline;
} }
.controls {
display: flex;
gap: 2rem;
margin-bottom: 2rem;
flex-wrap: wrap;
}
.filter-group,
.sort-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
label {
font-weight: 600;
color: #333;
}
select {
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
background-color: white;
font-size: 1rem;
cursor: pointer;
}
.summary { .summary {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));