├── docs ├── Product Backlog.md ├── sprint backlogs │ ├── Sprint 0.md │ ├── Sprint 1.md │ └── Sprint 2.md ├── sprint plans │ └── Sprint Plan 1.md ├── api docs │ └── .gitkeep ├── .gitkeep └── DoD.md ├── backend ├── config │ ├── .gitkeep │ └── db.js ├── models │ ├── .gitkeep │ ├── User.js │ └── Report.js ├── routes │ ├── .gitkeep │ ├── authRoutes.js │ ├── userRoutes.js │ └── reportRoutes.js ├── utils │ └── .gitkeep ├── controllers │ ├── .gitkeep │ ├── userController.js │ ├── authController.js │ └── reportController.js ├── middleware │ ├── .gitkeep │ ├── error.js │ ├── roles.js │ └── auth.js ├── example.env ├── vercel.json ├── package.json ├── app.js ├── test │ └── auth.test.js └── server.js ├── frontend ├── src │ ├── assets │ │ └── .gitkeep │ ├── hooks │ │ └── .gitkeep │ ├── pages │ │ ├── .gitkeep │ │ ├── Login.js │ │ ├── EcoComplianceHub.js │ │ ├── TopicDetail.js │ │ ├── Home.js │ │ └── QuizPage.js │ ├── utils │ │ └── .gitkeep │ ├── components │ │ ├── .gitkeep │ │ ├── FormCard.js │ │ ├── PageHeader.js │ │ ├── TextBox.js │ │ ├── SelectBox.js │ │ ├── Button.js │ │ ├── StatsCards.js │ │ ├── Header.js │ │ ├── FeaturesGrid.js │ │ ├── Footer.js │ │ ├── TechnologyStack.js │ │ └── UserDropdown.js │ ├── services │ │ ├── .gitkeep │ │ ├── authService.js │ │ ├── api.js │ │ └── reportService.js │ ├── routes │ │ ├── index.js │ │ └── ProtectedRoute.js │ ├── setupTests.js │ ├── reportWebVitals.js │ ├── index.js │ ├── App.css │ ├── index.css │ ├── context │ │ └── AuthContext.js │ ├── App.js │ ├── logo.svg │ ├── Styles │ │ └── global.css │ └── __tests__ │ │ └── Home.test.jsx ├── public │ ├── robots.txt │ ├── mpa.jpg │ ├── nav1.jpg │ ├── nav2.jpg │ ├── nav4.jpg │ ├── nav6.jpg │ ├── wild.jpg │ ├── anchor.jpg │ ├── favicon.ico │ ├── fishing.jpg │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── index.html ├── postcss.config.js ├── frontend-exmaple.env ├── tailwind.config.js ├── package.json └── README.md ├── .github └── workflows │ ├── lint.yml │ ├── build.yml │ ├── security.yml │ └── test.yml ├── RESPONSIBILITIES.md └── README.md /docs/Product Backlog.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/sprint backlogs/Sprint 0.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/sprint backlogs/Sprint 1.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/sprint backlogs/Sprint 2.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/sprint plans/Sprint Plan 1.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/api docs/.gitkeep: -------------------------------------------------------------------------------- 1 | # Documnet the APIs in this folder -------------------------------------------------------------------------------- /backend/config/.gitkeep: -------------------------------------------------------------------------------- 1 | # placeholder file to demostrate the folder structure 2 | -------------------------------------------------------------------------------- /backend/models/.gitkeep: -------------------------------------------------------------------------------- 1 | # placeholder file to demostrate the folder structure 2 | -------------------------------------------------------------------------------- /backend/routes/.gitkeep: -------------------------------------------------------------------------------- 1 | # placeholder file to demostrate the folder structure 2 | -------------------------------------------------------------------------------- /backend/utils/.gitkeep: -------------------------------------------------------------------------------- 1 | # placeholder file to demostrate the folder structure 2 | -------------------------------------------------------------------------------- /backend/controllers/.gitkeep: -------------------------------------------------------------------------------- 1 | # placeholder file to demostrate the folder structure 2 | -------------------------------------------------------------------------------- /backend/middleware/.gitkeep: -------------------------------------------------------------------------------- 1 | # placeholder file to demostrate the folder structure 2 | -------------------------------------------------------------------------------- /frontend/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | # placeholder file to demostrate the folder structure 2 | -------------------------------------------------------------------------------- /frontend/src/hooks/.gitkeep: -------------------------------------------------------------------------------- 1 | # placeholder file to demostrate the folder structure 2 | -------------------------------------------------------------------------------- /frontend/src/pages/.gitkeep: -------------------------------------------------------------------------------- 1 | # placeholder file to demostrate the folder structure 2 | -------------------------------------------------------------------------------- /frontend/src/utils/.gitkeep: -------------------------------------------------------------------------------- 1 | # placeholder file to demostrate the folder structure 2 | -------------------------------------------------------------------------------- /frontend/src/components/.gitkeep: -------------------------------------------------------------------------------- 1 | # placeholder file to demostrate the folder structure 2 | -------------------------------------------------------------------------------- /frontend/src/services/.gitkeep: -------------------------------------------------------------------------------- 1 | # placeholder file to demostrate the folder structure 2 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/public/mpa.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sasmithaK/MPA-Navigation-System/HEAD/frontend/public/mpa.jpg -------------------------------------------------------------------------------- /frontend/public/nav1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sasmithaK/MPA-Navigation-System/HEAD/frontend/public/nav1.jpg -------------------------------------------------------------------------------- /frontend/public/nav2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sasmithaK/MPA-Navigation-System/HEAD/frontend/public/nav2.jpg -------------------------------------------------------------------------------- /frontend/public/nav4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sasmithaK/MPA-Navigation-System/HEAD/frontend/public/nav4.jpg -------------------------------------------------------------------------------- /frontend/public/nav6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sasmithaK/MPA-Navigation-System/HEAD/frontend/public/nav6.jpg -------------------------------------------------------------------------------- /frontend/public/wild.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sasmithaK/MPA-Navigation-System/HEAD/frontend/public/wild.jpg -------------------------------------------------------------------------------- /frontend/src/routes/index.js: -------------------------------------------------------------------------------- 1 | // Route Components 2 | export { default as ProtectedRoute } from './ProtectedRoute'; 3 | -------------------------------------------------------------------------------- /frontend/public/anchor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sasmithaK/MPA-Navigation-System/HEAD/frontend/public/anchor.jpg -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sasmithaK/MPA-Navigation-System/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/fishing.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sasmithaK/MPA-Navigation-System/HEAD/frontend/public/fishing.jpg -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sasmithaK/MPA-Navigation-System/HEAD/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sasmithaK/MPA-Navigation-System/HEAD/frontend/public/logo512.png -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | '@tailwindcss/postcss': {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /frontend/frontend-exmaple.env: -------------------------------------------------------------------------------- 1 | #Configuration file for environment variables 2 | REACT_ENV=development 3 | 4 | # Backend API URL for Frontend 5 | REACT_APP_API_URL= 6 | 7 | -------------------------------------------------------------------------------- /docs/.gitkeep: -------------------------------------------------------------------------------- 1 | # placeholder folder for all the document artifacts needed 2 | # Product Backlog 3 | # Sprint Backlog 4 | # Wire frames 5 | # Definition of Done ✅ 6 | # Sprint Reports 7 | # Burndown Chart -------------------------------------------------------------------------------- /backend/example.env: -------------------------------------------------------------------------------- 1 | # Server Config 2 | PORT= 3 | NODE_ENV= 4 | 5 | # Database 6 | MONGO_URI= 7 | 8 | # JWT Authentication 9 | JWT_SECRET= 10 | JWT_EXPIRES_IN= 11 | 12 | # Frontend 13 | CLIENT_URL= 14 | -------------------------------------------------------------------------------- /backend/middleware/error.js: -------------------------------------------------------------------------------- 1 | export default function errorHandler(err, _req, res, _next){ 2 | console.error(err); 3 | const status = err.statusCode || 500; 4 | res.status(status).json({message: err.message || "Server error"}); 5 | } -------------------------------------------------------------------------------- /frontend/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /backend/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "server.js", 6 | "use": "@vercel/node" 7 | } 8 | ], 9 | "routes": [ 10 | { 11 | "src": "/(.*)", 12 | "dest": "server.js" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint Code 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: actions/setup-node@v4 11 | with: 12 | node-version: 20 13 | - run: npm ci 14 | - run: npm run lint 15 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Verify Build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: actions/setup-node@v4 12 | with: 13 | node-version: 20 14 | - run: npm ci 15 | - run: npm run build 16 | -------------------------------------------------------------------------------- /.github/workflows/security.yml: -------------------------------------------------------------------------------- 1 | name: Security Scan 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | analyze: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: github/codeql-action/init@v3 11 | with: 12 | languages: javascript 13 | - uses: github/codeql-action/analyze@v3 14 | -------------------------------------------------------------------------------- /backend/config/db.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | export default async function connectDB() { 4 | const uri = process.env.MONGO_URI; 5 | if (!uri) throw new Error("MongoDB URI is not defined in environment variables"); 6 | mongoose.set('strictQuery', true); 7 | await mongoose.connect(uri); 8 | console.log("MongoDB connected"); 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/components/FormCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const FormCard = ({ 4 | children, 5 | className = '' 6 | }) => { 7 | return ( 8 |
9 | {children} 10 |
11 | ); 12 | }; 13 | 14 | export default FormCard; 15 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-node@v4 12 | with: 13 | node-version: '22' 14 | 15 | - run: npm ci 16 | - run: npm test -------------------------------------------------------------------------------- /backend/middleware/roles.js: -------------------------------------------------------------------------------- 1 | export function requireRoles(...allowed) { 2 | return (req, res, next) => { 3 | if (!req.user) return res.status(401).json ({ message: "unauthorized "}); 4 | if (!allowed.includes(req.user.role)) { 5 | 6 | return res.status(403).json({ message: "Forbidden: No role assigned" }); 7 | } 8 | next(); 9 | 10 | }; 11 | } -------------------------------------------------------------------------------- /frontend/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /docs/DoD.md: -------------------------------------------------------------------------------- 1 | # Definition of Done (DoD) - EcoMarineWay 2 | 3 | A backlog item is considered **Done** when the following criteria are met: 4 | 5 | - Code is written, committed, and pushed to the repository 6 | - Code is reviewed and approved by at least one teammate 7 | - Feature works as expected and passes manual/automated tests 8 | - No high/critical bugs remain open 9 | - Integrated into the main branch without breaking existing features 10 | - Documentation (if needed) is updated 11 | 12 | This ensures consistency, quality, and shared understanding across the team. 13 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | const root = ReactDOM.createRoot(document.getElementById('root')); 8 | root.render( 9 | 10 | 11 | 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /backend/middleware/auth.js: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | import User from '../models/User.js'; 3 | 4 | export default async function auth(req, res, next) { 5 | try{ 6 | const header = req.headers.authorization || ""; 7 | const token = header.startsWith("Bearer") ? header.slice(7): null; 8 | if(!token) return res.status(401).json({message: "Unauthorized: No token provided"}); 9 | 10 | const payload = jwt.verify(token, process.env.JWT_SECRET); 11 | const user = await User.findById(payload.sub); 12 | if (!user) { 13 | return res.status(401).json({message: "Unauthorized: Invalid user"}); 14 | } 15 | 16 | req.user = user; 17 | next(); 18 | } catch { 19 | return res.status(401).json({message: "Unauthorized: Invalid token"}); 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "type": "module", 6 | "scripts": { 7 | "start": "node server.js", 8 | "test": "cross-env NODE_ENV=test NODE_OPTIONS=--experimental-vm-modules jest --detectOpenHandles --forceExit" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "description": "", 14 | "dependencies": { 15 | "bcrypt": "^6.0.0", 16 | "bcryptjs": "^3.0.2", 17 | "cookie-parser": "^1.4.7", 18 | "cors": "^2.8.5", 19 | "dotenv": "^17.2.1", 20 | "express": "^5.1.0", 21 | "express-validator": "^7.2.1", 22 | "jsonwebtoken": "^9.0.2", 23 | "mongoose": "^8.18.0", 24 | "morgan": "^1.10.1" 25 | }, 26 | "devDependencies": { 27 | "cross-env": "^10.0.0", 28 | "jest": "^30.1.3", 29 | "nodemon": "^3.1.10", 30 | "supertest": "^7.1.4" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /backend/app.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import cors from "cors"; 3 | import morgan from "morgan"; 4 | import cookieParser from "cookie-parser"; 5 | import authRoutes from "./routes/authRoutes.js"; 6 | import userRoutes from "./routes/userRoutes.js"; 7 | import reportRoutes from "./routes/reportRoutes.js"; 8 | import errorHandler from "./middleware/error.js"; 9 | 10 | const app = express(); 11 | 12 | app.use(morgan("dev")); 13 | app.use(express.json()); 14 | app.use(cookieParser()); 15 | app.use( 16 | cors({ 17 | origin: process.env.CORS_ORIGIN || "*", 18 | credentials: true, 19 | }) 20 | ); 21 | 22 | app.get("/", (_req, res) => res.json({ status: "ok", service: "maritime-api" })); 23 | 24 | app.use("/api/auth", authRoutes); 25 | app.use("/api/users", userRoutes); 26 | app.use("/api/reports", reportRoutes); 27 | 28 | // error handler last 29 | app.use(errorHandler); 30 | 31 | export default app; 32 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); 6 | 7 | body { 8 | margin: 0; 9 | font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 10 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 11 | sans-serif; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | code { 17 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 18 | monospace; 19 | } 20 | body { 21 | margin: 0; 22 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 23 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 24 | sans-serif; 25 | -webkit-font-smoothing: antialiased; 26 | -moz-osx-font-smoothing: grayscale; 27 | } 28 | 29 | code { 30 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 31 | monospace; 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/services/authService.js: -------------------------------------------------------------------------------- 1 | import api from './api'; 2 | 3 | export const authService = { 4 | // Register new user 5 | register: async (userData) => { 6 | const response = await api.post('/auth/register', userData); 7 | return response.data; 8 | }, 9 | 10 | // Login user 11 | login: async (credentials) => { 12 | const response = await api.post('/auth/login', credentials); 13 | return response.data; 14 | }, 15 | 16 | // Logout user 17 | logout: () => { 18 | localStorage.removeItem('token'); 19 | localStorage.removeItem('user'); 20 | }, 21 | 22 | // Get current user 23 | getCurrentUser: () => { 24 | const user = localStorage.getItem('user'); 25 | return user ? JSON.parse(user) : null; 26 | }, 27 | 28 | // Check if user is authenticated 29 | isAuthenticated: () => { 30 | return !!localStorage.getItem('token'); 31 | }, 32 | 33 | // Set auth data 34 | setAuthData: (token, user) => { 35 | localStorage.setItem('token', token); 36 | localStorage.setItem('user', JSON.stringify(user)); 37 | }, 38 | }; 39 | 40 | export default authService; 41 | -------------------------------------------------------------------------------- /frontend/src/services/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const API_BASE_URL = process.env.REACT_APP_API_URL + '/api'; 4 | console.log("ENV API URL:", API_BASE_URL); 5 | 6 | // Create axios instance with default config 7 | const api = axios.create({ 8 | baseURL: API_BASE_URL, 9 | headers: { 10 | 'Content-Type': 'application/json', 11 | }, 12 | }); 13 | 14 | // Request interceptor to add auth token 15 | api.interceptors.request.use( 16 | (config) => { 17 | const token = localStorage.getItem('token'); 18 | if (token) { 19 | config.headers.Authorization = `Bearer ${token}`; 20 | } 21 | return config; 22 | }, 23 | (error) => { 24 | return Promise.reject(error); 25 | } 26 | ); 27 | 28 | // Response interceptor to handle errors 29 | api.interceptors.response.use( 30 | (response) => response, 31 | (error) => { 32 | if (error.response?.status === 401) { 33 | localStorage.removeItem('token'); 34 | localStorage.removeItem('user'); 35 | window.location.href = '/login'; 36 | } 37 | return Promise.reject(error); 38 | } 39 | ); 40 | 41 | export default api; 42 | -------------------------------------------------------------------------------- /backend/routes/authRoutes.js: -------------------------------------------------------------------------------- 1 | // routes/authRoutes.js 2 | import { Router } from "express"; 3 | import { body } from "express-validator"; 4 | import { register, login, getProfile, updateProfile, deleteProfile } from "../controllers/authController.js"; 5 | import auth from "../middleware/auth.js"; 6 | 7 | const router = Router(); 8 | 9 | router.post( 10 | "/register", 11 | [ 12 | body("name").notEmpty().withMessage("Name is required"), 13 | body("email").isEmail().withMessage("Invalid email"), 14 | body("password").isLength({ min: 5 }).withMessage("Password must be at least 5 characters"), 15 | body("role").notEmpty().withMessage("Role is required"), 16 | body("vesselName").notEmpty().withMessage("Vessel name is required"), 17 | body("vesselType").notEmpty().withMessage("Vessel type is required"), 18 | ], 19 | register 20 | ); 21 | 22 | router.post( 23 | "/login", 24 | [ 25 | body("email").isEmail().withMessage("Invalid email"), 26 | body("password").notEmpty().withMessage("Password is required"), 27 | ], 28 | login 29 | ); 30 | 31 | router.get("/getProfile", auth, getProfile); 32 | router.put("/updateProfile", auth, updateProfile); 33 | router.delete("/deleteProfile",auth, deleteProfile); 34 | 35 | export default router; 36 | -------------------------------------------------------------------------------- /frontend/src/components/PageHeader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const PageHeader = ({ 4 | title, 5 | subtitle, 6 | icon, 7 | className = '' 8 | }) => { 9 | return ( 10 |
11 | {icon && ( 12 |
13 | {icon} 14 |
15 | )} 16 |

