init
This commit is contained in:
43
src/components/Bars.tsx
Normal file
43
src/components/Bars.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Typewriter } from "./Typewriter";
|
||||
|
||||
export function LeftBar() {
|
||||
return (
|
||||
<nav class="w-fit max-w-[25%] min-h-screen h-full border-r-2 border-r-maroon flex flex-col text-text text-xl font-bold py-10 px-4 gap-4 text-left">
|
||||
<Typewriter keepAlive={false}>
|
||||
<h3 class="text-2xl">Left Navigation</h3>
|
||||
<ul>
|
||||
<li class="hover:-translate-y-0.5 transition-transform duration-200 ease-in-out hover:text-green hover:font-bold hover:scale-110">
|
||||
<a href="#home">Home</a>
|
||||
</li>
|
||||
<li class="hover:-translate-y-0.5 transition-transform duration-200 ease-in-out hover:text-green hover:font-bold hover:scale-110">
|
||||
<a href="#about">About</a>
|
||||
</li>
|
||||
<li class="hover:-translate-y-0.5 transition-transform duration-200 ease-in-out hover:text-green hover:font-bold hover:scale-110">
|
||||
<a href="#services">Services</a>
|
||||
</li>
|
||||
</ul>
|
||||
</Typewriter>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
export function RightBar() {
|
||||
return (
|
||||
<nav class="w-fit max-w-[25%] min-h-screen h-full border-l-2 border-l-maroon flex flex-col text-text text-xl font-bold py-10 px-4 gap-4 text-right">
|
||||
<Typewriter keepAlive={false}>
|
||||
<h3 class="text-2xl">Right Navigation</h3>
|
||||
<ul>
|
||||
<li class="hover:-translate-y-0.5 transition-transform duration-200 ease-in-out hover:text-green hover:font-bold hover:scale-110">
|
||||
<a href="#home">Home</a>
|
||||
</li>
|
||||
<li class="hover:-translate-y-0.5 transition-transform duration-200 ease-in-out hover:text-green hover:font-bold hover:scale-110">
|
||||
<a href="#about">About</a>
|
||||
</li>
|
||||
<li class="hover:-translate-y-0.5 transition-transform duration-200 ease-in-out hover:text-green hover:font-bold hover:scale-110">
|
||||
<a href="#services">Services</a>
|
||||
</li>
|
||||
</ul>
|
||||
</Typewriter>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
57
src/components/TerminalSplash.tsx
Normal file
57
src/components/TerminalSplash.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Show, onMount, onCleanup, createSignal } from "solid-js";
|
||||
import { useSplash } from "../context/splash";
|
||||
|
||||
const spinnerChars = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
||||
|
||||
export function TerminalSplash() {
|
||||
const { showSplash, setShowSplash } = useSplash();
|
||||
const [showing, setShowing] = createSignal(0);
|
||||
const [isVisible, setIsVisible] = createSignal(true);
|
||||
|
||||
onMount(() => {
|
||||
const interval = setInterval(() => {
|
||||
setShowing((prev) => (prev + 1) % spinnerChars.length);
|
||||
}, 50);
|
||||
|
||||
// Hide splash after 1.5 seconds
|
||||
const timeoutId = setTimeout(() => {
|
||||
setShowSplash(false);
|
||||
}, 1500);
|
||||
|
||||
onCleanup(() => {
|
||||
clearInterval(interval);
|
||||
clearTimeout(timeoutId);
|
||||
});
|
||||
});
|
||||
|
||||
// Handle fade out when splash is hidden
|
||||
const shouldRender = () => showSplash() || isVisible();
|
||||
|
||||
// Trigger fade out, then hide after transition
|
||||
const opacity = () => {
|
||||
if (!showSplash() && isVisible()) {
|
||||
setTimeout(() => setIsVisible(false), 500);
|
||||
return "0";
|
||||
}
|
||||
if (showSplash()) {
|
||||
setIsVisible(true);
|
||||
return "1";
|
||||
}
|
||||
return "0";
|
||||
};
|
||||
|
||||
return (
|
||||
<Show when={shouldRender()}>
|
||||
<div
|
||||
class="fixed inset-0 z-50 w-screen h-screen bg-base flex overflow-hidden flex-col items-center justify-center mx-auto transition-opacity duration-500"
|
||||
style={{ opacity: opacity() }}
|
||||
>
|
||||
<div class="font-mono text-text text-4xl whitespace-pre-wrap p-8 max-w-3xl">
|
||||
<div class="flex justify-center items-center">
|
||||
{spinnerChars[showing()]}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
164
src/components/Typewriter.tsx
Normal file
164
src/components/Typewriter.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
import { JSX, onMount, createSignal, children } from "solid-js";
|
||||
import { useSplash } from "~/context/splash";
|
||||
|
||||
export function Typewriter(props: {
|
||||
children: JSX.Element;
|
||||
speed?: number;
|
||||
class?: string;
|
||||
keepAlive?: boolean | number;
|
||||
delay?: number;
|
||||
}) {
|
||||
const { keepAlive = true, delay = 0 } = props;
|
||||
let containerRef: HTMLDivElement | undefined;
|
||||
let cursorRef: HTMLDivElement | undefined;
|
||||
const [isTyping, setIsTyping] = createSignal(false);
|
||||
const [isDelaying, setIsDelaying] = createSignal(delay > 0);
|
||||
const [keepAliveCountdown, setKeepAliveCountdown] = createSignal(
|
||||
typeof keepAlive === "number" ? keepAlive : -1,
|
||||
);
|
||||
const resolved = children(() => props.children);
|
||||
const { showSplash } = useSplash();
|
||||
|
||||
onMount(() => {
|
||||
if (!containerRef || !cursorRef) return;
|
||||
|
||||
// FIRST: Walk DOM and hide all text immediately
|
||||
const textNodes: { node: Text; text: string; startIndex: number }[] = [];
|
||||
let totalChars = 0;
|
||||
|
||||
const walkDOM = (node: Node) => {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
const text = node.textContent || "";
|
||||
if (text.trim().length > 0) {
|
||||
textNodes.push({
|
||||
node: node as Text,
|
||||
text: text,
|
||||
startIndex: totalChars,
|
||||
});
|
||||
totalChars += text.length;
|
||||
|
||||
// Replace text with spans for each character
|
||||
const span = document.createElement("span");
|
||||
text.split("").forEach((char, i) => {
|
||||
const charSpan = document.createElement("span");
|
||||
charSpan.textContent = char;
|
||||
charSpan.style.opacity = "0";
|
||||
charSpan.setAttribute(
|
||||
"data-char-index",
|
||||
String(totalChars - text.length + i),
|
||||
);
|
||||
span.appendChild(charSpan);
|
||||
});
|
||||
node.parentNode?.replaceChild(span, node);
|
||||
}
|
||||
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
Array.from(node.childNodes).forEach(walkDOM);
|
||||
}
|
||||
};
|
||||
|
||||
walkDOM(containerRef);
|
||||
|
||||
// Position cursor at the first character location
|
||||
const firstChar = containerRef.querySelector(
|
||||
'[data-char-index="0"]',
|
||||
) as HTMLElement;
|
||||
if (firstChar && cursorRef) {
|
||||
// Insert cursor before the first character
|
||||
firstChar.parentNode?.insertBefore(cursorRef, firstChar);
|
||||
// Set cursor height to match first character
|
||||
cursorRef.style.height = `${firstChar.offsetHeight}px`;
|
||||
}
|
||||
|
||||
// THEN: Wait for splash to be hidden before starting the animation
|
||||
const checkSplashHidden = () => {
|
||||
if (showSplash()) {
|
||||
setTimeout(checkSplashHidden, 10);
|
||||
} else {
|
||||
// Start delay if specified
|
||||
if (delay > 0) {
|
||||
setTimeout(() => {
|
||||
setIsDelaying(false);
|
||||
startReveal();
|
||||
}, delay);
|
||||
} else {
|
||||
startReveal();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const startReveal = () => {
|
||||
setIsTyping(true); // Switch to typing cursor
|
||||
|
||||
// Animate revealing characters
|
||||
let currentIndex = 0;
|
||||
const speed = props.speed || 30;
|
||||
|
||||
const revealNextChar = () => {
|
||||
if (currentIndex < totalChars) {
|
||||
const charSpan = containerRef?.querySelector(
|
||||
`[data-char-index="${currentIndex}"]`,
|
||||
) as HTMLElement;
|
||||
|
||||
if (charSpan) {
|
||||
charSpan.style.opacity = "1";
|
||||
|
||||
// Move cursor after this character and match its height
|
||||
if (cursorRef) {
|
||||
charSpan.parentNode?.insertBefore(
|
||||
cursorRef,
|
||||
charSpan.nextSibling,
|
||||
);
|
||||
|
||||
// Match the height of the current character
|
||||
const charHeight = charSpan.offsetHeight;
|
||||
cursorRef.style.height = `${charHeight}px`;
|
||||
}
|
||||
}
|
||||
|
||||
currentIndex++;
|
||||
setTimeout(revealNextChar, 1000 / speed);
|
||||
} else {
|
||||
// Typing finished, switch to block cursor
|
||||
setIsTyping(false);
|
||||
|
||||
// Start keepAlive countdown if it's a number
|
||||
if (typeof keepAlive === "number") {
|
||||
const keepAliveInterval = setInterval(() => {
|
||||
setKeepAliveCountdown((prev) => {
|
||||
if (prev <= 1) {
|
||||
clearInterval(keepAliveInterval);
|
||||
return 0;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(revealNextChar, 100);
|
||||
};
|
||||
|
||||
checkSplashHidden();
|
||||
});
|
||||
|
||||
const getCursorClass = () => {
|
||||
if (isDelaying()) return "cursor-block"; // Blinking block during delay
|
||||
if (isTyping()) return "cursor-typing"; // Thin line while typing
|
||||
|
||||
// After typing is done
|
||||
if (typeof keepAlive === "number") {
|
||||
return keepAliveCountdown() > 0 ? "cursor-block" : "hidden";
|
||||
}
|
||||
return keepAlive ? "cursor-block" : "hidden";
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={containerRef} class={props.class}>
|
||||
{resolved()}
|
||||
<span ref={cursorRef} class={getCursorClass()}>
|
||||
{" "}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user