From 5151e8782454bfd36fb9ebd2f9d1e5765cc53575 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Mon, 7 Jul 2025 19:46:19 -0400 Subject: [PATCH] Add TypeScript frontend build system with shared components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Set up esbuild for fast TypeScript compilation of frontend code - Create SharedHeader component with factories for main/admin/privacy pages - Create SharedFooter component with standard and minimal variants - Add frontend build scripts (build:frontend, watch:frontend, dev:full) - Configure TypeScript for browser environment with DOM types - Add example page demonstrating shared component usage - Update .gitignore to exclude compiled frontend files Benefits: - Type-safe frontend components - Consistent headers/footers across all pages - Single source of truth for common UI elements - Built-in i18n and theme toggle support 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .gitignore | 1 + package-lock.json | 503 ++++++++++++++++++++++++ package.json | 11 +- public/example-shared-components.html | 47 +++ scripts/build-frontend.js | 68 ++++ src/frontend/app-admin.ts | 37 ++ src/frontend/app-main.ts | 31 ++ src/frontend/app-privacy.ts | 38 ++ src/frontend/components/SharedFooter.ts | 77 ++++ src/frontend/components/SharedHeader.ts | 154 ++++++++ src/frontend/tsconfig.json | 23 ++ 11 files changed, 986 insertions(+), 4 deletions(-) create mode 100644 public/example-shared-components.html create mode 100755 scripts/build-frontend.js create mode 100644 src/frontend/app-admin.ts create mode 100644 src/frontend/app-main.ts create mode 100644 src/frontend/app-privacy.ts create mode 100644 src/frontend/components/SharedFooter.ts create mode 100644 src/frontend/components/SharedHeader.ts create mode 100644 src/frontend/tsconfig.json diff --git a/.gitignore b/.gitignore index 3e787f1..c4cf9e7 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ Thumbs.db # Generated files public/style.css public/style.css.map +public/dist/ # TypeScript build outputs dist/ diff --git a/package-lock.json b/package-lock.json index f7c0947..bf10761 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "@types/cors": "^2.8.19", "@types/express": "^5.0.3", "@types/jest": "^30.0.0", + "@types/leaflet": "^1.9.19", "@types/node": "^24.0.10", "@types/node-cron": "^3.0.11", "@types/sqlite3": "^3.1.11", @@ -32,6 +33,7 @@ "@typescript-eslint/eslint-plugin": "^8.35.1", "@typescript-eslint/parser": "^8.35.1", "concurrently": "^9.2.0", + "esbuild": "^0.25.6", "eslint": "^9.30.1", "globals": "^16.3.0", "jest": "^29.7.0", @@ -727,6 +729,448 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", + "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz", + "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz", + "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz", + "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz", + "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz", + "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz", + "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz", + "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz", + "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz", + "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz", + "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz", + "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz", + "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz", + "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz", + "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz", + "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz", + "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz", + "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz", + "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz", + "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz", + "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz", + "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz", + "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz", + "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz", + "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz", + "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", @@ -2053,6 +2497,13 @@ "@types/send": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -2327,6 +2778,16 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "license": "MIT" }, + "node_modules/@types/leaflet": { + "version": "1.9.19", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.19.tgz", + "integrity": "sha512-pB+n2daHcZPF2FDaWa+6B0a0mSDf4dPU35y5iTXsx7x/PzzshiX5atYiS1jlBn43X7XvM8AP+AB26lnSk0J4GA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -4268,6 +4729,48 @@ "node": ">= 0.4" } }, + "node_modules/esbuild": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz", + "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.6", + "@esbuild/android-arm": "0.25.6", + "@esbuild/android-arm64": "0.25.6", + "@esbuild/android-x64": "0.25.6", + "@esbuild/darwin-arm64": "0.25.6", + "@esbuild/darwin-x64": "0.25.6", + "@esbuild/freebsd-arm64": "0.25.6", + "@esbuild/freebsd-x64": "0.25.6", + "@esbuild/linux-arm": "0.25.6", + "@esbuild/linux-arm64": "0.25.6", + "@esbuild/linux-ia32": "0.25.6", + "@esbuild/linux-loong64": "0.25.6", + "@esbuild/linux-mips64el": "0.25.6", + "@esbuild/linux-ppc64": "0.25.6", + "@esbuild/linux-riscv64": "0.25.6", + "@esbuild/linux-s390x": "0.25.6", + "@esbuild/linux-x64": "0.25.6", + "@esbuild/netbsd-arm64": "0.25.6", + "@esbuild/netbsd-x64": "0.25.6", + "@esbuild/openbsd-arm64": "0.25.6", + "@esbuild/openbsd-x64": "0.25.6", + "@esbuild/openharmony-arm64": "0.25.6", + "@esbuild/sunos-x64": "0.25.6", + "@esbuild/win32-arm64": "0.25.6", + "@esbuild/win32-ia32": "0.25.6", + "@esbuild/win32-x64": "0.25.6" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", diff --git a/package.json b/package.json index e87b012..8e67149 100644 --- a/package.json +++ b/package.json @@ -13,23 +13,24 @@ "watch-css": "sass src/scss/main.scss public/style.css --watch --style=expanded --source-map", "dev-with-css": "concurrently \"npm run watch-css\" \"npm run dev\"", "dev-with-css:ts": "concurrently \"npm run watch-css\" \"npm run dev:ts\"", - "build": "npm run build:ts && npm run build-css && npm run copy-i18n", + "build": "npm run build:ts && npm run build-css && npm run build:frontend && npm run copy-i18n", "build:ts": "tsc", "copy-i18n": "mkdir -p dist/i18n/locales && cp -r src/i18n/locales/* dist/i18n/locales/", "test": "jest --runInBand --forceExit", "test:coverage": "jest --coverage", "lint": "eslint src/ tests/", "lint:fix": "eslint src/ tests/ --fix", - "postinstall": "npm run build-css" + "postinstall": "npm run build-css", + "build:frontend": "node scripts/build-frontend.js", + "watch:frontend": "node scripts/build-frontend.js --watch", + "dev:full": "concurrently \"npm run watch-css\" \"npm run watch:frontend\" \"npm run dev:ts\"" }, "dependencies": { - "cors": "^2.8.5", "dotenv": "^17.0.1", "express": "^4.18.2", "express-rate-limit": "^7.5.1", "node-cron": "^3.0.3", - "sqlite3": "^5.1.6", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1" @@ -38,6 +39,7 @@ "@types/cors": "^2.8.19", "@types/express": "^5.0.3", "@types/jest": "^30.0.0", + "@types/leaflet": "^1.9.19", "@types/node": "^24.0.10", "@types/node-cron": "^3.0.11", "@types/sqlite3": "^3.1.11", @@ -47,6 +49,7 @@ "@typescript-eslint/eslint-plugin": "^8.35.1", "@typescript-eslint/parser": "^8.35.1", "concurrently": "^9.2.0", + "esbuild": "^0.25.6", "eslint": "^9.30.1", "globals": "^16.3.0", "jest": "^29.7.0", diff --git a/public/example-shared-components.html b/public/example-shared-components.html new file mode 100644 index 0000000..7d820a2 --- /dev/null +++ b/public/example-shared-components.html @@ -0,0 +1,47 @@ + + + + + + Shared Components Example + + + + + + +
+ +
+ +
+
+

