add daily reward

This commit is contained in:
2025-12-13 01:35:55 +05:00
parent 5e5f1aaa0a
commit eabc54680f
3 changed files with 304 additions and 2 deletions

View File

@ -426,8 +426,6 @@ export interface OpenCaseResponse {
reward: CaseItem;
}
// ===== КЕЙСЫ =====
export async function fetchCases(): Promise<Case[]> {
const response = await fetch(`${API_BASE_URL}/cases`);
if (!response.ok) {
@ -481,6 +479,72 @@ export async function openCase(
return await response.json();
}
// ===== КЕЙСЫ ===== \\
// ===== Ежедневная награда =====
export interface DailyStatusResponse {
ok: boolean;
can_claim: boolean;
seconds_to_next: number;
was_online_today: boolean;
next_claim_at_utc: string;
next_claim_at_local: string;
streak: number;
}
export interface DailyClaimResponse {
claimed: boolean;
coins_added?: number;
streak?: number;
reason?: string;
}
export async function fetchDailyStatus(): Promise<DailyStatusResponse> {
const { accessToken, clientToken } = getAuthTokens();
if (!accessToken || !clientToken) {
throw new Error('Нет токенов авторизации');
}
const params = new URLSearchParams({ accessToken, clientToken });
const response = await fetch(
`${API_BASE_URL}/users/daily/status?${params.toString()}`,
);
if (!response.ok) {
throw new Error('Не удалось получить статус ежедневной награды');
}
return await response.json();
}
export async function claimDaily(): Promise<DailyClaimResponse> {
const { accessToken, clientToken } = getAuthTokens();
if (!accessToken || !clientToken) {
throw new Error('Нет токенов авторизации');
}
const params = new URLSearchParams({ accessToken, clientToken });
const response = await fetch(
`${API_BASE_URL}/users/daily/claim?${params.toString()}`,
{ method: 'POST' },
);
if (!response.ok) {
let msg = 'Не удалось забрать ежедневную награду';
try {
const err = await response.json();
msg = err.message || err.detail || msg;
} catch {}
throw new Error(msg);
}
return await response.json();
}
// ===== Ежедневная награда ===== \\
export async function fetchMe(): Promise<MeResponse> {
const { accessToken, clientToken } = getAuthTokens();

View File

@ -0,0 +1,235 @@
import React, { useEffect, useMemo, useState } from 'react';
import {
Box,
Button,
Card,
CardContent,
LinearProgress,
Typography,
Alert,
} from '@mui/material';
import { claimDaily, fetchDailyStatus, DailyStatusResponse } from '../../api';
import CoinsDisplay from '../CoinsDisplay';
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) {
// ВАЖНО: синхронизируй с бэком. Сейчас у тебя в бэке: 10..50 :contentReference[oaicite:2]{index=2}
// Если хочешь 50..100 — поменяй здесь тоже.
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 DailyRewards({ onClaimed, onOpenGame }: Props) {
const [status, setStatus] = useState<DailyStatusCompat | null>(null);
const [loading, setLoading] = useState(false);
const [tick, setTick] = useState(0);
const [error, setError] = useState<string>('');
const [success, setSuccess] = useState<string>('');
const secondsLeft = status?.seconds_to_next ?? 0;
const streak = status?.streak ?? 0;
const wasOnlineToday = status?.was_online_today ?? false; // если бэк не прислал — считаем false
const canClaim = (status?.can_claim ?? false) && wasOnlineToday;
const nextClaimAt =
status?.next_claim_at_utc || status?.next_claim_at_local || '';
const todaysReward = useMemo(() => {
const effectiveStreak = canClaim
? Math.max(1, streak === 0 ? 1 : streak)
: streak;
return calcRewardByStreak(effectiveStreak);
}, [streak, canClaim]);
const progressValue = useMemo(() => {
const day = 24 * 3600;
const remaining = Math.min(day, Math.max(0, secondsLeft));
return ((day - remaining) / day) * 100;
}, [secondsLeft]);
const loadStatus = async () => {
setError('');
try {
const s = (await fetchDailyStatus()) as DailyStatusCompat;
setStatus(s);
} catch (e) {
setError(e instanceof Error ? e.message : 'Ошибка загрузки статуса');
}
};
useEffect(() => {
loadStatus();
}, []);
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 - tick);
}, [status, tick, canClaim]);
const handleClaim = async () => {
setLoading(true);
setError('');
setSuccess('');
try {
const res = await claimDaily();
if (res.claimed) {
const added = res.coins_added ?? 0;
setSuccess(`Вы получили ${added} монет!`);
if (onClaimed) onClaimed(added);
} else {
// если бэк вернёт reason=not_online_today — покажем по-человечески
if (res.reason === 'not_online_today') {
setError(
'Чтобы забрать награду — зайдите на сервер сегодня хотя бы на минуту.',
);
} else {
setError(res.reason || 'Награда недоступна');
}
}
await loadStatus();
setTick(0);
} catch (e) {
setError(e instanceof Error ? e.message : 'Ошибка при получении награды');
} finally {
setLoading(false);
}
};
const subtitle = useMemo(() => {
if (!status) return '';
if (!wasOnlineToday)
return 'Награда откроется после входа на сервер сегодня.';
if (canClaim) return 'Можно забрать прямо сейчас 🎁';
return `До следующей награды: ${formatHHMMSS(clientSecondsLeft)}`;
}, [status, wasOnlineToday, canClaim, clientSecondsLeft]);
return (
<Card
sx={{
width: '90%',
maxWidth: 520,
background: 'rgba(20,20,20,0.9)',
borderRadius: '2vw',
border: '1px solid rgba(255,255,255,0.08)',
boxShadow: '0 10px 40px rgba(0,0,0,0.8)',
}}
>
<CardContent sx={{ p: 3 }}>
<Typography
sx={{
fontFamily: 'Benzin-Bold',
fontSize: '1.2rem',
mb: 1,
backgroundImage:
'linear-gradient(136deg, rgb(242,113,33), rgb(233,64,87), rgb(138,35,135))',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
}}
>
Ежедневная награда
</Typography>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
{success && (
<Alert severity="success" sx={{ mb: 2 }}>
{success}
</Alert>
)}
{!status ? (
<Typography sx={{ color: 'rgba(255,255,255,0.7)' }}>
Загружаем статус...
</Typography>
) : (
<>
<Box
sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}
>
<Typography sx={{ color: 'rgba(255,255,255,0.75)' }}>
Серия дней: <b>{streak}</b>
</Typography>
<Typography
sx={{
color: 'rgba(255,255,255,0.75)',
display: 'flex',
gap: 1,
}}
>
Награда: <CoinsDisplay value={todaysReward} size="small" />
</Typography>
</Box>
<Box sx={{ mb: 1 }}>
<LinearProgress variant="determinate" value={progressValue} />
</Box>
<Typography sx={{ color: 'rgba(255,255,255,0.85)', mb: 2 }}>
{subtitle}
</Typography>
{!wasOnlineToday && (
<Typography
sx={{
color: 'rgba(255,255,255,0.6)',
mb: 2,
fontSize: '0.9rem',
}}
>
Зайдите на сервер сегодня после этого кнопка станет активной.
</Typography>
)}
<Button
variant="contained"
fullWidth
disabled={loading || !status.ok || !canClaim}
onClick={handleClaim}
sx={{
mt: 1,
transition: 'transform 0.3s ease',
background:
'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)',
fontFamily: 'Benzin-Bold',
borderRadius: '2.5vw',
'&:hover': { transform: 'scale(1.03)' },
}}
>
{loading ? 'Забираем...' : 'Забрать награду'}
</Button>
</>
)}
</CardContent>
</Card>
);
}

View File

@ -24,6 +24,7 @@ import {
import CapeCard from '../components/CapeCard';
import { FullScreenLoader } from '../components/FullScreenLoader';
import { OnlinePlayersPanel } from '../components/OnlinePlayersPanel';
import DailyRewards from '../components/Profile/DailyRewards';
export default function Profile() {
const fileInputRef = useRef<HTMLInputElement>(null);
@ -193,6 +194,7 @@ export default function Profile() {
gap: '100px',
width: '100%',
justifyContent: 'center',
overflowY: 'auto',
}}
>
{loading ? (
@ -451,6 +453,7 @@ export default function Profile() {
</Box>
</Box>
<OnlinePlayersPanel currentUsername={username} />
<DailyRewards />
</Box>
</>
)}