now its good :)

This commit is contained in:
Michael Freno
2025-12-31 12:25:47 -05:00
parent bc2a74be50
commit 86b1552e85
5 changed files with 559 additions and 555 deletions

View File

@@ -206,6 +206,8 @@ export function LeftBar() {
const [isMounted, setIsMounted] = createSignal(false); const [isMounted, setIsMounted] = createSignal(false);
const [signOutLoading, setSignOutLoading] = createSignal(false); const [signOutLoading, setSignOutLoading] = createSignal(false);
const [getLostText, setGetLostText] = createSignal("What's this?");
const [getLostVisible, setGetLostVisible] = createSignal(false);
const handleLinkClick = () => { const handleLinkClick = () => {
if (typeof window !== "undefined" && window.innerWidth < 768) { if (typeof window !== "undefined" && window.innerWidth < 768) {
@@ -252,6 +254,60 @@ export function LeftBar() {
onMount(() => { onMount(() => {
setIsMounted(true); setIsMounted(true);
// Terminal-style appearance animation for "Get Lost" button
const glitchChars = "!@#$%^&*()_+-=[]{}|;':\",./<>?~`";
const originalText = "What's this?";
let glitchInterval: NodeJS.Timeout;
// Delay appearance to match terminal vibe
setTimeout(() => {
// Make visible immediately so typing animation is visible
setGetLostVisible(true);
// Type-in animation with random characters resolving
let currentIndex = 0;
const typeInterval = setInterval(() => {
if (currentIndex <= originalText.length) {
let displayText = originalText.substring(0, currentIndex);
// Add random trailing characters
if (currentIndex < originalText.length) {
const remaining = originalText.length - currentIndex;
for (let i = 0; i < remaining; i++) {
displayText +=
glitchChars[Math.floor(Math.random() * glitchChars.length)];
}
}
setGetLostText(displayText);
currentIndex++;
} else {
clearInterval(typeInterval);
setGetLostText(originalText);
// Start regular glitch effect after typing completes
glitchInterval = setInterval(() => {
if (Math.random() > 0.9) {
// 10% chance to glitch
let glitched = "";
for (let i = 0; i < originalText.length; i++) {
if (Math.random() > 0.7) {
// 30% chance each character glitches
glitched +=
glitchChars[Math.floor(Math.random() * glitchChars.length)];
} else {
glitched += originalText[i];
}
}
setGetLostText(glitched);
setTimeout(() => {
setGetLostText(originalText);
}, 100);
}
}, 150);
}
}, 140); // Type speed (higher is slower)
}, 500); // Initial delay before appearing
if (ref) { if (ref) {
// Focus trap for accessibility on mobile // Focus trap for accessibility on mobile
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
@@ -291,6 +347,11 @@ export function LeftBar() {
onCleanup(() => { onCleanup(() => {
ref?.removeEventListener("keydown", handleKeyDown); ref?.removeEventListener("keydown", handleKeyDown);
clearInterval(glitchInterval);
});
} else {
onCleanup(() => {
clearInterval(glitchInterval);
}); });
} }
@@ -498,29 +559,39 @@ export function LeftBar() {
</button> </button>
</li> </li>
</Show> </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> </ul>
</Typewriter> </Typewriter>
{/* Get Lost button - outside Typewriter to allow glitch effect */}
<ul class="flex flex-col gap-4 pb-6">
<li
class="hover:text-subtext0 w-fit transition-all duration-500 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold"
classList={{
"opacity-0 pointer-events-none": !getLostVisible(),
"opacity-100": getLostVisible()
}}
>
<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 font-mono"
>
{getLostText()}
</button>
</li>
</ul>
<hr class="border-overlay0 -mx-4 my-auto" /> <hr class="border-overlay0 -mx-4 my-auto" />
<div class="my-auto"> <div class="my-auto">
<DarkModeToggle /> <DarkModeToggle />

View File

@@ -1,10 +1,7 @@
import { useNavigate } from "@solidjs/router"; import { useNavigate } from "@solidjs/router";
import { createSignal, onMount, onCleanup, For, Show } from "solid-js"; import { createSignal } from "solid-js";
import { import { TerminalErrorPage } from "~/components/TerminalErrorPage";
CommandHistoryItem, import { useDarkMode } from "~/context/darkMode";
createTerminalCommands,
executeTerminalCommand
} from "~/lib/terminal-commands";
export interface ErrorBoundaryFallbackProps { export interface ErrorBoundaryFallbackProps {
error: Error; error: Error;
@@ -22,282 +19,126 @@ export default function ErrorBoundaryFallback(
window.location.href = path; window.location.href = path;
}; };
} }
// Try to get dark mode, fallback to true (dark) if context unavailable
let isDark = true;
try {
const darkMode = useDarkMode();
isDark = darkMode.isDark();
} catch (e) {
// Context not available, use default
}
const [glitchText, setGlitchText] = createSignal("ERROR"); const [glitchText, setGlitchText] = createSignal("ERROR");
const [command, setCommand] = createSignal("");
const [history, setHistory] = createSignal<CommandHistoryItem[]>([]);
const [historyIndex, setHistoryIndex] = createSignal(-1);
let inputRef: HTMLInputElement | undefined;
console.error(props.error); console.error(props.error);
const addToHistory = ( const errorContent = (
cmd: string, <div class="mb-8 w-full max-w-4xl font-mono">
output: string, <div class="mb-4 flex items-center gap-2">
type: "success" | "error" | "info" <span class="text-red">fatal:</span>
) => { <span class="text-text">Unhandled Runtime Exception</span>
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";
const glitchInterval = setInterval(() => {
if (Math.random() > 0.8) {
let glitched = "";
for (let i = 0; i < originalText.length; i++) {
if (Math.random() > 0.6) {
glitched +=
glitchChars[Math.floor(Math.random() * glitchChars.length)];
} else {
glitched += originalText[i];
}
}
setGlitchText(glitched);
setTimeout(() => setGlitchText(originalText), 150);
}
}, 400);
inputRef?.focus();
onCleanup(() => {
clearInterval(glitchInterval);
});
});
return (
<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":
"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> </div>
{/* Main content */} <div class="border-red bg-mantle mb-6 border-l-4 p-4 text-sm">
<div class="relative z-10 flex min-h-screen flex-col items-start justify-start px-8 py-16 md:px-16"> <div class="mb-2 flex items-start gap-2">
{/* Terminal header */} <span class="text-red text-xl"></span>
<div class="mb-8 w-full max-w-4xl"> <div class="flex-1">
<div class="border-surface0 text-subtext0 flex items-center gap-2 border-b pb-2 font-mono text-sm"> <div class="text-red mb-2 text-lg">{glitchText()}</div>
<span class="text-green">freno@terminal</span> <div class="text-text">
<span class="text-subtext1">:</span> Application encountered an unexpected error
<span class="text-blue">~</span> </div>
<span class="text-subtext1">$</span> {props.error.message && (
</div> <div class="bg-surface0 text-subtext0 mt-2 rounded p-2">
</div> <div class="text-yellow mb-1">Message:</div>
<div class="text-text">{props.error.message}</div>
{/* 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> )}
{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 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>
{/* 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 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"
>
<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 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">~</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 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">..</span>
<span class="text-subtext1 ml-auto opacity-0 transition-opacity group-hover:opacity-100">
[Go back]
</span>
</button>
</div>
{/* 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="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>
</div> </div>
{/* Custom styles */} <div class="text-subtext0 space-y-2 text-sm">
<style>{` <div class="flex items-start gap-2">
@keyframes scanline { <span class="text-blue"></span>
0% { <span>
transform: translateY(-100%); Type <span class="text-green">help</span> to see available commands,
} or try one of the suggestions below
100% { </span>
transform: translateY(100%); </div>
} </div>
}
`}</style>
</div> </div>
); );
const quickActions = (
<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 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"
>
<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 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">~</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 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">..</span>
<span class="text-subtext1 ml-auto opacity-0 transition-opacity group-hover:opacity-100">
[Go back]
</span>
</button>
</div>
);
return (
<TerminalErrorPage
glitchText="ERROR"
glitchChars={"!@#$%^&*()_+-=[]{}|;':\",./<>?~`"}
glitchSpeed={400}
glitchThreshold={0.8}
glitchIntensity={0.6}
navigate={navigate!}
location={{
pathname: typeof window !== "undefined" ? window.location.pathname : "/"
}}
errorContent={errorContent}
quickActions={quickActions}
footer={
<>
<span class="text-red">ERR</span> <span class="text-subtext0">|</span>{" "}
Runtime Exception
</>
}
onGlitchTextChange={setGlitchText}
commandContext={{ isDark }}
/>
);
} }

View File

@@ -0,0 +1,250 @@
import {
createSignal,
createEffect,
onMount,
onCleanup,
For,
Show,
JSX
} from "solid-js";
import {
CommandHistoryItem,
createTerminalCommands,
executeTerminalCommand,
CommandContext
} from "~/lib/terminal-commands";
interface TerminalErrorPageProps {
glitchText: string;
glitchChars: string;
glitchSpeed?: number;
glitchThreshold?: number;
glitchIntensity?: number;
navigate: (path: string) => void;
location: { pathname: string };
errorContent: JSX.Element;
quickActions: JSX.Element;
footer: JSX.Element;
onGlitchTextChange?: (text: string) => void;
commandContext?: Partial<CommandContext>;
}
export function TerminalErrorPage(props: TerminalErrorPageProps) {
const [glitchText, setGlitchText] = createSignal(props.glitchText);
const [command, setCommand] = createSignal("");
const [history, setHistory] = createSignal<CommandHistoryItem[]>([]);
const [historyIndex, setHistoryIndex] = createSignal(-1);
let inputRef: HTMLInputElement | undefined;
let footerRef: HTMLDivElement | undefined;
// Auto-scroll to bottom when history changes
createEffect(() => {
if (history().length > 0) {
setTimeout(() => {
footerRef?.scrollIntoView({ behavior: "smooth", block: "end" });
}, 0);
}
});
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: props.navigate,
location: props.location,
addToHistory,
...props.commandContext
});
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 originalText = props.glitchText;
const glitchChars = props.glitchChars;
const glitchSpeed = props.glitchSpeed || 300;
const glitchThreshold = props.glitchThreshold || 0.85;
const glitchIntensity = props.glitchIntensity || 0.7;
const glitchInterval = setInterval(() => {
if (Math.random() > glitchThreshold) {
let glitched = "";
for (let i = 0; i < originalText.length; i++) {
if (Math.random() > glitchIntensity) {
glitched +=
glitchChars[Math.floor(Math.random() * glitchChars.length)];
} else {
glitched += originalText[i];
}
}
setGlitchText(glitched);
props.onGlitchTextChange?.(glitched);
setTimeout(() => {
setGlitchText(originalText);
props.onGlitchTextChange?.(originalText);
}, 100);
}
}, glitchSpeed);
inputRef?.focus();
onCleanup(() => {
clearInterval(glitchInterval);
});
});
return (
<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":
"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-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 Content - passed as prop */}
{props.errorContent}
{/* Quick Actions - passed as prop */}
{props.quickActions}
{/* 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
ref={footerRef}
class="text-subtext1 absolute right-4 bottom-4 font-mono text-xs"
>
{props.footerText}
</div>
</div>
{/* Custom styles */}
<style>{`
@keyframes scanline {
0% {
transform: translateY(-100%);
}
100% {
transform: translateY(100%);
}
}
`}</style>
</div>
);
}

View File

@@ -13,16 +13,19 @@ export interface CommandContext {
type: "success" | "error" | "info" type: "success" | "error" | "info"
) => void; ) => void;
triggerCrash?: () => void; triggerCrash?: () => void;
isDark?: boolean;
} }
export const createTerminalCommands = (context: CommandContext) => { export const createTerminalCommands = (context: CommandContext) => {
// Define available routes // Define available routes
const routes = [ const routes = [
{ path: "/", name: "home" },
{ path: "/blog", name: "blog" }, { path: "/blog", name: "blog" },
{ path: "/resume", name: "resume" }, { path: "/resume", name: "resume" },
{ path: "/contact", name: "contact" }, { path: "/contact", name: "contact" },
{ path: "/downloads", name: "downloads" }, { path: "/downloads", name: "downloads" },
{ path: "/account", name: "account" } { path: "/account", name: "account" },
{ path: "/login", name: "login" }
]; ];
const commands: Record< const commands: Record<
@@ -41,11 +44,15 @@ export const createTerminalCommands = (context: CommandContext) => {
action: () => window.history.back(), action: () => window.history.back(),
description: "Go back" description: "Go back"
}, },
cd: {
action: () => context.navigate("/"),
description: "Navigate to home"
},
ls: { ls: {
action: () => { action: () => {
context.addToHistory( context.addToHistory(
"ls", "ls",
"home blog resume contact downloads login account", "home blog resume contact downloads login account",
"success" "success"
); );
}, },
@@ -157,9 +164,10 @@ export const createTerminalCommands = (context: CommandContext) => {
}, },
neofetch: { neofetch: {
action: () => { action: () => {
const theme = context.isDark ? "Catppuccin-mocha" : "Gruvbox-light";
context.addToHistory( context.addToHistory(
"neofetch", "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 \`\"\"\"\" `, ` _,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: ${theme}\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" "info"
); );
}, },

View File

@@ -1,12 +1,9 @@
import { Title, Meta } from "@solidjs/meta"; import { Title, Meta } from "@solidjs/meta";
import { HttpStatusCode } from "@solidjs/start"; import { HttpStatusCode } from "@solidjs/start";
import { useNavigate, useLocation } from "@solidjs/router"; import { useNavigate, useLocation } from "@solidjs/router";
import { createSignal, onCleanup, onMount, For, Show } from "solid-js"; import { createSignal, Show } from "solid-js";
import { import { TerminalErrorPage } from "~/components/TerminalErrorPage";
CommandHistoryItem, import { useDarkMode } from "~/context/darkMode";
createTerminalCommands,
executeTerminalCommand
} from "~/lib/terminal-commands";
// Component that crashes when rendered // Component that crashes when rendered
function CrashComponent() { function CrashComponent() {
@@ -16,105 +13,92 @@ function CrashComponent() {
export default function NotFound() { export default function NotFound() {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const { isDark } = useDarkMode();
const [glitchText, setGlitchText] = createSignal("404"); const [glitchText, setGlitchText] = createSignal("404");
const [command, setCommand] = createSignal("");
const [history, setHistory] = createSignal<CommandHistoryItem[]>([]);
const [historyIndex, setHistoryIndex] = createSignal(-1);
const [shouldCrash, setShouldCrash] = createSignal(false); const [shouldCrash, setShouldCrash] = createSignal(false);
let inputRef: HTMLInputElement | undefined;
const addToHistory = ( const errorContent = (
cmd: string, <div class="mb-8 w-full max-w-4xl font-mono">
output: string, <div class="mb-4 flex items-center gap-2">
type: "success" | "error" | "info" <span class="text-red">error:</span>
) => { <span class="text-text">HTTP {glitchText()} - Not Found</span>
if (cmd === "clear") { </div>
setHistory([]);
} else {
setHistory([...history(), { command: cmd, output, type }]);
}
};
const commands = createTerminalCommands({ <div class="border-red bg-mantle mb-6 border-l-4 p-4 text-sm">
navigate, <div class="mb-2 flex items-start gap-2">
location, <span class="text-red"></span>
addToHistory, <div class="flex-1">
triggerCrash: () => setShouldCrash(true) <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>
const handleKeyDown = (e: KeyboardEvent) => { <div class="text-subtext1 mt-3">
if (e.key === "Enter") { <span class="text-yellow"></span> Location:{" "}
executeTerminalCommand(command(), commands, addToHistory); <span class="text-peach">{location.pathname}</span>
setCommand(""); </div>
setHistoryIndex(-1); </div>
} 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(() => { <div class="text-subtext0 space-y-2 text-sm">
const glitchChars = "!@#$%^&*()_+-=[]{}|;':\",./<>?~`0123456789"; <div class="flex items-start gap-2">
const originalText = "404"; <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>
);
const glitchInterval = setInterval(() => { const quickActions = (
if (Math.random() > 0.85) { <div class="mb-8 w-full max-w-4xl space-y-3 font-mono text-sm">
let glitched = ""; <div class="text-subtext1">Quick commands:</div>
for (let i = 0; i < originalText.length; i++) {
if (Math.random() > 0.7) {
glitched +=
glitchChars[Math.floor(Math.random() * glitchChars.length)];
} else {
glitched += originalText[i];
}
}
setGlitchText(glitched);
setTimeout(() => setGlitchText(originalText), 100); <button
} onClick={() => navigate("/")}
}, 300); 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">~</span>
<span class="text-subtext1 ml-auto opacity-0 transition-opacity group-hover:opacity-100">
[Return home]
</span>
</button>
inputRef?.focus(); <button
onClick={() => window.history.back()}
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">..</span>
<span class="text-subtext1 ml-auto opacity-0 transition-opacity group-hover:opacity-100">
[Go back]
</span>
</button>
onCleanup(() => { <button
clearInterval(glitchInterval); 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>
);
return ( return (
<> <>
<Show when={shouldCrash()}> <Show when={shouldCrash()}>
{/*@ts-ignore (this is intentional)*/} {/*@ts-ignore (intentional crash)*/}
<CrashComponent /> <CrashComponent />
</Show> </Show>
<Title>404 Not Found | Michael Freno</Title> <Title>404 Not Found | Michael Freno</Title>
@@ -123,178 +107,28 @@ export default function NotFound() {
content="404 - Page not found. The page you're looking for doesn't exist." content="404 - Page not found. The page you're looking for doesn't exist."
/> />
<HttpStatusCode code={404} /> <HttpStatusCode code={404} />
<div <TerminalErrorPage
class="relative min-h-screen w-full overflow-hidden" glitchText="404"
onClick={() => inputRef?.focus()} glitchChars={"!@#$%^&*()_+-=[]{}|;':\",./<>?~`0123456789"}
> glitchSpeed={300}
{/* Scanline effect */} glitchThreshold={0.85}
<div class="pointer-events-none absolute inset-0 z-20 opacity-5"> glitchIntensity={0.7}
<div navigate={navigate}
class="h-full w-full" location={location}
style={{ errorContent={errorContent}
"background-image": quickActions={quickActions}
"repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,0,0,0.2) 2px, rgba(0,0,0,0.2) 4px)", footer={
animation: "scanline 8s linear infinite" <>
}}
/>
</div>
{/* Main content */}
<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>
{/* 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>
{/* 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 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">~</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 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">..</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>
{/* 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="text-subtext1 absolute right-4 bottom-4 font-mono text-xs">
<span class="text-red">404</span>{" "} <span class="text-red">404</span>{" "}
<span class="text-subtext0">|</span> Page Not Found <span class="text-subtext0">|</span> Page Not Found
</div> </>
</div> }
onGlitchTextChange={setGlitchText}
{/* Custom styles */} commandContext={{
<style>{` triggerCrash: () => setShouldCrash(true),
@keyframes scanline { isDark: isDark()
0% { }}
transform: translateY(-100%); />
}
100% {
transform: translateY(100%);
}
}
`}</style>
</div>
</> </>
); );
} }