fix i cheto sdelal
This commit is contained in:
@ -23,6 +23,7 @@ import { News } from './pages/News';
|
|||||||
import PageHeader from './components/PageHeader';
|
import PageHeader from './components/PageHeader';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import DailyReward from './pages/DailyReward';
|
import DailyReward from './pages/DailyReward';
|
||||||
|
import DailyQuests from './pages/DailyQuests';
|
||||||
|
|
||||||
const AuthCheck = ({ children }: { children: ReactNode }) => {
|
const AuthCheck = ({ children }: { children: ReactNode }) => {
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
|
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
|
||||||
@ -182,6 +183,14 @@ const AppLayout = () => {
|
|||||||
</AuthCheck>
|
</AuthCheck>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/dailyquests"
|
||||||
|
element={
|
||||||
|
<AuthCheck>
|
||||||
|
<DailyQuests />
|
||||||
|
</AuthCheck>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/shop"
|
path="/shop"
|
||||||
element={
|
element={
|
||||||
|
|||||||
@ -248,6 +248,36 @@ export type BonusTypesResponse = {
|
|||||||
bonuses: BonusType[];
|
bonuses: BonusType[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface DailyQuest {
|
||||||
|
key: string;
|
||||||
|
title: string;
|
||||||
|
event?: string;
|
||||||
|
target?: string;
|
||||||
|
required: number;
|
||||||
|
progress: number;
|
||||||
|
reward: number;
|
||||||
|
status: 'active' | 'completed' | 'claimed';
|
||||||
|
claimed_at?: string;
|
||||||
|
completed_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DailyQuestsStatusResponse {
|
||||||
|
ok: boolean;
|
||||||
|
day: string;
|
||||||
|
was_online_today: boolean;
|
||||||
|
seconds_to_next: number;
|
||||||
|
next_reset_at_utc: string;
|
||||||
|
next_reset_at_local: string;
|
||||||
|
quests: DailyQuest[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DailyQuestClaimResponse {
|
||||||
|
claimed: boolean;
|
||||||
|
coins_added?: number;
|
||||||
|
reason?: string;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchBonusTypes(): Promise<BonusType[]> {
|
export async function fetchBonusTypes(): Promise<BonusType[]> {
|
||||||
const response = await fetch(`${API_BASE_URL}/api/bonuses/types`);
|
const response = await fetch(`${API_BASE_URL}/api/bonuses/types`);
|
||||||
|
|
||||||
@ -575,6 +605,54 @@ export async function claimDaily(): Promise<DailyClaimResponse> {
|
|||||||
|
|
||||||
// ===== Ежедневная награда ===== \\
|
// ===== Ежедневная награда ===== \\
|
||||||
|
|
||||||
|
// ===== Ежедневные квесты =====
|
||||||
|
|
||||||
|
export async function fetchDailyQuestsStatus(): Promise<DailyQuestsStatusResponse> {
|
||||||
|
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<DailyQuestClaimResponse> {
|
||||||
|
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<MeResponse> {
|
export async function fetchMe(): Promise<MeResponse> {
|
||||||
const { accessToken, clientToken } = getAuthTokens();
|
const { accessToken, clientToken } = getAuthTokens();
|
||||||
|
|
||||||
|
|||||||
@ -46,6 +46,13 @@ export default function PageHeader() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (path.startsWith('/dailyquests')) {
|
||||||
|
return {
|
||||||
|
title: 'Ежедневные задания',
|
||||||
|
subtitle: 'Выполняйте ежедневные задания разной сложности и получайте награды!',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (path.startsWith('/profile')) {
|
if (path.startsWith('/profile')) {
|
||||||
return {
|
return {
|
||||||
title: 'Профиль пользователя',
|
title: 'Профиль пользователя',
|
||||||
|
|||||||
345
src/renderer/pages/DailyQuests.tsx
Normal file
345
src/renderer/pages/DailyQuests.tsx
Normal file
@ -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 <Chip size="small" label="Получено" sx={{ bgcolor: 'rgba(156,255,198,0.15)', color: 'rgba(156,255,198,0.95)', fontWeight: 800 }} />;
|
||||||
|
if (status === 'completed')
|
||||||
|
return <Chip size="small" label="Выполнено" sx={{ bgcolor: 'rgba(242,113,33,0.18)', color: 'rgba(242,113,33,0.95)', fontWeight: 800 }} />;
|
||||||
|
return <Chip size="small" label="В процессе" sx={{ bgcolor: 'rgba(255,255,255,0.08)', color: 'rgba(255,255,255,0.85)', fontWeight: 800 }} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DailyQuests() {
|
||||||
|
const [status, setStatus] = useState<DailyQuestsStatusCompat | null>(null);
|
||||||
|
const [pageLoading, setPageLoading] = useState(true);
|
||||||
|
const [actionLoadingKey, setActionLoadingKey] = useState<string>('');
|
||||||
|
const [tick, setTick] = useState(0);
|
||||||
|
|
||||||
|
const [error, setError] = useState<string>('');
|
||||||
|
const [success, setSuccess] = useState<string>('');
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', mt: '10vh' }}>
|
||||||
|
<FullScreenLoader fullScreen={false} message="Загрузка ежедневных заданий..." />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ width: '85vw', height: '100%', paddingBottom: '5vh' }}>
|
||||||
|
<Paper
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
borderRadius: '1.2vw',
|
||||||
|
overflow: 'hidden',
|
||||||
|
background:
|
||||||
|
'radial-gradient(circle at 10% 10%, rgba(242,113,33,0.18), transparent 55%), radial-gradient(circle at 90% 20%, rgba(233,64,205,0.14), transparent 55%), rgba(10,10,20,0.94)',
|
||||||
|
boxShadow: '0 1.2vw 3.8vw rgba(0,0,0,0.55)',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
maxHeight: '76vh',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* sticky header */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'sticky',
|
||||||
|
top: 0,
|
||||||
|
zIndex: 5,
|
||||||
|
background:
|
||||||
|
'radial-gradient(circle at 10% 10%, rgba(242,113,33,0.18), transparent 55%), radial-gradient(circle at 90% 20%, rgba(233,64,205,0.14), transparent 55%), rgba(10,10,20,0.94)',
|
||||||
|
backdropFilter: 'blur(10px)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ px: '2vw', pt: '1.2vh' }}>
|
||||||
|
{error && (
|
||||||
|
<Alert severity="error" sx={{ mb: 1.5 }}>
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
{success && (
|
||||||
|
<Alert severity="success" sx={{ mb: 1.5 }}>
|
||||||
|
{success}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
px: '2vw',
|
||||||
|
pb: '1.5vw',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
gap: '2vw',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.6 }}>
|
||||||
|
|
||||||
|
<Typography sx={{ color: 'rgba(255,255,255,0.70)', fontWeight: 700 }}>
|
||||||
|
{subtitle}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Stack direction="row" alignItems="center" spacing={1.2}>
|
||||||
|
<Typography sx={{ color: 'rgba(255,255,255,0.75)', fontWeight: 800 }}>
|
||||||
|
Можно забрать сегодня:
|
||||||
|
</Typography>
|
||||||
|
<CoinsDisplay value={totalRewardLeft} size="small" />
|
||||||
|
<Button
|
||||||
|
disableRipple
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
borderRadius: '2.5vw',
|
||||||
|
fontFamily: 'Benzin-Bold',
|
||||||
|
borderColor: 'rgba(255,255,255,0.25)',
|
||||||
|
color: '#fff',
|
||||||
|
'&:hover': { borderColor: 'rgba(242,113,33,0.9)' },
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setTick(0);
|
||||||
|
loadStatus();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Обновить
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider sx={{ borderColor: 'rgba(255,255,255,0.10)' }} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* content */}
|
||||||
|
<Box sx={{ px: '2vw', py: '2vh', overflowY: 'auto', flex: 1 }}>
|
||||||
|
{!wasOnlineToday && (
|
||||||
|
<Alert severity="warning" sx={{ mb: 2 }}>
|
||||||
|
Зайдите на сервер сегодня, чтобы открыть получение наград за квесты.
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{quests.length === 0 ? (
|
||||||
|
<Typography sx={{ color: 'rgba(255,255,255,0.75)', mt: '6vh' }}>
|
||||||
|
На сегодня заданий нет.
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<Stack spacing={1.6}>
|
||||||
|
{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 (
|
||||||
|
<Paper
|
||||||
|
key={q.key}
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
p: '1.4vw',
|
||||||
|
borderRadius: '1.1vw',
|
||||||
|
bgcolor: 'rgba(255,255,255,0.05)',
|
||||||
|
border: '1px solid rgba(255,255,255,0.08)',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 1.2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', gap: 2 }}>
|
||||||
|
<Box sx={{ minWidth: 0 }}>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
color: '#fff',
|
||||||
|
fontWeight: 900,
|
||||||
|
fontSize: '1.25vw',
|
||||||
|
lineHeight: 1.15,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{q.title}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography sx={{ color: 'rgba(255,255,255,0.65)', fontWeight: 700, mt: 0.6 }}>
|
||||||
|
Прогресс: {Math.min(prog, req)}/{req}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Stack direction="row" alignItems="center" spacing={1}>
|
||||||
|
{statusChip(q.status)}
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<CoinsDisplay value={q.reward ?? 0} size="small" />
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<LinearProgress
|
||||||
|
variant="determinate"
|
||||||
|
value={pct}
|
||||||
|
sx={{
|
||||||
|
height: '0.75vw',
|
||||||
|
borderRadius: '999px',
|
||||||
|
bgcolor: 'rgba(255,255,255,0.08)',
|
||||||
|
'& .MuiLinearProgress-bar': {
|
||||||
|
background:
|
||||||
|
'linear-gradient(90deg, rgba(242,113,33,1) 0%, rgba(233,64,205,1) 55%, rgba(138,35,135,1) 100%)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||||
|
<Button
|
||||||
|
disableRipple
|
||||||
|
variant="contained"
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={() => handleClaim(q.key)}
|
||||||
|
sx={{
|
||||||
|
borderRadius: '2.5vw',
|
||||||
|
fontFamily: 'Benzin-Bold',
|
||||||
|
background:
|
||||||
|
canClaim
|
||||||
|
? 'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)'
|
||||||
|
: 'rgba(255,255,255,0.10)',
|
||||||
|
color: '#fff',
|
||||||
|
'&:hover': {
|
||||||
|
transform: canClaim ? 'scale(1.01)' : 'none',
|
||||||
|
},
|
||||||
|
transition: 'transform 0.2s ease',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{q.status === 'claimed'
|
||||||
|
? 'Получено'
|
||||||
|
: q.status === 'completed'
|
||||||
|
? actionLoadingKey === q.key
|
||||||
|
? 'Получаем...'
|
||||||
|
: 'Забрать'
|
||||||
|
: 'В процессе'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -66,6 +66,10 @@ export default function Profile() {
|
|||||||
navigate('/daily');
|
navigate('/daily');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const navigateDailyQuests = () => {
|
||||||
|
navigate('/dailyquests');
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const savedConfig = localStorage.getItem('launcher_config');
|
const savedConfig = localStorage.getItem('launcher_config');
|
||||||
if (savedConfig) {
|
if (savedConfig) {
|
||||||
@ -513,6 +517,22 @@ export default function Profile() {
|
|||||||
>
|
>
|
||||||
Ежедневные награды
|
Ежедневные награды
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
fullWidth
|
||||||
|
onClick={navigateDailyQuests}
|
||||||
|
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)' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Ежедневные квесты
|
||||||
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -26,7 +26,7 @@ import { getPlayerServer } from '../utils/playerOnlineCheck';
|
|||||||
import CaseRoulette from '../components/CaseRoulette';
|
import CaseRoulette from '../components/CaseRoulette';
|
||||||
import BonusShopItem from '../components/BonusShopItem';
|
import BonusShopItem from '../components/BonusShopItem';
|
||||||
import ShopItem from '../components/ShopItem';
|
import ShopItem from '../components/ShopItem';
|
||||||
import { playBuySound } from '../utils/sounds';
|
import { playBuySound, primeSounds } from '../utils/sounds';
|
||||||
|
|
||||||
function getRarityByWeight(
|
function getRarityByWeight(
|
||||||
weight?: number,
|
weight?: number,
|
||||||
@ -376,6 +376,7 @@ export default function Shop() {
|
|||||||
setRouletteCaseItems(caseItems);
|
setRouletteCaseItems(caseItems);
|
||||||
setRouletteReward(result.reward);
|
setRouletteReward(result.reward);
|
||||||
setRouletteOpen(true);
|
setRouletteOpen(true);
|
||||||
|
playBuySound();
|
||||||
|
|
||||||
// 4. уведомление
|
// 4. уведомление
|
||||||
setNotification({
|
setNotification({
|
||||||
@ -405,6 +406,13 @@ export default function Shop() {
|
|||||||
setRouletteOpen(false);
|
setRouletteOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onFirstUserGesture = () => primeSounds();
|
||||||
|
|
||||||
|
window.addEventListener('pointerdown', onFirstUserGesture, { once: true });
|
||||||
|
return () => window.removeEventListener('pointerdown', onFirstUserGesture);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
|||||||
@ -1,13 +1,52 @@
|
|||||||
import buySound from '../assets/sounds/buy.mp3';
|
import buySound from '../assets/sounds/buy.mp3';
|
||||||
|
|
||||||
let buyAudio: HTMLAudioElement | null = null;
|
let buyAudio: HTMLAudioElement | null = null;
|
||||||
|
let unlocked = false;
|
||||||
|
|
||||||
|
export const primeSounds = () => {
|
||||||
|
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<void>).then === 'function') {
|
||||||
|
(p as Promise<void>)
|
||||||
|
.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 = () => {
|
export const playBuySound = () => {
|
||||||
if (!buyAudio) {
|
try {
|
||||||
buyAudio = new Audio(buySound);
|
if (!buyAudio) {
|
||||||
buyAudio.volume = 0.6;
|
buyAudio = new Audio(buySound);
|
||||||
}
|
buyAudio.volume = 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
buyAudio.currentTime = 0;
|
buyAudio.currentTime = 0;
|
||||||
buyAudio.play().catch(() => {});
|
buyAudio.play().catch(() => {});
|
||||||
|
} catch {
|
||||||
|
// игнор
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user