fixed docs v1
This commit is contained in:
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
260
client/src/pages/docs.tsx
Normal 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 library—to
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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,9 +139,10 @@ 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 * 6.67
|
lifeRatio < 0.15
|
||||||
: lifeRatio > 0.7
|
? lifeRatio * 6.67
|
||||||
|
: lifeRatio > 0.7
|
||||||
? (1 - lifeRatio) * 3.33
|
? (1 - lifeRatio) * 3.33
|
||||||
: 1;
|
: 1;
|
||||||
|
|
||||||
@@ -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
|
<div className="flex flex-col items-center justify-center min-h-screen p-4 sm:p-6">
|
||||||
className="relative z-10 flex items-center gap-4 px-6 py-4 rounded-xl border border-border/30 backdrop-blur-sm"
|
<div className="flex-1 flex items-center justify-center w-full max-w-[90vw] md:max-w-none">
|
||||||
style={{ backgroundColor: "rgba(49, 50, 68, 0.8)" }}
|
<div
|
||||||
data-testid="command-box"
|
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)" }}
|
||||||
<span className="text-muted-foreground mr-1" data-testid="text-prompt">$</span>
|
data-testid="command-box"
|
||||||
<code
|
>
|
||||||
className="text-lg md:text-xl font-mono"
|
<div className="flex items-center gap-2 md:gap-4 flex-1 md:flex-none justify-center md:justify-start">
|
||||||
style={{ color: "#a6e3a1" }}
|
<span
|
||||||
data-testid="text-ssh-command"
|
className="text-muted-foreground shrink-0"
|
||||||
>
|
data-testid="text-prompt"
|
||||||
{SSH_COMMAND}
|
>
|
||||||
</code>
|
$
|
||||||
<Button
|
</span>
|
||||||
size="icon"
|
<code
|
||||||
variant="ghost"
|
className="text-sm sm:text-base md:text-lg lg:text-xl font-mono truncate"
|
||||||
onClick={handleCopy}
|
style={{ color: "#a6e3a1" }}
|
||||||
className={`ml-2 text-muted-foreground transition-transform duration-150 ${isAnimating ? "scale-90" : "scale-100"}`}
|
data-testid="text-ssh-command"
|
||||||
aria-label={copied ? "Copied to clipboard" : "Copy SSH command"}
|
>
|
||||||
data-testid="button-copy"
|
{SSH_COMMAND}
|
||||||
>
|
</code>
|
||||||
{copied ? (
|
</div>
|
||||||
<Check className="h-5 w-5" style={{ color: "#a6e3a1" }} />
|
<Button
|
||||||
) : (
|
size="icon"
|
||||||
<Copy className="h-5 w-5" />
|
variant="ghost"
|
||||||
)}
|
onClick={handleCopy}
|
||||||
</Button>
|
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-4 w-4 md:h-5 md:w-5"
|
||||||
|
style={{ color: "#a6e3a1" }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<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>
|
</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>
|
||||||
|
|||||||
Reference in New Issue
Block a user