Skip to content

Commit

Permalink
update product-card
Browse files Browse the repository at this point in the history
  • Loading branch information
sadmann7 committed Dec 6, 2023
1 parent ee4f9e9 commit 20e3dad
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 40 deletions.
1 change: 1 addition & 0 deletions src/app/(lobby)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export default async function IndexPage() {
category: products.category,
price: products.price,
inventory: products.inventory,
rating: products.rating,
stripeAccountId: stores.stripeAccountId,
})
.from(products)
Expand Down
3 changes: 2 additions & 1 deletion 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 } from "drizzle-orm"
import { and, desc, eq, not, sql } from "drizzle-orm"

import { formatPrice, toTitleCase } from "@/lib/utils"
import {
Expand Down Expand Up @@ -87,6 +87,7 @@ export default async function ProductPage({ params }: ProductPageProps) {
images: products.images,
category: products.category,
inventory: products.inventory,
rating: products.rating,
})
.from(products)
.limit(4)
Expand Down
124 changes: 88 additions & 36 deletions src/components/cards/product-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@ import * as React from "react"
import Image from "next/image"
import Link from "next/link"
import { type Product } from "@/db/schema"
import { CheckIcon, PlusIcon } from "@radix-ui/react-icons"
import {
CheckIcon,
EyeOpenIcon,
HeartIcon,
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"
Expand All @@ -21,10 +27,12 @@ import {
} from "@/components/ui/card"
import { Icons } from "@/components/icons"

import { Rating } from "../rating"

interface ProductCardProps extends React.HTMLAttributes<HTMLDivElement> {
product: Pick<
Product,
"id" | "name" | "price" | "images" | "category" | "inventory"
"id" | "name" | "price" | "images" | "category" | "inventory" | "rating"
>
variant?: "default" | "switchable"
isAddedToCart?: boolean
Expand All @@ -39,7 +47,8 @@ export function ProductCard({
className,
...props
}: ProductCardProps) {
const [isPending, startTransition] = React.useTransition()
const [isAddingToCart, startAddingToCart] = React.useTransition()
const [isFavoriting, startFavoriting] = React.useTransition()

return (
<Card
Expand Down Expand Up @@ -78,55 +87,98 @@ export function ProductCard({
<span className="sr-only">{product.name}</span>
</Link>
<Link href={`/product/${product.id}`} tabIndex={-1}>
<CardContent className="grid gap-2.5 p-4">
<CardContent className="space-y-1.5 p-4">
<CardTitle className="line-clamp-1">{product.name}</CardTitle>
<CardDescription className="line-clamp-2">
<CardDescription className="line-clamp-1">
{formatPrice(product.price)}
</CardDescription>
<Rating rating={Math.round(product.rating / 10)} />
</CardContent>
</Link>
<CardFooter className="p-4">
<CardFooter className="p-4 pt-2.5">
{variant === "default" ? (
<Button
aria-label="Add to cart"
size="sm"
className="h-8 w-full rounded-sm"
onClick={() => {
startTransition(async () => {
try {
await addToCart({
productId: product.id,
quantity: 1,
})
toast.success("Added to cart.")
} catch (err) {
catchError(err)
}
})
}}
disabled={isPending}
>
{isPending && (
<Icons.spinner
className="mr-2 h-4 w-4 animate-spin"
aria-hidden="true"
/>
)}
Add to cart
</Button>
<div className="flex w-full items-center space-x-2">
<Button
aria-label="Add to cart"
size="sm"
className="h-8 w-full rounded-sm"
onClick={() => {
startAddingToCart(async () => {
try {
await addToCart({
productId: product.id,
quantity: 1,
})
toast.success("Added to cart.")
} catch (err) {
catchError(err)
}
})
}}
disabled={isAddingToCart}
>
{isAddingToCart && (
<Icons.spinner
className="mr-2 h-4 w-4 animate-spin"
aria-hidden="true"
/>
)}
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)
}
})
}}
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>
</div>
) : (
<Button
aria-label={isAddedToCart ? "Remove from cart" : "Add to cart"}
size="sm"
className="h-8 w-full rounded-sm"
onClick={() => {
startTransition(async () => {
startAddingToCart(async () => {
await onSwitch?.()
})
}}
disabled={isPending}
disabled={isAddingToCart}
>
{isPending ? (
{isAddingToCart ? (
<Icons.spinner
className="mr-2 h-4 w-4 animate-spin"
aria-hidden="true"
Expand Down
24 changes: 24 additions & 0 deletions src/components/rating.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { StarIcon } from "@radix-ui/react-icons"

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

interface RatingProps {
rating: number
}

export function Rating({ rating }: RatingProps) {
return (
<div className="flex items-center space-x-1">
{Array.from({ length: 5 }).map((_, i) => (
<StarIcon
key={i}
className={cn(
"h-4 w-4",
rating >= i + 1 ? "text-yellow-500" : "text-muted-foreground"
)}
aria-hidden="true"
/>
))}
</div>
)
}
4 changes: 2 additions & 2 deletions src/components/skeletons/product-card-skeleton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ export function ProductCardSkeleton() {
<PlaceholderImage asChild className="rounded-none" />
</AspectRatio>
</CardHeader>
<CardContent className="grid gap-2.5 p-4">
<CardContent className="space-y-1.5 p-4">
<Skeleton className="h-4 w-1/2" />
<Skeleton className="h-4 w-1/4" />
</CardContent>
<CardFooter className="p-4">
<CardFooter className="p-4 pt-2.5">
<Skeleton className="h-8 w-full" />
</CardFooter>
</Card>
Expand Down
31 changes: 30 additions & 1 deletion src/lib/actions/product.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import { and, desc, eq, like, not } from "drizzle-orm"
import { z } from "zod"

import { getSubcategories, productTags } from "@/config/products"
import { getProductSchema, productSchema } from "@/lib/validations/product"
import {
getProductSchema,
productSchema,
updateProductRatingSchema,
} from "@/lib/validations/product"

export async function seedProducts({
storeId,
Expand Down Expand Up @@ -159,6 +163,31 @@ export async function updateProduct(
revalidatePath(`/dashboard/stores/${input.storeId}/products/${input.id}`)
}

export async function updateProductRating(
rawInput: z.infer<typeof updateProductRatingSchema>
) {
const input = updateProductRatingSchema.parse(rawInput)

const product = await db.query.products.findFirst({
columns: {
id: true,
rating: true,
},
where: eq(products.id, input.id),
})

if (!product) {
throw new Error("Product not found.")
}

await db
.update(products)
.set({ rating: input.rating })
.where(eq(products.id, input.id))

revalidatePath("/")
}

export async function deleteProduct(
rawInput: z.infer<typeof getProductSchema>
) {
Expand Down
5 changes: 5 additions & 0 deletions src/lib/validations/product.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,8 @@ export const getProductsSchema = z.object({
store_ids: z.string().optional().nullable(),
active: z.string().optional().nullable(),
})

export const updateProductRatingSchema = z.object({
id: z.number(),
rating: z.number(),
})

1 comment on commit 20e3dad

@vercel
Copy link

@vercel vercel bot commented on 20e3dad 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.