full website
This commit is contained in:
9
.env.example
Normal file
9
.env.example
Normal file
@@ -0,0 +1,9 @@
|
||||
# GEMINI_API_KEY: Required for Gemini AI API calls.
|
||||
# AI Studio automatically injects this at runtime from user secrets.
|
||||
# Users configure this via the Secrets panel in the AI Studio UI.
|
||||
GEMINI_API_KEY="MY_GEMINI_API_KEY"
|
||||
|
||||
# APP_URL: The URL where this applet is hosted.
|
||||
# AI Studio automatically injects this at runtime with the Cloud Run service URL.
|
||||
# Used for self-referential links, OAuth callbacks, and API endpoints.
|
||||
APP_URL="MY_APP_URL"
|
||||
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
node_modules/
|
||||
build/
|
||||
dist/
|
||||
coverage/
|
||||
.DS_Store
|
||||
*.log
|
||||
.env*
|
||||
!.env.example
|
||||
10
.idea/.gitignore
generated
vendored
Normal file
10
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Ignored default folder with query files
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
8
.idea/krishna-ayyalasomayajula-_-systems-&-quant-portfolio.iml
generated
Normal file
8
.idea/krishna-ayyalasomayajula-_-systems-&-quant-portfolio.iml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
4
.idea/misc.xml
generated
Normal file
4
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JsBuildToolPackageJson" sorting="DEFINITION_ORDER" />
|
||||
</project>
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/krishna-ayyalasomayajula-_-systems-&-quant-portfolio.iml" filepath="$PROJECT_DIR$/.idea/krishna-ayyalasomayajula-_-systems-&-quant-portfolio.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
20
README.md
Normal file
20
README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
<div align="center">
|
||||
<img width="1200" height="475" alt="GHBanner" src="https://ai.google.dev/static/site-assets/images/share-ais-513315318.png" />
|
||||
</div>
|
||||
|
||||
# Run and deploy your AI Studio app
|
||||
|
||||
This contains everything you need to run your app locally.
|
||||
|
||||
View your app in AI Studio: https://ai.studio/apps/6f494bb8-bd22-4364-a17e-ea58328fc8c9
|
||||
|
||||
## Run Locally
|
||||
|
||||
**Prerequisites:** Node.js
|
||||
|
||||
|
||||
1. Install dependencies:
|
||||
`npm install`
|
||||
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
|
||||
3. Run the app:
|
||||
`npm run dev`
|
||||
19
index.html
Normal file
19
index.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<!doctype html>
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Krishna Ayyalasomayajula | Systems & Quant Developer</title>
|
||||
<!-- IBM Plex Mono and JetBrains Mono Fonts -->
|
||||
<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=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=JetBrains+Mono:wght@100..900&display=swap" rel="stylesheet" />
|
||||
<!-- Material Symbols for elegant modern technical iconography -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap" rel="stylesheet" />
|
||||
</head>
|
||||
<body class="bg-[#0D0D0D] text-[#e2e2e2] antialiased selection:bg-white selection:text-black min-h-screen">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
6
metadata.json
Normal file
6
metadata.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "Krishna Ayyalasomayajula | Systems & Quant Portfolio",
|
||||
"description": "High-performance systems research, tensor mutation research, and quantitative trading engine architecture portfolio.",
|
||||
"requestFramePermissions": [],
|
||||
"majorCapabilities": ["MAJOR_CAPABILITY_SERVER_SIDE_GEMINI_API"]
|
||||
}
|
||||
4362
package-lock.json
generated
Normal file
4362
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
38
package.json
Normal file
38
package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "react-example",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --port=3000 --host=0.0.0.0",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"clean": "rm -rf dist server.js",
|
||||
"lint": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/genai": "^2.4.0",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^4.21.2",
|
||||
"lucide-react": "^0.546.0",
|
||||
"motion": "^12.23.24",
|
||||
"react": "^19.0.1",
|
||||
"react-dom": "^19.0.1",
|
||||
"smol-toml": "^1.6.1",
|
||||
"three": "^0.184.0",
|
||||
"vite": "^6.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/three": "^0.184.1",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"esbuild": "^0.25.0",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "~5.8.2",
|
||||
"vite": "^6.2.3"
|
||||
}
|
||||
}
|
||||
140
src/App.tsx
Normal file
140
src/App.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* @license
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { motion, useScroll, useSpring } from "motion/react";
|
||||
import ThreeBg from "./components/ThreeBg";
|
||||
import Hero from "./components/Hero";
|
||||
import Experience from "./components/Experience";
|
||||
import SelectedWorks from "./components/SelectedWorks";
|
||||
import ContactSection from "./components/ContactSection";
|
||||
import Footer from "./components/Footer";
|
||||
|
||||
const SECTIONS = [
|
||||
{ id: "home", label: "Home" },
|
||||
{ id: "experience", label: "Trajectory" },
|
||||
{ id: "projects", label: "Selected Works" },
|
||||
{ id: "contact", label: "Contact" },
|
||||
];
|
||||
|
||||
export default function App() {
|
||||
const [activeSection, setActiveSection] = useState("home");
|
||||
const { scrollYProgress } = useScroll();
|
||||
|
||||
// Create a smooth scroll progress indicator
|
||||
const scaleX = useSpring(scrollYProgress, {
|
||||
stiffness: 100,
|
||||
damping: 25,
|
||||
restDelta: 0.001,
|
||||
});
|
||||
|
||||
const handleScrollToId = (id: string) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
};
|
||||
|
||||
// Intersection observer to track which section is currently in view
|
||||
useEffect(() => {
|
||||
const observers = SECTIONS.map((sec) => {
|
||||
const el = document.getElementById(sec.id);
|
||||
if (!el) return null;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setActiveSection(sec.id);
|
||||
}
|
||||
},
|
||||
{
|
||||
// Trigger when the section occupies 40% or more of the viewport
|
||||
rootMargin: "-40% 0px -40% 0px",
|
||||
}
|
||||
);
|
||||
|
||||
observer.observe(el);
|
||||
return { observer, el };
|
||||
});
|
||||
|
||||
return () => {
|
||||
observers.forEach((obs) => {
|
||||
if (obs) {
|
||||
obs.observer.unobserve(obs.el);
|
||||
}
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative min-h-screen selection:bg-white selection:text-black antialiased overflow-x-hidden text-[#e2e2e2] bg-[#0D0D0D]">
|
||||
{/* 3D Falling Cubes & Mouse Tracker Proximity Glowing */}
|
||||
<ThreeBg />
|
||||
|
||||
{/* Smooth Apple-style Scroll Progress Indicator Line */}
|
||||
<motion.div
|
||||
className="fixed top-0 left-0 right-0 h-[2.5px] bg-white origin-left z-50 w-full"
|
||||
style={{ scaleX }}
|
||||
/>
|
||||
|
||||
{/* Apple-style Right Floating Scroll Navigation Sidebar */}
|
||||
<aside className="fixed right-6 top-1/2 -translate-y-1/2 z-50 flex flex-col items-end gap-3 pointer-events-none select-none">
|
||||
{SECTIONS.map((sec) => {
|
||||
const isActive = activeSection === sec.id;
|
||||
return (
|
||||
<div
|
||||
key={sec.id}
|
||||
className="flex items-center gap-3 group/dot cursor-pointer pointer-events-auto"
|
||||
onClick={() => handleScrollToId(sec.id)}
|
||||
>
|
||||
{/* Tooltip text showing section title */}
|
||||
<span className="font-mono text-[9px] uppercase tracking-widest text-zinc-500 bg-black/80 px-2 py-1 rounded border border-zinc-900 opacity-0 group-hover/dot:opacity-100 transition-opacity duration-200 select-none">
|
||||
{sec.label}
|
||||
</span>
|
||||
|
||||
{/* Dot circle indicator */}
|
||||
<div className="relative w-6 h-6 flex items-center justify-center">
|
||||
{isActive && (
|
||||
<motion.div
|
||||
layoutId="activeDotRing"
|
||||
className="absolute inset-0 rounded-full border border-white/40"
|
||||
transition={{ type: "spring", stiffness: 300, damping: 30 }}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={`w-1.5 h-1.5 rounded-full transition-all duration-300 ${
|
||||
isActive ? "bg-white scale-125" : "bg-zinc-700 hover:bg-zinc-400"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</aside>
|
||||
|
||||
{/* Core Portfolio Sections */}
|
||||
<main className="relative z-10 w-full flex flex-col">
|
||||
{/* Landing Hero Screen */}
|
||||
<Hero
|
||||
onViewProjects={() => handleScrollToId("projects")}
|
||||
onGetInTouch={() => handleScrollToId("contact")}
|
||||
/>
|
||||
|
||||
{/* Experience Timeline */}
|
||||
<Experience />
|
||||
|
||||
{/* Selected Projects Bento Grid */}
|
||||
<SelectedWorks />
|
||||
|
||||
{/* Action triggers and secure terminal communication forms */}
|
||||
<ContactSection />
|
||||
</main>
|
||||
|
||||
{/* Footer and dynamic copyrights */}
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
247
src/components/ContactSection.tsx
Normal file
247
src/components/ContactSection.tsx
Normal file
@@ -0,0 +1,247 @@
|
||||
import React, { useState } from "react";
|
||||
import { motion, AnimatePresence } from "motion/react";
|
||||
import { X, Terminal, Printer, Github, Linkedin, Twitter, Mail, FileText } from "lucide-react";
|
||||
import { ScramblerText } from "./DynamicTextEffects";
|
||||
import { CONTACT_LINKS, APP_CONFIG, RESUME_DATA } from "../data";
|
||||
|
||||
const IconMap: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||
github: Github,
|
||||
linkedin: Linkedin,
|
||||
twitter: Twitter,
|
||||
mail: Mail,
|
||||
file: FileText,
|
||||
};
|
||||
|
||||
export default function ContactSection() {
|
||||
const [resumeOpen, setResumeOpen] = useState(false);
|
||||
|
||||
const handlePrintResume = () => {
|
||||
window.print();
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="contact" className="relative py-28 bg-black overflow-hidden select-none">
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[550px] h-[550px] bg-white/[0.012] rounded-full blur-[130px] pointer-events-none" />
|
||||
|
||||
<div className="max-w-[1200px] mx-auto px-6 text-center z-10 relative space-y-6">
|
||||
|
||||
{/* Simplified Header */}
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 15 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="font-sans text-3xl md:text-5xl text-white font-extrabold tracking-tight"
|
||||
>
|
||||
Connect
|
||||
</motion.h2>
|
||||
|
||||
|
||||
|
||||
{/* Minimalist Interactive Link Grid */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: 0.2 }}
|
||||
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 pt-8 text-left"
|
||||
>
|
||||
{CONTACT_LINKS.map((link) => {
|
||||
const LinkIcon = IconMap[link.icon] || Mail;
|
||||
const isAction = link.url.startsWith("action:");
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
if (isAction) {
|
||||
e.preventDefault();
|
||||
if (link.url === "action:resume") {
|
||||
setResumeOpen(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const cardContent = (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-mono text-[9px] uppercase tracking-wider text-zinc-600 font-bold group-hover:text-zinc-400 transition-colors">
|
||||
{link.cmd}
|
||||
</span>
|
||||
<LinkIcon className="w-4 h-4 text-zinc-600 group-hover:text-white transition-colors animate-[pulse_3s_infinite]" />
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<div className="font-sans text-xs font-semibold text-white tracking-wide uppercase">
|
||||
{link.label}
|
||||
</div>
|
||||
<div className="font-mono text-[10px] text-zinc-500 truncate group-hover:text-zinc-300 transition-colors mt-0.5">
|
||||
{link.value}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const cardClassName = "group p-4 bg-zinc-950/40 border border-zinc-900 rounded-lg hover:border-zinc-700 hover:bg-zinc-900/40 transition-all duration-300 flex flex-col justify-between min-h-[105px] w-full text-left cursor-pointer";
|
||||
|
||||
if (isAction) {
|
||||
return (
|
||||
<button
|
||||
key={link.label}
|
||||
onClick={handleClick}
|
||||
className={cardClassName}
|
||||
>
|
||||
{cardContent}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
key={link.label}
|
||||
href={link.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cardClassName}
|
||||
>
|
||||
{cardContent}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* MODAL 2: VIEW PRINTABLE TECHNICAL RESUME */}
|
||||
<AnimatePresence>
|
||||
{resumeOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 overflow-y-auto">
|
||||
{/* Overlay backdrop */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={() => setResumeOpen(false)}
|
||||
className="absolute inset-0 bg-black/90 backdrop-blur-sm"
|
||||
/>
|
||||
|
||||
{/* Modal Container */}
|
||||
<motion.div
|
||||
initial={{ scale: 0.95, opacity: 0, y: 20 }}
|
||||
animate={{ scale: 1, opacity: 1, y: 0 }}
|
||||
exit={{ scale: 0.95, opacity: 0, y: 20 }}
|
||||
transition={{ type: "spring", duration: 0.5 }}
|
||||
className="relative w-full max-w-3xl bg-[#0d0d0d] border border-zinc-900 rounded-lg overflow-hidden flex flex-col my-8 h-auto max-h-[90vh]"
|
||||
>
|
||||
{/* Header control */}
|
||||
<div className="p-5 border-b border-zinc-900 flex justify-between items-center bg-[#09090b]/80 z-10">
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal className="w-4 h-4 text-white" />
|
||||
<span className="font-mono text-[10px] text-zinc-400 tracking-[0.2em] uppercase font-bold">
|
||||
PRINT_EMULATOR // PREVIEW
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={handlePrintResume}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 bg-white text-black font-bold text-[10px] uppercase font-mono tracking-wider transition-colors hover:bg-zinc-200 cursor-pointer"
|
||||
>
|
||||
<Printer className="w-3 h-3" />
|
||||
Print PDF
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setResumeOpen(false)}
|
||||
className="text-zinc-500 hover:text-white transition-colors cursor-pointer"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Condensed Technical Resume Layout */}
|
||||
{APP_CONFIG.resume_pdf_url ? (
|
||||
<div className="w-full h-[75vh] bg-black">
|
||||
<iframe
|
||||
src={APP_CONFIG.resume_pdf_url}
|
||||
className="w-full h-full border-0"
|
||||
title="Technical Resume"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-8 md:p-12 overflow-y-auto bg-black text-[#e2e2e2] font-sans md:space-y-8 space-y-6 printable-region text-[13px] leading-relaxed">
|
||||
{/* PDF Printable specific metadata style */}
|
||||
<div className="border-b border-zinc-900 pb-6 flex flex-col md:flex-row justify-between md:items-end gap-4">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-3xl font-extrabold text-white tracking-tighter">
|
||||
{RESUME_DATA.name}
|
||||
</h1>
|
||||
<p className="text-zinc-400 font-medium">
|
||||
{RESUME_DATA.title}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-left md:text-right font-mono text-[11px] text-zinc-500 space-y-0.5">
|
||||
<p>{RESUME_DATA.location}</p>
|
||||
<p>{RESUME_DATA.email}</p>
|
||||
<p>{RESUME_DATA.github}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Core Experience Grid */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="section-title text-white font-extrabold uppercase font-mono tracking-widest text-xs border-b border-zinc-900 pb-1.5">
|
||||
01 // Technical Experience
|
||||
</h3>
|
||||
<div className="space-y-6">
|
||||
{RESUME_DATA.experiences.map((exp, i) => (
|
||||
<div key={i} className="space-y-1.5">
|
||||
<div className="flex justify-between items-start font-mono text-[11px]">
|
||||
<span className="text-zinc-400 font-bold uppercase">{exp.company}</span>
|
||||
<span className="text-zinc-500">{exp.period}</span>
|
||||
</div>
|
||||
<p className="font-bold text-white text-sm">{exp.role}</p>
|
||||
<p className="text-zinc-400">{exp.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Research papers */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="section-title text-white font-extrabold uppercase font-mono tracking-widest text-xs border-b border-zinc-900 pb-1.5">
|
||||
02 // Research Work
|
||||
</h3>
|
||||
<div className="space-y-6">
|
||||
{RESUME_DATA.research.map((pub, i) => (
|
||||
<div key={i} className="space-y-1.5">
|
||||
<p className="font-bold text-white">{pub.title}</p>
|
||||
<p className="text-zinc-400 text-xs font-mono">{pub.venue}</p>
|
||||
<p className="text-zinc-400">{pub.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Core stacks */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="section-title text-white font-extrabold uppercase font-mono tracking-widest text-xs border-b border-zinc-900 pb-1.5">
|
||||
03 // Academic Credentials / Skills
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-1">
|
||||
<p className="font-bold text-white">{RESUME_DATA.education.school}</p>
|
||||
<p className="text-zinc-400 font-mono text-xs">
|
||||
{RESUME_DATA.education.years} // {RESUME_DATA.education.degree}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="font-bold text-white">Engineering Capabilities & Stacks</p>
|
||||
<p className="text-zinc-400 font-mono text-xs">
|
||||
{RESUME_DATA.education.skills}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
101
src/components/DynamicTextEffects.tsx
Normal file
101
src/components/DynamicTextEffects.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
|
||||
// === High Fidelity Viewport-Driven Matrix Decryption & Scrambler Effect ===
|
||||
interface ScramblerTextProps {
|
||||
text: string;
|
||||
speed?: number;
|
||||
delay?: number;
|
||||
className?: string;
|
||||
triggerOnHover?: boolean;
|
||||
}
|
||||
|
||||
export function ScramblerText({
|
||||
text,
|
||||
speed = 25,
|
||||
delay = 0,
|
||||
className = "",
|
||||
triggerOnHover = false,
|
||||
}: ScramblerTextProps) {
|
||||
const [displayedText, setDisplayedText] = useState("");
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const elementRef = useRef<HTMLSpanElement>(null);
|
||||
const hasTriggeredRef = useRef(false);
|
||||
const chars = "XYZ019864275_<>[]_*{}+=-/\\#@$%&";
|
||||
|
||||
const triggerScramble = () => {
|
||||
let tick = 0;
|
||||
const totalTicks = 30; // ~1 second total at 33ms interval
|
||||
const tickDuration = 33; // 33ms limit per tick
|
||||
|
||||
const interval = setInterval(() => {
|
||||
tick += 1;
|
||||
const revealWeight = tick / totalTicks;
|
||||
const revealedCount = Math.floor(revealWeight * text.length);
|
||||
|
||||
setDisplayedText(() => {
|
||||
return text
|
||||
.split("")
|
||||
.map((char, index) => {
|
||||
if (char === " " || char === "\n") return char;
|
||||
if (index < revealedCount) {
|
||||
return text[index];
|
||||
}
|
||||
return chars[Math.floor(Math.random() * chars.length)];
|
||||
})
|
||||
.join("");
|
||||
});
|
||||
|
||||
if (tick >= totalTicks) {
|
||||
clearInterval(interval);
|
||||
setDisplayedText(text);
|
||||
}
|
||||
}, tickDuration);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (triggerOnHover) {
|
||||
if (isHovered) {
|
||||
triggerScramble();
|
||||
} else {
|
||||
setDisplayedText(text);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Set up viewport-driven trigger
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting && !hasTriggeredRef.current) {
|
||||
hasTriggeredRef.current = true;
|
||||
const timer = setTimeout(() => {
|
||||
triggerScramble();
|
||||
}, delay);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.05, rootMargin: "0px 0px -50px 0px" } // trigger slightly before passing completely
|
||||
);
|
||||
|
||||
if (elementRef.current) {
|
||||
observer.observe(elementRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [text, speed, delay, isHovered, triggerOnHover]);
|
||||
|
||||
return (
|
||||
<span
|
||||
ref={elementRef}
|
||||
className={`${className} cursor-default`}
|
||||
onMouseEnter={() => triggerOnHover && setIsHovered(true)}
|
||||
onMouseLeave={() => triggerOnHover && setIsHovered(false)}
|
||||
>
|
||||
{displayedText || text}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
182
src/components/Experience.tsx
Normal file
182
src/components/Experience.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
import { useRef } from "react";
|
||||
import { motion, useScroll, useSpring } from "motion/react";
|
||||
import { EXPERIENCES, EXPERIENCE_HEADING } from "../data";
|
||||
import { Briefcase, GraduationCap } from "lucide-react";
|
||||
import { ScramblerText } from "./DynamicTextEffects";
|
||||
|
||||
export default function Experience() {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Track scroll specifically for the timeline container
|
||||
const { scrollYProgress } = useScroll({
|
||||
target: containerRef,
|
||||
offset: ["start center", "end center"],
|
||||
});
|
||||
|
||||
// Use a smooth spring to drive the glowing scroll line progress
|
||||
const smoothProgress = useSpring(scrollYProgress, {
|
||||
stiffness: 120,
|
||||
damping: 30,
|
||||
restDelta: 0.001
|
||||
});
|
||||
|
||||
const containerVariants = {
|
||||
hidden: {},
|
||||
visible: {
|
||||
transition: {
|
||||
staggerChildren: 0.15,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { opacity: 0, y: 32 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
type: "spring",
|
||||
stiffness: 70,
|
||||
damping: 15,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={containerRef}
|
||||
id="experience"
|
||||
className="relative py-28 bg-[#09090b]/40 grid-pattern border-t border-zinc-900 border-b border-zinc-900 select-none overflow-hidden"
|
||||
>
|
||||
<div className="absolute top-0 right-1/4 w-[350px] h-[350px] bg-white/[0.012] rounded-full blur-[80px] pointer-events-none" />
|
||||
|
||||
{/* Main Trajectory Header & Summary */}
|
||||
<div className="max-w-[1200px] mx-auto px-6 mb-20">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
transition={{ duration: 0.6 }}
|
||||
className="max-w-3xl space-y-4"
|
||||
>
|
||||
<h2 className="font-sans text-5xl md:text-[68px] uppercase tracking-tighter text-white font-extrabold leading-[1.05]">
|
||||
{EXPERIENCE_HEADING.title.split(" ")[0]}
|
||||
<br />
|
||||
{EXPERIENCE_HEADING.title.split(" ").slice(1).join(" ")}
|
||||
</h2>
|
||||
<p className="font-sans text-base md:text-lg text-zinc-400 max-w-2xl leading-relaxed">
|
||||
<ScramblerText text={EXPERIENCE_HEADING.description} delay={200} />
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Experience Dual Column Timeline Blocks */}
|
||||
<div className="max-w-[1200px] mx-auto px-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-8 relative">
|
||||
|
||||
{/* Side section info */}
|
||||
<div className="md:col-span-4 flex flex-col justify-start">
|
||||
<div className="sticky top-32 space-y-2">
|
||||
<motion.h3
|
||||
initial={{ opacity: 0, x: -15 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
className="font-sans text-2xl font-bold text-white tracking-tight"
|
||||
>
|
||||
{EXPERIENCE_HEADING.subtitle}
|
||||
</motion.h3>
|
||||
<span className="font-mono text-[10px] uppercase font-semibold text-zinc-500 tracking-[0.2em] block">
|
||||
CHRONOLOGICAL HISTORY
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Timeline Items List */}
|
||||
<div className="md:col-span-8 relative pl-6 md:pl-8">
|
||||
{/* Background inactive rail track */}
|
||||
<div className="absolute left-0 md:left-[-1.5rem] top-3 bottom-3 w-[1px] bg-zinc-800 hidden md:block opacity-40" />
|
||||
|
||||
{/* Glowing active rail scroll indicator */}
|
||||
<motion.div
|
||||
style={{ scaleY: smoothProgress, originY: 0 }}
|
||||
className="absolute left-[0.25px] md:left-[-1.51rem] top-3 bottom-3 w-[1.5px] bg-gradient-to-b from-white via-neutral-200 to-white hidden md:block shadow-[0_0_8px_rgba(255,255,255,0.7)]"
|
||||
/>
|
||||
|
||||
<motion.div
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
className="space-y-12"
|
||||
>
|
||||
{EXPERIENCES.map((exp) => {
|
||||
const isFounding = exp.role.includes("Founding");
|
||||
return (
|
||||
<motion.div
|
||||
key={exp.id}
|
||||
variants={itemVariants}
|
||||
className="relative group"
|
||||
>
|
||||
{/* Floating chronological timeline handle */}
|
||||
<div className="absolute -left-[2.1rem] top-3.5 w-3.5 h-3.5 bg-black rounded-full border-2 border-zinc-700 z-10 hidden md:block group-hover:border-white group-hover:scale-125 transition-all duration-300" />
|
||||
|
||||
{/* Timeline Glassmorphic Card */}
|
||||
<div className="glass-card p-6 md:p-8 bg-[#09090b]/80 border border-zinc-800 rounded-lg hover:border-zinc-700 hover:bg-zinc-950/20 transition-all duration-300 relative overflow-hidden">
|
||||
{/* Active green pulsing subtle border indicator */}
|
||||
{exp.active && (
|
||||
<div className="absolute top-0 left-0 w-full h-[2px] bg-gradient-to-r from-green-500 to-transparent opacity-80" />
|
||||
)}
|
||||
|
||||
<div className="flex flex-col md:flex-row md:items-start md:justify-between gap-4 mb-4">
|
||||
<div>
|
||||
<span className="font-mono text-[10px] tracking-widest text-[#b4c5ff] mb-2 uppercase block font-semibold">
|
||||
<ScramblerText text={exp.period} delay={100} />
|
||||
</span>
|
||||
<h4 className="font-sans text-xl md:text-2xl text-white font-bold tracking-tight mb-1">
|
||||
{exp.role}
|
||||
</h4>
|
||||
<div className="flex flex-wrap items-center gap-2 font-mono text-xs">
|
||||
<span className="text-zinc-300 font-semibold">{exp.company}</span>
|
||||
<span className="text-zinc-600">•</span>
|
||||
<span className="text-zinc-400">{exp.location}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Top corner role emblem */}
|
||||
<div className="text-zinc-600 group-hover:text-white transition-colors duration-300 self-start md:self-auto">
|
||||
{isFounding ? (
|
||||
<Briefcase className="w-5 h-5 opacity-60" />
|
||||
) : (
|
||||
<GraduationCap className="w-5 h-5 opacity-60" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="font-sans text-[#c4c7c8] text-sm leading-relaxed mb-6">
|
||||
<ScramblerText text={exp.description} delay={150} />
|
||||
</p>
|
||||
|
||||
{/* Skills Tags */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{exp.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="tech-tag font-mono text-[10px] font-medium border border-zinc-800 text-zinc-400 px-2.5 py-1 bg-zinc-900/30 hover:border-white hover:text-white hover:shadow-[0_0_8px_rgba(255,255,255,0.1)] transition-all duration-200 select-none rounded-[2px]"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
44
src/components/Footer.tsx
Normal file
44
src/components/Footer.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { motion } from "motion/react";
|
||||
import { FOOTER_DATA, CONTACT_LINKS } from "../data";
|
||||
|
||||
export default function Footer() {
|
||||
const handleScrollToTop = () => {
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
};
|
||||
|
||||
return (
|
||||
<footer className="bg-black/90 w-full py-16 border-t border-zinc-900 select-none">
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center max-w-[1200px] mx-auto px-6 gap-8">
|
||||
|
||||
{/* Left Side: Brand and Copyright Info */}
|
||||
<div className="space-y-3.5 text-left w-full md:w-auto">
|
||||
<button
|
||||
onClick={handleScrollToTop}
|
||||
className="font-mono text-xs tracking-[0.25em] font-extrabold text-white hover:opacity-75 transition-opacity cursor-pointer uppercase block"
|
||||
>
|
||||
KRISHNA A.
|
||||
</button>
|
||||
<p className="font-mono text-[10px] text-zinc-500 tracking-wide">
|
||||
{FOOTER_DATA.copyright}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Right Side: Social Media Links */}
|
||||
<div className="flex flex-wrap items-center gap-x-8 gap-y-2 w-full md:w-auto justify-start md:justify-end">
|
||||
{CONTACT_LINKS.map((link) => (
|
||||
<a
|
||||
key={link.label}
|
||||
href={link.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-mono text-[10px] uppercase text-zinc-500 hover:text-white transition-colors duration-350 tracking-wider"
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
110
src/components/Hero.tsx
Normal file
110
src/components/Hero.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import { useRef } from "react";
|
||||
import { motion, useScroll, useTransform } from "motion/react";
|
||||
import { ArrowDown } from "lucide-react";
|
||||
import { ScramblerText } from "./DynamicTextEffects";
|
||||
import { HERO } from "../data";
|
||||
|
||||
interface HeroProps {
|
||||
onViewProjects: () => void;
|
||||
onGetInTouch: () => void;
|
||||
}
|
||||
|
||||
export default function Hero({ onViewProjects, onGetInTouch }: HeroProps) {
|
||||
const sectionRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Track scroll specifically for the Hero section
|
||||
const { scrollYProgress } = useScroll({
|
||||
target: sectionRef,
|
||||
offset: ["start start", "end start"],
|
||||
});
|
||||
|
||||
// Scroll mapping transforms for Apple product style parallax depth
|
||||
const backgroundY = useTransform(scrollYProgress, [0, 1], ["0%", "20%"]);
|
||||
const backgroundScale = useTransform(scrollYProgress, [0, 1], [1, 1.1]);
|
||||
const textY = useTransform(scrollYProgress, [0, 1], [0, -80]);
|
||||
const contentOpacity = useTransform(scrollYProgress, [0, 0.75], [1, 0]);
|
||||
|
||||
// Container spring transition delays
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.15,
|
||||
delayChildren: 0.1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { opacity: 0, y: 30 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
type: "spring",
|
||||
stiffness: 90,
|
||||
damping: 15,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={sectionRef}
|
||||
id="home"
|
||||
className="relative min-h-screen flex flex-col justify-center items-center pt-28 pb-16 overflow-hidden select-none"
|
||||
>
|
||||
{/* Visual background picture and layers with parallax transforms */}
|
||||
<motion.div
|
||||
style={{ y: backgroundY, scale: backgroundScale }}
|
||||
className="absolute inset-0 z-0 overflow-hidden select-none pointer-events-none"
|
||||
>
|
||||
<img
|
||||
alt="Krishna Ayyalasomayajula workspace background"
|
||||
className="w-full h-full object-cover grayscale opacity-70 filter brightness-[0.35] saturate-[0.8]"
|
||||
src={HERO.background_image}
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-[#0D0D0D]/10 via-[#0D0D0D]/70 to-[#0D0D0D]" />
|
||||
<div className="absolute inset-0 grid-pattern opacity-50" />
|
||||
<div className="absolute inset-0 radial-glow" />
|
||||
</motion.div>
|
||||
|
||||
<div className="absolute top-1/4 -right-20 w-[450px] h-[450px] bg-white/[0.015] rounded-full blur-[110px] pointer-events-none" />
|
||||
|
||||
{/* Content wrapper with scroll link translate & opacity */}
|
||||
<motion.div
|
||||
style={{ y: textY, opacity: contentOpacity }}
|
||||
className="relative z-10 max-w-[1240px] mx-auto px-6 w-full flex flex-col items-center justify-center text-center"
|
||||
>
|
||||
<motion.div
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
className="max-w-3xl space-y-8 flex flex-col items-center"
|
||||
>
|
||||
{/* Heading Name */}
|
||||
<motion.h1
|
||||
variants={itemVariants}
|
||||
className="font-sans text-5xl md:text-[88px] text-white leading-[1.05] tracking-tighter font-extrabold"
|
||||
>
|
||||
{HERO.name} <br /> {HERO.surname}
|
||||
</motion.h1>
|
||||
|
||||
|
||||
{/* Action Buttons */}
|
||||
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
style={{ opacity: contentOpacity }}
|
||||
className="absolute bottom-6 left-1/2 -translate-x-1/2 animate-bounce opacity-40 pointer-events-none"
|
||||
>
|
||||
<ArrowDown className="w-5 h-5 text-white" />
|
||||
</motion.div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
219
src/components/SelectedWorks.tsx
Normal file
219
src/components/SelectedWorks.tsx
Normal file
@@ -0,0 +1,219 @@
|
||||
import React, { useState } from "react";
|
||||
import { motion, AnimatePresence } from "motion/react";
|
||||
import {
|
||||
LineChart, HardDrive, Terminal, Brain, Copy, Check,
|
||||
FileCode, Server, Cpu, GitBranch
|
||||
} from "lucide-react";
|
||||
import { ScramblerText } from "./DynamicTextEffects";
|
||||
import { PROJECTS } from "../data";
|
||||
import type { ProjectItem } from "../types";
|
||||
|
||||
const ProjectIconMap: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||
brain: Brain,
|
||||
"line-chart": LineChart,
|
||||
"hard-drive": HardDrive,
|
||||
terminal: Terminal,
|
||||
"file-code": FileCode,
|
||||
server: Server,
|
||||
cpu: Cpu,
|
||||
"git-branch": GitBranch,
|
||||
};
|
||||
|
||||
function resolveIcon(iconName?: string): React.ComponentType<{ className?: string }> {
|
||||
if (iconName && iconName in ProjectIconMap) {
|
||||
return ProjectIconMap[iconName];
|
||||
}
|
||||
return FileCode;
|
||||
}
|
||||
|
||||
function HeroCard({ project }: { project: ProjectItem }) {
|
||||
return (
|
||||
<div className="md:col-span-8 group relative overflow-hidden bg-zinc-950/25 border border-zinc-900 rounded-lg p-6 md:p-8 flex flex-col justify-end min-h-[400px] hover:border-zinc-700/80 hover:bg-zinc-950/40 transition-all duration-300">
|
||||
{project.image && (
|
||||
<>
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center transition-transform duration-700 group-hover:scale-[1.025] opacity-25 group-hover:opacity-45 grayscale group-hover:grayscale-0 select-none pointer-events-none"
|
||||
style={{ backgroundImage: `url('${project.image}')` }}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-zinc-950 via-zinc-950/70 to-zinc-950/20" />
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="relative z-10 space-y-3">
|
||||
{project.category && (
|
||||
<span className="font-mono text-[9px] font-bold text-blue-200 bg-blue-950/80 border border-blue-900 px-2.5 py-1 rounded inline-block uppercase tracking-wider">
|
||||
{project.category}
|
||||
</span>
|
||||
)}
|
||||
<h3 className="font-sans text-2xl md:text-3xl font-extrabold text-white tracking-tight">
|
||||
{project.title}
|
||||
</h3>
|
||||
<p className="font-sans text-zinc-400 text-sm md:text-base max-w-md leading-relaxed">
|
||||
<ScramblerText text={project.description} delay={200} />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function IconCard({ project }: { project: ProjectItem }) {
|
||||
const Icon = resolveIcon(project.icon);
|
||||
|
||||
return (
|
||||
<div className="md:col-span-4 group relative overflow-hidden bg-zinc-950/25 border border-zinc-900 rounded-lg p-6 md:p-8 flex flex-col justify-between min-h-[400px] hover:border-zinc-700/80 hover:bg-zinc-950/40 transition-all duration-300">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-white/[0.01] to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||
|
||||
<div className="space-y-6 relative z-10">
|
||||
<div className="w-12 h-12 rounded bg-zinc-900/60 border border-zinc-800 flex items-center justify-center text-zinc-400 group-hover:text-white group-hover:scale-105 group-hover:border-zinc-700 transition-all duration-300">
|
||||
<Icon className="w-5 h-5" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-sans text-xl md:text-2xl font-bold text-white tracking-tight">
|
||||
{project.title}
|
||||
</h3>
|
||||
<p className="font-sans text-zinc-400 text-sm leading-relaxed">
|
||||
<ScramblerText text={project.description} delay={250} />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{project.tags && project.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 pt-4 relative z-10">
|
||||
{project.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="font-mono text-[9px] text-zinc-500 border border-zinc-800 px-2 py-0.5 bg-zinc-900/10 rounded"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TerminalCard({ project }: { project: ProjectItem }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const copyCommand = () => {
|
||||
if (project.terminalCommand) {
|
||||
navigator.clipboard.writeText(project.terminalCommand);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="md:col-span-8 group relative overflow-hidden bg-zinc-950/40 border border-zinc-900 rounded-lg p-6 md:p-8 flex flex-col sm:flex-row items-center gap-8 min-h-[300px] hover:border-zinc-700 hover:bg-zinc-950/40 transition-all duration-300">
|
||||
<div className="flex-1 space-y-4">
|
||||
<div>
|
||||
{project.category && (
|
||||
<span className="font-mono text-[9px] text-zinc-500 font-bold mb-1.5 block tracking-widest uppercase">
|
||||
<ScramblerText text={project.category} delay={150} />
|
||||
</span>
|
||||
)}
|
||||
<h3 className="font-sans text-xl md:text-2xl font-extrabold text-white tracking-tight leading-snug">
|
||||
{project.title}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<p className="font-sans text-zinc-400 text-sm leading-relaxed">
|
||||
<ScramblerText text={project.description} delay={350} />
|
||||
</p>
|
||||
|
||||
<div
|
||||
onClick={copyCommand}
|
||||
className="group/term relative flex items-center justify-between bg-zinc-900/60 border border-zinc-800 rounded px-4 py-3 cursor-pointer hover:border-white transition-colors duration-300"
|
||||
>
|
||||
<div className="flex items-center gap-2.5 overflow-hidden">
|
||||
<Terminal className="w-4 h-4 text-zinc-500 shrink-0 group-hover/term:text-white" />
|
||||
<code className="font-mono text-xs text-zinc-300 group-hover/term:text-white truncate">
|
||||
{project.terminalCommand}
|
||||
</code>
|
||||
</div>
|
||||
<div className="shrink-0 pl-2">
|
||||
<AnimatePresence mode="wait">
|
||||
{copied ? (
|
||||
<motion.div
|
||||
key="check"
|
||||
initial={{ scale: 0.5, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0.5, opacity: 0 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
>
|
||||
<Check className="w-3.5 h-3.5 text-green-400" />
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="copy"
|
||||
initial={{ scale: 0.5, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0.5, opacity: 0 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
>
|
||||
<Copy className="w-3.5 h-3.5 text-zinc-500 group-hover/term:text-white" />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{project.image && (
|
||||
<div className="hidden sm:block w-1/3 aspect-square bg-zinc-950 border border-zinc-800 rounded flex items-center justify-center overflow-hidden">
|
||||
<img
|
||||
className="w-full h-full object-cover grayscale opacity-55 group-hover:grayscale-0 group-hover:opacity-100 transition-all duration-700"
|
||||
src={project.image}
|
||||
alt={`${project.title} preview`}
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SelectedWorks() {
|
||||
return (
|
||||
<section id="projects" className="relative py-28 max-w-[1200px] mx-auto px-6 overflow-hidden select-none">
|
||||
<div className="absolute top-1/4 -left-10 w-[300px] h-[300px] bg-white/[0.01] rounded-full blur-[90px] pointer-events-none" />
|
||||
|
||||
<div className="mb-14">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="space-y-4"
|
||||
>
|
||||
<h2 className="font-sans text-4xl md:text-[50px] font-extrabold text-white tracking-tight">
|
||||
Selected Works
|
||||
</h2>
|
||||
<div className="w-20 h-1 bg-white" />
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-6 h-auto">
|
||||
{PROJECTS.map((project, index) => (
|
||||
<motion.div
|
||||
key={project.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||
className="contents"
|
||||
>
|
||||
{project.terminalCommand ? (
|
||||
<TerminalCard project={project} />
|
||||
) : project.image ? (
|
||||
<HeroCard project={project} />
|
||||
) : (
|
||||
<IconCard project={project} />
|
||||
)}
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
181
src/components/ThreeBg.tsx
Normal file
181
src/components/ThreeBg.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import * as THREE from "three";
|
||||
|
||||
export default function ThreeBg() {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const mouseRef = useRef({ x: -1000, y: -1000 });
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const container = containerRef.current;
|
||||
|
||||
// Create scene and camera
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.PerspectiveCamera(
|
||||
75,
|
||||
container.clientWidth / container.clientHeight,
|
||||
0.1,
|
||||
1000
|
||||
);
|
||||
camera.position.z = 5;
|
||||
|
||||
// Create renderer
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
||||
renderer.setSize(container.clientWidth, container.clientHeight);
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
container.appendChild(renderer.domElement);
|
||||
|
||||
// List of wireframe cubes
|
||||
const cubes: THREE.Mesh[] = [];
|
||||
const cubeCount = 45;
|
||||
const geometry = new THREE.BoxGeometry(0.7, 0.7, 0.7);
|
||||
|
||||
for (let i = 0; i < cubeCount; i++) {
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
color: 0xffffff,
|
||||
wireframe: true,
|
||||
transparent: true,
|
||||
opacity: 0.08,
|
||||
});
|
||||
|
||||
const cube = new THREE.Mesh(geometry, material);
|
||||
|
||||
// Position them randomly in a 3D box region
|
||||
cube.position.x = (Math.random() - 0.5) * 16;
|
||||
cube.position.y = (Math.random() - 0.5) * 24;
|
||||
cube.position.z = (Math.random() - 0.5) * 8;
|
||||
|
||||
cube.rotation.x = Math.random() * Math.PI;
|
||||
cube.rotation.y = Math.random() * Math.PI;
|
||||
|
||||
// Attach custom speed and original opacity specs
|
||||
cube.userData = {
|
||||
speed: 0.002 + Math.random() * 0.006,
|
||||
rotationSpeed: (Math.random() - 0.5) * 0.01,
|
||||
originalOpacity: 0.03 + Math.random() * 0.06,
|
||||
};
|
||||
|
||||
scene.add(cube);
|
||||
cubes.push(cube);
|
||||
}
|
||||
|
||||
// Animation flag
|
||||
let animationId: number;
|
||||
|
||||
const animate = () => {
|
||||
animationId = requestAnimationFrame(animate);
|
||||
|
||||
// Projects client-screen space coordinates to three.js world coordinates roughly
|
||||
const worldMouse = new THREE.Vector3(
|
||||
(mouseRef.current.x / container.clientWidth) * 2 - 1,
|
||||
-(mouseRef.current.y / container.clientHeight) * 2 + 1,
|
||||
0.5
|
||||
).unproject(camera);
|
||||
|
||||
cubes.forEach((cube) => {
|
||||
// Fall down
|
||||
cube.position.y -= cube.userData.speed;
|
||||
|
||||
// Spin
|
||||
cube.rotation.x += cube.userData.rotationSpeed;
|
||||
cube.rotation.y += cube.userData.rotationSpeed * 0.5;
|
||||
|
||||
// Proximity glowing logic
|
||||
const distance = cube.position.distanceTo(worldMouse);
|
||||
const proximityThreshold = 4.0;
|
||||
const mat = cube.material as THREE.MeshBasicMaterial;
|
||||
|
||||
if (distance < proximityThreshold) {
|
||||
const glow = 1 - distance / proximityThreshold;
|
||||
|
||||
// Smooth increment of wireframe opacity on nearest cubes
|
||||
mat.opacity = THREE.MathUtils.lerp(
|
||||
mat.opacity,
|
||||
cube.userData.originalOpacity + glow * 0.45,
|
||||
0.15
|
||||
);
|
||||
|
||||
// Subtle scaling swell effect on proximity
|
||||
const targetScale = 1 + glow * 0.25;
|
||||
cube.scale.setScalar(
|
||||
THREE.MathUtils.lerp(cube.scale.x, targetScale, 0.1)
|
||||
);
|
||||
} else {
|
||||
// Fade back to normal spec
|
||||
mat.opacity = THREE.MathUtils.lerp(
|
||||
mat.opacity,
|
||||
cube.userData.originalOpacity,
|
||||
0.1
|
||||
);
|
||||
cube.scale.setScalar(
|
||||
THREE.MathUtils.lerp(cube.scale.x, 1, 0.1)
|
||||
);
|
||||
}
|
||||
|
||||
// Loop screen heights wrap around
|
||||
if (cube.position.y < -12) {
|
||||
cube.position.y = 12;
|
||||
cube.position.x = (Math.random() - 0.5) * 16;
|
||||
}
|
||||
});
|
||||
|
||||
renderer.render(scene, camera);
|
||||
};
|
||||
|
||||
animate();
|
||||
|
||||
// Mouse movement listener
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
mouseRef.current.x = e.clientX;
|
||||
mouseRef.current.y = e.clientY;
|
||||
|
||||
// Mouse interactive spot glowing
|
||||
const glowEl = document.querySelector(".radial-glow") as HTMLDivElement;
|
||||
if (glowEl) {
|
||||
glowEl.style.background = `radial-gradient(circle at ${e.clientX}px ${e.clientY}px, rgba(255, 255, 255, 0.035) 0%, rgba(0, 0, 0, 0) 50%)`;
|
||||
}
|
||||
};
|
||||
|
||||
// Responsive frame resizing handler
|
||||
const handleResize = () => {
|
||||
if (!container) return;
|
||||
camera.aspect = container.clientWidth / container.clientHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(container.clientWidth, container.clientHeight);
|
||||
};
|
||||
|
||||
window.addEventListener("mousemove", handleMouseMove);
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
// Cleanups on unmount
|
||||
return () => {
|
||||
cancelAnimationFrame(animationId);
|
||||
window.removeEventListener("mousemove", handleMouseMove);
|
||||
window.removeEventListener("resize", handleResize);
|
||||
|
||||
// Cleanup assets to avoid WebGL context leakage
|
||||
if (renderer && renderer.domElement) {
|
||||
container.removeChild(renderer.domElement);
|
||||
}
|
||||
geometry.dispose();
|
||||
cubes.forEach((cube) => {
|
||||
cube.geometry.dispose();
|
||||
if (Array.isArray(cube.material)) {
|
||||
cube.material.forEach((mat) => mat.dispose());
|
||||
} else {
|
||||
cube.material.dispose();
|
||||
}
|
||||
});
|
||||
renderer.dispose();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
id="canvas-container"
|
||||
className="fixed inset-0 z-0 pointer-events-none"
|
||||
/>
|
||||
);
|
||||
}
|
||||
143
src/data.toml
Normal file
143
src/data.toml
Normal file
@@ -0,0 +1,143 @@
|
||||
[config]
|
||||
resume_pdf_url = ""
|
||||
|
||||
[[experiences]]
|
||||
id = "exp-1"
|
||||
period = "Jan 2026 — Present"
|
||||
role = "Founding Engineer"
|
||||
company = "Coguide Labs Inc."
|
||||
location = "Remote"
|
||||
description = "Leading technical architecture and engineering for AI-integrated development environments. Optimizing high-performance inference pipelines and distributed systems for real-time collaboration."
|
||||
tags = ["LLMs", "Distributed Systems", "Rust", "C++"]
|
||||
active = true
|
||||
|
||||
[[projects]]
|
||||
id = "proj-1"
|
||||
title = "Tensor Mutation LLMs"
|
||||
category = "AI Research"
|
||||
description = "Research on embedding tensor mutations in LLaMA 3B for low-cost mathematical computation and improved inference efficiency."
|
||||
tags = ["LLaMA", "Tensor Mutations", "PyTorch"]
|
||||
image = "https://lh3.googleusercontent.com/aida-public/AB6AXuCW1kH6ZRsNDQ_HWOkrbRgfWlUyujgwJwMV4H4ueDAN66MvGhJ2ZqQErgYpaS1O1OZyLgl-LPIoMBlVxhc5GzncvKqgILG9eVfjMrA2u7htWjObGhlwuaFacB6kAicPPHo5Dg507aUCscAE-bPFqss3LQN80PNI4k1UrMVEz878PIIEEjTTl4FfGB663Ru7_gSzc8P9r5EKzYaF-TOWOZ96EgC7f1fhLva80hlFr7ZDbIhdGfzBZdmIizR6p0tzXFyZfajEsCRPI4g"
|
||||
icon = "brain"
|
||||
|
||||
[[projects]]
|
||||
id = "proj-2"
|
||||
title = "Quant Analysis Streamer"
|
||||
category = "Analytics"
|
||||
description = "High-frequency platform sustaining 100K datapoints/s via Kafka and C++ integration."
|
||||
tags = ["C++", "Kafka", "Python"]
|
||||
icon = "line-chart"
|
||||
|
||||
[[projects]]
|
||||
id = "proj-3"
|
||||
title = "Matrix FS"
|
||||
category = "Distributed Storage"
|
||||
description = "Layer 2 distributed storage protocol on IPFS implemented in Rust for immutable data integrity."
|
||||
tags = ["Rust", "IPFS", "Storage"]
|
||||
icon = "hard-drive"
|
||||
|
||||
[[projects]]
|
||||
id = "proj-4"
|
||||
title = "SSH-Driven TUI Portfolio"
|
||||
category = "CLI Experience"
|
||||
description = "Interactive terminal showcase via SSH. Built with custom terminal UI components for a pure CLI experience."
|
||||
tags = ["SSH", "TUI", "Go/Rust"]
|
||||
terminalCommand = "ssh portfolio@krishna.ayyalasomayajula.net"
|
||||
image = "https://lh3.googleusercontent.com/aida-public/AB6AXuD8p39ytgPLw35Do26BAHv1074aDsxibSQLLvyjfRNsKW-3DeUMvlu-2CJB2JgG7TJ1TKyy2MMh6YW4JsLb3qTNHW0ZBoYax0yY80z1R1LgiftuMeSI9Kl6NqMDJBSbbtZ9Dt1E-CE5AvyH3LM63K8Dnr9O89EPKAaq-l-AA5JvHEVBC5IZ62moSY9mlEg2gwezgag2oa2X708pHmMYcWnuCpDsk1xVqZDBAot7VjDbV_o4YboE0pBYyT8nZHc74uoHS4gwkRzF8pY"
|
||||
icon = "terminal"
|
||||
[[skill_categories]]
|
||||
id = "skills-sys"
|
||||
number = "01"
|
||||
title = "Systems & Security"
|
||||
icon = "shield"
|
||||
skills = [
|
||||
{ name = "Rust (async/unsafe)", level = "expert" },
|
||||
{ name = "C++", level = "expert" },
|
||||
{ name = "Linux (Arch/Debian)", level = "expert" },
|
||||
{ name = "eBPF & RE", level = "advanced" }
|
||||
]
|
||||
|
||||
[[skill_categories]]
|
||||
id = "skills-quant"
|
||||
number = "02"
|
||||
title = "Quantitative & Math"
|
||||
icon = "function"
|
||||
skills = [
|
||||
{ name = "BLAS / LAPACK", level = "expert" },
|
||||
{ name = "CUDA & PyTorch", level = "expert" },
|
||||
{ name = "JAX / Tensor Mutations", level = "expert" },
|
||||
{ name = "ODE/PDE Solvers", level = "expert" }
|
||||
]
|
||||
|
||||
[[skill_categories]]
|
||||
id = "skills-infra"
|
||||
number = "03"
|
||||
title = "Infrastructure & Ops"
|
||||
icon = "dns"
|
||||
skills = [
|
||||
{ name = "Proxmox / NixOS", level = "expert" },
|
||||
{ name = "K3s & Zero-Trust", level = "expert" },
|
||||
{ name = "HAProxy", level = "expert" },
|
||||
{ name = "ClickHouse", level = "expert" }
|
||||
]
|
||||
|
||||
[[contact_links]]
|
||||
label = "Resume"
|
||||
value = "View & Print Resume"
|
||||
url = "https://git.cyber.ayyalasomayajula.net/marsultor/resume-cv/raw/branch/master/resume.pdf"
|
||||
cmd = "cat resume.md"
|
||||
icon = "file"
|
||||
|
||||
[[contact_links]]
|
||||
label = "LinkedIn"
|
||||
value = "linkedin.com/in/krishna-ayyalasomayajula"
|
||||
url = "https://linkedin.com/in/krishna-ayyalasomayajula"
|
||||
cmd = "ln -s"
|
||||
icon = "linkedin"
|
||||
|
||||
[[contact_links]]
|
||||
label = "Email"
|
||||
value = "krishna@ayyalasomayajula.net"
|
||||
url = "mailto:krishna@ayyalasomayajula.net"
|
||||
cmd = "ping -c1"
|
||||
icon = "mail"
|
||||
|
||||
# --- NEW SECTIONS BELOW ---
|
||||
|
||||
[hero]
|
||||
name = "Krishna"
|
||||
surname = "Ayyalasomayajula"
|
||||
tagline = "Systems Software Engineer & Quantitative Developer"
|
||||
background_image = "https://lh3.googleusercontent.com/aida-public/AB6AXuCO8edImcl4S7-eGhtqSc4GT0tqdjkCgXvQY7x1ADKnxOKaxz3wvG_1liVrJV8bVqWZ0vKgVpH8VciYynN5C054oLPmI0hhDPdE1HmtPHS3_ruEBlLNeJ7Jl3C5m_TFOpE27bxnJxTm-BOe3pbDRo4-tK7oaiYkOBajGLR1iqIwp9IRT0_f1nID_SbQ_a6vqPlBD-N66xALwYv2tElsFED9abScHFJRSCV_PMhCPbB0nThQjrBwcYzjeyzbsP0LOy1hmKNvrum2308"
|
||||
|
||||
[experience_heading]
|
||||
title = "Professional Trajectory"
|
||||
subtitle = "Experience"
|
||||
description = "An architectural journey through engineering complex systems, building scalable infrastructure, and refining low-latency environments at global scale."
|
||||
|
||||
[footer]
|
||||
copyright = "© 2026 Krishna Ayyalasomayajula. Built with precision."
|
||||
|
||||
[resume]
|
||||
name = "Krishna Ayyalasomayajula"
|
||||
title = "Systems Software Engineer & Quantitative Developer"
|
||||
location = "Plano, Texas, USA"
|
||||
email = "krishna@ayyalasomayajula.net"
|
||||
github = "github.com/krishna-ayyalasomayajula"
|
||||
|
||||
[[resume.experiences]]
|
||||
company = "Coguide Labs Inc."
|
||||
period = "Oct 2024 - Present"
|
||||
role = "Founding Engineer // Remote"
|
||||
description = "Lead developers on distributed execution compilers. Engineered fast real-time synchronization graphs for AI code completion models operating under extreme bandwidth constraints. Reduced overall websocket ingestion latency by 35% using low-latency multi-channel pipelines."
|
||||
|
||||
[[resume.research]]
|
||||
title = "Tensor Mutations inside low-cost parameter LLaMA clusters"
|
||||
venue = "Published on Deep Computing Labs // Co-Author"
|
||||
description = "Investigated architectural methods to inject algebraic tensor mutation matrix operations into weights during local runtimes, increasing accuracy in complex mathematical calculations by 18.2% on 3B models."
|
||||
|
||||
[resume.education]
|
||||
school = "University of Texas at Austin"
|
||||
years = "Class of 2030"
|
||||
degree = "Computer Science"
|
||||
skills = "Rust (async, memory safety), C++17/20, CUDA, PyTorch, JAX, Linux, NixOS, K3s, HAProxy, ClickHouse"
|
||||
31
src/data.ts
Normal file
31
src/data.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { parse } from "smol-toml";
|
||||
import {
|
||||
ExperienceItem, ProjectItem, SkillCategory, ContactLink, AppConfig,
|
||||
HeroContent, ExperienceHeading, FooterData, ResumeData
|
||||
} from "./types";
|
||||
// @ts-ignore
|
||||
import dataToml from "./data.toml?raw";
|
||||
|
||||
const parsed = (parse(dataToml) as unknown) as {
|
||||
config?: AppConfig;
|
||||
hero: HeroContent;
|
||||
experience_heading: ExperienceHeading;
|
||||
footer: FooterData;
|
||||
resume: ResumeData;
|
||||
experiences: ExperienceItem[];
|
||||
projects: ProjectItem[];
|
||||
skill_categories: SkillCategory[];
|
||||
contact_links: ContactLink[];
|
||||
};
|
||||
|
||||
export const APP_CONFIG: AppConfig = parsed.config || {};
|
||||
export const HERO: HeroContent = parsed.hero;
|
||||
export const EXPERIENCE_HEADING: ExperienceHeading = parsed.experience_heading;
|
||||
export const FOOTER_DATA: FooterData = parsed.footer;
|
||||
export const RESUME_DATA: ResumeData = parsed.resume;
|
||||
export const EXPERIENCES: ExperienceItem[] = parsed.experiences;
|
||||
export const PROJECTS: ProjectItem[] = parsed.projects;
|
||||
export const SKILL_CATEGORIES: SkillCategory[] = parsed.skill_categories;
|
||||
export const CONTACT_LINKS: ContactLink[] = parsed.contact_links;
|
||||
|
||||
|
||||
63
src/index.css
Normal file
63
src/index.css
Normal file
@@ -0,0 +1,63 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--font-sans: "IBM Plex Mono", "JetBrains Mono", ui-monospace, SFMono-Regular, monospace;
|
||||
--font-mono: "IBM Plex Mono", "JetBrains Mono", ui-monospace, SFMono-Regular, monospace;
|
||||
|
||||
/* Custom timing curves for premium animations */
|
||||
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
--ease-reveal: cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
/* Enable high-fidelity IBM Code font with optimized weight rendering */
|
||||
html, body, button, input, textarea, select, h1, h2, h3, h4, h5, h6, pre, code {
|
||||
font-family: "IBM Plex Mono", "JetBrains Mono", ui-monospace, SFMono-Regular, monospace;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* Custom interactive blueprint grid */
|
||||
.grid-pattern {
|
||||
background-image:
|
||||
linear-gradient(to right, rgba(255, 255, 255, 0.035) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(255, 255, 255, 0.035) 1px, transparent 1px);
|
||||
background-size: 40px 40px;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
/* Scroll reveal helper states */
|
||||
.reveal-on-scroll {
|
||||
opacity: 0;
|
||||
transform: translateY(24px);
|
||||
transition: opacity 1000ms cubic-bezier(0.22, 1, 0.36, 1),
|
||||
transform 1000ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
.reveal-on-scroll.active {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Fluid scrollbar style */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #000000;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #27272a;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #3f3f46;
|
||||
}
|
||||
|
||||
/* Selection custom styling */
|
||||
::selection {
|
||||
background-color: #ffffff;
|
||||
color: #000000;
|
||||
}
|
||||
10
src/main.tsx
Normal file
10
src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import {StrictMode} from 'react';
|
||||
import {createRoot} from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
import './index.css';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
105
src/types.ts
Normal file
105
src/types.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Type declarations for the Obsidian Technical Portfolio
|
||||
*/
|
||||
|
||||
export interface ExperienceItem {
|
||||
id: string;
|
||||
period: string;
|
||||
role: string;
|
||||
company: string;
|
||||
location: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
export interface ProjectItem {
|
||||
id: string;
|
||||
title: string;
|
||||
category: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
image?: string;
|
||||
icon?: string;
|
||||
link?: string;
|
||||
terminalCommand?: string;
|
||||
}
|
||||
|
||||
export interface SkillCategory {
|
||||
id: string;
|
||||
number: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
skills: {
|
||||
name: string;
|
||||
level: "expert" | "advanced" | "familiar";
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface InquiryForm {
|
||||
name: string;
|
||||
email: string;
|
||||
subject: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface ContactLink {
|
||||
label: string;
|
||||
value: string;
|
||||
url: string;
|
||||
cmd: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export interface AppConfig {
|
||||
resume_pdf_url?: string;
|
||||
}
|
||||
|
||||
export interface HeroContent {
|
||||
name: string;
|
||||
surname: string;
|
||||
tagline: string;
|
||||
background_image: string;
|
||||
}
|
||||
|
||||
export interface ExperienceHeading {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface FooterData {
|
||||
copyright: string;
|
||||
}
|
||||
|
||||
export interface ResumeExperience {
|
||||
company: string;
|
||||
period: string;
|
||||
role: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface ResumeResearch {
|
||||
title: string;
|
||||
venue: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface ResumeEducation {
|
||||
school: string;
|
||||
years: string;
|
||||
degree: string;
|
||||
skills: string;
|
||||
}
|
||||
|
||||
export interface ResumeData {
|
||||
name: string;
|
||||
title: string;
|
||||
location: string;
|
||||
email: string;
|
||||
github: string;
|
||||
experiences: ResumeExperience[];
|
||||
research: ResumeResearch[];
|
||||
education: ResumeEducation;
|
||||
}
|
||||
|
||||
26
tsconfig.json
Normal file
26
tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"allowJs": true,
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
22
vite.config.ts
Normal file
22
vite.config.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'path';
|
||||
import {defineConfig} from 'vite';
|
||||
|
||||
export default defineConfig(() => {
|
||||
return {
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, '.'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
// HMR is disabled in AI Studio via DISABLE_HMR env var.
|
||||
// Do not modifyâfile watching is disabled to prevent flickering during agent edits.
|
||||
hmr: process.env.DISABLE_HMR !== 'true',
|
||||
// Disable file watching when DISABLE_HMR is true to save CPU during agent edits.
|
||||
watch: process.env.DISABLE_HMR === 'true' ? null : {},
|
||||
},
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user