fix overflow in daily rewards

This commit is contained in:
2025-12-13 16:27:30 +05:00
parent 712ae70e2a
commit 7d7136bac9

View File

@ -16,19 +16,38 @@ import ChevronRightRoundedIcon from '@mui/icons-material/ChevronRightRounded';
import TodayRoundedIcon from '@mui/icons-material/TodayRounded'; import TodayRoundedIcon from '@mui/icons-material/TodayRounded';
import CustomTooltip from '../components/Notifications/CustomTooltip'; import CustomTooltip from '../components/Notifications/CustomTooltip';
import CoinsDisplay from '../components/CoinsDisplay'; import CoinsDisplay from '../components/CoinsDisplay';
import { claimDaily, fetchDailyStatus, DailyStatusResponse, fetchDailyClaimDays } from '../api'; import {
claimDaily,
fetchDailyStatus,
DailyStatusResponse,
fetchDailyClaimDays,
} from '../api';
const RU_MONTHS = [ const RU_MONTHS = [
'Январь','Февраль','Март','Апрель','Май','Июнь', 'Январь',
'Июль','Август','Сентябрь','Октябрь','Ноябрь','Декабрь', 'Февраль',
'Март',
'Апрель',
'Май',
'Июнь',
'Июль',
'Август',
'Сентябрь',
'Октябрь',
'Ноябрь',
'Декабрь',
]; ];
const RU_WEEKDAYS = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс']; const RU_WEEKDAYS = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
const pad2 = (n: number) => String(n).padStart(2, '0'); const pad2 = (n: number) => String(n).padStart(2, '0');
const keyOf = (d: Date) => `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`; const keyOf = (d: Date) =>
const startOfDay = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDate()); `${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) => const isSameDay = (a: Date, b: Date) =>
a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); a.getFullYear() === b.getFullYear() &&
a.getMonth() === b.getMonth() &&
a.getDate() === b.getDate();
const weekdayMonFirst = (date: Date) => (date.getDay() + 6) % 7; const weekdayMonFirst = (date: Date) => (date.getDay() + 6) % 7;
const EKATERINBURG_TZ = 'Asia/Yekaterinburg'; const EKATERINBURG_TZ = 'Asia/Yekaterinburg';
@ -82,7 +101,9 @@ type DailyStatusCompat = DailyStatusResponse & {
export default function DailyReward({ onClaimed }: Props) { export default function DailyReward({ onClaimed }: Props) {
const today = useMemo(() => startOfDay(new Date()), []); const today = useMemo(() => startOfDay(new Date()), []);
const [view, setView] = useState(() => new Date(today.getFullYear(), today.getMonth(), 1)); const [view, setView] = useState(
() => new Date(today.getFullYear(), today.getMonth(), 1),
);
const [selected, setSelected] = useState<Date>(today); const [selected, setSelected] = useState<Date>(today);
// перенесённая логика статуса/клейма // перенесённая логика статуса/клейма
@ -96,14 +117,19 @@ export default function DailyReward({ onClaimed }: Props) {
const viewYear = view.getFullYear(); const viewYear = view.getFullYear();
const viewMonth = view.getMonth(); const viewMonth = view.getMonth();
const grid = useMemo(() => buildCalendarGrid(viewYear, viewMonth), [viewYear, viewMonth]); const grid = useMemo(
() => buildCalendarGrid(viewYear, viewMonth),
[viewYear, viewMonth],
);
const streak = status?.streak ?? 0; const streak = status?.streak ?? 0;
const wasOnlineToday = status?.was_online_today ?? false; const wasOnlineToday = status?.was_online_today ?? false;
const canClaim = (status?.can_claim ?? false) && wasOnlineToday; const canClaim = (status?.can_claim ?? false) && wasOnlineToday;
const goPrev = () => setView((v) => new Date(v.getFullYear(), v.getMonth() - 1, 1)); const goPrev = () =>
const goNext = () => setView((v) => new Date(v.getFullYear(), v.getMonth() + 1, 1)); 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 goToday = () => {
const t = new Date(today.getFullYear(), today.getMonth(), 1); const t = new Date(today.getFullYear(), today.getMonth(), 1);
setView(t); setView(t);
@ -156,13 +182,16 @@ export default function DailyReward({ onClaimed }: Props) {
}, [clientSecondsLeft]); }, [clientSecondsLeft]);
const todaysReward = useMemo(() => { const todaysReward = useMemo(() => {
const effectiveStreak = canClaim ? Math.max(1, streak === 0 ? 1 : streak) : streak; const effectiveStreak = canClaim
? Math.max(1, streak === 0 ? 1 : streak)
: streak;
return calcRewardByStreak(effectiveStreak); return calcRewardByStreak(effectiveStreak);
}, [streak, canClaim]); }, [streak, canClaim]);
const subtitle = useMemo(() => { const subtitle = useMemo(() => {
if (!status) return ''; if (!status) return '';
if (!wasOnlineToday) return 'Награда откроется после входа на сервер сегодня.'; if (!wasOnlineToday)
return 'Награда откроется после входа на сервер сегодня.';
if (canClaim) return 'Можно забрать прямо сейчас 🎁'; if (canClaim) return 'Можно забрать прямо сейчас 🎁';
return `До следующей награды: ${formatHHMMSS(clientSecondsLeft)}`; return `До следующей награды: ${formatHHMMSS(clientSecondsLeft)}`;
}, [status, wasOnlineToday, canClaim, clientSecondsLeft]); }, [status, wasOnlineToday, canClaim, clientSecondsLeft]);
@ -180,7 +209,9 @@ export default function DailyReward({ onClaimed }: Props) {
onClaimed?.(added); onClaimed?.(added);
} else { } else {
if (res.reason === 'not_online_today') { if (res.reason === 'not_online_today') {
setError('Чтобы забрать награду — зайдите на сервер сегодня хотя бы на минуту.'); setError(
'Чтобы забрать награду — зайдите на сервер сегодня хотя бы на минуту.',
);
} else { } else {
setError(res.reason || 'Награда недоступна'); setError(res.reason || 'Награда недоступна');
} }
@ -197,7 +228,17 @@ export default function DailyReward({ onClaimed }: Props) {
}; };
return ( return (
<Box sx={{ px: { xs: 2, md: 4 }, py: { xs: 2, md: 3 }, mt: '-3vh', width: '85%' }}> <Box
sx={{
// px: { xs: 2, md: 4 },
// py: { xs: 2, md: 3 },
mt: '-3vh',
width: '85%',
overflowY: 'auto',
paddingTop: '5vh',
paddingBottom: '5vh',
}}
>
<Paper <Paper
elevation={0} elevation={0}
sx={{ sx={{
@ -233,14 +274,24 @@ export default function DailyReward({ onClaimed }: Props) {
gap: 2, gap: 2,
}} }}
> >
<Box sx={{ minWidth: 220, display: 'flex', gap: '1vw', alignItems: 'center' }}> <Box
<Typography sx={{ color: 'rgba(255,255,255,0.75)', display: 'flex', gap: 1 }}> sx={{
minWidth: 220,
display: 'flex',
gap: '1vw',
alignItems: 'center',
}}
>
<Typography
sx={{ color: 'rgba(255,255,255,0.75)', display: 'flex', gap: 1 }}
>
<CoinsDisplay value={todaysReward} size="small" /> <CoinsDisplay value={todaysReward} size="small" />
</Typography> </Typography>
<Typography <Typography
sx={{ sx={{
fontFamily: 'Benzin-Bold, system-ui, -apple-system, Segoe UI, Roboto, Arial', fontFamily:
'Benzin-Bold, system-ui, -apple-system, Segoe UI, Roboto, Arial',
fontWeight: 800, fontWeight: 800,
fontSize: '1rem', fontSize: '1rem',
color: '#fff', color: '#fff',
@ -278,7 +329,14 @@ export default function DailyReward({ onClaimed }: Props) {
</IconButton> </IconButton>
<Box sx={{ minWidth: 160, textAlign: 'center', maxWidth: '10vw' }}> <Box sx={{ minWidth: 160, textAlign: 'center', maxWidth: '10vw' }}>
<Typography sx={{ color: '#fff', fontWeight: 800, letterSpacing: 0.2, fontSize: { xs: 14.5, md: 16 } }}> <Typography
sx={{
color: '#fff',
fontWeight: 800,
letterSpacing: 0.2,
fontSize: { xs: 14.5, md: 16 },
}}
>
{RU_MONTHS[viewMonth]} {viewYear} {RU_MONTHS[viewMonth]} {viewYear}
</Typography> </Typography>
</Box> </Box>
@ -300,7 +358,14 @@ export default function DailyReward({ onClaimed }: Props) {
{/* Calendar */} {/* Calendar */}
<Box sx={{ px: { xs: 2, md: 3 }, py: 2.5 }}> <Box sx={{ px: { xs: 2, md: 3 }, py: 2.5 }}>
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 1, mb: 1.2 }}> <Box
sx={{
display: 'grid',
gridTemplateColumns: 'repeat(7, 1fr)',
gap: 1,
mb: 1.2,
}}
>
{RU_WEEKDAYS.map((w, i) => ( {RU_WEEKDAYS.map((w, i) => (
<Typography <Typography
key={w} key={w}
@ -308,7 +373,8 @@ export default function DailyReward({ onClaimed }: Props) {
textAlign: 'center', textAlign: 'center',
fontSize: 12, fontSize: 12,
fontWeight: 700, fontWeight: 700,
color: i >= 5 ? 'rgba(255,255,255,0.75)' : 'rgba(255,255,255,0.6)', color:
i >= 5 ? 'rgba(255,255,255,0.75)' : 'rgba(255,255,255,0.6)',
userSelect: 'none', userSelect: 'none',
}} }}
> >
@ -317,7 +383,13 @@ export default function DailyReward({ onClaimed }: Props) {
))} ))}
</Box> </Box>
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 1 }}> <Box
sx={{
display: 'grid',
gridTemplateColumns: 'repeat(7, 1fr)',
gap: 1,
}}
>
{grid.map(({ date, inCurrentMonth }) => { {grid.map(({ date, inCurrentMonth }) => {
const d = startOfDay(date); const d = startOfDay(date);
const isToday = isSameDay(d, today); const isToday = isSameDay(d, today);
@ -336,12 +408,19 @@ export default function DailyReward({ onClaimed }: Props) {
borderRadius: 3, borderRadius: 3,
position: 'relative', position: 'relative',
overflow: 'hidden', overflow: 'hidden',
border: isSelected ? '1px solid rgba(242,113,33,0.85)' : 'none', border: isSelected
bgcolor: inCurrentMonth ? 'rgba(0,0,0,0.24)' : 'rgba(0,0,0,0.12)', ? '1px solid rgba(242,113,33,0.85)'
transition: 'transform 0.18s ease, background-color 0.18s ease, border-color 0.18s ease', : '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)', transform: isSelected ? 'scale(1.02)' : 'scale(1)',
'&:hover': { '&:hover': {
bgcolor: inCurrentMonth ? 'rgba(255,255,255,0.06)' : 'rgba(255,255,255,0.04)', bgcolor: inCurrentMonth
? 'rgba(255,255,255,0.06)'
: 'rgba(255,255,255,0.04)',
transform: 'translateY(-1px)', transform: 'translateY(-1px)',
}, },
}} }}
@ -351,7 +430,8 @@ export default function DailyReward({ onClaimed }: Props) {
sx={{ sx={{
position: 'absolute', position: 'absolute',
inset: -20, inset: -20,
background: 'radial-gradient(circle at 50% 50%, rgba(233,64,205,0.22), transparent 55%)', background:
'radial-gradient(circle at 50% 50%, rgba(233,64,205,0.22), transparent 55%)',
pointerEvents: 'none', pointerEvents: 'none',
}} }}
/> />
@ -373,7 +453,9 @@ export default function DailyReward({ onClaimed }: Props) {
sx={{ sx={{
fontSize: 14, fontSize: 14,
fontWeight: 800, fontWeight: 800,
color: inCurrentMonth ? '#fff' : 'rgba(255,255,255,0.35)', color: inCurrentMonth
? '#fff'
: 'rgba(255,255,255,0.35)',
lineHeight: 1, lineHeight: 1,
}} }}
> >
@ -386,8 +468,8 @@ export default function DailyReward({ onClaimed }: Props) {
color: claimed color: claimed
? 'rgba(156, 255, 198, 0.9)' ? 'rgba(156, 255, 198, 0.9)'
: isToday : isToday
? 'rgba(242,113,33,0.95)' ? 'rgba(242,113,33,0.95)'
: 'rgba(255,255,255,0.45)', : 'rgba(255,255,255,0.45)',
fontWeight: 700, fontWeight: 700,
userSelect: 'none', userSelect: 'none',
}} }}
@ -415,52 +497,67 @@ export default function DailyReward({ onClaimed }: Props) {
</Box> </Box>
{/* Footer actions */} {/* Footer actions */}
<Box sx={{ mt: 2.2, display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 2, flexWrap: 'wrap' }}> <Box
sx={{
mt: 2.2,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 2,
flexWrap: 'wrap',
}}
>
<Box> <Box>
<Typography sx={{ color: 'rgba(255,255,255,0.65)', fontSize: 12 }}> <Typography
Выбрано: <span style={{ color: '#fff', fontWeight: 800 }}>{selectedKey}</span> sx={{ color: 'rgba(255,255,255,0.65)', fontSize: 12 }}
>
Выбрано:{' '}
<span style={{ color: '#fff', fontWeight: 800 }}>
{selectedKey}
</span>
</Typography> </Typography>
</Box> </Box>
<Box sx={{ display: 'flex', gap: '1.2vw', alignItems: 'center' }}> <Box sx={{ display: 'flex', gap: '1.2vw', alignItems: 'center' }}>
<CustomTooltip title={subtitle} disableInteractive> <CustomTooltip title={subtitle} disableInteractive>
<Box <Box
sx={{
display: 'inline-block',
cursor: loading || !status?.ok || !canClaim ? 'help' : 'pointer',
}}
>
<Button
variant="contained"
disabled={loading || !status?.ok || !canClaim}
onClick={handleClaim}
sx={{ sx={{
px: 3, display: 'inline-block',
py: 1.2, cursor:
borderRadius: '2vw', loading || !status?.ok || !canClaim ? 'help' : 'pointer',
textTransform: 'uppercase',
fontFamily:
'Benzin-Bold, system-ui, -apple-system, Segoe UI, Roboto, Arial',
background:
'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)',
transition:
'transform 0.25s ease, box-shadow 0.25s ease, filter 0.25s ease',
'&:hover': {
transform: 'scale(0.98)',
filter: 'brightness(0.92)',
boxShadow: '0 0.5vw 1vw rgba(0, 0, 0, 0.3)',
},
'&.Mui-disabled': {
background: 'rgba(255,255,255,0.10)',
color: 'rgba(255,255,255,0.45)',
pointerEvents: 'none', // важно оставить
},
}} }}
> >
{loading ? 'Забираем...' : 'Забрать'} <Button
</Button> variant="contained"
</Box> disabled={loading || !status?.ok || !canClaim}
</CustomTooltip> onClick={handleClaim}
sx={{
px: 3,
py: 1.2,
borderRadius: '2vw',
textTransform: 'uppercase',
fontFamily:
'Benzin-Bold, system-ui, -apple-system, Segoe UI, Roboto, Arial',
background:
'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)',
transition:
'transform 0.25s ease, box-shadow 0.25s ease, filter 0.25s ease',
'&:hover': {
transform: 'scale(0.98)',
filter: 'brightness(0.92)',
boxShadow: '0 0.5vw 1vw rgba(0, 0, 0, 0.3)',
},
'&.Mui-disabled': {
background: 'rgba(255,255,255,0.10)',
color: 'rgba(255,255,255,0.45)',
pointerEvents: 'none', // важно оставить
},
}}
>
{loading ? 'Забираем...' : 'Забрать'}
</Button>
</Box>
</CustomTooltip>
<CustomTooltip title="Сбросить выбор на сегодня"> <CustomTooltip title="Сбросить выбор на сегодня">
<IconButton <IconButton