Moved to next js ssr

This commit is contained in:
anibus 2026-08-15 14:46:19 +03:00
parent 4e4f798d85
commit 2184f1642c
37 changed files with 854 additions and 17862 deletions

7
.env
View File

@ -1,2 +1,5 @@
#REACT_APP_HOST_URL=https://wiki-backend.dawn-of-war.pro
REACT_APP_HOST_URL=http://localhost:8082
NEXT_PUBLIC_HOST_URL=https://wiki-backend.dawn-of-war.pro
#NEXT_PUBLIC_HOST_URL=http://localhost:8082
# Домен сайта для sitemap/robots/canonical (замените на реальный)
NEXT_PUBLIC_SITE_URL=https://wiki.dawn-of-war.pro

1
.gitignore vendored
View File

@ -2,6 +2,7 @@
# dependencies
/node_modules
/.next
/.pnp
.pnp.js

78
app/layout.tsx Normal file
View File

@ -0,0 +1,78 @@
import type { Metadata, Viewport } from 'next';
import '@/src/index.css';
import '@/src/App.css';
import '@/src/css/Unit.css';
import '@/src/css/Building.css';
import { ThemeProvider } from '@/src/context/ThemeContext';
import AppShell from '@/components/AppShell';
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://dow-wiki.example.com';
export const metadata: Metadata = {
metadataBase: new URL(siteUrl),
title: {
default: 'Dawn of War Wiki — юниты, расы и здания всех модов',
template: '%s — Dawn of War Wiki',
},
description:
'Dawn of War wiki. Unification mod wiki. Unification mod unit stats. Supported all popular mods.',
manifest: '/manifest.json',
applicationName: 'Dawn of War Wiki',
verification: {
google: 'Q4XMn2UDpL2xmK7iBfaWQpVN8EPy8oGxN-7HqSKapb0',
},
openGraph: {
type: 'website',
siteName: 'Dawn of War Wiki',
title: 'Dawn of War Wiki — юниты, расы и здания всех модов',
description:
'Dawn of War wiki. Unification mod wiki. Unification mod unit stats. Supported all popular mods.',
images: ['/logo512.png'],
},
twitter: {
card: 'summary_large_image',
},
robots: {
index: true,
follow: true,
},
};
export const viewport: Viewport = {
themeColor: '#000000',
width: 'device-width',
initialScale: 1,
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
{/* Yandex.Metrika */}
<script
type="text/javascript"
dangerouslySetInnerHTML={{
__html:
"(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};m[i].l=1*new Date();for(var j=0;j<document.scripts.length;j++){if(document.scripts[j].src===r){return;}}k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})(window,document,\"script\",\"https://mc.yandex.ru/metrika/tag.js\",\"ym\");ym(99502794,\"init\",{clickmap:true,trackLinks:true,accurateTrackBounce:true,webvisor:true});",
}}
/>
<noscript>
<div>
<img
src="https://mc.yandex.ru/watch/99502794"
style={{ position: 'absolute', left: -9999 }}
alt=""
/>
</div>
</noscript>
<ThemeProvider>
<AppShell>{children}</AppShell>
</ThemeProvider>
</body>
</html>
);
}

33
app/mod/[modId]/page.tsx Normal file
View File

@ -0,0 +1,33 @@
import type { Metadata } from 'next';
import ModPageClient from '@/components/pages/ModPageClient';
import { getMod } from '@/lib/api-server';
export const dynamic = 'force-dynamic';
interface Props {
params: { modId: string };
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const mod = await getMod(params.modId);
if (!mod) {
return { title: 'Mod' };
}
const title = `${mod.name} (${mod.version})`;
const description = `Dawn of War wiki. ${mod.name} mod. Units, races, buildings and stats.`;
return {
title,
description,
alternates: {
canonical: `/mod/${params.modId}`,
},
openGraph: {
title,
description,
},
};
}
export default function ModPage() {
return <ModPageClient />;
}

View File

