Skip to content

Commit

Permalink
update product-card again
Browse files Browse the repository at this point in the history
  • Loading branch information
sadmann7 committed Dec 6, 2023
1 parent 20e3dad commit 8484809
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 70 deletions.
30 changes: 25 additions & 5 deletions src/app/(lobby)/product/[productId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { notFound } from "next/navigation"
import { db } from "@/db"
import { products, stores } from "@/db/schema"
import { env } from "@/env.mjs"
import { and, desc, eq, not, sql } from "drizzle-orm"
import { and, desc, eq, not } from "drizzle-orm"

import { formatPrice, toTitleCase } from "@/lib/utils"
import {
Expand All @@ -13,12 +13,15 @@ import {
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Separator } from "@/components/ui/separator"
import { ProductCard } from "@/components/cards/product-card"
import { AddToCartForm } from "@/components/forms/add-to-cart-form"
import { Breadcrumbs } from "@/components/pagers/breadcrumbs"
import { ProductImageCarousel } from "@/components/product-image-carousel"
import { Rating } from "@/components/rating"
import { Shell } from "@/components/shells/shell"
import { UpdateProductRatingButton } from "@/components/update-product-rating-button"

interface ProductPageProps {
params: {
Expand Down Expand Up @@ -61,6 +64,8 @@ export default async function ProductPage({ params }: ProductPageProps) {
price: true,
images: true,
category: true,
inventory: true,
rating: true,
storeId: true,
},
where: eq(products.id, productId),
Expand Down Expand Up @@ -143,9 +148,24 @@ export default async function ProductPage({ params }: ProductPageProps) {
) : null}
</div>
<Separator className="my-1.5" />
<p className="text-base text-muted-foreground">
{product.inventory} in stock
</p>
<div className="flex items-center justify-between">
<Rating rating={Math.round(product.rating / 5)} />
<UpdateProductRatingButton
productId={product.id}
rating={product.rating}
/>
</div>
<AddToCartForm productId={productId} />
<Separator className="mt-5" />
<Accordion type="single" collapsible className="w-full">
<Accordion
type="single"
collapsible
className="w-full"
defaultValue="description"
>
<AccordionItem value="description">
<AccordionTrigger>Description</AccordionTrigger>
<AccordionContent>
Expand All @@ -161,8 +181,8 @@ export default async function ProductPage({ params }: ProductPageProps) {
<h2 className="line-clamp-1 flex-1 text-2xl font-bold">
More products from {store.name}
</h2>
<div className="overflow-x-auto pb-2 pt-6">
<div className="flex w-fit gap-4">
<ScrollArea orientation="horizontal" className="pb-3.5 pt-6">
<div className="flex gap-4">
{otherProducts.map((product) => (
<ProductCard
key={product.id}
Expand All @@ -171,7 +191,7 @@ export default async function ProductPage({ params }: ProductPageProps) {
/>
))}
</div>
</div>
</ScrollArea>
</div>
) : null}
</Shell>
Expand Down
64 changes: 18 additions & 46 deletions src/components/cards/product-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,13 @@ import * as React from "react"
import Image from "next/image"
import Link from "next/link"
import { type Product } from "@/db/schema"
import {
CheckIcon,
EyeOpenIcon,
HeartIcon,
PlusIcon,
} from "@radix-ui/react-icons"
import { CheckIcon, EyeOpenIcon, PlusIcon } from "@radix-ui/react-icons"
import { toast } from "sonner"

import { addToCart } from "@/lib/actions/cart"
import { updateProductRating } from "@/lib/actions/product"
import { catchError, cn, formatPrice } from "@/lib/utils"
import { AspectRatio } from "@/components/ui/aspect-ratio"
import { Button } from "@/components/ui/button"
import { Button, buttonVariants } from "@/components/ui/button"
import {
Card,
CardContent,
Expand All @@ -28,6 +22,7 @@ import {
import { Icons } from "@/components/icons"

import { Rating } from "../rating"
import { UpdateProductRatingButton } from "../update-product-rating-button"

interface ProductCardProps extends React.HTMLAttributes<HTMLDivElement> {
product: Pick<
Expand All @@ -48,11 +43,10 @@ export function ProductCard({
...props
}: ProductCardProps) {
const [isAddingToCart, startAddingToCart] = React.useTransition()
const [isFavoriting, startFavoriting] = React.useTransition()

return (
<Card
className={cn("h-full overflow-hidden rounded-sm", className)}
className={cn("h-full w-full overflow-hidden rounded-sm", className)}
{...props}
>
<Link aria-label={product.name} href={`/product/${product.id}`}>
Expand Down Expand Up @@ -92,7 +86,7 @@ export function ProductCard({
<CardDescription className="line-clamp-1">
{formatPrice(product.price)}
</CardDescription>
<Rating rating={Math.round(product.rating / 10)} />
<Rating rating={Math.round(product.rating / 5)} />
</CardContent>
</Link>
<CardFooter className="p-4 pt-2.5">
Expand Down Expand Up @@ -125,46 +119,24 @@ export function ProductCard({
)}
Add to cart
</Button>
<Button
title="Favorite"
variant="secondary"
size="icon"
className="h-8 w-8 shrink-0"
onClick={() => {
startFavoriting(async () => {
try {
await updateProductRating({
id: product.id,
rating: product.rating + 1,
})
toast.success("Favorited product.")
} catch (err) {
catchError(err)
}
<UpdateProductRatingButton
productId={product.id}
rating={product.rating}
/>
<Link
href={`/product-preview/${product.id}`}
title="Preview"
className={cn(
buttonVariants({
variant: "secondary",
size: "icon",
className: "h-8 w-8 shrink-0",
})
}}
disabled={isFavoriting}
>
{isFavoriting ? (
<Icons.spinner
className="h-4 w-4 animate-spin"
aria-hidden="true"
/>
) : (
<HeartIcon className="h-4 w-4" aria-hidden="true" />
)}
<span className="sr-only">Favorite</span>
</Button>
<Button
title="Preview"
variant="secondary"
size="icon"
className="h-8 w-8 shrink-0"
disabled={isFavoriting}
>
<EyeOpenIcon className="h-4 w-4" aria-hidden="true" />
<span className="sr-only">Preview</span>
</Button>
</Link>
</div>
) : (
<Button
Expand Down
75 changes: 57 additions & 18 deletions src/components/forms/add-to-cart-form.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client"

import * as React from "react"
import { useRouter } from "next/navigation"
import { zodResolver } from "@hookform/resolvers/zod"
import { MinusIcon, PlusIcon } from "@radix-ui/react-icons"
import { useForm } from "react-hook-form"
Expand Down Expand Up @@ -30,7 +31,9 @@ type Inputs = z.infer<typeof updateCartItemSchema>

export function AddToCartForm({ productId }: AddToCartFormProps) {
const id = React.useId()
const [isPending, startTransition] = React.useTransition()
const router = useRouter()
const [isAddingToCart, startAddingToCart] = React.useTransition()
const [isBuyingNow, startBuyingNow] = React.useTransition()

// react-hook-form
const form = useForm<Inputs>({
Expand All @@ -41,7 +44,7 @@ export function AddToCartForm({ productId }: AddToCartFormProps) {
})

function onSubmit(data: Inputs) {
startTransition(async () => {
startAddingToCart(async () => {
try {
await addToCart({
productId,
Expand All @@ -57,7 +60,7 @@ export function AddToCartForm({ productId }: AddToCartFormProps) {
return (
<Form {...form}>
<form
className="flex items-center space-x-2"
className="max-w-xs space-y-4"
onSubmit={(...args) => void form.handleSubmit(onSubmit)(...args)}
>
<div className="flex items-center">
Expand All @@ -66,14 +69,14 @@ export function AddToCartForm({ productId }: AddToCartFormProps) {
type="button"
variant="outline"
size="icon"
className="h-8 w-8 rounded-r-none"
className="h-8 w-8 shrink-0 rounded-r-none"
onClick={() =>
form.setValue(
"quantity",
Math.max(0, form.getValues("quantity") - 1)
)
}
disabled={isPending}
disabled={isAddingToCart}
>
<MinusIcon className="h-3 w-3" aria-hidden="true" />
<span className="sr-only">Remove one item</span>
Expand All @@ -89,7 +92,7 @@ export function AddToCartForm({ productId }: AddToCartFormProps) {
type="number"
inputMode="numeric"
min={0}
className="h-8 w-14 rounded-none border-x-0"
className="h-8 w-16 rounded-none border-x-0"
{...field}
onChange={(e) => {
const value = e.target.value
Expand All @@ -108,26 +111,62 @@ export function AddToCartForm({ productId }: AddToCartFormProps) {
type="button"
variant="outline"
size="icon"
className="h-8 w-8 rounded-l-none"
className="h-8 w-8 shrink-0 rounded-l-none"
onClick={() =>
form.setValue("quantity", form.getValues("quantity") + 1)
}
disabled={isPending}
disabled={isAddingToCart}
>
<PlusIcon className="h-3 w-3" aria-hidden="true" />
<span className="sr-only">Add one item</span>
</Button>
</div>
<Button type="submit" size="sm" disabled={isPending}>
{isPending && (
<Icons.spinner
className="mr-2 h-4 w-4 animate-spin"
aria-hidden="true"
/>
)}
Add to cart
<span className="sr-only">Add to cart</span>
</Button>
<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>
<Button
aria-label="Add to cart"
type="submit"
variant="outline"
size="sm"
className="w-full"
disabled={isAddingToCart}
>
{isAddingToCart && (
<Icons.spinner
className="mr-2 h-4 w-4 animate-spin"
aria-hidden="true"
/>
)}
Add to cart
</Button>
</div>
</form>
</Form>
)
Expand Down
3 changes: 2 additions & 1 deletion src/components/skeletons/product-card-skeleton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ export function ProductCardSkeleton() {
</CardHeader>
<CardContent className="space-y-1.5 p-4">
<Skeleton className="h-4 w-1/2" />
<Skeleton className="h-4 w-1/4" />
<Skeleton className="h-4 w-1/6" />
<Skeleton className="h-4 w-4/12" />
</CardContent>
<CardFooter className="p-4 pt-2.5">
<Skeleton className="h-8 w-full" />
Expand Down
55 changes: 55 additions & 0 deletions src/components/update-product-rating-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"use client"

import * as React from "react"
import { HeartIcon } from "@radix-ui/react-icons"
import { toast } from "sonner"

import { updateProductRating } from "@/lib/actions/product"
import { catchError, cn } from "@/lib/utils"
import { Button, type ButtonProps } from "@/components/ui/button"
import { Icons } from "@/components/icons"

interface UpdateProductRatingButtonProps extends ButtonProps {
productId: number
rating: number
}

export function UpdateProductRatingButton({
productId,
rating,
className,
...props
}: UpdateProductRatingButtonProps) {
const [isFavoriting, startFavoriting] = React.useTransition()

return (
<Button
title="Favorite"
variant="secondary"
size="icon"
className={cn("h-8 w-8 shrink-0", className)}
onClick={() => {
startFavoriting(async () => {
try {
await updateProductRating({
id: productId,
rating: rating + 1,
})
toast.success("Favorited product.")
} catch (err) {
catchError(err)
}
})
}}
disabled={isFavoriting}
{...props}
>
{isFavoriting ? (
<Icons.spinner className="h-4 w-4 animate-spin" aria-hidden="true" />
) : (
<HeartIcon className="h-4 w-4" aria-hidden="true" />
)}
<span className="sr-only">Favorite</span>
</Button>
)
}

1 comment on commit 8484809

@vercel
Copy link

@vercel vercel bot commented on 8484809 Dec 6, 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.