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..a7ba244 --- /dev/null +++ b/app/components/filters/Tags.tsx @@ -0,0 +1,68 @@ +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) => ( + + {name} + + ))} + + + + ); +} + +export function Filter() { + return ( + + `${values.defaultClassName} border-t-2 border-black px-4 py-6 dark:border-white/50`} + > + + + + `${values.defaultClassName} flex w-full items-center gap-6 px-2 py-3 text-black/50 focus-visible:outline-0 dark:text-white/50`} + slot="trigger" + > + + + + + + + ); +} diff --git a/app/routes/_index/route.tsx b/app/routes/_index/route.tsx index 075699d..cb780de 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,32 @@ 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 ?? []) + .filter((tag) => !tag.link) + .map((tag) => tag.name) + ), + ), + ); + return { stars: stars.count, + tag: tagFilter.name, + tags, recs, filteredRecs: filterRecs({ recs, stars, + tag: tagFilter, }), }; } @@ -38,13 +52,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 +70,17 @@ 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 ?? []) + .filter((tag) => !tag.link) + .map((tag) => tag.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()); }