receipt notifications
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { motion, AnimatePresence } from 'motion/react'
|
||||
import { QRCodeSVG } from 'qrcode.react'
|
||||
import { IconCopyFilled, IconCheckFilled } from '@tabler/icons-react'
|
||||
import { useStore } from '../store'
|
||||
|
||||
export default function ReceiptNotification() {
|
||||
const { t } = useTranslation()
|
||||
const pending = useStore(s => s.pendingReceipt)
|
||||
const dismiss = useStore(s => s.dismissReceipt)
|
||||
const claim = useStore(s => s.claimPendingReceipt)
|
||||
const [claiming, setClaiming] = useState(false)
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const handleClaim = async () => {
|
||||
if (!pending) return
|
||||
setClaiming(true)
|
||||
await claim(pending.receipt.receipt_id)
|
||||
setClaiming(false)
|
||||
}
|
||||
|
||||
const copyUrl = () => {
|
||||
if (!pending) return
|
||||
navigator.clipboard.writeText(pending.receipt_url).then(() => {
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 1500)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{pending && (
|
||||
<motion.div className="receipt-notification"
|
||||
role="alertdialog" aria-label={t('receipt.claimTitle')}
|
||||
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||
transition={{ duration: 0.2, ease: [0.25, 0.1, 0.25, 1] }}>
|
||||
<h4 className="receipt-title">{t('receipt.claimTitle')}</h4>
|
||||
<div className="receipt-info">
|
||||
<p>{t('receipt.solvedOn', { instance: pending.receipt.instance_url })}</p>
|
||||
<p className="receipt-detail">{t('receipt.time', { time: pending.receipt.solve_time_secs })} - {t('receipt.hints', { hints: pending.receipt.hints_used })}</p>
|
||||
<p className="receipt-detail">{t('receipt.date', { date: pending.receipt.solve_date })}</p>
|
||||
{pending.receipt.room_code && (
|
||||
<p className="receipt-detail">{t('receipt.room', { code: pending.receipt.room_code, mode: pending.receipt.mode })}</p>
|
||||
)}
|
||||
{pending.receipt.players && pending.receipt.players.length > 0 && (
|
||||
<p className="receipt-detail">{t('receipt.with', { players: pending.receipt.players.join(', ') })}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="receipt-qr">
|
||||
<QRCodeSVG value={pending.receipt_url} size={96} bgColor="transparent" fgColor="currentColor" aria-label={t('receipt.qrLabel')} />
|
||||
</div>
|
||||
|
||||
<div className="receipt-url-row">
|
||||
<span className="receipt-url" title={pending.receipt_url}>{pending.receipt_url}</span>
|
||||
<button className="btn btn-sm btn-ghost receipt-copy-btn" onClick={copyUrl} aria-label={t('common.copyLink')}>
|
||||
{copied ? <span role="status"><IconCheckFilled size={12} aria-hidden="true" /><span className="visually-hidden">{t('common.copied')}</span></span> : <IconCopyFilled size={12} aria-hidden="true" />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="receipt-actions">
|
||||
<button className="btn btn-sm btn-accent" onClick={handleClaim} disabled={claiming}>
|
||||
{claiming ? t('receipt.claiming') : t('receipt.claim')}
|
||||
</button>
|
||||
<button className="btn btn-sm" onClick={() => dismiss(pending.receipt.receipt_id)}>
|
||||
{t('common.dismiss')}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user