Files
invbrain.neuroinf.jp/src/common/lib/HashLink.tsx
T
orrisroot 79e0e2295f feat: migrate from Create React App to Vite
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
2026-09-15 12:56:11 +09:00

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;