Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"lucide-react": "^0.540.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-leaflet": "^5.0.0"
"react-leaflet": "^5.0.0",
"react-router-dom": "^7.14.2"
},
"devDependencies": {
"@eslint/js": "^9.33.0",
Expand Down
56 changes: 56 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

63 changes: 30 additions & 33 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,48 +1,45 @@
import { useEffect, useState } from 'react';
import { Suspense, lazy } from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
import { Header } from '@/components/Layout/Header';
import { MapContainer } from '@/components/Map/MapContainer';
import { MapLegend } from '@/components/Map/MapLegend';
import { LayerControls } from '@/components/Map/LayerControls';
import '@/styles/globals.css';
import '@/styles/map.css';
import { FixtureReader } from './data/fixture-reader';
import type { LayerVisibilityMap } from './types/map';
import type { FeatureCollection } from './types/geometry';

// Lazy-load page components for code splitting
const MapPage = lazy(() => import('@/pages/MapPage').then(m => ({ default: m.MapPage })));
const AdminPage = lazy(() => import('@/pages/AdminPage').then(m => ({ default: m.AdminPage })));
const WorkflowPage = lazy(() => import('@/pages/WorkflowPage').then(m => ({ default: m.WorkflowPage })));
const NotFoundPage = lazy(() => import('@/pages/NotFoundPage').then(m => ({ default: m.NotFoundPage })));

/**
* Main application component that composes the entire UI
* Manages the map state and renders the map with its controls
* Main application component that handles routing and layout
* Provides the foundational navigation structure for the application
* with routes for Map View, Admin Dashboard, and Workflow pages.
* @component
* @returns {JSX.Element} The complete application layout with header and map interface
* @returns {JSX.Element} The complete application layout with routing
*/
function App() {
const [layers, setLayers] = useState<FeatureCollection[]>([])
const [layerVisibility, setLayerVisibility] = useState<LayerVisibilityMap>({})

useEffect(() => {
FixtureReader.collections()
.then(collections => {
setLayers([...collections])

// Take the name property of each collection and set it's initial visibility to true
const layerNames = collections.map((fc) => fc.name )
const visibilityMap = layerNames.reduce((map, name) => { map[name] = true; return map }, {} as LayerVisibilityMap)
setLayerVisibility({...visibilityMap})
},)
}, [])

const layersToRender = layers.filter((fc) => layerVisibility[fc.name])

return (
<div className="app-container">
<Header />
<main className="main-content">
<MapContainer layers={layersToRender} />
{/*<MapLegend />*/}
<LayerControls
visibilityMap={layerVisibility}
onLayerChange={setLayerVisibility}
/>
<Suspense fallback={<div className="page-placeholder"><div className="page-placeholder__icon">⏳</div><p>Loading...</p></div>}>
<Routes>
{/* Primary map view — renders map + layer controls */}
<Route path="/map" element={<MapPage />} />

{/* Admin dashboard — placeholder for future admin functionality */}
<Route path="/admin" element={<AdminPage />} />

{/* Workflow management — placeholder for future workflow tools */}
<Route path="/workflow" element={<WorkflowPage />} />

{/* Root redirect to map */}
<Route path="/" element={<Navigate to="/map" replace />} />

{/* 404 fallback for unknown routes */}
<Route path="*" element={<NotFoundPage />} />
</Routes>
</Suspense>
</main>
</div>
);
Expand Down
39 changes: 36 additions & 3 deletions src/components/Layout/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React, { useState, useEffect } from 'react';
import { Globe, Moon, Sun } from 'lucide-react';
import { NavLink } from 'react-router-dom';
import { Globe, Moon, Sun, Map, Settings, Workflow } from 'lucide-react';
import '@/styles/pages.css';

/**
* Header component with the application logo, title, and dark mode toggle
* Header component with the application logo, navigation links, and dark mode toggle
* @component
* @returns {JSX.Element} The application header with ProgramEarth branding and dark mode toggle
* @returns {JSX.Element} The application header with ProgramEarth branding, navigation, and dark mode toggle
*/
export const Header: React.FC = () => {
const [isDarkMode, setIsDarkMode] = useState(false);
Expand Down Expand Up @@ -39,6 +41,37 @@ export const Header: React.FC = () => {
</div>
<span>ProgramEarth</span>
</div>

{/* Navigation links */}
<nav className="header-nav">
<NavLink
to="/map"
className={({ isActive }) =>
`header-nav__link ${isActive ? 'header-nav__link--active' : ''}`
}
>
<Map size={16} style={{ marginRight: 6, verticalAlign: 'middle' }} />
Map
</NavLink>
<NavLink
to="/admin"
className={({ isActive }) =>
`header-nav__link ${isActive ? 'header-nav__link--active' : ''}`
}
>
<Settings size={16} style={{ marginRight: 6, verticalAlign: 'middle' }} />
Admin
</NavLink>
<NavLink
to="/workflow"
className={({ isActive }) =>
`header-nav__link ${isActive ? 'header-nav__link--active' : ''}`
}
>
<Workflow size={16} style={{ marginRight: 6, verticalAlign: 'middle' }} />
Workflow
</NavLink>
</nav>

<div className="header-controls">
<div className="dark-mode-container">
Expand Down
5 changes: 4 additions & 1 deletion src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App.tsx';

ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>,
);
22 changes: 22 additions & 0 deletions src/pages/AdminPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';
import '@/styles/pages.css';

/**
* Admin Dashboard placeholder page
* This page will be expanded with admin functionality
* such as user management and data oversight in future iterations.
* @component
* @returns {JSX.Element} The admin dashboard placeholder
*/
export const AdminPage: React.FC = () => {
return (
<div className="page-placeholder">
<div className="page-placeholder__icon">🛠️</div>
<h1 className="page-placeholder__title">Admin Dashboard</h1>
<p className="page-placeholder__description">
Administrative tools and user management will appear here.
</p>
<div className="page-placeholder__badge">Coming Soon</div>
</div>
);
};
43 changes: 43 additions & 0 deletions src/pages/MapPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useEffect, useState } from 'react';
import { MapContainer } from '@/components/Map/MapContainer';
import { LayerControls } from '@/components/Map/LayerControls';
import { FixtureReader } from '@/data/fixture-reader';
import type { LayerVisibilityMap } from '@/types/map';
import type { FeatureCollection } from '@/types/geometry';

/**
* Map Page - renders the interactive map with layer controls
* This is the primary view of the application, displaying
* geospatial data via Leaflet and allowing users to toggle layers.
* @component
* @returns {JSX.Element} The map view with layer controls
*/
export const MapPage = () => {
const [layers, setLayers] = useState<FeatureCollection[]>([]);
const [layerVisibility, setLayerVisibility] = useState<LayerVisibilityMap>({});

useEffect(() => {
FixtureReader.collections()
.then(collections => {
setLayers([...collections]);

// Take the name property of each collection and set it's initial visibility to true
const layerNames = collections.map((fc) => fc.name);
const visibilityMap = layerNames.reduce((map, name) => { map[name] = true; return map; }, {} as LayerVisibilityMap);
setLayerVisibility({ ...visibilityMap });
});
}, []);
Comment on lines +19 to +29
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n --type ts 'FixtureReader\.collections\(|useEffect\(' src/pages src/data

Repository: OpenSourceFellows/map_dashboard_hackathon

Length of output: 187


🏁 Script executed:

cat -n src/pages/MapPage.tsx | head -50

Repository: OpenSourceFellows/map_dashboard_hackathon

Length of output: 1940


🏁 Script executed:

rg -n 'FixtureReader' src/ -A 5 -B 2

Repository: OpenSourceFellows/map_dashboard_hackathon

Length of output: 2305


🏁 Script executed:

fd -t ts -name 'FixtureReader*'

Repository: OpenSourceFellows/map_dashboard_hackathon

Length of output: 316


🏁 Script executed:

cat -n src/data/fixture-reader.ts

Repository: OpenSourceFellows/map_dashboard_hackathon

Length of output: 909


Handle fixture loading failures and unmounts.

FixtureReader.collections() is fire-and-forget here: a rejected fetch (line 13 of fixture-reader.ts throws on failed response) will surface as an unhandled promise rejection, and state updates can still run after the user navigates away from the map. Add a cleanup guard and .catch() handler before setting layers and layerVisibility.

♻️ Suggested fix
 useEffect(() => {
-    FixtureReader.collections()
-      .then(collections => {
-        setLayers([...collections]);
-
-        // Take the name property of each collection and set it's initial visibility to true
-        const layerNames = collections.map((fc) => fc.name);
-        const visibilityMap = layerNames.reduce((map, name) => { map[name] = true; return map; }, {} as LayerVisibilityMap);
-        setLayerVisibility({ ...visibilityMap });
-      });
+    let cancelled = false;
+
+    FixtureReader.collections()
+      .then((collections) => {
+        if (cancelled) return;
+
+        setLayers(collections);
+        const visibilityMap = collections.reduce((map, fc) => {
+          map[fc.name] = true;
+          return map;
+        }, {} as LayerVisibilityMap);
+        setLayerVisibility(visibilityMap);
+      })
+      .catch((error) => {
+        console.error('Failed to load map layers', error);
+      });
+
+    return () => {
+      cancelled = true;
+    };
   }, []);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
FixtureReader.collections()
.then(collections => {
setLayers([...collections]);
// Take the name property of each collection and set it's initial visibility to true
const layerNames = collections.map((fc) => fc.name);
const visibilityMap = layerNames.reduce((map, name) => { map[name] = true; return map; }, {} as LayerVisibilityMap);
setLayerVisibility({ ...visibilityMap });
});
}, []);
useEffect(() => {
let cancelled = false;
FixtureReader.collections()
.then((collections) => {
if (cancelled) return;
setLayers(collections);
const visibilityMap = collections.reduce((map, fc) => {
map[fc.name] = true;
return map;
}, {} as LayerVisibilityMap);
setLayerVisibility(visibilityMap);
})
.catch((error) => {
console.error('Failed to load map layers', error);
});
return () => {
cancelled = true;
};
}, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/MapPage.tsx` around lines 19 - 29, The effect calling
FixtureReader.collections() lacks error handling and an unmount guard: catch
promise rejections from FixtureReader.collections() and handle errors (e.g., log
or show fallback) before attempting to setLayers or setLayerVisibility, and add
a cleanup guard (e.g., isMounted flag or AbortController) inside the useEffect
so you only call setLayers and setLayerVisibility (and construct the
LayerVisibilityMap) when the component is still mounted; ensure the promise
chain uses .catch(...) and the cleanup toggles the guard to prevent state
updates after unmount.


const layersToRender = layers.filter((fc) => layerVisibility[fc.name]);

return (
<>
<MapContainer layers={layersToRender} />
{/*<MapLegend />*/}
<LayerControls
visibilityMap={layerVisibility}
onLayerChange={setLayerVisibility}
/>
</>
);
};
Loading