minor fix marketplace + xyi ego znaet

This commit is contained in:
aurinex
2025-12-13 18:59:30 +05:00
parent 74a3e3c7cf
commit d9a3a1cd1f
16 changed files with 459 additions and 226 deletions

View File

@ -133,7 +133,7 @@ const AppLayout = () => {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: location.pathname === '/profile'
justifyContent: location.pathname === '/profile' || location.pathname.startsWith('/launch')
? 'center'
: 'flex-start',
overflowX: 'hidden',

48
src/renderer/assets.d.ts vendored Normal file
View File

@ -0,0 +1,48 @@
type Styles = Record<string, string>;
declare module '*.svg' {
import React = require('react');
export const ReactComponent: React.FC<React.SVGProps<SVGSVGElement>>;
const content: string;
export default content;
}
declare module '*.png' {
const content: string;
export default content;
}
declare module '*.jpg' {
const content: string;
export default content;
}
declare module '*.scss' {
const content: Styles;
export default content;
}
declare module '*.sass' {
const content: Styles;
export default content;
}
declare module '*.css' {
const content: Styles;
export default content;
}
declare module '*.mp3' {
const src: string;
export default src;
}
declare module '*.wav' {
const src: string;
export default src;
}
declare module '*.ogg' {
const src: string;
export default src;
}

Binary file not shown.

Binary file not shown.

View File

@ -53,28 +53,28 @@ export default function CoinsDisplay({
switch (size) {
case 'small':
return {
containerPadding: '4px 8px',
iconSize: '16px',
fontSize: '12px',
borderRadius: '12px',
gap: '6px',
containerPadding: '0.4vw 0.8vw',
iconSize: '1.4vw',
fontSize: '1vw',
borderRadius: '2vw',
gap: '0.6vw',
};
case 'large':
return {
containerPadding: '8px 16px',
iconSize: '28px',
fontSize: '18px',
borderRadius: '20px',
gap: '10px',
containerPadding: '0.4vw 1.2vw',
iconSize: '2.2vw',
fontSize: '1.6vw',
borderRadius: '1.8vw',
gap: '0.8vw',
};
case 'medium':
default:
return {
containerPadding: '6px 12px',
iconSize: '24px',
fontSize: '16px',
borderRadius: '16px',
gap: '8px',
containerPadding: '0.4vw 1vw',
iconSize: '2vw',
fontSize: '1.4vw',
borderRadius: '1.6vw',
gap: '0.6vw',
};
}
};

View File

@ -3,8 +3,7 @@ import { Box, Button, TextField, Typography, InputAdornment, IconButton } from '
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
import VisibilityIcon from '@mui/icons-material/Visibility';
import GradientTextField from '../GradientTextField';
import GradientVisibilityToggleIcon from '../Icons/GradientVisibilityToggleIcon'
import GradientVisibilityIcon from '../Icons/GradientVisibilityIcon'
import GradientVisibilityToggleIcon from '../../assets/Icons/GradientVisibilityToggleIcon'
interface AuthFormProps {
config: {

View File

@ -1,25 +1,187 @@
import { Slider } from '@mui/material';
import React from 'react';
import { Box, Slider, Typography } from '@mui/material';
interface MemorySliderProps {
memory: number;
onChange: (e: Event, value: number | number[]) => void;
min?: number;
max?: number;
step?: number;
}
const MemorySlider = ({ memory, onChange }: MemorySliderProps) => {
const gradientPrimary =
'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)';
const formatMb = (v: number) => `${v} MB`;
const formatGb = (v: number) => `${(v / 1024).toFixed(v % 1024 === 0 ? 0 : 1)} GB`;
const MemorySlider = ({
memory,
onChange,
min = 1024,
max = 32768,
step = 1024,
}: MemorySliderProps) => {
// marks только на “красивых” значениях, чтобы не было каши
const marks = [
{ value: 1024, label: '1 GB' },
{ value: 4096, label: '4 GB' },
{ value: 8192, label: '8 GB' },
{ value: 16384, label: '16 GB' },
{ value: 32768, label: '32 GB' },
].filter((m) => m.value >= min && m.value <= max);
return (
<Slider
name="memory"
aria-label="Memory"
defaultValue={4096}
valueLabelDisplay="auto"
shiftStep={1024}
step={1024}
marks
min={1024}
max={32628}
value={memory}
onChange={onChange}
/>
<Box sx={{ width: '100%' }}>
{/* Header */}
<Box
sx={{
display: 'flex',
alignItems: 'baseline',
justifyContent: 'space-between',
mb: '1.2vh',
}}
>
<Typography
sx={{
fontFamily: 'Benzin-Bold, system-ui, -apple-system, Segoe UI, Roboto, Arial',
fontWeight: 800,
fontSize: '1.1vw',
color: '#fff',
textTransform: 'uppercase',
letterSpacing: 0.5,
}}
>
Память
</Typography>
<Typography
sx={{
fontFamily: 'Benzin-Bold, system-ui, -apple-system, Segoe UI, Roboto, Arial',
fontWeight: 800,
fontSize: '1.1vw',
backgroundImage: gradientPrimary,
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
}}
>
{memory >= 1024 ? formatGb(memory) : formatMb(memory)}
</Typography>
</Box>
<Slider
name="memory"
aria-label="Memory"
valueLabelDisplay="auto"
valueLabelFormat={(v) => (v >= 1024 ? formatGb(v as number) : formatMb(v as number))}
shiftStep={step}
step={step}
marks={marks}
min={min}
max={max}
value={memory}
onChange={onChange}
sx={{
px: '0.2vw',
// rail (фон полосы)
'& .MuiSlider-rail': {
opacity: 1,
height: '0.9vh',
borderRadius: '999vw',
backgroundColor: 'rgba(255,255,255,0.10)',
boxShadow: 'inset 0 0.25vh 0.6vh rgba(0,0,0,0.45)',
},
// track (заполненная часть)
'& .MuiSlider-track': {
height: '0.9vh',
borderRadius: '999vw',
border: 'none',
background: gradientPrimary,
boxShadow: '0 0.6vh 1.6vh rgba(233,64,205,0.18)',
},
// thumb (ползунок)
'& .MuiSlider-thumb': {
width: '1.6vw',
height: '1.6vw',
minWidth: 14,
minHeight: 14,
borderRadius: '999vw',
background: 'rgba(10,10,20,0.92)',
border: '0.22vw solid rgba(255,255,255,0.18)',
boxShadow:
'0 0.9vh 2.4vh rgba(0,0,0,0.55), 0 0 1.2vw rgba(242,113,33,0.20)',
transition: 'transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease',
'&:hover': {
// transform: 'scale(1.06)',
borderColor: 'rgba(242,113,33,0.55)',
boxShadow:
'0 1.1vh 2.8vh rgba(0,0,0,0.62), 0 0 1.6vw rgba(233,64,205,0.28)',
},
// внутренний “свет”
'&:before': {
content: '""',
position: 'absolute',
inset: '18%',
borderRadius: '999vw',
background: gradientPrimary,
opacity: 0.85,
filter: 'blur(0.3vw)',
},
},
// value label (плашка значения)
'& .MuiSlider-valueLabel': {
fontFamily: 'Benzin-Bold, system-ui, -apple-system, Segoe UI, Roboto, Arial',
fontSize: '0.85vw',
borderRadius: '1.2vw',
padding: '0.4vh 0.8vw',
color: '#fff',
background: 'rgba(0,0,0,0.55)',
border: '1px solid rgba(255,255,255,0.10)',
backdropFilter: 'blur(10px)',
boxShadow: '0 1.2vh 3vh rgba(0,0,0,0.55)',
'&:before': { display: 'none' },
},
// marks (точки)
'& .MuiSlider-mark': {
width: '0.35vw',
height: '0.35vw',
minWidth: 4,
minHeight: 4,
borderRadius: '999vw',
backgroundColor: 'rgba(255,255,255,0.18)',
},
'& .MuiSlider-markActive': {
backgroundColor: 'rgba(255,255,255,0.55)',
},
// mark labels (подписи)
'& .MuiSlider-markLabel': {
color: 'rgba(255,255,255,0.55)',
fontSize: '0.75vw',
marginTop: '1vh',
userSelect: 'none',
},
// focus outline
'& .MuiSlider-thumb.Mui-focusVisible': {
outline: 'none',
boxShadow:
'0 0 0 0.25vw rgba(242,113,33,0.20), 0 1.1vh 2.8vh rgba(0,0,0,0.62)',
},
}}
/>
{/* Subtext */}
<Typography sx={{ mt: '1.2vh', color: 'rgba(255,255,255,0.55)', fontSize: '0.85vw' }}>
Шаг: {formatGb(step)} Рекомендуем: 48 GB для большинства сборок
</Typography>
</Box>
);
};

View File

@ -10,7 +10,7 @@ interface SettingsModalProps {
memory: number;
preserveFiles: string[];
};
onConfigChange: (newConfig: {
onConfigChange: (updater: (prev: { memory: number; preserveFiles: string[] }) => {
memory: number;
preserveFiles: string[];
}) => void;
@ -58,7 +58,7 @@ const SettingsModal = ({
packName={packName}
initialSelected={config.preserveFiles}
onSelectionChange={(selected) => {
onConfigChange({ ...config, preserveFiles: selected });
onConfigChange((prev) => ({ ...prev, preserveFiles: selected }));
}}
/>
<Typography variant="body1" sx={{ color: 'white' }}>
@ -66,8 +66,9 @@ const SettingsModal = ({
</Typography>
<MemorySlider
memory={config.memory}
onChange={(e, value) => {
onConfigChange({ ...config, memory: value as number });
onChange={(_, value) => {
const next = Array.isArray(value) ? value[0] : value;
onConfigChange((prev) => ({ ...prev, memory: next }));
}}
/>
<Button

View File

@ -245,142 +245,159 @@ export default function DailyReward({ onClaimed }: Props) {
};
return (
<Box
sx={{
// px: { xs: 2, md: 4 },
// py: { xs: 2, md: 3 },
mt: '-3vh',
width: '85%',
overflowY: 'auto',
paddingTop: '5vh',
paddingBottom: '5vh',
}}
>
<Box
sx={{
width: '85vw',
height: '100%',
paddingBottom: '5vh',
}}
>
<Paper
elevation={0}
sx={{
borderRadius: 4,
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 18px 60px rgba(0,0,0,0.55)',
boxShadow: '0 1.2vw 3.8vw rgba(0,0,0,0.55)',
display: 'flex',
flexDirection: 'column',
maxHeight: '76vh', // подстрой под свой layout
}}
>
{/* alerts */}
<Box sx={{ px: { xs: 2, md: 3 }, pt: 2 }}>
{error && (
<Alert severity="error" sx={{ mb: 1.5 }}>
{error}
</Alert>
)}
{success && (
<Alert severity="success" sx={{ mb: 1.5 }}>
{success}
</Alert>
)}
</Box>
{/* Header */}
<Box
sx={{
px: { xs: 2, md: 3 },
pb: 2,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 2,
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={{
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" />
</Typography>
<Typography
sx={{
fontFamily:
'Benzin-Bold, system-ui, -apple-system, Segoe UI, Roboto, Arial',
fontWeight: 800,
fontSize: '1rem',
color: '#fff',
lineHeight: 1.15,
textTransform: 'uppercase',
}}
>
Серия дней: <b>{streak}</b>
</Typography>
{/* alerts */}
<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>
<Stack direction="row" alignItems="center" spacing={1}>
<CustomTooltip title="К текущему месяцу">
{/* Header */}
<Box
sx={{
px: '2vw',
pb: '2vw',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: '2vw',
}}
>
<Box
sx={{
minWidth: 220,
display: 'flex',
gap: '1vw',
alignItems: 'center',
}}
>
<Typography
sx={{ color: 'rgba(255,255,255,0.75)', display: 'flex', gap: '0.7vw' }}
>
<CoinsDisplay value={todaysReward} size="small" />
</Typography>
<Typography
sx={{
fontFamily:
'Benzin-Bold, system-ui, -apple-system, Segoe UI, Roboto, Arial',
fontWeight: 800,
fontSize: '2vw',
color: '#fff',
lineHeight: 1.15,
textTransform: 'uppercase',
}}
>
Серия дней: <b>{streak}</b>
</Typography>
</Box>
<Stack direction="row" alignItems="center" spacing={1}>
<CustomTooltip title="К текущему месяцу">
<IconButton
onClick={goToday}
sx={{
color: 'rgba(255,255,255,0.9)',
bgcolor: 'rgba(0,0,0,0.22)',
'&:hover': { bgcolor: 'rgba(255,255,255,0.08)' },
}}
>
<TodayRoundedIcon sx={{ fontSize: '2vw', p: '0.2vw' }} />
</IconButton>
</CustomTooltip>
<IconButton
onClick={goToday}
onClick={goPrev}
sx={{
color: 'rgba(255,255,255,0.9)',
bgcolor: 'rgba(0,0,0,0.22)',
'&:hover': { bgcolor: 'rgba(255,255,255,0.08)' },
}}
>
<TodayRoundedIcon sx={{ fontSize: '2vw', p: '0.2vw' }} />
<ChevronLeftRoundedIcon sx={{ fontSize: '2vw', p: '0.2vw' }} />
</IconButton>
</CustomTooltip>
<IconButton
onClick={goPrev}
sx={{
color: 'rgba(255,255,255,0.9)',
bgcolor: 'rgba(0,0,0,0.22)',
'&:hover': { bgcolor: 'rgba(255,255,255,0.08)' },
}}
>
<ChevronLeftRoundedIcon />
</IconButton>
<Box sx={{ minWidth: 160, textAlign: 'center', maxWidth: '15vw' }}>
<Typography
sx={{
color: '#fff',
fontWeight: 800,
letterSpacing: 0.2,
fontSize: '1.5vw',
}}
>
{RU_MONTHS[viewMonth]} {viewYear}
</Typography>
</Box>
<Box sx={{ minWidth: 160, textAlign: 'center', maxWidth: '10vw' }}>
<Typography
<IconButton
onClick={goNext}
sx={{
color: '#fff',
fontWeight: 800,
letterSpacing: 0.2,
fontSize: { xs: 14.5, md: 16 },
color: 'rgba(255,255,255,0.9)',
bgcolor: 'rgba(0,0,0,0.22)',
'&:hover': { bgcolor: 'rgba(255,255,255,0.08)' },
}}
>
{RU_MONTHS[viewMonth]} {viewYear}
</Typography>
</Box>
<ChevronRightRoundedIcon sx={{ fontSize: '2vw', p: '0.2vw' }} />
</IconButton>
</Stack>
</Box>
<IconButton
onClick={goNext}
sx={{
color: 'rgba(255,255,255,0.9)',
bgcolor: 'rgba(0,0,0,0.22)',
'&:hover': { bgcolor: 'rgba(255,255,255,0.08)' },
}}
>
<ChevronRightRoundedIcon />
</IconButton>
</Stack>
<Divider sx={{ borderColor: 'rgba(255,255,255,0.10)' }} />
</Box>
<Divider sx={{ borderColor: 'rgba(255,255,255,0.10)' }} />
{/* Calendar */}
<Box sx={{ px: { xs: 2, md: 3 }, py: 2.5 }}>
<Box
sx={{
px: '2vw',
py: '2vh',
overflowY: 'auto',
flex: 1, // занимает всё оставшееся место под шапкой
}}
>
<Box
sx={{
display: 'grid',
gridTemplateColumns: 'repeat(7, 1fr)',
gap: 1,
mb: 1.2,
gap: '0.7vw',
mb: '1.2vh',
}}
>
{RU_WEEKDAYS.map((w, i) => (
@ -388,7 +405,7 @@ export default function DailyReward({ onClaimed }: Props) {
key={w}
sx={{
textAlign: 'center',
fontSize: 12,
fontSize: 'clamp(10px, 1.1vw, 14px)',
fontWeight: 700,
color:
i >= 5 ? 'rgba(255,255,255,0.75)' : 'rgba(255,255,255,0.6)',
@ -404,7 +421,7 @@ export default function DailyReward({ onClaimed }: Props) {
sx={{
display: 'grid',
gridTemplateColumns: 'repeat(7, 1fr)',
gap: 1,
gap: '0.7vw',
}}
>
{grid.map(({ date, inCurrentMonth }) => {
@ -422,7 +439,7 @@ export default function DailyReward({ onClaimed }: Props) {
sx={{
width: '100%',
aspectRatio: '1 / 1',
borderRadius: 3,
borderRadius: '1vw',
position: 'relative',
overflow: 'hidden',
border: isSelected
@ -471,7 +488,7 @@ export default function DailyReward({ onClaimed }: Props) {
>
<Typography
sx={{
fontSize: 14,
fontSize: '1.3vw',
fontWeight: 800,
color: inCurrentMonth
? '#fff'
@ -484,7 +501,7 @@ export default function DailyReward({ onClaimed }: Props) {
<Typography
sx={{
fontSize: 10.5,
fontSize: '1vw',
color: claimed
? 'rgba(156, 255, 198, 0.9)'
: isToday
@ -501,12 +518,12 @@ export default function DailyReward({ onClaimed }: Props) {
<Box
sx={{
position: 'absolute',
bottom: 8,
width: 6,
height: 6,
borderRadius: 999,
bottom: '1vh',
width: '0.45vw',
height: '0.45vw',
borderRadius: '999vw',
bgcolor: 'rgba(156, 255, 198, 0.95)',
boxShadow: '0 0 12px rgba(156, 255, 198, 0.35)',
boxShadow: '0 0 1vw rgba(156, 255, 198, 0.35)',
}}
/>
)}
@ -519,17 +536,17 @@ export default function DailyReward({ onClaimed }: Props) {
{/* Footer actions */}
<Box
sx={{
mt: 2.2,
mt: '2vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 2,
gap: '1vw',
flexWrap: 'wrap',
}}
>
<Box>
<Typography
sx={{ color: 'rgba(255,255,255,0.65)', fontSize: 12 }}
sx={{ color: 'rgba(255,255,255,0.65)', fontSize: '1.2vw' }}
>
Выбрано:{' '}
<span style={{ color: '#fff', fontWeight: 800 }}>
@ -552,8 +569,8 @@ export default function DailyReward({ onClaimed }: Props) {
disabled={loading || !status?.ok || !canClaim}
onClick={handleClaim}
sx={{
px: 3,
py: 1.2,
px: '2.4vw',
py: '1vh',
borderRadius: '2vw',
textTransform: 'uppercase',
fontFamily:

View File

@ -2,8 +2,6 @@ import {
Box,
Typography,
Button,
Snackbar,
Alert,
LinearProgress,
} from '@mui/material';
import { useEffect, useState } from 'react';
@ -13,6 +11,8 @@ import PopaPopa from '../components/popa-popa';
import SettingsIcon from '@mui/icons-material/Settings';
import React from 'react';
import SettingsModal from '../components/Settings/SettingsModal';
import CustomNotification from '../components/Notifications/CustomNotification';
import type { NotificationPosition } from '../components/Notifications/CustomNotification';
declare global {
interface Window {
@ -62,11 +62,16 @@ const LaunchPage = ({
const [progress, setProgress] = useState(0);
const [buffer, setBuffer] = useState(10);
const [installStatus, setInstallStatus] = useState('');
const [notification, setNotification] = useState<{
open: boolean;
message: string;
severity: 'success' | 'error' | 'info';
}>({ open: false, message: '', severity: 'info' });
const [notifOpen, setNotifOpen] = useState(false);
const [notifMsg, setNotifMsg] = useState<React.ReactNode>('');
const [notifSeverity, setNotifSeverity] = useState<
'success' | 'info' | 'warning' | 'error'
>('info');
const [notifPos, setNotifPos] = useState<NotificationPosition>({
vertical: 'bottom',
horizontal: 'center',
});
const [installStep, setInstallStep] = useState('');
const [installMessage, setInstallMessage] = useState('');
const [open, setOpen] = React.useState(false);
@ -223,14 +228,14 @@ const LaunchPage = ({
}, [versionId]);
const showNotification = (
message: string,
severity: 'success' | 'error' | 'info',
message: React.ReactNode,
severity: 'success' | 'info' | 'warning' | 'error' = 'info',
position: NotificationPosition = { vertical: 'bottom', horizontal: 'center' },
) => {
setNotification({ open: true, message, severity });
};
const handleCloseNotification = () => {
setNotification({ ...notification, open: false });
setNotifMsg(message);
setNotifSeverity(severity);
setNotifPos(position);
setNotifOpen(true);
};
// Функция для запуска игры с настройками выбранной версии
@ -522,19 +527,14 @@ const LaunchPage = ({
</Box>
)}
<Snackbar
open={notification.open}
autoHideDuration={6000}
onClose={handleCloseNotification}
>
<Alert
onClose={handleCloseNotification}
severity={notification.severity}
sx={{ width: '100%' }}
>
{notification.message}
</Alert>
</Snackbar>
<CustomNotification
open={notifOpen}
message={notifMsg}
severity={notifSeverity}
position={notifPos}
onClose={() => setNotifOpen(false)}
autoHideDuration={2500}
/>
<SettingsModal
open={open}

View File

@ -11,13 +11,15 @@ import {
Pagination,
Tabs,
Tab,
Alert,
Snackbar,
} from '@mui/material';
import { isPlayerOnline, getPlayerServer } from '../utils/playerOnlineCheck';
import { buyItem, fetchMarketplace, MarketplaceResponse, Server } from '../api';
import PlayerInventory from '../components/PlayerInventory';
import { FullScreenLoader } from '../components/FullScreenLoader';
import CustomNotification from '../components/Notifications/CustomNotification';
import type { NotificationPosition } from '../components/Notifications/CustomNotification';
import * as React from 'react';
import { playBuySound } from '../utils/sounds';
interface TabPanelProps {
children?: React.ReactNode;
@ -53,14 +55,15 @@ export default function Marketplace() {
const [page, setPage] = useState<number>(1);
const [totalPages, setTotalPages] = useState<number>(1);
const [tabValue, setTabValue] = useState<number>(0);
const [notification, setNotification] = useState<{
open: boolean;
message: string;
type: 'success' | 'error';
}>({
open: false,
message: '',
type: 'success',
const [notifOpen, setNotifOpen] = useState(false);
const [notifMsg, setNotifMsg] = useState<React.ReactNode>('');
const [notifSeverity, setNotifSeverity] = useState<
'success' | 'info' | 'warning' | 'error'
>('info');
const [notifPos, setNotifPos] = useState<NotificationPosition>({
vertical: 'bottom',
horizontal: 'center',
});
const translateServer = (server: Server) => {
@ -137,13 +140,14 @@ export default function Marketplace() {
if (username) {
const result = await buyItem(username, itemId);
setNotification({
open: true,
message:
result.message ||
playBuySound();
showNotification(
result.message ||
'Предмет успешно куплен! Он будет добавлен в ваш инвентарь.',
type: 'success',
});
'success',
{ vertical: 'bottom', horizontal: 'left' },
);
// Обновляем список предметов
if (playerServer) {
@ -152,20 +156,23 @@ export default function Marketplace() {
}
} catch (error) {
console.error('Ошибка при покупке предмета:', error);
setNotification({
open: true,
message:
error instanceof Error
? error.message
: 'Ошибка при покупке предмета',
type: 'error',
});
showNotification(
error instanceof Error ? error.message : 'Ошибка при покупке предмета',
'error',
{ vertical: 'bottom', horizontal: 'left' },
);
}
};
// Закрытие уведомления
const handleCloseNotification = () => {
setNotification({ ...notification, open: false });
const showNotification = (
message: React.ReactNode,
severity: 'success' | 'info' | 'warning' | 'error' = 'info',
position: NotificationPosition = { vertical: 'bottom', horizontal: 'center' },
) => {
setNotifMsg(message);
setNotifSeverity(severity);
setNotifPos(position);
setNotifOpen(true);
};
// Получаем имя пользователя из localStorage при монтировании компонента
@ -199,6 +206,14 @@ export default function Marketplace() {
gap: 2,
}}
>
<CustomNotification
open={notifOpen}
message={notifMsg}
severity={notifSeverity}
position={notifPos}
onClose={() => setNotifOpen(false)}
autoHideDuration={2500}
/>
<FullScreenLoader
fullScreen={true}
message="Проверяем, находитесь ли вы на сервере..."
@ -466,10 +481,9 @@ export default function Marketplace() {
}
// Показываем уведомление
setNotification({
open: true,
message: 'Предмет успешно выставлен на продажу!',
type: 'success',
showNotification('Предмет успешно выставлен на продажу!', 'success', {
vertical: 'bottom',
horizontal: 'left',
});
}}
/>
@ -483,21 +497,14 @@ export default function Marketplace() {
</Typography>
)}
</TabPanel>
{/* Уведомления */}
<Snackbar
open={notification.open}
autoHideDuration={6000}
onClose={handleCloseNotification}
>
<Alert
onClose={handleCloseNotification}
severity={notification.type}
sx={{ width: '100%' }}
>
{notification.message}
</Alert>
</Snackbar>
<CustomNotification
open={notifOpen}
message={notifMsg}
severity={notifSeverity}
position={notifPos}
onClose={() => setNotifOpen(false)}
autoHideDuration={2500}
/>
</Box>
);
}

View File

@ -26,6 +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';
function getRarityByWeight(
weight?: number,
@ -157,6 +158,9 @@ export default function Shop() {
try {
await purchaseCape(username, cape_id);
await loadUserCapes(username);
playBuySound();
setNotification({
open: true,
message: 'Плащ успешно куплен!',
@ -260,6 +264,9 @@ export default function Shop() {
await withProcessing(bonusTypeId, async () => {
try {
const res = await purchaseBonus(username, bonusTypeId);
playBuySound();
setNotification({
open: true,
message: res.message || 'Прокачка успешно куплена!',

View File

@ -0,0 +1,13 @@
import buySound from '../assets/sounds/buy.mp3';
let buyAudio: HTMLAudioElement | null = null;
export const playBuySound = () => {
if (!buyAudio) {
buyAudio = new Audio(buySound);
buyAudio.volume = 0.6;
}
buyAudio.currentTime = 0;
buyAudio.play().catch(() => {});
};