Files
EvoBGP/apps/web/src/components/reui/rating.tsx
T
DenozordecandCursor d56405af7b
quality / commitlint (push) Skipped
quality / changes (push) Successful in 9s
quality / docker-check (push) Skipped
quality / openapi (push) Successful in 39s
quality / web (push) Successful in 1m29s
quality / go (push) Successful in 1m3s
quality / bird2 (push) Successful in 15s
CD / quality (push) Successful in 3m43s
CD / publish (push) Failing after 8m29s
feat(web): upgrade lists to ReUI Table v9 and EventCalendar
Единый kitDataGridTableLayout и ResourcePage/FrameDataGrid на всех списках.
Календарь задач переведён на EventCalendar, lookup — на cascader, у операций появился вид доски.
Удалены settings-7 и самописные DataGridSection/toolbar.

Co-authored-by: Cursor <[email protected]>
2026-08-23 01:38:56 +07:00

164 lines
3.7 KiB
TypeScript

import { useState } from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@evobgp/ui/lib/utils"
import { StarIcon } from "lucide-react"
const ratingVariants = cva("flex items-center", {
variants: {
size: {
sm: "gap-2",
default: "gap-2.5",
lg: "gap-3",
},
},
defaultVariants: {
size: "default",
},
})
const starVariants = cva("", {
variants: {
size: {
sm: "w-4 h-4",
default: "w-5 h-5",
lg: "w-6 h-6",
},
},
defaultVariants: {
size: "default",
},
})
const valueVariants = cva("text-muted-foreground w-5", {
variants: {
size: {
sm: "text-xs",
default: "text-sm",
lg: "text-base",
},
},
defaultVariants: {
size: "default",
},
})
function Rating({
rating,
maxRating = 5,
size,
className,
starClassName,
showValue = false,
editable = false,
onRatingChange,
...props
}: React.ComponentProps<"div"> &
VariantProps<typeof ratingVariants> & {
/**
* Current rating value (supports decimal values for partial stars)
*/
rating: number
/**
* Maximum rating value (number of stars to show)
*/
maxRating?: number
/**
* Whether to show the numeric rating value
*/
showValue?: boolean
/**
* Class name for the value span
*/
starClassName?: string
/**
* Whether the rating is editable (clickable)
*/
editable?: boolean
/**
* Callback function called when rating changes
*/
onRatingChange?: (rating: number) => void
}) {
const [hoveredRating, setHoveredRating] = useState<number | null>(null)
const displayRating =
editable && hoveredRating !== null ? hoveredRating : rating
const handleStarClick = (starRating: number) => {
if (editable && onRatingChange) {
onRatingChange(starRating)
}
}
const handleStarMouseEnter = (starRating: number) => {
if (editable) {
setHoveredRating(starRating)
}
}
const handleStarMouseLeave = () => {
if (editable) {
setHoveredRating(null)
}
}
const renderStars = () => {
const stars = []
for (let i = 1; i <= maxRating; i++) {
const filled = displayRating >= i
const partiallyFilled = displayRating > i - 1 && displayRating < i
const fillPercentage = partiallyFilled
? (displayRating - (i - 1)) * 100
: 0
stars.push(
<div
key={i}
className={cn("relative", editable && "cursor-pointer")}
onClick={() => handleStarClick(i)}
onMouseEnter={() => handleStarMouseEnter(i)}
onMouseLeave={handleStarMouseLeave}
>
{/* Background star (empty) */}
<StarIcon data-slot="rating-star-empty" className={cn(starVariants({ size }), "text-muted-foreground/30")} />
{/* Filled star */}
<div
className="absolute inset-0 overflow-hidden"
style={{
width: filled ? "100%" : `${fillPercentage}%`,
}}
>
<StarIcon data-slot="rating-star-filled" className={cn(
starVariants({ size }),
"fill-yellow-400 text-yellow-400"
)} />
</div>
</div>
)
}
return stars
}
return (
<div
data-slot="rating"
className={cn(ratingVariants({ size }), className)}
{...props}
>
<div className="flex items-center">{renderStars()}</div>
{showValue && (
<span
data-slot="rating-value"
className={cn(valueVariants({ size }), starClassName)}
>
{displayRating.toFixed(1)}
</span>
)}
</div>
)
}
export { Rating }