diff --git a/src/routes/Resources/CategoryCard.tsx b/src/routes/Resources/CategoryCard.tsx
new file mode 100644
index 0000000..f7195dd
--- /dev/null
+++ b/src/routes/Resources/CategoryCard.tsx
@@ -0,0 +1,21 @@
+import { Link } from "react-router";
+
+interface Props {
+ name: string;
+ slug: string;
+}
+
+const CategoryCard = ({ name, slug }: Props) => {
+ return (
+ <>
+
+ {name}
+
+ >
+ );
+};
+
+export default CategoryCard;
diff --git a/src/routes/Resources/ResourceCard.tsx b/src/routes/Resources/ResourceCard.tsx
new file mode 100644
index 0000000..d7aeb7b
--- /dev/null
+++ b/src/routes/Resources/ResourceCard.tsx
@@ -0,0 +1,27 @@
+import { Entry } from "./types";
+
+const ResourceCard = ({ entry }: { entry: Entry }) => {
+ return (
+
+ );
+};
+export default ResourceCard;
diff --git a/src/routes/Resources/ResourcePage.tsx b/src/routes/Resources/ResourcePage.tsx
new file mode 100644
index 0000000..26b3c5a
--- /dev/null
+++ b/src/routes/Resources/ResourcePage.tsx
@@ -0,0 +1,92 @@
+import { useEffect, useState } from "react";
+import { useParams } from "react-router";
+import { JsonData, Entry } from "./types";
+import ResourceCard from "./ResourceCard";
+import { extractTags, sortEntries } from "./utils";
+
+const ResourcePage = () => {
+ const { category } = useParams();
+ const [data, setData] = useState
(null);
+ const [entries, setEntries] = useState([]);
+ const [tags, setTags] = useState([]);
+ const [filters, setFilters] = useState([]);
+
+ useEffect(() => {
+ const fetchData = async () => {
+ if (category) {
+ const dataObject = (await import(
+ `../../data/resources/${category}.json`
+ )) as { default: JsonData };
+ return dataObject.default;
+ } else {
+ return { pageName: "", entries: [] };
+ }
+ };
+
+ fetchData()
+ .then((json) => {
+ if (json.entries.length > 0) {
+ setData(json);
+ setEntries(sortEntries(json.entries));
+ setTags(extractTags(json.entries));
+ }
+ })
+ .catch(console.error);
+ }, [category]);
+
+ const filterColor = (tag: string) =>
+ filters.includes(tag) ? "bg-slate-400" : "bg-slate-300";
+
+ if (data) {
+ return (
+ <>
+
+
+ {data.pageName}
+
+
+
Categories
+
+ {tags.map((t) => (
+
{
+ setFilters([...filters, t]);
+ setEntries(entries.filter((e) => e.tags.includes(t)));
+ }}
+ >
+
+
+ ))}
+ {filters.length > 0 && (
+
+ )}
+
+
+
+
+ {entries.map((e) => (
+
+ ))}
+
+
+
+ >
+ );
+ }
+
+ return <>>;
+};
+export default ResourcePage;
diff --git a/src/routes/Resources/index.tsx b/src/routes/Resources/index.tsx
new file mode 100644
index 0000000..a34419c
--- /dev/null
+++ b/src/routes/Resources/index.tsx
@@ -0,0 +1,21 @@
+import CategoryCard from "./CategoryCard";
+
+const Resources = () => (
+
+
+
+);
+
+export default Resources;
diff --git a/src/routes/Resources/types.ts b/src/routes/Resources/types.ts
new file mode 100644
index 0000000..c87761f
--- /dev/null
+++ b/src/routes/Resources/types.ts
@@ -0,0 +1,10 @@
+export interface JsonData {
+ pageName: string;
+ entries: Entry[];
+}
+
+export interface Entry {
+ title: string;
+ url: string;
+ tags: string[];
+}
diff --git a/src/routes/Resources/utils.ts b/src/routes/Resources/utils.ts
new file mode 100644
index 0000000..cb04bd8
--- /dev/null
+++ b/src/routes/Resources/utils.ts
@@ -0,0 +1,16 @@
+import { Entry } from "./types";
+
+export const sortEntries = (entries: Entry[]) =>
+ entries.sort((a, b) => (sanitize(a.title) < sanitize(b.title) ? -1 : 1));
+
+const sanitize = (title: string) =>
+ title.toLowerCase().replace("the", "").trim();
+
+export const extractTags = (entries: Entry[]) => [
+ ...new Set(
+ entries.reduce((acc, cur) => {
+ acc.push(...cur.tags);
+ return acc;
+ }, []),
+ ),
+];