diff --git a/index.html b/index.html
index b1db749..9548cd5 100644
--- a/index.html
+++ b/index.html
@@ -49,7 +49,7 @@
diff --git a/src/App.tsx b/src/App.tsx
index d1d71f1..84e881b 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,23 +1,25 @@
-import React from 'react';
-import { BrowserRouter } from 'react-router-dom';
+import React, { useState, useCallback } from 'react';
+import { AnimatePresence } from 'framer-motion';
import { ThemeProvider } from './context/ThemeContext';
-import Navbar from './components/Navbar/Navbar';
-import ThemeSwitcher from './components/ThemeSwitcher/ThemeSwitcher';
-import FloatingChat from './components/FloatingChat/FloatingChat';
-import AppRouter from './router';
+import LoadingScreen from './components/LoadingScreen/LoadingScreen';
+import Terminal from './components/Terminal/Terminal';
import './styles/global.scss';
const App: React.FC = () => {
+ const [loading, setLoading] = useState(true);
+ const handleComplete = useCallback(() => setLoading(false), []);
+
return (
-
-
-
+
+
+
+ {loading && (
+
+ )}
+
+
+ {!loading && }
);
};
diff --git a/src/components/ContactForm/ContactForm.scss b/src/components/ContactForm/ContactForm.scss
deleted file mode 100644
index 79a766c..0000000
--- a/src/components/ContactForm/ContactForm.scss
+++ /dev/null
@@ -1,135 +0,0 @@
-@import '../../styles/mixins/glass';
-
-.contact-container {
- display: flex;
- gap: 50px;
- max-width: 1000px;
- margin: 0 auto;
- padding: 20px;
- align-items: flex-start;
-
- .contact-info {
- flex: 1;
-
- h2 {
- font-size: 2.5rem;
- margin-bottom: 15px;
- color: var(--fg);
- }
-
- p {
- color: var(--muted);
- margin-bottom: 30px;
- font-size: 1.1rem;
- }
-
- .info-items {
- display: flex;
- flex-direction: column;
- gap: 15px;
-
- .info-card {
- display: flex;
- align-items: center;
- gap: 15px;
- padding: 20px;
- transition: transform 0.2s;
-
- .icon {
- color: var(--primary);
- }
-
- span {
- color: var(--fg);
- }
-
- &:hover {
- transform: translateX(5px);
- border-color: var(--primary);
- }
- }
- }
- }
-
- .form-wrapper {
- flex: 1.2;
-
- .contact-form-card {
- padding: 30px;
-
- form {
- display: flex;
- flex-direction: column;
- gap: 20px;
-
- .form-group {
- display: flex;
- flex-direction: column;
- gap: 8px;
-
- label {
- font-size: 0.9rem;
- color: var(--muted);
- font-weight: 500;
- }
-
- input, textarea {
- background: rgba(255, 255, 255, 0.05);
- border: 1px solid var(--glass-border);
- padding: 12px 15px;
- border-radius: 12px;
- color: var(--fg);
- font-family: inherit;
- outline: none;
- transition: all 0.3s ease;
-
- &:focus {
- border-color: var(--primary);
- box-shadow: 0 0 10px rgba(0, 123, 255, 0.1);
- background: rgba(255, 255, 255, 0.1);
- }
-
- &::placeholder {
- color: rgba(150, 150, 150, 0.5);
- }
- }
-
- textarea {
- resize: vertical;
- }
- }
-
- .submit-btn {
- margin-top: 10px;
- background: var(--primary);
- color: white;
- padding: 12px;
- border-radius: 12px;
- font-weight: 600;
- display: flex;
- justify-content: center;
- align-items: center;
- gap: 10px;
- transition: all 0.3s ease;
-
- &:hover {
- transform: translateY(-2px);
- box-shadow: 0 5px 15px rgba(0, 123, 255, 0.3);
- }
- }
- }
- }
- }
-
- @media (max-width: 768px) {
- flex-direction: column;
-
- .contact-info {
- width: 100%;
- }
-
- .form-wrapper {
- width: 100%;
- }
- }
-}
diff --git a/src/components/ContactForm/ContactForm.tsx b/src/components/ContactForm/ContactForm.tsx
deleted file mode 100644
index c5a308d..0000000
--- a/src/components/ContactForm/ContactForm.tsx
+++ /dev/null
@@ -1,116 +0,0 @@
-import React, { useState } from 'react';
-import { motion } from 'framer-motion';
-import { Send, Mail, Phone, MapPin } from 'lucide-react';
-import GlassCard from '../GlassCard/GlassCard';
-import { resumeData } from '../../data/resume';
-import './ContactForm.scss';
-import { sendEmail } from '../../services/email'
-
-const ContactForm: React.FC = () => {
- const [formData, setFormData] = useState({
- name: '',
- email: '',
- message: ''
- });
-
- const handleSubmit = (e: React.FormEvent) => {
- e.preventDefault();
- const templateParams = {
- name: formData.name,
- email: formData.email,
- subject: "Hey, hello",
- message: formData.message,
- };
- try {
- sendEmail(templateParams);
- setFormData({ name: '', email: '', message: '' });
- } catch (error) {
- console.error('Failed to send email:', error);
- }
- };
-
- return (
-
-
-
- Get in Touch
- Have a project in mind or just want to say hi? Feel free to reach out!
-
-
-
-
- {resumeData.contact.email}
-
-
-
-
- {resumeData.contact.phone}
-
-
-
-
- {resumeData.location}
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default ContactForm;
diff --git a/src/components/FloatingChat/FloatingChat.scss b/src/components/FloatingChat/FloatingChat.scss
deleted file mode 100644
index e1ae482..0000000
--- a/src/components/FloatingChat/FloatingChat.scss
+++ /dev/null
@@ -1,157 +0,0 @@
-@import '../../styles/mixins/glass';
-
-.floating-chat-container {
- position: fixed;
- bottom: 30px;
- right: 30px;
- z-index: 2000;
- display: flex;
- flex-direction: column;
- align-items: flex-end;
-
- .chat-toggle-btn {
- @include glass-card;
- width: 60px;
- height: 60px;
- border-radius: 50%;
- display: flex;
- justify-content: center;
- align-items: center;
- color: var(--primary);
- background: var(--glass-bg);
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
-
- &:hover {
- box-shadow: 0 0 20px var(--primary);
- }
- }
-
- .chat-window {
- @include glass-card;
- width: 350px;
- height: 500px;
- margin-bottom: 20px;
- display: flex;
- flex-direction: column;
- overflow: hidden;
- border-radius: 20px;
-
- .chat-header {
- padding: 15px;
- border-bottom: 1px solid var(--glass-border);
- display: flex;
- justify-content: space-between;
- align-items: center;
- background: rgba(255, 255, 255, 0.05);
-
- .ai-info {
- display: flex;
- align-items: center;
- gap: 10px;
- font-weight: 600;
- color: var(--primary);
- }
-
- button {
- color: var(--muted);
- &:hover { color: var(--fg); }
- }
- }
-
- .chat-messages {
- flex: 1;
- padding: 15px;
- overflow-y: auto;
- display: flex;
- flex-direction: column;
- gap: 15px;
- @include custom-scrollbar;
-
- .message {
- display: flex;
- flex-direction: column;
- max-width: 80%;
-
- &.user {
- align-self: flex-end;
- align-items: flex-end;
-
- .bubble {
- background: var(--primary);
- color: white;
- border-radius: 15px 15px 0 15px;
- }
- }
-
- &.ai {
- align-self: flex-start;
-
- .bubble {
- background: rgba(255, 255, 255, 0.1);
- border: 1px solid var(--glass-border);
- border-radius: 15px 15px 15px 0;
- overflow-wrap: anywhere;
- }
- }
-
- .bubble {
- padding: 10px 15px;
- font-size: 0.9rem;
- line-height: 1.4;
-
- &.typing {
- display: flex;
- gap: 5px;
- span {
- animation: bounce 1s infinite;
- &:nth-child(2) { animation-delay: 0.2s; }
- &:nth-child(3) { animation-delay: 0.4s; }
- }
- }
- }
-
- .time {
- font-size: 0.7rem;
- color: var(--muted);
- margin-top: 5px;
- }
- }
- }
-
- .chat-input {
- padding: 15px;
- border-top: 1px solid var(--glass-border);
- display: flex;
- gap: 10px;
- align-items: center;
-
- input {
- flex: 1;
- background: rgba(255, 255, 255, 0.05);
- border: 1px solid var(--glass-border);
- padding: 10px;
- border-radius: 20px;
- color: var(--fg);
- outline: none;
-
- &:focus {
- border-color: var(--primary);
- }
- }
-
- button {
- color: var(--muted);
- &:hover { color: var(--primary); }
-
- &.send-btn {
- color: var(--primary);
- }
- }
- }
- }
-}
-
-@keyframes bounce {
- 0%, 100% { transform: translateY(0); }
- 50% { transform: translateY(-5px); }
-}
diff --git a/src/components/FloatingChat/FloatingChat.tsx b/src/components/FloatingChat/FloatingChat.tsx
deleted file mode 100644
index 49b4243..0000000
--- a/src/components/FloatingChat/FloatingChat.tsx
+++ /dev/null
@@ -1,271 +0,0 @@
-import React, { useState, useRef, useEffect } from 'react';
-import { motion, AnimatePresence } from 'framer-motion';
-import { MessageSquare, Send, X, Bot } from 'lucide-react';
-import './FloatingChat.scss';
-
-// Assuming these imports are correct based on the original code
-import { runRAG } from '../../services/rag';
-// NOTE: GoogleGenAI is no longer needed in the component as runRAG handles the call.
-// import { GoogleGenAI } from '@google/genai';
-
-interface Message {
- id: string;
- text: string;
- sender: 'user' | 'ai';
- timestamp: Date;
-}
-
-// NOTE: The separate verifyApiKey function is no longer needed
-// as validation is done by attempting to run the RAG query.
-
-const FloatingChat: React.FC = () => {
- const [isOpen, setIsOpen] = useState(false);
- const [input, setInput] = useState('');
- const [apiKey, setApiKey] = useState(() => sessionStorage.getItem("geminiKey") || "");
- const [awaitingApiKey, setAwaitingApiKey] = useState(false);
- const [pendingQuestion, setPendingQuestion] = useState(null);
-
- const [messages, setMessages] = useState([
- {
- id: '1',
- text: "Hi! I'm Rahul's folio.ai. Ask me anything about his skills, projects, or experience.",
- sender: 'ai',
- timestamp: new Date()
- }
- ]);
-
- const [isTyping, setIsTyping] = useState(false);
- const messagesEndRef = useRef(null);
-
- const scrollToBottom = () => {
- messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
- };
-
- useEffect(() => {
- scrollToBottom();
- }, [messages]);
-
- const runRagQuery = async (key: string, question: string) => {
- setIsTyping(true);
-
- try {
- const response = await runRAG(key, question);
-
- setMessages(prev => [
- ...prev,
- {
- id: Date.now().toString(),
- text: response.text,
- sender: 'ai',
- timestamp: new Date()
- }
- ]);
- } catch (e) {
- console.error(e)
- } finally {
- setIsTyping(false);
- }
- }
-
-
- const handleSend = async () => {
- if (!input.trim()) return;
-
- const userQuestionOrKey = input;
- setInput('');
-
- const userMsg: Message = {
- id: Date.now().toString(),
- text: userQuestionOrKey,
- sender: 'user',
- timestamp: new Date()
- };
- setMessages(prev => [...prev, userMsg]);
- scrollToBottom();
- setApiKey(sessionStorage.getItem("geminiKey") || "")
-
- if (awaitingApiKey) {
- const keyAttempt = userQuestionOrKey.trim();
- setAwaitingApiKey(false);
-
- const questionToRun = pendingQuestion;
- setPendingQuestion(null);
-
- setMessages(prev => [
- ...prev,
- {
- id: "checking_key",
- text: `⏳ Verifying key and attempting to answer the saved question...`,
- sender: "ai",
- timestamp: new Date(),
- }
- ]);
-
- scrollToBottom();
-
- let valid = false;
- if (questionToRun) {
- try {
- const response = await runRAG(keyAttempt, questionToRun);
-
- if (response?.text?.includes("Please check your API key")) {
- return
- }
- valid = true;
-
- sessionStorage.setItem("geminiKey", keyAttempt);
- setApiKey(keyAttempt);
-
- const verifiedMessage: Message = {
- id: "verified",
- text: "🎉 API Key verified! Here is the answer to your question:",
- sender: "ai",
- timestamp: new Date(),
- };
-
- const answerMessage: Message = {
- id: Date.now().toString(),
- text: response.text,
- sender: 'ai',
- timestamp: new Date()
- }
-
- setMessages(prev => [...prev.filter(m => m.id !== "checking_key"), verifiedMessage, answerMessage]);
-
- } catch (err) {
- valid = false;
- console.error("API Key verification via RAG failed:", err);
- }
- }
-
- if (!valid) {
- setAwaitingApiKey(true);
-
- if (questionToRun) setPendingQuestion(questionToRun);
-
- setMessages(prev => [
- ...prev.filter(m => m.id !== "checking_key"),
- {
- id: "invalid_key",
- text: "❌ Invalid API key. The attempt to run your question failed. Please try a different key.",
- sender: "ai",
- timestamp: new Date(),
- }
- ]);
- return;
- }
-
- return;
- }
-
- if (!apiKey) {
- setAwaitingApiKey(true);
- setPendingQuestion(userQuestionOrKey);
-
- setMessages(prev => [
- ...prev,
- {
- id: `request_api-${Date.now()}`,
- text: "🔑 Please enter your Gemini API key to continue. Your question has been saved and will run automatically after verification.",
- sender: "ai",
- timestamp: new Date(),
- },
- {
- id: "api_link",
- text: "You can create one for free at:\nhttps://aistudio.google.com/app/apikey",
- sender: "ai",
- timestamp: new Date(),
- },
- {
- id: "api_notice",
- text: "🔒 Your key is never stored on servers — only your browser.\n🧹 We recommend deleting it after use. Your question has been saved and will run automatically after verification.",
- sender: "ai",
- timestamp: new Date(),
- }
- ]);
- return;
- }
-
- if (apiKey) {
- await runRagQuery(apiKey, userQuestionOrKey);
- }
- };
-
- return (
-
-
- {isOpen && (
-
-
-
-
- Folio.ai
-
-
setIsOpen(false)} aria-label="Close chat">
-
-
-
-
- {/* Chat Messages */}
-
- {messages.map((msg) => (
-
-
{msg.text}
-
- {msg.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
-
-
- ))}
-
- {isTyping && (
-
- )}
-
-
-
-
- {/* Chat Input */}
-
- setInput(e.target.value)}
- onKeyPress={(e) => e.key === 'Enter' && handleSend()}
- aria-label="Chat message input"
- // Use the input type for API key entry to obscure the key (though security is minimal on client-side localStorage)
- // We keep it as 'text' for ease of copy/paste unless you explicitly want 'password'.
- // type={awaitingApiKey ? "password" : "text"}
- />
-
-
-
-
-
- )}
-
-
- {/* Floating Toggle Button */}
-
setIsOpen(!isOpen)}
- whileHover={{ scale: 1.1 }}
- whileTap={{ scale: 0.9 }}
- aria-label={isOpen ? "Close chat" : "Open chat"}
- >
- {isOpen ? : }
-
-
- );
-};
-
-export default FloatingChat;
\ No newline at end of file
diff --git a/src/components/GlassCard/GlassCard.scss b/src/components/GlassCard/GlassCard.scss
deleted file mode 100644
index 7f7ed5b..0000000
--- a/src/components/GlassCard/GlassCard.scss
+++ /dev/null
@@ -1,12 +0,0 @@
-@import '../../styles/mixins/glass';
-
-.glass-card {
- @include glass-card;
- padding: var(--spacing-md);
- transition: box-shadow 0.3s ease, border-color 0.3s ease;
-
- &:hover {
- border-color: var(--edge-glow);
- box-shadow: var(--glass-shadow), 0 0 20px var(--edge-glow);
- }
-}
diff --git a/src/components/GlassCard/GlassCard.tsx b/src/components/GlassCard/GlassCard.tsx
deleted file mode 100644
index cf56a7e..0000000
--- a/src/components/GlassCard/GlassCard.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import React from 'react';
-import { motion } from 'framer-motion';
-import './GlassCard.scss';
-
-interface GlassCardProps {
- children: React.ReactNode;
- className?: string;
- hoverEffect?: boolean;
- delay?: number;
-}
-
-const GlassCard: React.FC = ({
- children,
- className = '',
- hoverEffect = true,
- delay = 0
-}) => {
- return (
-
- {children}
-
- );
-};
-
-export default GlassCard;
diff --git a/src/components/Hero/Hero.scss b/src/components/Hero/Hero.scss
deleted file mode 100644
index 8badb97..0000000
--- a/src/components/Hero/Hero.scss
+++ /dev/null
@@ -1,301 +0,0 @@
-@import '../../styles/mixins/glass';
-
-.hero-section {
- min-height: 100vh;
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 0 10%;
- gap: 50px;
- position: relative;
- overflow: hidden;
-
- /* Background Elements */
- &::before {
- content: '';
- position: absolute;
- top: -10%;
- right: -5%;
- width: 500px;
- height: 500px;
- background: radial-gradient(circle, var(--primary) 0%, transparent 70%);
- opacity: 0.1;
- filter: blur(80px);
- z-index: -1;
- }
-
- &::after {
- content: '';
- position: absolute;
- bottom: -10%;
- left: -10%;
- width: 600px;
- height: 600px;
- background: radial-gradient(circle, var(--accent) 0%, transparent 70%);
- opacity: 0.1;
- filter: blur(100px);
- z-index: -1;
- }
-
- .hero-content {
- flex: 1;
- max-width: 600px;
- z-index: 10;
-
- .greeting {
- margin-bottom: 10px;
-
- .hello {
- font-size: 1.2rem;
- color: var(--primary);
- font-weight: 600;
- letter-spacing: 2px;
- text-transform: uppercase;
- display: block;
- margin-bottom: 10px;
- }
-
- .name {
- font-size: 4.5rem;
- font-weight: 800;
- line-height: 1.1;
- color: var(--fg);
-
- .dot {
- color: var(--primary);
- }
- }
- }
-
- .role {
- font-size: 2rem;
- font-weight: 500;
- color: var(--muted);
- margin-bottom: 20px;
-
- .separator {
- color: var(--primary);
- }
- }
-
- .bio {
- font-size: 1.1rem;
- line-height: 1.6;
- color: var(--muted);
- margin-bottom: 40px;
- max-width: 90%;
- }
-
- .cta-group {
- display: flex;
- gap: 20px;
- margin-bottom: 40px;
-
- .primary-btn,
- .secondary-btn {
- padding: 15px 30px;
- border-radius: 30px;
- font-weight: 600;
- display: flex;
- align-items: center;
- gap: 10px;
- transition: all 0.3s ease;
- }
-
- .primary-btn {
- background: var(--primary);
- color: white;
- box-shadow: 0 4px 15px rgba(0, 123, 255, 0.3);
-
- &:hover {
- transform: translateY(-2px);
- box-shadow: 0 8px 25px rgba(0, 123, 255, 0.4);
- }
- }
-
- .secondary-btn {
- @include glass-card;
- color: var(--fg);
- border: 1px solid var(--glass-border);
-
- &:hover {
- background: rgba(255, 255, 255, 0.1);
- border-color: var(--primary);
- color: var(--primary);
- }
- }
- }
-
- .social-links {
- display: flex;
- gap: 20px;
-
- a {
- width: 50px;
- height: 50px;
- border-radius: 50%;
- @include glass-card;
- display: flex;
- justify-content: center;
- align-items: center;
- color: var(--muted);
- transition: all 0.3s ease;
-
- &:hover {
- color: var(--primary);
- border-color: var(--primary);
- transform: translateY(-3px);
- box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
- }
- }
- }
- }
-
- .hero-visual {
- flex: 1;
- position: relative;
- height: 500px;
- display: flex;
- justify-content: center;
- align-items: center;
-
- .visual-card {
- position: absolute;
-
- &.main-card {
- width: 350px;
- height: 220px;
- z-index: 2;
- display: flex;
- align-items: center;
- justify-content: center;
- background: rgba(20, 20, 20, 0.8);
- /* Darker for code contrast */
- border: 1px solid rgba(255, 255, 255, 0.1);
-
- .code-snippet {
- font-family: 'Fira Code', monospace;
- font-size: 0.9rem;
- color: #e0e0e0;
-
- .keyword {
- color: #ff79c6;
- }
-
- .variable {
- color: #8be9fd;
- }
-
- .property {
- color: #f1fa8c;
- }
-
- .string {
- color: #50fa7b;
- }
- }
- }
-
- &.float-card-1 {
- top: 50px;
- right: 20px;
- padding: 20px;
- z-index: 3;
- animation: float 6s ease-in-out infinite;
- }
-
- &.float-card-2 {
- bottom: 50px;
- left: 20px;
- padding: 20px;
- z-index: 1;
- animation: float 8s ease-in-out infinite reverse;
- }
-
- .stat {
- text-align: center;
-
- .number {
- display: block;
- font-size: 2rem;
- font-weight: 700;
- color: var(--primary);
- }
-
- .label {
- font-size: 0.8rem;
- color: var(--muted);
- text-transform: uppercase;
- letter-spacing: 1px;
- }
- }
- }
- }
-
- @keyframes float {
-
- 0%,
- 100% {
- transform: translateY(0);
- }
-
- 50% {
- transform: translateY(-20px);
- }
- }
-
- @media (max-width: 1024px) {
- flex-direction: column-reverse;
- justify-content: center;
- padding: 100px 20px;
- text-align: center;
- gap: 40px;
-
- .hero-content {
- align-items: center;
- display: flex;
- flex-direction: column;
-
- .cta-group {
- justify-content: center;
- }
-
- .social-links {
- justify-content: center;
- }
- }
-
- .hero-visual {
- width: 100%;
- height: 400px;
- margin-top: 20px;
- margin-bottom: 80px;
-
- .visual-card {
- &.float-card-2 {
- z-index: 3;
- }
-
- }
- }
- }
-
- @media (max-width: 768px) {
- .hero-content {
- .greeting .name {
- font-size: 3rem;
- }
-
- .role {
- font-size: 1.5rem;
- }
- }
-
- .hero-visual {
- .main-card {
- width: 280px;
- font-size: 0.8rem;
- }
- }
- }
-}
\ No newline at end of file
diff --git a/src/components/Hero/Hero.tsx b/src/components/Hero/Hero.tsx
deleted file mode 100644
index bf77bff..0000000
--- a/src/components/Hero/Hero.tsx
+++ /dev/null
@@ -1,123 +0,0 @@
-import React from 'react';
-import { motion } from 'framer-motion';
-import { ArrowRight, Github, Linkedin, Mail, FileText } from 'lucide-react';
-import GlassCard from '../GlassCard/GlassCard';
-import { resumeData } from '../../data/resume';
-import './Hero.scss';
-import { Link } from 'react-router-dom';
-
-const Hero: React.FC = () => {
- const containerVariants = {
- hidden: { opacity: 0 },
- visible: {
- opacity: 1,
- transition: {
- staggerChildren: 0.2,
- },
- },
- };
-
- const itemVariants = {
- hidden: { y: 20, opacity: 0 },
- visible: {
- y: 0,
- opacity: 1,
- transition: {
- duration: 0.5,
- ease: "easeOut",
- },
- },
- };
-
- return (
-
-
-
- Hello, I'm
-
- {resumeData.name.split(' ')[0]}
- .
-
-
-
-
- {resumeData.hero.role1} & {resumeData.hero.role2}
-
-
-
- {resumeData.hero.bio}
-
-
-
-
- View Work
-
-
- Contact Me
-
-
-
-
-
-
- GitHub Profile
-
-
-
- LinkedIn Profile
-
-
-
- Send Email
-
-
-
- Download Resume
-
-
-
-
-
-
-
-
-
- const developer = {'{'}
- {'\n'} name : "{resumeData.hero.name}" ,
- {'\n'} skills : [{resumeData.hero.skills1} , {resumeData.hero.skills2} , {resumeData.hero.skills3} ],
- {'\n'} passion : {resumeData.hero.passion}
- {'\n'}{'}'};
-
-
-
-
-
-
-
- {resumeData.hero.exp}+
- Years Exp
-
-
-
-
-
- {resumeData.projects.length}+
- Projects
-
-
-
-
- );
-};
-
-export default Hero;
diff --git a/src/components/LoadingScreen/LoadingScreen.scss b/src/components/LoadingScreen/LoadingScreen.scss
new file mode 100644
index 0000000..8f2fc54
--- /dev/null
+++ b/src/components/LoadingScreen/LoadingScreen.scss
@@ -0,0 +1,99 @@
+.loading-screen {
+ position: fixed;
+ inset: 0;
+ z-index: 9998;
+ background-color: var(--bg);
+ background-image: var(--bg-gradient);
+ background-size: var(--bg-size, auto);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-family: var(--font-mono);
+
+ .ls-content {
+ width: min(480px, 90vw);
+ display: flex;
+ flex-direction: column;
+ gap: 28px;
+ }
+
+ /* Brand */
+ .ls-brand {
+ font-size: 1.3rem;
+ font-weight: 700;
+ color: var(--primary);
+ letter-spacing: 1px;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ text-shadow: 0 0 12px rgba(0, 240, 255, 0.45);
+
+ .ls-cursor {
+ color: var(--primary);
+ animation: blink 1s step-end infinite;
+ font-weight: 400;
+ }
+ }
+
+ /* Boot lines */
+ .ls-lines {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ min-height: 92px; /* reserve space so layout doesn't jump */
+
+ .ls-line {
+ font-size: 0.78rem;
+ line-height: 1.5;
+
+ .ls-ts { color: var(--muted); opacity: 0.7; }
+ .ls-msg { color: var(--outline); }
+ }
+ }
+
+ /* Progress bar */
+ .ls-progress-row {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+
+ .ls-bar {
+ font-size: 0.85rem;
+ color: var(--primary);
+ letter-spacing: -1px;
+ line-height: 1;
+
+ .ls-empty {
+ color: var(--border-color);
+ opacity: 0.45;
+ }
+ }
+
+ .ls-pct {
+ font-size: 0.72rem;
+ color: var(--primary);
+ min-width: 38px;
+ opacity: 0.85;
+ }
+ }
+
+ /* Status label */
+ .ls-status {
+ font-size: 0.68rem;
+ letter-spacing: 3px;
+ color: var(--muted);
+ opacity: 0.6;
+ }
+
+ .ls-skip {
+ font-size: 0.65rem;
+ color: var(--muted);
+ opacity: 0.4;
+ margin-top: -12px;
+ }
+
+ @keyframes blink {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0; }
+ }
+}
diff --git a/src/components/LoadingScreen/LoadingScreen.tsx b/src/components/LoadingScreen/LoadingScreen.tsx
new file mode 100644
index 0000000..ae23f62
--- /dev/null
+++ b/src/components/LoadingScreen/LoadingScreen.tsx
@@ -0,0 +1,114 @@
+import React, { useEffect, useState } from 'react';
+import { motion } from 'framer-motion';
+import './LoadingScreen.scss';
+
+interface Props {
+ onComplete: () => void;
+}
+
+const BOOT_LINES = [
+ { ts: '0.0001', msg: 'Initializing RahulOS...' },
+ { ts: '0.0042', msg: 'Loading kernel...' },
+ { ts: '0.0125', msg: 'Loading React...' },
+ { ts: '0.0210', msg: 'Loading portfolio...' },
+ { ts: '0.0340', msg: 'Connecting to GitHub...' },
+ { ts: '0.0480', msg: 'Loading projects...' },
+ { ts: '0.0610', msg: 'Loading experience...' },
+ { ts: '0.0700', msg: 'System Ready.' },
+];
+
+const BAR_BLOCKS = 20;
+const BOOT_DURATION = 2200;
+
+const LoadingScreen: React.FC = ({ onComplete }) => {
+ const [progress, setProgress] = useState(0);
+ const [lineCount, setLineCount] = useState(0);
+ const [skipped, setSkipped] = useState(false);
+
+ const skip = React.useCallback(() => {
+ if (skipped) return;
+ setSkipped(true);
+ setProgress(100);
+ setLineCount(BOOT_LINES.length);
+ onComplete();
+ }, [skipped, onComplete]);
+
+ useEffect(() => {
+ const tickMs = 60;
+ const step = 100 / (BOOT_DURATION / tickMs);
+ const progressTimer = setInterval(() => {
+ setProgress(p => {
+ const next = p + step;
+ if (next >= 100) {
+ clearInterval(progressTimer);
+ setTimeout(onComplete, 350);
+ return 100;
+ }
+ return next;
+ });
+ }, tickMs);
+
+ const lineTimers = BOOT_LINES.map((_, i) =>
+ setTimeout(() => setLineCount(i + 1), (BOOT_DURATION / BOOT_LINES.length) * i)
+ );
+
+ window.addEventListener('keydown', skip);
+ window.addEventListener('click', skip);
+
+ return () => {
+ clearInterval(progressTimer);
+ lineTimers.forEach(clearTimeout);
+ window.removeEventListener('keydown', skip);
+ window.removeEventListener('click', skip);
+ };
+ }, [onComplete, skip]);
+
+ const filled = Math.round((progress / 100) * BAR_BLOCKS);
+ const empty = BAR_BLOCKS - filled;
+
+ return (
+
+
+
+ RAHUL_OS_V2.0
+ ▋
+
+
+
+ {BOOT_LINES.slice(0, lineCount).map((line, i) => (
+
+ [{line.ts}]
+ {line.msg}
+
+ ))}
+
+
+
+
+ {'█'.repeat(filled)}
+ {'░'.repeat(empty)}
+
+ {Math.round(progress)}%
+
+
+
+ {progress < 100 ? 'BOOTING...' : 'READY'}
+
+
+
press any key to skip
+
+
+ );
+};
+
+export default LoadingScreen;
diff --git a/src/components/Navbar/Navbar.scss b/src/components/Navbar/Navbar.scss
deleted file mode 100644
index 7d5011d..0000000
--- a/src/components/Navbar/Navbar.scss
+++ /dev/null
@@ -1,125 +0,0 @@
-@import '../../styles/mixins/glass';
-
-.navbar {
- @include glass-card;
- position: fixed;
- top: 20px;
- left: 40%;
- width: min(60vw, 1200px);
- height: 70px;
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 0 20px;
- z-index: 1000;
- border-radius: 50px 0 0 50px;
- gap: 20px;
-
- .logo {
- font-size: 1.5rem;
- font-weight: 700;
- color: var(--fg);
-
- span {
- color: var(--primary);
- }
- }
-
- .nav-links {
- display: flex;
- width: 100%;
- justify-content: space-between;
- list-style: none;
- gap: 10px;
-
- li {
- position: relative;
-
- a {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 10px 20px;
- border-radius: 30px;
- color: var(--muted);
- transition: color 0.3s ease;
- position: relative;
- z-index: 1;
- @media (max-width: 1327px) {
- padding: 10px;
- }
-
- .icon {
- display: flex;
- align-items: center;
- }
-
- .label {
- font-weight: 500;
- }
-
- &:hover {
- color: var(--fg);
- }
-
- &.active {
- color: var(--fg);
- }
-
- .active-indicator {
- position: absolute;
- bottom: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(255, 255, 255, 0.1);
- border-radius: 30px;
- z-index: -1;
- border: 1px solid var(--glass-border);
- }
- }
- }
- }
-
- @media (max-width: 1022px) {
- gap: 30px;
-
- .label {
- display: none;
- }
- }
-
- @media (max-width: 768px) {
- padding: 0 10px;
-
- .logo, .label {
- display: none;
- }
-
- .nav-links {
- width: 100%;
- justify-content: space-between;
- gap: 0;
-
- li a {
- padding: 10px;
- justify-content: center;
-
- .icon {
- width: 24px;
- height: 24px;
- justify-content: center;
- }
- }
- }
- }
-}
-
- @media (max-width: 425px) {
- .navbar {
- gap: 0px;
- width: min(80vw, 1200px);
- left: 20%;
- }
- }
-
diff --git a/src/components/Navbar/Navbar.tsx b/src/components/Navbar/Navbar.tsx
deleted file mode 100644
index 45aa5d2..0000000
--- a/src/components/Navbar/Navbar.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import React from 'react';
-import { NavLink, Link } from 'react-router-dom';
-import { motion } from 'framer-motion';
-import { Home, Compass, Code, Layers, Mail } from 'lucide-react';
-import './Navbar.scss';
-import { resumeData } from '../../data/resume';
-
-
-const navItems = [
- { path: '/', label: 'Home', icon: },
- { path: '/journey', label: 'Journey', icon: },
- { path: '/skills', label: 'Skills', icon: },
- { path: '/projects', label: 'Projects', icon: },
- { path: '/contact', label: 'Contact', icon: },
-];
-
-const Navbar: React.FC = () => {
- return (
-
-
-
- {resumeData.hero.navName1}{resumeData.hero.navName2}
-
-
-
-
- {navItems.map((item) => (
-
- isActive ? 'active' : ''}
- aria-label={item.label}
- >
- {({ isActive }) => (
- <>
- {item.icon}
- {item.label}
- {isActive && (
-
- )}
- >
- )}
-
-
- ))}
-
-
- );
-};
-
-export default Navbar;
diff --git a/src/components/ProjectCard/ProjectCard.scss b/src/components/ProjectCard/ProjectCard.scss
deleted file mode 100644
index e5a7fba..0000000
--- a/src/components/ProjectCard/ProjectCard.scss
+++ /dev/null
@@ -1,107 +0,0 @@
-@import '../../styles/mixins/glass';
-
-.project-card {
- height: 100%;
- display: flex;
- flex-direction: column;
- padding: 25px;
-
- .card-header {
- display: flex;
- align-items: center;
- gap: 15px;
- margin-bottom: 20px;
-
- .icon-wrapper {
- width: 50px;
- height: 50px;
- border-radius: 12px;
- background: rgba(255, 255, 255, 0.05);
- display: flex;
- justify-content: center;
- align-items: center;
- color: var(--primary);
- border: 1px solid var(--glass-border);
- }
-
- .title-group {
- h3 {
- font-size: 1.2rem;
- color: var(--fg);
- margin-bottom: 4px;
- }
-
- .subtitle {
- font-size: 0.9rem;
- color: var(--muted);
- }
- }
- }
-
- .card-body {
- flex: 1;
- margin-bottom: 20px;
-
- .description-list {
- list-style: none;
-
- li {
- font-size: 0.95rem;
- color: var(--fg);
- margin-bottom: 8px;
- line-height: 1.5;
- opacity: 0.9;
-
- &::before {
- content: '•';
- color: var(--primary);
- margin-right: 8px;
- }
- }
- }
- }
-
- .card-footer {
- display: flex;
- justify-content: space-between;
- align-items: flex-end;
- gap: 10px;
-
- .tech-stack {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
-
- .tech-tag {
- font-size: 0.75rem;
- padding: 4px 10px;
- border-radius: 20px;
- background: rgba(255, 255, 255, 0.05);
- border: 1px solid var(--glass-border);
- color: var(--muted);
- }
- }
-
- .links {
- display: flex;
- gap: 10px;
-
- a {
- color: var(--muted);
- transition: color 0.2s;
-
- &:hover {
- color: var(--primary);
- }
- }
- }
- }
-
- &:hover {
- .card-header .icon-wrapper {
- background: var(--primary);
- color: white;
- box-shadow: 0 0 15px var(--primary);
- }
- }
-}
diff --git a/src/components/ProjectCard/ProjectCard.tsx b/src/components/ProjectCard/ProjectCard.tsx
deleted file mode 100644
index e89eadb..0000000
--- a/src/components/ProjectCard/ProjectCard.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-import React from 'react';
-import { motion } from 'framer-motion';
-import { Github, ExternalLink, Code } from 'lucide-react';
-import GlassCard from '../GlassCard/GlassCard';
-import './ProjectCard.scss';
-
-interface ProjectProps {
- project: {
- title: string;
- subtitle: string;
- tech: string[];
- description: string[];
- link?: string;
- github?: string;
- };
- index: number;
-}
-
-const ProjectCard: React.FC = ({ project, index }) => {
- return (
-
-
-
-
-
-
-
-
{project.title}
- {project.subtitle}
-
-
-
-
-
- {project.description.map((desc, i) => (
- {desc}
- ))}
-
-
-
-
-
- {project.tech.map((t) => (
- {t}
- ))}
-
-
-
-
-
-
- );
-};
-
-export default ProjectCard;
diff --git a/src/components/SkillGrid/SkillGrid.scss b/src/components/SkillGrid/SkillGrid.scss
deleted file mode 100644
index 37ba694..0000000
--- a/src/components/SkillGrid/SkillGrid.scss
+++ /dev/null
@@ -1,49 +0,0 @@
-@import '../../styles/mixins/glass';
-
-.skill-grid-container {
- max-width: 1000px;
- margin: 0 auto;
- padding: 20px;
-
- .skill-category {
- margin-bottom: 40px;
-
- .category-title {
- font-size: 1.5rem;
- color: var(--accent);
- margin-bottom: 20px;
- padding-bottom: 10px;
- border-bottom: 1px solid var(--glass-border);
- display: inline-block;
- }
-
- .skills-wrapper {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
- gap: 15px;
-
- .skill-card {
- padding: 15px;
- text-align: center;
- display: flex;
- justify-content: center;
- align-items: center;
- min-height: 80px;
- cursor: default;
-
- .skill-name {
- font-weight: 500;
- color: var(--fg);
- }
-
- &:hover {
- border-color: var(--primary);
-
- .skill-name {
- color: var(--primary);
- }
- }
- }
- }
- }
-}
diff --git a/src/components/SkillGrid/SkillGrid.tsx b/src/components/SkillGrid/SkillGrid.tsx
deleted file mode 100644
index 8e8719d..0000000
--- a/src/components/SkillGrid/SkillGrid.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import React from 'react';
-import { motion } from 'framer-motion';
-import GlassCard from '../GlassCard/GlassCard';
-import { resumeData } from '../../data/resume';
-import './SkillGrid.scss';
-
-const SkillGrid: React.FC = () => {
- const categories = Object.keys(resumeData.skills) as Array;
-
- const containerVariants = {
- hidden: { opacity: 0 },
- visible: {
- opacity: 1,
- transition: {
- staggerChildren: 0.1,
- },
- },
- };
-
- const itemVariants = {
- hidden: { scale: 0.8, opacity: 0 },
- visible: {
- scale: 1,
- opacity: 1,
- transition: { duration: 0.4 },
- },
- };
-
- return (
-
- {categories.map((category) => (
-
-
{category.charAt(0).toUpperCase() + category.slice(1)}
-
- {resumeData.skills[category].map((skill) => (
-
-
- {skill}
-
-
- ))}
-
-
- ))}
-
- );
-};
-
-export default SkillGrid;
diff --git a/src/components/Terminal/Terminal.scss b/src/components/Terminal/Terminal.scss
new file mode 100644
index 0000000..852fae9
--- /dev/null
+++ b/src/components/Terminal/Terminal.scss
@@ -0,0 +1,156 @@
+.terminal {
+ position: relative;
+ height: 100dvh;
+ width: 100%;
+ background-color: var(--bg);
+ background-image: var(--bg-gradient);
+ background-size: var(--bg-size, auto);
+ color: var(--fg);
+ font-family: var(--font-mono);
+ cursor: text;
+ overflow: hidden;
+}
+
+.terminal-scroll {
+ height: 100%;
+ overflow-y: auto;
+ padding: clamp(16px, 4vw, 40px);
+ max-width: 900px;
+ margin: 0 auto;
+ font-size: clamp(0.78rem, 1.6vw, 0.95rem);
+ line-height: 1.6;
+}
+
+.terminal-welcome {
+ margin-bottom: 16px;
+ color: var(--primary);
+
+ p:first-child {
+ font-weight: 700;
+ }
+}
+
+.terminal-block {
+ margin-bottom: 10px;
+}
+
+.terminal-line {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
+}
+
+.terminal-prompt {
+ color: var(--accent);
+ white-space: nowrap;
+}
+
+.terminal-command {
+ color: var(--fg);
+ word-break: break-all;
+}
+
+.terminal-input {
+ flex: 1;
+ min-width: 40px;
+ background: transparent;
+ border: none;
+ outline: none;
+ color: var(--fg);
+ font-family: inherit;
+ font-size: inherit;
+ caret-color: var(--primary);
+ caret-shape: block;
+}
+
+.terminal-output {
+ margin-top: 4px;
+ color: var(--fg);
+}
+
+.out-title {
+ color: var(--primary);
+ font-weight: 700;
+ margin-bottom: 2px;
+}
+
+.out-muted {
+ color: var(--muted);
+}
+
+.out-hint {
+ color: var(--muted);
+ margin-top: 6px;
+}
+
+.out-error {
+ color: var(--terminal-red);
+}
+
+.out-block > * + * {
+ margin-top: 4px;
+}
+
+.out-help .out-row {
+ display: flex;
+ gap: 16px;
+
+ .out-cmd {
+ color: var(--terminal-green);
+ min-width: 110px;
+ }
+
+ .out-desc {
+ color: var(--muted);
+ }
+}
+
+.out-skill-group,
+.out-project,
+.out-exp {
+ margin-bottom: 12px;
+}
+
+.out-check {
+ color: var(--terminal-green);
+}
+
+.out-desc-line {
+ color: var(--muted);
+ padding-left: 8px;
+}
+
+.terminal-suggestions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ margin-top: 4px;
+ opacity: 0.5;
+
+ .terminal-suggestion {
+ color: var(--muted);
+ }
+}
+
+.terminal-matrix {
+ position: fixed;
+ inset: 0;
+ z-index: 20;
+ pointer-events: none;
+ background:
+ repeating-linear-gradient(
+ 0deg,
+ rgba(0, 255, 100, 0.08) 0px,
+ rgba(0, 255, 100, 0.08) 1px,
+ transparent 1px,
+ transparent 3px
+ );
+ animation: matrix-scroll 0.6s linear infinite;
+ mix-blend-mode: screen;
+}
+
+@keyframes matrix-scroll {
+ 0% { background-position-y: 0; }
+ 100% { background-position-y: 40px; }
+}
diff --git a/src/components/Terminal/Terminal.tsx b/src/components/Terminal/Terminal.tsx
new file mode 100644
index 0000000..25bcbd5
--- /dev/null
+++ b/src/components/Terminal/Terminal.tsx
@@ -0,0 +1,162 @@
+import React, { useCallback, useEffect, useRef, useState } from 'react';
+import { useTheme, Theme } from '../../context/ThemeContext';
+import { runCommand, COMMAND_NAMES, CommandOutput } from './commands';
+import './Terminal.scss';
+
+interface LineEntry {
+ id: number;
+ command: string;
+ output: CommandOutput;
+}
+
+const THEME_ORDER: Theme[] = ['dark', 'neon', 'pastel', 'light'];
+const WELCOME = 'Type "help" to begin.';
+
+let idCounter = 0;
+
+const Terminal: React.FC = () => {
+ const { theme, setTheme } = useTheme();
+ const [lines, setLines] = useState([]);
+ const [input, setInput] = useState('');
+ const [history, setHistory] = useState([]);
+ const [historyIndex, setHistoryIndex] = useState(null);
+ const [matrixActive, setMatrixActive] = useState(false);
+
+ const inputRef = useRef(null);
+ const bottomRef = useRef(null);
+
+ useEffect(() => {
+ bottomRef.current?.scrollIntoView({ block: 'end' });
+ }, [lines]);
+
+ useEffect(() => {
+ inputRef.current?.focus();
+ }, []);
+
+ const cycleTheme = useCallback(() => {
+ const next = THEME_ORDER[(THEME_ORDER.indexOf(theme) + 1) % THEME_ORDER.length];
+ setTheme(next);
+ }, [theme, setTheme]);
+
+ const triggerMatrix = useCallback(() => {
+ setMatrixActive(true);
+ setTimeout(() => setMatrixActive(false), 3000);
+ }, []);
+
+ const focusInput = () => inputRef.current?.focus();
+
+ const submit = (raw: string) => {
+ const command = raw.trim();
+ if (!command) return;
+
+ if (command.toLowerCase() === 'clear') {
+ setLines([]);
+ } else {
+ const output = runCommand(command, {
+ theme,
+ cycleTheme,
+ clear: () => setLines([]),
+ triggerMatrix,
+ });
+ setLines(ls => [...ls, { id: idCounter++, command, output }]);
+ }
+
+ setHistory(h => [...h, command]);
+ setHistoryIndex(null);
+ setInput('');
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter') {
+ submit(input);
+ return;
+ }
+
+ if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ if (history.length === 0) return;
+ const nextIndex = historyIndex === null ? history.length - 1 : Math.max(0, historyIndex - 1);
+ setHistoryIndex(nextIndex);
+ setInput(history[nextIndex]);
+ return;
+ }
+
+ if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ if (historyIndex === null) return;
+ const nextIndex = historyIndex + 1;
+ if (nextIndex >= history.length) {
+ setHistoryIndex(null);
+ setInput('');
+ } else {
+ setHistoryIndex(nextIndex);
+ setInput(history[nextIndex]);
+ }
+ return;
+ }
+
+ if (e.key === 'Tab') {
+ e.preventDefault();
+ const [prefix] = input.split(/\s+/);
+ if (!prefix) return;
+ const matches = COMMAND_NAMES.filter(c => c.startsWith(prefix.toLowerCase()));
+ if (matches.length === 1) {
+ setInput(matches[0] + ' ');
+ }
+ }
+ };
+
+ const suggestions = input.trim()
+ ? COMMAND_NAMES.filter(c => c.startsWith(input.trim().toLowerCase()) && c !== input.trim().toLowerCase())
+ : [];
+
+ return (
+
+ {matrixActive &&
}
+
+
+
+
RahulOS v2.0 — interactive portfolio
+
{WELCOME}
+
+
+ {lines.map(line => (
+
+
+ visitor@rahul:~$
+ {line.command}
+
+ {line.output &&
{line.output}
}
+
+ ))}
+
+
+ visitor@rahul:~$
+ setInput(e.target.value)}
+ onKeyDown={handleKeyDown}
+ spellCheck={false}
+ autoComplete="off"
+ autoCapitalize="off"
+ aria-label="Terminal input"
+ />
+
+
+ {suggestions.length > 0 && (
+
+ {suggestions.map(s => (
+ {s}
+ ))}
+
+ )}
+
+
+
+
+ );
+};
+
+export default Terminal;
diff --git a/src/components/Terminal/commands.ts b/src/components/Terminal/commands.ts
new file mode 100644
index 0000000..564670a
--- /dev/null
+++ b/src/components/Terminal/commands.ts
@@ -0,0 +1,199 @@
+import React from 'react';
+import { resumeData } from '../../data/resume';
+import type { Theme } from '../../context/ThemeContext';
+
+export interface CommandContext {
+ theme: Theme;
+ cycleTheme: () => void;
+ clear: () => void;
+ triggerMatrix: () => void;
+}
+
+export type CommandOutput = React.ReactNode;
+export type CommandHandler = (args: string[], ctx: CommandContext) => CommandOutput;
+
+interface CommandDef {
+ name: string;
+ description: string;
+ handler: CommandHandler;
+}
+
+const slug = (title: string) => title.toLowerCase().replace(/\s+/g, '_');
+
+const openUrl = (url?: string) => {
+ if (url) window.open(url, '_blank', 'noopener,noreferrer');
+};
+
+const resolveProjectUrl = (arg: string): { url?: string; title?: string } => {
+ const byIndex = resumeData.projects[Number(arg) - 1];
+ if (byIndex) return { url: byIndex.link ?? byIndex.github, title: byIndex.title };
+ const bySlug = resumeData.projects.find(p => slug(p.title) === arg.toLowerCase());
+ if (bySlug) return { url: bySlug.link ?? bySlug.github, title: bySlug.title };
+ return {};
+};
+
+export const COMMANDS: CommandDef[] = [
+ {
+ name: 'help',
+ description: 'Show commands',
+ handler: () =>
+ React.createElement('div', { className: 'out-help' },
+ React.createElement('p', { className: 'out-title' }, 'Available Commands'),
+ COMMANDS.map(c =>
+ React.createElement('div', { className: 'out-row', key: c.name },
+ React.createElement('span', { className: 'out-cmd' }, c.name),
+ React.createElement('span', { className: 'out-desc' }, c.description)
+ )
+ )
+ ),
+ },
+ {
+ name: 'about',
+ description: 'About me',
+ handler: () =>
+ React.createElement('div', { className: 'out-block' },
+ React.createElement('p', null, `Hi! I'm ${resumeData.name}.`),
+ React.createElement('p', null, resumeData.hero.bio),
+ React.createElement('p', { className: 'out-muted' }, `Location: ${resumeData.location}`)
+ ),
+ },
+ {
+ name: 'skills',
+ description: 'Technologies',
+ handler: () =>
+ React.createElement('div', { className: 'out-block' },
+ Object.entries(resumeData.skills).map(([category, items]) =>
+ React.createElement('div', { className: 'out-skill-group', key: category },
+ React.createElement('p', { className: 'out-title' }, category.replace(/_/g, ' ')),
+ (items as string[]).map(item =>
+ React.createElement('p', { className: 'out-check', key: item }, `✔ ${item}`)
+ )
+ )
+ )
+ ),
+ },
+ {
+ name: 'projects',
+ description: 'Featured projects',
+ handler: () =>
+ React.createElement('div', { className: 'out-block' },
+ resumeData.projects.map((p, i) =>
+ React.createElement('div', { className: 'out-project', key: p.title },
+ React.createElement('p', { className: 'out-title' }, `[${i + 1}] ${p.title}`),
+ React.createElement('p', { className: 'out-muted' }, p.subtitle),
+ React.createElement('p', { className: 'out-muted' }, p.tech.join(' • '))
+ )
+ ),
+ React.createElement('p', { className: 'out-hint' }, 'Type: open e.g. open 1')
+ ),
+ },
+ {
+ name: 'open',
+ description: 'Open a project, github or linkedin',
+ handler: (args) => {
+ const target = args[0];
+ if (!target) return React.createElement('p', { className: 'out-error' }, 'Usage: open ');
+ if (target === 'github') { openUrl(resumeData.contact.github); return React.createElement('p', null, 'Opening GitHub...'); }
+ if (target === 'linkedin') { openUrl(resumeData.contact.linkedin); return React.createElement('p', null, 'Opening LinkedIn...'); }
+ const { url, title } = resolveProjectUrl(target);
+ if (!url) return React.createElement('p', { className: 'out-error' }, `No project found for "${target}"`);
+ openUrl(url);
+ return React.createElement('p', null, `Opening ${title}...`);
+ },
+ },
+ {
+ name: 'experience',
+ description: 'Work experience',
+ handler: () =>
+ React.createElement('div', { className: 'out-block' },
+ resumeData.experience.map((exp, i) =>
+ React.createElement('div', { className: 'out-exp', key: i },
+ React.createElement('p', { className: 'out-title' }, `${exp.role} @ ${exp.company}`),
+ React.createElement('p', { className: 'out-muted' }, exp.year),
+ exp.description.map((d, j) =>
+ React.createElement('p', { className: 'out-desc-line', key: j }, `- ${d}`)
+ )
+ )
+ )
+ ),
+ },
+ {
+ name: 'resume',
+ description: 'Download resume',
+ handler: () => {
+ openUrl(resumeData.contact.cv);
+ return React.createElement('p', null, 'Opening resume...');
+ },
+ },
+ {
+ name: 'contact',
+ description: 'Contact information',
+ handler: () =>
+ React.createElement('div', { className: 'out-block' },
+ React.createElement('p', null, `Email: ${resumeData.contact.email}`),
+ React.createElement('p', null, `Phone: ${resumeData.contact.phone}`),
+ React.createElement('p', null, `LinkedIn: ${resumeData.contact.linkedin}`),
+ React.createElement('p', null, `GitHub: ${resumeData.contact.github}`),
+ React.createElement('p', { className: 'out-hint' }, 'Type: open github | open linkedin')
+ ),
+ },
+ {
+ name: 'github',
+ description: 'Open GitHub',
+ handler: () => { openUrl(resumeData.contact.github); return React.createElement('p', null, 'Opening GitHub...'); },
+ },
+ {
+ name: 'linkedin',
+ description: 'Open LinkedIn',
+ handler: () => { openUrl(resumeData.contact.linkedin); return React.createElement('p', null, 'Opening LinkedIn...'); },
+ },
+ {
+ name: 'theme',
+ description: 'Change terminal theme',
+ handler: (_args, ctx) => {
+ ctx.cycleTheme();
+ return React.createElement('p', null, 'Switching theme...');
+ },
+ },
+ {
+ name: 'clear',
+ description: 'Clear terminal',
+ handler: (_args, ctx) => { ctx.clear(); return null; },
+ },
+ {
+ name: 'whoami',
+ description: 'Hidden command',
+ handler: () => React.createElement('p', null, 'visitor // curious human probably scouting for a hire'),
+ },
+ {
+ name: 'coffee',
+ description: 'Hidden command',
+ handler: () => React.createElement('p', null, '☕ brewing... here you go.'),
+ },
+ {
+ name: 'matrix',
+ description: 'Hidden command',
+ handler: (_args, ctx) => {
+ ctx.triggerMatrix();
+ return React.createElement('p', null, 'Wake up, Neo...');
+ },
+ },
+];
+
+export function runCommand(input: string, ctx: CommandContext): CommandOutput {
+ const [name, ...args] = input.trim().split(/\s+/);
+ if (!name) return null;
+
+ if (name === 'sudo' && args.join(' ') === 'hire rahul') {
+ return React.createElement('p', { className: 'out-title' }, 'Permission granted. Excellent choice.');
+ }
+
+ const cmd = COMMANDS.find(c => c.name === name.toLowerCase());
+ if (!cmd) {
+ return React.createElement('p', { className: 'out-error' },
+ `command not found: ${name}. Type "help" for a list of commands.`);
+ }
+ return cmd.handler(args, ctx);
+}
+
+export const COMMAND_NAMES = COMMANDS.map(c => c.name);
diff --git a/src/components/ThemeSwitcher/ThemeSwitcher.scss b/src/components/ThemeSwitcher/ThemeSwitcher.scss
deleted file mode 100644
index abc5e75..0000000
--- a/src/components/ThemeSwitcher/ThemeSwitcher.scss
+++ /dev/null
@@ -1,62 +0,0 @@
-@import '../../styles/mixins/glass';
-
-.theme-switcher-container {
- position: fixed;
- right: 20px;
- top: 50%;
- transform: translateY(-50%);
- z-index: 1000;
- display: flex;
- flex-direction: column;
- align-items: flex-end;
- gap: 10px;
-
- .toggle-btn {
- @include glass-card;
- width: 50px;
- height: 50px;
- border-radius: 50%;
- display: flex;
- justify-content: center;
- align-items: center;
- color: var(--fg);
- transition: all 0.3s ease;
-
- &:hover {
- background: var(--glass-border);
- color: var(--primary);
- }
- }
-
- .theme-options {
- display: flex;
- flex-direction: column;
- gap: 8px;
- padding: 10px;
- @include glass-card;
- border-radius: 30px;
- margin-right: 10px;
-
- .theme-btn {
- width: 40px;
- height: 40px;
- border-radius: 50%;
- display: flex;
- justify-content: center;
- align-items: center;
- color: var(--muted);
- transition: all 0.3s ease;
-
- &:hover {
- background: rgba(255, 255, 255, 0.1);
- color: var(--fg);
- }
-
- &.active {
- background: var(--primary);
- color: white;
- box-shadow: 0 0 10px var(--primary);
- }
- }
- }
-}
diff --git a/src/components/ThemeSwitcher/ThemeSwitcher.tsx b/src/components/ThemeSwitcher/ThemeSwitcher.tsx
deleted file mode 100644
index 568e76b..0000000
--- a/src/components/ThemeSwitcher/ThemeSwitcher.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import React, { useState } from 'react';
-import { motion, AnimatePresence } from 'framer-motion';
-import { Palette, Sun, Moon, Zap, Cloud } from 'lucide-react';
-import { useTheme, Theme } from '../../context/ThemeContext';
-import './ThemeSwitcher.scss';
-
-const themes: { id: Theme; icon: React.ReactNode; label: string; color: string }[] = [
- { id: 'light', icon: , label: 'Neo Light', color: '#f0f4f8' },
- { id: 'dark', icon: , label: 'Dark Carbon', color: '#101010' },
- { id: 'neon', icon: , label: 'Cyber Neon', color: '#050505' },
- { id: 'pastel', icon: , label: 'Pastel Ice', color: '#fdfbf7' },
-];
-
-const ThemeSwitcher: React.FC = () => {
- const { theme, setTheme } = useTheme();
- const [isOpen, setIsOpen] = useState(false);
-
- return (
-
-
- {isOpen && (
-
- {themes.map((t) => (
- setTheme(t.id)}
- title={t.label}
- aria-label={`Switch to ${t.label} theme`}
- >
- {t.icon}
-
- ))}
-
- )}
-
-
-
setIsOpen(!isOpen)}
- whileHover={{ scale: 1.1 }}
- whileTap={{ scale: 0.9 }}
- aria-label="Toggle theme menu"
- >
-
-
-
- );
-};
-
-export default ThemeSwitcher;
diff --git a/src/components/Timeline/Timeline.scss b/src/components/Timeline/Timeline.scss
deleted file mode 100644
index 2505b5b..0000000
--- a/src/components/Timeline/Timeline.scss
+++ /dev/null
@@ -1,120 +0,0 @@
-@import '../../styles/mixins/glass';
-
-.timeline-container {
- max-width: 800px;
- margin: 0 auto;
- padding: 20px;
-
- .section-title {
- font-size: 2rem;
- color: var(--primary);
- margin-bottom: 30px;
- padding-left: 20px;
- border-left: 4px solid var(--primary);
- }
-
- .timeline {
- position: relative;
- padding-left: 30px;
- margin-bottom: 60px;
-
- &::before {
- content: '';
- position: absolute;
- left: 0;
- top: 0;
- height: 100%;
- width: 2px;
- background: var(--glass-border);
- }
-
- .timeline-item {
- position: relative;
- margin-bottom: 40px;
-
- &:last-child {
- margin-bottom: 0;
- }
-
- .timeline-marker {
- position: absolute;
- left: -39px;
- top: 0;
- width: 20px;
- height: 20px;
- background: var(--bg);
- border: 2px solid var(--primary);
- border-radius: 50%;
- display: flex;
- justify-content: center;
- align-items: center;
- color: var(--primary);
- z-index: 1;
- padding: 8px;
- box-shadow: 0 0 10px var(--primary);
-
- &.edu {
- border-color: var(--accent);
- color: var(--accent);
- box-shadow: 0 0 10px var(--accent);
- }
- }
-
- .timeline-content {
- .timeline-card {
- padding: 25px;
-
- .header {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- margin-bottom: 10px;
- flex-wrap: wrap;
- gap: 10px;
-
- h3 {
- font-size: 1.2rem;
- color: var(--fg);
- font-weight: 600;
- }
-
- .company {
- font-size: 1rem;
- color: var(--primary);
- font-weight: 500;
- }
- }
-
- .meta {
- display: flex;
- align-items: center;
- gap: 8px;
- color: var(--muted);
- font-size: 0.9rem;
- margin-bottom: 15px;
- }
-
- .description {
- list-style-type: none;
-
- li {
- position: relative;
- padding-left: 20px;
- margin-bottom: 8px;
- color: var(--fg);
- font-size: 0.95rem;
- line-height: 1.5;
-
- &::before {
- content: '▹';
- position: absolute;
- left: 0;
- color: var(--primary);
- }
- }
- }
- }
- }
- }
- }
-}
diff --git a/src/components/Timeline/Timeline.tsx b/src/components/Timeline/Timeline.tsx
deleted file mode 100644
index 2362650..0000000
--- a/src/components/Timeline/Timeline.tsx
+++ /dev/null
@@ -1,100 +0,0 @@
-import React from 'react';
-import { motion } from 'framer-motion';
-import { Briefcase, GraduationCap, Calendar } from 'lucide-react';
-import GlassCard from '../GlassCard/GlassCard';
-import { resumeData } from '../../data/resume';
-import './Timeline.scss';
-
-const Timeline: React.FC = () => {
- const containerVariants = {
- hidden: { opacity: 0 },
- visible: {
- opacity: 1,
- transition: {
- staggerChildren: 0.3,
- },
- },
- };
-
- const itemVariants = {
- hidden: { x: -20, opacity: 0 },
- visible: {
- x: 0,
- opacity: 1,
- transition: { duration: 0.5 },
- },
- };
-
- return (
-
-
- Experience
-
- {resumeData.experience.map((exp, index) => (
-
-
-
-
-
-
-
-
{exp.role}
- {exp.company}
-
-
-
- {exp.year}
-
-
- {exp.description.map((desc, i) => (
- {desc}
- ))}
-
-
-
-
- ))}
-
-
-
-
- Education
-
- {resumeData.education.map((edu, index) => (
-
-
-
-
-
-
-
-
{edu.degree}
- {edu.institution}
-
-
-
- {edu.year}
-
-
-
-
- ))}
-
-
-
- );
-};
-
-export default Timeline;
diff --git a/src/pages/Contact/Contact.scss b/src/pages/Contact/Contact.scss
deleted file mode 100644
index 9b250bb..0000000
--- a/src/pages/Contact/Contact.scss
+++ /dev/null
@@ -1,13 +0,0 @@
-.contact-page {
- height: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 20px;
- overflow-y: auto;
-
- .page-content {
- width: 100%;
- max-width: 1200px;
- }
-}
diff --git a/src/pages/Contact/Contact.tsx b/src/pages/Contact/Contact.tsx
deleted file mode 100644
index d15974f..0000000
--- a/src/pages/Contact/Contact.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import React from 'react';
-import { motion } from 'framer-motion';
-import ContactForm from '../../components/ContactForm/ContactForm';
-import './Contact.scss';
-
-const Contact: React.FC = () => {
- return (
-
-
-
-
-
- );
-};
-
-export default Contact;
diff --git a/src/pages/Home/Home.scss b/src/pages/Home/Home.scss
deleted file mode 100644
index f32ebd8..0000000
--- a/src/pages/Home/Home.scss
+++ /dev/null
@@ -1,37 +0,0 @@
-@import '../../styles/mixins/glass';
-
-.home-page {
- height: 100%;
- display: flex;
- justify-content: center;
- align-items: center;
- padding: 20px;
-
- .hero-content {
- text-align: center;
- max-width: 800px;
-
- h1 {
- font-size: 4rem;
- font-weight: 800;
- margin-bottom: 1rem;
-
- .highlight {
- color: var(--primary);
- text-shadow: 0 0 20px rgba(0, 123, 255, 0.3);
- }
- }
-
- p {
- font-size: 1.5rem;
- color: var(--muted);
- margin-bottom: 2rem;
- }
-
- .intro-card {
- padding: 30px;
- font-size: 1.2rem;
- line-height: 1.6;
- }
- }
-}
diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx
deleted file mode 100644
index e714874..0000000
--- a/src/pages/Home/Home.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import React from 'react';
-import { motion } from 'framer-motion';
-import Hero from '../../components/Hero/Hero';
-import './Home.scss';
-
-const Home: React.FC = () => {
- return (
-
-
-
- );
-};
-
-export default Home;
diff --git a/src/pages/Journey/Journey.scss b/src/pages/Journey/Journey.scss
deleted file mode 100644
index b8058bf..0000000
--- a/src/pages/Journey/Journey.scss
+++ /dev/null
@@ -1,21 +0,0 @@
-.journey-page {
- padding: 40px 20px;
- height: 100%;
- overflow-y: auto;
-
- .page-header {
- text-align: center;
- margin-bottom: 50px;
-
- h1 {
- font-size: 3rem;
- margin-bottom: 10px;
- color: var(--fg);
- }
-
- p {
- color: var(--muted);
- font-size: 1.2rem;
- }
- }
-}
diff --git a/src/pages/Journey/Journey.tsx b/src/pages/Journey/Journey.tsx
deleted file mode 100644
index 0bca5cf..0000000
--- a/src/pages/Journey/Journey.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import React from 'react';
-import { motion } from 'framer-motion';
-import Timeline from '../../components/Timeline/Timeline';
-import './Journey.scss';
-
-const Journey: React.FC = () => {
- return (
-
-
-
My Journey
-
A timeline of my professional experience and education.
-
-
-
- );
-};
-
-export default Journey;
diff --git a/src/pages/Projects/Projects.scss b/src/pages/Projects/Projects.scss
deleted file mode 100644
index 4cb0a54..0000000
--- a/src/pages/Projects/Projects.scss
+++ /dev/null
@@ -1,29 +0,0 @@
-.projects-page {
- padding: 40px 20px;
- height: 100%;
- overflow-y: auto;
-
- .page-header {
- text-align: center;
- margin-bottom: 50px;
-
- h1 {
- font-size: 3rem;
- margin-bottom: 10px;
- color: var(--fg);
- }
-
- p {
- color: var(--muted);
- font-size: 1.2rem;
- }
- }
-
- .projects-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
- gap: 30px;
- max-width: 1200px;
- margin: 0 auto 50px;
- }
-}
diff --git a/src/pages/Projects/Projects.tsx b/src/pages/Projects/Projects.tsx
deleted file mode 100644
index dd34299..0000000
--- a/src/pages/Projects/Projects.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import React from 'react';
-import { motion } from 'framer-motion';
-import ProjectCard from '../../components/ProjectCard/ProjectCard';
-import { resumeData } from '../../data/resume';
-import './Projects.scss';
-
-const Projects: React.FC = () => {
- return (
-
-
-
Featured Projects
-
A selection of my recent work.
-
-
-
- {resumeData.projects.map((project, index) => (
-
- ))}
-
-
- );
-};
-
-export default Projects;
diff --git a/src/pages/Skills/Skills.scss b/src/pages/Skills/Skills.scss
deleted file mode 100644
index c4d1b67..0000000
--- a/src/pages/Skills/Skills.scss
+++ /dev/null
@@ -1,21 +0,0 @@
-.skills-page {
- padding: 40px 20px;
- height: 100%;
- overflow-y: auto;
-
- .page-header {
- text-align: center;
- margin-bottom: 50px;
-
- h1 {
- font-size: 3rem;
- margin-bottom: 10px;
- color: var(--fg);
- }
-
- p {
- color: var(--muted);
- font-size: 1.2rem;
- }
- }
-}
diff --git a/src/pages/Skills/Skills.tsx b/src/pages/Skills/Skills.tsx
deleted file mode 100644
index ab2a777..0000000
--- a/src/pages/Skills/Skills.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import React from 'react';
-import { motion } from 'framer-motion';
-import SkillGrid from '../../components/SkillGrid/SkillGrid';
-import './Skills.scss';
-
-const Skills: React.FC = () => {
- return (
-
-
-
Technical Skills
-
Technologies and tools I work with.
-
-
-
- );
-};
-
-export default Skills;
diff --git a/src/router.tsx b/src/router.tsx
deleted file mode 100644
index 75f74b6..0000000
--- a/src/router.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import React from 'react';
-import { Routes, Route, useLocation } from 'react-router-dom';
-import { AnimatePresence } from 'framer-motion';
-import Home from './pages/Home/Home';
-import Journey from './pages/Journey/Journey';
-import Skills from './pages/Skills/Skills';
-import Projects from './pages/Projects/Projects';
-import Contact from './pages/Contact/Contact';
-
-const AppRouter: React.FC = () => {
- const location = useLocation();
-
- return (
-
-
- } />
- } />
- } />
- } />
- } />
-
-
- );
-};
-
-export default AppRouter;
diff --git a/src/services/email.ts b/src/services/email.ts
deleted file mode 100644
index 85090a0..0000000
--- a/src/services/email.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import emailjs from '@emailjs/browser';
-
-export const sendEmail = (templateParams: Record) => {
- return emailjs.send(
- import.meta.env?.VITE_EMAILJS_SERVICE_ID,
- import.meta.env.VITE_EMAILJS_TEMPLATE_ID,
- templateParams,
- import.meta.env.VITE_EMAILJS_PUBLIC_KEY
- );
-};
diff --git a/src/services/rag.ts b/src/services/rag.ts
deleted file mode 100644
index b19981e..0000000
--- a/src/services/rag.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-// src/services/rag.ts
-import { GoogleGenAI } from "@google/genai";
-import { resumeData } from "../data/resume";
-
-export interface RAGResponse {
- text: string;
-}
-
-export async function runRAG(apiKey: string, query: string): Promise {
- const ai = new GoogleGenAI({ apiKey });
-
- const prompt = `
-You are Rahul's portfolio AI assistant, speaking to a visitor and your name is folio.ai.
-Always talk about Rahul in **third person**.
-Never speak as Rahul.
-
-Here is Rahul's COMPLETE DATA (use only this data, never invent new info):
-
-${JSON.stringify(resumeData, null, 2)}
-
-=======================
-INSTRUCTIONS
-=======================
-Your behavior rules:
-
-1. GREETINGS
- - If user says “hi”, “hello”, “hey”, greet warmly.
- - Example: “Hello! How can I help you learn more about Rahul?”
-
-2. CATEGORY LOGIC
- - If the question is about **projects**, answer ONLY using resumeData.projects.
- - If the question is about **experience**, answer ONLY using resumeData.experience.
- - If about **skills**, answer ONLY using resumeData.skills.
- - If asking about **current job / where he works / now working**, return the item where year includes “Present”.
- - If question matches multiple areas, choose the MOST relevant one.
- - If unrelated or general, give a simple helpful answer describing Rahul.
- - NEVER mix job experience with project lists.
-
-3. ANSWER STYLE
- - Always respond in third person (“Rahul is…”, “His experience includes…”).
- - Keep answers clear, concise, friendly.
- - Use bullet points when listing items.
- - Never fabricate information.
-
-FORMAT RULES:
-- Always answer in clean, natural paragraphs.
-- Do NOT use bullet points, lists, tables, markdown sections, or nested formatting.
-- Combine related skills into a concise paragraph (max 3 sentences).
-- Keep the response readable and conversational, not technical documentation.
-
-=======================
-USER QUESTION:
-${query}
-=======================
-
-Give the best possible answer using ONLY the provided resumeData.
-`;
-
- try {
- const response = await ai.models.generateContent({
- model: "gemini-2.5-flash",
- contents: prompt,
- config: {
- temperature: 0.2,
- }
- });
-
- const text =
- response?.text ??
- response?.candidates?.[0]?.content?.parts?.[0]?.text ??
- "Sorry, I couldn't generate a response.";
-
- return { text };
- } catch (err) {
- console.error("Gemini API error:", err);
- sessionStorage.removeItem("geminiKey")
- return {
- text: "There was an error processing the request. Please check your API key.",
- };
- }
-}
diff --git a/src/styles/global.scss b/src/styles/global.scss
index 907f6c5..4e5b436 100644
--- a/src/styles/global.scss
+++ b/src/styles/global.scss
@@ -15,14 +15,38 @@
========================= */
body {
font-family: var(--font-main);
- background: var(--bg);
+ background-color: var(--bg);
background-image: var(--bg-gradient);
+ background-size: var(--bg-size, auto);
color: var(--fg);
min-height: 100dvh;
- transition: background 0.3s ease, color 0.3s ease;
+ transition: background-color 0.3s ease, color 0.3s ease;
overflow-x: hidden;
}
+/* Scanline animation */
+.scanline {
+ width: 100%;
+ height: 2px;
+ background: rgba(0, 240, 255, 0.08);
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: 9999;
+ pointer-events: none;
+ animation: scanline-sweep 8s linear infinite;
+}
+
+@keyframes scanline-sweep {
+ 0% { top: 0; }
+ 100% { top: 100%; }
+}
+
+/* Glow utility */
+.glow-cyan {
+ text-shadow: 0 0 10px rgba(0, 240, 255, 0.5);
+}
+
/* =========================
Theme-Aware Scrollbar
========================= */
@@ -90,18 +114,25 @@ button {
Layout Container
========================= */
.page-container {
- min-height: 100dvh;
+ height: 100dvh;
width: 100%;
- padding-top: 80px; /* Space for Navbar */
display: flex;
flex-direction: column;
+ overflow: hidden; /* header + footer fixed via flex */
position: relative;
- overflow: hidden;
+}
- @media (min-width: 768px) {
- height: 100dvh;
- overflow: hidden; /* Prevent scrolling on desktop */
- }
+/* navbar-wrapper sits in flex flow — no sticky/fixed needed */
+.navbar-wrapper {
+ flex-shrink: 0;
+}
+
+/* scrollable content between header and footer */
+.app-content {
+ flex: 1;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
}
/* =========================
diff --git a/src/styles/variables.scss b/src/styles/variables.scss
index 3b15020..f2c3f41 100644
--- a/src/styles/variables.scss
+++ b/src/styles/variables.scss
@@ -19,6 +19,7 @@
/* Typography */
--font-main: 'Inter', sans-serif;
+ --font-mono: 'JetBrains Mono', 'Fira Code', monospace;
/* Spacing */
--spacing-xs: 0.5rem;
@@ -27,10 +28,18 @@
--spacing-lg: 2rem;
--spacing-xl: 3rem;
+ /* Terminal colors (shared across themes) */
+ --panel-bg: var(--glass-bg);
+ --border-color: var(--glass-border);
+ --terminal-green: #00aa55;
+ --terminal-amber: #e08800;
+ --terminal-red: #cc3344;
+ --outer-glow: none;
+
/* =========================
Scrollbar (Theme-Aware)
========================= */
- --scrollbar-size: 10px;
+ --scrollbar-size: 6px;
--scrollbar-radius: 999px;
--scrollbar-track: transparent;
@@ -39,21 +48,41 @@
}
/* =========================
- Dark Theme
+ Dark Theme (Terminal)
========================= */
[data-theme='dark'] {
- --bg: #101010;
- --bg-gradient: linear-gradient(135deg, #101010 0%, #202020 100%);
- --glass-bg: rgba(20, 20, 20, 0.6);
- --glass-border: rgba(255, 255, 255, 0.1);
- --glass-blur: 16px;
- --glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.5);
- --edge-glow: rgba(255, 255, 255, 0.1);
-
- --fg: #f0f4f8;
- --muted: #9fb3c8;
- --accent: #82cfff;
- --primary: #4dabf7;
+ /* Exact colors from reference HTML */
+ --bg: #131313;
+ --bg-gradient: radial-gradient(#1a1a1a 1px, transparent 1px);
+ --bg-size: 24px 24px;
+
+ --panel-bg: #0e0e0e; /* surface-container-lowest */
+ --panel-low: #1c1b1b; /* surface-container-low */
+ --panel-mid: #201f1f; /* surface-container */
+ --panel-high: #2a2a2a; /* surface-container-high */
+
+ --glass-bg: #1c1b1b;
+ --glass-border: #3b494b;
+ --glass-blur: 0px;
+ --glass-shadow: none;
+ --edge-glow: rgba(0, 240, 255, 0.15);
+
+ --fg: #e5e2e1; /* on-surface */
+ --muted: #b9cacb; /* on-surface-variant */
+ --outline: #849495; /* outline */
+ --primary: #00f0ff; /* primary-container */
+ --on-primary: #00363a; /* on-primary */
+ --accent: #00dbe9; /* primary-fixed-dim */
+
+ --border-color: #3b494b; /* outline-variant */
+ --terminal-green: #00f0ff;
+ --terminal-amber: #fed639; /* tertiary-container */
+ --terminal-red: #ffb4ab;
+
+ --outer-glow: none;
+
+ --scrollbar-thumb: rgba(0, 240, 255, 0.15);
+ --scrollbar-thumb-hover: rgba(0, 240, 255, 0.3);
}
/* =========================