From 1ec1be4dccbf74beb4d82c73d0c705efd2d5c722 Mon Sep 17 00:00:00 2001 From: ssbg04 Date: Fri, 5 Jun 2026 23:58:12 +0800 Subject: [PATCH] remove: ai integration, add: local ai --- .env.example | 3 - api/chat.php | 205 ---- api/test_models.php | 23 - assets/css/modules/_base.css | 110 ++ assets/css/modules/_cards.css | 263 +++++ assets/css/modules/_chatbot.css | 291 +++++ assets/css/modules/_contact.css | 89 ++ assets/css/modules/_hero.css | 227 ++++ assets/css/modules/_layout.css | 83 ++ assets/css/modules/_nav.css | 123 ++ assets/css/modules/_responsive.css | 242 ++++ assets/css/modules/_utilities.css | 99 ++ assets/css/modules/_variables.css | 58 + assets/css/style.css | 1735 +--------------------------- assets/js/chat.js | 255 ++-- assets/js/chatbot-engine.js | 134 +++ views/chatbot.php | 55 +- 17 files changed, 1832 insertions(+), 2163 deletions(-) delete mode 100644 api/chat.php delete mode 100644 api/test_models.php create mode 100644 assets/css/modules/_base.css create mode 100644 assets/css/modules/_cards.css create mode 100644 assets/css/modules/_chatbot.css create mode 100644 assets/css/modules/_contact.css create mode 100644 assets/css/modules/_hero.css create mode 100644 assets/css/modules/_layout.css create mode 100644 assets/css/modules/_nav.css create mode 100644 assets/css/modules/_responsive.css create mode 100644 assets/css/modules/_utilities.css create mode 100644 assets/css/modules/_variables.css create mode 100644 assets/js/chatbot-engine.js diff --git a/.env.example b/.env.example index 584528e..0cf977b 100644 --- a/.env.example +++ b/.env.example @@ -11,9 +11,6 @@ RECAPTCHA_SECRET_KEY=your-recaptcha-secret-key-here # Contact Email (used in FormSubmit action) CONTACT_EMAIL=your-email@gmail.com -# Groq AI API Key (free: 14,400 req/day — get yours at https://console.groq.com) -GROQ_API_KEY=your-groq-api-key-here - # Rate Limiting (requests per hour) RATE_LIMIT=10 diff --git a/api/chat.php b/api/chat.php deleted file mode 100644 index 83ed271..0000000 --- a/api/chat.php +++ /dev/null @@ -1,205 +0,0 @@ -= 30) { - http_response_code(429); - echo json_encode(['error' => 'Too many requests. Please wait a minute before sending another message.']); - exit; -} -$_SESSION['chat_requests'][] = $now; - -// Load DB connection which also loads environment variables -require_once __DIR__ . '/../config/db.php'; - -// Load Groq API key (free tier: 14,400 req/day) -$apiKey = $_ENV['GROQ_API_KEY'] ?? getenv('GROQ_API_KEY') ?? ''; -if (empty($apiKey)) { - http_response_code(500); - echo json_encode(['error' => 'API Key configuration is missing. Please set GROQ_API_KEY in your .env file.']); - exit; -} - -// Get the raw POST data -$inputData = json_decode(file_get_contents('php://input'), true); -if (!$inputData || !isset($inputData['message'])) { - http_response_code(400); - echo json_encode(['error' => 'Invalid request data.']); - exit; -} - -$userMessage = trim($inputData['message']); -if (empty($userMessage)) { - http_response_code(400); - echo json_encode(['error' => 'Message cannot be empty.']); - exit; -} - -// Fetch context data from database to construct system instructions -// 1. Settings / About -$settingsQuery = $mysqli->query("SELECT * FROM settings WHERE id = 1"); -$settings = $settingsQuery ? $settingsQuery->fetch_assoc() : null; - -// 2. Projects -$projectsQuery = $mysqli->query("SELECT title, des, techstack, url FROM projects"); -$projects = []; -if ($projectsQuery) { - while ($row = $projectsQuery->fetch_assoc()) { - $projects[] = $row; - } -} - -// 3. Certifications -$certsQuery = $mysqli->query("SELECT title, issuer, date_issued FROM certifications"); -$certs = []; -if ($certsQuery) { - while ($row = $certsQuery->fetch_assoc()) { - $certs[] = $row; - } -} - -// 4. Articles -$articlesQuery = $mysqli->query("SELECT title, excerpt, date_published, tag, url FROM articles"); -$articles = []; -if ($articlesQuery) { - while ($row = $articlesQuery->fetch_assoc()) { - $articles[] = $row; - } -} - -// Build the System Instruction -$systemPrompt = "You are a friendly, professional, and intelligent AI assistant built into the personal portfolio website of Cris Charles Garcia.\n"; -$systemPrompt .= "Your role is to represent Cris, assist recruiters, potential clients, and visitors, and answer questions about his background, experience, skills, and projects.\n\n"; - -$systemPrompt .= "Cris's Profile & Biography:\n"; -if ($settings) { - $systemPrompt .= "- Bio Paragraph 1: " . $settings['about_text_1'] . "\n"; - $systemPrompt .= "- Bio Paragraph 2: " . $settings['about_text_2'] . "\n"; - $systemPrompt .= "- Years coding: " . $settings['years_coding'] . "\n"; - $systemPrompt .= "- Projects completed: " . $settings['projects_count'] . "\n"; - $systemPrompt .= "- Certifications: " . $settings['certifications_count'] . "\n"; - $systemPrompt .= "- Github: " . $settings['github_url'] . "\n"; - $systemPrompt .= "- LinkedIn: " . $settings['linkedin_url'] . "\n"; - $systemPrompt .= "- Instagram: " . $settings['instagram_url'] . "\n"; - $systemPrompt .= "- Facebook: " . $settings['facebook_url'] . "\n"; - $systemPrompt .= "- TikTok: " . $settings['tiktok_url'] . "\n"; -} else { - $systemPrompt .= "- Cris is a senior IT student majoring in Software Development based in Laguna, Philippines.\n"; -} - -$systemPrompt .= "\nCris's Projects:\n"; -foreach ($projects as $p) { - $systemPrompt .= "- **" . $p['title'] . "**: " . $p['des'] . " (Tech: " . $p['techstack'] . "). Link: " . $p['url'] . "\n"; -} - -$systemPrompt .= "\nCris's Certifications:\n"; -foreach ($certs as $c) { - $systemPrompt .= "- " . $c['title'] . " by " . $c['issuer'] . " (Issued: " . $c['date_issued'] . ")\n"; -} - -$systemPrompt .= "\nCris's Latest Articles/Blog Posts:\n"; -foreach ($articles as $a) { - $systemPrompt .= "- \"" . $a['title'] . "\": " . $a['excerpt'] . " [Tag: " . $a['tag'] . ", Date: " . $a['date_published'] . ", Link: " . $a['url'] . "]\n"; -} - -$systemPrompt .= "\nGuidelines for your responses:\n"; -$systemPrompt .= "1. Respond in first-person (e.g., \"I am a senior IT student...\" or \"In my project UniMarket...\") or as a dedicated assistant of Cris. Represent Cris accurately, warmly, and professionally.\n"; -$systemPrompt .= "2. Keep responses brief, engaging, and directly related to the question. Recruiters appreciate concise answers.\n"; -$systemPrompt .= "3. If asked about contact options or hiring, point them to his contact form at the bottom of the page or his professional links (Github, LinkedIn).\n"; -$systemPrompt .= "4. Do not make up facts or external projects not listed in the context. If you don't know the answer, politely invite the user to contact Cris directly.\n"; -$systemPrompt .= "5. Format your output using simple markdown (like bolding or bullet points) to make it readable.\n"; - -// Build Groq messages array (OpenAI-compatible format) -// System prompt goes in its own 'system' role message -$messages = [ - ['role' => 'system', 'content' => $systemPrompt] -]; - -// Append chat history -if (isset($inputData['history']) && is_array($inputData['history']) && !empty($inputData['history'])) { - foreach ($inputData['history'] as $msg) { - if (isset($msg['role']) && isset($msg['text'])) { - $role = ($msg['role'] === 'user') ? 'user' : 'assistant'; - $messages[] = ['role' => $role, 'content' => $msg['text']]; - } - } -} - -// Add the current user message -$messages[] = ['role' => 'user', 'content' => $userMessage]; - -// Groq request payload (OpenAI-compatible) -$requestPayload = [ - 'model' => 'llama-3.3-70b-versatile', // 14,400 RPD free - 'messages' => $messages, - 'temperature' => 0.7, - 'max_tokens' => 800, - 'stream' => false -]; - -// Guard: cURL must be available -if (!function_exists('curl_init')) { - http_response_code(503); - echo json_encode(['error' => 'cURL is not available on this server. Please use the client-side fallback.']); - exit; -} - -// Call Groq API via cURL (OpenAI-compatible endpoint) -// Model: llama-3.3-70b-versatile — free tier, 14,400 req/day -$url = 'https://api.groq.com/openai/v1/chat/completions'; - -$ch = curl_init($url); -curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); -curl_setopt($ch, CURLOPT_POST, true); -curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($requestPayload)); -curl_setopt($ch, CURLOPT_HTTPHEADER, [ - 'Content-Type: application/json', - 'Authorization: Bearer ' . $apiKey -]); -curl_setopt($ch, CURLOPT_TIMEOUT, 15); -curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - -$response = curl_exec($ch); -$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - -if (curl_errno($ch)) { - $errorMsg = curl_error($ch); - curl_close($ch); - http_response_code(500); - echo json_encode(['error' => 'cURL error: ' . $errorMsg]); - exit; -} -curl_close($ch); - -if ($httpCode !== 200) { - http_response_code($httpCode); - $errorDetails = json_decode($response, true); - $errMsg = isset($errorDetails['error']['message']) ? $errorDetails['error']['message'] : 'Failed to retrieve response from Groq.'; - echo json_encode(['error' => $errMsg]); - exit; -} - -$responseData = json_decode($response, true); -$reply = ''; -if (isset($responseData['choices'][0]['message']['content'])) { - $reply = $responseData['choices'][0]['message']['content']; -} else { - http_response_code(502); - echo json_encode(['error' => 'Invalid response structure from Groq API.', 'raw' => $responseData]); - exit; -} - -echo json_encode(['reply' => $reply]); diff --git a/api/test_models.php b/api/test_models.php deleted file mode 100644 index caf424d..0000000 --- a/api/test_models.php +++ /dev/null @@ -1,23 +0,0 @@ -p { + color: var(--text-muted); + margin-bottom: 40px; + font-size: 1.05rem; + line-height: 1.8; +} + +.social-links { + display: flex; + justify-content: center; + gap: 14px; + margin-bottom: 50px; +} + +.social-btn { + display: flex; + align-items: center; + justify-content: center; + width: 50px; + height: 50px; + border-radius: 13px; + background: var(--card-bg); + border: 1px solid var(--border); + color: var(--text-muted); + transition: all 0.25s var(--ease); + text-decoration: none; +} + +.social-btn svg { + width: 20px; + height: 20px; +} + +.social-btn:hover { + border-color: var(--border-hover); + color: var(--accent-2); + transform: translateY(-3px); + background: rgba(37, 99, 235, 0.08); + box-shadow: 0 4px 12px rgba(37, 99, 235, 0.15); +} + +.email-form { + display: flex; + flex-direction: column; + gap: 14px; + text-align: left; +} + +input, +textarea { + width: 100%; + padding: 14px 18px; + background: var(--card-bg); + border: 1px solid var(--border); + color: var(--text-main); + border-radius: 12px; + font-family: inherit; + font-size: 0.95rem; + transition: border-color 0.25s var(--ease), box-shadow 0.25s var(--ease), background 0.25s; + backdrop-filter: blur(10px); + outline: none; +} + +input::placeholder, +textarea::placeholder { + color: var(--text-subtle); +} + +input:focus, +textarea:focus { + border-color: var(--border-hover); + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15); + background: rgba(37, 99, 235, 0.03); +} + +/* reCAPTCHA centering */ +.g-recaptcha { + display: flex; + justify-content: center; +} diff --git a/assets/css/modules/_hero.css b/assets/css/modules/_hero.css new file mode 100644 index 0000000..46ba510 --- /dev/null +++ b/assets/css/modules/_hero.css @@ -0,0 +1,227 @@ +/* ===================================================== + HERO SECTION + ===================================================== */ +.hero { + display: flex; + align-items: center; + justify-content: space-between; + gap: 60px; + padding: clamp(60px, 10vw, 120px) 5% clamp(50px, 8vw, 100px); + max-width: 1100px; + margin: 0 auto; + width: 100%; + box-sizing: border-box; +} + +.hero-text { + flex: 1; +} + +.hero-badge { + display: inline-flex; + align-items: center; + gap: 8px; + background: rgba(37, 99, 235, 0.1); + border: 1px solid rgba(37, 99, 235, 0.25); + border-radius: 50px; + padding: 6px 16px; + font-size: 0.8rem; + font-weight: 600; + color: var(--accent-2); + letter-spacing: 0.05em; + text-transform: uppercase; + margin-bottom: 20px; +} + +.hero-badge .dot { + width: 7px; + height: 7px; + background: #22c55e; + border-radius: 50%; + animation: pulse-dot 2s ease-in-out infinite; +} + +h1 { + font-family: 'Outfit', sans-serif; + font-size: clamp(2.8rem, 5.5vw, 4rem); + font-weight: 900; + line-height: 1.05; + letter-spacing: -2px; + margin-bottom: 20px; + color: var(--text-main); +} + +.highlight { + background: linear-gradient(135deg, var(--grad-1) 0%, var(--grad-2) 50%, var(--grad-3) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.subtitle { + color: var(--text-muted); + font-size: clamp(0.95rem, 2vw, 1.1rem); + margin-bottom: 30px; + max-width: 500px; + line-height: 1.7; +} + +.hero-btns { + display: flex; + gap: 14px; + flex-wrap: wrap; +} + +/* --- Hero Profile Image --- */ +.hero-img-container { + position: relative; + width: 280px; + height: 280px; + flex-shrink: 0; +} + +.img-ring { + position: absolute; + inset: -8px; + border-radius: 24px; + border: 2px solid var(--accent); + background: transparent; + z-index: 0; + opacity: 0.4; +} + +.profile-pic { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 20px; + border: 2px solid var(--border); + z-index: 2; + transition: opacity 0.4s var(--ease); + opacity: 0; +} + +/* Dark mode image states */ +.img-night-normal { opacity: 1; z-index: 3; cursor: default; } +.hero-img-container:hover .img-night-hover { opacity: 1; z-index: 4; cursor: default; } +.hero-img-container:hover .img-night-normal { opacity: 0; } + +/* Light mode image states */ +body:has(#dark-mode-toggle:not(:checked)) .img-night-normal, +body:has(#dark-mode-toggle:not(:checked)) .img-night-hover { opacity: 0 !important; } +body:has(#dark-mode-toggle:not(:checked)) .img-day-normal { opacity: 1; z-index: 3; cursor: default; } +body:has(#dark-mode-toggle:not(:checked)) .hero-img-container:hover .img-day-hover { opacity: 1; z-index: 4; cursor: default; } +body:has(#dark-mode-toggle:not(:checked)) .hero-img-container:hover .img-day-normal { opacity: 0; } + +.glow-effect { + position: absolute; + inset: 0; + background: var(--accent); + border-radius: 20px; + opacity: 0.12; + z-index: 1; + filter: blur(30px); + transform: translate(10px, 10px); + transition: opacity 0.4s; +} + +.hero-img-container:hover .glow-effect { + opacity: 0.2; +} + +/* ===================================================== + ABOUT SECTION & TECH STACK + ===================================================== */ +.about-content { + display: grid; + grid-template-columns: 3fr 2fr; + gap: 60px; + align-items: start; +} + +.about-text p { + color: var(--text-muted); + margin-bottom: 18px; + font-size: 1.05rem; + line-height: 1.8; +} + +.about-text p strong { + color: var(--text-main); + font-weight: 600; +} + +.tech-stack h3 { + font-family: 'Outfit', sans-serif; + font-size: 1rem; + font-weight: 700; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.1em; + margin-bottom: 16px; +} + +.tags { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.tags span { + background: var(--card-bg); + border: 1px solid var(--border); + padding: 7px 16px; + border-radius: 50px; + font-size: 0.82rem; + font-weight: 600; + color: var(--text-muted); + letter-spacing: 0.02em; + transition: all 0.2s var(--ease); + backdrop-filter: blur(10px); +} + +.tags span:hover { + border-color: var(--border-hover); + color: var(--accent-2); + background: rgba(37, 99, 235, 0.08); + transform: translateY(-1px); +} + +/* ===================================================== + STATS ROW + ===================================================== */ +.stats-row { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 20px; + margin-top: 50px; + padding-top: 50px; + border-top: 1px solid var(--border); +} + +.stat-item { + text-align: center; +} + +.stat-number { + font-family: 'Outfit', sans-serif; + font-size: 2.2rem; + font-weight: 900; + background: linear-gradient(135deg, var(--grad-1), var(--grad-2)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + line-height: 1; + margin-bottom: 6px; +} + +.stat-label { + font-size: 0.8rem; + color: var(--text-muted); + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.08em; +} diff --git a/assets/css/modules/_layout.css b/assets/css/modules/_layout.css new file mode 100644 index 0000000..9b807aa --- /dev/null +++ b/assets/css/modules/_layout.css @@ -0,0 +1,83 @@ +/* ===================================================== + LAYOUT — Site wrapper, sections, labels, dividers, page hero + ===================================================== */ + +.site-wrapper { + position: relative; + z-index: 1; + min-height: 100vh; + width: 100%; + display: flex; + flex-direction: column; + padding-top: 73px; /* offset for fixed floating nav */ + transition: background-color 0.4s var(--ease), color 0.4s var(--ease); +} + +/* --- Section Commons --- */ +.section-pad { + padding: clamp(60px, 8vw, 100px) 5%; + max-width: 1100px; + margin: 0 auto; + width: 100%; + box-sizing: border-box; +} + +.section-header { + margin-bottom: 60px; +} + +.section-label { + display: inline-block; + font-size: 0.75rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.15em; + color: var(--accent-2); + margin-bottom: 12px; +} + +.section-title { + font-family: 'Outfit', sans-serif; + font-size: clamp(1.8rem, 3vw, 2.5rem); + font-weight: 800; + letter-spacing: -1px; + background: linear-gradient(135deg, var(--text-main) 60%, var(--text-muted)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* --- Divider --- */ +.section-divider { + height: 1px; + background: linear-gradient(to right, transparent, var(--border), transparent); + margin: 0 5%; +} + +/* --- Page Hero (sub-pages: blog, projects, certifications) --- */ +.page-hero { + padding: clamp(50px, 8vw, 80px) 5% clamp(40px, 6vw, 60px); + max-width: 1100px; + margin: 0 auto; + width: 100%; + box-sizing: border-box; +} + +.page-hero h1 { + font-size: clamp(1.8rem, 5vw, 3rem); + margin-bottom: 14px; +} + +.page-hero .subtitle { + max-width: 500px; +} + +/* --- Footer --- */ +footer { + padding: 50px 5%; + text-align: center; + color: var(--text-subtle); + font-size: 0.85rem; + border-top: 1px solid var(--border); + margin-top: auto; +} diff --git a/assets/css/modules/_nav.css b/assets/css/modules/_nav.css new file mode 100644 index 0000000..f1d6463 --- /dev/null +++ b/assets/css/modules/_nav.css @@ -0,0 +1,123 @@ +/* ===================================================== + NAVIGATION + ===================================================== */ +nav { + position: fixed; + top: 0; + z-index: 1000; + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 5%; + width: 100%; + background: var(--glass-bg); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border-bottom: 1px solid var(--border); + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.18); + transition: background 0.3s var(--ease), border-color 0.3s var(--ease), box-shadow 0.3s var(--ease); +} + +.logo { + display: inline-flex; + align-items: baseline; +} + +.logo-link, +.logo-dot-link { + font-family: 'Outfit', sans-serif; + font-weight: 900; + font-size: 1.6rem; + letter-spacing: -2px; + background: linear-gradient(135deg, var(--grad-1), var(--grad-2), var(--grad-3)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-decoration: none; + display: inline-block; +} + +.logo-dot-link { + cursor: default; +} + +.nav-links { + display: flex; + align-items: center; + gap: 8px; +} + +.nav-links a { + color: var(--text-muted); + text-decoration: none; + font-size: 0.9rem; + font-weight: 500; + padding: 7px 14px; + border-radius: 8px; + transition: color 0.2s var(--ease), background 0.2s var(--ease); + letter-spacing: 0.02em; +} + +.nav-links a:hover { + color: var(--text-main); + background: var(--card-bg); +} + +.nav-links a.active-nav { + color: var(--accent); + background: rgba(37, 99, 235, 0.1); +} + +/* Theme Toggle */ +.mode-btn { + cursor: pointer; + margin-left: 8px; + width: 38px; + height: 38px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 10px; + background: var(--card-bg); + border: 1px solid var(--border); + font-size: 1.1rem; + transition: background 0.2s, border-color 0.2s, transform 0.2s; + user-select: none; +} + +.mode-btn:hover { + border-color: var(--border-hover); + transform: scale(1.05); +} + +body:has(#dark-mode-toggle:checked) .icon-sun { display: none; } +body:has(#dark-mode-toggle:checked) .icon-moon { display: flex; } +body:has(#dark-mode-toggle:not(:checked)) .icon-moon { display: none; } +body:has(#dark-mode-toggle:not(:checked)) .icon-sun { display: flex; } + +/* Hamburger (Mobile) */ +.hamburger { + display: none; + cursor: pointer; + flex-direction: column; + gap: 5px; + padding: 4px; +} + +.bar { + display: block; + width: 24px; + height: 2px; + background: var(--text-main); + border-radius: 2px; + transition: all 0.3s var(--ease); +} + +.hamburger.active .bar:nth-child(1) { transform: translateY(7px) rotate(45deg); } +.hamburger.active .bar:nth-child(2) { opacity: 0; transform: scaleX(0); } +.hamburger.active .bar:nth-child(3) { transform: translateY(-7px) rotate(-45deg); } + +/* Elements hidden on desktop by default */ +.nav-close-btn { display: none; } +.nav-overlay { display: none; } +.mode-btn-panel { display: none; } diff --git a/assets/css/modules/_responsive.css b/assets/css/modules/_responsive.css new file mode 100644 index 0000000..0971a3d --- /dev/null +++ b/assets/css/modules/_responsive.css @@ -0,0 +1,242 @@ +/* ===================================================== + RESPONSIVE — TABLET (≤ 900px) + ===================================================== */ +@media (max-width: 900px) { + .hero { + gap: 40px; + } + + .hero-img-container { + width: 220px; + height: 220px; + } + + .about-content { + grid-template-columns: 1fr; + gap: 40px; + } + + .stats-row { + grid-template-columns: repeat(3, 1fr); + } +} + +/* ===================================================== + RESPONSIVE — MOBILE (≤ 768px) + ===================================================== */ +@media (max-width: 768px) { + + /* Hamburger visible */ + .hamburger { + display: flex; + } + + /* Nav desktop links hidden by default on mobile */ + .nav-links { + position: fixed; + top: 0; + right: -280px; /* slide from right */ + left: auto; + width: 280px; + height: 100vh; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + text-align: left; + padding: 80px 28px 40px; + gap: 4px; + background: var(--glass-bg); + backdrop-filter: blur(28px); + -webkit-backdrop-filter: blur(28px); + border-left: 1px solid var(--border); + transition: right 0.35s var(--ease); + z-index: 1001; + box-shadow: -4px 0 40px rgba(0, 0, 0, 0.3); + pointer-events: none; /* prevent clicks when off-screen */ + } + + /* Slide in from right */ + .nav-links.active { + right: 0; + pointer-events: auto; /* re-enable clicks when open */ + } + + .nav-links a { + font-size: 1.1rem; + font-weight: 600; + display: block; + padding: 12px 16px; + width: 100%; + border-radius: 12px; + } + + /* Nav close button */ + .nav-close-btn { + display: flex; + position: absolute; + top: 22px; + right: 20px; + background: var(--card-bg); + border: 1px solid var(--border); + color: var(--text-main); + width: 36px; + height: 36px; + border-radius: 9px; + align-items: center; + justify-content: center; + font-size: 1.1rem; + cursor: pointer; + transition: background 0.2s, border-color 0.2s, transform 0.2s; + z-index: 1002; + } + + .nav-close-btn:hover { + background: rgba(37, 99, 235, 0.1); + border-color: var(--border-hover); + transform: rotate(90deg); + } + + /* Nav overlay backdrop */ + .nav-overlay { + display: block; + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.45); + z-index: 1000; + opacity: 0; + pointer-events: none; + transition: opacity 0.35s var(--ease); + } + + .nav-overlay.active { + opacity: 1; + pointer-events: auto; + } + + /* Theme toggle — inside the slide panel on mobile */ + .mode-btn-panel { + display: flex; + margin-top: 24px; + margin-left: 0; + align-self: flex-start; + } + + /* Hide the nav-bar toggle on mobile (it's now inside the panel) */ + .mode-btn-nav { + display: none; + } + + /* Hero */ + .hero { + flex-direction: column-reverse; + text-align: center; + gap: 30px; + } + + .hero-btns { + justify-content: center; + flex-wrap: wrap; + } + + .subtitle { + margin: 0 auto 24px; + } + + .hero-badge { + margin: 0 auto 16px; + } + + .hero-img-container { + width: 180px; + height: 180px; + } + + /* Stats */ + .stats-row { + grid-template-columns: repeat(3, 1fr); + gap: 8px; + } + + .stat-number { + font-size: 1.6rem; + } + + .stat-label { + font-size: 0.7rem; + } + + /* Certs */ + .cert-card { + flex-direction: column; + text-align: center; + } + + /* Project grid single column on very narrow */ + .project-grid, + .blog-grid { + grid-template-columns: 1fr; + } + + /* Back to top smaller — centered */ + #back-to-top { + bottom: 16px; + left: 50%; + right: auto; + width: 42px; + height: 42px; + } + + /* Section divider */ + .section-divider { + margin: 0 4%; + } +} + +/* ===================================================== + RESPONSIVE — SMALL MOBILE (≤ 480px) + ===================================================== */ +@media (max-width: 480px) { + h1 { + letter-spacing: -1px; + } + + .stats-row { + grid-template-columns: repeat(3, 1fr); + } + + .cert-grid { + grid-template-columns: 1fr; + } + + .btn { + padding: 11px 18px; + font-size: 0.9rem; + } + + .hero-img-container { + width: 150px; + height: 150px; + } + + .social-links { + gap: 10px; + } + + .social-btn { + width: 44px; + height: 44px; + } + + /* Chatbot */ + #portfolio-chatbot-widget { + bottom: 16px; + right: 16px; + } + + #chatbot-window { + width: calc(100vw - 32px); + right: 16px; + bottom: 80px; + height: 60vh; + } +} diff --git a/assets/css/modules/_utilities.css b/assets/css/modules/_utilities.css new file mode 100644 index 0000000..0217ea6 --- /dev/null +++ b/assets/css/modules/_utilities.css @@ -0,0 +1,99 @@ +/* ===================================================== + UTILITIES + ===================================================== */ + +/* --- Back to Top Button --- */ +#back-to-top { + position: fixed; + bottom: 30px; + left: 50%; + transform: translateX(-50%) translateY(20px); + width: 48px; + height: 48px; + background: linear-gradient(135deg, var(--grad-1), var(--grad-2)); + color: white; + border-radius: 13px; + display: flex; + justify-content: center; + align-items: center; + text-decoration: none; + box-shadow: 0 4px 12px rgba(37, 99, 235, 0.25); + z-index: 998; + opacity: 0; + visibility: hidden; + transition: all 0.3s var(--ease); +} + +#back-to-top.show { + opacity: 1; + visibility: visible; + transform: translateX(-50%) translateY(0); +} + +#back-to-top:hover { + transform: translateX(-50%) translateY(-4px); + box-shadow: 0 8px 20px rgba(37, 99, 235, 0.4); +} + +/* --- Scroll Reveal Animations --- */ +.reveal { + opacity: 0; + transform: translateY(40px); + transition: opacity 0.8s var(--ease), transform 0.8s var(--ease); +} + +.reveal.active { + opacity: 1; + transform: translateY(0); +} + +.reveal-delay-1 { transition-delay: 0.1s; } +.reveal-delay-2 { transition-delay: 0.2s; } +.reveal-delay-3 { transition-delay: 0.3s; } +.reveal-delay-4 { transition-delay: 0.4s; } + +/* --- Buttons --- */ +.btn { + display: inline-flex; + align-items: center; + gap: 8px; + text-decoration: none; + padding: 13px 26px; + border-radius: 10px; + font-weight: 600; + font-size: 0.95rem; + transition: all 0.25s var(--ease); + border: none; + cursor: pointer; + font-family: inherit; + letter-spacing: 0.01em; +} + +.btn.primary { + background: linear-gradient(135deg, var(--grad-1), var(--grad-2)); + color: #fff; + box-shadow: 0 4px 12px rgba(37, 99, 235, 0.25); +} + +.btn.primary:hover { + transform: translateY(-2px); + box-shadow: 0 8px 20px rgba(37, 99, 235, 0.4); +} + +.btn.secondary { + background: var(--card-bg); + color: var(--text-main); + border: 1px solid var(--border); + backdrop-filter: blur(10px); +} + +.btn.secondary:hover { + transform: translateY(-2px); + border-color: var(--border-hover); + background: rgba(37, 99, 235, 0.08); +} + +.btn.full-width { + width: 100%; + justify-content: center; +} diff --git a/assets/css/modules/_variables.css b/assets/css/modules/_variables.css new file mode 100644 index 0000000..dbb6ee0 --- /dev/null +++ b/assets/css/modules/_variables.css @@ -0,0 +1,58 @@ +/* ===================================================== + VARIABLES & THEME TOKENS + ===================================================== */ + +/* --- Google Fonts --- */ +@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800;900&family=Inter:wght@300;400;500;600&display=swap'); + +/* --- CSS Variables (Dark Mode Default) --- */ +:root { + /* Backgrounds */ + --bg: #090d16; + --bg-secondary: #0f172a; + --card-bg: rgba(30, 41, 59, 0.4); + --card-border: rgba(148, 163, 184, 0.1); + --glass-bg: rgba(9, 13, 22, 0.8); + + /* Text */ + --text-main: #f8fafc; + --text-muted: #94a3b8; + --text-subtle: #64748b; + + /* Accents */ + --accent: #2563eb; + --accent-2: #3b82f6; + --accent-glow: rgba(37, 99, 235, 0.15); + --grad-1: #3b82f6; + --grad-2: #2563eb; + --grad-3: #1d4ed8; + + /* Borders */ + --border: rgba(148, 163, 184, 0.12); + --border-hover: rgba(37, 99, 235, 0.45); + + /* Shadows */ + --shadow-card: 0 4px 20px rgba(0, 0, 0, 0.25); + --shadow-glow: 0 4px 20px rgba(37, 99, 235, 0.08); + + /* Transitions */ + --ease: cubic-bezier(0.4, 0, 0.2, 1); +} + +/* --- Light Mode Overrides --- */ +body:has(#dark-mode-toggle:not(:checked)) { + --bg: #f8fafc; + --bg-secondary: #f1f5f9; + --card-bg: #ffffff; + --card-border: rgba(148, 163, 184, 0.15); + --glass-bg: rgba(248, 250, 252, 0.85); + --text-main: #0f172a; + --text-muted: #475569; + --text-subtle: #94a3b8; + --accent: #2563eb; + --accent-glow: rgba(37, 99, 235, 0.1); + --border: rgba(148, 163, 184, 0.12); + --border-hover: rgba(37, 99, 235, 0.4); + --shadow-card: 0 4px 12px rgba(15, 23, 42, 0.05); + --shadow-glow: 0 4px 12px rgba(37, 99, 235, 0.05); +} diff --git a/assets/css/style.css b/assets/css/style.css index c2601de..aff6cb5 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -1,1726 +1,15 @@ /* ===================================================== - DESIGN SYSTEM — Premium Portfolio v2 + CRIS CHARLES GARCIA PORTFOLIO — v2 + Main Stylesheet (Entry Point) ===================================================== */ -/* --- Google Fonts --- */ -@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800;900&family=Inter:wght@300;400;500;600&display=swap'); - -/* Prevent horizontal scroll on all viewports */ -html, -body { - overflow-x: hidden; - max-width: 100%; -} - -/* --- CSS Variables (Dark Mode Default) --- */ -:root { - /* Backgrounds */ - --bg: #090d16; - --bg-secondary: #0f172a; - --card-bg: rgba(30, 41, 59, 0.4); - --card-border: rgba(148, 163, 184, 0.1); - --glass-bg: rgba(9, 13, 22, 0.8); - - /* Text */ - --text-main: #f8fafc; - --text-muted: #94a3b8; - --text-subtle: #64748b; - - /* Accents */ - --accent: #2563eb; - --accent-2: #3b82f6; - --accent-glow: rgba(37, 99, 235, 0.15); - --grad-1: #3b82f6; - --grad-2: #2563eb; - --grad-3: #1d4ed8; - - /* Borders */ - --border: rgba(148, 163, 184, 0.12); - --border-hover: rgba(37, 99, 235, 0.45); - - /* Shadows */ - --shadow-card: 0 4px 20px rgba(0, 0, 0, 0.25); - --shadow-glow: 0 4px 20px rgba(37, 99, 235, 0.08); - - /* Transitions */ - --ease: cubic-bezier(0.4, 0, 0.2, 1); -} - -/* --- Light Mode Overrides --- */ -body:has(#dark-mode-toggle:not(:checked)) { - --bg: #f8fafc; - --bg-secondary: #f1f5f9; - --card-bg: #ffffff; - --card-border: rgba(148, 163, 184, 0.15); - --glass-bg: rgba(248, 250, 252, 0.85); - --text-main: #0f172a; - --text-muted: #475569; - --text-subtle: #94a3b8; - --accent: #2563eb; - --accent-glow: rgba(37, 99, 235, 0.1); - --border: rgba(148, 163, 184, 0.12); - --border-hover: rgba(37, 99, 235, 0.4); - --shadow-card: 0 4px 12px rgba(15, 23, 42, 0.05); - --shadow-glow: 0 4px 12px rgba(37, 99, 235, 0.05); -} - -/* ===================================================== - RESET & BASE - ===================================================== */ -*, -*::before, -*::after { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -html { - scroll-behavior: smooth; - font-size: 16px; -} - -/* Custom Scrollbar */ -html::-webkit-scrollbar { - width: 6px; -} - -html::-webkit-scrollbar-track { - background: var(--bg); -} - -html::-webkit-scrollbar-thumb { - background: var(--accent); - border-radius: 3px; -} - -html::-webkit-scrollbar-thumb:hover { - background: var(--accent-2); -} - -body { - font-family: 'Inter', sans-serif; - line-height: 1.6; - background: var(--bg); - color: var(--text-main); - overflow-x: hidden; -} - -/* Hidden toggle */ -.toggle-checkbox { - display: none; -} - -/* ===================================================== - ANIMATED MESH BACKGROUND (Disabled for professional look) - ===================================================== */ -.mesh-bg { - display: none; -} - -.orb { - position: absolute; - border-radius: 50%; - filter: blur(100px); - opacity: 0.15; - animation: float-orb 20s ease-in-out infinite; -} - -.orb-1 { - width: 600px; - height: 600px; - background: radial-gradient(circle, #7c3aed, transparent 70%); - top: -200px; - left: -200px; - animation-duration: 25s; - animation-delay: 0s; -} - -.orb-2 { - width: 500px; - height: 500px; - background: radial-gradient(circle, #6366f1, transparent 70%); - top: 30%; - right: -150px; - animation-duration: 20s; - animation-delay: -8s; - opacity: 0.12; -} - -.orb-3 { - width: 400px; - height: 400px; - background: radial-gradient(circle, #06b6d4, transparent 70%); - bottom: 10%; - left: 20%; - animation-duration: 30s; - animation-delay: -15s; - opacity: 0.08; -} - -@keyframes float-orb { - - 0%, - 100% { - transform: translate(0, 0) scale(1); - } - - 33% { - transform: translate(40px, -60px) scale(1.08); - } - - 66% { - transform: translate(-30px, 40px) scale(0.95); - } -} - -/* Light mode: softer orbs */ -.toggle-checkbox:not(:checked)~.mesh-bg .orb { - opacity: 0.07; -} - -/* ===================================================== - SITE WRAPPER - ===================================================== */ -.site-wrapper { - position: relative; - z-index: 1; - min-height: 100vh; - width: 100%; - display: flex; - flex-direction: column; - padding-top: 73px; /* offset for fixed floating nav */ - transition: background-color 0.4s var(--ease), color 0.4s var(--ease); -} - -/* ===================================================== - NAVIGATION - ===================================================== */ -nav { - position: fixed; - top: 0; - z-index: 1000; - display: flex; - justify-content: space-between; - align-items: center; - padding: 16px 5%; - width: 100%; - background: var(--glass-bg); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border-bottom: 1px solid var(--border); - box-shadow: 0 4px 24px rgba(0, 0, 0, 0.18); - transition: background 0.3s var(--ease), border-color 0.3s var(--ease), box-shadow 0.3s var(--ease); -} - -.logo { - display: inline-flex; - align-items: baseline; -} - -.logo-link, -.logo-dot-link { - font-family: 'Outfit', sans-serif; - font-weight: 900; - font-size: 1.6rem; - letter-spacing: -2px; - background: linear-gradient(135deg, var(--grad-1), var(--grad-2), var(--grad-3)); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - text-decoration: none; - display: inline-block; -} - -.logo-dot-link { - cursor: default; -} - -.nav-links { - display: flex; - align-items: center; - gap: 8px; -} - -.nav-links a { - color: var(--text-muted); - text-decoration: none; - font-size: 0.9rem; - font-weight: 500; - padding: 7px 14px; - border-radius: 8px; - transition: color 0.2s var(--ease), background 0.2s var(--ease); - letter-spacing: 0.02em; -} - -.nav-links a:hover { - color: var(--text-main); - background: var(--card-bg); -} - -.nav-links a.active-nav { - color: var(--accent); - background: rgba(37, 99, 235, 0.1); -} - -/* Theme Toggle */ -.mode-btn { - cursor: pointer; - margin-left: 8px; - width: 38px; - height: 38px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 10px; - background: var(--card-bg); - border: 1px solid var(--border); - font-size: 1.1rem; - transition: background 0.2s, border-color 0.2s, transform 0.2s; - user-select: none; -} - -.mode-btn:hover { - border-color: var(--border-hover); - transform: scale(1.05); -} - -body:has(#dark-mode-toggle:checked) .icon-sun { - display: none; -} - -body:has(#dark-mode-toggle:checked) .icon-moon { - display: flex; -} - -body:has(#dark-mode-toggle:not(:checked)) .icon-moon { - display: none; -} - -body:has(#dark-mode-toggle:not(:checked)) .icon-sun { - display: flex; -} - -/* Hamburger */ -.hamburger { - display: none; - cursor: pointer; - flex-direction: column; - gap: 5px; - padding: 4px; -} - -/* Nav close button & overlay hidden on desktop */ -.nav-close-btn { - display: none; -} - -.nav-overlay { - display: none; -} - -/* Desktop: hide the duplicate mode-btn inside nav-links panel */ -.mode-btn-panel { - display: none; -} - -.bar { - display: block; - width: 24px; - height: 2px; - background: var(--text-main); - border-radius: 2px; - transition: all 0.3s var(--ease); -} - -.hamburger.active .bar:nth-child(1) { - transform: translateY(7px) rotate(45deg); -} - -.hamburger.active .bar:nth-child(2) { - opacity: 0; - transform: scaleX(0); -} - -.hamburger.active .bar:nth-child(3) { - transform: translateY(-7px) rotate(-45deg); -} - -/* ===================================================== - SCROLL REVEAL - ===================================================== */ -.reveal { - opacity: 0; - transform: translateY(40px); - transition: opacity 0.8s var(--ease), transform 0.8s var(--ease); -} - -.reveal.active { - opacity: 1; - transform: translateY(0); -} - -.reveal-delay-1 { - transition-delay: 0.1s; -} - -.reveal-delay-2 { - transition-delay: 0.2s; -} - -.reveal-delay-3 { - transition-delay: 0.3s; -} - -.reveal-delay-4 { - transition-delay: 0.4s; -} - -/* ===================================================== - HERO SECTION - ===================================================== */ -.hero { - display: flex; - align-items: center; - justify-content: space-between; - gap: 60px; - padding: clamp(60px, 10vw, 120px) 5% clamp(50px, 8vw, 100px); - max-width: 1100px; - margin: 0 auto; - width: 100%; - box-sizing: border-box; -} - -.hero-text { - flex: 1; -} - -.hero-badge { - display: inline-flex; - align-items: center; - gap: 8px; - background: rgba(37, 99, 235, 0.1); - border: 1px solid rgba(37, 99, 235, 0.25); - border-radius: 50px; - padding: 6px 16px; - font-size: 0.8rem; - font-weight: 600; - color: var(--accent-2); - letter-spacing: 0.05em; - text-transform: uppercase; - margin-bottom: 20px; -} - -.hero-badge .dot { - width: 7px; - height: 7px; - background: #22c55e; - border-radius: 50%; - animation: pulse-dot 2s ease-in-out infinite; -} - -@keyframes pulse-dot { - - 0%, - 100% { - opacity: 1; - transform: scale(1); - } - - 50% { - opacity: 0.6; - transform: scale(0.8); - } -} - -h1 { - font-family: 'Outfit', sans-serif; - font-size: clamp(2.8rem, 5.5vw, 4rem); - font-weight: 900; - line-height: 1.05; - letter-spacing: -2px; - margin-bottom: 20px; - color: var(--text-main); -} - -.highlight { - background: linear-gradient(135deg, var(--grad-1) 0%, var(--grad-2) 50%, var(--grad-3) 100%); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.subtitle { - color: var(--text-muted); - font-size: clamp(0.95rem, 2vw, 1.1rem); - margin-bottom: 30px; - max-width: 500px; - line-height: 1.7; -} - -.hero-btns { - display: flex; - gap: 14px; - flex-wrap: wrap; -} - -/* ===================================================== - BUTTONS - ===================================================== */ -.btn { - display: inline-flex; - align-items: center; - gap: 8px; - text-decoration: none; - padding: 13px 26px; - border-radius: 10px; - font-weight: 600; - font-size: 0.95rem; - transition: all 0.25s var(--ease); - border: none; - cursor: pointer; - font-family: inherit; - letter-spacing: 0.01em; -} - -.btn.primary { - background: linear-gradient(135deg, var(--grad-1), var(--grad-2)); - color: #fff; - box-shadow: 0 4px 12px rgba(37, 99, 235, 0.25); -} - -.btn.primary:hover { - transform: translateY(-2px); - box-shadow: 0 8px 20px rgba(37, 99, 235, 0.4); -} - -.btn.secondary { - background: var(--card-bg); - color: var(--text-main); - border: 1px solid var(--border); - backdrop-filter: blur(10px); -} - -.btn.secondary:hover { - transform: translateY(-2px); - border-color: var(--border-hover); - background: rgba(37, 99, 235, 0.08); -} - -.btn.full-width { - width: 100%; - justify-content: center; -} - -/* ===================================================== - HERO PROFILE IMAGE - ===================================================== */ -.hero-img-container { - position: relative; - width: 280px; - height: 280px; - flex-shrink: 0; -} - -.img-ring { - position: absolute; - inset: -8px; - border-radius: 24px; - border: 2px solid var(--accent); - background: transparent; - z-index: 0; - opacity: 0.4; -} - -.profile-pic { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - object-fit: cover; - border-radius: 20px; - border: 2px solid var(--border); - z-index: 2; - transition: opacity 0.4s var(--ease); - opacity: 0; -} - -/* Dark mode (default) image states */ -.img-night-normal { - opacity: 1; - z-index: 3; - cursor: default; -} - -.hero-img-container:hover .img-night-hover { - opacity: 1; - z-index: 4; - cursor: default; -} - -.hero-img-container:hover .img-night-normal { - opacity: 0; -} - -/* Light mode image states */ -body:has(#dark-mode-toggle:not(:checked)) .img-night-normal, -body:has(#dark-mode-toggle:not(:checked)) .img-night-hover { - opacity: 0 !important; -} - -body:has(#dark-mode-toggle:not(:checked)) .img-day-normal { - opacity: 1; - z-index: 3; - cursor: default; -} - -body:has(#dark-mode-toggle:not(:checked)) .hero-img-container:hover .img-day-hover { - opacity: 1; - z-index: 4; - cursor: default; -} - -body:has(#dark-mode-toggle:not(:checked)) .hero-img-container:hover .img-day-normal { - opacity: 0; -} - -.glow-effect { - position: absolute; - inset: 0; - background: var(--accent); - border-radius: 20px; - opacity: 0.12; - z-index: 1; - filter: blur(30px); - transform: translate(10px, 10px); - transition: opacity 0.4s; -} - -.hero-img-container:hover .glow-effect { - opacity: 0.2; -} - -/* ===================================================== - SECTION COMMONS - ===================================================== */ -.section-pad { - padding: clamp(60px, 8vw, 100px) 5%; - max-width: 1100px; - margin: 0 auto; - width: 100%; - box-sizing: border-box; -} - -.section-header { - margin-bottom: 60px; -} - -.section-label { - display: inline-block; - font-size: 0.75rem; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.15em; - color: var(--accent-2); - margin-bottom: 12px; -} - -.section-title { - font-family: 'Outfit', sans-serif; - font-size: clamp(1.8rem, 3vw, 2.5rem); - font-weight: 800; - letter-spacing: -1px; - background: linear-gradient(135deg, var(--text-main) 60%, var(--text-muted)); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -/* ===================================================== - ABOUT SECTION - ===================================================== */ -.about-content { - display: grid; - grid-template-columns: 3fr 2fr; - gap: 60px; - align-items: start; -} - -.about-text p { - color: var(--text-muted); - margin-bottom: 18px; - font-size: 1.05rem; - line-height: 1.8; -} - -.about-text p strong { - color: var(--text-main); - font-weight: 600; -} - -.tech-stack h3 { - font-family: 'Outfit', sans-serif; - font-size: 1rem; - font-weight: 700; - color: var(--text-muted); - text-transform: uppercase; - letter-spacing: 0.1em; - margin-bottom: 16px; -} - -.tags { - display: flex; - flex-wrap: wrap; - gap: 10px; -} - -.tags span { - background: var(--card-bg); - border: 1px solid var(--border); - padding: 7px 16px; - border-radius: 50px; - font-size: 0.82rem; - font-weight: 600; - color: var(--text-muted); - letter-spacing: 0.02em; - transition: all 0.2s var(--ease); - backdrop-filter: blur(10px); -} - -.tags span:hover { - border-color: var(--border-hover); - color: var(--accent-2); - background: rgba(37, 99, 235, 0.08); - transform: translateY(-1px); -} - -/* ===================================================== - STATS ROW - ===================================================== */ -.stats-row { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 20px; - margin-top: 50px; - padding-top: 50px; - border-top: 1px solid var(--border); -} - -.stat-item { - text-align: center; -} - -.stat-number { - font-family: 'Outfit', sans-serif; - font-size: 2.2rem; - font-weight: 900; - background: linear-gradient(135deg, var(--grad-1), var(--grad-2)); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - line-height: 1; - margin-bottom: 6px; -} - -.stat-label { - font-size: 0.8rem; - color: var(--text-muted); - font-weight: 500; - text-transform: uppercase; - letter-spacing: 0.08em; -} - -/* ===================================================== - PROJECT CARDS - ===================================================== */ -.project-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr)); - gap: 24px; -} - -a.project-card { - text-decoration: none; - color: inherit; - display: block; - background: var(--card-bg); - border: 1px solid var(--border); - border-radius: 16px; - overflow: hidden; - transition: transform 0.3s var(--ease), border-color 0.3s var(--ease), box-shadow 0.3s var(--ease); - backdrop-filter: blur(10px); -} - -a.project-card:hover { - transform: translateY(-6px); - border-color: var(--border-hover); - box-shadow: var(--shadow-glow); -} - -.card-img { - position: relative; - height: 200px; - overflow: hidden; -} - -.card-img img { - width: 100%; - height: 100%; - object-fit: cover; - transition: transform 0.5s var(--ease); -} - -a.project-card:hover .card-img img { - transform: scale(1.05); -} - -.card-img-overlay { - position: absolute; - inset: 0; - background: linear-gradient(to bottom, transparent 40%, rgba(9, 13, 22, 0.8)); - transition: opacity 0.3s; -} - -.card-content { - padding: 24px; -} - -.card-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: 10px; - gap: 10px; -} - -.card-header h3 { - font-family: 'Outfit', sans-serif; - font-size: 1.15rem; - font-weight: 700; - color: var(--text-main); - line-height: 1.3; -} - -.arrow-icon { - font-size: 1.1rem; - opacity: 0; - transform: translate(-6px, 6px); - transition: all 0.3s var(--ease); - color: var(--accent-2); - flex-shrink: 0; -} - -a.project-card:hover .arrow-icon { - opacity: 1; - transform: translate(0, 0); -} - -.card-content p { - color: var(--text-muted); - font-size: 0.88rem; - margin-bottom: 18px; - line-height: 1.7; -} - -.stack-badges { - display: flex; - flex-wrap: wrap; - gap: 6px; -} - -.stack-badge { - padding: 4px 10px; - border-radius: 50px; - font-size: 0.72rem; - font-weight: 700; - letter-spacing: 0.04em; - background: rgba(59, 130, 246, 0.12); - border: 1px solid rgba(59, 130, 246, 0.25); - color: var(--accent-2); -} - -/* ===================================================== - CERTIFICATIONS - ===================================================== */ -.cert-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr)); - gap: 20px; -} - -.cert-card { - background: var(--card-bg); - border: 1px solid var(--border); - border-radius: 16px; - padding: 28px; - display: flex; - align-items: center; - gap: 20px; - transition: all 0.3s var(--ease); - backdrop-filter: blur(10px); - cursor: default; -} - -.cert-card:hover { - border-color: var(--border-hover); - transform: translateY(-3px); - box-shadow: var(--shadow-glow); -} - -.cert-icon { - font-size: 2rem; - width: 56px; - height: 56px; - display: flex; - align-items: center; - justify-content: center; - background: rgba(37, 99, 235, 0.1); - border: 1px solid rgba(37, 99, 235, 0.2); - border-radius: 14px; - flex-shrink: 0; - transition: background 0.3s; -} - -.cert-card:hover .cert-icon { - background: rgba(37, 99, 235, 0.18); -} - -.cert-info h3 { - font-family: 'Outfit', sans-serif; - font-size: 1rem; - font-weight: 700; - margin-bottom: 4px; - color: var(--text-main); -} - -.issuer { - color: var(--accent-2); - font-size: 0.82rem; - font-weight: 600; -} - -.date { - display: block; - font-size: 0.75rem; - color: var(--text-subtle); - margin-top: 4px; -} - -/* ===================================================== - BLOG CARDS - ===================================================== */ -.blog-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr)); - gap: 24px; -} - -.blog-card { - background: var(--card-bg); - border: 1px solid var(--border); - padding: 32px; - border-radius: 16px; - display: flex; - flex-direction: column; - transition: all 0.3s var(--ease); - backdrop-filter: blur(10px); -} - -.blog-card:hover { - transform: translateY(-5px); - border-color: var(--border-hover); - box-shadow: var(--shadow-glow); -} - -.blog-meta { - display: flex; - justify-content: space-between; - align-items: center; - font-size: 0.75rem; - color: var(--text-subtle); - margin-bottom: 16px; - text-transform: uppercase; - letter-spacing: 0.08em; - font-weight: 600; -} - -.blog-tag { - color: var(--accent-2); - background: rgba(59, 130, 246, 0.12); - border: 1px solid rgba(59, 130, 246, 0.25); - padding: 3px 10px; - border-radius: 50px; -} - -.blog-title { - font-family: 'Outfit', sans-serif; - font-size: 1.15rem; - font-weight: 700; - margin-bottom: 12px; - line-height: 1.4; - color: var(--text-main); -} - -.blog-excerpt { - color: var(--text-muted); - font-size: 0.9rem; - margin-bottom: 24px; - flex-grow: 1; - line-height: 1.7; -} - -.read-more { - text-decoration: none; - color: var(--accent-2); - font-weight: 600; - font-size: 0.85rem; - display: inline-flex; - align-items: center; - gap: 6px; - transition: gap 0.2s var(--ease), opacity 0.2s; -} - -.read-more:hover { - gap: 10px; - opacity: 0.8; -} - -/* ===================================================== - VIEW ALL BUTTON - ===================================================== */ -.view-all-container { - display: flex; - justify-content: center; - margin-top: 50px; -} - -/* ===================================================== - CONTACT SECTION - ===================================================== */ -.contact-container { - max-width: 620px; - margin: 0 auto; - text-align: center; -} - -.contact-container>p { - color: var(--text-muted); - margin-bottom: 40px; - font-size: 1.05rem; - line-height: 1.8; -} - -.social-links { - display: flex; - justify-content: center; - gap: 14px; - margin-bottom: 50px; -} - -.social-btn { - display: flex; - align-items: center; - justify-content: center; - width: 50px; - height: 50px; - border-radius: 13px; - background: var(--card-bg); - border: 1px solid var(--border); - color: var(--text-muted); - transition: all 0.25s var(--ease); - text-decoration: none; -} - -.social-btn svg { - width: 20px; - height: 20px; -} - -.social-btn:hover { - border-color: var(--border-hover); - color: var(--accent-2); - transform: translateY(-3px); - background: rgba(37, 99, 235, 0.08); - box-shadow: 0 4px 12px rgba(37, 99, 235, 0.15); -} - -.email-form { - display: flex; - flex-direction: column; - gap: 14px; - text-align: left; -} - -input, -textarea { - width: 100%; - padding: 14px 18px; - background: var(--card-bg); - border: 1px solid var(--border); - color: var(--text-main); - border-radius: 12px; - font-family: inherit; - font-size: 0.95rem; - transition: border-color 0.25s var(--ease), box-shadow 0.25s var(--ease), background 0.25s; - backdrop-filter: blur(10px); - outline: none; -} - -input::placeholder, -textarea::placeholder { - color: var(--text-subtle); -} - -input:focus, -textarea:focus { - border-color: var(--border-hover); - box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15); - background: rgba(37, 99, 235, 0.03); -} - -/* reCAPTCHA centering */ -.g-recaptcha { - display: flex; - justify-content: center; -} - -/* ===================================================== - FOOTER - ===================================================== */ -footer { - padding: 50px 5%; - text-align: center; - color: var(--text-subtle); - font-size: 0.85rem; - border-top: 1px solid var(--border); - margin-top: auto; -} - -/* ===================================================== - BACK TO TOP - ===================================================== */ -#back-to-top { - position: fixed; - bottom: 30px; - left: 50%; - transform: translateX(-50%) translateY(20px); - width: 48px; - height: 48px; - background: linear-gradient(135deg, var(--grad-1), var(--grad-2)); - color: white; - border-radius: 13px; - display: flex; - justify-content: center; - align-items: center; - text-decoration: none; - box-shadow: 0 4px 12px rgba(37, 99, 235, 0.25); - z-index: 998; - opacity: 0; - visibility: hidden; - transition: all 0.3s var(--ease); -} - -#back-to-top.show { - opacity: 1; - visibility: visible; - transform: translateX(-50%) translateY(0); -} - -#back-to-top:hover { - transform: translateX(-50%) translateY(-4px); - box-shadow: 0 8px 20px rgba(37, 99, 235, 0.4); -} - -/* ===================================================== - PAGE HERO (Sub-pages) - ===================================================== */ -.page-hero { - padding: clamp(50px, 8vw, 80px) 5% clamp(40px, 6vw, 60px); - max-width: 1100px; - margin: 0 auto; - width: 100%; - box-sizing: border-box; -} - -.page-hero h1 { - font-size: clamp(1.8rem, 5vw, 3rem); - margin-bottom: 14px; -} - -.page-hero .subtitle { - max-width: 500px; -} - -/* ===================================================== - DIVIDER - ===================================================== */ -.section-divider { - height: 1px; - background: linear-gradient(to right, transparent, var(--border), transparent); - margin: 0 5%; -} - -/* ===================================================== - RESPONSIVE — TABLET (≤ 900px) - ===================================================== */ -@media (max-width: 900px) { - .hero { - gap: 40px; - } - - .hero-img-container { - width: 220px; - height: 220px; - } - - .about-content { - grid-template-columns: 1fr; - gap: 40px; - } - - .stats-row { - grid-template-columns: repeat(3, 1fr); - } -} - -/* ===================================================== - RESPONSIVE — MOBILE (≤ 768px) - ===================================================== */ -@media (max-width: 768px) { - - /* Hamburger visible */ - .hamburger { - display: flex; - } - - /* Nav desktop links hidden by default on mobile */ - .nav-links { - position: fixed; - top: 0; - right: -280px; /* slide from right */ - left: auto; - width: 280px; - height: 100vh; - flex-direction: column; - align-items: flex-start; - justify-content: flex-start; - text-align: left; - padding: 80px 28px 40px; - gap: 4px; - background: var(--glass-bg); - backdrop-filter: blur(28px); - -webkit-backdrop-filter: blur(28px); - border-left: 1px solid var(--border); - transition: right 0.35s var(--ease); - z-index: 1001; - box-shadow: -4px 0 40px rgba(0, 0, 0, 0.3); - pointer-events: none; /* prevent clicks when off-screen */ - } - - /* Slide in from right */ - .nav-links.active { - right: 0; - pointer-events: auto; /* re-enable clicks when open */ - } - - .nav-links a { - font-size: 1.1rem; - font-weight: 600; - display: block; - padding: 12px 16px; - width: 100%; - border-radius: 12px; - } - - /* Nav close button */ - .nav-close-btn { - display: flex; - position: absolute; - top: 22px; - right: 20px; - background: var(--card-bg); - border: 1px solid var(--border); - color: var(--text-main); - width: 36px; - height: 36px; - border-radius: 9px; - align-items: center; - justify-content: center; - font-size: 1.1rem; - cursor: pointer; - transition: background 0.2s, border-color 0.2s, transform 0.2s; - z-index: 1002; - } - - .nav-close-btn:hover { - background: rgba(37, 99, 235, 0.1); - border-color: var(--border-hover); - transform: rotate(90deg); - } - - /* Nav overlay backdrop */ - .nav-overlay { - display: block; - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.45); - z-index: 1000; - opacity: 0; - pointer-events: none; - transition: opacity 0.35s var(--ease); - } - - .nav-overlay.active { - opacity: 1; - pointer-events: auto; - } - - /* Theme toggle — inside the slide panel on mobile */ - .mode-btn-panel { - display: flex; - margin-top: 24px; - margin-left: 0; - align-self: flex-start; - } - - /* Hide the nav-bar toggle on mobile (it's now inside the panel) */ - .mode-btn-nav { - display: none; - } - - /* Hero */ - .hero { - flex-direction: column-reverse; - text-align: center; - gap: 30px; - } - - .hero-btns { - justify-content: center; - flex-wrap: wrap; - } - - .subtitle { - margin: 0 auto 24px; - } - - .hero-badge { - margin: 0 auto 16px; - } - - .hero-img-container { - width: 180px; - height: 180px; - } - - /* Stats */ - .stats-row { - grid-template-columns: repeat(3, 1fr); - gap: 8px; - } - - .stat-number { - font-size: 1.6rem; - } - - .stat-label { - font-size: 0.7rem; - } - - /* Certs */ - .cert-card { - flex-direction: column; - text-align: center; - } - - /* Project grid single column on very narrow */ - .project-grid, - .blog-grid { - grid-template-columns: 1fr; - } - - /* Back to top smaller — centered */ - #back-to-top { - bottom: 16px; - left: 50%; - right: auto; - width: 42px; - height: 42px; - } - - /* Section divider */ - .section-divider { - margin: 0 4%; - } -} - -/* ===================================================== - RESPONSIVE — SMALL MOBILE (≤ 480px) - ===================================================== */ -@media (max-width: 480px) { - h1 { - letter-spacing: -1px; - } - - .stats-row { - grid-template-columns: repeat(3, 1fr); - } - - .cert-grid { - grid-template-columns: 1fr; - } - - .btn { - padding: 11px 18px; - font-size: 0.9rem; - } - - .hero-img-container { - width: 150px; - height: 150px; - } - - .social-links { - gap: 10px; - } - - .social-btn { - width: 44px; - height: 44px; - } -} - -/* ===================================================== - PORTFOLIO AI CHATBOT - ===================================================== */ -#portfolio-chatbot-widget { - position: fixed; - bottom: 24px; - right: 24px; - z-index: 9999; - font-family: 'Inter', sans-serif; -} - -#chatbot-toggle-btn { - position: relative; - width: 56px; - height: 56px; - border-radius: 50%; - background: linear-gradient(135deg, var(--grad-1), var(--grad-2)); - color: #fff; - border: none; - cursor: pointer; - box-shadow: 0 4px 20px rgba(37, 99, 235, 0.3); - display: flex; - align-items: center; - justify-content: center; - transition: transform 0.25s var(--ease), box-shadow 0.25s var(--ease); -} - -#chatbot-toggle-btn:hover { - transform: scale(1.08) translateY(-2px); - box-shadow: 0 8px 25px rgba(37, 99, 235, 0.45); -} - -#chatbot-toggle-btn svg { - width: 24px; - height: 24px; -} - -.pulse-notification-dot { - position: absolute; - top: 2px; - right: 2px; - width: 12px; - height: 12px; - background: #22c55e; - border: 2px solid var(--bg); - border-radius: 50%; - animation: pulse-dot 2s ease-in-out infinite; -} - -#chatbot-window { - position: fixed; - bottom: 96px; - right: 24px; - width: 360px; - height: min(500px, 80vh); - display: flex; - flex-direction: column; - background: var(--glass-bg); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border: 1px solid var(--border); - border-radius: 16px; - box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); - overflow: hidden; - transition: transform 0.3s var(--ease), opacity 0.3s var(--ease); - transform: translateY(10px); - opacity: 0; - pointer-events: none; -} - -#chatbot-window.active { - transform: translateY(0); - opacity: 1; - pointer-events: auto; -} - -#chatbot-window.hidden { - display: none !important; -} - -#chatbot-header { - padding: 14px 18px; - background: rgba(30, 41, 59, 0.3); - border-bottom: 1px solid var(--border); - display: flex; - justify-content: space-between; - align-items: center; -} - -.header-avatar-info { - display: flex; - align-items: center; - gap: 10px; -} - -.avatar-container { - position: relative; - width: 36px; - height: 36px; -} - -.chat-avatar { - width: 100%; - height: 100%; - border-radius: 50%; - object-fit: cover; - border: 1.5px solid var(--accent); -} - -.status-indicator { - position: absolute; - bottom: 0; - right: 0; - width: 10px; - height: 10px; - border-radius: 50%; - border: 1.5px solid var(--bg); -} - -.status-indicator.online { - background: #22c55e; -} - -.header-text-info h4 { - font-family: 'Outfit', sans-serif; - font-size: 0.95rem; - font-weight: 700; - color: var(--text-main); - line-height: 1.2; -} - -.status-text { - font-size: 0.72rem; - color: var(--text-muted); -} - -#chatbot-close-btn { - background: transparent; - border: none; - color: var(--text-muted); - cursor: pointer; - padding: 4px; - display: flex; - align-items: center; - justify-content: center; - transition: color 0.2s; - border-radius: 6px; -} - -#chatbot-close-btn:hover { - color: var(--text-main); - background: var(--card-bg); -} - -#chatbot-messages { - flex-grow: 1; - overflow-y: auto; - padding: 16px; - display: flex; - flex-direction: column; - gap: 12px; - background: rgba(0, 0, 0, 0.05); -} - -/* Custom Scrollbar for Chat */ -#chatbot-messages::-webkit-scrollbar { - width: 4px; -} - -#chatbot-messages::-webkit-scrollbar-track { - background: transparent; -} - -#chatbot-messages::-webkit-scrollbar-thumb { - background: var(--border); - border-radius: 2px; -} - -.chat-message { - display: flex; - width: 100%; -} - -.chat-message.user-msg { - justify-content: flex-end; -} - -.chat-message.bot-msg { - justify-content: flex-start; -} - -.msg-bubble { - max-width: 80%; - padding: 10px 14px; - border-radius: 14px; - font-size: 0.88rem; - line-height: 1.5; - word-break: break-word; -} - -.user-msg .msg-bubble { - background: var(--accent); - color: #fff; - border-bottom-right-radius: 2px; - box-shadow: 0 4px 10px rgba(37, 99, 235, 0.2); -} - -.bot-msg .msg-bubble { - background: var(--card-bg); - color: var(--text-main); - border: 1px solid var(--border); - border-bottom-left-radius: 2px; -} - -.bot-msg.error-msg .msg-bubble { - border-color: rgba(239, 68, 68, 0.3); - background: rgba(239, 68, 68, 0.05); - color: #ef4444; -} - -/* Form & Input */ -#chatbot-input-form { - padding: 12px 16px; - background: var(--glass-bg); - border-top: 1px solid var(--border); - display: flex; - gap: 8px; - align-items: center; -} - -#chatbot-input-field { - flex-grow: 1; - background: var(--card-bg); - border: 1px solid var(--border); - color: var(--text-main); - padding: 10px 14px; - border-radius: 10px; - font-size: 0.88rem; - outline: none; - transition: border-color 0.2s; -} - -#chatbot-input-field:focus { - border-color: var(--border-hover); -} - -#chatbot-send-btn { - background: var(--accent); - color: #fff; - border: none; - width: 38px; - height: 38px; - border-radius: 8px; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - transition: background 0.2s, transform 0.2s; - flex-shrink: 0; -} - -#chatbot-send-btn:hover { - background: var(--accent-2); - transform: scale(1.05); -} - -#chatbot-send-btn svg { - width: 16px; - height: 16px; -} - -/* Typing Indicator Animation */ -.typing-bubble { - display: flex; - align-items: center; - gap: 4px; - padding: 12px 16px !important; -} - -.dot-bounce { - width: 6px; - height: 6px; - background: var(--text-muted); - border-radius: 50%; - display: inline-block; - animation: bounce 1.4s infinite ease-in-out both; -} - -.dot-bounce:nth-child(1) { - animation-delay: -0.32s; -} - -.dot-bounce:nth-child(2) { - animation-delay: -0.16s; -} - -@keyframes bounce { - - 0%, - 80%, - 100% { - transform: scale(0); - } - - 40% { - transform: scale(1.0); - } -} - -/* Markdown spacing inside bubbles */ -.msg-bubble ul { - padding-left: 20px; - margin: 6px 0; -} - -.msg-bubble li { - margin-bottom: 4px; -} - -/* Mobile Responsive */ -@media (max-width: 480px) { - #portfolio-chatbot-widget { - bottom: 16px; - right: 16px; - } - - #chatbot-window { - width: calc(100vw - 32px); - right: 16px; - bottom: 80px; - height: 60vh; - } -} \ No newline at end of file +@import url('modules/_variables.css'); +@import url('modules/_base.css'); +@import url('modules/_layout.css'); +@import url('modules/_nav.css'); +@import url('modules/_hero.css'); +@import url('modules/_cards.css'); +@import url('modules/_contact.css'); +@import url('modules/_chatbot.css'); +@import url('modules/_utilities.css'); +@import url('modules/_responsive.css'); \ No newline at end of file diff --git a/assets/js/chat.js b/assets/js/chat.js index 5473904..3be3d05 100644 --- a/assets/js/chat.js +++ b/assets/js/chat.js @@ -7,7 +7,8 @@ document.addEventListener("DOMContentLoaded", () => { const messagesContainer = document.getElementById("chatbot-messages"); const pulseDot = document.querySelector(".pulse-notification-dot"); - let chatHistory = []; + // Initialize the new JavaScript intent matching engine using injected PHP data + const engine = new window.PortfolioChatEngine(window.portfolioData || {}); let isWaiting = false; // Toggle Chat Window @@ -33,189 +34,121 @@ document.addEventListener("DOMContentLoaded", () => { const message = inputField.value.trim(); if (!message) return; - // Add user message to UI and history + // Add user message to UI addMessage(message, "user-msg"); - chatHistory.push({ role: "user", text: message }); inputField.value = ""; - - // Cap history to last 10 messages - if (chatHistory.length > 10) { - chatHistory = chatHistory.slice(-10); - } - - // Add typing indicator - const typingIndicator = showTypingIndicator(); + isWaiting = true; - // Try backend proxy first, fallback to client-side if it fails - let replySuccess = false; + // Show typing indicator + const typingId = showTypingIndicator(); - try { - const response = await fetch("api/chat.php", { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - message: message, - history: chatHistory.slice(0, -1) - }) - }); - - const data = await response.json(); - - if (response.ok && data.reply) { - removeTypingIndicator(typingIndicator); - const botReply = data.reply; - addMessage(botReply, "bot-msg"); - chatHistory.push({ role: "model", text: botReply }); - replySuccess = true; - } - } catch (error) { - console.warn("Backend API call failed, attempting client-side fallback. Details:", error); - } - - // CLIENT-SIDE FALLBACK: use Groq directly (OpenAI-compatible, 14,400 RPD free) - if (!replySuccess) { - const config = window.chatbotConfig; - if (config && config.groqApiKey && config.systemPrompt) { - try { - // Build OpenAI-compatible messages array - const messages = [ - { role: "system", content: config.systemPrompt } - ]; - - // Append history (excluding the latest user message which we add separately) - chatHistory.slice(0, -1).forEach(msg => { - messages.push({ - role: msg.role === "user" ? "user" : "assistant", - content: msg.text - }); - }); - - // Add current user message - messages.push({ role: "user", content: message }); - - const requestPayload = { - model: "llama-3.3-70b-versatile", // 14,400 RPD free - messages: messages, - temperature: 0.7, - max_tokens: 800, - stream: false - }; - - const response = await fetch("https://api.groq.com/openai/v1/chat/completions", { - method: "POST", - headers: { - "Content-Type": "application/json", - "Authorization": `Bearer ${config.groqApiKey}` - }, - body: JSON.stringify(requestPayload) - }); - - const data = await response.json(); - removeTypingIndicator(typingIndicator); - - if (response.ok && data.choices && data.choices[0].message.content) { - const botReply = data.choices[0].message.content; - addMessage(botReply, "bot-msg"); - chatHistory.push({ role: "model", text: botReply }); - replySuccess = true; - } else { - const errMsg = data.error?.message || "Sorry, I encountered an error. Please try again."; - addMessage(errMsg, "bot-msg error-msg"); - } - } catch (fallbackError) { - removeTypingIndicator(typingIndicator); - addMessage("Unable to connect to assistant. Please check your internet connection.", "bot-msg error-msg"); - console.error("Groq Fallback Error:", fallbackError); - } - } else { - removeTypingIndicator(typingIndicator); - addMessage("Chatbot is temporarily unavailable. Please try again later.", "bot-msg error-msg"); - } - } - - isWaiting = false; + // Simulate network delay for natural feel (600ms - 1200ms) + const delay = Math.floor(Math.random() * 600) + 600; + + setTimeout(() => { + // Remove typing indicator + removeTypingIndicator(typingId); + + // Get response from our JS engine instead of API + const responseText = engine.match(message); + + // Display formatted markdown response + addMessage(responseText, "bot-msg", true); + isWaiting = false; + + }, delay); }); - // Helper: Add message bubble to logs - function addMessage(text, className) { - const messageDiv = document.createElement("div"); - messageDiv.className = `chat-message ${className}`; + // ------------------------------------------------------------- + // UI Helpers + // ------------------------------------------------------------- + function addMessage(text, typeClass, isMarkdown = false) { + const msgDiv = document.createElement("div"); + msgDiv.className = `chat-message ${typeClass}`; - const bubbleDiv = document.createElement("div"); - bubbleDiv.className = "msg-bubble"; - - bubbleDiv.innerHTML = formatMessageText(text); - - messageDiv.appendChild(bubbleDiv); - messagesContainer.appendChild(messageDiv); + const bubble = document.createElement("div"); + bubble.className = "msg-bubble"; + + if (isMarkdown && typeClass !== "user-msg") { + bubble.innerHTML = parseSimpleMarkdown(text); + } else { + bubble.textContent = text; + } + + msgDiv.appendChild(bubble); + messagesContainer.appendChild(msgDiv); scrollToBottom(); } - // Helper: Show Typing Indicator function showTypingIndicator() { - const indicatorDiv = document.createElement("div"); - indicatorDiv.className = "chat-message bot-msg typing-indicator-container"; - indicatorDiv.innerHTML = ` -
- - - -
+ const msgDiv = document.createElement("div"); + msgDiv.className = "chat-message bot-msg typing-msg"; + const id = "typing-" + Date.now(); + msgDiv.id = id; + + const bubble = document.createElement("div"); + bubble.className = "msg-bubble typing-bubble"; + + bubble.innerHTML = ` + + + `; - messagesContainer.appendChild(indicatorDiv); + + msgDiv.appendChild(bubble); + messagesContainer.appendChild(msgDiv); scrollToBottom(); - return indicatorDiv; + return id; } - // Helper: Remove Typing Indicator - function removeTypingIndicator(indicatorElement) { - if (indicatorElement && indicatorElement.parentNode) { - indicatorElement.parentNode.removeChild(indicatorElement); - } + function removeTypingIndicator(id) { + const el = document.getElementById(id); + if (el) el.remove(); } - // Helper: Scroll to bottom function scrollToBottom() { - messagesContainer.scrollTop = messagesContainer.scrollHeight; + messagesContainer.scrollTo({ + top: messagesContainer.scrollHeight, + behavior: "smooth" + }); } - // Helper: Simple Markdown Formatter - function formatMessageText(text) { - let escaped = text - .replace(/&/g, "&") - .replace(//g, ">"); - - escaped = escaped.replace(/\*\*(.*?)\*\*/g, "$1"); - - const lines = escaped.split("\n"); - let inList = false; - let formatted = ""; - - lines.forEach(line => { - const trimmed = line.trim(); - if (trimmed.startsWith("- ") || trimmed.startsWith("* ")) { - if (!inList) { - formatted += ""; + if (inList) result.push(''); + text = result.join('\n'); + } else { + // Normal newlines + text = text.replace(/\n/g, '
'); } - - return formatted.replace(/(
\s*){3,}/g, "

