feat: implement song rating system with admin view and user persistence

This commit is contained in:
Hördle Bot
2025-11-23 10:34:45 +01:00
parent dc83c8372f
commit 4d3032df36
7 changed files with 191 additions and 11 deletions

View File

@@ -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>
);
}