Skip to content

Commit

Permalink
Implement optional catch all route
Browse files Browse the repository at this point in the history
  • Loading branch information
goncy committed Aug 30, 2024
1 parent a8979e9 commit 21f65da
Show file tree
Hide file tree
Showing 14 changed files with 544 additions and 560 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"lucide-react": "^0.331.0",
"next": "14.1.0",
"next": "14.2.7",
"next-themes": "^0.2.1",
"papaparse": "^5.4.1",
"react": "18.2.0",
Expand All @@ -27,7 +27,7 @@
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@next/eslint-plugin-next": "14.1.0",
"@next/eslint-plugin-next": "14.2.7",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^14.2.1",
"@types/jest": "^29.5.12",
Expand Down
538 changes: 301 additions & 237 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

35 changes: 35 additions & 0 deletions src/app/[[...product]]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type {Metadata} from "next";

import api from "~/product/api";

import StoreScreen from "@/modules/store/screens/Store";

export async function generateStaticParams() {
const products = await api.list();

return products.map((product) => ({product: [product.id]}));
}

export async function generateMetadata({
params,
}: {
params: {product?: string[]};
}): Promise<Metadata | undefined> {
if (params.product) {
const product = await api.fetch(params.product[0]);

return {
title: product.title,
description: product.description,
};
}
}

async function HomeAndProductPage({params}: {params: {product?: [product: string]}}) {
const products = await api.list();
const selected = params.product ? await api.fetch(params.product[0]) : null;

return <StoreScreen products={products} selected={selected} />;
}

export default HomeAndProductPage;
30 changes: 0 additions & 30 deletions src/app/[product]/client.tsx

This file was deleted.

32 changes: 0 additions & 32 deletions src/app/[product]/page.tsx

This file was deleted.

5 changes: 4 additions & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ export async function generateMetadata(): Promise<Metadata> {
const store = await api.fetch();

return {
title: store.title,
title: {
template: `${store.title} - %s`,
default: store.title,
},
description: store.subtitle,
};
}
Expand Down
20 changes: 0 additions & 20 deletions src/app/loading.tsx

This file was deleted.

16 changes: 0 additions & 16 deletions src/app/mocks/[mock]/page.tsx

This file was deleted.

10 changes: 0 additions & 10 deletions src/app/page.tsx

This file was deleted.

32 changes: 21 additions & 11 deletions src/components/ui/sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import * as React from "react";
import * as SheetPrimitive from "@radix-ui/react-dialog";
import {cva, type VariantProps} from "class-variance-authority";
import dynamic from "next/dynamic";

import {cn} from "@/lib/utils";

Expand Down Expand Up @@ -53,17 +54,26 @@ interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}

const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({side = "right", className, children, ...props}, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content ref={ref} className={cn(sheetVariants({side}), className)} {...props}>
{children}
</SheetPrimitive.Content>
</SheetPortal>
));
const SheetContent = dynamic(
// eslint-disable-next-line @typescript-eslint/require-await
async () =>
// eslint-disable-next-line react/display-name
React.forwardRef<React.ElementRef<typeof SheetPrimitive.Content>, SheetContentProps>(
({side = "right", className, children, ...props}, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({side}), className)}
{...props}
>
{children}
</SheetPrimitive.Content>
</SheetPortal>
),
),
{ssr: false},
);

SheetContent.displayName = SheetPrimitive.Content.displayName;

