From ce054264522db446504187fa124698b261a7e90c Mon Sep 17 00:00:00 2001 From: claude Date: Fri, 29 May 2026 09:12:19 +0300 Subject: [PATCH] refactor: Refactor main.js to use shared utilities (Step 2 of 6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactored main.js from 544 → 266 lines (51% reduction) by: - Removing duplicate functions now in utils/form-helpers.js - Removing duplicate i18n logic now in utils/i18n.js - Removing API loading code now in utils/api.js - Removing cookie consent handlers now in modules/cookie-consent.js Kept only page-specific form handlers: - Contact form submission with reCaptcha - Subscribe form submission with reCaptcha - Language switcher initialization - Footer year and version display All calls now use window.MyAi.* utilities for consistency. Updated index.html to load all utilities before main.js. Co-Authored-By: Claude Haiku 4.5 --- web/wwwroot/index.html | 4 + web/wwwroot/js/main.js | 367 +++++------------------------------------ 2 files changed, 49 insertions(+), 322 deletions(-) diff --git a/web/wwwroot/index.html b/web/wwwroot/index.html index 14b396a..f5eacf0 100644 --- a/web/wwwroot/index.html +++ b/web/wwwroot/index.html @@ -217,6 +217,10 @@ Cookie settings + + + + \ No newline at end of file diff --git a/web/wwwroot/js/main.js b/web/wwwroot/js/main.js index 4004763..4c173a3 100644 --- a/web/wwwroot/js/main.js +++ b/web/wwwroot/js/main.js @@ -1,94 +1,14 @@ /** * MyAi Main Application Logic * - * Core responsibilities: - * - i18n initialization and language switching - * - Header/nav behavior - * - Cookie consent management - * - Contact and subscribe forms (with reCaptcha) - * - API health checks and configuration loading + * Page-specific form handlers for contact and subscribe forms. + * Uses shared utilities from: i18n.js, utils/form-helpers.js, utils/api.js, modules/cookie-consent.js * - * Depends on: jQuery, i18n.js (translation dict) - * Shared utilities exposed on window.MyAi for use by other scripts. + * Depends on: jQuery, i18n.js (translation dict), shared utility modules */ (function ($) { "use strict"; - /* ============================================================ - INITIALIZATION & STATE - ============================================================ */ - - var reCaptchaSiteKey = null; - var gTagManagerId = null; - var CONSENT_KEY = "myai_cookie_consent"; - var LANG_KEY = "myai_lang"; - - window.MyAi = window.MyAi || {}; - - /* ============================================================ - TRANSLATION HELPERS - ============================================================ */ - - /** - * Get current language preference from localStorage or browser detection. - * Defaults to English. - */ - function browserLang() { - return ((navigator.language || navigator.userLanguage || 'en').toLowerCase().indexOf('ro') === 0) ? 'ro' : 'en'; - } - - /** - * Get the active language (localStorage takes precedence). - */ - function currentLang() { - return localStorage.getItem(LANG_KEY) || browserLang(); - } - - /** - * Translate a key into the current language. - * Falls back to English if key not found in the current language. - */ - function t(key) { - var lang = currentLang(); - return (window.MyAi.i18n[lang] && window.MyAi.i18n[lang][key]) || - window.MyAi.i18n.en[key] || key; - } - - // Expose shared translation helpers for other scripts - window.MyAi.currentLang = currentLang; - window.MyAi.t = t; - - /** - * Update all legal page links to point to the active language. - */ - function updateLegalLinks(lang) { - $('[data-legal]').each(function () { - var page = $(this).data('legal'); - $(this).attr('href', '/legal/' + page + '-' + lang + '.html'); - }); - } - - /** - * Apply language across the entire page: - * - Update localStorage and attribute - * - Translate all [data-i18n] and [data-i18n-placeholder] elements - * - Update legal page links - * - Update language button states - */ - function applyLanguage(lang) { - localStorage.setItem(LANG_KEY, lang); - document.documentElement.lang = lang; - updateLegalLinks(lang); - $('[data-i18n]').each(function () { - $(this).text(t($(this).data('i18n'))); - }); - $('[data-i18n-placeholder]').each(function () { - $(this).attr('placeholder', t($(this).data('i18n-placeholder'))); - }); - $('.lang-flag').attr('aria-pressed', 'false'); - $('.lang-flag[data-lang="' + lang + '"]').attr('aria-pressed', 'true'); - } - /* ============================================================ PAGE INITIALIZATION ============================================================ */ @@ -103,10 +23,10 @@ } }); - // Initialize language - applyLanguage(currentLang()); + // Initialize language using shared utility + window.MyAi.applyLanguage(window.MyAi.currentLang()); $('.lang-flag').on('click', function () { - applyLanguage($(this).data('lang')); + window.MyAi.applyLanguage($(this).data('lang')); }); /* ============================================================ @@ -132,74 +52,25 @@ }); /* ============================================================ - SHARED FORM HELPERS + FORM HELPERS (imported from utils/form-helpers.js) ============================================================ */ - - /** - * Display a field error and mark its container with is-invalid class. - * @param {string} errorId - ID of the error element - * @param {string} msg - Error message to display (or empty to clear) - */ - function showFieldError(errorId, msg) { - var $el = $('#' + errorId); - $el.text(msg || ''); - var $parent = $el.parent(); - var $container = $parent.is('label, .consent-inline') ? $parent : $el.prev(); - if (!$container.length || !$container.is('label, .consent-inline, .file-drop, .subscribe-row')) { - $container = $el.closest('form'); - } - $container.toggleClass('is-invalid', !!msg); - } - - /** - * Clear multiple field errors at once. - * @param {string[]} errorIds - Array of error element IDs - */ - function clearFieldErrors(errorIds) { - for (var i = 0; i < errorIds.length; i++) showFieldError(errorIds[i], ''); - } - - /** - * Validate email format with simple regex. - * @param {string} value - Email address to validate - * @returns {boolean} - True if email format is valid - */ - function isValidEmail(value) { - return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(value || '').trim()); - } - - /** - * Extract a user-facing error message from a failed API response. - * For 4xx responses, the server's error field is shown (intentional feedback). - * For 5xx or missing body, a generic i18n fallback is returned. - * @param {object|null} body - Parsed JSON response body - * @param {number} status - HTTP status code - * @param {string} fallbackKey - i18n key for 5xx/unknown errors - * @param {string} [rateLimitKey] - i18n key for 429 responses - * @returns {string} - User-facing error message - */ - function extractApiError(body, status, fallbackKey, rateLimitKey) { - if (status === 429) return t(rateLimitKey || 'form.rateLimited'); - var msg = body && (body.error || body.Error || body.title); - return (status >= 400 && status < 500 && msg) ? msg : t(fallbackKey); - } - - // Expose shared helpers for other scripts - window.MyAi.showFieldError = showFieldError; - window.MyAi.clearFieldErrors = clearFieldErrors; - window.MyAi.isValidEmail = isValidEmail; - window.MyAi.extractApiError = extractApiError; + // showFieldError, clearFieldErrors, isValidEmail, extractApiError + // are now loaded from utils/form-helpers.js and exposed on window.MyAi /* ============================================================ CONTACT FORM ============================================================ */ + /* ============================================================ + CONTACT FORM HANDLER + ============================================================ */ + /** * Display success message and reset form. */ function formSuccess() { $('#contactForm')[0].reset(); - submitMSG(true, t('form.thanks')); + submitMSG(true, window.MyAi.t('form.thanks')); } /** @@ -233,15 +104,15 @@ var name = $('#name').val().trim(); var email = $('#email').val().trim(); var message = $('#message').val().trim(); - clearFieldErrors(['nameError', 'emailError', 'messageError']); + window.MyAi.clearFieldErrors(['nameError', 'emailError', 'messageError']); $('#msgSubmit').removeClass().addClass('form-message').text(''); // Validate inputs var hasError = false; - if (!name) { showFieldError('nameError', t('form.required.name')); hasError = true; } - if (!email) { showFieldError('emailError', t('form.required.email')); hasError = true; } - else if (!isValidEmail(email)) { showFieldError('emailError', t('form.required.emailInvalid')); hasError = true; } - if (!message) { showFieldError('messageError', t('form.required.message')); hasError = true; } + if (!name) { window.MyAi.showFieldError('nameError', window.MyAi.t('form.required.name')); hasError = true; } + if (!email) { window.MyAi.showFieldError('emailError', window.MyAi.t('form.required.email')); hasError = true; } + else if (!window.MyAi.isValidEmail(email)) { window.MyAi.showFieldError('emailError', window.MyAi.t('form.required.emailInvalid')); hasError = true; } + if (!message) { window.MyAi.showFieldError('messageError', window.MyAi.t('form.required.message')); hasError = true; } if (hasError) return; loader.addClass('loader-visible'); @@ -266,9 +137,9 @@ dataType: 'json' }).done(function (resp) { if (resp && resp.ok === true) formSuccess(); - else submitMSG(false, t('form.captchaFailed')); + else submitMSG(false, window.MyAi.t('form.captchaFailed')); }).fail(function (jqXHR) { - submitMSG(false, extractApiError(jqXHR.responseJSON, jqXHR.status, 'form.failed'), jqXHR.status === 429 ? 'warning' : 'danger'); + submitMSG(false, window.MyAi.extractApiError(jqXHR.responseJSON, jqXHR.status, 'form.failed'), jqXHR.status === 429 ? 'warning' : 'danger'); formError(); }).always(function () { loader.removeClass('loader-visible'); @@ -276,12 +147,12 @@ }); } - if (window.grecaptcha && reCaptchaSiteKey) { + if (window.grecaptcha && window.MyAi.getReCaptchaSiteKey()) { grecaptcha.ready(function () { - grecaptcha.execute(reCaptchaSiteKey, { action: 'contact' }).then(postContact); + grecaptcha.execute(window.MyAi.getReCaptchaSiteKey(), { action: 'contact' }).then(postContact); }); } else { - submitMSG(false, t('form.captchaFailed')); + submitMSG(false, window.MyAi.t('form.captchaFailed')); formError(); loader.removeClass('loader-visible'); button.prop('disabled', false); @@ -289,7 +160,7 @@ }); /* ============================================================ - SUBSCRIBE FORM + SUBSCRIBE FORM HANDLER ============================================================ */ /** @@ -307,23 +178,23 @@ * Update subscribe form message. */ function setMsg(severity, key) { - $msg.removeClass().addClass('form-message text-' + severity).text(t(key)); + $msg.removeClass().addClass('form-message text-' + severity).text(window.MyAi.t(key)); } - clearFieldErrors(['subscribeEmailError', 'subscribeConsentError']); + window.MyAi.clearFieldErrors(['subscribeEmailError', 'subscribeConsentError']); $msg.removeClass().addClass('form-message').text(''); // Validate inputs var hasError = false; if (!email) { - showFieldError('subscribeEmailError', t('form.required.email')); + window.MyAi.showFieldError('subscribeEmailError', window.MyAi.t('form.required.email')); hasError = true; - } else if (!isValidEmail(email)) { - showFieldError('subscribeEmailError', t('form.required.emailInvalid')); + } else if (!window.MyAi.isValidEmail(email)) { + window.MyAi.showFieldError('subscribeEmailError', window.MyAi.t('form.required.emailInvalid')); hasError = true; } if (!consent) { - showFieldError('subscribeConsentError', t('subscribe.noConsent')); + window.MyAi.showFieldError('subscribeConsentError', window.MyAi.t('subscribe.noConsent')); hasError = true; } if (hasError) return; @@ -350,16 +221,16 @@ } }).fail(function (jqXHR) { $msg.removeClass().addClass('form-message text-' + (jqXHR.status === 429 ? 'warning' : 'danger')) - .text(extractApiError(jqXHR.responseJSON, jqXHR.status, 'subscribe.failed')); + .text(window.MyAi.extractApiError(jqXHR.responseJSON, jqXHR.status, 'subscribe.failed')); }).always(function () { $loader.removeClass('loader-visible'); $button.prop('disabled', false); }); } - if (window.grecaptcha && reCaptchaSiteKey) { + if (window.grecaptcha && window.MyAi.getReCaptchaSiteKey()) { grecaptcha.ready(function () { - grecaptcha.execute(reCaptchaSiteKey, { action: 'subscribe' }).then(postSubscribe); + grecaptcha.execute(window.MyAi.getReCaptchaSiteKey(), { action: 'subscribe' }).then(postSubscribe); }); } else { $loader.removeClass('loader-visible'); @@ -369,175 +240,27 @@ }); /* ============================================================ - COOKIE CONSENT + COOKIE CONSENT (handled by modules/cookie-consent.js) ============================================================ */ - - /** - * Parse consent object from localStorage. - * @returns {object|null} - Parsed consent object or null if not found - */ - function getConsent() { - try { - return JSON.parse(localStorage.getItem(CONSENT_KEY)); - } catch (e) { - return null; - } - } - - /** - * Persist consent object to localStorage. - */ - function setConsent(consent) { - localStorage.setItem(CONSENT_KEY, JSON.stringify(consent)); - } - - /** - * Apply stored consent preferences (e.g., load Google Tag Manager). - */ - function applyConsent(consent) { - if (consent && consent.analytics === true) loadGoogleTagManager(); - } - - /** - * Show cookie banner. - */ - function showBanner() { - $('#cookieBanner').fadeIn(200); - } - - /** - * Hide cookie banner. - */ - function hideBanner() { - $('#cookieBanner').fadeOut(200); - } - - /** - * Show "manage cookies" button. - */ - function showManage() { - $('#cookieManage').show(); - } - - $('#cookieReject, #cookieNecessary').on('click', function () { - setConsent({ - necessary: true, - analytics: false, - ts: new Date().toISOString() - }); - hideBanner(); - showManage(); - }); - - $('#cookieAccept').on('click', function () { - var consent = { - necessary: true, - analytics: true, - ts: new Date().toISOString() - }; - setConsent(consent); - applyConsent(consent); - hideBanner(); - showManage(); - }); - - $('#cookieManage').on('click', function (e) { - e.preventDefault(); - showBanner(); - }); - - /** - * Initialize consent banner based on stored preferences. - */ - function initConsent() { - var consent = getConsent(); - if (!consent) showBanner(); - else { - applyConsent(consent); - showManage(); - } - } + // Cookie banner, consent handlers, and storage are managed by + // modules/cookie-consent.js and exposed via window.MyAi /* ============================================================ - API & CONFIGURATION LOADING + API & CONFIGURATION LOADING (from utils/api.js) ============================================================ */ - - /** - * Check API /health/live endpoint and update status indicator. - */ - function checkApiLive() { - var $statusEl = $('#api-status'); - return $.ajax({ - url: '/api/health/live', - method: 'GET', - dataType: 'json', - timeout: 5000 - }).done(function (data) { - var status = (data && data.status) ? data.status : 'unknown'; - if ($statusEl.length) { - $statusEl.text('API: ' + status).removeClass('text-danger').addClass('text-success'); - } else { - console.log('API live status:', status); - } - }).fail(function (jqXHR, textStatus, errorThrown) { - if ($statusEl.length) { - $statusEl.text('API: offline').removeClass('text-success').addClass('text-danger'); - } - console.error('API health check failed:', textStatus, errorThrown); - }); - } - - /** - * Load reCaptcha public site key from API. - */ - function getRecaptchaWebKey() { - return $.get('/api/captcha').done(function (res) { - reCaptchaSiteKey = res; - if (reCaptchaSiteKey && !window.__recaptcha_loaded) { - window.__recaptcha_loaded = true; - var script = document.createElement('script'); - script.setAttribute('src', 'https://www.google.com/recaptcha/api.js?render=' + reCaptchaSiteKey); - document.head.appendChild(script); - } - }).fail(function () { - console.warn('Could not load reCaptcha site key from /api/captcha'); - }); - } - - /** - * Load Google Tag Manager ID from API. - */ - function getGoogleTagManagerId() { - return $.get('/api/google/tagmanager').done(function (res) { - gTagManagerId = res; - }).fail(function () { - console.warn('Could not load Google Tag Manager id from /api/google/tagmanager'); - }); - } - - /** - * Load Google Tag Manager script if consent given and not already loaded. - */ - function loadGoogleTagManager() { - if (window.__gtm_loaded || !gTagManagerId) return; - window.__gtm_loaded = true; - window.dataLayer = window.dataLayer || []; - window.dataLayer.push({ - 'gtm.start': new Date().getTime(), - event: 'gtm.js' - }); - var script = document.createElement('script'); - script.async = true; - script.src = 'https://www.googletagmanager.com/gtm/js?id=' + gTagManagerId; - document.head.appendChild(script); - } + // API utilities (checkApiLive, getRecaptchaWebKey, getGoogleTagManagerId, loadGoogleTagManager) + // are loaded from utils/api.js and exposed via window.MyAi /* ============================================================ STARTUP ============================================================ */ $(window).on('load', function () { - $.when(checkApiLive(), getRecaptchaWebKey(), getGoogleTagManagerId()).always(initConsent); + $.when( + window.MyAi.checkApiLive(), + window.MyAi.getRecaptchaWebKey(), + window.MyAi.getGoogleTagManagerId() + ).always(window.MyAi.initConsent); }); })(jQuery);