Compare commits
2 Commits
5283936705
...
1d369291e2
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d369291e2 | |||
| 999fc08ffe |
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" />
|
||||
<title>Keshav Anand - Portfolio</title>
|
||||
<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.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">
|
||||
|
||||
@@ -4,13 +4,14 @@ import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import Home from "@/pages/home";
|
||||
import NotFound from "@/pages/not-found";
|
||||
|
||||
import Docs from "@/pages/docs";
|
||||
|
||||
function Router() {
|
||||
return (
|
||||
<Switch>
|
||||
<Route path="/" component={Home} />
|
||||
<Route component={NotFound} />
|
||||
<Route path="/docs" component={Docs} />
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
|
||||
/* Catppuccin Mocha Theme - Dark Only */
|
||||
:root {
|
||||
--button-outline: rgba(255,255,255, .10);
|
||||
--badge-outline: rgba(255,255,255, .05);
|
||||
--button-outline: rgba(255, 255, 255, 0.1);
|
||||
--badge-outline: rgba(255, 255, 255, 0.05);
|
||||
--opaque-button-border-intensity: 9;
|
||||
--elevate-1: rgba(255,255,255, .04);
|
||||
--elevate-2: rgba(255,255,255, .09);
|
||||
--elevate-1: rgba(255, 255, 255, 0.04);
|
||||
--elevate-2: rgba(255, 255, 255, 0.09);
|
||||
--background: 240 21% 15%;
|
||||
--foreground: 226 64% 88%;
|
||||
--border: 240 17% 20%;
|
||||
@@ -43,51 +43,75 @@
|
||||
--chart-3: 197 37% 62%;
|
||||
--chart-4: 43 74% 68%;
|
||||
--chart-5: 27 87% 70%;
|
||||
--font-sans: 'JetBrains Mono', monospace;
|
||||
--font-sans: "JetBrains Mono", monospace;
|
||||
--font-serif: Georgia, serif;
|
||||
--font-mono: 'JetBrains Mono', monospace;
|
||||
--radius: .5rem;
|
||||
--shadow-2xs: 0px 2px 0px 0px hsl(0 0% 0% / 0.00);
|
||||
--shadow-xs: 0px 2px 0px 0px hsl(0 0% 0% / 0.00);
|
||||
--shadow-sm: 0px 2px 0px 0px hsl(0 0% 0% / 0.00), 0px 1px 2px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow: 0px 2px 0px 0px hsl(0 0% 0% / 0.00), 0px 1px 2px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow-md: 0px 2px 0px 0px hsl(0 0% 0% / 0.00), 0px 2px 4px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow-lg: 0px 2px 0px 0px hsl(0 0% 0% / 0.00), 0px 4px 6px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow-xl: 0px 2px 0px 0px hsl(0 0% 0% / 0.00), 0px 8px 10px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow-2xl: 0px 2px 0px 0px hsl(0 0% 0% / 0.00);
|
||||
--font-mono: "JetBrains Mono", monospace;
|
||||
--radius: 0.5rem;
|
||||
--shadow-2xs: 0px 2px 0px 0px hsl(0 0% 0% / 0);
|
||||
--shadow-xs: 0px 2px 0px 0px hsl(0 0% 0% / 0);
|
||||
--shadow-sm: 0px 2px 0px 0px hsl(0 0% 0% / 0),
|
||||
0px 1px 2px -1px hsl(0 0% 0% / 0);
|
||||
--shadow: 0px 2px 0px 0px hsl(0 0% 0% / 0), 0px 1px 2px -1px hsl(0 0% 0% / 0);
|
||||
--shadow-md: 0px 2px 0px 0px hsl(0 0% 0% / 0),
|
||||
0px 2px 4px -1px hsl(0 0% 0% / 0);
|
||||
--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;
|
||||
--spacing: 0.25rem;
|
||||
|
||||
/* Fallback for older browsers */
|
||||
/* Fallback for older browsers */
|
||||
--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 */
|
||||
--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 */
|
||||
--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 */
|
||||
--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 */
|
||||
--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 */
|
||||
--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 */
|
||||
--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 {
|
||||
* {
|
||||
@apply border-border;
|
||||
@@ -119,7 +143,6 @@
|
||||
* need to be distinguished from eachother visually.
|
||||
*/
|
||||
@layer utilities {
|
||||
|
||||
/* Hide ugly search cancel button in Chrome until we can style it properly */
|
||||
input[type="search"]::-webkit-search-cancel-button {
|
||||
@apply hidden;
|
||||
@@ -135,10 +158,11 @@
|
||||
/* .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
|
||||
* and program their own. */
|
||||
.no-default-hover-elevate {}
|
||||
|
||||
.no-default-active-elevate {}
|
||||
.no-default-hover-elevate {
|
||||
}
|
||||
|
||||
.no-default-active-elevate {
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggleable backgrounds go behind the content. Hoverable/active goes on top.
|
||||
|
||||
259
client/src/pages/docs.tsx
Normal file
259
client/src/pages/docs.tsx
Normal file
@@ -0,0 +1,259 @@
|
||||
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">
|
||||
<AnimatedSection title="What is it?" delay={200}>
|
||||
<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">
|
||||
Terminal 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>
|
||||
</AnimatedSection>
|
||||
</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">
|
||||
The who, what, when, where, why
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<CommandReference />
|
||||
|
||||
<div className="space-y-12 md:space-y-20">
|
||||
<AnimatedSection title="What is it?" delay={500}>
|
||||
<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, <strong>secure</strong>, and purely text-based
|
||||
interface.{" "}
|
||||
<span className="text-foreground font-bold">
|
||||
It is completely harmless to your system.
|
||||
</span>{" "}
|
||||
</p>
|
||||
</AnimatedSection>
|
||||
|
||||
<AnimatedSection title="Command Breakdown" delay={800}>
|
||||
<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 on my homeserver meant 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 IP (gateway).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AnimatedSection>
|
||||
|
||||
<AnimatedSection title="How to Run" delay={1100}>
|
||||
<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-[16px]">
|
||||
Ctrl + C
|
||||
</kbd>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</AnimatedSection>
|
||||
|
||||
<AnimatedSection title="Code Logic" delay={1400}>
|
||||
<div className="space-y-6">
|
||||
<p className="text-muted-foreground leading-relaxed text-sm md:text-base">
|
||||
This command gives authenticated access to the{" "}
|
||||
<span className="text-foreground font-bold">'portfolio'</span>{" "}
|
||||
user account on my private home server.
|
||||
</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 library—to
|
||||
handle real-time rendering and input.
|
||||
</p>
|
||||
</div>
|
||||
</AnimatedSection>
|
||||
|
||||
<AnimatedSection title="Resources" delay={1700}>
|
||||
<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-[12px] text-muted-foreground/60 text-center">
|
||||
Note: Web shell is a simulated environment, and native terminal is
|
||||
recommended
|
||||
</p>
|
||||
</AnimatedSection>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,8 @@ import { Button } from "@/components/ui/button";
|
||||
|
||||
const SSH_COMMAND = "ssh portfolio@keshavanand.net";
|
||||
|
||||
const CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%^&*()_+-=[]{}|;:,.<>?/\\~`";
|
||||
const CHARACTERS =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%^&*()_+-=[]{}|;:,.<>?/\\~`";
|
||||
|
||||
interface Particle {
|
||||
x: number;
|
||||
@@ -24,7 +25,10 @@ interface Particle {
|
||||
function ParticleBackground() {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
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 frameCountRef = useRef(0);
|
||||
|
||||
@@ -52,16 +56,24 @@ function ParticleBackground() {
|
||||
"#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 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 spread = isAmbient ? 200 : 120;
|
||||
return {
|
||||
x: x + (Math.random() - 0.5) * spread,
|
||||
y: y + (Math.random() - 0.5) * spread,
|
||||
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,
|
||||
targetY: y,
|
||||
vx: Math.cos(angle) * speed,
|
||||
@@ -89,13 +101,19 @@ function ParticleBackground() {
|
||||
|
||||
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 my = mouseRef.current.y;
|
||||
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 ry = Math.random() * canvas.height;
|
||||
particlesRef.current.push(createParticle(rx, ry, true));
|
||||
@@ -121,7 +139,8 @@ function ParticleBackground() {
|
||||
p.y += p.vy;
|
||||
|
||||
const lifeRatio = p.life / p.maxLife;
|
||||
const fadeOpacity = lifeRatio < 0.15
|
||||
const fadeOpacity =
|
||||
lifeRatio < 0.15
|
||||
? lifeRatio * 6.67
|
||||
: lifeRatio > 0.7
|
||||
? (1 - lifeRatio) * 3.33
|
||||
@@ -169,6 +188,14 @@ function ParticleBackground() {
|
||||
function CommandBox() {
|
||||
const [copied, setCopied] = 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 () => {
|
||||
try {
|
||||
@@ -183,40 +210,71 @@ function CommandBox() {
|
||||
}, []);
|
||||
|
||||
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
|
||||
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)" }}
|
||||
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
|
||||
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" }}
|
||||
data-testid="text-ssh-command"
|
||||
>
|
||||
{SSH_COMMAND}
|
||||
</code>
|
||||
</div>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
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"}
|
||||
data-testid="button-copy"
|
||||
>
|
||||
{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>
|
||||
</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() {
|
||||
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 />
|
||||
<CommandBox />
|
||||
</div>
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className="min-h-screen w-full flex items-center justify-center bg-gray-50">
|
||||
<Card className="w-full max-w-md mx-4">
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex mb-4 gap-2">
|
||||
<AlertCircle className="h-8 w-8 text-red-500" />
|
||||
<h1 className="text-2xl font-bold text-gray-900">404 Page Not Found</h1>
|
||||
</div>
|
||||
|
||||
<p className="mt-4 text-sm text-gray-600">
|
||||
Did you forget to add the page to the router?
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user