now its good :)
This commit is contained in:
@@ -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,7 +559,18 @@ 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">
|
</ul>
|
||||||
|
</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
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const lostUrls = [
|
const lostUrls = [
|
||||||
@@ -513,13 +585,12 @@ export function LeftBar() {
|
|||||||
navigate(randomUrl);
|
navigate(randomUrl);
|
||||||
handleLinkClick();
|
handleLinkClick();
|
||||||
}}
|
}}
|
||||||
class="text-left"
|
class="text-left font-mono"
|
||||||
>
|
>
|
||||||
Get Lost
|
{getLostText()}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</Typewriter>
|
|
||||||
|
|
||||||
<hr class="border-overlay0 -mx-4 my-auto" />
|
<hr class="border-overlay0 -mx-4 my-auto" />
|
||||||
<div class="my-auto">
|
<div class="my-auto">
|
||||||
|
|||||||
@@ -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,133 +19,21 @@ 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,
|
|
||||||
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 (
|
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* 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 Display */}
|
|
||||||
<div class="mb-8 w-full max-w-4xl font-mono">
|
<div class="mb-8 w-full max-w-4xl font-mono">
|
||||||
<div class="mb-4 flex items-center gap-2">
|
<div class="mb-4 flex items-center gap-2">
|
||||||
<span class="text-red">fatal:</span>
|
<span class="text-red">fatal:</span>
|
||||||
@@ -183,14 +68,15 @@ export default function ErrorBoundaryFallback(
|
|||||||
<div class="flex items-start gap-2">
|
<div class="flex items-start gap-2">
|
||||||
<span class="text-blue">ℹ</span>
|
<span class="text-blue">ℹ</span>
|
||||||
<span>
|
<span>
|
||||||
Type <span class="text-green">help</span> to see available
|
Type <span class="text-green">help</span> to see available commands,
|
||||||
commands, or try one of the suggestions below
|
or try one of the suggestions below
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
{/* Command options */}
|
const quickActions = (
|
||||||
<div class="mb-8 w-full max-w-4xl space-y-3 font-mono text-sm">
|
<div class="mb-8 w-full max-w-4xl space-y-3 font-mono text-sm">
|
||||||
<div class="text-subtext1">Quick actions:</div>
|
<div class="text-subtext1">Quick actions:</div>
|
||||||
|
|
||||||
@@ -230,74 +116,29 @@ export default function ErrorBoundaryFallback(
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
{/* Command history */}
|
return (
|
||||||
<Show when={history().length > 0}>
|
<TerminalErrorPage
|
||||||
<div class="mb-4 w-full max-w-4xl font-mono text-sm">
|
glitchText="ERROR"
|
||||||
<For each={history()}>
|
glitchChars={"!@#$%^&*()_+-=[]{}|;':\",./<>?~`"}
|
||||||
{(item) => (
|
glitchSpeed={400}
|
||||||
<div class="mb-3">
|
glitchThreshold={0.8}
|
||||||
<div class="text-subtext0 flex items-center gap-2">
|
glitchIntensity={0.6}
|
||||||
<span class="text-green">freno@terminal</span>
|
navigate={navigate!}
|
||||||
<span class="text-subtext1">:</span>
|
location={{
|
||||||
<span class="text-blue">~</span>
|
pathname: typeof window !== "undefined" ? window.location.pathname : "/"
|
||||||
<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"
|
|
||||||
}}
|
}}
|
||||||
>
|
errorContent={errorContent}
|
||||||
{item.output}
|
quickActions={quickActions}
|
||||||
</pre>
|
footer={
|
||||||
</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>{" "}
|
<span class="text-red">ERR</span> <span class="text-subtext0">|</span>{" "}
|
||||||
Runtime Exception
|
Runtime Exception
|
||||||
</div>
|
</>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Custom styles */}
|
|
||||||
<style>{`
|
|
||||||
@keyframes scanline {
|
|
||||||
0% {
|
|
||||||
transform: translateY(-100%);
|
|
||||||
}
|
}
|
||||||
100% {
|
onGlitchTextChange={setGlitchText}
|
||||||
transform: translateY(100%);
|
commandContext={{ isDark }}
|
||||||
}
|
/>
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
250
src/components/TerminalErrorPage.tsx
Normal file
250
src/components/TerminalErrorPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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,6 +44,10 @@ 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(
|
||||||
@@ -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"
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,142 +13,11 @@ 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,
|
|
||||||
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(() => {
|
|
||||||
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);
|
|
||||||
|
|
||||||
setTimeout(() => setGlitchText(originalText), 100);
|
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
|
|
||||||
inputRef?.focus();
|
|
||||||
|
|
||||||
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"
|
|
||||||
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>
|
|
||||||
|
|
||||||
{/* 404 Error Display */}
|
|
||||||
<div class="mb-8 w-full max-w-4xl font-mono">
|
<div class="mb-8 w-full max-w-4xl font-mono">
|
||||||
<div class="mb-4 flex items-center gap-2">
|
<div class="mb-4 flex items-center gap-2">
|
||||||
<span class="text-red">error:</span>
|
<span class="text-red">error:</span>
|
||||||
@@ -179,14 +45,15 @@ export default function NotFound() {
|
|||||||
<div class="flex items-start gap-2">
|
<div class="flex items-start gap-2">
|
||||||
<span class="text-blue">ℹ</span>
|
<span class="text-blue">ℹ</span>
|
||||||
<span>
|
<span>
|
||||||
Type <span class="text-green">help</span> to see available
|
Type <span class="text-green">help</span> to see available commands,
|
||||||
commands, or try one of the suggestions below
|
or try one of the suggestions below
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
{/* Command suggestions */}
|
const quickActions = (
|
||||||
<div class="mb-8 w-full max-w-4xl space-y-3 font-mono text-sm">
|
<div class="mb-8 w-full max-w-4xl space-y-3 font-mono text-sm">
|
||||||
<div class="text-subtext1">Quick commands:</div>
|
<div class="text-subtext1">Quick commands:</div>
|
||||||
|
|
||||||
@@ -226,75 +93,42 @@ export default function NotFound() {
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
{/* Command history */}
|
return (
|
||||||
<Show when={history().length > 0}>
|
<>
|
||||||
<div class="mb-4 w-full max-w-4xl font-mono text-sm">
|
<Show when={shouldCrash()}>
|
||||||
<For each={history()}>
|
{/*@ts-ignore (intentional crash)*/}
|
||||||
{(item) => (
|
<CrashComponent />
|
||||||
<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>
|
</Show>
|
||||||
|
<Title>404 Not Found | Michael Freno</Title>
|
||||||
{/* Interactive input */}
|
<Meta
|
||||||
<div class="w-full max-w-4xl font-mono text-sm">
|
name="description"
|
||||||
<div class="flex items-center gap-2">
|
content="404 - Page not found. The page you're looking for doesn't exist."
|
||||||
<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>
|
<HttpStatusCode code={404} />
|
||||||
</div>
|
<TerminalErrorPage
|
||||||
|
glitchText="404"
|
||||||
{/* Footer */}
|
glitchChars={"!@#$%^&*()_+-=[]{}|;':\",./<>?~`0123456789"}
|
||||||
<div class="text-subtext1 absolute right-4 bottom-4 font-mono text-xs">
|
glitchSpeed={300}
|
||||||
|
glitchThreshold={0.85}
|
||||||
|
glitchIntensity={0.7}
|
||||||
|
navigate={navigate}
|
||||||
|
location={location}
|
||||||
|
errorContent={errorContent}
|
||||||
|
quickActions={quickActions}
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* Custom styles */}
|
|
||||||
<style>{`
|
|
||||||
@keyframes scanline {
|
|
||||||
0% {
|
|
||||||
transform: translateY(-100%);
|
|
||||||
}
|
}
|
||||||
100% {
|
onGlitchTextChange={setGlitchText}
|
||||||
transform: translateY(100%);
|
commandContext={{
|
||||||
}
|
triggerCrash: () => setShouldCrash(true),
|
||||||
}
|
isDark: isDark()
|
||||||
`}</style>
|
}}
|
||||||
</div>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user