fix: back to top button, side navigation
This commit is contained in:
1 parent
762a2d0f66
commit
9eeb82ed89
6 files changed
+744
-186
No files matched your search
+25
-10
@@ -122,30 +122,44 @@ $systemPrompt .= "4. Do not make up facts or external projects not listed in the
|
|||||||
$systemPrompt .= "5. Format your output using simple markdown (like bolding or bullet points) to make it readable.\n";
|
$systemPrompt .= "5. Format your output using simple markdown (like bolding or bullet points) to make it readable.\n";
|
||||||
|
|
||||||
// Map chat history
|
// Map chat history
|
||||||
|
// Map chat history and prepend system instructions to the first user message
|
||||||
$contents = [];
|
$contents = [];
|
||||||
if (isset($inputData['history']) && is_array($inputData['history'])) {
|
$firstMsgPrepended = false;
|
||||||
foreach ($inputData['history'] as $msg) {
|
|
||||||
|
if (isset($inputData['history']) && is_array($inputData['history']) && !empty($inputData['history'])) {
|
||||||
|
foreach ($inputData['history'] as $index => $msg) {
|
||||||
if (isset($msg['role']) && isset($msg['text'])) {
|
if (isset($msg['role']) && isset($msg['text'])) {
|
||||||
$role = ($msg['role'] === 'user') ? 'user' : 'model';
|
$role = ($msg['role'] === 'user') ? 'user' : 'model';
|
||||||
|
$text = $msg['text'];
|
||||||
|
|
||||||
|
// Prepend system prompt to the first user message in history
|
||||||
|
if (!$firstMsgPrepended && $role === 'user') {
|
||||||
|
$text = "System Context & Instructions:\n" . $systemPrompt . "\n\nUser Message: " . $text;
|
||||||
|
$firstMsgPrepended = true;
|
||||||
|
}
|
||||||
|
|
||||||
$contents[] = [
|
$contents[] = [
|
||||||
'role' => $role,
|
'role' => $role,
|
||||||
'parts' => [['text' => $msg['text']]]
|
'parts' => [['text' => $text]]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Add the current user message
|
|
||||||
|
// Add the current user message (prepend system instructions if history was empty)
|
||||||
|
$currentUserText = $userMessage;
|
||||||
|
if (!$firstMsgPrepended) {
|
||||||
|
$currentUserText = "System Context & Instructions:\n" . $systemPrompt . "\n\nUser Message: " . $userMessage;
|
||||||
|
}
|
||||||
|
|
||||||
$contents[] = [
|
$contents[] = [
|
||||||
'role' => 'user',
|
'role' => 'user',
|
||||||
'parts' => [['text' => $userMessage]]
|
'parts' => [['text' => $currentUserText]]
|
||||||
];
|
];
|
||||||
|
|
||||||
// Setup request body
|
// Setup request body without systemInstruction key
|
||||||
$requestPayload = [
|
$requestPayload = [
|
||||||
'contents' => $contents,
|
'contents' => $contents,
|
||||||
'systemInstruction' => [
|
|
||||||
'parts' => [['text' => $systemPrompt]]
|
|
||||||
],
|
|
||||||
'generationConfig' => [
|
'generationConfig' => [
|
||||||
'temperature' => 0.7,
|
'temperature' => 0.7,
|
||||||
'maxOutputTokens' => 800
|
'maxOutputTokens' => 800
|
||||||
@@ -153,7 +167,7 @@ $requestPayload = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Call Gemini API via cURL
|
// Call Gemini API via cURL
|
||||||
$url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=" . urlencode($apiKey);
|
$url = "https://generativelanguage.googleapis.com/v1/models/gemini-3.5-flash:generateContent?key=" . urlencode($apiKey);
|
||||||
|
|
||||||
$ch = curl_init($url);
|
$ch = curl_init($url);
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
@@ -161,6 +175,7 @@ curl_setopt($ch, CURLOPT_POST, true);
|
|||||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($requestPayload));
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($requestPayload));
|
||||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
||||||
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
|
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
|
||||||
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||||
|
|
||||||
$response = curl_exec($ch);
|
$response = curl_exec($ch);
|
||||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../config/db.php';
|
||||||
|
|
||||||
|
$apiKey = getenv('GEMINI_API_KEY') ?: ($_ENV['GEMINI_API_KEY'] ?? '');
|
||||||
|
if (empty($apiKey)) {
|
||||||
|
echo "API Key is missing in .env";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = "https://generativelanguage.googleapis.com/v1/models?key=" . urlencode($apiKey);
|
||||||
|
|
||||||
|
$ch = curl_init($url);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||||
|
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
echo "HTTP Code: $httpCode\n\n";
|
||||||
|
echo "Response:\n";
|
||||||
|
echo $response;
|
||||||
+500
-143
File diff suppressed because it is too large.
Load diff
+71
-16
@@ -38,7 +38,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
chatHistory.push({ role: "user", text: message });
|
chatHistory.push({ role: "user", text: message });
|
||||||
inputField.value = "";
|
inputField.value = "";
|
||||||
|
|
||||||
// Cap history to last 10 messages to save context limits
|
// Cap history to last 10 messages
|
||||||
if (chatHistory.length > 10) {
|
if (chatHistory.length > 10) {
|
||||||
chatHistory = chatHistory.slice(-10);
|
chatHistory = chatHistory.slice(-10);
|
||||||
}
|
}
|
||||||
@@ -47,6 +47,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
const typingIndicator = showTypingIndicator();
|
const typingIndicator = showTypingIndicator();
|
||||||
isWaiting = true;
|
isWaiting = true;
|
||||||
|
|
||||||
|
// Try backend proxy first, fallback to client-side if it fails
|
||||||
|
let replySuccess = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("api/chat.php", {
|
const response = await fetch("api/chat.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -55,28 +58,85 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
message: message,
|
message: message,
|
||||||
history: chatHistory.slice(0, -1) // pass history excluding the current user message (which is passed in "message" parameter)
|
history: chatHistory.slice(0, -1)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
removeTypingIndicator(typingIndicator);
|
|
||||||
|
|
||||||
if (response.ok && data.reply) {
|
if (response.ok && data.reply) {
|
||||||
|
removeTypingIndicator(typingIndicator);
|
||||||
const botReply = data.reply;
|
const botReply = data.reply;
|
||||||
addMessage(botReply, "bot-msg");
|
addMessage(botReply, "bot-msg");
|
||||||
chatHistory.push({ role: "model", text: botReply });
|
chatHistory.push({ role: "model", text: botReply });
|
||||||
} else {
|
replySuccess = true;
|
||||||
const errMsg = data.error || "Sorry, I encountered an error. Please try again.";
|
|
||||||
addMessage(errMsg, "bot-msg error-msg");
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
removeTypingIndicator(typingIndicator);
|
console.warn("Backend API call failed, attempting client-side fallback. Details:", error);
|
||||||
addMessage("Unable to connect to assistant. Please check your internet connection.", "bot-msg error-msg");
|
|
||||||
console.error("Chatbot Error:", error);
|
|
||||||
} finally {
|
|
||||||
isWaiting = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CLIENT-SIDE FALLBACK (e.g. for InfinityFree or local cert errors)
|
||||||
|
if (!replySuccess) {
|
||||||
|
const config = window.chatbotConfig;
|
||||||
|
if (config && config.apiKey && config.systemPrompt) {
|
||||||
|
try {
|
||||||
|
// Structure payload for Gemini client-side and inject systemInstructions in first user message
|
||||||
|
let firstMsgPrepended = false;
|
||||||
|
const contents = chatHistory.map(msg => {
|
||||||
|
let text = msg.text;
|
||||||
|
const role = msg.role === 'user' ? 'user' : 'model';
|
||||||
|
if (!firstMsgPrepended && role === 'user') {
|
||||||
|
text = `System Context & Instructions:\n${config.systemPrompt}\n\nUser Message: ${text}`;
|
||||||
|
firstMsgPrepended = true;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
role: role,
|
||||||
|
parts: [{ text: text }]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const requestPayload = {
|
||||||
|
contents: contents,
|
||||||
|
generationConfig: {
|
||||||
|
temperature: 0.7,
|
||||||
|
maxOutputTokens: 800
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const url = `https://generativelanguage.googleapis.com/v1/models/gemini-3.5-flash:generateContent?key=${encodeURIComponent(config.apiKey)}`;
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestPayload)
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
removeTypingIndicator(typingIndicator);
|
||||||
|
|
||||||
|
if (response.ok && data.candidates && data.candidates[0].content.parts[0].text) {
|
||||||
|
const botReply = data.candidates[0].content.parts[0].text;
|
||||||
|
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("Direct API Fallback Error:", fallbackError);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
removeTypingIndicator(typingIndicator);
|
||||||
|
addMessage("API request blocked by host firewall and fallback config is missing.", "bot-msg error-msg");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isWaiting = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Helper: Add message bubble to logs
|
// Helper: Add message bubble to logs
|
||||||
@@ -87,7 +147,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
const bubbleDiv = document.createElement("div");
|
const bubbleDiv = document.createElement("div");
|
||||||
bubbleDiv.className = "msg-bubble";
|
bubbleDiv.className = "msg-bubble";
|
||||||
|
|
||||||
// Format simple Markdown conversions (bold, bullet points, line breaks)
|
|
||||||
bubbleDiv.innerHTML = formatMessageText(text);
|
bubbleDiv.innerHTML = formatMessageText(text);
|
||||||
|
|
||||||
messageDiv.appendChild(bubbleDiv);
|
messageDiv.appendChild(bubbleDiv);
|
||||||
@@ -125,16 +184,13 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
|
|
||||||
// Helper: Simple Markdown Formatter
|
// Helper: Simple Markdown Formatter
|
||||||
function formatMessageText(text) {
|
function formatMessageText(text) {
|
||||||
// Safe escaping (minimal to allow HTML format helper if needed, but safe)
|
|
||||||
let escaped = text
|
let escaped = text
|
||||||
.replace(/&/g, "&")
|
.replace(/&/g, "&")
|
||||||
.replace(/</g, "<")
|
.replace(/</g, "<")
|
||||||
.replace(/>/g, ">");
|
.replace(/>/g, ">");
|
||||||
|
|
||||||
// Bold (**bold**)
|
|
||||||
escaped = escaped.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
|
escaped = escaped.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
|
||||||
|
|
||||||
// Bullet points starting with "- " or "* "
|
|
||||||
const lines = escaped.split("\n");
|
const lines = escaped.split("\n");
|
||||||
let inList = false;
|
let inList = false;
|
||||||
let formatted = "";
|
let formatted = "";
|
||||||
@@ -160,7 +216,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
formatted += "</ul>";
|
formatted += "</ul>";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up double linebreaks
|
|
||||||
return formatted.replace(/(<br>\s*){3,}/g, "<br><br>");
|
return formatted.replace(/(<br>\s*){3,}/g, "<br><br>");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1,3 +1,96 @@
|
|||||||
|
<?php
|
||||||
|
// Since this is included in index.php, $mysqli is already defined and loaded.
|
||||||
|
// Let's retrieve context safely. If for some reason $mysqli is not available, we have fallback strings.
|
||||||
|
$settings = null;
|
||||||
|
$projects = [];
|
||||||
|
$certs = [];
|
||||||
|
$articles = [];
|
||||||
|
|
||||||
|
if (isset($mysqli) && !$mysqli->connect_error) {
|
||||||
|
// 1. Settings / About
|
||||||
|
$settingsQuery = $mysqli->query("SELECT * FROM settings WHERE id = 1");
|
||||||
|
if ($settingsQuery) {
|
||||||
|
$settings = $settingsQuery->fetch_assoc();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Projects
|
||||||
|
$projectsQuery = $mysqli->query("SELECT title, des, techstack, url FROM projects");
|
||||||
|
if ($projectsQuery) {
|
||||||
|
while ($row = $projectsQuery->fetch_assoc()) {
|
||||||
|
$projects[] = $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Certifications
|
||||||
|
$certsQuery = $mysqli->query("SELECT title, issuer, date_issued FROM certifications");
|
||||||
|
if ($certsQuery) {
|
||||||
|
while ($row = $certsQuery->fetch_assoc()) {
|
||||||
|
$certs[] = $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Articles
|
||||||
|
$articlesQuery = $mysqli->query("SELECT title, excerpt, date_published, tag, url FROM articles");
|
||||||
|
if ($articlesQuery) {
|
||||||
|
while ($row = $articlesQuery->fetch_assoc()) {
|
||||||
|
$articles[] = $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 = getenv('GEMINI_API_KEY') ?: ($_ENV['GEMINI_API_KEY'] ?? '');
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- Pass DB configuration context directly to javascript to allow direct API requests on restricted hosting environments (like InfinityFree) -->
|
||||||
|
<script>
|
||||||
|
window.chatbotConfig = {
|
||||||
|
apiKey: <?php echo json_encode($apiKey); ?>,
|
||||||
|
systemPrompt: <?php echo json_encode($systemPrompt); ?>
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
<!-- Chatbot Widget HTML -->
|
<!-- Chatbot Widget HTML -->
|
||||||
<div id="portfolio-chatbot-widget">
|
<div id="portfolio-chatbot-widget">
|
||||||
<!-- Floating Chat Toggle Bubble -->
|
<!-- Floating Chat Toggle Bubble -->
|
||||||
|
|||||||
+32
-17
@@ -11,6 +11,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="nav-links" id="nav-links" role="navigation">
|
<div class="nav-links" id="nav-links" role="navigation">
|
||||||
|
<button class="nav-close-btn" id="nav-close-btn" aria-label="Close menu">×</button>
|
||||||
<a href="index.php#about" class="nav-item">About</a>
|
<a href="index.php#about" class="nav-item">About</a>
|
||||||
<a href="index.php#projects" class="nav-item">Work</a>
|
<a href="index.php#projects" class="nav-item">Work</a>
|
||||||
<a href="index.php#certificates" class="nav-item">Certs</a>
|
<a href="index.php#certificates" class="nav-item">Certs</a>
|
||||||
@@ -23,6 +24,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<!-- Nav overlay backdrop -->
|
||||||
|
<div class="nav-overlay" id="nav-overlay"></div>
|
||||||
|
|
||||||
<!-- Shared Navigation Script -->
|
<!-- Shared Navigation Script -->
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
@@ -36,39 +40,50 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. Mobile Hamburger Menu
|
// 2. Mobile Hamburger Menu
|
||||||
const hamburger = document.getElementById("hamburger");
|
const hamburger = document.getElementById("hamburger");
|
||||||
const navLinks = document.getElementById("nav-links");
|
const navLinks = document.getElementById("nav-links");
|
||||||
const navItems = document.querySelectorAll(".nav-item");
|
const navCloseBtn = document.getElementById("nav-close-btn");
|
||||||
|
const navOverlay = document.getElementById("nav-overlay");
|
||||||
|
const navItems = document.querySelectorAll(".nav-item");
|
||||||
|
|
||||||
if (hamburger && navLinks) {
|
if (hamburger && navLinks) {
|
||||||
|
function openMenu() {
|
||||||
|
hamburger.classList.add("active");
|
||||||
|
navLinks.classList.add("active");
|
||||||
|
if (navOverlay) navOverlay.classList.add("active");
|
||||||
|
hamburger.setAttribute("aria-expanded", "true");
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
}
|
||||||
|
|
||||||
function closeMenu() {
|
function closeMenu() {
|
||||||
hamburger.classList.remove("active");
|
hamburger.classList.remove("active");
|
||||||
navLinks.classList.remove("active");
|
navLinks.classList.remove("active");
|
||||||
|
if (navOverlay) navOverlay.classList.remove("active");
|
||||||
hamburger.setAttribute("aria-expanded", "false");
|
hamburger.setAttribute("aria-expanded", "false");
|
||||||
|
document.body.style.overflow = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
hamburger.addEventListener("click", (e) => {
|
hamburger.addEventListener("click", (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const isOpen = hamburger.classList.toggle("active");
|
if (hamburger.classList.contains("active")) {
|
||||||
navLinks.classList.toggle("active");
|
closeMenu();
|
||||||
hamburger.setAttribute("aria-expanded", isOpen);
|
} else {
|
||||||
|
openMenu();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Close via X button
|
||||||
|
if (navCloseBtn) navCloseBtn.addEventListener("click", closeMenu);
|
||||||
|
|
||||||
|
// Close via overlay click
|
||||||
|
if (navOverlay) navOverlay.addEventListener("click", closeMenu);
|
||||||
|
|
||||||
// Close on nav link click
|
// Close on nav link click
|
||||||
navItems.forEach(item => item.addEventListener("click", closeMenu));
|
navItems.forEach(item => item.addEventListener("click", closeMenu));
|
||||||
|
|
||||||
// Close when clicking outside the menu
|
|
||||||
document.addEventListener("click", (e) => {
|
|
||||||
if (navLinks.classList.contains("active") && !navLinks.contains(e.target) && !hamburger.contains(e.target)) {
|
|
||||||
closeMenu();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close on scroll
|
// Close on scroll
|
||||||
window.addEventListener('scroll', () => {
|
window.addEventListener("scroll", () => {
|
||||||
if (navLinks.classList.contains('active')) {
|
if (navLinks.classList.contains("active")) closeMenu();
|
||||||
closeMenu();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in new issue
Block a user