Skip to content

Commit 5da9740

Browse files
authored
Merge pull request #50 from mcinquin/ui
feat(ui): add multiselector for tags
2 parents 4336ed2 + 7b419c4 commit 5da9740

5 files changed

Lines changed: 88 additions & 79 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "hobbyhoops",
3-
"version": "1.14.0",
3+
"version": "1.13.1",
44
"private": true,
55
"engines": {
66
"node": ">=24 <25"

src/app/(app)/collection/page.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,14 @@ function toSearchParams(
1616
): URLSearchParams {
1717
const searchParams = new URLSearchParams();
1818
for (const [key, value] of Object.entries(params)) {
19-
const raw = Array.isArray(value) ? value[0] : value;
20-
if (raw) searchParams.set(key, raw);
19+
if (value === undefined) continue;
20+
if (Array.isArray(value)) {
21+
for (const entry of value) {
22+
if (entry) searchParams.append(key, entry);
23+
}
24+
} else if (value) {
25+
searchParams.set(key, value);
26+
}
2127
}
2228
return searchParams;
2329
}

src/components/card-table.tsx

Lines changed: 21 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ import {
99
type ColumnDef,
1010
} from "@tanstack/react-table";
1111
import {
12+
COLLECTION_TAG_VALUES,
1213
setsForBrandFilter,
1314
variationsForFilters,
1415
type CollectionSortKey,
16+
type CollectionTagValue,
1517
} from "@/lib/collection-query";
1618
import { Card, type References } from "@/lib/types";
1719
import { CardBadges } from "@/components/card-badges";
@@ -115,11 +117,10 @@ export function CardTable({
115117
const { filters: urlFilters, updateFilters, toggleSort, isPending } =
116118
useCollectionUrlFilters();
117119

118-
const rookieOnly = urlFilters.tag === "rookie";
119-
const autoOnly = urlFilters.tag === "autograph";
120-
const memoOnly = urlFilters.tag === "memorabilia";
121-
const serialOnly = urlFilters.tag === "numbered";
122-
const tradableOnly = urlFilters.tag === "tradable";
120+
const selectedTags = useMemo(
121+
() => new Set(urlFilters.tags),
122+
[urlFilters.tags]
123+
);
123124

124125
function sortState(column: CollectionSortKey): false | "asc" | "desc" {
125126
if (urlFilters.sort !== column) return false;
@@ -265,11 +266,11 @@ export function CardTable({
265266
}
266267
}, [urlFilters.variation, variationSuggestions, updateFilters]);
267268

268-
function toggleTag(tag: typeof urlFilters.tag) {
269-
updateFilters(
270-
{ tag: urlFilters.tag === tag ? "" : tag },
271-
{ immediate: true }
272-
);
269+
function toggleTag(tag: CollectionTagValue) {
270+
const next = selectedTags.has(tag)
271+
? urlFilters.tags.filter((value) => value !== tag)
272+
: [...urlFilters.tags, tag];
273+
updateFilters({ tags: next }, { immediate: true });
273274
}
274275

275276
const activeFilters = useMemo(
@@ -290,21 +291,9 @@ export function CardTable({
290291
urlFilters.variation
291292
? t("cards.activeVariation", { value: urlFilters.variation })
292293
: "",
293-
rookieOnly ? t("badges.rookie") : "",
294-
autoOnly ? t("badges.autograph") : "",
295-
memoOnly ? t("badges.memorabilia") : "",
296-
serialOnly ? t("badges.numbered") : "",
297-
tradableOnly ? t("badges.tradable") : "",
294+
...urlFilters.tags.map((tag) => badgeLabels[tag]),
298295
].filter(Boolean),
299-
[
300-
autoOnly,
301-
memoOnly,
302-
rookieOnly,
303-
serialOnly,
304-
t,
305-
tradableOnly,
306-
urlFilters,
307-
]
296+
[badgeLabels, t, urlFilters]
308297
);
309298

310299
return (
@@ -381,26 +370,14 @@ export function CardTable({
381370
/>
382371

383372
<div className="flex flex-wrap gap-1 sm:col-span-2 lg:col-span-1">
384-
<FilterChipButton
385-
label={t("badges.rookie")}
386-
pressed={rookieOnly}
387-
onPressedChange={() => toggleTag("rookie")}
388-
/>
389-
<FilterChipButton
390-
label={t("badges.autograph")}
391-
pressed={autoOnly}
392-
onPressedChange={() => toggleTag("autograph")}
393-
/>
394-
<FilterChipButton
395-
label={t("badges.memorabilia")}
396-
pressed={memoOnly}
397-
onPressedChange={() => toggleTag("memorabilia")}
398-
/>
399-
<FilterChipButton
400-
label={t("badges.numbered")}
401-
pressed={serialOnly}
402-
onPressedChange={() => toggleTag("numbered")}
403-
/>
373+
{COLLECTION_TAG_VALUES.map((tag) => (
374+
<FilterChipButton
375+
key={tag}
376+
label={badgeLabels[tag]}
377+
pressed={selectedTags.has(tag)}
378+
onPressedChange={() => toggleTag(tag)}
379+
/>
380+
))}
404381
</div>
405382
</div>
406383

src/hooks/use-collection-url-filters.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import {
66
parseCollectionSearchParams,
77
type CollectionListQuery,
88
type CollectionSortKey,
9-
type CollectionTagFilter,
9+
type CollectionTagValue,
1010
} from "@/lib/collection-query";
1111

12-
export type { CollectionTagFilter, CollectionSortKey };
12+
export type { CollectionTagValue, CollectionSortKey };
1313

1414
export type CollectionFiltersState = Pick<
1515
CollectionListQuery,
@@ -20,7 +20,7 @@ export type CollectionFiltersState = Pick<
2020
| "brand"
2121
| "set"
2222
| "variation"
23-
| "tag"
23+
| "tags"
2424
| "page"
2525
| "sort"
2626
| "sortDesc"
@@ -48,7 +48,9 @@ export function useCollectionUrlFilters() {
4848
if (next.brand) params.set("brand", next.brand);
4949
if (next.set) params.set("set", next.set);
5050
if (next.variation) params.set("variation", next.variation);
51-
if (next.tag) params.set("tag", next.tag);
51+
for (const tag of next.tags) {
52+
params.append("tag", tag);
53+
}
5254
if (next.page > 1) params.set("page", String(next.page));
5355
if (next.sort !== "player") params.set("sort", next.sort);
5456
if (next.sortDesc) params.set("sortDir", "desc");

src/lib/collection-query.ts

Lines changed: 52 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,21 @@ import type { References } from "@/lib/types";
44
export const COLLECTION_PAGE_SIZE = 50;
55
export const COLLECTION_MAX_PAGE_SIZE = 100;
66

7-
export type CollectionTagFilter =
8-
| ""
7+
export type CollectionTagValue =
98
| "rookie"
109
| "autograph"
1110
| "memorabilia"
1211
| "numbered"
1312
| "tradable";
1413

14+
export const COLLECTION_TAG_VALUES: CollectionTagValue[] = [
15+
"rookie",
16+
"autograph",
17+
"memorabilia",
18+
"numbered",
19+
"tradable",
20+
];
21+
1522
export type CollectionSortKey =
1623
| "player"
1724
| "team"
@@ -33,15 +40,29 @@ const sortKeySchema = z.enum([
3340
"grading",
3441
]);
3542

36-
const tagSchema = z
37-
.union([
38-
z.literal(""),
39-
z.enum(["rookie", "autograph", "memorabilia", "numbered", "tradable"]),
40-
])
41-
.catch("");
43+
const tagValueSchema = z.enum([
44+
"rookie",
45+
"autograph",
46+
"memorabilia",
47+
"numbered",
48+
"tradable",
49+
]);
4250

4351
const filterValueSchema = z.string().trim().max(128).catch("");
4452

53+
function parseCollectionTags(params: URLSearchParams): CollectionTagValue[] {
54+
const seen = new Set<CollectionTagValue>();
55+
const tags: CollectionTagValue[] = [];
56+
for (const raw of params.getAll("tag")) {
57+
const parsed = tagValueSchema.safeParse(raw);
58+
if (parsed.success && !seen.has(parsed.data)) {
59+
seen.add(parsed.data);
60+
tags.push(parsed.data);
61+
}
62+
}
63+
return tags;
64+
}
65+
4566
export interface CollectionListQuery {
4667
search: string;
4768
player: string;
@@ -50,7 +71,8 @@ export interface CollectionListQuery {
5071
brand: string;
5172
set: string;
5273
variation: string;
53-
tag: CollectionTagFilter;
74+
/** Filtres cumulés (ET) : ex. rookie + autograph = RC et Auto. */
75+
tags: CollectionTagValue[];
5476
page: number;
5577
pageSize: number;
5678
sort: CollectionSortKey;
@@ -99,7 +121,7 @@ export function parseCollectionSearchParams(
99121
brand: filterValueSchema.parse(params.get("brand") ?? ""),
100122
set: filterValueSchema.parse(params.get("set") ?? ""),
101123
variation: filterValueSchema.parse(params.get("variation") ?? ""),
102-
tag: tagSchema.parse(params.get("tag") ?? ""),
124+
tags: parseCollectionTags(params),
103125
page,
104126
pageSize,
105127
sort,
@@ -153,24 +175,26 @@ export function buildCollectionWhereClause(
153175
params.push(`%${escapeLike(query.variation.trim().toLowerCase())}%`);
154176
}
155177

156-
switch (query.tag) {
157-
case "rookie":
158-
clauses.push("rookie = 1");
159-
break;
160-
case "autograph":
161-
clauses.push("autograph = 1");
162-
break;
163-
case "memorabilia":
164-
clauses.push("memorabilia = 1");
165-
break;
166-
case "numbered":
167-
clauses.push("serial_number IS NOT NULL AND serial_number != ''");
168-
break;
169-
case "tradable":
170-
clauses.push("tradable = 1");
171-
break;
172-
default:
173-
break;
178+
for (const tag of query.tags) {
179+
switch (tag) {
180+
case "rookie":
181+
clauses.push("rookie = 1");
182+
break;
183+
case "autograph":
184+
clauses.push("autograph = 1");
185+
break;
186+
case "memorabilia":
187+
clauses.push("memorabilia = 1");
188+
break;
189+
case "numbered":
190+
clauses.push("serial_number IS NOT NULL AND serial_number != ''");
191+
break;
192+
case "tradable":
193+
clauses.push("tradable = 1");
194+
break;
195+
default:
196+
break;
197+
}
174198
}
175199

176200
const whereSql = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";

0 commit comments

Comments
 (0)