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 as the other converted sites. - react-router-dom v5 -> react-router v8: Switch/Redirect become Routes/Navigate, and withRouter gives way to useLocation and useNavigate; the original /modules/... addresses are kept - swap the unmaintained dependencies: axios -> ky, react-html-parser -> @orrisroot/react-html-parser, react-ga -> react-ga4, react-helmet -> @dr.pogodin/react-helmet, moment -> date-fns, react-spinner-material -> a CSS spinner, and a local HashLink in place of react-router-hash-link - drop react-app-polyfill, the jest setup, xregexp and the unused async-lock, rc-tree, react-overlays and react-image-lightbox - replace ESLint with Biome, yarn with npm, and match the other sites' tsconfigs and sanitize.css base stylesheet - serve static-contents/ next to the repository with sirv in the dev and preview servers, keeping it out of dist/ - convert the page components to function components; each module loads its index once, sharing the request under StrictMode - resolve relative links in page bodies the way a browser does, since react-router 8 resolves them against the route hierarchy - fix the glossary search restricted to a category, whose query matched nothing - set the GA4 measurement ID replacing the shut-down UA property - correct the keywords meta tag copied from dynamicbrain - write down the data layout and the deployment procedure
54 lines
1.6 KiB
TypeScript
54 lines
1.6 KiB
TypeScript
import type { ComponentProps, MouseEvent } from 'react';
|
|
import { Link, type To } from 'react-router';
|
|
|
|
type LinkProps = ComponentProps<typeof Link>;
|
|
|
|
const RETRY_INTERVAL_MS = 16;
|
|
const RETRY_COUNT = 20;
|
|
|
|
const hashOf = (to: To): string => {
|
|
if (typeof to === 'string') {
|
|
const idx = to.indexOf('#');
|
|
return idx === -1 ? '' : to.slice(idx + 1);
|
|
}
|
|
return (to.hash ?? '').replace(/^#/, '');
|
|
};
|
|
|
|
const scrollToHash = (hash: string, remaining = RETRY_COUNT) => {
|
|
if (hash === '') {
|
|
return;
|
|
}
|
|
const id = decodeURIComponent(hash);
|
|
const target = document.getElementById(id) ?? document.querySelector(`[name="${CSS.escape(id)}"]`);
|
|
if (target !== null) {
|
|
target.scrollIntoView();
|
|
return;
|
|
}
|
|
// The target may not be in the DOM yet (pico pages fetch their content).
|
|
if (remaining > 0) {
|
|
setTimeout(() => {
|
|
scrollToHash(hash, remaining - 1);
|
|
}, RETRY_INTERVAL_MS);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* A `<Link>` that also scrolls to the element named by the URL fragment.
|
|
* Replaces `react-router-hash-link`, which is unmaintained and still targets react-router v5.
|
|
*/
|
|
const HashLink = ({ onClick, to, ...rest }: LinkProps) => {
|
|
const handleClick = (e: MouseEvent<HTMLAnchorElement>) => {
|
|
onClick?.(e);
|
|
if (e.defaultPrevented) {
|
|
return;
|
|
}
|
|
// Let react-router commit the navigation before looking for the target.
|
|
setTimeout(() => {
|
|
scrollToHash(hashOf(to));
|
|
}, 0);
|
|
};
|
|
return <Link {...rest} to={to} onClick={handleClick} />;
|
|
};
|
|
|
|
export default HashLink;
|