diff --git a/eslint.config.mjs b/eslint.config.mjs index eb37089..449111b 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -62,6 +62,41 @@ export default [ 'no-trailing-spaces': 'error' } }, + { + files: ['src/frontend/**/*.ts'], + languageOptions: { + parser: typescriptParser, + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module' + }, + globals: { + ...globals.browser + } + }, + plugins: { + '@typescript-eslint': typescript + }, + rules: { + // TypeScript rules + '@typescript-eslint/no-unused-vars': 'error', + '@typescript-eslint/no-explicit-any': 'warn', + + // General rules + 'no-console': 'off', // Allow console.log for debugging + 'no-var': 'error', + 'prefer-const': 'error', + 'eqeqeq': 'error', + 'no-unused-vars': 'off', // Use TypeScript version instead + + // Style rules + 'indent': ['error', 2], + 'quotes': ['error', 'single'], + 'semi': ['error', 'always'], + 'comma-dangle': ['error', 'never'], + 'no-trailing-spaces': 'error' + } + }, { files: ['tests/**/*.ts'], languageOptions: { diff --git a/src/frontend/app-admin.ts b/src/frontend/app-admin.ts index 4672327..f13f232 100644 --- a/src/frontend/app-admin.ts +++ b/src/frontend/app-admin.ts @@ -20,7 +20,7 @@ function initializeAdminApp() { header.render(); } } - + // Admin page doesn't have a footer in the current design // but we could add one if needed } diff --git a/src/frontend/app-main.ts b/src/frontend/app-main.ts index f1ff7d1..f46bf6c 100644 --- a/src/frontend/app-main.ts +++ b/src/frontend/app-main.ts @@ -10,11 +10,11 @@ function initializeApp() { // Render header const header = SharedHeader.createMainHeader(); header.render(); - + // Render footer const footer = SharedFooter.createStandardFooter(); footer.render(); - + // The rest of the app logic (map, form, etc.) remains in the existing app.js // This just adds the shared components } diff --git a/src/frontend/app-privacy.ts b/src/frontend/app-privacy.ts index dd91d04..2fdfbc3 100644 --- a/src/frontend/app-privacy.ts +++ b/src/frontend/app-privacy.ts @@ -13,11 +13,11 @@ function initializePrivacyApp() { const headerContainer = document.createElement('div'); headerContainer.id = 'header-container'; privacyHeader.parentNode?.replaceChild(headerContainer, privacyHeader); - + const header = SharedHeader.createPrivacyHeader(); header.render(); } - + // Add footer if there's a container for it const footerContainer = document.getElementById('footer-container'); if (footerContainer) { diff --git a/src/frontend/components/SharedFooter.ts b/src/frontend/components/SharedFooter.ts index 276e059..52a7d84 100644 --- a/src/frontend/components/SharedFooter.ts +++ b/src/frontend/components/SharedFooter.ts @@ -52,7 +52,7 @@ export class SharedFooter { `; container.appendChild(footer); - + // Update translations if i18n is available if ((window as any).i18n?.updatePageTranslations) { (window as any).i18n.updatePageTranslations(); diff --git a/src/frontend/components/SharedHeader.ts b/src/frontend/components/SharedHeader.ts index 0c87e55..9c8445f 100644 --- a/src/frontend/components/SharedHeader.ts +++ b/src/frontend/components/SharedHeader.ts @@ -58,12 +58,12 @@ export class SharedHeader { `; container.appendChild(header); - + // Initialize components after rendering if (this.config.showThemeToggle) { this.initializeThemeToggle(); } - + // Update translations if i18n is available if ((window as any).i18n?.updatePageTranslations) { (window as any).i18n.updatePageTranslations(); @@ -79,15 +79,15 @@ export class SharedHeader { } private renderButton(btn: ButtonConfig): string { - const attrs = btn.attributes - ? Object.entries(btn.attributes).map(([k, v]) => `${k}="${v}"`).join(' ') + const attrs = btn.attributes + ? Object.entries(btn.attributes).map(([k, v]) => `${k}="${v}"`).join(' ') : ''; - + const i18nAttr = btn.i18nKey ? `data-i18n="${btn.i18nKey}"` : ''; const text = btn.text || ''; const icon = btn.icon || ''; const idAttr = btn.id ? `id="${btn.id}"` : ''; - + if (btn.href) { return `${icon}${icon && text ? ' ' : ''}${text}`; } else { @@ -110,7 +110,7 @@ export class SharedHeader { themeToggle.addEventListener('click', () => { const currentTheme = document.documentElement.getAttribute('data-theme'); const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; - + document.documentElement.setAttribute('data-theme', newTheme); localStorage.setItem('theme', newTheme); updateThemeIcon(); diff --git a/src/i18n/index.ts b/src/i18n/index.ts index 1dbe7a6..c4b2bec 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -19,7 +19,7 @@ export class I18nService { */ private loadTranslations(): void { const localesDir = path.join(__dirname, 'locales'); - + for (const locale of this.availableLocales) { try { const filePath = path.join(localesDir, `${locale}.json`); @@ -37,7 +37,7 @@ export class I18nService { */ public t(keyPath: string, locale: string = this.defaultLocale, params?: Record): string { const translations = this.translations.get(locale) || this.translations.get(this.defaultLocale); - + if (!translations) { console.warn(`No translations found for locale: ${locale}`); return keyPath; @@ -133,10 +133,10 @@ export class I18nService { if (this.isLocaleSupported(lang.code)) { return lang.code; } - + // Check for language match (e.g., "es" matches "es-MX") const languageCode = lang.code.split('-')[0]; - const matchingLocale = this.availableLocales.find(locale => + const matchingLocale = this.availableLocales.find(locale => locale.startsWith(languageCode) ); if (matchingLocale) { diff --git a/src/routes/i18n.ts b/src/routes/i18n.ts index 846b645..35ecc5c 100644 --- a/src/routes/i18n.ts +++ b/src/routes/i18n.ts @@ -22,7 +22,7 @@ export function createI18nRoutes(): Router { // Get translations for the locale const translations = i18nService.getTranslations(locale); - + if (!translations) { res.status(404).json({ error: 'Translations not found for locale', diff --git a/src/server.ts b/src/server.ts index bf7c4ad..3c42e70 100644 --- a/src/server.ts +++ b/src/server.ts @@ -36,14 +36,14 @@ app.use((req: Request, res: Response, next: NextFunction) => { // Detect user's preferred locale from Accept-Language header or cookie const cookieLocale = req.headers.cookie?.split(';') .find(c => c.trim().startsWith('locale='))?.split('=')[1]; - + const detectedLocale = cookieLocale || i18nService.detectLocale(req.get('Accept-Language')); - + // Add locale to request object for use in routes (req as any).locale = detectedLocale; - (req as any).t = (key: string, params?: Record) => + (req as any).t = (key: string, params?: Record) => i18nService.t(key, detectedLocale, params); - + next(); }); @@ -211,20 +211,20 @@ function setupRoutes(): void { try { // Get locale from query parameter or use detected locale const requestedLocale = req.query.locale as string; - const locale = requestedLocale && i18nService.isLocaleSupported(requestedLocale) - ? requestedLocale + const locale = requestedLocale && i18nService.isLocaleSupported(requestedLocale) + ? requestedLocale : (req as any).locale; - + // Helper function for translations const t = (key: string) => i18nService.t(key, locale); - + const locations = await locationModel.getActive(); const formatTimeRemaining = (createdAt: string, isPersistent?: boolean): string => { if (isPersistent) { return t('time.persistent'); } - + const created = new Date(createdAt); const now = new Date(); const diffMs = (48 * 60 * 60 * 1000) - (now.getTime() - created.getTime()); @@ -248,10 +248,10 @@ function setupRoutes(): void { const escapeHtml = (text: string): string => { return text.replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); }; const tableRows = locations.map((location, index) => ` @@ -383,12 +383,12 @@ function setupRoutes(): void { console.log('Handling form submission for non-JS users'); const { address, description, locale: formLocale } = req.body; - + // Get locale from form or use detected locale - const locale = formLocale && i18nService.isLocaleSupported(formLocale) - ? formLocale + const locale = formLocale && i18nService.isLocaleSupported(formLocale) + ? formLocale : (req as any).locale; - + // Helper function for translations const t = (key: string) => i18nService.t(key, locale); @@ -473,7 +473,7 @@ function setupRoutes(): void { console.log('Generating static map image'); try { const locations = await locationModel.getActive(); - + // Parse query parameters for customization const width = parseInt(req.query.width as string) || 800; const height = parseInt(req.query.height as string) || 600; diff --git a/src/services/MapImageService.ts b/src/services/MapImageService.ts index 4a56cb9..eff9be8 100644 --- a/src/services/MapImageService.ts +++ b/src/services/MapImageService.ts @@ -18,18 +18,18 @@ export class MapImageService { */ async generateMapImage(locations: Location[], options: Partial = {}): Promise { const opts = { ...this.defaultOptions, ...options }; - + console.info('Generating Mapbox static map focused on location data'); console.info('Canvas size:', opts.width, 'x', opts.height); console.info('Number of locations:', locations.length); const mapboxBuffer = await this.fetchMapboxStaticMapAutoFit(opts, locations); - + if (mapboxBuffer) { return mapboxBuffer; } else { // Return a simple error image if Mapbox fails - return this.generateErrorImage(opts); + return this.generateErrorImage(); } } @@ -54,10 +54,10 @@ export class MapImageService { overlays += `pin-s-${label}+${color}(${location.longitude},${location.latitude}),`; } }); - + // Remove trailing comma overlays = overlays.replace(/,$/, ''); - + console.info('Generated overlays string:', overlays); // Build Mapbox Static Maps URL with auto-fit @@ -65,7 +65,7 @@ export class MapImageService { if (overlays) { // Check if we have only one location const validLocations = locations.filter(loc => loc.latitude && loc.longitude); - + if (validLocations.length === 1) { // For single location, use fixed zoom level to avoid zooming too close const location = validLocations[0]; @@ -81,13 +81,13 @@ export class MapImageService { const fallbackLng = -85.67402711517647; mapboxUrl = `https://api.mapbox.com/styles/v1/mapbox/streets-v12/static/${fallbackLng},${fallbackLat},10/${options.width}x${options.height}?access_token=${mapboxToken}`; } - + console.info('Fetching Mapbox static map...'); if (overlays && locations.filter(loc => loc.latitude && loc.longitude).length === 1) { console.info('Using fixed zoom level for single location'); } console.info('URL:', mapboxUrl.replace(mapboxToken, 'TOKEN_HIDDEN')); - + return new Promise((resolve) => { const request = https.get(mapboxUrl, { timeout: 10000 }, (response) => { if (response.statusCode === 200) { @@ -102,12 +102,12 @@ export class MapImageService { resolve(null); } }); - + request.on('error', (err) => { console.error('Error fetching Mapbox map:', err.message); resolve(null); }); - + request.on('timeout', () => { console.error('Mapbox request timeout'); request.destroy(); @@ -120,7 +120,7 @@ export class MapImageService { /** * Generate a simple error image when Mapbox fails */ - private generateErrorImage(options: MapOptions): Buffer { + private generateErrorImage(): Buffer { // Generate a simple 1x1 transparent PNG as fallback // This is a valid PNG header + IHDR + IDAT + IEND for a 1x1 transparent pixel const transparentPng = Buffer.from([ @@ -134,7 +134,7 @@ export class MapImageService { 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, // IEND chunk 0x42, 0x60, 0x82 ]); - + console.info('Generated transparent PNG fallback due to Mapbox failure'); return transparentPng; }