@ -0,0 +1,37 @@
import type { Metadata } from 'next';
import BuildingPageClient from '@/components/pages/BuildingPageClient';
import { getBuilding, getMod } from '@/lib/api-server';
export const dynamic = 'force-dynamic';
interface Props {
params: { modId: string; raceId: string; buildingId: string };
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const [mod, building] = await Promise.all([getMod(params.modId), getBuilding(params.buildingId)]);
if (!mod || !building) {
return { title: 'Building' };
}
let buildingName = building.name;
if (!buildingName) {
buildingName = building.filename.replaceAll('_', ' ').replace('.rgd', '');
}
const title = `${buildingName}${mod.name}`;
const description = `${buildingName} building in ${mod.name} mod. Stats, production, researches.`;
return {
title,
description,
alternates: {
canonical: `/mod/${params.modId}/race/${params.raceId}/building/${params.buildingId}`,
},
openGraph: {
title,
description,
},
};
}
export default function BuildingPage() {
return <BuildingPageClient />;
}

View File

@ -0,0 +1,33 @@
import type { Metadata } from 'next';
import RacePageClient from '@/components/pages/RacePageClient';
import { getMod, getRace } from '@/lib/api-server';
export const dynamic = 'force-dynamic';
interface Props {
params: { modId: string; raceId: string };
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const [mod, race] = await Promise.all([getMod(params.modId), getRace(params.raceId)]);
if (!mod || !race) {
return { title: 'Race' };
}
const title = `${race.name}${mod.name}`;
const description = `${race.name} race in ${mod.name} mod. Units, buildings and stats.`;
return {
title,
description,
alternates: {
canonical: `/mod/${params.modId}/race/${params.raceId}`,
},
openGraph: {
title,
description,
},
};
}
export default function RacePage() {
return <RacePageClient />;
}

View File

@ -0,0 +1,35 @@
import type { Metadata } from 'next';
import UnitPageClient from '@/components/pages/UnitPageClient';
import { getMod, getUnit } from '@/lib/api-server';
export const dynamic = 'force-dynamic';
interface Props {
params: { modId: string; raceId: string; unitId: string };
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const [mod, unit] = await Promise.all([getMod(params.modId), getUnit(params.unitId)]);
if (!mod || !unit) {
return { title: 'Unit' };
}
const title = `${unit.name}${mod.name}`;
const description = unit.description
? `${unit.name} unit in ${mod.name} mod. ${unit.description}`
: `${unit.name} unit in ${mod.name} mod. Stats, weapons, abilities.`;
return {
title,
description,
alternates: {
canonical: `/mod/${params.modId}/race/${params.raceId}/unit/${params.unitId}`,
},
openGraph: {
title,
description,
},
};
}
export default function UnitPage() {
return <UnitPageClient />;
}

21
app/page.tsx Normal file
View File

@ -0,0 +1,21 @@
import type { Metadata } from 'next';
import ModsPageClient from '@/components/pages/ModsPageClient';
import { getMods } from '@/lib/api-server';
import { IMod } from '@/src/types/Imod';
export const metadata: Metadata = {
title: 'Dawn of War Wiki — юниты, расы и здания всех модов',
description:
'Dawn of War wiki. Unification mod wiki. Unification mod unit stats. Supported all popular mods.',
alternates: {
canonical: '/',
},
};
export const dynamic = 'force-dynamic';
export default async function HomePage() {
const mods = (await getMods()) ?? [];
const visibleMods = mods.filter(mod => !mod.isHide);
return <ModsPageClient initialMods={visibleMods} />;
}

12
app/robots.ts Normal file
View File

@ -0,0 +1,12 @@
import type { MetadataRoute } from 'next';
import { SITE_URL } from '@/lib/api-server';
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
},
sitemap: `${SITE_URL}/sitemap.xml`,
};
}

78
app/sitemap.ts Normal file
View File

