play history
This commit is contained in:
@@ -0,0 +1,81 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { motion } from 'motion/react'
|
||||||
|
import { IconX } from '@tabler/icons-react'
|
||||||
|
import { fetchHistory, type HistoryEntry } from '../api'
|
||||||
|
import { useStore } from '../store'
|
||||||
|
import { useFocusTrap } from '../hooks/useFocusTrap'
|
||||||
|
|
||||||
|
function formatTime(secs: number): string {
|
||||||
|
const m = Math.floor(secs / 60)
|
||||||
|
const s = secs % 60
|
||||||
|
return `${m}:${s.toString().padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PuzzleHistory({ onClose }: { onClose: () => void }) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const trapRef = useFocusTrap(true)
|
||||||
|
const [items, setItems] = useState<HistoryEntry[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const loadByShortId = useStore(s => s.loadByShortId)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchHistory().then(setItems).finally(() => setLoading(false))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div className="settings-overlay" onClick={onClose}
|
||||||
|
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.15 }}>
|
||||||
|
<motion.div ref={trapRef} className="settings-panel" onClick={e => e.stopPropagation()}
|
||||||
|
role="dialog" aria-modal="true" aria-label={t('history.title')}
|
||||||
|
initial={{ opacity: 0, scale: 0.95, y: 12 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.95, y: 12 }}
|
||||||
|
transition={{ duration: 0.2, ease: [0.25, 0.1, 0.25, 1] }}>
|
||||||
|
<div className="settings-header">
|
||||||
|
<h3>{t('history.title')}</h3>
|
||||||
|
<button className="btn btn-sm btn-ghost" onClick={onClose} aria-label={t('common.close')}>
|
||||||
|
<IconX size={16} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div style={{ padding: '0 20px 20px', overflowY: 'auto' }}>
|
||||||
|
{loading && <p className="settings-hint">{t('common.loading')}</p>}
|
||||||
|
{!loading && items.length === 0 && (
|
||||||
|
<p className="settings-hint">{t('history.empty')}</p>
|
||||||
|
)}
|
||||||
|
{items.map((item, i) => (
|
||||||
|
<div key={i} className="history-item" style={{
|
||||||
|
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||||||
|
padding: '8px 0', borderBottom: '1px solid var(--border)',
|
||||||
|
}}>
|
||||||
|
<div>
|
||||||
|
<span style={{ fontWeight: 500 }}>
|
||||||
|
{item.preset ? item.preset.charAt(0).toUpperCase() + item.preset.slice(1) : 'Custom'}
|
||||||
|
</span>
|
||||||
|
<span style={{ color: 'var(--text-secondary)', marginLeft: 8, fontSize: '0.85em' }}>
|
||||||
|
{item.date}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12, fontSize: '0.85em' }}>
|
||||||
|
<span>{formatTime(item.time)}</span>
|
||||||
|
<span style={{ color: 'var(--text-tertiary)' }}>
|
||||||
|
{item.hints} {item.hints === 1 ? t('history.hint') : t('history.hints')}
|
||||||
|
</span>
|
||||||
|
{item.short_id && (
|
||||||
|
<button className="btn btn-sm" onClick={() => {
|
||||||
|
const sid = item.short_id
|
||||||
|
if (sid) loadByShortId(sid)
|
||||||
|
onClose()
|
||||||
|
}}>
|
||||||
|
{t('history.playAgain')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user