From bc2a74be50c8bc074729ef56d0acb283b7cbae17 Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Wed, 31 Dec 2025 12:00:44 -0500 Subject: [PATCH] decent 404/error page --- src/app.tsx | 18 +- src/components/Bars.tsx | 20 ++ src/components/ErrorBoundaryFallback.tsx | 354 ++++++++++++++--------- src/lib/terminal-commands.ts | 275 ++++++++++++++++++ src/routes/[...404].tsx | 352 +++++++++++++--------- src/routes/error-test.tsx | 27 ++ 6 files changed, 770 insertions(+), 276 deletions(-) create mode 100644 src/lib/terminal-commands.ts create mode 100644 src/routes/error-test.tsx diff --git a/src/app.tsx b/src/app.tsx index 66184ff..52a9aa5 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -181,12 +181,20 @@ function AppLayout(props: { children: any }) { JavaScript is disabled. Features will be limited. -
( + + )} > - }>{props.children} -
+
+ }> + {props.children} + +
+ diff --git a/src/components/Bars.tsx b/src/components/Bars.tsx index c91f6d3..8535825 100644 --- a/src/components/Bars.tsx +++ b/src/components/Bars.tsx @@ -498,6 +498,26 @@ export function LeftBar() { +
  • + +
  • diff --git a/src/components/ErrorBoundaryFallback.tsx b/src/components/ErrorBoundaryFallback.tsx index 8b98f75..91ba2e8 100644 --- a/src/components/ErrorBoundaryFallback.tsx +++ b/src/components/ErrorBoundaryFallback.tsx @@ -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([]); + 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 ( -
    -
    - - {(particle) => ( -
    -
    -
    - )} - -
    - - {/* Animated grid background */} -
    +
    inputRef?.focus()} + > + {/* Scanline effect */} +
    {/* Main content */} -
    - {/* Glitchy ERROR text */} -
    -

    - {glitchText()} -

    -
    +
    + {/* Terminal header */} +
    +
    + freno@terminal + : + ~ + $ +
    - {/* Error message */} -
    -

    - Huh. -

    -

    - An unexpected error has disrupted the flow of ... something. -
    - But don't worry, you can try again or navigate back to safety. -

    - {props.error.message && ( -

    - Error: {props.error.message} -

    - )} + {/* Error Display */} +
    +
    + fatal: + Unhandled Runtime Exception +
    + +
    +
    + +
    +
    {glitchText()}
    +
    + Application encountered an unexpected error +
    + {props.error.message && ( +
    +
    Message:
    +
    {props.error.message}
    +
    + )} + {props.error.stack && ( +
    +
    Stack trace:
    +
    {props.error.stack}
    +
    + )} +
    +
    +
    + +
    +
    + + + Type help to see available + commands, or try one of the suggestions below + +
    +
    -
    + {/* Command options */} +
    +
    Quick actions:
    +
    - {/* Floating elements */} -
    -
    -
    -
    -
    -
    -
    -
    + {/* Command history */} + 0}> +
    + + {(item) => ( +
    +
    + freno@terminal + : + ~ + $ + {item.command} +
    +
    +                    {item.output}
    +                  
    +
    + )} +
    +
    +
    + + {/* Interactive input */} +
    +
    + freno@terminal + : + ~ + $ + 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} + /> +
    {/* Footer */} -
    -

    - System Error • Something went wrong -

    +
    + ERR |{" "} + Runtime Exception
    {/* Custom styles */} diff --git a/src/lib/terminal-commands.ts b/src/lib/terminal-commands.ts new file mode 100644 index 0000000..09994c9 --- /dev/null +++ b/src/lib/terminal-commands.ts @@ -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" + ); + } +}; diff --git a/src/routes/[...404].tsx b/src/routes/[...404].tsx index 918dcdc..304477f 100644 --- a/src/routes/[...404].tsx +++ b/src/routes/[...404].tsx @@ -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([]); + 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 ( <> + + {/*@ts-ignore (this is intentional)*/} + + 404 Not Found | Michael Freno -
    - {/* Animated particle background */} -
    - - {(particle) => ( -
    -
    -
    - )} - -
    - - {/* Animated grid background */} -
    +
    inputRef?.focus()} + > + {/* Scanline effect */} +
    {/* Main content */} -
    - {/* Glitchy 404 */} -
    -

    - {glitchText()} -

    -
    +
    + {/* Terminal header */} +
    +
    + freno@terminal + : + ~ + $ +
    - {/* Error message with typewriter effect */} -
    -

    - You seem to have drifted off into space... -

    -

    - ...or the page you're looking for has drifted into the void. -
    - But don't worry, we can navigate you back to safety. -

    + {/* 404 Error Display */} +
    +
    + error: + HTTP {glitchText()} - Not Found +
    + +
    +
    + +
    +
    Failed to resolve route
    +
    + The requested path does not exist in the routing table +
    +
    +
    + +
    + Location:{" "} + {location.pathname} +
    +
    + +
    +
    + + + Type help to see available + commands, or try one of the suggestions below + +
    +
    - {/* Action buttons */} -
    + {/* Command suggestions */} +
    +
    Quick commands:
    + + +
    - {/* Floating elements */} -
    -
    -
    -
    -
    -
    -
    -
    + {/* Command history */} + 0}> +
    + + {(item) => ( +
    +
    + freno@terminal + : + ~ + $ + {item.command} +
    +
    + {item.output} +
    +
    + )} +
    +
    +
    + + {/* Interactive input */} +
    +
    + freno@terminal + : + ~ + $ + 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} + /> +
    {/* Footer */} -
    -

    - Error Code: 404 • Page Not Found -

    +
    + 404{" "} + | Page Not Found
    {/* Custom styles */} diff --git a/src/routes/error-test.tsx b/src/routes/error-test.tsx new file mode 100644 index 0000000..f0b37e2 --- /dev/null +++ b/src/routes/error-test.tsx @@ -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 ( +
    +
    +

    Error Boundary Test

    +

    + Click the button below to trigger the error boundary +

    + +
    +
    + ); +}