@ -0,0 +1,78 @@
import type { MetadataRoute } from 'next';
import {
getBuildingsForMod,
getMods,
getUnitsForMod,
SITE_URL,
} from '@/lib/api-server';
export const dynamic = 'force-dynamic';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const mods = (await getMods()) ?? [];
const modEntries: MetadataRoute.Sitemap = mods.map((mod) => ({
url: `${SITE_URL}/mod/${mod.id}`,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.8,
}));
const entries: MetadataRoute.Sitemap = [...modEntries];
// Gather races/units/buildings per mod (limit parallelism)
const details = await Promise.all(
mods.slice(0, 20).map(async (mod) => {
const [units, buildings] = await Promise.all([
getUnitsForMod(mod.id),
getBuildingsForMod(mod.id),
]);
return { mod, units, buildings };
})
);
for (const { mod, units, buildings } of details) {
const raceIds = new Set<string>();
for (const unitGroup of units ?? []) raceIds.add(unitGroup.race.id);
for (const buildingGroup of buildings ?? []) raceIds.add(buildingGroup.race.id);
for (const raceId of raceIds) {
entries.push({
url: `${SITE_URL}/mod/${mod.id}/race/${raceId}`,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.7,
});
}
for (const unitGroup of units ?? []) {
for (const unit of [...unitGroup.infantry, ...unitGroup.support, ...unitGroup.tech]) {
entries.push({
url: `${SITE_URL}/mod/${mod.id}/race/${unitGroup.race.id}/unit/${unit.id}`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.5,
});
}
}
for (const buildingGroup of buildings ?? []) {
for (const building of [...buildingGroup.buildings, ...buildingGroup.buildingsAdvanced]) {
entries.push({
url: `${SITE_URL}/mod/${mod.id}/race/${buildingGroup.race.id}/building/${building.id}`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.5,
});
}
}
}
return [
{
url: SITE_URL,
lastModified: new Date(),
changeFrequency: 'daily',
priority: 1,
},
...entries,
];
}

View File

