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 ``;
} 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;
}