17 | {title} 18 |

19 | {subtitle && ( 20 |

21 | {subtitle} 22 |

23 | )} 24 |
25 |
26 | Maritime Navigation 27 |
28 |
29 |
30 | ); 31 | }; 32 | 33 | export default PageHeader; 34 | -------------------------------------------------------------------------------- /frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./src/**/*.{js,jsx,ts,tsx}", 5 | ], 6 | theme: { 7 | extend: { 8 | colors: { 9 | primary: { 10 | 50: '#eff6ff', 100: '#dbeafe', 200: '#bfdbfe', 300: '#93c5fd', 400: '#60a5fa', 11 | 500: '#3b82f6', 600: '#2563eb', 700: '#1d4ed8', 800: '#1e40af', 900: '#1e3a8a', 12 | }, 13 | maritime: { 14 | 50: '#f0f9ff', 100: '#e0f2fe', 200: '#bae6fd', 300: '#7dd3fc', 400: '#38bdf8', 15 | 500: '#0ea5e9', 600: '#0284c7', 700: '#0369a1', 800: '#075985', 900: '#0c4a6e', 16 | }, 17 | navy: { 18 | 50: '#f0f4ff', 19 | 100: '#e0e9ff', 20 | 200: '#c7d2fe', 21 | 300: '#a5b4fc', 22 | 400: '#818cf8', 23 | 500: '#6366f1', 24 | 600: '#4f46e5', 25 | 700: '#4338ca', 26 | 800: '#3730a3', 27 | 900: '#312e81', 28 | } 29 | }, 30 | fontFamily: { 31 | sans: ['Inter', 'system-ui', 'sans-serif'], 32 | }, 33 | backgroundImage: { 34 | 'maritime-gradient': 'linear-gradient(135deg, #1e3a8a 0%, #3b82f6 100%)', 35 | 'ocean-gradient': 'linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%)', 36 | } 37 | }, 38 | }, 39 | plugins: [], 40 | } 41 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@tailwindcss/postcss": "^4.1.14", 7 | "@testing-library/dom": "^10.4.1", 8 | "@testing-library/user-event": "^13.5.0", 9 | "axios": "^1.12.2", 10 | "lucide-react": "^0.541.0", 11 | "react": "^19.1.1", 12 | "react-dom": "^19.1.1", 13 | "react-hook-form": "^7.64.0", 14 | "react-hot-toast": "^2.6.0", 15 | "react-icons": "^5.5.0", 16 | "react-router-dom": "^6.8.0", 17 | "react-scripts": "5.0.1", 18 | "web-vitals": "^2.1.4" 19 | }, 20 | "scripts": { 21 | "start": "react-scripts start", 22 | "build": "react-scripts build", 23 | "test": "react-scripts test", 24 | "eject": "react-scripts eject" 25 | }, 26 | "eslintConfig": { 27 | "extends": [ 28 | "react-app", 29 | "react-app/jest" 30 | ] 31 | }, 32 | "browserslist": { 33 | "production": [ 34 | ">0.2%", 35 | "not dead", 36 | "not op_mini all" 37 | ], 38 | "development": [ 39 | "last 1 chrome version", 40 | "last 1 firefox version", 41 | "last 1 safari version" 42 | ] 43 | }, 44 | "devDependencies": { 45 | "@testing-library/jest-dom": "^6.8.0", 46 | "@testing-library/react": "^16.3.0", 47 | "autoprefixer": "^10.4.16", 48 | "jest": "^27.5.1", 49 | "postcss": "^8.4.32", 50 | "tailwindcss": "^3.4.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /backend/routes/userRoutes.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import {body} from "express-validator"; 3 | import { listUsers, getUserById, createUser, deleteUser, updateUser } from "../controllers/userController.js"; 4 | import auth from "../middleware/auth.js"; 5 | import {requireRoles} from "../middleware/roles.js"; 6 | 7 | const router = Router(); 8 | 9 | router.use(auth); 10 | 11 | //rules for the validations 12 | 13 | const userValidationRules = [ 14 | body("name").notEmpty().withMessage("Name is required"), 15 | body("email").isEmail() 16 | .matches(/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/) 17 | .withMessage("Valid email is required"), 18 | body("password") 19 | .optional({ checkFalsy: true }) // optional for updates 20 | .isLength({ min: 6 }) 21 | .withMessage("Password must be at least 6 characters"), 22 | body("role").optional().isIn(["sailor", "captain", "admin"]), 23 | body("vesselName").notEmpty().withMessage("Vessel name is required"), 24 | body("vesselType").notEmpty().withMessage("Vessel type is required").isIn(["cargo", "fishing", "pleasure", "tanker", "passenger", "other"]), 25 | body("isActive").optional().isBoolean(), 26 | ]; 27 | 28 | // Routes 29 | router.get("/", auth, requireRoles("admin"), listUsers); 30 | router.get("/:id", auth, requireRoles("admin"), getUserById); 31 | router.post("/", auth, requireRoles("admin"), userValidationRules, createUser); 32 | router.put("/:id", auth, requireRoles("admin"), userValidationRules, updateUser); 33 | router.delete("/:id", auth, requireRoles("admin"), deleteUser); 34 | 35 | export default router; -------------------------------------------------------------------------------- /frontend/src/components/TextBox.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const TextBox = ({ 4 | id, 5 | type = 'text', 6 | label, 7 | placeholder, 8 | icon, 9 | register, 10 | error, 11 | className = '', 12 | ...props 13 | }) => { 14 | return ( 15 |
16 | {label && ( 17 | 20 | )} 21 |
22 | {icon && ( 23 |
24 | {icon} 25 |
26 | )} 27 | 37 |
38 | {error && ( 39 |

40 | 41 | 42 | 43 | {error} 44 |

45 | )} 46 |
47 | ); 48 | }; 49 | 50 | export default TextBox; 51 | -------------------------------------------------------------------------------- /backend/test/auth.test.js: -------------------------------------------------------------------------------- 1 | import request from "supertest"; 2 | import mongoose from "mongoose"; 3 | import app from "../app.js"; 4 | import User from "../models/User.js"; 5 | import dotenv from "dotenv"; 6 | 7 | dotenv.config({ path: ".env" }); // Ensure env vars are loaded 8 | 9 | describe("Auth API", () => { 10 | beforeAll(async () => { 11 | if (!process.env.MONGO_URI) throw new Error("MONGO_URI not defined in .env"); 12 | await mongoose.connect(process.env.MONGO_URI); 13 | }); 14 | 15 | afterEach(async () => { 16 | await User.deleteMany({ email: /testuser/ }); 17 | }); 18 | 19 | afterAll(async () => { 20 | await mongoose.connection.close(); 21 | }); 22 | 23 | it("should register a new user", async () => { 24 | const res = await request(app) 25 | .post("/api/auth/register") 26 | .send({ 27 | name: "Test User", 28 | email: `testuser${Date.now()}@example.com`, 29 | password: "password123", 30 | role: "captain", 31 | vesselName: "Voyager", 32 | vesselType: "cargo", 33 | }); 34 | 35 | expect(res.statusCode).toBe(201); 36 | expect(res.body).toHaveProperty("token"); 37 | }); 38 | 39 | it("should login an existing user", async () => { 40 | const email = `testuser${Date.now()}@example.com`; 41 | const password = "password123"; 42 | 43 | await request(app).post("/api/auth/register").send({ 44 | name: "Login User", 45 | email, 46 | password, 47 | role: "captain", 48 | vesselName: "Navigator", 49 | vesselType: "cargo", 50 | }); 51 | 52 | const res = await request(app).post("/api/auth/login").send({ email, password }); 53 | expect(res.statusCode).toBe(200); 54 | expect(res.body).toHaveProperty("token"); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /frontend/src/context/AuthContext.js: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useState, useEffect } from 'react'; 2 | import { authService } from '../services/authService'; 3 | 4 | const AuthContext = createContext(); 5 | 6 | export const useAuth = () => { 7 | const context = useContext(AuthContext); 8 | if (!context) { 9 | throw new Error('useAuth must be used within an AuthProvider'); 10 | } 11 | return context; 12 | }; 13 | 14 | export const AuthProvider = ({ children }) => { 15 | const [user, setUser] = useState(null); 16 | const [loading, setLoading] = useState(true); 17 | 18 | useEffect(() => { 19 | // Check if user is already logged in 20 | const currentUser = authService.getCurrentUser(); 21 | if (currentUser) { 22 | setUser(currentUser); 23 | } 24 | setLoading(false); 25 | }, []); 26 | 27 | const login = async (credentials) => { 28 | try { 29 | const response = await authService.login(credentials); 30 | const { token, user } = response; 31 | authService.setAuthData(token, user); 32 | setUser(user); 33 | return response; 34 | } catch (error) { 35 | throw error; 36 | } 37 | }; 38 | 39 | const register = async (userData) => { 40 | try { 41 | const response = await authService.register(userData); 42 | const { token, user } = response; 43 | authService.setAuthData(token, user); 44 | setUser(user); 45 | return response; 46 | } catch (error) { 47 | throw error; 48 | } 49 | }; 50 | 51 | const logout = () => { 52 | authService.logout(); 53 | setUser(null); 54 | }; 55 | 56 | const value = { 57 | user, 58 | login, 59 | register, 60 | logout, 61 | loading, 62 | isAuthenticated: !!user, 63 | }; 64 | 65 | return ( 66 | 67 | {children} 68 | 69 | ); 70 | }; 71 | -------------------------------------------------------------------------------- /frontend/src/components/SelectBox.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const SelectBox = ({ 4 | id, 5 | label, 6 | icon, 7 | register, 8 | error, 9 | options = [], 10 | placeholder = 'Select an option', 11 | className = '', 12 | ...props 13 | }) => { 14 | return ( 15 |
16 | {label && ( 17 | 20 | )} 21 |
22 | {icon && ( 23 |
24 | {icon} 25 |
26 | )} 27 | 42 |
43 | {error && ( 44 |

45 | 46 | 47 | 48 | {error} 49 |

50 | )} 51 |
52 | ); 53 | }; 54 | 55 | export default SelectBox; 56 | -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; 3 | import { Toaster } from 'react-hot-toast'; 4 | import { AuthProvider } from './context/AuthContext'; 5 | import Login from './pages/Login'; 6 | import Register from './pages/Register'; 7 | import Home from './pages/Home'; 8 | import Profile from './pages/Profile'; 9 | import ReportForm from './pages/ReportForm'; 10 | import EcoComplianceHub from './pages/EcoComplianceHub'; 11 | import TopicDetail from './pages/TopicDetail'; 12 | import QuizPage from './pages/QuizPage'; 13 | 14 | function App() { 15 | return ( 16 | 17 | 18 |
19 | 29 | 30 | 31 | {/* Public Routes */} 32 | } /> 33 | } /> 34 | } /> 35 | } /> 36 | } /> 37 | } /> 38 | } /> 39 | } /> 40 | 41 | {/* Redirect root to register */} 42 | } /> 43 | 44 | {/* Catch all route */} 45 | } /> 46 | 47 | 48 |
49 |
50 |
51 | ); 52 | } 53 | 54 | export default App; 55 | -------------------------------------------------------------------------------- /frontend/src/services/reportService.js: -------------------------------------------------------------------------------- 1 | import api from './api'; 2 | 3 | export const reportService = { 4 | // Create a new report 5 | createReport: async (reportData) => { 6 | const response = await api.post('/reports', reportData); 7 | return response.data; 8 | }, 9 | 10 | // Get all reports with optional filters 11 | getReports: async (filters = {}) => { 12 | const params = new URLSearchParams(); 13 | 14 | if (filters.type) params.append('type', filters.type); 15 | if (filters.severity) params.append('severity', filters.severity); 16 | if (filters.limit) params.append('limit', filters.limit); 17 | 18 | const response = await api.get(`/reports?${params.toString()}`); 19 | return response.data; 20 | }, 21 | 22 | // Get a single report by ID 23 | getReportById: async (id) => { 24 | const response = await api.get(`/reports/${id}`); 25 | return response.data; 26 | }, 27 | 28 | // Get current user's reports 29 | getMyReports: async () => { 30 | const response = await api.get('/reports/user/my-reports'); 31 | return response.data; 32 | }, 33 | 34 | // Update a report 35 | updateReport: async (id, reportData) => { 36 | const response = await api.patch(`/reports/${id}`, reportData); 37 | return response.data; 38 | }, 39 | 40 | // Delete a report 41 | deleteReport: async (id) => { 42 | const response = await api.delete(`/reports/${id}`); 43 | return response.data; 44 | }, 45 | 46 | // Get reports near a location 47 | getReportsNearLocation: async (latitude, longitude, maxDistance = 50000) => { 48 | const response = await api.get('/reports/nearby', { 49 | params: { latitude, longitude, maxDistance } 50 | }); 51 | return response.data; 52 | }, 53 | 54 | // Toggle report status (admin only) 55 | toggleReportStatus: async (id) => { 56 | const response = await api.patch(`/reports/${id}/toggle-status`); 57 | return response.data; 58 | } 59 | }; 60 | 61 | export default reportService; 62 | 63 | -------------------------------------------------------------------------------- /backend/models/User.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import bcrypt from 'bcrypt'; 3 | 4 | const userSchema = new mongoose.Schema( 5 | { 6 | name : { 7 | type: String, 8 | required: true, 9 | }, 10 | 11 | email : { 12 | type: String, 13 | required: true, 14 | unique: true, 15 | lowercase: true, 16 | }, 17 | 18 | password :{ 19 | type: String, 20 | required: true, 21 | minlength: 5, 22 | }, 23 | 24 | role :{ 25 | type: String, 26 | enum: ["sailor","captain","admin","user"], 27 | default: "sailor", 28 | }, 29 | 30 | vesselName : { 31 | type: String, 32 | required: true, 33 | 34 | }, 35 | 36 | vesselType : { 37 | type : String, 38 | enum : ["cargo", "fishing", "pleasure", "tanker", "passenger", "other"], 39 | default : "other", 40 | required : true, 41 | }, 42 | 43 | isActive : { 44 | type: Boolean, 45 | default: true 46 | } 47 | }, 48 | 49 | { timestamps: true } 50 | 51 | ); 52 | 53 | 54 | //password hashing before saving 55 | userSchema.pre("save", async function (next) { 56 | if (!this.isModified("password")) return next(); 57 | 58 | const salt = await bcrypt.genSalt(10); 59 | this.password = await bcrypt.hash(this.password, salt); 60 | next(); 61 | }); 62 | 63 | //compare password method 64 | userSchema.methods.comparePassword = function(plain){ 65 | return bcrypt.compare(plain, this.password); 66 | }; 67 | 68 | // To get clean JSON response without sensitive data 69 | userSchema.methods.toJSON = function() { 70 | const user = this.toObject(); 71 | delete user.password; 72 | delete user.__v; 73 | return user; 74 | } 75 | 76 | 77 | export default mongoose.model("User", userSchema); 78 | -------------------------------------------------------------------------------- /frontend/src/components/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Button = ({ 4 | children, 5 | type = 'button', 6 | variant = 'primary', 7 | size = 'md', 8 | loading = false, 9 | disabled = false, 10 | className = '', 11 | icon, 12 | ...props 13 | }) => { 14 | const baseClasses = 'inline-flex items-center justify-center font-semibold rounded-xl transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed'; 15 | 16 | const variants = { 17 | primary: 'text-white bg-gradient-to-r from-blue-800 via-blue-900 to-indigo-900 hover:from-blue-900 hover:via-indigo-900 hover:to-blue-800 focus:ring-blue-500 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5', 18 | secondary: 'text-gray-700 bg-white border-2 border-gray-300 hover:bg-gray-50 focus:ring-gray-500 shadow-md hover:shadow-lg', 19 | danger: 'text-white bg-gradient-to-r from-red-500 to-pink-500 hover:from-red-600 hover:to-pink-600 focus:ring-red-500 shadow-lg hover:shadow-xl', 20 | ghost: 'text-blue-600 hover:text-blue-500 hover:bg-blue-50 focus:ring-blue-500' 21 | }; 22 | 23 | const sizes = { 24 | sm: 'px-3 py-2 text-sm', 25 | md: 'px-4 py-3 text-base', 26 | lg: 'px-6 py-4 text-lg' 27 | }; 28 | 29 | return ( 30 | 51 | ); 52 | }; 53 | 54 | export default Button; 55 | -------------------------------------------------------------------------------- /backend/server.js: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | dotenv.config(); 3 | 4 | import app from "./app.js"; 5 | import connectDB from "./config/db.js"; 6 | 7 | const PORT = parseInt(process.env.PORT, 10) || 5000; 8 | let server; 9 | 10 | /** 11 | * Start the server: 12 | * 1. Connect to DB 13 | * 2. Start Express 14 | */ 15 | async function start() { 16 | try { 17 | await connectDB(); 18 | server = app.listen(PORT, () => 19 | console.log(`🚢 Server running on http://localhost:${PORT} (env: ${process.env.NODE_ENV || "development"})`) 20 | ); 21 | } catch (err) { 22 | console.error("Failed to start server:", err); 23 | // if DB connection fails, exit with failure 24 | process.exit(1); 25 | } 26 | } 27 | 28 | start(); 29 | 30 | /** 31 | * Graceful shutdown helpers 32 | */ 33 | function shutdown(signal) { 34 | return async () => { 35 | console.log(`\nReceived ${signal}. Closing server...`); 36 | try { 37 | if (server) { 38 | await new Promise((resolve, reject) => { 39 | server.close((err) => (err ? reject(err) : resolve())); 40 | }); 41 | } 42 | // close DB (mongoose connection) if available 43 | try { 44 | // mongoose keeps the connection in the default connection 45 | // import mongoose lazily to avoid circular deps if not needed earlier 46 | const mongoose = await import("mongoose"); 47 | await mongoose.connection.close(false); // don't force close in-flight ops 48 | console.log("MongoDB connection closed."); 49 | } catch (e) { 50 | console.warn("Error closing MongoDB connection:", e.message || e); 51 | } 52 | 53 | console.log("Shutdown complete. Bye 👋"); 54 | process.exit(0); 55 | } catch (e) { 56 | console.error("Error during shutdown:", e); 57 | process.exit(1); 58 | } 59 | }; 60 | } 61 | 62 | process.on("SIGINT", shutdown("SIGINT")); 63 | process.on("SIGTERM", shutdown("SIGTERM")); 64 | 65 | // Capture unhandled rejections & uncaught exceptions to avoid silent failures 66 | process.on("unhandledRejection", (reason) => { 67 | console.error("Unhandled Rejection:", reason); 68 | // allow graceful shutdown 69 | shutdown("unhandledRejection")(); 70 | }); 71 | 72 | process.on("uncaughtException", (err) => { 73 | console.error("Uncaught Exception:", err); 74 | // allow graceful shutdown 75 | shutdown("uncaughtException")(); 76 | }); 77 | -------------------------------------------------------------------------------- /frontend/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/controllers/userController.js: -------------------------------------------------------------------------------- 1 | import { validationResult } from "express-validator"; 2 | import User from "../models/User.js"; 3 | 4 | //get all users 5 | export const listUsers = async (req, res, next) => { 6 | try { 7 | const users = await User.find(); 8 | res.status(200).json({ users }); 9 | } catch (error) { 10 | next(error); 11 | } 12 | }; 13 | 14 | //get single user 15 | export const getUserById = async (req, res, next) => { 16 | try{ 17 | const user = await User.findById(req.params.id); 18 | if (!user) return res.status(404).json({ message: "User not found"}); 19 | res.status(200).json({ user }); 20 | } catch (error) { 21 | next(error); 22 | } 23 | }; 24 | 25 | //create user 26 | export const createUser = async (req, res, next) => { 27 | try { 28 | const errors = validationResult(req); 29 | if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() }); 30 | 31 | const { name , email, password, role, vesselName, vesselType } = req.body; 32 | 33 | // check email is unique or not 34 | const exitingUser = await User.findOne ({ email }); 35 | if (exitingUser) return res.status(400).json({ message: "Email already exists"}); 36 | 37 | const user = await User.create({name, email, password, role, vesselName, vesselType}); 38 | res.status(201).json({ user }); 39 | } catch (error) { 40 | next(error); 41 | } 42 | }; 43 | 44 | // update user 45 | export const updateUser = async (req, res, next) => { 46 | try { 47 | const errors = validationResult(req); 48 | if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() }); 49 | 50 | const { name, email, password, role, vesselName, vesselType } = req.body; 51 | 52 | const user = await User.findByIdAndUpdate( 53 | req.params.id, 54 | {name, role, vesselName, vesselType, isActive}, 55 | { 56 | new: true, runValidators: true } 57 | ); 58 | 59 | if (!user) return res.status(404).json({ message: "User not found" }); 60 | res.json({ user}); 61 | }catch(err) { 62 | next(err); 63 | } 64 | }; 65 | 66 | //delete user 67 | export const deleteUser = async (req, res, next) => { 68 | try { 69 | const deleted_user = await User.findByIdAndDelete(req.params.id); 70 | if (!deleted_user) return res.status(404).json({ message: "User not found" }); 71 | res.status(204).send(); 72 | } catch (error) { 73 | next(error); 74 | } 75 | }; 76 | -------------------------------------------------------------------------------- /frontend/src/Styles/global.css: -------------------------------------------------------------------------------- 1 | @keyframes pulse { 2 | 0%, 100% { opacity: 1; } 3 | 50% { opacity: 0.5; } 4 | } 5 | 6 | * { 7 | box-sizing: border-box; 8 | } 9 | 10 | body { 11 | margin: 0; 12 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 13 | } 14 | 15 | input::placeholder { 16 | color: rgba(255,255,255,0.7); 17 | } 18 | 19 | .leaflet-popup-content-wrapper { 20 | border-radius: 12px; 21 | box-shadow: 0 12px 40px rgba(0,0,0,0.15); 22 | } 23 | 24 | .leaflet-popup-tip { 25 | border-radius: 0 0 0 2px; 26 | } 27 | 28 | .ship-marker { 29 | transition: all 0.4s ease; 30 | } 31 | 32 | .ship-marker:hover { 33 | transform: scale(1.15); 34 | z-index: 1000; 35 | filter: drop-shadow(0 4px 8px rgba(0,0,0,0.3)); 36 | } 37 | 38 | /* Enhanced scrollbar styling */ 39 | ::-webkit-scrollbar { 40 | width: 10px; 41 | } 42 | 43 | ::-webkit-scrollbar-track { 44 | background: linear-gradient(135deg, #f1f5f9, #e2e8f0); 45 | border-radius: 5px; 46 | } 47 | 48 | ::-webkit-scrollbar-thumb { 49 | background: linear-gradient(135deg, #3b82f6, #1e40af); 50 | border-radius: 5px; 51 | border: 1px solid #e2e8f0; 52 | } 53 | 54 | ::-webkit-scrollbar-thumb:hover { 55 | background: linear-gradient(135deg, #2563eb, #1e3a8a); 56 | } 57 | 58 | /* Smooth transitions for all interactive elements */ 59 | button, a, .interactive { 60 | transition: all 0.3s ease; 61 | } 62 | 63 | /* Enhanced map container styling */ 64 | .leaflet-container { 65 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 66 | } 67 | 68 | .leaflet-popup-content { 69 | margin: 0; 70 | } 71 | 72 | .leaflet-popup-close-button { 73 | color: #6b7280 !important; 74 | font-size: 18px !important; 75 | font-weight: bold !important; 76 | } 77 | 78 | .leaflet-popup-close-button:hover { 79 | color: #374151 !important; 80 | } 81 | 82 | /* Map controls styling */ 83 | .leaflet-control-zoom a { 84 | background-color: white !important; 85 | border: 1px solid #e5e7eb !important; 86 | color: #374151 !important; 87 | } 88 | 89 | .leaflet-control-zoom a:hover { 90 | background-color: #f9fafb !important; 91 | border-color: #d1d5db !important; 92 | } 93 | 94 | /* Animation for stats cards */ 95 | @keyframes fadeInUp { 96 | from { 97 | opacity: 0; 98 | transform: translateY(20px); 99 | } 100 | to { 101 | opacity: 1; 102 | transform: translateY(0); 103 | } 104 | } 105 | 106 | /* Responsive design improvements */ 107 | @media (max-width: 768px) { 108 | .ship-marker div { 109 | font-size: 8px !important; 110 | } 111 | 112 | .leaflet-popup-content-wrapper { 113 | min-width: 200px; 114 | } 115 | } -------------------------------------------------------------------------------- /backend/controllers/authController.js: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | import User from '../models/User.js'; 3 | import { validationResult } from "express-validator"; 4 | 5 | function signToken(user) { 6 | return jwt.sign( { 7 | sub: user._id, role: user.role }, 8 | process.env.JWT_SECRET, 9 | { expiresIn: process.env.JWT_EXPIRES_IN || "7d" } 10 | ); 11 | } 12 | 13 | 14 | export const register = async (req, res, next) => { 15 | try{ 16 | const errors = validationResult(req); 17 | if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() }); 18 | 19 | const { name, email, password, role, vesselName, vesselType } = req.body; 20 | 21 | const exists = await User.findOne({ email }); 22 | if (exists) return res.status(409).json({ message: "Email already in use" }); 23 | 24 | 25 | const user = await User.create({ name, email, password, role, vesselName, vesselType }); 26 | const token = signToken(user); 27 | res.status(201).json({ user, token }); 28 | } catch (error) { 29 | next(error); 30 | } 31 | }; 32 | 33 | export const login = async (req, res, next) => { 34 | try { 35 | const errors = validationResult(req); 36 | if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() }); 37 | 38 | const { email, password } = req.body; 39 | const user = await User.findOne({ email }).select("+password"); 40 | if (!user) return res.status(401).json({ message: "Invalid email or password" }); 41 | 42 | const isMatch = await user.comparePassword(password); 43 | if (!isMatch) return res.status(401).json({ message: "Invalid email or password" }); 44 | 45 | const token = signToken(user); 46 | res.json({user: user.toJSON(),token}); 47 | } catch (error) { 48 | next(error); 49 | } 50 | 51 | }; 52 | 53 | export const getProfile = async (req, res) => { 54 | res.json({ user: req.user }); 55 | 56 | }; 57 | 58 | export const updateProfile = async (req, res, next) => { 59 | try { 60 | const errors = validationResult(req); 61 | if (!errors.isEmpty()) 62 | return res.status(400).json({ errors: errors.array() }); 63 | 64 | const { name, password, vesselName, vesselType } = req.body; 65 | 66 | const user = await User.findById(req.user._id).select("+password"); 67 | if (!user) return res.status(404).json({ message: "User not found" }); 68 | 69 | // update only provided fields 70 | if (name) user.name = name; 71 | if (vesselName) user.vesselName = vesselName; 72 | if (vesselType) user.vesselType = vesselType; 73 | if (password) user.password = password; 74 | 75 | await user.save(); 76 | 77 | res.json({ message: "Profile updated successfully", user }); 78 | } catch (error) { 79 | next(error); 80 | } 81 | }; 82 | 83 | export const deleteProfile = async (req, res, next) => { 84 | try { 85 | const user = await User.findByIdAndDelete(req.user._id); 86 | if (!user) return res.status(404).json({ message: "User not found" }); 87 | 88 | res.json({ message: "Profile deleted successfully" }); 89 | } catch (error) { 90 | next(error); 91 | } 92 | }; 93 | -------------------------------------------------------------------------------- /backend/models/Report.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const reportSchema = new mongoose.Schema( 4 | { 5 | type: { 6 | type: String, 7 | enum: ['hotspot', 'pollution'], 8 | required: true, 9 | }, 10 | 11 | location: { 12 | type: { 13 | type: String, 14 | enum: ['Point'], 15 | default: 'Point', 16 | required: true 17 | }, 18 | coordinates: { 19 | type: [Number], // [longitude, latitude] 20 | required: true, 21 | validate: { 22 | validator: function(coords) { 23 | return coords.length === 2 && 24 | coords[0] >= -180 && coords[0] <= 180 && 25 | coords[1] >= -90 && coords[1] <= 90; 26 | }, 27 | message: 'Invalid coordinates. Longitude must be between -180 and 180, latitude between -90 and 90.' 28 | } 29 | } 30 | }, 31 | 32 | title: { 33 | type: String, 34 | required: true, 35 | trim: true, 36 | maxlength: 100 37 | }, 38 | 39 | description: { 40 | type: String, 41 | required: true, 42 | trim: true, 43 | maxlength: 1000 44 | }, 45 | 46 | // For wildlife hotspot reports 47 | species: { 48 | type: String, 49 | trim: true, 50 | maxlength: 100, 51 | required: function() { 52 | return this.type === 'hotspot'; 53 | } 54 | }, 55 | 56 | severity: { 57 | type: String, 58 | enum: ['low', 'medium', 'high', 'critical'], 59 | default: 'medium', 60 | required: true 61 | }, 62 | 63 | // Optional image URL 64 | imageUrl: { 65 | type: String, 66 | trim: true 67 | }, 68 | 69 | // User who submitted the report 70 | submittedBy: { 71 | type: mongoose.Schema.Types.ObjectId, 72 | ref: 'User', 73 | required: true 74 | }, 75 | 76 | // Additional metadata 77 | vesselInfo: { 78 | vesselName: String, 79 | vesselType: String 80 | }, 81 | 82 | isActive: { 83 | type: Boolean, 84 | default: true 85 | } 86 | }, 87 | { 88 | timestamps: true 89 | } 90 | ); 91 | 92 | // Create geospatial index for location-based queries 93 | reportSchema.index({ location: '2dsphere' }); 94 | 95 | // Index for filtering by type and severity 96 | reportSchema.index({ type: 1, severity: 1 }); 97 | 98 | // Index for user's reports 99 | reportSchema.index({ submittedBy: 1, createdAt: -1 }); 100 | 101 | // Virtual for formatted location 102 | reportSchema.virtual('latitude').get(function() { 103 | return this.location.coordinates[1]; 104 | }); 105 | 106 | reportSchema.virtual('longitude').get(function() { 107 | return this.location.coordinates[0]; 108 | }); 109 | 110 | // Clean JSON response 111 | reportSchema.methods.toJSON = function() { 112 | const report = this.toObject({ virtuals: true }); 113 | delete report.__v; 114 | return report; 115 | }; 116 | 117 | export default mongoose.model('Report', reportSchema); 118 | 119 | -------------------------------------------------------------------------------- /frontend/src/routes/ProtectedRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Navigate, useLocation } from 'react-router-dom'; 3 | import { useAuth } from '../context/AuthContext'; 4 | import { MdRefresh, MdWarning } from 'react-icons/md'; 5 | 6 | /** 7 | * ProtectedRoute Component 8 | * 9 | * A higher-order component that protects routes by checking authentication status. 10 | * Redirects unauthenticated users to the login page and shows a loading spinner 11 | * while checking authentication status. 12 | * 13 | * @param {Object} props - Component props 14 | * @param {React.ReactNode} props.children - The components to render if authenticated 15 | * @param {string} props.redirectTo - Custom redirect path (defaults to '/login') 16 | * @param {boolean} props.requireAuth - Whether authentication is required (defaults to true) 17 | * @param {string[]} props.allowedRoles - Array of allowed roles (optional) 18 | * @returns {React.ReactNode} - The protected content or redirect 19 | */ 20 | const ProtectedRoute = ({ 21 | children, 22 | redirectTo = '/login', 23 | requireAuth = true, 24 | allowedRoles = [] 25 | }) => { 26 | const { isAuthenticated, loading, user } = useAuth(); 27 | const location = useLocation(); 28 | 29 | // Show loading spinner while checking authentication 30 | if (loading) { 31 | return ( 32 |
33 |
34 |
35 | 36 |
37 |

Loading...

38 |

Checking authentication

39 |
40 |
41 | ); 42 | } 43 | 44 | // If authentication is not required, render children 45 | if (!requireAuth) { 46 | return children; 47 | } 48 | 49 | // If not authenticated, redirect to login 50 | if (!isAuthenticated) { 51 | return ; 52 | } 53 | 54 | // Check role-based access if allowedRoles is specified 55 | if (allowedRoles.length > 0 && user) { 56 | const hasRequiredRole = allowedRoles.includes(user.role); 57 | if (!hasRequiredRole) { 58 | return ( 59 |
60 |
61 |
62 | 63 |
64 |

