react-scripts 4 on React 17 no longer builds on a current toolchain. Move to Vite 8 with React 19 and TypeScript 7, and take the same shape the other two converted sites settled on. - react-router-dom v5 -> react-router v8: Switch/Redirect become Routes/Navigate, and withRouter and RouteComponentProps give way to useParams, useLocation and useNavigate - swap the unmaintained dependencies: axios -> ky, react-html-parser -> @orrisroot/react-html-parser, react-ga -> react-ga4, react-helmet -> @dr.pogodin/react-helmet, react-image-lightbox -> yet-another-react-lightbox, moment -> date-fns, and a local HashLink in place of react-router-hash-link - drop react-app-polyfill, the jest setup and xregexp - replace ESLint with Biome, and match the other sites' tsconfigs - serve the dumped JSON from static-contents/modules/ at runtime instead of compiling it into the bundle, and mount that directory with sirv in both the dev and preview servers - lazy-load NewArticleAlert: at 342 kB of static bibliography it is the largest file in the app and only ever renders on the top page, which cuts the entry chunk from 580 kB to 200 kB - restate the theme sizes the modules/ move and border-box need, and keep the browser's own font on form controls - set the GA4 measurement ID replacing the shut-down UA property - write down the deployment procedure
96 lines
3.3 KiB
TypeScript
96 lines
3.3 KiB
TypeScript
import Tree from 'rc-tree';
|
|
import type { DataNode, EventDataNode } from 'rc-tree/lib/interface';
|
|
import { type Key, useMemo, useState } from 'react';
|
|
import { useNavigate } from 'react-router';
|
|
import type { MultiLang } from '../../config';
|
|
import Functions from '../../functions';
|
|
import IndexUtil, { INDEX_ID_PUBLIC, type Index } from '../lib/IndexUtil';
|
|
import styles from './IndexTree.module.css';
|
|
|
|
interface Props {
|
|
lang: MultiLang;
|
|
}
|
|
|
|
interface TreeData {
|
|
elements: DataNode[];
|
|
expandableKeys: string[];
|
|
defaultExpandedKeys: string[];
|
|
}
|
|
|
|
const load = (lang: MultiLang): TreeData => {
|
|
const expandableKeys: string[] = [];
|
|
const defaultExpandedKeys: string[] = [];
|
|
const makeTreeNode = (index: Index, depth: number): DataNode => {
|
|
const title = Functions.mlang(index.title, lang) + (index.numOfItems > 0 ? ` (${index.numOfItems})` : '');
|
|
const children = IndexUtil.getChildren(index.id);
|
|
if (children.length === 0) {
|
|
return { key: String(index.id), title: title };
|
|
}
|
|
const childTreeNodes = children.map((value: Index) => makeTreeNode(value, depth + 1));
|
|
if (depth < 1) {
|
|
defaultExpandedKeys.push(String(index.id));
|
|
}
|
|
expandableKeys.push(String(index.id));
|
|
return { key: String(index.id), title: title, children: childTreeNodes };
|
|
};
|
|
const elements: DataNode[] = [];
|
|
const index = IndexUtil.get(INDEX_ID_PUBLIC);
|
|
if (index !== null) {
|
|
elements.push(makeTreeNode(index, 0));
|
|
}
|
|
return { elements, expandableKeys, defaultExpandedKeys };
|
|
};
|
|
|
|
const toStringKeys = (keys: Key[]): string[] => keys.map((key) => (typeof key === 'string' ? key : String(key)));
|
|
|
|
const IndexTree = (props: Props) => {
|
|
const { lang } = props;
|
|
const data = useMemo(() => load(lang), [lang]);
|
|
const [expandedKeys, setExpandedKeys] = useState<string[]>(data.defaultExpandedKeys);
|
|
const navigate = useNavigate();
|
|
|
|
const handleExpand = (
|
|
keys: Key[],
|
|
_info: { node: EventDataNode<DataNode>; expanded: boolean; nativeEvent: MouseEvent }
|
|
) => {
|
|
setExpandedKeys(toStringKeys(keys));
|
|
};
|
|
|
|
const handleSelect = (
|
|
selectedKeys: Key[],
|
|
_info: {
|
|
event: 'select';
|
|
selected: boolean;
|
|
node: EventDataNode<DataNode>;
|
|
selectedNodes: DataNode[];
|
|
nativeEvent: MouseEvent;
|
|
}
|
|
) => {
|
|
const selectedKey = selectedKeys[0] ?? 0;
|
|
const key = typeof selectedKey === 'string' ? parseInt(selectedKey, 10) : Number(selectedKey);
|
|
void navigate(IndexUtil.getUrl(key));
|
|
};
|
|
|
|
return (
|
|
<div className={styles.container}>
|
|
<button type="button" className={styles.formButton} onClick={() => setExpandedKeys(data.expandableKeys)}>
|
|
open all
|
|
</button>
|
|
<button type="button" className={styles.formButton} onClick={() => setExpandedKeys([])}>
|
|
close all
|
|
</button>
|
|
<Tree
|
|
className={styles.indexTree}
|
|
expandedKeys={expandedKeys}
|
|
selectedKeys={[]}
|
|
onExpand={handleExpand}
|
|
onSelect={handleSelect}
|
|
showIcon={false}
|
|
treeData={data.elements}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default IndexTree;
|