From 295645e15063bec73066d6424809b7d96e77934b Mon Sep 17 00:00:00 2001 From: Mark Malstrom Date: Tue, 3 Jun 2025 23:13:37 -0500 Subject: [PATCH 1/2] feat(filters): add tag-based filtering --- app/components/filters/Filters.tsx | 3 +- app/components/filters/Tags.tsx | 73 ++++++++++++++++++++++++++++++ app/routes/_index/route.tsx | 23 ++++++++-- app/routes/_index/utilities.ts | 17 ++++++- 4 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 app/components/filters/Tags.tsx diff --git a/app/components/filters/Filters.tsx b/app/components/filters/Filters.tsx index fce34c3..7ad33d0 100644 --- a/app/components/filters/Filters.tsx +++ b/app/components/filters/Filters.tsx @@ -1,11 +1,12 @@ import { DisclosureGroup } from "react-aria-components"; import * as Stars from "./Stars"; +import * as Tags from "./Tags"; export function Filters() { return ( {/* TODO: Search */} - {/* TODO: Tags */} + {/* TODO: Date? */} diff --git a/app/components/filters/Tags.tsx b/app/components/filters/Tags.tsx new file mode 100644 index 0000000..887f94e --- /dev/null +++ b/app/components/filters/Tags.tsx @@ -0,0 +1,73 @@ +import { ChevronRightIcon } from "@heroicons/react/24/outline"; +import { cx } from "cva"; +import { Form, useLoaderData } from "react-router"; +import { TokenButton } from "../Token"; +import type { Route } from "../../routes/_index/+types/route"; +import { Button, Disclosure, DisclosurePanel, Heading } from "react-aria-components"; + +export function Header() { + return ( + <> + + + + + Tags + + + ); +} + +export function Options() { + const { tags, tag } = useLoaderData() as Route.ComponentProps["loaderData"]; + + return ( + +
+ {tags.map((name: string) => ( + + ))} +
+ +
+
+
+ ); +} + +export function Filter() { + return ( + + `${values.defaultClassName} border-t-2 border-black px-4 py-6 dark:border-white/50`} + > +
+ + + + + +
+ ); +} diff --git a/app/routes/_index/route.tsx b/app/routes/_index/route.tsx index 075699d..ccddae5 100644 --- a/app/routes/_index/route.tsx +++ b/app/routes/_index/route.tsx @@ -5,7 +5,7 @@ import { Recommendation } from "~/components/Recommendation"; import { Filters } from "~/components/filters/Filters"; import { getCollection } from "~/lib/content"; import { stores } from "~/lib/stores.client"; -import { filterRecs, Stars } from "./utilities"; +import { filterRecs, Stars, TagFilter } from "./utilities"; import type { Route } from "./+types/route"; import { TokenButton } from "../../components/Token"; @@ -17,18 +17,26 @@ export async function loader({ request }: Route.LoaderArgs) { } const stars = new Stars(request); + const tagFilter = new TagFilter(request); const recs = collection.map((recommendation) => ({ ...recommendation.data, slug: recommendation.slug as string, description: recommendation.body, })); + const tags = Array.from( + new Set(recs.flatMap((rec) => rec.tags?.map((t) => t.name) ?? [])), + ); + return { stars: stars.count, + tag: tagFilter.name, + tags, recs, filteredRecs: filterRecs({ recs, stars, + tag: tagFilter, }), }; } @@ -38,13 +46,16 @@ const CACHE_KEY = "recommendations"; export async function clientLoader({ request, serverLoader }: Route.ClientLoaderArgs) { const stars = new Stars(request); + const tagFilter = new TagFilter(request); async function fetchData() { - const { recs } = await serverLoader(); + const { recs, tags } = await serverLoader(); await stores.cache.set(CACHE_KEY, recs); return { stars: stars.count, - filteredRecs: filterRecs({ recs, stars }), + tag: tagFilter.name, + tags, + filteredRecs: filterRecs({ recs, stars, tag: tagFilter }), }; } @@ -53,7 +64,11 @@ export async function clientLoader({ request, serverLoader }: Route.ClientLoader return cachedRecs ? { stars: stars.count, - filteredRecs: filterRecs({ recs: cachedRecs, stars }), + tag: tagFilter.name, + tags: Array.from( + new Set(cachedRecs.flatMap((rec) => rec.tags?.map((t) => t.name) ?? [])), + ), + filteredRecs: filterRecs({ recs: cachedRecs, stars, tag: tagFilter }), } : null; } diff --git a/app/routes/_index/utilities.ts b/app/routes/_index/utilities.ts index b574659..02f199f 100644 --- a/app/routes/_index/utilities.ts +++ b/app/routes/_index/utilities.ts @@ -9,8 +9,23 @@ export class Stars { } } -export function filterRecs({ recs, stars }: { recs: HydratedRec[]; stars: Stars }): HydratedRec[] { +export class TagFilter { + readonly name: string | null; + + constructor(request: Request) { + this.name = new URL(request.url).searchParams.get("tag"); + } +} + +export function filterRecs( + { recs, stars, tag }: { recs: HydratedRec[]; stars: Stars; tag: TagFilter }, +): HydratedRec[] { return recs .filter((rec) => (stars.count !== null ? rec.stars === stars.count : true)) + .filter((rec) => + tag.name !== null + ? (rec.tags ?? []).some((t) => t.name.toLowerCase() === tag.name!.toLowerCase()) + : true + ) .sort((lhs, rhs) => rhs.createdOn.getTime() - lhs.createdOn.getTime()); } From 334d289bdfe98f662dd4d65cb85828527bf63a32 Mon Sep 17 00:00:00 2001 From: Mark Malstrom Date: Tue, 3 Jun 2025 23:27:01 -0500 Subject: [PATCH 2/2] Filter by tags without links --- app/components/filters/Tags.tsx | 21 ++++++++------------- app/routes/_index/route.tsx | 16 ++++++++++++++-- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/app/components/filters/Tags.tsx b/app/components/filters/Tags.tsx index 887f94e..a7ba244 100644 --- a/app/components/filters/Tags.tsx +++ b/app/components/filters/Tags.tsx @@ -23,28 +23,23 @@ export function Options() { return ( -
+
{tags.map((name: string) => ( - + ))} -
- -
+
); diff --git a/app/routes/_index/route.tsx b/app/routes/_index/route.tsx index ccddae5..cb780de 100644 --- a/app/routes/_index/route.tsx +++ b/app/routes/_index/route.tsx @@ -25,7 +25,13 @@ export async function loader({ request }: Route.LoaderArgs) { })); const tags = Array.from( - new Set(recs.flatMap((rec) => rec.tags?.map((t) => t.name) ?? [])), + new Set( + recs.flatMap((rec) => + (rec.tags ?? []) + .filter((tag) => !tag.link) + .map((tag) => tag.name) + ), + ), ); return { @@ -66,7 +72,13 @@ export async function clientLoader({ request, serverLoader }: Route.ClientLoader stars: stars.count, tag: tagFilter.name, tags: Array.from( - new Set(cachedRecs.flatMap((rec) => rec.tags?.map((t) => t.name) ?? [])), + new Set( + cachedRecs.flatMap((rec) => + (rec.tags ?? []) + .filter((tag) => !tag.link) + .map((tag) => tag.name) + ), + ), ), filteredRecs: filterRecs({ recs: cachedRecs, stars, tag: tagFilter }), }