Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/components/filters/Filters.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { DisclosureGroup } from "react-aria-components";
import * as Stars from "./Stars";
import * as Tags from "./Tags";

export function Filters() {
return (
<DisclosureGroup className="sticky top-0 mx-20 hidden h-screen pt-10 lg:block">
{/* TODO: Search */}
{/* TODO: Tags */}
<Tags.Filter />
<Stars.Filter />
{/* TODO: Date? */}
</DisclosureGroup>
Expand Down
68 changes: 68 additions & 0 deletions app/components/filters/Tags.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<span className="flex items-center">
<ChevronRightIcon className="h-5 w-5" />
</span>
<span className="font-serif-text text-xl font-medium text-gray-900 dark:text-gray-50">
Tags
</span>
</>
);
}

export function Options() {
const { tags, tag } = useLoaderData() as Route.ComponentProps["loaderData"];

return (
<DisclosurePanel>
<div className="flex flex-wrap gap-3 pl-11 pt-4">
{tags.map((name: string) => (
<TokenButton
aria-label={`Show ${name} recommendations`}
className={cx(tag === name ? undefined : "opacity-50")}
key={name}
name="tag"
type="submit"
value={name}
>
{name}
</TokenButton>
))}
<TokenButton
aria-label="Clear tag filter and show all recommendations"
label="Clear"
/>
</div>
</DisclosurePanel>
);
}

export function Filter() {
return (
<Disclosure
className={(values) =>
`${values.defaultClassName} border-t-2 border-black px-4 py-6 dark:border-white/50`}
>
<Form>
<Heading>
<Button
className={(values) =>
`${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"
>
<Header />
</Button>
</Heading>
<Options />
</Form>
</Disclosure>
);
}
35 changes: 31 additions & 4 deletions app/routes/_index/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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,
}),
};
}
Expand All @@ -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 }),
};
}

Expand All @@ -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;
}
Expand Down
17 changes: 16 additions & 1 deletion app/routes/_index/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}