2026-08-15 18:42:06 +03:00

249 lines
9.5 KiB
TypeScript

'use client';
import React, { useEffect } from 'react';
import {
Box,
Typography,
Grid,
Card,
CardContent,
CardActions,
Chip,
Button,
useTheme,
Link,
} from '@mui/material';
import { styled } from '@mui/material/styles';
import { IMod } from '@/src/types/Imod';
const ModCard = styled(Card)(({ theme }) => ({
height: '100%',
display: 'flex',
flexDirection: 'column',
background: theme.palette.mode === 'dark'
? 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)'
: 'linear-gradient(145deg, #ffffff 0%, #f5f5f5 100%)',
border: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
borderRadius: '16px',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
position: 'relative',
overflow: 'hidden',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '3px',
background: theme.palette.mode === 'dark'
? 'linear-gradient(90deg, #dee2e6 0%, #cccccc 100%)'
: 'linear-gradient(90deg, #000000 0%, #333333 100%)',
transform: 'scaleX(0)',
transition: 'transform 0.3s ease',
},
'&:hover': {
boxShadow: theme.palette.mode === 'dark' ? '0 20px 40px rgba(255, 255, 255, 0.1)' : '0 20px 40px rgba(0, 0, 0, 0.2)',
borderColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)',
'&::before': {
transform: 'scaleX(1)',
},
},
}));
const VersionLink = styled(Link)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
padding: theme.spacing(1.5, 2),
marginBottom: theme.spacing(1),
background: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.05)',
borderRadius: '10px',
textDecoration: 'none',
color: 'inherit',
transition: 'all 0.2s ease',
border: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
'&:hover': {
background: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)',
borderColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)',
},
}));
const ModTitle = styled(Typography)(({ theme }) => ({
fontWeight: 700,
mb: 1,
background: theme.palette.mode === 'dark'
? 'linear-gradient(135deg, #dee2e6 0%, #e0e0e0 100%)'
: 'none',
WebkitBackgroundClip: theme.palette.mode === 'dark' ? 'text' : 'initial',
WebkitTextFillColor: theme.palette.mode === 'dark' ? '#dee2e6' : 'initial',
color: theme.palette.mode === 'dark' ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)',
}));
const VersionsChip = styled(Chip)(({ theme }) => ({
backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.15)' : 'rgba(0, 0, 0, 0.1)',
color: theme.palette.mode === 'dark' ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)',
fontWeight: 600,
}));
const VersionText = styled(Typography)(({ theme }) => ({
fontWeight: 600,
color: theme.palette.text.secondary,
}));
const VersionArrow = styled(Typography)(({ theme }) => ({
color: theme.palette.text.secondary,
}));
const BetaVersionsLabel = styled(Typography)(({ theme }) => ({
color: 'rgba(255, 193, 7, 0.9)',
fontWeight: 600,
mb: 1,
}));
interface ModsPageClientProps {
initialMods: IMod[];
}
export default function ModsPageClient({ initialMods }: ModsPageClientProps) {
const theme = useTheme();
// Очистка кэша сайта при заходе на главную страницу (один раз за сессию)
useEffect(() => {
if (!sessionStorage.getItem('cache_cleared')) {
sessionStorage.setItem('cache_cleared', '1');
(async () => {
try {
if ('caches' in window) {
const names = await caches.keys();
await Promise.all(names.map(name => caches.delete(name)));
}
if ('serviceWorker' in navigator) {
const regs = await navigator.serviceWorker.getRegistrations();
await Promise.all(regs.map(r => r.unregister()));
}
} catch (e) {
console.error('Cache clear failed:', e);
}
})();
}
}, []);
// Sort all mods by order, then group by name
const sortedMods = [...initialMods].sort((a, b) => a.order - b.order);
const mapWithModVersions = new Map<string, IMod[]>();
sortedMods.forEach(mod => {
const versionList = mapWithModVersions.get(mod.name);
if (versionList == null) {
mapWithModVersions.set(mod.name, [mod]);
} else {
versionList.push(mod);
}
});
const getLatestVersion = (modName: string) => {
const sameMods = mapWithModVersions.get(modName) ?? [];
// The latest version is the one with the maximum id
const latest = sameMods.reduce((max, mod) => mod.id > max.id ? mod : max, sameMods[0]);
const betaMods = sameMods.filter(m => m.isBeta);
return {
latest,
allVersions: sameMods,
hasBeta: betaMods.length > 0,
betaVersion: betaMods[0],
};
};
function ModCardComponent(modName: string) {
const sameMods = mapWithModVersions.get(modName) ?? [];
const { latest, hasBeta, betaVersion } = getLatestVersion(modName);
return (
<ModCard>
<CardContent sx={{ flexGrow: 1, p: 3 }}>
<Box sx={{ mb: 2 }}>
<ModTitle variant="h5">
{modName}
</ModTitle>
</Box>
<Box sx={{ display: 'flex', gap: 1, mb: 2 }}>
<VersionsChip
label={`${sameMods.length} versions`}
size="small"
/>
</Box>
{sameMods.filter(m => !m.isBeta).map(mod => (
<VersionLink key={mod.id} href={"/mod/" + mod.id}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
<VersionText variant="body2">
Version {mod.version}
</VersionText>
<VersionArrow variant="caption">
</VersionArrow>
</Box>
</VersionLink>
))}
{hasBeta && (
<Box sx={{ mt: 2 }}>
<BetaVersionsLabel variant="subtitle2">
Beta versions:
</BetaVersionsLabel>
{betaVersion && (
<VersionLink href={"/mod/" + betaVersion.id}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
Version {betaVersion.version} (Beta)
</Typography>
<VersionArrow variant="caption">
</VersionArrow>
</Box>
</VersionLink>
)}
</Box>
)}
</CardContent>
<CardActions sx={{ px: 3, pb: 3 }}>
<Button
size="medium"
variant="outlined"
href={"/mod/" + latest?.id}
sx={{
width: '100%',
color: theme.palette.mode === 'dark' ? '#FFD700' : '#1976d2',
borderColor: theme.palette.mode === 'dark' ? 'rgba(255, 215, 0, 0.5)' : 'rgba(25, 118, 210, 0.5)',
fontWeight: 600,
borderRadius: '10px',
textTransform: 'none',
fontSize: '1rem',
py: 1.5,
'&:hover': {
borderColor: theme.palette.mode === 'dark' ? '#FFD700' : '#1976d2',
backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 215, 0, 0.1)' : 'rgba(25, 118, 210, 0.1)',
boxShadow: theme.palette.mode === 'dark' ? '0 8px 20px rgba(255, 215, 0, 0.2)' : '0 8px 20px rgba(25, 118, 210, 0.2)',
},
}}
>
Open latest {latest?.version ?? ''}
</Button>
</CardActions>
</ModCard>
);
}
return (
<Box>
<Grid container spacing={3}>
{[...new Set(sortedMods.map(m => m.name))].map((modName) => (
<Grid item xs={12} sm={6} md={4}>
{ModCardComponent(modName)}
</Grid>
))}
</Grid>
</Box>
);
}