Access Denied

65 |

66 | You don't have permission to access this page. 67 |

68 | 74 |
75 |
76 | ); 77 | } 78 | } 79 | 80 | // If authenticated and has required role (if specified), render children 81 | return children; 82 | }; 83 | 84 | export default ProtectedRoute; 85 | -------------------------------------------------------------------------------- /backend/routes/reportRoutes.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { body } from 'express-validator'; 3 | import auth from '../middleware/auth.js'; 4 | import { requireRoles } from '../middleware/roles.js'; 5 | import { 6 | createReport, 7 | getReports, 8 | getReportById, 9 | getMyReports, 10 | updateReport, 11 | deleteReport, 12 | getReportsNearLocation, 13 | toggleReportStatus 14 | } from '../controllers/reportController.js'; 15 | 16 | const router = express.Router(); 17 | 18 | // Validation rules for creating a report 19 | const createReportValidation = [ 20 | body('type') 21 | .isIn(['hotspot', 'pollution']) 22 | .withMessage('Type must be either hotspot or pollution'), 23 | body('latitude') 24 | .isFloat({ min: -90, max: 90 }) 25 | .withMessage('Latitude must be between -90 and 90'), 26 | body('longitude') 27 | .isFloat({ min: -180, max: 180 }) 28 | .withMessage('Longitude must be between -180 and 180'), 29 | body('title') 30 | .trim() 31 | .notEmpty() 32 | .withMessage('Title is required') 33 | .isLength({ max: 100 }) 34 | .withMessage('Title must not exceed 100 characters'), 35 | body('description') 36 | .trim() 37 | .notEmpty() 38 | .withMessage('Description is required') 39 | .isLength({ max: 1000 }) 40 | .withMessage('Description must not exceed 1000 characters'), 41 | body('species') 42 | .optional() 43 | .trim() 44 | .isLength({ max: 100 }) 45 | .withMessage('Species name must not exceed 100 characters'), 46 | body('severity') 47 | .optional() 48 | .isIn(['low', 'medium', 'high', 'critical']) 49 | .withMessage('Severity must be low, medium, high, or critical'), 50 | body('imageUrl') 51 | .optional({ values: 'falsy' }) 52 | .trim() 53 | .isURL() 54 | .withMessage('Invalid image URL') 55 | ]; 56 | 57 | // Validation rules for updating a report 58 | const updateReportValidation = [ 59 | body('title') 60 | .optional() 61 | .trim() 62 | .notEmpty() 63 | .withMessage('Title cannot be empty') 64 | .isLength({ max: 100 }) 65 | .withMessage('Title must not exceed 100 characters'), 66 | body('description') 67 | .optional() 68 | .trim() 69 | .notEmpty() 70 | .withMessage('Description cannot be empty') 71 | .isLength({ max: 1000 }) 72 | .withMessage('Description must not exceed 1000 characters'), 73 | body('species') 74 | .optional() 75 | .trim() 76 | .isLength({ max: 100 }) 77 | .withMessage('Species name must not exceed 100 characters'), 78 | body('severity') 79 | .optional() 80 | .isIn(['low', 'medium', 'high', 'critical']) 81 | .withMessage('Severity must be low, medium, high, or critical'), 82 | body('imageUrl') 83 | .optional() 84 | .trim() 85 | ]; 86 | 87 | // Public routes 88 | router.get('/', getReports); // Get all reports with filters 89 | router.get('/nearby', getReportsNearLocation); // Get reports near a location 90 | router.get('/:id', getReportById); // Get single report 91 | 92 | // Protected routes (require authentication) 93 | router.post('/', auth, createReportValidation, createReport); // Create new report 94 | router.get('/user/my-reports', auth, getMyReports); // Get current user's reports 95 | router.patch('/:id', auth, updateReportValidation, updateReport); // Update report 96 | router.delete('/:id', auth, deleteReport); // Delete report 97 | 98 | // Admin only routes 99 | router.patch('/:id/toggle-status', auth, requireRoles('admin'), toggleReportStatus); // Toggle active status 100 | 101 | export default router; 102 | 103 | -------------------------------------------------------------------------------- /frontend/src/components/StatsCards.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Ship, Route, Clock, MapPin } from 'lucide-react'; 3 | 4 | const StatsCards = () => { 5 | return ( 6 |
12 | {[ 13 | { icon: Ship, label: 'Active Vessels', value: '4', subtext: 'Currently Tracked', color: '#3b82f6', trend: '+2 today' }, 14 | { icon: Route, label: 'Ocean Routes', value: '8', subtext: 'Major Shipping Lanes', color: '#10b981', trend: '4 active' }, 15 | { icon: Clock, label: 'Avg. Speed', value: '15.2 kn', subtext: 'Fleet Average', color: '#f59e0b', trend: '+0.8 kn' }, 16 | { icon: MapPin, label: 'Ports Served', value: '12', subtext: 'Global Network', color: '#8b5cf6', trend: '6 continents' } 17 | ].map((stat, index) => ( 18 |
{ 33 | e.target.closest('div').style.transform = 'translateY(-8px)'; 34 | e.target.closest('div').style.boxShadow = '0 20px 40px rgba(0,0,0,0.12)'; 35 | }} 36 | onMouseLeave={(e) => { 37 | e.target.closest('div').style.transform = 'translateY(0)'; 38 | e.target.closest('div').style.boxShadow = '0 8px 24px rgba(0,0,0,0.08)'; 39 | }} 40 | > 41 | {/* Background Decoration */} 42 |
51 | 52 |
64 | 65 |
66 | 67 |

73 | {stat.value} 74 |

75 | 76 |

82 | {stat.label} 83 |

84 | 85 |

90 | {stat.subtext} 91 |

92 | 93 |
105 |
111 | {stat.trend} 112 |
113 |
114 | ))} 115 |
116 | ); 117 | }; 118 | 119 | export default StatsCards; -------------------------------------------------------------------------------- /frontend/src/__tests__/Home.test.jsx: -------------------------------------------------------------------------------- 1 | // __tests__/Home.test.jsx 2 | import React from 'react'; 3 | import { render, screen } from '@testing-library/react'; 4 | import '@testing-library/jest-dom'; 5 | import Home from '../pages/Home'; 6 | 7 | 8 | 9 | // Mock child components to avoid rendering complexity 10 | jest.mock('../components/Header', () => () =>
); 11 | jest.mock('../components/Footer', () => () =>
); 12 | jest.mock('../components/ShipMap', () => () =>
); 13 | jest.mock('../components/StatsCards', () => () =>
); 14 | jest.mock('../components/FeaturesGrid', () => () =>
); 15 | jest.mock('../components/TechnologyStack', () => () =>
); 16 | 17 | describe('Home Component', () => { 18 | test('renders main page title', () => { 19 | render(); 20 | expect( 21 | screen.getByText(/Smart Maritime Navigator For Ships/i) 22 | ).toBeInTheDocument(); 23 | }); 24 | 25 | test('renders subtitle correctly', () => { 26 | render(); 27 | expect( 28 | screen.getByText(/Real-time vessel tracking and sustainable route optimization/i) 29 | ).toBeInTheDocument(); 30 | }); 31 | 32 | test('renders Header and Footer', () => { 33 | render(); 34 | expect(screen.getByTestId('header')).toBeInTheDocument(); 35 | expect(screen.getByTestId('footer')).toBeInTheDocument(); 36 | }); 37 | 38 | test('renders ShipMap inside the map container', () => { 39 | render(); 40 | expect(screen.getByTestId('shipmap')).toBeInTheDocument(); 41 | }); 42 | 43 | test('renders live status indicators', () => { 44 | render(); 45 | expect(screen.getByText(/System Status/i)).toBeInTheDocument(); 46 | expect(screen.getByText(/GPS Accuracy/i)).toBeInTheDocument(); 47 | expect(screen.getByText(/Data Update/i)).toBeInTheDocument(); 48 | }); 49 | 50 | test('renders stats section values', () => { 51 | render(); 52 | expect(screen.getByText(/2,847 nm/i)).toBeInTheDocument(); 53 | expect(screen.getByText(/4 major routes/i)).toBeInTheDocument(); 54 | expect(screen.getByText(/15.2 knots/i)).toBeInTheDocument(); 55 | expect(screen.getByText(/3 monitored/i)).toBeInTheDocument(); 56 | }); 57 | }); 58 | 59 | // Mock child components to avoid rendering complexity 60 | jest.mock('../components/Header', () => () =>
); 61 | jest.mock('../components/Footer', () => () =>
); 62 | jest.mock('../components/ShipMap', () => () =>
); 63 | jest.mock('../components/StatsCards', () => () =>
); 64 | jest.mock('../components/FeaturesGrid', () => () =>
); 65 | jest.mock('../components/TechnologyStack', () => () =>
); 66 | 67 | describe('Home Component', () => { 68 | test('renders main page title', () => { 69 | render(); 70 | expect( 71 | screen.getByText(/Smart Maritime Navigator For Ships/i) 72 | ).toBeInTheDocument(); 73 | }); 74 | 75 | test('renders subtitle correctly', () => { 76 | render(); 77 | expect( 78 | screen.getByText(/Real-time vessel tracking and sustainable route optimization/i) 79 | ).toBeInTheDocument(); 80 | }); 81 | 82 | test('renders Header and Footer', () => { 83 | render(); 84 | expect(screen.getByTestId('header')).toBeInTheDocument(); 85 | expect(screen.getByTestId('footer')).toBeInTheDocument(); 86 | }); 87 | 88 | test('renders ShipMap inside the map container', () => { 89 | render(); 90 | expect(screen.getByTestId('shipmap')).toBeInTheDocument(); 91 | }); 92 | 93 | test('renders live status indicators', () => { 94 | render(); 95 | expect(screen.getByText(/System Status/i)).toBeInTheDocument(); 96 | expect(screen.getByText(/GPS Accuracy/i)).toBeInTheDocument(); 97 | expect(screen.getByText(/Data Update/i)).toBeInTheDocument(); 98 | }); 99 | 100 | test('renders stats section values', () => { 101 | render(); 102 | expect(screen.getByText(/2,847 nm/i)).toBeInTheDocument(); 103 | expect(screen.getByText(/4 major routes/i)).toBeInTheDocument(); 104 | expect(screen.getByText(/15.2 knots/i)).toBeInTheDocument(); 105 | expect(screen.getByText(/3 monitored/i)).toBeInTheDocument(); 106 | }); 107 | }); 108 | 109 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # EcoNav MPA Frontend 2 | 3 | A modern React-based frontend for the EcoNav Maritime Protected Area Navigation System, built with Tailwind CSS. 4 | 5 | ## Features 6 | 7 | ### Authentication 8 | - **Login Page**: Secure user authentication with email and password 9 | - **Register Page**: User registration with vessel information 10 | - **Protected Routes**: Automatic redirection for unauthenticated users 11 | - **JWT Token Management**: Secure token-based authentication 12 | 13 | ### User Interface 14 | - **Modern Design**: Clean, responsive interface with maritime theme 15 | - **Tailwind CSS**: Utility-first CSS framework for rapid development 16 | - **Responsive Layout**: Works perfectly on desktop, tablet, and mobile 17 | - **Toast Notifications**: User feedback for all operations 18 | - **Loading States**: Smooth loading indicators throughout the app 19 | 20 | ### Dashboard 21 | - **Welcome Section**: Personalized greeting with user information 22 | - **Statistics Cards**: Key metrics display (Active Vessels, Protected Areas, etc.) 23 | - **Quick Actions**: Easy access to main features 24 | - **Recent Activity**: Real-time activity feed 25 | - **Vessel Information**: Display user's vessel details 26 | 27 | ## Technology Stack 28 | 29 | - **React 19.1.1**: Modern React with hooks 30 | - **React Router DOM 6.8.0**: Client-side routing 31 | - **Tailwind CSS 3.4.0**: Utility-first CSS framework 32 | - **Axios 1.6.0**: HTTP client for API communication 33 | - **React Hook Form 7.48.0**: Form handling and validation 34 | - **React Hot Toast 2.4.1**: Toast notifications 35 | 36 | ## Project Structure 37 | 38 | ``` 39 | src/ 40 | ├── components/ 41 | │ └── ProtectedRoute.js # Authentication guard component 42 | ├── context/ 43 | │ └── AuthContext.js # Authentication state management 44 | ├── pages/ 45 | │ ├── Login.js # Login page 46 | │ ├── Register.js # Registration page 47 | │ └── Dashboard.js # Main dashboard/home page 48 | ├── services/ 49 | │ ├── api.js # Axios configuration 50 | │ └── authService.js # Authentication API calls 51 | ├── App.js # Main app with routing 52 | └── index.css # Tailwind CSS imports 53 | ``` 54 | 55 | ## Getting Started 56 | 57 | ### Prerequisites 58 | - Node.js (v14 or higher) 59 | - npm or yarn 60 | - Backend server running (see backend README) 61 | 62 | ### Installation 63 | 64 | 1. Navigate to the frontend directory: 65 | ```bash 66 | cd MPA-Navigation-System/frontend 67 | ``` 68 | 69 | 2. Install dependencies: 70 | ```bash 71 | npm install 72 | ``` 73 | 74 | 3. Start the development server: 75 | ```bash 76 | npm start 77 | ``` 78 | 79 | The application will open at `http://localhost:3000` 80 | 81 | ## API Integration 82 | 83 | The frontend communicates with the backend through RESTful APIs: 84 | 85 | ### Authentication Endpoints 86 | - `POST /api/auth/register` - Register new user 87 | - `POST /api/auth/login` - User login 88 | 89 | ### User Data Structure 90 | ```javascript 91 | { 92 | name: "John Doe", 93 | email: "john@example.com", 94 | role: "captain", // sailor, captain, admin 95 | vesselName: "Ocean Explorer", 96 | vesselType: "cargo", // cargo, fishing, pleasure, tanker, passenger, other 97 | isActive: true 98 | } 99 | ``` 100 | 101 | ## User Roles 102 | 103 | ### Sailor 104 | - Basic crew member 105 | - Can view navigation data 106 | - Limited system access 107 | 108 | ### Captain 109 | - Vessel commander 110 | - Enhanced navigation access 111 | - Crew management capabilities 112 | 113 | ### Admin 114 | - System administrator 115 | - Full user management access 116 | - System configuration privileges 117 | 118 | ## Vessel Types 119 | 120 | - **Cargo**: Commercial cargo vessels 121 | - **Fishing**: Fishing vessels 122 | - **Pleasure**: Recreational vessels 123 | - **Tanker**: Oil and chemical tankers 124 | - **Passenger**: Passenger ships 125 | - **Other**: Miscellaneous vessels 126 | 127 | ## Design System 128 | 129 | ### Colors 130 | - **Primary**: Blue shades (#1e3a8a to #3b82f6) 131 | - **Maritime**: Ocean blue shades (#0ea5e9 to #0284c7) 132 | - **Success**: Green (#059669) 133 | - **Warning**: Orange (#d97706) 134 | - **Error**: Red (#dc2626) 135 | 136 | ### Typography 137 | - **Font Family**: Inter (Google Fonts) 138 | - **Weights**: 300, 400, 500, 600, 700 139 | 140 | ### Components 141 | - **Buttons**: Gradient backgrounds with hover effects 142 | - **Forms**: Clean input fields with validation states 143 | - **Cards**: White backgrounds with subtle shadows 144 | - **Navigation**: Responsive header with user info 145 | 146 | ## Development 147 | 148 | ### Available Scripts 149 | 150 | - `npm start` - Start development server 151 | - `npm build` - Build for production 152 | - `npm test` - Run tests 153 | - `npm eject` - Eject from Create React App 154 | 155 | ### Code Style 156 | 157 | - Follow React best practices 158 | - Use functional components with hooks 159 | - Implement proper error handling 160 | - Maintain responsive design 161 | - Follow maritime theme styling 162 | - Use Tailwind CSS utility classes 163 | 164 | ## Deployment 165 | 166 | 1. Build the application: 167 | ```bash 168 | npm run build 169 | ``` 170 | 171 | 2. Deploy the `build` folder to your web server 172 | 173 | 3. Configure environment variables for production: 174 | ```bash 175 | REACT_APP_API_URL=https://your-backend-url.com/api 176 | ``` 177 | 178 | ## Contributing 179 | 180 | 1. Follow the existing code structure 181 | 2. Use Tailwind CSS for styling 182 | 3. Add proper error handling 183 | 4. Test on multiple devices 184 | 5. Update documentation as needed 185 | 186 | ## License 187 | 188 | This project is part of the EcoNav MPA Navigation System. 189 | -------------------------------------------------------------------------------- /frontend/src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { Search, Anchor, AlertTriangle, FileText, Info } from 'lucide-react'; 4 | import { useAuth } from '../context/AuthContext'; 5 | import UserDropdown from './UserDropdown'; 6 | 7 | const Header = ({ activeSection, setActiveSection }) => { 8 | const { isAuthenticated } = useAuth(); 9 | 10 | return ( 11 |
20 |
27 | {/* Logo */} 28 | 40 | 41 | EcoNav 42 | 49 | LIVE 50 | 51 | 52 | 53 | {/* Navigation */} 54 | 98 | 99 | {/* Search Bar and User Menu */} 100 |
105 |
114 | 115 | 127 |
128 | 129 | {/* Authentication Section */} 130 | {isAuthenticated ? ( 131 | /* User Dropdown for logged in users */ 132 | 133 | ) : ( 134 | /* Login button for non-logged in users */ 135 | { 149 | e.target.style.backgroundColor = 'rgba(255,255,255,0.2)'; 150 | }} 151 | onMouseLeave={(e) => { 152 | e.target.style.backgroundColor = 'rgba(255,255,255,0.1)'; 153 | }} 154 | > 155 | Login 156 | 157 | )} 158 |
159 |
160 |
161 | ); 162 | }; 163 | 164 | export default Header; 165 | -------------------------------------------------------------------------------- /frontend/src/components/FeaturesGrid.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AlertTriangle, BarChart3 } from 'lucide-react'; 3 | 4 | const FeaturesGrid = () => { 5 | return ( 6 |
12 |
21 | {/* Background decoration */} 22 |
31 | 32 |

40 | 41 | Environmental Compliance 42 |

43 | 44 |

50 | Advanced monitoring system for marine protected areas, emission control zones, and environmental regulations. 51 |

52 | 53 |
57 | {[ 58 | { icon: '🌊', title: 'Marine Protected Areas', desc: 'Real-time zone monitoring and alerts' }, 59 | { icon: '📊', title: 'Emission Tracking', desc: 'Comprehensive environmental impact analysis' }, 60 | { icon: '🎯', title: 'Route Optimization', desc: 'Eco-friendly path recommendations' } 61 | ].map((feature, index) => ( 62 |
71 |
76 | {feature.icon} 77 |
78 |
79 |
84 | {feature.title} 85 |
86 |
90 | {feature.desc} 91 |
92 |
93 |
94 | ))} 95 |
96 |
97 | 98 |
107 | {/* Background decoration */} 108 |
117 | 118 |

126 | 127 | Advanced Analytics 128 |

129 | 130 |

136 | Comprehensive fleet performance insights with AI-powered optimization and predictive analytics. 137 |

138 | 139 |
143 | {[ 144 | { icon: '⛽', title: 'Fuel Efficiency', desc: 'Real-time consumption monitoring' }, 145 | { icon: '🔍', title: 'Performance Metrics', desc: 'Detailed operational analytics' }, 146 | { icon: '🌍', title: 'Carbon Footprint', desc: 'Environmental impact assessment' } 147 | ].map((feature, index) => ( 148 |
157 |
162 | {feature.icon} 163 |
164 |
165 |
170 | {feature.title} 171 |
172 |
176 | {feature.desc} 177 |
178 |
179 |
180 | ))} 181 |
182 |
183 |
184 | ); 185 | }; 186 | 187 | export default FeaturesGrid; -------------------------------------------------------------------------------- /backend/controllers/reportController.js: -------------------------------------------------------------------------------- 1 | import Report from '../models/Report.js'; 2 | import User from '../models/User.js'; 3 | import { validationResult } from 'express-validator'; 4 | 5 | // Create a new report 6 | export const createReport = async (req, res, next) => { 7 | try { 8 | const errors = validationResult(req); 9 | if (!errors.isEmpty()) { 10 | return res.status(400).json({ errors: errors.array() }); 11 | } 12 | 13 | const { type, latitude, longitude, title, description, species, severity, imageUrl } = req.body; 14 | 15 | // Get user's vessel info 16 | const user = await User.findById(req.user._id); 17 | 18 | const report = await Report.create({ 19 | type, 20 | location: { 21 | type: 'Point', 22 | coordinates: [parseFloat(longitude), parseFloat(latitude)] 23 | }, 24 | title, 25 | description, 26 | species: type === 'hotspot' ? species : undefined, 27 | severity, 28 | imageUrl, 29 | submittedBy: req.user._id, 30 | vesselInfo: { 31 | vesselName: user.vesselName, 32 | vesselType: user.vesselType 33 | } 34 | }); 35 | 36 | // Populate submittedBy field 37 | await report.populate('submittedBy', 'name email role vesselName'); 38 | 39 | res.status(201).json({ 40 | message: 'Report created successfully', 41 | report 42 | }); 43 | 44 | } catch (error) { 45 | next(error); 46 | } 47 | }; 48 | 49 | // Get all reports with optional filters 50 | export const getReports = async (req, res, next) => { 51 | try { 52 | const { type, severity, limit = 100, includeInactive = false } = req.query; 53 | 54 | const filter = {}; 55 | 56 | if (type && ['hotspot', 'pollution'].includes(type)) { 57 | filter.type = type; 58 | } 59 | 60 | if (severity && ['low', 'medium', 'high', 'critical'].includes(severity)) { 61 | filter.severity = severity; 62 | } 63 | 64 | if (!includeInactive) { 65 | filter.isActive = true; 66 | } 67 | 68 | const reports = await Report.find(filter) 69 | .populate('submittedBy', 'name email role vesselName vesselType') 70 | .sort({ createdAt: -1 }) 71 | .limit(parseInt(limit)); 72 | 73 | res.json({ 74 | count: reports.length, 75 | reports 76 | }); 77 | 78 | } catch (error) { 79 | next(error); 80 | } 81 | }; 82 | 83 | // Get a single report by ID 84 | export const getReportById = async (req, res, next) => { 85 | try { 86 | const { id } = req.params; 87 | 88 | const report = await Report.findById(id) 89 | .populate('submittedBy', 'name email role vesselName vesselType'); 90 | 91 | if (!report) { 92 | return res.status(404).json({ message: 'Report not found' }); 93 | } 94 | 95 | res.json({ report }); 96 | 97 | } catch (error) { 98 | next(error); 99 | } 100 | }; 101 | 102 | // Get reports by current user 103 | export const getMyReports = async (req, res, next) => { 104 | try { 105 | const reports = await Report.find({ submittedBy: req.user._id }) 106 | .sort({ createdAt: -1 }); 107 | 108 | res.json({ 109 | count: reports.length, 110 | reports 111 | }); 112 | 113 | } catch (error) { 114 | next(error); 115 | } 116 | }; 117 | 118 | // Update a report (only by owner or admin) 119 | export const updateReport = async (req, res, next) => { 120 | try { 121 | const errors = validationResult(req); 122 | if (!errors.isEmpty()) { 123 | return res.status(400).json({ errors: errors.array() }); 124 | } 125 | 126 | const { id } = req.params; 127 | const { title, description, species, severity, imageUrl } = req.body; 128 | 129 | const report = await Report.findById(id); 130 | 131 | if (!report) { 132 | return res.status(404).json({ message: 'Report not found' }); 133 | } 134 | 135 | // Check if user is owner or admin 136 | if (report.submittedBy.toString() !== req.user._id.toString() && req.user.role !== 'admin') { 137 | return res.status(403).json({ message: 'You can only update your own reports' }); 138 | } 139 | 140 | // Update fields 141 | if (title) report.title = title; 142 | if (description) report.description = description; 143 | if (species && report.type === 'hotspot') report.species = species; 144 | if (severity) report.severity = severity; 145 | if (imageUrl !== undefined) report.imageUrl = imageUrl; 146 | 147 | await report.save(); 148 | await report.populate('submittedBy', 'name email role vesselName vesselType'); 149 | 150 | res.json({ 151 | message: 'Report updated successfully', 152 | report 153 | }); 154 | 155 | } catch (error) { 156 | next(error); 157 | } 158 | }; 159 | 160 | // Delete a report (only by owner or admin) 161 | export const deleteReport = async (req, res, next) => { 162 | try { 163 | const { id } = req.params; 164 | 165 | const report = await Report.findById(id); 166 | 167 | if (!report) { 168 | return res.status(404).json({ message: 'Report not found' }); 169 | } 170 | 171 | // Check if user is owner or admin 172 | if (report.submittedBy.toString() !== req.user._id.toString() && req.user.role !== 'admin') { 173 | return res.status(403).json({ message: 'You can only delete your own reports' }); 174 | } 175 | 176 | await Report.findByIdAndDelete(id); 177 | 178 | res.json({ message: 'Report deleted successfully' }); 179 | 180 | } catch (error) { 181 | next(error); 182 | } 183 | }; 184 | 185 | // Get reports near a location (for alerts) 186 | export const getReportsNearLocation = async (req, res, next) => { 187 | try { 188 | const { latitude, longitude, maxDistance = 50000 } = req.query; // maxDistance in meters (default 50km) 189 | 190 | if (!latitude || !longitude) { 191 | return res.status(400).json({ message: 'Latitude and longitude are required' }); 192 | } 193 | 194 | const reports = await Report.find({ 195 | isActive: true, 196 | location: { 197 | $near: { 198 | $geometry: { 199 | type: 'Point', 200 | coordinates: [parseFloat(longitude), parseFloat(latitude)] 201 | }, 202 | $maxDistance: parseInt(maxDistance) 203 | } 204 | } 205 | }) 206 | .populate('submittedBy', 'name role vesselName') 207 | .limit(20); 208 | 209 | res.json({ 210 | count: reports.length, 211 | reports 212 | }); 213 | 214 | } catch (error) { 215 | next(error); 216 | } 217 | }; 218 | 219 | // Toggle report active status (admin only) 220 | export const toggleReportStatus = async (req, res, next) => { 221 | try { 222 | const { id } = req.params; 223 | 224 | const report = await Report.findById(id); 225 | 226 | if (!report) { 227 | return res.status(404).json({ message: 'Report not found' }); 228 | } 229 | 230 | report.isActive = !report.isActive; 231 | await report.save(); 232 | 233 | res.json({ 234 | message: `Report ${report.isActive ? 'activated' : 'deactivated'} successfully`, 235 | report 236 | }); 237 | 238 | } catch (error) { 239 | next(error); 240 | } 241 | }; 242 | 243 | -------------------------------------------------------------------------------- /RESPONSIBILITIES.md: -------------------------------------------------------------------------------- 1 | # EcoMarineWay: Team Roles & Sprint Responsibilities 2 | 3 | 👥 **Team Members:** Sasmitha, Isara, Dinithi, Olivia 4 | 5 | 🔁 **Project:** 5-Sprint Agile Development (Sprint 0 + 4 development sprints) 6 | 7 | 🎯 **Goal:** Build a Minimum Viable Product (MVP) for Marine Protected Area (MPA) navigation, reporting, and alerts. 8 | 9 | --- 10 | 11 | ## 🧩 Overview: Team Roles & Responsibilities 12 | 13 | | Role | General Responsibilities | 14 | |--------------------------|---------------------------| 15 | | **Product Owner (PO)** | Defines the project vision, manages and prioritizes the product backlog, makes sure features match stakeholder needs | 16 | | **Scrum Master (SM)** | Facilitates Scrum meetings (planning, daily standups, reviews, retrospectives), supports the team, removes obstacles, ensures Scrum rules are followed | 17 | | **Full-Stack Dev** | Builds and connects both frontend (React) and backend (Node.js + MongoDB), integrates APIs, tests features, and ensures the app works as expected | 18 | 19 | --- 20 | 21 | ## 🔁 Role Rotation & Handover 22 | 23 | * PO and SM rotate across sprints as assigned above to share ownership and learning. 24 | * At the end of each sprint the outgoing PO/SM writes a short **handover note** (1–2 bullets) in ClickUp to the incoming role covering: blockers, pending decisions, and open technical debts. 25 | 26 | --- 27 | 28 | ## 🔌 REST API (initial) 29 | 30 | | Method | Path | Purpose | Auth | 31 | | ------ | ------------------------- | --------------------------------------------------------------------- | ---------- | 32 | | POST | `/api/auth/signup` | Register user | Public | 33 | | POST | `/api/auth/login` | Obtain JWT | Public | 34 | | GET | `/api/users/me` | Current user profile | JWT | 35 | | GET | `/api/mpa` | List MPA zones (geojson bbox or all) | Public | 36 | | POST | `/api/reports` | Create report (hotspot/pollution) | JWT | 37 | | GET | `/api/reports` | List reports (approved + own pending) | Public/JWT | 38 | | GET | `/api/reports/:id` | Get one report | Public | 39 | | PATCH | `/api/reports/:id/status` | Admin approve/reject | Admin | 40 | | POST | `/api/alerts/check` | Send `{lat,lng}` → server returns alert(s) if inside MPA/near hotspot | JWT | 41 | 42 | --- 43 | 44 | ## 📍 Frontend Pages 45 | 46 | * **Public:** Home/Map (view MPAs & markers), Login, Signup. 47 | * **Authenticated:** Report Form (Hotspot/Pollution), My Profile (my reports), Live Simulate (move marker, see alerts). 48 | * **Admin:** Simple moderation table (approve/reject reports). 49 | 50 | --- 51 | 52 | ## 🗓️ Sprint Plans 53 | 54 | > **Timebox:** 1–2 weeks per sprint. Each sprint includes planning, daily stand-ups, review, and retrospective. 55 | 56 | --- 57 | 58 | ### 🗓️ Sprint 0 — Setup & Planning (1 week) [ 9th of August - 16th of August ] 59 | **PO = Dinithi | SM = Sasmitha** 60 | 61 | **Sprint Goal:** Environments ready; skeleton apps deploy; initial backlog. 62 | 63 | **Backend Initialisation (Isara)** 64 | * Create Node/Express project 65 | * Connect to **MongoDB Atlas** via env vars. 66 | * Health endpoint `GET /health`. 67 | * Seed script for demo MPA polygons (GeoJSON). 68 | * CI: run `npm test`, `npm run lint`. 69 | 70 | **Frontend Initialisation (Olivia)** 71 | * Create a React app with Tailwind. 72 | * Install Leaflet; render base map (OpenStreetMap tiles). 73 | * Project layout (Navbar, Container, Footer). 74 | * CI: build check. 75 | 76 | **Process (SM + PO)** 77 | * ClickUp: space, lists (Product Backlog, Sprint Backlog, Bugs). 78 | * Define **DoD**, **Definition of Ready**, working agreements. 79 | * Release plan + risk log. 80 | 81 | **Deliverables:** 82 | - BE & FE skeleton deployed (Render/Railway & Netlify). 83 | - ClickUp board with epics/stories and priorities. 84 | - Repo README with run scripts & env setup. 85 | 86 | --- 87 | 88 | ### 🗓️ Sprint 1 — Authentication & Static Map - (2 weeks) [ 16th of August - 30th of August ] 89 | **PO = Isara | SM = Olivea** 90 | 91 | **Sprint Goal:** Users can sign up/login; map shows MPA zones. 92 | 93 | **Map Integration (Sasmitha)** 94 | * `/api/mpa` returns seeded polygons (GeoJSON). 95 | * Unit tests (auth + mpa). 96 | * Leaflet renders MPA polygons with legend. 97 | 98 | **User Management Features (Dinithi)** 99 | * Auth pages (Signup/Login) with validation. 100 | * `/api/auth/signup`, `/api/auth/login` with JWT. 101 | * `/api/users/me` (JWT). 102 | * JWT storage + protected routes. 103 | 104 | **Scrum (SM)** 105 | * Facilitate daily standups, burndown, and remove blockers. 106 | 107 | **Product (PO)** 108 | * Refine acceptance criteria for auth & map stories. 109 | 110 | **Deliverables:** 111 | - Register/login/logout works end-to-end. 112 | - MPA polygons visible on the map. 113 | 114 | --- 115 | 116 | (‼ We will be updating the scope of sprint 2 after re-evaluating the sprint 1 ‼) 117 | 118 | ### 🗓️ Sprint 2 — Reporting (Hotspots & Pollution) 119 | **PO = Sasmitha | SM = Olivia** 120 | 121 | **Sprint Goal:** Users submit/view reports; DB stores & lists them. 122 | 123 | **User reporting feature (Isara)** 124 | * `POST /api/reports`, `GET /api/reports`, `GET /api/reports/:id`. 125 | * Admin approval API. 126 | * Tests: create/list/status change. 127 | 128 | **Frontend (Olivia)** 129 | * Report form (Hotspot + Pollution). 130 | * Map click → prefill location. 131 | * Marker layer for reports + popup details. 132 | * Profile: “My Reports” table with status. 133 | 134 | **Scrum (SM)** 135 | * Track sprint progress, remove blockers. 136 | 137 | **Product (PO)** 138 | * Approve UX flow, update backlog. 139 | 140 | **Deliverables:** 141 | - Create & list hotspot/pollution reports. 142 | - Map markers with popups. 143 | - Admin approval working. 144 | 145 | --- 146 | 147 | ### 🗓️ Sprint 3 — Alerts & Map Filtering 148 | **PO = Dinithi | SM = Olivea** 149 | 150 | **Sprint Goal:** Show proximity/zone alerts; improve map usability. 151 | 152 | **Backend (Isara)** 153 | * `/api/alerts/check` with turf.js. 154 | * Return alerts if inside MPA or near a hotspot 155 | * Geometry tests. 156 | 157 | **Frontend (Olivia)** 158 | * Vessel simulation page (play/pause). 159 | * On position change → call `/alerts/check` → show alert. 160 | * Filters for toggling layers. 161 | * Marker detail panel. 162 | 163 | **Scrum (SM)** 164 | * Facilitate sprint events, track burndown. 165 | 166 | **Product (PO)** 167 | * Define alert UX, approve filtering requirements. 168 | 169 | **Deliverables:** 170 | - Moving vessel marker demo. 171 | - Real-time alerts. 172 | - Map filtering. 173 | 174 | --- 175 | 176 | ### 🗓️ Sprint 4 — Profiles, Polish, Docs & Demo 177 | **PO = Isara | SM = Sasmitha** 178 | 179 | **Sprint Goal:** Finalise UX, profiles, performance, and documentation. 180 | 181 | **Backend (Isara)** 182 | * `/api/users/me` returns user’s reports summary. 183 | * Pagination + caching for MPAs. 184 | 185 | **Frontend (Olivia)** 186 | * Profile page: user’s reports list. 187 | * UI polish (empty states, error handling). 188 | * Accessibility checks. 189 | 190 | **Scrum (SM)** 191 | * Sprint review & retrospective. 192 | * Final burndown & velocity snapshot. 193 | 194 | **Product (PO)** 195 | * Final acceptance, demo prep, documentation review. 196 | 197 | **Deliverables:** 198 | - Final integrated system (map + reports + alerts + filters). 199 | - Profile page working. 200 | - Final docs, slides, and Scrum evidence. 201 | 202 | --- 203 | 204 | ## ✅ Definition of Done (DoD) 205 | 206 | A story is **Done** when: 207 | * Code merged to `main` via PR with review and passing CI. 208 | * Tests (unit/integration) cover key paths. 209 | * UX validated on desktop & mobile. 210 | * Deployed to staging (Netlify + Render). 211 | * Documentation updated (README, API docs). 212 | * No critical console/server errors. 213 | 214 | --- 215 | 216 | ## 🧭 Risks & Mitigations 217 | 218 | * **Map performance:** Use clustering & pagination. 219 | * **Geometry accuracy:** Use turf.js + tests. 220 | * **Auth pitfalls:** JWT expiry, protected routes. 221 | * **Free-tier limits:** Small payloads, optional image upload. 222 | 223 | --- 224 | 225 | ## 🚫 Out of Scope (v1) 226 | 227 | * Real vessel AIS data (use simulated path only). 228 | * Offline support & advanced analytics. 229 | * Push notifications. 230 | * Complex moderation workflows. 231 | 232 | --- 233 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🌊 EcoMarineWay 2 | 3 | **EcoMarineWay** is a low-impact maritime navigation alert application for ships to navigate safely through marine protected areas (MPAs). 4 | 5 | **Status:** 🚧 Development in progress 6 | **Timeline:** 🗓️ Sprint 0 + 4 development sprints (8 weeks) 7 | **MVP Goal:** 🎯 Deliver the core features required for ships to navigate MPAs safely. 8 | 9 | --- 10 | 11 | ## 📋 Overview 12 | EcoMarineWay is a lightweight maritime navigation tool designed to help ships safely traverse marine protected areas while supporting community-driven reporting of marine wildlife and pollution incidents. Using Agile Scrum, we are delivering a Minimum Viable Product (MVP) over 8 weeks with a 4-member team. 13 | 14 | --- 15 | 16 | ## 🛠️ Tech Stack 17 | - **Frontend:** React 18 | - **Backend:** Node.js + Express 19 | - **Database:** MongoDB Atlas 20 | - **Deployment:** Netlify / Render 21 | - **Project Management:** ClickUp 22 | 23 | --- 24 | 25 | ## ⭐ MVP Features 26 | - **🗺️ Interactive Map:** View and navigate MPAs on an interactive map. 27 | - **⚠️ Proximity Alerts:** Receive notifications when approaching protected areas or reported hotspots. 28 | - **🐟 Hotspot Reporting:** Submit and view marine animal sightings. 29 | - **🛢️ Pollution Reporting:** Report pollution incidents with location details. 30 | - **🔍 Map Filtering:** Filter visible reports by type (hotspots or pollution). 31 | 32 | --- 33 | 34 | ## 🚀 Agile Sprint Plan 35 | 36 | ### Sprint 0 – Setup & Planning 37 | **Goal:** Establish development environment, project setup, and initial backlog. 38 | **Tasks:** 39 | - Create GitHub repo & branch strategy. 40 | - Set up MERN skeleton (React + Express + MongoDB Atlas). 41 | - Configure CI/CD (deploy skeleton to Netlify/Render). 42 | - Set up ClickUp workspace, backlog, and board. 43 | - Design wireframes for core screens (map, report form, login). 44 | 45 | **Deliverables:** 46 | - Working MERN skeleton deployed online. 47 | - Wireframes for core features. 48 | - Initial prioritized product backlog in ClickUp. 49 | 50 | --- 51 | 52 | ### Sprint 1 – Core Setup & Map 53 | **Goal:** Enable user access and provide a basic map with MPAs. 54 | **Tasks:** 55 | - Implement user signup/login (JWT) and roles (user/admin). 56 | - Build interactive map with static MPA polygons. 57 | - Create basic UI layout and navigation. 58 | - Write initial test cases for authentication. 59 | 60 | **Deliverables:** 61 | - Users can register/login/logout. 62 | - Map with visible protected zones. 63 | - Deployed increment demonstrating login + map. 64 | 65 | --- 66 | 67 | ### Sprint 2 – Reporting Features 68 | **Goal:** Allow users to report and view hotspots and pollution incidents. 69 | **Tasks:** 70 | - Build hotspot reporting form (location + species + notes). 71 | - Display hotspot markers on map. 72 | - Implement pollution report form (location + description). 73 | - Create list view of reports. 74 | - Add backend APIs for reports. 75 | 76 | **Deliverables:** 77 | - Hotspot reports visible on map. 78 | - Pollution reports created and listed. 79 | - Database stores reports reliably. 80 | 81 | --- 82 | 83 | ### Sprint 3 – Alerts & Filtering 84 | **Goal:** Improve usability with alerts and filtering. 85 | **Tasks:** 86 | - Implement simulated vessel path with moving marker. 87 | - Add proximity alert when near hotspots/protected areas. 88 | - Implement map filtering (toggle hotspots/pollution). 89 | - Improve UI for report details (click marker to view info). 90 | 91 | **Deliverables:** 92 | - Vessel marker moves along a path. 93 | - Alerts shown when entering protected zones/hotspots. 94 | - Filter buttons working on map. 95 | - Usable clickable markers. 96 | 97 | --- 98 | 99 | ### Sprint 4 – Polishing & Finalization 100 | **Goal:** Refine the system, add profile view, and prepare for presentation. 101 | **Tasks:** 102 | - Add basic user profile page (list of user’s submitted reports). 103 | - Fix bugs and improve UI styling. 104 | - Write documentation for features. 105 | - Prepare burndown charts, sprint logs, and final report. 106 | - Run rehearsal for demo/presentation. 107 | 108 | **Deliverables:** 109 | - Final integrated system (map + reports + alerts + filters). 110 | - User profile page functional. 111 | - Polished UI and bug-free demo. 112 | - Final report + presentation slides + evidence of Scrum events. 113 | 114 | --- 115 | 116 | ## 🏗️ Agile Practices 117 | - **Daily Standups:** 15 mins, sync progress and blockers 118 | - **Sprint Reviews:** Demo features to stakeholders 119 | - **Retrospectives:** Reflect and improve team process 120 | - **User Testing:** At least 5 test users per sprint 121 | - **Backlog Grooming:** Weekly refinement using MoSCoW prioritization 122 | 123 | --- 124 | 125 | ## ✅ Definition of Done (DoD) 126 | A feature is considered "Done" when: 127 | - Code is peer-reviewed and merged 128 | - Unit/integration tests pass 129 | - Responsive and accessible UI 130 | - Documented in project wiki 131 | - Tested in staging environment 132 | 133 | --- 134 | 135 | ## 📌 Backlog & Scope Management 136 | - **MVP focus:** Only must-have features included 137 | - **Change control:** New requests go to backlog for post-MVP 138 | - **Lean approach:** Use existing APIs and component libraries to save development time 139 | 140 | --- 141 | 142 | ## 🔒 Key Technical Considerations 143 | - **Security:** HTTPS, JWT authentication, Stripe PCI compliance 144 | - **Performance:** Optimized API responses, lazy loading 145 | - **Trust:** Transparent impact reporting, verified charities (TBD post-MVP) 146 | - **Scalability:** Flexible MongoDB schema supports reporting growth 147 | 148 | --- 149 | 150 | Got it 👍 Since **EcoMarineWay** is a maritime navigation + reporting app, the risk management table should be tailored to its domain instead of payment/donation risks. Here’s an updated version: 151 | 152 | --- 153 | 154 | ## ⚠️ Risk Management 155 | 156 | | Risk | Impact | Mitigation | 157 | | ----------------------------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | 158 | | Inaccurate or missing marine data (MPA boundaries, reports) | Ships may not receive correct alerts → reduces reliability of system | Use reliable open-data sources (e.g., government, NGOs), allow community validation of reports, maintain update pipeline | 159 | | Poor internet connectivity at sea | Users may lose access to map and alerts in critical moments | Provide offline caching of MPA boundaries, queue reports for sync when online | 160 | | Over-reporting / false reporting by users | Map clutter, unreliable alerts | Add basic moderation (admin role), allow users to flag/report incorrect entries, filter by trusted sources | 161 | | Performance issues on low-power ship devices | Laggy map rendering or slow API responses | Optimize map with lazy loading & clustering, lightweight UI, caching of common queries | 162 | | Limited adoption by ship crews | Low engagement and limited reporting → app loses value | Conduct early user testing with seafarers, design simple UI, provide training/demo materials | 163 | | Security vulnerabilities (data leaks, account misuse) | Compromised trust, data misuse | Implement HTTPS, JWT authentication, regular security audits, minimal PII storage | 164 | | Team workload and tight sprint schedule | Risk of incomplete features within 8 weeks | Prioritize MVP scope, parallelize frontend/backend tasks, reuse existing libraries | 165 | 166 | --- 167 | 168 | ## 🤝 Stakeholder Engagement 169 | - Regular demos after each sprint 170 | - Feedback incorporated into backlog 171 | - Maintain open communication through ClickUp & Slack 172 | 173 | --- 174 | 175 | ## 🎯 Conclusion 176 | Delivering a functional, lightweight maritime navigation tool in 5 sprints is feasible with: 177 | 178 | - Clear MVP scope 179 | - Lean tech stack (MongoDB, React, Node.js) 180 | - Iterative development and continuous feedback 181 | - Focused Scrum practices 182 | 183 | By the end of Sprint 4, EcoMarineWay will be production-ready with all core features live, tested, and usable for safe navigation through MPAs. 184 | 185 | --- 186 | 187 | ## 📂 Project Resources 188 | - Figma Wireframes (link when available) 189 | - API Documentation (to be created) 190 | -------------------------------------------------------------------------------- /frontend/src/pages/Login.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Link, useNavigate } from 'react-router-dom'; 3 | import { useForm } from 'react-hook-form'; 4 | import { useAuth } from '../context/AuthContext'; 5 | import toast from 'react-hot-toast'; 6 | import { 7 | MdEmail, 8 | MdLock, 9 | MdLogin, 10 | MdDirectionsBoat, 11 | MdRefresh 12 | } from 'react-icons/md'; 13 | 14 | const Login = () => { 15 | const [loading, setLoading] = useState(false); 16 | const { login } = useAuth(); 17 | const navigate = useNavigate(); 18 | 19 | const { 20 | register, 21 | handleSubmit, 22 | formState: { errors }, 23 | } = useForm(); 24 | 25 | const onSubmit = async (data) => { 26 | setLoading(true); 27 | try { 28 | await login(data); 29 | toast.success('Login successful!'); 30 | // Stay on login page or redirect to a different page if needed 31 | navigate('/home'); // 👈 redirect to Home.js 32 | } catch (error) { 33 | const message = error.response?.data?.message || 'Login failed'; 34 | toast.error(message); 35 | } finally { 36 | setLoading(false); 37 | } 38 | }; 39 | 40 | return ( 41 |
42 | {/* Static background elements */} 43 |
44 | {/* Static ship icons */} 45 |
46 | 47 |
48 | 49 |
50 | 51 |
52 |
53 | 54 | {/* Content */} 55 |
56 |
57 | {/* Header */} 58 |
59 |
60 | 61 |
62 |