"); + + return text; } }); diff --git a/assets/js/chatbot-engine.js b/assets/js/chatbot-engine.js new file mode 100644 index 0000000..2ead02c --- /dev/null +++ b/assets/js/chatbot-engine.js @@ -0,0 +1,134 @@ +class PortfolioChatEngine { + constructor(data) { + this.data = data || { settings: null, projects: [], certs: [], articles: [] }; + } + + match(message) { + const text = message.toLowerCase(); + + const intents = [ + { + name: 'greeting', + keywords: ['hi', 'hello', 'hey', 'what\'s up', 'howdy', 'greetings'], + response: () => "Hi there! I'm Cris's virtual assistant. I can tell you about his **skills**, **projects**, **certifications**, or how to **contact** him. What would you like to know?" + }, + { + name: 'about', + keywords: ['who are you', 'about', 'introduce', 'background', 'cris charles', 'who is cris'], + response: () => { + if (this.data.settings) { + return `${this.data.settings.about_text_1}\n\n${this.data.settings.about_text_2}`; + } + return "Cris is a senior IT student majoring in Software Development based in Laguna, Philippines. He's passionate about building scalable apps and beautiful UIs."; + } + }, + { + name: 'skills', + keywords: ['skills', 'technologies', 'stack', 'languages', 'php', 'react', 'javascript', 'css', 'html', 'python', 'java'], + response: () => { + return "Cris works with a variety of modern technologies. His core stack includes **PHP, JavaScript, Python, Java, C++** and frameworks like **React, NextJS, Node.js, and Express**. He's also experienced with **MySQL, MongoDB, Tailwind CSS, and Git**."; + } + }, + { + name: 'projects', + keywords: ['projects', 'built', 'work', 'portfolio', 'apps', 'websites', 'what have you done'], + response: () => { + if (this.data.projects && this.data.projects.length > 0) { + let res = `Cris has built ${this.data.projects.length} main projects. Here are some of them:\n\n`; + this.data.projects.slice(0, 3).forEach(p => { + res += `- **${p.title}**: ${p.des} (Built with ${p.techstack})\n`; + }); + res += `\nYou can check out the full list in the **Projects** section!`; + return res; + } + return "Cris has built several full-stack web applications, including e-commerce platforms, chat apps, and student management systems. You can view them all in the Projects section above!"; + } + }, + { + name: 'certifications', + keywords: ['certifications', 'certificates', 'courses', 'credentials', 'cert'], + response: () => { + if (this.data.certs && this.data.certs.length > 0) { + let res = `Cris holds ${this.data.certs.length} certifications. A few recent ones:\n\n`; + this.data.certs.slice(0, 3).forEach(c => { + res += `- **${c.title}** by ${c.issuer}\n`; + }); + return res; + } + return "Cris has completed multiple certifications from Cisco, freeCodeCamp, and other reputable institutions to continuously improve his software development skills."; + } + }, + { + name: 'blog', + keywords: ['blog', 'articles', 'writing', 'posts', 'read'], + response: () => { + if (this.data.articles && this.data.articles.length > 0) { + let res = `Cris occasionally writes about tech. Here are his latest articles:\n\n`; + this.data.articles.slice(0, 2).forEach(a => { + res += `- **[${a.title}](${a.url})**: ${a.excerpt}\n`; + }); + return res; + } + return "Cris shares his thoughts on software development and technology in his Blog section. Feel free to check it out on the site!"; + } + }, + { + name: 'experience', + keywords: ['experience', 'years', 'coding', 'how long'], + response: () => { + let years = this.data.settings ? this.data.settings.years_coding : "several"; + return `Cris has been coding for **${years} years** and has completed multiple projects ranging from front-end designs to complex full-stack applications.`; + } + }, + { + name: 'social', + keywords: ['github', 'linkedin', 'instagram', 'tiktok', 'social', 'facebook'], + response: () => { + if (this.data.settings) { + return `You can connect with Cris on his social platforms:\n- [GitHub](${this.data.settings.github_url})\n- [LinkedIn](${this.data.settings.linkedin_url})\n- [Instagram](${this.data.settings.instagram_url})\n- [TikTok](${this.data.settings.tiktok_url})`; + } + return "You can find links to Cris's GitHub, LinkedIn, and other social media profiles at the bottom of this page in the contact section."; + } + }, + { + name: 'contact', + keywords: ['contact', 'hire', 'email', 'reach', 'work together', 'message'], + response: () => { + return "If you'd like to reach out, you can scroll down to the bottom of the page and use the **Contact Form** to send Cris a direct message. He usually replies within 24 hours!"; + } + } + ]; + + let bestMatch = null; + let maxScore = 0; + + for (const intent of intents) { + let score = 0; + for (const keyword of intent.keywords) { + if (text.includes(keyword)) { + // Exact word matches get higher score than partial + const regex = new RegExp(`\\b${keyword}\\b`, 'i'); + if (regex.test(text)) { + score += 2; + } else { + score += 1; + } + } + } + if (score > maxScore) { + maxScore = score; + bestMatch = intent; + } + } + + if (bestMatch && maxScore > 0) { + return bestMatch.response(); + } + + // Fallback + return "I'm not quite sure how to answer that! I mostly know about Cris's **projects, skills, and background**. If you have a specific question for Cris, I recommend using the **Contact Form** at the bottom of the page to send him an email directly."; + } +} + +// Make globally available +window.PortfolioChatEngine = PortfolioChatEngine; diff --git a/views/chatbot.php b/views/chatbot.php index c353bd7..36502eb 100644 --- a/views/chatbot.php +++ b/views/chatbot.php @@ -37,57 +37,15 @@ if (isset($mysqli) && !$mysqli->connect_error) { } } } - -// Build system instructions -$systemPrompt = "You are a friendly, professional, and intelligent AI assistant built into the personal portfolio website of Cris Charles Garcia.\n"; -$systemPrompt .= "Your role is to represent Cris, assist recruiters, potential clients, and visitors, and answer questions about his background, experience, skills, and projects.\n\n"; - -$systemPrompt .= "Cris's Profile & Biography:\n"; -if ($settings) { - $systemPrompt .= "- Bio Paragraph 1: " . $settings['about_text_1'] . "\n"; - $systemPrompt .= "- Bio Paragraph 2: " . $settings['about_text_2'] . "\n"; - $systemPrompt .= "- Years coding: " . $settings['years_coding'] . "\n"; - $systemPrompt .= "- Projects completed: " . $settings['projects_count'] . "\n"; - $systemPrompt .= "- Certifications: " . $settings['certifications_count'] . "\n"; - $systemPrompt .= "- Github: " . $settings['github_url'] . "\n"; - $systemPrompt .= "- LinkedIn: " . $settings['linkedin_url'] . "\n"; - $systemPrompt .= "- Instagram: " . $settings['instagram_url'] . "\n"; - $systemPrompt .= "- Facebook: " . $settings['facebook_url'] . "\n"; - $systemPrompt .= "- TikTok: " . $settings['tiktok_url'] . "\n"; -} else { - $systemPrompt .= "- Cris is a senior IT student majoring in Software Development based in Laguna, Philippines.\n"; -} - -$systemPrompt .= "\nCris's Projects:\n"; -foreach ($projects as $p) { - $systemPrompt .= "- **" . $p['title'] . "**: " . $p['des'] . " (Tech: " . $p['techstack'] . "). Link: " . $p['url'] . "\n"; -} - -$systemPrompt .= "\nCris's Certifications:\n"; -foreach ($certs as $c) { - $systemPrompt .= "- " . $c['title'] . " by " . $c['issuer'] . " (Issued: " . $c['date_issued'] . ")\n"; -} - -$systemPrompt .= "\nCris's Latest Articles/Blog Posts:\n"; -foreach ($articles as $a) { - $systemPrompt .= "- \"" . $a['title'] . "\": " . $a['excerpt'] . " [Tag: " . $a['tag'] . ", Date: " . $a['date_published'] . ", Link: " . $a['url'] . "]\n"; -} - -$systemPrompt .= "\nGuidelines for your responses:\n"; -$systemPrompt .= "1. Respond in first-person (e.g., \"I am a senior IT student...\" or \"In my project UniMarket...\") or as a dedicated assistant of Cris. Represent Cris accurately, warmly, and professionally.\n"; -$systemPrompt .= "2. Keep responses brief, engaging, and directly related to the question. Recruiters appreciate concise answers.\n"; -$systemPrompt .= "3. If asked about contact options or hiring, point them to his contact form at the bottom of the page or his professional links (Github, LinkedIn).\n"; -$systemPrompt .= "4. Do not make up facts or external projects not listed in the context. If you don't know the answer, politely invite the user to contact Cris directly.\n"; -$systemPrompt .= "5. Format your output using simple markdown (like bolding or bullet points) to make it readable.\n"; - -$apiKey = $_ENV['GROQ_API_KEY'] ?? getenv('GROQ_API_KEY') ?? ''; ?> - + @@ -145,4 +103,5 @@ window.chatbotConfig = { +