Files
MSPE/js/main.js
T

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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
/**
* 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
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';
}
});
});
}