├── 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 |
18 | {label}
19 |
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 | You need to enable JavaScript to run this app.
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 |
18 | {label}
19 |
20 | )}
21 |
22 | {icon && (
23 |
24 | {icon}
25 |
26 | )}
27 |
35 | {placeholder}
36 | {options.map(option => (
37 |
38 | {option.label}
39 |
40 | ))}
41 |
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 |
36 | {loading ? (
37 |
38 |
39 |
40 |
41 |
42 | {children}
43 |
44 | ) : (
45 |
46 | {icon && {icon} }
47 | {children}
48 |
49 | )}
50 |
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 |
window.history.back()}
70 | className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
71 | >
72 | Go Back
73 |
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 |
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 |
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 |
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 |
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 |
handleQuickAction('videos')}
240 | className="bg-white rounded-lg p-4 flex flex-col items-center justify-center shadow-sm hover:shadow-md"
241 | >
242 |
243 | Watch Videos
244 |
245 |
handleQuickAction('quiz')}
247 | className="bg-white rounded-lg p-4 flex flex-col items-center justify-center shadow-sm hover:shadow-md"
248 | >
249 |
250 | Take Quiz
251 |
252 |
handleQuickAction('maps')}
254 | className="bg-white rounded-lg p-4 flex flex-col items-center justify-center shadow-sm hover:shadow-md"
255 | >
256 |
257 | Find MPAs
258 |
259 |
handleQuickAction('report')}
261 | className="bg-white rounded-lg p-4 flex flex-col items-center justify-center shadow-sm hover:shadow-md"
262 | >
263 |
264 | Report Issue
265 |
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 | VIDEO
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
{topic.title}
105 |
{topic.subtitle}
106 |
107 |
108 |
109 |
110 |
{topic.readTime}
111 |
2.3k views
112 |
Share
113 |
114 |
115 |
116 | {/* Topic Overview */}
117 |
118 |
119 |
120 | Topic Overview
121 |
122 | {topic.description}
123 |
124 |
125 | {/* Detailed Section */}
126 |
127 | Comprehensive Guide
128 |
129 | {getDetailedContent(topic.id)}
130 |
131 |
132 |
133 | {/* Video Link Section */}
134 | {/* Read Detailed Documentary (documentary = long text, separate from video) */}
135 |
139 |
140 |
141 |
142 |
143 |
144 |
145 | {showDocumentary ? 'Hide Documentary' : 'Read Detailed Documentary'}
146 |
147 |
148 | Explore more insights with expert interviews and real-world examples.
149 |
150 |
151 |
156 |
157 |
158 | {/* Expanded documentary text (inline) */}
159 | {showDocumentary && (
160 |
161 |
162 | {topic.longDescription || getDetailedContent(topic.id)}
163 |
164 |
165 | )}
166 |
167 |
168 |
169 |
170 |
171 |
172 | {/* Take Action Button */}
173 |
177 |
178 | Take Action
179 |
180 |
181 | );
182 | };
183 |
184 | export default TopicDetail;
185 |
--------------------------------------------------------------------------------
/frontend/src/pages/Home.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Header from '../components/Header';
3 | import Footer from '../components/Footer';
4 | import ShipMap from '../components/ShipMap';
5 | import StatsCards from '../components/StatsCards';
6 | import FeaturesGrid from '../components/FeaturesGrid';
7 | import TechnologyStack from '../components/TechnologyStack';
8 | import { Activity, Compass, Route, Waves, Navigation, Settings, AlertTriangle } from 'lucide-react';
9 | import '../Styles/global.css';
10 |
11 | const Home = () => {
12 | const [activeSection, setActiveSection] = useState('tracker');
13 |
14 | return (
15 |
22 |
23 |
24 | {/* Main Content */}
25 |
30 | {/* Enhanced Page Title */}
31 |
40 |
48 |
49 |
60 | Smart Maritime Navigator For Ships
61 |
62 |
70 | Real-time vessel tracking and sustainable route optimization
71 |
72 |
73 | {/* Live Status Indicators */}
74 |
80 | {[
81 | { icon: Activity, label: 'System Status', value: 'Online', color: '#10b981' },
82 | { icon: Compass, label: 'GPS Accuracy', value: '±2m', color: '#3b82f6' },
83 | { icon: Route, label: 'Data Update', value: 'Real-time', color: '#f59e0b' }
84 | ].map((status, index) => (
85 |
95 |
96 |
97 | {status.label}:
98 |
99 |
100 | {status.value}
101 |
102 |
103 | ))}
104 |
105 |
106 |
107 |
108 |
109 | {/* Enhanced Map Section - Made Wider */}
110 |
119 | {/* Map Header */}
120 |
125 |
132 |
133 |
141 |
142 | Live Maritime Traffic Map
143 |
144 |
149 | Real-time vessel positions, routes, and navigation data
150 |
151 |
152 |
153 |
159 | {/* Legend */}
160 |
169 | {[
170 | { color: '#e74c3c', label: 'Cargo' },
171 | { color: '#f39c12', label: 'Tanker' },
172 | { color: '#1abc9c', label: 'Cruise' },
173 | { color: '#9b59b6', label: 'Container' }
174 | ].map((type, index) => (
175 |
180 |
186 |
{type.label}
187 |
188 | ))}
189 |
190 |
191 |
201 |
208 |
Live Tracking Active
209 |
210 |
211 |
{
226 | e.target.style.backgroundColor = '#f9fafb';
227 | e.target.style.borderColor = '#9ca3af';
228 | }}
229 | onMouseLeave={(e) => {
230 | e.target.style.backgroundColor = 'white';
231 | e.target.style.borderColor = '#d1d5db';
232 | }}
233 | >
234 |
235 | Map Settings
236 |
237 |
238 |
239 |
240 | {/* Enhanced Map Stats */}
241 |
247 | {[
248 | { label: 'Total Distance Tracked', value: '2,847 nm', icon: Route },
249 | { label: 'Active Shipping Lanes', value: '4 major routes', icon: Navigation },
250 | { label: 'Avg Fleet Speed', value: '15.2 knots', icon: Activity },
251 | { label: 'Environmental Zones', value: '3 monitored', icon: AlertTriangle }
252 | ].map((stat, index) => (
253 |
262 |
271 |
272 |
273 |
274 |
279 | {stat.value}
280 |
281 |
285 | {stat.label}
286 |
287 |
288 |
289 | ))}
290 |
291 |
292 |
293 | {/* Map Container - Made Full Width */}
294 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 | );
315 | };
316 |
317 | export default Home;
--------------------------------------------------------------------------------
/frontend/src/components/UserDropdown.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef, useEffect } from 'react';
2 | import { useNavigate } from 'react-router-dom';
3 | import { useAuth } from '../context/AuthContext';
4 | import { MdPerson, MdLogout, MdKeyboardArrowDown, MdDelete } from 'react-icons/md';
5 | import toast from 'react-hot-toast';
6 |
7 | const UserDropdown = () => {
8 | const [isOpen, setIsOpen] = useState(false);
9 | const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
10 | const [deleteLoading, setDeleteLoading] = useState(false);
11 | const { user, logout, deleteAccount } = useAuth();
12 | const navigate = useNavigate();
13 | const dropdownRef = useRef(null);
14 |
15 | // Close dropdown when clicking outside
16 | useEffect(() => {
17 | const handleClickOutside = (event) => {
18 | if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
19 | setIsOpen(false);
20 | }
21 | };
22 |
23 | document.addEventListener('mousedown', handleClickOutside);
24 | return () => {
25 | document.removeEventListener('mousedown', handleClickOutside);
26 | };
27 | }, []);
28 |
29 | const handleProfileClick = () => {
30 | setIsOpen(false);
31 | navigate('/profile');
32 | };
33 |
34 | const handleLogoutClick = () => {
35 | setIsOpen(false);
36 | logout();
37 | toast.success('Logged out successfully');
38 | navigate('/login');
39 | };
40 |
41 | const handleDeleteAccount = async () => {
42 | setDeleteLoading(true);
43 | try {
44 | await deleteAccount();
45 | toast.success('Account deleted successfully');
46 | // User will be redirected to login page automatically
47 | } catch (error) {
48 | const message = error.response?.data?.message || error.message || 'Failed to delete account';
49 | toast.error(message);
50 | } finally {
51 | setDeleteLoading(false);
52 | setShowDeleteConfirm(false);
53 | setIsOpen(false);
54 | }
55 | };
56 |
57 | if (!user) {
58 | return null;
59 | }
60 |
61 | return (
62 |
63 | {/* Avatar Button */}
64 |
setIsOpen(!isOpen)}
66 | style={{
67 | display: 'flex',
68 | alignItems: 'center',
69 | gap: '0.5rem',
70 | padding: '0.5rem',
71 | borderRadius: '8px',
72 | backgroundColor: 'transparent',
73 | border: 'none',
74 | color: 'white',
75 | cursor: 'pointer',
76 | transition: 'background-color 0.2s ease'
77 | }}
78 | onMouseEnter={(e) => {
79 | e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.1)';
80 | }}
81 | onMouseLeave={(e) => {
82 | e.currentTarget.style.backgroundColor = 'transparent';
83 | }}
84 | >
85 |
95 |
100 | {user.name?.charAt(0)?.toUpperCase() || 'U'}
101 |
102 |
103 |
109 |
{user.name}
115 |
{user.role?.charAt(0)?.toUpperCase() + user.role?.slice(1) || 'User'}
120 |
121 |
129 |
130 |
131 | {/* Dropdown Menu */}
132 | {isOpen && (
133 |
146 | {/* User Info Header */}
147 |
151 |
{user.name}
157 |
{user.email}
162 |
163 |
173 | {user.role?.charAt(0)?.toUpperCase() + user.role?.slice(1) || 'User'}
174 |
175 |
176 |
177 |
178 | {/* Menu Items */}
179 |
180 |
{
195 | e.currentTarget.style.backgroundColor = '#f3f4f6';
196 | }}
197 | onMouseLeave={(e) => {
198 | e.currentTarget.style.backgroundColor = 'transparent';
199 | }}
200 | >
201 |
202 | View Profile
203 |
204 |
205 |
209 |
210 |
setShowDeleteConfirm(true)}
212 | style={{
213 | display: 'flex',
214 | alignItems: 'center',
215 | width: '100%',
216 | padding: '8px 16px',
217 | fontSize: '14px',
218 | color: '#dc2626',
219 | backgroundColor: 'transparent',
220 | border: 'none',
221 | cursor: 'pointer',
222 | transition: 'background-color 0.15s ease'
223 | }}
224 | onMouseEnter={(e) => {
225 | e.currentTarget.style.backgroundColor = '#fef2f2';
226 | }}
227 | onMouseLeave={(e) => {
228 | e.currentTarget.style.backgroundColor = 'transparent';
229 | }}
230 | >
231 |
232 | Delete Account
233 |
234 |
235 |
239 |
240 |
{
255 | e.currentTarget.style.backgroundColor = '#f3f4f6';
256 | }}
257 | onMouseLeave={(e) => {
258 | e.currentTarget.style.backgroundColor = 'transparent';
259 | }}
260 | >
261 |
262 | Logout
263 |
264 |
265 |
266 | )}
267 |
268 | {/* Delete Account Confirmation Modal */}
269 | {showDeleteConfirm && (
270 |
283 |
292 |
302 |
303 |
304 |
310 | Delete Account
311 |
312 |
318 | Are you sure you want to delete your account? This action cannot be undone and all your data will be permanently removed.
319 |
320 |
321 |
325 | setShowDeleteConfirm(false)}
327 | style={{
328 | flex: 1,
329 | padding: '12px 16px',
330 | fontSize: '14px',
331 | fontWeight: '500',
332 | color: '#374151',
333 | backgroundColor: '#f3f4f6',
334 | border: 'none',
335 | borderRadius: '8px',
336 | cursor: 'pointer',
337 | transition: 'background-color 0.2s ease'
338 | }}
339 | onMouseEnter={(e) => {
340 | e.currentTarget.style.backgroundColor = '#e5e7eb';
341 | }}
342 | onMouseLeave={(e) => {
343 | e.currentTarget.style.backgroundColor = '#f3f4f6';
344 | }}
345 | >
346 | Cancel
347 |
348 | {
364 | if (!deleteLoading) {
365 | e.currentTarget.style.backgroundColor = '#b91c1c';
366 | }
367 | }}
368 | onMouseLeave={(e) => {
369 | if (!deleteLoading) {
370 | e.currentTarget.style.backgroundColor = '#dc2626';
371 | }
372 | }}
373 | >
374 | {deleteLoading ? 'Deleting...' : 'Delete Account'}
375 |
376 |
377 |
378 |
379 | )}
380 |
381 | );
382 | };
383 |
384 | export default UserDropdown;
385 |
386 |
--------------------------------------------------------------------------------
/frontend/src/pages/QuizPage.js:
--------------------------------------------------------------------------------
1 | // src/pages/QuizPage.js
2 | import React, { useState } from 'react';
3 | import { ArrowLeft, Award, CheckCircle2, XCircle, Fish, Leaf, Waves, Anchor, AlertTriangle, Trophy, RefreshCcw } from 'lucide-react';
4 | import Header from '../components/Header';
5 | import Footer from '../components/Footer';
6 |
7 | const categories = [
8 | { id: 1, name: 'Biodiversity', description: 'Test your knowledge about marine species and ecosystems', icon: Fish, color: 'blue', questionCount: 6 },
9 | { id: 2, name: 'Conservation', description: 'Learn about marine protection and sustainability', icon: Leaf, color: 'green', questionCount: 6 },
10 | { id: 3, name: 'Ocean Pollution', description: 'Understand pollution threats and prevention', icon: Waves, color: 'cyan', questionCount: 5 },
11 | { id: 4, name: 'Sustainable Fishing', description: 'Explore responsible fishing practices', icon: Anchor, color: 'yellow', questionCount: 5 },
12 | { id: 5, name: 'Climate Change', description: 'Discover climate impacts on oceans', icon: AlertTriangle, color: 'red', questionCount: 5 },
13 | ];
14 |
15 | const questions = [
16 | // Biodiversity (Category 1)
17 | { id: 1, categoryId: 1, question: 'How many species of marine life are estimated to exist in the ocean?', options: ['50,000','250,000','1 million','Over 2 million'], correctAnswer: 3, explanation: 'Scientists estimate over 2 million species.' },
18 | { id: 2, categoryId: 1, question: 'Which marine animal has three hearts?', options: ['Dolphin','Octopus','Whale','Shark'], correctAnswer: 1, explanation: 'Octopuses have three hearts.' },
19 | { id: 3, categoryId: 1, question: 'Which ocean zone has the most sunlight?', options: ['Abyssal','Sunlight','Twilight','Midnight'], correctAnswer: 1, explanation: 'The Sunlight Zone has the most light.' },
20 | { id: 4, categoryId: 1, question: 'What is the largest animal on Earth?', options: ['Great White Shark','Blue Whale','Elephant','Giant Squid'], correctAnswer: 1, explanation: 'The Blue Whale is the largest animal on Earth.' },
21 | { id: 5, categoryId: 1, question: 'Which marine creature is known as the "forest of the sea"?', options: ['Kelp','Seagrass','Coral','Mangrove'], correctAnswer: 0, explanation: 'Kelp forests provide shelter and food for many marine species.' },
22 | { id: 6, categoryId: 1, question: 'Which tiny marine organism produces oxygen?', options: ['Phytoplankton','Jellyfish','Starfish','Crab'], correctAnswer: 0, explanation: 'Phytoplankton produces at least 50% of the Earth’s oxygen.' },
23 | { id: 7, categoryId: 1, question: 'Sea turtles can hold their breath for how long?', options: ['30 mins','2 hours','5 hours','10 hours'], correctAnswer: 2, explanation: 'They can hold their breath for up to 5 hours.' },
24 |
25 | // Conservation (Category 2)
26 | { id: 8, categoryId: 2, question: 'What does MPA stand for?', options: ['Marine Protection Area','Marine Protected Area','Marine Preservation Area','Marine Public Area'], correctAnswer: 1, explanation: 'MPA stands for Marine Protected Area.' },
27 | { id: 9, categoryId: 2, question: 'Which species is protected by conservation efforts?', options: ['Dodo','Octopus','Whale','Shark'], correctAnswer: 2, explanation: 'Many whales are protected to prevent extinction.' },
28 | { id: 10, categoryId: 2, question: 'Who can participate in community conservation?', options: ['Only scientists','Local communities','Only governments','Tourists'], correctAnswer: 1, explanation: 'Local communities help in conservation efforts.' },
29 | { id: 11, categoryId: 2, question: 'Why protect coral reefs?', options: ['For tourism','To support marine life','For fishing','For oil extraction'], correctAnswer: 1, explanation: 'Coral reefs support thousands of marine species.' },
30 | { id: 12, categoryId: 2, question: 'What is “ghost fishing”?', options: ['Fishing at night','Abandoned fishing gear trapping fish','Deep sea fishing','Underwater fishing'], correctAnswer: 1, explanation: 'Ghost fishing happens when lost fishing gear traps marine life.' },
31 | { id: 13, categoryId: 2, question: 'Why create marine protected areas?', options: ['For fun','To protect ecosystems','For mining','For shipping routes'], correctAnswer: 1, explanation: 'MPAs are created to protect marine ecosystems.' },
32 |
33 | // Ocean Pollution (Category 3)
34 | { id: 14, categoryId: 3, question: 'How much plastic enters the ocean yearly?', options: ['1M tons','5M tons','8M tons','15M tons'], correctAnswer: 2, explanation: 'About 8 million tons of plastic enter the ocean every year.' },
35 | { id: 15, categoryId: 3, question: 'What takes 450 years to decompose?', options: ['Glass bottle','Plastic bottle','Paper bag','Aluminum can'], correctAnswer: 1, explanation: 'Plastic bottles take ~450 years to break down.' },
36 | { id: 16, categoryId: 3, question: 'Which is a major microplastic source?', options: ['Glass','Synthetic clothes','Paper','Metal'], correctAnswer: 1, explanation: 'Synthetic clothes release microplastics when washed.' },
37 | { id: 17, categoryId: 3, question: 'Dead zones in the ocean are caused by?', options: ['Too much salt','Nutrient pollution','Cold water','Oil spills'], correctAnswer: 1, explanation: 'Excess nutrients cause oxygen depletion creating dead zones.' },
38 | { id: 18, categoryId: 3, question: 'What is the Great Pacific Garbage Patch?', options: ['Garbage island','Floating debris','Beach trash','Ocean trench waste'], correctAnswer: 1, explanation: 'It is a floating mass of plastic debris in the ocean.' },
39 | { id: 19, categoryId: 3, question: 'How can we reduce ocean pollution?', options: ['Recycling','Fishing more','Deforestation','Boating'], correctAnswer: 0, explanation: 'Recycling helps reduce plastic pollution in oceans.' },
40 |
41 | // Sustainable Fishing (Category 4)
42 | { id: 20, categoryId: 4, question: 'What is overfishing?', options: ['Fishing too little','Fishing too much','Fishing responsibly','Illegal fishing'], correctAnswer: 1, explanation: 'Overfishing reduces fish populations drastically.' },
43 | { id: 21, categoryId: 4, question: 'What does MSC certification indicate?', options: ['Maximum safety','Marine Stewardship Council','Most sustainable catch','Marine science'], correctAnswer: 1, explanation: 'MSC certifies sustainable fisheries.' },
44 | { id: 22, categoryId: 4, question: 'Which fishing method damages the seabed?', options: ['Line fishing','Bottom trawling','Spear fishing','Net fishing'], correctAnswer: 1, explanation: 'Bottom trawling destroys habitats like coral reefs.' },
45 | { id: 23, categoryId: 4, question: 'Bycatch means?', options: ['Accidental catch of non-target species','Fishing by hand','Deep sea fishing','Illegal fishing'], correctAnswer: 0, explanation: 'Bycatch is unintentional catch of other species.' },
46 | { id: 24, categoryId: 4, question: 'Why have fishing quotas?', options: ['More profit','Prevent overfishing','Reduce taxes','Competition'], correctAnswer: 1, explanation: 'Quotas help fish populations recover.' },
47 | { id: 25, categoryId: 4, question: 'Sustainable fishing helps?', options: ['Ecosystem health','Pollution','Oil spills','Coral bleaching'], correctAnswer: 0, explanation: 'It protects ecosystems and maintains fish stocks.' },
48 |
49 | // Climate Change (Category 5)
50 | { id: 26, categoryId: 5, question: 'What is ocean acidification?', options: ['Dirty water','Decreased pH from CO2','Increased pH','Salt change'], correctAnswer: 1, explanation: 'Ocean absorbs CO2, lowering pH.' },
51 | { id: 27, categoryId: 5, question: 'Rising sea levels are caused by?', options: ['Melting ice','Volcanoes','Fishing','Shipping'], correctAnswer: 0, explanation: 'Melting ice due to warming raises sea levels.' },
52 | { id: 28, categoryId: 5, question: 'What are blue carbon ecosystems?', options: ['Arctic','Mangroves & seagrass','Deep ocean','Cold currents'], correctAnswer: 1, explanation: 'These ecosystems store carbon efficiently.' },
53 | { id: 29, categoryId: 5, question: 'Coral bleaching occurs due to?', options: ['Sunlight','Temperature stress','Lack of food','Currents'], correctAnswer: 1, explanation: 'High temperatures stress corals causing bleaching.' },
54 | { id: 30, categoryId: 5, question: 'Oceans absorb how much CO2 from humans?', options: ['10%','20%','30%','50%'], correctAnswer: 2, explanation: 'Oceans have absorbed about 30% of human-produced CO2.' },
55 | { id: 31, categoryId: 5, question: 'Climate change mainly affects?', options: ['Oceans','Mountains','Deserts','Cities'], correctAnswer: 0, explanation: 'Oceans are heavily impacted by climate change.' }
56 | ];
57 |
58 |
59 | const colorClasses = {
60 | blue: { bg: 'bg-blue-500', hover: 'hover:bg-blue-600', light: 'bg-blue-100', text: 'text-blue-500' },
61 | green: { bg: 'bg-green-500', hover: 'hover:bg-green-600', light: 'bg-green-100', text: 'text-green-500' },
62 | cyan: { bg: 'bg-cyan-500', hover: 'hover:bg-cyan-600', light: 'bg-cyan-100', text: 'text-cyan-500' },
63 | yellow: { bg: 'bg-yellow-500', hover: 'hover:bg-yellow-600', light: 'bg-yellow-100', text: 'text-yellow-500' },
64 | red: { bg: 'bg-red-500', hover: 'hover:bg-red-600', light: 'bg-red-100', text: 'text-red-500' },
65 | };
66 |
67 | // ... Questions array (same as your code, omitted for brevity)
68 |
69 | function QuizPage() {
70 | const [selectedCategory, setSelectedCategory] = useState(null);
71 | const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
72 | const [answers, setAnswers] = useState([]);
73 | const [selectedOption, setSelectedOption] = useState(null);
74 | const [hasAnswered, setHasAnswered] = useState(false);
75 | const [showCompletionModal, setShowCompletionModal] = useState(false);
76 |
77 | const categoryQuestions = selectedCategory
78 | ? questions.filter(q => q.categoryId === selectedCategory.id)
79 | : [];
80 |
81 | const currentQuestion = selectedCategory ? categoryQuestions[currentQuestionIndex] : null;
82 |
83 | const selectedColor = selectedCategory ? colorClasses[selectedCategory.color] : colorClasses.blue;
84 |
85 | const handleCategorySelect = (category) => {
86 | if (!category) {
87 | setSelectedCategory(null);
88 | return;
89 | }
90 | setSelectedCategory(category);
91 | setCurrentQuestionIndex(0);
92 | setAnswers([]);
93 | setSelectedOption(null);
94 | setHasAnswered(false);
95 | setShowCompletionModal(false);
96 | };
97 |
98 | const handleAnswerSelect = (index) => {
99 | if (hasAnswered) return;
100 | setSelectedOption(index);
101 | setHasAnswered(true);
102 |
103 | const isCorrect = index === currentQuestion.correctAnswer;
104 | setAnswers([...answers, { questionId: currentQuestion.id, isCorrect }]);
105 | };
106 |
107 | const handleNextQuestion = () => {
108 | if (currentQuestionIndex < categoryQuestions.length - 1) {
109 | setCurrentQuestionIndex(currentQuestionIndex + 1);
110 | setSelectedOption(null);
111 | setHasAnswered(false);
112 | } else {
113 | setShowCompletionModal(true);
114 | }
115 | };
116 |
117 | const handleRestartQuiz = () => {
118 | setSelectedCategory(null);
119 | setCurrentQuestionIndex(0);
120 | setSelectedOption(null);
121 | setAnswers([]);
122 | setHasAnswered(false);
123 | setShowCompletionModal(false);
124 | };
125 |
126 | const getScore = () => {
127 | const correct = answers.filter(a => a.isCorrect).length;
128 | const total = answers.length;
129 | return { correct, total, percentage: total > 0 ? Math.round((correct / total) * 100) : 0 };
130 | };
131 |
132 | return (
133 |
134 |
135 |
136 | {!selectedCategory && (
137 |
138 |
139 |
140 |
Marine Knowledge Quiz
141 |
Test your knowledge and learn about marine conservation
142 |
143 |
Choose a Category
144 |
145 | {categories.map(cat => {
146 | const Icon = cat.icon;
147 | const color = colorClasses[cat.color];
148 | return (
149 |
handleCategorySelect(cat)}
152 | className="bg-white rounded-xl p-6 shadow hover:shadow-lg transition flex items-start gap-4 w-full"
153 | >
154 |
155 |
156 |
157 |
158 |
{cat.name}
159 |
{cat.description}
160 |
{cat.questionCount} Questions
161 |
162 |
163 | );
164 | })}
165 |
166 |
167 | )}
168 |
169 | {selectedCategory && currentQuestion && (
170 |
171 |
172 |
178 |
179 | Question {currentQuestionIndex + 1} of {categoryQuestions.length}
180 |
181 |
182 |
183 |
Question {currentQuestionIndex + 1}
184 |
{currentQuestion.question}
185 |
186 | {currentQuestion.options.map((opt, idx) => {
187 | const isSelected = selectedOption === idx;
188 | const isCorrect = idx === currentQuestion.correctAnswer;
189 | const classes = `flex justify-between items-center p-4 rounded-lg border transition
190 | ${isSelected ? 'border-blue-500 bg-blue-100' : 'border-transparent bg-gray-50'}
191 | ${hasAnswered && isCorrect ? 'border-green-500 bg-green-100' : ''}
192 | ${hasAnswered && isSelected && !isCorrect ? 'border-red-500 bg-red-100' : ''}`;
193 | return (
194 | handleAnswerSelect(idx)}
197 | disabled={hasAnswered}
198 | className={classes}
199 | >
200 | {opt}
201 | {hasAnswered && isCorrect && }
202 | {hasAnswered && isSelected && !isCorrect && }
203 |
204 | );
205 | })}
206 |
207 |
208 | {hasAnswered && (
209 |
210 | {selectedOption === currentQuestion.correctAnswer ? (
211 |
{currentQuestion.explanation}
212 | ) : (
213 |
214 | Correct: {currentQuestion.options[currentQuestion.correctAnswer]} - {currentQuestion.explanation}
215 |
216 | )}
217 |
218 | )}
219 |
220 | {hasAnswered && (
221 |
225 | {currentQuestionIndex < categoryQuestions.length - 1 ? 'Next Question' : 'Finish Quiz'}
226 |
227 | )}
228 |
229 |
230 | )}
231 |
232 |
233 |
234 |
235 | {showCompletionModal && (
236 |
237 |
238 |
239 |
Quiz Complete! 🎊
240 |
241 | You scored {getScore().correct} / {getScore().total}
242 |
243 |
{getScore().percentage}%
244 |
245 |
249 | Try Again
250 |
251 | handleCategorySelect(null)}
253 | className="bg-gray-100 text-gray-700 py-2 rounded-lg hover:bg-gray-200 transition"
254 | >
255 | Choose Another Category
256 |
257 |
258 |
259 |
260 | )}
261 |
262 | );
263 | }
264 |
265 | export default QuizPage;
266 |
--------------------------------------------------------------------------------