puzzle ratings
This commit is contained in:
@@ -0,0 +1,59 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { IconStarFilled } from '@tabler/icons-react'
|
||||||
|
import { motion } from 'motion/react'
|
||||||
|
import * as api from '../api'
|
||||||
|
import { useStore } from '../store'
|
||||||
|
|
||||||
|
export default function PuzzleRating() {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const puzzleId = useStore(s => s.puzzleId)
|
||||||
|
const [avg, setAvg] = useState(0)
|
||||||
|
const [count, setCount] = useState(0)
|
||||||
|
const [myRating, setMyRating] = useState<number | null>(null)
|
||||||
|
const [hover, setHover] = useState(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!puzzleId) return
|
||||||
|
api.fetchRating(puzzleId).then(r => {
|
||||||
|
setAvg(r.average)
|
||||||
|
setCount(r.count)
|
||||||
|
setMyRating(r.my_rating)
|
||||||
|
})
|
||||||
|
}, [puzzleId])
|
||||||
|
|
||||||
|
const rate = async (rating: number) => {
|
||||||
|
if (!puzzleId) return
|
||||||
|
const r = await api.ratePuzzle(puzzleId, rating)
|
||||||
|
setAvg(r.average)
|
||||||
|
setCount(r.count)
|
||||||
|
setMyRating(r.my_rating)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="puzzle-rating"
|
||||||
|
initial={{ opacity: 0, y: -8 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.3, delay: 0.3 }}
|
||||||
|
>
|
||||||
|
<span className="rating-label">{t('rating.label')}</span>
|
||||||
|
<div className="rating-stars" onMouseLeave={() => setHover(0)}>
|
||||||
|
{[1, 2, 3, 4, 5].map(n => (
|
||||||
|
<button
|
||||||
|
key={n}
|
||||||
|
className={`rating-star ${n <= (hover || myRating || 0) ? 'rating-star-active' : ''}`}
|
||||||
|
onMouseEnter={() => setHover(n)}
|
||||||
|
onClick={() => rate(n)}
|
||||||
|
aria-label={t('rating.star', { n })}
|
||||||
|
>
|
||||||
|
<IconStarFilled size={18} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{count > 0 && (
|
||||||
|
<span className="rating-info">{t('rating.avg', { avg, count, unit: count === 1 ? t('rating.rating') : t('rating.ratings') })}</span>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user