Expand Down
16 changes: 10 additions & 6 deletions src/modules/product/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@ function normalize(data: (RawProduct | RawOption | RawUnknown)[]) {

const api = {
list: async (): Promise<IProduct[]> => {
// Uncomment to use the mock data
// return await import(`./mocks/default.json`).then(
// (module: {default: IProduct[]}) => module.default,
// );

return fetch(process.env.PRODUCTS!, {next: {tags: ["products"]}}).then(async (response) => {
const csv = await response.text();

Expand All @@ -123,19 +128,18 @@ const api = {
});
},
fetch: async (id: IProduct["id"]): Promise<IProduct> => {
// Uncomment to use the mock data
// return await import(`./mocks/default.json`).then(
// (module: {default: IProduct[]}) => module.default.find((product) => product.id === id)!,
// );

const products = await api.list();
const product = products.find((product) => product.id === id);

if (!product) return notFound();

return product;
},
mock: {
list: (mock: string): Promise<IProduct[]> =>
import(`./mocks/${mock}.json`).then((result: {default: (RawProduct | RawOption)[]}) =>
normalize(result.default),
),
},
};

export default api;
83 changes: 29 additions & 54 deletions src/modules/product/components/ProductCard.tsx
Original file line number Diff line number Diff line change
@@ -1,69 +1,44 @@
"use client";

import type {CartItem} from "~/cart/types";

import type {Product} from "../types";

import {useState, useMemo} from "react";
import {ImageOff} from "lucide-react";

import CartItemDrawer from "~/cart/components/CartItemDrawer";
import {parseCurrency} from "~/currency/utils";

function ProductCard({product, onAdd}: {product: Product; onAdd: (product: Product) => void}) {
const [isModalOpen, setIsModalOpen] = useState(false);
const cartItem = useMemo<CartItem>(() => ({...product, quantity: 1}), [product]);

function ProductCard({product}: {product: Product}) {
return (
<>
<div
key={product.id}
className="border-white/300 flex cursor-pointer items-center justify-between gap-3 rounded-md border"
data-testid="product"
onClick={() => {
setIsModalOpen(true);
}}
>
<div className="flex h-full w-full gap-4 p-4">
<div className="flex w-full flex-col justify-between gap-1">
<div className="flex flex-col gap-1">
<p className="line-clamp-[1] font-medium sm:line-clamp-[2]">{product.title}</p>
<p className="line-clamp-[2] text-sm text-muted-foreground sm:line-clamp-3">
{product.description}
</p>
</div>
<div className="flex items-end">
<p className="text-sm font-medium text-incentive">{parseCurrency(product.price)}</p>
</div>
<div
key={product.id}
className="border-white/300 flex items-center justify-between gap-3 rounded-md border"
data-testid="product"
>
<div className="flex h-full w-full gap-4 p-4">
<div className="flex w-full flex-col justify-between gap-1">
<div className="flex flex-col gap-1">
<p className="line-clamp-[1] font-medium sm:line-clamp-[2]">{product.title}</p>
<p className="line-clamp-[2] text-sm text-muted-foreground sm:line-clamp-3">
{product.description}
</p>
</div>
<div className="flex items-end">
<p className="text-sm font-medium text-incentive">{parseCurrency(product.price)}</p>
</div>
{product.image ? (
<img
alt={product.title}
className="aspect-square h-24 w-24 min-w-24 rounded-md bg-muted/50 object-cover sm:h-36 sm:w-36 sm:min-w-36"
loading="lazy"
src={product.image}
/>
) : (
<div className="flex aspect-square h-24 w-24 min-w-24 items-center justify-center rounded-md bg-muted/50 object-cover sm:h-36 sm:w-36 sm:min-w-36">
<ImageOff className="m-auto h-12 w-12 opacity-10 sm:h-16 sm:w-16" />
</div>
)}
</div>
{product.image ? (
<img
alt={product.title}
className="aspect-square h-24 w-24 min-w-24 rounded-md bg-muted/50 object-cover sm:h-36 sm:w-36 sm:min-w-36"
loading="lazy"
src={product.image}
/>
) : (
<div className="flex aspect-square h-24 w-24 min-w-24 items-center justify-center rounded-md bg-muted/50 object-cover sm:h-36 sm:w-36 sm:min-w-36">
<ImageOff className="m-auto h-12 w-12 opacity-10 sm:h-16 sm:w-16" />
</div>
)}
</div>
{isModalOpen ? (
<CartItemDrawer
open
item={cartItem}
onClose={() => {
setIsModalOpen(false);
}}
onSubmit={(item: CartItem) => {
onAdd(item);
setIsModalOpen(false);
}}
/>
) : null}
</>
</div>
);
}

Expand Down
Loading

0 comments on commit 21f65da

Please sign in to comment.