Vibe-code дизайн dark theme re-vibecode

This commit is contained in:
anibus 2026-07-21 02:27:22 +03:00
parent 63455578b1
commit b603732dd6
31 changed files with 689 additions and 476 deletions

BIN
public/images/dowdelogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -4,14 +4,16 @@ import {
AppBar, AppBar,
Box, Box,
Container, Container,
createTheme, IconButton,
ThemeProvider,
Toolbar, Toolbar,
Typography, Typography,
useTheme
} from "@mui/material"; } from "@mui/material";
import { useNavigate, BrowserRouter } from "react-router-dom";
import { MyRoutes } from "./Routes"; import { MyRoutes } from "./Routes";
import { styled } from '@mui/material/styles'; import { styled } from '@mui/material/styles';
import { ThemeProvider as CustomThemeProvider, useThemeContext } from './context/ThemeContext';
import DarkModeIcon from '@mui/icons-material/DarkMode';
import LightModeIcon from '@mui/icons-material/LightMode';
// Styled components // Styled components
const StyledAppBar = styled(AppBar)(({ theme }) => ({ const StyledAppBar = styled(AppBar)(({ theme }) => ({
@ -20,12 +22,20 @@ const StyledAppBar = styled(AppBar)(({ theme }) => ({
borderBottom: '1px solid rgba(255, 255, 255, 0.1)', borderBottom: '1px solid rgba(255, 255, 255, 0.1)',
})); }));
const StyledAppBarLight = styled(AppBar)(({ theme }) => ({
background: 'linear-gradient(135deg, #ffffff 0%, #f0f0f0 50%, #e0e0e0 100%)',
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.1)',
borderBottom: '1px solid rgba(0, 0, 0, 0.1)',
}));
const LogoText = styled(Typography)(({ theme }) => ({ const LogoText = styled(Typography)(({ theme }) => ({
fontSize: '1.5rem', fontSize: '1.5rem',
fontWeight: 800, fontWeight: 800,
background: 'linear-gradient(135deg, #e94560 0%, #ff6b6b 100%)', background: theme.palette.mode === 'dark'
? 'linear-gradient(135deg, #dee2e6 0%, #cccccc 100%)'
: 'linear-gradient(135deg, #1a1a2e 0%, #333333 100%)',
WebkitBackgroundClip: 'text', WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent', WebkitTextFillColor: theme.palette.mode === 'dark' ? '#dee2e6' : '#1a1a2e',
letterSpacing: '-0.5px', letterSpacing: '-0.5px',
cursor: 'pointer', cursor: 'pointer',
'&:hover': { '&:hover': {
@ -34,68 +44,78 @@ const LogoText = styled(Typography)(({ theme }) => ({
}, },
})); }));
function App() { const ThemeToggle = styled(IconButton)(({ theme }) => ({
const theme = useTheme(); color: 'inherit',
'&:hover': {
backgroundColor: 'rgba(255, 255, 255, 0.1)',
},
}));
const themeConfig = createTheme({ const ThemeToggleLight = styled(IconButton)(({ theme }) => ({
breakpoints: { color: 'inherit',
values: { '&:hover': {
xs: 0, backgroundColor: 'rgba(0, 0, 0, 0.05)',
sm: 600,
md: 900,
lg: 1320,
xl: 1536,
}, },
}, }));
palette: {
primary: { function AppBarContent() {
main: "#1a1a2e", const { mode, toggleTheme } = useThemeContext();
}, const navigate = useNavigate();
background: { const isDark = mode === 'dark';
default: '#0a0a0a',
paper: '#1a1a2e', const handleLogoClick = () => navigate('/');
},
},
typography: {
fontFamily: '"Inter", "Roboto", "Helvetica", sans-serif',
},
shape: {
borderRadius: 12,
},
components: {
MuiTableCell: {
styleOverrides: {
root: {
color: 'rgba(255, 255, 255, 0.85)',
},
},
},
},
});
return ( return (
<Box sx={{ minHeight: '100vh', backgroundColor: '#0a0a0a' }}> <>
<ThemeProvider theme={themeConfig}> {isDark ? (
<a href="/">
<StyledAppBar position="sticky"> <StyledAppBar position="sticky">
<Container maxWidth="lg"> <Container maxWidth="lg">
<Toolbar disableGutters sx={{ justifyContent: 'space-between' }}> <Toolbar disableGutters sx={{ justifyContent: 'space-between' }}>
{/* Left side - Logo */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<LogoText variant="h6" sx={{ textDecoration: 'none' }}> <img src="/images/dowdelogo.png" alt="logo" style={{ height: 32, cursor: 'pointer' }} onClick={handleLogoClick}/>
🎮 Autogenerated wiki <LogoText variant="h6" sx={{ textDecoration: 'none', cursor: 'pointer' }} onClick={handleLogoClick}>
Autogenerated wiki
</LogoText> </LogoText>
</Box> </Box>
<ThemeToggle onClick={toggleTheme} size="large">
<LightModeIcon />
</ThemeToggle>
</Toolbar> </Toolbar>
</Container> </Container>
</StyledAppBar> </StyledAppBar>
</a> ) : (
<StyledAppBarLight position="sticky">
<Container maxWidth="lg">
<Toolbar disableGutters sx={{ justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<img src="/images/dowdelogo.png" alt="logo" style={{ height: 32, cursor: 'pointer' }} onClick={handleLogoClick}/>
<LogoText variant="h6" sx={{ textDecoration: 'none', cursor: 'pointer' }} onClick={handleLogoClick}>
Autogenerated wiki
</LogoText>
</Box>
<ThemeToggleLight onClick={toggleTheme} size="large">
<DarkModeIcon />
</ThemeToggleLight>
</Toolbar>
</Container>
</StyledAppBarLight>
)}
</>
);
}
function App() {
return (
<CustomThemeProvider>
<Box sx={{ minHeight: '100vh' }}>
<BrowserRouter>
<AppBarContent />
<Container maxWidth="lg" sx={{ py: 4 }}> <Container maxWidth="lg" sx={{ py: 4 }}>
<MyRoutes /> <MyRoutes />
</Container> </Container>
</ThemeProvider> </BrowserRouter>
</Box> </Box>
</CustomThemeProvider>
); );
} }

View File

@ -1,4 +1,4 @@
import {Routes, Route, BrowserRouter} from "react-router-dom"; import {Routes, Route} from "react-router-dom";
import ModsPage from "./pages/ModsPage"; import ModsPage from "./pages/ModsPage";
import ModPage from "./pages/ModPage"; import ModPage from "./pages/ModPage";
import RacePageFast from "./pages/RacePageFast"; import RacePageFast from "./pages/RacePageFast";
@ -8,7 +8,6 @@ import BuildingPage from "./pages/BuildingPage";
export const MyRoutes = () => { export const MyRoutes = () => {
return( return(
<BrowserRouter>
<Routes> <Routes>
<Route path="/" element={<ModsPage/>}/> <Route path="/" element={<ModsPage/>}/>
<Route path="/mod/:modId" element={<ModPage/>}/> <Route path="/mod/:modId" element={<ModPage/>}/>
@ -16,6 +15,5 @@ export const MyRoutes = () => {
<Route path="/mod/:modId/race/:raceId/unit/:unitId" element={<UnitPage/>}/> <Route path="/mod/:modId/race/:raceId/unit/:unitId" element={<UnitPage/>}/>
<Route path="/mod/:modId/race/:raceId/building/:buildingId" element={<BuildingPage/>}/> <Route path="/mod/:modId/race/:raceId/building/:buildingId" element={<BuildingPage/>}/>
</Routes> </Routes>
</BrowserRouter>
) )
} }

View File

@ -20,18 +20,18 @@ function Ability(props: IAbilityProps){
return <div className='addon-research-accordion' id={"research-" + ability.id}><StyledAccordion TransitionProps={{ unmountOnExit: true, timeout: 100 }}> return <div className='addon-research-accordion' id={"research-" + ability.id}><StyledAccordion TransitionProps={{ unmountOnExit: true, timeout: 100 }}>
<AccordionSummary <AccordionSummary
expandIcon={<ExpandMore sx={{color: '#ffffff'}}/>} sx={{color: '#dee2e6', '& .MuiAccordionSummary-content': { margin: '12px 0' }, '& .MuiAccordionSummary-content.Mui-expanded': { margin: '12px 0' }}}
expandIcon={<ExpandMore sx={{color: '#dee2e6'}}/>}
aria-controls="panel1-content" aria-controls="panel1-content"
sx={{color: '#ffffff'}}
> >
<span style={{fontSize: 20, color: '#ffffff'}}> <span style={{fontSize: 20, color: '#dee2e6', display: 'flex', alignItems: 'center'}}>
{ability.activationType === "Passive ability" ? <img className="abilityIcon" src="/images/PassiveIcon.gif"/> : <img className="abilityIcon" src={getIcon(ability.icon)}/>} {ability.activationType === "Passive ability" ? <img className="abilityIcon" src="/images/PassiveIcon.gif"/> : <img className="abilityIcon" src={getIcon(ability.icon)}/>}
&nbsp; {ability.name ? ability.name : ability.fileName.charAt(0).toUpperCase() + ability.fileName.slice(1).replaceAll('_', ' ').replace('.rgd', '')} &nbsp; {ability.name ? ability.name : ability.fileName.charAt(0).toUpperCase() + ability.fileName.slice(1).replaceAll('_', ' ').replace('.rgd', '')}
<i style={{fontSize: 12, color: 'rgba(255,255,255,0.5)'}}> ({ability.activationType})</i> <i style={{fontSize: 12, color: 'rgba(255,255,255,0.5)'}}> ({ability.activationType})</i>
</span> </span>
</AccordionSummary> </AccordionSummary>
<AccordionDetails sx={{color: 'rgba(255,255,255,0.85)'}}> <AccordionDetails sx={{color: '#dee2e6'}}>
<AbilityFull abilityId={ability.id} mod={props.mod} race={props.race} /> <AbilityFull abilityId={ability.id} mod={props.mod} race={props.race} />
</AccordionDetails> </AccordionDetails>
</StyledAccordion></div> </StyledAccordion></div>

View File

@ -5,7 +5,6 @@ import ArmorTypeNames from "../types/ArmorTypeValues";
import { import {
AccordionDetails, AccordionDetails,
Grid2, Grid2,
Paper,
styled, styled,
Table, Table,
TableBody, TableBody,
@ -28,6 +27,7 @@ import ability from "./Ability";
import AbilitySpawnedObject from "./AbilitySpawnedObject"; import AbilitySpawnedObject from "./AbilitySpawnedObject";
import DpsTable from "./DpsTable"; import DpsTable from "./DpsTable";
import {DescriptionBox} from "../commons/DescriptionBox"; import {DescriptionBox} from "../commons/DescriptionBox";
import {StyledPaper} from "../commons/StyledPaper";
interface IAbilityFullState { interface IAbilityFullState {
ability?: IAbilityFill; ability?: IAbilityFill;
@ -42,36 +42,22 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
const StyledTableCell = styled(TableCell)(({theme}) => ({ const StyledTableCell = styled(TableCell)(({theme}) => ({
[`&.${tableCellClasses.head}`]: { [`&.${tableCellClasses.head}`]: {
backgroundColor: 'rgba(233, 69, 96, 0.15)', backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(233, 69, 96, 0.15)',
fontWeight: 700, fontWeight: 700,
marginRight: 'auto', marginRight: 'auto',
marginLeft: 'auto', marginLeft: 'auto',
paddingLeft: 10, paddingLeft: 10,
borderBottom: '2px solid rgba(233, 69, 96, 0.3)', borderBottom: `2px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.3)' : 'rgba(233, 69, 96, 0.3)'}`,
}, },
[`&.${tableCellClasses.body}`]: { [`&.${tableCellClasses.body}`]: {
fontSize: 12, fontSize: 12,
textAlign: 'center', textAlign: 'center',
color: 'rgba(255, 255, 255, 0.85)', color: '#dee2e6',
paddingRight: 18, paddingRight: 18,
paddingLeft: 10, paddingLeft: 10,
}, },
})); }));
const StyledPaper = styled(Paper)(({ theme }) => ({
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '12px',
overflow: 'hidden',
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-root': {
color: 'rgba(255, 255, 255, 0.9)',
},
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': {
color: '#e94560',
fontWeight: 600,
},
}));
useEffect(() => { useEffect(() => {
if(props.abilityFull !== undefined){ if(props.abilityFull !== undefined){
@ -108,11 +94,11 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
const showTargetTable = !showDpsTable && ability.abilityEnvironment == null && (ability.targetFilter.length > 0 || ability.radius !== 0) const showTargetTable = !showDpsTable && ability.abilityEnvironment == null && (ability.targetFilter.length > 0 || ability.radius !== 0)
return ( return (
<div style={{color: 'rgba(255, 255, 255, 0.85)'}}> <div style={{color: '#dee2e6'}}>
<Grid2 container spacing={2}> <Grid2 container spacing={2}>
<Grid2 size={{xs: 12}}> <Grid2 size={{xs: 12}}>
{ability.initialDelayTime !== undefined && props.isChild === true && {ability.initialDelayTime !== undefined && props.isChild === true &&
<b><br/><center style={{color: 'rgba(255,255,255,0.7)'}}>&nbsp;After {ability.initialDelayTime.toFixed(1)}s&nbsp; <b><br/><center style={{color: '#dee2e6'}}>&nbsp;After {ability.initialDelayTime.toFixed(1)}s&nbsp;
<img style={{verticalAlign: "top"}} <img style={{verticalAlign: "top"}}
src="/images/Time_icon.webp"/></center></b>} src="/images/Time_icon.webp"/></center></b>}
</Grid2> </Grid2>
@ -215,7 +201,7 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
</Grid2> </Grid2>
{showDpsTable ? {showDpsTable ?
<Grid2 size={12}> <Grid2 size={12}>
{(ability.refreshTime !== undefined && ability.durationTime !== undefined && ability.refreshTime * 2 < ability.durationTime) && <i style={{color: 'rgba(255,255,255,0.7)'}}>Every <b style={{color: '#ffffff'}}>{ability.refreshTime.toFixed(1)}s</b> deal damege:</i>} {(ability.refreshTime !== undefined && ability.durationTime !== undefined && ability.refreshTime * 2 < ability.durationTime) && <i style={{color: '#dee2e6'}}>Every <b style={{color: '#dee2e6'}}>{ability.refreshTime.toFixed(1)}s</b> deal damege:</i>}
<DpsTable mod={props.mod} minDamageValue={ability.minDamageValue} minDamage={ability.minDamage} maxDamage={ability.maxDamage} piercings={ability.piercings} targetFilter={ability.targetFilter} moraleDamage={ability.moraleDamage} <DpsTable mod={props.mod} minDamageValue={ability.minDamageValue} minDamage={ability.minDamage} maxDamage={ability.maxDamage} piercings={ability.piercings} targetFilter={ability.targetFilter} moraleDamage={ability.moraleDamage}
/> />
</Grid2> : showTargetTable ? </Grid2> : showTargetTable ?

View File

@ -1,7 +1,7 @@
import { import {
Accordion, Accordion,
AccordionDetails, AccordionDetails,
AccordionSummary, Grid2, Paper, AccordionSummary, Grid2,
Table, Table,
TableBody, TableBody,
TableCell, TableContainer, TableCell, TableContainer,
@ -24,6 +24,7 @@ import Ability from "./Ability";
import DeathExplosion from "./DeathExplosion"; import DeathExplosion from "./DeathExplosion";
import {styled} from '@mui/material/styles'; import {styled} from '@mui/material/styles';
import {DescriptionBox} from "../commons/DescriptionBox"; import {DescriptionBox} from "../commons/DescriptionBox";
import {StyledPaper} from "../commons/StyledPaper";
const SectionTitle = styled(Typography)(({ theme }) => ({ const SectionTitle = styled(Typography)(({ theme }) => ({
fontWeight: 700, fontWeight: 700,
@ -31,20 +32,6 @@ const SectionTitle = styled(Typography)(({ theme }) => ({
fontSize: '1.25rem', fontSize: '1.25rem',
})); }));
const StyledPaper = styled(Paper)(({ theme }) => ({
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '12px',
overflow: 'hidden',
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-root': {
color: 'rgba(255, 255, 255, 0.9)',
},
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': {
color: '#e94560',
fontWeight: 600,
},
}));
interface AbilityEnvironmentProps { interface AbilityEnvironmentProps {
abilityEnvironment: IAbilityEnvironment, abilityEnvironment: IAbilityEnvironment,
@ -56,7 +43,7 @@ function AbilitySpawnedObject(props: AbilityEnvironmentProps) {
const abilityEnvironment = props.abilityEnvironment const abilityEnvironment = props.abilityEnvironment
return <div style={{color: 'rgba(255, 255, 255, 0.85)'}}> return <div style={{color: '#dee2e6'}}>
<SectionTitle> <SectionTitle>
{props.abilityEnvironment.name ?? props.abilityEnvironment.filename.charAt(0).toUpperCase() + props.abilityEnvironment.filename.slice(1).replaceAll('_', ' ').replace('.rgd', '')} {props.abilityEnvironment.name ?? props.abilityEnvironment.filename.charAt(0).toUpperCase() + props.abilityEnvironment.filename.slice(1).replaceAll('_', ' ').replace('.rgd', '')}
<i style={{fontSize: 11, color: 'rgba(255,255,255,0.5)'}}> (Spawned object)</i> <i style={{fontSize: 11, color: 'rgba(255,255,255,0.5)'}}> (Spawned object)</i>

View File

@ -1,6 +1,6 @@
import { import {
AccordionDetails, AccordionDetails,
AccordionSummary, Grid2, Paper, AccordionSummary, Grid2,
Table, Table,
TableBody, TableBody,
TableCell, TableContainer, TableHead, TableCell, TableContainer, TableHead,
@ -24,22 +24,9 @@ import Required from "./Required";
import ArmorTypeNames from "../types/ArmorTypeValues"; import ArmorTypeNames from "../types/ArmorTypeValues";
import DpsTable from "./DpsTable"; import DpsTable from "./DpsTable";
import {StyledAccordion} from "../commons/StyledAccordion"; import {StyledAccordion} from "../commons/StyledAccordion";
import {StyledPaper} from "../commons/StyledPaper";
import {styled} from "@mui/material/styles"; import {styled} from "@mui/material/styles";
const StyledPaper = styled(Paper)(({ theme }) => ({
background: 'rgba(255, 255, 255, 0.03)',
border: '1px solid rgba(255, 255, 255, 0.05)',
borderRadius: '12px',
overflow: 'hidden',
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-root': {
color: 'rgba(255, 255, 255, 0.85)',
},
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': {
color: '#e94560',
fontWeight: 600,
},
}));
interface DeathExpProps { interface DeathExpProps {
deathExplosion: IDeathExplosion, deathExplosion: IDeathExplosion,
@ -54,15 +41,15 @@ function DeathExplosion(props: DeathExpProps) {
return chanceTotalNotZero ? return chanceTotalNotZero ?
<div className='addon-research-accordion' id='death-explosion'><StyledAccordion> <div className='addon-research-accordion' id='death-explosion'><StyledAccordion>
<AccordionSummary <AccordionSummary
expandIcon={<ExpandMore sx={{color: '#ffffff'}}/>} sx={{color: '#dee2e6', '& .MuiAccordionSummary-content': { margin: '12px 0' }, '& .MuiAccordionSummary-content.Mui-expanded': { margin: '12px 0' }}}
expandIcon={<ExpandMore sx={{color: '#dee2e6'}}/>}
aria-controls="panel1-content" aria-controls="panel1-content"
sx={{color: '#ffffff'}}
> >
<span style={{fontSize: 20, color: '#ffffff'}}> <span style={{fontSize: 20, color: '#dee2e6', display: 'flex', alignItems: 'center'}}>
Death Explosion Death Explosion
</span> </span>
</AccordionSummary> </AccordionSummary>
<AccordionDetails sx={{color: 'rgba(255,255,255,0.85)'}}> <AccordionDetails sx={{color: '#dee2e6'}}>
<Grid2 container spacing={2}> <Grid2 container spacing={2}>
<Grid2 size={{xs: 12, md: 4}}> <Grid2 size={{xs: 12, md: 4}}>
<TableContainer component={StyledPaper} elevation={0}> <TableContainer component={StyledPaper} elevation={0}>

View File

@ -19,17 +19,17 @@ export default function DpsTable(props: { mod: IMod, minDamageValue?: number, mi
const StyledTableCell = styled(TableCell)(({theme}) => ({ const StyledTableCell = styled(TableCell)(({theme}) => ({
[`&.${tableCellClasses.head}`]: { [`&.${tableCellClasses.head}`]: {
backgroundColor: 'rgba(233, 69, 96, 0.15)', backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(233, 69, 96, 0.15)',
fontWeight: 700, fontWeight: 700,
marginRight: 'auto', marginRight: 'auto',
marginLeft: 'auto', marginLeft: 'auto',
paddingLeft: 10, paddingLeft: 10,
borderBottom: '2px solid rgba(233, 69, 96, 0.3)', borderBottom: `2px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.3)' : 'rgba(233, 69, 96, 0.3)'}`,
}, },
[`&.${tableCellClasses.body}`]: { [`&.${tableCellClasses.body}`]: {
fontSize: 12, fontSize: 12,
textAlign: 'center', textAlign: 'center',
color: 'rgba(255, 255, 255, 0.85)', color: '#dee2e6',
paddingRight: 18, paddingRight: 18,
paddingLeft: 10, paddingLeft: 10,
}, },

View File

@ -8,17 +8,7 @@ import {IMod} from "../types/Imod";
import AvTimerOutlinedIcon from "@mui/icons-material/AvTimer"; import AvTimerOutlinedIcon from "@mui/icons-material/AvTimer";
import {styled} from '@mui/material/styles'; import {styled} from '@mui/material/styles';
import {StyledLink} from "../commons/StyledLink"; import {StyledLink} from "../commons/StyledLink";
import {StyledPaper} from "../commons/StyledPaper";
// Styled components for dark theme
const StyledPaper = styled(Paper)(({ theme }) => ({
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '12px',
overflow: 'hidden',
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-root': {
color: 'rgba(255, 255, 255, 0.85)',
},
}));
interface IModifiersProvidesTable { interface IModifiersProvidesTable {
@ -162,8 +152,8 @@ export function ModifiersProvidesAddonTable(props: IModifiersProvidesTable) {
<TableRow <TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
<TableCell component="th" scope="row" sx={{color: 'rgba(255,255,255,0.85)'}}>{getModName(m.reference)}</TableCell> <TableCell component="th" scope="row" sx={{color: '#dee2e6'}}>{getModName(m.reference)}</TableCell>
<TableCell component="th" scope="row" sx={{color: 'rgba(255,255,255,0.85)'}}>{getModIcon(m.reference)} {getChangeDescription(m.usageType, m.value)}</TableCell> <TableCell component="th" scope="row" sx={{color: '#dee2e6'}}>{getModIcon(m.reference)} {getChangeDescription(m.usageType, m.value)}</TableCell>
</TableRow> </TableRow>
)} )}
</Table> </Table>
@ -247,9 +237,9 @@ export function ModifiersProvidesTable(props: IModifiersProvidesResearchTable) {
<TableRow <TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
<TableCell component="th" scope="row" sx={{color: 'rgba(255,255,255,0.85)'}}>{getTarget(m.target)}</TableCell> <TableCell component="th" scope="row" sx={{color: '#dee2e6'}}>{getTarget(m.target)}</TableCell>
<TableCell component="th" scope="row" sx={{color: 'rgba(255,255,255,0.85)', minWidth: 120}}>{getModName(m.reference)}</TableCell> <TableCell component="th" scope="row" sx={{color: '#dee2e6', minWidth: 120}}>{getModName(m.reference)}</TableCell>
<TableCell component="th" scope="row" sx={{color: 'rgba(255,255,255,0.85)'}}>{getModIcon(m.reference)} {getChangeDescription(m.usageType, m.value)} <TableCell component="th" scope="row" sx={{color: '#dee2e6'}}>{getModIcon(m.reference)} {getChangeDescription(m.usageType, m.value)}
{m.maxLifeTime !== undefined && m.maxLifeTime > 0 && {m.maxLifeTime !== undefined && m.maxLifeTime > 0 &&
<span><img style={{verticalAlign: "top"}} src='/images/Time_icon.webp'/>{m.maxLifeTime}s <span><img style={{verticalAlign: "top"}} src='/images/Time_icon.webp'/>{m.maxLifeTime}s
</span>} </span>}

View File

@ -115,20 +115,20 @@ function Required (props: IRequiredProps) {
return ( return (
<div style={{color: 'rgba(255, 255, 255, 0.85)'}}> <div style={{color: '#dee2e6'}}>
<h4 style={{color: '#ffffff', fontWeight: 700}}>Required: </h4> <h4 style={{color: '#dee2e6', fontWeight: 700}}>Required: </h4>
{requirement.requiredTotalPop !== undefined && requirement.requiredTotalPop !== null && {requirement.requiredTotalPop !== undefined && requirement.requiredTotalPop !== null &&
<div style={{color: 'rgba(255, 255, 255, 0.85)'}}>&nbsp; Population: <img style={{verticalAlign: "top", height: 25}} <div style={{color: '#dee2e6'}}>&nbsp; Population: <img style={{verticalAlign: "top", height: 25}}
src="/images/Resource_orksquadcap.gif"/>&nbsp; src="/images/Resource_orksquadcap.gif"/>&nbsp;
{requirement.requiredTotalPop}</div> {requirement.requiredTotalPop}</div>
} }
{requirement.mobBonus !== null && requirement.mobBonus !== undefined && {requirement.mobBonus !== null && requirement.mobBonus !== undefined &&
<div style={{color: 'rgba(255, 255, 255, 0.85)'}}>&nbsp; Mob requirement: <img style={{verticalAlign: "top", height: 25}} <div style={{color: '#dee2e6'}}>&nbsp; Mob requirement: <img style={{verticalAlign: "top", height: 25}}
src="/images/Mob_bonus.gif"/>&nbsp; src="/images/Mob_bonus.gif"/>&nbsp;
{requirement.mobBonus.mobvalueRequired} in radius {requirement.mobBonus.proximityRequired}</div> {requirement.mobBonus.mobvalueRequired} in radius {requirement.mobBonus.proximityRequired}</div>
} }
{requirement.requireAddon !== null && {requirement.requireAddon !== null &&
<div style={{color: 'rgba(255, 255, 255, 0.85)'}}>&nbsp; Addon: <img style={{verticalAlign: "top", height: 25}} <div style={{color: '#dee2e6'}}>&nbsp; Addon: <img style={{verticalAlign: "top", height: 25}}
src={IconUrl + requirement.requireAddon.icon.replaceAll('\\', '/')}/>&nbsp; src={IconUrl + requirement.requireAddon.icon.replaceAll('\\', '/')}/>&nbsp;
<StyledLink href={`/mod/${props.modId}/race/${props.raceId}/building/${requirement.requireAddon.buildingId}#addon-${requirement.requireAddon.id}`}> <StyledLink href={`/mod/${props.modId}/race/${props.raceId}/building/${requirement.requireAddon.buildingId}#addon-${requirement.requireAddon.id}`}>
{requirement.requireAddon.name} {requirement.requireAddon.name}
@ -137,26 +137,26 @@ function Required (props: IRequiredProps) {
</div> </div>
} }
{requirement.requirementAddonExclusive != null && {requirement.requirementAddonExclusive != null &&
<div style={{color: 'rgba(255, 255, 255, 0.85)'}}>{renderRequiremenAddonExclusive(requirement.requirementAddonExclusive,)}</div>} <div style={{color: '#dee2e6'}}>{renderRequiremenAddonExclusive(requirement.requirementAddonExclusive,)}</div>}
{requirement.requirementBuildings.map(b => {requirement.requirementBuildings.map(b =>
<div key={b.id} style={{color: 'rgba(255, 255, 255, 0.85)'}}>&nbsp; Building: <img style={{verticalAlign: "top", height: 25}} <div key={b.id} style={{color: '#dee2e6'}}>&nbsp; Building: <img style={{verticalAlign: "top", height: 25}}
src={IconUrl + b.icon.replaceAll('\\', '/')}/>&nbsp; src={IconUrl + b.icon.replaceAll('\\', '/')}/>&nbsp;
<StyledLink href= {'/mod/'+ props.modId +'/race/'+ props.raceId +'/building/' + b.id + "/"}>{b.name}</StyledLink></div>) <StyledLink href= {'/mod/'+ props.modId +'/race/'+ props.raceId +'/building/' + b.id + "/"}>{b.name}</StyledLink></div>)
} }
{requirement.requirementResearches.length !== 0 && {requirement.requirementResearches.length !== 0 &&
<div style={{color: 'rgba(255, 255, 255, 0.85)'}}>{renderRequirementResearches(requirement.requirementResearches,)}</div>} <div style={{color: '#dee2e6'}}>{renderRequirementResearches(requirement.requirementResearches,)}</div>}
{requirement.requirementSquads.length !== 0 && {requirement.requirementSquads.length !== 0 &&
<div style={{color: 'rgba(255, 255, 255, 0.85)'}}>{renderRequirementSquads(requirement.requirementSquads,)}</div>} <div style={{color: '#dee2e6'}}>{renderRequirementSquads(requirement.requirementSquads,)}</div>}
{requirement.requirementResearchesEither.length !== 0 && {requirement.requirementResearchesEither.length !== 0 &&
<div style={{color: 'rgba(255, 255, 255, 0.85)'}}>{renderRequirementResearchesEither(requirement.requirementResearchesEither,)}</div>} <div style={{color: '#dee2e6'}}>{renderRequirementResearchesEither(requirement.requirementResearchesEither,)}</div>}
{requirement.requirementBuildingsEither.length !== 0 && {requirement.requirementBuildingsEither.length !== 0 &&
<div style={{color: 'rgba(255, 255, 255, 0.85)'}}>{renderRequirementBuildingEither(requirement.requirementBuildingsEither,)}</div>} <div style={{color: '#dee2e6'}}>{renderRequirementBuildingEither(requirement.requirementBuildingsEither,)}</div>}
{requirement.requirementStructureExclusive != null && {requirement.requirementStructureExclusive != null &&
<div style={{color: 'rgba(255, 255, 255, 0.85)'}}>{renderRequiremenStructureExclusive(requirement.requirementStructureExclusive,)}</div>} <div style={{color: '#dee2e6'}}>{renderRequiremenStructureExclusive(requirement.requirementStructureExclusive,)}</div>}
{requirement.limitByBuilding !== undefined && requirement.limitByBuilding !== null && <div style={{color: 'rgba(255, 255, 255, 0.85)'}}>{renderLimitPerBuilding(requirement.limitByBuilding)}</div>} {requirement.limitByBuilding !== undefined && requirement.limitByBuilding !== null && <div style={{color: '#dee2e6'}}>{renderLimitPerBuilding(requirement.limitByBuilding)}</div>}
{requirement.requirementsGlobalAddons.length !== 0 && {requirement.requirementsGlobalAddons.length !== 0 &&
<div style={{color: 'rgba(255, 255, 255, 0.85)'}}>{renderRequirementGlobalAddons(requirement.requirementsGlobalAddons,)}</div>} <div style={{color: '#dee2e6'}}>{renderRequirementGlobalAddons(requirement.requirementsGlobalAddons,)}</div>}
<div style={{color: 'rgba(255, 255, 255, 0.85)'}}>{renderRequirementOwnership(requirement.requiredOwnership)}</div> <div style={{color: '#dee2e6'}}>{renderRequirementOwnership(requirement.requiredOwnership)}</div>
</div> </div>
) )
} }

View File

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import {ISergeant, IShortWeapon} from "../types/IUnit"; import {ISergeant, IShortWeapon} from "../types/IUnit";
import {Grid2, Paper, Table, TableBody, TableCell, TableContainer, TableRow, Tooltip, Typography} from "@mui/material"; import {Grid2, Table, TableBody, TableCell, TableContainer, TableRow, Tooltip, Typography} from "@mui/material";
import AvTimerOutlinedIcon from "@mui/icons-material/AvTimer"; import AvTimerOutlinedIcon from "@mui/icons-material/AvTimer";
import ArmorType from "./ArmorType"; import ArmorType from "./ArmorType";
import WeaponSlot from "./WeaponSlot"; import WeaponSlot from "./WeaponSlot";
@ -14,20 +14,7 @@ import Ability from "./Ability";
import DeathExplosion from "./DeathExplosion"; import DeathExplosion from "./DeathExplosion";
import {styled} from '@mui/material/styles'; import {styled} from '@mui/material/styles';
import {DescriptionBox} from "../commons/DescriptionBox"; import {DescriptionBox} from "../commons/DescriptionBox";
import {StyledPaper} from "../commons/StyledPaper";
const StyledPaper = styled(Paper)(({ theme }) => ({
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '12px',
overflow: 'hidden',
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-root': {
color: 'rgba(255, 255, 255, 0.9)',
},
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': {
color: '#e94560',
fontWeight: 600,
},
}));
const SectionTitle = styled(Typography)(({ theme }) => ({ const SectionTitle = styled(Typography)(({ theme }) => ({
fontWeight: 700, fontWeight: 700,
@ -60,7 +47,7 @@ const Sergeant = (props: SergeantProps) => {
}) })
return ( return (
<div style={{color: 'rgba(255, 255, 255, 0.85)'}}> <div style={{color: '#dee2e6'}}>
<Grid2 container spacing={2}> <Grid2 container spacing={2}>
<Grid2 size= {{xs: 12, md: 4}}> <Grid2 size= {{xs: 12, md: 4}}>
<TableContainer component={StyledPaper} elevation={0}> <TableContainer component={StyledPaper} elevation={0}>

View File

@ -5,30 +5,22 @@ import {styled} from '@mui/material/styles';
import {IRaceUnits, IUnitShort} from "../types/IUnitShort"; import {IRaceUnits, IUnitShort} from "../types/IUnitShort";
import {IBuildingShort, IRaceBuildings} from "../types/IBuildingShort"; import {IBuildingShort, IRaceBuildings} from "../types/IBuildingShort";
import {StyledLink} from "../commons/StyledLink"; import {StyledLink} from "../commons/StyledLink";
import {StyledPaper} from "../commons/StyledPaper";
// Styled components for dark theme
const StyledPaper = styled(Paper)(({ theme }) => ({
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '12px',
overflow: 'hidden',
}));
const StyledTable = styled(Table)(({ theme }) => ({ const StyledTable = styled(Table)(({ theme }) => ({
'& .MuiTableCell-head': { '& .MuiTableCell-head': {
backgroundColor: 'rgba(233, 69, 96, 0.15)', backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(233, 69, 96, 0.15)',
fontWeight: 700, fontWeight: 700,
fontSize: '1rem', fontSize: '1rem',
borderBottom: '2px solid rgba(233, 69, 96, 0.3)', borderBottom: `2px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.3)' : 'rgba(233, 69, 96, 0.3)'}`,
}, },
'& .MuiTableCell-body': { '& .MuiTableCell-body': {
color: 'rgba(255, 255, 255, 0.85)', color: theme.palette.text.primary,
borderBottom: '1px solid rgba(255, 255, 255, 0.05)', borderBottom: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.05)'}`,
fontSize: '0.9rem', fontSize: '0.9rem',
}, },
'& .MuiTableRow-hover:hover': { '& .MuiTableRow-hover:hover': {
backgroundColor: 'rgba(233, 69, 96, 0.08)', backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(233, 69, 96, 0.08)',
}, },
})); }));
@ -36,7 +28,7 @@ const StyledHeaderLink = styled(StyledLink)(({ theme }) => ({
fontWeight: 700, fontWeight: 700,
fontSize: '1.1rem', fontSize: '1.1rem',
'&:hover': { '&:hover': {
color: '#e94560', color: theme.palette.mode === 'dark' ? '#dee2e6' : '#e94560',
}, },
})); }));

View File

@ -37,21 +37,21 @@ class Weapon extends React.Component<IWeaponProps, any> {
return ( return (
<div style={{marginBottom: 10}}><StyledAccordion TransitionProps={{ unmountOnExit: true, timeout: 100 }}> <div style={{marginBottom: 10}}><StyledAccordion TransitionProps={{ unmountOnExit: true, timeout: 100 }}>
<AccordionSummary <AccordionSummary
expandIcon={<ExpandMore sx={{color: '#ffffff'}}/>} expandIcon={<ExpandMore sx={{color: '#dee2e6'}}/>}
aria-controls="panel1-content" aria-controls="panel1-content"
id="panel1-header" id="panel1-header"
sx={{color: '#ffffff'}} sx={{color: '#dee2e6', '& .MuiAccordionSummary-content': { margin: '12px 0' }, '& .MuiAccordionSummary-content.Mui-expanded': { margin: '12px 0' }}}
> >
<span style={{fontSize: 18, color: '#ffffff'}}> {!this.props.isDefault && weapon.icon && !weapon.icon.endsWith("upgrade.png") ? <span style={{fontSize: 18, color: '#dee2e6', display: 'flex', alignItems: 'center'}}> {!this.props.isDefault && weapon.icon && !weapon.icon.endsWith("upgrade.png") ?
<img className="weaponIcon" src={IconUrl + weapon.icon.replaceAll('\\', '/')}/> : ( <img className="weaponIcon" src={IconUrl + weapon.icon.replaceAll('\\', '/')}/> : (
weapon.isMeleeWeapon ? weapon.isMeleeWeapon ?
<img className="weaponIcon" src="/images/MeleeStance_icon_bw.jpg"/> : <img className="weaponIcon" src="/images/MeleeStance_icon_bw.jpg"/> :
<img className="weaponIcon" src="/images/RangedStance_icon_bw.jpg"/> <img className="weaponIcon" src="/images/RangedStance_icon_bw.jpg"/>
)} {weapon.name ? weapon.name : this.humanReadableName(weapon.filename)} )} &nbsp; {weapon.name ? weapon.name : this.humanReadableName(weapon.filename)}
{this.props.isDefault && <i style={{fontSize: 12, color: 'rgba(255,255,255,0.5)'}}> (default)</i>} {this.props.isDefault && <i style={{fontSize: 12, color: 'rgba(255,255,255,0.5)'}}> (default)</i>}
</span> </span>
</AccordionSummary> </AccordionSummary>
<AccordionDetails sx={{color: 'rgba(255,255,255,0.85)'}}> <AccordionDetails sx={{color: '#dee2e6'}}>
<WeaponFull weaponId={weapon.id} isDefault={this.props.isDefault} mod={this.props.mod} race={this.props.race} haveReinforceMenu={this.props.haveReinforceMenu}/> <WeaponFull weaponId={weapon.id} isDefault={this.props.isDefault} mod={this.props.mod} race={this.props.race} haveReinforceMenu={this.props.haveReinforceMenu}/>
</AccordionDetails> </AccordionDetails>
</StyledAccordion></div> </StyledAccordion></div>

View File

@ -5,7 +5,6 @@ import ArmorTypeNames from "../types/ArmorTypeValues";
import { import {
AccordionDetails, AccordionDetails,
Grid2, Grid2,
Paper,
styled, styled,
Table, Table,
TableBody, TableBody,
@ -23,6 +22,7 @@ import {IRaceUnits} from "../types/IUnitShort";
import {IRaceBuildings} from "../types/IBuildingShort"; import {IRaceBuildings} from "../types/IBuildingShort";
import {ModifiersProvidesTable} from "./ModifiersProvideTable"; import {ModifiersProvidesTable} from "./ModifiersProvideTable";
import {DescriptionBox} from "../commons/DescriptionBox"; import {DescriptionBox} from "../commons/DescriptionBox";
import {StyledPaper} from "../commons/StyledPaper";
import AvTimerOutlinedIcon from "@mui/icons-material/AvTimer"; import AvTimerOutlinedIcon from "@mui/icons-material/AvTimer";
interface IWeaponFull { interface IWeaponFull {
@ -39,35 +39,21 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
const StyledTableCell = styled(TableCell)(({theme}) => ({ const StyledTableCell = styled(TableCell)(({theme}) => ({
[`&.${tableCellClasses.head}`]: { [`&.${tableCellClasses.head}`]: {
backgroundColor: 'rgba(233, 69, 96, 0.15)', backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(233, 69, 96, 0.15)',
marginRight: 'auto', marginRight: 'auto',
marginLeft: 'auto', marginLeft: 'auto',
paddingLeft: 10, paddingLeft: 10,
borderBottom: '2px solid rgba(233, 69, 96, 0.3)', borderBottom: `2px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.3)' : 'rgba(233, 69, 96, 0.3)'}`,
}, },
[`&.${tableCellClasses.body}`]: { [`&.${tableCellClasses.body}`]: {
fontSize: 12, fontSize: 12,
textAlign: 'center', textAlign: 'center',
color: 'rgba(255, 255, 255, 0.85)', color: '#dee2e6',
paddingRight: 18, paddingRight: 18,
paddingLeft: 10, paddingLeft: 10,
}, },
})); }));
const StyledPaper = styled(Paper)(({ theme }) => ({
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '12px',
overflow: 'hidden',
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-root': {
color: 'rgba(255, 255, 255, 0.9)',
},
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': {
color: '#e94560',
fontWeight: 600,
},
}));
useEffect(() => { useEffect(() => {
fetch(WeaponUrl + "/" + props.mod.id + "/" + props.weaponId) fetch(WeaponUrl + "/" + props.mod.id + "/" + props.weaponId)
@ -151,7 +137,7 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
return ( return (
<div style={{color: 'rgba(255, 255, 255, 0.85)'}}> <div style={{color: '#dee2e6'}}>
<Grid2 container spacing={2}> <Grid2 container spacing={2}>
{!props.isDefault && <Grid2 size={12}> {!props.isDefault && <Grid2 size={12}>
<DescriptionBox> <DescriptionBox>
@ -262,8 +248,8 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
onChange={handleChange} onChange={handleChange}
sx={{ mb: 2 }} sx={{ mb: 2 }}
> >
<ToggleButton size="small" value="dps" sx={{ color: '#ffffff', '&.Mui-selected': { borderColor: '#e94560', color: '#e94560', backgroundColor: 'rgba(233, 69, 96, 0.15)' } }}>Dps</ToggleButton> <ToggleButton size="small" value="dps" sx={{ color: '#dee2e6', '&.Mui-selected': { borderColor: '#dee2e6', color: '#dee2e6', backgroundColor: 'rgba(255, 255, 255, 0.15)' } }}>Dps</ToggleButton>
<ToggleButton size="small" value="one hit" sx={{ color: '#ffffff', '&.Mui-selected': {borderColor: '#e94560', color: '#e94560', backgroundColor: 'rgba(233, 69, 96, 0.15)' } }}>One hit average damage</ToggleButton> <ToggleButton size="small" value="one hit" sx={{ color: '#dee2e6', '&.Mui-selected': {borderColor: '#dee2e6', color: '#dee2e6', backgroundColor: 'rgba(255, 255, 255, 0.15)' } }}>One hit average damage</ToggleButton>
</ToggleButtonGroup> </ToggleButtonGroup>
<TableContainer> <TableContainer>
{ props.mod !== undefined && props.mod.technicalName === 'UltimateApocalypse' ? { props.mod !== undefined && props.mod.technicalName === 'UltimateApocalypse' ?
@ -327,7 +313,7 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
name={ArmorTypeNames.BuildingSuper}/></StyledTableCell> name={ArmorTypeNames.BuildingSuper}/></StyledTableCell>
<StyledTableCell><img style={{verticalAlign: "top"}} <StyledTableCell><img style={{verticalAlign: "top"}}
src="/images/ARM_Morale.webp"/> src="/images/ARM_Morale.webp"/>
<div style={{width: 20, fontSize: 12, height: 50, color: 'rgba(255,255,255,0.7)'}}> <div style={{width: 20, fontSize: 12, height: 50, color: '#dee2e6'}}>
<i>Morale</i></div> <i>Morale</i></div>
</StyledTableCell> </StyledTableCell>
</TableRow> </TableRow>
@ -381,7 +367,7 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
name={ArmorTypeNames.BuildingHigh}/></StyledTableCell> name={ArmorTypeNames.BuildingHigh}/></StyledTableCell>
<StyledTableCell><img style={{verticalAlign: "top"}} <StyledTableCell><img style={{verticalAlign: "top"}}
src="/images/ARM_Morale.webp"/> src="/images/ARM_Morale.webp"/>
<div style={{width: 20, fontSize: 12, height: 50, color: 'rgba(255,255,255,0.7)'}}> <div style={{width: 20, fontSize: 12, height: 50, color: '#dee2e6'}}>
<i>Morale</i></div> <i>Morale</i></div>
</StyledTableCell> </StyledTableCell>
</TableRow> </TableRow>
@ -413,7 +399,7 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
</Grid2> </Grid2>
{weapon.modifiers.length > 0 && <Grid2 size={12}> {weapon.modifiers.length > 0 && <Grid2 size={12}>
<h3 style={{color: '#ffffff', fontWeight: 700}}>Modifiers</h3> <h3 style={{color: '#dee2e6', fontWeight: 700}}>Modifiers</h3>
<ModifiersProvidesTable modifiers={weapon.modifiers} modId={props.mod.id} race={props.race} affectedData={null}/> <ModifiersProvidesTable modifiers={weapon.modifiers} modId={props.mod.id} race={props.race} affectedData={null}/>
</Grid2>} </Grid2>}
{weapon.requirements !== null && !props.isDefault && props.haveReinforceMenu && {weapon.requirements !== null && !props.isDefault && props.haveReinforceMenu &&

View File

@ -3,7 +3,6 @@ import {
AccordionDetails, AccordionDetails,
AccordionSummary, AccordionSummary,
Grid2, Grid2,
Paper,
Table, Table,
TableBody, TableBody,
TableCell, TableCell,
@ -19,24 +18,10 @@ import {getIcon, ModifiersProvidesTable,} from "../ModifiersProvideTable";
import Required from "../Required"; import Required from "../Required";
import {StyledAccordion} from "../../commons/StyledAccordion"; import {StyledAccordion} from "../../commons/StyledAccordion";
import {DescriptionBox} from "../../commons/DescriptionBox"; import {DescriptionBox} from "../../commons/DescriptionBox";
import {StyledPaper} from "../../commons/StyledPaper";
import {styled} from "@mui/material/styles"; import {styled} from "@mui/material/styles";
const StyledPaper = styled(Paper)(({ theme }) => ({
background: 'rgba(255, 255, 255, 0.03)',
border: '1px solid rgba(255, 255, 255, 0.05)',
borderRadius: '12px',
overflow: 'hidden',
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-root': {
color: 'rgba(255, 255, 255, 0.85)',
},
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': {
color: '#e94560',
fontWeight: 600,
},
}));
interface IBuildingAddonProps { interface IBuildingAddonProps {
addon: IBuildingAddon, addon: IBuildingAddon,
building: IBuilding, building: IBuilding,
@ -52,16 +37,16 @@ function BuildingAddon(props: IBuildingAddonProps){
return <div className='addon-research-accordion' id={"addon-" + addon.id} ><StyledAccordion> return <div className='addon-research-accordion' id={"addon-" + addon.id} ><StyledAccordion>
<AccordionSummary <AccordionSummary
expandIcon={<ExpandMore sx={{color: '#ffffff'}}/>} sx={{color: '#dee2e6', '& .MuiAccordionSummary-content': { margin: '12px 0' }, '& .MuiAccordionSummary-content.Mui-expanded': { margin: '12px 0' }}}
expandIcon={<ExpandMore sx={{color: '#dee2e6'}}/>}
aria-controls="panel1-content" aria-controls="panel1-content"
sx={{color: '#ffffff'}}
> >
<span style={{fontSize: 20, color: '#ffffff'}}> <span style={{fontSize: 20, color: '#dee2e6', display: 'flex', alignItems: 'center'}}>
<img className="sergeantIcon" src={getIcon(addon.icon)}/> <img className="sergeantIcon" src={getIcon(addon.icon)}/>
&nbsp; {addon.name} &nbsp; {addon.name}
</span> </span>
</AccordionSummary> </AccordionSummary>
<AccordionDetails sx={{color: 'rgba(255,255,255,0.85)'}}> <AccordionDetails sx={{color: '#dee2e6'}}>
<Grid2 container spacing={2}> <Grid2 container spacing={2}>
<Grid2 size={{xs: 12, md: 4}}> <Grid2 size={{xs: 12, md: 4}}>
<TableContainer component={StyledPaper} elevation={0}> <TableContainer component={StyledPaper} elevation={0}>

View File

@ -23,10 +23,10 @@ import {StyledAccordion} from "../../commons/StyledAccordion";
import {styled} from "@mui/material/styles"; import {styled} from "@mui/material/styles";
const StyledLink = styled('a')(({ theme }) => ({ const StyledLink = styled('a')(({ theme }) => ({
color: '#e94560', color: theme.palette.mode === 'dark' ? '#dee2e6' : '#e94560',
textDecoration: 'none', textDecoration: 'none',
'&:hover': { '&:hover': {
color: '#ff6b6b', color: theme.palette.mode === 'dark' ? '#cccccc' : '#ff6b6b',
textDecoration: 'underline', textDecoration: 'underline',
}, },
})); }));
@ -45,16 +45,16 @@ function Research(props: IResearchProps){
return <div className='addon-research-accordion' id={"research-" + research.id}><StyledAccordion TransitionProps={{ unmountOnExit: true, timeout: 100 }}> return <div className='addon-research-accordion' id={"research-" + research.id}><StyledAccordion TransitionProps={{ unmountOnExit: true, timeout: 100 }}>
<AccordionSummary <AccordionSummary
expandIcon={<ExpandMore sx={{color: '#ffffff'}}/>} sx={{color: '#dee2e6', '& .MuiAccordionSummary-content': { margin: '12px 0' }, '& .MuiAccordionSummary-content.Mui-expanded': { margin: '12px 0' }}}
expandIcon={<ExpandMore sx={{color: '#dee2e6'}}/>}
aria-controls="panel1-content" aria-controls="panel1-content"
sx={{color: '#ffffff'}}
> >
<span style={{fontSize: 20, color: '#ffffff'}}> <span style={{fontSize: 20, color: '#dee2e6', display: 'flex', alignItems: 'center'}}>
<img className="sergeantIcon" src={getIcon(research.icon)}/> <img className="sergeantIcon" src={getIcon(research.icon)}/>
&nbsp; {research.name} &nbsp; {research.name}
</span> </span>
</AccordionSummary> </AccordionSummary>
<AccordionDetails sx={{color: 'rgba(255,255,255,0.85)'}}> <AccordionDetails sx={{color: '#dee2e6'}}>
<ResearchFull id={research.id} building={building}/> <ResearchFull id={research.id} building={building}/>
</AccordionDetails> </AccordionDetails>
</StyledAccordion></div> </StyledAccordion></div>
@ -71,7 +71,7 @@ export function renderAffectedResearches(researches: IResearchShort[], modId: nu
</StyledLink></span> </StyledLink></span>
} }
return <div style={{color: 'rgba(255, 255, 255, 0.85)'}}> {researches.map(rs => return <div style={{color: '#dee2e6'}}> {researches.map(rs =>
rs.buildingId == null ? <span key={rs.id}></span> : rs.buildingId == null ? <span key={rs.id}></span> :
<span key={rs.id}>Research affect: {researchLink(rs)}<br/></span> <span key={rs.id}>Research affect: {researchLink(rs)}<br/></span>
)}</div> )}</div>

View File

@ -5,7 +5,6 @@ import {
AccordionDetails, AccordionDetails,
AccordionSummary, AccordionSummary,
Grid2, Grid2,
Paper,
Table, Table,
TableBody, TableBody,
TableCell, TableCell,
@ -24,20 +23,7 @@ import {ResearchUrl, WeaponUrl} from "../../core/api";
import {IWeapon} from "../../types/IUnit"; import {IWeapon} from "../../types/IUnit";
import {styled} from '@mui/material/styles'; import {styled} from '@mui/material/styles';
import {DescriptionBox} from "../../commons/DescriptionBox"; import {DescriptionBox} from "../../commons/DescriptionBox";
import {StyledPaper} from "../../commons/StyledPaper";
const StyledPaper = styled(Paper)(({ theme }) => ({
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '12px',
overflow: 'hidden',
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-root': {
color: 'rgba(255, 255, 255, 0.9)',
},
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': {
color: '#e94560',
fontWeight: 600,
},
}));
interface IResearchFull { interface IResearchFull {

View File

@ -3,12 +3,12 @@ import {styled} from '@mui/material/styles';
export const BackButton = styled(Button)(({ theme }) => ({ export const BackButton = styled(Button)(({ theme }) => ({
marginBottom: theme.spacing(3), marginBottom: theme.spacing(3),
color: '#e94560BB', color: '#FFD700',
borderColor: 'rgba(233, 69, 96, 0.5)', borderColor: 'rgba(255, 215, 0, 0.5)',
textTransform: 'none', textTransform: 'none',
fontWeight: 600, fontWeight: 600,
'&:hover': { '&:hover': {
borderColor: '#e94560', borderColor: '#FFD700',
backgroundColor: 'rgba(233, 69, 96, 0.1)', backgroundColor: 'rgba(255, 215, 0, 0.1)',
}, },
})); }));

View File

@ -1,12 +1,12 @@
import {styled} from '@mui/material/styles'; import {styled} from '@mui/material/styles';
export const DescriptionBox = styled('div')(({ theme }) => ({ export const DescriptionBox = styled('div')(({ theme }) => ({
color: 'rgba(255, 255, 255, 0.85)', color: theme.palette.text.primary,
lineHeight: 1.7, lineHeight: 1.7,
fontSize: '0.95rem', fontSize: '0.95rem',
padding: theme.spacing(3), padding: theme.spacing(3),
background: 'rgba(255, 255, 255, 0.03)', background: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.03)' : 'rgba(0, 0, 0, 0.03)',
borderRadius: '12px', borderRadius: '12px',
border: '1px solid rgba(255, 255, 255, 0.05)', border: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.05)'}`,
whiteSpace: 'pre-wrap', whiteSpace: 'pre-wrap',
})); }));

View File

@ -2,8 +2,10 @@ import {Accordion} from '@mui/material';
import {styled} from '@mui/material/styles'; import {styled} from '@mui/material/styles';
export const StyledAccordion = styled(Accordion)(({ theme }) => ({ export const StyledAccordion = styled(Accordion)(({ theme }) => ({
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)', background: theme.palette.mode === 'dark'
border: '1px solid rgba(255, 255, 255, 0.1)', ? '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: '12px', borderRadius: '12px',
'&:before': { display: 'none' }, '&:before': { display: 'none' },
'&.Mui-expanded': { margin: '0 0 8px 0' }, '&.Mui-expanded': { margin: '0 0 8px 0' },

View File

@ -2,11 +2,11 @@ import {Link} from "@mui/material";
import {styled} from '@mui/material/styles'; import {styled} from '@mui/material/styles';
export const StyledLink = styled(Link)(({ theme }) => ({ export const StyledLink = styled(Link)(({ theme }) => ({
color: '#e94560BB', color: theme.palette.mode === 'dark' ? '#abc3ff' : '#e94560BB',
textDecoration: 'none', textDecoration: 'none',
transition: 'all 0.2s ease', transition: 'all 0.2s ease',
'&:hover': { '&:hover': {
color: '#ff6b6bBB', color: theme.palette.mode === 'dark' ? '#ffffff' : '#ff6b6bBB',
textDecoration: 'underline', textDecoration: 'underline',
}, },
})); }));

View File

@ -0,0 +1,19 @@
import {Paper} from '@mui/material';
import {styled} from '@mui/material/styles';
export const StyledPaper = styled(Paper)(({ theme }) => ({
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: '12px',
overflow: 'hidden',
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-root': {
color: theme.palette.text.primary,
},
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': {
color: theme.palette.mode === 'dark' ? '#dee2e6' : '#e94560',
fontWeight: 600,
},
}));

View File

@ -0,0 +1,48 @@
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { ThemeProvider as MuiThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import { darkTheme, lightTheme } from '../themes/theme';
type ThemeMode = 'dark' | 'light';
interface ThemeContextType {
mode: ThemeMode;
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export const ThemeProvider = ({ children }: { children: ReactNode }) => {
const [mode, setMode] = useState<ThemeMode>(() => {
const saved = localStorage.getItem('theme');
return (saved === 'light' ? 'light' : 'dark') as ThemeMode;
});
useEffect(() => {
localStorage.setItem('theme', mode);
}, [mode]);
const toggleTheme = () => {
setMode(prev => prev === 'dark' ? 'light' : 'dark');
};
const theme = mode === 'dark' ? darkTheme : lightTheme;
return (
<ThemeContext.Provider value={{ mode, toggleTheme }}>
<MuiThemeProvider theme={theme}>
<CssBaseline />
{children}
</MuiThemeProvider>
</ThemeContext.Provider>
);
};
export const useThemeContext = () => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useThemeContext must be used within a ThemeProvider');
}
return context;
};

View File

@ -1,4 +1,7 @@
import React from 'react';
import { useTheme } from '@mui/material/styles';
export const UserUrl = process.env.REACT_APP_HOST_URL + '/api/v1/user'; export const UserUrl = process.env.REACT_APP_HOST_URL + '/api/v1/user';
export const WeaponUrl = process.env.REACT_APP_HOST_URL + '/api/v1/weapon'; export const WeaponUrl = process.env.REACT_APP_HOST_URL + '/api/v1/weapon';
export const AbilityUrl = process.env.REACT_APP_HOST_URL + '/api/v1/ability'; export const AbilityUrl = process.env.REACT_APP_HOST_URL + '/api/v1/ability';
@ -8,3 +11,11 @@ export const AvailableRacesPart = process.env.REACT_APP_HOST_URL + '/api/v1/race
export const AvailableUnits = process.env.REACT_APP_HOST_URL + '/api/v1/units'; export const AvailableUnits = process.env.REACT_APP_HOST_URL + '/api/v1/units';
export const AvailableBuildings = process.env.REACT_APP_HOST_URL + '/api/v1/buildings'; export const AvailableBuildings = process.env.REACT_APP_HOST_URL + '/api/v1/buildings';
export const IconUrl = process.env.REACT_APP_HOST_URL + '/api/v1/grapics/icon/'; export const IconUrl = process.env.REACT_APP_HOST_URL + '/api/v1/grapics/icon/';
export function withTheme(Component) {
return function WithTheme(props) {
const theme = useTheme();
return <Component theme={theme} {...props} />;
};
}

View File

@ -1,5 +1,35 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
:root {
--bg-primary: #0a0a0a;
--bg-secondary: #1a1a2e;
--bg-tertiary: #16213e;
--text-primary: #dee2e6;
--text-secondary: #dee2e6;
--text-muted: rgba(255, 255, 255, 0.5);
--border-color: rgba(255, 255, 255, 0.1);
--border-light: rgba(255, 255, 255, 0.05);
--card-bg: rgba(255, 255, 255, 0.03);
--accent: #dee2e6;
--accent-light: #cccccc;
--scrollbar-track: #1a1a2e;
}
body.light-theme {
--bg-primary: #f5f5f5;
--bg-secondary: #ffffff;
--bg-tertiary: #e8e8e8;
--text-primary: rgba(0, 0, 0, 0.85);
--text-secondary: rgba(0, 0, 0, 0.6);
--text-muted: rgba(0, 0, 0, 0.5);
--border-color: rgba(0, 0, 0, 0.1);
--border-light: rgba(0, 0, 0, 0.05);
--card-bg: rgba(0, 0, 0, 0.03);
--accent: #e94560;
--accent-light: #ff6b6b;
--scrollbar-track: #e8e8e8;
}
body { body {
margin: 0; margin: 0;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
@ -7,8 +37,12 @@ body {
sans-serif; sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
background-color: #0a0a0a; background-color: var(--bg-primary);
color: rgba(255, 255, 255, 0.85); color: var(--text-primary);
}
body.light-theme code {
background: rgba(0, 0, 0, 0.1);
} }
code { code {
@ -21,7 +55,7 @@ code {
* { * {
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: #e94560 #1a1a2e; scrollbar-color: var(--accent) var(--scrollbar-track);
} }
*::-webkit-scrollbar { *::-webkit-scrollbar {
@ -29,11 +63,11 @@ code {
} }
*::-webkit-scrollbar-track { *::-webkit-scrollbar-track {
background: #1a1a2e; background: var(--scrollbar-track);
} }
*::-webkit-scrollbar-thumb { *::-webkit-scrollbar-thumb {
background-color: #e94560; background-color: var(--accent);
border-radius: 4px; border-radius: 4px;
} }

View File

@ -17,7 +17,7 @@ import {
TableCell, TableCell,
TableRow, TableRow,
Tooltip, Tooltip,
Typography, LinearProgress Typography, LinearProgress, Theme,
} from "@mui/material"; } from "@mui/material";
import ArmorType from "../classes/ArmorType"; import ArmorType from "../classes/ArmorType";
import AvTimerOutlinedIcon from '@mui/icons-material/AvTimer'; import AvTimerOutlinedIcon from '@mui/icons-material/AvTimer';
@ -34,6 +34,7 @@ import {ModifiersProvidesTable} from "../classes/ModifiersProvideTable";
import Ability from "../classes/Ability"; import Ability from "../classes/Ability";
import DeathExplosion from "../classes/DeathExplosion"; import DeathExplosion from "../classes/DeathExplosion";
import {styled} from '@mui/material/styles'; import {styled} from '@mui/material/styles';
import {withTheme} from "../core/api";
import {DescriptionBox} from "../commons/DescriptionBox"; import {DescriptionBox} from "../commons/DescriptionBox";
import {ArrowBack} from "@mui/icons-material"; import {ArrowBack} from "@mui/icons-material";
import {BackButton} from "../commons/BackButton"; import {BackButton} from "../commons/BackButton";
@ -46,51 +47,80 @@ const SectionTitle = styled(Typography)(({ theme }) => ({
})); }));
const StatsPaper = styled(Paper)(({ theme }) => ({ const StatsPaper = styled(Paper)(({ theme }) => ({
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)', background: theme.palette.mode === 'dark'
border: '1px solid rgba(255, 255, 255, 0.1)', ? '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: '12px', borderRadius: '12px',
overflow: 'hidden', overflow: 'hidden',
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-root': { '& .MuiTableBody .MuiTableRow-root .MuiTableCell-root': {
color: 'rgba(255, 255, 255, 0.9)', color: theme.palette.text.primary,
}, },
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': { '& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': {
color: '#e94560', color: theme.palette.mode === 'dark' ? '#dee2e6' : '#e94560',
fontWeight: 600, fontWeight: 600,
}, },
})); }));
const StyledDivider = styled(Divider)(({ theme }) => ({
my: 3,
borderColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)',
}));
const BuildingTitle = styled(Typography)(({ theme }) => ({
fontWeight: 800,
mb: 1,
display: 'flex',
alignItems: 'center',
gap: 2,
}));
const BuildingSubtitle = styled(Typography)(({ theme }) => ({
color: theme.palette.text.secondary,
fontWeight: 500,
}));
const UnitLink = styled(Link)(({ theme }) => ({
color: theme.palette.text.primary,
textDecoration: 'none',
'&:hover': { color: theme.palette.mode === 'dark' ? '#dee2e6' : '#e94560' },
}));
const UnitListItem = styled(ListItem)(({ theme }) => ({
color: theme.palette.text.primary,
padding: '8px 12px',
background: theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.03)',
borderRadius: '8px',
border: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)'}`,
transition: 'all 0.2s ease',
'&:hover': { background: theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.1)' : 'rgba(233, 69, 96, 0.1)', borderColor: theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.3)' : 'rgba(233, 69, 96, 0.3)' },
}));
interface UintPageState { interface UintPageState {
building: IBuilding, building: IBuilding,
mod: IMod, mod: IMod,
} }
function Unit (unit: IUnitShort, modId: number, raceId: String) { function Unit (unit: IUnitShort, modId: number, raceId: String, theme: Theme) {
return (<Grid2 key={unit.id} size={{xs: 12, md: 3}}> return (<Grid2 key={unit.id} size={{xs: 12, md: 3}}>
<Link href={"/mod/" + modId + "/race/" + raceId + "/unit/" + unit.id} sx={{color: 'rgba(255,255,255,0.85)', textDecoration: 'none', '&:hover': {color: '#e94560'}}}> <UnitLink href={"/mod/" + modId + "/race/" + raceId + "/unit/" + unit.id}>
<ListItem sx={{ <UnitListItem>
color: 'rgba(255,255,255,0.85)',
padding: '8px 12px',
background: 'rgba(255,255,255,0.03)',
borderRadius: '8px',
border: '1px solid rgba(255,255,255,0.05)',
transition: 'all 0.2s ease',
'&:hover': { background: 'rgba(233, 69, 96, 0.1)', borderColor: 'rgba(233, 69, 96, 0.3)' }
}}>
{unit.icon && <img className="unitIcon" src={IconUrl + unit.icon.replaceAll('\\', '/')}/> } {unit.icon && <img className="unitIcon" src={IconUrl + unit.icon.replaceAll('\\', '/')}/> }
{unit.name} {unit.name}
{unit.canDetect && <span>&nbsp;<img style={{verticalAlign: "top"}} {unit.canDetect && <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/DETECT_YES.webp"/></span>} src="/images/DETECT_YES.webp"/></span>}
</ListItem> </UnitListItem>
</Link> </UnitLink>
</Grid2>) </Grid2>)
} }
function Building(building: IBuilding, mod: IMod) { function Building(building: IBuilding, mod: IMod, theme: Theme) {
document.title = building.name + " — " + mod.name document.title = building.name + " — " + mod.name
const isDark = theme.palette.mode === 'dark';
let mapBuildingWeapons: Map<number, Map<number, IShortWeapon>> = new Map(); let mapBuildingWeapons: Map<number, Map<number, IShortWeapon>> = new Map();
@ -113,23 +143,14 @@ function Building(building: IBuilding, mod: IMod) {
return ( return (
<Box> <Box>
<Box sx={{ mb: 3 }}> <Box sx={{ mb: 3 }}>
<Typography variant="h4" component="h2" sx={{ <BuildingTitle variant="h4">
fontWeight: 800,
mb: 1,
display: 'flex',
alignItems: 'center',
gap: 2,
}}>
{building.icon && {building.icon &&
<img className="unitIcon" src={IconUrl + building.icon.replaceAll('\\', '/')}/>} <img className="unitIcon" src={IconUrl + building.icon.replaceAll('\\', '/')}/>}
{buildingName} {buildingName}
</Typography> </BuildingTitle>
<Typography variant="subtitle1" sx={{ <BuildingSubtitle variant="subtitle1">
color: 'rgba(255, 255, 255, 0.6)',
fontWeight: 500,
}}>
{mod.name} ({mod.version}) {mod.name} ({mod.version})
</Typography> </BuildingSubtitle>
</Box> </Box>
<Grid2 container spacing={3}> <Grid2 container spacing={3}>
@ -232,7 +253,7 @@ function Building(building: IBuilding, mod: IMod) {
<SectionTitle>Unit production</SectionTitle> <SectionTitle>Unit production</SectionTitle>
<Grid2 container spacing={2}> <Grid2 container spacing={2}>
{building.units.map(unit => {building.units.map(unit =>
Unit(unit, mod.id, building.race.id) Unit(unit, mod.id, building.race.id, theme)
)} )}
</Grid2> </Grid2>
</Grid2>} </Grid2>}
@ -271,7 +292,7 @@ function Building(building: IBuilding, mod: IMod) {
</Grid2> </Grid2>
<b className="hotkey" >Hotkey: {building.hotkey}</b> <b className="hotkey" >Hotkey: {building.hotkey}</b>
</Grid2> </Grid2>
<Divider sx={{ my: 3, borderColor: 'rgba(255, 255, 255, 0.1)' }} /> <StyledDivider />
<UnitsTable modId={mod.id}/> <UnitsTable modId={mod.id}/>
</Box>) </Box>)
} }
@ -298,9 +319,10 @@ class BuildingPage extends React.Component<any, UintPageState> {
if (this.state != null && this.state.building != null && this.state.mod != null) { if (this.state != null && this.state.building != null && this.state.mod != null) {
const backRef = "/mod/" + this.props.match.params.modId + "/race/" + this.props.match.params.raceId const backRef = "/mod/" + this.props.match.params.modId + "/race/" + this.props.match.params.raceId
const { theme } = this.props;
return ( return (
<Container maxWidth="lg" sx={{ py: 4 }}> <Container maxWidth="lg">
<BackButton <BackButton
variant="outlined" variant="outlined"
startIcon={<ArrowBack/>} startIcon={<ArrowBack/>}
@ -308,7 +330,7 @@ class BuildingPage extends React.Component<any, UintPageState> {
> >
Back to race Back to race
</BackButton> </BackButton>
{Building(this.state.building, this.state.mod)} {Building(this.state.building, this.state.mod, theme)}
</Container> </Container>
); );
} else { } else {
@ -321,4 +343,4 @@ class BuildingPage extends React.Component<any, UintPageState> {
} }
} }
export default withRouter(BuildingPage); export default withRouter(withTheme(BuildingPage));

View File

@ -1,6 +1,7 @@
import {AvailableMods, AvailableRacesPart, AvailableUnits, IconUrl} from "../core/api"; import {AvailableMods, AvailableRacesPart, AvailableUnits, IconUrl} from "../core/api";
import React, { useState } from "react"; import React, { useState } from "react";
import {withRouter} from "../core/withrouter"; import {withRouter} from "../core/withrouter";
import {withTheme} from "../core/api";
import {IMod} from "../types/Imod"; import {IMod} from "../types/Imod";
import { import {
ArrowBack, ArrowBack,
@ -19,6 +20,12 @@ import UnitsTable from "../classes/UnitsTable";
import {BackButton} from "../commons/BackButton"; import {BackButton} from "../commons/BackButton";
const ModNotFound = styled(Typography)(({ theme }) => ({
color: theme.palette.text.secondary,
}));
interface ModPageState { interface ModPageState {
mod: IMod | null, mod: IMod | null,
loading: boolean, loading: boolean,
@ -60,6 +67,7 @@ class ModPage extends React.Component<any, ModPageState> {
if (this.state != null && this.state.mod != null) { if (this.state != null && this.state.mod != null) {
document.title = this.state.mod.name + " — ModWiki"; document.title = this.state.mod.name + " — ModWiki";
const isDark = this.props.theme.palette.mode === 'dark';
return ( return (
<Container maxWidth="lg"> <Container maxWidth="lg">
@ -71,12 +79,20 @@ class ModPage extends React.Component<any, ModPageState> {
Back to mods list Back to mods list
</BackButton> </BackButton>
<Typography variant="h6" sx={{ <Typography variant="h3" component="h1" sx={{
color: 'rgba(255, 255, 255, 0.6)', fontWeight: 800,
fontWeight: 500, mb: 1,
paddingBottom: 1 WebkitBackgroundClip: isDark ? 'text' : 'initial',
WebkitTextFillColor: isDark ? '#dee2e6' : 'initial',
color: isDark ? '#dee2e6' : '#000000',
}}> }}>
{this.state.mod.name} ({this.state.mod.version}) {this.state.mod.name}
</Typography>
<Typography variant="h6" sx={{
color: this.props.theme.palette.text.secondary,
fontWeight: 500,
}}>
({this.state.mod.version})
</Typography> </Typography>
<Box> <Box>
@ -87,9 +103,9 @@ class ModPage extends React.Component<any, ModPageState> {
} else { } else {
return ( return (
<Container maxWidth="lg" sx={{ py: 8, textAlign: 'center' }}> <Container maxWidth="lg" sx={{ py: 8, textAlign: 'center' }}>
<Typography variant="h5" sx={{ color: 'rgba(255, 255, 255, 0.5)' }}> <ModNotFound variant="h5">
Mod not found Mod not found
</Typography> </ModNotFound>
</Container> </Container>
); );
} }
@ -98,4 +114,4 @@ class ModPage extends React.Component<any, ModPageState> {
export default withRouter(ModPage); export default withRouter(withTheme(ModPage));

View File

@ -23,7 +23,9 @@ import { styled } from '@mui/material/styles';
const PageHeader = styled(Paper)(({ theme }) => ({ const PageHeader = styled(Paper)(({ theme }) => ({
padding: theme.spacing(6, 4), padding: theme.spacing(6, 4),
marginBottom: theme.spacing(6), marginBottom: theme.spacing(6),
background: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)', 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', borderRadius: '16px',
position: 'relative', position: 'relative',
overflow: 'hidden', overflow: 'hidden',
@ -34,7 +36,7 @@ const PageHeader = styled(Paper)(({ theme }) => ({
left: 0, left: 0,
right: 0, right: 0,
bottom: 0, bottom: 0,
background: 'radial-gradient(circle at 30% 50%, rgba(233, 69, 96, 0.1) 0%, transparent 50%)', background: 'radial-gradient(circle at 30% 50%, rgba(255, 255, 255, 0.05) 0%, #dee2e6 50%)',
pointerEvents: 'none', pointerEvents: 'none',
}, },
})); }));
@ -43,8 +45,10 @@ const ModCard = styled(Card)(({ theme }) => ({
height: '100%', height: '100%',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)', background: theme.palette.mode === 'dark'
border: '1px solid rgba(255, 255, 255, 0.1)', ? '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', borderRadius: '16px',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
position: 'relative', position: 'relative',
@ -56,14 +60,15 @@ const ModCard = styled(Card)(({ theme }) => ({
left: 0, left: 0,
right: 0, right: 0,
height: '3px', height: '3px',
background: 'linear-gradient(90deg, #e94560 0%, #ff6b6b 100%)', background: theme.palette.mode === 'dark'
? 'linear-gradient(90deg, #dee2e6 0%, #cccccc 100%)'
: 'linear-gradient(90deg, #e94560 0%, #ff6b6b 100%)',
transform: 'scaleX(0)', transform: 'scaleX(0)',
transition: 'transform 0.3s ease', transition: 'transform 0.3s ease',
}, },
'&:hover': { '&:hover': {
transform: 'translateY(-8px)', boxShadow: theme.palette.mode === 'dark' ? '0 20px 40px rgba(255, 255, 255, 0.1)' : '0 20px 40px rgba(233, 69, 96, 0.2)',
boxShadow: '0 20px 40px rgba(233, 69, 96, 0.2)', borderColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.3)' : 'rgba(233, 69, 96, 0.3)',
borderColor: 'rgba(233, 69, 96, 0.3)',
'&::before': { '&::before': {
transform: 'scaleX(1)', transform: 'scaleX(1)',
}, },
@ -75,16 +80,79 @@ const VersionLink = styled(NavLink)(({ theme }) => ({
alignItems: 'center', alignItems: 'center',
padding: theme.spacing(1.5, 2), padding: theme.spacing(1.5, 2),
marginBottom: theme.spacing(1), marginBottom: theme.spacing(1),
background: 'rgba(255, 255, 255, 0.05)', background: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.05)',
borderRadius: '10px', borderRadius: '10px',
textDecoration: 'none', textDecoration: 'none',
color: 'inherit', color: 'inherit',
transition: 'all 0.2s ease', transition: 'all 0.2s ease',
border: '1px solid rgba(255, 255, 255, 0.1)', border: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
'&:hover': { '&:hover': {
background: 'rgba(233, 69, 96, 0.15)', background: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(233, 69, 96, 0.15)',
borderColor: 'rgba(233, 69, 96, 0.3)', borderColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.3)' : 'rgba(233, 69, 96, 0.3)',
transform: 'translateX(4px)', },
}));
const ModTitle = styled(Typography)(({ theme }) => ({
fontWeight: 700,
mb: 1,
background: theme.palette.mode === 'dark'
? 'linear-gradient(135deg, #dee2e6 0%, #e0e0e0 100%)'
: 'linear-gradient(135deg, #000000 0%, #333333 100%)',
WebkitBackgroundClip: theme.palette.mode === 'dark' ? 'text' : 'initial',
WebkitTextFillColor: theme.palette.mode === 'dark' ? '#dee2e6' : 'initial',
color: theme.palette.mode === 'dark' ? '#dee2e6' : '#000000',
}));
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(233, 69, 96, 0.2)',
color: theme.palette.mode === 'dark' ? '#dee2e6' : '#ff6b6b',
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 }) => ({
width: '100%',
color: '#FFD700',
borderColor: 'rgba(255, 215, 0, 0.5)',
fontWeight: 600,
borderRadius: '10px',
textTransform: 'none',
fontSize: '1rem',
py: 1.5,
'&:hover': {
borderColor: '#FFD700',
backgroundColor: 'rgba(255, 215, 0, 0.1)',
boxShadow: '0 8px 20px rgba(255, 215, 0, 0.2)',
}, },
})); }));
@ -128,88 +196,48 @@ function Mods({ mods }: ModsProps) {
const { latest, hasBeta, betaVersion } = getLatestVersion(modName); const { latest, hasBeta, betaVersion } = getLatestVersion(modName);
return ( return (
<ModCard sx={{ animationDelay: `${index * 100}ms` }}> <ModCard>
<CardContent sx={{ flexGrow: 1, p: 3 }}> <CardContent sx={{ flexGrow: 1, p: 3 }}>
<Box sx={{ mb: 2 }}> <Box sx={{ mb: 2 }}>
<Typography variant="h5" component="h2" sx={{ <ModTitle variant="h5">
fontWeight: 700,
mb: 1,
background: 'linear-gradient(135deg, #ffffff 0%, #e0e0e0 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
}}>
{modName} {modName}
</Typography> </ModTitle>
<Typography variant="body2" sx={{
color: 'rgba(255, 255, 255, 0.7)',
lineHeight: 1.6,
}}>
</Typography>
</Box> </Box>
<Box sx={{ display: 'flex', gap: 1, mb: 2 }}> <Box sx={{ display: 'flex', gap: 1, mb: 2 }}>
<Chip <VersionsChip
label={`${sameMods.length} versions`} label={`${sameMods.length} versions`}
size="small" size="small"
sx={{
backgroundColor: 'rgba(233, 69, 96, 0.2)',
color: '#ff6b6b',
fontWeight: 600,
}}
/> />
{hasBeta && (
<Chip
label="Beta"
size="small"
color="warning"
sx={{ fontWeight: 600 }}
/>
)}
</Box> </Box>
<Divider sx={{ my: 2, borderColor: 'rgba(255, 255, 255, 0.1)' }} />
<Typography variant="subtitle2" sx={{
color: 'rgba(255, 255, 255, 0.9)',
fontWeight: 600,
mb: 1.5,
}}>
Main versions:
</Typography>
{sameMods.filter(m => !m.isBeta).map(mod => ( {sameMods.filter(m => !m.isBeta).map(mod => (
<VersionLink key={mod.id} to={"/mod/" + mod.id} state={mod.id}> <VersionLink key={mod.id} to={"/mod/" + mod.id} state={mod.id}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}> <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
<Typography variant="body2" sx={{ fontWeight: 600, color: 'rgba(255, 255, 255, 0.7)' }}> <VersionText variant="body2">
Version {mod.version} Version {mod.version}
</Typography> </VersionText>
<Typography variant="caption" sx={{ <VersionArrow variant="caption">
color: 'rgba(255, 255, 255, 0.5)',
}}>
</Typography> </VersionArrow>
</Box> </Box>
</VersionLink> </VersionLink>
))} ))}
{hasBeta && ( {hasBeta && (
<Box sx={{ mt: 2 }}> <Box sx={{ mt: 2 }}>
<Typography variant="subtitle2" sx={{ <BetaVersionsLabel variant="subtitle2">
color: 'rgba(255, 193, 7, 0.9)',
fontWeight: 600,
mb: 1,
}}>
Beta versions: Beta versions:
</Typography> </BetaVersionsLabel>
{betaVersion && ( {betaVersion && (
<VersionLink to={"/mod/" + betaVersion.id} state={betaVersion.id}> <VersionLink to={"/mod/" + betaVersion.id} state={betaVersion.id}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}> <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
<Typography variant="body2" sx={{ fontWeight: 600 }}> <Typography variant="body2" sx={{ fontWeight: 600 }}>
Version {betaVersion.version} (Beta) Version {betaVersion.version} (Beta)
</Typography> </Typography>
<Typography variant="caption" sx={{ color: 'rgba(255, 255, 255, 0.5)' }}> <VersionArrow variant="caption">
</Typography> </VersionArrow>
</Box> </Box>
</VersionLink> </VersionLink>
)} )}
@ -226,18 +254,17 @@ function Mods({ mods }: ModsProps) {
state={latest?.id} state={latest?.id}
sx={{ sx={{
width: '100%', width: '100%',
color: '#e94560', color: '#FFD700',
borderColor: 'rgba(233, 69, 96, 0.5)', borderColor: 'rgba(255, 215, 0, 0.5)',
fontWeight: 600, fontWeight: 600,
borderRadius: '10px', borderRadius: '10px',
textTransform: 'none', textTransform: 'none',
fontSize: '1rem', fontSize: '1rem',
py: 1.5, py: 1.5,
'&:hover': { '&:hover': {
borderColor: '#e94560', borderColor: '#FFD700',
backgroundColor: 'rgba(233, 69, 96, 0.1)', backgroundColor: 'rgba(255, 215, 0, 0.1)',
transform: 'translateY(-2px)', boxShadow: '0 8px 20px rgba(255, 215, 0, 0.2)',
boxShadow: '0 8px 20px rgba(233, 69, 96, 0.2)',
}, },
}} }}
> >

View File

@ -1,5 +1,5 @@
import {AvailableBuildings, AvailableMods, AvailableRacesPart, AvailableUnits, IconUrl} from "../core/api"; import {AvailableBuildings, AvailableMods, AvailableRacesPart, AvailableUnits, IconUrl} from "../core/api";
import React, {useState} from "react"; import React from "react";
import {withRouter} from "../core/withrouter"; import {withRouter} from "../core/withrouter";
import {IMod} from "../types/Imod"; import {IMod} from "../types/Imod";
import {Irace} from "../types/Irace"; import {Irace} from "../types/Irace";
@ -18,11 +18,13 @@ import {
ListItem, ListItem,
Paper, Paper,
Typography, Typography,
Theme,
} from "@mui/material"; } from "@mui/material";
import {ArrowBack, ExpandMore} from "@mui/icons-material"; import {ArrowBack, ExpandMore} from "@mui/icons-material";
import {IRaceUnits, IUnitShort} from "../types/IUnitShort"; import {IRaceUnits, IUnitShort} from "../types/IUnitShort";
import {IBuildingShort, IRaceBuildings} from "../types/IBuildingShort"; import {IBuildingShort, IRaceBuildings} from "../types/IBuildingShort";
import {styled} from '@mui/material/styles'; import {styled} from '@mui/material/styles';
import {withTheme} from "../core/api";
import {StyledLink} from "../commons/StyledLink"; import {StyledLink} from "../commons/StyledLink";
import {BackButton} from "../commons/BackButton"; import {BackButton} from "../commons/BackButton";
@ -33,19 +35,30 @@ const SectionTitle = styled(Typography)(({ theme }) => ({
})); }));
const UnitCard = styled(Paper)(({ theme }) => ({ const UnitCard = styled(Paper)(({ theme }) => ({
background: 'rgba(255, 255, 255, 0.03)', background: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.03)' : 'rgba(0, 0, 0, 0.03)',
border: '1px solid rgba(255, 255, 255, 0.05)', border: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.05)'}`,
borderRadius: '10px', borderRadius: '10px',
padding: theme.spacing(1.5, 2), padding: theme.spacing(1.5, 2),
transition: 'all 0.2s ease', transition: 'all 0.2s ease',
'&:hover': { '&:hover': {
background: 'rgba(233, 69, 96, 0.1)', background: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(233, 69, 96, 0.1)',
borderColor: 'rgba(233, 69, 96, 0.3)', borderColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.3)' : 'rgba(233, 69, 96, 0.3)',
}, },
})); }));
const BuildingCard = styled(UnitCard)(({ theme }) => ({})); const BuildingCard = styled(UnitCard)(({ theme }) => ({}));
const UnitLink = styled(Link)(({ theme }) => ({
color: theme.palette.text.primary,
textDecoration: 'none',
display: 'block',
'&:hover': { color: theme.palette.mode === 'dark' ? '#dee2e6' : '#e94560' }
}));
const LoadingText = styled(Typography)(({ theme }) => ({
color: theme.palette.text.secondary,
}));
interface RacePageState { interface RacePageState {
mod: IMod, mod: IMod,
race: Irace, race: Irace,
@ -53,29 +66,29 @@ interface RacePageState {
} }
function Unit(unit: IUnitShort, modId: number, raceId: String) { function Unit(unit: IUnitShort, modId: number, raceId: String, theme: Theme) {
const isDark = theme.palette.mode === 'dark';
const borderColor = isDark ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)';
const bgColor = isDark ? 'rgba(255,255,255,0.02)' : 'rgba(0,0,0,0.02)';
return (<Link href={"/mod/" + modId + "/race/" + raceId + "/unit/" + unit.id} sx={{ return (
color: 'rgba(255,255,255,0.85)', <UnitLink href={"/mod/" + modId + "/race/" + raceId + "/unit/" + unit.id}>
textDecoration: 'none',
display: 'block',
'&:hover': { color: '#e94560' }
}}>
<ListItem sx={{ <ListItem sx={{
color: 'rgba(255,255,255,0.85)', color: theme.palette.text.primary,
padding: '6px 12px', padding: '6px 12px',
background: 'rgba(255,255,255,0.02)', background: bgColor,
borderRadius: '8px', borderRadius: '8px',
border: '1px solid rgba(255,255,255,0.05)', border: `1px solid ${borderColor}`,
transition: 'all 0.2s ease', transition: 'all 0.2s ease',
'&:hover': { background: 'rgba(233, 69, 96, 0.1)', borderColor: 'rgba(233, 69, 96, 0.3)' } '&:hover': { background: isDark ? 'rgba(255,255,255,0.1)' : 'rgba(233, 69, 96, 0.1)', borderColor: isDark ? 'rgba(255,255,255,0.3)' : 'rgba(233, 69, 96, 0.3)' }
}}> }}>
{unit.icon && <img className="unitIcon" src={IconUrl + unit.icon.replaceAll('\\', '/')}/>} {unit.icon && <img className="unitIcon" src={IconUrl + unit.icon.replaceAll('\\', '/')}/>}
{unit.name} {unit.name}
{unit.canDetect && <span>&nbsp;<img style={{verticalAlign: "top"}} {unit.canDetect && <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/DETECT_YES.webp"/></span>} src="/images/DETECT_YES.webp"/></span>}
</ListItem> </ListItem>
</Link>) </UnitLink>
)
} }
function UnitSmall(unit: IUnitShort, modId: number, raceId: String) { function UnitSmall(unit: IUnitShort, modId: number, raceId: String) {
@ -94,13 +107,14 @@ function UnitSmall(unit: IUnitShort, modId: number, raceId: String) {
src="/images/DETECT_YES.webp"/></span>}<br/></StyledLink>) src="/images/DETECT_YES.webp"/></span>}<br/></StyledLink>)
} }
function Building(building: IBuildingShort, modId: number, raceId: String) { function Building(building: IBuildingShort, modId: number, raceId: String, theme: Theme) {
const isDark = theme.palette.mode === 'dark';
return (<Grid2 size={{xs: 12, md: 3}}> return (<Grid2 size={{xs: 12, md: 3}}>
<Link href={"/mod/" + modId + "/race/" + raceId + "/building/" + building.id} sx={{ <Link href={"/mod/" + modId + "/race/" + raceId + "/building/" + building.id} sx={{
textDecoration: 'none', textDecoration: 'none',
display: 'block', display: 'block',
'&:hover': { color: '#e94560' } '&:hover': { color: isDark ? '#dee2e6' : '#e94560' }
}}> }}>
<BuildingCard elevation={0}> <BuildingCard elevation={0}>
<ListItem sx={{ <ListItem sx={{
@ -128,7 +142,7 @@ interface UnitsState {
buildings: IRaceBuildings | null, buildings: IRaceBuildings | null,
} }
class Units extends React.Component<UnitsProps, UnitsState> { class Units extends React.Component<UnitsProps & { theme: Theme }, UnitsState> {
constructor(props: any) { constructor(props: any) {
@ -157,7 +171,8 @@ class Units extends React.Component<UnitsProps, UnitsState> {
} }
render() { render() {
const { theme } = this.props;
const isDark = theme.palette.mode === 'dark';
if (this.state && this.state.units) { if (this.state && this.state.units) {
@ -165,65 +180,73 @@ class Units extends React.Component<UnitsProps, UnitsState> {
document.title = this.state.buildings?.race.name document.title = this.state.buildings?.race.name
} }
const accordionSx = { const accordionSx = isDark ? {
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)', background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
border: '1px solid rgba(255, 255, 255, 0.1)', border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '12px', borderRadius: '12px',
'&:before': { display: 'none' }, '&:before': { display: 'none' },
'&.Mui-expanded': { margin: '0 0 8px 0' }, '&.Mui-expanded': { margin: '0 0 8px 0' },
} : {
background: 'linear-gradient(145deg, #ffffff 0%, #f5f5f5 100%)',
border: '1px solid rgba(0, 0, 0, 0.1)',
borderRadius: '12px',
'&:before': { display: 'none' },
'&.Mui-expanded': { margin: '0 0 8px 0' },
}; };
return (this.state.buildings != null ? return (this.state.buildings != null ?
<Box> <Box>
<Grid2 container spacing={2}> <Grid2 container spacing={2}>
{this.state.buildings.buildings.map(building => Building(building, this.props.modId, this.props.raceId))} {this.state.buildings.buildings.map(building => Building(building, this.props.modId, this.props.raceId, theme))}
</Grid2> </Grid2>
{this.state.buildings.buildingsAdvanced.length > 0 && <Box sx={{ mt: 3 }}> {this.state.buildings.buildingsAdvanced.length > 0 && <Box sx={{ mt: 3 }}>
<Divider sx={{ my: 2, borderColor: 'rgba(255, 255, 255, 0.1)' }} /> <Divider sx={{ my: 2, borderColor: isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)' }} />
<SectionTitle>Advanced buildings</SectionTitle> <SectionTitle>Advanced buildings</SectionTitle>
<Grid2 container spacing={2}> <Grid2 container spacing={2}>
{this.state.buildings.buildingsAdvanced.map(building => Building(building, this.props.modId, this.props.raceId))} {this.state.buildings.buildingsAdvanced.map(building => Building(building, this.props.modId, this.props.raceId, theme))}
</Grid2><br/></Box>} </Grid2><br/></Box>}
<Box sx={{ mt: 3 }}> <Box sx={{ mt: 3 }}>
<Accordion sx={accordionSx}> <Accordion sx={accordionSx}>
<AccordionSummary <AccordionSummary
expandIcon={<ExpandMore sx={{color: '#ffffff'}}/>} expandIcon={<ExpandMore sx={{color: isDark ? '#dee2e6' : '#000000'}}/>}
aria-controls="units-accordion" aria-controls="units-accordion"
id="units-accordion" id="units-accordion"
sx={{color: '#ffffff'}} sx={{color: isDark ? '#dee2e6' : '#000000', '& .MuiAccordionSummary-content': { margin: '12px 0' }, '& .MuiAccordionSummary-content.Mui-expanded': { margin: '12px 0' }}}
> >
<SectionTitle sx={{ mb: 0 }}>All units</SectionTitle> <SectionTitle sx={{ mb: 0, display: 'flex', alignItems: 'center' }}>All units</SectionTitle>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>
<Grid2 container spacing={2}> <Grid2 container spacing={2}>
<Grid2 size={{xs: 12, md: 4}}> <Grid2 size={{xs: 12, md: 4}}>
<Typography variant="h6" sx={{ color: '#ffffff', fontWeight: 600, mb: 2 }}>Infantry</Typography> <Typography variant="h6" sx={{ color: isDark ? '#dee2e6' : '#000000', fontWeight: 600, mb: 2 }}>Infantry</Typography>
{this.state.units.infantry.map(unit => Unit(unit, this.props.modId, this.props.raceId))} {this.state.units.infantry.map(unit => Unit(unit, this.props.modId, this.props.raceId, theme))}
</Grid2> </Grid2>
<Grid2 size={{xs: 12, md: 4}}> <Grid2 size={{xs: 12, md: 4}}>
<Typography variant="h6" sx={{ color: '#ffffff', fontWeight: 600, mb: 2 }}>Tech</Typography> <Typography variant="h6" sx={{ color: isDark ? '#dee2e6' : '#000000', fontWeight: 600, mb: 2 }}>Tech</Typography>
<List> <List>
{this.state.units.tech.map(unit => Unit(unit, this.props.modId, this.props.raceId))} {this.state.units.tech.map(unit => Unit(unit, this.props.modId, this.props.raceId, theme))}
</List> </List>
</Grid2> </Grid2>
<Grid2 size={{xs: 12, md: 4}}> <Grid2 size={{xs: 12, md: 4}}>
<Typography variant="h6" sx={{ color: '#ffffff', fontWeight: 600, mb: 2 }}>Support</Typography> <Typography variant="h6" sx={{ color: isDark ? '#dee2e6' : '#000000', fontWeight: 600, mb: 2 }}>Support</Typography>
<List> <List>
{this.state.units.support.map(unit => Unit(unit, this.props.modId, this.props.raceId))} {this.state.units.support.map(unit => Unit(unit, this.props.modId, this.props.raceId, theme))}
</List> </List>
</Grid2> </Grid2>
</Grid2> </Grid2>
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
</Box> </Box>
</Box> : <Box sx={{ color: 'rgba(255, 255, 255, 0.5)' }}>Loading</Box> </Box> : <LoadingText>Loading</LoadingText>
) )
} else { } else {
return <Box sx={{ color: 'rgba(255, 255, 255, 0.5)' }}>Loading...</Box>; return <LoadingText>Loading...</LoadingText>;
} }
} }
} }
const UnitsWithTheme = withTheme(Units) as React.ComponentType<UnitsProps>;
class RacePageFast extends React.Component<any, RacePageState> { class RacePageFast extends React.Component<any, RacePageState> {
constructor(props: any) { constructor(props: any) {
@ -252,9 +275,11 @@ class RacePageFast extends React.Component<any, RacePageState> {
if (this.state != null && this.state.mod != null && this.state.race != null) { if (this.state != null && this.state.mod != null && this.state.race != null) {
const backRef = "/mod/" + this.state.mod.id const backRef = "/mod/" + this.state.mod.id
const theme = this.props.theme;
const isDark = theme.palette.mode === 'dark';
return ( return (
<Container maxWidth="lg" sx={{ py: 4 }}> <Container maxWidth="lg">
<BackButton <BackButton
variant="outlined" variant="outlined"
startIcon={<ArrowBack/>} startIcon={<ArrowBack/>}
@ -267,26 +292,29 @@ class RacePageFast extends React.Component<any, RacePageState> {
<Typography variant="h3" component="h1" sx={{ <Typography variant="h3" component="h1" sx={{
fontWeight: 800, fontWeight: 800,
mb: 1, mb: 1,
background: 'linear-gradient(135deg, #ffffff 0%, #e0e0e0 100%)', background: isDark
WebkitBackgroundClip: 'text', ? 'linear-gradient(135deg, #dee2e6 0%, #e0e0e0 100%)'
WebkitTextFillColor: 'transparent', : 'linear-gradient(135deg, #000000 0%, #333333 100%)',
WebkitBackgroundClip: isDark ? 'text' : 'initial',
WebkitTextFillColor: isDark ? '#dee2e6' : 'initial',
color: isDark ? '#dee2e6' : '#000000',
}}> }}>
{this.state.race.name} {this.state.race.name}
</Typography> </Typography>
<Typography variant="h6" sx={{ <Typography variant="h6" sx={{
color: 'rgba(255, 255, 255, 0.6)', color: theme.palette.text.secondary,
fontWeight: 500, fontWeight: 500,
}}> }}>
{this.state.mod.name} ({this.state.mod.version}) {this.state.mod.name} ({this.state.mod.version})
</Typography> </Typography>
</Box> </Box>
<Units raceId={this.state.race.id} modId={this.state.mod.id}/> <UnitsWithTheme raceId={this.state.race.id} modId={this.state.mod.id}/>
</Container> </Container>
); );
} else { } else {
return ( return (
<Container maxWidth="lg" sx={{ py: 4 }}> <Container maxWidth="lg">
<LinearProgress sx={{ width: '200px', mx: 'auto', display: 'block' }} /> <LinearProgress sx={{ width: '200px', mx: 'auto', display: 'block' }} />
</Container> </Container>
); );
@ -294,4 +322,4 @@ class RacePageFast extends React.Component<any, RacePageState> {
} }
} }
export default withRouter(RacePageFast); export default withRouter(withTheme(RacePageFast));

View File

@ -18,7 +18,7 @@ import {
TableCell, TableCell,
TableRow, TableRow,
Tooltip, Tooltip,
Typography, Typography, Theme,
} from "@mui/material"; } from "@mui/material";
import {ArrowBack, ExpandMore} from "@mui/icons-material"; import {ArrowBack, ExpandMore} from "@mui/icons-material";
import ArmorType from "../classes/ArmorType"; import ArmorType from "../classes/ArmorType";
@ -34,6 +34,7 @@ import {ModifiersProvidesTable} from "../classes/ModifiersProvideTable";
import Ability from "../classes/Ability"; import Ability from "../classes/Ability";
import DeathExplosion from "../classes/DeathExplosion"; import DeathExplosion from "../classes/DeathExplosion";
import {styled} from '@mui/material/styles'; import {styled} from '@mui/material/styles';
import {withTheme} from "../core/api";
import {DescriptionBox} from "../commons/DescriptionBox"; import {DescriptionBox} from "../commons/DescriptionBox";
import {BackButton} from "../commons/BackButton"; import {BackButton} from "../commons/BackButton";
@ -44,19 +45,50 @@ const SectionTitle = styled(Typography)(({ theme }) => ({
})); }));
const StatsPaper = styled(Paper)(({ theme }) => ({ const StatsPaper = styled(Paper)(({ theme }) => ({
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)', background: theme.palette.mode === 'dark'
border: '1px solid rgba(255, 255, 255, 0.1)', ? '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: '12px', borderRadius: '12px',
overflow: 'hidden', overflow: 'hidden',
'& .MuiTableBody .MuiTableRow-root': { '& .MuiTableBody .MuiTableRow-root': {
'&:last-child td, &:last-child th': { border: 0 }, '&:last-child td, &:last-child th': { border: 0 },
}, },
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': { '& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': {
color: '#e94560', color: theme.palette.mode === 'dark' ? '#dee2e6' : '#e94560',
fontWeight: 600, fontWeight: 600,
}, },
})); }));
const SergeantAccordion = styled(Accordion)(({ theme }) => ({
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: '12px',
mb: 1,
'&:before': { display: 'none' },
'&.Mui-expanded': { margin: '0 0 8px 0' },
}));
const StyledDivider = styled(Divider)(({ theme }) => ({
my: 3,
borderColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)',
}));
const UnitTitle = styled(Typography)(({ theme }) => ({
fontWeight: 800,
mb: 1,
display: 'flex',
alignItems: 'center',
gap: 2,
}));
const UnitSubtitle = styled(Typography)(({ theme }) => ({
color: theme.palette.text.secondary,
fontWeight: 500,
}));
interface UintPageState { interface UintPageState {
unit: IUnit, unit: IUnit,
@ -64,10 +96,12 @@ interface UintPageState {
} }
function Unit(unit: IUnit, mod: IMod) { function Unit(unit: IUnit, mod: IMod, theme: Theme) {
document.title = unit.name + " — " + mod.name document.title = unit.name + " — " + mod.name
const isDark = theme.palette.mode === 'dark';
const morale = (unit.moraleMax !== null) ? <span> const morale = (unit.moraleMax !== null) ? <span>
<img style={{verticalAlign: "top"}} src="/images/ARM_Morale.webp"/>&nbsp;{unit.moraleMax} <img style={{verticalAlign: "top"}} src="/images/ARM_Morale.webp"/>&nbsp;{unit.moraleMax}
+{unit.moraleRegeneration}/s +{unit.moraleRegeneration}/s
@ -98,7 +132,7 @@ function Unit(unit: IUnit, mod: IMod) {
const SergeantShort = (props: sergeantProps) => { const SergeantShort = (props: sergeantProps) => {
return ( return (
<span style={{fontSize: 20, color: '#ffffff'}}> <span style={{fontSize: 20, color: isDark ? '#dee2e6' : '#000000', display: 'flex', alignItems: 'center'}}>
{props.icon && <img className="sergeantIcon" src={IconUrl + props.icon.replaceAll('\\', '/')}/> } {props.icon && <img className="sergeantIcon" src={IconUrl + props.icon.replaceAll('\\', '/')}/> }
&nbsp; {props.name} &nbsp; {props.name}
{props.canDetect && <span>&nbsp;<img style={{verticalAlign: "top"}} {props.canDetect && <span>&nbsp;<img style={{verticalAlign: "top"}}
@ -111,23 +145,14 @@ function Unit(unit: IUnit, mod: IMod) {
return ( return (
<Box> <Box>
<Box sx={{ mb: 3 }}> <Box sx={{ mb: 3 }}>
<Typography variant="h4" component="h2" sx={{ <UnitTitle variant="h4">
fontWeight: 800,
mb: 1,
display: 'flex',
alignItems: 'center',
gap: 2,
}}>
{unit.icon && {unit.icon &&
<img className="unitIcon" src={IconUrl + unit.icon.replaceAll('\\', '/')}/>} <img className="unitIcon" src={IconUrl + unit.icon.replaceAll('\\', '/')}/>}
{unit.name} {unit.name}
</Typography> </UnitTitle>
<Typography variant="subtitle1" sx={{ <UnitSubtitle variant="subtitle1">
color: 'rgba(255, 255, 255, 0.6)',
fontWeight: 500,
}}>
{mod.name} ({mod.version}) {mod.name} ({mod.version})
</Typography> </UnitSubtitle>
</Box> </Box>
<Grid2 container spacing={3}> <Grid2 container spacing={3}>
@ -297,22 +322,15 @@ function Unit(unit: IUnit, mod: IMod) {
</Grid2>} </Grid2>}
<Grid2 size={12}> <Grid2 size={12}>
{unit.sergeants.map(s => {unit.sergeants.map(s =>
<Accordion key={s.id} sx={{ <SergeantAccordion key={s.id}>
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '12px',
mb: 1,
'&:before': { display: 'none' },
'&.Mui-expanded': { margin: '0 0 8px 0' },
}}>
<AccordionSummary <AccordionSummary
sx={{color: '#ffffff'}}> sx={{color: isDark ? '#dee2e6' : '#000000', '& .MuiAccordionSummary-content': { margin: '12px 0' }, '& .MuiAccordionSummary-content.Mui-expanded': { margin: '12px 0' }}}>
<SergeantShort name={s.name} icon={s.icon} canDetect={s.detectRadius > 0}/> <SergeantShort name={s.name} icon={s.icon} canDetect={s.detectRadius > 0}/>
</AccordionSummary> </AccordionSummary>
<AccordionDetails sx={{color: 'rgba(255, 255, 255, 0.85)'}}> <AccordionDetails sx={{color: theme.palette.text.primary}}>
<Sergeant mod={mod} sergeant={s} race={unit.race}/> <Sergeant mod={mod} sergeant={s} race={unit.race}/>
</AccordionDetails> </AccordionDetails>
</Accordion>)} </SergeantAccordion>)}
</Grid2> </Grid2>
{unit.moraleMax !== null && !(unit.moraleBrakeModifiers.find(m => m.reference.includes("accuracy_weapon_modifier") && m.value === 0.2) !== undefined && {unit.moraleMax !== null && !(unit.moraleBrakeModifiers.find(m => m.reference.includes("accuracy_weapon_modifier") && m.value === 0.2) !== undefined &&
@ -355,7 +373,7 @@ function Unit(unit: IUnit, mod: IMod) {
<Box sx={{ mt: 3, mb: 2, fontWeight: 600, fontSize: '0.85rem' }}> <Box sx={{ mt: 3, mb: 2, fontWeight: 600, fontSize: '0.85rem' }}>
Hotkey: {unit.hotkey} Hotkey: {unit.hotkey}
</Box> </Box>
<Divider sx={{ my: 3, borderColor: 'rgba(255, 255, 255, 0.1)' }} /> <StyledDivider />
<UnitsTable modId={mod.id}/> <UnitsTable modId={mod.id}/>
</Box> </Box>
) )
@ -387,9 +405,10 @@ class UnitPage extends React.Component<any, UintPageState> {
if (this.state != null && this.state.unit != null && this.state.mod != null) { if (this.state != null && this.state.unit != null && this.state.mod != null) {
const backRef = "/mod/" + this.props.match.params.modId + "/race/" + this.props.match.params.raceId const backRef = "/mod/" + this.props.match.params.modId + "/race/" + this.props.match.params.raceId
const { theme } = this.props;
return ( return (
<Container maxWidth="lg" sx={{ py: 4 }}> <Container maxWidth="lg">
<BackButton <BackButton
variant="outlined" variant="outlined"
startIcon={<ArrowBack/>} startIcon={<ArrowBack/>}
@ -397,7 +416,7 @@ class UnitPage extends React.Component<any, UintPageState> {
> >
Back to race Back to race
</BackButton> </BackButton>
{Unit(this.state.unit, this.state.mod)} {Unit(this.state.unit, this.state.mod, theme)}
</Container> </Container>
); );
} else { } else {
@ -410,4 +429,4 @@ class UnitPage extends React.Component<any, UintPageState> {
} }
} }
export default withRouter(UnitPage); export default withRouter(withTheme(UnitPage));

83
src/themes/theme.ts Normal file
View File

@ -0,0 +1,83 @@
import { createTheme } from '@mui/material/styles';
export const darkTheme = createTheme({
breakpoints: {
values: {
xs: 0,
sm: 600,
md: 900,
lg: 1320,
xl: 1536,
},
},
palette: {
mode: 'dark',
primary: {
main: "#1a1a2e",
},
background: {
default: '#0a0a0a',
paper: '#1a1a2e',
},
text: {
primary: '#dee2e6',
secondary: '#dee2e6',
},
},
typography: {
fontFamily: '"Inter", "Roboto", "Helvetica", sans-serif',
},
shape: {
borderRadius: 12,
},
components: {
MuiTableCell: {
styleOverrides: {
root: {
color: '#dee2e6',
},
},
},
},
});
export const lightTheme = createTheme({
breakpoints: {
values: {
xs: 0,
sm: 600,
md: 900,
lg: 1320,
xl: 1536,
},
},
palette: {
mode: 'light',
primary: {
main: "#1a1a2e",
},
background: {
default: '#f5f5f5',
paper: '#ffffff',
},
text: {
primary: 'rgba(0, 0, 0, 0.85)',
secondary: 'rgba(0, 0, 0, 0.6)',
},
},
typography: {
fontFamily: '"Inter", "Roboto", "Helvetica", sans-serif',
},
shape: {
borderRadius: 12,
},
components: {
MuiTableCell: {
styleOverrides: {
root: {
color: 'rgba(0, 0, 0, 0.85)',
},
},
},
},
});