From abb45c38387138a799eb867b58c0487210e084db Mon Sep 17 00:00:00 2001 From: aurinex Date: Sat, 13 Dec 2025 20:14:24 +0500 Subject: [PATCH] fix i cheto sdelal --- src/renderer/App.tsx | 9 + src/renderer/api.ts | 78 ++++++ src/renderer/components/PageHeader.tsx | 7 + src/renderer/pages/DailyQuests.tsx | 345 +++++++++++++++++++++++++ src/renderer/pages/Profile.tsx | 20 ++ src/renderer/pages/Shop.tsx | 10 +- src/renderer/utils/sounds.ts | 51 +++- 7 files changed, 513 insertions(+), 7 deletions(-) create mode 100644 src/renderer/pages/DailyQuests.tsx diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index a006b34..c2c041f 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -23,6 +23,7 @@ import { News } from './pages/News'; import PageHeader from './components/PageHeader'; import { useLocation } from 'react-router-dom'; import DailyReward from './pages/DailyReward'; +import DailyQuests from './pages/DailyQuests'; const AuthCheck = ({ children }: { children: ReactNode }) => { const [isAuthenticated, setIsAuthenticated] = useState(null); @@ -182,6 +183,14 @@ const AppLayout = () => { } /> + + + + } + /> { const response = await fetch(`${API_BASE_URL}/api/bonuses/types`); @@ -575,6 +605,54 @@ export async function claimDaily(): Promise { // ===== Ежедневная награда ===== \\ +// ===== Ежедневные квесты ===== + +export async function fetchDailyQuestsStatus(): Promise { + 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-quests/status?${params.toString()}`, + ); + + if (!response.ok) { + throw new Error('Не удалось получить статус ежедневных квестов'); + } + + return await response.json(); +} + +export async function claimDailyQuest(questKey: string): Promise { + const { accessToken, clientToken } = getAuthTokens(); + + if (!accessToken || !clientToken) { + throw new Error('Нет токенов авторизации'); + } + + const params = new URLSearchParams({ accessToken, clientToken, quest_key: questKey }); + const response = await fetch( + `${API_BASE_URL}/users/daily-quests/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 { const { accessToken, clientToken } = getAuthTokens(); diff --git a/src/renderer/components/PageHeader.tsx b/src/renderer/components/PageHeader.tsx index e8cb1dd..c561fd2 100644 --- a/src/renderer/components/PageHeader.tsx +++ b/src/renderer/components/PageHeader.tsx @@ -46,6 +46,13 @@ export default function PageHeader() { }; } + if (path.startsWith('/dailyquests')) { + return { + title: 'Ежедневные задания', + subtitle: 'Выполняйте ежедневные задания разной сложности и получайте награды!', + }; + } + if (path.startsWith('/profile')) { return { title: 'Профиль пользователя', diff --git a/src/renderer/pages/DailyQuests.tsx b/src/renderer/pages/DailyQuests.tsx new file mode 100644 index 0000000..00e2c0a --- /dev/null +++ b/src/renderer/pages/DailyQuests.tsx @@ -0,0 +1,345 @@ +// src/renderer/pages/DailyQuests.tsx +import React, { useEffect, useMemo, useState } from 'react'; +import { + Alert, + Box, + Button, + Chip, + Divider, + LinearProgress, + Paper, + Stack, + Typography, +} from '@mui/material'; +import CoinsDisplay from '../components/CoinsDisplay'; +import { FullScreenLoader } from '../components/FullScreenLoader'; +import { claimDailyQuest, fetchDailyQuestsStatus, DailyQuestsStatusResponse } from '../api'; + +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}`; +} + +type Quest = { + key: string; + title: string; + event?: string; + target?: string; + required: number; + progress: number; + reward: number; + status: 'active' | 'completed' | 'claimed'; + claimed_at?: string; + completed_at?: string; +}; + +type DailyQuestsStatusCompat = DailyQuestsStatusResponse & { + was_online_today?: boolean; + seconds_to_next?: number; + next_reset_at_utc?: string; + next_reset_at_local?: string; + quests?: Quest[]; +}; + +function statusChip(status: Quest['status']) { + if (status === 'claimed') + return ; + if (status === 'completed') + return ; + return ; +} + +export default function DailyQuests() { + const [status, setStatus] = useState(null); + const [pageLoading, setPageLoading] = useState(true); + const [actionLoadingKey, setActionLoadingKey] = useState(''); + const [tick, setTick] = useState(0); + + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + + const loadStatus = async () => { + setError(''); + try { + const s = (await fetchDailyQuestsStatus()) as DailyQuestsStatusCompat; + setStatus(s); + } catch (e) { + setError(e instanceof Error ? e.message : 'Ошибка загрузки ежедневных заданий'); + } + }; + + useEffect(() => { + (async () => { + setPageLoading(true); + await loadStatus(); + setPageLoading(false); + })(); + }, []); + + useEffect(() => { + const id = setInterval(() => setTick((x) => x + 1), 1000); + return () => clearInterval(id); + }, []); + + const wasOnlineToday = status?.was_online_today ?? false; + + const clientSecondsLeft = useMemo(() => { + if (!status) return 0; + return Math.max(0, (status.seconds_to_next ?? 0) - tick); + }, [status, tick]); + + const subtitle = useMemo(() => { + if (!status) return ''; + if (!wasOnlineToday) return 'Награды откроются после входа на сервер сегодня.'; + return `До обновления заданий: ${formatHHMMSS(clientSecondsLeft)}`; + }, [status, wasOnlineToday, clientSecondsLeft]); + + const quests: Quest[] = useMemo(() => (status?.quests ?? []) as Quest[], [status]); + const totalRewardLeft = useMemo(() => { + // сколько ещё можно забрать сегодня (completed, но не claimed) + return quests + .filter((q) => q.status === 'completed') + .reduce((sum, q) => sum + (q.reward ?? 0), 0); + }, [quests]); + + const handleClaim = async (questKey: string) => { + setActionLoadingKey(questKey); + setError(''); + setSuccess(''); + try { + const res = await claimDailyQuest(questKey); + + if (res.claimed) { + const added = res.coins_added ?? 0; + setSuccess(`Вы получили ${added} монет!`); + } else { + if (res.reason === 'not_online_today') { + setError('Чтобы забрать награду — зайдите на сервер сегодня хотя бы на минуту.'); + } else if (res.reason === 'not_completed') { + setError('Сначала выполните задание, затем заберите награду.'); + } else if (res.reason === 'already_claimed') { + setError('Награда уже получена.'); + } else { + setError(res.message || res.reason || 'Награда недоступна'); + } + } + + await loadStatus(); + setTick(0); + } catch (e) { + setError(e instanceof Error ? e.message : 'Ошибка при получении награды'); + } finally { + setActionLoadingKey(''); + } + }; + + if (pageLoading) { + return ( + + + + ); + } + + return ( + + + {/* sticky header */} + + + {error && ( + + {error} + + )} + {success && ( + + {success} + + )} + + + + + + + {subtitle} + + + + + + Можно забрать сегодня: + + + + + + + + + + {/* content */} + + {!wasOnlineToday && ( + + Зайдите на сервер сегодня, чтобы открыть получение наград за квесты. + + )} + + {quests.length === 0 ? ( + + На сегодня заданий нет. + + ) : ( + + {quests.map((q) => { + const req = Math.max(1, q.required ?? 1); + const prog = Math.max(0, q.progress ?? 0); + const pct = Math.min(100, (prog / req) * 100); + + const canClaim = wasOnlineToday && q.status === 'completed'; + const disabled = !canClaim || actionLoadingKey === q.key; + + return ( + + + + + {q.title} + + + + Прогресс: {Math.min(prog, req)}/{req} + + + + + {statusChip(q.status)} + + + + + + + + + + + + + ); + })} + + )} + + + + ); +} diff --git a/src/renderer/pages/Profile.tsx b/src/renderer/pages/Profile.tsx index 77fbbe0..8f240e3 100644 --- a/src/renderer/pages/Profile.tsx +++ b/src/renderer/pages/Profile.tsx @@ -66,6 +66,10 @@ export default function Profile() { navigate('/daily'); }; + const navigateDailyQuests = () => { + navigate('/dailyquests'); + }; + useEffect(() => { const savedConfig = localStorage.getItem('launcher_config'); if (savedConfig) { @@ -513,6 +517,22 @@ export default function Profile() { > Ежедневные награды + )} diff --git a/src/renderer/pages/Shop.tsx b/src/renderer/pages/Shop.tsx index 4e0bf36..cc46336 100644 --- a/src/renderer/pages/Shop.tsx +++ b/src/renderer/pages/Shop.tsx @@ -26,7 +26,7 @@ import { getPlayerServer } from '../utils/playerOnlineCheck'; import CaseRoulette from '../components/CaseRoulette'; import BonusShopItem from '../components/BonusShopItem'; import ShopItem from '../components/ShopItem'; -import { playBuySound } from '../utils/sounds'; +import { playBuySound, primeSounds } from '../utils/sounds'; function getRarityByWeight( weight?: number, @@ -376,6 +376,7 @@ export default function Shop() { setRouletteCaseItems(caseItems); setRouletteReward(result.reward); setRouletteOpen(true); + playBuySound(); // 4. уведомление setNotification({ @@ -405,6 +406,13 @@ export default function Shop() { setRouletteOpen(false); }; + useEffect(() => { + const onFirstUserGesture = () => primeSounds(); + + window.addEventListener('pointerdown', onFirstUserGesture, { once: true }); + return () => window.removeEventListener('pointerdown', onFirstUserGesture); + }, []); + return ( { + try { + if (unlocked) return; + + if (!buyAudio) { + buyAudio = new Audio(buySound); + buyAudio.volume = 0; // тихо, чтобы не слышно + } + + // попытка "разлочить" аудио в контексте user gesture + const p = buyAudio.play(); + if (p && typeof (p as Promise).then === 'function') { + (p as Promise) + .then(() => { + buyAudio?.pause(); + if (buyAudio) buyAudio.currentTime = 0; + if (buyAudio) buyAudio.volume = 0.6; // вернуть норм громкость + unlocked = true; + }) + .catch(() => { + // если заблокировано — попробуем снова при следующем клике + }); + } else { + // на всякий: если play синхронный + buyAudio.pause(); + buyAudio.currentTime = 0; + buyAudio.volume = 0.6; + unlocked = true; + } + } catch { + // не ломаем UI + } +}; export const playBuySound = () => { - if (!buyAudio) { - buyAudio = new Audio(buySound); - buyAudio.volume = 0.6; - } + try { + if (!buyAudio) { + buyAudio = new Audio(buySound); + buyAudio.volume = 0.6; + } - buyAudio.currentTime = 0; - buyAudio.play().catch(() => {}); + buyAudio.currentTime = 0; + buyAudio.play().catch(() => {}); + } catch { + // игнор + } };