This commit is contained in:
parent
ebc307fe04
commit
aba6eaa57f
120 changed files with 598 additions and 1052 deletions
60
src/js/about.js
Normal file
60
src/js/about.js
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// --- Interactive Values Section Logic ---
|
||||
const valuesData = [
|
||||
{ title: "Beauty", description: "We treat all stake holders with politeness, dignity and civility that equals the outward beauty of our environment and the environments we create." },
|
||||
{ title: "Innovation", description: "We challenge the status quo. We seek better, smarter and more beautiful ways to solve problems and deliver value. When ideas fall short, we learn, adapt and try again- never settling, always solving." },
|
||||
{ title: "Loyalty", description: "We are faithful to our mission and vision and that of our stakeholders and the organizations they represent." },
|
||||
{ title: "Diligence", description: "We give our utmost attention to all our projects. We do this in earnest and with precision." },
|
||||
{ title: "Excellence", description: "We are committed to delivering exceptional results in every area of our operations." },
|
||||
{ title: "Resilience", description: "In the face of daunting challenges, setbacks and failures; we learn and document lessons, bounce back and persist in the pursuit of goals, targets, objectives and deadlines. We do this with focused optimism." },
|
||||
{ title: "Studiousness", description: "We are committed to learning and advancing our services, products, and tools to best serve our stakeholders." }
|
||||
];
|
||||
|
||||
const valuesListContainer = document.getElementById('values-list');
|
||||
const valuesContentContainer = document.getElementById('values-content');
|
||||
const valuesSection = document.getElementById('values-section');
|
||||
|
||||
if (valuesListContainer && valuesContentContainer && valuesSection) {
|
||||
|
||||
// Populate the list and content blocks
|
||||
valuesListContainer.innerHTML = valuesData.map((value, index) => `
|
||||
<li class="value-item ${index === 0 ? 'active' : ''}" data-index="${index}">
|
||||
${value.title}
|
||||
</li>
|
||||
`).join('');
|
||||
|
||||
valuesContentContainer.innerHTML = valuesData.map((value, index) => `
|
||||
<div class="value-content-block ${index === 0 ? 'active' : ''}" data-index="${index}">
|
||||
<!-- Background Number -->
|
||||
<div class="absolute -top-4 -left-4 text-8xl md:text-9xl font-extrabold text-neutral-100 -z-10">0${index + 1}</div>
|
||||
<!-- Title for mobile view -->
|
||||
<h3 class="text-3xl font-bold text-kh3-red mb-4 md:hidden">${value.title}</h3>
|
||||
<!-- Description -->
|
||||
<p class="text-lg md:text-xl text-neutral-600 leading-relaxed">${value.description}</p>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
const valueItems = valuesListContainer.querySelectorAll('.value-item');
|
||||
const contentBlocks = valuesContentContainer.querySelectorAll('.value-content-block');
|
||||
|
||||
// Function to update the active state
|
||||
const updateActiveValue = (index) => {
|
||||
valueItems.forEach(item => item.classList.remove('active'));
|
||||
contentBlocks.forEach(block => block.classList.remove('active'));
|
||||
|
||||
valuesListContainer.querySelector(`.value-item[data-index='${index}']`).classList.add('active');
|
||||
valuesContentContainer.querySelector(`.value-content-block[data-index='${index}']`).classList.add('active');
|
||||
};
|
||||
|
||||
// Add hover event listeners for desktop
|
||||
valueItems.forEach((item, index) => {
|
||||
item.addEventListener('mouseenter', () => {
|
||||
// Only trigger on hover if not a touch device (basic check)
|
||||
if (window.matchMedia('(hover: hover)').matches) {
|
||||
updateActiveValue(index);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
84
src/js/carousel.js
Normal file
84
src/js/carousel.js
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Generic function to handle the carousel logic
|
||||
function initializeCarousel(currentImgEl, nextImgEl, imageArray) {
|
||||
if (!currentImgEl || !nextImgEl) {
|
||||
return; // If the elements aren't on this page, do nothing
|
||||
}
|
||||
|
||||
currentImgEl.classList.add('opacity-0');
|
||||
currentImgEl.style.animation = "fadeInRight 1s ease-out 0.3s forwards";
|
||||
|
||||
let currentIndex = 0;
|
||||
let isTransitioning = false;
|
||||
const slideDirections = ["left", "right"];
|
||||
let slideIndex = 0;
|
||||
|
||||
function performSlideZoomTransition() {
|
||||
if (isTransitioning) return;
|
||||
isTransitioning = true;
|
||||
const nextIndex = (currentIndex + 1) % imageArray.length;
|
||||
const slideDirection = slideDirections[slideIndex % slideDirections.length];
|
||||
slideIndex++;
|
||||
|
||||
nextImgEl.src = imageArray[nextIndex];
|
||||
nextImgEl.style.opacity = "1";
|
||||
|
||||
if (slideDirection === "left") {
|
||||
currentImgEl.style.animation = "slideOutLeft 1s ease-in-out forwards";
|
||||
nextImgEl.style.animation = "zoomInFromRight 1s ease-in-out forwards";
|
||||
} else {
|
||||
currentImgEl.style.animation = "slideOutRight 1s ease-in-out forwards";
|
||||
nextImgEl.style.animation = "zoomInFromLeft 1s ease-in-out forwards";
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
// The animation has finished. The 'nextImg' is now fully visible on top.
|
||||
// The 'currentImg' is now hidden behind it, having completed its slide-out.
|
||||
|
||||
// 1. Reset the OLD current image. It will become the 'next' image container for the next cycle.
|
||||
// Clear its animation and explicitly hide it.
|
||||
currentImgEl.style.animation = "";
|
||||
currentImgEl.style.opacity = "0"; // Force it to be hidden
|
||||
|
||||
// 2. The 'next' image now becomes the 'current' image.
|
||||
// We need to update its properties to be the new baseline.
|
||||
nextImgEl.style.animation = ""; // Clear its 'zoomIn' animation
|
||||
nextImgEl.style.opacity = "1"; // Ensure it is and stays visible
|
||||
|
||||
// 3. Swap their IDs in the DOM. This is the key to the new logic.
|
||||
// The element that was 'nextImg' is now officially 'currentImg'.
|
||||
let tempId = currentImgEl.id;
|
||||
currentImgEl.id = nextImgEl.id;
|
||||
nextImgEl.id = tempId;
|
||||
|
||||
// 4. Re-assign our script's variables to point to the newly ID'd elements.
|
||||
currentImgEl = document.getElementById('currentImg');
|
||||
nextImgEl = document.getElementById('nextImg');
|
||||
|
||||
// 5. Update the index and state.
|
||||
currentIndex = nextIndex;
|
||||
isTransitioning = false;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Start the transition cycle after initial page animations
|
||||
setTimeout(() => {
|
||||
setInterval(performSlideZoomTransition, 5000);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// --- Carousel Definitions ---
|
||||
// IDs for the index page carousel
|
||||
const indexCurrentImg = document.getElementById("currentImg");
|
||||
const indexNextImg = document.getElementById("nextImg");
|
||||
const indexImgs = ["assets/images/room.webp", "assets/images/chair.webp", "assets/images/google.webp"];
|
||||
|
||||
// IDs for the services page carousel
|
||||
const servicesCurrentImg = document.getElementById("servicesCurrentImg");
|
||||
const servicesNextImg = document.getElementById("servicesNextImg");
|
||||
const servicesImgs = ["assets/images/room.webp", "assets/images/chair.webp", "assets/images/google.webp"];
|
||||
|
||||
// Initialize the carousels for any page that has the correct elements
|
||||
initializeCarousel(indexCurrentImg, indexNextImg, indexImgs);
|
||||
initializeCarousel(servicesCurrentImg, servicesNextImg, servicesImgs);
|
||||
});
|
||||
122
src/js/contact.js
Normal file
122
src/js/contact.js
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
// Contact page specific scripts
|
||||
|
||||
// Form submission
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const contactForm = document.getElementById("contactForm");
|
||||
if (contactForm) {
|
||||
contactForm.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const submitButton = contactForm.querySelector('button[type="submit"]');
|
||||
const originalText = submitButton.innerHTML;
|
||||
|
||||
// Show loading state
|
||||
submitButton.innerHTML = "<span>Sending...</span>";
|
||||
submitButton.disabled = true;
|
||||
|
||||
try {
|
||||
const formData = new FormData(contactForm);
|
||||
const name = formData.get("name");
|
||||
const email = formData.get("email");
|
||||
const phone = formData.get("phone");
|
||||
const company = formData.get("company");
|
||||
const service = formData.get("service");
|
||||
const budget = formData.get("budget");
|
||||
const message = formData.get("message");
|
||||
|
||||
// Send to Resend API
|
||||
const response = await fetch("https://api.resend.com/emails", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer re_YOUR_API_KEY_HERE", // Replace with your Resend API key
|
||||
},
|
||||
body: JSON.stringify({
|
||||
from: "KH3 Website <noreply@yourdomain.com>", // Replace with your verified domain
|
||||
to: ["george.fleef@gmail.com"],
|
||||
subject: `New contact form submission from ${name}`,
|
||||
html: `
|
||||
<h2>New Contact Form Submission</h2>
|
||||
<p><strong>Name:</strong> ${name}</p>
|
||||
<p><strong>Email:</strong> ${email}</p>
|
||||
<p><strong>Phone:</strong> ${phone || "Not provided"}</p>
|
||||
<p><strong>Company:</strong> ${company || "Not provided"}</p>
|
||||
<p><strong>Service Interest:</strong> ${
|
||||
service || "Not specified"
|
||||
}</p>
|
||||
<p><strong>Budget:</strong> ${budget || "Not specified"}</p>
|
||||
<p><strong>Message:</strong></p>
|
||||
<p>${message.replace(/\n/g, "<br>")}</p>
|
||||
`,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert(
|
||||
"Thank you for your message! We'll get back to you within 24 hours."
|
||||
);
|
||||
contactForm.reset();
|
||||
} else {
|
||||
throw new Error("Failed to send email");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
alert(
|
||||
"Sorry, there was an error sending your message. Please try again or contact us directly at george.fleef@gmail.com"
|
||||
);
|
||||
} finally {
|
||||
// Reset button
|
||||
submitButton.innerHTML = originalText;
|
||||
submitButton.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Staggered entrance for hero and sections
|
||||
window.addEventListener("load", () => {
|
||||
const heroTitle = document.querySelector(
|
||||
"section.relative h1.masked-reveal"
|
||||
);
|
||||
const heroSub = document.querySelector("section.relative div.text-xl");
|
||||
if (heroTitle) {
|
||||
heroTitle.style.animation = "maskReveal 900ms ease-out 150ms forwards";
|
||||
}
|
||||
if (heroSub) {
|
||||
heroSub.style.opacity = "0";
|
||||
heroSub.style.animation = "fadeUp 700ms ease-out 320ms forwards";
|
||||
}
|
||||
|
||||
const contactSectionFade = document.querySelector("section.py-20.bg-white");
|
||||
if (contactSectionFade) {
|
||||
contactSectionFade.style.opacity = "0";
|
||||
contactSectionFade.style.animation =
|
||||
"fadeUp 800ms ease-out 600ms forwards";
|
||||
}
|
||||
|
||||
const mapSection = document.querySelector("section.py-20.bg-kh3-black");
|
||||
if (mapSection) {
|
||||
mapSection.style.opacity = "0";
|
||||
mapSection.style.animation = "fadeUp 800ms ease-out 850ms forwards";
|
||||
}
|
||||
});
|
||||
|
||||
// Gentle auto-scroll like WHAT page: nudge ~1.1vh after 3s with easing
|
||||
setTimeout(() => {
|
||||
const startScroll = window.pageYOffset;
|
||||
const targetScroll = window.innerHeight * 1.0;
|
||||
const distance = targetScroll - startScroll;
|
||||
const duration = 2000;
|
||||
let start = null;
|
||||
|
||||
function animateScroll(ts) {
|
||||
if (start === null) start = ts;
|
||||
const elapsed = ts - start;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
const easeOutQuart = 1 - Math.pow(1 - progress, 4);
|
||||
window.scrollTo(0, startScroll + distance * easeOutQuart);
|
||||
if (progress < 1) requestAnimationFrame(animateScroll);
|
||||
}
|
||||
|
||||
requestAnimationFrame(animateScroll);
|
||||
}, 3000);
|
||||
});
|
||||
17
src/js/include.js
Normal file
17
src/js/include.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
async function loadIncludes() {
|
||||
const includes = document.querySelectorAll("[data-include]");
|
||||
for (const el of includes) {
|
||||
const file = el.getAttribute("data-include");
|
||||
if (!file) continue;
|
||||
try {
|
||||
const res = await fetch(file);
|
||||
if (res.ok) {
|
||||
el.innerHTML = await res.text();
|
||||
} else {
|
||||
console.error("Include failed:", file, res.status);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Include failed:", file, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
196
src/js/main.js
Normal file
196
src/js/main.js
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
// Main JavaScript for KH3 Website
|
||||
|
||||
function safeInit(selector, callback) {
|
||||
const el = document.querySelector(selector);
|
||||
if (el) callback(el);
|
||||
}
|
||||
|
||||
function updateYear() {
|
||||
safeInit('#current-year', (el) => {
|
||||
el.textContent = new Date().getFullYear();
|
||||
});
|
||||
}
|
||||
|
||||
// This function will handle all animations defined by data-attributes in the HTML
|
||||
function initDynamicAnimations() {
|
||||
const animatedElements = document.querySelectorAll("[data-anim]");
|
||||
animatedElements.forEach((el) => {
|
||||
const delay = el.getAttribute("data-anim-delay") || "0";
|
||||
el.style.animationDelay = `${delay}ms`;
|
||||
// CORRECTED: Adds the required 'animate-' prefix for Tailwind to recognize the animation
|
||||
el.classList.add(`animate-${el.getAttribute('data-anim')}`);
|
||||
el.style.animationFillMode = 'forwards'; // Ensure animation state persists after it runs
|
||||
});
|
||||
}
|
||||
|
||||
// Navigation functionality
|
||||
function initNavigation() {
|
||||
const menuToggle = document.getElementById("menuToggle");
|
||||
const navMenu = document.getElementById("navMenu");
|
||||
const menuGrid = document.getElementById("menuGrid");
|
||||
const navLinks = document.querySelectorAll(".nav-link");
|
||||
|
||||
if (menuToggle && navMenu && menuGrid) {
|
||||
if (navMenu.dataset.navInitialized === "true") return;
|
||||
navMenu.dataset.navInitialized = "true";
|
||||
|
||||
let isMenuOpen = false;
|
||||
const menuPetal = document.getElementById("menuPetal");
|
||||
const menuWrap = document.getElementById("menuWrap");
|
||||
|
||||
if (menuWrap) menuWrap.classList.remove("open");
|
||||
|
||||
menuToggle.addEventListener("click", () => {
|
||||
isMenuOpen = !isMenuOpen; // Toggle the state
|
||||
navMenu.classList.toggle("hidden");
|
||||
if (menuWrap) menuWrap.classList.toggle("open");
|
||||
});
|
||||
|
||||
if (document.body.dataset.navGlobalInitialized !== "true") {
|
||||
document.body.dataset.navGlobalInitialized = "true";
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
if (isMenuOpen && !menuToggle.contains(e.target) && !navMenu.contains(e.target)) {
|
||||
navMenu.classList.add("hidden");
|
||||
if (menuWrap) menuWrap.classList.remove("open");
|
||||
isMenuOpen = false;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape" && isMenuOpen) {
|
||||
navMenu.classList.add("hidden");
|
||||
if (menuWrap) menuWrap.classList.remove("open");
|
||||
isMenuOpen = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
navLinks.forEach((link) => {
|
||||
link.addEventListener("click", () => {
|
||||
navMenu.classList.add("hidden");
|
||||
if (menuWrap) menuWrap.classList.remove("open");
|
||||
isMenuOpen = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Custom cursor
|
||||
function initCustomCursor() {
|
||||
if (window.innerWidth <= 768) return;
|
||||
const cursor = document.getElementById("customCursor");
|
||||
if (!cursor) return;
|
||||
|
||||
document.addEventListener("mousemove", (e) => {
|
||||
cursor.style.left = e.clientX - 12 + "px";
|
||||
cursor.style.top = e.clientY - 12 + "px";
|
||||
});
|
||||
}
|
||||
|
||||
// Loading screen
|
||||
function initLoadingScreen() {
|
||||
const loadingScreen = document.getElementById("loadingScreen");
|
||||
if (loadingScreen) {
|
||||
setTimeout(() => {
|
||||
loadingScreen.style.opacity = "0";
|
||||
setTimeout(() => {
|
||||
loadingScreen.style.display = "none";
|
||||
}, 500);
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- Main Execution ---
|
||||
document.addEventListener("DOMContentLoaded", async function () {
|
||||
initDynamicAnimations();
|
||||
initNavigation();
|
||||
initActiveNav();
|
||||
initCustomCursor();
|
||||
initLoadingScreen();
|
||||
loadTeamMembers(); //
|
||||
// Add other initializations here if needed
|
||||
|
||||
updateYear();
|
||||
if (typeof loadIncludes === 'function') {
|
||||
await loadIncludes();
|
||||
|
||||
// SECOND PASS (handles injected markup)
|
||||
initNavigation();
|
||||
initActiveNav();
|
||||
// Re-initialize animations for dynamically included content like the footer
|
||||
initDynamicAnimations();
|
||||
}
|
||||
updateYear();
|
||||
});
|
||||
|
||||
// Handle back/forward navigation cache
|
||||
window.addEventListener('pageshow', function (event) {
|
||||
if (event.persisted) {
|
||||
// Page was loaded from cache, so we reload to ensure scripts run correctly
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
|
||||
// NEW: Function to load team members from JSON
|
||||
async function loadTeamMembers() {
|
||||
const container = document.getElementById('teamGridContainer');
|
||||
// Only run this function if the container exists on the current page (i.e., on who.html)
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('data/team.json');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const teamMembers = await response.json();
|
||||
|
||||
const teamHTML = teamMembers.map((member, index) => {
|
||||
// Create the HTML for each team member card
|
||||
return `
|
||||
<div class="group relative team-card opacity-0" data-anim="fade-in-up" data-anim-delay="${200 + index * 100}">
|
||||
<div class="bg-kh3-grey/20 p-8 rounded-lg hover:bg-kh3-grey/30 transition-all duration-500 transform hover:scale-105 hover:-translate-y-2">
|
||||
<div class="w-48 h-64 bg-gradient-to-br from-kh3-red/20 to-kh3-grey/40 rounded-lg mx-auto mb-6 flex items-center justify-center overflow-hidden group-hover:shadow-lg group-hover:shadow-red-500/20 transition-all duration-500">
|
||||
<img src="${member.image}" alt="${member.name.join(' ')}" class="w-full h-full object-cover rounded-lg transition-all duration-500 group-hover:scale-110" />
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<h3 class="text-2xl font-bold text-white mb-2 transition-all duration-300 group-hover:text-kh3-red group-hover:scale-105">
|
||||
${member.name.join('<br>')}
|
||||
</h3>
|
||||
<p class="text-kh3-red font-medium transition-all duration-300 group-hover:text-red-400">
|
||||
${member.title}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
container.innerHTML = teamHTML;
|
||||
// After adding the cards to the DOM, re-run the animation initializer to apply the staggered delays
|
||||
initDynamicAnimations();
|
||||
|
||||
} catch (error) {
|
||||
console.error("Could not load team members:", error);
|
||||
container.innerHTML = `<p class="text-center text-kh3-red col-span-full">Failed to load team members. Please try again later.</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function initActiveNav() {
|
||||
let currentPath = window.location.pathname.split('/').pop();
|
||||
let currentPage = currentPath ? currentPath.split('?')[0].split('#')[0] : 'index.html';
|
||||
|
||||
document.querySelectorAll('#navMenu a').forEach(link => {
|
||||
let linkPath = link.getAttribute('href').split('/').pop();
|
||||
let linkPage = linkPath ? linkPath : 'index.html';
|
||||
|
||||
if (currentPage === linkPage) {
|
||||
link.classList.add('active');
|
||||
} else {
|
||||
link.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
163
src/js/projects.js
Normal file
163
src/js/projects.js
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// --- Start of projects.js specific logic ---
|
||||
|
||||
let projectsData = [];
|
||||
let currentProject = null;
|
||||
let currentImageIndex = 0;
|
||||
|
||||
// --- Core Functions for fetching and rendering project cards ---
|
||||
async function loadProjects() {
|
||||
try {
|
||||
const response = await fetch("data/projects.json");
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
projectsData = await response.json();
|
||||
renderProjects();
|
||||
} catch (error) {
|
||||
console.error("Error loading projects:", error);
|
||||
const grid = document.getElementById("projectsGrid");
|
||||
if(grid) grid.innerHTML = `<p class="text-center text-kh3-red col-span-full">Failed to load projects.</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderProjects() {
|
||||
const grid = document.getElementById("projectsGrid");
|
||||
if (!grid) return;
|
||||
|
||||
grid.innerHTML = projectsData.map((project, index) => `
|
||||
<div class="project-card group relative overflow-hidden rounded-lg bg-kh3-grey/10 border border-white/10 cursor-pointer opacity-0"
|
||||
data-anim="fade-in-up" data-anim-delay="${200 + index * 100}"
|
||||
onclick="openProjectDetails('${project.id}')">
|
||||
<div class="relative h-64 overflow-hidden">
|
||||
<img src="${project.images[0]}" alt="${project.name}" class="project-image w-full h-full object-cover transition-all duration-600" />
|
||||
<div class="project-overlay absolute inset-0 bg-black/80 flex items-center justify-center"><div class="text-center"><div class="w-12 h-12 bg-kh3-red rounded-full flex items-center justify-center mx-auto mb-4"><svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path></svg></div><p class="text-white font-medium">VIEW PROJECT</p></div></div>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between mb-3"><span class="text-kh3-red text-sm font-medium tracking-wider">${project.category}</span><span class="text-white/60 text-sm">${project.completionDate}</span></div>
|
||||
<h3 class="text-xl font-bold text-white mb-2 group-hover:text-kh3-red transition-colors duration-300">${project.name}</h3>
|
||||
<p class="text-white/70 text-sm mb-4 line-clamp-2">${project.description}</p>
|
||||
<div class="flex items-center justify-between"><span class="text-white/50 text-sm">${project.size}</span><span class="text-white/50 text-sm">${project.location}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
`).join("");
|
||||
if (window.initDynamicAnimations) window.initDynamicAnimations();
|
||||
}
|
||||
|
||||
// --- MODAL Functionality (Restored from original) ---
|
||||
function openProjectDetails(projectId) {
|
||||
const project = projectsData.find((p) => p.id === projectId);
|
||||
if (!project) return;
|
||||
currentProject = project;
|
||||
currentImageIndex = 0;
|
||||
document.getElementById("modalTitle").textContent = project.name;
|
||||
document.getElementById("modalCategory").textContent = project.category;
|
||||
document.getElementById("modalDescription").textContent = project.description;
|
||||
document.getElementById("modalClient").textContent = project.client;
|
||||
document.getElementById("modalTypeOfWork").textContent = project.typeOfWork;
|
||||
document.getElementById("modalSize").textContent = project.size;
|
||||
document.getElementById("modalCompletionDate").textContent = project.completionDate;
|
||||
document.getElementById("modalLocation").textContent = project.location;
|
||||
document.getElementById("modalHighlights").innerHTML = project.highlights.map(h => `<li class="flex items-start space-x-2"><div class="w-1.5 h-1.5 bg-kh3-red rounded-full mt-2 flex-shrink-0"></div><span class="text-white/80 text-sm">${h}</span></li>`).join("");
|
||||
|
||||
setupImageCarousel(project.images); // RESTORED CALL
|
||||
|
||||
const modal = document.getElementById("projectModal");
|
||||
const modalContent = document.getElementById("modalContent");
|
||||
modal.classList.remove("hidden");
|
||||
document.body.style.overflow = "hidden";
|
||||
setTimeout(() => modalContent.classList.remove("scale-95", "opacity-0"), 10);
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
const modal = document.getElementById("projectModal");
|
||||
const modalContent = document.getElementById("modalContent");
|
||||
modalContent.classList.add("scale-95", "opacity-0");
|
||||
setTimeout(() => {
|
||||
modal.classList.add("hidden");
|
||||
document.body.style.overflow = "auto";
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// --- MODAL CAROUSEL Functionality (Restored from original) ---
|
||||
function setupImageCarousel(images) {
|
||||
const mainImage = document.getElementById("modalMainImage");
|
||||
const imageDots = document.getElementById("imageDots");
|
||||
const imageCounter = document.getElementById("imageCounter");
|
||||
const totalImages = document.getElementById("totalImages");
|
||||
|
||||
mainImage.src = images[0];
|
||||
mainImage.alt = currentProject.name;
|
||||
totalImages.textContent = images.length;
|
||||
imageDots.innerHTML = images.map((_, index) => `<button class="w-2 h-2 rounded-full transition-all duration-300 ${index === 0 ? "bg-kh3-red" : "bg-white/30"}" onclick="goToImage(${index})"></button>`).join("");
|
||||
updateImageCarousel(); // Initial call to set counter
|
||||
}
|
||||
|
||||
function updateImageCarousel() {
|
||||
const mainImage = document.getElementById("modalMainImage");
|
||||
const imageDots = document.getElementById("imageDots");
|
||||
const imageCounter = document.getElementById("imageCounter");
|
||||
if (!mainImage || !currentProject) return;
|
||||
|
||||
mainImage.style.opacity = "0";
|
||||
setTimeout(() => {
|
||||
mainImage.src = currentProject.images[currentImageIndex];
|
||||
mainImage.style.opacity = "1";
|
||||
}, 250);
|
||||
|
||||
imageCounter.textContent = currentImageIndex + 1;
|
||||
const dots = imageDots.querySelectorAll("button");
|
||||
dots.forEach((dot, index) => {
|
||||
dot.classList.toggle("bg-kh3-red", index === currentImageIndex);
|
||||
dot.classList.toggle("bg-white/30", index !== currentImageIndex);
|
||||
});
|
||||
}
|
||||
|
||||
// --- IMAGE ENLARGEMENT Functionality (Restored from original) ---
|
||||
function enlargeImage() {
|
||||
if (!currentProject) return;
|
||||
const enlargedImage = document.getElementById("enlargedImage");
|
||||
document.getElementById("enlargedImageCounter").textContent = currentImageIndex + 1;
|
||||
document.getElementById("enlargedTotalImages").textContent = currentProject.images.length;
|
||||
enlargedImage.src = currentProject.images[currentImageIndex];
|
||||
document.getElementById("imageEnlargeModal").classList.remove("hidden");
|
||||
}
|
||||
|
||||
function closeEnlargeModal() {
|
||||
document.getElementById("imageEnlargeModal").classList.add("hidden");
|
||||
}
|
||||
|
||||
// --- Global Accessibility for onclick and keyboard events (Restored) ---
|
||||
window.openProjectDetails = openProjectDetails;
|
||||
window.closeModal = closeModal;
|
||||
window.enlargeImage = enlargeImage;
|
||||
window.goToImage = (index) => { currentImageIndex = index; updateImageCarousel(); };
|
||||
window.nextImage = () => { currentImageIndex = (currentImageIndex + 1) % currentProject.images.length; updateImageCarousel(); };
|
||||
window.prevImage = () => { currentImageIndex = (currentImageIndex === 0) ? currentProject.images.length - 1 : currentImageIndex - 1; updateImageCarousel(); };
|
||||
|
||||
// Event listeners for modal controls
|
||||
document.getElementById("closeModal")?.addEventListener("click", closeModal);
|
||||
document.getElementById("projectModal")?.addEventListener("click", (e) => { if (e.target.id === "projectModal") closeModal(); });
|
||||
document.getElementById("prevImage")?.addEventListener("click", window.prevImage);
|
||||
document.getElementById("nextImage")?.addEventListener("click", window.nextImage);
|
||||
document.getElementById("closeEnlargeModal")?.addEventListener("click", closeEnlargeModal);
|
||||
document.getElementById("enlargePrevImage")?.addEventListener("click", () => { window.prevImage(); enlargeImage(); });
|
||||
document.getElementById("enlargeNextImage")?.addEventListener("click", () => { window.nextImage(); enlargeImage(); });
|
||||
document.getElementById("imageEnlargeModal")?.addEventListener("click", (e) => { if (e.target.id === "imageEnlargeModal") closeEnlargeModal(); });
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
const isProjectModalOpen = !document.getElementById("projectModal")?.classList.contains("hidden");
|
||||
const isEnlargeModalOpen = !document.getElementById("imageEnlargeModal")?.classList.contains("hidden");
|
||||
|
||||
if (isEnlargeModalOpen) {
|
||||
if (e.key === "ArrowLeft") { window.prevImage(); enlargeImage(); }
|
||||
if (e.key === "ArrowRight") { window.nextImage(); enlargeImage(); }
|
||||
if (e.key === "Escape") closeEnlargeModal();
|
||||
} else if (isProjectModalOpen) {
|
||||
if (e.key === "ArrowLeft") window.prevImage();
|
||||
if (e.key === "ArrowRight") window.nextImage();
|
||||
if (e.key === "Escape") closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
// --- Initialize Page ---
|
||||
loadProjects();
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue