class ThemeManager {
constructor() {
this.themeKey = 'lazyllama-theme';
this.themes = ['auto', 'light', 'dark'];
this.currentTheme = this.getStoredTheme() || 'auto';
this.init();
}
init() {
this.applyTheme(this.currentTheme);
this.setupThemeToggle();
this.setupSystemThemeListener();
this.updateThemeIcon();
this.updateThemeAwareImages();
}
getStoredTheme() {
try {
return localStorage.getItem(this.themeKey);
} catch (e) {
console.warn('LocalStorage not available, using default theme');
return null;
}
}
storeTheme(theme) {
try {
localStorage.setItem(this.themeKey, theme);
} catch (e) {
console.warn('Cannot store theme preference');
}
}
applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
this.currentTheme = theme;
this.storeTheme(theme);
this.updateThemeAwareImages();
}
getNextTheme() {
const currentIndex = this.themes.indexOf(this.currentTheme);
const nextIndex = (currentIndex + 1) % this.themes.length;
return this.themes[nextIndex];
}
getSystemTheme() {
if (typeof window !== 'undefined' && window.matchMedia) {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
return 'light';
}
getEffectiveTheme() {
if (this.currentTheme === 'auto') {
return this.getSystemTheme();
}
return this.currentTheme;
}
updateThemeIcon() {
const themeIcon = document.querySelector('.theme-icon');
const themeToggle = document.querySelector('.theme-toggle');
if (!themeIcon || !themeToggle) return;
const icons = {
auto: '🔄',
light: '☀️',
dark: '🌙'
};
themeIcon.textContent = icons[this.currentTheme];
const tooltips = {
auto: 'Theme: Auto',
light: 'Theme: Light',
dark: 'Theme: Dark'
};
themeToggle.setAttribute('title', tooltips[this.currentTheme]);
themeToggle.setAttribute('aria-label', `Current theme: ${this.currentTheme}`);
this.updateDropdownActiveState();
}
updateDropdownActiveState() {
const themeOptions = document.querySelectorAll('.theme-option');
themeOptions.forEach(option => {
const optionTheme = option.getAttribute('data-theme');
if (optionTheme === this.currentTheme) {
option.classList.add('active');
} else {
option.classList.remove('active');
}
});
const mobileThemeOptions = document.querySelectorAll('.mobile-theme-option');
mobileThemeOptions.forEach(option => {
const optionTheme = option.getAttribute('data-theme');
if (optionTheme === this.currentTheme) {
option.classList.add('active');
} else {
option.classList.remove('active');
}
});
}
setupThemeToggle() {
const themeToggle = document.getElementById('themeToggle');
const themeOptions = document.querySelectorAll('.theme-option');
if (!themeToggle) {
console.warn('Theme toggle button not found');
return;
}
themeOptions.forEach(option => {
option.addEventListener('click', (e) => {
e.stopPropagation();
const selectedTheme = option.getAttribute('data-theme');
this.applyTheme(selectedTheme);
this.updateThemeIcon();
option.style.transform = 'scale(0.95)';
setTimeout(() => {
option.style.transform = '';
}, 150);
});
option.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
option.click();
}
});
});
themeToggle.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
const nextTheme = this.getNextTheme();
this.applyTheme(nextTheme);
this.updateThemeIcon();
}
});
this.setupMobileThemeButtons();
}
setupMobileThemeButtons() {
const mobileThemeOptions = document.querySelectorAll('.mobile-theme-option');
mobileThemeOptions.forEach(option => {
option.addEventListener('click', () => {
const theme = option.getAttribute('data-theme');
this.applyTheme(theme);
this.updateThemeIcon();
});
});
}
setupSystemThemeListener() {
if (typeof window === 'undefined' || !window.matchMedia) return;
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleSystemThemeChange = (e) => {
if (this.currentTheme === 'auto') {
this.updateThemeIcon();
this.updateThemeAwareImages();
this.announceThemeChange();
}
};
if (mediaQuery.addEventListener) {
mediaQuery.addEventListener('change', handleSystemThemeChange);
}
else if (mediaQuery.addListener) {
mediaQuery.addListener(handleSystemThemeChange);
}
}
updateThemeAwareImages() {
const effectiveTheme = this.getEffectiveTheme();
const isDarkMode = effectiveTheme === 'dark';
const themeAwareImages = document.querySelectorAll('img[data-theme-aware]');
themeAwareImages.forEach(img => {
const originalSrc = img.getAttribute('data-original-src') || img.src;
if (!img.hasAttribute('data-original-src')) {
img.setAttribute('data-original-src', originalSrc);
}
if (isDarkMode) {
const darkModeSrc = this.getDarkModePath(originalSrc);
this.checkImageExists(darkModeSrc).then(exists => {
if (exists && this.getEffectiveTheme() === 'dark') {
img.src = darkModeSrc;
}
});
} else {
img.src = originalSrc;
}
});
}
getDarkModePath(path) {
const lastDotIndex = path.lastIndexOf('.');
if (lastDotIndex === -1) return path;
const basePath = path.substring(0, lastDotIndex);
const extension = path.substring(lastDotIndex);
if (basePath.endsWith('-darkmode')) {
return path;
}
return `${basePath}-darkmode${extension}`;
}
checkImageExists(imageSrc) {
if (!this.imageExistsCache) {
this.imageExistsCache = new Map();
}
if (this.imageExistsCache.has(imageSrc)) {
return Promise.resolve(this.imageExistsCache.get(imageSrc));
}
return new Promise((resolve) => {
const img = new Image();
img.onload = () => {
this.imageExistsCache.set(imageSrc, true);
resolve(true);
};
img.onerror = () => {
this.imageExistsCache.set(imageSrc, false);
resolve(false);
};
img.src = imageSrc;
});
}
announceThemeChange() {
const announcement = document.createElement('div');
announcement.setAttribute('aria-live', 'polite');
announcement.setAttribute('aria-atomic', 'true');
announcement.className = 'sr-only';
announcement.style.cssText = `
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
`;
const effectiveTheme = this.getEffectiveTheme();
announcement.textContent = `Theme switched to ${effectiveTheme} mode`;
document.body.appendChild(announcement);
setTimeout(() => {
document.body.removeChild(announcement);
}, 1000);
}
setTheme(theme) {
if (this.themes.includes(theme)) {
this.applyTheme(theme);
this.updateThemeIcon();
} else {
console.warn(`Invalid theme: ${theme}. Available themes:`, this.themes);
}
}
getThemeInfo() {
return {
current: this.currentTheme,
effective: this.getEffectiveTheme(),
available: this.themes,
system: this.getSystemTheme()
};
}
}
class DesignManager {
constructor() {
this.designKey = 'lazyllama-design';
this.designs = ['classic', 'glassmorphism'];
this.currentDesign = 'glassmorphism';
this.init();
}
init() {
this.applyDesign(this.currentDesign);
this.setupDesignToggle();
this.updateDesignIcon();
}
getStoredDesign() {
try {
return localStorage.getItem(this.designKey);
} catch (e) {
console.warn('LocalStorage not available, using default design');
return null;
}
}
storeDesign(design) {
try {
localStorage.setItem(this.designKey, design);
} catch (e) {
console.warn('Cannot store design preference');
}
}
applyDesign(design) {
document.documentElement.setAttribute('data-design', design);
this.currentDesign = design;
this.storeDesign(design);
}
toggleDesign() {
const currentIndex = this.designs.indexOf(this.currentDesign);
const nextIndex = (currentIndex + 1) % this.designs.length;
return this.designs[nextIndex];
}
updateDesignIcon() {
const designIcon = document.querySelector('.design-icon');
const designToggle = document.querySelector('.design-toggle');
if (!designIcon || !designToggle) return;
const icons = {
classic: '✨',
glassmorphism: '🎨'
};
designIcon.textContent = icons[this.currentDesign];
const tooltips = {
classic: 'Switch to Glassmorphism design',
glassmorphism: 'Switch to Classic design'
};
designToggle.setAttribute('title', tooltips[this.currentDesign]);
designToggle.setAttribute('aria-label', tooltips[this.currentDesign]);
}
setupDesignToggle() {
const designToggle = document.getElementById('designToggle');
if (!designToggle) {
console.warn('Design toggle button not found');
return;
}
designToggle.addEventListener('click', () => {
const nextDesign = this.toggleDesign();
this.applyDesign(nextDesign);
this.updateDesignIcon();
this.announceDesignChange();
designToggle.style.transform = 'scale(0.9) rotate(180deg)';
setTimeout(() => {
designToggle.style.transform = '';
}, 300);
});
designToggle.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
designToggle.click();
}
});
this.setupMobileDesignButtons();
}
setupMobileDesignButtons() {
const mobileDesignButtons = document.querySelectorAll('.mobile-menu-subitem[data-design]');
mobileDesignButtons.forEach(button => {
button.addEventListener('click', () => {
const design = button.getAttribute('data-design');
this.applyDesign(design);
this.updateDesignIcon();
this.updateMobileDesignButtons();
this.announceDesignChange();
});
});
this.updateMobileDesignButtons();
}
updateMobileDesignButtons() {
const mobileDesignButtons = document.querySelectorAll('.mobile-menu-subitem[data-design]');
mobileDesignButtons.forEach(button => {
const design = button.getAttribute('data-design');
if (design === this.currentDesign) {
button.classList.add('active');
} else {
button.classList.remove('active');
}
});
}
announceDesignChange() {
const announcement = document.createElement('div');
announcement.setAttribute('aria-live', 'polite');
announcement.setAttribute('aria-atomic', 'true');
announcement.className = 'sr-only';
announcement.style.cssText = `
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
`;
const designName = this.currentDesign === 'glassmorphism' ? 'Glassmorphism' : 'Classic';
announcement.textContent = `Design switched to ${designName}`;
document.body.appendChild(announcement);
setTimeout(() => {
document.body.removeChild(announcement);
}, 1000);
}
setDesign(design) {
if (this.designs.includes(design)) {
this.applyDesign(design);
this.updateDesignIcon();
} else {
console.warn(`Invalid design: ${design}. Available designs:`, this.designs);
}
}
getDesignInfo() {
return {
current: this.currentDesign,
available: this.designs
};
}
}
class SmoothScroll {
constructor() {
this.offset = 80; this.init();
}
init() {
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', (e) => {
e.preventDefault();
const targetId = anchor.getAttribute('href').substring(1);
const targetElement = document.getElementById(targetId);
if (targetElement) {
this.scrollToElement(targetElement);
}
});
});
}
scrollToElement(element) {
const elementPosition = element.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.pageYOffset - this.offset;
window.scrollTo({
top: offsetPosition,
behavior: 'smooth'
});
}
}
class PerformanceEnhancements {
constructor() {
this.init();
}
init() {
this.setupLazyLoading();
this.setupScrollAnimations();
this.preloadResources();
}
setupLazyLoading() {
if ('IntersectionObserver' in window) {
const images = document.querySelectorAll('img[data-src]');
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.removeAttribute('data-src');
imageObserver.unobserve(img);
}
});
});
images.forEach(img => imageObserver.observe(img));
}
}
setupScrollAnimations() {
if ('IntersectionObserver' in window) {
const animatedElements = document.querySelectorAll('.feature-card, .link-card');
const animationObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = '1';
entry.target.style.transform = 'translateY(0)';
}
});
}, {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
});
animatedElements.forEach(el => {
el.style.opacity = '0';
el.style.transform = 'translateY(20px)';
el.style.transition = 'opacity 0.6s ease, transform 0.6s ease';
animationObserver.observe(el);
});
}
}
preloadResources() {
}
}
class MobileMenu {
constructor() {
this.menuToggle = document.getElementById('mobileMenuToggle');
this.navLinks = document.getElementById('navLinks');
this.navLinksItems = document.querySelectorAll('.nav-link');
this.isOpen = false;
this.init();
}
init() {
if (!this.menuToggle || !this.navLinks) {
console.warn('Mobile menu elements not found');
return;
}
this.menuToggle.addEventListener('click', () => this.toggleMenu());
this.navLinksItems.forEach(link => {
link.addEventListener('click', () => {
if (this.isOpen) {
this.closeMenu();
}
});
});
document.addEventListener('click', (e) => {
if (this.isOpen &&
!this.navLinks.contains(e.target) &&
!this.menuToggle.contains(e.target)) {
this.closeMenu();
}
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.isOpen) {
this.closeMenu();
}
});
window.addEventListener('resize', () => {
if (window.innerWidth > 768 && this.isOpen) {
this.closeMenu();
}
});
}
toggleMenu() {
if (this.isOpen) {
this.closeMenu();
} else {
this.openMenu();
}
}
openMenu() {
this.navLinks.classList.add('active');
this.menuToggle.classList.add('active');
this.menuToggle.setAttribute('aria-expanded', 'true');
this.isOpen = true;
document.body.style.overflow = 'hidden';
}
closeMenu() {
this.navLinks.classList.remove('active');
this.menuToggle.classList.remove('active');
this.menuToggle.setAttribute('aria-expanded', 'false');
this.isOpen = false;
document.body.style.overflow = '';
}
}
class CookieConsent {
constructor() {
this.cookieName = 'lazyllama_cookie_consent';
this.cookieDuration = 365; this.banner = document.getElementById('cookieConsent');
this.acceptBtn = document.getElementById('cookieAccept');
this.init();
}
init() {
if (!this.banner || !this.acceptBtn) return;
if (this.hasConsented()) {
this.hideBanner();
} else {
this.showBanner();
}
this.acceptBtn.addEventListener('click', () => this.acceptCookies());
this.acceptBtn.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this.acceptCookies();
}
});
}
hasConsented() {
return this.getCookie(this.cookieName) === 'true';
}
acceptCookies() {
this.setCookie(this.cookieName, 'true', this.cookieDuration);
this.hideBanner();
}
showBanner() {
if (this.banner) {
this.banner.removeAttribute('hidden');
void this.banner.offsetHeight; this.banner.style.display = 'block';
}
}
hideBanner() {
if (this.banner) {
this.banner.style.animation = 'slideDownOut 0.3s ease-in';
setTimeout(() => {
this.banner.setAttribute('hidden', '');
this.banner.style.display = 'none';
}, 300);
}
}
setCookie(name, value, days) {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
const expires = `expires=${date.toUTCString()}`;
document.cookie = `${name}=${value};${expires};path=/;SameSite=Strict`;
}
getCookie(name) {
const nameEQ = name + "=";
const ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
deleteCookie(name) {
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 UTC;path=/;SameSite=Strict`;
console.log(`Cookie ${name} deleted`);
}
}
function fixMobileViewportHeight() {
const setVH = () => {
const vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);
};
setVH();
let resizeTimeout;
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(setVH, 100);
});
window.addEventListener('orientationchange', () => {
setTimeout(setVH, 100);
});
}
document.addEventListener('DOMContentLoaded', () => {
window.themeManager = new ThemeManager();
window.designManager = new DesignManager();
window.cookieConsent = new CookieConsent();
window.mobileMenu = new MobileMenu();
new SmoothScroll();
new PerformanceEnhancements();
fixMobileViewportHeight();
document.body.classList.add('loaded');
});
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
document.body.classList.add('page-hidden');
} else {
document.body.classList.remove('page-hidden');
}
});
if (typeof module !== 'undefined' && module.exports) {
module.exports = { ThemeManager, DesignManager, SmoothScroll, PerformanceEnhancements, CookieConsent, MobileMenu };
}