feat: add route-level error page and per-section error boundary #3
@@ -0,0 +1,16 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Link } from '$shared/ui';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route-level error boundary — shown when an unhandled error escapes
|
||||||
|
* the [[...slug]] segment. Mirrors the not-found layout.
|
||||||
|
*/
|
||||||
|
export default function ErrorPage() {
|
||||||
|
return (
|
||||||
|
<main className="flex-1 flex flex-col items-center justify-center gap-6">
|
||||||
|
<h1 className="font-heading text-[clamp(8rem,20vw,18rem)] leading-none">500</h1>
|
||||||
|
<Link href="/">Back to main</Link>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import type { ErrorInfo, ReactNode } from 'react';
|
||||||
|
import { Component } from 'react';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
/**
|
||||||
|
* Section content to render
|
||||||
|
*/
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
/**
|
||||||
|
* Whether an error was caught
|
||||||
|
*/
|
||||||
|
hasError: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Isolates a single section's render errors so a broken section
|
||||||
|
* does not crash the rest of the page.
|
||||||
|
*/
|
||||||
|
export class SectionErrorBoundary extends Component<Props, State> {
|
||||||
|
state: State = { hasError: false };
|
||||||
|
|
||||||
|
static getDerivedStateFromError(): State {
|
||||||
|
return { hasError: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
override componentDidCatch(error: Error, info: ErrorInfo) {
|
||||||
|
console.error('[SectionErrorBoundary]', error, info.componentStack);
|
||||||
|
}
|
||||||
|
|
||||||
|
override render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import ExperienceSection from '../../../ExperienceSection/ui/ExperienceSection/E
|
|||||||
import IntroSection from '../../../IntroSection/ui/IntroSection/IntroSection';
|
import IntroSection from '../../../IntroSection/ui/IntroSection/IntroSection';
|
||||||
import ProjectsSection from '../../../ProjectsSection/ui/ProjectsSection/ProjectsSection';
|
import ProjectsSection from '../../../ProjectsSection/ui/ProjectsSection/ProjectsSection';
|
||||||
import SkillsSection from '../../../SkillsSection/ui/SkillsSection/SkillsSection';
|
import SkillsSection from '../../../SkillsSection/ui/SkillsSection/SkillsSection';
|
||||||
|
import { SectionErrorBoundary } from '../SectionErrorBoundary/SectionErrorBoundary';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Props for the SectionFactory widget.
|
* Props for the SectionFactory widget.
|
||||||
@@ -36,5 +37,9 @@ export function SectionFactory({ slug }: SectionFactoryProps) {
|
|||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Component />;
|
return (
|
||||||
|
<SectionErrorBoundary>
|
||||||
|
<Component />
|
||||||
|
</SectionErrorBoundary>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user