Example Page Using Shared Components

+

This page demonstrates the new TypeScript-based shared header and footer components.

+ +

Benefits:

+
    +
  • ✅ Type-safe TypeScript components
  • +
  • ✅ Consistent headers/footers across all pages
  • +
  • ✅ Easy to maintain - change once, update everywhere
  • +
  • ✅ Built-in i18n support
  • +
  • ✅ Automatic theme toggle functionality
  • +
+ +

How to use:

+
// Include the compiled bundle
+<script src="dist/app-main.js"></script>
+
+// The shared components are automatically rendered
+// into #header-container and #footer-container
+
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/scripts/build-frontend.js b/scripts/build-frontend.js new file mode 100755 index 0000000..69b99dc --- /dev/null +++ b/scripts/build-frontend.js @@ -0,0 +1,68 @@ +#!/usr/bin/env node + +const esbuild = require('esbuild'); +const path = require('path'); +const fs = require('fs'); + +// Ensure output directory exists +const outdir = path.join(__dirname, '..', 'public', 'dist'); +if (!fs.existsSync(outdir)) { + fs.mkdirSync(outdir, { recursive: true }); +} + +// Build configuration +const buildOptions = { + entryPoints: [ + // Main app bundles + 'src/frontend/app-main.ts', + 'src/frontend/app-admin.ts', + 'src/frontend/app-privacy.ts', + + // Shared components will be imported by the above + ], + bundle: true, + outdir: outdir, + format: 'iife', // Immediately Invoked Function Expression for browsers + platform: 'browser', + target: ['es2020'], // Modern browsers, matching our TypeScript target + sourcemap: process.env.NODE_ENV !== 'production', + minify: process.env.NODE_ENV === 'production', + loader: { + '.ts': 'ts', + }, + define: { + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development') + }, + // Don't bundle these - they'll be loaded from CDN + external: ['leaflet'], +}; + +// Build function +async function build() { + const isWatch = process.argv.includes('--watch'); + + try { + if (isWatch) { + // Watch mode for development + const ctx = await esbuild.context(buildOptions); + await ctx.watch(); + console.log('👀 Watching for frontend changes...'); + } else { + // One-time build + console.log('🔨 Building frontend...'); + const result = await esbuild.build(buildOptions); + console.log('✅ Frontend build complete!'); + + if (result.errors.length > 0) { + console.error('❌ Build errors:', result.errors); + process.exit(1); + } + } + } catch (error) { + console.error('❌ Build failed:', error); + process.exit(1); + } +} + +// Run the build +build(); \ No newline at end of file diff --git a/src/frontend/app-admin.ts b/src/frontend/app-admin.ts new file mode 100644 index 0000000..4672327 --- /dev/null +++ b/src/frontend/app-admin.ts @@ -0,0 +1,37 @@ +/** + * Admin app entry point - uses shared components + */ + +import { SharedHeader } from './components/SharedHeader'; +import { SharedFooter } from './components/SharedFooter'; + +// Initialize shared components when DOM is ready +function initializeAdminApp() { + // Render header (only when logged in) + const adminSection = document.getElementById('admin-section'); + if (adminSection && adminSection.style.display !== 'none') { + const header = SharedHeader.createAdminHeader(); + // For admin, we need to replace the existing header + const existingHeader = document.querySelector('.admin-header'); + if (existingHeader) { + const headerContainer = document.createElement('div'); + headerContainer.id = 'header-container'; + existingHeader.parentNode?.replaceChild(headerContainer, existingHeader); + header.render(); + } + } + + // Admin page doesn't have a footer in the current design + // but we could add one if needed +} + +// Wait for DOM to be ready +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initializeAdminApp); +} else { + initializeAdminApp(); +} + +// Export for use in other scripts if needed +(window as any).SharedHeader = SharedHeader; +(window as any).SharedFooter = SharedFooter; \ No newline at end of file diff --git a/src/frontend/app-main.ts b/src/frontend/app-main.ts new file mode 100644 index 0000000..f1ff7d1 --- /dev/null +++ b/src/frontend/app-main.ts @@ -0,0 +1,31 @@ +/** + * Main app entry point - uses shared components + */ + +import { SharedHeader } from './components/SharedHeader'; +import { SharedFooter } from './components/SharedFooter'; + +// Initialize shared components when DOM is ready +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 +} + +// Wait for DOM and i18n to be ready +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initializeApp); +} else { + initializeApp(); +} + +// Export for use in other scripts if needed +(window as any).SharedHeader = SharedHeader; +(window as any).SharedFooter = SharedFooter; \ No newline at end of file diff --git a/src/frontend/app-privacy.ts b/src/frontend/app-privacy.ts new file mode 100644 index 0000000..dd91d04 --- /dev/null +++ b/src/frontend/app-privacy.ts @@ -0,0 +1,38 @@ +/** + * Privacy page entry point - uses shared components + */ + +import { SharedHeader } from './components/SharedHeader'; +import { SharedFooter } from './components/SharedFooter'; + +// Initialize shared components when DOM is ready +function initializePrivacyApp() { + // For privacy page, we need to replace the existing header structure + const privacyHeader = document.querySelector('.privacy-header'); + if (privacyHeader) { + 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) { + const footer = SharedFooter.createStandardFooter(); + footer.render(); + } +} + +// Wait for DOM to be ready +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initializePrivacyApp); +} else { + initializePrivacyApp(); +} + +// Export for use in other scripts if needed +(window as any).SharedHeader = SharedHeader; +(window as any).SharedFooter = SharedFooter; \ No newline at end of file diff --git a/src/frontend/components/SharedFooter.ts b/src/frontend/components/SharedFooter.ts new file mode 100644 index 0000000..276e059 --- /dev/null +++ b/src/frontend/components/SharedFooter.ts @@ -0,0 +1,77 @@ +/** + * Shared footer component for consistent footers across pages + */ + +export interface SharedFooterConfig { + containerId?: string; + showSafetyNotice?: boolean; + showDisclaimer?: boolean; +} + +export class SharedFooter { + private config: Required; + + constructor(config: SharedFooterConfig = {}) { + this.config = { + containerId: config.containerId || 'footer-container', + showSafetyNotice: config.showSafetyNotice !== false, + showDisclaimer: config.showDisclaimer !== false + }; + } + + public render(): void { + const container = document.getElementById(this.config.containerId); + if (!container) { + console.error(`Container with id "${this.config.containerId}" not found`); + return; + } + + const footer = document.createElement('footer'); + footer.innerHTML = ` + ${this.config.showSafetyNotice ? ` +

