Moved to next js ssr
This commit is contained in:
parent
4e4f798d85
commit
2184f1642c
7
.env
7
.env
@ -1,2 +1,5 @@
|
|||||||
#REACT_APP_HOST_URL=https://wiki-backend.dawn-of-war.pro
|
NEXT_PUBLIC_HOST_URL=https://wiki-backend.dawn-of-war.pro
|
||||||
REACT_APP_HOST_URL=http://localhost:8082
|
#NEXT_PUBLIC_HOST_URL=http://localhost:8082
|
||||||
|
|
||||||
|
# Домен сайта для sitemap/robots/canonical (замените на реальный)
|
||||||
|
NEXT_PUBLIC_SITE_URL=https://wiki.dawn-of-war.pro
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
/node_modules
|
/node_modules
|
||||||
|
/.next
|
||||||
/.pnp
|
/.pnp
|
||||||
.pnp.js
|
.pnp.js
|
||||||
|
|
||||||
|
|||||||
78
app/layout.tsx
Normal file
78
app/layout.tsx
Normal 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
33
app/mod/[modId]/page.tsx
Normal 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 />;
|
||||||
|
}
|
||||||
37
app/mod/[modId]/race/[raceId]/building/[buildingId]/page.tsx
Normal file
37
app/mod/[modId]/race/[raceId]/building/[buildingId]/page.tsx
Normal 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 />;
|
||||||
|
}
|
||||||
33
app/mod/[modId]/race/[raceId]/page.tsx
Normal file
33
app/mod/[modId]/race/[raceId]/page.tsx
Normal 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 />;
|
||||||
|
}
|
||||||
35
app/mod/[modId]/race/[raceId]/unit/[unitId]/page.tsx
Normal file
35
app/mod/[modId]/race/[raceId]/unit/[unitId]/page.tsx
Normal 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
21
app/page.tsx
Normal 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
12
app/robots.ts
Normal 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
78
app/sitemap.ts
Normal 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,
|
||||||
|
];
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import './App.css';
|
'use client';
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
AppBar,
|
AppBar,
|
||||||
Box,
|
Box,
|
||||||
@ -7,11 +7,10 @@ import {
|
|||||||
IconButton,
|
IconButton,
|
||||||
Toolbar,
|
Toolbar,
|
||||||
Typography,
|
Typography,
|
||||||
} from "@mui/material";
|
} from '@mui/material';
|
||||||
import { useNavigate, BrowserRouter } from "react-router-dom";
|
import Link from 'next/link';
|
||||||
import { MyRoutes } from "./Routes";
|
|
||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
import { ThemeProvider as CustomThemeProvider, useThemeContext } from './context/ThemeContext';
|
import { useThemeContext } from '@/src/context/ThemeContext';
|
||||||
import DarkModeIcon from '@mui/icons-material/DarkMode';
|
import DarkModeIcon from '@mui/icons-material/DarkMode';
|
||||||
import LightModeIcon from '@mui/icons-material/LightMode';
|
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 { mode, toggleTheme } = useThemeContext();
|
||||||
const navigate = useNavigate();
|
|
||||||
const isDark = mode === 'dark';
|
const isDark = mode === 'dark';
|
||||||
|
|
||||||
const handleLogoClick = () => navigate('/');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Box sx={{ minHeight: '100vh' }}>
|
||||||
{isDark ? (
|
{isDark ? (
|
||||||
<StyledAppBar position="sticky">
|
<StyledAppBar position="sticky">
|
||||||
<Container maxWidth="lg" sx={{ px: { xs: 1, sm: 3, md: 3 } }}>
|
<Container maxWidth="lg" sx={{ px: { xs: 1, sm: 3, md: 3 } }}>
|
||||||
<Toolbar disableGutters sx={{ justifyContent: 'space-between' }}>
|
<Toolbar disableGutters sx={{ justifyContent: 'space-between' }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
<LogoLink href="/">
|
||||||
<img src="/images/dowdelogo.png" alt="logo" style={{ height: 32, cursor: 'pointer' }} onClick={handleLogoClick}/>
|
<img src="/images/dowdelogo.png" alt="logo" style={{ height: 32, cursor: 'pointer' }} />
|
||||||
<LogoText variant="h6" sx={{ textDecoration: 'none', cursor: 'pointer' }} onClick={handleLogoClick}>
|
<LogoText variant="h6">
|
||||||
Wiki
|
Wiki
|
||||||
</LogoText>
|
</LogoText>
|
||||||
</Box>
|
</LogoLink>
|
||||||
<ThemeToggle onClick={toggleTheme} size="large">
|
<ThemeToggle onClick={toggleTheme} size="large">
|
||||||
<LightModeIcon />
|
<LightModeIcon />
|
||||||
</ThemeToggle>
|
</ThemeToggle>
|
||||||
@ -87,12 +91,12 @@ function AppBarContent() {
|
|||||||
<StyledAppBarLight position="sticky">
|
<StyledAppBarLight position="sticky">
|
||||||
<Container maxWidth="lg" sx={{ px: { xs: 1, sm: 3, md: 3 } }}>
|
<Container maxWidth="lg" sx={{ px: { xs: 1, sm: 3, md: 3 } }}>
|
||||||
<Toolbar disableGutters sx={{ justifyContent: 'space-between' }}>
|
<Toolbar disableGutters sx={{ justifyContent: 'space-between' }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
<LogoLink href="/">
|
||||||
<img src="/images/dowdelogo.png" alt="logo" style={{ height: 32, cursor: 'pointer' }} onClick={handleLogoClick}/>
|
<img src="/images/dowdelogo.png" alt="logo" style={{ height: 32, cursor: 'pointer' }} />
|
||||||
<LogoText variant="h6" sx={{ textDecoration: 'none', cursor: 'pointer' }} onClick={handleLogoClick}>
|
<LogoText variant="h6">
|
||||||
Wiki
|
Wiki
|
||||||
</LogoText>
|
</LogoText>
|
||||||
</Box>
|
</LogoLink>
|
||||||
<ThemeToggleLight onClick={toggleTheme} size="large">
|
<ThemeToggleLight onClick={toggleTheme} size="large">
|
||||||
<DarkModeIcon sx={{ color: '#000000' }} />
|
<DarkModeIcon sx={{ color: '#000000' }} />
|
||||||
</ThemeToggleLight>
|
</ThemeToggleLight>
|
||||||
@ -100,23 +104,9 @@ function AppBarContent() {
|
|||||||
</Container>
|
</Container>
|
||||||
</StyledAppBarLight>
|
</StyledAppBarLight>
|
||||||
)}
|
)}
|
||||||
</>
|
<Container maxWidth="lg" sx={{ py: 4, px: { xs: 1, sm: 3, md: 3 } }}>
|
||||||
|
{children}
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function App() {
|
|
||||||
return (
|
|
||||||
<CustomThemeProvider>
|
|
||||||
<Box sx={{ minHeight: '100vh' }}>
|
|
||||||
<BrowserRouter>
|
|
||||||
<AppBarContent />
|
|
||||||
<Container maxWidth="lg" sx={{ py: 4, px: { xs: 1, sm: 3, md: 3 } }}>
|
|
||||||
<MyRoutes />
|
|
||||||
</Container>
|
|
||||||
</BrowserRouter>
|
|
||||||
</Box>
|
|
||||||
</CustomThemeProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
16
components/pages/BuildingPageClient.tsx
Normal file
16
components/pages/BuildingPageClient.tsx
Normal 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 />;
|
||||||
|
}
|
||||||
16
components/pages/ModPageClient.tsx
Normal file
16
components/pages/ModPageClient.tsx
Normal 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 />;
|
||||||
|
}
|
||||||
14
components/pages/ModsPageClient.tsx
Normal file
14
components/pages/ModsPageClient.tsx
Normal 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} />;
|
||||||
|
}
|
||||||
16
components/pages/RacePageClient.tsx
Normal file
16
components/pages/RacePageClient.tsx
Normal 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 />;
|
||||||
|
}
|
||||||
16
components/pages/UnitPageClient.tsx
Normal file
16
components/pages/UnitPageClient.tsx
Normal 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
49
lib/api-server.ts
Normal 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
5
next-env.d.ts
vendored
Normal 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
6
next.config.mjs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
reactStrictMode: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
17882
package-lock.json
generated
17882
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
45
package.json
45
package.json
@ -1,43 +1,26 @@
|
|||||||
{
|
{
|
||||||
"name": "dow-wiki-frontend",
|
"name": "dow-wiki-frontend",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.0",
|
"@emotion/styled": "^11.14.0",
|
||||||
"@mui/icons-material": "^6.3.1",
|
"@mui/icons-material": "^6.3.1",
|
||||||
"@mui/material": "^6.3.1",
|
"@mui/material": "^6.3.1",
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
"next": "^14.2.23",
|
||||||
"@testing-library/react": "^13.4.0",
|
|
||||||
"@testing-library/user-event": "^13.5.0",
|
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0"
|
||||||
"react-router-dom": "^6.28.1",
|
|
||||||
"react-scripts": "5.0.1",
|
|
||||||
"web-vitals": "^2.1.4"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"devDependencies": {
|
||||||
"start": "react-scripts start",
|
"@types/node": "^20.11.0",
|
||||||
"build": "react-scripts build",
|
"@types/react": "^18.2.0",
|
||||||
"test": "react-scripts test",
|
"@types/react-dom": "^18.2.0",
|
||||||
"eject": "react-scripts eject"
|
"typescript": "^5.3.0"
|
||||||
},
|
|
||||||
"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"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"short_name": "React App",
|
"short_name": "DoW Wiki",
|
||||||
"name": "Create React App Sample",
|
"name": "Dawn of War Wiki",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "favicon.ico",
|
"src": "favicon.ico",
|
||||||
|
|||||||
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -235,7 +235,7 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
|
|||||||
{ label: <ArmorType name={ArmorTypeNames.BuildingHigh}/>, value: () => isAbilityApplyTo(ArmorTypeNames.BuildingHigh) },
|
{ label: <ArmorType name={ArmorTypeNames.BuildingHigh}/>, value: () => isAbilityApplyTo(ArmorTypeNames.BuildingHigh) },
|
||||||
];
|
];
|
||||||
|
|
||||||
const chunks = [];
|
const chunks: any[] = [];
|
||||||
for (let i = 0; i < items.length; i += columnsPerRow) {
|
for (let i = 0; i < items.length; i += columnsPerRow) {
|
||||||
chunks.push(items.slice(i, i + columnsPerRow));
|
chunks.push(items.slice(i, i + columnsPerRow));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -120,7 +120,7 @@ export default function DpsTable(props: { mod: IMod, minDamageValue?: number, mi
|
|||||||
{ label: <ArmorType name="Morale damage"/>, value: () => getMoraleDamage() },
|
{ label: <ArmorType name="Morale damage"/>, value: () => getMoraleDamage() },
|
||||||
];
|
];
|
||||||
|
|
||||||
const chunks = [];
|
const chunks: any[] = [];
|
||||||
for (let i = 0; i < items.length; i += columnsPerRow) {
|
for (let i = 0; i < items.length; i += columnsPerRow) {
|
||||||
chunks.push(items.slice(i, i + columnsPerRow));
|
chunks.push(items.slice(i, i + columnsPerRow));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -277,7 +277,7 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
|
|||||||
{ label: <ArmorType name="Morale damage"/>, value: () => getMoraleDamage() },
|
{ label: <ArmorType name="Morale damage"/>, value: () => getMoraleDamage() },
|
||||||
];
|
];
|
||||||
|
|
||||||
const chunks = [];
|
const chunks: any[] = [];
|
||||||
for (let i = 0; i < items.length; i += columnsPerRow) {
|
for (let i = 0; i < items.length; i += columnsPerRow) {
|
||||||
chunks.push(items.slice(i, i + columnsPerRow));
|
chunks.push(items.slice(i, i + columnsPerRow));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
'use client';
|
||||||
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||||
import { ThemeProvider as MuiThemeProvider } from '@mui/material/styles';
|
import { ThemeProvider as MuiThemeProvider } from '@mui/material/styles';
|
||||||
import CssBaseline from '@mui/material/CssBaseline';
|
import CssBaseline from '@mui/material/CssBaseline';
|
||||||
@ -14,10 +15,14 @@ const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
|||||||
|
|
||||||
|
|
||||||
export const ThemeProvider = ({ children }: { children: ReactNode }) => {
|
export const ThemeProvider = ({ children }: { children: ReactNode }) => {
|
||||||
const [mode, setMode] = useState<ThemeMode>(() => {
|
const [mode, setMode] = useState<ThemeMode>('dark');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
const saved = localStorage.getItem('theme');
|
const saved = localStorage.getItem('theme');
|
||||||
return (saved === 'light' ? 'light' : 'dark') as ThemeMode;
|
if (saved === 'light') {
|
||||||
});
|
setMode('light');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem('theme', mode);
|
localStorage.setItem('theme', mode);
|
||||||
|
|||||||
@ -2,15 +2,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
|
|
||||||
export const UserUrl = process.env.REACT_APP_HOST_URL + '/api/v1/user';
|
export const UserUrl = process.env.NEXT_PUBLIC_HOST_URL + '/api/v1/user';
|
||||||
export const WeaponUrl = process.env.REACT_APP_HOST_URL + '/api/v1/weapon';
|
export const WeaponUrl = process.env.NEXT_PUBLIC_HOST_URL + '/api/v1/weapon';
|
||||||
export const AbilityUrl = process.env.REACT_APP_HOST_URL + '/api/v1/ability';
|
export const AbilityUrl = process.env.NEXT_PUBLIC_HOST_URL + '/api/v1/ability';
|
||||||
export const ResearchUrl = process.env.REACT_APP_HOST_URL + '/api/v1/research';
|
export const ResearchUrl = process.env.NEXT_PUBLIC_HOST_URL + '/api/v1/research';
|
||||||
export const AvailableMods = process.env.REACT_APP_HOST_URL + '/api/v1/mods';
|
export const AvailableMods = process.env.NEXT_PUBLIC_HOST_URL + '/api/v1/mods';
|
||||||
export const AvailableRacesPart = process.env.REACT_APP_HOST_URL + '/api/v1/races';
|
export const AvailableRacesPart = process.env.NEXT_PUBLIC_HOST_URL + '/api/v1/races';
|
||||||
export const AvailableUnits = process.env.REACT_APP_HOST_URL + '/api/v1/units';
|
export const AvailableUnits = process.env.NEXT_PUBLIC_HOST_URL + '/api/v1/units';
|
||||||
export const AvailableBuildings = process.env.REACT_APP_HOST_URL + '/api/v1/buildings';
|
export const AvailableBuildings = process.env.NEXT_PUBLIC_HOST_URL + '/api/v1/buildings';
|
||||||
export const IconUrl = process.env.REACT_APP_HOST_URL + '/api/v1/grapics/icon/';
|
export const IconUrl = process.env.NEXT_PUBLIC_HOST_URL + '/api/v1/grapics/icon/';
|
||||||
|
|
||||||
export function withTheme(Component) {
|
export function withTheme(Component) {
|
||||||
return function WithTheme(props) {
|
return function WithTheme(props) {
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { useParams } from 'react-router-dom';
|
'use client';
|
||||||
|
import { useParams } from 'next/navigation';
|
||||||
|
|
||||||
export function withRouter(Children) {
|
export function withRouter(Children) {
|
||||||
return (props) => {
|
return (props) => {
|
||||||
|
|||||||
@ -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>
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
@ -118,8 +118,6 @@ function Unit (unit: IUnitShort, modId: number, raceId: String, theme: Theme) {
|
|||||||
|
|
||||||
function Building(building: IBuilding, mod: IMod, theme: Theme) {
|
function Building(building: IBuilding, mod: IMod, theme: Theme) {
|
||||||
|
|
||||||
document.title = building.name + " — " + mod.name
|
|
||||||
|
|
||||||
const isDark = theme.palette.mode === 'dark';
|
const isDark = theme.palette.mode === 'dark';
|
||||||
|
|
||||||
let mapBuildingWeapons: Map<number, Map<number, IShortWeapon>> = new Map();
|
let mapBuildingWeapons: Map<number, Map<number, IShortWeapon>> = new Map();
|
||||||
@ -66,7 +66,6 @@ class ModPage extends React.Component<any, ModPageState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.state != null && this.state.mod != null) {
|
if (this.state != null && this.state.mod != null) {
|
||||||
document.title = this.state.mod.name + " — ModWiki";
|
|
||||||
const isDark = this.props.theme.palette.mode === 'dark';
|
const isDark = this.props.theme.palette.mode === 'dark';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -1,7 +1,4 @@
|
|||||||
import {AvailableMods} from "../core/api";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { NavLink } from "react-router-dom";
|
|
||||||
import { IMod } from "../types/Imod";
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Container,
|
Container,
|
||||||
@ -16,30 +13,11 @@ import {
|
|||||||
useTheme,
|
useTheme,
|
||||||
Paper,
|
Paper,
|
||||||
LinearProgress,
|
LinearProgress,
|
||||||
|
Link,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { styled } from '@mui/material/styles';
|
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 }) => ({
|
const ModCard = styled(Card)(({ theme }) => ({
|
||||||
height: '100%',
|
height: '100%',
|
||||||
@ -75,7 +53,7 @@ const ModCard = styled(Card)(({ theme }) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const VersionLink = styled(NavLink)(({ theme }) => ({
|
const VersionLink = styled(Link)(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
padding: theme.spacing(1.5, 2),
|
padding: theme.spacing(1.5, 2),
|
||||||
@ -214,7 +192,7 @@ function Mods({ mods }: ModsProps) {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{sameMods.filter(m => !m.isBeta).map(mod => (
|
{sameMods.filter(m => !m.isBeta).map(mod => (
|
||||||
<VersionLink key={mod.id} to={"/mod/" + mod.id} state={mod.id}>
|
<VersionLink key={mod.id} href={"/mod/" + mod.id}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
|
||||||
<VersionText variant="body2">
|
<VersionText variant="body2">
|
||||||
Version {mod.version}
|
Version {mod.version}
|
||||||
@ -232,7 +210,7 @@ function Mods({ mods }: ModsProps) {
|
|||||||
Beta versions:
|
Beta versions:
|
||||||
</BetaVersionsLabel>
|
</BetaVersionsLabel>
|
||||||
{betaVersion && (
|
{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%' }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
|
||||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||||
Version {betaVersion.version} (Beta)
|
Version {betaVersion.version} (Beta)
|
||||||
@ -251,9 +229,7 @@ function Mods({ mods }: ModsProps) {
|
|||||||
<Button
|
<Button
|
||||||
size="medium"
|
size="medium"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
component={NavLink}
|
href={"/mod/" + latest?.id}
|
||||||
to={"/mod/" + latest?.id}
|
|
||||||
state={latest?.id}
|
|
||||||
sx={{
|
sx={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
color: theme.palette.mode === 'dark' ? '#FFD700' : '#1976d2',
|
color: theme.palette.mode === 'dark' ? '#FFD700' : '#1976d2',
|
||||||
@ -296,13 +272,17 @@ interface ModsPageState {
|
|||||||
loading: boolean;
|
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);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
mods: [],
|
mods: props.initialMods ?? [],
|
||||||
loading: true
|
loading: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -323,18 +303,6 @@ class ModsPage extends React.Component<any, ModsPageState> {
|
|||||||
console.error('Cache clear failed:', e);
|
console.error('Cache clear failed:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(AvailableMods);
|
|
||||||
const data: IMod[] = await response.json();
|
|
||||||
this.setState({
|
|
||||||
mods: data.filter(mod => !mod.isHide),
|
|
||||||
loading: false
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading mods:', error);
|
|
||||||
this.setState({ loading: false });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -184,7 +184,7 @@ class Units extends React.Component<UnitsProps & { theme: Theme }, UnitsState> {
|
|||||||
if (this.state && this.state.units) {
|
if (this.state && this.state.units) {
|
||||||
|
|
||||||
if(this.state.buildings?.race.name !== undefined){
|
if(this.state.buildings?.race.name !== undefined){
|
||||||
document.title = this.state.buildings?.race.name
|
// title managed by Helmet in parent component
|
||||||
}
|
}
|
||||||
|
|
||||||
const accordionSx = isDark ? {
|
const accordionSx = isDark ? {
|
||||||
@ -270,8 +270,6 @@ class RacePageFast extends React.Component<any, RacePageState> {
|
|||||||
mod: modData
|
mod: modData
|
||||||
});
|
});
|
||||||
|
|
||||||
document.title = `${modData.name} (${modData.version})`;
|
|
||||||
|
|
||||||
const response = await fetch(AvailableRacesPart + "/" + this.props.match.params.raceId);
|
const response = await fetch(AvailableRacesPart + "/" + this.props.match.params.raceId);
|
||||||
const racesData: Irace = await response.json();
|
const racesData: Irace = await response.json();
|
||||||
|
|
||||||
@ -99,8 +99,6 @@ interface UintPageState {
|
|||||||
|
|
||||||
function Unit(unit: IUnit, mod: IMod, theme: Theme) {
|
function Unit(unit: IUnit, mod: IMod, theme: Theme) {
|
||||||
|
|
||||||
document.title = unit.name + " — " + mod.name
|
|
||||||
|
|
||||||
const isDark = theme.palette.mode === 'dark';
|
const isDark = theme.palette.mode === 'dark';
|
||||||
|
|
||||||
const morale = (unit.moraleMax !== null) ? <span>
|
const morale = (unit.moraleMax !== null) ? <span>
|
||||||
@ -1,26 +1,42 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES6",
|
|
||||||
"lib": [
|
"lib": [
|
||||||
"dom",
|
"dom",
|
||||||
"dom.iterable",
|
"dom.iterable",
|
||||||
"esnext"
|
"esnext"
|
||||||
],
|
],
|
||||||
|
"target": "ES2017",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
"strict": false,
|
||||||
|
"strictNullChecks": false,
|
||||||
|
"noEmit": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"strict": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "bundler",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"jsx": "preserve",
|
||||||
"jsx": "react-jsx"
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"strictNullChecks": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src"
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user