круто
This commit is contained in:
260
src/renderer/components/CaseItemsDialog.tsx
Normal file
260
src/renderer/components/CaseItemsDialog.tsx
Normal file
@ -0,0 +1,260 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
Box,
|
||||
Typography,
|
||||
Grid,
|
||||
Paper,
|
||||
CircularProgress,
|
||||
} from '@mui/material';
|
||||
import type { Case, CaseItem } from '../api';
|
||||
import { fetchCase } from '../api';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
|
||||
const CARD_BG =
|
||||
'radial-gradient(circle at 10% 10%, rgba(242,113,33,0.14), transparent 55%), radial-gradient(circle at 90% 20%, rgba(233,64,205,0.12), transparent 55%), rgba(10,10,20,0.86)';
|
||||
|
||||
const CardFacePaperSx = {
|
||||
borderRadius: '1.2vw',
|
||||
background: CARD_BG,
|
||||
border: '1px solid rgba(255,255,255,0.08)',
|
||||
boxShadow: '0 1.2vw 3.2vw rgba(0,0,0,0.55)',
|
||||
color: 'white',
|
||||
} as const;
|
||||
|
||||
const GLASS_PAPER_SX = {
|
||||
borderRadius: '1.2vw',
|
||||
overflow: 'hidden',
|
||||
background:
|
||||
'radial-gradient(circle at 10% 10%, rgba(242,113,33,0.14), transparent 55%), radial-gradient(circle at 90% 20%, rgba(233,64,205,0.12), transparent 55%), rgba(10,10,20,0.86)',
|
||||
border: '1px solid rgba(255,255,255,0.08)',
|
||||
boxShadow: '0 1.2vw 3.2vw rgba(0,0,0,0.55)',
|
||||
color: 'white',
|
||||
backdropFilter: 'blur(16px)',
|
||||
} as const;
|
||||
|
||||
function stripMinecraftColors(text?: string | null): string {
|
||||
if (!text) return '';
|
||||
return text.replace(/§[0-9A-FK-ORa-fk-or]/g, '');
|
||||
}
|
||||
|
||||
function getChancePercent(itemWeight: number, total: number) {
|
||||
if (!total || total <= 0) return 0;
|
||||
return (itemWeight / total) * 100;
|
||||
}
|
||||
|
||||
function getRarityByWeight(weight?: number): 'common' | 'rare' | 'epic' | 'legendary' {
|
||||
if (weight === undefined || weight === null) return 'common';
|
||||
if (weight <= 5) return 'legendary';
|
||||
if (weight <= 20) return 'epic';
|
||||
if (weight <= 50) return 'rare';
|
||||
return 'common';
|
||||
}
|
||||
|
||||
function getRarityColor(weight?: number): string {
|
||||
const rarity = getRarityByWeight(weight);
|
||||
switch (rarity) {
|
||||
case 'legendary':
|
||||
return 'rgba(255, 215, 0, 1)'; // gold
|
||||
case 'epic':
|
||||
return 'rgba(186, 85, 211, 1)'; // purple
|
||||
case 'rare':
|
||||
return 'rgba(65, 105, 225, 1)'; // blue
|
||||
default:
|
||||
return 'rgba(255, 255, 255, 0.75)';
|
||||
}
|
||||
}
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
caseId: string;
|
||||
caseName?: string;
|
||||
};
|
||||
|
||||
export default function CaseItemsDialog({ open, onClose, caseId, caseName }: Props) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [caseData, setCaseData] = useState<Case | null>(null);
|
||||
const items: CaseItem[] = useMemo(() => {
|
||||
const list = caseData?.items ?? [];
|
||||
return [...list].sort((a, b) => {
|
||||
const wa = a.weight ?? Infinity;
|
||||
const wb = b.weight ?? Infinity;
|
||||
return wa - wb; // 🔥 по возрастанию weight (легендарки сверху)
|
||||
});
|
||||
}, [caseData]);
|
||||
|
||||
const totalWeight = useMemo(() => {
|
||||
return items.reduce((sum, it) => sum + (Number(it.weight) || 0), 0);
|
||||
}, [items]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const full = await fetchCase(caseId);
|
||||
if (!cancelled) setCaseData(full);
|
||||
} catch (e) {
|
||||
console.error('Ошибка при загрузке предметов кейса:', e);
|
||||
if (!cancelled) setCaseData(null);
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [open, caseId]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
fullWidth
|
||||
maxWidth="md"
|
||||
PaperProps={{
|
||||
sx: GLASS_PAPER_SX,
|
||||
}}
|
||||
>
|
||||
<DialogTitle
|
||||
sx={{
|
||||
fontFamily: 'Benzin-Bold',
|
||||
pr: 6, // место под крестик
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
Предметы кейса{caseName ? ` — ${caseName}` : ''}
|
||||
|
||||
<IconButton
|
||||
onClick={onClose}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 10,
|
||||
right: 10,
|
||||
color: 'rgba(255,255,255,0.9)',
|
||||
border: '1px solid rgba(255,255,255,0.12)',
|
||||
background: 'rgba(255,255,255,0.06)',
|
||||
backdropFilter: 'blur(12px)',
|
||||
'&:hover': { transform: 'scale(1.05)', background: 'rgba(255,255,255,0.10)' },
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent dividers sx={{ borderColor: 'rgba(255,255,255,0.10)' }}>
|
||||
{loading ? (
|
||||
<Box sx={{ py: 6, display: 'flex', justifyContent: 'center' }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : !items.length ? (
|
||||
<Typography sx={{ color: 'rgba(255,255,255,0.75)' }}>
|
||||
Предметы не найдены (или кейс временно недоступен).
|
||||
</Typography>
|
||||
) : (
|
||||
<>
|
||||
|
||||
<Grid container spacing={2}>
|
||||
{items.map((it) => {
|
||||
const w = Number(it.weight) || 0;
|
||||
const chance = getChancePercent(w, totalWeight);
|
||||
const displayNameRaw = it.meta?.display_name ?? it.name ?? it.material ?? 'Предмет';
|
||||
const displayName = stripMinecraftColors(displayNameRaw);
|
||||
|
||||
const texture = it.material
|
||||
? `https://cdn.minecraft.popa-popa.ru/textures/${it.material.toLowerCase()}.png`
|
||||
: '';
|
||||
|
||||
return (
|
||||
<Grid item xs={3} key={it.id}>
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
...CardFacePaperSx,
|
||||
width: '12vw',
|
||||
height: '12vw',
|
||||
minWidth: 110,
|
||||
minHeight: 110,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{/* верхняя плашка (редкость) */}
|
||||
<Box
|
||||
sx={{
|
||||
px: 1,
|
||||
py: 0.7,
|
||||
borderBottom: '1px solid rgba(255,255,255,0.08)',
|
||||
color: getRarityColor(it.weight),
|
||||
fontFamily: 'Benzin-Bold',
|
||||
fontSize: '0.75rem',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
title={displayName}
|
||||
>
|
||||
{displayName}
|
||||
</Box>
|
||||
|
||||
{/* иконка */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', flexGrow: 1 }}>
|
||||
{texture ? (
|
||||
<Box
|
||||
component="img"
|
||||
src={texture}
|
||||
alt={displayName}
|
||||
draggable={false}
|
||||
sx={{
|
||||
width: '5vw',
|
||||
height: '5vw',
|
||||
minWidth: 40,
|
||||
minHeight: 40,
|
||||
objectFit: 'contain',
|
||||
imageRendering: 'pixelated',
|
||||
userSelect: 'none',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Typography sx={{ color: 'rgba(255,255,255,0.6)' }}>?</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* низ: шанс/вес/кол-во */}
|
||||
<Box
|
||||
sx={{
|
||||
px: 1,
|
||||
py: 0.9,
|
||||
borderTop: '1px solid rgba(255,255,255,0.08)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ fontSize: '0.75rem', fontFamily: 'Benzin-Bold' }}>
|
||||
{chance.toFixed(2)}%
|
||||
</Typography>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@ -13,6 +13,8 @@ import CoinsDisplay from './CoinsDisplay';
|
||||
import { CapePreview } from './CapePreview';
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
import CapePreviewModal from './PlayerPreviewModal';
|
||||
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||
import CaseItemsDialog from './CaseItemsDialog';
|
||||
|
||||
export type ShopItemType = 'case' | 'cape';
|
||||
|
||||
@ -32,6 +34,7 @@ export interface ShopItemProps {
|
||||
|
||||
export default function ShopItem({
|
||||
type,
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
imageUrl,
|
||||
@ -46,6 +49,7 @@ export default function ShopItem({
|
||||
type === 'case' ? (isOpening ? 'Открываем...' : 'Открыть кейс') : 'Купить';
|
||||
|
||||
const [previewOpen, setPreviewOpen] = useState(false);
|
||||
const [caseInfoOpen, setCaseInfoOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Card
|
||||
@ -193,15 +197,35 @@ export default function ShopItem({
|
||||
)}
|
||||
|
||||
{type === 'case' && typeof itemsCount === 'number' && (
|
||||
<Typography
|
||||
sx={{
|
||||
color: 'rgba(255,255,255,0.6)',
|
||||
fontSize: '0.75rem',
|
||||
mt: 1,
|
||||
}}
|
||||
>
|
||||
Предметов в кейсе: {itemsCount}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mt: 1 }}>
|
||||
<Typography
|
||||
sx={{
|
||||
color: 'rgba(255,255,255,0.6)',
|
||||
fontSize: '0.75rem',
|
||||
}}
|
||||
>
|
||||
Предметов в кейсе: {itemsCount}
|
||||
</Typography>
|
||||
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation(); // важно: чтобы не сработало onClick карточки/кнопки открытия
|
||||
setCaseInfoOpen(true);
|
||||
}}
|
||||
sx={{
|
||||
ml: 1,
|
||||
color: 'rgba(255,255,255,0.85)',
|
||||
border: '1px solid rgba(255,255,255,0.12)',
|
||||
background: 'rgba(255,255,255,0.04)',
|
||||
'&:hover': { transform: 'scale(1.05)' },
|
||||
transition: 'all 0.25s ease',
|
||||
}}
|
||||
>
|
||||
<InfoOutlinedIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@ -236,6 +260,14 @@ export default function ShopItem({
|
||||
skinUrl={playerSkinUrl}
|
||||
/>
|
||||
)}
|
||||
{type === 'case' && (
|
||||
<CaseItemsDialog
|
||||
open={caseInfoOpen}
|
||||
onClose={() => setCaseInfoOpen(false)}
|
||||
caseId={id}
|
||||
caseName={name}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@ -207,6 +207,8 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem('launcher_config');
|
||||
localStorage.removeItem(`coins:${username}`);
|
||||
localStorage.removeItem('last_route');
|
||||
navigate('/login');
|
||||
window.electron.ipcRenderer.invoke('auth-changed', { isAuthed: false });
|
||||
};
|
||||
@ -574,6 +576,19 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
|
||||
<PersonIcon sx={{ fontSize: '2vw' }} /> Профиль
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
handleAvatarMenuClose();
|
||||
navigate('/inventory');
|
||||
}}
|
||||
sx={[
|
||||
{ fontSize: '1.5vw', gap: '0.5vw', py: '0.7vw' },
|
||||
theme.launcher.topbar.menuItem,
|
||||
]}
|
||||
>
|
||||
<CategoryIcon sx={{ fontSize: '2vw' }} /> Инвентарь
|
||||
</MenuItem>
|
||||
|
||||
{/* ===== 2 строка: ежедневные задания ===== */}
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
@ -602,19 +617,6 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
|
||||
<EmojiEventsIcon sx={{ fontSize: '2vw' }} /> Ежедневная награда
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
handleAvatarMenuClose();
|
||||
navigate('/inventory');
|
||||
}}
|
||||
sx={[
|
||||
{ fontSize: '1.5vw', gap: '0.5vw', py: '0.7vw' },
|
||||
theme.launcher.topbar.menuItem,
|
||||
]}
|
||||
>
|
||||
<CategoryIcon sx={{ fontSize: '2vw' }} /> Инвентарь
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
handleAvatarMenuClose();
|
||||
|
||||
Reference in New Issue
Block a user