+ Safety Notice: This is a community tool for awareness. Stay safe and + know your rights. +

+ ` : ''} + + ${this.config.showDisclaimer ? ` +
+ + This website is for informational purposes only. Verify information independently. Reports are automatically deleted after 48 hours. • + Privacy Policy + +
+ ` : ''} + `; + + container.appendChild(footer); + + // Update translations if i18n is available + if ((window as any).i18n?.updatePageTranslations) { + (window as any).i18n.updatePageTranslations(); + } + } + + // Factory method for standard footer + public static createStandardFooter(): SharedFooter { + return new SharedFooter({ + showSafetyNotice: true, + showDisclaimer: true + }); + } + + // Factory method for minimal footer (e.g., admin pages) + public static createMinimalFooter(): SharedFooter { + return new SharedFooter({ + showSafetyNotice: false, + showDisclaimer: true + }); + } +} \ No newline at end of file diff --git a/src/frontend/components/SharedHeader.ts b/src/frontend/components/SharedHeader.ts new file mode 100644 index 0000000..0c87e55 --- /dev/null +++ b/src/frontend/components/SharedHeader.ts @@ -0,0 +1,154 @@ +/** + * Shared header component for consistent headers across pages + */ + +export interface ButtonConfig { + id?: string; + href?: string; + class?: string; + icon?: string; + text?: string; + i18nKey?: string; + attributes?: Record; +} + +export interface SharedHeaderConfig { + showLanguageSelector?: boolean; + showThemeToggle?: boolean; + additionalButtons?: ButtonConfig[]; + titleKey?: string; + subtitleKey?: string | null; + containerId?: string; +} + +export class SharedHeader { + private config: Required; + + constructor(config: SharedHeaderConfig = {}) { + this.config = { + showLanguageSelector: config.showLanguageSelector !== false, + showThemeToggle: config.showThemeToggle !== false, + additionalButtons: config.additionalButtons || [], + titleKey: config.titleKey || 'common.appName', + subtitleKey: config.subtitleKey || null, + containerId: config.containerId || 'header-container' + }; + } + + public render(): void { + const container = document.getElementById(this.config.containerId); + if (!container) { + console.error(`Container with id "${this.config.containerId}" not found`); + return; + } + + const header = document.createElement('header'); + header.innerHTML = ` +
+
+

