fixed docs v1

This commit is contained in:
2026-01-17 23:47:09 -06:00
parent 5283936705
commit 999fc08ffe
5 changed files with 439 additions and 80 deletions

View File

@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" />
<title>Keshav Anand - Portfolio</title> <title>Keshav Anand - Portfolio</title>
<meta name="description" content="Connect with Keshav Anand via SSH. A minimalist portfolio experience." /> <meta name="description" content="Connect with Keshav Anand via SSH. A minimalist portfolio experience." />
<link rel="icon" type="image/png" href="/favicon.webp"> <link rel="icon" type="image/png" href="/favicon.png" />
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Architects+Daughter&family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&family=Fira+Code:wght@300..700&family=Geist+Mono:wght@100..900&family=Geist:wght@100..900&family=IBM+Plex+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&family=IBM+Plex+Sans:ital,wght@0,100..700;1,100..700&family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&family=Lora:ital,wght@0,400..700;1,400..700&family=Merriweather:ital,opsz,wght@0,18..144,300..900;1,18..144,300..900&family=Montserrat:ital,wght@0,100..900;1,100..900&family=Open+Sans:ital,wght@0,300..800;1,300..800&family=Outfit:wght@100..900&family=Oxanium:wght@200..800&family=Playfair+Display:ital,wght@0,400..900;1,400..900&family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Roboto:ital,wght@0,100..900;1,100..900&family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&family=Space+Grotesk:wght@300..700&family=Space+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Architects+Daughter&family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&family=Fira+Code:wght@300..700&family=Geist+Mono:wght@100..900&family=Geist:wght@100..900&family=IBM+Plex+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&family=IBM+Plex+Sans:ital,wght@0,100..700;1,100..700&family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&family=Lora:ital,wght@0,400..700;1,400..700&family=Merriweather:ital,opsz,wght@0,18..144,300..900;1,18..144,300..900&family=Montserrat:ital,wght@0,100..900;1,100..900&family=Open+Sans:ital,wght@0,300..800;1,300..800&family=Outfit:wght@100..900&family=Oxanium:wght@200..800&family=Playfair+Display:ital,wght@0,400..900;1,400..900&family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Roboto:ital,wght@0,100..900;1,100..900&family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&family=Space+Grotesk:wght@300..700&family=Space+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet">

View File

