add daily reward
This commit is contained in:
@ -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();
|
||||
|
||||
|
||||
235
src/renderer/components/Profile/DailyRewards.tsx
Normal file
235
src/renderer/components/Profile/DailyRewards.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user