Rating
src/components/thegridcn/rating.tsx
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
interface RatingProps extends React.HTMLAttributes<HTMLDivElement> {
label?: string;
max?: number;
showValue?: boolean;
size?: "sm" | "md" | "lg";
value: number;
variant?: "default" | "success" | "warning" | "danger";
}
const sizeConfig: Record<string, { dim: number; gap: string; text: string }> = {
lg: { dim: 20, gap: "gap-1.5", text: "text-xs" },
md: { dim: 16, gap: "gap-1", text: "text-[10px]" },
sm: { dim: 12, gap: "gap-0.5", text: "text-[9px]" },
};
const variantColors: Record<string, { active: string; glow: string }> = {
danger: {
active: "text-red-500",
glow: "drop-shadow-[0_0_3px_rgba(239,68,68,0.5)]",
},
default: {
active: "text-primary",
glow: "drop-shadow-[0_0_3px_var(--primary)]",
},
success: {
active: "text-green-500",
glow: "drop-shadow-[0_0_3px_rgba(34,197,94,0.5)]",
},
warning: {
active: "text-amber-500",
glow: "drop-shadow-[0_0_3px_rgba(245,158,11,0.5)]",
},
};
function DiamondIcon({
filled,
dim,
color,
glow,
}: {
filled: boolean;
dim: number;
color: string;
glow: string;
}) {
return (
<svg
aria-hidden="true"
width={dim}
height={dim}
viewBox="0 0 16 16"
className={cn(
"transition-all duration-300",
filled ? cn(color, glow) : "text-foreground/15"
)}
>
<path
d="M8 1l5 5-5 8-5-8 5-5z"
fill={filled ? "currentColor" : "none"}
stroke="currentColor"
strokeWidth="1.2"
strokeLinejoin="round"
/>
</svg>
);
}
export function Rating({
value,
max = 5,
label,
size = "md",
variant = "default",
showValue = false,
className,
...props
}: RatingProps) {
const config = sizeConfig[size];
const colors = variantColors[variant];
// Animate reveal
const [revealedIdx, setRevealedIdx] = React.useState(-1);
React.useEffect(() => {
let idx = 0;
const interval = setInterval(() => {
setRevealedIdx(idx);
idx++;
if (idx >= max) {
clearInterval(interval);
}
}, 80);
return () => clearInterval(interval);
}, [max]);
return (
<div
data-slot="tron-rating"
className={cn("inline-flex items-center gap-2", className)}
{...props}
>
{label ? (
<span
className={cn(
"font-mono text-foreground/40 uppercase tracking-widest",
config.text
)}
>
{label}
</span>
) : null}
<div className={cn("flex items-center", config.gap)}>
{Array.from({ length: max }, (_, i) => (
<span
key={i}
className={cn(
"transition-all duration-200",
i <= revealedIdx ? "scale-100 opacity-100" : "scale-50 opacity-0"
)}
>
<DiamondIcon
filled={i < value}
dim={config.dim}
color={colors.active}
glow={colors.glow}
/>
</span>
))}
</div>
{showValue ? (
<span
className={cn(
"font-mono text-foreground/50 tabular-nums",
config.text
)}
>
{value}/{max}
</span>
) : null}
</div>
);
}
No selection tags yet — empty means no evidence, not lack of support.