@@ -6,10 +6,27 @@ import { TooltipProvider } from "@/components/ui/tooltip";
import Home from "@/pages/home"; import Home from "@/pages/home";
import NotFound from "@/pages/not-found"; import NotFound from "@/pages/not-found";
import Docs from "@/pages/docs";
function Router() { function Router() {
return ( return (
<Switch> <Switch>
<Route path="/" component={Home} /> <Route path="/" component={Home} />
<Route path="/docs" component={Docs} />
<Route path="/webshell">
<div className="min-h-screen bg-black flex flex-col items-center justify-center p-8 font-mono">
<div className="text-destructive text-4xl mb-4">
404: Web Shell Not Found
</div>
<p className="text-muted-foreground text-center max-w-md">
The web-based shell environment is currently unavailable. Please use
your native terminal for the full experience.
</p>
<a href="/" className="mt-8 text-primary hover:underline">
$ exit
</a>
</div>
</Route>
<Route component={NotFound} /> <Route component={NotFound} />
</Switch> </Switch>
); );

View File

@@ -4,11 +4,11 @@
/* Catppuccin Mocha Theme - Dark Only */ /* Catppuccin Mocha Theme - Dark Only */
:root { :root {
--button-outline: rgba(255,255,255, .10); --button-outline: rgba(255, 255, 255, 0.1);
--badge-outline: rgba(255,255,255, .05); --badge-outline: rgba(255, 255, 255, 0.05);
--opaque-button-border-intensity: 9; --opaque-button-border-intensity: 9;
--elevate-1: rgba(255,255,255, .04); --elevate-1: rgba(255, 255, 255, 0.04);
--elevate-2: rgba(255,255,255, .09); --elevate-2: rgba(255, 255, 255, 0.09);
--background: 240 21% 15%; --background: 240 21% 15%;
--foreground: 226 64% 88%; --foreground: 226 64% 88%;
--border: 240 17% 20%; --border: 240 17% 20%;
@@ -43,51 +43,75 @@
--chart-3: 197 37% 62%; --chart-3: 197 37% 62%;
--chart-4: 43 74% 68%; --chart-4: 43 74% 68%;
--chart-5: 27 87% 70%; --chart-5: 27 87% 70%;
--font-sans: 'JetBrains Mono', monospace; --font-sans: "JetBrains Mono", monospace;
--font-serif: Georgia, serif; --font-serif: Georgia, serif;
--font-mono: 'JetBrains Mono', monospace; --font-mono: "JetBrains Mono", monospace;
--radius: .5rem; --radius: 0.5rem;
--shadow-2xs: 0px 2px 0px 0px hsl(0 0% 0% / 0.00); --shadow-2xs: 0px 2px 0px 0px hsl(0 0% 0% / 0);
--shadow-xs: 0px 2px 0px 0px hsl(0 0% 0% / 0.00); --shadow-xs: 0px 2px 0px 0px hsl(0 0% 0% / 0);
--shadow-sm: 0px 2px 0px 0px hsl(0 0% 0% / 0.00), 0px 1px 2px -1px hsl(0 0% 0% / 0.00); --shadow-sm: 0px 2px 0px 0px hsl(0 0% 0% / 0),
--shadow: 0px 2px 0px 0px hsl(0 0% 0% / 0.00), 0px 1px 2px -1px hsl(0 0% 0% / 0.00); 0px 1px 2px -1px hsl(0 0% 0% / 0);
--shadow-md: 0px 2px 0px 0px hsl(0 0% 0% / 0.00), 0px 2px 4px -1px hsl(0 0% 0% / 0.00); --shadow: 0px 2px 0px 0px hsl(0 0% 0% / 0), 0px 1px 2px -1px hsl(0 0% 0% / 0);
--shadow-lg: 0px 2px 0px 0px hsl(0 0% 0% / 0.00), 0px 4px 6px -1px hsl(0 0% 0% / 0.00); --shadow-md: 0px 2px 0px 0px hsl(0 0% 0% / 0),
--shadow-xl: 0px 2px 0px 0px hsl(0 0% 0% / 0.00), 0px 8px 10px -1px hsl(0 0% 0% / 0.00); 0px 2px 4px -1px hsl(0 0% 0% / 0);
--shadow-2xl: 0px 2px 0px 0px hsl(0 0% 0% / 0.00); --shadow-lg: 0px 2px 0px 0px hsl(0 0% 0% / 0),
0px 4px 6px -1px hsl(0 0% 0% / 0);
--shadow-xl: 0px 2px 0px 0px hsl(0 0% 0% / 0),
0px 8px 10px -1px hsl(0 0% 0% / 0);
--shadow-2xl: 0px 2px 0px 0px hsl(0 0% 0% / 0);
--tracking-normal: 0em; --tracking-normal: 0em;
--spacing: 0.25rem; --spacing: 0.25rem;
/* Fallback for older browsers */ /* Fallback for older browsers */
--sidebar-primary-border: hsl(var(--sidebar-primary)); --sidebar-primary-border: hsl(var(--sidebar-primary));
--sidebar-primary-border: hsl(from hsl(var(--sidebar-primary)) h s calc(l + var(--opaque-button-border-intensity)) / alpha); --sidebar-primary-border: hsl(
from hsl(var(--sidebar-primary)) h s
calc(l + var(--opaque-button-border-intensity)) / alpha
);
/* Fallback for older browsers */ /* Fallback for older browsers */
--sidebar-accent-border: hsl(var(--sidebar-accent)); --sidebar-accent-border: hsl(var(--sidebar-accent));
--sidebar-accent-border: hsl(from hsl(var(--sidebar-accent)) h s calc(l + var(--opaque-button-border-intensity)) / alpha); --sidebar-accent-border: hsl(
from hsl(var(--sidebar-accent)) h s
calc(l + var(--opaque-button-border-intensity)) / alpha
);
/* Fallback for older browsers */ /* Fallback for older browsers */
--primary-border: hsl(var(--primary)); --primary-border: hsl(var(--primary));
--primary-border: hsl(from hsl(var(--primary)) h s calc(l + var(--opaque-button-border-intensity)) / alpha); --primary-border: hsl(
from hsl(var(--primary)) h s calc(l + var(--opaque-button-border-intensity)) /
alpha
);
/* Fallback for older browsers */ /* Fallback for older browsers */
--secondary-border: hsl(var(--secondary)); --secondary-border: hsl(var(--secondary));
--secondary-border: hsl(from hsl(var(--secondary)) h s calc(l + var(--opaque-button-border-intensity)) / alpha); --secondary-border: hsl(
from hsl(var(--secondary)) h s
calc(l + var(--opaque-button-border-intensity)) / alpha
);
/* Fallback for older browsers */ /* Fallback for older browsers */
--muted-border: hsl(var(--muted)); --muted-border: hsl(var(--muted));
--muted-border: hsl(from hsl(var(--muted)) h s calc(l + var(--opaque-button-border-intensity)) / alpha); --muted-border: hsl(
from hsl(var(--muted)) h s calc(l + var(--opaque-button-border-intensity)) /
alpha
);
/* Fallback for older browsers */ /* Fallback for older browsers */
--accent-border: hsl(var(--accent)); --accent-border: hsl(var(--accent));
--accent-border: hsl(from hsl(var(--accent)) h s calc(l + var(--opaque-button-border-intensity)) / alpha); --accent-border: hsl(
from hsl(var(--accent)) h s calc(l + var(--opaque-button-border-intensity)) /
alpha
);
/* Fallback for older browsers */ /* Fallback for older browsers */
--destructive-border: hsl(var(--destructive)); --destructive-border: hsl(var(--destructive));
--destructive-border: hsl(from hsl(var(--destructive)) h s calc(l + var(--opaque-button-border-intensity)) / alpha); --destructive-border: hsl(
from hsl(var(--destructive)) h s
calc(l + var(--opaque-button-border-intensity)) / alpha
);
} }
@layer base { @layer base {
* { * {
@apply border-border; @apply border-border;
@@ -119,7 +143,6 @@
* need to be distinguished from eachother visually. * need to be distinguished from eachother visually.
*/ */
@layer utilities { @layer utilities {
/* Hide ugly search cancel button in Chrome until we can style it properly */ /* Hide ugly search cancel button in Chrome until we can style it properly */
input[type="search"]::-webkit-search-cancel-button { input[type="search"]::-webkit-search-cancel-button {
@apply hidden; @apply hidden;
@@ -135,10 +158,11 @@
/* .no-default-hover-elevate/no-default-active-elevate is an escape hatch so consumers of /* .no-default-hover-elevate/no-default-active-elevate is an escape hatch so consumers of
* buttons/badges can remove the automatic brightness adjustment on interactions * buttons/badges can remove the automatic brightness adjustment on interactions
* and program their own. */ * and program their own. */
.no-default-hover-elevate {} .no-default-hover-elevate {
}
.no-default-active-elevate {}
.no-default-active-elevate {
}
/** /**
* Toggleable backgrounds go behind the content. Hoverable/active goes on top. * Toggleable backgrounds go behind the content. Hoverable/active goes on top.

260
client/src/pages/docs.tsx Normal file
View File

@@ -0,0 +1,260 @@
import { useEffect, useState, useRef } from "react";
import {
Copy,
Check,
ArrowRight,
Terminal,
Github,
ExternalLink,
ChevronRight,
} from "lucide-react";
import { Button } from "@/components/ui/button";
const SSH_COMMAND = "ssh portfolio@keshavanand.net";
interface SectionProps {
title: string;
children: React.ReactNode;
delay?: number;
}
function AnimatedSection({ title, children, delay = 0 }: SectionProps) {
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const timer = setTimeout(() => setIsVisible(true), delay);
return () => clearTimeout(timer);
}, [delay]);
return (
<div
className={`transition-all duration-1000 transform ${
isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-10"
} mb-16 last:mb-0`}
>
<h2 className="text-primary text-xl font-mono mb-6 flex items-center gap-2">
<ChevronRight className="w-5 h-5" />
{title}
</h2>
<div className="pl-7 border-l border-border/20">{children}</div>
</div>
);
}
function CommandReference() {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
await navigator.clipboard.writeText(SSH_COMMAND);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div className="relative group p-6 rounded-xl border border-border/30 bg-surface/40 backdrop-blur-sm mb-12">
<div className="flex items-center gap-4 mb-4">
<Terminal className="w-5 h-5 text-primary" />
<span className="text-xs text-muted-foreground uppercase tracking-widest font-mono">
Reference Command
</span>
</div>
<div className="flex items-center justify-between gap-4">
<code className="text-lg md:text-xl font-mono text-primary break-all">
{SSH_COMMAND}
</code>
<Button
size="icon"
variant="ghost"
onClick={handleCopy}
className="shrink-0 text-muted-foreground hover:text-primary"
>
{copied ? (
<Check className="h-5 w-5 text-primary" />
) : (
<Copy className="h-5 w-5" />
)}
</Button>
</div>
</div>
);
}
export default function Docs() {
return (
<div className="min-h-screen bg-black text-foreground font-mono selection:bg-primary/30">
<div className="max-w-3xl mx-auto px-6 py-20 md:py-32">
<div className="mb-20 text-center md:text-left">
<a
href="/"
className="inline-block text-muted-foreground hover:text-primary mb-8 transition-colors"
>
$ cd ..
</a>
<h1 className="text-4xl md:text-5xl font-bold tracking-tight mb-4">
Documentation
</h1>
<p className="text-muted-foreground">
Technical architecture and connection manual.
</p>
</div>
<CommandReference />
<div className="space-y-12 md:space-y-20">
<AnimatedSection title="What is it?" delay={200}>
<p className="text-muted-foreground leading-relaxed text-sm md:text-base">
When executed in your terminal, this command renders a fully
interactive shell portfolio experience. Much like a digital resume
or personal site, it allows you to explore my work and background
through a safe, secure, and purely text-based interface. It is
completely harmless to your system.
</p>
</AnimatedSection>
<AnimatedSection title="Command Breakdown" delay={400}>
<div className="space-y-8">
<div className="relative p-4 md:p-6 bg-surface/20 rounded-lg border border-border/10">
<div className="text-primary text-base md:text-xl mb-6 font-mono text-center tracking-widest break-all">
ssh portfolio@keshavanand.net
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6">
<div className="space-y-2">
<div className="text-primary text-sm flex items-center gap-1">
ssh <ArrowRight className="w-3 h-3" />
</div>
<p className="text-[11px] md:text-xs text-muted-foreground leading-relaxed">
<span className="text-foreground font-bold">
Secure Shell:
</span>{" "}
The standard terminal protocol for logging into and
controlling remote computers safely.
</p>
</div>
<div className="space-y-2">
<div className="text-primary text-sm flex items-center gap-1">
portfolio <ArrowRight className="w-3 h-3" />
</div>
<p className="text-[11px] md:text-xs text-muted-foreground leading-relaxed">
<span className="text-foreground font-bold">User:</span> A
passwordless, restricted user profile specifically
provisioned for public access.
</p>
</div>
<div className="space-y-2">
<div className="text-primary text-sm flex items-center gap-1">
keshavanand.net <ArrowRight className="w-3 h-3" />
</div>
<p className="text-[11px] md:text-xs text-muted-foreground leading-relaxed">
<span className="text-foreground font-bold">Domain:</span>{" "}
Points to a web record that directs your request to my
server's public gateway.
</p>
</div>
</div>
</div>
</div>
</AnimatedSection>
<AnimatedSection title="Execution Guide" delay={600}>
<div className="space-y-4 text-muted-foreground text-sm md:text-base">
<div className="flex gap-4">
<span className="text-primary shrink-0">01</span>
<p>Copy the command above using the clipboard tool.</p>
</div>
<div className="flex gap-4">
<span className="text-primary shrink-0">02</span>
<p>
Open your native terminal (Terminal on Mac/Linux, PowerShell
on Windows).
</p>
</div>
<div className="flex gap-4">
<span className="text-primary shrink-0">03</span>
<p>
Paste the command and accept the host identity by typing 'yes'
if prompted.
</p>
</div>
<div className="flex gap-4">
<span className="text-primary shrink-0">04</span>
<p>Press Enter to launch the interactive environment.</p>
</div>
<div className="flex gap-4">
<span className="text-primary shrink-0">05</span>
<p>
Terminate the process at any time by pressing{" "}
<kbd className="px-1 py-0.5 rounded bg-surface/80 border border-border/30 text-foreground text-[10px]">
Ctrl + C
</kbd>
.
</p>
</div>
</div>
</AnimatedSection>
<AnimatedSection title="Technical Summary" delay={800}>
<div className="space-y-6">
<p className="text-muted-foreground leading-relaxed text-sm md:text-base">
This command facilitates authenticated access to the{" "}
<span className="text-foreground font-bold">'portfolio'</span>{" "}
user account on my private home infrastructure. No password is
required, as the user environment is strictly isolated.
</p>
<div className="p-4 md:p-6 bg-surface/20 rounded-lg border border-border/10 space-y-4">
<p className="text-xs md:text-sm text-muted-foreground italic">
"The portfolio user is a specialized account with no
system-level permissions and a modified shell."
</p>
<p className="text-muted-foreground leading-relaxed text-sm md:text-base">
Instead of a standard bash or zsh shell, the user session
triggers a custom-coded{" "}
<span className="text-foreground font-bold">
C++ executable
</span>
. This binary utilizes{" "}
<span className="text-foreground font-bold">FTXUI</span>a
sophisticated functional terminal user interface libraryto
handle real-time rendering and input.
</p>
</div>
</div>
</AnimatedSection>
<AnimatedSection title="Resources" delay={1000}>
<div className="flex flex-col md:flex-row gap-4">
<a
href="https://git.keshavanand.net/KeshavAnandCode/terminal-portfolio"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-between p-4 rounded-lg border border-border/20 bg-surface/10 hover:bg-surface/20 hover:border-primary/40 transition-all group flex-1"
>
<div className="flex items-center gap-3">
<Github className="w-5 h-5 text-primary" />
<span className="text-sm">View Source Code</span>
</div>
<ExternalLink className="w-4 h-4 opacity-0 group-hover:opacity-100 transition-opacity" />
</a>
<a
href="/webshell"
className="flex items-center justify-between p-4 rounded-lg border border-border/20 bg-surface/10 hover:bg-surface/20 hover:border-primary/40 transition-all group flex-1"
>
<div className="flex items-center gap-3">
<Terminal className="w-5 h-5 text-primary" />
<span className="text-sm">Run Web Shell</span>
</div>
<ArrowRight className="w-4 h-4 opacity-0 group-hover:opacity-100 transition-opacity" />
</a>
</div>
<p className="mt-4 text-[10px] text-muted-foreground/60 text-center">
Note: Web shell is a simulated environment. Native terminal is
recommended for optimal performance.
</p>
</AnimatedSection>
</div>
</div>
</div>
);
}

View File

@@ -4,7 +4,8 @@ import { Button } from "@/components/ui/button";
const SSH_COMMAND = "ssh portfolio@keshavanand.net"; const SSH_COMMAND = "ssh portfolio@keshavanand.net";
const CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%^&*()_+-=[]{}|;:,.<>?/\\~`"; const CHARACTERS =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%^&*()_+-=[]{}|;:,.<>?/\\~`";
interface Particle { interface Particle {
x: number; x: number;
@@ -24,7 +25,10 @@ interface Particle {
function ParticleBackground() { function ParticleBackground() {
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
const particlesRef = useRef<Particle[]>([]); const particlesRef = useRef<Particle[]>([]);
const mouseRef = useRef({ x: window.innerWidth / 2, y: window.innerHeight / 2 }); const mouseRef = useRef({
x: window.innerWidth / 2,
y: window.innerHeight / 2,
});
const animationRef = useRef<number>(); const animationRef = useRef<number>();
const frameCountRef = useRef(0); const frameCountRef = useRef(0);
@@ -52,16 +56,24 @@ function ParticleBackground() {
"#cba6f7", "#cba6f7",
]; ];
const createParticle = (x: number, y: number, isAmbient = false): Particle => { const createParticle = (
x: number,
y: number,
isAmbient = false
): Particle => {
const angle = Math.random() * Math.PI * 2; const angle = Math.random() * Math.PI * 2;
const speed = isAmbient ? Math.random() * 0.5 + 0.2 : Math.random() * 2 + 1; const speed = isAmbient
? Math.random() * 0.5 + 0.2
: Math.random() * 2 + 1;
const color = colors[Math.floor(Math.random() * colors.length)]; const color = colors[Math.floor(Math.random() * colors.length)];
const spread = isAmbient ? 200 : 120; const spread = isAmbient ? 200 : 120;
return { return {
x: x + (Math.random() - 0.5) * spread, x: x + (Math.random() - 0.5) * spread,
y: y + (Math.random() - 0.5) * spread, y: y + (Math.random() - 0.5) * spread,
char: CHARACTERS[Math.floor(Math.random() * CHARACTERS.length)], char: CHARACTERS[Math.floor(Math.random() * CHARACTERS.length)],
opacity: isAmbient ? Math.random() * 0.4 + 0.1 : Math.random() * 0.8 + 0.2, opacity: isAmbient
? Math.random() * 0.4 + 0.1
: Math.random() * 0.8 + 0.2,
targetX: x, targetX: x,
targetY: y, targetY: y,
vx: Math.cos(angle) * speed, vx: Math.cos(angle) * speed,
@@ -89,13 +101,19 @@ function ParticleBackground() {
frameCountRef.current++; frameCountRef.current++;
if (frameCountRef.current % 3 === 0 && particlesRef.current.length < 400) { if (
frameCountRef.current % 3 === 0 &&
particlesRef.current.length < 400
) {
const mx = mouseRef.current.x; const mx = mouseRef.current.x;
const my = mouseRef.current.y; const my = mouseRef.current.y;
particlesRef.current.push(createParticle(mx, my, true)); particlesRef.current.push(createParticle(mx, my, true));
} }
if (frameCountRef.current % 8 === 0 && particlesRef.current.length < 400) { if (
frameCountRef.current % 8 === 0 &&
particlesRef.current.length < 400
) {
const rx = Math.random() * canvas.width; const rx = Math.random() * canvas.width;
const ry = Math.random() * canvas.height; const ry = Math.random() * canvas.height;
particlesRef.current.push(createParticle(rx, ry, true)); particlesRef.current.push(createParticle(rx, ry, true));
@@ -121,7 +139,8 @@ function ParticleBackground() {
p.y += p.vy; p.y += p.vy;
const lifeRatio = p.life / p.maxLife; const lifeRatio = p.life / p.maxLife;
const fadeOpacity = lifeRatio < 0.15 const fadeOpacity =
lifeRatio < 0.15
? lifeRatio * 6.67 ? lifeRatio * 6.67
: lifeRatio > 0.7 : lifeRatio > 0.7
? (1 - lifeRatio) * 3.33 ? (1 - lifeRatio) * 3.33
@@ -169,6 +188,14 @@ function ParticleBackground() {
function CommandBox() { function CommandBox() {
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const [isAnimating, setIsAnimating] = useState(false); const [isAnimating, setIsAnimating] = useState(false);
const [showHowItWorks, setShowHowItWorks] = useState(false);
useEffect(() => {
const timer = setTimeout(() => {
setShowHowItWorks(true);
}, 2000);
return () => clearTimeout(timer);
}, []);
const handleCopy = useCallback(async () => { const handleCopy = useCallback(async () => {
try { try {
@@ -183,40 +210,71 @@ function CommandBox() {
}, []); }, []);
return ( return (
<div className="flex flex-col items-center justify-center min-h-screen p-4 sm:p-6">
<div className="flex-1 flex items-center justify-center w-full max-w-[90vw] md:max-w-none">
<div <div
className="relative z-10 flex items-center gap-4 px-6 py-4 rounded-xl border border-border/30 backdrop-blur-sm" className="relative z-10 flex flex-wrap items-center justify-center md:justify-start gap-3 md:gap-4 px-4 py-3 md:px-6 md:py-4 rounded-xl border border-border/30 backdrop-blur-sm shadow-2xl w-full md:w-auto overflow-hidden"
style={{ backgroundColor: "rgba(49, 50, 68, 0.8)" }} style={{ backgroundColor: "rgba(49, 50, 68, 0.8)" }}
data-testid="command-box" data-testid="command-box"
> >
<span className="text-muted-foreground mr-1" data-testid="text-prompt">$</span> <div className="flex items-center gap-2 md:gap-4 flex-1 md:flex-none justify-center md:justify-start">
<span
className="text-muted-foreground shrink-0"
data-testid="text-prompt"
>
$
</span>
<code <code
className="text-lg md:text-xl font-mono" className="text-sm sm:text-base md:text-lg lg:text-xl font-mono truncate"
style={{ color: "#a6e3a1" }} style={{ color: "#a6e3a1" }}
data-testid="text-ssh-command" data-testid="text-ssh-command"
> >
{SSH_COMMAND} {SSH_COMMAND}
</code> </code>
</div>
<Button <Button
size="icon" size="icon"
variant="ghost" variant="ghost"
onClick={handleCopy} onClick={handleCopy}
className={`ml-2 text-muted-foreground transition-transform duration-150 ${isAnimating ? "scale-90" : "scale-100"}`} className={`shrink-0 text-muted-foreground transition-transform duration-150 ${
isAnimating ? "scale-90" : "scale-100"
}`}
aria-label={copied ? "Copied to clipboard" : "Copy SSH command"} aria-label={copied ? "Copied to clipboard" : "Copy SSH command"}
data-testid="button-copy" data-testid="button-copy"
> >
{copied ? ( {copied ? (
<Check className="h-5 w-5" style={{ color: "#a6e3a1" }} /> <Check
className="h-4 w-4 md:h-5 md:w-5"
style={{ color: "#a6e3a1" }}
/>
) : ( ) : (
<Copy className="h-5 w-5" /> <Copy className="h-4 w-4 md:h-5 md:w-5" />
)} )}
</Button> </Button>
</div> </div>
</div>
<div className="pb-12 md:pb-16">
<a
href="/docs"
className={`relative z-10 px-6 py-3 rounded-lg border border-border/20 backdrop-blur-md text-sm font-mono text-muted-foreground transition-all duration-1000 hover:border-primary/40 hover:text-primary hover-elevate ${
showHowItWorks
? "opacity-100 translate-y-0"
: "opacity-0 translate-y-8"
}`}
style={{ backgroundColor: "rgba(30, 30, 46, 0.4)" }}
data-testid="link-how-it-works"
>
How it Works
</a>
</div>
</div>
); );
} }
export default function Home() { export default function Home() {
return ( return (
<div className="min-h-screen w-full flex items-center justify-center overflow-hidden"> <div className="min-h-screen w-full relative overflow-hidden bg-black">
<ParticleBackground /> <ParticleBackground />
<CommandBox /> <CommandBox />
</div> </div>