@ -1,5 +1,5 @@
import './App.css';
import React from "react";
'use client';
import React from 'react';
import {
AppBar,
Box,
@ -7,11 +7,10 @@ import {
IconButton,
Toolbar,
Typography,
} from "@mui/material";
import { useNavigate, BrowserRouter } from "react-router-dom";
import { MyRoutes } from "./Routes";
} from '@mui/material';
import Link from 'next/link';
import { styled } from '@mui/material/styles';
import { ThemeProvider as CustomThemeProvider, useThemeContext } from './context/ThemeContext';
import { useThemeContext } from '@/src/context/ThemeContext';
import DarkModeIcon from '@mui/icons-material/DarkMode';
import LightModeIcon from '@mui/icons-material/LightMode';
@ -58,25 +57,30 @@ const ThemeToggleLight = styled(IconButton)(({ theme }) => ({
},
}));
function AppBarContent() {
const LogoLink = styled(Link)({
display: 'flex',
alignItems: 'center',
gap: 8,
textDecoration: 'none',
color: 'inherit',
});
export default function AppShell({ children }: { children: React.ReactNode }) {
const { mode, toggleTheme } = useThemeContext();
const navigate = useNavigate();
const isDark = mode === 'dark';
const handleLogoClick = () => navigate('/');
return (
<>
<Box sx={{ minHeight: '100vh' }}>
{isDark ? (
<StyledAppBar position="sticky">
<Container maxWidth="lg" sx={{ px: { xs: 1, sm: 3, md: 3 } }}>
<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}>
<LogoLink href="/">
<img src="/images/dowdelogo.png" alt="logo" style={{ height: 32, cursor: 'pointer' }} />
<LogoText variant="h6">
Wiki
</LogoText>
</Box>
</LogoLink>
<ThemeToggle onClick={toggleTheme} size="large">
<LightModeIcon />
</ThemeToggle>
@ -87,12 +91,12 @@ function AppBarContent() {
<StyledAppBarLight position="sticky">
<Container maxWidth="lg" sx={{ px: { xs: 1, sm: 3, md: 3 } }}>
<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}>
<LogoLink href="/">
<img src="/images/dowdelogo.png" alt="logo" style={{ height: 32, cursor: 'pointer' }} />
<LogoText variant="h6">
Wiki
</LogoText>
</Box>
</LogoLink>
<ThemeToggleLight onClick={toggleTheme} size="large">
<DarkModeIcon sx={{ color: '#000000' }} />
</ThemeToggleLight>
@ -100,23 +104,9 @@ function AppBarContent() {
</Container>
</StyledAppBarLight>
)}
</>
);
}
function App() {
return (
<CustomThemeProvider>
<Box sx={{ minHeight: '100vh' }}>
<BrowserRouter>
<AppBarContent />
<Container maxWidth="lg" sx={{ py: 4, px: { xs: 1, sm: 3, md: 3 } }}>
<MyRoutes />
{children}
</Container>
</BrowserRouter>
</Box>
</CustomThemeProvider>
);
}
export default App;

View File

@ -0,0 +1,16 @@
'use client';
import dynamic from 'next/dynamic';
import { Box, LinearProgress } from '@mui/material';
const BuildingPageLegacy = dynamic(() => import('@/src/legacy-pages/BuildingPage'), {
ssr: false,
loading: () => (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
<LinearProgress sx={{ width: '200px' }} />
</Box>
),
});
export default function BuildingPageClient() {
return <BuildingPageLegacy />;
}

View File

@ -0,0 +1,16 @@
'use client';
import dynamic from 'next/dynamic';
import { Box, LinearProgress } from '@mui/material';
const ModPageLegacy = dynamic(() => import('@/src/legacy-pages/ModPage'), {
ssr: false,
loading: () => (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
<LinearProgress sx={{ width: '200px' }} />
</Box>
),
});
export default function ModPageClient() {
return <ModPageLegacy />;
}

View File

@ -0,0 +1,14 @@
'use client';
import dynamic from 'next/dynamic';
import { Box, LinearProgress } from '@mui/material';
import { IMod } from '@/src/types/Imod';
const ModsPageLegacy = dynamic(() => import('@/src/legacy-pages/ModsPage'));
interface ModsPageClientProps {
initialMods: IMod[];
}
export default function ModsPageClient({ initialMods }: ModsPageClientProps) {
return <ModsPageLegacy initialMods={initialMods} />;
}

View File

@ -0,0 +1,16 @@
'use client';
import dynamic from 'next/dynamic';
import { Box, LinearProgress } from '@mui/material';
const RacePageLegacy = dynamic(() => import('@/src/legacy-pages/RacePageFast'), {
ssr: false,
loading: () => (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
<LinearProgress sx={{ width: '200px' }} />
</Box>
),
});
export default function RacePageClient() {
return <RacePageLegacy />;
}

View File

@ -0,0 +1,16 @@
'use client';
import dynamic from 'next/dynamic';
import { Box, LinearProgress } from '@mui/material';
const UnitPageLegacy = dynamic(() => import('@/src/legacy-pages/UnitPage'), {
ssr: false,
loading: () => (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
<LinearProgress sx={{ width: '200px' }} />
</Box>
),
});
export default function UnitPageClient() {
return <UnitPageLegacy />;
}

49
lib/api-server.ts Normal file
View File

@ -0,0 +1,49 @@
// Server-side API helpers for metadata generation.
// Runs only on the server (Node.js) — uses NEXT_PUBLIC_HOST_URL which is inlined at build time.
import { IMod } from '@/src/types/Imod';
const API = process.env.NEXT_PUBLIC_HOST_URL || '';
export const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://dow-wiki.example.com';
async function fetchJson<T>(url: string): Promise<T | null> {
try {
const res = await fetch(url, { cache: 'no-store' });
if (!res.ok) return null;
return (await res.json()) as T;
} catch {
return null;
}
}
export async function getMod(modId: string) {
return fetchJson<{ name: string; version: string }>(`${API}/api/v1/mods/${modId}`);
}
export async function getRace(raceId: string) {
return fetchJson<{ name: string }>(`${API}/api/v1/races/${raceId}`);
}
export async function getUnit(unitId: string) {
return fetchJson<{ name: string; description: string }>(`${API}/api/v1/units/${unitId}`);
}
export async function getBuilding(buildingId: string) {
return fetchJson<{ name: string; filename: string }>(`${API}/api/v1/buildings/${buildingId}`);
}
export async function getMods() {
return fetchJson<IMod[]>(`${API}/api/v1/mods`);
}
export async function getRacesForMod(modId: string | number) {
return fetchJson<Array<{ race: { id: string; name: string } }>>(`${API}/api/v1/units/mod/${modId}`);
}
export async function getUnitsForMod(modId: string | number) {
return fetchJson<Array<{ race: { id: string }; infantry: Array<{ id: number }>; support: Array<{ id: number }>; tech: Array<{ id: number }> }>>(`${API}/api/v1/units/mod/${modId}`);
}
export async function getBuildingsForMod(modId: string | number) {
return fetchJson<Array<{ race: { id: string }; buildings: Array<{ id: number }>; buildingsAdvanced: Array<{ id: number }> }>>(`${API}/api/v1/buildings/mod/${modId}`);
}

5
next-env.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.

6
next.config.mjs Normal file
View File

@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
};
export default nextConfig;

