Add TypeScript frontend build system with shared components
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
7bee175003
commit
5151e87824
11 changed files with 986 additions and 4 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -30,6 +30,7 @@ Thumbs.db
|
|||
# Generated files
|
||||
public/style.css
|
||||
public/style.css.map
|
||||
public/dist/
|
||||
|
||||
# TypeScript build outputs
|
||||
dist/
|
||||
|
|
503
package-lock.json
generated
503
package-lock.json
generated
|
@ -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",
|
||||
|
|
11
package.json
11
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",
|
||||
|
|
47
public/example-shared-components.html
Normal file
47
public/example-shared-components.html
Normal file
|
@ -0,0 +1,47 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Shared Components Example</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
|
||||
<!-- Internationalization -->
|
||||
<script src="i18n.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- Container for shared header -->
|
||||
<div id="header-container"></div>
|
||||
|
||||
<div class="content">
|
||||
<div class="form-section">
|
||||
<h2>Example Page Using Shared Components</h2>
|
||||
<p>This page demonstrates the new TypeScript-based shared header and footer components.</p>
|
||||
|
||||
<h3>Benefits:</h3>
|
||||
<ul>
|
||||
<li>✅ Type-safe TypeScript components</li>
|
||||
<li>✅ Consistent headers/footers across all pages</li>
|
||||
<li>✅ Easy to maintain - change once, update everywhere</li>
|
||||
<li>✅ Built-in i18n support</li>
|
||||
<li>✅ Automatic theme toggle functionality</li>
|
||||
</ul>
|
||||
|
||||
<h3>How to use:</h3>
|
||||
<pre><code>// Include the compiled bundle
|
||||
<script src="dist/app-main.js"></script>
|
||||
|
||||
// The shared components are automatically rendered
|
||||
// into #header-container and #footer-container</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Container for shared footer -->
|
||||
<div id="footer-container"></div>
|
||||
</div>
|
||||
|
||||
<!-- Include the compiled TypeScript bundle -->
|
||||
<script src="dist/app-main.js"></script>
|
||||
</body>
|
||||
</html>
|
68
scripts/build-frontend.js
Executable file
68
scripts/build-frontend.js
Executable file
|
@ -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();
|
37
src/frontend/app-admin.ts
Normal file
37
src/frontend/app-admin.ts
Normal file
|
@ -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;
|
31
src/frontend/app-main.ts
Normal file
31
src/frontend/app-main.ts
Normal file
|
@ -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;
|
38
src/frontend/app-privacy.ts
Normal file
38
src/frontend/app-privacy.ts
Normal file
|
@ -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;
|
77
src/frontend/components/SharedFooter.ts
Normal file
77
src/frontend/components/SharedFooter.ts
Normal file
|
@ -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<SharedFooterConfig>;
|
||||
|
||||
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 ? `
|
||||
<p>
|
||||
<span data-i18n="footer.safetyNotice">Safety Notice: This is a community tool for awareness. Stay safe and</span>
|
||||
<a href="https://www.aclu.org/know-your-rights/immigrants-rights"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style="color: #007bff; text-decoration: underline;"
|
||||
data-i18n="footer.knowRights">know your rights</a>.
|
||||
</p>
|
||||
` : ''}
|
||||
|
||||
${this.config.showDisclaimer ? `
|
||||
<div class="disclaimer">
|
||||
<small>
|
||||
<span data-i18n="footer.disclaimer">This website is for informational purposes only. Verify information independently. Reports are automatically deleted after 48 hours. •</span>
|
||||
<a href="/privacy"
|
||||
style="color: #007bff; text-decoration: underline;"
|
||||
data-i18n="common.privacyPolicy">Privacy Policy</a>
|
||||
</small>
|
||||
</div>
|
||||
` : ''}
|
||||
`;
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
154
src/frontend/components/SharedHeader.ts
Normal file
154
src/frontend/components/SharedHeader.ts
Normal file
|
@ -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<string, string>;
|
||||
}
|
||||
|
||||
export interface SharedHeaderConfig {
|
||||
showLanguageSelector?: boolean;
|
||||
showThemeToggle?: boolean;
|
||||
additionalButtons?: ButtonConfig[];
|
||||
titleKey?: string;
|
||||
subtitleKey?: string | null;
|
||||
containerId?: string;
|
||||
}
|
||||
|
||||
export class SharedHeader {
|
||||
private config: Required<SharedHeaderConfig>;
|
||||
|
||||
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 = `
|
||||
<div class="header-content">
|
||||
<div class="header-text">
|
||||
<h1><a href="/" style="text-decoration: none; color: inherit;">❄️</a> <span data-i18n="${this.config.titleKey}"></span></h1>
|
||||
${this.config.subtitleKey ? `<p data-i18n="${this.config.subtitleKey}"></p>` : ''}
|
||||
</div>
|
||||
<div class="header-controls">
|
||||
${this.config.showLanguageSelector ? '<div id="language-selector-container" class="language-selector-container"></div>' : ''}
|
||||
${this.config.showThemeToggle ? this.renderThemeToggle() : ''}
|
||||
${this.config.additionalButtons.map(btn => this.renderButton(btn)).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 `
|
||||
<button id="theme-toggle" class="theme-toggle js-only" title="Toggle dark mode" data-i18n-title="common.darkMode">
|
||||
<span class="theme-icon">🌙</span>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
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 `<a href="${btn.href}" class="${btn.class || 'header-btn'}" ${attrs} ${i18nAttr}>${icon}${icon && text ? ' ' : ''}${text}</a>`;
|
||||
} else {
|
||||
return `<button ${idAttr} class="${btn.class || 'header-btn'}" ${attrs} ${i18nAttr}>${icon}${icon && text ? ' ' : ''}${text}</button>`;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
23
src/frontend/tsconfig.json
Normal file
23
src/frontend/tsconfig.json
Normal file
|
@ -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"
|
||||
]
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue