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 (
+
+ );
+}