Skip to content

Commit 194ba1d

Browse files
committed
feat: add multi-tag filtering with AND logic
1 parent 864ab22 commit 194ba1d

File tree

2 files changed

+51
-10
lines changed

2 files changed

+51
-10
lines changed

src/components/Tags/Tags.tsx

+27-5
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,32 @@ import { cn } from "@/lib/utils";
1111
type TagProps = {
1212
tag: string;
1313
isActive?: boolean;
14+
activeTags?: string[];
1415
} & Omit<LinkProps, "href"> &
1516
ComponentPropsWithoutRef<"a">;
1617

17-
export function Tag({ tag, isActive, className, ...props }: TagProps) {
18+
export function Tag({
19+
tag,
20+
isActive,
21+
activeTags = [],
22+
className,
23+
...props
24+
}: TagProps) {
1825
const Icon = isActive ? IconRemove : IconTag;
26+
27+
// Generate URL based on tag state:
28+
// - If active: remove this tag from URL
29+
// - If inactive: add this tag to existing active tags
30+
const remainingTags = isActive
31+
? activeTags.filter((t) => t !== tag)
32+
: [...activeTags, tag];
33+
const href = remainingTags.length ? `/?tag=${remainingTags.join(",")}` : "/";
34+
1935
return (
2036
<Link
2137
{...props}
2238
className={cn(styles.tag, className, isActive && styles.active)}
23-
href={isActive ? "/" : `/?tag=${encodeURIComponent(tag)}`}
39+
href={href}
2440
>
2541
<Icon className={cn(styles.icon)} />
2642
<span className={styles.label}>{tag}</span>
@@ -30,17 +46,23 @@ export function Tag({ tag, isActive, className, ...props }: TagProps) {
3046

3147
interface TagsProps {
3248
tags: string[];
33-
activeTag?: string;
49+
activeTags: string[];
3450
className?: string;
3551
}
3652

37-
export function Tags({ tags, activeTag, className }: TagsProps) {
53+
export function Tags({ tags, activeTags, className }: TagsProps) {
3854
const label = getLabel("filterByTag");
3955
return (
4056
<div className={cn(styles.tags, className)}>
4157
{!!label && <h3>{label}</h3>}
4258
{tags.map((tag) => (
43-
<Tag key={tag} tag={tag} isActive={activeTag == tag} scroll={false} />
59+
<Tag
60+
key={tag}
61+
tag={tag}
62+
isActive={activeTags.includes(tag)}
63+
activeTags={activeTags}
64+
scroll={false}
65+
/>
4466
))}
4567
</div>
4668
);

src/pages/index.tsx

+24-5
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { CustomPage } from "@/pages/_app";
2020

2121
const Home: CustomPage = () => {
2222
const router = useRouter();
23-
const tag = router.query.tag as string | undefined;
23+
const tagQuery = router.query.tag as string | undefined;
2424
const appName = getAppName();
2525
const metaDescription = getLabel("metaDescription");
2626
const chartConfig = getChartConfig();
@@ -29,9 +29,28 @@ const Home: CustomPage = () => {
2929
const rings = getRings();
3030
const quadrants = getQuadrants();
3131
const tags = getTags();
32-
const items = getItems(undefined, true).filter(
33-
(item) => !tag || item.tags?.includes(tag),
34-
);
32+
33+
// Parse active tags from URL, filtering out empty values
34+
const activeTags = tagQuery?.split(",").filter(Boolean) ?? [];
35+
36+
// Remove empty tag parameter from URL
37+
if (tagQuery === "") {
38+
router.replace("/", undefined, { shallow: true });
39+
}
40+
41+
// Filter items based on selected tags
42+
const items = getItems(undefined, true).filter((item) => {
43+
// Show all items if no tags are selected
44+
if (!activeTags.length) return true;
45+
46+
// Handle potentially undefined tags with a default empty array
47+
const itemTags = item.tags ?? [];
48+
49+
// Show item only if it has all selected tags (AND logic)
50+
return (
51+
itemTags.length > 0 && activeTags.every((tag) => itemTags.includes(tag))
52+
);
53+
});
3554

3655
return (
3756
<>
@@ -65,7 +84,7 @@ const Home: CustomPage = () => {
6584
return (
6685
getToggle("showTagFilter") &&
6786
tags.length > 0 && (
68-
<Tags key={section} tags={tags} activeTag={tag} />
87+
<Tags key={section} tags={tags} activeTags={activeTags} />
6988
)
7089
);
7190
case "list":

0 commit comments

Comments
 (0)