diff --git a/.gitignore b/.gitignore
index 484d815..a7f4b92 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,7 +11,9 @@
# production
/build
-
+/deploy
+dow-wiki-deploy.tar.gz
+tsconfig.tsbuildinfo
# misc
.DS_Store
.env.local
diff --git a/KODA.md b/KODA.md
new file mode 100644
index 0000000..ed74818
--- /dev/null
+++ b/KODA.md
@@ -0,0 +1,4 @@
+#### Сборка
+
+При сборке дистрибутива формируй архив dow-wiki-deploy.tar.gz
+Предварительно удали всё что было до этого в папке deploy и собирай туда
\ No newline at end of file
diff --git a/app/layout.tsx b/app/layout.tsx
index 815738d..8020d0e 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,4 +1,5 @@
import type { Metadata, Viewport } from 'next';
+import { cookies } from 'next/headers';
import '@/src/index.css';
import '@/src/App.css';
import '@/src/css/Unit.css';
@@ -49,8 +50,12 @@ export default function RootLayout({
}: {
children: React.ReactNode;
}) {
+ // Читаем тему из cookie на сервере, чтобы сразу отрендерить нужную тему без мерцания.
+ const themeCookie = cookies().get('theme')?.value;
+ const initialMode = themeCookie === 'light' ? 'light' : 'dark';
+
return (
-
+
{/* Yandex.Metrika */}
-
+
{children}
diff --git a/app/mod/[modId]/page.tsx b/app/mod/[modId]/page.tsx
index 63db37e..ab0756f 100644
--- a/app/mod/[modId]/page.tsx
+++ b/app/mod/[modId]/page.tsx
@@ -1,6 +1,7 @@
import type { Metadata } from 'next';
import ModPageClient from '@/components/pages/ModPageClient';
-import { getMod } from '@/lib/api-server';
+import { getMod, getUnitsForMod, getBuildingsForMod } from '@/lib/api-server';
+import { IRaceBuildings } from '@/src/types/IBuildingShort';
export const dynamic = 'force-dynamic';
@@ -28,6 +29,15 @@ export async function generateMetadata({ params }: Props): Promise {
};
}
-export default function ModPage() {
- return ;
+export default async function ModPage({ params }: Props) {
+ const [mod, racesUnits] = await Promise.all([
+ getMod(params.modId),
+ getUnitsForMod(params.modId),
+ ]);
+ const units = racesUnits ?? [];
+ let racesBuildings: IRaceBuildings[] = [];
+ if (units.length <= 10) {
+ racesBuildings = (await getBuildingsForMod(params.modId)) ?? [];
+ }
+ return ;
}
\ No newline at end of file
diff --git a/app/mod/[modId]/race/[raceId]/building/[buildingId]/page.tsx b/app/mod/[modId]/race/[raceId]/building/[buildingId]/page.tsx
index d62d187..dc57c99 100644
--- a/app/mod/[modId]/race/[raceId]/building/[buildingId]/page.tsx
+++ b/app/mod/[modId]/race/[raceId]/building/[buildingId]/page.tsx
@@ -1,6 +1,8 @@
import type { Metadata } from 'next';
import BuildingPageClient from '@/components/pages/BuildingPageClient';
import { getBuilding, getMod } from '@/lib/api-server';
+import { IBuilding } from '@/src/types/IBuilding';
+import { IMod } from '@/src/types/Imod';
export const dynamic = 'force-dynamic';
@@ -32,6 +34,16 @@ export async function generateMetadata({ params }: Props): Promise {
};
}
-export default function BuildingPage() {
- return ;
+export default async function BuildingPage({ params }: Props) {
+ const [mod, building] = await Promise.all([
+ getMod(params.modId),
+ getBuilding(params.buildingId),
+ ]);
+ return (
+
+ );
}
\ No newline at end of file
diff --git a/app/mod/[modId]/race/[raceId]/page.tsx b/app/mod/[modId]/race/[raceId]/page.tsx
index 3c0476d..0e5b6a4 100644
--- a/app/mod/[modId]/race/[raceId]/page.tsx
+++ b/app/mod/[modId]/race/[raceId]/page.tsx
@@ -1,6 +1,6 @@
import type { Metadata } from 'next';
import RacePageClient from '@/components/pages/RacePageClient';
-import { getMod, getRace } from '@/lib/api-server';
+import { getMod, getRace, getRaceUnits, getRaceBuildings } from '@/lib/api-server';
export const dynamic = 'force-dynamic';
@@ -28,6 +28,19 @@ export async function generateMetadata({ params }: Props): Promise {
};
}
-export default function RacePage() {
- return ;
+export default async function RacePage({ params }: Props) {
+ const [mod, race, raceUnits, raceBuildings] = await Promise.all([
+ getMod(params.modId),
+ getRace(params.raceId),
+ getRaceUnits(params.modId, params.raceId),
+ getRaceBuildings(params.modId, params.raceId),
+ ]);
+ return (
+
+ );
}
\ No newline at end of file
diff --git a/app/mod/[modId]/race/[raceId]/unit/[unitId]/page.tsx b/app/mod/[modId]/race/[raceId]/unit/[unitId]/page.tsx
index c384cae..abe8810 100644
--- a/app/mod/[modId]/race/[raceId]/unit/[unitId]/page.tsx
+++ b/app/mod/[modId]/race/[raceId]/unit/[unitId]/page.tsx
@@ -1,6 +1,8 @@
import type { Metadata } from 'next';
import UnitPageClient from '@/components/pages/UnitPageClient';
import { getMod, getUnit } from '@/lib/api-server';
+import { IUnit } from '@/src/types/IUnit';
+import { IMod } from '@/src/types/Imod';
export const dynamic = 'force-dynamic';
@@ -30,6 +32,16 @@ export async function generateMetadata({ params }: Props): Promise {
};
}
-export default function UnitPage() {
- return ;
+export default async function UnitPage({ params }: Props) {
+ const [mod, unit] = await Promise.all([
+ getMod(params.modId),
+ getUnit(params.unitId),
+ ]);
+ return (
+
+ );
}
\ No newline at end of file
diff --git a/components/pages/BuildingPageClient.tsx b/components/pages/BuildingPageClient.tsx
index 556ffda..dd81140 100644
--- a/components/pages/BuildingPageClient.tsx
+++ b/components/pages/BuildingPageClient.tsx
@@ -1,16 +1,358 @@
'use client';
-import dynamic from 'next/dynamic';
-import { Box, LinearProgress } from '@mui/material';
+import React, { useEffect, useState } from 'react';
+import {
+ Box,
+ Container,
+ Divider,
+ Grid2,
+ Link,
+ ListItem,
+ Paper,
+ Table,
+ TableBody,
+ TableCell,
+ TableRow,
+ Tooltip,
+ Typography,
+ LinearProgress,
+ Theme,
+} from '@mui/material';
+import { styled } from '@mui/material/styles';
+import { useTheme } from '@mui/material/styles';
+import { useParams } from 'next/navigation';
+import { ArrowBack } from '@mui/icons-material';
+import AvTimerOutlinedIcon from '@mui/icons-material/AvTimer';
+import { AvailableBuildings, AvailableMods, AvailableUnits, IconUrl } from '@/src/core/api';
+import { IShortWeapon } from '@/src/types/IUnit';
+import { IRaceUnits } from '@/src/types/IUnitShort';
+import { IRaceBuildings } from '@/src/types/IBuildingShort';
+import '@/src/css/Building.css';
+import ArmorType from '@/src/classes/ArmorType';
+import WeaponSlot from '@/src/classes/WeaponSlot';
+import UnitsTable from '@/src/classes/UnitsTable';
+import { IMod } from '@/src/types/Imod';
+import { IBuilding } from '@/src/types/IBuilding';
+import Vision from '@/src/classes/Vision';
+import BuildingAddon from '@/src/classes/building/BuildingAddon';
+import { IUnitShort } from '@/src/types/IUnitShort';
+import Research, { AffectedResearches } from '@/src/classes/building/Research';
+import Required from '@/src/classes/Required';
+import { ModifiersProvidesTable } from '@/src/classes/ModifiersProvideTable';
+import Ability from '@/src/classes/Ability';
+import DeathExplosion from '@/src/classes/DeathExplosion';
+import { DescriptionBox } from '@/src/commons/DescriptionBox';
+import { BackButton } from '@/src/commons/BackButton';
-const BuildingPageLegacy = dynamic(() => import('@/src/legacy-pages/BuildingPage'), {
- ssr: false,
- loading: () => (
-
-
-
- ),
-});
+const SectionTitle = styled(Typography)(({ theme }) => ({
+ fontWeight: 700,
+ mb: 2,
+ fontSize: '1.25rem',
+}));
-export default function BuildingPageClient() {
- return ;
+const StatsPaper = 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' : 'rgba(0, 0, 0, 0.85)',
+ 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' : 'rgba(0, 0, 0, 0.85)' },
+}));
+
+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(0,0,0,0.08)', borderColor: theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.3)' : 'rgba(0,0,0,0.2)' },
+}));
+
+function Unit(unit: IUnitShort, modId: number, raceId: string, theme: Theme) {
+ return (
+
+
+ {unit.icon &&
}
+ {unit.name}
+ {unit.canDetect &&
}
+
+
+ )
+}
+
+function Building(building: IBuilding, mod: IMod, theme: Theme, racesUnits: IRaceUnits[], racesBuildings: IRaceBuildings[]) {
+ const isDark = theme.palette.mode === 'dark';
+
+ let mapBuildingWeapons: Map> = new Map();
+
+ building.weapons.forEach(weapon => {
+ const weaponMap = mapBuildingWeapons.get(weapon.hardpoint)
+ if (weaponMap == null) {
+ const weaponMap = new Map()
+ weaponMap.set(weapon.hardpointOrder, weapon.weapon)
+ mapBuildingWeapons.set(weapon.hardpoint, weaponMap)
+ } else {
+ weaponMap.set(weapon.hardpointOrder, weapon.weapon)
+ }
+ })
+
+ var buildingName = building.name;
+ if (building.name == null) {
+ buildingName = building.filename.replaceAll('_', ' ').replace('.rgd', '');
+ }
+
+ return (
+
+
+
+ {building.icon &&
+
}
+ {buildingName}
+
+
+ {mod.name} ({mod.version})
+
+
+
+
+
+
+
+
+
+ Cost
+
+ {building.buildCostRequisition > 0 &&
+
+ {building.buildCostRequisition.toFixed(0)}}
+ {building.buildCostPower > 0 &&
+ {building.buildCostPower.toFixed(0)}}
+ {(building.buildCostPopulation !== undefined && building.buildCostPopulation > 0) &&
+
+ {building.buildCostPopulation.toFixed(0)}}
+ {(building.buildCostFaith !== undefined && building.buildCostFaith > 0) &&
+
+ {building.buildCostFaith}}
+ {(building.buildCostSouls !== undefined && building.buildCostSouls > 0) &&
+
+ {building.buildCostSouls.toFixed(0)}}
+ {(building.buildCostTime !== undefined && building.buildCostTime > 0) &&
+
+ {building.buildCostTime}s}
+
+
+ {(building.requisitionIncome !== undefined && building.requisitionIncome > 0 || building.powerIncome !== undefined && building.powerIncome !== null || building.faithIncome !== undefined && building.faithIncome !== null) &&
+
+ Resource income
+
+ {building.requisitionIncome !== undefined && building.requisitionIncome != null &&
+
+ {building.requisitionIncome}}
+ {building.powerIncome !== undefined && building.powerIncome > 0 &&
+
+ {building.powerIncome}}
+ {building.faithIncome !== undefined && building.faithIncome > 0 &&
+
+ {building.faithIncome}}
+
+
+ }
+
+ Armor type
+
+ {building.armorType2 == null ?
+ : }
+
+
+
+ Health
+
+
+ {building.health} {building.healthRegeneration > 0 &&
+ +{building.healthRegeneration}/s}
+
+
+
+ Sight
+
+
+
+
+
+ Repair max
+ {building.repairMax}
+
+
+
+
+
+
+
+ {building.description}
+
+
+ {building.requirements !== null &&
+
+
+ }
+ {building.modifiers.length > 0 &&
+
+ Affected on
+
+ }
+ {building.units.length > 0 &&
+ Unit production
+
+ {building.units.map(unit =>
+ Unit(unit, mod.id, building.race.id, theme)
+ )}
+
+ }
+ {building.abilities.length > 0 &&
+ Abilities
+ {building.abilities.map(a =>
+
+ )}
+ }
+ {building.deathExplosions.length > 0 &&
+
+ {building.deathExplosions.map(da =>
+
+ )}
+
+ }
+ {building.addons.length > 0 &&
+ Addons
+ {building.addons.map(b =>
+
+ )}
+ }
+ {building.researches.length > 0 &&
+ Researches
+ {building.researches.map(r =>
+
+ )}
+ }
+
+ {[...mapBuildingWeapons.keys()].sort(function (a, b) {
+ return a - b;
+ }).map(h => )}
+
+
+
+
+ Hotkey: {building.hotkey} Filename: {building.filename}
+
+
+
+ )
+}
+
+export default function BuildingPageClient({
+ initialBuilding,
+ initialMod,
+}: {
+ initialBuilding: IBuilding | null;
+ initialMod: IMod | null;
+}) {
+ const params = useParams() as Record;
+ const { modId, raceId, buildingId } = params;
+ const theme = useTheme();
+ const [building, setBuilding] = useState(initialBuilding);
+ const [mod, setMod] = useState(initialMod);
+ const [racesUnits, setRacesUnits] = useState([]);
+ const [racesBuildings, setRacesBuildings] = useState([]);
+
+ useEffect(() => {
+ let cancelled = false;
+ if (building == null) {
+ fetch(AvailableBuildings + "/" + buildingId)
+ .then(res => res.json())
+ .then((res: IBuilding) => { if (!cancelled) setBuilding(res); });
+ }
+ if (mod == null) {
+ fetch(AvailableMods + "/" + modId)
+ .then(res => res.json())
+ .then((res: IMod) => { if (!cancelled) setMod(res); });
+ }
+ fetch(AvailableUnits + "/mod/" + modId)
+ .then(res => res.json())
+ .then((resUnits: IRaceUnits[]) => {
+ if (cancelled) return;
+ setRacesUnits(resUnits);
+ if (resUnits.length <= 10) {
+ fetch(AvailableBuildings + "/mod/" + modId)
+ .then(res => res.json())
+ .then((resBuildings: IRaceBuildings[]) => {
+ if (!cancelled) setRacesBuildings(resBuildings);
+ });
+ }
+ });
+ return () => { cancelled = true; };
+ }, [modId, raceId, buildingId]);
+
+ if (building != null && mod != null) {
+ const backRef = "/mod/" + modId + "/race/" + raceId;
+
+ return (
+
+ }
+ href={backRef}
+ >
+ Back to race
+
+ {Building(building, mod, theme, racesUnits, racesBuildings)}
+
+ );
+ } else {
+ return (
+
+
+
+ );
+ }
}
diff --git a/components/pages/ModPageClient.tsx b/components/pages/ModPageClient.tsx
index 79991a6..49b2d59 100644
--- a/components/pages/ModPageClient.tsx
+++ b/components/pages/ModPageClient.tsx
@@ -1,16 +1,71 @@
'use client';
-import dynamic from 'next/dynamic';
-import { Box, LinearProgress } from '@mui/material';
+import React from 'react';
+import { ArrowBack } from '@mui/icons-material';
+import {
+ Box,
+ Container,
+ Typography,
+} from '@mui/material';
+import { styled } from '@mui/material/styles';
+import { useTheme } from '@mui/material/styles';
+import { IMod } from '@/src/types/Imod';
+import { IRaceUnits } from '@/src/types/IUnitShort';
+import { IRaceBuildings } from '@/src/types/IBuildingShort';
+import UnitsTable from '@/src/classes/UnitsTable';
+import { BackButton } from '@/src/commons/BackButton';
-const ModPageLegacy = dynamic(() => import('@/src/legacy-pages/ModPage'), {
- ssr: false,
- loading: () => (
-
-
-
- ),
-});
+const ModNotFound = styled(Typography)(({ theme }) => ({
+ color: theme.palette.text.secondary,
+}));
-export default function ModPageClient() {
- return ;
+interface ModPageClientProps {
+ initialMod: IMod | null;
+ racesUnits: IRaceUnits[];
+ racesBuildings: IRaceBuildings[];
+}
+
+export default function ModPageClient({ initialMod, racesUnits, racesBuildings }: ModPageClientProps) {
+ const theme = useTheme();
+
+ if (initialMod != null) {
+ const isDark = theme.palette.mode === 'dark';
+
+ return (
+
+ }
+ href="/"
+ >
+ Back to mods list
+
+
+
+ {initialMod.name} ({initialMod.version})
+
+
+
+
+
+
+ );
+ } else {
+ return (
+
+
+ Mod not found
+
+
+ );
+ }
}
diff --git a/components/pages/ModsPageClient.tsx b/components/pages/ModsPageClient.tsx
index 2de5015..3219c68 100644
--- a/components/pages/ModsPageClient.tsx
+++ b/components/pages/ModsPageClient.tsx
@@ -1,14 +1,248 @@
'use client';
-import dynamic from 'next/dynamic';
-import { Box, LinearProgress } from '@mui/material';
+import React, { useEffect } from 'react';
+import {
+ Box,
+ Typography,
+ Grid,
+ Card,
+ CardContent,
+ CardActions,
+ Chip,
+ Button,
+ useTheme,
+ Link,
+} from '@mui/material';
+import { styled } from '@mui/material/styles';
import { IMod } from '@/src/types/Imod';
-const ModsPageLegacy = dynamic(() => import('@/src/legacy-pages/ModsPage'));
+const ModCard = styled(Card)(({ theme }) => ({
+ height: '100%',
+ display: 'flex',
+ flexDirection: 'column',
+ background: theme.palette.mode === 'dark'
+ ? 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)'
+ : 'linear-gradient(145deg, #ffffff 0%, #f5f5f5 100%)',
+ border: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
+ borderRadius: '16px',
+ transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
+ position: 'relative',
+ overflow: 'hidden',
+ '&::before': {
+ content: '""',
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ right: 0,
+ height: '3px',
+ background: theme.palette.mode === 'dark'
+ ? 'linear-gradient(90deg, #dee2e6 0%, #cccccc 100%)'
+ : 'linear-gradient(90deg, #000000 0%, #333333 100%)',
+ transform: 'scaleX(0)',
+ transition: 'transform 0.3s ease',
+ },
+ '&:hover': {
+ boxShadow: theme.palette.mode === 'dark' ? '0 20px 40px rgba(255, 255, 255, 0.1)' : '0 20px 40px rgba(0, 0, 0, 0.2)',
+ borderColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)',
+ '&::before': {
+ transform: 'scaleX(1)',
+ },
+ },
+}));
+
+const VersionLink = styled(Link)(({ theme }) => ({
+ display: 'flex',
+ alignItems: 'center',
+ padding: theme.spacing(1.5, 2),
+ marginBottom: theme.spacing(1),
+ background: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.05)',
+ borderRadius: '10px',
+ textDecoration: 'none',
+ color: 'inherit',
+ transition: 'all 0.2s ease',
+ border: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
+ '&:hover': {
+ background: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)',
+ borderColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)',
+ },
+}));
+
+const ModTitle = styled(Typography)(({ theme }) => ({
+ fontWeight: 700,
+ mb: 1,
+ background: theme.palette.mode === 'dark'
+ ? 'linear-gradient(135deg, #dee2e6 0%, #e0e0e0 100%)'
+ : 'none',
+ WebkitBackgroundClip: theme.palette.mode === 'dark' ? 'text' : 'initial',
+ WebkitTextFillColor: theme.palette.mode === 'dark' ? '#dee2e6' : 'initial',
+ color: theme.palette.mode === 'dark' ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)',
+}));
+
+const VersionsChip = styled(Chip)(({ theme }) => ({
+ backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.15)' : 'rgba(0, 0, 0, 0.1)',
+ color: theme.palette.mode === 'dark' ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)',
+ fontWeight: 600,
+}));
+
+const VersionText = styled(Typography)(({ theme }) => ({
+ fontWeight: 600,
+ color: theme.palette.text.secondary,
+}));
+
+const VersionArrow = styled(Typography)(({ theme }) => ({
+ color: theme.palette.text.secondary,
+}));
+
+const BetaVersionsLabel = styled(Typography)(({ theme }) => ({
+ color: 'rgba(255, 193, 7, 0.9)',
+ fontWeight: 600,
+ mb: 1,
+}));
interface ModsPageClientProps {
initialMods: IMod[];
}
export default function ModsPageClient({ initialMods }: ModsPageClientProps) {
- return ;
+ const theme = useTheme();
+
+ // Очистка кэша сайта при заходе на главную страницу (один раз за сессию)
+ useEffect(() => {
+ if (!sessionStorage.getItem('cache_cleared')) {
+ sessionStorage.setItem('cache_cleared', '1');
+ (async () => {
+ try {
+ if ('caches' in window) {
+ const names = await caches.keys();
+ await Promise.all(names.map(name => caches.delete(name)));
+ }
+ if ('serviceWorker' in navigator) {
+ const regs = await navigator.serviceWorker.getRegistrations();
+ await Promise.all(regs.map(r => r.unregister()));
+ }
+ } catch (e) {
+ console.error('Cache clear failed:', e);
+ }
+ })();
+ }
+ }, []);
+
+ // Sort all mods by order, then group by name
+ const sortedMods = [...initialMods].sort((a, b) => a.order - b.order);
+
+ const mapWithModVersions = new Map();
+ sortedMods.forEach(mod => {
+ const versionList = mapWithModVersions.get(mod.name);
+ if (versionList == null) {
+ mapWithModVersions.set(mod.name, [mod]);
+ } else {
+ versionList.push(mod);
+ }
+ });
+
+ const getLatestVersion = (modName: string) => {
+ const sameMods = mapWithModVersions.get(modName) ?? [];
+ // The latest version is the one with the maximum id
+ const latest = sameMods.reduce((max, mod) => mod.id > max.id ? mod : max, sameMods[0]);
+ const betaMods = sameMods.filter(m => m.isBeta);
+ return {
+ latest,
+ allVersions: sameMods,
+ hasBeta: betaMods.length > 0,
+ betaVersion: betaMods[0],
+ };
+ };
+
+ function ModCardComponent(modName: string) {
+ const sameMods = mapWithModVersions.get(modName) ?? [];
+ const { latest, hasBeta, betaVersion } = getLatestVersion(modName);
+
+ return (
+
+
+
+
+ {modName}
+
+
+
+
+
+
+
+ {sameMods.filter(m => !m.isBeta).map(mod => (
+
+
+
+ Version {mod.version}
+
+
+ →
+
+
+
+ ))}
+
+ {hasBeta && (
+
+
+ Beta versions:
+
+ {betaVersion && (
+
+
+
+ Version {betaVersion.version} (Beta)
+
+
+ →
+
+
+
+ )}
+
+ )}
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+ {[...new Set(sortedMods.map(m => m.name))].map((modName) => (
+
+ {ModCardComponent(modName)}
+
+ ))}
+
+
+ );
}
diff --git a/components/pages/RacePageClient.tsx b/components/pages/RacePageClient.tsx
index eec2a5c..41d54d2 100644
--- a/components/pages/RacePageClient.tsx
+++ b/components/pages/RacePageClient.tsx
@@ -1,16 +1,262 @@
'use client';
-import dynamic from 'next/dynamic';
-import { Box, LinearProgress } from '@mui/material';
+import React from 'react';
+import {
+ Accordion,
+ AccordionDetails,
+ AccordionSummary,
+ Box,
+ Container,
+ Divider,
+ Grid2,
+ Link,
+ List,
+ ListItem,
+ Paper,
+ Typography,
+ Theme,
+} from '@mui/material';
+import { styled } from '@mui/material/styles';
+import { useTheme } from '@mui/material/styles';
+import { ArrowBack, ExpandMore } from '@mui/icons-material';
+import { IconUrl } from '@/src/core/api';
+import { IMod } from '@/src/types/Imod';
+import { Irace } from '@/src/types/Irace';
+import '@/src/css/Unit.css';
+import { IRaceUnits, IUnitShort } from '@/src/types/IUnitShort';
+import { IBuildingShort, IRaceBuildings } from '@/src/types/IBuildingShort';
+import { StyledLink } from '@/src/commons/StyledLink';
+import { BackButton } from '@/src/commons/BackButton';
-const RacePageLegacy = dynamic(() => import('@/src/legacy-pages/RacePageFast'), {
- ssr: false,
- loading: () => (
-
-
-
- ),
-});
+const SectionTitle = styled(Typography)(({ theme }) => ({
+ fontWeight: 700,
+ mb: 2,
+ fontSize: '1.25rem',
+}));
-export default function RacePageClient() {
- return ;
+const UnitCard = 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: '16px',
+ transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
+ '&:hover': {
+ boxShadow: theme.palette.mode === 'dark' ? '0 20px 40px rgba(255, 255, 255, 0.1)' : '0 20px 40px rgba(0, 0, 0, 0.2)',
+ borderColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)',
+ },
+}));
+
+const BuildingCard = styled(UnitCard)(({ theme }) => ({
+ padding: theme.spacing(1),
+}));
+
+const UnitLink = styled(Link)(({ theme }) => ({
+ color: theme.palette.text.primary,
+ textDecoration: 'none',
+ display: 'block',
+ '&:hover': { color: theme.palette.mode === 'dark' ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)' }
+}));
+
+const LoadingText = styled(Typography)(({ theme }) => ({
+ color: theme.palette.text.secondary,
+}));
+
+function Unit(unit: IUnitShort, modId: number, raceId: string, theme: Theme) { const isDark = theme.palette.mode === 'dark';
+
+ return (
+
+
+ {unit.icon &&
}
+ {unit.name}
+ {unit.canDetect &&
}
+
+
+ )
+}
+
+function UnitSmall(unit: IUnitShort, modId: number, raceId: string) {
+ var unitName = ""
+ if (unit.name.length > 21) {
+ unitName = unit.name.substring(0, 19) + "...";
+ } else {
+ unitName = unit.name;
+ }
+
+ return (
+ {unit.icon &&
}
+ {unitName}
+ {unit.canDetect &&
}
)
+}
+
+function Building(building: IBuildingShort, modId: number, raceId: string, theme: Theme) {
+ const isDark = theme.palette.mode === 'dark';
+
+ return (
+
+
+
+ {building.icon &&
}
+ {building.name}
+ {building.canDetect &&
}
+
+ {building.units.map(unit => UnitSmall(unit, modId, raceId))}
+
+
+ )
+}
+
+interface UnitsProps {
+ raceId: string;
+ modId: number;
+ units: IRaceUnits | null;
+ buildings: IRaceBuildings | null;
+}
+
+function Units({ raceId, modId, units, buildings }: UnitsProps) {
+ const theme = useTheme();
+ const isDark = theme.palette.mode === 'dark';
+
+ if (units) {
+ const accordionSx = isDark ? {
+ background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
+ border: '1px solid rgba(255, 255, 255, 0.1)',
+ borderRadius: '12px',
+ '&:before': { display: 'none' },
+ '&.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 (buildings != null ?
+
+
+ {buildings.buildings.map(building => Building(building, modId, raceId, theme))}
+
+ {buildings.buildingsAdvanced.length > 0 &&
+
+ Advanced buildings
+
+ {buildings.buildingsAdvanced.map(building => Building(building, modId, raceId, theme))}
+
}
+
+
+ }
+ aria-controls="units-accordion"
+ id="units-accordion"
+ sx={{ color: isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)', '& .MuiAccordionSummary-content': { margin: '12px 0' }, '& .MuiAccordionSummary-content.Mui-expanded': { margin: '12px 0' } }}
+ >
+ All units
+
+
+
+
+ Infantry
+ {units.infantry.map(unit => Unit(unit, modId, raceId, theme))}
+
+
+ Tech
+
+ {units.tech.map(unit => Unit(unit, modId, raceId, theme))}
+
+
+
+ Support
+
+ {units.support.map(unit => Unit(unit, modId, raceId, theme))}
+
+
+
+
+
+
+ : Loading
+ )
+ } else {
+ return Loading...;
+ }
+}
+
+interface RacePageClientProps {
+ initialMod: IMod | null;
+ initialRace: Irace | null;
+ raceUnits: IRaceUnits | null;
+ raceBuildings: IRaceBuildings | null;
+}
+
+export default function RacePageClient({ initialMod, initialRace, raceUnits, raceBuildings }: RacePageClientProps) {
+ const theme = useTheme();
+
+ if (initialMod != null && initialRace != null) {
+ const backRef = "/mod/" + initialMod.id;
+ const isDark = theme.palette.mode === 'dark';
+
+ return (
+
+ }
+ href={backRef}
+ >
+ Back to mod
+
+
+
+
+ {initialRace.name}
+
+
+ {initialMod.name} ({initialMod.version})
+
+
+
+
+
+ );
+ } else {
+ return (
+
+
+ Race not found
+
+
+ );
+ }
}
diff --git a/components/pages/UnitPageClient.tsx b/components/pages/UnitPageClient.tsx
index 5933461..e939444 100644
--- a/components/pages/UnitPageClient.tsx
+++ b/components/pages/UnitPageClient.tsx
@@ -1,16 +1,443 @@
'use client';
-import dynamic from 'next/dynamic';
-import { Box, LinearProgress } from '@mui/material';
+import React, { useEffect, useState } from 'react';
+import {
+ Accordion,
+ AccordionDetails,
+ AccordionSummary,
+ Box,
+ Button,
+ Container,
+ Divider,
+ Grid2,
+ LinearProgress,
+ Paper,
+ Table,
+ TableBody,
+ TableCell,
+ TableRow,
+ Tooltip,
+ Typography,
+ Theme,
+} from '@mui/material';
+import { styled } from '@mui/material/styles';
+import { useTheme } from '@mui/material/styles';
+import { useParams } from 'next/navigation';
+import { ArrowBack, ExpandMore } from '@mui/icons-material';
+import AvTimerOutlinedIcon from '@mui/icons-material/AvTimer';
+import { AvailableBuildings, AvailableMods, AvailableUnits, IconUrl } from '@/src/core/api';
+import { IShortWeapon, IUnit } from '@/src/types/IUnit';
+import { IRaceUnits } from '@/src/types/IUnitShort';
+import { IRaceBuildings } from '@/src/types/IBuildingShort';
+import '@/src/css/Unit.css';
+import ArmorType from '@/src/classes/ArmorType';
+import Sergeant from '@/src/classes/Sergeant';
+import WeaponSlot from '@/src/classes/WeaponSlot';
+import UnitsTable from '@/src/classes/UnitsTable';
+import { IMod } from '@/src/types/Imod';
+import Vision from '@/src/classes/Vision';
+import Required from '@/src/classes/Required';
+import { AffectedResearches } from '@/src/classes/building/Research';
+import { ModifiersProvidesTable } from '@/src/classes/ModifiersProvideTable';
+import Ability from '@/src/classes/Ability';
+import DeathExplosion from '@/src/classes/DeathExplosion';
+import Jump from '@/src/classes/Jump';
+import { DescriptionBox } from '@/src/commons/DescriptionBox';
+import { BackButton } from '@/src/commons/BackButton';
-const UnitPageLegacy = dynamic(() => import('@/src/legacy-pages/UnitPage'), {
- ssr: false,
- loading: () => (
-
-
+const SectionTitle = styled(Typography)(({ theme }) => ({
+ fontWeight: 700,
+ mb: 2,
+ fontSize: '1.25rem',
+}));
+
+const StatsPaper = 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': {
+ '&:last-child td, &:last-child th': { border: 0 },
+ },
+ '& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': {
+ color: theme.palette.mode === 'dark' ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)',
+ 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,
+}));
+
+function Unit(unit: IUnit, mod: IMod, theme: Theme, racesUnits: IRaceUnits[], racesBuildings: IRaceBuildings[]) {
+ const isDark = theme.palette.mode === 'dark';
+
+ const morale = (unit.moraleMax !== null) ?
+
{unit.moraleMax}
+ +{unit.moraleRegeneration}/s
+ {unit.moraleDeathPenalty > 0 &&
-{unit.moraleDeathPenalty}}
+ : "-"
+
+ let mapWithUnitWeapons: Map> = new Map();
+
+ unit.weapons.forEach(weapon => {
+ const weaponMap = mapWithUnitWeapons.get(weapon.hardpoint)
+ if (weaponMap == null) {
+ const weaponMap = new Map()
+ weaponMap.set(weapon.hardpointOrder, weapon.weapon)
+ mapWithUnitWeapons.set(weapon.hardpoint, weaponMap)
+ } else {
+ weaponMap.set(weapon.hardpointOrder, weapon.weapon)
+ }
+ })
+
+ type sergeantProps = {
+ name: string
+ icon: String
+ canDetect: Boolean
+ }
+
+ const SergeantShort = (props: sergeantProps) => {
+ return (
+
+ {props.icon &&
}
+ {props.name}
+ {props.canDetect &&
}
+
+ )
+ }
+
+ return (
+
+
+
+ {unit.icon &&
+
}
+ {unit.name}
+
+
+ {mod.name} ({mod.version})
+
+
+
+
+
+
+
+
+
+ Cost
+
+ {unit.buildCostRequisition > 0 &&
+
+ {unit.buildCostRequisition.toFixed(0)}}
+ {unit.buildCostPower > 0 &&
+ {unit.buildCostPower.toFixed(0)}}
+ {(unit.buildCostPopulation !== undefined && unit.buildCostPopulation > 0) &&
+
+ {unit.buildCostPopulation.toFixed(0)}}
+ {(unit.buildCostFaith !== undefined && unit.buildCostFaith > 0) &&
+
+ {unit.buildCostFaith}}
+ {(unit.buildCostSouls !== undefined && unit.buildCostSouls > 0) &&
+
+ {unit.buildCostSouls.toFixed(0)}}
+ {unit.capInfantry > 0 &&
+ {unit.capInfantry}}
+ {unit.capSupport > 0 &&
+ {unit.capSupport}}
+ {(unit.buildCostTime !== undefined && unit.buildCostTime > 0) &&
+
+ {unit.buildCostTime}s}
+
+
+ {(unit?.reinforceTime !== 0 && unit.reinforceTime !== null && unit.squadMaxSize > 1) &&
+
+ Reinforce cost
+
+ {unit.reinforceCostRequisition && unit.reinforceCostRequisition > 0 ?
+
+ {unit.reinforceCostRequisition.toFixed(0)} : }
+ {unit.reinforceCostPower !== undefined && unit.reinforceCostPower > 0 &&
+
+ {unit.reinforceCostPower.toFixed(0)}}
+ {(unit.reinforceCostPopulation !== undefined && unit.reinforceCostPopulation > 0) &&
+
+ {unit.reinforceCostPopulation.toFixed(0)}}
+ {(unit.reinforceCostFaith !== undefined && unit.reinforceCostFaith > 0) &&
+
+ {unit.reinforceCostFaith}}
+ {(unit.reinforceCostSouls !== undefined && unit.reinforceCostSouls > 0) &&
+
+ {unit.reinforceCostSouls.toFixed(0)}}
+ {(unit.reinforceTime !== undefined && unit.reinforceTime > 0) &&
+
+ {unit.reinforceTime}s}
+
+
+ }
+ {(unit.requisitionIncome !== undefined && unit.requisitionIncome !== null || unit.powerIncome !== undefined && unit.powerIncome !== null || unit.faithIncome !== undefined && unit.faithIncome !== null) &&
+
+ Resource income
+
+ {unit.requisitionIncome !== undefined && unit.requisitionIncome > 0 &&
+
+ {unit.requisitionIncome}}
+ {unit.powerIncome !== undefined && unit.powerIncome > 0 &&
+
+ {unit.powerIncome}}
+ {unit.faithIncome !== undefined && unit.faithIncome > 0 &&
+
+ {unit.faithIncome}}
+
+
+ }
+ {unit.squadMaxSize > 1 &&
+
+ Squad size
+ {unit.squadStartSize} / {unit.squadMaxSize}
+
+ }
+
+ Armor type
+
+ {unit.armorType2 == null ? : }
+
+
+
+ Health
+
+
+ {unit.health} {unit.healthRegeneration > 0 &&
+ +{unit.healthRegeneration}/s}
+ {unit.armour !== undefined && unit.armour !== 0 &&
{unit.armour} }
+
+
+
+ Move speed
+ {unit.moveSpeed}
+
+
+ Morale
+ {morale}
+
+
+ Mass
+ {unit.mass}
+
+
+ Vision
+
+
+ {unit.repairMax !== undefined && unit.repairMax !== null &&
+
+ Repair max
+ {unit.repairMax}
+
+ }
+ {unit.repairSpeed !== undefined && unit.repairSpeed !== null && unit.repairCostPercent !== null &&
+
+ Repair
+ {unit.repairSpeed} hp/s; {unit.repairCostPercent}% cost
+
+ }
+ {unit.mobValue != null &&
+
+ Mob value
+
{unit.mobValue}
+
+ }
+ {unit.squadLimit !== undefined && unit.squadLimit !== null &&
+
+ Limit
+ {unit.squadLimit}
+
+ }
+
+
+
+
+
+
+ {unit.description}
+
+
+ {unit.requirements !== null &&
+
+
+ }
+
+ {unit.sergeants.map(s =>
+
+
+ 0} />
+
+
+
+
+ )}
+
+
+ {unit.moraleMax !== null && !(unit.moraleBrakeModifiers.find(m => m.reference.includes("accuracy_weapon_modifier") && m.value === 0.2) !== undefined &&
+ unit.moraleBrakeModifiers.find(m => m.reference.includes("speed_maximum_modifier") && m.value === 1.2) !== undefined &&
+ unit.moraleBrakeModifiers.length === 2)
+ &&
+
Morale broken
+
+ }
+
+ {unit.modifiers.length > 0 &&
+ Affected on
+
+ }
+
+ {unit.abilities.length > 0 &&
+ Abilities
+ {unit.abilities.map(a =>
+
+ )}
+ }
+
+ {unit.jumps != null &&
+
+ }
+
+
+ {[...mapWithUnitWeapons.keys()].sort(function (a, b) {
+ return a - b;
+ }).map(h => )}
+
+ {unit.deathExplosions.length > 0 &&
+
+ {unit.deathExplosions.map(da =>
+
+ )}
+
+ }
+
+
+
+
+
+
+ Hotkey: {unit.hotkey} Filename: {unit.filename}
+
+
+
- ),
-});
-
-export default function UnitPageClient() {
- return ;
+ )
+}
+
+export default function UnitPageClient({
+ initialUnit,
+ initialMod,
+}: {
+ initialUnit: IUnit | null;
+ initialMod: IMod | null;
+}) {
+ const params = useParams() as Record;
+ const { modId, raceId, unitId } = params;
+ const theme = useTheme();
+ const [unit, setUnit] = useState(initialUnit);
+ const [mod, setMod] = useState(initialMod);
+ const [racesUnits, setRacesUnits] = useState([]);
+ const [racesBuildings, setRacesBuildings] = useState([]);
+
+ useEffect(() => {
+ let cancelled = false;
+ if (unit == null) {
+ fetch(AvailableUnits + "/" + unitId)
+ .then(res => res.json())
+ .then((res: IUnit) => { if (!cancelled) setUnit(res); });
+ }
+ if (mod == null) {
+ fetch(AvailableMods + "/" + modId)
+ .then(res => res.json())
+ .then((res: IMod) => { if (!cancelled) setMod(res); });
+ }
+ fetch(AvailableUnits + "/mod/" + modId)
+ .then(res => res.json())
+ .then((resUnits: IRaceUnits[]) => {
+ if (cancelled) return;
+ setRacesUnits(resUnits);
+ if (resUnits.length <= 10) {
+ fetch(AvailableBuildings + "/mod/" + modId)
+ .then(res => res.json())
+ .then((resBuildings: IRaceBuildings[]) => {
+ if (!cancelled) setRacesBuildings(resBuildings);
+ });
+ }
+ });
+ return () => { cancelled = true; };
+ }, [modId, raceId, unitId]);
+
+ if (unit != null && mod != null) {
+ const backRef = "/mod/" + modId + "/race/" + raceId;
+
+ return (
+
+ }
+ href={backRef}
+ >
+ Back to race
+
+ {Unit(unit, mod, theme, racesUnits, racesBuildings)}
+
+ );
+ } else {
+ return (
+
+
+
+ );
+ }
}
diff --git a/lib/api-server.ts b/lib/api-server.ts
index 7ebe34e..6b33080 100644
--- a/lib/api-server.ts
+++ b/lib/api-server.ts
@@ -2,6 +2,11 @@
// Runs only on the server (Node.js) — uses NEXT_PUBLIC_HOST_URL which is inlined at build time.
import { IMod } from '@/src/types/Imod';
+import { Irace } from '@/src/types/Irace';
+import { IRaceUnits } from '@/src/types/IUnitShort';
+import { IRaceBuildings } from '@/src/types/IBuildingShort';
+import { IUnit } from '@/src/types/IUnit';
+import { IBuilding } from '@/src/types/IBuilding';
const API = process.env.NEXT_PUBLIC_HOST_URL || '';
export const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://dow-wiki.example.com';
@@ -17,19 +22,19 @@ async function fetchJson(url: string): Promise {
}
export async function getMod(modId: string) {
- return fetchJson<{ name: string; version: string }>(`${API}/api/v1/mods/${modId}`);
+ return fetchJson(`${API}/api/v1/mods/${modId}`);
}
export async function getRace(raceId: string) {
- return fetchJson<{ name: string }>(`${API}/api/v1/races/${raceId}`);
+ return fetchJson(`${API}/api/v1/races/${raceId}`);
}
export async function getUnit(unitId: string) {
- return fetchJson<{ name: string; description: string }>(`${API}/api/v1/units/${unitId}`);
+ return fetchJson(`${API}/api/v1/units/${unitId}`);
}
export async function getBuilding(buildingId: string) {
- return fetchJson<{ name: string; filename: string }>(`${API}/api/v1/buildings/${buildingId}`);
+ return fetchJson(`${API}/api/v1/buildings/${buildingId}`);
}
export async function getMods() {
@@ -41,9 +46,17 @@ export async function getRacesForMod(modId: string | number) {
}
export async function getUnitsForMod(modId: string | number) {
- return fetchJson; support: Array<{ id: number }>; tech: Array<{ id: number }> }>>(`${API}/api/v1/units/mod/${modId}`);
+ return fetchJson(`${API}/api/v1/units/mod/${modId}`);
}
export async function getBuildingsForMod(modId: string | number) {
- return fetchJson; buildingsAdvanced: Array<{ id: number }> }>>(`${API}/api/v1/buildings/mod/${modId}`);
+ return fetchJson(`${API}/api/v1/buildings/mod/${modId}`);
+}
+
+export async function getRaceUnits(modId: string | number, raceId: string) {
+ return fetchJson(`${API}/api/v1/units/${modId}/${raceId}`);
+}
+
+export async function getRaceBuildings(modId: string | number, raceId: string) {
+ return fetchJson(`${API}/api/v1/buildings/${modId}/${raceId}`);
}
diff --git a/src/classes/UnitsTable.tsx b/src/classes/UnitsTable.tsx
index 4e21e69..e0c7662 100644
--- a/src/classes/UnitsTable.tsx
+++ b/src/classes/UnitsTable.tsx
@@ -1,6 +1,5 @@
-import React, {useEffect, useState} from "react";
-import {AvailableBuildings, AvailableUnits} from "../core/api";
-import {Grid2, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper} from "@mui/material";
+import React from "react";
+import {Grid2, Table, TableBody, TableCell, TableHead, TableRow} from "@mui/material";
import {styled} from '@mui/material/styles';
import {IRaceUnits, IUnitShort} from "../types/IUnitShort";
import {IBuildingShort, IRaceBuildings} from "../types/IBuildingShort";
@@ -37,13 +36,13 @@ interface IUnitsTable {
racesBuildings: IRaceBuildings[]
}
-export default function UnitsTable(prop: {modId: number}) {
+export default function UnitsTable(prop: {modId: number, racesUnits: IRaceUnits[], racesBuildings: IRaceBuildings[]}) {
- const [unitsTable, setUnitsTable] = useState({
- racesUnits: [],
- racesBuildings: []
- });
+ const unitsTable: IUnitsTable = {
+ racesUnits: prop.racesUnits,
+ racesBuildings: prop.racesBuildings
+ };
function getUnitRef(modId: number, raceId: string, unit: IUnitShort) {
@@ -62,29 +61,6 @@ export default function UnitsTable(prop: {modId: number}) {
}
- useEffect(() => {
-
- fetch(AvailableUnits + "/mod/" + prop.modId)
- .then(resUnits => resUnits.json())
- .then((resUnits: IRaceUnits[]) => {
- if(resUnits.length <= 10){
- fetch(AvailableBuildings + "/mod/" + prop.modId)
- .then(resBuildings => resBuildings.json())
- .then((resBuildings: IRaceBuildings[]) => {
- setUnitsTable({
- racesUnits : resUnits,
- racesBuildings: resBuildings
- });
- });
- }else {
- setUnitsTable({
- racesUnits : resUnits,
- racesBuildings: []
- });
- }
- })
- }, []);
-
function armourTypePriority(armorTypeId: string): number {
switch(armorTypeId) {
case 'Infantry Low':
diff --git a/src/context/ThemeContext.tsx b/src/context/ThemeContext.tsx
index 7d76ccf..0211ff8 100644
--- a/src/context/ThemeContext.tsx
+++ b/src/context/ThemeContext.tsx
@@ -11,23 +11,32 @@ interface ThemeContextType {
toggleTheme: () => void;
}
+interface ThemeProviderProps {
+ initialMode?: ThemeMode;
+ children: ReactNode;
+}
+
const ThemeContext = createContext(undefined);
+const THEME_COOKIE_MAX_AGE = 60 * 60 * 24 * 365; // 1 год
-export const ThemeProvider = ({ children }: { children: ReactNode }) => {
- const [mode, setMode] = useState('dark');
+/**
+ * Синхронизирует выбранную тему с , cookie и localStorage.
+ * Cookie нужен, чтобы сервер (SSR) сразу отрендерил правильную тему без мерцания.
+ */
+function applyModeToDocument(mode: ThemeMode) {
+ document.cookie = `theme=${mode}; path=/; max-age=${THEME_COOKIE_MAX_AGE}; SameSite=Lax`;
+}
+export const ThemeProvider = ({ initialMode = 'dark', children }: ThemeProviderProps) => {
+ const [mode, setMode] = useState(initialMode);
+
+ // Применяем тему к и синхронизируем с cookie/localStorage.
useEffect(() => {
- const saved = localStorage.getItem('theme');
- if (saved === 'light') {
- setMode('light');
- }
- }, []);
-
- useEffect(() => {
- localStorage.setItem('theme', mode);
+ applyModeToDocument(mode);
}, [mode]);
+
const toggleTheme = () => {
setMode(prev => prev === 'dark' ? 'light' : 'dark');
};
diff --git a/src/index.css b/src/index.css
index ecc94a3..e505143 100644
--- a/src/index.css
+++ b/src/index.css
@@ -15,7 +15,7 @@
--scrollbar-track: #1a1a2e;
}
-body.light-theme {
+html[data-theme="light"] {
--bg-primary: #f5f5f5;
--bg-secondary: #ffffff;
--bg-tertiary: #e8e8e8;
@@ -41,7 +41,7 @@ body {
color: var(--text-primary);
}
-body.light-theme code {
+html[data-theme="light"] code {
background: rgba(0, 0, 0, 0.1);
}
diff --git a/src/legacy-pages/BuildingPage.tsx b/src/legacy-pages/BuildingPage.tsx
deleted file mode 100644
index 79bc5fb..0000000
--- a/src/legacy-pages/BuildingPage.tsx
+++ /dev/null
@@ -1,344 +0,0 @@
-import {AvailableBuildings, AvailableMods, IconUrl} from "../core/api";
-import React from "react";
-import {withRouter} from "../core/withrouter";
-import {IShortWeapon} from "../types/IUnit";
-import '../css/Building.css'
-import {
- Button,
- Box,
- Container,
- Divider,
- Grid2,
- Link,
- ListItem,
- Paper,
- Table,
- TableBody,
- TableCell,
- TableRow,
- Tooltip,
- Typography, LinearProgress, Theme,
-} from "@mui/material";
-import ArmorType from "../classes/ArmorType";
-import AvTimerOutlinedIcon from '@mui/icons-material/AvTimer';
-import WeaponSlot from "../classes/WeaponSlot";
-import UnitsTable from "../classes/UnitsTable";
-import {IMod} from "../types/Imod";
-import {IBuilding} from "../types/IBuilding";
-import Vision from "../classes/Vision";
-import BuildingAddon from "../classes/building/BuildingAddon";
-import {IUnitShort} from "../types/IUnitShort";
-import Research, {AffectedResearches} from "../classes/building/Research";
-import Required from "../classes/Required";
-import {ModifiersProvidesTable} from "../classes/ModifiersProvideTable";
-import Ability from "../classes/Ability";
-import DeathExplosion from "../classes/DeathExplosion";
-import {styled} from '@mui/material/styles';
-import {withTheme} from "../core/api";
-import {DescriptionBox} from "../commons/DescriptionBox";
-import {ArrowBack} from "@mui/icons-material";
-import {BackButton} from "../commons/BackButton";
-
-
-const SectionTitle = styled(Typography)(({ theme }) => ({
- fontWeight: 700,
- mb: 2,
- fontSize: '1.25rem',
-}));
-
-const StatsPaper = 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' : 'rgba(0, 0, 0, 0.85)',
- 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' : 'rgba(0, 0, 0, 0.85)' },
-}));
-
-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(0,0,0,0.08)', borderColor: theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.3)' : 'rgba(0,0,0,0.2)' },
-}));
-
-interface UintPageState {
- building: IBuilding,
- mod: IMod,
-}
-
-function Unit (unit: IUnitShort, modId: number, raceId: String, theme: Theme) {
-
- return (
-
-
- {unit.icon &&
}
- {unit.name}
- {unit.canDetect &&
}
-
-
- )
-}
-
-
-function Building(building: IBuilding, mod: IMod, theme: Theme) {
-
- const isDark = theme.palette.mode === 'dark';
-
- let mapBuildingWeapons: Map> = new Map();
-
- building.weapons.forEach(weapon => {
- const weaponMap = mapBuildingWeapons.get(weapon.hardpoint)
- if (weaponMap == null) {
- const weaponMap = new Map()
- weaponMap.set(weapon.hardpointOrder, weapon.weapon)
- mapBuildingWeapons.set(weapon.hardpoint, weaponMap)
- } else {
- weaponMap.set(weapon.hardpointOrder, weapon.weapon)
- }
- })
-
- var buildingName = building.name;
- if(building.name == null){
- buildingName = building.filename.replaceAll('_', ' ').replace('.rgd', '');
- }
-
- return (
-
-
-
- {building.icon &&
-
}
- {buildingName}
-
-
- {mod.name} ({mod.version})
-
-
-
-
-
-
-
-
-
- Cost
-
- {building.buildCostRequisition > 0 &&
-
- {building.buildCostRequisition.toFixed(0)}}
- {building.buildCostPower > 0 &&
- {building.buildCostPower.toFixed(0)}}
- {(building.buildCostPopulation !== undefined && building.buildCostPopulation > 0) &&
-
- {building.buildCostPopulation.toFixed(0)}}
- {(building.buildCostFaith !== undefined && building.buildCostFaith > 0) &&
-
- {building.buildCostFaith}}
- {(building.buildCostSouls !== undefined && building.buildCostSouls > 0) &&
-
- {building.buildCostSouls.toFixed(0)}}
- {(building.buildCostTime !== undefined && building.buildCostTime > 0) &&
-
- {building.buildCostTime}s}
-
-
- {(building.requisitionIncome !== undefined && building.requisitionIncome > 0 || building.powerIncome !== undefined && building.powerIncome !== null || building.faithIncome !== undefined && building.faithIncome !== null) &&
-
- Resource income
-
- {building.requisitionIncome !== undefined && building.requisitionIncome != null &&
-
- {building.requisitionIncome}}
- {building.powerIncome !== undefined && building.powerIncome > 0 &&
-
- {building.powerIncome}}
- {building.faithIncome !== undefined && building.faithIncome > 0 &&
-
- {building.faithIncome}}
-
-
- }
-
- Armor type
-
- {building.armorType2 == null ?
- : }
-
-
-
- Health
-
-
- {building.health} {building.healthRegeneration > 0 &&
- +{building.healthRegeneration}/s}
-
-
-
- Sight
-
-
-
-
-
- Repair max
- {building.repairMax}
-
-
-
-
-
-
-
- {building.description}
-
-
- {building.requirements !== null &&
-
-
- }
- {building.modifiers.length > 0 &&
-
- Affected on
-
- }
- {building.units.length > 0 &&
- Unit production
-
- {building.units.map(unit =>
- Unit(unit, mod.id, building.race.id, theme)
- )}
-
- }
- {building.abilities.length > 0 &&
- Abilities
- {building.abilities.map(a =>
-
- )}
- }
- {building.deathExplosions.length > 0 &&
-
- {building.deathExplosions.map(da =>
-
- )}
-
- }
- {building.addons.length > 0 &&
- Addons
- {building.addons.map(b =>
-
- )}
- }
- {building.researches.length > 0 &&
- Researches
- {building.researches.map(r =>
-
- )}
- }
-
- {[...mapBuildingWeapons.keys()].sort(function (a, b) {
- return a - b;
- }).map(h => )}
-
-
-
-
- Hotkey: {building.hotkey} Filename: {building.filename}
-
-
-
- )
-}
-
-class BuildingPage extends React.Component {
-
- async componentDidMount() {
- const buildingResponse = await fetch(AvailableBuildings + "/" + this.props.match.params.buildingId);
- const buildingData: IBuilding = await buildingResponse.json();
-
- this.setState({
- building: buildingData
- });
-
- const responseMod = await fetch(AvailableMods + "/" + this.props.match.params.modId);
- const modData: IMod = await responseMod.json();
-
- this.setState({
- mod: modData
- });
- }
-
- render() {
-
- 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 { theme } = this.props;
-
- return (
-
- }
- href={backRef}
- >
- Back to race
-
- {Building(this.state.building, this.state.mod, theme)}
-
- );
- } else {
- return (
-
-
-
- );
- }
- }
-}
-
-export default withRouter(withTheme(BuildingPage));
diff --git a/src/legacy-pages/ModPage.tsx b/src/legacy-pages/ModPage.tsx
deleted file mode 100644
index 36abb62..0000000
--- a/src/legacy-pages/ModPage.tsx
+++ /dev/null
@@ -1,114 +0,0 @@
-import {AvailableMods, AvailableRacesPart, AvailableUnits, IconUrl} from "../core/api";
-import React, { useState } from "react";
-import {withRouter} from "../core/withrouter";
-import {withTheme} from "../core/api";
-import {IMod} from "../types/Imod";
-import {
- ArrowBack,
-} from "@mui/icons-material";
-import {
- Box,
- Button,
- Container,
- Paper,
- Typography,
- Divider,
- LinearProgress
-} from "@mui/material";
-import {styled} from '@mui/material/styles';
-import UnitsTable from "../classes/UnitsTable";
-import {BackButton} from "../commons/BackButton";
-
-
-
-const ModNotFound = styled(Typography)(({ theme }) => ({
- color: theme.palette.text.secondary,
-}));
-
-
-interface ModPageState {
- mod: IMod | null,
- loading: boolean,
-}
-
-
-
-class ModPage extends React.Component {
-
- constructor(props: any) {
- super(props);
- this.state = {
- mod: null,
- loading: true
- };
-
- fetch(AvailableMods + "/" + this.props.match.params.modId)
- .then(res => res.json())
- .then((res: IMod) => {
- this.setState({
- mod: res,
- loading: false
- });
- })
- .catch(() => {
- this.setState({ loading: false });
- });
- }
-
-
- render() {
- if (this.state.loading) {
- return (
-
-
-
- );
- }
-
- if (this.state != null && this.state.mod != null) {
- const isDark = this.props.theme.palette.mode === 'dark';
-
- return (
-
- }
- href="/"
- >
- Back to mods list
-
-
-
- {this.state.mod.name} ({this.state.mod.version})
-
-
-
-
-
-
- );
- } else {
- return (
-
-
- Mod not found
-
-
- );
- }
- }
-}
-
-
-
-export default withRouter(withTheme(ModPage));
diff --git a/src/legacy-pages/ModsPage.tsx b/src/legacy-pages/ModsPage.tsx
deleted file mode 100644
index 9083b85..0000000
--- a/src/legacy-pages/ModsPage.tsx
+++ /dev/null
@@ -1,321 +0,0 @@
-import React from "react";
-import {
- Box,
- Container,
- Typography,
- Grid,
- Card,
- CardContent,
- CardActions,
- Chip,
- Button,
- Divider,
- useTheme,
- Paper,
- LinearProgress,
- Link,
-} from "@mui/material";
-import { styled } from '@mui/material/styles';
-import { IMod } from "../types/Imod";
-
-
-const ModCard = styled(Card)(({ theme }) => ({
- height: '100%',
- display: 'flex',
- flexDirection: 'column',
- background: theme.palette.mode === 'dark'
- ? 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)'
- : 'linear-gradient(145deg, #ffffff 0%, #f5f5f5 100%)',
- border: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
- borderRadius: '16px',
- transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
- position: 'relative',
- overflow: 'hidden',
- '&::before': {
- content: '""',
- position: 'absolute',
- top: 0,
- left: 0,
- right: 0,
- height: '3px',
- background: theme.palette.mode === 'dark'
- ? 'linear-gradient(90deg, #dee2e6 0%, #cccccc 100%)'
- : 'linear-gradient(90deg, #000000 0%, #333333 100%)',
- transform: 'scaleX(0)',
- transition: 'transform 0.3s ease',
- },
- '&:hover': {
- boxShadow: theme.palette.mode === 'dark' ? '0 20px 40px rgba(255, 255, 255, 0.1)' : '0 20px 40px rgba(0, 0, 0, 0.2)',
- borderColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)',
- '&::before': {
- transform: 'scaleX(1)',
- },
- },
-}));
-
-const VersionLink = styled(Link)(({ theme }) => ({
- display: 'flex',
- alignItems: 'center',
- padding: theme.spacing(1.5, 2),
- marginBottom: theme.spacing(1),
- background: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.05)',
- borderRadius: '10px',
- textDecoration: 'none',
- color: 'inherit',
- transition: 'all 0.2s ease',
- border: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
- '&:hover': {
- background: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)',
- borderColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)',
- },
-}));
-
-const ModTitle = styled(Typography)(({ theme }) => ({
- fontWeight: 700,
- mb: 1,
- background: theme.palette.mode === 'dark'
- ? 'linear-gradient(135deg, #dee2e6 0%, #e0e0e0 100%)'
- : 'none',
- WebkitBackgroundClip: theme.palette.mode === 'dark' ? 'text' : 'initial',
- WebkitTextFillColor: theme.palette.mode === 'dark' ? '#dee2e6' : 'initial',
- color: theme.palette.mode === 'dark' ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)',
-}));
-
-const ModDescription = styled(Typography)(({ theme }) => ({
- color: theme.palette.text.secondary,
- lineHeight: 1.6,
-}));
-
-const VersionsChip = styled(Chip)(({ theme }) => ({
- backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.15)' : 'rgba(0, 0, 0, 0.1)',
- color: theme.palette.mode === 'dark' ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)',
- fontWeight: 600,
-}));
-
-const StyledDivider = styled(Divider)(({ theme }) => ({
- my: 2,
- borderColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)',
-}));
-
-const MainVersionsLabel = styled(Typography)(({ theme }) => ({
- color: theme.palette.text.primary,
- fontWeight: 600,
- mb: 1.5,
-}));
-
-const VersionText = styled(Typography)(({ theme }) => ({
- fontWeight: 600,
- color: theme.palette.text.secondary,
-}));
-
-const VersionArrow = styled(Typography)(({ theme }) => ({
- color: theme.palette.text.secondary,
-}));
-
-const BetaVersionsLabel = styled(Typography)(({ theme }) => ({
- color: 'rgba(255, 193, 7, 0.9)',
- fontWeight: 600,
- mb: 1,
-}));
-
-const OpenLatestButton = styled(Button)(({ theme }) => {
- const isDark = theme.palette.mode === 'dark';
- return {
- width: '100%',
- color: isDark ? '#FFD700' : '#1976d2',
- borderColor: isDark ? 'rgba(255, 215, 0, 0.5)' : 'rgba(25, 118, 210, 0.5)',
- fontWeight: 600,
- borderRadius: '10px',
- textTransform: 'none',
- fontSize: '1rem',
- py: 1.5,
- '&:hover': {
- borderColor: isDark ? '#FFD700' : '#1976d2',
- backgroundColor: isDark ? 'rgba(255, 215, 0, 0.1)' : 'rgba(25, 118, 210, 0.1)',
- boxShadow: isDark ? '0 8px 20px rgba(255, 215, 0, 0.2)' : '0 8px 20px rgba(25, 118, 210, 0.2)',
- },
- };
-});
-
-interface ModsProps {
- mods: IMod[];
-}
-
-function Mods({ mods }: ModsProps) {
- const theme = useTheme();
-
- // Sort all mods by order, then group by name
- const sortedMods = [...mods].sort((a, b) => a.order - b.order);
-
- const mapWithModVersions = new Map();
- sortedMods.forEach(mod => {
- const versionList = mapWithModVersions.get(mod.name);
- if (versionList == null) {
- mapWithModVersions.set(mod.name, [mod]);
- } else {
- versionList.push(mod);
- }
- });
-
- const getLatestVersion = (modName: String) => {
- const sameMods = mapWithModVersions.get(modName) ?? [];
- // The latest version is the one with the maximum id
- const latest = sameMods.reduce((max, mod) => mod.id > max.id ? mod : max, sameMods[0]);
- const betaMods = sameMods.filter(m => m.isBeta);
- return {
- latest: latest,
- allVersions: sameMods,
- hasBeta: betaMods.length > 0,
- betaVersion: betaMods[0]
- };
- };
-
-
- function ModCardComponent(modName: String, index: number) {
- const sameMods = mapWithModVersions.get(modName) ?? [];
- const { latest, hasBeta, betaVersion } = getLatestVersion(modName);
-
- return (
-
-
-
-
- {modName}
-
-
-
-
-
-
-
- {sameMods.filter(m => !m.isBeta).map(mod => (
-
-
-
- Version {mod.version}
-
-
- →
-
-
-
- ))}
-
- {hasBeta && (
-
-
- Beta versions:
-
- {betaVersion && (
-
-
-
- Version {betaVersion.version} (Beta)
-
-
- →
-
-
-
- )}
-
- )}
-
-
-
-
-
-
- );
- }
-
- return (
-
-
- {[...new Set(sortedMods.map(m => m.name))].map((modName, index) => (
-
- {ModCardComponent(modName, index)}
-
- ))}
-
-
-
- );
-}
-
-interface ModsPageState {
- mods: IMod[];
- loading: boolean;
-}
-
-interface ModsPageProps {
- initialMods?: IMod[];
-}
-
-class ModsPage extends React.Component {
-
- constructor(props: ModsPageProps) {
- super(props);
- this.state = {
- mods: props.initialMods ?? [],
- loading: false
- };
- }
-
- async componentDidMount() {
- // Очистка кэша сайта при заходе на главную страницу (один раз за сессию)
- if (!sessionStorage.getItem('cache_cleared')) {
- sessionStorage.setItem('cache_cleared', '1');
- try {
- if ('caches' in window) {
- const names = await caches.keys();
- await Promise.all(names.map(name => caches.delete(name)));
- }
- if ('serviceWorker' in navigator) {
- const regs = await navigator.serviceWorker.getRegistrations();
- await Promise.all(regs.map(r => r.unregister()));
- }
- } catch (e) {
- console.error('Cache clear failed:', e);
- }
- }
- }
-
- render() {
- if (this.state.loading) {
- return (
-
-
-
- );
- }
-
- return ;
- }
-}
-
-export default ModsPage;
diff --git a/src/legacy-pages/RacePageFast.tsx b/src/legacy-pages/RacePageFast.tsx
deleted file mode 100644
index 0d9c549..0000000
--- a/src/legacy-pages/RacePageFast.tsx
+++ /dev/null
@@ -1,332 +0,0 @@
-import {AvailableBuildings, AvailableMods, AvailableRacesPart, AvailableUnits, IconUrl} from "../core/api";
-import React from "react";
-import {withRouter} from "../core/withrouter";
-import {IMod} from "../types/Imod";
-import {Irace} from "../types/Irace";
-import '../css/Unit.css'
-import {
- Accordion,
- AccordionDetails,
- AccordionSummary,
- Box,
- Button,
- Container,
- Divider,
- Grid2, LinearProgress,
- Link,
- List,
- ListItem,
- Paper,
- Typography,
- Theme,
-} from "@mui/material";
-import {ArrowBack, ExpandMore} from "@mui/icons-material";
-import {IRaceUnits, IUnitShort} from "../types/IUnitShort";
-import {IBuildingShort, IRaceBuildings} from "../types/IBuildingShort";
-import {styled} from '@mui/material/styles';
-import {withTheme} from "../core/api";
-import {StyledLink} from "../commons/StyledLink";
-import {BackButton} from "../commons/BackButton";
-
-const SectionTitle = styled(Typography)(({ theme }) => ({
- fontWeight: 700,
- mb: 2,
- fontSize: '1.25rem',
-}));
-
-const UnitCard = 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: '16px',
- transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
- '&:hover': {
- boxShadow: theme.palette.mode === 'dark' ? '0 20px 40px rgba(255, 255, 255, 0.1)' : '0 20px 40px rgba(0, 0, 0, 0.2)',
- borderColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)',
- },
-}));
-
-const BuildingCard = styled(UnitCard)(({ theme }) => ({
- padding: theme.spacing(1),
-}));
-
-const UnitLink = styled(Link)(({ theme }) => ({
- color: theme.palette.text.primary,
- textDecoration: 'none',
- display: 'block',
- '&:hover': { color: theme.palette.mode === 'dark' ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)' }
-}));
-
-const LoadingText = styled(Typography)(({ theme }) => ({
- color: theme.palette.text.secondary,
-}));
-
-interface RacePageState {
- mod: IMod,
- race: Irace,
- units: IUnitShort[],
-}
-
-
-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 (
-
-
- {unit.icon &&
}
- {unit.name}
- {unit.canDetect &&
}
-
-
- )
-}
-
-function UnitSmall(unit: IUnitShort, modId: number, raceId: String) {
-
- var unitName = ""
- if (unit.name.length > 21) {
- unitName = unit.name.substring(0, 19) + "...";
- } else {
- unitName = unit.name;
- }
-
- return (
- {unit.icon &&
}
- {unitName}
- {unit.canDetect &&
}
)
-}
-
-function Building(building: IBuildingShort, modId: number, raceId: String, theme: Theme) {
- const isDark = theme.palette.mode === 'dark';
-
- return (
-
-
-
- {building.icon &&
}
- {building.name}
- {building.canDetect &&
}
-
- {building.units.map(unit => UnitSmall(unit, modId, raceId))}
-
-
- )
-}
-
-interface UnitsProps {
- raceId: string,
- modId: number,
-}
-
-interface UnitsState {
- selectedUnits: String | null,
- units: IRaceUnits | null,
- buildings: IRaceBuildings | null,
-}
-
-class Units extends React.Component {
-
-
- constructor(props: any) {
-
- super(props);
- let urlUnits = AvailableUnits + "/" + this.props.modId + "/" + this.props.raceId;
-
-
- fetch(urlUnits)
- .then(res => res.json())
- .then((res: IRaceUnits) => {
- this.setState({
- units: res,
- })
- })
-
- let urlBuildings = AvailableBuildings + "/" + this.props.modId + "/" + this.props.raceId;
-
- fetch(urlBuildings)
- .then(res => res.json())
- .then((res: IRaceBuildings) => {
- this.setState({
- buildings: res,
- })
- })
- }
-
- render() {
- const { theme } = this.props;
- const isDark = theme.palette.mode === 'dark';
-
- if (this.state && this.state.units) {
-
- if(this.state.buildings?.race.name !== undefined){
- // title managed by Helmet in parent component
- }
-
- const accordionSx = isDark ? {
- background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
- border: '1px solid rgba(255, 255, 255, 0.1)',
- borderRadius: '12px',
- '&:before': { display: 'none' },
- '&.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 ?
-
-
- {this.state.buildings.buildings.map(building => Building(building, this.props.modId, this.props.raceId, theme))}
-
- {this.state.buildings.buildingsAdvanced.length > 0 &&
-
- Advanced buildings
-
- {this.state.buildings.buildingsAdvanced.map(building => Building(building, this.props.modId, this.props.raceId, theme))}
-
}
-
-
- }
- aria-controls="units-accordion"
- id="units-accordion"
- sx={{color: isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)', '& .MuiAccordionSummary-content': { margin: '12px 0' }, '& .MuiAccordionSummary-content.Mui-expanded': { margin: '12px 0' }}}
- >
- All units
-
-
-
-
- Infantry
- {this.state.units.infantry.map(unit => Unit(unit, this.props.modId, this.props.raceId, theme))}
-
-
- Tech
-
- {this.state.units.tech.map(unit => Unit(unit, this.props.modId, this.props.raceId, theme))}
-
-
-
- Support
-
- {this.state.units.support.map(unit => Unit(unit, this.props.modId, this.props.raceId, theme))}
-
-
-
-
-
-
- : Loading
- )
- } else {
- return Loading...;
- }
- }
-}
-
-const UnitsWithTheme = withTheme(Units) as React.ComponentType;
-
-class RacePageFast extends React.Component {
-
- constructor(props: any) {
- super(props);
- }
-
-
- async componentDidMount() {
-
- const responseMod = await fetch(AvailableMods + "/" + this.props.match.params.modId);
- const modData: IMod = await responseMod.json();
-
- this.setState({
- mod: modData
- });
-
- const response = await fetch(AvailableRacesPart + "/" + this.props.match.params.raceId);
- const racesData: Irace = await response.json();
-
- this.setState({
- race: racesData
- });
- }
-
- render() {
-
- if (this.state != null && this.state.mod != null && this.state.race != null) {
- const backRef = "/mod/" + this.state.mod.id
- const theme = this.props.theme;
- const isDark = theme.palette.mode === 'dark';
-
- return (
-
- }
- href={backRef}
- >
- Back to mod
-
-
-
-
- {this.state.race.name}
-
-
- {this.state.mod.name} ({this.state.mod.version})
-
-
-
-
-
- );
- } else {
- return (
-
-
-
- );
- }
- }
-}
-
-export default withRouter(withTheme(RacePageFast));
diff --git a/src/legacy-pages/UnitPage.tsx b/src/legacy-pages/UnitPage.tsx
deleted file mode 100644
index 2e19b89..0000000
--- a/src/legacy-pages/UnitPage.tsx
+++ /dev/null
@@ -1,435 +0,0 @@
-import {AvailableMods, AvailableUnits, IconUrl} from "../core/api";
-import React from "react";
-import {withRouter} from "../core/withrouter";
-import {IShortWeapon, IUnit} from "../types/IUnit";
-import '../css/Unit.css'
-import {
- Accordion,
- AccordionDetails,
- AccordionSummary,
- Box,
- Button,
- Container,
- Divider,
- Grid2, LinearProgress,
- Paper,
- Table,
- TableBody,
- TableCell,
- TableRow,
- Tooltip,
- Typography, Theme,
-} from "@mui/material";
-import {ArrowBack, ExpandMore} from "@mui/icons-material";
-import ArmorType from "../classes/ArmorType";
-import AvTimerOutlinedIcon from '@mui/icons-material/AvTimer';
-import Sergeant from "../classes/Sergeant";
-import WeaponSlot from "../classes/WeaponSlot";
-import UnitsTable from "../classes/UnitsTable";
-import {IMod} from "../types/Imod";
-import Vision from "../classes/Vision";
-import Required from "../classes/Required";
-import {AffectedResearches} from "../classes/building/Research";
-import {ModifiersProvidesTable} from "../classes/ModifiersProvideTable";
-import Ability from "../classes/Ability";
-import DeathExplosion from "../classes/DeathExplosion";
-import Jump from "../classes/Jump";
-import {styled} from '@mui/material/styles';
-import {withTheme} from "../core/api";
-import {DescriptionBox} from "../commons/DescriptionBox";
-import {BackButton} from "../commons/BackButton";
-
-const SectionTitle = styled(Typography)(({ theme }) => ({
- fontWeight: 700,
- mb: 2,
- fontSize: '1.25rem',
-}));
-
-const StatsPaper = 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': {
- '&:last-child td, &:last-child th': { border: 0 },
- },
- '& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': {
- color: theme.palette.mode === 'dark' ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)',
- 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 {
- unit: IUnit,
- mod: IMod,
-}
-
-
-function Unit(unit: IUnit, mod: IMod, theme: Theme) {
-
- const isDark = theme.palette.mode === 'dark';
-
- const morale = (unit.moraleMax !== null) ?
-
{unit.moraleMax}
- +{unit.moraleRegeneration}/s
- {unit.moraleDeathPenalty > 0 &&
-{unit.moraleDeathPenalty}}
-
- : "-"
-
- let mapWithUnitWeapons: Map> = new Map();
-
- unit.weapons.forEach(weapon => {
- const weaponMap = mapWithUnitWeapons.get(weapon.hardpoint)
- if (weaponMap == null) {
- const weaponMap = new Map()
- weaponMap.set(weapon.hardpointOrder, weapon.weapon)
- mapWithUnitWeapons.set(weapon.hardpoint, weaponMap)
- } else {
- weaponMap.set(weapon.hardpointOrder, weapon.weapon)
- }
- })
-
- type sergeantProps = {
- name: string
- icon: String
- canDetect: Boolean
- }
-
- const SergeantShort = (props: sergeantProps) => {
-
- return (
-
- {props.icon &&
}
- {props.name}
- {props.canDetect &&
}
-
- )
- }
-
-
- return (
-
-
-
- {unit.icon &&
-
}
- {unit.name}
-
-
- {mod.name} ({mod.version})
-
-
-
-
-
-
-
-
-
- Cost
-
- {unit.buildCostRequisition > 0 &&
-
- {unit.buildCostRequisition.toFixed(0)}}
- {unit.buildCostPower > 0 &&
- {unit.buildCostPower.toFixed(0)}}
- {(unit.buildCostPopulation !== undefined && unit.buildCostPopulation > 0) &&
-
- {unit.buildCostPopulation.toFixed(0)}}
- {(unit.buildCostFaith !== undefined && unit.buildCostFaith > 0) &&
-
- {unit.buildCostFaith}}
- {(unit.buildCostSouls !== undefined && unit.buildCostSouls > 0) &&
-
- {unit.buildCostSouls.toFixed(0)}}
-
- {unit.capInfantry > 0 &&
- {unit.capInfantry}}
- {unit.capSupport > 0 &&
- {unit.capSupport}}
- {(unit.buildCostTime !== undefined && unit.buildCostTime > 0) &&
-
- {unit.buildCostTime}s}
-
-
- {(unit?.reinforceTime !== 0 && unit.reinforceTime !== null && unit.squadMaxSize > 1) &&
-
- Reinforce cost
-
- {unit.reinforceCostRequisition && unit.reinforceCostRequisition > 0 ?
-
- {unit.reinforceCostRequisition.toFixed(0)}:}
- {unit.reinforceCostPower !== undefined && unit.reinforceCostPower > 0 &&
-
- {unit.reinforceCostPower.toFixed(0)}}
- {(unit.reinforceCostPopulation !== undefined && unit.reinforceCostPopulation > 0) &&
-
- {unit.reinforceCostPopulation.toFixed(0)}}
- {(unit.reinforceCostFaith !== undefined && unit.reinforceCostFaith > 0) &&
-
- {unit.reinforceCostFaith}}
- {(unit.reinforceCostSouls !== undefined && unit.reinforceCostSouls > 0) &&
-
- {unit.reinforceCostSouls.toFixed(0)}}
- {(unit.reinforceTime !== undefined && unit.reinforceTime > 0) &&
-
- {unit.reinforceTime}s}
-
-
- }
- {(unit.requisitionIncome !== undefined && unit.requisitionIncome !== null || unit.powerIncome !== undefined && unit.powerIncome !== null || unit.faithIncome !== undefined && unit.faithIncome !== null) &&
-
- Resource income
-
- {unit.requisitionIncome !== undefined && unit.requisitionIncome > 0 &&
-
- {unit.requisitionIncome}}
- {unit.powerIncome !== undefined && unit.powerIncome > 0 &&
-
- {unit.powerIncome}}
- {unit.faithIncome !== undefined && unit.faithIncome > 0 &&
-
- {unit.faithIncome}}
-
-
- }
- {unit.squadMaxSize > 1 &&
-
- Squad size
- {unit.squadStartSize} / {unit.squadMaxSize}
-
- }
-
- Armor type
-
- {unit.armorType2 == null ? : }
-
-
-
- Health
-
-
- {unit.health} {unit.healthRegeneration > 0 &&
- +{unit.healthRegeneration}/s}
- {unit.armour !== undefined && unit.armour !== 0 &&
{unit.armour} }
-
-
-
- Move speed
- {unit.moveSpeed}
-
-
- Morale
- {morale}
-
-
- Mass
- {unit.mass}
-
-
- Vision
-
-
- {unit.repairMax !== undefined && unit.repairMax !== null &&
-
- Repair max
- {unit.repairMax}
-
- }
- {unit.repairSpeed !== undefined && unit.repairSpeed !== null && unit.repairCostPercent !== null &&
-
- Repair
- {unit.repairSpeed} hp/s; {unit.repairCostPercent}% cost
-
- }
- {unit.mobValue != null &&
-
- Mob value
-
{unit.mobValue}
-
- }
- {unit.squadLimit !== undefined && unit.squadLimit !== null &&
-
- Limit
- {unit.squadLimit}
-
- }
-
-
-
-
-
-
- {unit.description}
-
-
- {unit.requirements !== null &&
-
-
- }
-
- {unit.sergeants.map(s =>
-
-
- 0}/>
-
-
-
-
- )}
-
-
- {unit.moraleMax !== null && !(unit.moraleBrakeModifiers.find(m => m.reference.includes("accuracy_weapon_modifier") && m.value === 0.2) !== undefined &&
- unit.moraleBrakeModifiers.find(m => m.reference.includes("speed_maximum_modifier") && m.value === 1.2) !== undefined &&
- unit.moraleBrakeModifiers.length === 2)
- &&
-
Morale broken
-
- }
-
- {unit.modifiers.length > 0 &&
- Affected on
-
- }
-
- {unit.abilities.length > 0 &&
- Abilities
- {unit.abilities.map(a =>
-
- )}
- }
-
- {unit.jumps != null &&
-
- }
-
-
- {[...mapWithUnitWeapons.keys()].sort(function (a, b) {
- return a - b;
- }).map(h => )}
-
- {unit.deathExplosions.length > 0 &&
-
- {unit.deathExplosions.map(da =>
-
- )}
-
- }
-
-
-
-
-
-
- Hotkey: {unit.hotkey} Filename: {unit.filename}
-
-
-
-
- )
-}
-
-
-
-
-class UnitPage extends React.Component {
-
-
- async componentDidMount() {
- const unitsResponse = await fetch(AvailableUnits + "/" + this.props.match.params.unitId);
- const unitsData: IUnit = await unitsResponse.json();
-
- this.setState({
- unit: unitsData
- });
-
- const responseMod = await fetch(AvailableMods + "/" + this.props.match.params.modId);
- const modData: IMod = await responseMod.json();
-
- this.setState({
- mod: modData
- });
- }
-
- render() {
-
- 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 { theme } = this.props;
-
- return (
-
- }
- href={backRef}
- >
- Back to race
-
- {Unit(this.state.unit, this.state.mod, theme)}
-
- );
- } else {
- return (
-
-
-
- );
- }
- }
-}
-
-export default withRouter(withTheme(UnitPage));