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
+
+