63 | Welcome Back 64 |

65 |

66 | Sign in to your EcoNav MPA account 67 |

68 |
69 |
70 | Maritime Navigation 71 |
72 |
73 |
74 | 75 | {/* Login Form */} 76 |
77 |
78 |
79 |
80 | 83 |
84 |
85 | 86 |
87 | 102 |
103 | {errors.email && ( 104 |

105 | 106 | 107 | 108 | {errors.email.message} 109 |

110 | )} 111 |
112 | 113 |
114 | 117 |
118 |
119 | 120 |
121 | 136 |
137 | {errors.password && ( 138 |

139 | 140 | 141 | 142 | {errors.password.message} 143 |

144 | )} 145 |
146 |
147 | 148 |
149 | 166 |
167 | 168 |
169 |

170 | Don't have an account?{' '} 171 | 175 | Sign up here 176 | 177 |

178 |
179 |
180 |
181 |
182 |
183 |
184 | ); 185 | }; 186 | 187 | export default Login; 188 | -------------------------------------------------------------------------------- /frontend/src/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Anchor } from 'lucide-react'; 3 | 4 | const Footer = () => { 5 | return ( 6 |
12 |
16 |
22 | {/* Company Info */} 23 |
24 |
31 | 32 | EcoNav 33 |
34 |

40 | Leading the future of sustainable maritime navigation with cutting-edge real-time tracking, 41 | environmental optimization, and AI-powered route planning solutions. 42 |

43 |
47 | {['🌊', '⚓', '🚢', '🌍'].map((emoji, index) => ( 48 |
{ 63 | e.target.style.backgroundColor = 'rgba(96, 165, 250, 0.2)'; 64 | e.target.style.transform = 'translateY(-2px)'; 65 | }} 66 | onMouseLeave={(e) => { 67 | e.target.style.backgroundColor = 'rgba(96, 165, 250, 0.1)'; 68 | e.target.style.transform = 'translateY(0)'; 69 | }} 70 | > 71 | {emoji} 72 |
73 | ))} 74 |
75 |
76 | 77 | {/* Quick Links */} 78 |
79 |

85 | Navigation 86 |

87 | 123 |
124 | 125 | {/* Contact Info */} 126 |
127 |

133 | Get In Touch 134 |

135 |
136 |
142 |
151 | 📧 152 |
153 | support@econav.maritime 154 |
155 |
161 |
170 | 📞 171 |
172 | +1 (555) 123-4567 173 |
174 |
180 |
189 | 📍 190 |
191 | Maritime Technology Center, Port District 192 |
193 | 194 |
200 |
206 |
215 | 🚨 216 |
217 | 24/7 Maritime Support 218 |
219 |

220 | Emergency assistance and technical support available around the clock 221 |

222 |
223 |
224 |
225 |
226 | 227 | {/* Bottom Bar */} 228 |
237 |
244 | © 2025 EcoNav Maritime Solutions. 245 | All rights reserved. 246 |
252 | Pioneering Sustainable Navigation 253 |
254 | 255 |
260 | {[ 261 | 'Privacy Policy', 262 | 'Terms of Service', 263 | 'Cookie Policy', 264 | 'Security Center', 265 | 'Compliance' 266 | ].map((link, index) => ( 267 | e.target.style.color = '#60a5fa'} 278 | onMouseLeave={(e) => e.target.style.color = '#9ca3af'} 279 | > 280 | {link} 281 | 282 | ))} 283 |
284 |
285 |
286 |
287 | ); 288 | }; 289 | 290 | export default Footer; -------------------------------------------------------------------------------- /frontend/src/pages/EcoComplianceHub.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useState } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { 4 | Leaf, 5 | Fish, 6 | Anchor, 7 | AlertTriangle, 8 | BookOpen, 9 | Award, 10 | Users, 11 | Globe, 12 | Camera, 13 | MapPin, 14 | Waves, 15 | Shield, 16 | Info, 17 | ChevronRight, 18 | PlayCircle, 19 | FileText, 20 | Clock, 21 | } from 'lucide-react'; 22 | import Header from '../components/Header'; 23 | import Footer from '../components/Footer'; 24 | 25 | const EcoComplianceHub = () => { 26 | const [activeSection, setActiveSection] = useState('education'); 27 | const [headerOpacity, setHeaderOpacity] = useState(1); 28 | const navigate = useNavigate(); 29 | 30 | const handleTopicClick = (topic) => { 31 | // Fixed navigation: pass topicId to TopicDetail 32 | navigate('/topic/:topicParam-detail', { state: { topicId: topic.id } }); 33 | }; 34 | 35 | useEffect(() => { 36 | const onScroll = () => { 37 | const y = window.scrollY; 38 | const min = 0; 39 | const max = 100; 40 | const t = Math.max(0, Math.min(1, (y - min) / (max - min))); 41 | setHeaderOpacity(1 - t * 0.1); // from 1 to 0.9 42 | }; 43 | window.addEventListener('scroll', onScroll, { passive: true }); 44 | return () => window.removeEventListener('scroll', onScroll); 45 | }, []); 46 | 47 | const educationalTopics = useMemo( 48 | () => [ 49 | { 50 | id: 1, 51 | title: 'Marine Protected Areas', 52 | subtitle: 'Understanding conservation zones', 53 | icon: Shield, 54 | color: '#10b981', 55 | gradient: ['#10b981', '#059669'], 56 | description: 'Learn about different types of MPAs, their boundaries, and protection levels.', 57 | readTime: '5 min read', 58 | image: '/mpa.jpg', 59 | }, 60 | { 61 | id: 2, 62 | title: 'Marine Wildlife Protection', 63 | subtitle: 'Protecting ocean biodiversity', 64 | icon: Fish, 65 | color: '#3b82f6', 66 | gradient: ['#3b82f6', '#1d4ed8'], 67 | description: 'Discover endangered species, migration patterns, and conservation efforts.', 68 | readTime: '7 min read', 69 | image: '/wild.jpg', 70 | }, 71 | { 72 | id: 3, 73 | title: 'Sustainable Fishing Practices', 74 | subtitle: 'Responsible fishing guidelines', 75 | icon: Anchor, 76 | color: '#f5e90bff', 77 | gradient: ['#d6f50bff', '#d9d206ff'], 78 | description: 'Best practices for sustainable fishing and marine resource management.', 79 | readTime: '6 min read', 80 | image: '/fishing.jpg', 81 | }, 82 | { 83 | id: 4, 84 | title: 'Ocean Pollution Prevention', 85 | subtitle: 'Keeping our oceans clean', 86 | icon: Waves, 87 | color: '#06b6d4', 88 | gradient: ['#06b6d4', '#0891b2'], 89 | description: 'Understanding pollution sources and prevention strategies.', 90 | readTime: '4 min read', 91 | image: '/nav4.jpg', 92 | }, 93 | { 94 | id: 5, 95 | title: 'Compliance Regulations', 96 | subtitle: 'Maritime laws & guidelines', 97 | icon: FileText, 98 | color: '#8b5cf6', 99 | gradient: ['#8b5cf6', '#7c3aed'], 100 | description: 'International and local regulations for marine conservation.', 101 | readTime: '8 min read', 102 | image: '/anchor.jpg', 103 | }, 104 | { 105 | id: 6, 106 | title: 'Climate Change Impact', 107 | subtitle: 'Ocean warming & acidification', 108 | icon: Globe, 109 | color: '#ef4444', 110 | gradient: ['#ef4444', '#dc2626'], 111 | description: 'How climate change affects marine ecosystems and biodiversity.', 112 | readTime: '9 min read', 113 | image: '/nav6.jpg', 114 | }, 115 | ], 116 | [] 117 | ); 118 | 119 | const stats = useMemo( 120 | () => [ 121 | { label: 'Protected Areas', value: '15,000+', icon: Shield }, 122 | { label: 'Species Protected', value: '8,500+', icon: Fish }, 123 | { label: 'Active Users', value: '25,000+', icon: Users }, 124 | { label: 'Educational Resources', value: '200+', icon: BookOpen }, 125 | ], 126 | [] 127 | ); 128 | 129 | const handleQuickAction = (action) => { 130 | console.log('Quick action:', action); 131 | }; 132 | 133 | return ( 134 |
135 |
139 |
140 |
141 | 142 |
143 | {/* Hero Card */} 144 |
145 |
146 |
147 |
148 | 149 |
150 |

151 | Eco-Compliance & Awareness Hub 152 |

153 |

154 | Learn, protect, and preserve our marine ecosystems through education and responsible 155 | practices 156 |

157 | 158 |
159 | {stats.map((s, i) => ( 160 |
161 | 162 |
{s.value}
163 |
{s.label}
164 |
165 | ))} 166 |
167 |
168 |
169 | 170 | {/* Featured Header */} 171 |
172 |
173 | 174 |

Featured Learning Topics

175 |
176 |

177 | Explore comprehensive guides on marine conservation and compliance 178 |

179 |
180 | 181 | {/* Topics Grid */} 182 |
183 | {educationalTopics.map((topic) => ( 184 |
handleTopicClick(topic)} 188 | > 189 |
190 |
191 |
192 |
196 | 197 |
198 |
199 |

{topic.title}

200 |

{topic.subtitle}

201 |
202 | 203 |
204 | 205 |
206 | {topic.title} 212 |
213 | 214 |

{topic.description}

215 | 216 |
217 |
218 | 219 | {topic.readTime} 220 |
221 | 222 |
226 | Learn More 227 |
228 |
229 |
230 |
231 | ))} 232 |
233 | 234 | {/* Quick Actions */} 235 |
236 |

Quick Actions

237 |
238 | 245 | 252 | 259 | 266 |
267 |
268 |
269 |
270 |
271 | ); 272 | }; 273 | 274 | export default EcoComplianceHub; 275 | -------------------------------------------------------------------------------- /frontend/src/components/TechnologyStack.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Satellite, Map, Database, Cpu, Shield, Cloud, Navigation, Waves, AlertTriangle, Zap, Globe, BarChart3 } from 'lucide-react'; 3 | 4 | const TechnologyStack = () => { 5 | const technologies = [ 6 | { 7 | category: "Mapping & Visualization", 8 | items: [ 9 | { 10 | name: "Leaflet Maps API", 11 | description: "Open-source JavaScript library for interactive maps", 12 | icon: , 13 | color: "#28a745", 14 | features: ["Custom vessel markers", "Interactive zone polygons", "Real-time position updates"] 15 | }, 16 | { 17 | name: "Esri World Imagery", 18 | description: "High-resolution satellite and aerial imagery", 19 | icon: , 20 | color: "#007bff", 21 | features: ["Base map layer", "Oceanographic data", "Coastal details"] 22 | } 23 | ] 24 | }, 25 | { 26 | category: "Environmental Data", 27 | items: [ 28 | { 29 | name: "Marine Protected Areas API", 30 | description: "Global database of sensitive marine zones", 31 | icon: , 32 | color: "#ffc107", 33 | features: ["Real-world coordinates", "Restriction details", "Authority information"] 34 | }, 35 | { 36 | name: "AIS Vessel Tracking", 37 | description: "Automatic Identification System data integration", 38 | icon: , 39 | color: "#17a2b8", 40 | features: ["Live vessel positions", "Ship metadata", "Movement patterns"] 41 | } 42 | ] 43 | }, 44 | { 45 | category: "Backend & Processing", 46 | items: [ 47 | { 48 | name: "WebSocket API", 49 | description: "Real-time bidirectional communication", 50 | icon: , 51 | color: "#fd7e14", 52 | features: ["Live data streaming", "Instant updates", "Low latency connection"] 53 | }, 54 | { 55 | name: "Geospatial Processing", 56 | description: "Coordinate calculations and route optimization", 57 | icon: , 58 | color: "#6f42c1", 59 | features: ["Bearing calculations", "Collision detection", "Zone violation alerts"] 60 | } 61 | ] 62 | }, 63 | { 64 | category: "Data & Infrastructure", 65 | items: [ 66 | { 67 | name: "CDN Resources", 68 | description: "Content delivery for mapping libraries", 69 | icon: , 70 | color: "#6c757d", 71 | features: ["Fast loading", "Reliable delivery", "Global availability"] 72 | }, 73 | { 74 | name: "Maritime Databases", 75 | description: "Vessel information and zone specifications", 76 | icon: , 77 | color: "#20c997", 78 | features: ["Ship registry data", "Environmental regulations", "Historical tracking"] 79 | } 80 | ] 81 | } 82 | ]; 83 | 84 | return ( 85 |
95 | {/* Decorative elements */} 96 |
105 | 106 |
116 | 117 |
123 |

134 | Advanced Technology Stack 135 |

136 | 137 |

144 | Our maritime monitoring system integrates multiple cutting-edge technologies 145 | to provide comprehensive vessel tracking and environmental protection. 146 |

147 | 148 | {technologies.map((category, categoryIndex) => ( 149 |
150 |

160 | 168 | {categoryIndex === 0 && } 169 | {categoryIndex === 1 && } 170 | {categoryIndex === 2 && } 171 | {categoryIndex === 3 && } 172 | 173 | {category.category} 174 |

175 | 176 |
181 | {category.items.map((tech, techIndex) => ( 182 |
{ 195 | e.currentTarget.style.backgroundColor = '#f1f5f9'; 196 | e.currentTarget.style.borderColor = '#cbd5e1'; 197 | e.currentTarget.style.transform = 'translateY(-4px)'; 198 | e.currentTarget.style.boxShadow = '0 10px 25px rgba(0,0,0,0.1)'; 199 | }} 200 | onMouseLeave={(e) => { 201 | e.currentTarget.style.backgroundColor = '#f8fafc'; 202 | e.currentTarget.style.borderColor = '#e2e8f0'; 203 | e.currentTarget.style.transform = 'translateY(0)'; 204 | e.currentTarget.style.boxShadow = 'none'; 205 | }} 206 | > 207 |
215 | 216 |
221 |
229 | {tech.icon} 230 |
231 |
232 |

237 | {tech.name} 238 |

239 |

244 | {tech.description} 245 |

246 |
247 |
248 | 249 |
    254 | {tech.features.map((feature, featureIndex) => ( 255 |
  • 260 | {feature} 261 |
  • 262 | ))} 263 |
264 |
265 | ))} 266 |
267 |
268 | ))} 269 | 270 |
279 |
287 | 288 |
289 |
290 |

291 | Integrated Data Ecosystem 292 |

293 |

294 | All technologies work together seamlessly to provide real-time maritime intelligence 295 | and environmental protection monitoring. 296 |

297 |
298 |
299 |
300 |
301 | ); 302 | }; 303 | 304 | export default TechnologyStack; -------------------------------------------------------------------------------- /frontend/src/pages/TopicDetail.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useMemo } from 'react'; 2 | import { useLocation, useNavigate } from 'react-router-dom'; 3 | import { 4 | ArrowLeft, Play, Shield, Fish, Anchor, Waves, FileText, Globe, 5 | Clock, Eye, Share, BookOpen, AlertCircle, ChevronRight, ExternalLink 6 | } from 'lucide-react'; 7 | import Header from '../components/Header'; 8 | import Footer from '../components/Footer'; 9 | 10 | const TopicDetail = () => { 11 | const location = useLocation(); 12 | const navigate = useNavigate(); 13 | 14 | const topicId = location.state?.topicId || 1; 15 | const [activeSection, setActiveSection] = useState('education'); 16 | const [showDocumentary, setShowDocumentary] = useState(false); 17 | 18 | // Topics Data 19 | const topics = [ 20 | { 21 | id: 1, 22 | title: 'Marine Protected Areas', 23 | subtitle: 'Understanding conservation zones', 24 | color: '#10b981', 25 | description: 'Learn about different types of MPAs.', 26 | readTime: '5 min read', 27 | video: 'https://youtu.be/fnz-JszBVNM?si=nMykJXyuQZdu_Zqs' ,// ✅ Make sure this file exists in your public/videos folder 28 | longDescription:`As areas of protected marine biodiversity expand, there has been an increase in ocean science funding, essential for preserving marine resources.[11] In 2020, only around 7.5 to 8% of the global ocean area falls under a conservation designation.[12] This area is equivalent to 27 million square kilometres, equivalent to the land areas of Russia and Canada combined, although some argue that the effective conservation zones (ones with the strictest regulations) occupy only 5% of the ocean area (about equivalent to the land area of Russia alone). Marine conservation zones, as with their terrestrial equivalents, vary in terms of rules and regulations. Few zones rule out completely any sort of human activity within their area, as activities such as fishing, tourism, and transport of essential goods and services by ship, are part of the fabric of nation states.History 29 | The form of marine protected areas trace the origins to the World Congress on National Parks in 1962. In 1976, a process was delivered to the excessive rights to every sovereign state to establish marine protected areas at over 200 nautical miles. 30 | 31 | Over the next two decades, a scientific body of evidence marked the utility in the designation of marine protected areas. In the aftermath of the 1992 Earth Summit in Rio de Janeiro, an international target was established with the encompassment of ten percent of the world's marine protected areas. 32 | 33 | On 28 October 2016 in Hobart, Australia, the Convention for the Conservation of Antarctic Marine Living Resources agreed to establish the first Antarctic and largest marine protected area in the world encompassing 1.55 million km2 (600,000 sq mi) in the Ross Sea.[18] Other large MPAs are in the Indian, Pacific, and Atlantic Oceans, in certain exclusive economic zones of Australia and overseas territories of France, the United Kingdom and the United States, with major (990,000 square kilometres (380,000 sq mi) or larger) new or expanded MPAs by these nations since 2012—such as Natural Park of the Coral Sea, Pacific Islands Heritage Marine National Monument, Coral Sea Commonwealth Marine Reserve and South Georgia and the South Sandwich Islands Marine Protected Area. 34 | 35 | When counted with MPAs of all sizes from many other countries, as of April 2023 there are more than 16,615 MPAs, encompassing 7.2% of the world's oceans (26,146,645 km2), with less than half of that area – encompassing 2.9% of the world's oceans – assessed to be fully or highly protected according to the MPA Guide Framework.[19] Efforts to reach the 10% target by 2020 were not met, but a new target of 30% of the world's oceans protected by 2030 has been proposed and is under consideration by the Convention on Biological Diversity.[20]`, 36 | }, 37 | ]; 38 | 39 | const topic = topics.find(t => t.id === topicId) || topics[0]; 40 | 41 | const IconComponent = useMemo(() => { 42 | const icons = { 1: Shield, 2: Fish, 3: Anchor, 4: Waves, 5: FileText, 6: Globe }; 43 | return icons[topic.id] || Shield; 44 | }, [topic.id]); 45 | 46 | // Handlers 47 | const handleBackPress = () => navigate('/eco-compliance-hub'); 48 | const handleVideoPress = () => alert('Play video functionality not implemented yet.'); 49 | const handleMoreInfoVideo = () => { 50 | setShowDocumentary(prev => !prev); // toggle documentary panel 51 | }; 52 | const handleTakeAction = () => navigate('/quiz/:quizId'); 53 | 54 | // Detailed content 55 | const getDetailedContent = (id) => { 56 | const contents = { 57 | 1: `A marine protected area (MPA) is a protected area of the world's seas, oceans, estuaries or in the US, the Great Lakes. These marine areas can come in many forms ranging from wildlife refuges to research facilities.MPAs restrict human activity for a conservation purpose, typically to protect natural or cultural resources. Such marine resources are protected by local, state, territorial, native, regional, national, or international authorities and differ substantially among and between nations. This variation includes different limitations on development, fishing practices, fishing seasons and catch limits, moorings and bans on removing or disrupting marine life. MPAs can provide economic benefits by supporting the fishing industry through the revival of fish stocks, as well as job creation and other market benefits via ecotourism. MPAs can provide value to mobile species. 58 | There are a number of global examples of large marine conservation areas. The Papahānaumokuākea Marine National Monument, is situated in the central Pacific Ocean, around Hawaii, occupying an area of 1.5 million square kilometers. The area is rich in wild life, including the green turtle and the Hawaiian monkfish, alongside 7,000 other species, and 14 million seabirds. In 2017 the Cook Islands passed the Marae Moana Act designating the whole of the country's marine exclusive economic zone, which has an area of 1.9 million square kilometers as a zone with the purpose of protecting and conserving the "ecological, biodiversity and heritage values of the Cook Islands marine environment". Other large marine conservation areas include those around Antarctica, New Caledonia, Greenland, Alaska, Ascension Island, and Brazil..`, 59 | 2: `Marine wildlife faces unprecedented threats in the 21st century, from climate change and pollution...`, 60 | 3: `Sustainable fishing practices are essential for maintaining healthy marine ecosystems...`, 61 | 4: `Ocean pollution threatens marine ecosystems, human health, and the global economy...`, 62 | 5: `Maritime compliance regulations form the legal framework for protecting marine environments...`, 63 | 6: `Climate change represents the greatest long-term threat to marine ecosystems...`, 64 | }; 65 | return contents[id] || contents[1]; 66 | }; 67 | 68 | return ( 69 |
70 | {/* Header */} 71 |
72 | 73 | {/* Main Content */} 74 |
75 | 76 | {/* Back Button */} 77 |
81 | 82 | Back to Hub 83 |
84 | 85 | {/* Hero Section */} 86 |
88 |