❄️

+ ${this.config.subtitleKey ? `

` : ''} +
+
+ ${this.config.showLanguageSelector ? '
' : ''} + ${this.config.showThemeToggle ? this.renderThemeToggle() : ''} + ${this.config.additionalButtons.map(btn => this.renderButton(btn)).join('')} +
+
+ `; + + 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(); + } + } + + private renderThemeToggle(): string { + return ` + + `; + } + + private renderButton(btn: ButtonConfig): string { + 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 { + return ``; + } + } + + private initializeThemeToggle(): void { + const themeToggle = document.getElementById('theme-toggle') as HTMLButtonElement; + if (!themeToggle) return; + + const updateThemeIcon = (): void => { + const theme = document.documentElement.getAttribute('data-theme'); + const icon = themeToggle.querySelector('.theme-icon'); + if (icon) { + icon.textContent = theme === 'dark' ? '☀️' : '🌙'; + } + }; + + 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(); + }); + + // Set initial icon + updateThemeIcon(); + } + + // Factory methods for common headers + public static createMainHeader(): SharedHeader { + return new SharedHeader({ + titleKey: 'common.appName', + subtitleKey: 'meta.subtitle', + showLanguageSelector: true, + showThemeToggle: true + }); + } + + public static createAdminHeader(): SharedHeader { + return new SharedHeader({ + titleKey: 'admin.adminPanel', + showLanguageSelector: true, + showThemeToggle: true, + additionalButtons: [ + { href: '/', class: 'header-btn btn-home', icon: '🏠', text: 'Homepage', i18nKey: 'common.homepage' }, + { id: 'refresh-btn', class: 'header-btn btn-refresh', icon: '🔄', text: 'Refresh Data', i18nKey: 'common.refresh' }, + { id: 'logout-btn', class: 'header-btn btn-logout', icon: '🚪', text: 'Logout', i18nKey: 'common.logout' } + ] + }); + } + + public static createPrivacyHeader(): SharedHeader { + return new SharedHeader({ + titleKey: 'privacy.title', + subtitleKey: 'common.appName', + showLanguageSelector: false, + showThemeToggle: true + }); + } +} \ No newline at end of file diff --git a/src/frontend/tsconfig.json b/src/frontend/tsconfig.json new file mode 100644 index 0000000..122ca5b --- /dev/null +++ b/src/frontend/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "jsx": "react", + "moduleResolution": "node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "types": ["leaflet"] + }, + "include": [ + "**/*.ts", + "**/*.tsx" + ] +} \ No newline at end of file