17872
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,43 +1,26 @@
{
"name": "dow-wiki-frontend",
"version": "0.1.0",
"version": "0.2.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^6.3.1",
"@mui/material": "^6.3.1",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"next": "^14.2.23",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.28.1",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
"react-dom": "^18.2.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
"devDependencies": {
"@types/node": "^20.11.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"typescript": "^5.3.0"
}
}

View File

@ -1,71 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="Cache-Control" content="no-cache, must-revalidate" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Dawn of War wiki. Unification mod wiki. Unification mod unit stats. Supported all popular mods."
/>
<meta name="google-site-verification" content="Q4XMn2UDpL2xmK7iBfaWQpVN8EPy8oGxN-7HqSKapb0" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Dawn of War Wiki</title>
</head>
<body>
<!-- Yandex.Metrika counter --> <script type="text/javascript" > (function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)}; m[i].l=1*new Date(); for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }} k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)}) (window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym"); ym(99502794, "init", { clickmap:true, trackLinks:true, accurateTrackBounce:true, webvisor:true }); </script> <noscript><div><img src="https://mc.yandex.ru/watch/99502794" style="position:absolute; left:-9999px;" alt="" /></div></noscript> <!-- /Yandex.Metrika counter -->
<noscript>You need to enable JavaScript to run this app.</noscript>
<script>
(function () {
var VERSION = 'v1.0.1';
var stored = localStorage.getItem('app_version');
if (stored !== VERSION) {
localStorage.setItem('app_version', VERSION);
if ('caches' in window) {
caches.keys().then(function (names) {
names.forEach(function (name) { caches.delete(name); });
});
}
if ('serviceWorker' in navigator) {
navigator.serviceWorker.getRegistrations().then(function (regs) {
regs.forEach(function (r) { r.unregister(); });
});
}
}
// bfcache: если страница восстановлена из back-forward cache — перезагружаем
window.addEventListener('pageshow', function (event) {
if (event.persisted) {
window.location.reload();
}
});
})();
</script>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@ -1,6 +1,6 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"short_name": "DoW Wiki",
"name": "Dawn of War Wiki",
"icons": [
{
"src": "favicon.ico",

View File

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

View File

@ -235,7 +235,7 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
{ label: <ArmorType name={ArmorTypeNames.BuildingHigh}/>, value: () => isAbilityApplyTo(ArmorTypeNames.BuildingHigh) },
];
const chunks = [];
const chunks: any[] = [];
for (let i = 0; i < items.length; i += columnsPerRow) {
chunks.push(items.slice(i, i + columnsPerRow));
}

View File

@ -120,7 +120,7 @@ export default function DpsTable(props: { mod: IMod, minDamageValue?: number, mi
{ label: <ArmorType name="Morale damage"/>, value: () => getMoraleDamage() },
];
const chunks = [];
const chunks: any[] = [];
for (let i = 0; i < items.length; i += columnsPerRow) {
chunks.push(items.slice(i, i + columnsPerRow));
}

View File

@ -277,7 +277,7 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
{ label: <ArmorType name="Morale damage"/>, value: () => getMoraleDamage() },
];
const chunks = [];
const chunks: any[] = [];
for (let i = 0; i < items.length; i += columnsPerRow) {
chunks.push(items.slice(i, i + columnsPerRow));
}

View File

@ -1,3 +1,4 @@
'use client';
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { ThemeProvider as MuiThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
@ -14,10 +15,14 @@ const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export const ThemeProvider = ({ children }: { children: ReactNode }) => {
const [mode, setMode] = useState<ThemeMode>(() => {
const [mode, setMode] = useState<ThemeMode>('dark');
useEffect(() => {
const saved = localStorage.getItem('theme');
return (saved === 'light' ? 'light' : 'dark') as ThemeMode;
});
if (saved === 'light') {
setMode('light');
}
}, []);
useEffect(() => {
localStorage.setItem('theme', mode);

View File

@ -2,15 +2,15 @@
import React from 'react';
import { useTheme } from '@mui/material/styles';
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 AbilityUrl = process.env.REACT_APP_HOST_URL + '/api/v1/ability';
export const ResearchUrl = process.env.REACT_APP_HOST_URL + '/api/v1/research';
export const AvailableMods = process.env.REACT_APP_HOST_URL + '/api/v1/mods';
export const AvailableRacesPart = process.env.REACT_APP_HOST_URL + '/api/v1/races';
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 IconUrl = process.env.REACT_APP_HOST_URL + '/api/v1/grapics/icon/';
export const UserUrl = process.env.NEXT_PUBLIC_HOST_URL + '/api/v1/user';
export const WeaponUrl = process.env.NEXT_PUBLIC_HOST_URL + '/api/v1/weapon';
export const AbilityUrl = process.env.NEXT_PUBLIC_HOST_URL + '/api/v1/ability';
export const ResearchUrl = process.env.NEXT_PUBLIC_HOST_URL + '/api/v1/research';
export const AvailableMods = process.env.NEXT_PUBLIC_HOST_URL + '/api/v1/mods';
export const AvailableRacesPart = process.env.NEXT_PUBLIC_HOST_URL + '/api/v1/races';
export const AvailableUnits = process.env.NEXT_PUBLIC_HOST_URL + '/api/v1/units';
export const AvailableBuildings = process.env.NEXT_PUBLIC_HOST_URL + '/api/v1/buildings';
export const IconUrl = process.env.NEXT_PUBLIC_HOST_URL + '/api/v1/grapics/icon/';
export function withTheme(Component) {
return function WithTheme(props) {

View File

@ -1,4 +1,5 @@
import { useParams } from 'react-router-dom';
'use client';
import { useParams } from 'next/navigation';
export function withRouter(Children) {
return (props) => {

View File

@ -1,13 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -118,8 +118,6 @@ function Unit (unit: IUnitShort, modId: number, raceId: String, theme: Theme) {
function Building(building: IBuilding, mod: IMod, theme: Theme) {
document.title = building.name + " — " + mod.name
const isDark = theme.palette.mode === 'dark';
let mapBuildingWeapons: Map<number, Map<number, IShortWeapon>> = new Map();

View File

@ -66,7 +66,6 @@ class ModPage extends React.Component<any, ModPageState> {
}
if (this.state != null && this.state.mod != null) {
document.title = this.state.mod.name + " — ModWiki";
const isDark = this.props.theme.palette.mode === 'dark';
return (

View File

@ -1,7 +1,4 @@
import {AvailableMods} from "../core/api";
import React from "react";
import { NavLink } from "react-router-dom";
import { IMod } from "../types/Imod";
import {
Box,
Container,
@ -16,30 +13,11 @@ import {
useTheme,
Paper,
LinearProgress,
Link,
} from "@mui/material";
import { styled } from '@mui/material/styles';
import { IMod } from "../types/Imod";
// Styled components
const PageHeader = styled(Paper)(({ theme }) => ({
padding: theme.spacing(6, 4),
marginBottom: theme.spacing(6),
background: theme.palette.mode === 'dark'
? 'linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)'
: 'linear-gradient(135deg, #ffffff 0%, #f0f0f0 50%, #e0e0e0 100%)',
borderRadius: '16px',
position: 'relative',
overflow: 'hidden',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'radial-gradient(circle at 30% 50%, rgba(255, 255, 255, 0.05) 0%, #dee2e6 50%)',
pointerEvents: 'none',
},
}));
const ModCard = styled(Card)(({ theme }) => ({
height: '100%',
@ -75,7 +53,7 @@ const ModCard = styled(Card)(({ theme }) => ({
},
}));
const VersionLink = styled(NavLink)(({ theme }) => ({
const VersionLink = styled(Link)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
padding: theme.spacing(1.5, 2),
@ -214,7 +192,7 @@ function Mods({ mods }: ModsProps) {
</Box>
{sameMods.filter(m => !m.isBeta).map(mod => (
<VersionLink key={mod.id} to={"/mod/" + mod.id} state={mod.id}>
<VersionLink key={mod.id} href={"/mod/" + mod.id}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
<VersionText variant="body2">
Version {mod.version}
@ -232,7 +210,7 @@ function Mods({ mods }: ModsProps) {
Beta versions:
</BetaVersionsLabel>
{betaVersion && (
<VersionLink to={"/mod/" + betaVersion.id} state={betaVersion.id}>
<VersionLink href={"/mod/" + betaVersion.id}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
Version {betaVersion.version} (Beta)
@ -251,9 +229,7 @@ function Mods({ mods }: ModsProps) {
<Button
size="medium"
variant="outlined"
component={NavLink}
to={"/mod/" + latest?.id}
state={latest?.id}
href={"/mod/" + latest?.id}
sx={{
width: '100%',
color: theme.palette.mode === 'dark' ? '#FFD700' : '#1976d2',
@ -296,13 +272,17 @@ interface ModsPageState {
loading: boolean;
}
class ModsPage extends React.Component<any, ModsPageState> {
interface ModsPageProps {
initialMods?: IMod[];
}
constructor({props}: { props: any }) {
class ModsPage extends React.Component<ModsPageProps, ModsPageState> {
constructor(props: ModsPageProps) {
super(props);
this.state = {
mods: [],
loading: true
mods: props.initialMods ?? [],
loading: false
};
}
@ -323,18 +303,6 @@ class ModsPage extends React.Component<any, ModsPageState> {
console.error('Cache clear failed:', e);
}
}
try {
const response = await fetch(AvailableMods);
const data: IMod[] = await response.json();
this.setState({
mods: data.filter(mod => !mod.isHide),
loading: false
});
} catch (error) {
console.error('Error loading mods:', error);
this.setState({ loading: false });
}
}
render() {

View File

@ -184,7 +184,7 @@ class Units extends React.Component<UnitsProps & { theme: Theme }, UnitsState> {
if (this.state && this.state.units) {
if(this.state.buildings?.race.name !== undefined){
document.title = this.state.buildings?.race.name
// title managed by Helmet in parent component
}
const accordionSx = isDark ? {
@ -270,8 +270,6 @@ class RacePageFast extends React.Component<any, RacePageState> {
mod: modData
});
document.title = `${modData.name} (${modData.version})`;
const response = await fetch(AvailableRacesPart + "/" + this.props.match.params.raceId);
const racesData: Irace = await response.json();

View File

@ -99,8 +99,6 @@ interface UintPageState {
function Unit(unit: IUnit, mod: IMod, theme: Theme) {
document.title = unit.name + " — " + mod.name
const isDark = theme.palette.mode === 'dark';
const morale = (unit.moraleMax !== null) ? <span>

View File

@ -1,26 +1,42 @@
{
"compilerOptions": {
"target": "ES6",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"target": "ES2017",
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"strictNullChecks": false,
"noEmit": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./*"
]
},
"strictNullChecks": true
},
"include": [
"src"
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}