Add NotFound page for virtual 404s inside the SPA
All checks were successful
/ image_build (push) Successful in 1m19s

This commit is contained in:
2026-09-25 14:07:22 -04:00
parent 75d05beb7a
commit 503f075b98
7 changed files with 128 additions and 23 deletions

52
src/404/404.sass Normal file
View File

@@ -0,0 +1,52 @@
@use "../about/about"
.not-found
background: about.$reader-bg
display: flex
flex-direction: column
flex-grow: 1
justify-content: center
align-items: center
h1,h2,h3,h4
font-family: Neuton, serif
.nf-ctr
display: flex
flex-direction: column
h1,h2,h3,p
text-align: center
margin: 0
.nf-title
background: white
color: black
border-radius: 15px 15px 0 0
padding: 15px
display: flex
flex-direction: column
h1
margin: 10px 0
font-size: 100px
h3
font-size: 30px
margin-bottom: 10px
.nf-desc
background: black
color: white
padding: 15px
border-radius: 0 0 15px 15px
.routes-list
padding: 15px
display: flex
flex-direction: row
flex-wrap: wrap
gap: 15px
justify-content: center
align-items: center
a
font-size: 20px
text-decoration: underline

35
src/404/NotFound.tsx Normal file
View File

@@ -0,0 +1,35 @@
import "./404.sass";
import { ROUTES } from "../router/Route";
import { Link } from "../router/Link";
import { motion } from "motion/react";
import { AnimateSpin, AnimateFade } from "../animations";
import { type Path } from "../router/router";
export default function NotFoundPage() {
return <motion.main className="not-found" {...AnimateFade()}>
<motion.section {...AnimateSpin()} className="nf-ctr">
<div className="nf-title">
<h1>404</h1>
<h3>This page was not found</h3>
</div>
<div className="nf-desc">
<p>Did you mistype the URL? Here are a list of common links</p>
<section className="routes-list">
{
ROUTES.map(
([path,name]) => <LinkBox key={name} path={path} name={name}/>
)
}
</section>
</div>
</motion.section>
</motion.main>
}
function LinkBox({path, name}: {path: Path, name: string}) {
return (
<Link key={path} href={path}>
<div>{name}</div>
</Link>
);
}

View File

@@ -47,7 +47,6 @@
border-radius: 0 15px 0 0 border-radius: 0 15px 0 0
padding: 10px padding: 10px
font-weight: bold font-weight: bold
font-family: Neuton, serif
border-bottom: white 2px solid border-bottom: white 2px solid
h2 h2
margin: 0 margin: 0
@@ -55,9 +54,11 @@
.link-desc .link-desc
background: black background: black
color: white color: white
font-family: Inter, sans-serif padding: 15px
padding: 10px
flex-grow: 1 flex-grow: 1
justify-content: center justify-content: center
align-items: center align-items: center
border-radius: 0 0 15px 0 border-radius: 0 0 15px 0
font-family: Neuton, serif
font-size: 20px

View File

@@ -1,8 +1,8 @@
import React, {PropsWithChildren} from "react"; import React, {type PropsWithChildren, ReactElement} from "react";
import {useRouter} from "./RouterContext"; import {useRouter} from "./RouterContext";
import {Path} from "./router"; import {Path} from "./router";
type Route = [Path, string]; export type Route = [Path, string];
type Routes = [Route, Route, Route, Route]; type Routes = [Route, Route, Route, Route];
export const ROUTES: Routes = [ export const ROUTES: Routes = [
["/", "Home"], ["/", "Home"],
@@ -11,7 +11,8 @@ export const ROUTES: Routes = [
["/links", "Links"] ["/links", "Links"]
]; ];
function Route(props: PropsWithChildren & {route: Route}) { type RouteProps = PropsWithChildren & {route: Route};
function Route(props: RouteProps) {
const {route: [path], children} = props; const {route: [path], children} = props;
const router = useRouter(); const router = useRouter();
// remove search params // remove search params
@@ -25,11 +26,26 @@ const Canvas = React.lazy(() => import("../index/Canvas"));
const About = React.lazy(() => import("../about/AboutPage")); const About = React.lazy(() => import("../about/AboutPage"));
const ArticlesList = React.lazy(() => import("../articles/ArticlesList")); const ArticlesList = React.lazy(() => import("../articles/ArticlesList"));
const LinkTree = React.lazy(() => import("../links/LinkTree")); const LinkTree = React.lazy(() => import("../links/LinkTree"));
const NotFound = React.lazy(() => import("../404/NotFound"));
// ensures only one route is rendered at a time, with a virtual 404 page as fallback
function RouteSwitch({children}: {children: ReactElement<RouteProps, typeof Route>[]}) {
const router = useRouter();
const child = children.find(child => {
const [expected] = child.props.route;
const [current] = /\/?\w*[^?]/.exec(router.current) ?? [location.pathname];
return current === expected
});
return <>{child || <NotFound/>}</>
}
export function Routes() { export function Routes() {
const [index, about, articles, links] = ROUTES; const [index, about, articles, links] = ROUTES;
return <> return <>
<RouteSwitch>
<Route route={index}> <Route route={index}>
<Canvas/> <Canvas/>
</Route> </Route>
@@ -42,5 +58,6 @@ export function Routes() {
<Route route={links}> <Route route={links}>
<LinkTree/> <LinkTree/>
</Route> </Route>
</RouteSwitch>
</> </>
} }