79 lines
2.0 KiB
TypeScript
79 lines
2.0 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
|
|
interface StoryRatingProps {
|
|
rating: number;
|
|
onRatingChange: (rating: number) => void;
|
|
readonly?: boolean;
|
|
size?: 'sm' | 'md' | 'lg';
|
|
}
|
|
|
|
export default function StoryRating({
|
|
rating,
|
|
onRatingChange,
|
|
readonly = false,
|
|
size = 'md'
|
|
}: StoryRatingProps) {
|
|
const [hoveredRating, setHoveredRating] = useState(0);
|
|
const [updating, setUpdating] = useState(false);
|
|
|
|
const sizeClasses = {
|
|
sm: 'text-sm',
|
|
md: 'text-lg',
|
|
lg: 'text-2xl',
|
|
};
|
|
|
|
const handleRatingClick = async (newRating: number) => {
|
|
if (readonly || updating) return;
|
|
|
|
try {
|
|
setUpdating(true);
|
|
await onRatingChange(newRating);
|
|
} catch (error) {
|
|
console.error('Failed to update rating:', error);
|
|
} finally {
|
|
setUpdating(false);
|
|
}
|
|
};
|
|
|
|
const displayRating = hoveredRating || rating;
|
|
|
|
return (
|
|
<div className="flex items-center gap-1">
|
|
{[1, 2, 3, 4, 5].map((star) => (
|
|
<button
|
|
key={star}
|
|
onClick={() => handleRatingClick(star)}
|
|
onMouseEnter={() => !readonly && setHoveredRating(star)}
|
|
onMouseLeave={() => !readonly && setHoveredRating(0)}
|
|
disabled={readonly || updating}
|
|
className={`${sizeClasses[size]} ${
|
|
star <= displayRating
|
|
? 'text-yellow-400'
|
|
: 'text-gray-300 dark:text-gray-600'
|
|
} ${
|
|
readonly
|
|
? 'cursor-default'
|
|
: updating
|
|
? 'cursor-not-allowed'
|
|
: 'cursor-pointer hover:text-yellow-400'
|
|
} transition-colors`}
|
|
aria-label={`Rate ${star} star${star !== 1 ? 's' : ''}`}
|
|
>
|
|
★
|
|
</button>
|
|
))}
|
|
|
|
{!readonly && (
|
|
<span className="ml-2 text-sm theme-text">
|
|
{rating > 0 ? `(${rating}/5)` : 'Rate this story'}
|
|
</span>
|
|
)}
|
|
|
|
{updating && (
|
|
<span className="ml-2 text-sm theme-text">Saving...</span>
|
|
)}
|
|
</div>
|
|
);
|
|
} |