diff --git a/client/index.html b/client/index.html index 6c1f5e6..de64ebb 100644 --- a/client/index.html +++ b/client/index.html @@ -5,7 +5,7 @@ Keshav Anand - Portfolio - + diff --git a/client/src/App.tsx b/client/src/App.tsx index 8a6f3b6..4144d70 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -6,10 +6,27 @@ 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 ( + + +
+
+ 404: Web Shell Not Found +
+

+ The web-based shell environment is currently unavailable. Please use + your native terminal for the full experience. +

+ + $ exit + +
+
); diff --git a/client/src/index.css b/client/src/index.css index 91199fe..b435d8c 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -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. diff --git a/client/src/pages/docs.tsx b/client/src/pages/docs.tsx new file mode 100644 index 0000000..592a953 --- /dev/null +++ b/client/src/pages/docs.tsx @@ -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 ( +
+

+ + {title} +

+
{children}
+
+ ); +} + +function CommandReference() { + const [copied, setCopied] = useState(false); + const handleCopy = async () => { + await navigator.clipboard.writeText(SSH_COMMAND); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+
+ + + Reference Command + +
+
+ + {SSH_COMMAND} + + +
+
+ ); +} + +export default function Docs() { + return ( +
+
+
+ + $ cd .. + +

+ Documentation +

+

+ Technical architecture and connection manual. +

+
+ + + +
+ +

+ 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. +

+
+ + +
+
+
+ ssh portfolio@keshavanand.net +
+ +
+
+
+ ssh +
+

+ + Secure Shell: + {" "} + The standard terminal protocol for logging into and + controlling remote computers safely. +

+
+ +
+
+ portfolio +
+

+ User: A + passwordless, restricted user profile specifically + provisioned for public access. +

+
+ +
+
+ keshavanand.net +
+

+ Domain:{" "} + Points to a web record that directs your request to my + server's public gateway. +

+
+
+
+
+
+ + +
+
+ 01 +

Copy the command above using the clipboard tool.

+
+
+ 02 +

+ Open your native terminal (Terminal on Mac/Linux, PowerShell + on Windows). +

+
+
+ 03 +

+ Paste the command and accept the host identity by typing 'yes' + if prompted. +

+
+
+ 04 +

Press Enter to launch the interactive environment.

+
+
+ 05 +

+ Terminate the process at any time by pressing{" "} + + Ctrl + C + + . +

+
+
+
+ + +
+

+ This command facilitates authenticated access to the{" "} + 'portfolio'{" "} + user account on my private home infrastructure. No password is + required, as the user environment is strictly isolated. +

+
+

+ "The portfolio user is a specialized account with no + system-level permissions and a modified shell." +

+

+ Instead of a standard bash or zsh shell, the user session + triggers a custom-coded{" "} + + C++ executable + + . This binary utilizes{" "} + FTXUI—a + sophisticated functional terminal user interface library—to + handle real-time rendering and input. +

+
+
+
+ + + +

+ Note: Web shell is a simulated environment. Native terminal is + recommended for optimal performance. +

+
+
+
+
+ ); +} diff --git a/client/src/pages/home.tsx b/client/src/pages/home.tsx index 7d1dd99..43e8f40 100644 --- a/client/src/pages/home.tsx +++ b/client/src/pages/home.tsx @@ -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(null); const particlesRef = useRef([]); - 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(); 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, @@ -75,7 +87,7 @@ function ParticleBackground() { const handleMouseMove = (e: MouseEvent) => { mouseRef.current = { x: e.clientX, y: e.clientY }; - + for (let i = 0; i < 5; i++) { if (particlesRef.current.length < 400) { particlesRef.current.push(createParticle(e.clientX, e.clientY)); @@ -86,16 +98,22 @@ function ParticleBackground() { const animate = () => { ctx.fillStyle = "#000000"; ctx.fillRect(0, 0, canvas.width, canvas.height); - + 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)); @@ -103,33 +121,34 @@ function ParticleBackground() { particlesRef.current = particlesRef.current.filter((p) => { p.life++; - + const dx = mouseRef.current.x - p.x; const dy = mouseRef.current.y - p.y; const distance = Math.sqrt(dx * dx + dy * dy) || 1; - + if (distance < 200) { const force = (200 - distance) / 200; p.vx += (dx / distance) * force * 0.4; p.vy += (dy / distance) * force * 0.4; } - + p.vx *= 0.97; p.vy *= 0.97; - + p.x += p.vx; p.y += p.vy; - + const lifeRatio = p.life / p.maxLife; - const fadeOpacity = lifeRatio < 0.15 - ? lifeRatio * 6.67 - : lifeRatio > 0.7 - ? (1 - lifeRatio) * 3.33 + const fadeOpacity = + lifeRatio < 0.15 + ? lifeRatio * 6.67 + : lifeRatio > 0.7 + ? (1 - lifeRatio) * 3.33 : 1; - + const distanceOpacity = Math.max(0.15, 1 - distance / 400); const finalOpacity = p.opacity * fadeOpacity * distanceOpacity; - + if (finalOpacity > 0.01) { ctx.font = `${p.size}px 'JetBrains Mono', monospace`; ctx.fillStyle = p.color; @@ -137,7 +156,7 @@ function ParticleBackground() { ctx.fillText(p.char, p.x, p.y); ctx.globalAlpha = 1; } - + return p.life < p.maxLife; }); @@ -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 ( -
- $ - - {SSH_COMMAND} - - +
+
+
+
+ + $ + + + {SSH_COMMAND} + +
+ +
+
+ +
); } export default function Home() { return ( -
+