import React, { useEffect, useMemo, useState } from 'react'; import { Box, Typography, IconButton, Stack, Paper, ButtonBase, Divider, LinearProgress, Alert, Button, } from '@mui/material'; import ChevronLeftRoundedIcon from '@mui/icons-material/ChevronLeftRounded'; import ChevronRightRoundedIcon from '@mui/icons-material/ChevronRightRounded'; import TodayRoundedIcon from '@mui/icons-material/TodayRounded'; import CustomTooltip from '../components/Notifications/CustomTooltip'; import CoinsDisplay from '../components/CoinsDisplay'; import { claimDaily, fetchDailyStatus, DailyStatusResponse, fetchDailyClaimDays, } from '../api'; const pulseGradient = { '@keyframes pulseGlow': { '0%': { opacity: 0.35, transform: 'scale(0.9)', }, '50%': { opacity: 0.7, transform: 'scale(1.05)', }, '100%': { opacity: 0.35, transform: 'scale(0.9)', }, }, }; const RU_MONTHS = [ 'Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь', ]; const RU_WEEKDAYS = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс']; const pad2 = (n: number) => String(n).padStart(2, '0'); const keyOf = (d: Date) => `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`; const startOfDay = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDate()); const isSameDay = (a: Date, b: Date) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); const weekdayMonFirst = (date: Date) => (date.getDay() + 6) % 7; const EKATERINBURG_TZ = 'Asia/Yekaterinburg'; function keyOfInTZ(date: Date, timeZone: string) { // en-CA даёт ровно YYYY-MM-DD return new Intl.DateTimeFormat('en-CA', { timeZone, year: 'numeric', month: '2-digit', day: '2-digit', }).format(date); } type Cell = { date: Date; inCurrentMonth: boolean }; function buildCalendarGrid(viewYear: number, viewMonth: number): Cell[] { const first = new Date(viewYear, viewMonth, 1); const lead = weekdayMonFirst(first); const total = 42; const cells: Cell[] = []; for (let i = 0; i < total; i++) { const d = new Date(viewYear, viewMonth, 1 - lead + i); cells.push({ date: d, inCurrentMonth: d.getMonth() === viewMonth }); } return cells; } function formatHHMMSS(totalSeconds: number) { const s = Math.max(0, Math.floor(totalSeconds)); const hh = String(Math.floor(s / 3600)).padStart(2, '0'); const mm = String(Math.floor((s % 3600) / 60)).padStart(2, '0'); const ss = String(s % 60).padStart(2, '0'); return `${hh}:${mm}:${ss}`; } function calcRewardByStreak(streak: number) { return Math.min(10 + Math.max(0, streak - 1) * 10, 50); } type Props = { onClaimed?: (coinsAdded: number) => void; onOpenGame?: () => void; }; type DailyStatusCompat = DailyStatusResponse & { was_online_today?: boolean; next_claim_at_utc?: string; next_claim_at_local?: string; }; export default function DailyReward({ onClaimed }: Props) { const today = useMemo(() => startOfDay(new Date()), []); const [view, setView] = useState( () => new Date(today.getFullYear(), today.getMonth(), 1), ); const [selected, setSelected] = useState(today); // перенесённая логика статуса/клейма const [status, setStatus] = useState(null); const [loading, setLoading] = useState(false); const [tick, setTick] = useState(0); const [error, setError] = useState(''); const [success, setSuccess] = useState(''); const [claimDays, setClaimDays] = useState>(new Set()); const viewYear = view.getFullYear(); const viewMonth = view.getMonth(); const grid = useMemo( () => buildCalendarGrid(viewYear, viewMonth), [viewYear, viewMonth], ); const streak = status?.streak ?? 0; const wasOnlineToday = status?.was_online_today ?? false; const canClaim = (status?.can_claim ?? false) && wasOnlineToday; const goPrev = () => setView((v) => new Date(v.getFullYear(), v.getMonth() - 1, 1)); const goNext = () => setView((v) => new Date(v.getFullYear(), v.getMonth() + 1, 1)); const goToday = () => { const t = new Date(today.getFullYear(), today.getMonth(), 1); setView(t); setSelected(today); }; const selectedKey = keyOf(selected); const loadStatus = async () => { setError(''); try { const s = (await fetchDailyStatus()) as DailyStatusCompat; setStatus(s); } catch (e) { setError(e instanceof Error ? e.message : 'Ошибка загрузки статуса'); } }; const loadClaimDays = async () => { try { const r = await fetchDailyClaimDays(180); if (r.ok) setClaimDays(new Set(r.days)); } catch (e) { console.error('Ошибка загрузки дней наград:', e); // можно setError(...) если хочешь показывать } }; useEffect(() => { loadStatus(); loadClaimDays(); }, []); useEffect(() => { const id = setInterval(() => setTick((x) => x + 1), 1000); return () => clearInterval(id); }, []); const clientSecondsLeft = useMemo(() => { if (!status) return 0; if (canClaim) return 0; return Math.max(0, (status.seconds_to_next ?? 0) - tick); }, [status, tick, canClaim]); // ✅ фикс прогресса: считаем от clientSecondsLeft, а не от status.seconds_to_next (который не меняется) const progressValue = useMemo(() => { const day = 24 * 3600; const remaining = Math.min(day, Math.max(0, clientSecondsLeft)); return ((day - remaining) / day) * 100; }, [clientSecondsLeft]); const todaysReward = useMemo(() => { const effectiveStreak = canClaim ? Math.max(1, streak === 0 ? 1 : streak) : streak; return calcRewardByStreak(effectiveStreak); }, [streak, canClaim]); const subtitle = useMemo(() => { if (!status) return ''; if (!wasOnlineToday) return 'Награда откроется после входа на сервер сегодня.'; if (canClaim) return 'Можно забрать прямо сейчас 🎁'; return `До следующей награды: ${formatHHMMSS(clientSecondsLeft)}`; }, [status, wasOnlineToday, canClaim, clientSecondsLeft]); const handleClaim = async () => { setLoading(true); setError(''); setSuccess(''); try { const res = await claimDaily(); if (res.claimed) { const added = res.coins_added ?? 0; setSuccess(`Вы получили ${added} монет!`); onClaimed?.(added); } else { if (res.reason === 'not_online_today') { setError( 'Чтобы забрать награду — зайдите на сервер сегодня хотя бы на минуту.', ); } else { setError(res.reason || 'Награда недоступна'); } } await loadStatus(); await loadClaimDays(); setTick(0); } catch (e) { setError(e instanceof Error ? e.message : 'Ошибка при получении награды'); } finally { setLoading(false); } }; return ( {/* alerts */} {error && ( {error} )} {success && ( {success} )} {/* Header */} Серия дней: {streak} {RU_MONTHS[viewMonth]} {viewYear} {/* Calendar */} {RU_WEEKDAYS.map((w, i) => ( = 5 ? 'rgba(255,255,255,0.75)' : 'rgba(255,255,255,0.6)', userSelect: 'none', }} > {w} ))} {grid.map(({ date, inCurrentMonth }) => { const d = startOfDay(date); const isToday = isSameDay(d, today); const isSelected = isSameDay(d, selected); const dayKeyEkb = keyOfInTZ(d, EKATERINBURG_TZ); const claimed = claimDays.has(dayKeyEkb); return ( setSelected(d)} sx={{ width: '100%', aspectRatio: '1 / 1', borderRadius: 3, position: 'relative', overflow: 'hidden', border: isSelected ? '1px solid rgba(242,113,33,0.85)' : 'none', bgcolor: inCurrentMonth ? 'rgba(0,0,0,0.24)' : 'rgba(0,0,0,0.12)', transition: 'transform 0.18s ease, background-color 0.18s ease, border-color 0.18s ease', transform: isSelected ? 'scale(1.02)' : 'scale(1)', '&:hover': { bgcolor: inCurrentMonth ? 'rgba(255,255,255,0.06)' : 'rgba(255,255,255,0.04)', transform: 'translateY(-1px)', }, }} > {isToday && ( )} {d.getDate()} {claimed ? 'получено' : isToday ? 'сегодня' : ''} {claimed && ( )} ); })} {/* Footer actions */} Выбрано:{' '} {selectedKey} setSelected(today)} sx={{ color: 'rgba(255,255,255,0.9)', bgcolor: 'rgba(0,0,0,0.22)', '&:hover': { bgcolor: 'rgba(255,255,255,0.08)' }, }} > ); }