From 84f9720bc0ce02e7674021dffa690c74508e73f1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Apr 2026 01:29:59 +0000 Subject: [PATCH 1/2] Initial plan From 1c06d4668bf4875286c2a74ae517dfe50eb77150 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Apr 2026 01:41:30 +0000 Subject: [PATCH 2/2] chore(frontend): cleanup unused code and update README - Remove dead code: fileUpload.ts (empty), maplibre/bounds.ts, maplibre/priorityStyle.ts, YieldFileUpload.tsx, CoordinateFileUpload.tsx, fileUploadService.ts, PrescriptionsPage.tsx - Remove commented-out PrescriptionsPage import and route in AppRoutes - Consolidate normalizeErrors into utils/errors.ts; remove duplicate from ProfilePage - Extract page gradient background into utils/theme.ts; update all 6 usages in pages and FieldTable - Rewrite README.md to accurately reflect current structure and conventions Agent-Logs-Url: https://github.com/JosephPBaruch/CharAI/sessions/2018982f-9e14-412a-a709-60990d351466 Co-authored-by: JosephPBaruch <132173774+JosephPBaruch@users.noreply.github.com> --- frontend/README.md | 282 +++++++++--------- frontend/src/components/AppRoutes.tsx | 7 - .../features/farm/CoordinateFileUpload.tsx | 150 ---------- .../src/features/farm/YieldFileUpload.tsx | 150 ---------- frontend/src/features/farm/index.ts | 2 - .../src/features/fieldtable/FieldTable.tsx | 6 +- frontend/src/pages/HomePage.tsx | 6 +- frontend/src/pages/LandingPage.tsx | 6 +- frontend/src/pages/LoginPage.tsx | 6 +- frontend/src/pages/PrescriptionsPage.tsx | 5 - frontend/src/pages/ProfilePage.tsx | 27 +- frontend/src/pages/SignupPage.tsx | 6 +- frontend/src/services/fileUploadService.ts | 44 --- frontend/src/types/fileUpload.ts | 1 - frontend/src/types/maplibre/bounds.ts | 61 ---- frontend/src/types/maplibre/priorityStyle.ts | 49 --- frontend/src/utils/errors.ts | 19 ++ frontend/src/utils/theme.ts | 9 + 18 files changed, 184 insertions(+), 652 deletions(-) delete mode 100644 frontend/src/features/farm/CoordinateFileUpload.tsx delete mode 100644 frontend/src/features/farm/YieldFileUpload.tsx delete mode 100644 frontend/src/pages/PrescriptionsPage.tsx delete mode 100644 frontend/src/services/fileUploadService.ts delete mode 100644 frontend/src/types/fileUpload.ts delete mode 100644 frontend/src/types/maplibre/bounds.ts delete mode 100644 frontend/src/types/maplibre/priorityStyle.ts create mode 100644 frontend/src/utils/theme.ts diff --git a/frontend/README.md b/frontend/README.md index 9d84830..136edeb 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,193 +1,195 @@ -# CharAI Frontend Setup +# CharAI Frontend -This guide outlines how to initialize and manage the CharAI frontend using **Vite**, **React**, **TypeScript**, and **Material UI**. +React + TypeScript SPA for the CharAI precision-agriculture platform. Users draw or upload field boundaries, configure crop and biochar settings, and view AI-generated prescription maps. --- -## Recreate Project from `package.json` +## Quick Start ```bash # Install dependencies +cd frontend npm install -# or if using yarn -yarn install # Start development server npm run dev -``` - ---- -## Initialization (New Project) - -```bash -# Create new Vite project with React and TypeScript -npm create vite@latest charai-frontend -- --template react-ts +# Build for production +npm run build -# Navigate to project directory -cd charai-frontend +# Type check +npx tsc --noEmit -# Install core dependencies -npm install # Windows/Linux/macOS: same command +# Lint +npm run lint ``` --- -## Development Server +## Stack -```bash -# Start the development server -npm run dev - -# Build for production -npm run build - -# Preview production build -npm run preview -``` +| Tool | Version | Purpose | +|------|---------|---------| +| React | 19 | UI framework | +| TypeScript | 5.9 | Type safety | +| Vite (rolldown) | latest | Build tool / dev server | +| Material UI | 7 | Component library | +| React Router | 7 | Client-side routing | +| Leaflet + react-leaflet | latest | Interactive maps | +| @turf/turf | latest | Client-side geospatial operations | +| Playwright | latest | End-to-end testing | --- -## Project Structure +## Directory Structure ``` frontend/ ├── src/ -│ ├── components/ # Reusable UI components (Header, ProtectedRoute, PublicRoute, AppRoutes) -│ ├── pages/ # Route-level components (LoginPage, SignupPage, HomePage, App) -│ ├── services/ # API client and auth service (authService.ts) -│ ├── contexts/ # React context for state management (AuthContext) -│ ├── types/ # TypeScript type definitions (auth.ts) -│ ├── utils/ # Utility functions -│ ├── App.tsx # Root component -│ ├── main.tsx # Entry point -│ └── index.css # Global styles -├── public/ # Static assets -├── index.html # HTML template -├── package.json # Dependencies and scripts -├── tsconfig.json # TypeScript configuration -├── vite.config.ts # Vite configuration -└── README.md # This file +│ ├── main.tsx # App entry point — mounts context providers +│ ├── index.css # Global styles +│ │ +│ ├── api/ +│ │ └── fetch.ts # All backend API calls (POSTFieldData, GETFields, etc.) +│ │ +│ ├── components/ # Shared, reusable UI components +│ │ ├── AppRoutes.tsx # Route definitions +│ │ ├── Header.tsx # Navigation bar with theme toggle and auth menu +│ │ ├── FormTextField.tsx # Controlled MUI text field wrapper +│ │ └── LoadingProgress.tsx # Full-page spinner +│ │ +│ ├── contexts/ # React context providers (global state) +│ │ ├── AuthContext.tsx # Authentication state (user, login, logout, register) +│ │ ├── CoordinateContext.tsx # Field boundary GeoJSON shared across components +│ │ ├── ThemeContext.tsx # Light/dark mode toggle (persisted to localStorage) +│ │ └── ToastContext.tsx # App-wide notification toasts +│ │ +│ ├── features/ # Feature-based modules (barrel-exported via index.ts) +│ │ ├── auth/ # Route guards +│ │ │ ├── ProtectedRoute.tsx # Redirects unauthenticated users to /login +│ │ │ ├── PublicRoute.tsx # Redirects authenticated users away from /login and /signup +│ │ │ └── index.ts +│ │ ├── farm/ # Field creation workflow (modal form) +│ │ │ ├── FarmBiocharForm.tsx # Full modal form orchestrating the field creation flow +│ │ │ ├── FieldsList.tsx # Field metadata inputs (name, description, crop type, price) +│ │ │ ├── FileUploadSection.tsx # Upload tab container (wraps ManualCoordinateUpload) +│ │ │ ├── ManualCoordinateUpload.tsx # Interactive Leaflet map for drawing field boundaries +│ │ │ ├── BudgetSettings.tsx # Biochar cost/rate inputs +│ │ │ ├── SubmitSection.tsx # Submit button with validation state +│ │ │ └── index.ts +│ │ ├── fieldtable/ # Field list with status polling and prescription viewer +│ │ │ └── FieldTable.tsx +│ │ ├── map/ # Standalone interactive Leaflet map widget +│ │ │ ├── InteractiveFarmMap.tsx +│ │ │ └── index.ts +│ │ ├── prescriptions/ # Prescription map viewer +│ │ │ ├── PrescriptionMapViewer.tsx # Main map viewer (Leaflet + canvas layer) +│ │ │ ├── Dialog.tsx # Field detail modal that hosts the viewer +│ │ │ ├── StatsPanel.tsx # Grid statistics summary +│ │ │ ├── PaybackLegend.tsx # Payback-period color legend +│ │ │ ├── EmptyPrescriptionData.tsx # Empty-state placeholder +│ │ │ ├── GridCanvasLayer.ts # Custom Leaflet canvas layer for prescription grid +│ │ │ ├── helpers.ts # Utility functions for prescription data +│ │ │ ├── types.ts # TypeScript interfaces for prescription data +│ │ │ └── index.ts +│ │ └── index.ts # Re-exports all feature modules +│ │ +│ ├── pages/ # Route-level page components (thin wrappers) +│ │ ├── LandingPage.tsx # Public landing page with hero, features, and field creation CTA +│ │ ├── HomePage.tsx # Authenticated dashboard with quick-action cards +│ │ ├── LoginPage.tsx # Login form +│ │ ├── SignupPage.tsx # Registration form +│ │ ├── ProfilePage.tsx # User profile, password change, account deletion +│ │ └── FieldPage.tsx # Fields list page (wraps FieldTable) +│ │ +│ ├── services/ # Business-logic helpers (non-UI) +│ │ ├── authService.ts # Auth API calls and token management +│ │ └── coordinateService.ts # parseFileToGeoJSON — converts uploaded files to GeoJSON +│ │ +│ ├── styles/ # MUI theme and color constants +│ │ ├── theme.ts # createAppTheme(mode) — MUI theme factory +│ │ └── colors.ts # Named color palette constants (COLORS) +│ │ +│ ├── types/ # TypeScript type definitions +│ │ ├── auth.ts # Auth-related interfaces (User, LoginRequest, FieldErrors, etc.) +│ │ └── fetch.ts # API response types (CropType, etc.) +│ │ +│ └── utils/ # Shared utility functions +│ ├── errors.ts # normalizeSignupErrors, normalizeErrors +│ ├── format.ts # formatTimestamp, formatPrice, truncateId, capitalize +│ └── theme.ts # getPageGradientBg(theme) — standard page background helper +│ +├── tests/ +│ ├── charai.spec.ts # Playwright E2E tests +│ └── playwright.config.ts # Playwright configuration +│ +├── public/ # Static assets +├── index.html # HTML entry template +├── package.json +├── tsconfig.json +├── vite.config.ts +└── README.md ``` --- -## Authentication - -The frontend integrates with Django REST Framework using **TokenAuthentication** and **SessionAuthentication**: +## Routes -- **Token Storage**: Auth tokens are stored in `localStorage` under the key `authToken` -- **Session Support**: Requests include `credentials: 'include'` to support session-based auth -- **Protected Routes**: Routes are guarded by `ProtectedRoute` and `PublicRoute` components -- **Auth Context**: Centralized auth state via `AuthContext` (provides `login`, `register`, `logout`, `checkAuth`, `isAuthenticated`, `user`, `isLoading`) -- **Error Handling**: Errors from auth attempts are logged to console only; no UI error messages are displayed +| Path | Auth | Component | +|------|------|-----------| +| `/` | Adaptive | `HomePage` (authenticated) or `LandingPage` (anonymous) | +| `/login` | Public only | `LoginPage` | +| `/signup` | Public only | `SignupPage` | +| `/profile` | Protected | `ProfilePage` | +| `/fields` | Protected | `FieldPage` → `FieldTable` | +| `*` | Adaptive | Same as `/` | -### Authentication Flow - -1. User submits login/signup form -2. Frontend calls `authService.login()` or `authService.register()` -3. Backend validates and returns token (+ user data) -4. Frontend stores token locally -5. Subsequent requests include `Authorization: Token ` header -6. On logout, token is cleared and user is redirected to login +--- -### Key Files +## Authentication -- `src/services/authService.ts` - API calls and token management -- `src/contexts/AuthContext.tsx` - Auth state and methods -- `src/types/auth.ts` - TypeScript types for auth -- `src/components/ProtectedRoute.tsx` - Guards authenticated pages -- `src/components/PublicRoute.tsx` - Prevents logged-in users from seeing login/signup -- `src/pages/LoginPage.tsx` - Login form -- `src/pages/SignupPage.tsx` - Registration form -- `src/pages/HomePage.tsx` - Protected home page (shown only to authenticated users) +- **Token storage**: Auth token stored in `localStorage` under `authToken` +- **Protected routes**: Guarded by `ProtectedRoute` / `PublicRoute` components in `features/auth/` +- **Auth context**: Centralized state via `AuthContext` (`login`, `logout`, `register`, `checkAuth`, `isAuthenticated`, `user`, `isLoading`) +- **API auth**: All authenticated requests include `Authorization: Token ` header --- -## TypeScript Compilation & Linting +## Coding Conventions -```bash -# Type check and build -npm run build - -# Lint the codebase -npm run lint -``` - -The following scripts are available in `package.json`: - -```json -{ - "scripts": { - "dev": "vite", - "build": "tsc -b && vite build", - "lint": "eslint .", - "preview": "vite preview" - } -} -``` +- **Feature-based organization**: Group related components, hooks, and types under `features//` with barrel `index.ts` exports +- **Pages are thin**: Page components compose feature components. Business logic stays in features/services/contexts +- **Context for global state**: `AuthContext`, `CoordinateContext`, `ThemeContext`, `ToastContext` — no Redux +- **API layer**: All backend calls go through `src/api/fetch.ts` using native `fetch()` +- **TypeScript strict mode**: All types defined in `src/types/`. Avoid `any` +- **MUI theming**: Use MUI components and `@emotion/styled`. Avoid raw CSS where MUI handles it +- **`data-testid` attributes**: Add to all interactive elements — Playwright E2E tests depend on them +- **Logging**: Use `console.debug` for dev-only output. No `console.log` in production paths --- -## Freeze Dependencies +## Testing -To capture current dependencies for other developers: +E2E tests use Playwright in `frontend/tests/`. Selectors rely on `data-testid` attributes. ```bash -# Create package.json.lock (npm) -npm install +# Run headless +cd tests && npx playwright test -# or yarn.lock (yarn) -yarn install +# Run headed (for debugging) +cd tests && HEADLESS=false npx playwright test ``` -Commit both `package.json` and the lock file to version control. +Always update `charai.spec.ts` when changing `data-testid` values, form inputs, API payload shapes, or UI flow. --- -## Summary - -- **Package manager**: `npm` -- **Framework**: React 18+ with TypeScript -- **UI Library**: Material UI (MUI) -- **Routing**: React Router v6 -- **Package manager**: `npm` -- **Project root**: `src/` -- **Main commands**: - - `npm run dev` - start development server - - `npm run build` - build for production and type check - - `npm run preview` - preview production build - - `npm run lint` - lint codebase -- **Configuration**: - - TypeScript: `tsconfig.json` - - Vite: `vite.config.ts` - - Environment: `.env` +## Theme ---- - -## Getting Started - -1. **Install dependencies**: - - ```bash - npm install - ``` - -2. **Ensure backend is running**: +The app supports light and dark modes, toggled from the header. The selected mode is persisted to `localStorage`. - ```bash - # In the backend directory - python manage.py runserver - ``` +- Theme is created by `createAppTheme(mode)` in `src/styles/theme.ts` +- Color constants live in `src/styles/colors.ts` +- `ThemeContext` wraps the app and provides the toggle +- Page-level background gradients use `getPageGradientBg(theme)` from `src/utils/theme.ts` -3. **Start the dev server**: - - ```bash - npm run dev - ``` - -4. **Open the app**: - - Navigate to configured frontend server URL - - Sign up or log in to see the protected home page - ---- diff --git a/frontend/src/components/AppRoutes.tsx b/frontend/src/components/AppRoutes.tsx index 8369986..5fbc2b8 100644 --- a/frontend/src/components/AppRoutes.tsx +++ b/frontend/src/components/AppRoutes.tsx @@ -5,7 +5,6 @@ import SignupPage from "../pages/SignupPage"; import HomePage from "../pages/HomePage"; import ProfilePage from "../pages/ProfilePage"; import { ProtectedRoute, PublicRoute } from "../features/auth"; -// import PrescriptionsPage from "../pages/PrescriptionsPage"; import FieldPage from "../pages/FieldPage"; export default function AppRoutes() { @@ -30,12 +29,6 @@ export default function AppRoutes() { element={} />} /> - {/* Prescriptions page - requires authentication - } />} - /> */} - } />} diff --git a/frontend/src/features/farm/CoordinateFileUpload.tsx b/frontend/src/features/farm/CoordinateFileUpload.tsx deleted file mode 100644 index 5c024bc..0000000 --- a/frontend/src/features/farm/CoordinateFileUpload.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import { - Box, - Button, - IconButton, - Stack, - Typography, - CircularProgress, -} from "@mui/material"; -import { styled } from "@mui/material/styles"; -import React from "react"; -import CloseIcon from "@mui/icons-material/Close"; -import { parseFileToGeoJSON } from "../../services/coordinateService"; -import { useCoordinates } from "../../contexts/CoordinateContext"; -import { COLORS } from "../../styles/colors"; - -const VisuallyHiddenInput = styled("input")({ - clip: "rect(0 0 0 0)", - clipPath: "inset(50%)", - height: 1, - overflow: "hidden", - position: "absolute", - bottom: 0, - left: 0, - whiteSpace: "nowrap", - width: 1, -}); - -export default function CoordinateFileUpload(props: { - onSelect?: (file: File | null) => void; - onUploadComplete?: () => void; -}) { - const { onSelect, onUploadComplete } = props || {}; - const { setCoordinateData } = useCoordinates(); - const [selectedFile, setSelectedFile] = React.useState(null); - const [isLoading, setIsLoading] = React.useState(false); - - const handleFileChange = (event: React.ChangeEvent) => { - if (event.target.files === null) { - throw Error("Error: No files selected"); - } - const f = event.target.files[0]; - setSelectedFile(f); - onSelect?.(f); - }; - - const handleFileSubmit = async () => { - if (!selectedFile) return; - - setIsLoading(true); - try { - // Parse file locally and save to context (temporary localStorage-based storage) - const geojson = await parseFileToGeoJSON(selectedFile); - setCoordinateData(geojson); - console.log("Success! Coordinates saved locally"); - setSelectedFile(null); - onSelect?.(null); - onUploadComplete?.(); - } catch (err: any) { - console.error("Failed to process coordinates:", err); - // TODO: Show error message in UI - } finally { - setIsLoading(false); - } - }; - - const handleClearFile = () => { - setSelectedFile(null); - onSelect?.(null); - }; - - return ( - - {!selectedFile ? ( - - ) : ( - - - {selectedFile.name} - - - - - - )} - - {selectedFile && ( - - )} - - ); -} diff --git a/frontend/src/features/farm/YieldFileUpload.tsx b/frontend/src/features/farm/YieldFileUpload.tsx deleted file mode 100644 index 279971c..0000000 --- a/frontend/src/features/farm/YieldFileUpload.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import { - Box, - Button, - IconButton, - Stack, - Typography, - CircularProgress, -} from "@mui/material"; -import { styled } from "@mui/material/styles"; -import React from "react"; -import CloseIcon from "@mui/icons-material/Close"; -import { uploadYieldFile } from "../../services/fileUploadService"; -import { COLORS } from "../../styles/colors"; - -const VisuallyHiddenInput = styled("input")({ - clip: "rect(0 0 0 0)", - clipPath: "inset(50%)", - height: 1, - overflow: "hidden", - position: "absolute", - bottom: 0, - left: 0, - whiteSpace: "nowrap", - width: 1, -}); - -export default function YieldFileUpload(props: { - onSelect?: (file: File | null) => void; - onUploadComplete?: () => void; -}) { - const { onSelect, onUploadComplete } = props || {}; - const [selectedFile, setSelectedFile] = React.useState(null); - const [isLoading, setIsLoading] = React.useState(false); - - const handleFileChange = (event: React.ChangeEvent) => { - if (event.target.files === null) { - throw Error("Error: No files selected"); - } - const f = event.target.files[0]; - setSelectedFile(f); - onSelect?.(f); - }; - - const handleFileSubmit = async () => { - if (!selectedFile) return; - - setIsLoading(true); - const formData = new FormData(); - formData.append("file", selectedFile); - - try { - const response = await uploadYieldFile(formData); - if (response) { - console.log("Success! File uploaded"); - setSelectedFile(null); - onSelect?.(null); - onUploadComplete?.(); - } - } catch (err: any) { - console.error(err); - } finally { - setIsLoading(false); - } - }; - - const handleClearFile = () => { - setSelectedFile(null); - onSelect?.(null); - }; - - return ( - - {!selectedFile ? ( - - ) : ( - - - {selectedFile.name} - - - - - - )} - - {selectedFile && ( - - )} - - ); -} diff --git a/frontend/src/features/farm/index.ts b/frontend/src/features/farm/index.ts index ce72150..98b7b35 100644 --- a/frontend/src/features/farm/index.ts +++ b/frontend/src/features/farm/index.ts @@ -5,5 +5,3 @@ export { default as FieldsList } from "./FieldsList"; export type { FieldEntry } from "./FieldsList"; export { default as FileUploadSection } from "./FileUploadSection"; export { default as SubmitSection } from "./SubmitSection"; -export { default as CoordinateFileUpload } from "./CoordinateFileUpload"; -export { default as YieldFileUpload } from "./YieldFileUpload"; diff --git a/frontend/src/features/fieldtable/FieldTable.tsx b/frontend/src/features/fieldtable/FieldTable.tsx index 4ce7fae..bbf4c43 100644 --- a/frontend/src/features/fieldtable/FieldTable.tsx +++ b/frontend/src/features/fieldtable/FieldTable.tsx @@ -24,6 +24,7 @@ import { DeleteField, GETFields } from "../../api/fetch"; import FieldDialog from "../prescriptions/Dialog"; import { FarmBiocharForm } from ".."; import { formatTimestamp, formatPrice, truncateId } from "../../utils/format"; +import { getPageGradientBg } from "../../utils/theme"; type FieldRecord = { id: number; @@ -138,10 +139,7 @@ export default function FieldTable() { diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index 5c4611d..6d7e452 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -4,6 +4,7 @@ import { Link as RouterLink } from "react-router"; import { useAuth } from "../contexts/AuthContext"; import AgricultureIcon from "@mui/icons-material/Agriculture"; import TableChartIcon from "@mui/icons-material/TableChart"; +import { getPageGradientBg } from "../utils/theme"; const HomePage = () => { const { user } = useAuth(); @@ -13,10 +14,7 @@ const HomePage = () => { diff --git a/frontend/src/pages/LandingPage.tsx b/frontend/src/pages/LandingPage.tsx index da6897e..7367e62 100644 --- a/frontend/src/pages/LandingPage.tsx +++ b/frontend/src/pages/LandingPage.tsx @@ -17,6 +17,7 @@ import FieldsList from "../features/farm/FieldsList"; import type { FieldEntry } from "../features/farm/FieldsList"; import FileUploadSection from "../features/farm/FileUploadSection"; import SubmitSection from "../features/farm/SubmitSection"; +import { getPageGradientBg } from "../utils/theme"; interface FeatureCardProps { icon: React.ReactNode; @@ -92,10 +93,7 @@ const LandingPage = () => { field.biocharCostPerTon !== "" && field.biocharCostPerTon > 0; const canSubmit = coordsReady && isPriceValid && isBiocharCostValid; - const gradientBg = - theme.palette.mode === "dark" - ? `linear-gradient(180deg, #0a0a0a 0%, ${theme.palette.background.default} 100%)` - : `linear-gradient(180deg, #f0f0f5 0%, ${theme.palette.background.default} 100%)`; + const gradientBg = getPageGradientBg(theme); return ( diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx index a831a65..c92fbda 100644 --- a/frontend/src/pages/LoginPage.tsx +++ b/frontend/src/pages/LoginPage.tsx @@ -12,6 +12,7 @@ import { useTheme } from "@mui/material/styles"; import { useAuth } from "../contexts/AuthContext"; import type { LoginRequest } from "../types/auth"; import { FormTextField } from "../components/FormTextField"; +import { getPageGradientBg } from "../utils/theme"; const LoginPage = () => { const navigate = useNavigate(); @@ -41,10 +42,7 @@ const LoginPage = () => { } }; - const gradientBg = - theme.palette.mode === "dark" - ? `linear-gradient(180deg, #0a0a0a 0%, ${theme.palette.background.default} 100%)` - : `linear-gradient(180deg, #f0f0f5 0%, ${theme.palette.background.default} 100%)`; + const gradientBg = getPageGradientBg(theme); return ( ; -} diff --git a/frontend/src/pages/ProfilePage.tsx b/frontend/src/pages/ProfilePage.tsx index 383a913..2f37528 100644 --- a/frontend/src/pages/ProfilePage.tsx +++ b/frontend/src/pages/ProfilePage.tsx @@ -22,25 +22,9 @@ import { useAuth } from "../contexts/AuthContext"; import { useToast } from "../contexts/ToastContext"; import { FormTextField } from "../components/FormTextField"; import * as authService from "../services/authService"; -import type { FieldErrors, DjangoErrorResponse } from "../types/auth"; - -function normalizeErrors(err: unknown): FieldErrors { - const errors: FieldErrors = {}; - if (err && typeof err === "object") { - for (const [key, value] of Object.entries(err as DjangoErrorResponse)) { - if (Array.isArray(value)) { - errors[key] = value.join(" "); - } else if (typeof value === "string") { - errors[key] = value; - } - } - } - if (Object.keys(errors).length === 0) { - errors.general = "An unexpected error occurred."; - } - return errors; -} - +import type { FieldErrors } from "../types/auth"; +import { normalizeErrors } from "../utils/errors"; +import { getPageGradientBg } from "../utils/theme"; const ProfilePage = () => { const { user, logout } = useAuth(); const { showToast } = useToast(); @@ -112,10 +96,7 @@ const ProfilePage = () => { } }; - const gradientBg = - theme.palette.mode === "dark" - ? `linear-gradient(180deg, #0a0a0a 0%, ${theme.palette.background.default} 100%)` - : `linear-gradient(180deg, #f0f0f5 0%, ${theme.palette.background.default} 100%)`; + const gradientBg = getPageGradientBg(theme); return ( { const navigate = useNavigate(); @@ -47,10 +48,7 @@ const SignupPage = () => { } }; - const gradientBg = - theme.palette.mode === "dark" - ? `linear-gradient(180deg, #0a0a0a 0%, ${theme.palette.background.default} 100%)` - : `linear-gradient(180deg, #f0f0f5 0%, ${theme.palette.background.default} 100%)`; + const gradientBg = getPageGradientBg(theme); return ( => { - const response = await fetch(`${API_URL}/coordinate-file-upload/`, { - method: "POST", - credentials: "include", - body: data, - }); - - const result = await handleResponse(response); - - return result as any; -}; - -export const uploadYieldFile = async (data: any): Promise => { - const response = await fetch(`${API_URL}/yield-file-upload/`, { - method: "POST", - credentials: "include", - body: data, - }); - - const result = await handleResponse(response); - - return result as any; -}; diff --git a/frontend/src/types/fileUpload.ts b/frontend/src/types/fileUpload.ts deleted file mode 100644 index 5f64d65..0000000 --- a/frontend/src/types/fileUpload.ts +++ /dev/null @@ -1 +0,0 @@ -// this will be filled out when backend defines apis. diff --git a/frontend/src/types/maplibre/bounds.ts b/frontend/src/types/maplibre/bounds.ts deleted file mode 100644 index 2f92e7c..0000000 --- a/frontend/src/types/maplibre/bounds.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { FeatureCollection, Geometry } from "geojson"; - -// Compute LngLatBoundsLike ([[minLng, minLat], [maxLng, maxLat]]) from a GeoJSON FeatureCollection -export function computeBoundsFromGeoJSON( - data: FeatureCollection, -): [[number, number], [number, number]] | null { - let minLng = Infinity; - let minLat = Infinity; - let maxLng = -Infinity; - let maxLat = -Infinity; - - const expand = (coords: any): void => { - if (!coords) return; - if (typeof coords[0] === "number" && typeof coords[1] === "number") { - const lng = coords[0]; - const lat = coords[1]; - if (lng < minLng) minLng = lng; - if (lat < minLat) minLat = lat; - if (lng > maxLng) maxLng = lng; - if (lat > maxLat) maxLat = lat; - } else if (Array.isArray(coords)) { - for (const c of coords) expand(c); - } - }; - - for (const feature of data.features) { - const geom: Geometry | null = feature.geometry as any; - if (!geom) continue; - switch (geom.type) { - case "Point": - case "MultiPoint": - case "LineString": - case "MultiLineString": - case "Polygon": - case "MultiPolygon": - expand((geom as any).coordinates); - break; - case "GeometryCollection": - for (const g of (geom as any).geometries || []) { - expand((g as any).coordinates); - } - break; - default: - break; - } - } - - if ( - minLng === Infinity || - minLat === Infinity || - maxLng === -Infinity || - maxLat === -Infinity - ) { - return null; - } - - return [ - [minLng, minLat], - [maxLng, maxLat], - ]; -} diff --git a/frontend/src/types/maplibre/priorityStyle.ts b/frontend/src/types/maplibre/priorityStyle.ts deleted file mode 100644 index 627b642..0000000 --- a/frontend/src/types/maplibre/priorityStyle.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { COLORS } from "../../styles/colors"; - -// MapLibre GL paint expression for fill-color. -// Primary: derive color by numeric ROI metric `paybackPeriod` (e.g., months). -// - Quickest payback -> red (urgent) -// - Slowest payback -> green (fine) -// Fallback: if `paybackPeriod` missing, use categorical priority/priorityRange mapping. -export const priorityFillColorExpression = [ - "case", - ["has", "paybackPeriod"], - // step(paybackPeriod, baseColor, stop1, color1, stop2, color2, ...) - [ - "step", - ["get", "paybackPeriod"], - COLORS.dataRed, - 6, - COLORS.dataOrange, - 12, - COLORS.dataYellow, - 18, - COLORS.dataLightGreen, - 24, - COLORS.dataGreen, - ], - // Fallback to categorical priority mapping - [ - "match", - [ - "downcase", - [ - "coalesce", - ["to-string", ["get", "priority"]], - ["to-string", ["get", "priorityRange"]], - "", - ], - ], - "high", - COLORS.dataRed, - "medium-high", - COLORS.dataOrange, - "medium", - COLORS.dataYellow, - "medium-low", - COLORS.dataLightGreen, - "low", - COLORS.dataGreen, - COLORS.dataDefault, - ], -]; diff --git a/frontend/src/utils/errors.ts b/frontend/src/utils/errors.ts index 068db54..467051f 100644 --- a/frontend/src/utils/errors.ts +++ b/frontend/src/utils/errors.ts @@ -13,3 +13,22 @@ export function normalizeSignupErrors(error: DjangoErrorResponse): FieldErrors { return fieldErrors; } + +// Parses a generic API error response into field-level error messages. +// Joins array values into a single string and falls back to a generic message. +export function normalizeErrors(err: unknown): FieldErrors { + const errors: FieldErrors = {}; + if (err && typeof err === "object") { + for (const [key, value] of Object.entries(err as DjangoErrorResponse)) { + if (Array.isArray(value)) { + errors[key] = value.join(" "); + } else if (typeof value === "string") { + errors[key] = value; + } + } + } + if (Object.keys(errors).length === 0) { + errors.general = "An unexpected error occurred."; + } + return errors; +} diff --git a/frontend/src/utils/theme.ts b/frontend/src/utils/theme.ts new file mode 100644 index 0000000..c895399 --- /dev/null +++ b/frontend/src/utils/theme.ts @@ -0,0 +1,9 @@ +import type { Theme } from "@mui/material/styles"; + +// Returns the standard page gradient background for the current theme mode. +// Used consistently across page-level components. +export function getPageGradientBg(theme: Theme): string { + return theme.palette.mode === "dark" + ? `linear-gradient(180deg, #0a0a0a 0%, ${theme.palette.background.default} 100%)` + : `linear-gradient(180deg, #f0f0f5 0%, ${theme.palette.background.default} 100%)`; +}