diff --git a/src/components/Badge/Badge.module.css b/src/components/Badge/Badge.module.css
index 3ef0dfe..330aa87 100644
--- a/src/components/Badge/Badge.module.css
+++ b/src/components/Badge/Badge.module.css
@@ -7,6 +7,7 @@
border: 1px solid transparent;
border-radius: 13px;
font-size: 12px;
+ line-height: 1;
overflow: hidden;
text-decoration: none;
}
diff --git a/src/components/Badge/Badge.tsx b/src/components/Badge/Badge.tsx
index 5cdb260..fe83d4b 100644
--- a/src/components/Badge/Badge.tsx
+++ b/src/components/Badge/Badge.tsx
@@ -28,8 +28,11 @@ export function Badge({
() => (color ? ({ "--badge": color } as CSSProperties) : undefined),
[color],
);
+
+ const Component = props.onClick ? "button" : "span";
+
return (
-
+
);
}
diff --git a/src/components/ItemList/ItemList.module.css b/src/components/ItemList/ItemList.module.css
index 7299129..5ac0b4c 100644
--- a/src/components/ItemList/ItemList.module.css
+++ b/src/components/ItemList/ItemList.module.css
@@ -29,3 +29,11 @@
opacity: 1;
}
}
+
+.isSmall {
+ font-size: 14px;
+
+ .link {
+ padding: 8px;
+ }
+}
diff --git a/src/components/ItemList/ItemList.tsx b/src/components/ItemList/ItemList.tsx
index 30954b4..e70a678 100644
--- a/src/components/ItemList/ItemList.tsx
+++ b/src/components/ItemList/ItemList.tsx
@@ -6,14 +6,25 @@ import { FlagBadge } from "@/components/Badge/Badge";
import { Item } from "@/lib/types";
import { cn } from "@/lib/utils";
-interface ItemListProps {
+export interface ItemListProps {
items: Item[];
activeId?: string;
+ size?: "small" | "default";
+ className?: string;
}
-export function ItemList({ items, activeId }: ItemListProps) {
+export function ItemList({
+ items,
+ activeId,
+ size = "default",
+ className,
+}: ItemListProps) {
return (
-
+
{items.map((item) => (
-
{item.title}
-
+
))}
diff --git a/src/components/QuadrantList/QuadrantList.module.css b/src/components/QuadrantList/QuadrantList.module.css
new file mode 100644
index 0000000..14b294f
--- /dev/null
+++ b/src/components/QuadrantList/QuadrantList.module.css
@@ -0,0 +1,35 @@
+.quadrants {
+ --cols: 1;
+ --gap: 60px;
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--gap);
+}
+
+.quadrant {
+ margin-bottom: 20px;
+ flex: 1 0
+ calc(100% / var(--cols) - var(--gap) / var(--cols) * (var(--cols) - 1));
+}
+
+.header {
+ padding: 10px 0;
+ margin-bottom: 20px;
+ border-bottom: 1px solid var(--border);
+}
+
+.title {
+ margin: 0;
+}
+
+.link {
+}
+
+@media (min-width: 1024px) {
+ .quadrant {
+ --cols: 2;
+ }
+}
diff --git a/src/components/QuadrantList/QuadrantList.tsx b/src/components/QuadrantList/QuadrantList.tsx
new file mode 100644
index 0000000..7e6b348
--- /dev/null
+++ b/src/components/QuadrantList/QuadrantList.tsx
@@ -0,0 +1,36 @@
+import Link from "next/link";
+
+import styles from "./QuadrantList.module.css";
+
+import { RingList } from "@/components/RingList/RingList";
+import {
+ getQuadrant,
+ groupItemsByQuadrant,
+ groupItemsByRing,
+} from "@/lib/data";
+import { Item } from "@/lib/types";
+
+interface RingListProps {
+ items: Item[];
+}
+export function QuadrantList({ items }: RingListProps) {
+ const quadrants = groupItemsByQuadrant(items);
+ return (
+
+ {Object.entries(quadrants).map(([quadrantId, items]) => {
+ const quadrant = getQuadrant(quadrantId);
+ if (!quadrant) return null;
+ return (
+ -
+
+
+ {quadrant.title}
+
+
+
+
+ );
+ })}
+
+ );
+}
diff --git a/src/components/RingList/RingList.module.css b/src/components/RingList/RingList.module.css
new file mode 100644
index 0000000..9a5ec0b
--- /dev/null
+++ b/src/components/RingList/RingList.module.css
@@ -0,0 +1,39 @@
+.rings {
+ --cols: 1;
+ --gap: 30px;
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--gap);
+}
+
+.ring {
+ margin-bottom: 20px;
+ flex: 1 0
+ calc(100% / var(--cols) - var(--gap) / var(--cols) * (var(--cols) - 1));
+}
+
+.badge {
+ margin-bottom: 20px;
+}
+
+.list {
+}
+
+@media (min-width: 768px) {
+ .ring {
+ --cols: 2;
+ }
+
+ .isSmall .ring {
+ --cols: 4;
+ }
+}
+
+@media (min-width: 1024px) {
+ .ring {
+ --cols: 4;
+ }
+}
diff --git a/src/components/RingList/RingList.tsx b/src/components/RingList/RingList.tsx
new file mode 100644
index 0000000..26c54ad
--- /dev/null
+++ b/src/components/RingList/RingList.tsx
@@ -0,0 +1,27 @@
+import styles from "./RingList.module.css";
+
+import { RingBadge } from "@/components/Badge/Badge";
+import { ItemList, ItemListProps } from "@/components/ItemList/ItemList";
+import { groupItemsByRing } from "@/lib/data";
+import { Item } from "@/lib/types";
+import { cn } from "@/lib/utils";
+
+interface RingListProps {
+ items: Item[];
+ size?: ItemListProps["size"];
+}
+export function RingList({ items, size }: RingListProps) {
+ const rings = groupItemsByRing(items);
+ return (
+
+ {Object.entries(rings).map(([ring, items]) => {
+ return (
+ -
+
+
+
+ );
+ })}
+
+ );
+}
diff --git a/src/lib/data.ts b/src/lib/data.ts
index a2fd732..637cb5b 100644
--- a/src/lib/data.ts
+++ b/src/lib/data.ts
@@ -36,8 +36,11 @@ export function getQuadrant(id: string): Quadrant | undefined {
return getQuadrants().find((q) => q.id === id);
}
-export function getItems(featured?: boolean): Item[] {
- return data.items.filter((item) => !featured || item.featured) as Item[];
+export function getItems(quadrant?: string, featured?: boolean): Item[] {
+ return data.items.filter((item) => {
+ if (quadrant && item.quadrant !== quadrant) return false;
+ return !(featured && !item.featured);
+ }) as Item[];
}
export function getItem(id: string): Item | undefined {
@@ -46,3 +49,27 @@ export function getItem(id: string): Item | undefined {
export const sortByFeaturedAndTitle = (a: Item, b: Item) =>
Number(b.featured) - Number(a.featured) || a.title.localeCompare(b.title);
+
+export const groupItemsByRing = (items: Item[]) => {
+ return getRings().reduce(
+ (acc, ring) => {
+ const ringItems = items.filter((item) => item.ring === ring.id);
+ if (ringItems.length) acc[ring.id] = ringItems;
+ return acc;
+ },
+ {} as { [ringId: string]: Item[] },
+ );
+};
+
+export const groupItemsByQuadrant = (items: Item[]) => {
+ return getQuadrants().reduce(
+ (acc, quadrant) => {
+ const quadrantItems = items.filter(
+ (item) => item.quadrant === quadrant.id,
+ );
+ if (quadrantItems.length) acc[quadrant.id] = quadrantItems;
+ return acc;
+ },
+ {} as { [quadrantId: string]: Item[] },
+ );
+};
diff --git a/src/pages/[quadrant]/[id].tsx b/src/pages/[quadrant]/[id].tsx
new file mode 100644
index 0000000..a8b376a
--- /dev/null
+++ b/src/pages/[quadrant]/[id].tsx
@@ -0,0 +1,57 @@
+import Head from "next/head";
+import { useRouter } from "next/router";
+import { useMemo } from "react";
+
+import { RingBadge } from "@/components/Badge/Badge";
+import { ItemList } from "@/components/ItemList/ItemList";
+import {
+ getItem,
+ getItems,
+ getQuadrant,
+ sortByFeaturedAndTitle,
+} from "@/lib/data";
+import { formatTitle } from "@/lib/format";
+import { CustomPage } from "@/pages/_app";
+
+const ItemPage: CustomPage = () => {
+ const { query } = useRouter();
+ const quadrant = getQuadrant(query.quadrant as string);
+ const item = getItem(query.id as string);
+
+ const relatedItems = useMemo(() => {
+ return getItems()
+ .filter((i) => i.quadrant === quadrant?.id && i.ring == item?.ring)
+ .sort(sortByFeaturedAndTitle);
+ }, [quadrant?.id, item?.ring]);
+
+ if (!quadrant || !item) return null;
+
+ return (
+ <>
+
+ {formatTitle(item.title, quadrant.title)}
+
+
+
+ {item.title}
+
+
+
+ >
+ );
+};
+
+export default ItemPage;
+
+export const getStaticPaths = async () => {
+ const items = getItems();
+ const paths = items.map((item) => ({
+ params: { quadrant: item.quadrant, id: item.id },
+ }));
+
+ return { paths, fallback: false };
+};
+
+export const getStaticProps = async () => {
+ return { props: {} };
+};
diff --git a/src/pages/[quadrant]/index.tsx b/src/pages/[quadrant]/index.tsx
index 3710118..4a0513c 100644
--- a/src/pages/[quadrant]/index.tsx
+++ b/src/pages/[quadrant]/index.tsx
@@ -1,15 +1,25 @@
import Head from "next/head";
import { useRouter } from "next/router";
+import { useMemo } from "react";
-import { getQuadrant, getQuadrants } from "@/lib/data";
+import { RingList } from "@/components/RingList/RingList";
+import {
+ getItems,
+ getQuadrant,
+ getQuadrants,
+ sortByFeaturedAndTitle,
+} from "@/lib/data";
import { formatTitle } from "@/lib/format";
import { CustomPage } from "@/pages/_app";
const QuadrantPage: CustomPage = () => {
const { query } = useRouter();
const quadrant = getQuadrant(query.quadrant as string);
-
- if (!quadrant) return null;
+ const items = useMemo(
+ () => quadrant?.id && getItems(quadrant.id).sort(sortByFeaturedAndTitle),
+ [quadrant?.id],
+ );
+ if (!quadrant || !items) return null;
return (
<>
@@ -18,8 +28,10 @@ const QuadrantPage: CustomPage = () => {
- Quadrant: {query.quadrant}
-
{JSON.stringify(quadrant)}
+ {quadrant.title}
+ {quadrant.description}
+
+
>
);
};
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index ed6e75e..93a5037 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -1,17 +1,19 @@
import { ItemList } from "@/components/ItemList/ItemList";
+import { QuadrantList } from "@/components/QuadrantList/QuadrantList";
import { getAppName, getItems, getReleases } from "@/lib/data";
import { CustomPage } from "@/pages/_app";
const Home: CustomPage = () => {
const appName = getAppName();
const version = getReleases().length;
+ const items = getItems(undefined, true);
return (
<>
{appName}{" "}
Version #{version}
-
+
>
);
};
diff --git a/src/styles/globals.css b/src/styles/globals.css
index c7edd34..b8d1901 100644
--- a/src/styles/globals.css
+++ b/src/styles/globals.css
@@ -75,7 +75,7 @@ h2 {
}
h3 {
- font-size: 18px;
+ font-size: 20px;
}
ul,