feat: implement song rating system with admin view and user persistence
This commit is contained in:
@@ -5,7 +5,7 @@ import AudioPlayer from './AudioPlayer';
|
||||
import GuessInput from './GuessInput';
|
||||
import Statistics from './Statistics';
|
||||
import { useGameState } from '../lib/gameState';
|
||||
import { sendGotifyNotification } from '../app/actions';
|
||||
import { sendGotifyNotification, submitRating } from '../app/actions';
|
||||
|
||||
interface GameProps {
|
||||
dailyPuzzle: {
|
||||
@@ -34,6 +34,7 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
|
||||
const [lastAction, setLastAction] = useState<'GUESS' | 'SKIP' | null>(null);
|
||||
const [isProcessingGuess, setIsProcessingGuess] = useState(false);
|
||||
const [timeUntilNext, setTimeUntilNext] = useState('');
|
||||
const [hasRated, setHasRated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const updateCountdown = () => {
|
||||
@@ -180,6 +181,35 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (dailyPuzzle) {
|
||||
const ratedPuzzles = JSON.parse(localStorage.getItem('hoerdle_rated_puzzles') || '[]');
|
||||
if (ratedPuzzles.includes(dailyPuzzle.id)) {
|
||||
setHasRated(true);
|
||||
} else {
|
||||
setHasRated(false);
|
||||
}
|
||||
}
|
||||
}, [dailyPuzzle]);
|
||||
|
||||
const handleRatingSubmit = async (rating: number) => {
|
||||
if (!dailyPuzzle) return;
|
||||
|
||||
try {
|
||||
await submitRating(dailyPuzzle.songId, rating, genre);
|
||||
setHasRated(true);
|
||||
|
||||
// Persist to localStorage
|
||||
const ratedPuzzles = JSON.parse(localStorage.getItem('hoerdle_rated_puzzles') || '[]');
|
||||
if (!ratedPuzzles.includes(dailyPuzzle.id)) {
|
||||
ratedPuzzles.push(dailyPuzzle.id);
|
||||
localStorage.setItem('hoerdle_rated_puzzles', JSON.stringify(ratedPuzzles));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to submit rating', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<header className="header">
|
||||
@@ -265,6 +295,11 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
|
||||
</audio>
|
||||
</div>
|
||||
|
||||
{/* Rating Component */}
|
||||
<div style={{ marginBottom: '1rem' }}>
|
||||
<StarRating onRate={handleRatingSubmit} hasRated={hasRated} />
|
||||
</div>
|
||||
|
||||
{statistics && <Statistics statistics={statistics} />}
|
||||
<button onClick={handleShare} className="btn-primary" style={{ marginTop: '1rem' }}>
|
||||
{shareText}
|
||||
@@ -294,6 +329,11 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
|
||||
</audio>
|
||||
</div>
|
||||
|
||||
{/* Rating Component */}
|
||||
<div style={{ marginBottom: '1rem' }}>
|
||||
<StarRating onRate={handleRatingSubmit} hasRated={hasRated} />
|
||||
</div>
|
||||
|
||||
{statistics && <Statistics statistics={statistics} />}
|
||||
<button onClick={handleShare} className="btn-primary" style={{ marginTop: '1rem' }}>
|
||||
{shareText}
|
||||
@@ -305,3 +345,46 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StarRating({ onRate, hasRated }: { onRate: (rating: number) => void, hasRated: boolean }) {
|
||||
const [hover, setHover] = useState(0);
|
||||
const [rating, setRating] = useState(0);
|
||||
|
||||
if (hasRated) {
|
||||
return <div style={{ color: '#666', fontStyle: 'italic' }}>Thanks for rating!</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="star-rating" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.5rem' }}>
|
||||
<span style={{ fontSize: '0.875rem', color: '#666', fontWeight: '500' }}>Rate this song:</span>
|
||||
<div style={{ display: 'flex', gap: '0.25rem', justifyContent: 'center' }}>
|
||||
{[...Array(5)].map((_, index) => {
|
||||
const ratingValue = index + 1;
|
||||
return (
|
||||
<button
|
||||
key={index}
|
||||
type="button"
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
fontSize: '2rem',
|
||||
color: ratingValue <= (hover || rating) ? '#ffc107' : '#9ca3af',
|
||||
transition: 'color 0.2s',
|
||||
padding: '0 0.25rem'
|
||||
}}
|
||||
onClick={() => {
|
||||
setRating(ratingValue);
|
||||
onRate(ratingValue);
|
||||
}}
|
||||
onMouseEnter={() => setHover(ratingValue)}
|
||||
onMouseLeave={() => setHover(0)}
|
||||
>
|
||||
★
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user