Fazekit

Code

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

export interface RatingStarsProps {
  /** 0–max, supports halves. */
  value: number;
  max?: number;
  /** Make it an input. */
  onChange?: (value: number) => void;
  count?: number;
  size?: number;
  className?: string;
}

export function RatingStars({ value, max = 5, onChange, count, size = 18, className }: RatingStarsProps) {
  const [hover, setHover] = React.useState<number | null>(null);
  const shown = hover ?? value;
  const interactive = Boolean(onChange);

  return (
    <div className={cn("inline-flex items-center gap-2", className)}>
      <div
        className="flex items-center"
        role={interactive ? "slider" : "img"}
        aria-label={`Rated ${value} out of ${max}`}
        aria-valuenow={interactive ? value : undefined}
        aria-valuemin={interactive ? 0 : undefined}
        aria-valuemax={interactive ? max : undefined}
        tabIndex={interactive ? 0 : undefined}
        onKeyDown={(e) => {
          if (!onChange) return;
          if (e.key === "ArrowRight") onChange(Math.min(max, value + 0.5));
          if (e.key === "ArrowLeft") onChange(Math.max(0, value - 0.5));
        }}
        onMouseLeave={() => setHover(null)}
      >
        {Array.from({ length: max }, (_, i) => {
          const fill = Math.max(0, Math.min(1, shown - i));
          return (
            <span
              key={i}
              className={cn("relative", interactive && "cursor-pointer")}
              style={{ width: size, height: size }}
              onMouseMove={(e) => {
                if (!interactive) return;
                const r = e.currentTarget.getBoundingClientRect();
                setHover(i + (e.clientX - r.left < r.width / 2 ? 0.5 : 1));
              }}
              onClick={() => hover != null && onChange?.(hover)}
            >
              <Star className="absolute inset-0 text-muted-foreground/30" style={{ width: size, height: size }} fill="currentColor" strokeWidth={0} />
              <span className="absolute inset-0 overflow-hidden" style={{ width: `${fill * 100}%` }}>
                <Star className="text-amber-400" style={{ width: size, height: size }} fill="currentColor" strokeWidth={0} />
              </span>
            </span>
          );
        })}
      </div>
      <span className="text-sm font-medium tabular-nums">{shown.toFixed(1)}</span>
      {count !== undefined && <span className="text-sm text-muted-foreground">({count.toLocaleString()} reviews)</span>}
    </div>
  );
}

More in E-commerce

View all →