diff --git a/src/components/Bars.tsx b/src/components/Bars.tsx index 8535825..137e9c3 100644 --- a/src/components/Bars.tsx +++ b/src/components/Bars.tsx @@ -206,6 +206,8 @@ export function LeftBar() { const [isMounted, setIsMounted] = createSignal(false); const [signOutLoading, setSignOutLoading] = createSignal(false); + const [getLostText, setGetLostText] = createSignal("What's this?"); + const [getLostVisible, setGetLostVisible] = createSignal(false); const handleLinkClick = () => { if (typeof window !== "undefined" && window.innerWidth < 768) { @@ -252,6 +254,60 @@ export function LeftBar() { onMount(() => { 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) { // Focus trap for accessibility on mobile const handleKeyDown = (e: KeyboardEvent) => { @@ -291,6 +347,11 @@ export function LeftBar() { onCleanup(() => { ref?.removeEventListener("keydown", handleKeyDown); + clearInterval(glitchInterval); + }); + } else { + onCleanup(() => { + clearInterval(glitchInterval); }); } @@ -498,29 +559,39 @@ export function LeftBar() { -
  • - -
  • + {/* Get Lost button - outside Typewriter to allow glitch effect */} + +
    diff --git a/src/components/ErrorBoundaryFallback.tsx b/src/components/ErrorBoundaryFallback.tsx index 91ba2e8..963788c 100644 --- a/src/components/ErrorBoundaryFallback.tsx +++ b/src/components/ErrorBoundaryFallback.tsx @@ -1,10 +1,7 @@ import { useNavigate } from "@solidjs/router"; -import { createSignal, onMount, onCleanup, For, Show } from "solid-js"; -import { - CommandHistoryItem, - createTerminalCommands, - executeTerminalCommand -} from "~/lib/terminal-commands"; +import { createSignal } from "solid-js"; +import { TerminalErrorPage } from "~/components/TerminalErrorPage"; +import { useDarkMode } from "~/context/darkMode"; export interface ErrorBoundaryFallbackProps { error: Error; @@ -22,282 +19,126 @@ export default function ErrorBoundaryFallback( 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 [command, setCommand] = createSignal(""); - const [history, setHistory] = createSignal([]); - const [historyIndex, setHistoryIndex] = createSignal(-1); - let inputRef: HTMLInputElement | undefined; console.error(props.error); - 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"; - - 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 ( -
    inputRef?.focus()} - > - {/* Scanline effect */} -
    -
    + const errorContent = ( +
    +
    + fatal: + Unhandled Runtime Exception
    - {/* Main content */} -
    - {/* Terminal header */} -
    -
    - freno@terminal - : - ~ - $ -
    -
    - - {/* 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}
    -
    - )} +
    +
    + +
    +
    {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:
    - - - - - - -
    - - {/* 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 */} -
    - ERR |{" "} - Runtime Exception
    - {/* Custom styles */} - +
    +
    + + + Type help to see available commands, + or try one of the suggestions below + +
    +
    ); + + const quickActions = ( +
    +
    Quick actions:
    + + + + + + +
    + ); + + return ( + ?~`"} + glitchSpeed={400} + glitchThreshold={0.8} + glitchIntensity={0.6} + navigate={navigate!} + location={{ + pathname: typeof window !== "undefined" ? window.location.pathname : "/" + }} + errorContent={errorContent} + quickActions={quickActions} + footer={ + <> + ERR |{" "} + Runtime Exception + + } + onGlitchTextChange={setGlitchText} + commandContext={{ isDark }} + /> + ); } diff --git a/src/components/TerminalErrorPage.tsx b/src/components/TerminalErrorPage.tsx new file mode 100644 index 0000000..4af7901 --- /dev/null +++ b/src/components/TerminalErrorPage.tsx @@ -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; +} + +export function TerminalErrorPage(props: TerminalErrorPageProps) { + const [glitchText, setGlitchText] = createSignal(props.glitchText); + const [command, setCommand] = createSignal(""); + const [history, setHistory] = createSignal([]); + 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 ( +
    inputRef?.focus()} + > + {/* Scanline effect */} +
    +
    +
    + + {/* Main content */} +
    + {/* Terminal header */} +
    +
    + freno@terminal + : + ~ + $ +
    +
    + + {/* Error Content - passed as prop */} + {props.errorContent} + + {/* Quick Actions - passed as prop */} + {props.quickActions} + + {/* 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 */} +
    + {props.footerText} +
    +
    + + {/* Custom styles */} + +
    + ); +} diff --git a/src/lib/terminal-commands.ts b/src/lib/terminal-commands.ts index 09994c9..d53c261 100644 --- a/src/lib/terminal-commands.ts +++ b/src/lib/terminal-commands.ts @@ -13,16 +13,19 @@ export interface CommandContext { type: "success" | "error" | "info" ) => void; triggerCrash?: () => void; + isDark?: boolean; } export const createTerminalCommands = (context: CommandContext) => { // Define available routes const routes = [ + { path: "/", name: "home" }, { path: "/blog", name: "blog" }, { path: "/resume", name: "resume" }, { path: "/contact", name: "contact" }, { path: "/downloads", name: "downloads" }, - { path: "/account", name: "account" } + { path: "/account", name: "account" }, + { path: "/login", name: "login" } ]; const commands: Record< @@ -41,11 +44,15 @@ export const createTerminalCommands = (context: CommandContext) => { action: () => window.history.back(), description: "Go back" }, + cd: { + action: () => context.navigate("/"), + description: "Navigate to home" + }, ls: { action: () => { context.addToHistory( "ls", - "home blog resume contact downloads login account", + "home blog resume contact downloads login account", "success" ); }, @@ -157,9 +164,10 @@ export const createTerminalCommands = (context: CommandContext) => { }, neofetch: { action: () => { + const theme = context.isDark ? "Catppuccin-mocha" : "Gruvbox-light"; 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 \`\"\"\"\" `, + ` _,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" ); }, diff --git a/src/routes/[...404].tsx b/src/routes/[...404].tsx index 304477f..918a513 100644 --- a/src/routes/[...404].tsx +++ b/src/routes/[...404].tsx @@ -1,12 +1,9 @@ import { Title, Meta } from "@solidjs/meta"; import { HttpStatusCode } from "@solidjs/start"; import { useNavigate, useLocation } from "@solidjs/router"; -import { createSignal, onCleanup, onMount, For, Show } from "solid-js"; -import { - CommandHistoryItem, - createTerminalCommands, - executeTerminalCommand -} from "~/lib/terminal-commands"; +import { createSignal, Show } from "solid-js"; +import { TerminalErrorPage } from "~/components/TerminalErrorPage"; +import { useDarkMode } from "~/context/darkMode"; // Component that crashes when rendered function CrashComponent() { @@ -16,105 +13,92 @@ function CrashComponent() { export default function NotFound() { const navigate = useNavigate(); const location = useLocation(); + const { isDark } = useDarkMode(); 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; - const addToHistory = ( - cmd: string, - output: string, - type: "success" | "error" | "info" - ) => { - if (cmd === "clear") { - setHistory([]); - } else { - setHistory([...history(), { command: cmd, output, type }]); - } - }; + const errorContent = ( +
    +
    + error: + HTTP {glitchText()} - Not Found +
    - const commands = createTerminalCommands({ - navigate, - location, - addToHistory, - triggerCrash: () => setShouldCrash(true) - }); +
    +
    + +
    +
    Failed to resolve route
    +
    + The requested path does not exist in the routing table +
    +
    +
    - 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([]); - } - }; +
    + Location:{" "} + {location.pathname} +
    +
    - onMount(() => { - const glitchChars = "!@#$%^&*()_+-=[]{}|;':\",./<>?~`0123456789"; - const originalText = "404"; +
    +
    + + + Type help to see available commands, + or try one of the suggestions below + +
    +
    +
    + ); - const glitchInterval = setInterval(() => { - if (Math.random() > 0.85) { - let glitched = ""; - 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); + const quickActions = ( +
    +
    Quick commands:
    - setTimeout(() => setGlitchText(originalText), 100); - } - }, 300); + - inputRef?.focus(); + - onCleanup(() => { - clearInterval(glitchInterval); - }); - }); + +
    + ); return ( <> - {/*@ts-ignore (this is intentional)*/} + {/*@ts-ignore (intentional crash)*/} 404 Not Found | Michael Freno @@ -123,178 +107,28 @@ export default function NotFound() { content="404 - Page not found. The page you're looking for doesn't exist." /> -
    inputRef?.focus()} - > - {/* Scanline effect */} -
    -
    -
    - - {/* Main content */} -
    - {/* Terminal header */} -
    -
    - freno@terminal - : - ~ - $ -
    -
    - - {/* 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 - -
    -
    -
    - - {/* Command suggestions */} -
    -
    Quick commands:
    - - - - - - -
    - - {/* 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 */} -
    + ?~`0123456789"} + glitchSpeed={300} + glitchThreshold={0.85} + glitchIntensity={0.7} + navigate={navigate} + location={location} + errorContent={errorContent} + quickActions={quickActions} + footer={ + <> 404{" "} | Page Not Found -
    -
    - - {/* Custom styles */} - -
    + + } + onGlitchTextChange={setGlitchText} + commandContext={{ + triggerCrash: () => setShouldCrash(true), + isDark: isDark() + }} + /> ); }