From 1f6e49b7e8655afe64df7e722160c7d301b67b05 Mon Sep 17 00:00:00 2001
From: Caspian Almerud
Date: Sat, 12 Jul 2025 23:24:57 +0200
Subject: [PATCH 001/111] Forgot to commit previously, but this is the basic
setup, and start to the landing page, auth flow and some of the basic
functionality.
---
.gitignore | 19 +-
Procfile | 1 -
frontend/.prettierrc | 8 +
frontend/config/README.md | 28 ++
frontend/config/lighthouserc.json | 20 +
frontend/config/postcss.config.js | 9 +
frontend/config/vitest.config.js | 22 ++
frontend/dist/assets/index-BJRqtcJi.css | 1 +
frontend/dist/assets/index-CGuCVMO8.js | 113 ++++++
frontend/dist/assets/vendor-BtP0CW_r.js | 32 ++
frontend/dist/index.html | 15 +
frontend/dist/vite.svg | 1 +
frontend/package.json | 30 +-
frontend/src/App.jsx | 34 +-
.../src/components/auth/ProtectedRoute.jsx | 20 +
frontend/src/components/ui/Button.jsx | 33 ++
frontend/src/components/ui/Navbar.jsx | 67 ++++
frontend/src/contexts/AuthContext.jsx | 174 +++++++++
frontend/src/index.css | 61 +++
frontend/src/lib/store.js | 105 ++++++
frontend/src/lib/utils.js | 5 +
frontend/src/pages/DashboardPage.jsx | 225 +++++++++++
frontend/src/pages/LandingPage.jsx | 201 ++++++++++
frontend/src/pages/LoginPage.jsx | 236 ++++++++++++
frontend/src/pages/RegisterPage.jsx | 352 ++++++++++++++++++
frontend/src/test/setup.js | 32 ++
frontend/tailwind.config.js | 43 +++
frontend/vite.config.js | 26 ++
package.json | 10 +-
29 files changed, 1913 insertions(+), 10 deletions(-)
delete mode 100644 Procfile
create mode 100644 frontend/.prettierrc
create mode 100644 frontend/config/README.md
create mode 100644 frontend/config/lighthouserc.json
create mode 100644 frontend/config/postcss.config.js
create mode 100644 frontend/config/vitest.config.js
create mode 100644 frontend/dist/assets/index-BJRqtcJi.css
create mode 100644 frontend/dist/assets/index-CGuCVMO8.js
create mode 100644 frontend/dist/assets/vendor-BtP0CW_r.js
create mode 100644 frontend/dist/index.html
create mode 100644 frontend/dist/vite.svg
create mode 100644 frontend/src/components/auth/ProtectedRoute.jsx
create mode 100644 frontend/src/components/ui/Button.jsx
create mode 100644 frontend/src/components/ui/Navbar.jsx
create mode 100644 frontend/src/contexts/AuthContext.jsx
create mode 100644 frontend/src/lib/store.js
create mode 100644 frontend/src/lib/utils.js
create mode 100644 frontend/src/pages/DashboardPage.jsx
create mode 100644 frontend/src/pages/LandingPage.jsx
create mode 100644 frontend/src/pages/LoginPage.jsx
create mode 100644 frontend/src/pages/RegisterPage.jsx
create mode 100644 frontend/src/test/setup.js
create mode 100644 frontend/tailwind.config.js
diff --git a/.gitignore b/.gitignore
index 3d70248ba2..cf8c2d4d0d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,4 +12,21 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
-package-lock.json
\ No newline at end of file
+package-lock.json
+
+.AI_context
+Frontend_prd
+frontend_tasklist.txt
+.cursor
+.claude
+
+.env.example
+.eslintrc.json
+.prettierrc.json
+.prettierignore
+.prettierignore
+CLAUDE.md
+.cursor/rules/frontend-assumptions.mdc
+.cursor/rules/frontend-assumptions.md
+.cursor/rules/frontend-assumptions.txt
+.cursor/rules/frontend-assumptions.txt
\ No newline at end of file
diff --git a/Procfile b/Procfile
deleted file mode 100644
index dc14c0b63a..0000000000
--- a/Procfile
+++ /dev/null
@@ -1 +0,0 @@
-web: npm start --prefix backend
\ No newline at end of file
diff --git a/frontend/.prettierrc b/frontend/.prettierrc
new file mode 100644
index 0000000000..bd8d004fc4
--- /dev/null
+++ b/frontend/.prettierrc
@@ -0,0 +1,8 @@
+{
+ "semi": true,
+ "trailingComma": "es5",
+ "singleQuote": true,
+ "printWidth": 80,
+ "tabWidth": 2,
+ "useTabs": false
+}
\ No newline at end of file
diff --git a/frontend/config/README.md b/frontend/config/README.md
new file mode 100644
index 0000000000..1cc429383f
--- /dev/null
+++ b/frontend/config/README.md
@@ -0,0 +1,28 @@
+# Configuration Files
+
+This directory contains all configuration files for the frontend application.
+
+## Files Overview
+
+| File | Purpose | Documentation |
+| -------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------- |
+| `.eslintrc.js` | ESLint configuration for code quality and style enforcement | [ESLint Docs](https://eslint.org/docs/user-guide/configuring/) |
+| `.prettierrc` | Prettier configuration for code formatting | [Prettier Docs](https://prettier.io/docs/en/configuration.html) |
+| `tailwind.config.js` | Tailwind CSS configuration and design tokens | [Tailwind Docs](https://tailwindcss.com/docs/configuration) |
+| `postcss.config.js` | PostCSS configuration for CSS processing | [PostCSS Docs](https://postcss.org/) |
+| `vitest.config.js` | Vitest configuration for testing | [Vitest Docs](https://vitest.dev/config/) |
+| `lighthouserc.json` | Lighthouse CI configuration for performance monitoring | [Lighthouse CI Docs](https://github.com/GoogleChrome/lighthouse-ci) |
+
+## Usage
+
+Most tools automatically detect their config files in this directory through explicit references in:
+
+- `package.json` scripts
+- `vite.config.js`
+- GitHub Actions workflow
+
+## Notes
+
+- Paths in config files are relative to the project root, not the config directory
+- Changes to these files may require restarting development servers
+- All configs follow the project's ESLint and Prettier rules
diff --git a/frontend/config/lighthouserc.json b/frontend/config/lighthouserc.json
new file mode 100644
index 0000000000..be5ff202d6
--- /dev/null
+++ b/frontend/config/lighthouserc.json
@@ -0,0 +1,20 @@
+{
+ "ci": {
+ "collect": {
+ "startServerCommand": "npm run preview",
+ "url": ["http://localhost:4173"],
+ "numberOfRuns": 3
+ },
+ "assert": {
+ "assertions": {
+ "categories:performance": ["error", {"minScore": 0.9}],
+ "categories:accessibility": ["error", {"minScore": 0.9}],
+ "categories:best-practices": ["error", {"minScore": 0.9}],
+ "categories:seo": ["error", {"minScore": 0.9}]
+ }
+ },
+ "upload": {
+ "target": "temporary-public-storage"
+ }
+ }
+}
\ No newline at end of file
diff --git a/frontend/config/postcss.config.js b/frontend/config/postcss.config.js
new file mode 100644
index 0000000000..760d963261
--- /dev/null
+++ b/frontend/config/postcss.config.js
@@ -0,0 +1,9 @@
+import tailwindcss from 'tailwindcss'
+import autoprefixer from 'autoprefixer'
+
+export default {
+ plugins: [
+ tailwindcss({ config: './tailwind.config.js' }),
+ autoprefixer,
+ ],
+}
diff --git a/frontend/config/vitest.config.js b/frontend/config/vitest.config.js
new file mode 100644
index 0000000000..0cb7e064a6
--- /dev/null
+++ b/frontend/config/vitest.config.js
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vitest/config'
+import react from '@vitejs/plugin-react'
+import path from 'path'
+
+export default defineConfig({
+ plugins: [react()],
+ test: {
+ globals: true,
+ environment: 'jsdom',
+ setupFiles: './src/test/setup.js',
+ },
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, '../src'),
+ '@components': path.resolve(__dirname, '../src/components'),
+ '@pages': path.resolve(__dirname, '../src/pages'),
+ '@hooks': path.resolve(__dirname, '../src/hooks'),
+ '@utils': path.resolve(__dirname, '../src/utils'),
+ '@assets': path.resolve(__dirname, '../src/assets'),
+ },
+ },
+})
\ No newline at end of file
diff --git a/frontend/dist/assets/index-BJRqtcJi.css b/frontend/dist/assets/index-BJRqtcJi.css
new file mode 100644
index 0000000000..0264354b76
--- /dev/null
+++ b/frontend/dist/assets/index-BJRqtcJi.css
@@ -0,0 +1 @@
+*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root{--color-primary: 14 165 233;--color-secondary: 132 204 22;--color-success: 34 197 94;--color-warning: 245 158 11;--color-error: 239 68 68}.dark{--color-primary: 14 165 233;--color-secondary: 132 204 22;--color-success: 34 197 94;--color-warning: 245 158 11;--color-error: 239 68 68}body{font-feature-settings:"rlig" 1,"calt" 1}.btn{display:inline-flex;align-items:center;justify-content:center;border-radius:.375rem;font-size:.875rem;line-height:1.25rem;font-weight:500;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.btn:focus-visible{outline:2px solid transparent;outline-offset:2px;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000);--tw-ring-offset-width: 2px}.btn:disabled{pointer-events:none;opacity:.5}.btn-primary{display:inline-flex;align-items:center;justify-content:center;border-radius:.375rem;font-size:.875rem;line-height:1.25rem;font-weight:500;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.btn-primary:focus-visible{outline:2px solid transparent;outline-offset:2px;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000);--tw-ring-offset-width: 2px}.btn-primary:disabled{pointer-events:none;opacity:.5}.btn-primary{--tw-bg-opacity: 1;background-color:rgb(2 132 199 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.btn-primary:hover{--tw-bg-opacity: 1;background-color:rgb(3 105 161 / var(--tw-bg-opacity, 1))}.btn-secondary{display:inline-flex;align-items:center;justify-content:center;border-radius:.375rem;font-size:.875rem;line-height:1.25rem;font-weight:500;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.btn-secondary:focus-visible{outline:2px solid transparent;outline-offset:2px;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000);--tw-ring-offset-width: 2px}.btn-secondary:disabled{pointer-events:none;opacity:.5}.btn-secondary{--tw-bg-opacity: 1;background-color:rgb(101 163 13 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.btn-secondary:hover{--tw-bg-opacity: 1;background-color:rgb(77 124 15 / var(--tw-bg-opacity, 1))}.btn-outline{display:inline-flex;align-items:center;justify-content:center;border-radius:.375rem;font-size:.875rem;line-height:1.25rem;font-weight:500;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.btn-outline:focus-visible{outline:2px solid transparent;outline-offset:2px;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000);--tw-ring-offset-width: 2px}.btn-outline:disabled{pointer-events:none;opacity:.5}.btn-outline{border-width:1px;--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1));--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity, 1))}.btn-outline:hover{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.btn-outline:is(.dark *){--tw-border-opacity: 1;border-color:rgb(75 85 99 / var(--tw-border-opacity, 1));--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.btn-outline:hover:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-y-0{top:0;bottom:0}.left-0{left:0}.right-0{right:0}.top-0{top:0}.z-10{z-index:10}.z-50{z-index:50}.mx-auto{margin-left:auto;margin-right:auto}.-ml-1{margin-left:-.25rem}.mb-16{margin-bottom:4rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-2{margin-left:.5rem}.ml-4{margin-left:1rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-12{height:3rem}.h-16{height:4rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-8{height:2rem}.min-h-screen{min-height:100vh}.w-12{width:3rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-full{width:100%}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-7xl{max-width:80rem}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-12{gap:3rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(1rem * var(--tw-space-x-reverse));margin-left:calc(1rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-t{border-top-width:1px}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity, 1))}.border-gray-300{--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1))}.border-gray-800{--tw-border-opacity: 1;border-color:rgb(31 41 55 / var(--tw-border-opacity, 1))}.border-green-600{--tw-border-opacity: 1;border-color:rgb(22 163 74 / var(--tw-border-opacity, 1))}.border-red-200{--tw-border-opacity: 1;border-color:rgb(254 202 202 / var(--tw-border-opacity, 1))}.border-red-300{--tw-border-opacity: 1;border-color:rgb(252 165 165 / var(--tw-border-opacity, 1))}.border-transparent{border-color:transparent}.border-white{--tw-border-opacity: 1;border-color:rgb(255 255 255 / var(--tw-border-opacity, 1))}.bg-blue-100{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity, 1))}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.bg-gray-900{--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity, 1))}.bg-green-100{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity, 1))}.bg-green-50{--tw-bg-opacity: 1;background-color:rgb(240 253 244 / var(--tw-bg-opacity, 1))}.bg-green-600{--tw-bg-opacity: 1;background-color:rgb(22 163 74 / var(--tw-bg-opacity, 1))}.bg-purple-100{--tw-bg-opacity: 1;background-color:rgb(243 232 255 / var(--tw-bg-opacity, 1))}.bg-red-100{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity, 1))}.bg-red-50{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity, 1))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-white\/80{background-color:#fffc}.bg-yellow-100{--tw-bg-opacity: 1;background-color:rgb(254 249 195 / var(--tw-bg-opacity, 1))}.p-2{padding:.5rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-20{padding-top:5rem;padding-bottom:5rem}.py-24{padding-top:6rem;padding-bottom:6rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.pb-4{padding-bottom:1rem}.pr-10{padding-right:2.5rem}.pr-3{padding-right:.75rem}.pt-20{padding-top:5rem}.pt-8{padding-top:2rem}.text-center{text-align:center}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-extrabold{font-weight:800}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.tracking-tight{letter-spacing:-.025em}.text-blue-600{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity, 1))}.text-gray-900{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity, 1))}.text-green-100{--tw-text-opacity: 1;color:rgb(220 252 231 / var(--tw-text-opacity, 1))}.text-green-200{--tw-text-opacity: 1;color:rgb(187 247 208 / var(--tw-text-opacity, 1))}.text-green-400{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.text-green-500{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity, 1))}.text-green-600{--tw-text-opacity: 1;color:rgb(22 163 74 / var(--tw-text-opacity, 1))}.text-purple-600{--tw-text-opacity: 1;color:rgb(147 51 234 / var(--tw-text-opacity, 1))}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.text-red-700{--tw-text-opacity: 1;color:rgb(185 28 28 / var(--tw-text-opacity, 1))}.text-red-800{--tw-text-opacity: 1;color:rgb(153 27 27 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-yellow-600{--tw-text-opacity: 1;color:rgb(202 138 4 / var(--tw-text-opacity, 1))}.opacity-25{opacity:.25}.opacity-75{opacity:.75}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline{outline-style:solid}.backdrop-blur-sm{--tw-backdrop-blur: blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.hover\:bg-gray-100:hover{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.hover\:bg-green-50:hover{--tw-bg-opacity: 1;background-color:rgb(240 253 244 / var(--tw-bg-opacity, 1))}.hover\:bg-green-700:hover{--tw-bg-opacity: 1;background-color:rgb(21 128 61 / var(--tw-bg-opacity, 1))}.hover\:bg-white:hover{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.hover\:text-green-500:hover{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity, 1))}.hover\:text-green-600:hover{--tw-text-opacity: 1;color:rgb(22 163 74 / var(--tw-text-opacity, 1))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.focus\:border-transparent:focus{border-color:transparent}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-green-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(34 197 94 / var(--tw-ring-opacity, 1))}.focus\:ring-offset-2:focus{--tw-ring-offset-width: 2px}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}@media (min-width: 640px){.sm\:flex-row{flex-direction:row}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}}@media (min-width: 768px){.md\:flex{display:flex}.md\:hidden{display:none}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:text-2xl{font-size:1.5rem;line-height:2rem}.md\:text-4xl{font-size:2.25rem;line-height:2.5rem}.md\:text-6xl{font-size:3.75rem;line-height:1}}@media (min-width: 1024px){.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:px-8{padding-left:2rem;padding-right:2rem}}
diff --git a/frontend/dist/assets/index-CGuCVMO8.js b/frontend/dist/assets/index-CGuCVMO8.js
new file mode 100644
index 0000000000..1532a39b77
--- /dev/null
+++ b/frontend/dist/assets/index-CGuCVMO8.js
@@ -0,0 +1,113 @@
+import{r as Xt,g as Zt,a as Qt}from"./vendor-BtP0CW_r.js";function Fr(e,t){for(var r=0;rn[a]})}}}return Object.freeze(Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}))}(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const a of document.querySelectorAll('link[rel="modulepreload"]'))n(a);new MutationObserver(a=>{for(const o of a)if(o.type==="childList")for(const i of o.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&n(i)}).observe(document,{childList:!0,subtree:!0});function r(a){const o={};return a.integrity&&(o.integrity=a.integrity),a.referrerPolicy&&(o.referrerPolicy=a.referrerPolicy),a.crossOrigin==="use-credentials"?o.credentials="include":a.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function n(a){if(a.ep)return;a.ep=!0;const o=r(a);fetch(a.href,o)}})();var Ze={exports:{}},me={};/**
+ * @license React
+ * react-jsx-runtime.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */var St;function Ur(){if(St)return me;St=1;var e=Xt(),t=Symbol.for("react.element"),r=Symbol.for("react.fragment"),n=Object.prototype.hasOwnProperty,a=e.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,o={key:!0,ref:!0,__self:!0,__source:!0};function i(c,l,h){var u,d={},m=null,p=null;h!==void 0&&(m=""+h),l.key!==void 0&&(m=""+l.key),l.ref!==void 0&&(p=l.ref);for(u in l)n.call(l,u)&&!o.hasOwnProperty(u)&&(d[u]=l[u]);if(c&&c.defaultProps)for(u in l=c.defaultProps,l)d[u]===void 0&&(d[u]=l[u]);return{$$typeof:t,type:c,key:m,ref:p,props:d,_owner:a.current}}return me.Fragment=r,me.jsx=i,me.jsxs=i,me}var Ct;function Vr(){return Ct||(Ct=1,Ze.exports=Ur()),Ze.exports}var s=Vr(),f=Xt();const Z=Zt(f),qr=Fr({__proto__:null,default:Z},[f]);var Pe={},Pt;function Hr(){if(Pt)return Pe;Pt=1;var e=Qt();return Pe.createRoot=e.createRoot,Pe.hydrateRoot=e.hydrateRoot,Pe}var Gr=Hr();const Jr=Zt(Gr);Qt();/**
+ * @remix-run/router v1.23.0
+ *
+ * Copyright (c) Remix Software Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.md file in the root directory of this source tree.
+ *
+ * @license MIT
+ */function xe(){return xe=Object.assign?Object.assign.bind():function(e){for(var t=1;t"u")throw new Error(t)}function er(e,t){if(!e){typeof console<"u"&&console.warn(t);try{throw new Error(t)}catch{}}}function Kr(){return Math.random().toString(36).substr(2,8)}function Rt(e,t){return{usr:e.state,key:e.key,idx:t}}function st(e,t,r,n){return r===void 0&&(r=null),xe({pathname:typeof e=="string"?e:e.pathname,search:"",hash:""},typeof t=="string"?ie(t):t,{state:r,key:t&&t.key||n||Kr()})}function Me(e){let{pathname:t="/",search:r="",hash:n=""}=e;return r&&r!=="?"&&(t+=r.charAt(0)==="?"?r:"?"+r),n&&n!=="#"&&(t+=n.charAt(0)==="#"?n:"#"+n),t}function ie(e){let t={};if(e){let r=e.indexOf("#");r>=0&&(t.hash=e.substr(r),e=e.substr(0,r));let n=e.indexOf("?");n>=0&&(t.search=e.substr(n),e=e.substr(0,n)),e&&(t.pathname=e)}return t}function Xr(e,t,r,n){n===void 0&&(n={});let{window:a=document.defaultView,v5Compat:o=!1}=n,i=a.history,c=V.Pop,l=null,h=u();h==null&&(h=0,i.replaceState(xe({},i.state,{idx:h}),""));function u(){return(i.state||{idx:null}).idx}function d(){c=V.Pop;let g=u(),x=g==null?null:g-h;h=g,l&&l({action:c,location:v.location,delta:x})}function m(g,x){c=V.Push;let y=st(v.location,g,x);h=u()+1;let w=Rt(y,h),N=v.createHref(y);try{i.pushState(w,"",N)}catch(P){if(P instanceof DOMException&&P.name==="DataCloneError")throw P;a.location.assign(N)}o&&l&&l({action:c,location:v.location,delta:1})}function p(g,x){c=V.Replace;let y=st(v.location,g,x);h=u();let w=Rt(y,h),N=v.createHref(y);i.replaceState(w,"",N),o&&l&&l({action:c,location:v.location,delta:0})}function b(g){let x=a.location.origin!=="null"?a.location.origin:a.location.href,y=typeof g=="string"?g:Me(g);return y=y.replace(/ $/,"%20"),R(x,"No window.location.(origin|href) available to create URL for href: "+y),new URL(y,x)}let v={get action(){return c},get location(){return e(a,i)},listen(g){if(l)throw new Error("A history only accepts one active listener");return a.addEventListener(Et,d),l=g,()=>{a.removeEventListener(Et,d),l=null}},createHref(g){return t(a,g)},createURL:b,encodeLocation(g){let x=b(g);return{pathname:x.pathname,search:x.search,hash:x.hash}},push:m,replace:p,go(g){return i.go(g)}};return v}var Lt;(function(e){e.data="data",e.deferred="deferred",e.redirect="redirect",e.error="error"})(Lt||(Lt={}));function Zr(e,t,r){return r===void 0&&(r="/"),Qr(e,t,r)}function Qr(e,t,r,n){let a=typeof t=="string"?ie(t):t,o=ft(a.pathname||"/",r);if(o==null)return null;let i=tr(e);en(i);let c=null;for(let l=0;c==null&&l{let l={relativePath:c===void 0?o.path||"":c,caseSensitive:o.caseSensitive===!0,childrenIndex:i,route:o};l.relativePath.startsWith("/")&&(R(l.relativePath.startsWith(n),'Absolute route path "'+l.relativePath+'" nested under path '+('"'+n+'" is not valid. An absolute child route path ')+"must start with the combined path of all its parent routes."),l.relativePath=l.relativePath.slice(n.length));let h=q([n,l.relativePath]),u=r.concat(l);o.children&&o.children.length>0&&(R(o.index!==!0,"Index routes must not have child routes. Please remove "+('all child routes from route path "'+h+'".')),tr(o.children,t,u,h)),!(o.path==null&&!o.index)&&t.push({path:h,score:ln(h,o.index),routesMeta:u})};return e.forEach((o,i)=>{var c;if(o.path===""||!((c=o.path)!=null&&c.includes("?")))a(o,i);else for(let l of rr(o.path))a(o,i,l)}),t}function rr(e){let t=e.split("/");if(t.length===0)return[];let[r,...n]=t,a=r.endsWith("?"),o=r.replace(/\?$/,"");if(n.length===0)return a?[o,""]:[o];let i=rr(n.join("/")),c=[];return c.push(...i.map(l=>l===""?o:[o,l].join("/"))),a&&c.push(...i),c.map(l=>e.startsWith("/")&&l===""?"/":l)}function en(e){e.sort((t,r)=>t.score!==r.score?r.score-t.score:cn(t.routesMeta.map(n=>n.childrenIndex),r.routesMeta.map(n=>n.childrenIndex)))}const tn=/^:[\w-]+$/,rn=3,nn=2,sn=1,an=10,on=-2,It=e=>e==="*";function ln(e,t){let r=e.split("/"),n=r.length;return r.some(It)&&(n+=on),t&&(n+=nn),r.filter(a=>!It(a)).reduce((a,o)=>a+(tn.test(o)?rn:o===""?sn:an),n)}function cn(e,t){return e.length===t.length&&e.slice(0,-1).every((n,a)=>n===t[a])?e[e.length-1]-t[t.length-1]:0}function dn(e,t,r){let{routesMeta:n}=e,a={},o="/",i=[];for(let c=0;c{let{paramName:m,isOptional:p}=u;if(m==="*"){let v=c[d]||"";i=o.slice(0,o.length-v.length).replace(/(.)\/+$/,"$1")}const b=c[d];return p&&!b?h[m]=void 0:h[m]=(b||"").replace(/%2F/g,"/"),h},{}),pathname:o,pathnameBase:i,pattern:e}}function hn(e,t,r){t===void 0&&(t=!1),r===void 0&&(r=!0),er(e==="*"||!e.endsWith("*")||e.endsWith("/*"),'Route path "'+e+'" will be treated as if it were '+('"'+e.replace(/\*$/,"/*")+'" because the `*` character must ')+"always follow a `/` in the pattern. To get rid of this warning, "+('please change the route path to "'+e.replace(/\*$/,"/*")+'".'));let n=[],a="^"+e.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(i,c,l)=>(n.push({paramName:c,isOptional:l!=null}),l?"/?([^\\/]+)?":"/([^\\/]+)"));return e.endsWith("*")?(n.push({paramName:"*"}),a+=e==="*"||e==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):r?a+="\\/*$":e!==""&&e!=="/"&&(a+="(?:(?=\\/|$))"),[new RegExp(a,t?void 0:"i"),n]}function mn(e){try{return e.split("/").map(t=>decodeURIComponent(t).replace(/\//g,"%2F")).join("/")}catch(t){return er(!1,'The URL path "'+e+'" could not be decoded because it is is a malformed URL segment. This is probably due to a bad percent '+("encoding ("+t+").")),e}}function ft(e,t){if(t==="/")return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let r=t.endsWith("/")?t.length-1:t.length,n=e.charAt(r);return n&&n!=="/"?null:e.slice(r)||"/"}function fn(e,t){t===void 0&&(t="/");let{pathname:r,search:n="",hash:a=""}=typeof e=="string"?ie(e):e;return{pathname:r?r.startsWith("/")?r:pn(r,t):t,search:vn(n),hash:yn(a)}}function pn(e,t){let r=t.replace(/\/+$/,"").split("/");return e.split("/").forEach(a=>{a===".."?r.length>1&&r.pop():a!=="."&&r.push(a)}),r.length>1?r.join("/"):"/"}function Qe(e,t,r,n){return"Cannot include a '"+e+"' character in a manually specified "+("`to."+t+"` field ["+JSON.stringify(n)+"]. Please separate it out to the ")+("`to."+r+"` field. Alternatively you may provide the full path as ")+'a string in and the router will parse it for you.'}function gn(e){return e.filter((t,r)=>r===0||t.route.path&&t.route.path.length>0)}function pt(e,t){let r=gn(e);return t?r.map((n,a)=>a===r.length-1?n.pathname:n.pathnameBase):r.map(n=>n.pathnameBase)}function gt(e,t,r,n){n===void 0&&(n=!1);let a;typeof e=="string"?a=ie(e):(a=xe({},e),R(!a.pathname||!a.pathname.includes("?"),Qe("?","pathname","search",a)),R(!a.pathname||!a.pathname.includes("#"),Qe("#","pathname","hash",a)),R(!a.search||!a.search.includes("#"),Qe("#","search","hash",a)));let o=e===""||a.pathname==="",i=o?"/":a.pathname,c;if(i==null)c=r;else{let d=t.length-1;if(!n&&i.startsWith("..")){let m=i.split("/");for(;m[0]==="..";)m.shift(),d-=1;a.pathname=m.join("/")}c=d>=0?t[d]:"/"}let l=fn(a,c),h=i&&i!=="/"&&i.endsWith("/"),u=(o||i===".")&&r.endsWith("/");return!l.pathname.endsWith("/")&&(h||u)&&(l.pathname+="/"),l}const q=e=>e.join("/").replace(/\/\/+/g,"/"),xn=e=>e.replace(/\/+$/,"").replace(/^\/*/,"/"),vn=e=>!e||e==="?"?"":e.startsWith("?")?e:"?"+e,yn=e=>!e||e==="#"?"":e.startsWith("#")?e:"#"+e;function bn(e){return e!=null&&typeof e.status=="number"&&typeof e.statusText=="string"&&typeof e.internal=="boolean"&&"data"in e}const nr=["post","put","patch","delete"];new Set(nr);const wn=["get",...nr];new Set(wn);/**
+ * React Router v6.30.1
+ *
+ * Copyright (c) Remix Software Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.md file in the root directory of this source tree.
+ *
+ * @license MIT
+ */function ve(){return ve=Object.assign?Object.assign.bind():function(e){for(var t=1;t{c.current=!0}),f.useCallback(function(h,u){if(u===void 0&&(u={}),!c.current)return;if(typeof h=="number"){n.go(h);return}let d=gt(h,JSON.parse(i),o,u.relative==="path");e==null&&t!=="/"&&(d.pathname=d.pathname==="/"?t:q([t,d.pathname])),(u.replace?n.replace:n.push)(d,u.state,u)},[t,n,i,o,e])}function or(e,t){let{relative:r}=t===void 0?{}:t,{future:n}=f.useContext(H),{matches:a}=f.useContext(G),{pathname:o}=be(),i=JSON.stringify(pt(a,n.v7_relativeSplatPath));return f.useMemo(()=>gt(e,JSON.parse(i),o,r==="path"),[e,i,o,r])}function Sn(e,t){return Cn(e,t)}function Cn(e,t,r,n){le()||R(!1);let{navigator:a}=f.useContext(H),{matches:o}=f.useContext(G),i=o[o.length-1],c=i?i.params:{};i&&i.pathname;let l=i?i.pathnameBase:"/";i&&i.route;let h=be(),u;if(t){var d;let g=typeof t=="string"?ie(t):t;l==="/"||(d=g.pathname)!=null&&d.startsWith(l)||R(!1),u=g}else u=h;let m=u.pathname||"/",p=m;if(l!=="/"){let g=l.replace(/^\//,"").split("/");p="/"+m.replace(/^\//,"").split("/").slice(g.length).join("/")}let b=Zr(e,{pathname:p}),v=In(b&&b.map(g=>Object.assign({},g,{params:Object.assign({},c,g.params),pathname:q([l,a.encodeLocation?a.encodeLocation(g.pathname).pathname:g.pathname]),pathnameBase:g.pathnameBase==="/"?l:q([l,a.encodeLocation?a.encodeLocation(g.pathnameBase).pathname:g.pathnameBase])})),o,r,n);return t&&v?f.createElement(We.Provider,{value:{location:ve({pathname:"/",search:"",hash:"",state:null,key:"default"},u),navigationType:V.Pop}},v):v}function Pn(){let e=Mn(),t=bn(e)?e.status+" "+e.statusText:e instanceof Error?e.message:JSON.stringify(e),r=e instanceof Error?e.stack:null,a={padding:"0.5rem",backgroundColor:"rgba(200,200,200, 0.5)"};return f.createElement(f.Fragment,null,f.createElement("h2",null,"Unexpected Application Error!"),f.createElement("h3",{style:{fontStyle:"italic"}},t),r?f.createElement("pre",{style:a},r):null,null)}const En=f.createElement(Pn,null);class Rn extends f.Component{constructor(t){super(t),this.state={location:t.location,revalidation:t.revalidation,error:t.error}}static getDerivedStateFromError(t){return{error:t}}static getDerivedStateFromProps(t,r){return r.location!==t.location||r.revalidation!=="idle"&&t.revalidation==="idle"?{error:t.error,location:t.location,revalidation:t.revalidation}:{error:t.error!==void 0?t.error:r.error,location:r.location,revalidation:t.revalidation||r.revalidation}}componentDidCatch(t,r){console.error("React Router caught the following error during render",t,r)}render(){return this.state.error!==void 0?f.createElement(G.Provider,{value:this.props.routeContext},f.createElement(sr.Provider,{value:this.state.error,children:this.props.component})):this.props.children}}function Ln(e){let{routeContext:t,match:r,children:n}=e,a=f.useContext(xt);return a&&a.static&&a.staticContext&&(r.route.errorElement||r.route.ErrorBoundary)&&(a.staticContext._deepestRenderedBoundaryId=r.route.id),f.createElement(G.Provider,{value:t},n)}function In(e,t,r,n){var a;if(t===void 0&&(t=[]),r===void 0&&(r=null),n===void 0&&(n=null),e==null){var o;if(!r)return null;if(r.errors)e=r.matches;else if((o=n)!=null&&o.v7_partialHydration&&t.length===0&&!r.initialized&&r.matches.length>0)e=r.matches;else return null}let i=e,c=(a=r)==null?void 0:a.errors;if(c!=null){let u=i.findIndex(d=>d.route.id&&(c==null?void 0:c[d.route.id])!==void 0);u>=0||R(!1),i=i.slice(0,Math.min(i.length,u+1))}let l=!1,h=-1;if(r&&n&&n.v7_partialHydration)for(let u=0;u=0?i=i.slice(0,h+1):i=[i[0]];break}}}return i.reduceRight((u,d,m)=>{let p,b=!1,v=null,g=null;r&&(p=c&&d.route.id?c[d.route.id]:void 0,v=d.route.errorElement||En,l&&(h<0&&m===0?(Tn("route-fallback"),b=!0,g=null):h===m&&(b=!0,g=d.route.hydrateFallbackElement||null)));let x=t.concat(i.slice(0,m+1)),y=()=>{let w;return p?w=v:b?w=g:d.route.Component?w=f.createElement(d.route.Component,null):d.route.element?w=d.route.element:w=u,f.createElement(Ln,{match:d,routeContext:{outlet:u,matches:x,isDataRoute:r!=null},children:w})};return r&&(d.route.ErrorBoundary||d.route.errorElement||m===0)?f.createElement(Rn,{location:r.location,revalidation:r.revalidation,component:v,error:p,children:y(),routeContext:{outlet:null,matches:x,isDataRoute:!0}}):y()},null)}var ir=function(e){return e.UseBlocker="useBlocker",e.UseRevalidator="useRevalidator",e.UseNavigateStable="useNavigate",e}(ir||{}),lr=function(e){return e.UseBlocker="useBlocker",e.UseLoaderData="useLoaderData",e.UseActionData="useActionData",e.UseRouteError="useRouteError",e.UseNavigation="useNavigation",e.UseRouteLoaderData="useRouteLoaderData",e.UseMatches="useMatches",e.UseRevalidator="useRevalidator",e.UseNavigateStable="useNavigate",e.UseRouteId="useRouteId",e}(lr||{});function On(e){let t=f.useContext(xt);return t||R(!1),t}function An(e){let t=f.useContext(jn);return t||R(!1),t}function _n(e){let t=f.useContext(G);return t||R(!1),t}function cr(e){let t=_n(),r=t.matches[t.matches.length-1];return r.route.id||R(!1),r.route.id}function Mn(){var e;let t=f.useContext(sr),r=An(),n=cr();return t!==void 0?t:(e=r.errors)==null?void 0:e[n]}function $n(){let{router:e}=On(ir.UseNavigateStable),t=cr(lr.UseNavigateStable),r=f.useRef(!1);return ar(()=>{r.current=!0}),f.useCallback(function(a,o){o===void 0&&(o={}),r.current&&(typeof a=="number"?e.navigate(a):e.navigate(a,ve({fromRouteId:t},o)))},[e,t])}const Ot={};function Tn(e,t,r){Ot[e]||(Ot[e]=!0)}function Bn(e,t){e==null||e.v7_startTransition,e==null||e.v7_relativeSplatPath}function Dn(e){let{to:t,replace:r,state:n,relative:a}=e;le()||R(!1);let{future:o,static:i}=f.useContext(H),{matches:c}=f.useContext(G),{pathname:l}=be(),h=ze(),u=gt(t,pt(c,o.v7_relativeSplatPath),l,a==="path"),d=JSON.stringify(u);return f.useEffect(()=>h(JSON.parse(d),{replace:r,state:n,relative:a}),[h,d,a,r,n]),null}function fe(e){R(!1)}function Wn(e){let{basename:t="/",children:r=null,location:n,navigationType:a=V.Pop,navigator:o,static:i=!1,future:c}=e;le()&&R(!1);let l=t.replace(/^\/*/,"/"),h=f.useMemo(()=>({basename:l,navigator:o,static:i,future:ve({v7_relativeSplatPath:!1},c)}),[l,c,o,i]);typeof n=="string"&&(n=ie(n));let{pathname:u="/",search:d="",hash:m="",state:p=null,key:b="default"}=n,v=f.useMemo(()=>{let g=ft(u,l);return g==null?null:{location:{pathname:g,search:d,hash:m,state:p,key:b},navigationType:a}},[l,u,d,m,p,b,a]);return v==null?null:f.createElement(H.Provider,{value:h},f.createElement(We.Provider,{children:r,value:v}))}function zn(e){let{children:t,location:r}=e;return Sn(at(t),r)}new Promise(()=>{});function at(e,t){t===void 0&&(t=[]);let r=[];return f.Children.forEach(e,(n,a)=>{if(!f.isValidElement(n))return;let o=[...t,a];if(n.type===f.Fragment){r.push.apply(r,at(n.props.children,o));return}n.type!==fe&&R(!1),!n.props.index||!n.props.children||R(!1);let i={id:n.props.id||o.join("-"),caseSensitive:n.props.caseSensitive,element:n.props.element,Component:n.props.Component,index:n.props.index,path:n.props.path,loader:n.props.loader,action:n.props.action,errorElement:n.props.errorElement,ErrorBoundary:n.props.ErrorBoundary,hasErrorBoundary:n.props.ErrorBoundary!=null||n.props.errorElement!=null,shouldRevalidate:n.props.shouldRevalidate,handle:n.props.handle,lazy:n.props.lazy};n.props.children&&(i.children=at(n.props.children,o)),r.push(i)}),r}/**
+ * React Router DOM v6.30.1
+ *
+ * Copyright (c) Remix Software Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.md file in the root directory of this source tree.
+ *
+ * @license MIT
+ */function ot(){return ot=Object.assign?Object.assign.bind():function(e){for(var t=1;t=0)&&(r[a]=e[a]);return r}function Un(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function Vn(e,t){return e.button===0&&(!t||t==="_self")&&!Un(e)}const qn=["onClick","relative","reloadDocument","replace","state","target","to","preventScrollReset","viewTransition"],Hn="6";try{window.__reactRouterVersion=Hn}catch{}const Gn="startTransition",At=qr[Gn];function Jn(e){let{basename:t,children:r,future:n,window:a}=e,o=f.useRef();o.current==null&&(o.current=Yr({window:a,v5Compat:!0}));let i=o.current,[c,l]=f.useState({action:i.action,location:i.location}),{v7_startTransition:h}=n||{},u=f.useCallback(d=>{h&&At?At(()=>l(d)):l(d)},[l,h]);return f.useLayoutEffect(()=>i.listen(u),[i,u]),f.useEffect(()=>Bn(n),[n]),f.createElement(Wn,{basename:t,children:r,location:c.location,navigationType:c.action,navigator:i,future:n})}const Yn=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u",Kn=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,$=f.forwardRef(function(t,r){let{onClick:n,relative:a,reloadDocument:o,replace:i,state:c,target:l,to:h,preventScrollReset:u,viewTransition:d}=t,m=Fn(t,qn),{basename:p}=f.useContext(H),b,v=!1;if(typeof h=="string"&&Kn.test(h)&&(b=h,Yn))try{let w=new URL(window.location.href),N=h.startsWith("//")?new URL(w.protocol+h):new URL(h),P=ft(N.pathname,p);N.origin===w.origin&&P!=null?h=P+N.search+N.hash:v=!0}catch{}let g=Nn(h,{relative:a}),x=Xn(h,{replace:i,state:c,target:l,preventScrollReset:u,relative:a,viewTransition:d});function y(w){n&&n(w),w.defaultPrevented||x(w)}return f.createElement("a",ot({},m,{href:b||g,onClick:v||o?n:y,ref:r,target:l}))});var _t;(function(e){e.UseScrollRestoration="useScrollRestoration",e.UseSubmit="useSubmit",e.UseSubmitFetcher="useSubmitFetcher",e.UseFetcher="useFetcher",e.useViewTransitionState="useViewTransitionState"})(_t||(_t={}));var Mt;(function(e){e.UseFetcher="useFetcher",e.UseFetchers="useFetchers",e.UseScrollRestoration="useScrollRestoration"})(Mt||(Mt={}));function Xn(e,t){let{target:r,replace:n,state:a,preventScrollReset:o,relative:i,viewTransition:c}=t===void 0?{}:t,l=ze(),h=be(),u=or(e,{relative:i});return f.useCallback(d=>{if(Vn(d,r)){d.preventDefault();let m=n!==void 0?n:Me(h)===Me(u);l(e,{replace:m,state:a,preventScrollReset:o,relative:i,viewTransition:c})}},[h,l,u,n,a,r,e,o,i,c])}var O=function(){return O=Object.assign||function(t){for(var r,n=1,a=arguments.length;n0?I(ce,--M):0,ne--,L===10&&(ne=1,Ue--),L}function T(){return L=M2||lt(L)>3?"":" "}function is(e,t){for(;--t&&T()&&!(L<48||L>102||L>57&&L<65||L>70&&L<97););return qe(e,Ie()+(t<6&&K()==32&&T()==32))}function ct(e){for(;T();)switch(L){case e:return M;case 34:case 39:e!==34&&e!==39&&ct(L);break;case 40:e===41&&ct(e);break;case 92:T();break}return M}function ls(e,t){for(;T()&&e+L!==57;)if(e+L===84&&K()===47)break;return"/*"+qe(t,M-1)+"*"+yt(e===47?e:T())}function cs(e){for(;!lt(K());)T();return qe(e,M)}function ds(e){return as(Oe("",null,null,null,[""],e=ss(e),0,[0],e))}function Oe(e,t,r,n,a,o,i,c,l){for(var h=0,u=0,d=i,m=0,p=0,b=0,v=1,g=1,x=1,y=0,w="",N=a,P=o,C=n,k=w;g;)switch(b=y,y=T()){case 40:if(b!=108&&I(k,d-1)==58){Le(k+=j(et(y),"&","&\f"),"&\f",hr(h?c[h-1]:0))!=-1&&(x=-1);break}case 34:case 39:case 91:k+=et(y);break;case 9:case 10:case 13:case 32:k+=os(b);break;case 92:k+=is(Ie()-1,7);continue;case 47:switch(K()){case 42:case 47:pe(us(ls(T(),Ie()),t,r,l),l);break;default:k+="/"}break;case 123*v:c[h++]=B(k)*x;case 125*v:case 59:case 0:switch(y){case 0:case 125:g=0;case 59+u:x==-1&&(k=j(k,/\f/g,"")),p>0&&B(k)-d&&pe(p>32?Bt(k+";",n,r,d-1,l):Bt(j(k," ","")+";",n,r,d-2,l),l);break;case 59:k+=";";default:if(pe(C=Tt(k,t,r,h,u,a,c,w,N=[],P=[],d,o),o),y===123)if(u===0)Oe(k,t,C,C,N,o,d,c,P);else switch(m===99&&I(k,3)===110?100:m){case 100:case 108:case 109:case 115:Oe(e,C,C,n&&pe(Tt(e,C,C,0,0,a,c,w,a,N=[],d,P),P),a,P,d,c,n?N:P);break;default:Oe(k,C,C,C,[""],P,0,c,P)}}h=u=p=0,v=x=1,w=k="",d=i;break;case 58:d=1+B(k),p=b;default:if(v<1){if(y==123)--v;else if(y==125&&v++==0&&ns()==125)continue}switch(k+=yt(y),y*v){case 38:x=u>0?1:(k+="\f",-1);break;case 44:c[h++]=(B(k)-1)*x,x=1;break;case 64:K()===45&&(k+=et(T())),m=K(),u=d=B(w=k+=cs(Ie())),y++;break;case 45:b===45&&B(k)==2&&(v=0)}}return o}function Tt(e,t,r,n,a,o,i,c,l,h,u,d){for(var m=a-1,p=a===0?o:[""],b=fr(p),v=0,g=0,x=0;v0?p[y]+" "+w:j(w,/&\f/g,p[y])))&&(l[x++]=N);return Ve(e,t,r,a===0?Fe:c,l,h,u,d)}function us(e,t,r,n){return Ve(e,t,r,dr,yt(rs()),re(e,2,-2),0,n)}function Bt(e,t,r,n,a){return Ve(e,t,r,vt,re(e,0,n),re(e,n+1,-1),n,a)}function gr(e,t,r){switch(es(e,t)){case 5103:return S+"print-"+e+e;case 5737:case 4201:case 3177:case 3433:case 1641:case 4457:case 2921:case 5572:case 6356:case 5844:case 3191:case 6645:case 3005:case 6391:case 5879:case 5623:case 6135:case 4599:case 4855:case 4215:case 6389:case 5109:case 5365:case 5621:case 3829:return S+e+e;case 4789:return ge+e+e;case 5349:case 4246:case 4810:case 6968:case 2756:return S+e+ge+e+E+e+e;case 5936:switch(I(e,t+11)){case 114:return S+e+E+j(e,/[svh]\w+-[tblr]{2}/,"tb")+e;case 108:return S+e+E+j(e,/[svh]\w+-[tblr]{2}/,"tb-rl")+e;case 45:return S+e+E+j(e,/[svh]\w+-[tblr]{2}/,"lr")+e}case 6828:case 4268:case 2903:return S+e+E+e+e;case 6165:return S+e+E+"flex-"+e+e;case 5187:return S+e+j(e,/(\w+).+(:[^]+)/,S+"box-$1$2"+E+"flex-$1$2")+e;case 5443:return S+e+E+"flex-item-"+j(e,/flex-|-self/g,"")+(W(e,/flex-|baseline/)?"":E+"grid-row-"+j(e,/flex-|-self/g,""))+e;case 4675:return S+e+E+"flex-line-pack"+j(e,/align-content|flex-|-self/g,"")+e;case 5548:return S+e+E+j(e,"shrink","negative")+e;case 5292:return S+e+E+j(e,"basis","preferred-size")+e;case 6060:return S+"box-"+j(e,"-grow","")+S+e+E+j(e,"grow","positive")+e;case 4554:return S+j(e,/([^-])(transform)/g,"$1"+S+"$2")+e;case 6187:return j(j(j(e,/(zoom-|grab)/,S+"$1"),/(image-set)/,S+"$1"),e,"")+e;case 5495:case 3959:return j(e,/(image-set\([^]*)/,S+"$1$`$1");case 4968:return j(j(e,/(.+:)(flex-)?(.*)/,S+"box-pack:$3"+E+"flex-pack:$3"),/s.+-b[^;]+/,"justify")+S+e+e;case 4200:if(!W(e,/flex-|baseline/))return E+"grid-column-align"+re(e,t)+e;break;case 2592:case 3360:return E+j(e,"template-","")+e;case 4384:case 3616:return r&&r.some(function(n,a){return t=a,W(n.props,/grid-\w+-end/)})?~Le(e+(r=r[t].value),"span",0)?e:E+j(e,"-start","")+e+E+"grid-row-span:"+(~Le(r,"span",0)?W(r,/\d+/):+W(r,/\d+/)-+W(e,/\d+/))+";":E+j(e,"-start","")+e;case 4896:case 4128:return r&&r.some(function(n){return W(n.props,/grid-\w+-start/)})?e:E+j(j(e,"-end","-span"),"span ","")+e;case 4095:case 3583:case 4068:case 2532:return j(e,/(.+)-inline(.+)/,S+"$1$2")+e;case 8116:case 7059:case 5753:case 5535:case 5445:case 5701:case 4933:case 4677:case 5533:case 5789:case 5021:case 4765:if(B(e)-1-t>6)switch(I(e,t+1)){case 109:if(I(e,t+4)!==45)break;case 102:return j(e,/(.+:)(.+)-([^]+)/,"$1"+S+"$2-$3$1"+ge+(I(e,t+3)==108?"$3":"$2-$3"))+e;case 115:return~Le(e,"stretch",0)?gr(j(e,"stretch","fill-available"),t,r)+e:e}break;case 5152:case 5920:return j(e,/(.+?):(\d+)(\s*\/\s*(span)?\s*(\d+))?(.*)/,function(n,a,o,i,c,l,h){return E+a+":"+o+h+(i?E+a+"-span:"+(c?l:+l-+o)+h:"")+e});case 4949:if(I(e,t+6)===121)return j(e,":",":"+S)+e;break;case 6444:switch(I(e,I(e,14)===45?18:11)){case 120:return j(e,/(.+:)([^;\s!]+)(;|(\s+)?!.+)?/,"$1"+S+(I(e,14)===45?"inline-":"")+"box$3$1"+S+"$2$3$1"+E+"$2box$3")+e;case 100:return j(e,":",":"+E)+e}break;case 5719:case 2647:case 2135:case 3927:case 2391:return j(e,"scroll-","scroll-snap-")+e}return e}function Te(e,t){for(var r="",n=0;n-1&&!e.return)switch(e.type){case vt:e.return=gr(e.value,e.length,r);return;case ur:return Te([U(e,{value:j(e.value,"@","@"+S)})],n);case Fe:if(e.length)return ts(r=e.props,function(a){switch(W(a,n=/(::plac\w+|:read-\w+)/)){case":read-only":case":read-write":ee(U(e,{props:[j(a,/:(read-\w+)/,":"+ge+"$1")]})),ee(U(e,{props:[a]})),it(e,{props:$t(r,n)});break;case"::placeholder":ee(U(e,{props:[j(a,/:(plac\w+)/,":"+S+"input-$1")]})),ee(U(e,{props:[j(a,/:(plac\w+)/,":"+ge+"$1")]})),ee(U(e,{props:[j(a,/:(plac\w+)/,E+"input-$1")]})),ee(U(e,{props:[a]})),it(e,{props:$t(r,n)});break}return""})}}var gs={animationIterationCount:1,aspectRatio:1,borderImageOutset:1,borderImageSlice:1,borderImageWidth:1,boxFlex:1,boxFlexGroup:1,boxOrdinalGroup:1,columnCount:1,columns:1,flex:1,flexGrow:1,flexPositive:1,flexShrink:1,flexNegative:1,flexOrder:1,gridRow:1,gridRowEnd:1,gridRowSpan:1,gridRowStart:1,gridColumn:1,gridColumnEnd:1,gridColumnSpan:1,gridColumnStart:1,msGridRow:1,msGridRowSpan:1,msGridColumn:1,msGridColumnSpan:1,fontWeight:1,lineHeight:1,opacity:1,order:1,orphans:1,tabSize:1,widows:1,zIndex:1,zoom:1,WebkitLineClamp:1,fillOpacity:1,floodOpacity:1,stopOpacity:1,strokeDasharray:1,strokeDashoffset:1,strokeMiterlimit:1,strokeOpacity:1,strokeWidth:1},A={},se=typeof process<"u"&&A!==void 0&&(A.REACT_APP_SC_ATTR||A.SC_ATTR)||"data-styled",xr="active",vr="data-styled-version",He="6.1.19",bt=`/*!sc*/
+`,Be=typeof window<"u"&&typeof document<"u",xs=!!(typeof SC_DISABLE_SPEEDY=="boolean"?SC_DISABLE_SPEEDY:typeof process<"u"&&A!==void 0&&A.REACT_APP_SC_DISABLE_SPEEDY!==void 0&&A.REACT_APP_SC_DISABLE_SPEEDY!==""?A.REACT_APP_SC_DISABLE_SPEEDY!=="false"&&A.REACT_APP_SC_DISABLE_SPEEDY:typeof process<"u"&&A!==void 0&&A.SC_DISABLE_SPEEDY!==void 0&&A.SC_DISABLE_SPEEDY!==""&&A.SC_DISABLE_SPEEDY!=="false"&&A.SC_DISABLE_SPEEDY),Ge=Object.freeze([]),ae=Object.freeze({});function vs(e,t,r){return r===void 0&&(r=ae),e.theme!==r.theme&&e.theme||t||r.theme}var yr=new Set(["a","abbr","address","area","article","aside","audio","b","base","bdi","bdo","big","blockquote","body","br","button","canvas","caption","cite","code","col","colgroup","data","datalist","dd","del","details","dfn","dialog","div","dl","dt","em","embed","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","hr","html","i","iframe","img","input","ins","kbd","keygen","label","legend","li","link","main","map","mark","menu","menuitem","meta","meter","nav","noscript","object","ol","optgroup","option","output","p","param","picture","pre","progress","q","rp","rt","ruby","s","samp","script","section","select","small","source","span","strong","style","sub","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","track","u","ul","use","var","video","wbr","circle","clipPath","defs","ellipse","foreignObject","g","image","line","linearGradient","marker","mask","path","pattern","polygon","polyline","radialGradient","rect","stop","svg","text","tspan"]),ys=/[!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~-]+/g,bs=/(^-|-$)/g;function Dt(e){return e.replace(ys,"-").replace(bs,"")}var ws=/(a)(d)/gi,Ee=52,Wt=function(e){return String.fromCharCode(e+(e>25?39:97))};function dt(e){var t,r="";for(t=Math.abs(e);t>Ee;t=t/Ee|0)r=Wt(t%Ee)+r;return(Wt(t%Ee)+r).replace(ws,"$1-$2")}var tt,br=5381,te=function(e,t){for(var r=t.length;r;)e=33*e^t.charCodeAt(--r);return e},wr=function(e){return te(br,e)};function js(e){return dt(wr(e)>>>0)}function Ns(e){return e.displayName||e.name||"Component"}function rt(e){return typeof e=="string"&&!0}var jr=typeof Symbol=="function"&&Symbol.for,Nr=jr?Symbol.for("react.memo"):60115,ks=jr?Symbol.for("react.forward_ref"):60112,Ss={childContextTypes:!0,contextType:!0,contextTypes:!0,defaultProps:!0,displayName:!0,getDefaultProps:!0,getDerivedStateFromError:!0,getDerivedStateFromProps:!0,mixins:!0,propTypes:!0,type:!0},Cs={name:!0,length:!0,prototype:!0,caller:!0,callee:!0,arguments:!0,arity:!0},kr={$$typeof:!0,compare:!0,defaultProps:!0,displayName:!0,propTypes:!0,type:!0},Ps=((tt={})[ks]={$$typeof:!0,render:!0,defaultProps:!0,displayName:!0,propTypes:!0},tt[Nr]=kr,tt);function zt(e){return("type"in(t=e)&&t.type.$$typeof)===Nr?kr:"$$typeof"in e?Ps[e.$$typeof]:Ss;var t}var Es=Object.defineProperty,Rs=Object.getOwnPropertyNames,Ft=Object.getOwnPropertySymbols,Ls=Object.getOwnPropertyDescriptor,Is=Object.getPrototypeOf,Ut=Object.prototype;function Sr(e,t,r){if(typeof t!="string"){if(Ut){var n=Is(t);n&&n!==Ut&&Sr(e,n,r)}var a=Rs(t);Ft&&(a=a.concat(Ft(t)));for(var o=zt(e),i=zt(t),c=0;c0?" Args: ".concat(t.join(", ")):""))}var Os=function(){function e(t){this.groupSizes=new Uint32Array(512),this.length=512,this.tag=t}return e.prototype.indexOfGroup=function(t){for(var r=0,n=0;n=this.groupSizes.length){for(var n=this.groupSizes,a=n.length,o=a;t>=o;)if((o<<=1)<0)throw we(16,"".concat(t));this.groupSizes=new Uint32Array(o),this.groupSizes.set(n),this.length=o;for(var i=a;i=this.length||this.groupSizes[t]===0)return r;for(var n=this.groupSizes[t],a=this.indexOfGroup(t),o=a+n,i=a;i=0){var n=document.createTextNode(r);return this.element.insertBefore(n,this.nodes[t]||null),this.length++,!0}return!1},e.prototype.deleteRule=function(t){this.element.removeChild(this.nodes[t]),this.length--},e.prototype.getRule=function(t){return t0&&(g+="".concat(x,","))}),l+="".concat(b).concat(v,'{content:"').concat(g,'"}').concat(bt)},u=0;u0?".".concat(t):m},u=l.slice();u.push(function(m){m.type===Fe&&m.value.includes("&")&&(m.props[0]=m.props[0].replace(Us,r).replace(n,h))}),i.prefix&&u.push(ps),u.push(hs);var d=function(m,p,b,v){p===void 0&&(p=""),b===void 0&&(b=""),v===void 0&&(v="&"),t=v,r=p,n=new RegExp("\\".concat(r,"\\b"),"g");var g=m.replace(Vs,""),x=ds(b||p?"".concat(b," ").concat(p," { ").concat(g," }"):g);i.namespace&&(x=Er(x,i.namespace));var y=[];return Te(x,ms(u.concat(fs(function(w){return y.push(w)})))),y};return d.hash=l.length?l.reduce(function(m,p){return p.name||we(15),te(m,p.name)},br).toString():"",d}var Hs=new Pr,ht=qs(),Rr=Z.createContext({shouldForwardProp:void 0,styleSheet:Hs,stylis:ht});Rr.Consumer;Z.createContext(void 0);function Gt(){return f.useContext(Rr)}var Gs=function(){function e(t,r){var n=this;this.inject=function(a,o){o===void 0&&(o=ht);var i=n.name+o.hash;a.hasNameForId(n.id,i)||a.insertRules(n.id,i,o(n.rules,i,"@keyframes"))},this.name=t,this.id="sc-keyframes-".concat(t),this.rules=r,jt(this,function(){throw we(12,String(n.name))})}return e.prototype.getName=function(t){return t===void 0&&(t=ht),this.name+t.hash},e}(),Js=function(e){return e>="A"&&e<="Z"};function Jt(e){for(var t="",r=0;r>>0);if(!r.hasNameForId(this.componentId,i)){var c=n(o,".".concat(i),void 0,this.componentId);r.insertRules(this.componentId,i,c)}a=Y(a,i),this.staticRulesId=i}else{for(var l=te(this.baseHash,n.hash),h="",u=0;u>>0);r.hasNameForId(this.componentId,p)||r.insertRules(this.componentId,p,n(h,".".concat(p),void 0,this.componentId)),a=Y(a,p)}}return a},e}(),Or=Z.createContext(void 0);Or.Consumer;var nt={};function Zs(e,t,r){var n=wt(e),a=e,o=!rt(e),i=t.attrs,c=i===void 0?Ge:i,l=t.componentId,h=l===void 0?function(N,P){var C=typeof N!="string"?"sc":Dt(N);nt[C]=(nt[C]||0)+1;var k="".concat(C,"-").concat(js(He+C+nt[C]));return P?"".concat(P,"-").concat(k):k}(t.displayName,t.parentComponentId):l,u=t.displayName,d=u===void 0?function(N){return rt(N)?"styled.".concat(N):"Styled(".concat(Ns(N),")")}(e):u,m=t.displayName&&t.componentId?"".concat(Dt(t.displayName),"-").concat(t.componentId):t.componentId||h,p=n&&a.attrs?a.attrs.concat(c).filter(Boolean):c,b=t.shouldForwardProp;if(n&&a.shouldForwardProp){var v=a.shouldForwardProp;if(t.shouldForwardProp){var g=t.shouldForwardProp;b=function(N,P){return v(N,P)&&g(N,P)}}else b=v}var x=new Xs(r,m,n?a.componentStyle:void 0);function y(N,P){return function(C,k,Q){var je=C.attrs,Mr=C.componentStyle,$r=C.defaultProps,Tr=C.foldedComponentIds,Br=C.styledComponentId,Dr=C.target,Wr=Z.useContext(Or),zr=Gt(),Ye=C.shouldForwardProp||zr.shouldForwardProp,Nt=vs(k,Wr,$r)||ae,D=function(ke,ue,Se){for(var he,J=O(O({},ue),{className:void 0,theme:Se}),Xe=0;Xe{const[e,t]=f.useState(!1),r=()=>t(!e),n="text-gray-700 hover:text-green-600 transition-colors px-4 py-2 text-base font-medium",a=s.jsxs(s.Fragment,{children:[s.jsx("a",{href:"#features",className:n,children:"Features"}),s.jsx("a",{href:"#about",className:n,children:"About"}),s.jsx($,{to:"/login",className:n,children:"Login"}),s.jsx($,{to:"/register",className:"ml-2 bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 transition-colors text-base font-medium",children:"Get Started"})]});return s.jsxs("nav",{className:"fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-sm border-b border-gray-200",children:[s.jsx("div",{className:"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8",children:s.jsxs("div",{className:"flex justify-between items-center h-16",children:[s.jsx($,{to:"/",className:"text-2xl font-extrabold text-green-600 tracking-tight",children:"Nanwa"}),s.jsx("div",{className:"hidden md:flex items-center gap-4",children:a}),s.jsx("button",{"aria-label":"Toggle menu",className:"md:hidden text-gray-700 hover:text-green-600 focus:outline-none",onClick:r,children:e?s.jsx("svg",{className:"h-6 w-6",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:s.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M6 18L18 6M6 6l12 12"})}):s.jsx("svg",{className:"h-6 w-6",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:s.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M4 6h16M4 12h16M4 18h16"})})})]})}),e&&s.jsx("div",{className:"md:hidden border-t border-gray-200 bg-white px-4 pb-4 space-y-1",children:a})]})},ta=_.section`
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
+ min-height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+ overflow: hidden;
+`,ra=_.div`
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-image: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.1'%3E%3Ccircle cx='30' cy='30' r='2'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
+ opacity: 0.3;
+`,na=()=>s.jsxs("div",{className:"min-h-screen bg-white",children:[s.jsx(ea,{}),s.jsxs(ta,{className:"pt-20",children:[s.jsx(ra,{}),s.jsx("div",{className:"relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-24",children:s.jsxs("div",{className:"text-center",children:[s.jsxs("h1",{className:"text-4xl md:text-6xl font-bold text-white mb-6",children:["Discover, Monitor & Export",s.jsx("span",{className:"block text-green-200",children:"Real-Time Tree Insights"})]}),s.jsx("p",{className:"text-xl md:text-2xl text-green-100 mb-8 max-w-3xl mx-auto",children:"Nanwa provides investors, growers, and admins with comprehensive tools to track every tree in our registry with real-time data and actionable insights."}),s.jsxs("div",{className:"flex flex-col sm:flex-row gap-4 justify-center",children:[s.jsx($,{to:"/register",className:"bg-white text-green-600 px-8 py-4 rounded-lg text-lg font-semibold hover:bg-gray-100 transition-colors",children:"Start Your Free Trial"}),s.jsx("a",{href:"#features",className:"border-2 border-white text-white px-8 py-4 rounded-lg text-lg font-semibold hover:bg-white hover:text-green-600 transition-colors",children:"Learn More"})]})]})})]}),s.jsx("section",{id:"features",className:"py-20 bg-gray-50",children:s.jsxs("div",{className:"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8",children:[s.jsxs("div",{className:"text-center mb-16",children:[s.jsx("h2",{className:"text-3xl md:text-4xl font-bold text-gray-900 mb-4",children:"Powerful Features for Tree Management"}),s.jsx("p",{className:"text-xl text-gray-600 max-w-2xl mx-auto",children:"Everything you need to monitor, analyze, and export tree data with precision and ease."})]}),s.jsxs("div",{className:"grid md:grid-cols-3 gap-8",children:[s.jsxs("div",{className:"bg-white p-8 rounded-xl shadow-lg",children:[s.jsx("div",{className:"w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center mb-6",children:s.jsx("svg",{className:"w-6 h-6 text-green-600",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:s.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"})})}),s.jsx("h3",{className:"text-xl font-semibold text-gray-900 mb-4",children:"Real-Time Analytics"}),s.jsx("p",{className:"text-gray-600",children:"Monitor survival rates, average height, and CO₂ absorption with live-updating charts and metrics."})]}),s.jsxs("div",{className:"bg-white p-8 rounded-xl shadow-lg",children:[s.jsx("div",{className:"w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center mb-6",children:s.jsxs("svg",{className:"w-6 h-6 text-green-600",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:[s.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"}),s.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M15 11a3 3 0 11-6 0 3 3 0 016 0z"})]})}),s.jsx("h3",{className:"text-xl font-semibold text-gray-900 mb-4",children:"Interactive Mapping"}),s.jsx("p",{className:"text-gray-600",children:"Visualize forests and individual trees with marker clustering and detailed tree information."})]}),s.jsxs("div",{className:"bg-white p-8 rounded-xl shadow-lg",children:[s.jsx("div",{className:"w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center mb-6",children:s.jsx("svg",{className:"w-6 h-6 text-green-600",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:s.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"})})}),s.jsx("h3",{className:"text-xl font-semibold text-gray-900 mb-4",children:"Data Export"}),s.jsx("p",{className:"text-gray-600",children:"Export filtered datasets to CSV or XLSX format for reporting and analysis."})]})]})]})}),s.jsx("section",{id:"about",className:"py-20 bg-white",children:s.jsx("div",{className:"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8",children:s.jsxs("div",{className:"grid md:grid-cols-2 gap-12 items-center",children:[s.jsxs("div",{children:[s.jsx("h2",{className:"text-3xl md:text-4xl font-bold text-gray-900 mb-6",children:"Empowering Sustainable Forestry"}),s.jsx("p",{className:"text-lg text-gray-600 mb-6",children:"Nanwa is dedicated to providing comprehensive tree monitoring solutions that help investors, growers, and environmental analysts make data-driven decisions for sustainable forestry projects."}),s.jsx("p",{className:"text-lg text-gray-600 mb-8",children:"Our platform offers real-time insights, advanced analytics, and seamless data export capabilities to support your forestry management needs."}),s.jsx($,{to:"/register",className:"bg-green-600 text-white px-6 py-3 rounded-lg text-lg font-semibold hover:bg-green-700 transition-colors inline-block",children:"Join Nanwa Today"})]}),s.jsx("div",{className:"bg-green-50 p-8 rounded-xl",children:s.jsxs("div",{className:"grid grid-cols-2 gap-4",children:[s.jsxs("div",{className:"text-center",children:[s.jsx("div",{className:"text-3xl font-bold text-green-600 mb-2",children:"10K+"}),s.jsx("div",{className:"text-gray-600",children:"Trees Monitored"})]}),s.jsxs("div",{className:"text-center",children:[s.jsx("div",{className:"text-3xl font-bold text-green-600 mb-2",children:"95%"}),s.jsx("div",{className:"text-gray-600",children:"Survival Rate"})]}),s.jsxs("div",{className:"text-center",children:[s.jsx("div",{className:"text-3xl font-bold text-green-600 mb-2",children:"50+"}),s.jsx("div",{className:"text-gray-600",children:"Forest Projects"})]}),s.jsxs("div",{className:"text-center",children:[s.jsx("div",{className:"text-3xl font-bold text-green-600 mb-2",children:"24/7"}),s.jsx("div",{className:"text-gray-600",children:"Real-Time Data"})]})]})})]})})}),s.jsx("footer",{className:"bg-gray-900 text-white py-12",children:s.jsxs("div",{className:"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8",children:[s.jsxs("div",{className:"grid md:grid-cols-4 gap-8",children:[s.jsxs("div",{children:[s.jsx("h3",{className:"text-2xl font-bold text-green-400 mb-4",children:"Nanwa"}),s.jsx("p",{className:"text-gray-400",children:"Empowering sustainable forestry through data-driven insights and real-time monitoring."})]}),s.jsxs("div",{children:[s.jsx("h4",{className:"text-lg font-semibold mb-4",children:"Product"}),s.jsxs("ul",{className:"space-y-2 text-gray-400",children:[s.jsx("li",{children:s.jsx("a",{href:"#features",className:"hover:text-white transition-colors",children:"Features"})}),s.jsx("li",{children:s.jsx($,{to:"/login",className:"hover:text-white transition-colors",children:"Login"})}),s.jsx("li",{children:s.jsx($,{to:"/register",className:"hover:text-white transition-colors",children:"Register"})})]})]}),s.jsxs("div",{children:[s.jsx("h4",{className:"text-lg font-semibold mb-4",children:"Company"}),s.jsxs("ul",{className:"space-y-2 text-gray-400",children:[s.jsx("li",{children:s.jsx("a",{href:"#about",className:"hover:text-white transition-colors",children:"About"})}),s.jsx("li",{children:s.jsx("a",{href:"#",className:"hover:text-white transition-colors",children:"Contact"})}),s.jsx("li",{children:s.jsx("a",{href:"#",className:"hover:text-white transition-colors",children:"Privacy"})})]})]}),s.jsxs("div",{children:[s.jsx("h4",{className:"text-lg font-semibold mb-4",children:"Support"}),s.jsxs("ul",{className:"space-y-2 text-gray-400",children:[s.jsx("li",{children:s.jsx("a",{href:"#",className:"hover:text-white transition-colors",children:"Help Center"})}),s.jsx("li",{children:s.jsx("a",{href:"#",className:"hover:text-white transition-colors",children:"Documentation"})}),s.jsx("li",{children:s.jsx("a",{href:"#",className:"hover:text-white transition-colors",children:"API"})})]})]})]}),s.jsx("div",{className:"border-t border-gray-800 mt-8 pt-8 text-center text-gray-400",children:s.jsx("p",{children:"© 2024 Nanwa. All rights reserved."})})]})})]}),_r=f.createContext(),Je=()=>{const e=f.useContext(_r);if(!e)throw new Error("useAuth must be used within an AuthProvider");return e},sa=({children:e})=>{const[t,r]=f.useState(null),[n,a]=f.useState(!0),o=ze();f.useEffect(()=>{const d=localStorage.getItem("authToken"),m=localStorage.getItem("userData");if(d&&m)try{r(JSON.parse(m))}catch(p){console.error("Error parsing user data:",p),localStorage.removeItem("authToken"),localStorage.removeItem("userData")}a(!1)},[]);const u={user:t,loading:n,login:async(d,m)=>{try{a(!0);const p=await aa(d,m),{token:b,user:v}=p;return localStorage.setItem("authToken",b),localStorage.setItem("userData",JSON.stringify(v)),r(v),o("/dashboard"),{success:!0}}catch(p){return{success:!1,error:p.message}}finally{a(!1)}},register:async(d,m,p)=>{try{if(a(!0),m!==p)throw new Error("Passwords do not match");const b=await oa(d,m),{token:v,user:g}=b;return localStorage.setItem("authToken",v),localStorage.setItem("userData",JSON.stringify(g)),r(g),o("/dashboard"),{success:!0}}catch(b){return{success:!1,error:b.message}}finally{a(!1)}},logout:()=>{localStorage.removeItem("authToken"),localStorage.removeItem("userData"),r(null),o("/")},isAdmin:()=>(t==null?void 0:t.role)==="admin"};return s.jsx(_r.Provider,{value:u,children:e})},aa=async(e,t)=>{if(await new Promise(r=>setTimeout(r,1e3)),!e||!t)throw new Error("Email and password are required");if(e==="admin@nanwa.com"&&t==="admin123")return{token:"mock-jwt-token-admin",user:{id:1,email:"admin@nanwa.com",role:"admin",name:"Admin User"}};if(e==="user@nanwa.com"&&t==="user123")return{token:"mock-jwt-token-user",user:{id:2,email:"user@nanwa.com",role:"user",name:"Regular User"}};throw new Error("Invalid email or password")},oa=async(e,t)=>{if(await new Promise(r=>setTimeout(r,1e3)),!e||!t)throw new Error("Email and password are required");if(t.length<6)throw new Error("Password must be at least 6 characters long");return{token:"mock-jwt-token-new-user",user:{id:Math.floor(Math.random()*1e3),email:e,role:"user",name:e.split("@")[0]}}},ia=_.div`
+ min-height: 100vh;
+ background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 1rem;
+`,la=_.div`
+ background: white;
+ border-radius: 1rem;
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+ padding: 2rem;
+ width: 100%;
+ max-width: 400px;
+`,ca=()=>{const[e,t]=f.useState({email:"",password:""}),[r,n]=f.useState({}),[a,o]=f.useState(!1),[i,c]=f.useState(!1),{login:l}=Je();ze();const h=()=>{const m={};return e.email?/\S+@\S+\.\S+/.test(e.email)||(m.email="Email is invalid"):m.email="Email is required",e.password?e.password.length<6&&(m.password="Password must be at least 6 characters"):m.password="Password is required",n(m),Object.keys(m).length===0},u=m=>{const{name:p,value:b}=m.target;t(v=>({...v,[p]:b})),r[p]&&n(v=>({...v,[p]:""}))},d=async m=>{if(m.preventDefault(),!!h()){o(!0);try{const p=await l(e.email,e.password);p.success||n({general:p.error})}catch{n({general:"An unexpected error occurred. Please try again."})}finally{o(!1)}}};return s.jsx(ia,{children:s.jsxs(la,{children:[s.jsxs("div",{className:"text-center mb-8",children:[s.jsx($,{to:"/",className:"inline-block mb-6",children:s.jsx("h1",{className:"text-3xl font-bold text-green-600",children:"Nanwa"})}),s.jsx("h2",{className:"text-2xl font-bold text-gray-900 mb-2",children:"Welcome back"}),s.jsx("p",{className:"text-gray-600",children:"Sign in to your account to continue"})]}),s.jsxs("form",{onSubmit:d,className:"space-y-6",children:[r.general&&s.jsx("div",{className:"bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg",children:r.general}),s.jsxs("div",{children:[s.jsx("label",{htmlFor:"email",className:"block text-sm font-medium text-gray-700 mb-2",children:"Email address"}),s.jsx("input",{type:"email",id:"email",name:"email",value:e.email,onChange:u,className:`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent ${r.email?"border-red-300":"border-gray-300"}`,placeholder:"Enter your email",disabled:a,"aria-describedby":r.email?"email-error":void 0}),r.email&&s.jsx("p",{id:"email-error",className:"mt-1 text-sm text-red-600",children:r.email})]}),s.jsxs("div",{children:[s.jsx("label",{htmlFor:"password",className:"block text-sm font-medium text-gray-700 mb-2",children:"Password"}),s.jsxs("div",{className:"relative",children:[s.jsx("input",{type:i?"text":"password",id:"password",name:"password",value:e.password,onChange:u,className:`w-full px-3 py-2 pr-10 border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent ${r.password?"border-red-300":"border-gray-300"}`,placeholder:"Enter your password",disabled:a,"aria-describedby":r.password?"password-error":void 0}),s.jsx("button",{type:"button",onClick:()=>c(!i),className:"absolute inset-y-0 right-0 pr-3 flex items-center",disabled:a,children:i?s.jsx("svg",{className:"h-5 w-5 text-gray-400",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:s.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21"})}):s.jsxs("svg",{className:"h-5 w-5 text-gray-400",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:[s.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M15 12a3 3 0 11-6 0 3 3 0 016 0z"}),s.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"})]})})]}),r.password&&s.jsx("p",{id:"password-error",className:"mt-1 text-sm text-red-600",children:r.password})]}),s.jsxs("div",{className:"flex items-center justify-between",children:[s.jsxs("div",{className:"flex items-center",children:[s.jsx("input",{id:"remember-me",name:"remember-me",type:"checkbox",className:"h-4 w-4 text-green-600 focus:ring-green-500 border-gray-300 rounded"}),s.jsx("label",{htmlFor:"remember-me",className:"ml-2 block text-sm text-gray-900",children:"Remember me"})]}),s.jsx("div",{className:"text-sm",children:s.jsx("a",{href:"#",className:"font-medium text-green-600 hover:text-green-500",children:"Forgot your password?"})})]}),s.jsx("button",{type:"submit",disabled:a,className:"w-full flex justify-center py-2 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled:opacity-50 disabled:cursor-not-allowed",children:a?s.jsxs("div",{className:"flex items-center",children:[s.jsxs("svg",{className:"animate-spin -ml-1 mr-3 h-5 w-5 text-white",xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",children:[s.jsx("circle",{className:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"currentColor",strokeWidth:"4"}),s.jsx("path",{className:"opacity-75",fill:"currentColor",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"})]}),"Signing in..."]}):"Sign in"})]}),s.jsx("div",{className:"mt-6 text-center",children:s.jsxs("p",{className:"text-sm text-gray-600",children:["Don't have an account?"," ",s.jsx($,{to:"/register",className:"font-medium text-green-600 hover:text-green-500",children:"Sign up"})]})}),s.jsxs("div",{className:"mt-8 p-4 bg-gray-50 rounded-lg",children:[s.jsx("p",{className:"text-sm text-gray-600 mb-2",children:"Demo credentials:"}),s.jsxs("div",{className:"text-xs text-gray-500 space-y-1",children:[s.jsxs("p",{children:[s.jsx("strong",{children:"Admin:"})," admin@nanwa.com / admin123"]}),s.jsxs("p",{children:[s.jsx("strong",{children:"User:"})," user@nanwa.com / user123"]})]})]})]})})},da=_.div`
+ min-height: 100vh;
+ background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 1rem;
+`,ua=_.div`
+ background: white;
+ border-radius: 1rem;
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+ padding: 2rem;
+ width: 100%;
+ max-width: 450px;
+`,ha=_.div`
+ width: 100%;
+ height: 4px;
+ background-color: #e5e7eb;
+ border-radius: 2px;
+ overflow: hidden;
+ margin-top: 0.5rem;
+`,ma=_.div`
+ height: 100%;
+ background-color: ${e=>e.strength==="weak"?"#ef4444":e.strength==="medium"?"#f59e0b":e.strength==="strong"?"#10b981":"#e5e7eb"};
+ width: ${e=>e.strength==="weak"?"33%":e.strength==="medium"?"66%":e.strength==="strong"?"100%":"0%"};
+ transition: all 0.3s ease;
+`,fa=()=>{const[e,t]=f.useState({email:"",password:"",confirmPassword:""}),[r,n]=f.useState({}),[a,o]=f.useState(!1),[i,c]=f.useState(!1),[l,h]=f.useState(!1),{register:u}=Je(),m=(x=>{if(!x)return{strength:"none",score:0};let y=0;return x.length>=8&&(y+=1),/[a-z]/.test(x)&&(y+=1),/[A-Z]/.test(x)&&(y+=1),/[0-9]/.test(x)&&(y+=1),/[^A-Za-z0-9]/.test(x)&&(y+=1),y<=2?{strength:"weak",score:y}:y<=3?{strength:"medium",score:y}:{strength:"strong",score:y}})(e.password),p=()=>{const x={};return e.email?/\S+@\S+\.\S+/.test(e.email)||(x.email="Email is invalid"):x.email="Email is required",e.password?e.password.length<6?x.password="Password must be at least 6 characters":m.strength==="weak"&&(x.password="Password is too weak. Include uppercase, lowercase, numbers, and special characters."):x.password="Password is required",e.confirmPassword?e.password!==e.confirmPassword&&(x.confirmPassword="Passwords do not match"):x.confirmPassword="Please confirm your password",n(x),Object.keys(x).length===0},b=x=>{const{name:y,value:w}=x.target;t(N=>({...N,[y]:w})),r[y]&&n(N=>({...N,[y]:""}))},v=async x=>{if(x.preventDefault(),!!p()){o(!0);try{const y=await u(e.email,e.password,e.confirmPassword);y.success||n({general:y.error})}catch{n({general:"An unexpected error occurred. Please try again."})}finally{o(!1)}}},g=()=>{switch(m.strength){case"weak":return"Weak password";case"medium":return"Medium strength password";case"strong":return"Strong password";default:return""}};return s.jsx(da,{children:s.jsxs(ua,{children:[s.jsxs("div",{className:"text-center mb-8",children:[s.jsx($,{to:"/",className:"inline-block mb-6",children:s.jsx("h1",{className:"text-3xl font-bold text-green-600",children:"Nanwa"})}),s.jsx("h2",{className:"text-2xl font-bold text-gray-900 mb-2",children:"Create your account"}),s.jsx("p",{className:"text-gray-600",children:"Join Nanwa to start monitoring your trees"})]}),s.jsxs("form",{onSubmit:v,className:"space-y-6",children:[r.general&&s.jsx("div",{className:"bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg",children:r.general}),s.jsxs("div",{children:[s.jsx("label",{htmlFor:"email",className:"block text-sm font-medium text-gray-700 mb-2",children:"Email address"}),s.jsx("input",{type:"email",id:"email",name:"email",value:e.email,onChange:b,className:`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent ${r.email?"border-red-300":"border-gray-300"}`,placeholder:"Enter your email",disabled:a,"aria-describedby":r.email?"email-error":void 0}),r.email&&s.jsx("p",{id:"email-error",className:"mt-1 text-sm text-red-600",children:r.email})]}),s.jsxs("div",{children:[s.jsx("label",{htmlFor:"password",className:"block text-sm font-medium text-gray-700 mb-2",children:"Password"}),s.jsxs("div",{className:"relative",children:[s.jsx("input",{type:i?"text":"password",id:"password",name:"password",value:e.password,onChange:b,className:`w-full px-3 py-2 pr-10 border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent ${r.password?"border-red-300":"border-gray-300"}`,placeholder:"Create a strong password",disabled:a,"aria-describedby":r.password?"password-error":void 0}),s.jsx("button",{type:"button",onClick:()=>c(!i),className:"absolute inset-y-0 right-0 pr-3 flex items-center",disabled:a,children:i?s.jsx("svg",{className:"h-5 w-5 text-gray-400",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:s.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21"})}):s.jsxs("svg",{className:"h-5 w-5 text-gray-400",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:[s.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M15 12a3 3 0 11-6 0 3 3 0 016 0z"}),s.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"})]})})]}),e.password&&s.jsxs("div",{className:"mt-2",children:[s.jsx(ha,{children:s.jsx(ma,{strength:m.strength})}),s.jsx("p",{className:`text-xs mt-1 ${m.strength==="weak"?"text-red-600":m.strength==="medium"?"text-yellow-600":m.strength==="strong"?"text-green-600":"text-gray-500"}`,children:g()})]}),r.password&&s.jsx("p",{id:"password-error",className:"mt-1 text-sm text-red-600",children:r.password})]}),s.jsxs("div",{children:[s.jsx("label",{htmlFor:"confirmPassword",className:"block text-sm font-medium text-gray-700 mb-2",children:"Confirm password"}),s.jsxs("div",{className:"relative",children:[s.jsx("input",{type:l?"text":"password",id:"confirmPassword",name:"confirmPassword",value:e.confirmPassword,onChange:b,className:`w-full px-3 py-2 pr-10 border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent ${r.confirmPassword?"border-red-300":"border-gray-300"}`,placeholder:"Confirm your password",disabled:a,"aria-describedby":r.confirmPassword?"confirm-password-error":void 0}),s.jsx("button",{type:"button",onClick:()=>h(!l),className:"absolute inset-y-0 right-0 pr-3 flex items-center",disabled:a,children:l?s.jsx("svg",{className:"h-5 w-5 text-gray-400",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:s.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21"})}):s.jsxs("svg",{className:"h-5 w-5 text-gray-400",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:[s.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M15 12a3 3 0 11-6 0 3 3 0 016 0z"}),s.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"})]})})]}),r.confirmPassword&&s.jsx("p",{id:"confirm-password-error",className:"mt-1 text-sm text-red-600",children:r.confirmPassword})]}),s.jsxs("div",{className:"flex items-center",children:[s.jsx("input",{id:"terms",name:"terms",type:"checkbox",required:!0,className:"h-4 w-4 text-green-600 focus:ring-green-500 border-gray-300 rounded"}),s.jsxs("label",{htmlFor:"terms",className:"ml-2 block text-sm text-gray-900",children:["I agree to the"," ",s.jsx("a",{href:"#",className:"font-medium text-green-600 hover:text-green-500",children:"Terms of Service"})," ","and"," ",s.jsx("a",{href:"#",className:"font-medium text-green-600 hover:text-green-500",children:"Privacy Policy"})]})]}),s.jsx("button",{type:"submit",disabled:a,className:"w-full flex justify-center py-2 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled:opacity-50 disabled:cursor-not-allowed",children:a?s.jsxs("div",{className:"flex items-center",children:[s.jsxs("svg",{className:"animate-spin -ml-1 mr-3 h-5 w-5 text-white",xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",children:[s.jsx("circle",{className:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"currentColor",strokeWidth:"4"}),s.jsx("path",{className:"opacity-75",fill:"currentColor",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"})]}),"Creating account..."]}):"Create account"})]}),s.jsx("div",{className:"mt-6 text-center",children:s.jsxs("p",{className:"text-sm text-gray-600",children:["Already have an account?"," ",s.jsx($,{to:"/login",className:"font-medium text-green-600 hover:text-green-500",children:"Sign in"})]})})]})})},pa=_.div`
+ min-height: 100vh;
+ background-color: #f9fafb;
+`,ga=_.header`
+ background: white;
+ border-bottom: 1px solid #e5e7eb;
+ padding: 1rem 0;
+`,xa=_.aside`
+ background: white;
+ border-right: 1px solid #e5e7eb;
+ width: 250px;
+ position: fixed;
+ top: 0;
+ left: 0;
+ height: 100vh;
+ overflow-y: auto;
+ z-index: 10;
+`,va=_.main`
+ margin-left: 250px;
+ padding: 2rem;
+`,ya=()=>{const{user:e,logout:t,isAdmin:r}=Je(),n=()=>{t()};return s.jsxs(pa,{children:[s.jsx(ga,{children:s.jsx("div",{className:"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8",children:s.jsxs("div",{className:"flex justify-between items-center",children:[s.jsx("div",{className:"flex items-center",children:s.jsx("h1",{className:"text-2xl font-bold text-green-600",children:"Nanwa Dashboard"})}),s.jsxs("div",{className:"flex items-center space-x-4",children:[s.jsxs("div",{className:"text-sm text-gray-700",children:["Welcome, ",s.jsx("span",{className:"font-medium",children:e==null?void 0:e.name}),r()&&s.jsx("span",{className:"ml-2 px-2 py-1 bg-red-100 text-red-800 text-xs rounded-full",children:"Admin"})]}),s.jsx("button",{onClick:n,className:"text-gray-700 hover:text-green-600 transition-colors",children:s.jsx("svg",{className:"h-5 w-5",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:s.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"})})})]})]})})}),s.jsx(xa,{children:s.jsx("div",{className:"p-6",children:s.jsxs("nav",{className:"space-y-2",children:[s.jsxs("a",{href:"#dashboard",className:"flex items-center px-3 py-2 text-sm font-medium text-green-600 bg-green-50 rounded-lg",children:[s.jsxs("svg",{className:"mr-3 h-5 w-5",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:[s.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z"}),s.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M8 5a2 2 0 012-2h4a2 2 0 012 2v6H8V5z"})]}),"Dashboard"]}),s.jsxs("a",{href:"#map",className:"flex items-center px-3 py-2 text-sm font-medium text-gray-700 hover:text-green-600 hover:bg-green-50 rounded-lg transition-colors",children:[s.jsxs("svg",{className:"mr-3 h-5 w-5",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:[s.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"}),s.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M15 11a3 3 0 11-6 0 3 3 0 016 0z"})]}),"Map View"]}),s.jsxs("a",{href:"#export",className:"flex items-center px-3 py-2 text-sm font-medium text-gray-700 hover:text-green-600 hover:bg-green-50 rounded-lg transition-colors",children:[s.jsx("svg",{className:"mr-3 h-5 w-5",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:s.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"})}),"Data Export"]}),r()&&s.jsxs("a",{href:"#audit",className:"flex items-center px-3 py-2 text-sm font-medium text-gray-700 hover:text-green-600 hover:bg-green-50 rounded-lg transition-colors",children:[s.jsx("svg",{className:"mr-3 h-5 w-5",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:s.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"})}),"Audit Log"]})]})})}),s.jsx(va,{children:s.jsxs("div",{className:"max-w-7xl mx-auto",children:[s.jsxs("div",{className:"mb-8",children:[s.jsx("h2",{className:"text-3xl font-bold text-gray-900 mb-2",children:"Welcome to Nanwa"}),s.jsx("p",{className:"text-gray-600",children:"Monitor your forests and track tree growth with real-time insights."})]}),s.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8",children:[s.jsx("div",{className:"bg-white p-6 rounded-lg shadow-sm border border-gray-200",children:s.jsxs("div",{className:"flex items-center",children:[s.jsx("div",{className:"p-2 bg-green-100 rounded-lg",children:s.jsx("svg",{className:"h-6 w-6 text-green-600",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:s.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"})})}),s.jsxs("div",{className:"ml-4",children:[s.jsx("p",{className:"text-sm font-medium text-gray-600",children:"Total Trees"}),s.jsx("p",{className:"text-2xl font-bold text-gray-900",children:"10,247"})]})]})}),s.jsx("div",{className:"bg-white p-6 rounded-lg shadow-sm border border-gray-200",children:s.jsxs("div",{className:"flex items-center",children:[s.jsx("div",{className:"p-2 bg-blue-100 rounded-lg",children:s.jsx("svg",{className:"h-6 w-6 text-blue-600",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:s.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"})})}),s.jsxs("div",{className:"ml-4",children:[s.jsx("p",{className:"text-sm font-medium text-gray-600",children:"Survival Rate"}),s.jsx("p",{className:"text-2xl font-bold text-gray-900",children:"95.2%"})]})]})}),s.jsx("div",{className:"bg-white p-6 rounded-lg shadow-sm border border-gray-200",children:s.jsxs("div",{className:"flex items-center",children:[s.jsx("div",{className:"p-2 bg-yellow-100 rounded-lg",children:s.jsx("svg",{className:"h-6 w-6 text-yellow-600",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:s.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z"})})}),s.jsxs("div",{className:"ml-4",children:[s.jsx("p",{className:"text-sm font-medium text-gray-600",children:"Avg Height"}),s.jsx("p",{className:"text-2xl font-bold text-gray-900",children:"2.4m"})]})]})}),s.jsx("div",{className:"bg-white p-6 rounded-lg shadow-sm border border-gray-200",children:s.jsxs("div",{className:"flex items-center",children:[s.jsx("div",{className:"p-2 bg-purple-100 rounded-lg",children:s.jsx("svg",{className:"h-6 w-6 text-purple-600",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:s.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M13 10V3L4 14h7v7l9-11h-7z"})})}),s.jsxs("div",{className:"ml-4",children:[s.jsx("p",{className:"text-sm font-medium text-gray-600",children:"CO₂ Absorbed"}),s.jsx("p",{className:"text-2xl font-bold text-gray-900",children:"1.2t"})]})]})})]}),s.jsxs("div",{className:"bg-white p-8 rounded-lg shadow-sm border border-gray-200",children:[s.jsx("h3",{className:"text-xl font-semibold text-gray-900 mb-4",children:"Coming Soon"}),s.jsx("p",{className:"text-gray-600 mb-4",children:"The dashboard is currently being developed. Soon you'll be able to:"}),s.jsxs("ul",{className:"space-y-2 text-gray-600",children:[s.jsxs("li",{className:"flex items-center",children:[s.jsx("svg",{className:"h-4 w-4 text-green-500 mr-2",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:s.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M5 13l4 4L19 7"})}),"View real-time charts and analytics"]}),s.jsxs("li",{className:"flex items-center",children:[s.jsx("svg",{className:"h-4 w-4 text-green-500 mr-2",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:s.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M5 13l4 4L19 7"})}),"Explore interactive maps with tree locations"]}),s.jsxs("li",{className:"flex items-center",children:[s.jsx("svg",{className:"h-4 w-4 text-green-500 mr-2",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:s.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M5 13l4 4L19 7"})}),"Export data in CSV or XLSX format"]}),s.jsxs("li",{className:"flex items-center",children:[s.jsx("svg",{className:"h-4 w-4 text-green-500 mr-2",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:s.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M5 13l4 4L19 7"})}),"Filter and search through tree data"]})]})]})]})})]})},ba=({children:e})=>{const{user:t,loading:r}=Je();return r?s.jsx("div",{className:"min-h-screen flex items-center justify-center",children:s.jsx("div",{className:"animate-spin rounded-full h-12 w-12 border-b-2 border-green-600"})}):t?e:s.jsx(Dn,{to:"/login",replace:!0})},wa=()=>s.jsx(Jn,{children:s.jsx(sa,{children:s.jsxs(zn,{children:[s.jsx(fe,{path:"/",element:s.jsx(na,{})}),s.jsx(fe,{path:"/login",element:s.jsx(ca,{})}),s.jsx(fe,{path:"/register",element:s.jsx(fa,{})}),s.jsx(fe,{path:"/dashboard",element:s.jsx(ba,{children:s.jsx(ya,{})})})]})})});Jr.createRoot(document.getElementById("root")).render(s.jsx(Z.StrictMode,{children:s.jsx(wa,{})}));
diff --git a/frontend/dist/assets/vendor-BtP0CW_r.js b/frontend/dist/assets/vendor-BtP0CW_r.js
new file mode 100644
index 0000000000..257971db96
--- /dev/null
+++ b/frontend/dist/assets/vendor-BtP0CW_r.js
@@ -0,0 +1,32 @@
+function Pc(R){return R&&R.__esModule&&Object.prototype.hasOwnProperty.call(R,"default")?R.default:R}var wi={exports:{}},D={};/**
+ * @license React
+ * react.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */var ga;function kc(){if(ga)return D;ga=1;var R=Symbol.for("react.element"),b=Symbol.for("react.portal"),m=Symbol.for("react.fragment"),dt=Symbol.for("react.strict_mode"),Fe=Symbol.for("react.profiler"),Ke=Symbol.for("react.provider"),Ye=Symbol.for("react.context"),Te=Symbol.for("react.forward_ref"),de=Symbol.for("react.suspense"),je=Symbol.for("react.memo"),ln=Symbol.for("react.lazy"),ie=Symbol.iterator;function ne(f){return f===null||typeof f!="object"?null:(f=ie&&f[ie]||f["@@iterator"],typeof f=="function"?f:null)}var pt={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},Ue=Object.assign,K={};function B(f,h,M){this.props=f,this.context=h,this.refs=K,this.updater=M||pt}B.prototype.isReactComponent={},B.prototype.setState=function(f,h){if(typeof f!="object"&&typeof f!="function"&&f!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,f,h,"setState")},B.prototype.forceUpdate=function(f){this.updater.enqueueForceUpdate(this,f,"forceUpdate")};function mt(){}mt.prototype=B.prototype;function lt(f,h,M){this.props=f,this.context=h,this.refs=K,this.updater=M||pt}var Xe=lt.prototype=new mt;Xe.constructor=lt,Ue(Xe,B.prototype),Xe.isPureReactComponent=!0;var pe=Array.isArray,Ge=Object.prototype.hasOwnProperty,ge={current:null},ke={key:!0,ref:!0,__self:!0,__source:!0};function Ve(f,h,M){var O,F={},j=null,H=null;if(h!=null)for(O in h.ref!==void 0&&(H=h.ref),h.key!==void 0&&(j=""+h.key),h)Ge.call(h,O)&&!ke.hasOwnProperty(O)&&(F[O]=h[O]);var V=arguments.length-2;if(V===1)F.children=M;else if(1>>1,h=S[f];if(0>>1;fFe(F,C))jFe(H,F)?(S[f]=H,S[j]=C,f=j):(S[f]=F,S[O]=C,f=O);else if(jFe(H,C))S[f]=H,S[j]=C,f=j;else break e}}return T}function Fe(S,T){var C=S.sortIndex-T.sortIndex;return C!==0?C:S.id-T.id}if(typeof performance=="object"&&typeof performance.now=="function"){var Ke=performance;R.unstable_now=function(){return Ke.now()}}else{var Ye=Date,Te=Ye.now();R.unstable_now=function(){return Ye.now()-Te}}var de=[],je=[],ln=1,ie=null,ne=3,pt=!1,Ue=!1,K=!1,B=typeof setTimeout=="function"?setTimeout:null,mt=typeof clearTimeout=="function"?clearTimeout:null,lt=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function Xe(S){for(var T=m(je);T!==null;){if(T.callback===null)dt(je);else if(T.startTime<=S)dt(je),T.sortIndex=T.expirationTime,b(de,T);else break;T=m(je)}}function pe(S){if(K=!1,Xe(S),!Ue)if(m(de)!==null)Ue=!0,Ee(Ge);else{var T=m(je);T!==null&&J(pe,T.startTime-S)}}function Ge(S,T){Ue=!1,K&&(K=!1,mt(Ve),Ve=-1),pt=!0;var C=ne;try{for(Xe(T),ie=m(de);ie!==null&&(!(ie.expirationTime>T)||S&&!$t());){var f=ie.callback;if(typeof f=="function"){ie.callback=null,ne=ie.priorityLevel;var h=f(ie.expirationTime<=T);T=R.unstable_now(),typeof h=="function"?ie.callback=h:ie===m(de)&&dt(de),Xe(T)}else dt(de);ie=m(de)}if(ie!==null)var M=!0;else{var O=m(je);O!==null&&J(pe,O.startTime-T),M=!1}return M}finally{ie=null,ne=C,pt=!1}}var ge=!1,ke=null,Ve=-1,_t=5,vt=-1;function $t(){return!(R.unstable_now()-vt<_t)}function ut(){if(ke!==null){var S=R.unstable_now();vt=S;var T=!0;try{T=ke(!0,S)}finally{T?Le():(ge=!1,ke=null)}}else ge=!1}var Le;if(typeof lt=="function")Le=function(){lt(ut)};else if(typeof MessageChannel<"u"){var Ze=new MessageChannel,it=Ze.port2;Ze.port1.onmessage=ut,Le=function(){it.postMessage(null)}}else Le=function(){B(ut,0)};function Ee(S){ke=S,ge||(ge=!0,Le())}function J(S,T){Ve=B(function(){S(R.unstable_now())},T)}R.unstable_IdlePriority=5,R.unstable_ImmediatePriority=1,R.unstable_LowPriority=4,R.unstable_NormalPriority=3,R.unstable_Profiling=null,R.unstable_UserBlockingPriority=2,R.unstable_cancelCallback=function(S){S.callback=null},R.unstable_continueExecution=function(){Ue||pt||(Ue=!0,Ee(Ge))},R.unstable_forceFrameRate=function(S){0>S||125f?(S.sortIndex=C,b(je,S),m(de)===null&&S===m(je)&&(K?(mt(Ve),Ve=-1):K=!0,J(pe,C-f))):(S.sortIndex=h,b(de,S),Ue||pt||(Ue=!0,Ee(Ge))),S},R.unstable_shouldYield=$t,R.unstable_wrapCallback=function(S){var T=ne;return function(){var C=ne;ne=T;try{return S.apply(this,arguments)}finally{ne=C}}}}(Ei)),Ei}var ka;function _c(){return ka||(ka=1,ki.exports=Cc()),ki.exports}/**
+ * @license React
+ * react-dom.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */var Ea;function xc(){if(Ea)return ze;Ea=1;var R=Ec(),b=_c();function m(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),de=Object.prototype.hasOwnProperty,je=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,ln={},ie={};function ne(e){return de.call(ie,e)?!0:de.call(ln,e)?!1:je.test(e)?ie[e]=!0:(ln[e]=!0,!1)}function pt(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function Ue(e,t,n,r){if(t===null||typeof t>"u"||pt(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function K(e,t,n,r,l,u,i){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=l,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=u,this.removeEmptyString=i}var B={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){B[e]=new K(e,0,!1,e,null,!1,!1)}),[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];B[t]=new K(t,1,!1,e[1],null,!1,!1)}),["contentEditable","draggable","spellCheck","value"].forEach(function(e){B[e]=new K(e,2,!1,e.toLowerCase(),null,!1,!1)}),["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){B[e]=new K(e,2,!1,e,null,!1,!1)}),"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){B[e]=new K(e,3,!1,e.toLowerCase(),null,!1,!1)}),["checked","multiple","muted","selected"].forEach(function(e){B[e]=new K(e,3,!0,e,null,!1,!1)}),["capture","download"].forEach(function(e){B[e]=new K(e,4,!1,e,null,!1,!1)}),["cols","rows","size","span"].forEach(function(e){B[e]=new K(e,6,!1,e,null,!1,!1)}),["rowSpan","start"].forEach(function(e){B[e]=new K(e,5,!1,e.toLowerCase(),null,!1,!1)});var mt=/[\-:]([a-z])/g;function lt(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(mt,lt);B[t]=new K(t,1,!1,e,null,!1,!1)}),"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(mt,lt);B[t]=new K(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)}),["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(mt,lt);B[t]=new K(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)}),["tabIndex","crossOrigin"].forEach(function(e){B[e]=new K(e,1,!1,e.toLowerCase(),null,!1,!1)}),B.xlinkHref=new K("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1),["src","href","action","formAction"].forEach(function(e){B[e]=new K(e,1,!1,e.toLowerCase(),null,!0,!0)});function Xe(e,t,n,r){var l=B.hasOwnProperty(t)?B[t]:null;(l!==null?l.type!==0:r||!(2o||l[i]!==u[o]){var s=`
+`+l[i].replace(" at new "," at ");return e.displayName&&s.includes("")&&(s=s.replace("",e.displayName)),s}while(1<=i&&0<=o);break}}}finally{M=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?h(e):""}function F(e){switch(e.tag){case 5:return h(e.type);case 16:return h("Lazy");case 13:return h("Suspense");case 19:return h("SuspenseList");case 0:case 2:case 15:return e=O(e.type,!1),e;case 11:return e=O(e.type.render,!1),e;case 1:return e=O(e.type,!0),e;default:return""}}function j(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case ke:return"Fragment";case ge:return"Portal";case _t:return"Profiler";case Ve:return"StrictMode";case Le:return"Suspense";case Ze:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case $t:return(e.displayName||"Context")+".Consumer";case vt:return(e._context.displayName||"Context")+".Provider";case ut:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case it:return t=e.displayName||null,t!==null?t:j(e.type)||"Memo";case Ee:t=e._payload,e=e._init;try{return j(e(t))}catch{}}return null}function H(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return j(t);case 8:return t===Ve?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function V(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function Y(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function Re(e){var t=Y(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var l=n.get,u=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return l.call(this)},set:function(i){r=""+i,u.call(this,i)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(i){r=""+i},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function hr(e){e._valueTracker||(e._valueTracker=Re(e))}function Ci(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=Y(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function yr(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function Pl(e,t){var n=t.checked;return C({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function _i(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=V(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function xi(e,t){t=t.checked,t!=null&&Xe(e,"checked",t,!1)}function Nl(e,t){xi(e,t);var n=V(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?zl(e,t.type,n):t.hasOwnProperty("defaultValue")&&zl(e,t.type,V(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function Pi(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function zl(e,t,n){(t!=="number"||yr(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var Rn=Array.isArray;function un(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l"+t.valueOf().toString()+"",t=gr.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function Mn(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var Dn={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},_a=["Webkit","ms","Moz","O"];Object.keys(Dn).forEach(function(e){_a.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Dn[t]=Dn[e]})});function Mi(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||Dn.hasOwnProperty(e)&&Dn[e]?(""+t).trim():t+"px"}function Di(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,l=Mi(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,l):e[n]=l}}var xa=C({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function Rl(e,t){if(t){if(xa[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(m(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(m(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(m(61))}if(t.style!=null&&typeof t.style!="object")throw Error(m(62))}}function Ml(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var Dl=null;function Ol(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var Il=null,on=null,sn=null;function Oi(e){if(e=tr(e)){if(typeof Il!="function")throw Error(m(280));var t=e.stateNode;t&&(t=Br(t),Il(e.stateNode,e.type,t))}}function Ii(e){on?sn?sn.push(e):sn=[e]:on=e}function Fi(){if(on){var e=on,t=sn;if(sn=on=null,Oi(e),t)for(e=0;e>>=0,e===0?32:31-(Fa(e)/ja|0)|0}var Cr=64,_r=4194304;function jn(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function xr(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,l=e.suspendedLanes,u=e.pingedLanes,i=n&268435455;if(i!==0){var o=i&~l;o!==0?r=jn(o):(u&=i,u!==0&&(r=jn(u)))}else i=n&~l,i!==0?r=jn(i):u!==0&&(r=jn(u));if(r===0)return 0;if(t!==0&&t!==r&&(t&l)===0&&(l=r&-r,u=t&-t,l>=u||l===16&&(u&4194240)!==0))return t;if((r&4)!==0&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function Un(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-Je(t),e[t]=n}function Ba(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=Kn),fo=" ",co=!1;function po(e,t){switch(e){case"keyup":return hf.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function mo(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var cn=!1;function gf(e,t){switch(e){case"compositionend":return mo(t);case"keypress":return t.which!==32?null:(co=!0,fo);case"textInput":return e=t.data,e===fo&&co?null:e;default:return null}}function wf(e,t){if(cn)return e==="compositionend"||!bl&&po(e,t)?(e=lo(),Lr=Yl=Tt=null,cn=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=ko(n)}}function Co(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Co(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function _o(){for(var e=window,t=yr();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=yr(e.document)}return t}function nu(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function zf(e){var t=_o(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&Co(n.ownerDocument.documentElement,n)){if(r!==null&&nu(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var l=n.textContent.length,u=Math.min(r.start,l);r=r.end===void 0?u:Math.min(r.end,l),!e.extend&&u>r&&(l=r,r=u,u=l),l=Eo(n,u);var i=Eo(n,r);l&&i&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==i.node||e.focusOffset!==i.offset)&&(t=t.createRange(),t.setStart(l.node,l.offset),e.removeAllRanges(),u>r?(e.addRange(t),e.extend(i.node,i.offset)):(t.setEnd(i.node,i.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,dn=null,ru=null,Zn=null,lu=!1;function xo(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;lu||dn==null||dn!==yr(r)||(r=dn,"selectionStart"in r&&nu(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Zn&&Gn(Zn,r)||(Zn=r,r=Ur(ru,"onSelect"),0yn||(e.current=hu[yn],hu[yn]=null,yn--)}function W(e,t){yn++,hu[yn]=e.current,e.current=t}var Dt={},me=Mt(Dt),Ce=Mt(!1),Yt=Dt;function gn(e,t){var n=e.type.contextTypes;if(!n)return Dt;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var l={},u;for(u in n)l[u]=t[u];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}function _e(e){return e=e.childContextTypes,e!=null}function Hr(){Q(Ce),Q(me)}function Ao(e,t,n){if(me.current!==Dt)throw Error(m(168));W(me,t),W(Ce,n)}function Bo(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var l in r)if(!(l in t))throw Error(m(108,H(e)||"Unknown",l));return C({},n,r)}function Wr(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Dt,Yt=me.current,W(me,e),W(Ce,Ce.current),!0}function Ho(e,t,n){var r=e.stateNode;if(!r)throw Error(m(169));n?(e=Bo(e,t,Yt),r.__reactInternalMemoizedMergedChildContext=e,Q(Ce),Q(me),W(me,e)):Q(Ce),W(Ce,n)}var yt=null,$r=!1,yu=!1;function Wo(e){yt===null?yt=[e]:yt.push(e)}function Af(e){$r=!0,Wo(e)}function Ot(){if(!yu&&yt!==null){yu=!0;var e=0,t=A;try{var n=yt;for(A=1;e>=i,l-=i,gt=1<<32-Je(t)+l|n<L?(ae=z,z=null):ae=z.sibling;var U=v(c,z,d[L],w);if(U===null){z===null&&(z=ae);break}e&&z&&U.alternate===null&&t(c,z),a=u(U,a,L),N===null?P=U:N.sibling=U,N=U,z=ae}if(L===d.length)return n(c,z),X&&Gt(c,L),P;if(z===null){for(;LL?(ae=z,z=null):ae=z.sibling;var Wt=v(c,z,U.value,w);if(Wt===null){z===null&&(z=ae);break}e&&z&&Wt.alternate===null&&t(c,z),a=u(Wt,a,L),N===null?P=Wt:N.sibling=Wt,N=Wt,z=ae}if(U.done)return n(c,z),X&&Gt(c,L),P;if(z===null){for(;!U.done;L++,U=d.next())U=g(c,U.value,w),U!==null&&(a=u(U,a,L),N===null?P=U:N.sibling=U,N=U);return X&&Gt(c,L),P}for(z=r(c,z);!U.done;L++,U=d.next())U=k(z,c,L,U.value,w),U!==null&&(e&&U.alternate!==null&&z.delete(U.key===null?L:U.key),a=u(U,a,L),N===null?P=U:N.sibling=U,N=U);return e&&z.forEach(function(Sc){return t(c,Sc)}),X&&Gt(c,L),P}function te(c,a,d,w){if(typeof d=="object"&&d!==null&&d.type===ke&&d.key===null&&(d=d.props.children),typeof d=="object"&&d!==null){switch(d.$$typeof){case Ge:e:{for(var P=d.key,N=a;N!==null;){if(N.key===P){if(P=d.type,P===ke){if(N.tag===7){n(c,N.sibling),a=l(N,d.props.children),a.return=c,c=a;break e}}else if(N.elementType===P||typeof P=="object"&&P!==null&&P.$$typeof===Ee&&Go(P)===N.type){n(c,N.sibling),a=l(N,d.props),a.ref=nr(c,N,d),a.return=c,c=a;break e}n(c,N);break}else t(c,N);N=N.sibling}d.type===ke?(a=rn(d.props.children,c.mode,w,d.key),a.return=c,c=a):(w=gl(d.type,d.key,d.props,null,c.mode,w),w.ref=nr(c,a,d),w.return=c,c=w)}return i(c);case ge:e:{for(N=d.key;a!==null;){if(a.key===N)if(a.tag===4&&a.stateNode.containerInfo===d.containerInfo&&a.stateNode.implementation===d.implementation){n(c,a.sibling),a=l(a,d.children||[]),a.return=c,c=a;break e}else{n(c,a);break}else t(c,a);a=a.sibling}a=mi(d,c.mode,w),a.return=c,c=a}return i(c);case Ee:return N=d._init,te(c,a,N(d._payload),w)}if(Rn(d))return _(c,a,d,w);if(T(d))return x(c,a,d,w);Xr(c,d)}return typeof d=="string"&&d!==""||typeof d=="number"?(d=""+d,a!==null&&a.tag===6?(n(c,a.sibling),a=l(a,d),a.return=c,c=a):(n(c,a),a=pi(d,c.mode,w),a.return=c,c=a),i(c)):n(c,a)}return te}var En=Zo(!0),Jo=Zo(!1),Gr=Mt(null),Zr=null,Cn=null,Cu=null;function _u(){Cu=Cn=Zr=null}function xu(e){var t=Gr.current;Q(Gr),e._currentValue=t}function Pu(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,r!==null&&(r.childLanes|=t)):r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t),e===n)break;e=e.return}}function _n(e,t){Zr=e,Cu=Cn=null,e=e.dependencies,e!==null&&e.firstContext!==null&&((e.lanes&t)!==0&&(xe=!0),e.firstContext=null)}function He(e){var t=e._currentValue;if(Cu!==e)if(e={context:e,memoizedValue:t,next:null},Cn===null){if(Zr===null)throw Error(m(308));Cn=e,Zr.dependencies={lanes:0,firstContext:e}}else Cn=Cn.next=e;return t}var Zt=null;function Nu(e){Zt===null?Zt=[e]:Zt.push(e)}function qo(e,t,n,r){var l=t.interleaved;return l===null?(n.next=n,Nu(t)):(n.next=l.next,l.next=n),t.interleaved=n,St(e,r)}function St(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var It=!1;function zu(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function bo(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function kt(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function Ft(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,(I&2)!==0){var l=r.pending;return l===null?t.next=t:(t.next=l.next,l.next=t),r.pending=t,St(e,n)}return l=r.interleaved,l===null?(t.next=t,Nu(r)):(t.next=l.next,l.next=t),r.interleaved=t,St(e,n)}function Jr(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194240)!==0)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,Hl(e,n)}}function es(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var l=null,u=null;if(n=n.firstBaseUpdate,n!==null){do{var i={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};u===null?l=u=i:u=u.next=i,n=n.next}while(n!==null);u===null?l=u=t:u=u.next=t}else l=u=t;n={baseState:r.baseState,firstBaseUpdate:l,lastBaseUpdate:u,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function qr(e,t,n,r){var l=e.updateQueue;It=!1;var u=l.firstBaseUpdate,i=l.lastBaseUpdate,o=l.shared.pending;if(o!==null){l.shared.pending=null;var s=o,p=s.next;s.next=null,i===null?u=p:i.next=p,i=s;var y=e.alternate;y!==null&&(y=y.updateQueue,o=y.lastBaseUpdate,o!==i&&(o===null?y.firstBaseUpdate=p:o.next=p,y.lastBaseUpdate=s))}if(u!==null){var g=l.baseState;i=0,y=p=s=null,o=u;do{var v=o.lane,k=o.eventTime;if((r&v)===v){y!==null&&(y=y.next={eventTime:k,lane:0,tag:o.tag,payload:o.payload,callback:o.callback,next:null});e:{var _=e,x=o;switch(v=t,k=n,x.tag){case 1:if(_=x.payload,typeof _=="function"){g=_.call(k,g,v);break e}g=_;break e;case 3:_.flags=_.flags&-65537|128;case 0:if(_=x.payload,v=typeof _=="function"?_.call(k,g,v):_,v==null)break e;g=C({},g,v);break e;case 2:It=!0}}o.callback!==null&&o.lane!==0&&(e.flags|=64,v=l.effects,v===null?l.effects=[o]:v.push(o))}else k={eventTime:k,lane:v,tag:o.tag,payload:o.payload,callback:o.callback,next:null},y===null?(p=y=k,s=g):y=y.next=k,i|=v;if(o=o.next,o===null){if(o=l.shared.pending,o===null)break;v=o,o=v.next,v.next=null,l.lastBaseUpdate=v,l.shared.pending=null}}while(!0);if(y===null&&(s=g),l.baseState=s,l.firstBaseUpdate=p,l.lastBaseUpdate=y,t=l.shared.interleaved,t!==null){l=t;do i|=l.lane,l=l.next;while(l!==t)}else u===null&&(l.shared.lanes=0);bt|=i,e.lanes=i,e.memoizedState=g}}function ts(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=Du.transition;Du.transition={};try{e(!1),t()}finally{A=n,Du.transition=r}}function Ss(){return We().memoizedState}function $f(e,t,n){var r=At(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},ks(e))Es(t,n);else if(n=qo(e,t,n,r),n!==null){var l=Se();rt(n,e,r,l),Cs(n,t,r)}}function Qf(e,t,n){var r=At(e),l={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(ks(e))Es(t,l);else{var u=e.alternate;if(e.lanes===0&&(u===null||u.lanes===0)&&(u=t.lastRenderedReducer,u!==null))try{var i=t.lastRenderedState,o=u(i,n);if(l.hasEagerState=!0,l.eagerState=o,qe(o,i)){var s=t.interleaved;s===null?(l.next=l,Nu(t)):(l.next=s.next,s.next=l),t.interleaved=l;return}}catch{}finally{}n=qo(e,t,l,r),n!==null&&(l=Se(),rt(n,e,r,l),Cs(n,t,r))}}function ks(e){var t=e.alternate;return e===Z||t!==null&&t===Z}function Es(e,t){ir=tl=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Cs(e,t,n){if((n&4194240)!==0){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,Hl(e,n)}}var ll={readContext:He,useCallback:ve,useContext:ve,useEffect:ve,useImperativeHandle:ve,useInsertionEffect:ve,useLayoutEffect:ve,useMemo:ve,useReducer:ve,useRef:ve,useState:ve,useDebugValue:ve,useDeferredValue:ve,useTransition:ve,useMutableSource:ve,useSyncExternalStore:ve,useId:ve,unstable_isNewReconciler:!1},Kf={readContext:He,useCallback:function(e,t){return ft().memoizedState=[e,t===void 0?null:t],e},useContext:He,useEffect:ds,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,nl(4194308,4,vs.bind(null,t,e),n)},useLayoutEffect:function(e,t){return nl(4194308,4,e,t)},useInsertionEffect:function(e,t){return nl(4,2,e,t)},useMemo:function(e,t){var n=ft();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=ft();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=$f.bind(null,Z,e),[r.memoizedState,e]},useRef:function(e){var t=ft();return e={current:e},t.memoizedState=e},useState:fs,useDebugValue:Au,useDeferredValue:function(e){return ft().memoizedState=e},useTransition:function(){var e=fs(!1),t=e[0];return e=Wf.bind(null,e[1]),ft().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=Z,l=ft();if(X){if(n===void 0)throw Error(m(407));n=n()}else{if(n=t(),se===null)throw Error(m(349));(qt&30)!==0||us(r,t,n)}l.memoizedState=n;var u={value:n,getSnapshot:t};return l.queue=u,ds(os.bind(null,r,u,e),[e]),r.flags|=2048,ar(9,is.bind(null,r,u,n,t),void 0,null),n},useId:function(){var e=ft(),t=se.identifierPrefix;if(X){var n=wt,r=gt;n=(r&~(1<<32-Je(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=or++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=i.createElement(n,{is:r.is}):(e=i.createElement(n),n==="select"&&(i=e,r.multiple?i.multiple=!0:r.size&&(i.size=r.size))):e=i.createElementNS(e,n),e[st]=t,e[er]=r,Ws(e,t,!1,!1),t.stateNode=e;e:{switch(i=Ml(n,r),n){case"dialog":$("cancel",e),$("close",e),l=r;break;case"iframe":case"object":case"embed":$("load",e),l=r;break;case"video":case"audio":for(l=0;lTn&&(t.flags|=128,r=!0,fr(u,!1),t.lanes=4194304)}else{if(!r)if(e=br(i),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),fr(u,!0),u.tail===null&&u.tailMode==="hidden"&&!i.alternate&&!X)return he(t),null}else 2*ee()-u.renderingStartTime>Tn&&n!==1073741824&&(t.flags|=128,r=!0,fr(u,!1),t.lanes=4194304);u.isBackwards?(i.sibling=t.child,t.child=i):(n=u.last,n!==null?n.sibling=i:t.child=i,u.last=i)}return u.tail!==null?(t=u.tail,u.rendering=t,u.tail=t.sibling,u.renderingStartTime=ee(),t.sibling=null,n=G.current,W(G,r?n&1|2:n&1),t):(he(t),null);case 22:case 23:return fi(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&(t.mode&1)!==0?(Ie&1073741824)!==0&&(he(t),t.subtreeFlags&6&&(t.flags|=8192)):he(t),null;case 24:return null;case 25:return null}throw Error(m(156,t.tag))}function ec(e,t){switch(wu(t),t.tag){case 1:return _e(t.type)&&Hr(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return xn(),Q(Ce),Q(me),Mu(),e=t.flags,(e&65536)!==0&&(e&128)===0?(t.flags=e&-65537|128,t):null;case 5:return Lu(t),null;case 13:if(Q(G),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(m(340));kn()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return Q(G),null;case 4:return xn(),null;case 10:return xu(t.type._context),null;case 22:case 23:return fi(),null;case 24:return null;default:return null}}var sl=!1,ye=!1,tc=typeof WeakSet=="function"?WeakSet:Set,E=null;function Nn(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){q(e,t,r)}else n.current=null}function qu(e,t,n){try{n()}catch(r){q(e,t,r)}}var Ks=!1;function nc(e,t){if(fu=zr,e=_o(),nu(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var l=r.anchorOffset,u=r.focusNode;r=r.focusOffset;try{n.nodeType,u.nodeType}catch{n=null;break e}var i=0,o=-1,s=-1,p=0,y=0,g=e,v=null;t:for(;;){for(var k;g!==n||l!==0&&g.nodeType!==3||(o=i+l),g!==u||r!==0&&g.nodeType!==3||(s=i+r),g.nodeType===3&&(i+=g.nodeValue.length),(k=g.firstChild)!==null;)v=g,g=k;for(;;){if(g===e)break t;if(v===n&&++p===l&&(o=i),v===u&&++y===r&&(s=i),(k=g.nextSibling)!==null)break;g=v,v=g.parentNode}g=k}n=o===-1||s===-1?null:{start:o,end:s}}else n=null}n=n||{start:0,end:0}}else n=null;for(cu={focusedElem:e,selectionRange:n},zr=!1,E=t;E!==null;)if(t=E,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,E=e;else for(;E!==null;){t=E;try{var _=t.alternate;if((t.flags&1024)!==0)switch(t.tag){case 0:case 11:case 15:break;case 1:if(_!==null){var x=_.memoizedProps,te=_.memoizedState,c=t.stateNode,a=c.getSnapshotBeforeUpdate(t.elementType===t.type?x:et(t.type,x),te);c.__reactInternalSnapshotBeforeUpdate=a}break;case 3:var d=t.stateNode.containerInfo;d.nodeType===1?d.textContent="":d.nodeType===9&&d.documentElement&&d.removeChild(d.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(m(163))}}catch(w){q(t,t.return,w)}if(e=t.sibling,e!==null){e.return=t.return,E=e;break}E=t.return}return _=Ks,Ks=!1,_}function cr(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var l=r=r.next;do{if((l.tag&e)===e){var u=l.destroy;l.destroy=void 0,u!==void 0&&qu(t,n,u)}l=l.next}while(l!==r)}}function al(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function bu(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function Ys(e){var t=e.alternate;t!==null&&(e.alternate=null,Ys(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[st],delete t[er],delete t[vu],delete t[Uf],delete t[Vf])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function Xs(e){return e.tag===5||e.tag===3||e.tag===4}function Gs(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||Xs(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function ei(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=Ar));else if(r!==4&&(e=e.child,e!==null))for(ei(e,t,n),e=e.sibling;e!==null;)ei(e,t,n),e=e.sibling}function ti(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(ti(e,t,n),e=e.sibling;e!==null;)ti(e,t,n),e=e.sibling}var fe=null,tt=!1;function jt(e,t,n){for(n=n.child;n!==null;)Zs(e,t,n),n=n.sibling}function Zs(e,t,n){if(ot&&typeof ot.onCommitFiberUnmount=="function")try{ot.onCommitFiberUnmount(Er,n)}catch{}switch(n.tag){case 5:ye||Nn(n,t);case 6:var r=fe,l=tt;fe=null,jt(e,t,n),fe=r,tt=l,fe!==null&&(tt?(e=fe,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):fe.removeChild(n.stateNode));break;case 18:fe!==null&&(tt?(e=fe,n=n.stateNode,e.nodeType===8?mu(e.parentNode,n):e.nodeType===1&&mu(e,n),Wn(e)):mu(fe,n.stateNode));break;case 4:r=fe,l=tt,fe=n.stateNode.containerInfo,tt=!0,jt(e,t,n),fe=r,tt=l;break;case 0:case 11:case 14:case 15:if(!ye&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){l=r=r.next;do{var u=l,i=u.destroy;u=u.tag,i!==void 0&&((u&2)!==0||(u&4)!==0)&&qu(n,t,i),l=l.next}while(l!==r)}jt(e,t,n);break;case 1:if(!ye&&(Nn(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(o){q(n,t,o)}jt(e,t,n);break;case 21:jt(e,t,n);break;case 22:n.mode&1?(ye=(r=ye)||n.memoizedState!==null,jt(e,t,n),ye=r):jt(e,t,n);break;default:jt(e,t,n)}}function Js(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new tc),t.forEach(function(r){var l=cc.bind(null,e,r);n.has(r)||(n.add(r),r.then(l,l))})}}function nt(e,t){var n=t.deletions;if(n!==null)for(var r=0;rl&&(l=i),r&=~u}if(r=l,r=ee()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*lc(r/1960))-r,10e?16:e,Vt===null)var r=!1;else{if(e=Vt,Vt=null,ml=0,(I&6)!==0)throw Error(m(331));var l=I;for(I|=4,E=e.current;E!==null;){var u=E,i=u.child;if((E.flags&16)!==0){var o=u.deletions;if(o!==null){for(var s=0;see()-li?tn(e,0):ri|=n),Ne(e,t)}function fa(e,t){t===0&&((e.mode&1)===0?t=1:(t=_r,_r<<=1,(_r&130023424)===0&&(_r=4194304)));var n=Se();e=St(e,t),e!==null&&(Un(e,t,n),Ne(e,n))}function fc(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),fa(e,n)}function cc(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,l=e.memoizedState;l!==null&&(n=l.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(m(314))}r!==null&&r.delete(t),fa(e,n)}var ca;ca=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||Ce.current)xe=!0;else{if((e.lanes&n)===0&&(t.flags&128)===0)return xe=!1,qf(e,t,n);xe=(e.flags&131072)!==0}else xe=!1,X&&(t.flags&1048576)!==0&&$o(t,Kr,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;ol(e,t),e=t.pendingProps;var l=gn(t,me.current);_n(t,n),l=Iu(null,t,r,e,l,n);var u=Fu();return t.flags|=1,typeof l=="object"&&l!==null&&typeof l.render=="function"&&l.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,_e(r)?(u=!0,Wr(t)):u=!1,t.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,zu(t),l.updater=ul,t.stateNode=l,l._reactInternals=t,Hu(t,r,e,n),t=Ku(null,t,r,!0,u,n)):(t.tag=0,X&&u&&gu(t),we(null,t,l,n),t=t.child),t;case 16:r=t.elementType;e:{switch(ol(e,t),e=t.pendingProps,l=r._init,r=l(r._payload),t.type=r,l=t.tag=pc(r),e=et(r,e),l){case 0:t=Qu(null,t,r,e,n);break e;case 1:t=js(null,t,r,e,n);break e;case 11:t=Ms(null,t,r,e,n);break e;case 14:t=Ds(null,t,r,et(r.type,e),n);break e}throw Error(m(306,r,""))}return t;case 0:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:et(r,l),Qu(e,t,r,l,n);case 1:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:et(r,l),js(e,t,r,l,n);case 3:e:{if(Us(t),e===null)throw Error(m(387));r=t.pendingProps,u=t.memoizedState,l=u.element,bo(e,t),qr(t,r,null,n);var i=t.memoizedState;if(r=i.element,u.isDehydrated)if(u={element:r,isDehydrated:!1,cache:i.cache,pendingSuspenseBoundaries:i.pendingSuspenseBoundaries,transitions:i.transitions},t.updateQueue.baseState=u,t.memoizedState=u,t.flags&256){l=Pn(Error(m(423)),t),t=Vs(e,t,r,n,l);break e}else if(r!==l){l=Pn(Error(m(424)),t),t=Vs(e,t,r,n,l);break e}else for(Oe=Rt(t.stateNode.containerInfo.firstChild),De=t,X=!0,be=null,n=Jo(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(kn(),r===l){t=Et(e,t,n);break e}we(e,t,r,n)}t=t.child}return t;case 5:return ns(t),e===null&&ku(t),r=t.type,l=t.pendingProps,u=e!==null?e.memoizedProps:null,i=l.children,du(r,l)?i=null:u!==null&&du(r,u)&&(t.flags|=32),Fs(e,t),we(e,t,i,n),t.child;case 6:return e===null&&ku(t),null;case 13:return As(e,t,n);case 4:return Tu(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=En(t,null,r,n):we(e,t,r,n),t.child;case 11:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:et(r,l),Ms(e,t,r,l,n);case 7:return we(e,t,t.pendingProps,n),t.child;case 8:return we(e,t,t.pendingProps.children,n),t.child;case 12:return we(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,l=t.pendingProps,u=t.memoizedProps,i=l.value,W(Gr,r._currentValue),r._currentValue=i,u!==null)if(qe(u.value,i)){if(u.children===l.children&&!Ce.current){t=Et(e,t,n);break e}}else for(u=t.child,u!==null&&(u.return=t);u!==null;){var o=u.dependencies;if(o!==null){i=u.child;for(var s=o.firstContext;s!==null;){if(s.context===r){if(u.tag===1){s=kt(-1,n&-n),s.tag=2;var p=u.updateQueue;if(p!==null){p=p.shared;var y=p.pending;y===null?s.next=s:(s.next=y.next,y.next=s),p.pending=s}}u.lanes|=n,s=u.alternate,s!==null&&(s.lanes|=n),Pu(u.return,n,t),o.lanes|=n;break}s=s.next}}else if(u.tag===10)i=u.type===t.type?null:u.child;else if(u.tag===18){if(i=u.return,i===null)throw Error(m(341));i.lanes|=n,o=i.alternate,o!==null&&(o.lanes|=n),Pu(i,n,t),i=u.sibling}else i=u.child;if(i!==null)i.return=u;else for(i=u;i!==null;){if(i===t){i=null;break}if(u=i.sibling,u!==null){u.return=i.return,i=u;break}i=i.return}u=i}we(e,t,l.children,n),t=t.child}return t;case 9:return l=t.type,r=t.pendingProps.children,_n(t,n),l=He(l),r=r(l),t.flags|=1,we(e,t,r,n),t.child;case 14:return r=t.type,l=et(r,t.pendingProps),l=et(r.type,l),Ds(e,t,r,l,n);case 15:return Os(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:et(r,l),ol(e,t),t.tag=1,_e(r)?(e=!0,Wr(t)):e=!1,_n(t,n),xs(t,r,l),Hu(t,r,l,n),Ku(null,t,r,!0,e,n);case 19:return Hs(e,t,n);case 22:return Is(e,t,n)}throw Error(m(156,t.tag))};function da(e,t){return $i(e,t)}function dc(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Qe(e,t,n,r){return new dc(e,t,n,r)}function di(e){return e=e.prototype,!(!e||!e.isReactComponent)}function pc(e){if(typeof e=="function")return di(e)?1:0;if(e!=null){if(e=e.$$typeof,e===ut)return 11;if(e===it)return 14}return 2}function Ht(e,t){var n=e.alternate;return n===null?(n=Qe(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function gl(e,t,n,r,l,u){var i=2;if(r=e,typeof e=="function")di(e)&&(i=1);else if(typeof e=="string")i=5;else e:switch(e){case ke:return rn(n.children,l,u,t);case Ve:i=8,l|=8;break;case _t:return e=Qe(12,n,t,l|2),e.elementType=_t,e.lanes=u,e;case Le:return e=Qe(13,n,t,l),e.elementType=Le,e.lanes=u,e;case Ze:return e=Qe(19,n,t,l),e.elementType=Ze,e.lanes=u,e;case J:return wl(n,l,u,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case vt:i=10;break e;case $t:i=9;break e;case ut:i=11;break e;case it:i=14;break e;case Ee:i=16,r=null;break e}throw Error(m(130,e==null?e:typeof e,""))}return t=Qe(i,n,t,l),t.elementType=e,t.type=r,t.lanes=u,t}function rn(e,t,n,r){return e=Qe(7,e,r,t),e.lanes=n,e}function wl(e,t,n,r){return e=Qe(22,e,r,t),e.elementType=J,e.lanes=n,e.stateNode={isHidden:!1},e}function pi(e,t,n){return e=Qe(6,e,null,t),e.lanes=n,e}function mi(e,t,n){return t=Qe(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function mc(e,t,n,r,l){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=Bl(0),this.expirationTimes=Bl(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Bl(0),this.identifierPrefix=r,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function vi(e,t,n,r,l,u,i,o,s){return e=new mc(e,t,n,o,s),t===1?(t=1,u===!0&&(t|=8)):t=0,u=Qe(3,null,null,t),e.current=u,u.stateNode=e,u.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},zu(u),e}function vc(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(R)}catch(b){console.error(b)}}return R(),Si.exports=xc(),Si.exports}export{Nc as a,Pc as g,Ec as r};
diff --git a/frontend/dist/index.html b/frontend/dist/index.html
new file mode 100644
index 0000000000..54085eb331
--- /dev/null
+++ b/frontend/dist/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+ Technigo React Vite Boiler Plate
+
+
+
+
+
+
+
+
diff --git a/frontend/dist/vite.svg b/frontend/dist/vite.svg
new file mode 100644
index 0000000000..e7b8dfb1b2
--- /dev/null
+++ b/frontend/dist/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/package.json b/frontend/package.json
index 7b2747e949..a54983467c 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -7,20 +7,44 @@
"dev": "vite",
"build": "vite build",
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
- "preview": "vite preview"
+ "lint:fix": "eslint . --ext js,jsx --fix",
+ "format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,md}\"",
+ "format:check": "prettier --check \"src/**/*.{js,jsx,ts,tsx,json,css,md}\"",
+ "preview": "vite preview",
+ "prepare": "husky install",
+ "test": "vitest --config config/vitest.config.js",
+ "test:ui": "vitest --ui --config config/vitest.config.js"
},
"dependencies": {
+ "autoprefixer": "^10.4.14",
+ "clsx": "^2.0.0",
+ "postcss": "^8.4.24",
"react": "^18.2.0",
- "react-dom": "^18.2.0"
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.14.0",
+ "styled-components": "^6.1.19",
+ "tailwindcss": "^3.3.0",
+ "zustand": "^4.4.0"
},
"devDependencies": {
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@vitejs/plugin-react": "^4.0.3",
+ "concurrently": "^9.2.0",
"eslint": "^8.45.0",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
- "vite": "^6.3.5"
+ "husky": "^8.0.3",
+ "jsdom": "^22.1.0",
+ "lint-staged": "^13.2.3",
+ "prettier": "^3.0.0",
+ "vite": "^6.3.5",
+ "vitest": "^0.34.0"
+ },
+ "msw": {
+ "workerDirectory": "public"
}
}
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 0a24275e6e..0e69f49ae9 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -1,8 +1,34 @@
-export const App = () => {
+import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
+import { LandingPage } from './pages/LandingPage';
+import { LoginPage } from './pages/LoginPage';
+import { RegisterPage } from './pages/RegisterPage';
+import { DashboardPage } from './pages/DashboardPage';
+import { ProtectedRoute } from './components/auth/ProtectedRoute';
+import { AuthProvider } from './contexts/AuthContext';
+export const App = () => {
return (
- <>
- Welcome to Final Project!
- >
+
+
+
+ } />
+ } />
+ } />
+
+
+
+ }
+ />
+
+
+
);
};
diff --git a/frontend/src/components/auth/ProtectedRoute.jsx b/frontend/src/components/auth/ProtectedRoute.jsx
new file mode 100644
index 0000000000..4738d4c084
--- /dev/null
+++ b/frontend/src/components/auth/ProtectedRoute.jsx
@@ -0,0 +1,20 @@
+import { Navigate } from 'react-router-dom';
+import { useAuth } from '../../contexts/AuthContext';
+
+export const ProtectedRoute = ({ children }) => {
+ const { user, loading } = useAuth();
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (!user) {
+ return ;
+ }
+
+ return children;
+};
\ No newline at end of file
diff --git a/frontend/src/components/ui/Button.jsx b/frontend/src/components/ui/Button.jsx
new file mode 100644
index 0000000000..d70601ef1c
--- /dev/null
+++ b/frontend/src/components/ui/Button.jsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import { cn } from '@/lib/utils';
+
+const buttonVariants = {
+ primary: 'btn-primary',
+ secondary: 'btn-secondary',
+ outline: 'btn-outline',
+};
+
+const buttonSizes = {
+ sm: 'h-8 px-3 text-xs',
+ md: 'h-10 px-4 py-2',
+ lg: 'h-12 px-8',
+};
+
+export const Button = React.forwardRef(
+ ({ className, variant = 'primary', size = 'md', ...props }, ref) => {
+ return (
+
+ );
+ }
+);
+
+Button.displayName = 'Button';
\ No newline at end of file
diff --git a/frontend/src/components/ui/Navbar.jsx b/frontend/src/components/ui/Navbar.jsx
new file mode 100644
index 0000000000..681c3fc915
--- /dev/null
+++ b/frontend/src/components/ui/Navbar.jsx
@@ -0,0 +1,67 @@
+import { useState } from 'react';
+import { Link } from 'react-router-dom';
+
+export const Navbar = () => {
+ const [open, setOpen] = useState(false);
+
+ const toggleMenu = () => setOpen(!open);
+
+ const linkBase =
+ 'text-gray-700 hover:text-green-600 transition-colors px-4 py-2 text-base font-medium';
+
+ const menuItems = (
+ <>
+
+ Features
+
+
+ About
+
+
+ Login
+
+
+ Get Started
+
+ >
+ );
+
+ return (
+
+
+
+ {/* Logo */}
+
+ Nanwa
+
+
+ {/* Desktop Menu */}
+
{menuItems}
+
+ {/* Mobile Toggle */}
+
+ {open ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+
+
+ {/* Mobile Menu Dropdown */}
+ {open && {menuItems}
}
+
+ );
+};
\ No newline at end of file
diff --git a/frontend/src/contexts/AuthContext.jsx b/frontend/src/contexts/AuthContext.jsx
new file mode 100644
index 0000000000..9f97c27dd6
--- /dev/null
+++ b/frontend/src/contexts/AuthContext.jsx
@@ -0,0 +1,174 @@
+import { createContext, useContext, useState, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+
+const AuthContext = createContext();
+
+export const useAuth = () => {
+ const context = useContext(AuthContext);
+ if (!context) {
+ throw new Error('useAuth must be used within an AuthProvider');
+ }
+ return context;
+};
+
+export const AuthProvider = ({ children }) => {
+ const [user, setUser] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const navigate = useNavigate();
+
+ // Check for existing token on app load
+ useEffect(() => {
+ const token = localStorage.getItem('authToken');
+ const userData = localStorage.getItem('userData');
+
+ if (token && userData) {
+ try {
+ setUser(JSON.parse(userData));
+ } catch (error) {
+ console.error('Error parsing user data:', error);
+ localStorage.removeItem('authToken');
+ localStorage.removeItem('userData');
+ }
+ }
+ setLoading(false);
+ }, []);
+
+ const login = async (email, password) => {
+ try {
+ setLoading(true);
+ // TODO: Replace with actual API call when backend is ready
+ // For now, simulate API call
+ const response = await simulateLoginAPI(email, password);
+
+ const { token, user: userData } = response;
+
+ // Store token and user data securely
+ localStorage.setItem('authToken', token);
+ localStorage.setItem('userData', JSON.stringify(userData));
+
+ setUser(userData);
+ navigate('/dashboard');
+
+ return { success: true };
+ } catch (error) {
+ return { success: false, error: error.message };
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const register = async (email, password, confirmPassword) => {
+ try {
+ setLoading(true);
+
+ if (password !== confirmPassword) {
+ throw new Error('Passwords do not match');
+ }
+
+ // TODO: Replace with actual API call when backend is ready
+ const response = await simulateRegisterAPI(email, password);
+
+ const { token, user: userData } = response;
+
+ localStorage.setItem('authToken', token);
+ localStorage.setItem('userData', JSON.stringify(userData));
+
+ setUser(userData);
+ navigate('/dashboard');
+
+ return { success: true };
+ } catch (error) {
+ return { success: false, error: error.message };
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const logout = () => {
+ localStorage.removeItem('authToken');
+ localStorage.removeItem('userData');
+ setUser(null);
+ navigate('/');
+ };
+
+ const isAdmin = () => {
+ return user?.role === 'admin';
+ };
+
+ const value = {
+ user,
+ loading,
+ login,
+ register,
+ logout,
+ isAdmin,
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+// Temporary mock API functions - replace with real API calls when backend is ready
+const simulateLoginAPI = async (email, password) => {
+ // Simulate API delay
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ // Basic validation
+ if (!email || !password) {
+ throw new Error('Email and password are required');
+ }
+
+ if (email === 'admin@nanwa.com' && password === 'admin123') {
+ return {
+ token: 'mock-jwt-token-admin',
+ user: {
+ id: 1,
+ email: 'admin@nanwa.com',
+ role: 'admin',
+ name: 'Admin User'
+ }
+ };
+ }
+
+ if (email === 'user@nanwa.com' && password === 'user123') {
+ return {
+ token: 'mock-jwt-token-user',
+ user: {
+ id: 2,
+ email: 'user@nanwa.com',
+ role: 'user',
+ name: 'Regular User'
+ }
+ };
+ }
+
+ throw new Error('Invalid email or password');
+};
+
+const simulateRegisterAPI = async (email, password) => {
+ // Simulate API delay
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ // Basic validation
+ if (!email || !password) {
+ throw new Error('Email and password are required');
+ }
+
+ if (password.length < 6) {
+ throw new Error('Password must be at least 6 characters long');
+ }
+
+ // Simulate successful registration
+ return {
+ token: 'mock-jwt-token-new-user',
+ user: {
+ id: Math.floor(Math.random() * 1000),
+ email,
+ role: 'user',
+ name: email.split('@')[0]
+ }
+ };
+};
\ No newline at end of file
diff --git a/frontend/src/index.css b/frontend/src/index.css
index e69de29bb2..206b591f08 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -0,0 +1,61 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ :root {
+ --color-primary: 14 165 233;
+ --color-secondary: 132 204 22;
+ --color-success: 34 197 94;
+ --color-warning: 245 158 11;
+ --color-error: 239 68 68;
+ }
+
+ .dark {
+ --color-primary: 14 165 233;
+ --color-secondary: 132 204 22;
+ --color-success: 34 197 94;
+ --color-warning: 245 158 11;
+ --color-error: 239 68 68;
+ }
+
+ body {
+ font-feature-settings: "rlig" 1, "calt" 1;
+ }
+}
+
+@layer components {
+ .btn {
+ @apply inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none;
+ }
+
+ .btn-primary {
+ @apply btn bg-primary-600 text-white hover:bg-primary-700;
+ }
+
+ .btn-secondary {
+ @apply btn bg-secondary-600 text-white hover:bg-secondary-700;
+ }
+
+ .btn-outline {
+ @apply btn border border-gray-300 bg-white text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700;
+ }
+
+ .form-input {
+ @apply flex h-10 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 dark:placeholder:text-gray-400;
+ }
+
+ .form-label {
+ @apply text-sm font-medium leading-none;
+ }
+
+ .card {
+ @apply rounded-lg border border-gray-200 bg-white text-gray-950 shadow-sm dark:border-gray-800 dark:bg-gray-950 dark:text-gray-50;
+ }
+}
+
+@layer utilities {
+ .skip-link {
+ @apply absolute left-0 top-0 z-50 -translate-y-full transform bg-primary-600 px-4 py-2 text-white transition-transform focus:translate-y-0;
+ }
+}
diff --git a/frontend/src/lib/store.js b/frontend/src/lib/store.js
new file mode 100644
index 0000000000..9034fe2835
--- /dev/null
+++ b/frontend/src/lib/store.js
@@ -0,0 +1,105 @@
+import { create } from 'zustand';
+import { devtools, persist } from 'zustand/middleware';
+
+// Auth store
+export const useAuthStore = create(
+ devtools(
+ persist(
+ (set, get) => ({
+ user: null,
+ token: null,
+ isAuthenticated: false,
+ isLoading: false,
+
+ login: (userData, token) => {
+ localStorage.setItem('authToken', token);
+ set({
+ user: userData,
+ token,
+ isAuthenticated: true,
+ isLoading: false,
+ });
+ },
+
+ logout: () => {
+ localStorage.removeItem('authToken');
+ set({
+ user: null,
+ token: null,
+ isAuthenticated: false,
+ isLoading: false,
+ });
+ },
+
+ setLoading: (loading) => set({ isLoading: loading }),
+
+ updateUser: (userData) => set({ user: userData }),
+ }),
+ {
+ name: 'auth-storage',
+ partialize: (state) => ({
+ user: state.user,
+ token: state.token,
+ isAuthenticated: state.isAuthenticated
+ }),
+ }
+ ),
+ { name: 'auth-store' }
+ )
+);
+
+// App store for global state
+export const useAppStore = create(
+ devtools(
+ (set, get) => ({
+ // Global filters
+ filters: {
+ dateRange: { start: null, end: null },
+ forests: [],
+ regions: [],
+ },
+
+ // UI state
+ sidebarOpen: false,
+ darkMode: false,
+
+ // Data
+ forests: [],
+ trees: [],
+ loading: false,
+ error: null,
+
+ // Actions
+ setFilters: (filters) => set((state) => ({
+ filters: { ...state.filters, ...filters }
+ })),
+
+ clearFilters: () => set({
+ filters: {
+ dateRange: { start: null, end: null },
+ forests: [],
+ regions: [],
+ }
+ }),
+
+ setSidebarOpen: (open) => set({ sidebarOpen: open }),
+
+ setDarkMode: (dark) => {
+ if (dark) {
+ document.documentElement.classList.add('dark');
+ } else {
+ document.documentElement.classList.remove('dark');
+ }
+ set({ darkMode: dark });
+ },
+
+ setForests: (forests) => set({ forests }),
+ setTrees: (trees) => set({ trees }),
+ setLoading: (loading) => set({ loading }),
+ setError: (error) => set({ error }),
+
+ clearError: () => set({ error: null }),
+ }),
+ { name: 'app-store' }
+ )
+);
\ No newline at end of file
diff --git a/frontend/src/lib/utils.js b/frontend/src/lib/utils.js
new file mode 100644
index 0000000000..5f1dc6001a
--- /dev/null
+++ b/frontend/src/lib/utils.js
@@ -0,0 +1,5 @@
+import { clsx } from 'clsx';
+
+export function cn(...inputs) {
+ return clsx(inputs);
+}
\ No newline at end of file
diff --git a/frontend/src/pages/DashboardPage.jsx b/frontend/src/pages/DashboardPage.jsx
new file mode 100644
index 0000000000..b0ac2a7cfc
--- /dev/null
+++ b/frontend/src/pages/DashboardPage.jsx
@@ -0,0 +1,225 @@
+import { useAuth } from '../contexts/AuthContext';
+import styled from 'styled-components';
+
+const DashboardContainer = styled.div`
+ min-height: 100vh;
+ background-color: #f9fafb;
+`;
+
+const Header = styled.header`
+ background: white;
+ border-bottom: 1px solid #e5e7eb;
+ padding: 1rem 0;
+`;
+
+const Sidebar = styled.aside`
+ background: white;
+ border-right: 1px solid #e5e7eb;
+ width: 250px;
+ position: fixed;
+ top: 0;
+ left: 0;
+ height: 100vh;
+ overflow-y: auto;
+ z-index: 10;
+`;
+
+const MainContent = styled.main`
+ margin-left: 250px;
+ padding: 2rem;
+`;
+
+export const DashboardPage = () => {
+ const { user, logout, isAdmin } = useAuth();
+
+ const handleLogout = () => {
+ logout();
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
Nanwa Dashboard
+
+
+
+ Welcome, {user?.name}
+ {isAdmin() && (
+
+ Admin
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ {/* Sidebar */}
+
+
+
+
+ {/* Main Content */}
+
+
+ {/* Welcome Section */}
+
+
Welcome to Nanwa
+
+ Monitor your forests and track tree growth with real-time insights.
+
+
+
+ {/* Stats Cards */}
+
+
+
+
+
+
+
+
Survival Rate
+
95.2%
+
+
+
+
+
+
+
+
+
+ {/* Placeholder Content */}
+
+
Coming Soon
+
+ The dashboard is currently being developed. Soon you'll be able to:
+
+
+
+
+
+
+ View real-time charts and analytics
+
+
+
+
+
+ Explore interactive maps with tree locations
+
+
+
+
+
+ Export data in CSV or XLSX format
+
+
+
+
+
+ Filter and search through tree data
+
+
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/frontend/src/pages/LandingPage.jsx b/frontend/src/pages/LandingPage.jsx
new file mode 100644
index 0000000000..984009f73f
--- /dev/null
+++ b/frontend/src/pages/LandingPage.jsx
@@ -0,0 +1,201 @@
+import { Link } from 'react-router-dom';
+import styled from 'styled-components';
+import { Navbar } from '../components/ui/Navbar';
+
+const HeroSection = styled.section`
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
+ min-height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+ overflow: hidden;
+`;
+
+const HeroBackground = styled.div`
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-image: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.1'%3E%3Ccircle cx='30' cy='30' r='2'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
+ opacity: 0.3;
+`;
+
+export const LandingPage = () => {
+ return (
+
+ {/* Navigation Header */}
+
+
+ {/* Hero Section */}
+
+
+
+
+
+ Discover, Monitor & Export
+ Real-Time Tree Insights
+
+
+ Nanwa provides investors, growers, and admins with comprehensive tools to track every tree in our registry with real-time data and actionable insights.
+
+
+
+
+
+
+ {/* Features Section */}
+
+
+
+
+ Powerful Features for Tree Management
+
+
+ Everything you need to monitor, analyze, and export tree data with precision and ease.
+
+
+
+
+
+
+
Real-Time Analytics
+
+ Monitor survival rates, average height, and CO₂ absorption with live-updating charts and metrics.
+
+
+
+
+
+
Interactive Mapping
+
+ Visualize forests and individual trees with marker clustering and detailed tree information.
+
+
+
+
+
+
Data Export
+
+ Export filtered datasets to CSV or XLSX format for reporting and analysis.
+
+
+
+
+
+
+ {/* About Section */}
+
+
+
+
+
+ Empowering Sustainable Forestry
+
+
+ Nanwa is dedicated to providing comprehensive tree monitoring solutions that help investors, growers, and environmental analysts make data-driven decisions for sustainable forestry projects.
+
+
+ Our platform offers real-time insights, advanced analytics, and seamless data export capabilities to support your forestry management needs.
+
+
+ Join Nanwa Today
+
+
+
+
+
+
10K+
+
Trees Monitored
+
+
+
+
50+
+
Forest Projects
+
+
+
24/7
+
Real-Time Data
+
+
+
+
+
+
+
+ {/* Footer */}
+
+
+
+
+
Nanwa
+
+ Empowering sustainable forestry through data-driven insights and real-time monitoring.
+
+
+
+
+
+
+
+
© 2024 Nanwa. All rights reserved.
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/frontend/src/pages/LoginPage.jsx b/frontend/src/pages/LoginPage.jsx
new file mode 100644
index 0000000000..cb1f784baf
--- /dev/null
+++ b/frontend/src/pages/LoginPage.jsx
@@ -0,0 +1,236 @@
+import { useState } from 'react';
+import { Link, useNavigate } from 'react-router-dom';
+import { useAuth } from '../contexts/AuthContext';
+import styled from 'styled-components';
+
+const LoginContainer = styled.div`
+ min-height: 100vh;
+ background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 1rem;
+`;
+
+const LoginCard = styled.div`
+ background: white;
+ border-radius: 1rem;
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+ padding: 2rem;
+ width: 100%;
+ max-width: 400px;
+`;
+
+export const LoginPage = () => {
+ const [formData, setFormData] = useState({
+ email: '',
+ password: '',
+ });
+ const [errors, setErrors] = useState({});
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [showPassword, setShowPassword] = useState(false);
+
+ const { login } = useAuth();
+ const navigate = useNavigate();
+
+ const validateForm = () => {
+ const newErrors = {};
+
+ if (!formData.email) {
+ newErrors.email = 'Email is required';
+ } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
+ newErrors.email = 'Email is invalid';
+ }
+
+ if (!formData.password) {
+ newErrors.password = 'Password is required';
+ } else if (formData.password.length < 6) {
+ newErrors.password = 'Password must be at least 6 characters';
+ }
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const handleInputChange = (e) => {
+ const { name, value } = e.target;
+ setFormData(prev => ({
+ ...prev,
+ [name]: value
+ }));
+
+ // Clear error when user starts typing
+ if (errors[name]) {
+ setErrors(prev => ({
+ ...prev,
+ [name]: ''
+ }));
+ }
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ if (!validateForm()) {
+ return;
+ }
+
+ setIsSubmitting(true);
+
+ try {
+ const result = await login(formData.email, formData.password);
+
+ if (!result.success) {
+ setErrors({ general: result.error });
+ }
+ } catch (error) {
+ setErrors({ general: 'An unexpected error occurred. Please try again.' });
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ return (
+
+
+
+
+
Nanwa
+
+
Welcome back
+
Sign in to your account to continue
+
+
+
+
+
+
+ Don't have an account?{' '}
+
+ Sign up
+
+
+
+
+ {/* Demo credentials */}
+
+
Demo credentials:
+
+
Admin: admin@nanwa.com / admin123
+
User: user@nanwa.com / user123
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/frontend/src/pages/RegisterPage.jsx b/frontend/src/pages/RegisterPage.jsx
new file mode 100644
index 0000000000..a241e1293c
--- /dev/null
+++ b/frontend/src/pages/RegisterPage.jsx
@@ -0,0 +1,352 @@
+import { useState } from 'react';
+import { Link } from 'react-router-dom';
+import { useAuth } from '../contexts/AuthContext';
+import styled from 'styled-components';
+
+const RegisterContainer = styled.div`
+ min-height: 100vh;
+ background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 1rem;
+`;
+
+const RegisterCard = styled.div`
+ background: white;
+ border-radius: 1rem;
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+ padding: 2rem;
+ width: 100%;
+ max-width: 450px;
+`;
+
+const PasswordStrengthBar = styled.div`
+ width: 100%;
+ height: 4px;
+ background-color: #e5e7eb;
+ border-radius: 2px;
+ overflow: hidden;
+ margin-top: 0.5rem;
+`;
+
+const PasswordStrengthFill = styled.div`
+ height: 100%;
+ background-color: ${props => {
+ if (props.strength === 'weak') return '#ef4444';
+ if (props.strength === 'medium') return '#f59e0b';
+ if (props.strength === 'strong') return '#10b981';
+ return '#e5e7eb';
+ }};
+ width: ${props => {
+ if (props.strength === 'weak') return '33%';
+ if (props.strength === 'medium') return '66%';
+ if (props.strength === 'strong') return '100%';
+ return '0%';
+ }};
+ transition: all 0.3s ease;
+`;
+
+export const RegisterPage = () => {
+ const [formData, setFormData] = useState({
+ email: '',
+ password: '',
+ confirmPassword: '',
+ });
+ const [errors, setErrors] = useState({});
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [showPassword, setShowPassword] = useState(false);
+ const [showConfirmPassword, setShowConfirmPassword] = useState(false);
+
+ const { register } = useAuth();
+
+ const getPasswordStrength = (password) => {
+ if (!password) return { strength: 'none', score: 0 };
+
+ let score = 0;
+ if (password.length >= 8) score += 1;
+ if (/[a-z]/.test(password)) score += 1;
+ if (/[A-Z]/.test(password)) score += 1;
+ if (/[0-9]/.test(password)) score += 1;
+ if (/[^A-Za-z0-9]/.test(password)) score += 1;
+
+ if (score <= 2) return { strength: 'weak', score };
+ if (score <= 3) return { strength: 'medium', score };
+ return { strength: 'strong', score };
+ };
+
+ const passwordStrength = getPasswordStrength(formData.password);
+
+ const validateForm = () => {
+ const newErrors = {};
+
+ if (!formData.email) {
+ newErrors.email = 'Email is required';
+ } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
+ newErrors.email = 'Email is invalid';
+ }
+
+ if (!formData.password) {
+ newErrors.password = 'Password is required';
+ } else if (formData.password.length < 6) {
+ newErrors.password = 'Password must be at least 6 characters';
+ } else if (passwordStrength.strength === 'weak') {
+ newErrors.password = 'Password is too weak. Include uppercase, lowercase, numbers, and special characters.';
+ }
+
+ if (!formData.confirmPassword) {
+ newErrors.confirmPassword = 'Please confirm your password';
+ } else if (formData.password !== formData.confirmPassword) {
+ newErrors.confirmPassword = 'Passwords do not match';
+ }
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const handleInputChange = (e) => {
+ const { name, value } = e.target;
+ setFormData(prev => ({
+ ...prev,
+ [name]: value
+ }));
+
+ // Clear error when user starts typing
+ if (errors[name]) {
+ setErrors(prev => ({
+ ...prev,
+ [name]: ''
+ }));
+ }
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ if (!validateForm()) {
+ return;
+ }
+
+ setIsSubmitting(true);
+
+ try {
+ const result = await register(formData.email, formData.password, formData.confirmPassword);
+
+ if (!result.success) {
+ setErrors({ general: result.error });
+ }
+ } catch (error) {
+ setErrors({ general: 'An unexpected error occurred. Please try again.' });
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const getPasswordStrengthText = () => {
+ switch (passwordStrength.strength) {
+ case 'weak':
+ return 'Weak password';
+ case 'medium':
+ return 'Medium strength password';
+ case 'strong':
+ return 'Strong password';
+ default:
+ return '';
+ }
+ };
+
+ return (
+
+
+
+
+
Nanwa
+
+
Create your account
+
Join Nanwa to start monitoring your trees
+
+
+
+
+
+
+ Already have an account?{' '}
+
+ Sign in
+
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/frontend/src/test/setup.js b/frontend/src/test/setup.js
new file mode 100644
index 0000000000..0f9d923351
--- /dev/null
+++ b/frontend/src/test/setup.js
@@ -0,0 +1,32 @@
+import '@testing-library/jest-dom';
+
+// Mock IntersectionObserver
+global.IntersectionObserver = class IntersectionObserver {
+ constructor() {}
+ disconnect() {}
+ observe() {}
+ unobserve() {}
+};
+
+// Mock ResizeObserver
+global.ResizeObserver = class ResizeObserver {
+ constructor() {}
+ disconnect() {}
+ observe() {}
+ unobserve() {}
+};
+
+// Mock matchMedia
+Object.defineProperty(window, 'matchMedia', {
+ writable: true,
+ value: vi.fn().mockImplementation((query) => ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addListener: vi.fn(), // deprecated
+ removeListener: vi.fn(), // deprecated
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ dispatchEvent: vi.fn(),
+ })),
+});
\ No newline at end of file
diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js
new file mode 100644
index 0000000000..dc0bb2b7f8
--- /dev/null
+++ b/frontend/tailwind.config.js
@@ -0,0 +1,43 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: [
+ "./index.html",
+ "./src/**/*.{js,jsx,ts,tsx}",
+ ],
+ theme: {
+ extend: {
+ colors: {
+ primary: {
+ 50: '#f0f9ff',
+ 100: '#e0f2fe',
+ 500: '#0ea5e9',
+ 600: '#0284c7',
+ 700: '#0369a1',
+ },
+ secondary: {
+ 50: '#f7fee7',
+ 100: '#ecfccb',
+ 500: '#84cc16',
+ 600: '#65a30d',
+ 700: '#4d7c0f',
+ },
+ success: '#22c55e',
+ warning: '#f59e0b',
+ error: '#ef4444',
+ },
+ spacing: {
+ '18': '4.5rem',
+ '88': '22rem',
+ },
+ screens: {
+ 'xs': '320px',
+ },
+ maxWidth: {
+ '8xl': '88rem',
+ },
+ },
+ },
+ plugins: [],
+ darkMode: 'class',
+}
+
diff --git a/frontend/vite.config.js b/frontend/vite.config.js
index 5a33944a9b..18fd82041a 100644
--- a/frontend/vite.config.js
+++ b/frontend/vite.config.js
@@ -1,7 +1,33 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
+import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
+ css: {
+ postcss: './config/postcss.config.js',
+ },
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, './src'),
+ '@components': path.resolve(__dirname, './src/components'),
+ '@pages': path.resolve(__dirname, './src/pages'),
+ '@hooks': path.resolve(__dirname, './src/hooks'),
+ '@utils': path.resolve(__dirname, './src/utils'),
+ '@assets': path.resolve(__dirname, './src/assets'),
+ },
+ },
+ build: {
+ rollupOptions: {
+ output: {
+ manualChunks: {
+ vendor: ['react', 'react-dom'],
+ },
+ },
+ },
+ },
+ server: {
+ port: 3000,
+ },
})
diff --git a/package.json b/package.json
index 680d190772..17d32c2fa7 100644
--- a/package.json
+++ b/package.json
@@ -2,6 +2,14 @@
"name": "project-final-parent",
"version": "1.0.0",
"scripts": {
- "postinstall": "npm install --prefix backend"
+ "postinstall": "npm install --prefix backend && npm install --prefix frontend",
+ "dev": "npm run dev --prefix frontend",
+ "dev:backend": "npm run dev --prefix backend",
+ "dev:frontend": "npm run dev --prefix frontend",
+ "dev:all": "npx concurrently \"npm run dev:backend\" \"npm run dev:frontend\" --names \"backend,frontend\" --prefix name",
+ "build": "npm run build --prefix frontend",
+ "start": "npm start --prefix backend",
+ "lint": "npm run lint --prefix frontend",
+ "test": "npm run test --prefix frontend"
}
}
\ No newline at end of file
From bf4627276d69d013fed0c4263000a980c750a7a7 Mon Sep 17 00:00:00 2001
From: Caspian Almerud
Date: Sun, 13 Jul 2025 00:15:08 +0200
Subject: [PATCH 002/111] feat: implement chart components with Recharts
library
- Add SurvivalRateChart with pie chart and percentage display
- Add AverageHeightChart with line chart showing time series data
- Add CO2AbsorptionChart with bar chart for absorption tracking
- Update dashboard with responsive chart grid layout
- Replace placeholder content with real chart components
- Add custom tooltips and legends for better UX
---
frontend/package.json | 1 +
.../components/charts/AverageHeightChart.jsx | 96 +++++++++++++++
.../components/charts/CO2AbsorptionChart.jsx | 93 +++++++++++++++
.../components/charts/SurvivalRateChart.jsx | 112 ++++++++++++++++++
frontend/src/components/charts/index.js | 3 +
frontend/src/pages/DashboardPage.jsx | 82 ++++++++-----
6 files changed, 358 insertions(+), 29 deletions(-)
create mode 100644 frontend/src/components/charts/AverageHeightChart.jsx
create mode 100644 frontend/src/components/charts/CO2AbsorptionChart.jsx
create mode 100644 frontend/src/components/charts/SurvivalRateChart.jsx
create mode 100644 frontend/src/components/charts/index.js
diff --git a/frontend/package.json b/frontend/package.json
index a54983467c..67723d0e2a 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -22,6 +22,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.14.0",
+ "recharts": "^3.1.0",
"styled-components": "^6.1.19",
"tailwindcss": "^3.3.0",
"zustand": "^4.4.0"
diff --git a/frontend/src/components/charts/AverageHeightChart.jsx b/frontend/src/components/charts/AverageHeightChart.jsx
new file mode 100644
index 0000000000..82828b2bff
--- /dev/null
+++ b/frontend/src/components/charts/AverageHeightChart.jsx
@@ -0,0 +1,96 @@
+import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
+import styled from 'styled-components';
+
+const ChartContainer = styled.div`
+ background: white;
+ border-radius: 0.75rem;
+ padding: 1.5rem;
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+ border: 1px solid #e5e7eb;
+`;
+
+const ChartHeader = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 1rem;
+`;
+
+const ChartTitle = styled.h3`
+ font-size: 1.125rem;
+ font-weight: 600;
+ color: #111827;
+ margin: 0;
+`;
+
+const CustomTooltip = styled.div`
+ background: white;
+ border: 1px solid #e5e7eb;
+ border-radius: 0.5rem;
+ padding: 0.75rem;
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
+`;
+
+export const AverageHeightChart = ({
+ data = [
+ { month: 'Jan', height: 1.2 },
+ { month: 'Feb', height: 1.4 },
+ { month: 'Mar', height: 1.6 },
+ { month: 'Apr', height: 1.8 },
+ { month: 'May', height: 2.0 },
+ { month: 'Jun', height: 2.2 },
+ { month: 'Jul', height: 2.4 },
+ { month: 'Aug', height: 2.6 },
+ { month: 'Sep', height: 2.8 },
+ { month: 'Oct', height: 3.0 },
+ { month: 'Nov', height: 3.2 },
+ { month: 'Dec', height: 3.4 }
+ ]
+}) => {
+ const CustomTooltipContent = ({ active, payload, label }) => {
+ if (active && payload && payload.length) {
+ return (
+
+ {label}
+
+ Average Height: {payload[0].value}m
+
+
+ );
+ }
+ return null;
+ };
+
+ return (
+
+
+ Average Tree Height Over Time
+
+
+
+
+
+
+
+ } />
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/frontend/src/components/charts/CO2AbsorptionChart.jsx b/frontend/src/components/charts/CO2AbsorptionChart.jsx
new file mode 100644
index 0000000000..957d391b8d
--- /dev/null
+++ b/frontend/src/components/charts/CO2AbsorptionChart.jsx
@@ -0,0 +1,93 @@
+import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
+import styled from 'styled-components';
+
+const ChartContainer = styled.div`
+ background: white;
+ border-radius: 0.75rem;
+ padding: 1.5rem;
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+ border: 1px solid #e5e7eb;
+`;
+
+const ChartHeader = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 1rem;
+`;
+
+const ChartTitle = styled.h3`
+ font-size: 1.125rem;
+ font-weight: 600;
+ color: #111827;
+ margin: 0;
+`;
+
+const CustomTooltip = styled.div`
+ background: white;
+ border: 1px solid #e5e7eb;
+ border-radius: 0.5rem;
+ padding: 0.75rem;
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
+`;
+
+export const CO2AbsorptionChart = ({
+ data = [
+ { month: 'Jan', co2: 0.8 },
+ { month: 'Feb', co2: 1.2 },
+ { month: 'Mar', co2: 1.6 },
+ { month: 'Apr', co2: 2.0 },
+ { month: 'May', co2: 2.4 },
+ { month: 'Jun', co2: 2.8 },
+ { month: 'Jul', co2: 3.2 },
+ { month: 'Aug', co2: 3.6 },
+ { month: 'Sep', co2: 4.0 },
+ { month: 'Oct', co2: 4.4 },
+ { month: 'Nov', co2: 4.8 },
+ { month: 'Dec', co2: 5.2 }
+ ]
+}) => {
+ const CustomTooltipContent = ({ active, payload, label }) => {
+ if (active && payload && payload.length) {
+ return (
+
+ {label}
+
+ CO₂ Absorption: {payload[0].value} tons
+
+
+ );
+ }
+ return null;
+ };
+
+ return (
+
+
+ CO₂ Absorption Over Time
+
+
+
+
+
+
+
+ } />
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/frontend/src/components/charts/SurvivalRateChart.jsx b/frontend/src/components/charts/SurvivalRateChart.jsx
new file mode 100644
index 0000000000..c1f5a4861a
--- /dev/null
+++ b/frontend/src/components/charts/SurvivalRateChart.jsx
@@ -0,0 +1,112 @@
+import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from 'recharts';
+import styled from 'styled-components';
+
+const ChartContainer = styled.div`
+ background: white;
+ border-radius: 0.75rem;
+ padding: 1.5rem;
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+ border: 1px solid #e5e7eb;
+`;
+
+const ChartHeader = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 1rem;
+`;
+
+const ChartTitle = styled.h3`
+ font-size: 1.125rem;
+ font-weight: 600;
+ color: #111827;
+ margin: 0;
+`;
+
+const PercentageDisplay = styled.div`
+ text-align: center;
+ margin-bottom: 1rem;
+`;
+
+const PercentageValue = styled.div`
+ font-size: 2.5rem;
+ font-weight: 700;
+ color: #10b981;
+ line-height: 1;
+`;
+
+const PercentageLabel = styled.div`
+ font-size: 0.875rem;
+ color: #6b7280;
+ margin-top: 0.25rem;
+`;
+
+const CustomTooltip = styled.div`
+ background: white;
+ border: 1px solid #e5e7eb;
+ border-radius: 0.5rem;
+ padding: 0.75rem;
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
+`;
+
+export const SurvivalRateChart = ({ data = { survived: 95.2, lost: 4.8 } }) => {
+ const chartData = [
+ { name: 'Survived', value: data.survived, color: '#10b981' },
+ { name: 'Lost', value: data.lost, color: '#ef4444' }
+ ];
+
+ const CustomTooltipContent = ({ active, payload }) => {
+ if (active && payload && payload.length) {
+ return (
+
+ {payload[0].name}
+ {payload[0].value}%
+
+ );
+ }
+ return null;
+ };
+
+ return (
+
+
+ Tree Survival Rate
+
+
+
+ {data.survived}%
+ Survival Rate
+
+
+
+
+
+ {chartData.map((entry, index) => (
+ |
+ ))}
+
+ } />
+
+
+
+
+
+
+
Survived ({data.survived}%)
+
+
+
+
Lost ({data.lost}%)
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/frontend/src/components/charts/index.js b/frontend/src/components/charts/index.js
new file mode 100644
index 0000000000..62cfafaa1a
--- /dev/null
+++ b/frontend/src/components/charts/index.js
@@ -0,0 +1,3 @@
+export { SurvivalRateChart } from './SurvivalRateChart';
+export { AverageHeightChart } from './AverageHeightChart';
+export { CO2AbsorptionChart } from './CO2AbsorptionChart';
\ No newline at end of file
diff --git a/frontend/src/pages/DashboardPage.jsx b/frontend/src/pages/DashboardPage.jsx
index b0ac2a7cfc..44ae97ca90 100644
--- a/frontend/src/pages/DashboardPage.jsx
+++ b/frontend/src/pages/DashboardPage.jsx
@@ -1,5 +1,6 @@
import { useAuth } from '../contexts/AuthContext';
import styled from 'styled-components';
+import { SurvivalRateChart, AverageHeightChart, CO2AbsorptionChart } from '../components/charts';
const DashboardContainer = styled.div`
min-height: 100vh;
@@ -185,38 +186,61 @@ export const DashboardPage = () => {
- {/* Placeholder Content */}
+ {/* Charts Grid */}
+
+
+ {/* Additional Dashboard Content */}
-
Coming Soon
+
Forest Overview
- The dashboard is currently being developed. Soon you'll be able to:
+ Your forests are performing excellently with strong growth indicators and high survival rates.
-
-
-
-
-
- View real-time charts and analytics
-
-
-
-
-
- Explore interactive maps with tree locations
-
-
-
-
-
- Export data in CSV or XLSX format
-
-
-
-
-
- Filter and search through tree data
-
-
+
+
+
Recent Activity
+
+
+
+ New measurements recorded for Forest A
+
+
+
+ Survival rate improved by 2.3%
+
+
+
+ CO₂ absorption increased by 15%
+
+
+
+
+
Next Actions
+
+
+
+ Schedule maintenance for Forest B
+
+
+
+ Review growth data for Q4 report
+
+
+
+ Address low-performing areas in Forest C
+
+
+
+
From 9c5a1d99e0251159efd23fbea0aed126dace545e Mon Sep 17 00:00:00 2001
From: Caspian Almerud
Date: Sun, 13 Jul 2025 00:16:45 +0200
Subject: [PATCH 003/111] feat: implement global filters with date range and
forest selection
- Add DateRangePicker component with react-datepicker
- Add ForestSelector component with multi-select and search
- Add GlobalFilters component that combines both filters
- Implement debounced filter updates (1s delay)
- Add filter state management and active filter display
- Update dashboard to include global filters
- Add reset functionality and clear all filters
---
frontend/package.json | 1 +
.../components/filters/DateRangePicker.jsx | 175 +++++++++++++
.../src/components/filters/ForestSelector.jsx | 244 ++++++++++++++++++
.../src/components/filters/GlobalFilters.jsx | 187 ++++++++++++++
frontend/src/components/filters/index.js | 3 +
frontend/src/pages/DashboardPage.jsx | 12 +
6 files changed, 622 insertions(+)
create mode 100644 frontend/src/components/filters/DateRangePicker.jsx
create mode 100644 frontend/src/components/filters/ForestSelector.jsx
create mode 100644 frontend/src/components/filters/GlobalFilters.jsx
create mode 100644 frontend/src/components/filters/index.js
diff --git a/frontend/package.json b/frontend/package.json
index 67723d0e2a..20e947695f 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -20,6 +20,7 @@
"clsx": "^2.0.0",
"postcss": "^8.4.24",
"react": "^18.2.0",
+ "react-datepicker": "^8.4.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.14.0",
"recharts": "^3.1.0",
diff --git a/frontend/src/components/filters/DateRangePicker.jsx b/frontend/src/components/filters/DateRangePicker.jsx
new file mode 100644
index 0000000000..f15e01f99a
--- /dev/null
+++ b/frontend/src/components/filters/DateRangePicker.jsx
@@ -0,0 +1,175 @@
+import { useState, useEffect } from 'react';
+import DatePicker from 'react-datepicker';
+import styled from 'styled-components';
+import 'react-datepicker/dist/react-datepicker.css';
+
+const FilterContainer = styled.div`
+ background: white;
+ border-radius: 0.75rem;
+ padding: 1.5rem;
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+ border: 1px solid #e5e7eb;
+`;
+
+const FilterHeader = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 1rem;
+`;
+
+const FilterTitle = styled.h3`
+ font-size: 1.125rem;
+ font-weight: 600;
+ color: #111827;
+ margin: 0;
+`;
+
+const DateInputGroup = styled.div`
+ display: flex;
+ gap: 1rem;
+ align-items: center;
+ flex-wrap: wrap;
+`;
+
+const DateInputWrapper = styled.div`
+ display: flex;
+ flex-direction: column;
+ min-width: 150px;
+`;
+
+const DateLabel = styled.label`
+ font-size: 0.875rem;
+ font-weight: 500;
+ color: #374151;
+ margin-bottom: 0.5rem;
+`;
+
+const StyledDatePicker = styled(DatePicker)`
+ width: 100%;
+ padding: 0.5rem 0.75rem;
+ border: 1px solid #d1d5db;
+ border-radius: 0.5rem;
+ font-size: 0.875rem;
+ color: #111827;
+ background-color: white;
+
+ &:focus {
+ outline: none;
+ border-color: #10b981;
+ box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
+ }
+
+ &:hover {
+ border-color: #9ca3af;
+ }
+`;
+
+const ResetButton = styled.button`
+ padding: 0.5rem 1rem;
+ background-color: #f3f4f6;
+ color: #374151;
+ border: 1px solid #d1d5db;
+ border-radius: 0.5rem;
+ font-size: 0.875rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s;
+
+ &:hover {
+ background-color: #e5e7eb;
+ border-color: #9ca3af;
+ }
+
+ &:focus {
+ outline: none;
+ box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
+ }
+`;
+
+export const DateRangePicker = ({ onDateChange, initialStartDate, initialEndDate }) => {
+ const [startDate, setStartDate] = useState(initialStartDate || new Date(new Date().getFullYear(), 0, 1)); // Start of current year
+ const [endDate, setEndDate] = useState(initialEndDate || new Date()); // Today
+ const [isUpdating, setIsUpdating] = useState(false);
+
+ // Debounced update effect
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ if (onDateChange && startDate && endDate) {
+ onDateChange({ startDate, endDate });
+ }
+ }, 1000); // 1 second debounce
+
+ return () => clearTimeout(timer);
+ }, [startDate, endDate, onDateChange]);
+
+ const handleStartDateChange = (date) => {
+ setStartDate(date);
+ if (date && endDate && date > endDate) {
+ setEndDate(date);
+ }
+ };
+
+ const handleEndDateChange = (date) => {
+ setEndDate(date);
+ if (date && startDate && date < startDate) {
+ setStartDate(date);
+ }
+ };
+
+ const handleReset = () => {
+ const defaultStartDate = new Date(new Date().getFullYear(), 0, 1);
+ const defaultEndDate = new Date();
+ setStartDate(defaultStartDate);
+ setEndDate(defaultEndDate);
+ if (onDateChange) {
+ onDateChange({ startDate: defaultStartDate, endDate: defaultEndDate });
+ }
+ };
+
+ return (
+
+
+ Date Range
+
+ Reset
+
+
+
+
+
+ Start Date
+
+
+
+
+ to
+
+
+
+ End Date
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/frontend/src/components/filters/ForestSelector.jsx b/frontend/src/components/filters/ForestSelector.jsx
new file mode 100644
index 0000000000..bfb388451e
--- /dev/null
+++ b/frontend/src/components/filters/ForestSelector.jsx
@@ -0,0 +1,244 @@
+import { useState, useEffect, useRef } from 'react';
+import styled from 'styled-components';
+
+const FilterContainer = styled.div`
+ background: white;
+ border-radius: 0.75rem;
+ padding: 1.5rem;
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+ border: 1px solid #e5e7eb;
+`;
+
+const FilterHeader = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 1rem;
+`;
+
+const FilterTitle = styled.h3`
+ font-size: 1.125rem;
+ font-weight: 600;
+ color: #111827;
+ margin: 0;
+`;
+
+const SearchInput = styled.input`
+ width: 100%;
+ padding: 0.5rem 0.75rem;
+ border: 1px solid #d1d5db;
+ border-radius: 0.5rem;
+ font-size: 0.875rem;
+ color: #111827;
+ background-color: white;
+ margin-bottom: 1rem;
+
+ &:focus {
+ outline: none;
+ border-color: #10b981;
+ box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
+ }
+
+ &:hover {
+ border-color: #9ca3af;
+ }
+`;
+
+const ForestList = styled.div`
+ max-height: 200px;
+ overflow-y: auto;
+ border: 1px solid #e5e7eb;
+ border-radius: 0.5rem;
+ background-color: white;
+`;
+
+const ForestItem = styled.label`
+ display: flex;
+ align-items: center;
+ padding: 0.75rem;
+ cursor: pointer;
+ border-bottom: 1px solid #f3f4f6;
+ transition: background-color 0.2s;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ &:hover {
+ background-color: #f9fafb;
+ }
+
+ &:focus-within {
+ background-color: #f0fdf4;
+ }
+`;
+
+const Checkbox = styled.input`
+ margin-right: 0.75rem;
+ width: 1rem;
+ height: 1rem;
+ accent-color: #10b981;
+`;
+
+const ForestInfo = styled.div`
+ flex: 1;
+`;
+
+const ForestName = styled.div`
+ font-weight: 500;
+ color: #111827;
+ font-size: 0.875rem;
+`;
+
+const ForestDetails = styled.div`
+ font-size: 0.75rem;
+ color: #6b7280;
+ margin-top: 0.25rem;
+`;
+
+const SelectedCount = styled.div`
+ font-size: 0.75rem;
+ color: #6b7280;
+ margin-top: 0.5rem;
+`;
+
+const ResetButton = styled.button`
+ padding: 0.5rem 1rem;
+ background-color: #f3f4f6;
+ color: #374151;
+ border: 1px solid #d1d5db;
+ border-radius: 0.5rem;
+ font-size: 0.875rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s;
+
+ &:hover {
+ background-color: #e5e7eb;
+ border-color: #9ca3af;
+ }
+
+ &:focus {
+ outline: none;
+ box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
+ }
+`;
+
+export const ForestSelector = ({
+ onForestChange,
+ forests = [
+ { id: 1, name: 'Forest A', region: 'North Region', treeCount: 2500, area: '150 ha' },
+ { id: 2, name: 'Forest B', region: 'South Region', treeCount: 3200, area: '200 ha' },
+ { id: 3, name: 'Forest C', region: 'East Region', treeCount: 1800, area: '120 ha' },
+ { id: 4, name: 'Forest D', region: 'West Region', treeCount: 2750, area: '180 ha' },
+ { id: 5, name: 'Forest E', region: 'Central Region', treeCount: 4100, area: '250 ha' }
+ ],
+ initialSelected = []
+}) => {
+ const [selectedForests, setSelectedForests] = useState(initialSelected);
+ const [searchTerm, setSearchTerm] = useState('');
+ const [filteredForests, setFilteredForests] = useState(forests);
+
+ // Filter forests based on search term
+ useEffect(() => {
+ const filtered = forests.filter(forest =>
+ forest.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ forest.region.toLowerCase().includes(searchTerm.toLowerCase())
+ );
+ setFilteredForests(filtered);
+ }, [searchTerm, forests]);
+
+ // Debounced update effect
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ if (onForestChange) {
+ onForestChange(selectedForests);
+ }
+ }, 1000); // 1 second debounce
+
+ return () => clearTimeout(timer);
+ }, [selectedForests, onForestChange]);
+
+ const handleForestToggle = (forestId) => {
+ setSelectedForests(prev => {
+ if (prev.includes(forestId)) {
+ return prev.filter(id => id !== forestId);
+ } else {
+ return [...prev, forestId];
+ }
+ });
+ };
+
+ const handleSelectAll = () => {
+ setSelectedForests(forests.map(forest => forest.id));
+ };
+
+ const handleSelectNone = () => {
+ setSelectedForests([]);
+ };
+
+ const handleReset = () => {
+ setSelectedForests(initialSelected);
+ setSearchTerm('');
+ };
+
+ return (
+
+
+ Forest Selection
+
+ Reset
+
+
+
+ setSearchTerm(e.target.value)}
+ />
+
+
+
+ Select All
+
+
+ Select None
+
+
+
+
+ {filteredForests.map(forest => (
+
+ handleForestToggle(forest.id)}
+ />
+
+ {forest.name}
+
+ {forest.region} • {forest.treeCount} trees • {forest.area}
+
+
+
+ ))}
+ {filteredForests.length === 0 && (
+
+ No forests found matching your search.
+
+ )}
+
+
+
+ {selectedForests.length} of {forests.length} forests selected
+
+
+ );
+};
\ No newline at end of file
diff --git a/frontend/src/components/filters/GlobalFilters.jsx b/frontend/src/components/filters/GlobalFilters.jsx
new file mode 100644
index 0000000000..1a4cb3a0cc
--- /dev/null
+++ b/frontend/src/components/filters/GlobalFilters.jsx
@@ -0,0 +1,187 @@
+import { useState, useEffect } from 'react';
+import styled from 'styled-components';
+import { DateRangePicker } from './DateRangePicker';
+import { ForestSelector } from './ForestSelector';
+
+const FiltersContainer = styled.div`
+ background: #f9fafb;
+ border-radius: 0.75rem;
+ padding: 1.5rem;
+ margin-bottom: 2rem;
+`;
+
+const FiltersHeader = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 1.5rem;
+`;
+
+const FiltersTitle = styled.h2`
+ font-size: 1.5rem;
+ font-weight: 700;
+ color: #111827;
+ margin: 0;
+`;
+
+const FiltersSubtitle = styled.p`
+ color: #6b7280;
+ margin: 0.5rem 0 0 0;
+ font-size: 0.875rem;
+`;
+
+const FiltersGrid = styled.div`
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 1.5rem;
+
+ @media (min-width: 768px) {
+ grid-template-columns: 1fr 1fr;
+ }
+
+ @media (min-width: 1024px) {
+ grid-template-columns: 1fr 1fr;
+ }
+`;
+
+const ActiveFilters = styled.div`
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ margin-top: 1rem;
+ padding-top: 1rem;
+ border-top: 1px solid #e5e7eb;
+`;
+
+const FilterTag = styled.span`
+ display: inline-flex;
+ align-items: center;
+ padding: 0.25rem 0.75rem;
+ background-color: #10b981;
+ color: white;
+ border-radius: 9999px;
+ font-size: 0.75rem;
+ font-weight: 500;
+`;
+
+const ClearAllButton = styled.button`
+ padding: 0.5rem 1rem;
+ background-color: #ef4444;
+ color: white;
+ border: none;
+ border-radius: 0.5rem;
+ font-size: 0.875rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s;
+
+ &:hover {
+ background-color: #dc2626;
+ }
+
+ &:focus {
+ outline: none;
+ box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
+ }
+`;
+
+export const GlobalFilters = ({ onFiltersChange, initialFilters = {} }) => {
+ const [filters, setFilters] = useState({
+ dateRange: {
+ startDate: new Date(new Date().getFullYear(), 0, 1),
+ endDate: new Date()
+ },
+ selectedForests: [],
+ ...initialFilters
+ });
+
+ const [activeFilters, setActiveFilters] = useState([]);
+
+ // Update active filters display
+ useEffect(() => {
+ const active = [];
+
+ if (filters.dateRange.startDate && filters.dateRange.endDate) {
+ const startDate = filters.dateRange.startDate.toLocaleDateString();
+ const endDate = filters.dateRange.endDate.toLocaleDateString();
+ active.push(`Date: ${startDate} - ${endDate}`);
+ }
+
+ if (filters.selectedForests.length > 0) {
+ active.push(`${filters.selectedForests.length} forests selected`);
+ }
+
+ setActiveFilters(active);
+ }, [filters]);
+
+ // Notify parent component of filter changes
+ useEffect(() => {
+ if (onFiltersChange) {
+ onFiltersChange(filters);
+ }
+ }, [filters, onFiltersChange]);
+
+ const handleDateChange = (dateRange) => {
+ setFilters(prev => ({
+ ...prev,
+ dateRange
+ }));
+ };
+
+ const handleForestChange = (selectedForests) => {
+ setFilters(prev => ({
+ ...prev,
+ selectedForests
+ }));
+ };
+
+ const handleClearAll = () => {
+ setFilters({
+ dateRange: {
+ startDate: new Date(new Date().getFullYear(), 0, 1),
+ endDate: new Date()
+ },
+ selectedForests: []
+ });
+ };
+
+ return (
+
+
+
+ Global Filters
+
+ Filter your data by date range and forest selection
+
+
+ {activeFilters.length > 0 && (
+
+ Clear All
+
+ )}
+
+
+
+
+
+
+
+ {activeFilters.length > 0 && (
+
+ {activeFilters.map((filter, index) => (
+
+ {filter}
+
+ ))}
+
+ )}
+
+ );
+};
\ No newline at end of file
diff --git a/frontend/src/components/filters/index.js b/frontend/src/components/filters/index.js
new file mode 100644
index 0000000000..c904774420
--- /dev/null
+++ b/frontend/src/components/filters/index.js
@@ -0,0 +1,3 @@
+export { DateRangePicker } from './DateRangePicker';
+export { ForestSelector } from './ForestSelector';
+export { GlobalFilters } from './GlobalFilters';
\ No newline at end of file
diff --git a/frontend/src/pages/DashboardPage.jsx b/frontend/src/pages/DashboardPage.jsx
index 44ae97ca90..e32c628077 100644
--- a/frontend/src/pages/DashboardPage.jsx
+++ b/frontend/src/pages/DashboardPage.jsx
@@ -1,6 +1,8 @@
+import { useState } from 'react';
import { useAuth } from '../contexts/AuthContext';
import styled from 'styled-components';
import { SurvivalRateChart, AverageHeightChart, CO2AbsorptionChart } from '../components/charts';
+import { GlobalFilters } from '../components/filters';
const DashboardContainer = styled.div`
min-height: 100vh;
@@ -32,11 +34,18 @@ const MainContent = styled.main`
export const DashboardPage = () => {
const { user, logout, isAdmin } = useAuth();
+ const [filters, setFilters] = useState({});
const handleLogout = () => {
logout();
};
+ const handleFiltersChange = (newFilters) => {
+ setFilters(newFilters);
+ // TODO: Update charts and data based on filters
+ console.log('Filters changed:', newFilters);
+ };
+
return (
{/* Header */}
@@ -127,6 +136,9 @@ export const DashboardPage = () => {
+ {/* Global Filters */}
+
+
{/* Stats Cards */}
From 4541c9d2923585ce7e73a2158ee541b4c6ede109 Mon Sep 17 00:00:00 2001
From: Caspian Almerud
Date: Sun, 13 Jul 2025 00:18:57 +0200
Subject: [PATCH 004/111] feat: implement interactive forest map with Leaflet
- Add ForestMap component with custom tree markers
- Create MapPage with tree detail panel
- Integrate global filters with map view
- Add map controls and legend
- Implement tree selection and detail display
- Add map route to App.jsx
- Update navigation between dashboard and map
- Add custom tree icons with health status colors
---
frontend/package.json | 2 +
frontend/src/App.jsx | 9 +
frontend/src/components/map/ForestMap.jsx | 242 ++++++++++++++++++++
frontend/src/pages/DashboardPage.jsx | 2 +-
frontend/src/pages/MapPage.jsx | 260 ++++++++++++++++++++++
5 files changed, 514 insertions(+), 1 deletion(-)
create mode 100644 frontend/src/components/map/ForestMap.jsx
create mode 100644 frontend/src/pages/MapPage.jsx
diff --git a/frontend/package.json b/frontend/package.json
index 20e947695f..61686db9c5 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -18,10 +18,12 @@
"dependencies": {
"autoprefixer": "^10.4.14",
"clsx": "^2.0.0",
+ "leaflet": "^1.9.4",
"postcss": "^8.4.24",
"react": "^18.2.0",
"react-datepicker": "^8.4.0",
"react-dom": "^18.2.0",
+ "react-leaflet": "^5.0.0",
"react-router-dom": "^6.14.0",
"recharts": "^3.1.0",
"styled-components": "^6.1.19",
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 0e69f49ae9..bf74567bb8 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -3,6 +3,7 @@ import { LandingPage } from './pages/LandingPage';
import { LoginPage } from './pages/LoginPage';
import { RegisterPage } from './pages/RegisterPage';
import { DashboardPage } from './pages/DashboardPage';
+import { MapPage } from './pages/MapPage';
import { ProtectedRoute } from './components/auth/ProtectedRoute';
import { AuthProvider } from './contexts/AuthContext';
@@ -27,6 +28,14 @@ export const App = () => {
}
/>
+
+
+
+ }
+ />
diff --git a/frontend/src/components/map/ForestMap.jsx b/frontend/src/components/map/ForestMap.jsx
new file mode 100644
index 0000000000..443930dd28
--- /dev/null
+++ b/frontend/src/components/map/ForestMap.jsx
@@ -0,0 +1,242 @@
+import { useState, useEffect } from 'react';
+import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet';
+import L from 'leaflet';
+import styled from 'styled-components';
+import 'leaflet/dist/leaflet.css';
+
+// Fix for default markers in react-leaflet
+delete L.Icon.Default.prototype._getIconUrl;
+L.Icon.Default.mergeOptions({
+ iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
+ iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
+ shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
+});
+
+const MapContainerStyled = styled.div`
+ height: 600px;
+ width: 100%;
+ border-radius: 0.75rem;
+ overflow: hidden;
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+ border: 1px solid #e5e7eb;
+`;
+
+const MapHeader = styled.div`
+ background: white;
+ padding: 1rem 1.5rem;
+ border-bottom: 1px solid #e5e7eb;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+`;
+
+const MapTitle = styled.h3`
+ font-size: 1.125rem;
+ font-weight: 600;
+ color: #111827;
+ margin: 0;
+`;
+
+const MapControls = styled.div`
+ display: flex;
+ gap: 0.5rem;
+`;
+
+const ControlButton = styled.button`
+ padding: 0.5rem;
+ background: white;
+ border: 1px solid #d1d5db;
+ border-radius: 0.375rem;
+ cursor: pointer;
+ transition: all 0.2s;
+
+ &:hover {
+ background: #f9fafb;
+ border-color: #9ca3af;
+ }
+
+ &:focus {
+ outline: none;
+ box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
+ }
+`;
+
+// Custom tree marker icon
+const createTreeIcon = (type = 'healthy') => {
+ const colors = {
+ healthy: '#10b981',
+ warning: '#f59e0b',
+ critical: '#ef4444'
+ };
+
+ return L.divIcon({
+ className: 'custom-tree-marker',
+ html: `
+
+ 🌳
+
+ `,
+ iconSize: [20, 20],
+ iconAnchor: [10, 10]
+ });
+};
+
+// Map controller component
+const MapController = ({ onZoomChange }) => {
+ const map = useMap();
+
+ useEffect(() => {
+ const handleZoomEnd = () => {
+ onZoomChange(map.getZoom());
+ };
+
+ map.on('zoomend', handleZoomEnd);
+ return () => {
+ map.off('zoomend', handleZoomEnd);
+ };
+ }, [map, onZoomChange]);
+
+ return null;
+};
+
+export const ForestMap = ({
+ trees = [
+ { id: 1, lat: 59.3293, lng: 18.0686, name: 'Tree A-001', health: 'healthy', height: 2.4, species: 'Pine' },
+ { id: 2, lat: 59.3300, lng: 18.0690, name: 'Tree A-002', health: 'healthy', height: 2.1, species: 'Oak' },
+ { id: 3, lat: 59.3285, lng: 18.0675, name: 'Tree A-003', health: 'warning', height: 1.8, species: 'Birch' },
+ { id: 4, lat: 59.3310, lng: 18.0700, name: 'Tree A-004', health: 'critical', height: 1.5, species: 'Spruce' },
+ { id: 5, lat: 59.3275, lng: 18.0660, name: 'Tree A-005', health: 'healthy', height: 2.7, species: 'Pine' },
+ { id: 6, lat: 59.3320, lng: 18.0710, name: 'Tree A-006', health: 'healthy', height: 2.3, species: 'Oak' },
+ { id: 7, lat: 59.3265, lng: 18.0650, name: 'Tree A-007', health: 'warning', height: 1.9, species: 'Birch' },
+ { id: 8, lat: 59.3330, lng: 18.0720, name: 'Tree A-008', health: 'healthy', height: 2.5, species: 'Spruce' }
+ ],
+ onTreeSelect,
+ filters = {}
+}) => {
+ const [zoom, setZoom] = useState(13);
+ const [selectedTree, setSelectedTree] = useState(null);
+
+ // Filter trees based on applied filters
+ const filteredTrees = trees.filter(tree => {
+ // TODO: Apply date and forest filters when backend is ready
+ return true;
+ });
+
+ const handleTreeClick = (tree) => {
+ setSelectedTree(tree);
+ if (onTreeSelect) {
+ onTreeSelect(tree);
+ }
+ };
+
+ const handleZoomChange = (newZoom) => {
+ setZoom(newZoom);
+ };
+
+ const centerMap = () => {
+ // TODO: Center map on selected forest or all trees
+ };
+
+ const fitBounds = () => {
+ // TODO: Fit map to show all trees
+ };
+
+ return (
+
+
+ Forest Map
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {filteredTrees.map(tree => (
+ handleTreeClick(tree)
+ }}
+ >
+
+
+
{tree.name}
+
Species: {tree.species}
+
Height: {tree.height}m
+
Health:
+
+ {tree.health}
+
+
+
+
+
+ ))}
+
+
+
+ {/* Map Legend */}
+
+
Legend
+
+
+
+
+
+ Zoom level: {zoom}
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/frontend/src/pages/DashboardPage.jsx b/frontend/src/pages/DashboardPage.jsx
index e32c628077..56a7a60db6 100644
--- a/frontend/src/pages/DashboardPage.jsx
+++ b/frontend/src/pages/DashboardPage.jsx
@@ -92,7 +92,7 @@ export const DashboardPage = () => {
Dashboard
diff --git a/frontend/src/pages/MapPage.jsx b/frontend/src/pages/MapPage.jsx
new file mode 100644
index 0000000000..f49ea5616b
--- /dev/null
+++ b/frontend/src/pages/MapPage.jsx
@@ -0,0 +1,260 @@
+import { useState } from 'react';
+import { useAuth } from '../contexts/AuthContext';
+import styled from 'styled-components';
+import { ForestMap } from '../components/map/ForestMap';
+import { GlobalFilters } from '../components/filters';
+
+const MapPageContainer = styled.div`
+ min-height: 100vh;
+ background-color: #f9fafb;
+`;
+
+const Header = styled.header`
+ background: white;
+ border-bottom: 1px solid #e5e7eb;
+ padding: 1rem 0;
+`;
+
+const Sidebar = styled.aside`
+ background: white;
+ border-right: 1px solid #e5e7eb;
+ width: 250px;
+ position: fixed;
+ top: 0;
+ left: 0;
+ height: 100vh;
+ overflow-y: auto;
+ z-index: 10;
+`;
+
+const MainContent = styled.main`
+ margin-left: 250px;
+ padding: 2rem;
+`;
+
+const TreeDetailPanel = styled.div`
+ background: white;
+ border-radius: 0.75rem;
+ padding: 1.5rem;
+ margin-bottom: 1.5rem;
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+ border: 1px solid #e5e7eb;
+`;
+
+const TreeDetailHeader = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 1rem;
+`;
+
+const TreeDetailTitle = styled.h3`
+ font-size: 1.125rem;
+ font-weight: 600;
+ color: #111827;
+ margin: 0;
+`;
+
+const CloseButton = styled.button`
+ padding: 0.5rem;
+ background: #f3f4f6;
+ border: 1px solid #d1d5db;
+ border-radius: 0.375rem;
+ cursor: pointer;
+ transition: all 0.2s;
+
+ &:hover {
+ background: #e5e7eb;
+ }
+`;
+
+export const MapPage = () => {
+ const { user, logout, isAdmin } = useAuth();
+ const [filters, setFilters] = useState({});
+ const [selectedTree, setSelectedTree] = useState(null);
+
+ const handleLogout = () => {
+ logout();
+ };
+
+ const handleFiltersChange = (newFilters) => {
+ setFilters(newFilters);
+ // TODO: Update map data based on filters
+ console.log('Map filters changed:', newFilters);
+ };
+
+ const handleTreeSelect = (tree) => {
+ setSelectedTree(tree);
+ };
+
+ const handleCloseTreeDetail = () => {
+ setSelectedTree(null);
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
Nanwa Map View
+
+
+
+ Welcome, {user?.name}
+ {isAdmin() && (
+
+ Admin
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ {/* Sidebar */}
+
+
+
+
+ {/* Main Content */}
+
+
+ {/* Page Header */}
+
+
Forest Map
+
+ Explore your forests and individual trees with interactive mapping.
+
+
+
+ {/* Global Filters */}
+
+
+ {/* Selected Tree Detail */}
+ {selectedTree && (
+
+
+ Tree Details
+
+
+
+
+
+
+
+
+
+
Basic Information
+
+
+ Name:
+ {selectedTree.name}
+
+
+ Species:
+ {selectedTree.species}
+
+
+ Height:
+ {selectedTree.height}m
+
+
+ Health:
+
+ {selectedTree.health}
+
+
+
+
+
+
+
Location
+
+
+ Latitude:
+ {selectedTree.lat}
+
+
+ Longitude:
+ {selectedTree.lng}
+
+
+
+
+
+ View Full Details
+
+
+
+
+
+ )}
+
+ {/* Map */}
+
+
+
+
+ );
+};
\ No newline at end of file
From 98377b647e8f538f72d8776cb727858902bf7a74 Mon Sep 17 00:00:00 2001
From: Caspian Almerud
Date: Sun, 13 Jul 2025 00:47:33 +0200
Subject: [PATCH 005/111] fixed infinite loop bug
---
frontend/package.json | 2 +-
.../components/filters/DateRangePicker.jsx | 27 +-
.../src/components/filters/ForestSelector.jsx | 57 ++--
.../src/components/filters/GlobalFilters.jsx | 59 ++--
frontend/src/components/map/ForestMap.jsx | 26 +-
frontend/src/pages/DashboardPage.jsx | 6 +-
frontend/src/pages/LoginPage.jsx | 256 +++++++++---------
frontend/src/pages/MapPage.jsx | 6 +-
8 files changed, 239 insertions(+), 200 deletions(-)
diff --git a/frontend/package.json b/frontend/package.json
index 61686db9c5..01d79c0516 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -23,7 +23,7 @@
"react": "^18.2.0",
"react-datepicker": "^8.4.0",
"react-dom": "^18.2.0",
- "react-leaflet": "^5.0.0",
+ "react-leaflet": "^4.2.1",
"react-router-dom": "^6.14.0",
"recharts": "^3.1.0",
"styled-components": "^6.1.19",
diff --git a/frontend/src/components/filters/DateRangePicker.jsx b/frontend/src/components/filters/DateRangePicker.jsx
index f15e01f99a..6699dcc873 100644
--- a/frontend/src/components/filters/DateRangePicker.jsx
+++ b/frontend/src/components/filters/DateRangePicker.jsx
@@ -1,4 +1,4 @@
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useRef } from 'react';
import DatePicker from 'react-datepicker';
import styled from 'styled-components';
import 'react-datepicker/dist/react-datepicker.css';
@@ -91,17 +91,30 @@ export const DateRangePicker = ({ onDateChange, initialStartDate, initialEndDate
const [startDate, setStartDate] = useState(initialStartDate || new Date(new Date().getFullYear(), 0, 1)); // Start of current year
const [endDate, setEndDate] = useState(initialEndDate || new Date()); // Today
const [isUpdating, setIsUpdating] = useState(false);
+ const onDateChangeRef = useRef(onDateChange);
+ const hasMounted = useRef(false);
- // Debounced update effect
+ // Keep the ref up to date
useEffect(() => {
+ onDateChangeRef.current = onDateChange;
+ }, [onDateChange]);
+
+ // Debounced update effect - FIXED: removed onDateChange from dependencies and prevent initial call
+ useEffect(() => {
+ // Don't call callback on initial mount
+ if (!hasMounted.current) {
+ hasMounted.current = true;
+ return;
+ }
+
const timer = setTimeout(() => {
- if (onDateChange && startDate && endDate) {
- onDateChange({ startDate, endDate });
+ if (onDateChangeRef.current && startDate && endDate) {
+ onDateChangeRef.current({ startDate, endDate });
}
}, 1000); // 1 second debounce
return () => clearTimeout(timer);
- }, [startDate, endDate, onDateChange]);
+ }, [startDate, endDate]); // Only depend on the date values, not the callback function
const handleStartDateChange = (date) => {
setStartDate(date);
@@ -122,8 +135,8 @@ export const DateRangePicker = ({ onDateChange, initialStartDate, initialEndDate
const defaultEndDate = new Date();
setStartDate(defaultStartDate);
setEndDate(defaultEndDate);
- if (onDateChange) {
- onDateChange({ startDate: defaultStartDate, endDate: defaultEndDate });
+ if (onDateChangeRef.current) {
+ onDateChangeRef.current({ startDate: defaultStartDate, endDate: defaultEndDate });
}
};
diff --git a/frontend/src/components/filters/ForestSelector.jsx b/frontend/src/components/filters/ForestSelector.jsx
index bfb388451e..f1d9278161 100644
--- a/frontend/src/components/filters/ForestSelector.jsx
+++ b/frontend/src/components/filters/ForestSelector.jsx
@@ -1,4 +1,4 @@
-import { useState, useEffect, useRef } from 'react';
+import { useState, useEffect } from 'react';
import styled from 'styled-components';
const FilterContainer = styled.div`
@@ -124,18 +124,20 @@ const ResetButton = styled.button`
}
`;
+// Default forests array moved outside component to make it stable
+const DEFAULT_FORESTS = [
+ { id: 1, name: 'Forest A', region: 'North Region', treeCount: 2500, area: '150 ha' },
+ { id: 2, name: 'Forest B', region: 'South Region', treeCount: 3200, area: '200 ha' },
+ { id: 3, name: 'Forest C', region: 'East Region', treeCount: 1800, area: '120 ha' },
+ { id: 4, name: 'Forest D', region: 'West Region', treeCount: 2750, area: '180 ha' },
+ { id: 5, name: 'Forest E', region: 'Central Region', treeCount: 4100, area: '250 ha' }
+];
+
export const ForestSelector = ({
- onForestChange,
- forests = [
- { id: 1, name: 'Forest A', region: 'North Region', treeCount: 2500, area: '150 ha' },
- { id: 2, name: 'Forest B', region: 'South Region', treeCount: 3200, area: '200 ha' },
- { id: 3, name: 'Forest C', region: 'East Region', treeCount: 1800, area: '120 ha' },
- { id: 4, name: 'Forest D', region: 'West Region', treeCount: 2750, area: '180 ha' },
- { id: 5, name: 'Forest E', region: 'Central Region', treeCount: 4100, area: '250 ha' }
- ],
- initialSelected = []
+ selectedForests = [],
+ onChange,
+ forests = DEFAULT_FORESTS
}) => {
- const [selectedForests, setSelectedForests] = useState(initialSelected);
const [searchTerm, setSearchTerm] = useState('');
const [filteredForests, setFilteredForests] = useState(forests);
@@ -148,37 +150,32 @@ export const ForestSelector = ({
setFilteredForests(filtered);
}, [searchTerm, forests]);
- // Debounced update effect
- useEffect(() => {
- const timer = setTimeout(() => {
- if (onForestChange) {
- onForestChange(selectedForests);
- }
- }, 1000); // 1 second debounce
-
- return () => clearTimeout(timer);
- }, [selectedForests, onForestChange]);
-
const handleForestToggle = (forestId) => {
- setSelectedForests(prev => {
- if (prev.includes(forestId)) {
- return prev.filter(id => id !== forestId);
+ if (onChange) {
+ if (selectedForests.includes(forestId)) {
+ onChange(selectedForests.filter(id => id !== forestId));
} else {
- return [...prev, forestId];
+ onChange([...selectedForests, forestId]);
}
- });
+ }
};
const handleSelectAll = () => {
- setSelectedForests(forests.map(forest => forest.id));
+ if (onChange) {
+ onChange(forests.map(forest => forest.id));
+ }
};
const handleSelectNone = () => {
- setSelectedForests([]);
+ if (onChange) {
+ onChange([]);
+ }
};
const handleReset = () => {
- setSelectedForests(initialSelected);
+ if (onChange) {
+ onChange([]);
+ }
setSearchTerm('');
};
diff --git a/frontend/src/components/filters/GlobalFilters.jsx b/frontend/src/components/filters/GlobalFilters.jsx
index 1a4cb3a0cc..e14a82268f 100644
--- a/frontend/src/components/filters/GlobalFilters.jsx
+++ b/frontend/src/components/filters/GlobalFilters.jsx
@@ -1,4 +1,4 @@
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useRef, useCallback } from 'react';
import styled from 'styled-components';
import { DateRangePicker } from './DateRangePicker';
import { ForestSelector } from './ForestSelector';
@@ -86,56 +86,79 @@ const ClearAllButton = styled.button`
`;
export const GlobalFilters = ({ onFiltersChange, initialFilters = {} }) => {
- const [filters, setFilters] = useState({
+ // Only use initialFilters on mount
+ const didInit = useRef(false);
+ const onFiltersChangeRef = useRef(onFiltersChange);
+ const hasMounted = useRef(false);
+
+ // Keep the ref up to date
+ useEffect(() => {
+ onFiltersChangeRef.current = onFiltersChange;
+ }, [onFiltersChange]);
+
+ const [filters, setFilters] = useState(() => ({
dateRange: {
startDate: new Date(new Date().getFullYear(), 0, 1),
endDate: new Date()
},
selectedForests: [],
...initialFilters
- });
+ }));
+
+ useEffect(() => {
+ if (!didInit.current && Object.keys(initialFilters).length > 0) {
+ setFilters(prev => ({ ...prev, ...initialFilters }));
+ didInit.current = true;
+ }
+ }, []); // Only run on mount
const [activeFilters, setActiveFilters] = useState([]);
// Update active filters display
useEffect(() => {
const active = [];
-
if (filters.dateRange.startDate && filters.dateRange.endDate) {
const startDate = filters.dateRange.startDate.toLocaleDateString();
const endDate = filters.dateRange.endDate.toLocaleDateString();
active.push(`Date: ${startDate} - ${endDate}`);
}
-
if (filters.selectedForests.length > 0) {
active.push(`${filters.selectedForests.length} forests selected`);
}
-
setActiveFilters(active);
}, [filters]);
- // Notify parent component of filter changes
+ // Debounced notify parent of filter changes - FIXED: removed onFiltersChange from dependencies and prevent initial call
useEffect(() => {
- if (onFiltersChange) {
- onFiltersChange(filters);
+ // Don't call callback on initial mount
+ if (!hasMounted.current) {
+ hasMounted.current = true;
+ return;
}
- }, [filters, onFiltersChange]);
- const handleDateChange = (dateRange) => {
+ const timer = setTimeout(() => {
+ if (onFiltersChangeRef.current) {
+ onFiltersChangeRef.current(filters);
+ }
+ }, 1000); // 1 second debounce
+ return () => clearTimeout(timer);
+ }, [filters]); // Only depend on filters, not the callback function
+
+ const handleDateChange = useCallback((dateRange) => {
setFilters(prev => ({
...prev,
dateRange
}));
- };
+ }, []);
- const handleForestChange = (selectedForests) => {
+ const handleForestChange = useCallback((selectedForests) => {
setFilters(prev => ({
...prev,
selectedForests
}));
- };
+ }, []);
- const handleClearAll = () => {
+ const handleClearAll = useCallback(() => {
setFilters({
dateRange: {
startDate: new Date(new Date().getFullYear(), 0, 1),
@@ -143,7 +166,7 @@ export const GlobalFilters = ({ onFiltersChange, initialFilters = {} }) => {
},
selectedForests: []
});
- };
+ }, []);
return (
@@ -168,8 +191,8 @@ export const GlobalFilters = ({ onFiltersChange, initialFilters = {} }) => {
initialEndDate={filters.dateRange.endDate}
/>
diff --git a/frontend/src/components/map/ForestMap.jsx b/frontend/src/components/map/ForestMap.jsx
index 443930dd28..b423ba7532 100644
--- a/frontend/src/components/map/ForestMap.jsx
+++ b/frontend/src/components/map/ForestMap.jsx
@@ -1,4 +1,4 @@
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useCallback } from 'react';
import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet';
import L from 'leaflet';
import styled from 'styled-components';
@@ -94,20 +94,22 @@ const createTreeIcon = (type = 'healthy') => {
});
};
-// Map controller component
+// Map controller component - FIXED: removed function dependency from useEffect
const MapController = ({ onZoomChange }) => {
const map = useMap();
useEffect(() => {
const handleZoomEnd = () => {
- onZoomChange(map.getZoom());
+ if (onZoomChange) {
+ onZoomChange(map.getZoom());
+ }
};
map.on('zoomend', handleZoomEnd);
return () => {
map.off('zoomend', handleZoomEnd);
};
- }, [map, onZoomChange]);
+ }, [map]); // Only depend on map, not the callback function
return null;
};
@@ -135,24 +137,24 @@ export const ForestMap = ({
return true;
});
- const handleTreeClick = (tree) => {
+ const handleTreeClick = useCallback((tree) => {
setSelectedTree(tree);
if (onTreeSelect) {
onTreeSelect(tree);
}
- };
+ }, [onTreeSelect]);
- const handleZoomChange = (newZoom) => {
+ const handleZoomChange = useCallback((newZoom) => {
setZoom(newZoom);
- };
+ }, []);
- const centerMap = () => {
+ const centerMap = useCallback(() => {
// TODO: Center map on selected forest or all trees
- };
+ }, []);
- const fitBounds = () => {
+ const fitBounds = useCallback(() => {
// TODO: Fit map to show all trees
- };
+ }, []);
return (
diff --git a/frontend/src/pages/DashboardPage.jsx b/frontend/src/pages/DashboardPage.jsx
index 56a7a60db6..6f174d53ee 100644
--- a/frontend/src/pages/DashboardPage.jsx
+++ b/frontend/src/pages/DashboardPage.jsx
@@ -1,4 +1,4 @@
-import { useState } from 'react';
+import { useState, useCallback } from 'react';
import { useAuth } from '../contexts/AuthContext';
import styled from 'styled-components';
import { SurvivalRateChart, AverageHeightChart, CO2AbsorptionChart } from '../components/charts';
@@ -40,11 +40,11 @@ export const DashboardPage = () => {
logout();
};
- const handleFiltersChange = (newFilters) => {
+ const handleFiltersChange = useCallback((newFilters) => {
setFilters(newFilters);
// TODO: Update charts and data based on filters
console.log('Filters changed:', newFilters);
- };
+ }, []);
return (
diff --git a/frontend/src/pages/LoginPage.jsx b/frontend/src/pages/LoginPage.jsx
index cb1f784baf..370136bdc9 100644
--- a/frontend/src/pages/LoginPage.jsx
+++ b/frontend/src/pages/LoginPage.jsx
@@ -2,6 +2,7 @@ import { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import styled from 'styled-components';
+import { Navbar } from '../components/ui/Navbar';
const LoginContainer = styled.div`
min-height: 100vh;
@@ -91,146 +92,149 @@ export const LoginPage = () => {
};
return (
-
-
-
-
-
Nanwa
-
-
Welcome back
-
Sign in to your account to continue
-
-
-