Skip to content

Commit

Permalink
add product-preview page
Browse files Browse the repository at this point in the history
  • Loading branch information
sadmann7 committed Dec 7, 2023
1 parent 60ca66c commit d4efb80
Show file tree
Hide file tree
Showing 11 changed files with 181 additions and 80 deletions.
11 changes: 11 additions & 0 deletions src/app/(lobby)/@modal/(.)preview/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { AlertDialog, AlertDialogContent } from "@/components/ui/alert-dialog"

export default function ModalLayout({ children }: React.PropsWithChildren) {
return (
<AlertDialog defaultOpen>
<AlertDialogContent className="max-w-3xl overflow-hidden p-0">
{children}
</AlertDialogContent>
</AlertDialog>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ErrorCard } from "@/components/cards/error-card"
import { Shell } from "@/components/shells/shell"

export default function ProductNotFound() {
return (
<Shell variant="centered" className="max-w-md">
<ErrorCard
title="Product not found"
description="The product may have expired or you may have already updated your product"
retryLink="/"
retryLinkText="Go to Home"
/>
</Shell>
)
}
100 changes: 100 additions & 0 deletions src/app/(lobby)/@modal/(.)preview/product/[productId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import Image from "next/image"
import Link from "next/link"
import { notFound } from "next/navigation"
import { db } from "@/db"
import type { StoredFile } from "@/types"
import { EnterFullScreenIcon } from "@radix-ui/react-icons"
import { eq, sql } from "drizzle-orm"
import { products } from "drizzle/schema"

import { cn, formatPrice } from "@/lib/utils"
import { AlertDialogAction } from "@/components/ui/alert-dialog"
import { AspectRatio } from "@/components/ui/aspect-ratio"
import { buttonVariants } from "@/components/ui/button"
import { AddToCartForm } from "@/components/forms/add-to-cart-form"
import { PlaceholderImage } from "@/components/placeholder-image"
import { Rating } from "@/components/rating"
import { DialogShell } from "@/components/shells/dialog-shell"

interface ProductModalPageProps {
params: {
productId: string
}
}

export default async function ProductModalPage({
params,
}: ProductModalPageProps) {
const productId = Number(params.productId)

const product = await db
.select({
id: products.id,
name: products.name,
description: products.description,
images: sql<StoredFile[] | null>`${products.images}`,
category: products.category,
price: products.price,
inventory: products.inventory,
rating: products.rating,
})
.from(products)
.where(eq(products.id, productId))
.execute()
.then((rows) => rows[0])

if (!product) {
notFound()
}

return (
<DialogShell className="flex flex-col gap-2 overflow-visible sm:flex-row">
<AlertDialogAction
className={cn(
buttonVariants({
variant: "ghost",
size: "icon",
className:
"absolute right-10 top-4 h-auto w-auto shrink-0 rounded-sm bg-transparent p-0 text-foreground opacity-70 ring-offset-background transition-opacity hover:bg-transparent hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground",
})
)}
asChild
>
<Link href={`/product/${product.id}`}>
<EnterFullScreenIcon className="h-4 w-4" aria-hidden="true" />
</Link>
</AlertDialogAction>
<AspectRatio ratio={16 / 9} className="w-full">
{product.images?.length ? (
<Image
src={product.images[0]?.url ?? "/images/product-placeholder.webp"}
alt={product.images[0]?.name ?? product.name}
className="object-cover"
sizes="(min-width: 1024px) 20vw, (min-width: 768px) 25vw, (min-width: 640px) 33vw, (min-width: 475px) 50vw, 100vw"
fill
loading="lazy"
/>
) : (
<PlaceholderImage className="rounded-none" asChild />
)}
</AspectRatio>
<div className="w-full space-y-8 p-6 sm:p-10">
<div className="space-y-2">
<h1 className="line-clamp-2 text-2xl font-bold">{product.name}</h1>
<p className="text-base text-muted-foreground">
{formatPrice(product.price)}
</p>
<Rating rating={Math.round(product.rating / 5)} />
<p className="text-base text-muted-foreground">
{product.inventory} in stock
</p>
</div>
<AddToCartForm productId={product.id} />

<p className="line-clamp-2 text-base text-muted-foreground">
{product.description}
</p>
</div>
</DialogShell>
)
}
11 changes: 0 additions & 11 deletions src/app/(lobby)/@modal/(.)product-preview/[productId]/layout.tsx

This file was deleted.

15 changes: 0 additions & 15 deletions src/app/(lobby)/@modal/(.)product-preview/[productId]/page.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion src/app/(lobby)/product/[productId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ export default async function ProductPage({ params }: ProductPageProps) {
rating={product.rating}
/>
</div>
<AddToCartForm productId={productId} />
<AddToCartForm productId={productId} showBuyNow={true} />
<Separator className="mt-5" />
<Accordion
type="single"
Expand Down
22 changes: 6 additions & 16 deletions src/components/cards/product-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ import {
CardTitle,
} from "@/components/ui/card"
import { Icons } from "@/components/icons"

import { Rating } from "../rating"
import { UpdateProductRatingButton } from "../update-product-rating-button"
import { PlaceholderImage } from "@/components/placeholder-image"
import { Rating } from "@/components/rating"
import { UpdateProductRatingButton } from "@/components/update-product-rating-button"

interface ProductCardProps extends React.HTMLAttributes<HTMLDivElement> {
product: Pick<
Expand Down Expand Up @@ -52,7 +52,7 @@ export function ProductCard({
<Link aria-label={product.name} href={`/product/${product.id}`}>
<CardHeader className="border-b p-0">
<AspectRatio ratio={4 / 3}>
{product?.images?.length ? (
{product.images?.length ? (
<Image
src={
product.images[0]?.url ?? "/images/product-placeholder.webp"
Expand All @@ -64,17 +64,7 @@ export function ProductCard({
loading="lazy"
/>
) : (
<div
aria-label="Placeholder"
role="img"
aria-roledescription="placeholder"
className="flex h-full w-full items-center justify-center bg-secondary"
>
<Icons.placeholder
className="h-9 w-9 text-muted-foreground"
aria-hidden="true"
/>
</div>
<PlaceholderImage className="rounded-none" asChild />
)}
</AspectRatio>
</CardHeader>
Expand Down Expand Up @@ -124,7 +114,7 @@ export function ProductCard({
rating={product.rating}
/>
<Link
href={`/product-preview/${product.id}`}
href={`/preview/product/${product.id}`}
title="Preview"
className={cn(
buttonVariants({
Expand Down
70 changes: 38 additions & 32 deletions src/components/forms/add-to-cart-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { toast } from "sonner"
import type { z } from "zod"

import { addToCart } from "@/lib/actions/cart"
import { catchError } from "@/lib/utils"
import { catchError, cn } from "@/lib/utils"
import { updateCartItemSchema } from "@/lib/validations/cart"
import { Button } from "@/components/ui/button"
import {
Expand All @@ -25,11 +25,12 @@ import { Icons } from "@/components/icons"

interface AddToCartFormProps {
productId: number
showBuyNow?: boolean
}

type Inputs = z.infer<typeof updateCartItemSchema>

export function AddToCartForm({ productId }: AddToCartFormProps) {
export function AddToCartForm({ productId, showBuyNow }: AddToCartFormProps) {
const id = React.useId()
const router = useRouter()
const [isAddingToCart, startAddingToCart] = React.useTransition()
Expand Down Expand Up @@ -60,7 +61,10 @@ export function AddToCartForm({ productId }: AddToCartFormProps) {
return (
<Form {...form}>
<form
className="max-w-[260px] space-y-4"
className={cn(
"flex max-w-[260px] gap-4",
showBuyNow ? "flex-col" : "flex-row"
)}
onSubmit={form.handleSubmit(onSubmit)}
>
<div className="flex items-center">
Expand Down Expand Up @@ -122,38 +126,40 @@ export function AddToCartForm({ productId }: AddToCartFormProps) {
</Button>
</div>
<div className="flex items-center space-x-2.5">
<Button
type="button"
aria-label="Buy now"
size="sm"
className="w-full"
onClick={() => {
startBuyingNow(async () => {
try {
await addToCart({
productId,
quantity: form.getValues("quantity"),
})
router.push("/cart")
} catch (err) {
catchError(err)
}
})
}}
disabled={isBuyingNow}
>
{isBuyingNow && (
<Icons.spinner
className="mr-2 h-4 w-4 animate-spin"
aria-hidden="true"
/>
)}
Buy now
</Button>
{showBuyNow ? (
<Button
type="button"
aria-label="Buy now"
size="sm"
className="w-full"
onClick={() => {
startBuyingNow(async () => {
try {
await addToCart({
productId,
quantity: form.getValues("quantity"),
})
router.push("/cart")
} catch (err) {
catchError(err)
}
})
}}
disabled={isBuyingNow}
>
{isBuyingNow && (
<Icons.spinner
className="mr-2 h-4 w-4 animate-spin"
aria-hidden="true"
/>
)}
Buy now
</Button>
) : null}
<Button
aria-label="Add to cart"
type="submit"
variant="outline"
variant={showBuyNow ? "outline" : "default"}
size="sm"
className="w-full"
disabled={isAddingToCart}
Expand Down
8 changes: 6 additions & 2 deletions src/components/placeholder-image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,16 @@ export function PlaceholderImage({
const Comp = asChild ? Slot : AspectRatio

return (
<Comp ratio={16 / 9} {...props} className={cn(className)}>
<Comp
ratio={16 / 9}
{...props}
className={cn("overflow-hidden rounded-lg", className)}
>
<div
aria-label="Placeholder"
role="img"
aria-roledescription="placeholder"
className="flex h-full w-full items-center justify-center rounded-lg bg-secondary"
className="flex h-full w-full items-center justify-center bg-secondary"
>
<Icons.placeholder
className="h-9 w-9 text-muted-foreground"
Expand Down
7 changes: 4 additions & 3 deletions src/components/shells/dialog-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import { Cross2Icon } from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"

interface DialogShellProps
extends React.PropsWithChildren<React.HTMLAttributes<HTMLDivElement>> {}
interface DialogShellProps extends React.HTMLAttributes<HTMLDivElement> {}

export function DialogShell({
children,
Expand All @@ -32,7 +31,9 @@ export function DialogShell({
return (
<div className={cn(className)} {...props}>
<Button
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100"
variant="ghost"
size="icon"
className="absolute right-4 top-4 h-auto w-auto shrink-0 rounded-sm opacity-70 ring-offset-background transition-opacity hover:bg-transparent hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
onClick={() => router.back()}
>
<Cross2Icon className="h-4 w-4" aria-hidden="true" />
Expand Down

1 comment on commit d4efb80

@vercel
Copy link

@vercel vercel bot commented on d4efb80 Dec 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.