1221 lines
46 KiB
JavaScript
Executable File
1221 lines
46 KiB
JavaScript
Executable File
/* ========================================
|
|
MSPE Website - Main JavaScript
|
|
Multiple Service Provider Experts
|
|
======================================== */
|
|
|
|
// API Base URL - adjust based on your hosting setup
|
|
const API_BASE_URL = '/api';
|
|
|
|
// Central icon map: change values here to swap icons site-wide where data-icon-key is used.
|
|
const ICON_CLASS_MAP = {
|
|
service_it_support: 'fas fa-headset',
|
|
service_cybersecurity: 'fas fa-shield-alt',
|
|
service_cloud: 'fas fa-cloud',
|
|
service_consulting: 'fas fa-chess-king',
|
|
framework_iso27001: 'fas fa-certificate',
|
|
framework_nist_csf: 'fas fa-sitemap',
|
|
framework_soc2: 'fas fa-user-shield',
|
|
framework_pci_dss: 'fas fa-credit-card',
|
|
framework_hipaa: 'fas fa-heartbeat',
|
|
framework_gdpr_ccpa: 'fas fa-scale-balanced',
|
|
framework_nist_800: 'fas fa-building-shield',
|
|
framework_cobit_telco: 'fas fa-network-wired'
|
|
};
|
|
|
|
/**
|
|
* Escape HTML special characters to prevent XSS when inserting into innerHTML.
|
|
* Use for ALL user/API data interpolated into template literals.
|
|
*/
|
|
function escapeHTML(str) {
|
|
if (str == null) return '';
|
|
return String(str)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
/**
|
|
* Escape a string for safe use inside an HTML attribute (e.g. href, src, data-*).
|
|
* Rejects javascript: and data: URIs to prevent script injection via attributes.
|
|
*/
|
|
function escapeAttr(str) {
|
|
if (str == null) return '';
|
|
const s = String(str).trim();
|
|
if (/^(javascript|data):/i.test(s)) return '';
|
|
return s
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Initialize all modules
|
|
initPremiumExperience();
|
|
initPromoBar();
|
|
initSiteLinkEnhancements();
|
|
enforceSafeBlankLinks();
|
|
initReplaceableIcons();
|
|
optimizeImagesForCLS();
|
|
loadPublicSiteSettings();
|
|
initPreloader();
|
|
initNavigation();
|
|
initHeroSlider();
|
|
initCounters();
|
|
initTestimonials();
|
|
initBackToTop();
|
|
initWhatsAppButton();
|
|
initScrollAnimations();
|
|
initContactForm();
|
|
initNewsletterForm();
|
|
initFAQ();
|
|
|
|
// Load dynamic content from API
|
|
loadNews();
|
|
loadTestimonials();
|
|
loadPortfolio();
|
|
loadServices();
|
|
loadTeam();
|
|
});
|
|
|
|
function initPromoBar() {
|
|
const promoBar = document.getElementById('promo-bar');
|
|
if (!promoBar) return;
|
|
|
|
promoBar.querySelectorAll('.promo-close').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
promoBar.style.display = 'none';
|
|
document.body.classList.remove('has-promo');
|
|
});
|
|
});
|
|
}
|
|
|
|
function enforceSafeBlankLinks(scope = document) {
|
|
scope.querySelectorAll('a[target="_blank"]').forEach(link => {
|
|
const currentRel = (link.getAttribute('rel') || '').toLowerCase();
|
|
const relParts = new Set(currentRel.split(/\s+/).filter(Boolean));
|
|
relParts.add('noopener');
|
|
relParts.add('noreferrer');
|
|
link.setAttribute('rel', Array.from(relParts).join(' '));
|
|
});
|
|
}
|
|
|
|
function sanitizeIconClasses(iconClasses, fallback = 'fas fa-circle') {
|
|
const tokens = String(iconClasses || '')
|
|
.trim()
|
|
.split(/\s+/)
|
|
.filter(Boolean)
|
|
.filter(token => /^fa[a-z0-9-]+$/i.test(token));
|
|
|
|
if (!tokens.length) {
|
|
return fallback.split(/\s+/);
|
|
}
|
|
|
|
const hasStyleToken = tokens.some(token => /^(fas|far|fab|fal|fat|fa-solid|fa-regular|fa-brands)$/i.test(token));
|
|
if (!hasStyleToken) {
|
|
tokens.unshift('fas');
|
|
}
|
|
|
|
return tokens;
|
|
}
|
|
|
|
function initReplaceableIcons(scope = document) {
|
|
const iconNodes = scope.querySelectorAll('[data-icon-key], [data-icon]');
|
|
if (!iconNodes.length) return;
|
|
|
|
iconNodes.forEach(node => {
|
|
const targetIcon = node.matches('i') ? node : node.querySelector('i');
|
|
if (!targetIcon) return;
|
|
|
|
const explicitIcon = node.getAttribute('data-icon') || targetIcon.getAttribute('data-icon');
|
|
const iconKey = node.getAttribute('data-icon-key') || targetIcon.getAttribute('data-icon-key');
|
|
const mappedIcon = iconKey ? ICON_CLASS_MAP[iconKey] : '';
|
|
const selected = explicitIcon || mappedIcon;
|
|
if (!selected) return;
|
|
|
|
Array.from(targetIcon.classList).forEach(cls => {
|
|
if (/^fa/i.test(cls)) {
|
|
targetIcon.classList.remove(cls);
|
|
}
|
|
});
|
|
|
|
sanitizeIconClasses(selected).forEach(cls => targetIcon.classList.add(cls));
|
|
targetIcon.setAttribute('aria-hidden', 'true');
|
|
});
|
|
}
|
|
|
|
function initSiteLinkEnhancements() {
|
|
// Fallback social URLs: only used for placeholder links (href="#" or "javascript:void(0)").
|
|
// Preferred approach: set correct URLs directly in HTML. These are a safety net.
|
|
const socialProfiles = {
|
|
facebook: 'https://www.facebook.com/mspe.pro',
|
|
linkedin: 'https://www.linkedin.com/company/mspe-pro',
|
|
twitter: 'https://x.com/mspe_pro',
|
|
instagram: 'https://www.instagram.com/mspe.pro',
|
|
whatsapp: 'https://wa.me/96178782023'
|
|
};
|
|
|
|
const iconToNetwork = [
|
|
{ selector: '.fa-facebook-f, .fa-facebook', network: 'facebook' },
|
|
{ selector: '.fa-linkedin-in, .fa-linkedin', network: 'linkedin' },
|
|
{ selector: '.fa-twitter, .fa-x-twitter', network: 'twitter' },
|
|
{ selector: '.fa-instagram', network: 'instagram' },
|
|
{ selector: '.fa-whatsapp', network: 'whatsapp' }
|
|
];
|
|
|
|
document.querySelectorAll('a[href="javascript:void(0)"], a[href="#"]').forEach(link => {
|
|
const icon = link.querySelector('i');
|
|
const label = (link.getAttribute('aria-label') || link.getAttribute('title') || '').toLowerCase();
|
|
|
|
let network = '';
|
|
|
|
if (icon) {
|
|
const iconMatch = iconToNetwork.find(item => icon.matches(item.selector));
|
|
if (iconMatch) network = iconMatch.network;
|
|
}
|
|
|
|
if (!network) {
|
|
if (label.includes('facebook')) network = 'facebook';
|
|
if (label.includes('linkedin')) network = 'linkedin';
|
|
if (label.includes('twitter') || label.includes('x')) network = 'twitter';
|
|
if (label.includes('instagram')) network = 'instagram';
|
|
if (label.includes('whatsapp')) network = 'whatsapp';
|
|
}
|
|
|
|
if (network && socialProfiles[network]) {
|
|
link.href = socialProfiles[network];
|
|
link.target = '_blank';
|
|
link.rel = 'noopener noreferrer';
|
|
link.title = `Follow us on ${network.charAt(0).toUpperCase()}${network.slice(1)}`;
|
|
}
|
|
});
|
|
|
|
document.querySelectorAll('a').forEach(link => {
|
|
const text = (link.textContent || '').trim().toLowerCase();
|
|
if (text === 'privacy policy') {
|
|
link.href = 'privacy.html';
|
|
}
|
|
if (text === 'terms of service' || text === 'terms') {
|
|
link.href = 'terms.html';
|
|
}
|
|
});
|
|
}
|
|
|
|
function optimizeImagesForCLS() {
|
|
const images = document.querySelectorAll('img');
|
|
if (!images.length) return;
|
|
|
|
images.forEach(img => {
|
|
if (!img.hasAttribute('loading')) {
|
|
img.setAttribute('loading', 'lazy');
|
|
}
|
|
|
|
if (!img.hasAttribute('decoding')) {
|
|
img.setAttribute('decoding', 'async');
|
|
}
|
|
|
|
if (img.classList.contains('logo-image') && !img.hasAttribute('width') && !img.hasAttribute('height')) {
|
|
img.setAttribute('width', '200');
|
|
img.setAttribute('height', '50');
|
|
}
|
|
|
|
const setNaturalDimensions = () => {
|
|
if ((!img.hasAttribute('width') || !img.hasAttribute('height')) && img.naturalWidth && img.naturalHeight) {
|
|
img.setAttribute('width', String(img.naturalWidth));
|
|
img.setAttribute('height', String(img.naturalHeight));
|
|
}
|
|
};
|
|
|
|
if (img.complete) {
|
|
setNaturalDimensions();
|
|
} else {
|
|
img.addEventListener('load', setNaturalDimensions, { once: true });
|
|
}
|
|
});
|
|
}
|
|
|
|
async function loadPublicSiteSettings() {
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/public-settings.php`);
|
|
const result = await response.json();
|
|
|
|
if (!result || !result.success || !result.data) return;
|
|
applyPublicSiteSettings(result.data);
|
|
} catch (error) {
|
|
console.log('Public settings unavailable');
|
|
}
|
|
}
|
|
|
|
function applyPublicSiteSettings(settings) {
|
|
if (settings.contact_email) {
|
|
document.querySelectorAll('a[href^="mailto:"]').forEach(link => {
|
|
link.href = `mailto:${settings.contact_email}`;
|
|
if (!link.textContent.trim() || link.textContent.includes('@')) {
|
|
link.textContent = settings.contact_email;
|
|
}
|
|
});
|
|
|
|
document.querySelectorAll('[data-site-email]').forEach(el => {
|
|
el.textContent = settings.contact_email;
|
|
});
|
|
}
|
|
|
|
if (settings.contact_phone) {
|
|
const phoneDigits = settings.contact_phone.replace(/\s+/g, '');
|
|
|
|
document.querySelectorAll('a[href^="tel:"]').forEach(link => {
|
|
link.href = `tel:${phoneDigits}`;
|
|
});
|
|
|
|
document.querySelectorAll('[data-site-phone]').forEach(el => {
|
|
el.textContent = settings.contact_phone;
|
|
});
|
|
}
|
|
|
|
if (settings.business_hours) {
|
|
document.querySelectorAll('[data-business-hours]').forEach(el => {
|
|
el.textContent = settings.business_hours;
|
|
});
|
|
}
|
|
|
|
// Site name & tagline
|
|
if (settings.site_name) {
|
|
document.querySelectorAll('[data-site-name]').forEach(el => {
|
|
el.textContent = settings.site_name;
|
|
});
|
|
// Update page title prefix if it uses site name
|
|
if (document.title && !document.title.startsWith(settings.site_name)) {
|
|
const sep = document.title.includes('|') ? ' | ' : ' - ';
|
|
const suffix = document.title.split(/[|\-]/)[1]?.trim();
|
|
document.title = suffix ? `${settings.site_name}${sep}${suffix}` : settings.site_name;
|
|
}
|
|
}
|
|
|
|
if (settings.site_tagline) {
|
|
document.querySelectorAll('[data-site-tagline]').forEach(el => {
|
|
el.textContent = settings.site_tagline;
|
|
});
|
|
}
|
|
|
|
// Social links — wire up any <a data-social="facebook"> etc.
|
|
const socialKeys = ['facebook', 'linkedin', 'twitter', 'instagram', 'youtube', 'github'];
|
|
socialKeys.forEach(platform => {
|
|
const url = settings[`social_${platform}`];
|
|
if (url) {
|
|
document.querySelectorAll(`a[data-social="${platform}"]`).forEach(link => {
|
|
link.href = url;
|
|
link.removeAttribute('hidden');
|
|
link.style.display = '';
|
|
});
|
|
}
|
|
});
|
|
|
|
// Brand colors via CSS custom properties
|
|
const colorMap = {
|
|
primary_color: '--primary-color',
|
|
secondary_color: '--secondary-color',
|
|
accent_color: '--accent-color'
|
|
};
|
|
Object.entries(colorMap).forEach(([key, cssVar]) => {
|
|
if (settings[key]) {
|
|
document.documentElement.style.setProperty(cssVar, settings[key]);
|
|
}
|
|
});
|
|
}
|
|
|
|
/* ==================== Premium Experience ==================== */
|
|
function initPremiumExperience() {
|
|
document.body.classList.add('premium-ui');
|
|
setActiveNavLinkByPath();
|
|
initPremiumReveal();
|
|
initSmoothAnchorOffset();
|
|
initCardPointerLift();
|
|
}
|
|
|
|
function setActiveNavLinkByPath() {
|
|
const navLinks = document.querySelectorAll('.nav-link[href]');
|
|
if (!navLinks.length) return;
|
|
|
|
const currentPath = window.location.pathname.split('/').pop() || 'index.html';
|
|
|
|
navLinks.forEach(link => {
|
|
const href = link.getAttribute('href') || '';
|
|
if (href.startsWith('#') || href.includes('#')) return;
|
|
|
|
const linkPath = href.split('/').pop();
|
|
if (linkPath === currentPath) {
|
|
navLinks.forEach(item => item.classList.remove('active'));
|
|
link.classList.add('active');
|
|
}
|
|
});
|
|
}
|
|
|
|
function initPremiumReveal() {
|
|
const revealTargets = document.querySelectorAll(
|
|
'.section-header, .service-card, .value-card, .team-card, .news-card, .portfolio-card, .contact-card, .feature-item, .process-step, .stat-item, .testimonial-card'
|
|
);
|
|
|
|
if (!revealTargets.length || !('IntersectionObserver' in window)) return;
|
|
|
|
revealTargets.forEach((el, index) => {
|
|
el.classList.add('premium-reveal');
|
|
el.style.transitionDelay = `${Math.min(index % 6, 5) * 70}ms`;
|
|
});
|
|
|
|
const observer = new IntersectionObserver((entries, currentObserver) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
entry.target.classList.add('is-visible');
|
|
currentObserver.unobserve(entry.target);
|
|
}
|
|
});
|
|
}, { threshold: 0.14, rootMargin: '0px 0px -40px 0px' });
|
|
|
|
revealTargets.forEach(el => observer.observe(el));
|
|
}
|
|
|
|
function initSmoothAnchorOffset() {
|
|
const anchorLinks = document.querySelectorAll('a[href^="#"]:not([href="#"])');
|
|
if (!anchorLinks.length) return;
|
|
|
|
anchorLinks.forEach(link => {
|
|
link.addEventListener('click', (event) => {
|
|
const targetId = link.getAttribute('href');
|
|
const target = document.querySelector(targetId);
|
|
if (!target) return;
|
|
|
|
event.preventDefault();
|
|
const header = document.getElementById('header') || document.querySelector('.header');
|
|
const offset = header ? header.offsetHeight + 18 : 90;
|
|
const top = target.getBoundingClientRect().top + window.scrollY - offset;
|
|
|
|
window.scrollTo({ top, behavior: 'smooth' });
|
|
});
|
|
});
|
|
}
|
|
|
|
function initCardPointerLift() {
|
|
const cards = document.querySelectorAll('.service-card, .value-card, .team-card, .portfolio-card, .news-card, .contact-card');
|
|
if (!cards.length || window.innerWidth < 768 || 'ontouchstart' in window || window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
|
|
|
cards.forEach(card => {
|
|
card.addEventListener('pointermove', (event) => {
|
|
const rect = card.getBoundingClientRect();
|
|
const x = event.clientX - rect.left;
|
|
const y = event.clientY - rect.top;
|
|
const rotateY = ((x / rect.width) - 0.5) * 4;
|
|
const rotateX = ((y / rect.height) - 0.5) * -4;
|
|
card.style.transform = `translateY(-4px) rotateX(${rotateX.toFixed(2)}deg) rotateY(${rotateY.toFixed(2)}deg)`;
|
|
});
|
|
|
|
card.addEventListener('pointerleave', () => {
|
|
card.style.transform = '';
|
|
});
|
|
});
|
|
}
|
|
|
|
/* ==================== Preloader ==================== */
|
|
function initPreloader() {
|
|
// Support both preloader structures used across pages
|
|
const preloader = document.getElementById('preloader') || document.querySelector('.preloader');
|
|
|
|
if (!preloader) return;
|
|
|
|
window.addEventListener('load', () => {
|
|
setTimeout(() => {
|
|
preloader.classList.add('hidden');
|
|
preloader.style.opacity = '0';
|
|
preloader.style.visibility = 'hidden';
|
|
document.body.classList.remove('no-scroll');
|
|
|
|
// Remove preloader from DOM after animation
|
|
setTimeout(() => {
|
|
preloader.style.display = 'none';
|
|
}, 500);
|
|
}, 500);
|
|
});
|
|
}
|
|
|
|
/* ==================== Navigation ==================== */
|
|
function initNavigation() {
|
|
const header = document.getElementById('header') || document.querySelector('.header');
|
|
// Support both nav toggle button selectors used across pages
|
|
const navToggle = document.getElementById('nav-toggle') || document.querySelector('.mobile-toggle') || document.querySelector('.nav-toggle');
|
|
const navMenu = document.getElementById('nav-menu') || document.querySelector('.nav-menu');
|
|
const navLinks = document.querySelectorAll('.nav-link');
|
|
|
|
if (!header) return;
|
|
|
|
// Scroll effect - throttled for performance
|
|
let scrollTicking = false;
|
|
window.addEventListener('scroll', () => {
|
|
if (!scrollTicking) {
|
|
requestAnimationFrame(() => {
|
|
if (window.scrollY > 100) {
|
|
header.classList.add('scrolled');
|
|
} else {
|
|
header.classList.remove('scrolled');
|
|
}
|
|
scrollTicking = false;
|
|
});
|
|
scrollTicking = true;
|
|
}
|
|
}, { passive: true });
|
|
|
|
// Mobile menu toggle
|
|
if (navToggle && navMenu) {
|
|
navToggle.addEventListener('click', () => {
|
|
navMenu.classList.toggle('active');
|
|
document.body.classList.toggle('no-scroll');
|
|
|
|
// Animate hamburger + update aria-expanded
|
|
navToggle.classList.toggle('active');
|
|
const isExpanded = navMenu.classList.contains('active');
|
|
navToggle.setAttribute('aria-expanded', String(isExpanded));
|
|
});
|
|
}
|
|
|
|
// Close menu on link click
|
|
navLinks.forEach(link => {
|
|
link.addEventListener('click', () => {
|
|
if (navMenu) navMenu.classList.remove('active');
|
|
document.body.classList.remove('no-scroll');
|
|
if (navToggle) navToggle.classList.remove('active');
|
|
});
|
|
});
|
|
|
|
// Close menu on outside click
|
|
document.addEventListener('click', (e) => {
|
|
if (navMenu && navToggle && !navMenu.contains(e.target) && !navToggle.contains(e.target)) {
|
|
navMenu.classList.remove('active');
|
|
document.body.classList.remove('no-scroll');
|
|
if (navToggle) navToggle.classList.remove('active');
|
|
}
|
|
});
|
|
|
|
// Handle dropdown menus
|
|
const dropdowns = document.querySelectorAll('.nav-item.dropdown');
|
|
dropdowns.forEach(dropdown => {
|
|
const link = dropdown.querySelector('.nav-link');
|
|
if (link && window.innerWidth <= 1024) {
|
|
link.addEventListener('click', (e) => {
|
|
if (window.innerWidth <= 1024) {
|
|
e.preventDefault();
|
|
dropdown.classList.toggle('active');
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
/* ==================== Hero Slider ==================== */
|
|
function initHeroSlider() {
|
|
const slides = document.querySelectorAll('.hero-slide');
|
|
const dots = document.querySelectorAll('.dot');
|
|
const prevBtn = document.getElementById('hero-prev');
|
|
const nextBtn = document.getElementById('hero-next');
|
|
|
|
if (!slides.length) return;
|
|
|
|
let currentSlide = 0;
|
|
let slideInterval;
|
|
|
|
function goToSlide(index) {
|
|
slides[currentSlide].classList.remove('active');
|
|
dots[currentSlide].classList.remove('active');
|
|
|
|
currentSlide = index;
|
|
|
|
if (currentSlide >= slides.length) currentSlide = 0;
|
|
if (currentSlide < 0) currentSlide = slides.length - 1;
|
|
|
|
slides[currentSlide].classList.add('active');
|
|
dots[currentSlide].classList.add('active');
|
|
}
|
|
|
|
function nextSlide() {
|
|
goToSlide(currentSlide + 1);
|
|
}
|
|
|
|
function prevSlide() {
|
|
goToSlide(currentSlide - 1);
|
|
}
|
|
|
|
function startAutoSlide() {
|
|
slideInterval = setInterval(nextSlide, 6000);
|
|
}
|
|
|
|
function stopAutoSlide() {
|
|
clearInterval(slideInterval);
|
|
}
|
|
|
|
// Event listeners
|
|
if (prevBtn) {
|
|
prevBtn.addEventListener('click', () => {
|
|
stopAutoSlide();
|
|
prevSlide();
|
|
startAutoSlide();
|
|
});
|
|
}
|
|
|
|
if (nextBtn) {
|
|
nextBtn.addEventListener('click', () => {
|
|
stopAutoSlide();
|
|
nextSlide();
|
|
startAutoSlide();
|
|
});
|
|
}
|
|
|
|
dots.forEach((dot, index) => {
|
|
dot.addEventListener('click', () => {
|
|
stopAutoSlide();
|
|
goToSlide(index);
|
|
startAutoSlide();
|
|
});
|
|
});
|
|
|
|
// Start auto slide
|
|
startAutoSlide();
|
|
}
|
|
|
|
/* ==================== Counter Animation ==================== */
|
|
function initCounters() {
|
|
const counters = document.querySelectorAll('.counter');
|
|
|
|
const observerOptions = {
|
|
threshold: 0.5
|
|
};
|
|
|
|
const observer = new IntersectionObserver((entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
const counter = entry.target;
|
|
const target = parseInt(counter.closest('.stat-item').dataset.count);
|
|
animateCounter(counter, target);
|
|
observer.unobserve(counter);
|
|
}
|
|
});
|
|
}, observerOptions);
|
|
|
|
counters.forEach(counter => observer.observe(counter));
|
|
}
|
|
|
|
function animateCounter(element, target) {
|
|
let current = 0;
|
|
const increment = target / 50;
|
|
const duration = 2000;
|
|
const stepTime = duration / 50;
|
|
|
|
const timer = setInterval(() => {
|
|
current += increment;
|
|
if (current >= target) {
|
|
element.textContent = target;
|
|
clearInterval(timer);
|
|
} else {
|
|
element.textContent = Math.floor(current);
|
|
}
|
|
}, stepTime);
|
|
}
|
|
|
|
/* ==================== Testimonials Slider ==================== */
|
|
let _testimonialInterval = null;
|
|
function initTestimonials() {
|
|
const testimonials = document.querySelectorAll('.testimonial-card');
|
|
const prevBtn = document.querySelector('.testimonial-prev');
|
|
const nextBtn = document.querySelector('.testimonial-next');
|
|
const dotsContainer = document.getElementById('testimonial-dots');
|
|
|
|
if (!testimonials.length) return;
|
|
|
|
// Clear any previous interval to prevent stacking
|
|
if (_testimonialInterval) {
|
|
clearInterval(_testimonialInterval);
|
|
_testimonialInterval = null;
|
|
}
|
|
|
|
let currentIndex = 0;
|
|
|
|
// Clear previous dots before recreating
|
|
if (dotsContainer) dotsContainer.innerHTML = '';
|
|
|
|
// Create dots
|
|
testimonials.forEach((_, index) => {
|
|
const dot = document.createElement('span');
|
|
dot.classList.add('dot');
|
|
if (index === 0) dot.classList.add('active');
|
|
dot.addEventListener('click', () => goToTestimonial(index));
|
|
dotsContainer.appendChild(dot);
|
|
});
|
|
|
|
const dots = dotsContainer.querySelectorAll('.dot');
|
|
|
|
function goToTestimonial(index) {
|
|
testimonials[currentIndex].classList.remove('active');
|
|
dots[currentIndex].classList.remove('active');
|
|
|
|
currentIndex = index;
|
|
|
|
if (currentIndex >= testimonials.length) currentIndex = 0;
|
|
if (currentIndex < 0) currentIndex = testimonials.length - 1;
|
|
|
|
testimonials[currentIndex].classList.add('active');
|
|
dots[currentIndex].classList.add('active');
|
|
}
|
|
|
|
if (prevBtn) {
|
|
prevBtn.addEventListener('click', () => goToTestimonial(currentIndex - 1));
|
|
}
|
|
|
|
if (nextBtn) {
|
|
nextBtn.addEventListener('click', () => goToTestimonial(currentIndex + 1));
|
|
}
|
|
|
|
// Auto slide with pause on hover/focus for accessibility
|
|
let testimonialPaused = false;
|
|
const testimonialContainer = document.querySelector('.testimonials-slider, .testimonial-slider, .testimonials');
|
|
if (testimonialContainer) {
|
|
testimonialContainer.addEventListener('mouseenter', () => { testimonialPaused = true; });
|
|
testimonialContainer.addEventListener('mouseleave', () => { testimonialPaused = false; });
|
|
testimonialContainer.addEventListener('focusin', () => { testimonialPaused = true; });
|
|
testimonialContainer.addEventListener('focusout', () => { testimonialPaused = false; });
|
|
}
|
|
_testimonialInterval = setInterval(() => {
|
|
if (!testimonialPaused) goToTestimonial(currentIndex + 1);
|
|
}, 8000);
|
|
}
|
|
|
|
/* ==================== Back to Top ==================== */
|
|
function initBackToTop() {
|
|
// Support both selectors used across pages
|
|
const backToTop = document.getElementById('back-to-top') || document.querySelector('.back-to-top');
|
|
|
|
if (!backToTop) return;
|
|
|
|
let btTicking = false;
|
|
window.addEventListener('scroll', () => {
|
|
if (!btTicking) {
|
|
requestAnimationFrame(() => {
|
|
if (window.scrollY > 500) {
|
|
backToTop.classList.add('visible');
|
|
backToTop.style.opacity = '1';
|
|
backToTop.style.visibility = 'visible';
|
|
} else {
|
|
backToTop.classList.remove('visible');
|
|
backToTop.style.opacity = '0';
|
|
backToTop.style.visibility = 'hidden';
|
|
}
|
|
btTicking = false;
|
|
});
|
|
btTicking = true;
|
|
}
|
|
}, { passive: true });
|
|
|
|
backToTop.addEventListener('click', () => {
|
|
window.scrollTo({
|
|
top: 0,
|
|
behavior: 'smooth'
|
|
});
|
|
});
|
|
}
|
|
|
|
/* ==================== WhatsApp Float Button ==================== */
|
|
function initWhatsAppButton() {
|
|
if (document.querySelector('.whatsapp-float')) return;
|
|
const btn = document.createElement('a');
|
|
btn.href = 'https://wa.me/96178782023';
|
|
btn.className = 'whatsapp-float';
|
|
btn.target = '_blank';
|
|
btn.rel = 'noopener noreferrer';
|
|
btn.setAttribute('aria-label', 'Chat with us on WhatsApp');
|
|
btn.setAttribute('title', 'Chat with us on WhatsApp');
|
|
btn.innerHTML = '<i class="fab fa-whatsapp"></i>';
|
|
document.body.appendChild(btn);
|
|
}
|
|
|
|
/* ==================== Scroll Animations ==================== */
|
|
function initScrollAnimations() {
|
|
const elements = document.querySelectorAll('[data-aos]');
|
|
|
|
const observer = new IntersectionObserver((entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
const delay = entry.target.dataset.aosDelay || 0;
|
|
setTimeout(() => {
|
|
entry.target.classList.add('aos-animate');
|
|
}, delay);
|
|
}
|
|
});
|
|
}, { threshold: 0.1 });
|
|
|
|
elements.forEach(el => observer.observe(el));
|
|
}
|
|
|
|
/* ==================== Form Handling ==================== */
|
|
function initContactForm() {
|
|
const form = document.getElementById('contact-form');
|
|
|
|
if (!form) return;
|
|
|
|
// Check if form already has a submit handler from page-specific script
|
|
if (form.dataset.initialized) return;
|
|
form.dataset.initialized = 'true';
|
|
|
|
form.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
|
|
const formData = new FormData(form);
|
|
const data = Object.fromEntries(formData);
|
|
|
|
// Handle checkbox for newsletter
|
|
data.newsletter = formData.get('newsletter') === 'yes';
|
|
|
|
const submitBtn = form.querySelector('button[type="submit"]');
|
|
const formMessage = document.getElementById('form-message');
|
|
const originalText = submitBtn.innerHTML;
|
|
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Sending...';
|
|
submitBtn.disabled = true;
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/contact.php`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(data)
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
showNotification(result.message || 'Message sent successfully!', 'success');
|
|
if (formMessage) {
|
|
formMessage.className = 'form-message success';
|
|
formMessage.innerHTML = '<i class="fas fa-check-circle"></i> ' + escapeHTML(result.message || 'Thank you! Your message has been sent successfully.');
|
|
formMessage.style.display = 'block';
|
|
}
|
|
form.reset();
|
|
} else {
|
|
showNotification(result.message || 'Failed to send message. Please try again.', 'error');
|
|
if (formMessage) {
|
|
formMessage.className = 'form-message error';
|
|
formMessage.innerHTML = '<i class="fas fa-exclamation-circle"></i> ' + escapeHTML(result.message || 'Failed to send message.');
|
|
formMessage.style.display = 'block';
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Contact form error:', error);
|
|
showNotification('An error occurred. Please try again later.', 'error');
|
|
if (formMessage) {
|
|
formMessage.className = 'form-message error';
|
|
formMessage.innerHTML = '<i class="fas fa-exclamation-circle"></i> An error occurred. Please try again or email us directly.';
|
|
formMessage.style.display = 'block';
|
|
}
|
|
} finally {
|
|
submitBtn.innerHTML = originalText;
|
|
submitBtn.disabled = false;
|
|
|
|
// Hide message after 5 seconds
|
|
if (formMessage) {
|
|
setTimeout(() => {
|
|
formMessage.style.display = 'none';
|
|
}, 5000);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/* ==================== Notification ==================== */
|
|
function showNotification(message, type = 'info') {
|
|
const notification = document.createElement('div');
|
|
notification.className = `notification notification-${type}`;
|
|
notification.innerHTML = `
|
|
<i class="fas fa-${type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-circle' : 'info-circle'}"></i>
|
|
<span>${escapeHTML(message)}</span>
|
|
`;
|
|
|
|
document.body.appendChild(notification);
|
|
|
|
setTimeout(() => {
|
|
notification.classList.add('show');
|
|
}, 100);
|
|
|
|
setTimeout(() => {
|
|
notification.classList.remove('show');
|
|
setTimeout(() => {
|
|
notification.remove();
|
|
}, 300);
|
|
}, 5000);
|
|
}
|
|
|
|
/* ==================== Load News from API ==================== */
|
|
async function loadNews(limit = 3) {
|
|
const newsGrid = document.getElementById('news-grid');
|
|
const newsSection = document.getElementById('news-section');
|
|
const comingSoonSection = document.getElementById('coming-soon-section');
|
|
|
|
// Only proceed if news grid exists
|
|
if (!newsGrid) return;
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/news.php?limit=${limit}&status=published`);
|
|
const result = await response.json();
|
|
|
|
// Handle both response formats
|
|
const news = result.data || result;
|
|
|
|
if (news && news.length > 0) {
|
|
// Show news, hide coming soon
|
|
if (newsSection) newsSection.style.display = 'block';
|
|
if (comingSoonSection) comingSoonSection.style.display = 'none';
|
|
|
|
newsGrid.innerHTML = news.map(item => {
|
|
const date = new Date(item.created_at);
|
|
const categoryLabels = {
|
|
'news': 'Company News',
|
|
'events': 'Events',
|
|
'insights': 'Industry Insights',
|
|
'updates': 'Product Updates'
|
|
};
|
|
return `
|
|
<div class="news-card" data-aos="fade-up">
|
|
<div class="news-image" style="background-image: url('${escapeAttr(item.featured_image || '/images/logo/logo.png')}'); background-size: cover; background-position: center;">
|
|
<div class="news-date">
|
|
<span class="day">${date.getDate()}</span>
|
|
<span class="month">${date.toLocaleString('default', { month: 'short' })}</span>
|
|
</div>
|
|
</div>
|
|
<div class="news-content">
|
|
<span class="news-category">${escapeHTML(categoryLabels[item.category] || item.category || 'News')}</span>
|
|
<h3 class="news-title">${escapeHTML(item.title)}</h3>
|
|
<p class="news-excerpt">${escapeHTML(item.excerpt || (item.content ? item.content.substring(0, 120) + '...' : 'Read more about this topic.'))}</p>
|
|
<a href="news.html?id=${escapeAttr(item.id)}" class="news-link">Read More <i class="fas fa-arrow-right"></i></a>
|
|
</div>
|
|
</div>
|
|
`}).join('');
|
|
} else {
|
|
// No news found - show coming soon
|
|
if (newsSection) newsSection.style.display = 'none';
|
|
if (comingSoonSection) comingSoonSection.style.display = 'block';
|
|
// Clear spinner on pages that have no coming-soon section (e.g. homepage)
|
|
if (!newsSection) {
|
|
newsGrid.innerHTML = '';
|
|
const parentSection = newsGrid.closest('section');
|
|
if (parentSection) parentSection.style.display = 'none';
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.log('Error loading news:', error);
|
|
// Fallback to showing coming soon on error
|
|
if (newsSection) newsSection.style.display = 'none';
|
|
if (comingSoonSection) comingSoonSection.style.display = 'block';
|
|
if (!newsSection) {
|
|
newsGrid.innerHTML = '';
|
|
const parentSection = newsGrid.closest('section');
|
|
if (parentSection) parentSection.style.display = 'none';
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ==================== Load Testimonials from API ==================== */
|
|
async function loadTestimonials() {
|
|
const slider = document.getElementById('testimonials-slider');
|
|
|
|
if (!slider) return;
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/testimonials.php?status=published`);
|
|
const result = await response.json();
|
|
|
|
const testimonials = result.data || result;
|
|
|
|
if (testimonials && testimonials.length > 0) {
|
|
slider.innerHTML = testimonials.map((item, index) => {
|
|
const name = item.name || 'Anonymous';
|
|
const initials = name.split(' ').map(n => n[0]).join('');
|
|
const text = item.text || item.content || '';
|
|
return `
|
|
<div class="testimonial-card ${index === 0 ? 'active' : ''}">
|
|
<div class="testimonial-content">
|
|
<div class="quote-icon"><i class="fas fa-quote-left"></i></div>
|
|
<p class="testimonial-text">"${escapeHTML(text)}"</p>
|
|
<div class="testimonial-author">
|
|
${item.photo
|
|
? `<img class="author-avatar" src="${escapeAttr(item.photo)}" alt="${escapeAttr(name)}" width="50" height="50" loading="lazy" decoding="async" style="width: 50px; height: 50px; border-radius: 50%; object-fit: cover;">`
|
|
: `<div class="author-avatar">${escapeHTML(initials)}</div>`
|
|
}
|
|
<div class="author-info">
|
|
<h4>${escapeHTML(name)}</h4>
|
|
<p>${escapeHTML(item.position || '')}${item.position && item.company ? ', ' : ''}${escapeHTML(item.company || '')}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`}).join('');
|
|
|
|
// Reinitialize testimonials slider
|
|
initTestimonials();
|
|
}
|
|
} catch (error) {
|
|
console.log('Using static testimonials content or API unavailable');
|
|
}
|
|
}
|
|
|
|
/* ==================== Load Portfolio from API ==================== */
|
|
async function loadPortfolio(limit = 6) {
|
|
const portfolioGrid = document.getElementById('portfolio-grid');
|
|
const portfolioSection = document.getElementById('portfolio-section');
|
|
const comingSoonSection = document.getElementById('coming-soon-section');
|
|
|
|
// Only proceed if we're on the portfolio page (or homepage if it has portfolio section)
|
|
if (!portfolioGrid) return;
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/portfolio.php?limit=${limit}`);
|
|
const result = await response.json();
|
|
|
|
const projects = result.data || result;
|
|
|
|
if (projects && projects.length > 0) {
|
|
// Show portfolio, hide coming soon
|
|
if (portfolioSection) portfolioSection.style.display = 'block';
|
|
if (comingSoonSection) comingSoonSection.style.display = 'none';
|
|
|
|
portfolioGrid.innerHTML = projects.map(item => {
|
|
const categoryLabels = {
|
|
'it-support': 'IT Support',
|
|
'cybersecurity': 'Cybersecurity',
|
|
'cloud': 'Cloud Services',
|
|
'consulting': 'Consulting'
|
|
};
|
|
return `
|
|
<div class="portfolio-card" data-category="${escapeAttr(item.category || 'all')}" data-aos="fade-up">
|
|
<div class="portfolio-image">
|
|
<img src="${escapeAttr(item.image || '/images/logo/logo.png')}" alt="${escapeAttr(item.title)}" width="800" height="600" loading="lazy" decoding="async">
|
|
<div class="portfolio-overlay">
|
|
<div class="portfolio-info">
|
|
<span class="portfolio-category">${escapeHTML(categoryLabels[item.category] || item.category || 'Project')}</span>
|
|
<h3 class="portfolio-title">${escapeHTML(item.title)}</h3>
|
|
${item.client ? `<p class="portfolio-client">Client: ${escapeHTML(item.client)}</p>` : ''}
|
|
<a href="#" class="portfolio-link" data-id="${escapeAttr(item.id)}">View Details <i class="fas fa-arrow-right"></i></a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`}).join('');
|
|
} else {
|
|
// No projects found - show coming soon
|
|
if (portfolioSection) portfolioSection.style.display = 'none';
|
|
if (comingSoonSection) comingSoonSection.style.display = 'block';
|
|
}
|
|
} catch (error) {
|
|
console.log('Error loading portfolio:', error);
|
|
// Fallback to showing coming soon on error
|
|
if (portfolioSection) portfolioSection.style.display = 'none';
|
|
if (comingSoonSection) comingSoonSection.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
/* ==================== Load Services from API ==================== */
|
|
async function loadServices() {
|
|
const servicesGrids = document.querySelectorAll('[data-services-grid]');
|
|
|
|
if (!servicesGrids.length) return;
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/services.php`);
|
|
const result = await response.json();
|
|
const services = result.data || result;
|
|
|
|
if (services && services.length > 0) {
|
|
const servicesMarkup = services.map((service, index) => {
|
|
const icon = service.icon || 'fa-cogs';
|
|
const features = (service.features || '')
|
|
.split(/\r?\n/)
|
|
.map(item => item.trim())
|
|
.filter(Boolean)
|
|
.slice(0, 3);
|
|
const slug = (service.slug || service.name || 'service')
|
|
.toString()
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, '-')
|
|
.replace(/(^-|-$)/g, '');
|
|
const ctaText = service.cta_text || 'Learn More';
|
|
|
|
return `
|
|
<div class="service-card" id="${escapeAttr(slug)}" data-aos="fade-up" data-aos-delay="${index * 100}">
|
|
<div class="service-icon" data-icon="${escapeAttr(icon)}">
|
|
<i class="fas ${escapeAttr(icon)}"></i>
|
|
</div>
|
|
<h3 class="service-title">${escapeHTML(service.name || 'Service')}</h3>
|
|
<p class="service-description">${escapeHTML(service.description || '')}</p>
|
|
${features.length ? `
|
|
<ul class="service-features">
|
|
${features.map(f => `<li><i class="fas fa-check"></i> ${escapeHTML(f)}</li>`).join('')}
|
|
</ul>
|
|
` : ''}
|
|
<a href="contact.html?service=${escapeAttr(slug)}" class="service-link">${escapeHTML(ctaText)} <i class="fas fa-arrow-right"></i></a>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
servicesGrids.forEach(grid => {
|
|
grid.innerHTML = servicesMarkup;
|
|
});
|
|
|
|
initReplaceableIcons();
|
|
} else {
|
|
// No API data returned — static fallback cards already rendered in HTML
|
|
}
|
|
} catch (error) {
|
|
// Static fallback cards already rendered in HTML — do not hide the section
|
|
console.log('Using static services content or API unavailable');
|
|
}
|
|
}
|
|
|
|
/* ==================== Load Team from API ==================== */
|
|
async function loadTeam() {
|
|
const teamGrid = document.querySelector('[data-team-grid]');
|
|
const teamSection = teamGrid ? teamGrid.closest('.team-section') : null;
|
|
|
|
if (!teamGrid) return;
|
|
|
|
// Keep hidden until admin-managed content exists
|
|
if (teamSection) {
|
|
teamSection.hidden = true;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/team.php`);
|
|
const result = await response.json();
|
|
const members = result.data || result;
|
|
|
|
if (members && members.length > 0) {
|
|
teamGrid.innerHTML = members.map(member => {
|
|
let photo = member.photo || `https://ui-avatars.com/api/?name=${encodeURIComponent(member.name || 'Team')}&background=0ea5e9&color=fff`;
|
|
// Ensure uploaded photos have a leading slash for absolute path
|
|
if (photo && !photo.startsWith('http') && !photo.startsWith('/')) {
|
|
photo = '/' + photo;
|
|
}
|
|
const bioHtml = member.bio
|
|
? `<small style="font-size:0.78rem;color:#64748b;line-height:1.5;margin-top:0.4rem;display:block;">${escapeHTML(member.bio)}</small>`
|
|
: '';
|
|
return `
|
|
<div class="team-card">
|
|
<div class="team-image">
|
|
<img src="${escapeAttr(photo)}" alt="${escapeAttr(member.name || 'Team Member')}" width="400" height="400" loading="lazy" decoding="async">
|
|
<div class="team-social">
|
|
${member.linkedin ? `<a href="${escapeAttr(member.linkedin)}" aria-label="LinkedIn" target="_blank" rel="noopener noreferrer"><i class="fab fa-linkedin-in"></i></a>` : ''}
|
|
${member.twitter ? `<a href="${escapeAttr(member.twitter)}" aria-label="Twitter" target="_blank" rel="noopener noreferrer"><i class="fab fa-twitter"></i></a>` : ''}
|
|
${member.github ? `<a href="${escapeAttr(member.github)}" aria-label="GitHub" target="_blank" rel="noopener noreferrer"><i class="fab fa-github"></i></a>` : ''}
|
|
${member.email ? `<a href="mailto:${escapeAttr(member.email)}" aria-label="Email"><i class="fas fa-envelope"></i></a>` : ''}
|
|
</div>
|
|
</div>
|
|
<div class="team-info">
|
|
<h4>${escapeHTML(member.name || 'Team Member')}</h4>
|
|
<p>${escapeHTML(member.role || '')}</p>
|
|
${bioHtml}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
if (teamSection) {
|
|
teamSection.hidden = false;
|
|
}
|
|
} else {
|
|
teamGrid.innerHTML = '';
|
|
}
|
|
} catch (error) {
|
|
teamGrid.innerHTML = '';
|
|
console.log('Using static team content or API unavailable');
|
|
}
|
|
}
|
|
|
|
/* ==================== Newsletter Form ==================== */
|
|
function initNewsletterForm() {
|
|
const newsletterForms = document.querySelectorAll('#newsletter-form, .newsletter-form');
|
|
|
|
newsletterForms.forEach(form => {
|
|
if (form.dataset.initialized) return;
|
|
form.dataset.initialized = 'true';
|
|
|
|
form.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
|
|
const emailInput = form.querySelector('input[type="email"]');
|
|
const submitBtn = form.querySelector('button[type="submit"]');
|
|
const email = emailInput.value;
|
|
|
|
if (!email) return;
|
|
|
|
const originalText = submitBtn.innerHTML;
|
|
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
|
submitBtn.disabled = true;
|
|
|
|
try {
|
|
// Subscribe via dedicated subscribe API
|
|
const response = await fetch(`${API_BASE_URL}/subscribe.php`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
email: email,
|
|
name: '',
|
|
source: 'newsletter_form'
|
|
})
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
showNotification('Thank you for subscribing!', 'success');
|
|
form.reset();
|
|
} else {
|
|
showNotification(result.message || 'Subscription failed. Please try again.', 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Newsletter error:', error);
|
|
showNotification('An error occurred. Please try again.', 'error');
|
|
} finally {
|
|
submitBtn.innerHTML = originalText;
|
|
submitBtn.disabled = false;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Initialize dynamic content on first DOMContentLoaded (no duplicate listener needed)
|
|
|
|
/* ==================== FAQ Accordion ==================== */
|
|
function initFAQ() {
|
|
const faqItems = document.querySelectorAll('.faq-item');
|
|
if (!faqItems.length) return;
|
|
|
|
faqItems.forEach(item => {
|
|
const question = item.querySelector('.faq-question');
|
|
if (!question) return;
|
|
|
|
question.addEventListener('click', () => {
|
|
const isActive = item.classList.contains('active');
|
|
|
|
// Close all open items
|
|
faqItems.forEach(i => {
|
|
i.classList.remove('active');
|
|
const icon = i.querySelector('.faq-toggle i');
|
|
if (icon) icon.className = 'fas fa-plus';
|
|
});
|
|
|
|
// Open clicked item if it was closed
|
|
if (!isActive) {
|
|
item.classList.add('active');
|
|
const icon = item.querySelector('.faq-toggle i');
|
|
if (icon) icon.className = 'fas fa-minus';
|
|
}
|
|
});
|
|
});
|
|
}
|