diff --git a/client/index.html b/client/index.html index 5ac9c7f..de64ebb 100644 --- a/client/index.html +++ b/client/index.html @@ -3,6 +3,8 @@ + Keshav Anand - Portfolio + diff --git a/client/src/App.tsx b/client/src/App.tsx index b4c5b9e..8a6f3b6 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -3,14 +3,13 @@ import { queryClient } from "./lib/queryClient"; 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"; function Router() { return ( - {/* Add pages below */} - {/* */} - {/* Fallback to 404 */} + ); diff --git a/client/src/index.css b/client/src/index.css index c197613..91199fe 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -2,50 +2,50 @@ @tailwind components; @tailwind utilities; -/* LIGHT MODE */ +/* Catppuccin Mocha Theme - Dark Only */ :root { - --button-outline: rgba(0,0,0, .10); - --badge-outline: rgba(0,0,0, .05); - --opaque-button-border-intensity: -8; - --elevate-1: rgba(0,0,0, .03); - --elevate-2: rgba(0,0,0, .08); - --background: 0 0% 100%; - --foreground: 0 0% 9%; - --border: 0 0% 89%; - --card: 0 0% 98%; - --card-foreground: 0 0% 9%; - --card-border: 0 0% 94%; - --sidebar: 0 0% 96%; - --sidebar-foreground: 0 0% 9%; - --sidebar-border: 0 0% 92%; - --sidebar-primary: 115 54% 45%; - --sidebar-primary-foreground: 115 54% 98%; - --sidebar-accent: 0 0% 92%; - --sidebar-accent-foreground: 0 0% 9%; - --sidebar-ring: 115 54% 45%; - --popover: 0 0% 94%; - --popover-foreground: 0 0% 9%; - --popover-border: 0 0% 90%; - --primary: 115 54% 35%; - --primary-foreground: 115 54% 98%; - --secondary: 0 0% 90%; - --secondary-foreground: 0 0% 9%; - --muted: 0 0% 92%; - --muted-foreground: 0 0% 40%; - --accent: 115 15% 92%; - --accent-foreground: 115 15% 12%; - --destructive: 0 72% 42%; - --destructive-foreground: 0 72% 98%; - --input: 0 0% 75%; - --ring: 115 54% 45%; - --chart-1: 115 54% 35%; - --chart-2: 173 58% 32%; - --chart-3: 197 37% 38%; - --chart-4: 43 74% 42%; - --chart-5: 27 87% 48%; - --font-sans: Open Sans, sans-serif; + --button-outline: rgba(255,255,255, .10); + --badge-outline: rgba(255,255,255, .05); + --opaque-button-border-intensity: 9; + --elevate-1: rgba(255,255,255, .04); + --elevate-2: rgba(255,255,255, .09); + --background: 240 21% 15%; + --foreground: 226 64% 88%; + --border: 240 17% 20%; + --card: 240 21% 17%; + --card-foreground: 226 64% 88%; + --card-border: 240 17% 22%; + --sidebar: 240 21% 16%; + --sidebar-foreground: 226 64% 88%; + --sidebar-border: 240 17% 19%; + --sidebar-primary: 115 54% 76%; + --sidebar-primary-foreground: 240 21% 12%; + --sidebar-accent: 240 17% 19%; + --sidebar-accent-foreground: 226 64% 88%; + --sidebar-ring: 115 54% 76%; + --popover: 240 21% 18%; + --popover-foreground: 226 64% 88%; + --popover-border: 240 17% 21%; + --primary: 115 54% 76%; + --primary-foreground: 240 21% 12%; + --secondary: 240 17% 22%; + --secondary-foreground: 226 64% 88%; + --muted: 240 17% 21%; + --muted-foreground: 226 40% 65%; + --accent: 170 57% 73%; + --accent-foreground: 240 21% 12%; + --destructive: 0 62% 32%; + --destructive-foreground: 0 62% 96%; + --input: 240 17% 35%; + --ring: 115 54% 76%; + --chart-1: 115 54% 76%; + --chart-2: 170 57% 73%; + --chart-3: 197 37% 62%; + --chart-4: 43 74% 68%; + --chart-5: 27 87% 70%; + --font-sans: 'JetBrains Mono', monospace; --font-serif: Georgia, serif; - --font-mono: Menlo, monospace; + --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); @@ -87,83 +87,6 @@ --destructive-border: hsl(from hsl(var(--destructive)) h s calc(l + var(--opaque-button-border-intensity)) / alpha); } -.dark { - --button-outline: rgba(255,255,255, .10); - --badge-outline: rgba(255,255,255, .05); - --opaque-button-border-intensity: 9; - --elevate-1: rgba(255,255,255, .04); - --elevate-2: rgba(255,255,255, .09); - --background: 240 21% 12%; - --foreground: 226 64% 88%; - --border: 240 17% 20%; - --card: 240 21% 14%; - --card-foreground: 226 64% 88%; - --card-border: 240 17% 17%; - --sidebar: 240 21% 16%; - --sidebar-foreground: 226 64% 88%; - --sidebar-border: 240 17% 19%; - --sidebar-primary: 115 54% 55%; - --sidebar-primary-foreground: 240 21% 12%; - --sidebar-accent: 240 17% 19%; - --sidebar-accent-foreground: 226 64% 88%; - --sidebar-ring: 115 54% 55%; - --popover: 240 21% 18%; - --popover-foreground: 226 64% 88%; - --popover-border: 240 17% 21%; - --primary: 115 54% 30%; - --primary-foreground: 115 54% 96%; - --secondary: 240 17% 22%; - --secondary-foreground: 226 64% 88%; - --muted: 240 17% 21%; - --muted-foreground: 226 40% 65%; - --accent: 115 12% 22%; - --accent-foreground: 115 12% 88%; - --destructive: 0 62% 32%; - --destructive-foreground: 0 62% 96%; - --input: 240 17% 35%; - --ring: 115 54% 55%; - --chart-1: 115 54% 65%; - --chart-2: 173 58% 60%; - --chart-3: 197 37% 62%; - --chart-4: 43 74% 68%; - --chart-5: 27 87% 70%; - --shadow-2xs: 0px 2px 0px 0px hsl(240 21% 12% / 0.00); - --shadow-xs: 0px 2px 0px 0px hsl(240 21% 12% / 0.00); - --shadow-sm: 0px 2px 0px 0px hsl(240 21% 12% / 0.00), 0px 1px 2px -1px hsl(240 21% 12% / 0.00); - --shadow: 0px 2px 0px 0px hsl(240 21% 12% / 0.00), 0px 1px 2px -1px hsl(240 21% 12% / 0.00); - --shadow-md: 0px 2px 0px 0px hsl(240 21% 12% / 0.00), 0px 2px 4px -1px hsl(240 21% 12% / 0.00); - --shadow-lg: 0px 2px 0px 0px hsl(240 21% 12% / 0.00), 0px 4px 6px -1px hsl(240 21% 12% / 0.00); - --shadow-xl: 0px 2px 0px 0px hsl(240 21% 12% / 0.00), 0px 8px 10px -1px hsl(240 21% 12% / 0.00); - --shadow-2xl: 0px 2px 0px 0px hsl(240 21% 12% / 0.00); - -/* 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); - - /* 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); - - /* 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); - - /* 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); - - /* 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); - - /* 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); - - /* 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); -} @layer base { * { diff --git a/client/src/pages/home.tsx b/client/src/pages/home.tsx new file mode 100644 index 0000000..abd71b7 --- /dev/null +++ b/client/src/pages/home.tsx @@ -0,0 +1,208 @@ +import { useEffect, useRef, useState, useCallback } from "react"; +import { Copy, Check } from "lucide-react"; +import { Button } from "@/components/ui/button"; + +const SSH_COMMAND = "ssh portfolio@keshavanand.net"; + +const CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%^&*()_+-=[]{}|;:,.<>?/\\~`"; + +interface Particle { + x: number; + y: number; + char: string; + opacity: number; + targetX: number; + targetY: number; + vx: number; + vy: number; + life: number; + maxLife: number; + size: number; + color: string; +} + +function ParticleBackground() { + const canvasRef = useRef(null); + const particlesRef = useRef([]); + const mouseRef = useRef({ x: 0, y: 0 }); + const animationRef = useRef(); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + const resizeCanvas = () => { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + }; + + resizeCanvas(); + window.addEventListener("resize", resizeCanvas); + + const colors = [ + "#a6e3a1", + "#94e2d5", + "#89dceb", + "#74c7ec", + "#89b4fa", + "#cba6f7", + ]; + + const createParticle = (x: number, y: number): Particle => { + const angle = Math.random() * Math.PI * 2; + const speed = Math.random() * 2 + 1; + const color = colors[Math.floor(Math.random() * colors.length)]; + return { + x: x + (Math.random() - 0.5) * 100, + y: y + (Math.random() - 0.5) * 100, + char: CHARACTERS[Math.floor(Math.random() * CHARACTERS.length)], + opacity: Math.random() * 0.8 + 0.2, + targetX: x, + targetY: y, + vx: Math.cos(angle) * speed, + vy: Math.sin(angle) * speed, + life: 0, + maxLife: Math.random() * 60 + 40, + size: Math.random() * 10 + 12, + color, + }; + }; + + const handleMouseMove = (e: MouseEvent) => { + mouseRef.current = { x: e.clientX, y: e.clientY }; + + for (let i = 0; i < 3; i++) { + if (particlesRef.current.length < 200) { + particlesRef.current.push(createParticle(e.clientX, e.clientY)); + } + } + }; + + const animate = () => { + ctx.fillStyle = "#000000"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + 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); + + if (distance < 150) { + const force = (150 - distance) / 150; + p.vx += (dx / distance) * force * 0.3; + p.vy += (dy / distance) * force * 0.3; + } + + p.vx *= 0.96; + p.vy *= 0.96; + + p.x += p.vx; + p.y += p.vy; + + const lifeRatio = p.life / p.maxLife; + const fadeOpacity = lifeRatio < 0.2 + ? lifeRatio * 5 + : lifeRatio > 0.7 + ? (1 - lifeRatio) * 3.33 + : 1; + + const distanceOpacity = Math.max(0, 1 - distance / 300); + const finalOpacity = p.opacity * fadeOpacity * distanceOpacity; + + if (finalOpacity > 0.01) { + ctx.font = `${p.size}px 'JetBrains Mono', monospace`; + ctx.fillStyle = p.color; + ctx.globalAlpha = finalOpacity; + ctx.fillText(p.char, p.x, p.y); + ctx.globalAlpha = 1; + } + + return p.life < p.maxLife; + }); + + animationRef.current = requestAnimationFrame(animate); + }; + + window.addEventListener("mousemove", handleMouseMove); + animate(); + + return () => { + window.removeEventListener("resize", resizeCanvas); + window.removeEventListener("mousemove", handleMouseMove); + if (animationRef.current) { + cancelAnimationFrame(animationRef.current); + } + }; + }, []); + + return ( + + ); +} + +function CommandBox() { + const [copied, setCopied] = useState(false); + const [isAnimating, setIsAnimating] = useState(false); + + const handleCopy = useCallback(async () => { + try { + await navigator.clipboard.writeText(SSH_COMMAND); + setIsAnimating(true); + setCopied(true); + setTimeout(() => setIsAnimating(false), 150); + setTimeout(() => setCopied(false), 2000); + } catch (err) { + console.error("Failed to copy:", err); + } + }, []); + + return ( +
+ $ + + {SSH_COMMAND} + + +
+ ); +} + +export default function Home() { + return ( +
+ + +
+ ); +}