import {useRef} from 'react'; import {Link} from 'react-router-dom'; import {useTranslation} from 'react-i18next'; import {Coins, LockKeyhole} from 'lucide-react'; import {toast} from 'sonner'; import {Badge} from '@/components/ui/badge'; import {Button} from '@/components/ui/button'; import {Card} from '@/components/ui/card'; import {Skeleton} from '@/components/ui/skeleton'; import {cn} from '@/lib/utils'; import EffectBackground from '../components/EffectBackground.jsx'; import FitText from '../components/FitText.jsx'; import PageContainer from '../components/PageContainer.jsx'; import {usePreferences} from '../context/PreferencesContext.jsx'; import useInView from '../hooks/useInView.js'; import {formatDate} from '../utils/format.js'; import {buyItem, equipItem, LOOKS, SLOTS} from '../utils/rewards.js'; const GAME_KINDS = ['STARTED', 'FINISHED', 'FINISHED_MANUAL', 'COMPLETED']; const HOW_TO = ['STARTED', 'FINISHED', 'COMPLETED', 'FINISHED_MANUAL', 'MONTH', 'STREAK', 'UNLOCK']; export default function PointsPage() { const {t, i18n} = useTranslation(); const {wallet, reloadWallet} = usePreferences(); if (!wallet) { return ( ); } const act = async (send) => { try { const response = await send(); if (response.ok) { await reloadWallet(); } else if (response.status === 409) { const body = await response.json().catch(() => null); if (body?.code === 'NOT_ENOUGH_POINTS') toast.error(t('POINTS.ERROR_NOT_ENOUGH')); await reloadWallet(); } else if (response.status !== 401) { toast.error(t('SETTINGS.ERROR_SAVE')); } } catch (err) { console.error('Points save error:', err); toast.error(t('INFO.ERROR.NETWORK_FAILURE')); } }; const entryLabel = (entry) => { if (GAME_KINDS.includes(entry.kind)) { return ( ); } if (['BUY', 'UNLOCK'].includes(entry.kind)) return t(`POINTS.ITEMS.${entry.ref}`); if (['MONTH', 'STREAK'].includes(entry.kind)) return entry.ref; return null; }; return (

{t('POINTS.TITLE')}

{t('POINTS.BALANCE')}: {wallet.balance}

{t('POINTS.HOW_TO')}

    {HOW_TO.map((kind) =>
  • {t(`POINTS.RULES.${kind}`)}
  • )}

{t('POINTS.SHOP')}

{SLOTS.map((slot) => (

{t(`POINTS.SLOTS.${slot}`)}

    {wallet.catalog.filter((item) => item.slot === slot).map((item) => { const owned = wallet.owned.includes(item.code); const worn = wallet.equipped[slot] === item.code; const locked = !owned && item.track; return (
  • {t(`POINTS.ITEMS.${item.code}`)} {worn && {t('POINTS.EQUIPPED')}} {owned ? ( ) : locked ? ( ) : ( <> {item.price} )}
  • ); })}
))}

{t('POINTS.HISTORY')}

{wallet.history.length === 0 ? (

{t('POINTS.EMPTY_HISTORY')}

) : (
    {wallet.history.map((entry) => { const label = entryLabel(entry); return (
  • {formatDate(entry.eventAt, i18n.language)} {t(`POINTS.KINDS.${entry.kind}`)} {label && {label}} {entry.points !== 0 && ( {entry.points > 0 ? `+${entry.points}` : entry.points} )}
  • ); })}
)}
); } // A sample of the look: a ring on a circle, a pill with the badge icon, a color dot, a framed box, a tile with its // effect or a box with the animated background. function Preview({code}) { const look = LOOKS[code]; const ref = useRef(null); // The shop shows 20 animated previews at once; the ones off screen hold their frame. const inView = useInView(ref, !!(look?.cover || look?.effect)); if (!look) return null; if (look.Icon) { return ; } if (look.skin) { return ( ); } if (look.accent) { return ; } if (look.cover) { return ; } if (look.effect) { return ( ); } return ; } // An unlock-only item that is not earned yet: the lock, the progress as text and as a bar. Never a price or a Buy button. function LockedProgress({item, progress}) { const {t} = useTranslation(); const count = Math.min(progress, item.target); const text = t(`POINTS.PROGRESS.${item.track}`, {count, target: item.target}); return (
{text}
); }