feat: migrate from Create React App to Vite

Rebuild the site on the stack the other converted sites share: Vite 8,
React 19, TypeScript 7, react-router 8, Biome and sanitize.css, with the
site data moved out of the bundle into static-contents/.

- base the database, credits and pico code on cerebellum.neuroinf.jp,
  keeping this site's item types, rankings and menus
- port the MediaWiki pages to function components
- serve all module data from modules/, redirecting the old download and
  /database/file URLs
- tile the plate texture and keep form controls on white
- track page views with GA4
This commit is contained in:
2026-09-14 16:57:14 +09:00
parent 7069196a5b
commit 87c0274bc1
190 changed files with 6515 additions and 14960 deletions
+31 -33
View File
@@ -1,20 +1,17 @@
import React, { Component } from 'react';
import { Redirect, RouteComponentProps, withRouter } from 'react-router';
import { Navigate, useLocation } from 'react-router';
import PageNotFound from '../common/lib/PageNotFound';
import { MultiLang } from '../config';
import type { MultiLang } from '../config';
import Functions from '../functions';
interface Props extends RouteComponentProps {
interface Props {
lang: MultiLang;
}
class DatabaseXoopsPathRedirect extends Component<Props> {
getRedirectUrl(): string {
const { location } = this.props;
const getRedirectUrl = (location: { pathname: string; search: string }): string => {
{
const pathname = location.pathname || '';
const query = new URLSearchParams(location.search);
const search = new RegExp('^/modules/xoonips(?:/+(.*))?$');
const search = /^\/modules\/xoonips(?:\/+(.*))?$/;
const matches = pathname.match(search);
if (matches === null) {
return '';
@@ -28,11 +25,11 @@ class DatabaseXoopsPathRedirect extends Component<Props> {
case 'detail.php': {
const id = query.get('id');
if (id !== null) {
return '/database/item/id/' + Functions.escape(id);
return `/database/item/id/${Functions.escape(id)}`;
}
const itemId = query.get('item_id');
if (itemId !== null && itemId.match(/^\d+$/) !== null) {
return '/database/item/' + Functions.escape(itemId);
return `/database/item/${Functions.escape(itemId)}`;
}
return '';
}
@@ -40,13 +37,13 @@ class DatabaseXoopsPathRedirect extends Component<Props> {
const indexId = query.get('index_id');
if (indexId !== null && indexId.match(/^\d+$/) !== null) {
const params = new URLSearchParams();
const map: any = {
const map: Record<string, { key: string; isNumber: boolean }> = {
orderby: { key: 'orderby', isNumber: false },
order_dir: { key: 'order_dir', isNumber: true },
itemcount: { key: 'itemcount', isNumber: true },
page: { key: 'page', isNumber: true }
page: { key: 'page', isNumber: true },
};
for (let k in map) {
for (const k in map) {
const v = query.get(k);
if (v === null || v.length === 0) {
continue;
@@ -57,7 +54,7 @@ class DatabaseXoopsPathRedirect extends Component<Props> {
params.set(map[k].key, v);
}
const paramStr = params.toString();
return '/database/list/' + Functions.escape(indexId) + (paramStr.length > 0 ? '?' + paramStr : '');
return `/database/list/${Functions.escape(indexId)}${paramStr.length > 0 ? `?${paramStr}` : ''}`;
}
break;
}
@@ -78,13 +75,13 @@ class DatabaseXoopsPathRedirect extends Component<Props> {
return '';
}
const params = new URLSearchParams({ type, keyword });
const map: any = {
const map: Record<string, { key: string; isNumber: boolean }> = {
orderby: { key: 'orderby', isNumber: false },
orderdir: { key: 'order_dir', isNumber: true },
item_per_page: { key: 'itemcount', isNumber: true },
page: { key: 'page', isNumber: true }
page: { key: 'page', isNumber: true },
};
for (let k in map) {
for (const k in map) {
const v = query.get(k);
if (v === null || v.length === 0) {
continue;
@@ -94,7 +91,7 @@ class DatabaseXoopsPathRedirect extends Component<Props> {
}
params.set(map[k].key, v);
}
return '/database/search?' + params.toString();
return `/database/search?${params.toString()}`;
}
case 'itemtypesearch': {
const itemType = query.get('search_itemtype');
@@ -102,22 +99,22 @@ class DatabaseXoopsPathRedirect extends Component<Props> {
return '';
}
const type = itemType.replace('xnp', '');
return '/database/search/itemtype/' + Functions.escape(type);
return `/database/search/itemtype/${Functions.escape(type)}`;
}
case 'itemsubtypesearch': {
let type = '';
let subtype = '';
query.forEach((v, k) => {
if (k.match(/^xnp[a-z]+$/) !== null && !!v) {
if (k.match(/^xnp[a-z]+$/) !== null && v) {
type = k.replace('xnp', '');
return;
}
})
});
if (type === '') {
return '';
}
query.forEach((v, k) => {
if (k.match(`^xnp${type}_.+$`) !== null && !!v) {
if (k.match(`^xnp${type}_.+$`) !== null && v) {
subtype = v;
return;
}
@@ -125,7 +122,7 @@ class DatabaseXoopsPathRedirect extends Component<Props> {
if (subtype === '') {
return '';
}
return '/database/search/itemtype/' + Functions.escape(type) + '/' + Functions.escape(subtype);
return `/database/search/itemtype/${Functions.escape(type)}/${Functions.escape(subtype)}`;
}
}
return '';
@@ -136,15 +133,16 @@ class DatabaseXoopsPathRedirect extends Component<Props> {
}
return '';
}
};
render() {
const { lang } = this.props;
const url = this.getRedirectUrl();
if (url === '') {
return <PageNotFound lang={lang} />;
}
return <Redirect to={url} />;
const DatabaseXoopsPathRedirect = (props: Props) => {
const { lang } = props;
const location = useLocation();
const url = getRedirectUrl(location);
if (url === '') {
return <PageNotFound lang={lang} />;
}
}
return <Navigate to={url} replace />;
};
export default withRouter(DatabaseXoopsPathRedirect);
export default DatabaseXoopsPathRedirect;