diff --git a/package.json b/package.json index c8ff39c..404067c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aa6f413..d2a1585 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: react-leaflet: specifier: ^5.0.0 version: 5.0.0(leaflet@1.9.4)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + react-router-dom: + specifier: ^7.14.2 + version: 7.14.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1) devDependencies: '@eslint/js': specifier: ^9.33.0 @@ -440,56 +443,67 @@ packages: resolution: {integrity: sha512-aL6hRwu0k7MTUESgkg7QHY6CoqPgr6gdQXRJI1/VbFlUMwsSzPGSR7sG5d+MCbYnJmJwThc2ol3nixj1fvI/zQ==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.52.0': resolution: {integrity: sha512-BTs0M5s1EJejgIBJhCeiFo7GZZ2IXWkFGcyZhxX4+8usnIo5Mti57108vjXFIQmmJaRyDwmV59Tw64Ap1dkwMw==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.52.0': resolution: {integrity: sha512-uj672IVOU9m08DBGvoPKPi/J8jlVgjh12C9GmjjBxCTQc3XtVmRkRKyeHSmIKQpvJ7fIm1EJieBUcnGSzDVFyw==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.52.0': resolution: {integrity: sha512-/+IVbeDMDCtB/HP/wiWsSzduD10SEGzIZX2945KSgZRNi4TSkjHqRJtNTVtVb8IRwhJ65ssI56krlLik+zFWkw==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.52.0': resolution: {integrity: sha512-U1vVzvSWtSMWKKrGoROPBXMh3Vwn93TA9V35PldokHGqiUbF6erSzox/5qrSMKp6SzakvyjcPiVF8yB1xKr9Pg==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.52.0': resolution: {integrity: sha512-X/4WfuBAdQRH8cK3DYl8zC00XEE6aM472W+QCycpQJeLWVnHfkv7RyBFVaTqNUMsTgIX8ihMjCvFF9OUgeABzw==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.52.0': resolution: {integrity: sha512-xIRYc58HfWDBZoLmWfWXg2Sq8VCa2iJ32B7mqfWnkx5mekekl0tMe7FHpY8I72RXEcUkaWawRvl3qA55og+cwQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.52.0': resolution: {integrity: sha512-mbsoUey05WJIOz8U1WzNdf+6UMYGwE3fZZnQqsM22FZ3wh1N887HT6jAOjXs6CNEK3Ntu2OBsyQDXfIjouI4dw==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.52.0': resolution: {integrity: sha512-qP6aP970bucEi5KKKR4AuPFd8aTx9EF6BvutvYxmZuWLJHmnq4LvBfp0U+yFDMGwJ+AIJEH5sIP+SNypauMWzg==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.52.0': resolution: {integrity: sha512-nmSVN+F2i1yKZ7rJNKO3G7ZzmxJgoQBQZ/6c4MuS553Grmr7WqR7LLDcYG53Z2m9409z3JLt4sCOhLdbKQ3HmA==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.52.0': resolution: {integrity: sha512-2d0qRo33G6TfQVjaMR71P+yJVGODrt5V6+T0BDYH4EMfGgdC/2HWDVjSSFw888GSzAZUwuska3+zxNUCDco6rQ==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openharmony-arm64@4.52.0': resolution: {integrity: sha512-A1JalX4MOaFAAyGgpO7XP5khquv/7xKzLIyLmhNrbiCxWpMlnsTYr8dnsWM7sEeotNmxvSOEL7F65j0HXFcFsw==} @@ -1039,6 +1053,10 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1430,6 +1448,23 @@ packages: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} + react-router-dom@7.14.2: + resolution: {integrity: sha512-YZcM5ES8jJSM+KrJ9BdvHHqlnGTg5tH3sC5ChFRj4inosKctdyzBDhOyyHdGk597q2OT6NTrCA1OvB/YDwfekQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.14.2: + resolution: {integrity: sha512-yCqNne6I8IB6rVCH7XUvlBK7/QKyqypBFGv+8dj4QBFJiiRX+FG7/nkdAvGElyvVZ/HQP5N19wzteuTARXi5Gw==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + react@19.1.1: resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==} engines: {node: '>=0.10.0'} @@ -1468,6 +1503,9 @@ packages: engines: {node: '>=10'} hasBin: true + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -3295,6 +3333,8 @@ snapshots: convert-source-map@2.0.0: {} + cookie@1.1.1: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -3671,6 +3711,20 @@ snapshots: react-refresh@0.17.0: {} + react-router-dom@7.14.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + dependencies: + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + react-router: 7.14.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + + react-router@7.14.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + dependencies: + cookie: 1.1.1 + react: 19.1.1 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.1.1(react@19.1.1) + react@19.1.1: {} resolve-from@4.0.0: {} @@ -3719,6 +3773,8 @@ snapshots: semver@7.7.2: {} + set-cookie-parser@2.7.2: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 diff --git a/src/App.tsx b/src/App.tsx index 3b14711..21aa0d5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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([]) - const [layerVisibility, setLayerVisibility] = useState({}) - - 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 (
- - {/**/} - +
⏳

Loading...

}> + + {/* Primary map view β€” renders map + layer controls */} + } /> + + {/* Admin dashboard β€” placeholder for future admin functionality */} + } /> + + {/* Workflow management β€” placeholder for future workflow tools */} + } /> + + {/* Root redirect to map */} + } /> + + {/* 404 fallback for unknown routes */} + } /> + + ); diff --git a/src/components/Layout/Header.tsx b/src/components/Layout/Header.tsx index d1b7a77..e567826 100644 --- a/src/components/Layout/Header.tsx +++ b/src/components/Layout/Header.tsx @@ -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); @@ -39,6 +41,37 @@ export const Header: React.FC = () => { ProgramEarth + + {/* Navigation links */} +
diff --git a/src/main.tsx b/src/main.tsx index e5775c0..b82e4a1 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -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( - + + + , ); diff --git a/src/pages/AdminPage.tsx b/src/pages/AdminPage.tsx new file mode 100644 index 0000000..9bec15c --- /dev/null +++ b/src/pages/AdminPage.tsx @@ -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 ( +
+
πŸ› οΈ
+

Admin Dashboard

+

+ Administrative tools and user management will appear here. +

+
Coming Soon
+
+ ); +}; diff --git a/src/pages/MapPage.tsx b/src/pages/MapPage.tsx new file mode 100644 index 0000000..2585470 --- /dev/null +++ b/src/pages/MapPage.tsx @@ -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([]); + const [layerVisibility, setLayerVisibility] = useState({}); + + 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 ( + <> + + {/**/} + + + ); +}; diff --git a/src/pages/NotFoundPage.tsx b/src/pages/NotFoundPage.tsx new file mode 100644 index 0000000..70efad7 --- /dev/null +++ b/src/pages/NotFoundPage.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import '@/styles/pages.css'; + +/** + * 404 Not Found page + * Displayed when a user navigates to a route that doesn't exist. + * Provides a link back to the map view. + * @component + * @returns {JSX.Element} The 404 page with navigation back to map + */ +export const NotFoundPage: React.FC = () => { + return ( +
+
πŸ—ΊοΈ
+

Page Not Found

+

+ The page you're looking for doesn't exist. +

+ + ← Back to Map + +
+ ); +}; diff --git a/src/pages/WorkflowPage.tsx b/src/pages/WorkflowPage.tsx new file mode 100644 index 0000000..d3fee3f --- /dev/null +++ b/src/pages/WorkflowPage.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import '@/styles/pages.css'; + +/** + * Workflow page placeholder + * This page will be expanded with workflow management functionality + * such as task tracking and process automation in future iterations. + * @component + * @returns {JSX.Element} The workflow page placeholder + */ +export const WorkflowPage: React.FC = () => { + return ( +
+
⚑
+

Workflow

+

+ Workflow management and task tracking will appear here. +

+
Coming Soon
+
+ ); +}; diff --git a/src/styles/pages.css b/src/styles/pages.css new file mode 100644 index 0000000..3483508 --- /dev/null +++ b/src/styles/pages.css @@ -0,0 +1,128 @@ +/* Placeholder page styles for Admin, Workflow, and NotFound pages */ +.page-placeholder { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + gap: 16px; + padding: 40px; + text-align: center; +} + +.page-placeholder__icon { + font-size: 64px; + line-height: 1; + filter: grayscale(0.2); + animation: float 3s ease-in-out infinite; +} + +@keyframes float { + 0%, 100% { transform: translateY(0px); } + 50% { transform: translateY(-10px); } +} + +.page-placeholder__title { + font-size: 28px; + font-weight: 700; + color: #2c3e50; + margin: 0; + letter-spacing: -0.5px; +} + +.dark .page-placeholder__title { + color: #e2e8f0; +} + +.page-placeholder__description { + font-size: 16px; + color: #64748b; + margin: 0; + max-width: 400px; + line-height: 1.6; +} + +.dark .page-placeholder__description { + color: #94a3b8; +} + +.page-placeholder__badge { + display: inline-flex; + align-items: center; + padding: 6px 16px; + border-radius: 20px; + font-size: 13px; + font-weight: 600; + letter-spacing: 0.5px; + text-transform: uppercase; + background: linear-gradient(135deg, rgba(102, 126, 234, 0.15), rgba(118, 75, 162, 0.15)); + color: #667eea; + border: 1px solid rgba(102, 126, 234, 0.3); +} + +.dark .page-placeholder__badge { + background: linear-gradient(135deg, rgba(102, 126, 234, 0.2), rgba(118, 75, 162, 0.2)); + color: #a5b4fc; + border-color: rgba(102, 126, 234, 0.4); +} + +.page-placeholder__link { + display: inline-flex; + align-items: center; + padding: 10px 24px; + border-radius: 12px; + font-size: 14px; + font-weight: 600; + text-decoration: none; + color: #fff; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); +} + +.page-placeholder__link:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4); +} + +/* Navigation link styles for Header */ +.header-nav { + display: flex; + align-items: center; + gap: 4px; +} + +.header-nav__link { + padding: 8px 16px; + border-radius: 10px; + font-size: 14px; + font-weight: 500; + text-decoration: none; + color: #4a5568; + transition: all 0.2s ease; +} + +.header-nav__link:hover { + background: rgba(102, 126, 234, 0.1); + color: #667eea; +} + +.header-nav__link--active { + background: rgba(102, 126, 234, 0.15); + color: #667eea; + font-weight: 600; +} + +.dark .header-nav__link { + color: #a0aec0; +} + +.dark .header-nav__link:hover { + background: rgba(102, 126, 234, 0.2); + color: #a5b4fc; +} + +.dark .header-nav__link--active { + background: rgba(102, 126, 234, 0.25); + color: #a5b4fc; +}