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 code on cerebellum.neuroinf.jp and keep the pupil platform's table layout, about page and menu - load the Flickr badge feed as JSONP without extra libraries - serve the XooNIps data from modules/xoonips/, redirecting the old download and /database/file URLs - restate the column widths as border-box totals and keep form controls on white - track page views with GA4
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;
|