diff --git a/src/frontend/src/index.ts b/src/frontend/src/index.ts
deleted file mode 100755
index 0189803552..0000000000
--- a/src/frontend/src/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-import('./App');
diff --git a/src/frontend/src/main.tsx b/src/frontend/src/main.tsx
new file mode 100644
index 0000000000..b744675768
--- /dev/null
+++ b/src/frontend/src/main.tsx
@@ -0,0 +1,114 @@
+import '@/index.css';
+import 'ol/ol.css';
+import 'react-loading-skeleton/dist/skeleton.css';
+
+import axios from 'axios';
+import React from 'react';
+import ReactDOM from 'react-dom';
+// Uncomment for React 18
+// import { createRoot } from 'react-dom/client';
+
+import environment from '@/environment';
+import App from './App';
+
+// Added Fix of Console Error of MUI Issue
+const consoleError = console.error;
+const SUPPRESSED_WARNINGS = [
+ 'MUI: The `value` provided to the Tabs component is invalid.',
+ 'React does not recognize the `%s` prop on a DOM element. If you intentionally want it to appear in the DOM as a custom attribute, spell it as lowercase `%s` instead.',
+ 'Using kebab-case for css properties in objects is not supported. Did you mean WebkitBoxOrient?',
+ 'Using kebab-case for css properties in objects is not supported. Did you mean WebkitLineClamp?',
+ 'If you used to conditionally omit it with %s={condition && value}, pass %s={condition ? value : undefined} instead.%s',
+];
+
+console.error = function filterWarnings(msg, ...args) {
+ if (typeof msg !== 'string') {
+ consoleError(...args);
+ } else if (!SUPPRESSED_WARNINGS.some((entry) => msg.includes(entry))) {
+ consoleError(msg, ...args);
+ }
+};
+
+axios.interceptors.request.use(
+ (config) => {
+ // Do something before request is sent
+
+ // const excludedDomains = ['xxx', 'xxx'];
+ // const urlIsExcluded = excludedDomains.some((domain) => config.url.includes(domain));
+ // if (!urlIsExcluded) {
+ // config.withCredentials = true;
+ // }
+
+ config.withCredentials = true;
+
+ return config;
+ },
+ (error) =>
+ // Do something with request error
+ Promise.reject(error),
+);
+
+(function sentryInit() {
+ // Immediately invoked function to enable Sentry monitoring
+ if (import.meta.env.MODE === 'development' || import.meta.env.BASE_URL !== 'fmtm.hotosm.org') {
+ return;
+ }
+ console.log('Adding Sentry');
+
+ import('@sentry/react').then((Sentry) => {
+ Sentry.init({
+ dsn: 'https://35c80d0894e441f593c5ac5dfa1094a0@o68147.ingest.sentry.io/4505557311356928',
+ integrations: [
+ new Sentry.BrowserTracing({
+ // Set 'tracePropagationTargets' to control for which URLs distributed tracing should be enabled
+ tracePropagationTargets: ['https://fmtm.hotosm.org/'],
+ }),
+ new Sentry.Replay(),
+ ],
+ // Performance Monitoring
+ tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production!
+ // Session Replay
+ replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.
+ replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
+ });
+ });
+})();
+
+(function matomoTrackingInit() {
+ // Immediately invoked function to enable Matomo tracking
+ if (import.meta.env.MODE === 'development' || import.meta.env.BASE_URL !== 'fmtm.hotosm.org') {
+ return;
+ }
+ console.log('Adding Matomo');
+
+ // Set matomo tracking id
+ window.site_id = environment.matomoTrackingId;
+
+ // Create optout-form div for banner
+ const optoutDiv = document.createElement('div');
+ optoutDiv.id = 'optout-form'; // Set an ID if needed
+ document.body.appendChild(optoutDiv);
+
+ // Load CDN script
+ const script = document.createElement('script');
+ script.src = 'https://cdn.hotosm.org/tracking-v3.js';
+ document.body.appendChild(script);
+ // Manually trigger DOMContentLoaded, that script hooks
+ // https://github.com/hotosm/matomo-tracking/blob/9b95230cb5f0bf2a902f00379152f3af9204c641/tracking-v3.js#L125
+ script.onload = () => {
+ optoutDiv.dispatchEvent(
+ new Event('DOMContentLoaded', {
+ bubbles: true,
+ cancelable: true,
+ }),
+ );
+ };
+})();
+
+// React 17 setup
+ReactDOM.render(
, document.getElementById('app'));
+
+// // React 18 setup
+// createRoot(document.getElementById('app')!).render(
+//
,
+// );
diff --git a/src/frontend/src/styles/OfflineReadyPrompt.css b/src/frontend/src/styles/OfflineReadyPrompt.css
new file mode 100644
index 0000000000..3332dc3401
--- /dev/null
+++ b/src/frontend/src/styles/OfflineReadyPrompt.css
@@ -0,0 +1,32 @@
+.OfflineReadyPrompt-container {
+ padding: 0;
+ margin: 0;
+ width: 0;
+ height: 0;
+}
+.OfflineReadyPrompt-date {
+ visibility: hidden;
+}
+.OfflineReadyPrompt-toast {
+ position: fixed;
+ right: 0;
+ bottom: 0;
+ margin: 16px;
+ padding: 12px;
+ border: 1px solid #8885;
+ border-radius: 4px;
+ z-index: 1;
+ text-align: left;
+ box-shadow: 3px 4px 5px 0 #8885;
+ background-color: white;
+}
+.OfflineReadyPrompt-toast-message {
+ margin-bottom: 8px;
+}
+.OfflineReadyPrompt-toast-button {
+ border: 1px solid #8885;
+ outline: none;
+ margin-right: 5px;
+ border-radius: 2px;
+ padding: 3px 10px;
+}
diff --git a/src/frontend/src/sum.js b/src/frontend/src/sum.js
deleted file mode 100644
index 506a413438..0000000000
--- a/src/frontend/src/sum.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export function sum(a, b) {
- return a + b;
-}
diff --git a/src/frontend/src/views/OsmAuth.tsx b/src/frontend/src/views/OsmAuth.tsx
index 1c74797a17..f78ed5e651 100644
--- a/src/frontend/src/views/OsmAuth.tsx
+++ b/src/frontend/src/views/OsmAuth.tsx
@@ -11,7 +11,7 @@ function OsmAuth() {
const [isReadyToRedirect, setIsReadyToRedirect] = useState(false);
useEffect(() => {
- // Redirect workaround require for locahost, until PR is merged:
+ // Redirect workaround required for locahost, until PR is merged:
// https://github.com/openstreetmap/openstreetmap-website/pull/4287
if (window.location.href.includes('127.0.0.1:7051')) {
// Pass through same url params
diff --git a/src/frontend/tests/App.test.jsx b/src/frontend/tests/App.test.jsx
index ffff8d275c..99b1b720b9 100644
--- a/src/frontend/tests/App.test.jsx
+++ b/src/frontend/tests/App.test.jsx
@@ -1,6 +1,9 @@
-import { sum } from '../src/sum';
import { describe, expect, test } from 'vitest';
+function sum(a, b) {
+ return a + b;
+}
+
describe('Test', () => {
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
diff --git a/src/frontend/tsconfig.json b/src/frontend/tsconfig.json
index 712ca4b0d1..7cf8b89df4 100644
--- a/src/frontend/tsconfig.json
+++ b/src/frontend/tsconfig.json
@@ -4,7 +4,6 @@
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
- "checkJs": true,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
@@ -16,13 +15,13 @@
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
- // "jsx": "react-jsx",
"jsx": "react",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
- "noImplicitAny": false, //FIXME: Change This "true" to "false" To Integrate Types Instead Of "Any" Types.
+ // Once all (var: any) definitions are removed, enable this for strict checking
+ "noImplicitAny": false,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
@@ -31,14 +30,8 @@
"strictPropertyInitialization": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
- "types": ["@testing-library/jest-dom"]
+ "types": ["@testing-library/jest-dom", "vite-plugin-pwa/react"]
},
- "include": [
- // ".eslintrc.cjs",
- // ".eslintrc.js",
- "src",
- "src/**/*.ts",
- "tests"
- ],
+ "include": ["src", "src/**/*.ts", "tests"],
"exclude": ["node_modules", "dist"]
}
diff --git a/src/frontend/vite.config.ts b/src/frontend/vite.config.ts
index 234c519517..3e1f110406 100644
--- a/src/frontend/vite.config.ts
+++ b/src/frontend/vite.config.ts
@@ -4,20 +4,58 @@ import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { configDefaults } from 'vitest/config';
import { VitePWA } from 'vite-plugin-pwa';
+import type { VitePWAOptions } from 'vite-plugin-pwa';
+
+const pwaOptions: Partial
= {
+ registerType: 'autoUpdate',
+ devOptions: {
+ enabled: true,
+ },
+ // // add this to cache all the imports, including favicon
+ workbox: {
+ globPatterns: ['**/*.{js,css,html}', '**/*.{ico,svg,png,jpg,gif}'],
+ // maximumFileSizeToCacheInBytes: 3000000,
+ // navigateFallback: null,
+ },
+ // add this to cache all the static assets in the public folder
+ includeAssets: ['**/*'],
+ manifest: {
+ name: 'Field Mapping Tasking Manager',
+ short_name: 'FMTM',
+ description: 'Coordinated field mapping for Open Mapping campaigns.',
+ display: 'standalone',
+ theme_color: '#d63f3f',
+ background_color: '#d63f3f',
+ icons: [
+ {
+ src: 'pwa-64x64.png',
+ sizes: '64x64',
+ type: 'image/png',
+ },
+ {
+ src: 'pwa-192x192.png',
+ sizes: '192x192',
+ type: 'image/png',
+ },
+ {
+ src: 'pwa-512x512.png',
+ sizes: '512x512',
+ type: 'image/png',
+ },
+ {
+ src: 'maskable-icon-512x512.png',
+ sizes: '512x512',
+ type: 'image/png',
+ purpose: 'maskable',
+ },
+ ],
+ },
+};
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
return {
- plugins: [
- react(),
- // VitePWA({
- // registerType: 'autoUpdate',
- // devOptions: {
- // enabled: true,
- // },
- // selfDestroying: false,
- // }),
- ],
+ plugins: [react(), VitePWA(pwaOptions)],
server: {
port: 7051,
host: '0.0.0.0',