249 lines
9.5 KiB
TypeScript
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>
|
|
);
|
|
}
|