Initial commit: KH3 Group Limited website with enhanced animations and slide+zoom transitions
This commit is contained in:
commit
ecebd00021
23 changed files with 5879 additions and 0 deletions
392
js/main.js
Normal file
392
js/main.js
Normal file
|
|
@ -0,0 +1,392 @@
|
|||
// Main JavaScript for FBS Website
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Initialize all functionality
|
||||
initNavigation();
|
||||
initScrollAnimations();
|
||||
initSmoothScrolling();
|
||||
initSlider();
|
||||
initCustomCursor();
|
||||
initScrollProgress();
|
||||
initLoadingScreen();
|
||||
initTextReveal();
|
||||
initParallax();
|
||||
});
|
||||
|
||||
// Navigation functionality
|
||||
function initNavigation() {
|
||||
const menuToggle = document.getElementById("menuToggle");
|
||||
const navOverlay = document.getElementById("navOverlay");
|
||||
const navClose = document.getElementById("navClose");
|
||||
const navLinks = document.querySelectorAll(".nav-link");
|
||||
|
||||
// Open navigation
|
||||
menuToggle.addEventListener("click", function () {
|
||||
navOverlay.classList.add("active");
|
||||
document.body.style.overflow = "hidden";
|
||||
});
|
||||
|
||||
// Close navigation
|
||||
navClose.addEventListener("click", function () {
|
||||
navOverlay.classList.remove("active");
|
||||
document.body.style.overflow = "";
|
||||
});
|
||||
|
||||
// Close navigation when clicking outside
|
||||
navOverlay.addEventListener("click", function (e) {
|
||||
if (e.target === navOverlay) {
|
||||
navOverlay.classList.remove("active");
|
||||
document.body.style.overflow = "";
|
||||
}
|
||||
});
|
||||
|
||||
// Close navigation when clicking on a link
|
||||
navLinks.forEach((link) => {
|
||||
link.addEventListener("click", function () {
|
||||
navOverlay.classList.remove("active");
|
||||
document.body.style.overflow = "";
|
||||
});
|
||||
});
|
||||
|
||||
// Close navigation with Escape key
|
||||
document.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Escape" && navOverlay.classList.contains("active")) {
|
||||
navOverlay.classList.remove("active");
|
||||
document.body.style.overflow = "";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Scroll-triggered animations
|
||||
function initScrollAnimations() {
|
||||
const observerOptions = {
|
||||
threshold: 0.1,
|
||||
rootMargin: "0px 0px -50px 0px",
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver(function (entries) {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add("animated");
|
||||
}
|
||||
});
|
||||
}, observerOptions);
|
||||
|
||||
// Observe all elements with animation classes
|
||||
const animatedElements = document.querySelectorAll(
|
||||
".animate-on-scroll, .animate-on-scroll-left, .animate-on-scroll-right, .animate-on-scroll-scale"
|
||||
);
|
||||
|
||||
animatedElements.forEach((el) => {
|
||||
observer.observe(el);
|
||||
});
|
||||
}
|
||||
|
||||
// Smooth scrolling for anchor links
|
||||
function initSmoothScrolling() {
|
||||
const links = document.querySelectorAll('a[href^="#"]');
|
||||
|
||||
links.forEach((link) => {
|
||||
link.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
const targetId = this.getAttribute("href");
|
||||
const targetElement = document.querySelector(targetId);
|
||||
|
||||
if (targetElement) {
|
||||
const offsetTop = targetElement.offsetTop;
|
||||
window.scrollTo({
|
||||
top: offsetTop,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Global scroll to section function
|
||||
function scrollToSection(sectionId) {
|
||||
const section = document.getElementById(sectionId);
|
||||
if (section) {
|
||||
section.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Slider functionality
|
||||
function initSlider() {
|
||||
const sliderDots = document.querySelectorAll(".slider-dot");
|
||||
let currentSlide = 0;
|
||||
|
||||
sliderDots.forEach((dot, index) => {
|
||||
dot.addEventListener("click", function () {
|
||||
// Remove active class from all dots
|
||||
sliderDots.forEach((d) => d.classList.remove("active"));
|
||||
|
||||
// Add active class to clicked dot
|
||||
this.classList.add("active");
|
||||
|
||||
// Update current slide
|
||||
currentSlide = index;
|
||||
|
||||
// Here you would typically change the background image
|
||||
// For now, we'll just add a visual effect
|
||||
const heroImage = document.querySelector(".hero-image");
|
||||
heroImage.style.transform = `scale(1.05)`;
|
||||
|
||||
setTimeout(() => {
|
||||
heroImage.style.transform = "scale(1)";
|
||||
}, 300);
|
||||
});
|
||||
});
|
||||
|
||||
// Auto-slide functionality (optional)
|
||||
setInterval(() => {
|
||||
currentSlide = (currentSlide + 1) % sliderDots.length;
|
||||
sliderDots.forEach((dot, index) => {
|
||||
dot.classList.toggle("active", index === currentSlide);
|
||||
});
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Custom cursor
|
||||
function initCustomCursor() {
|
||||
// Only enable on desktop
|
||||
if (window.innerWidth <= 768) return;
|
||||
|
||||
const cursor = document.createElement("div");
|
||||
cursor.className = "custom-cursor";
|
||||
document.body.appendChild(cursor);
|
||||
|
||||
document.addEventListener("mousemove", function (e) {
|
||||
cursor.style.left = e.clientX + "px";
|
||||
cursor.style.top = e.clientY + "px";
|
||||
});
|
||||
|
||||
// Add hover effect for interactive elements
|
||||
const interactiveElements = document.querySelectorAll(
|
||||
"a, button, .cta-button, .service-card"
|
||||
);
|
||||
|
||||
interactiveElements.forEach((el) => {
|
||||
el.addEventListener("mouseenter", function () {
|
||||
cursor.classList.add("hover");
|
||||
});
|
||||
|
||||
el.addEventListener("mouseleave", function () {
|
||||
cursor.classList.remove("hover");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Scroll progress indicator
|
||||
function initScrollProgress() {
|
||||
const progressBar = document.createElement("div");
|
||||
progressBar.className = "scroll-progress";
|
||||
document.body.appendChild(progressBar);
|
||||
|
||||
window.addEventListener("scroll", function () {
|
||||
const scrollTop = window.pageYOffset;
|
||||
const docHeight = document.body.scrollHeight - window.innerHeight;
|
||||
const scrollPercent = (scrollTop / docHeight) * 100;
|
||||
|
||||
progressBar.style.width = scrollPercent + "%";
|
||||
});
|
||||
}
|
||||
|
||||
// Loading screen
|
||||
function initLoadingScreen() {
|
||||
const loading = document.createElement("div");
|
||||
loading.className = "loading";
|
||||
loading.innerHTML = '<div class="loading-spinner"></div>';
|
||||
document.body.appendChild(loading);
|
||||
|
||||
// Hide loading screen after page loads
|
||||
window.addEventListener("load", function () {
|
||||
setTimeout(() => {
|
||||
loading.classList.add("hidden");
|
||||
setTimeout(() => {
|
||||
loading.remove();
|
||||
}, 500);
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
// Text reveal animations
|
||||
function initTextReveal() {
|
||||
const textElements = document.querySelectorAll(
|
||||
".main-headline, .section-title"
|
||||
);
|
||||
|
||||
textElements.forEach((element) => {
|
||||
const text = element.textContent;
|
||||
element.innerHTML = "";
|
||||
element.classList.add("text-reveal");
|
||||
|
||||
text.split("").forEach((char) => {
|
||||
const span = document.createElement("span");
|
||||
span.textContent = char === " " ? "\u00A0" : char;
|
||||
element.appendChild(span);
|
||||
});
|
||||
});
|
||||
|
||||
// Trigger text reveal on scroll
|
||||
const textObserver = new IntersectionObserver(
|
||||
function (entries) {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add("revealed");
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.5 }
|
||||
);
|
||||
|
||||
document.querySelectorAll(".text-reveal").forEach((el) => {
|
||||
textObserver.observe(el);
|
||||
});
|
||||
}
|
||||
|
||||
// Parallax effects
|
||||
function initParallax() {
|
||||
const parallaxElements = document.querySelectorAll(
|
||||
".hero-image, .vertical-line"
|
||||
);
|
||||
|
||||
window.addEventListener("scroll", function () {
|
||||
const scrolled = window.pageYOffset;
|
||||
|
||||
parallaxElements.forEach((element) => {
|
||||
const speed = element.dataset.speed || 0.5;
|
||||
const yPos = -(scrolled * speed);
|
||||
element.style.transform = `translateY(${yPos}px)`;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
// Handle window resize
|
||||
window.addEventListener(
|
||||
"resize",
|
||||
debounce(function () {
|
||||
// Reinitialize cursor on resize
|
||||
const cursor = document.querySelector(".custom-cursor");
|
||||
if (cursor && window.innerWidth <= 768) {
|
||||
cursor.remove();
|
||||
} else if (!cursor && window.innerWidth > 768) {
|
||||
initCustomCursor();
|
||||
}
|
||||
}, 250)
|
||||
);
|
||||
|
||||
// Add ripple effect to buttons
|
||||
document.addEventListener("click", function (e) {
|
||||
if (
|
||||
e.target.classList.contains("cta-button") ||
|
||||
e.target.closest(".cta-button")
|
||||
) {
|
||||
const button = e.target.classList.contains("cta-button")
|
||||
? e.target
|
||||
: e.target.closest(".cta-button");
|
||||
button.classList.add("ripple");
|
||||
|
||||
setTimeout(() => {
|
||||
button.classList.remove("ripple");
|
||||
}, 600);
|
||||
}
|
||||
});
|
||||
|
||||
// Form handling (for contact forms)
|
||||
function handleFormSubmit(form) {
|
||||
const formData = new FormData(form);
|
||||
const submitButton = form.querySelector('button[type="submit"]');
|
||||
const originalText = submitButton.textContent;
|
||||
|
||||
// Show loading state
|
||||
submitButton.textContent = "Sending...";
|
||||
submitButton.disabled = true;
|
||||
|
||||
// Simulate form submission (replace with actual endpoint)
|
||||
setTimeout(() => {
|
||||
submitButton.textContent = "Sent!";
|
||||
submitButton.style.backgroundColor = "var(--color-primary)";
|
||||
|
||||
setTimeout(() => {
|
||||
submitButton.textContent = originalText;
|
||||
submitButton.disabled = false;
|
||||
submitButton.style.backgroundColor = "";
|
||||
form.reset();
|
||||
}, 2000);
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
// Initialize form handlers
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const forms = document.querySelectorAll("form");
|
||||
forms.forEach((form) => {
|
||||
form.addEventListener("submit", function (e) {
|
||||
e.preventDefault();
|
||||
handleFormSubmit(this);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Add some interactive effects to service cards
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const serviceCards = document.querySelectorAll(".service-card");
|
||||
|
||||
serviceCards.forEach((card) => {
|
||||
card.addEventListener("mouseenter", function () {
|
||||
this.style.transform = "translateY(-10px) scale(1.02)";
|
||||
});
|
||||
|
||||
card.addEventListener("mouseleave", function () {
|
||||
this.style.transform = "translateY(0) scale(1)";
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Keyboard navigation support
|
||||
document.addEventListener("keydown", function (e) {
|
||||
// Tab navigation for accessibility
|
||||
if (e.key === "Tab") {
|
||||
document.body.classList.add("keyboard-navigation");
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("mousedown", function () {
|
||||
document.body.classList.remove("keyboard-navigation");
|
||||
});
|
||||
|
||||
// Performance optimization: Lazy load images
|
||||
function initLazyLoading() {
|
||||
const images = document.querySelectorAll("img[data-src]");
|
||||
|
||||
const imageObserver = new IntersectionObserver(function (entries) {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const img = entry.target;
|
||||
img.src = img.dataset.src;
|
||||
img.classList.remove("lazy");
|
||||
imageObserver.unobserve(img);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
images.forEach((img) => imageObserver.observe(img));
|
||||
}
|
||||
|
||||
// Initialize lazy loading
|
||||
document.addEventListener("DOMContentLoaded", initLazyLoading);
|
||||
130
js/projects.js
Normal file
130
js/projects.js
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
// Projects Page JavaScript
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const pills = document.querySelectorAll("[data-filter]");
|
||||
const grid = document.querySelector("#projectsGrid");
|
||||
let projectsData = [];
|
||||
|
||||
// Load projects data
|
||||
async function loadProjects() {
|
||||
try {
|
||||
const response = await fetch("/data/projects.json");
|
||||
projectsData = await response.json();
|
||||
renderProjects("All");
|
||||
} catch (error) {
|
||||
console.error("Error loading projects:", error);
|
||||
grid.innerHTML =
|
||||
'<p class="error-message">Error loading projects. Please try again later.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
// Render projects based on category
|
||||
function renderProjects(category = "All") {
|
||||
const filteredProjects =
|
||||
category === "All"
|
||||
? projectsData
|
||||
: projectsData.filter((project) => project.category === category);
|
||||
|
||||
grid.classList.add("fade-out");
|
||||
|
||||
setTimeout(() => {
|
||||
grid.innerHTML = filteredProjects
|
||||
.map(
|
||||
(project) => `
|
||||
<div class="project-card" onclick="window.location.href='project-${project.slug}.html'">
|
||||
<div class="project-card-image" style="background-image: url('${project.coverImage}')">
|
||||
<div class="project-card-overlay"></div>
|
||||
</div>
|
||||
<div class="project-card-content">
|
||||
<div class="project-card-category">${project.category}</div>
|
||||
<h3 class="project-card-title">${project.title}</h3>
|
||||
<p class="project-card-location">${project.location}</p>
|
||||
<a href="project-${project.slug}.html" class="project-card-link">View Project</a>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
|
||||
grid.classList.remove("fade-out");
|
||||
grid.classList.add("fade-in");
|
||||
|
||||
setTimeout(() => {
|
||||
grid.classList.remove("fade-in");
|
||||
}, 180);
|
||||
}, 120);
|
||||
}
|
||||
|
||||
// Handle filter pill clicks
|
||||
pills.forEach((pill) => {
|
||||
pill.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
const category = pill.dataset.filter;
|
||||
|
||||
// Update pill states
|
||||
pills.forEach((p) => {
|
||||
p.setAttribute("aria-pressed", p === pill ? "true" : "false");
|
||||
});
|
||||
|
||||
// Update URL
|
||||
const url = new URL(location);
|
||||
if (category === "All") {
|
||||
url.searchParams.delete("category");
|
||||
} else {
|
||||
url.searchParams.set("category", category);
|
||||
}
|
||||
history.pushState({}, "", url);
|
||||
|
||||
// Render filtered projects
|
||||
renderProjects(category);
|
||||
});
|
||||
});
|
||||
|
||||
// Handle browser back/forward
|
||||
window.addEventListener("popstate", () => {
|
||||
const category = new URL(location).searchParams.get("category") || "All";
|
||||
const activePill = document.querySelector(`[data-filter="${category}"]`);
|
||||
|
||||
if (activePill) {
|
||||
pills.forEach((p) => {
|
||||
p.setAttribute("aria-pressed", p === activePill ? "true" : "false");
|
||||
});
|
||||
renderProjects(category);
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize with URL category if present
|
||||
const initialCategory =
|
||||
new URL(location).searchParams.get("category") || "All";
|
||||
const initialPill = document.querySelector(
|
||||
`[data-filter="${initialCategory}"]`
|
||||
);
|
||||
|
||||
if (initialPill) {
|
||||
pills.forEach((p) => {
|
||||
p.setAttribute("aria-pressed", p === initialPill ? "true" : "false");
|
||||
});
|
||||
}
|
||||
|
||||
// Load projects on page load
|
||||
loadProjects();
|
||||
});
|
||||
|
||||
// Keyboard navigation for filter pills
|
||||
document.addEventListener("keydown", function (e) {
|
||||
const activePill = document.querySelector('.pill[aria-pressed="true"]');
|
||||
const pills = Array.from(document.querySelectorAll(".pill"));
|
||||
const currentIndex = pills.indexOf(activePill);
|
||||
|
||||
if (e.key === "ArrowRight" || e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
const nextIndex = (currentIndex + 1) % pills.length;
|
||||
pills[nextIndex].click();
|
||||
pills[nextIndex].focus();
|
||||
} else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
const prevIndex = currentIndex === 0 ? pills.length - 1 : currentIndex - 1;
|
||||
pills[prevIndex].click();
|
||||
pills[prevIndex].focus();
|
||||
}
|
||||
});
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue