Files
popa-launcher/src/renderer/components/CaseItemsDialog.tsx
2025-12-17 13:16:59 +05:00

261 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}