decent 404/error page

This commit is contained in:
Michael Freno
2025-12-31 12:00:44 -05:00
parent ff681e6b3a
commit bc2a74be50
6 changed files with 770 additions and 276 deletions

View File

@@ -181,12 +181,20 @@ function AppLayout(props: { children: any }) {
JavaScript is disabled. Features will be limited.
</div>
</noscript>
<div
onMouseUp={handleCenterTapRelease}
onTouchEnd={handleCenterTapRelease}
<ErrorBoundary
fallback={(error, reset) => (
<ErrorBoundaryFallback error={error} reset={reset} />
)}
>
<Suspense fallback={<TerminalSplash />}>{props.children}</Suspense>
</div>
<div
onMouseUp={handleCenterTapRelease}
onTouchEnd={handleCenterTapRelease}
>
<Suspense fallback={<TerminalSplash />}>
{props.children}
</Suspense>
</div>
</ErrorBoundary>
</div>
<RightBar />
</div>

View File

@@ -498,6 +498,26 @@ export function LeftBar() {
</button>
</li>
</Show>
<li class="hover:text-subtext0 w-fit transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold">
<button
onClick={() => {
const lostUrls = [
"/dev/null",
"/segfault",
"/void",
"/404",
"/lost-and-still-lost"
];
const randomUrl =
lostUrls[Math.floor(Math.random() * lostUrls.length)];
navigate(randomUrl);
handleLinkClick();
}}
class="text-left"
>
Get Lost
</button>
</li>
</ul>
</Typewriter>

View File

@@ -1,5 +1,10 @@
import { useNavigate } from "@solidjs/router";
import { createSignal, For, onMount, onCleanup } from "solid-js";
import { createSignal, onMount, onCleanup, For, Show } from "solid-js";
import {
CommandHistoryItem,
createTerminalCommands,
executeTerminalCommand
} from "~/lib/terminal-commands";
export interface ErrorBoundaryFallbackProps {
error: Error;
@@ -9,22 +14,83 @@ export interface ErrorBoundaryFallbackProps {
export default function ErrorBoundaryFallback(
props: ErrorBoundaryFallbackProps
) {
// Try to get navigate, but handle case where we're outside router context
let navigate: ((path: string) => void) | undefined;
try {
navigate = useNavigate();
} catch (e) {
// If we're outside router context, fallback to window.location
navigate = (path: string) => {
window.location.href = path;
};
}
const [glitchText, setGlitchText] = createSignal("ERROR");
const [command, setCommand] = createSignal("");
const [history, setHistory] = createSignal<CommandHistoryItem[]>([]);
const [historyIndex, setHistoryIndex] = createSignal(-1);
let inputRef: HTMLInputElement | undefined;
// Log error immediately (safe for SSR)
console.error(props.error);
// Client-only glitch animation
const addToHistory = (
cmd: string,
output: string,
type: "success" | "error" | "info"
) => {
if (cmd === "clear") {
setHistory([]);
} else {
setHistory([...history(), { command: cmd, output, type }]);
}
};
const commands = createTerminalCommands({
navigate: navigate!,
location: {
pathname: typeof window !== "undefined" ? window.location.pathname : "/"
},
addToHistory
});
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Enter") {
executeTerminalCommand(command(), commands, addToHistory);
setCommand("");
setHistoryIndex(-1);
} else if (e.key === "ArrowUp") {
e.preventDefault();
const allCommands = history().map((h) => h.command);
if (allCommands.length > 0) {
const newIndex =
historyIndex() === -1
? allCommands.length - 1
: Math.max(0, historyIndex() - 1);
setHistoryIndex(newIndex);
setCommand(allCommands[newIndex]);
}
} else if (e.key === "ArrowDown") {
e.preventDefault();
const allCommands = history().map((h) => h.command);
if (historyIndex() !== -1) {
const newIndex = Math.min(allCommands.length - 1, historyIndex() + 1);
setHistoryIndex(newIndex);
setCommand(allCommands[newIndex]);
}
} else if (e.key === "Tab") {
e.preventDefault();
const typed = command().toLowerCase();
const matches = Object.keys(commands).filter((cmd) =>
cmd.startsWith(typed)
);
if (matches.length === 1) {
setCommand(matches[0]);
} else if (matches.length > 1) {
addToHistory(command(), matches.join(" "), "info");
}
} else if (e.key === "l" && e.ctrlKey) {
e.preventDefault();
setHistory([]);
}
};
onMount(() => {
const glitchChars = "!@#$%^&*()_+-=[]{}|;':\",./<>?~`";
const originalText = "ERROR";
@@ -46,183 +112,189 @@ export default function ErrorBoundaryFallback(
}
}, 400);
onCleanup(() => clearInterval(glitchInterval));
inputRef?.focus();
onCleanup(() => {
clearInterval(glitchInterval);
});
});
const createParticles = () => {
return Array.from({ length: 40 }, (_, i) => ({
id: i,
left: `${Math.random() * 100}%`,
top: `${Math.random() * 100}%`,
animationDelay: `${Math.random() * 3}s`,
animationDuration: `${2 + Math.random() * 3}s`
}));
};
return (
<div class="relative min-h-screen w-full overflow-hidden bg-gradient-to-bl from-slate-900 via-red-950/20 to-slate-900 dark:from-black dark:via-red-950/30 dark:to-black">
<div class="absolute inset-0 overflow-hidden">
<For each={createParticles()}>
{(particle) => (
<div
class="absolute animate-pulse"
style={{
left: particle.left,
top: particle.top,
"animation-delay": particle.animationDelay,
"animation-duration": particle.animationDuration
}}
>
<div class="h-1 w-1 rounded-full bg-red-400 opacity-40 dark:bg-red-300" />
</div>
)}
</For>
</div>
{/* Animated grid background */}
<div class="absolute inset-0 opacity-10">
<div
class="bg-crust relative min-h-screen w-full overflow-hidden"
onClick={() => inputRef?.focus()}
>
{/* Scanline effect */}
<div class="pointer-events-none absolute inset-0 z-20 opacity-5">
<div
class="h-full w-full"
style={{
"background-image": `
linear-gradient(rgba(239, 68, 68, 0.3) 1px, transparent 1px),
linear-gradient(90deg, rgba(239, 68, 68, 0.3) 1px, transparent 1px)
`,
"background-size": "60px 60px",
animation: "grid-move 25s linear infinite"
"background-image":
"repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,0,0,0.2) 2px, rgba(0,0,0,0.2) 4px)",
animation: "scanline 8s linear infinite"
}}
/>
</div>
{/* Main content */}
<div class="relative z-10 flex min-h-screen flex-col items-center justify-center px-4 text-center">
{/* Glitchy ERROR text */}
<div class="mb-8">
<h1
class="bg-gradient-to-r from-red-400 via-orange-500 to-red-600 bg-clip-text text-7xl font-bold text-transparent select-none md:text-8xl"
style={{
"text-shadow": "0 0 30px rgba(239, 68, 68, 0.5)",
filter: "drop-shadow(0 0 10px rgba(239, 68, 68, 0.3))"
}}
>
{glitchText()}
</h1>
<div class="mx-auto mt-2 h-1 w-40 animate-pulse bg-gradient-to-r from-transparent via-red-500 to-transparent" />
<div class="relative z-10 flex min-h-screen flex-col items-start justify-start px-8 py-16 md:px-16">
{/* Terminal header */}
<div class="mb-8 w-full max-w-4xl">
<div class="border-surface0 text-subtext0 flex items-center gap-2 border-b pb-2 font-mono text-sm">
<span class="text-green">freno@terminal</span>
<span class="text-subtext1">:</span>
<span class="text-blue">~</span>
<span class="text-subtext1">$</span>
</div>
</div>
{/* Error message */}
<div class="max-w-2xl space-y-4">
<h2 class="animate-fade-in text-2xl font-light text-slate-700 md:text-3xl dark:text-slate-400">
Huh.
</h2>
<p class="animate-fade-in-delay text-lg text-slate-600 dark:text-slate-500">
An unexpected error has disrupted the flow of ... something.
<br />
But don't worry, you can try again or navigate back to safety.
</p>
{props.error.message && (
<p class="animate-fade-in-delay-2 font-mono text-sm text-slate-600 dark:text-slate-600">
Error: {props.error.message}
</p>
)}
{/* Error Display */}
<div class="mb-8 w-full max-w-4xl font-mono">
<div class="mb-4 flex items-center gap-2">
<span class="text-red">fatal:</span>
<span class="text-text">Unhandled Runtime Exception</span>
</div>
<div class="border-red bg-mantle mb-6 border-l-4 p-4 text-sm">
<div class="mb-2 flex items-start gap-2">
<span class="text-red text-xl"></span>
<div class="flex-1">
<div class="text-red mb-2 text-lg">{glitchText()}</div>
<div class="text-text">
Application encountered an unexpected error
</div>
{props.error.message && (
<div class="bg-surface0 text-subtext0 mt-2 rounded p-2">
<div class="text-yellow mb-1">Message:</div>
<div class="text-text">{props.error.message}</div>
</div>
)}
{props.error.stack && (
<div class="bg-surface0 text-subtext1 mt-3 max-h-40 overflow-auto rounded p-2 text-xs">
<div class="text-yellow mb-1">Stack trace:</div>
<pre class="whitespace-pre-wrap">{props.error.stack}</pre>
</div>
)}
</div>
</div>
</div>
<div class="text-subtext0 space-y-2 text-sm">
<div class="flex items-start gap-2">
<span class="text-blue"></span>
<span>
Type <span class="text-green">help</span> to see available
commands, or try one of the suggestions below
</span>
</div>
</div>
</div>
<div class="mt-12 flex flex-col gap-4 sm:flex-row">
{/* Command options */}
<div class="mb-8 w-full max-w-4xl space-y-3 font-mono text-sm">
<div class="text-subtext1">Quick actions:</div>
<button
onClick={() => props.reset()}
class="group relative overflow-hidden rounded-lg bg-gradient-to-r from-red-600 to-orange-600 px-8 py-4 text-lg font-medium text-white shadow-lg transition-all duration-300 hover:scale-105 hover:shadow-xl hover:shadow-red-500/25 active:scale-95"
class="group border-surface0 bg-mantle hover:border-yellow hover:bg-surface0 flex w-full items-center gap-2 border px-4 py-3 text-left transition-all"
>
<div class="absolute inset-0 bg-gradient-to-r from-red-700 to-orange-700 opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
<span class="relative flex items-center gap-2">🔄 Try Again</span>
<span class="text-green">$</span>
<span class="text-yellow group-hover:text-peach">./reset</span>
<span class="text-subtext1">--state</span>
<span class="text-subtext1 ml-auto opacity-0 transition-opacity group-hover:opacity-100">
[Try again]
</span>
</button>
<button
onClick={() => navigate("/")}
class="group relative overflow-hidden rounded-lg bg-gradient-to-r from-blue-600 to-purple-600 px-8 py-4 text-lg font-medium text-white shadow-lg transition-all duration-300 hover:scale-105 hover:shadow-xl hover:shadow-blue-500/25 active:scale-95"
onClick={() => navigate!("/")}
class="group border-surface0 bg-mantle hover:border-blue hover:bg-surface0 flex w-full items-center gap-2 border px-4 py-3 text-left transition-all"
>
<div class="absolute inset-0 bg-gradient-to-r from-blue-700 to-purple-700 opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
<span class="relative flex items-center gap-2">🏠 cd ~</span>
<span class="text-green">$</span>
<span class="text-blue group-hover:text-sky">cd</span>
<span class="text-text group-hover:text-blue">~</span>
<span class="text-subtext1 ml-auto opacity-0 transition-opacity group-hover:opacity-100">
[Return home]
</span>
</button>
<button
onClick={() => window.history.back()}
class="group relative overflow-hidden rounded-lg border-2 border-slate-600 bg-transparent px-8 py-4 text-lg font-medium text-slate-600 transition-all duration-300 hover:border-red-500 hover:bg-red-500/10 hover:text-red-400 active:scale-95"
class="group border-surface0 bg-mantle hover:border-blue hover:bg-surface0 flex w-full items-center gap-2 border px-4 py-3 text-left transition-all"
>
<span class="relative flex items-center gap-2"> Go Back</span>
<span class="text-green">$</span>
<span class="text-blue group-hover:text-sky">cd</span>
<span class="text-text group-hover:text-blue">..</span>
<span class="text-subtext1 ml-auto opacity-0 transition-opacity group-hover:opacity-100">
[Go back]
</span>
</button>
</div>
{/* Floating elements */}
<div class="animate-bounce-slow absolute top-20 left-10">
<div class="h-6 w-6 rotate-45 bg-gradient-to-br from-red-400 to-orange-500 opacity-60" />
</div>
<div class="animate-bounce-slow-delay absolute top-32 right-16">
<div class="h-4 w-4 rounded-full bg-gradient-to-br from-orange-400 to-red-500 opacity-60" />
</div>
<div class="animate-bounce-slow absolute bottom-20 left-20">
<div
class="h-5 w-5 bg-gradient-to-br from-red-500 to-orange-400 opacity-60"
style={{ "clip-path": "polygon(50% 0%, 0% 100%, 100% 100%)" }}
/>
{/* Command history */}
<Show when={history().length > 0}>
<div class="mb-4 w-full max-w-4xl font-mono text-sm">
<For each={history()}>
{(item) => (
<div class="mb-3">
<div class="text-subtext0 flex items-center gap-2">
<span class="text-green">freno@terminal</span>
<span class="text-subtext1">:</span>
<span class="text-blue">~</span>
<span class="text-subtext1">$</span>
<span class="text-text">{item.command}</span>
</div>
<pre
class="mt-1 whitespace-pre-wrap"
classList={{
"text-text": item.type === "success",
"text-red": item.type === "error",
"text-blue": item.type === "info"
}}
>
{item.output}
</pre>
</div>
)}
</For>
</div>
</Show>
{/* Interactive input */}
<div class="w-full max-w-4xl font-mono text-sm">
<div class="flex items-center gap-2">
<span class="text-green">freno@terminal</span>
<span class="text-subtext1">:</span>
<span class="text-blue">~</span>
<span class="text-subtext1">$</span>
<input
ref={inputRef}
type="text"
value={command()}
onInput={(e) => setCommand(e.currentTarget.value)}
onKeyDown={handleKeyDown}
class="text-text caret-text ml-1 flex-1 border-none bg-transparent outline-none"
autocomplete="off"
spellcheck={false}
/>
</div>
</div>
{/* Footer */}
<div class="absolute bottom-8 left-1/2 -translate-x-1/2">
<p class="text-sm text-slate-500 dark:text-slate-600">
System Error Something went wrong
</p>
<div class="text-subtext1 absolute right-4 bottom-4 font-mono text-xs">
<span class="text-red">ERR</span> <span class="text-subtext0">|</span>{" "}
Runtime Exception
</div>
</div>
{/* Custom styles */}
<style>{`
@keyframes grid-move {
@keyframes scanline {
0% {
transform: translate(0, 0);
transform: translateY(-100%);
}
100% {
transform: translate(60px, 60px);
}
}
.animate-fade-in {
animation: fadeIn 1s ease-out 0.5s both;
}
.animate-fade-in-delay {
animation: fadeIn 1s ease-out 1s both;
}
.animate-fade-in-delay-2 {
animation: fadeIn 1s ease-out 1.5s both;
}
.animate-bounce-slow {
animation: bounce-slow 3s ease-in-out infinite;
}
.animate-bounce-slow-delay {
animation: bounce-slow 3s ease-in-out infinite 1.5s;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes bounce-slow {
0%,
100% {
transform: translateY(0) rotate(0deg);
}
50% {
transform: translateY(-20px) rotate(180deg);
transform: translateY(100%);
}
}
`}</style>

View File

@@ -0,0 +1,275 @@
export interface CommandHistoryItem {
command: string;
output: string;
type: "success" | "error" | "info";
}
export interface CommandContext {
navigate: (path: string) => void;
location: { pathname: string };
addToHistory: (
cmd: string,
output: string,
type: "success" | "error" | "info"
) => void;
triggerCrash?: () => void;
}
export const createTerminalCommands = (context: CommandContext) => {
// Define available routes
const routes = [
{ path: "/blog", name: "blog" },
{ path: "/resume", name: "resume" },
{ path: "/contact", name: "contact" },
{ path: "/downloads", name: "downloads" },
{ path: "/account", name: "account" }
];
const commands: Record<
string,
{ action: () => void; description: string; hidden?: boolean }
> = {
"cd ~": {
action: () => context.navigate("/"),
description: "Navigate to home"
},
"cd /": {
action: () => context.navigate("/"),
description: "Navigate to home"
},
"cd ..": {
action: () => window.history.back(),
description: "Go back"
},
ls: {
action: () => {
context.addToHistory(
"ls",
"home blog resume contact downloads login account",
"success"
);
},
description: "List available routes"
},
"ls -la": {
action: () => {
context.addToHistory(
"ls -la",
"drwxr-xr-x blog/\n-rw-r--r-- resume\n-rw-r--r-- contact\n-rw-r--r-- downloads\n-rw-r--r-- account",
"success"
);
},
description: "List available routes (detailed)"
},
pwd: {
action: () => {
context.addToHistory("pwd", context.location.pathname, "success");
},
description: "Print current path"
},
whoami: {
action: () => {
context.addToHistory("whoami", "guest", "success");
},
description: "Show current user"
},
help: {
action: () => {
const helpText = Object.entries(commands)
.filter(([, info]) => !info.hidden)
.map(([cmd, info]) => ` ${cmd.padEnd(20)} - ${info.description}`)
.join("\n");
context.addToHistory(
"help",
`Available commands:\n${helpText}`,
"info"
);
},
description: "Show this help message"
},
clear: {
action: () => {
context.addToHistory("clear", "", "info");
// Clear will be handled by the component
},
description: "Clear terminal history"
},
exit: {
action: () => context.navigate("/"),
description: "Exit (go home)"
},
crash: {
action: () => {
if (context.triggerCrash) {
context.triggerCrash();
} else {
throw new Error("💣");
}
},
description: "💣"
},
"sudo rm -rf /": {
action: () => {
context.addToHistory(
"sudo rm -rf /",
"Hey man... \nthat's like... not very cool.",
"error"
);
},
description: "Don't try this at home, kids"
},
cowsay: {
action: () => {
context.addToHistory(
"cowsay",
" ___________\n< You're lost! >\n -----------\n \\ ^__^\n \\ (oo)\\_______\n (__)\\ )\\/\\\n ||----w |\n || ||",
"success"
);
},
description: "Make the cow speak"
},
fortune: {
action: () => {
const fortunes = [
"The page you seek cannot be found, but fortune cookies remain plentiful.",
"A 404 error is just an opportunity to explore somewhere new.",
"You will find what you seek... just not here.",
"The path forward is unclear. Try going back.",
"Your lucky numbers are: 4, 0, 4",
"An error today keeps the bugs away... wait, that's not right.",
"In the land of the lost, the one with a map is king.",
"You will soon discover something that was always there."
];
const fortune = fortunes[Math.floor(Math.random() * fortunes.length)];
context.addToHistory("fortune", fortune, "success");
},
description: "Get your fortune"
},
"echo $PATH": {
action: () => {
context.addToHistory(
"echo $PATH",
"/home:/blog:/resume:/contact:/downloads:/account",
"success"
);
},
description: "Show available paths"
},
neofetch: {
action: () => {
context.addToHistory(
"neofetch",
` _,met$$$$$gg. guest@freno.dev\n ,g$$$$$$$$$$$$$$$P. ----------------\n ,g$$P\"\" \"\"\"Y$$.\" OS: 404 Not Found\n ,$$P' \`$$$. Shell: terminal-shell\n',$$P ,ggs. \`$$b: Resolution: Lost\n\`d$$' ,$P\"' . $$$ Theme: Catppuccin\n $$P d$' , $$P Terminal: web-terminal\n $$: $$. - ,d$$' CPU: Confusion (404)\n $$; Y$b._ _,d$P' Memory: ???\n Y$$. \`.\`\"Y$$$$P\"' \n \`$$b \"-.__ \n \`Y$$ \n \`Y$$. \n \`$$b. \n \`Y$$b. \n \`\"Y$b._ \n \`\"\"\"\" `,
"info"
);
},
description: "Display system info"
},
"cat /dev/urandom": {
action: () => {
const random = Array.from({ length: 10 }, () =>
Math.random().toString(36).substring(2, 15)
).join("\n");
context.addToHistory(
"cat /dev/urandom",
random + "\n^C (interrupted)",
"success"
);
},
description: "Show random data"
},
uptime: {
action: () => {
context.addToHistory(
"uptime",
"up way too long, load average: ∞, ∞, ∞",
"success"
);
},
description: "Show system uptime"
},
date: {
action: () => {
context.addToHistory("date", new Date().toString(), "success");
},
description: "Display current date and time"
},
"uname -a": {
action: () => {
context.addToHistory(
"uname -a",
"ErrorOS 404.0.0 #1 SMP PREEMPT_DYNAMIC Web x86_64 GNU/Browser",
"success"
);
},
description: "Print system information"
}
};
// Add all cd variants for each route
routes.forEach((route) => {
// cd blog (visible in help)
commands[`cd ${route.name}`] = {
action: () => context.navigate(route.path),
description: `Navigate to ${route.name}`
};
// cd /blog (hidden alias)
commands[`cd ${route.path}`] = {
action: () => context.navigate(route.path),
description: `Navigate to ${route.name}`,
hidden: true
};
// cd ~/blog (hidden alias)
commands[`cd ~${route.path}`] = {
action: () => context.navigate(route.path),
description: `Navigate to ${route.name}`,
hidden: true
};
});
return commands;
};
export const executeTerminalCommand = (
cmd: string,
commands: Record<
string,
{ action: () => void; description: string; hidden?: boolean }
>,
addToHistory: (
cmd: string,
output: string,
type: "success" | "error" | "info"
) => void
) => {
const trimmed = cmd.trim();
if (!trimmed) return;
if (commands[trimmed]) {
commands[trimmed].action();
} else if (trimmed.startsWith("cd ")) {
const path = trimmed.slice(3);
addToHistory(trimmed, `cd: ${path}: No such file or directory`, "error");
} else if (trimmed.startsWith("echo ")) {
const text = trimmed.slice(5);
addToHistory(trimmed, text, "success");
} else if (
trimmed.startsWith("cat ") ||
trimmed.startsWith("less ") ||
trimmed.startsWith("more ")
) {
const file = trimmed.split(" ")[1];
addToHistory(
trimmed,
`${trimmed.split(" ")[0]}: ${file}: No such file or directory`,
"error"
);
} else {
addToHistory(
trimmed,
`command not found: ${trimmed}\nType 'help' for available commands`,
"error"
);
}
};

View File

@@ -1,14 +1,90 @@
import { Title, Meta } from "@solidjs/meta";
import { HttpStatusCode } from "@solidjs/start";
import { useNavigate } from "@solidjs/router";
import { createEffect, createSignal, For } from "solid-js";
import { useNavigate, useLocation } from "@solidjs/router";
import { createSignal, onCleanup, onMount, For, Show } from "solid-js";
import {
CommandHistoryItem,
createTerminalCommands,
executeTerminalCommand
} from "~/lib/terminal-commands";
// Component that crashes when rendered
function CrashComponent() {
throw new Error("Terminal crash test - triggering error boundary");
}
export default function NotFound() {
const navigate = useNavigate();
const location = useLocation();
const [glitchText, setGlitchText] = createSignal("404");
const [command, setCommand] = createSignal("");
const [history, setHistory] = createSignal<CommandHistoryItem[]>([]);
const [historyIndex, setHistoryIndex] = createSignal(-1);
const [shouldCrash, setShouldCrash] = createSignal(false);
let inputRef: HTMLInputElement | undefined;
createEffect(() => {
const glitchChars = "!@#$%^&*()_+-=[]{}|;':\",./<>?~`";
const addToHistory = (
cmd: string,
output: string,
type: "success" | "error" | "info"
) => {
if (cmd === "clear") {
setHistory([]);
} else {
setHistory([...history(), { command: cmd, output, type }]);
}
};
const commands = createTerminalCommands({
navigate,
location,
addToHistory,
triggerCrash: () => setShouldCrash(true)
});
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Enter") {
executeTerminalCommand(command(), commands, addToHistory);
setCommand("");
setHistoryIndex(-1);
} else if (e.key === "ArrowUp") {
e.preventDefault();
const allCommands = history().map((h) => h.command);
if (allCommands.length > 0) {
const newIndex =
historyIndex() === -1
? allCommands.length - 1
: Math.max(0, historyIndex() - 1);
setHistoryIndex(newIndex);
setCommand(allCommands[newIndex]);
}
} else if (e.key === "ArrowDown") {
e.preventDefault();
const allCommands = history().map((h) => h.command);
if (historyIndex() !== -1) {
const newIndex = Math.min(allCommands.length - 1, historyIndex() + 1);
setHistoryIndex(newIndex);
setCommand(allCommands[newIndex]);
}
} else if (e.key === "Tab") {
e.preventDefault();
const typed = command().toLowerCase();
const matches = Object.keys(commands).filter((cmd) =>
cmd.startsWith(typed)
);
if (matches.length === 1) {
setCommand(matches[0]);
} else if (matches.length > 1) {
addToHistory(command(), matches.join(" "), "info");
}
} else if (e.key === "l" && e.ctrlKey) {
e.preventDefault();
setHistory([]);
}
};
onMount(() => {
const glitchChars = "!@#$%^&*()_+-=[]{}|;':\",./<>?~`0123456789";
const originalText = "404";
const glitchInterval = setInterval(() => {
@@ -28,177 +104,193 @@ export default function NotFound() {
}
}, 300);
return () => clearInterval(glitchInterval);
});
inputRef?.focus();
const createParticles = () => {
return Array.from({ length: 50 }, (_, i) => ({
id: i,
left: `${Math.random() * 100}%`,
top: `${Math.random() * 100}%`,
animationDelay: `${Math.random() * 3}s`,
animationDuration: `${2 + Math.random() * 3}s`
}));
};
onCleanup(() => {
clearInterval(glitchInterval);
});
});
return (
<>
<Show when={shouldCrash()}>
{/*@ts-ignore (this is intentional)*/}
<CrashComponent />
</Show>
<Title>404 Not Found | Michael Freno</Title>
<Meta
name="description"
content="404 - Page not found. The page you're looking for doesn't exist."
/>
<HttpStatusCode code={404} />
<div class="relative min-h-screen w-full overflow-hidden bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 dark:from-black dark:via-slate-900 dark:to-black">
{/* Animated particle background */}
<div class="absolute inset-0 overflow-hidden">
<For each={createParticles()}>
{(particle) => (
<div
class="absolute animate-pulse"
style={{
left: particle.left,
top: particle.top,
"animation-delay": particle.animationDelay,
"animation-duration": particle.animationDuration
}}
>
<div class="h-1 w-1 rounded-full bg-blue-400 opacity-30 dark:bg-blue-300" />
</div>
)}
</For>
</div>
{/* Animated grid background */}
<div class="absolute inset-0 opacity-10">
<div
class="relative min-h-screen w-full overflow-hidden"
onClick={() => inputRef?.focus()}
>
{/* Scanline effect */}
<div class="pointer-events-none absolute inset-0 z-20 opacity-5">
<div
class="h-full w-full"
style={{
"background-image": `
linear-gradient(rgba(59, 130, 246, 0.3) 1px, transparent 1px),
linear-gradient(90deg, rgba(59, 130, 246, 0.3) 1px, transparent 1px)
`,
"background-size": "50px 50px",
animation: "grid-move 20s linear infinite"
"background-image":
"repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,0,0,0.2) 2px, rgba(0,0,0,0.2) 4px)",
animation: "scanline 8s linear infinite"
}}
/>
</div>
{/* Main content */}
<div class="relative z-10 flex min-h-screen flex-col items-center justify-center px-4 text-center">
{/* Glitchy 404 */}
<div class="mb-8">
<h1
class="bg-gradient-to-r from-blue-400 via-purple-500 to-blue-600 bg-clip-text text-8xl font-bold text-transparent select-none md:text-9xl"
style={{
"text-shadow": "0 0 30px rgba(59, 130, 246, 0.5)",
filter: "drop-shadow(0 0 10px rgba(59, 130, 246, 0.3))"
}}
>
{glitchText()}
</h1>
<div class="mx-auto mt-2 h-1 w-32 animate-pulse bg-gradient-to-r from-transparent via-blue-500 to-transparent" />
<div class="relative z-10 flex min-h-screen flex-col items-start justify-start px-8 py-16 md:px-16">
{/* Terminal header */}
<div class="mb-8 w-full max-w-4xl">
<div class="border-surface0 text-subtext0 flex items-center gap-2 border-b pb-2 font-mono text-sm">
<span class="text-green">freno@terminal</span>
<span class="text-subtext1">:</span>
<span class="text-blue">~</span>
<span class="text-subtext1">$</span>
</div>
</div>
{/* Error message with typewriter effect */}
<div class="max-w-2xl space-y-4">
<h2 class="animate-fade-in text-2xl font-light text-slate-300 md:text-3xl dark:text-slate-400">
You seem to have drifted off into space...
</h2>
<p class="animate-fade-in-delay text-lg text-slate-400 dark:text-slate-500">
...or the page you're looking for has drifted into the void.
<br />
But don't worry, we can navigate you back to safety.
</p>
{/* 404 Error Display */}
<div class="mb-8 w-full max-w-4xl font-mono">
<div class="mb-4 flex items-center gap-2">
<span class="text-red">error:</span>
<span class="text-text">HTTP {glitchText()} - Not Found</span>
</div>
<div class="border-red bg-mantle mb-6 border-l-4 p-4 text-sm">
<div class="mb-2 flex items-start gap-2">
<span class="text-red"></span>
<div class="flex-1">
<div class="text-text">Failed to resolve route</div>
<div class="text-subtext0 mt-1">
The requested path does not exist in the routing table
</div>
</div>
</div>
<div class="text-subtext1 mt-3">
<span class="text-yellow"></span> Location:{" "}
<span class="text-peach">{location.pathname}</span>
</div>
</div>
<div class="text-subtext0 space-y-2 text-sm">
<div class="flex items-start gap-2">
<span class="text-blue"></span>
<span>
Type <span class="text-green">help</span> to see available
commands, or try one of the suggestions below
</span>
</div>
</div>
</div>
{/* Action buttons */}
<div class="mt-12 flex flex-col gap-4 sm:flex-row">
{/* Command suggestions */}
<div class="mb-8 w-full max-w-4xl space-y-3 font-mono text-sm">
<div class="text-subtext1">Quick commands:</div>
<button
onClick={() => navigate("/")}
class="group relative overflow-hidden rounded-lg bg-gradient-to-r from-blue-600 to-purple-600 px-8 py-4 text-lg font-medium text-white shadow-lg transition-all duration-300 hover:scale-105 hover:shadow-xl hover:shadow-blue-500/25 active:scale-95"
class="group border-surface0 bg-mantle hover:border-blue hover:bg-surface0 flex w-full items-center gap-2 border px-4 py-3 text-left transition-all"
>
<div class="absolute inset-0 bg-gradient-to-r from-blue-700 to-purple-700 opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
<span class="relative flex items-center gap-2">
🏠 Return Home
<span class="text-green">$</span>
<span class="text-blue group-hover:text-sky">cd</span>
<span class="text-text group-hover:text-blue">~</span>
<span class="text-subtext1 ml-auto opacity-0 transition-opacity group-hover:opacity-100">
[Return home]
</span>
</button>
<button
onClick={() => window.history.back()}
class="group relative overflow-hidden rounded-lg border-2 border-slate-600 bg-transparent px-8 py-4 text-lg font-medium text-slate-300 transition-all duration-300 hover:border-blue-500 hover:bg-blue-500/10 hover:text-blue-400 active:scale-95"
class="group border-surface0 bg-mantle hover:border-blue hover:bg-surface0 flex w-full items-center gap-2 border px-4 py-3 text-left transition-all"
>
<span class="relative flex items-center gap-2"> Go Back</span>
<span class="text-green">$</span>
<span class="text-blue group-hover:text-sky">cd</span>
<span class="text-text group-hover:text-blue">..</span>
<span class="text-subtext1 ml-auto opacity-0 transition-opacity group-hover:opacity-100">
[Go back]
</span>
</button>
<button
onClick={() => navigate("/blog")}
class="group border-surface0 bg-mantle hover:border-blue hover:bg-surface0 flex w-full items-center gap-2 border px-4 py-3 text-left transition-all"
>
<span class="text-green">$</span>
<span class="text-blue group-hover:text-sky">cd</span>
<span class="text-text group-hover:text-blue">~/blog</span>
<span class="text-subtext1 ml-auto opacity-0 transition-opacity group-hover:opacity-100">
[View blog]
</span>
</button>
</div>
{/* Floating elements */}
<div class="animate-bounce-slow absolute top-20 left-10">
<div class="h-6 w-6 rotate-45 bg-gradient-to-br from-blue-400 to-purple-500 opacity-60" />
</div>
<div class="animate-bounce-slow-delay absolute top-32 right-16">
<div class="h-4 w-4 rounded-full bg-gradient-to-br from-purple-400 to-blue-500 opacity-60" />
</div>
<div class="animate-bounce-slow absolute bottom-20 left-20">
<div
class="h-5 w-5 bg-gradient-to-br from-blue-500 to-purple-400 opacity-60"
style={{ "clip-path": "polygon(50% 0%, 0% 100%, 100% 100%)" }}
/>
{/* Command history */}
<Show when={history().length > 0}>
<div class="mb-4 w-full max-w-4xl font-mono text-sm">
<For each={history()}>
{(item) => (
<div class="mb-3">
<div class="text-subtext0 flex items-center gap-2">
<span class="text-green">freno@terminal</span>
<span class="text-subtext1">:</span>
<span class="text-blue">~</span>
<span class="text-subtext1">$</span>
<span class="text-text">{item.command}</span>
</div>
<div
class="mt-1 whitespace-pre-wrap"
classList={{
"text-text": item.type === "success",
"text-red": item.type === "error",
"text-blue": item.type === "info"
}}
>
{item.output}
</div>
</div>
)}
</For>
</div>
</Show>
{/* Interactive input */}
<div class="w-full max-w-4xl font-mono text-sm">
<div class="flex items-center gap-2">
<span class="text-green">freno@terminal</span>
<span class="text-subtext1">:</span>
<span class="text-blue">~</span>
<span class="text-subtext1">$</span>
<input
ref={inputRef}
type="text"
value={command()}
onInput={(e) => setCommand(e.currentTarget.value)}
onKeyDown={handleKeyDown}
class="text-text caret-text ml-1 flex-1 border-none bg-transparent outline-none"
autocomplete="off"
spellcheck={false}
/>
</div>
</div>
{/* Footer */}
<div class="absolute bottom-8 left-1/2 -translate-x-1/2">
<p class="text-sm text-slate-500 dark:text-slate-600">
Error Code: 404 Page Not Found
</p>
<div class="text-subtext1 absolute right-4 bottom-4 font-mono text-xs">
<span class="text-red">404</span>{" "}
<span class="text-subtext0">|</span> Page Not Found
</div>
</div>
{/* Custom styles */}
<style>{`
@keyframes grid-move {
@keyframes scanline {
0% {
transform: translate(0, 0);
transform: translateY(-100%);
}
100% {
transform: translate(50px, 50px);
}
}
.animate-fade-in {
animation: fadeIn 1s ease-out 0.5s both;
}
.animate-fade-in-delay {
animation: fadeIn 1s ease-out 1s both;
}
.animate-bounce-slow {
animation: bounce-slow 3s ease-in-out infinite;
}
.animate-bounce-slow-delay {
animation: bounce-slow 3s ease-in-out infinite 1.5s;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes bounce-slow {
0%,
100% {
transform: translateY(0) rotate(0deg);
}
50% {
transform: translateY(-20px) rotate(180deg);
transform: translateY(100%);
}
}
`}</style>

27
src/routes/error-test.tsx Normal file
View File

@@ -0,0 +1,27 @@
import { createSignal, onMount } from "solid-js";
export default function ErrorTest() {
const [shouldCrash, setShouldCrash] = createSignal(false);
// Crash on mount if flag is set
if (shouldCrash()) {
throw new Error("Test error - Error boundary triggered!");
}
return (
<div class="bg-crust flex min-h-screen items-center justify-center">
<div class="bg-surface0 max-w-md rounded-lg p-8 text-center shadow-lg">
<h1 class="text-text mb-4 text-2xl font-bold">Error Boundary Test</h1>
<p class="text-subtext0 mb-6">
Click the button below to trigger the error boundary
</p>
<button
onClick={() => setShouldCrash(true)}
class="bg-red hover:bg-maroon rounded px-6 py-3 text-base font-bold transition"
>
💥 Trigger Error Boundary
</button>
</div>
</div>
);
}