Fazekit

Code

"use client";
import * as React from "react";
import { Check, Heart, ShoppingBag, Star } from "lucide-react";
import { cn } from "@/lib/utils";

export interface ProductCardProps {
  name: string;
  brand?: string;
  price: number;
  compareAt?: number;
  currency?: string;
  rating?: number;
  reviews?: number;
  /** Two images (second shows on hover) or a single ReactNode visual. */
  images?: [string, string?];
  visual?: React.ReactNode;
  badge?: string;
  swatches?: string[];
  onAdd?: () => void;
  className?: string;
}

export function ProductCard({
  name,
  brand,
  price,
  compareAt,
  currency = "EUR",
  rating,
  reviews,
  images,
  visual,
  badge,
  swatches = [],
  onAdd,
  className,
}: ProductCardProps) {
  const [added, setAdded] = React.useState(false);
  const [liked, setLiked] = React.useState(false);
  const [swatch, setSwatch] = React.useState(0);
  const fmt = new Intl.NumberFormat("en-IE", { style: "currency", currency });
  const off = compareAt && compareAt > price ? Math.round((1 - price / compareAt) * 100) : 0;

  function add() {
    onAdd?.();
    setAdded(true);
    setTimeout(() => setAdded(false), 1600);
  }

  return (
    <div className={cn("group w-full max-w-[280px] overflow-hidden rounded-2xl border bg-card text-card-foreground shadow-xs transition hover:shadow-xl", className)}>
      <div className="relative aspect-[4/5] overflow-hidden bg-muted">
        {images ? (
          <>
            {/* eslint-disable-next-line @next/next/no-img-element */}
            <img src={images[0]} alt={name} className="absolute inset-0 size-full object-cover transition duration-500 group-hover:scale-105" />
            {images[1] && (
              // eslint-disable-next-line @next/next/no-img-element
              <img src={images[1]} alt="" className="absolute inset-0 size-full object-cover opacity-0 transition duration-500 group-hover:opacity-100" />
            )}
          </>
        ) : (
          <div className="absolute inset-0 transition duration-500 group-hover:scale-105">{visual}</div>
        )}
        <div className="absolute left-3 top-3 flex gap-1.5">
          {badge && <span className="rounded-full bg-foreground px-2.5 py-1 text-[11px] font-semibold text-background">{badge}</span>}
          {off > 0 && <span className="rounded-full bg-rose-500 px-2.5 py-1 text-[11px] font-semibold text-white">-{off}%</span>}
        </div>
        <button
          onClick={() => setLiked((l) => !l)}
          aria-label="Add to wishlist"
          aria-pressed={liked}
          className="absolute right-3 top-3 grid size-9 place-items-center rounded-full bg-background/80 backdrop-blur transition hover:scale-110"
        >
          <Heart className={cn("size-4 transition", liked && "fill-rose-500 text-rose-500")} />
        </button>
        <button
          onClick={add}
          className={cn(
            "absolute inset-x-3 bottom-3 flex h-11 translate-y-3 items-center justify-center gap-2 rounded-xl text-sm font-medium opacity-0 shadow-lg transition-all duration-300 group-hover:translate-y-0 group-hover:opacity-100 focus-visible:translate-y-0 focus-visible:opacity-100",
            added ? "bg-emerald-500 text-white" : "bg-foreground text-background",
          )}
        >
          {added ? <Check className="size-4" /> : <ShoppingBag className="size-4" />}
          {added ? "Added to cart" : "Quick add"}
        </button>
      </div>
      <div className="space-y-2 p-4">
        {brand && <p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">{brand}</p>}
        <p className="line-clamp-2 font-medium leading-snug">{name}</p>
        {rating !== undefined && (
          <p className="flex items-center gap-1 text-xs text-muted-foreground">
            <Star className="size-3.5 fill-amber-400 text-amber-400" />
            <span className="font-medium text-foreground">{rating.toFixed(1)}</span>
            {reviews !== undefined && <span>({reviews})</span>}
          </p>
        )}
        <div className="flex items-center justify-between">
          <p className="flex items-baseline gap-2">
            <span className="text-lg font-semibold">{fmt.format(price)}</span>
            {off > 0 && <span className="text-sm text-muted-foreground line-through">{fmt.format(compareAt!)}</span>}
          </p>
          {swatches.length > 0 && (
            <div className="flex gap-1">
              {swatches.map((c, i) => (
                <button
                  key={c}
                  aria-label={`Color ${i + 1}`}
                  onClick={() => setSwatch(i)}
                  className={cn("size-4 rounded-full border ring-offset-2 ring-offset-card transition", swatch === i && "ring-2 ring-foreground/60")}
                  style={{ background: c }}
                />
              ))}
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

More in E-commerce

View all →