354 lines
12 KiB
TypeScript
354 lines
12 KiB
TypeScript
import {AvailableMods} from "../core/api";
|
|
import React from "react";
|
|
import { NavLink } from "react-router-dom";
|
|
import { IMod } from "../types/Imod";
|
|
import {
|
|
Box,
|
|
Container,
|
|
Typography,
|
|
Grid,
|
|
Card,
|
|
CardContent,
|
|
CardActions,
|
|
Chip,
|
|
Button,
|
|
Divider,
|
|
useTheme,
|
|
Paper,
|
|
LinearProgress,
|
|
} from "@mui/material";
|
|
import { styled } from '@mui/material/styles';
|
|
|
|
// Styled components
|
|
const PageHeader = styled(Paper)(({ theme }) => ({
|
|
padding: theme.spacing(6, 4),
|
|
marginBottom: theme.spacing(6),
|
|
background: theme.palette.mode === 'dark'
|
|
? 'linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)'
|
|
: 'linear-gradient(135deg, #ffffff 0%, #f0f0f0 50%, #e0e0e0 100%)',
|
|
borderRadius: '16px',
|
|
position: 'relative',
|
|
overflow: 'hidden',
|
|
'&::before': {
|
|
content: '""',
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
background: 'radial-gradient(circle at 30% 50%, rgba(255, 255, 255, 0.05) 0%, #dee2e6 50%)',
|
|
pointerEvents: 'none',
|
|
},
|
|
}));
|
|
|
|
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(NavLink)(({ 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 ModDescription = styled(Typography)(({ theme }) => ({
|
|
color: theme.palette.text.secondary,
|
|
lineHeight: 1.6,
|
|
}));
|
|
|
|
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 StyledDivider = styled(Divider)(({ theme }) => ({
|
|
my: 2,
|
|
borderColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)',
|
|
}));
|
|
|
|
const MainVersionsLabel = styled(Typography)(({ theme }) => ({
|
|
color: theme.palette.text.primary,
|
|
fontWeight: 600,
|
|
mb: 1.5,
|
|
}));
|
|
|
|
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,
|
|
}));
|
|
|
|
const OpenLatestButton = styled(Button)(({ theme }) => {
|
|
const isDark = theme.palette.mode === 'dark';
|
|
return {
|
|
width: '100%',
|
|
color: isDark ? '#FFD700' : '#1976d2',
|
|
borderColor: isDark ? '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: isDark ? '#FFD700' : '#1976d2',
|
|
backgroundColor: isDark ? 'rgba(255, 215, 0, 0.1)' : 'rgba(25, 118, 210, 0.1)',
|
|
boxShadow: isDark ? '0 8px 20px rgba(255, 215, 0, 0.2)' : '0 8px 20px rgba(25, 118, 210, 0.2)',
|
|
},
|
|
};
|
|
});
|
|
|
|
interface ModsProps {
|
|
mods: IMod[];
|
|
}
|
|
|
|
function Mods({ mods }: ModsProps) {
|
|
const theme = useTheme();
|
|
|
|
// Sort all mods by order, then group by name
|
|
const sortedMods = [...mods].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: latest,
|
|
allVersions: sameMods,
|
|
hasBeta: betaMods.length > 0,
|
|
betaVersion: betaMods[0]
|
|
};
|
|
};
|
|
|
|
|
|
function ModCardComponent(modName: String, index: number) {
|
|
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} to={"/mod/" + mod.id} state={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 to={"/mod/" + betaVersion.id} state={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"
|
|
component={NavLink}
|
|
to={"/mod/" + latest?.id}
|
|
state={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, index) => (
|
|
<Grid item xs={12} sm={6} md={4} >
|
|
{ModCardComponent(modName, index)}
|
|
</Grid>
|
|
))}
|
|
</Grid>
|
|
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
interface ModsPageState {
|
|
mods: IMod[];
|
|
loading: boolean;
|
|
}
|
|
|
|
class ModsPage extends React.Component<any, ModsPageState> {
|
|
|
|
constructor({props}: { props: any }) {
|
|
super(props);
|
|
this.state = {
|
|
mods: [],
|
|
loading: true
|
|
};
|
|
}
|
|
|
|
async componentDidMount() {
|
|
// Очистка кэша сайта при заходе на главную страницу (один раз за сессию)
|
|
if (!sessionStorage.getItem('cache_cleared')) {
|
|
sessionStorage.setItem('cache_cleared', '1');
|
|
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);
|
|
}
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(AvailableMods);
|
|
const data: IMod[] = await response.json();
|
|
this.setState({
|
|
mods: data.filter(mod => !mod.isHide),
|
|
loading: false
|
|
});
|
|
} catch (error) {
|
|
console.error('Error loading mods:', error);
|
|
this.setState({ loading: false });
|
|
}
|
|
}
|
|
|
|
render() {
|
|
if (this.state.loading) {
|
|
return (
|
|
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
|
|
<LinearProgress sx={{ width: '200px' }} />
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
return <Mods mods={this.state.mods} />;
|
|
}
|
|
}
|
|
|
|
export default ModsPage;
|