main-01.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896
  1. // Initialize particles.js
  2. if (typeof particlesJS !== 'undefined' && document.getElementById('particles-js')) {
  3. particlesJS('particles-js', particlesConfig);
  4. }
  5. // Load SVG icon sprites
  6. (function loadIconSprite() {
  7. const script = document.createElement('script');
  8. script.src = 'assets/data/icons-sprite.js';
  9. script.async = false;
  10. document.head.appendChild(script);
  11. })();
  12. // Initialize all components when DOM loads
  13. document.addEventListener('DOMContentLoaded', async function() {
  14. try {
  15. // Load services data for Typed.js
  16. if (typeof Typed !== 'undefined' && document.getElementById('typed-element')) {
  17. try {
  18. // Initialize Typed.js
  19. const typed = new Typed('#typed-element', {
  20. strings: servicesData.services,
  21. typeSpeed: servicesData.typewriterConfig.typeSpeed,
  22. backSpeed: servicesData.typewriterConfig.deleteSpeed,
  23. startDelay: 1500,
  24. backDelay: servicesData.typewriterConfig.deleteDelay,
  25. loop: servicesData.typewriterConfig.loop,
  26. showCursor: servicesData.typewriterConfig.showCursor,
  27. cursorChar: servicesData.typewriterConfig.cursorChar,
  28. autoInsertCss: true,
  29. });
  30. window.serviceTyped = typed;
  31. } catch (error) {
  32. // Fallback with default strings
  33. const typed = new Typed('#typed-element', {
  34. strings: ['Web Development', 'Digital Marketing', 'SEO Optimization'],
  35. typeSpeed: 50,
  36. backSpeed: 30,
  37. backDelay: 1500,
  38. loop: true,
  39. showCursor: true,
  40. cursorChar: '|'
  41. });
  42. window.serviceTyped = typed;
  43. }
  44. }
  45. // Initialize Portfolio
  46. initPortfolioIsotope();
  47. // Refresh AOS
  48. setTimeout(() => {
  49. if (typeof AOS !== 'undefined') {
  50. AOS.refresh();
  51. }
  52. }, 100);
  53. } catch (error) {
  54. // Initialize AOS as fallback
  55. try {
  56. if (typeof AOS !== 'undefined') {
  57. AOS.init({
  58. duration: 800,
  59. delay: 100,
  60. once: false,
  61. mirror: true,
  62. offset: 50,
  63. easing: 'ease-out-cubic'
  64. });
  65. }
  66. } catch (aosError) {
  67. // Silent fail
  68. }
  69. }
  70. });
  71. /**
  72. * Enhanced portfolio initialization function
  73. */
  74. function initPortfolioIsotope() {
  75. const portfolioGrid = document.querySelector('.portfolio-grid');
  76. const filterButtons = document.querySelectorAll('.filter-btn');
  77. if (!portfolioGrid) {
  78. return;
  79. }
  80. if (filterButtons.length === 0) {
  81. console.warn('⚠️ No filter buttons found');
  82. return;
  83. }
  84. // Wait for images to load
  85. waitForImages(portfolioGrid).then(() => {
  86. // Create CustomIsotope instance
  87. const customIsotope = new CustomIsotope(portfolioGrid, {
  88. itemSelector: '.portfolio-item',
  89. gutter: 24, // Match your CSS gap
  90. transitionDuration: 500,
  91. hiddenStyle: {
  92. opacity: 0,
  93. transform: 'scale(0.9) translateY(20px)'
  94. },
  95. visibleStyle: {
  96. opacity: 1,
  97. transform: 'scale(1) translateY(0px)'
  98. }
  99. });
  100. // Store instance globally
  101. window.portfolioIsotope = customIsotope;
  102. window.customIsotope = customIsotope; // Additional alias
  103. // Setup filter buttons
  104. setupFilterButtons(customIsotope, filterButtons);
  105. // Refresh AOS after initial layout
  106. setTimeout(() => {
  107. if (typeof AOS !== 'undefined') {
  108. AOS.refresh();
  109. }
  110. }, 1000);
  111. }).catch(error => {
  112. console.error('❌ Error loading images:', error);
  113. // Initialize anyway after timeout
  114. setTimeout(() => {
  115. const customIsotope = new CustomIsotope(portfolioGrid);
  116. window.portfolioIsotope = customIsotope;
  117. setupFilterButtons(customIsotope, filterButtons);
  118. }, 1000);
  119. });
  120. }
  121. // Typed.js control functions
  122. function pauseTyping() {
  123. if (window.serviceTyped) {
  124. window.serviceTyped.stop();
  125. }
  126. }
  127. function resumeTyping() {
  128. if (window.serviceTyped) {
  129. window.serviceTyped.start();
  130. }
  131. }
  132. function resetTyping() {
  133. if (window.serviceTyped) {
  134. window.serviceTyped.reset();
  135. }
  136. }
  137. // AOS control functions
  138. function refreshAOS() {
  139. if (typeof AOS !== 'undefined') {
  140. AOS.refresh();
  141. }
  142. }
  143. function disableAOS() {
  144. if (typeof AOS !== 'undefined') {
  145. AOS.init({ disable: true });
  146. }
  147. }
  148. function enableAOS() {
  149. if (typeof AOS !== 'undefined') {
  150. AOS.init({
  151. duration: 800,
  152. delay: 100,
  153. once: false,
  154. mirror: true,
  155. offset: 50,
  156. easing: 'ease-out-cubic'
  157. });
  158. }
  159. }
  160. // Handle page visibility changes
  161. document.addEventListener('visibilitychange', function() {
  162. if (document.hidden) {
  163. pauseTyping();
  164. } else {
  165. resumeTyping();
  166. refreshAOS();
  167. }
  168. });
  169. // Expose utility functions globally
  170. window.AOSUtils = {
  171. refresh: refreshAOS,
  172. disable: disableAOS,
  173. enable: enableAOS
  174. };
  175. window.TypingUtils = {
  176. pause: pauseTyping,
  177. resume: resumeTyping,
  178. reset: resetTyping
  179. };
  180. // ==========================================================================
  181. // RESPONSIVE HEADER
  182. // ==========================================================================
  183. function initResponsiveHeader() {
  184. const header = document.querySelector('.header');
  185. const mobileMenuToggle = document.getElementById('mobile-menu-toggle');
  186. const navbar = document.getElementById('navbar');
  187. const body = document.body;
  188. const dropdownToggles = document.querySelectorAll('.dropdown-toggle');
  189. const dropdownMenus = document.querySelectorAll('.dropdown-menu');
  190. if (!header || !mobileMenuToggle || !navbar) return;
  191. // Funciones del menú móvil
  192. function openMobileMenu() {
  193. navbar.classList.add('mobile-menu-open');
  194. mobileMenuToggle.classList.add('active');
  195. body.style.overflow = 'hidden';
  196. }
  197. function closeMobileMenu() {
  198. navbar.classList.remove('mobile-menu-open');
  199. mobileMenuToggle.classList.remove('active');
  200. body.style.overflow = '';
  201. closeAllDropdowns();
  202. }
  203. function toggleMobileMenu() {
  204. if (navbar.classList.contains('mobile-menu-open')) {
  205. closeMobileMenu();
  206. } else {
  207. openMobileMenu();
  208. }
  209. }
  210. // Event listeners del menú móvil
  211. mobileMenuToggle.addEventListener('click', function(e) {
  212. e.preventDefault();
  213. e.stopPropagation();
  214. toggleMobileMenu();
  215. });
  216. document.addEventListener('click', function(e) {
  217. const isClickInsideMenu = navbar.contains(e.target);
  218. const isClickOnToggle = mobileMenuToggle.contains(e.target);
  219. if (!isClickInsideMenu && !isClickOnToggle && navbar.classList.contains('mobile-menu-open')) {
  220. closeMobileMenu();
  221. }
  222. });
  223. const navLinks = document.querySelectorAll('.nav-link');
  224. navLinks.forEach(link => {
  225. link.addEventListener('click', function() {
  226. if (window.innerWidth <= 992) {
  227. closeMobileMenu();
  228. }
  229. });
  230. });
  231. function closeAllDropdowns() {
  232. dropdownMenus.forEach(menu => {
  233. menu.classList.remove('dropdown-open');
  234. });
  235. dropdownToggles.forEach(toggle => {
  236. toggle.classList.remove('active');
  237. });
  238. }
  239. function toggleDropdown(dropdownId, toggleElement) {
  240. const dropdownMenu = document.getElementById(dropdownId);
  241. if (!dropdownMenu) return;
  242. const isOpen = dropdownMenu.classList.contains('dropdown-open');
  243. closeAllDropdowns();
  244. if (!isOpen) {
  245. dropdownMenu.classList.add('dropdown-open');
  246. toggleElement.classList.add('active');
  247. }
  248. }
  249. // Event listeners de dropdown
  250. dropdownToggles.forEach(toggle => {
  251. toggle.addEventListener('click', function(e) {
  252. e.preventDefault();
  253. e.stopPropagation();
  254. const dropdownId = this.getAttribute('data-dropdown');
  255. toggleDropdown(dropdownId, this);
  256. });
  257. });
  258. document.addEventListener('click', function(e) {
  259. if (window.innerWidth <= 992) {
  260. const isClickInsideDropdown = Array.from(dropdownMenus).some(menu =>
  261. menu.contains(e.target)
  262. );
  263. const isClickOnToggle = Array.from(dropdownToggles).some(toggle =>
  264. toggle.contains(e.target)
  265. );
  266. if (!isClickInsideDropdown && !isClickOnToggle) {
  267. closeAllDropdowns();
  268. }
  269. }
  270. });
  271. const dropdownItems = document.querySelectorAll('.dropdown-item');
  272. dropdownItems.forEach(item => {
  273. item.addEventListener('click', function() {
  274. if (window.innerWidth <= 992) {
  275. closeAllDropdowns();
  276. closeMobileMenu();
  277. }
  278. });
  279. });
  280. function handleHeaderScroll() {
  281. if (window.scrollY > 100) {
  282. header.classList.add('header-scrolled');
  283. header.classList.remove('header-transparent');
  284. //修改logo
  285. var that= document.getElementById("lmdlogo");
  286. that.src="assets/images/lmdlogo/lmd22.png";
  287. } else {
  288. header.classList.remove('header-scrolled');
  289. header.classList.add('header-transparent');
  290. var that= document.getElementById("lmdlogo");
  291. that.src="assets/images/lmdlogo/lmd2.png";
  292. }
  293. }
  294. if (window.scrollY > 100) {
  295. header.classList.add('header-scrolled');
  296. } else {
  297. header.classList.add('header-transparent');
  298. }
  299. window.addEventListener('scroll', handleHeaderScroll);
  300. function handleWindowResize() {
  301. if (window.innerWidth > 992) {
  302. closeMobileMenu();
  303. closeAllDropdowns();
  304. }
  305. }
  306. window.addEventListener('resize', handleWindowResize);
  307. document.addEventListener('keydown', function(e) {
  308. if (e.key === 'Escape') {
  309. if (navbar.classList.contains('mobile-menu-open')) {
  310. closeMobileMenu();
  311. }
  312. closeAllDropdowns();
  313. }
  314. });
  315. }
  316. initResponsiveHeader();
  317. // ==========================================================================
  318. // CUSTOMERS CAROUSEL
  319. // ==========================================================================
  320. document.addEventListener('DOMContentLoaded', function() {
  321. if (typeof UniversalSlider !== 'undefined' && document.querySelector('.customers-carousel2')) {
  322. const customersSlider = new UniversalSlider('.customers-carousel2', {
  323. slidesPerView: {
  324. desktop: 3,
  325. tablet: 3,
  326. mobile: 2
  327. },
  328. slideBy: 1,
  329. breakpoints: {
  330. mobile: 576,
  331. tablet: 992
  332. },
  333. autoplay: true,
  334. autoplayDelay: 3000,
  335. pagination: false,
  336. arrows: false,
  337. loop: true,
  338. touchEnabled: true,
  339. });
  340. }
  341. });
  342. // ==========================================================================
  343. // NEWSLETTER FORM SUBMISSION
  344. // ==========================================================================
  345. const newsletterForm = document.getElementById('newsletterForm');
  346. if (newsletterForm) {
  347. newsletterForm.addEventListener('submit', function(e) {
  348. e.preventDefault();
  349. const checkbox = this.querySelector('input[name="newsletter-agreement"]');
  350. if (checkbox && !checkbox.checked) {
  351. alert('Please agree to the Privacy Policy');
  352. return;
  353. }
  354. alert('Thank you for subscribing to our newsletter!');
  355. this.reset();
  356. });
  357. }
  358. // ==========================================================================
  359. // SCROLL TO TOP FUNCTIONALITY
  360. // ==========================================================================
  361. const scrollToTop = document.getElementById('scrollToTop');
  362. if (scrollToTop) {
  363. window.addEventListener('scroll', function() {
  364. if (window.pageYOffset > 300) {
  365. scrollToTop.classList.add('show');
  366. } else {
  367. scrollToTop.classList.remove('show');
  368. }
  369. });
  370. scrollToTop.addEventListener('click', function() {
  371. window.scrollTo({
  372. top: 0,
  373. behavior: 'smooth'
  374. });
  375. });
  376. }
  377. // Smooth scrolling for footer links
  378. document.querySelectorAll('.footer-link[href^="#"]').forEach(link => {
  379. link.addEventListener('click', function(e) {
  380. e.preventDefault();
  381. const target = document.querySelector(this.getAttribute('href'));
  382. if (target) {
  383. target.scrollIntoView({
  384. behavior: 'smooth',
  385. block: 'start'
  386. });
  387. }
  388. });
  389. });
  390. /**
  391. * Page Loader Controller - MultiIT
  392. * Handles the loading screen until all resources are loaded
  393. */
  394. class PageLoader {
  395. constructor() {
  396. this.loader = document.getElementById('page-loader');
  397. this.progressBar = document.querySelector('.progress-bar');
  398. this.isLoaded = false;
  399. this.minLoadTime = 1500; // Minimum loading time in ms
  400. this.maxLoadTime = 8000; // Maximum loading time in ms (fallback)
  401. this.startTime = Date.now();
  402. this.currentProgress = 0;
  403. this.loadingSteps = [];
  404. this.completedSteps = 0;
  405. // Only initialize if loader exists
  406. if (this.loader) {
  407. this.init();
  408. } else {
  409. console.warn('MultiIT Loader: #page-loader element not found');
  410. }
  411. }
  412. init() {
  413. try {
  414. // Add loading class to body
  415. document.body.classList.add('loading');
  416. // Start progress animation
  417. this.animateProgress();
  418. // Wait for page to load completely
  419. this.waitForPageLoad();
  420. // Safety timeout to prevent infinite loading
  421. this.setSafetyTimeout();
  422. } catch (error) {
  423. this.forceHide();
  424. }
  425. }
  426. waitForPageLoad() {
  427. const promises = [];
  428. // Wait for DOM to be ready
  429. if (document.readyState === 'loading') {
  430. promises.push(new Promise(resolve => {
  431. document.addEventListener('DOMContentLoaded', resolve, { once: true });
  432. this.loadingSteps.push('DOM Content Loaded');
  433. }));
  434. }
  435. // Wait for all resources (images, CSS, JS) to load
  436. if (document.readyState !== 'complete') {
  437. promises.push(new Promise(resolve => {
  438. window.addEventListener('load', resolve, { once: true });
  439. this.loadingSteps.push('Window Load');
  440. }));
  441. }
  442. // Wait for all images to load
  443. promises.push(this.waitForImages());
  444. this.loadingSteps.push('Images Loaded');
  445. // Wait for fonts to load (if using web fonts)
  446. if (document.fonts && document.fonts.ready) {
  447. promises.push(document.fonts.ready.catch(() => {
  448. console.warn('MultiIT Loader: Fonts failed to load');
  449. }));
  450. this.loadingSteps.push('Fonts Loaded');
  451. }
  452. // Wait for critical CSS to load
  453. promises.push(this.waitForCriticalAssets());
  454. this.loadingSteps.push('Critical Assets');
  455. // Execute when everything is loaded
  456. Promise.allSettled(promises).then((results) => {
  457. // Log any failed promises
  458. results.forEach((result, index) => {
  459. if (result.status === 'rejected') {
  460. console.warn(`MultiIT Loader: Step "${this.loadingSteps[index]}" failed:`, result.reason);
  461. }
  462. });
  463. this.isLoaded = true;
  464. this.checkMinimumLoadTime();
  465. });
  466. }
  467. waitForImages() {
  468. return new Promise(resolve => {
  469. const images = document.querySelectorAll('img[src], img[data-src]');
  470. if (images.length === 0) {
  471. resolve();
  472. return;
  473. }
  474. let loadedImages = 0;
  475. const totalImages = images.length;
  476. const imagePromises = Array.from(images).map((img, index) => {
  477. return new Promise(imageResolve => {
  478. const handleImageLoad = () => {
  479. loadedImages++;
  480. // Update progress based on image loading
  481. const imageProgress = (loadedImages / totalImages) * 30; // Images contribute 30% to progress
  482. this.updateProgressStep(imageProgress);
  483. imageResolve();
  484. };
  485. if (img.complete && img.naturalHeight !== 0) {
  486. handleImageLoad();
  487. } else {
  488. img.addEventListener('load', handleImageLoad, { once: true });
  489. img.addEventListener('error', () => {
  490. console.warn(`MultiIT Loader: Image failed to load: ${img.src || img.dataset.src}`);
  491. handleImageLoad(); // Continue even if image fails
  492. }, { once: true });
  493. // Timeout fallback for stuck images
  494. setTimeout(handleImageLoad, 3000);
  495. }
  496. });
  497. });
  498. Promise.all(imagePromises).then(resolve);
  499. });
  500. }
  501. waitForCriticalAssets() {
  502. return new Promise(resolve => {
  503. // Check for critical CSS files
  504. const criticalStylesheets = document.querySelectorAll('link[rel="stylesheet"][href*="critical"], link[rel="stylesheet"][href*="main"]');
  505. if (criticalStylesheets.length === 0) {
  506. resolve();
  507. return;
  508. }
  509. let loadedSheets = 0;
  510. const totalSheets = criticalStylesheets.length;
  511. criticalStylesheets.forEach(sheet => {
  512. if (sheet.sheet) {
  513. loadedSheets++;
  514. if (loadedSheets === totalSheets) resolve();
  515. } else {
  516. sheet.addEventListener('load', () => {
  517. loadedSheets++;
  518. if (loadedSheets === totalSheets) resolve();
  519. }, { once: true });
  520. sheet.addEventListener('error', () => {
  521. console.warn('MultiIT Loader: Critical stylesheet failed to load:', sheet.href);
  522. loadedSheets++;
  523. if (loadedSheets === totalSheets) resolve();
  524. }, { once: true });
  525. }
  526. });
  527. // Timeout fallback
  528. setTimeout(resolve, 2000);
  529. });
  530. }
  531. setSafetyTimeout() {
  532. setTimeout(() => {
  533. if (!this.isLoaded) {
  534. this.isLoaded = true;
  535. this.checkMinimumLoadTime();
  536. }
  537. }, this.maxLoadTime);
  538. }
  539. checkMinimumLoadTime() {
  540. const elapsed = Date.now() - this.startTime;
  541. const remaining = Math.max(0, this.minLoadTime - elapsed);
  542. // Complete progress bar
  543. this.setProgress(100);
  544. setTimeout(() => {
  545. this.hideLoader();
  546. }, remaining);
  547. }
  548. animateProgress() {
  549. const progressInterval = setInterval(() => {
  550. if (this.isLoaded) {
  551. this.setProgress(100);
  552. clearInterval(progressInterval);
  553. return;
  554. }
  555. // More realistic progress simulation
  556. const increment = Math.random() * 8 + 2; // 2-10% increments
  557. this.currentProgress = Math.min(this.currentProgress + increment, 85);
  558. this.setProgress(this.currentProgress);
  559. if (this.currentProgress >= 85) {
  560. clearInterval(progressInterval);
  561. }
  562. }, 300);
  563. }
  564. updateProgressStep(stepProgress) {
  565. // Update progress based on completed steps
  566. const baseProgress = (this.completedSteps / this.loadingSteps.length) * 70;
  567. this.currentProgress = Math.min(baseProgress + stepProgress, 90);
  568. this.setProgress(this.currentProgress);
  569. }
  570. setProgress(percentage) {
  571. if (this.progressBar) {
  572. // Smooth progress animation
  573. this.progressBar.style.width = `${Math.min(100, Math.max(0, percentage))}%`;
  574. }
  575. }
  576. hideLoader() {
  577. if (!this.loader) return;
  578. const loadTime = Date.now() - this.startTime;
  579. // Add loaded class for animation
  580. this.loader.classList.add('loaded');
  581. document.body.classList.remove('loading');
  582. document.body.classList.add('loaded');
  583. // Remove loader from DOM after animation
  584. setTimeout(() => {
  585. if (this.loader && this.loader.parentNode) {
  586. this.loader.parentNode.removeChild(this.loader);
  587. }
  588. }, 800);
  589. // Trigger custom event for other scripts
  590. this.dispatchLoadedEvent(loadTime);
  591. }
  592. dispatchLoadedEvent(loadTime) {
  593. try {
  594. const event = new CustomEvent('pageLoaded', {
  595. detail: {
  596. loadTime,
  597. timestamp: Date.now(),
  598. userAgent: navigator.userAgent,
  599. connection: navigator.connection ? {
  600. effectiveType: navigator.connection.effectiveType,
  601. downlink: navigator.connection.downlink
  602. } : null
  603. }
  604. });
  605. window.dispatchEvent(event);
  606. } catch (error) {
  607. console.warn('MultiIT Loader: Failed to dispatch pageLoaded event', error);
  608. }
  609. }
  610. // Public method to force hide loader
  611. forceHide() {
  612. this.isLoaded = true;
  613. this.hideLoader();
  614. }
  615. // Public method to show loader (for dynamic content loading)
  616. show() {
  617. if (this.loader) {
  618. this.loader.classList.remove('loaded');
  619. document.body.classList.add('loading');
  620. document.body.classList.remove('loaded');
  621. this.currentProgress = 0;
  622. this.setProgress(0);
  623. }
  624. }
  625. // Public method to update progress manually
  626. updateProgress(percentage) {
  627. this.setProgress(percentage);
  628. }
  629. }
  630. /* ========================================
  631. INITIALIZATION
  632. ======================================== */
  633. let pageLoader;
  634. function initializePageLoader() {
  635. try {
  636. pageLoader = new PageLoader();
  637. } catch (error) {
  638. console.error('MultiIT Loader: Failed to initialize', error);
  639. }
  640. }
  641. // Start immediately if DOM is already ready, otherwise wait
  642. if (document.readyState === 'loading') {
  643. document.addEventListener('DOMContentLoaded', initializePageLoader, { once: true });
  644. } else {
  645. initializePageLoader();
  646. }
  647. /* ========================================
  648. GLOBAL LOADER API
  649. ======================================== */
  650. // Enhanced global API
  651. window.MultiITLoader = {
  652. hide: () => pageLoader?.forceHide(),
  653. show: () => pageLoader?.show(),
  654. setProgress: (percentage) => pageLoader?.updateProgress(percentage),
  655. isLoaded: () => pageLoader?.isLoaded || false,
  656. getInstance: () => pageLoader
  657. };
  658. /* ========================================
  659. EVENT LISTENERS
  660. ======================================== */
  661. // Listen for page loaded event with enhanced logging
  662. window.addEventListener('pageLoaded', (event) => {
  663. const { loadTime, connection } = event.detail;
  664. const connectionInfo = connection ? `(${connection.effectiveType}, ${connection.downlink}Mbps)` : '';
  665. });
  666. // ✅ Inicializar AOS cuando el loader termine
  667. window.addEventListener('pageLoaded', () => {
  668. if (typeof AOS !== 'undefined') {
  669. AOS.init({
  670. duration: 800,
  671. delay: 100,
  672. once: false,
  673. mirror: true,
  674. offset: 50,
  675. easing: 'ease-out-cubic'
  676. });
  677. }
  678. });
  679. // Handle page visibility change (when user switches tabs)
  680. document.addEventListener('visibilitychange', () => {
  681. if (document.visibilityState === 'visible' && pageLoader) {
  682. // Page became visible again, ensure loader behavior is correct
  683. setTimeout(() => {
  684. if (pageLoader && document.readyState === 'complete') {
  685. pageLoader.forceHide();
  686. }
  687. }, 100);
  688. }
  689. });
  690. // Handle network connection changes (if supported)
  691. if (navigator.connection) {
  692. navigator.connection.addEventListener('change', () => {
  693. if (pageLoader && !pageLoader.isLoaded) {
  694. }
  695. });
  696. }
  697. // Handle errors globally
  698. window.addEventListener('error', (event) => {
  699. return;
  700. });
  701. // Performance timing
  702. window.addEventListener('load', () => {
  703. if (window.performance) {
  704. try {
  705. // Método moderno con Navigation Timing API Level 2
  706. if (performance.getEntriesByType && performance.getEntriesByType('navigation').length > 0) {
  707. const navigationEntry = performance.getEntriesByType('navigation')[0];
  708. }
  709. // Fallback para navegadores que no soportan Navigation Timing Level 2
  710. else if (performance.timing) {
  711. // Solo usar si es absolutamente necesario para compatibilidad
  712. const timing = performance.timing;
  713. const loadTime = timing.loadEventEnd - timing.fetchStart;
  714. }
  715. } catch (error) {
  716. console.warn('MultiIt Performance: Could not measure performance timing', error);
  717. }
  718. }
  719. });
  720. // --- THEME SWITCHER LOGIC ---
  721. // Wait for the initial HTML document to be completely loaded and parsed
  722. document.addEventListener('DOMContentLoaded', () => {
  723. // Select the DOM elements
  724. const themeToggleBtn = document.getElementById('theme-toggle');
  725. const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
  726. const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
  727. // Function to update the visibility of the icons
  728. const updateIcons = (isDarkMode) => {
  729. if (isDarkMode) {
  730. // If dark mode is active, show the light mode icon (sun)
  731. themeToggleDarkIcon.classList.add('hidden');
  732. themeToggleLightIcon.classList.remove('hidden');
  733. } else {
  734. // Otherwise, show the dark mode icon (moon)
  735. themeToggleDarkIcon.classList.remove('hidden');
  736. themeToggleLightIcon.classList.add('hidden');
  737. }
  738. };
  739. // Checks for a saved theme, defaulting to light mode.
  740. const applyInitialTheme = () => {
  741. // The new logic: The theme is light by default, and dark ONLY if explicitly saved as 'dark'.
  742. if (localStorage.getItem('theme') === 'dark') {
  743. document.body.classList.add('dark-mode');
  744. updateIcons(true);
  745. } else {
  746. // For all other cases (first visit or theme saved as 'light'), default to light mode.
  747. document.body.classList.remove('dark-mode');
  748. updateIcons(false);
  749. }
  750. };
  751. // Only add the event listener if the toggle button exists on the page
  752. if (themeToggleBtn) {
  753. themeToggleBtn.addEventListener('click', () => {
  754. // Toggle the 'dark-mode' class on the body.
  755. // .toggle() returns true if the class was added, false if removed.
  756. const isDarkMode = document.body.classList.toggle('dark-mode');
  757. // Save the user's preference to localStorage
  758. if (isDarkMode) {
  759. localStorage.setItem('theme', 'dark');
  760. } else {
  761. localStorage.setItem('theme', 'light');
  762. }
  763. // Update the button's icon to reflect the new state
  764. updateIcons(isDarkMode);
  765. });
  766. }
  767. applyInitialTheme();
  768. });