From b8ad1e4ee6b87e132c34c0d932ec6acd231499f6 Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Thu, 31 Jul 2025 22:14:58 -0700 Subject: [PATCH 01/21] feat(admin): add new edit site page with server API integration - Create new edit site page component - Implement interface for new server endpoints - Add initial API request parsing logic --- src/ListItems.tsx | 7 ++ src/admin/AdminBody.tsx | 3 + src/admin/NewEditSite.tsx | 160 ++++++++++++++++++++++++++++++++++++++ src/index.tsx | 2 + src/types.d.ts | 4 +- 5 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 src/admin/NewEditSite.tsx diff --git a/src/ListItems.tsx b/src/ListItems.tsx index 3458980..10377b5 100644 --- a/src/ListItems.tsx +++ b/src/ListItems.tsx @@ -7,6 +7,7 @@ import ManageAccountsIcon from '@mui/icons-material/ManageAccounts'; import SyncIcon from '@mui/icons-material/Sync'; import HomeIcon from '@mui/icons-material/Home'; import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings'; +import EditIcon from '@mui/icons-material/Edit'; import { ListItemButton } from '@mui/material'; export const mainListItems = ( @@ -30,6 +31,12 @@ export const mainListItems = ( + window.open('/admin/new-edit-site', '_self')}> + + + + + ); diff --git a/src/admin/AdminBody.tsx b/src/admin/AdminBody.tsx index 730ce4b..f5fe4ff 100644 --- a/src/admin/AdminBody.tsx +++ b/src/admin/AdminBody.tsx @@ -1,6 +1,7 @@ import UserPage from './UserPage'; import EditSite from './EditSite'; import EditData from './EditData'; +import NewEditSite from './NewEditSite'; interface AdminBodyProps { page: AdminPage; @@ -14,6 +15,8 @@ export default function AdminBody(props: AdminBodyProps) { return ; case 'edit-data': return ; + case 'new-edit-site': + return ; default: return

Error

; } diff --git a/src/admin/NewEditSite.tsx b/src/admin/NewEditSite.tsx new file mode 100644 index 0000000..949079d --- /dev/null +++ b/src/admin/NewEditSite.tsx @@ -0,0 +1,160 @@ +import { useEffect, useState } from 'react'; +import { + Box, + Container, + Paper, + Typography, + Button, + Fab, + List, + ListItem, + ListItemText, +} from '@mui/material'; +import { Add as AddIcon } from '@mui/icons-material'; +import { apiClient } from '@/utils/fetch'; + +const parseSitesFromJSON = (jsonString: string): Site[] => { + try { + const parsed = JSON.parse(jsonString); + + if (!Array.isArray(parsed.sites)) { + throw new Error("Invalid format: 'sites' should be an array"); + } + + const sites: Site[] = parsed.sites.map((site: any): Site => { + return { + name: site.name, + latitude: site.latitude, + longitude: site.longitude, + status: site.status as SiteStatus, + address: site.address, + cell_id: site.cell_id, + color: site.color, + boundary: site.boundary?.map((point: any) => ({ + lat: point.lat, + lng: point.lng + })) ?? [], + }; + }); + + return sites; + } catch (error) { + console.error("Failed to parse sites JSON:", error); + return []; + } +}; + + +export default function NewEditSite() { + const [sites, setSites] = useState([]); + const handleEdit = (siteName: string) => { + console.log(`Edit site with ID: ${siteName}`); + }; + + const handleDelete = (siteName: string) => { + console.log(`Delete site with ID: ${siteName}`); + }; + + const handleAdd = () => { + console.log('Add new site'); + }; + + const reloadSites = () => { + apiClient + .GET('/api/public-sites') + .then(res => { + const { data, error } = res; + if (error || !data) { + console.log(`Unable to query sites: ${error}`); + return; + } + setSites(parseSitesFromJSON(JSON.stringify(data))); + }) + .catch(err => { + return
; + }); + }; + useEffect(() => { + reloadSites(); + }); + + return ( + + + + Site Management + + + + {sites.map((site) => ( + + + {site.name} + + } + sx={{ flexGrow: 1 }} + /> + + + + + + ))} + + + + {/* Floating Action Button */} + + + + + ); +} diff --git a/src/index.tsx b/src/index.tsx index 793b884..9909197 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,6 +3,7 @@ import { BrowserRouter, Routes, Route } from 'react-router-dom'; import Vis from './vis/Vis'; import Login from './admin/Login'; import AdminPortal from './admin/AdminPortal'; +import NewEditSite from './admin/NewEditSite'; import './index.css'; // Get the root element @@ -23,6 +24,7 @@ root.render( } /> } /> } /> + } /> } diff --git a/src/types.d.ts b/src/types.d.ts index e213942..3b6d948 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -22,7 +22,7 @@ type DisplayOption = { type SiteStatus = 'active' | 'confirmed' | 'in-conversation' | 'unknown'; -type AdminPage = 'users' | 'edit-site' | 'edit-data'; +type AdminPage = 'users' | 'edit-site' | 'edit-data' | 'new-edit-site' | 'test'; type UserRow = { identity: string; @@ -77,4 +77,4 @@ type Measurement = { ping: number; site: string; device_id: number; -}; +}; \ No newline at end of file From feae45b6bebdd7148745f31154cc6cd8526ae1dd Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Thu, 31 Jul 2025 22:42:15 -0700 Subject: [PATCH 02/21] fix: update the schema file so that calls can be made properly to the new endpoints --- src/ListItems.tsx | 4 +- src/admin/NewEditSite.tsx | 34 +++--- src/types.d.ts | 2 +- src/types/api.d.ts | 229 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 250 insertions(+), 19 deletions(-) diff --git a/src/ListItems.tsx b/src/ListItems.tsx index 10377b5..7ded149 100644 --- a/src/ListItems.tsx +++ b/src/ListItems.tsx @@ -31,7 +31,9 @@ export const mainListItems = ( - window.open('/admin/new-edit-site', '_self')}> + window.open('/admin/new-edit-site', '_self')} + > diff --git a/src/admin/NewEditSite.tsx b/src/admin/NewEditSite.tsx index 949079d..f8a0698 100644 --- a/src/admin/NewEditSite.tsx +++ b/src/admin/NewEditSite.tsx @@ -30,21 +30,21 @@ const parseSitesFromJSON = (jsonString: string): Site[] => { address: site.address, cell_id: site.cell_id, color: site.color, - boundary: site.boundary?.map((point: any) => ({ - lat: point.lat, - lng: point.lng - })) ?? [], + boundary: + site.boundary?.map((point: any) => ({ + lat: point.lat, + lng: point.lng, + })) ?? [], }; }); return sites; } catch (error) { - console.error("Failed to parse sites JSON:", error); + console.error('Failed to parse sites JSON:', error); return []; } }; - export default function NewEditSite() { const [sites, setSites] = useState([]); const handleEdit = (siteName: string) => { @@ -79,14 +79,14 @@ export default function NewEditSite() { }); return ( - + - + Site Management - + - {sites.map((site) => ( + {sites.map(site => ( + {site.name} } @@ -106,8 +106,8 @@ export default function NewEditSite() { /> + + + setName(e.target.value)} + sx={{ mb: 2 }} + /> + + + setLongitude(e.target.value)} + /> + setLatitude(e.target.value)} + /> + + + + Status + + + + setAddress(e.target.value)} + sx={{ mb: 2 }} + /> + + + + Cells + + + + + + {cells.map((cell) => ( + + Cell ID + updateCellId(cell.id, e.target.value)} + sx={{ flexGrow: 1 }} + /> + + + ))} + + + + + + setColorEnabled(e.target.checked)} + /> + } + label="Color" + /> + {colorEnabled && ( + setColorValue(e.target.value)} + sx={{ mt: 1 }} + /> + )} + + + + setBoundaryEnabled(e.target.checked)} + /> + } + label="Boundary" + /> + {boundaryEnabled && ( + + + + )} + + + {boundaryEnabled && boundaryPoints.map((point) => ( + + (Lat, Long) + updateBoundaryPoint(point.id, 'lat', e.target.value)} + sx={{ flexGrow: 1 }} + /> + updateBoundaryPoint(point.id, 'lng', e.target.value)} + sx={{ flexGrow: 1 }} + /> + + + ))} + + + + ); +} diff --git a/src/admin/ListSites.tsx b/src/admin/ListSites.tsx index 5d50a3d..624c03e 100644 --- a/src/admin/ListSites.tsx +++ b/src/admin/ListSites.tsx @@ -46,6 +46,7 @@ export default function ListSites() { const [sites, setSites] = useState([]); const handleEdit = (siteName: string) => { console.log(`Edit site with ID: ${siteName}`); + window.open('/admin/create-edit-site', '_self'); }; const handleDelete = (siteName: string) => { @@ -58,6 +59,7 @@ export default function ListSites() { const handleAdd = () => { console.log('Add new site'); + window.open('/admin/create-edit-site', '_self') }; const reloadSites = () => { @@ -125,7 +127,7 @@ export default function ListSites() { }).catch(err => { console.error(`Error creating site: ${err}`); }); - + } return ( @@ -185,8 +187,6 @@ export default function ListSites() { ))} - - {/* Floating Action Button */} ); } -} \ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx index fd6cc29..ff2f64b 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -25,6 +25,10 @@ root.render( } /> } /> } /> + } + /> } diff --git a/src/types.d.ts b/src/types.d.ts index a4e1fa8..4f46c03 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -22,7 +22,7 @@ type DisplayOption = { type SiteStatus = 'active' | 'confirmed' | 'in-conversation' | 'unknown'; -type AdminPage = 'users' | 'edit-site' | 'edit-data' | 'list-sites' | 'test'; +type AdminPage = 'users' | 'edit-site' | 'edit-data' | 'list-sites' | 'create-edit-site'; type UserRow = { identity: string; From e9559b48637217c1738bae3df91a96ae44989ace Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Sun, 3 Aug 2025 15:11:17 -0700 Subject: [PATCH 06/21] fix: change the CreateEditSite and ListSites pages to use the correct class - Update routing to reflect two new modes - Use Site class instead of the component from the schema - Add an imported color picker --- src/admin/AdminBody.tsx | 6 ++-- src/admin/CreateEditSite.tsx | 60 ++++++++++++++++++++++++++++++------ src/admin/ListSites.tsx | 45 +++++++++++++++++++-------- src/index.tsx | 14 ++++++--- src/types.d.ts | 6 +++- 5 files changed, 103 insertions(+), 28 deletions(-) diff --git a/src/admin/AdminBody.tsx b/src/admin/AdminBody.tsx index f6ee7a8..b83a422 100644 --- a/src/admin/AdminBody.tsx +++ b/src/admin/AdminBody.tsx @@ -18,8 +18,10 @@ export default function AdminBody(props: AdminBodyProps) { return ; case 'list-sites': return ; - case 'create-edit-site': - return ; + case 'create-site': + return ; + case 'new-edit-site': + return ; default: return

Error

; } diff --git a/src/admin/CreateEditSite.tsx b/src/admin/CreateEditSite.tsx index c34d702..b9687c2 100644 --- a/src/admin/CreateEditSite.tsx +++ b/src/admin/CreateEditSite.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { Box, Container, @@ -20,6 +20,7 @@ import { Add as AddIcon, Delete as DeleteIcon, } from '@mui/icons-material'; +import ColorPicker from 'react-pick-color'; interface CellEntry { id: string; @@ -32,7 +33,12 @@ interface BoundaryPoint { lng: string; } -export default function CreateEditSite() { +interface CreateEditSiteProps { + mode: 'create' | 'edit'; + site?: Site; +} + +export default function CreateEditSite({ mode, site }: CreateEditSiteProps) { const [name, setName] = useState(''); const [longitude, setLongitude] = useState(''); const [latitude, setLatitude] = useState(''); @@ -40,10 +46,11 @@ export default function CreateEditSite() { const [address, setAddress] = useState(''); const [cells, setCells] = useState([]); const [colorEnabled, setColorEnabled] = useState(true); - const [colorValue, setColorValue] = useState(''); + const [colorValue, setColorValue] = useState('#fff'); const [boundaryEnabled, setBoundaryEnabled] = useState(true); const [boundaryPoints, setBoundaryPoints] = useState([]); + const handleBack = () => { console.log('Navigate back'); window.open('/admin/list-sites', '_self'); @@ -51,6 +58,18 @@ export default function CreateEditSite() { const handleSave = () => { console.log('Save site'); + if (validateSite()) { + const site: Site = { + name, + latitude: parseFloat(latitude), + longitude: parseFloat(longitude), + status: status as SiteStatus, + address, + cell_id: cells.map(cell => cell.cellId), + color: colorEnabled ? colorValue : undefined, + boundary: boundaryEnabled ? boundaryPoints.map(point => [parseFloat(point.lat), parseFloat(point.lng)]) : undefined, + }; + } }; const addCell = () => { @@ -88,6 +107,34 @@ export default function CreateEditSite() { )); }; + const validateSite = () : boolean => { + if (name === '') { + alert('Name is required'); + return false; + } + if (longitude === '' || isNaN(Number(longitude))) { + alert('Valid Longitude is required'); + return false; + } + if (latitude === '' || isNaN(Number(latitude))) { + alert('Valid Latitude is required'); + return false; + } + if (status === '') { + alert('Status is required'); + return false; + } + if (address === '') { + alert('Address is required'); + return false; + } + if (cells.length === 0) { + alert('At least one Cell ID is required'); + return false; + } + return true; + } + return ( @@ -212,12 +259,7 @@ export default function CreateEditSite() { label="Color" /> {colorEnabled && ( - setColorValue(e.target.value)} - sx={{ mt: 1 }} - /> + setColorValue(color.hex)} /> )} diff --git a/src/admin/ListSites.tsx b/src/admin/ListSites.tsx index 624c03e..92d2bd8 100644 --- a/src/admin/ListSites.tsx +++ b/src/admin/ListSites.tsx @@ -14,7 +14,7 @@ import { Add as AddIcon } from '@mui/icons-material'; import { apiClient } from '@/utils/fetch'; import { components } from '@/types/api'; -const parseSitesFromJSON = (jsonString: string): components['schemas']['Site'][] => { +const parseSitesFromJSON = (jsonString: string): Site[] => { try { const parsed = JSON.parse(jsonString); @@ -22,7 +22,7 @@ const parseSitesFromJSON = (jsonString: string): components['schemas']['Site'][] throw new Error("Invalid format: 'sites' should be an array"); } - const sites: components['schemas']['Site'][] = parsed.sites.map((site: any): components['schemas']['Site'] => { + const sites: Site[] = parsed.sites.map((site: any): Site => { return { name: site.name, latitude: site.latitude, @@ -43,10 +43,10 @@ const parseSitesFromJSON = (jsonString: string): components['schemas']['Site'][] }; export default function ListSites() { - const [sites, setSites] = useState([]); + const [sites, setSites] = useState([]); const handleEdit = (siteName: string) => { console.log(`Edit site with ID: ${siteName}`); - window.open('/admin/create-edit-site', '_self'); + window.open('/admin/new-edit-site', '_self'); }; const handleDelete = (siteName: string) => { @@ -59,7 +59,7 @@ export default function ListSites() { const handleAdd = () => { console.log('Add new site'); - window.open('/admin/create-edit-site', '_self') + window.open('/admin/create-site', '_self') }; const reloadSites = () => { @@ -81,9 +81,30 @@ export default function ListSites() { reloadSites(); }); - const deleteSite = (site: components['schemas']['Site']) => { + const siteToSchema = (site: Site): components['schemas']['Site'] => { + return { + name: site.name, + latitude: site.latitude, + longitude: site.longitude, + status: siteStatusToSchema(site.status), + address: site.address, + cell_id: site.cell_id, + color: site.color, + boundary: site.boundary + }; + } + + const siteStatusToSchema = (siteStatus: SiteStatus): components['parameters']['SiteStatus'] => { + if (siteStatus === 'unknown') { + throw new Error(`Invalid site status: ${siteStatus}`); + } else { + return siteStatus as components['parameters']['SiteStatus']; + } + } + + const deleteSite = (site: Site) => { apiClient.DELETE('/api/secure-site', { - body: site + body: siteToSchema(site) }).then(res => { const { data, error } = res; if (error) { @@ -97,9 +118,9 @@ export default function ListSites() { }); }; - const editSite = (site: components['schemas']['Site']) => { + const editSite = (site: Site) => { apiClient.PUT('/api/secure-site', { - body: site + body: siteToSchema(site) }).then(res => { const { data, error } = res; if (error) { @@ -113,9 +134,9 @@ export default function ListSites() { }); }; - const createSite = (site: components['schemas']['Site']) => { + const createSite = (site: Site) => { apiClient.POST('/api/secure-site', { - body: site + body: siteToSchema(site) }).then(res => { const { data, error } = res; if (error) { @@ -127,7 +148,7 @@ export default function ListSites() { }).catch(err => { console.error(`Error creating site: ${err}`); }); - } + }; return ( diff --git a/src/index.tsx b/src/index.tsx index ff2f64b..2fe99f9 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,7 +3,6 @@ import { BrowserRouter, Routes, Route } from 'react-router-dom'; import Vis from './vis/Vis'; import Login from './admin/Login'; import AdminPortal from './admin/AdminPortal'; -import ListSites from './admin/ListSites'; import './index.css'; // Get the root element @@ -24,10 +23,17 @@ root.render( } /> } /> } /> - } /> } + path='/admin/list-sites' + element={} + /> + } + /> + } /> Date: Sun, 3 Aug 2025 15:35:49 -0700 Subject: [PATCH 07/21] fix: remove old attempt at passing in props --- src/admin/AdminBody.tsx | 4 ++-- src/admin/CreateEditSite.tsx | 7 +------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/admin/AdminBody.tsx b/src/admin/AdminBody.tsx index b83a422..ebda326 100644 --- a/src/admin/AdminBody.tsx +++ b/src/admin/AdminBody.tsx @@ -19,9 +19,9 @@ export default function AdminBody(props: AdminBodyProps) { case 'list-sites': return ; case 'create-site': - return ; + return ; case 'new-edit-site': - return ; + return ; default: return

Error

; } diff --git a/src/admin/CreateEditSite.tsx b/src/admin/CreateEditSite.tsx index b9687c2..16ca641 100644 --- a/src/admin/CreateEditSite.tsx +++ b/src/admin/CreateEditSite.tsx @@ -33,12 +33,7 @@ interface BoundaryPoint { lng: string; } -interface CreateEditSiteProps { - mode: 'create' | 'edit'; - site?: Site; -} - -export default function CreateEditSite({ mode, site }: CreateEditSiteProps) { +export default function CreateEditSite() { const [name, setName] = useState(''); const [longitude, setLongitude] = useState(''); const [latitude, setLatitude] = useState(''); From ec7cebc1220c39d7db2a7b9b9a8104460788598d Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Sun, 3 Aug 2025 15:59:46 -0700 Subject: [PATCH 08/21] feat: use URL to load site information when editing --- src/admin/AdminBody.tsx | 4 ++-- src/admin/CreateEditSite.tsx | 41 +++++++++++++++++++++++++++++++++++- src/admin/ListSites.tsx | 6 +++++- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/admin/AdminBody.tsx b/src/admin/AdminBody.tsx index ebda326..068dbbb 100644 --- a/src/admin/AdminBody.tsx +++ b/src/admin/AdminBody.tsx @@ -19,9 +19,9 @@ export default function AdminBody(props: AdminBodyProps) { case 'list-sites': return ; case 'create-site': - return ; + return ; case 'new-edit-site': - return ; + return ; default: return

Error

; } diff --git a/src/admin/CreateEditSite.tsx b/src/admin/CreateEditSite.tsx index 16ca641..e157015 100644 --- a/src/admin/CreateEditSite.tsx +++ b/src/admin/CreateEditSite.tsx @@ -33,7 +33,11 @@ interface BoundaryPoint { lng: string; } -export default function CreateEditSite() { +interface CreateEditSiteProps { + mode: 'create' | 'edit'; +} + +export default function CreateEditSite({ mode }: CreateEditSiteProps) { const [name, setName] = useState(''); const [longitude, setLongitude] = useState(''); const [latitude, setLatitude] = useState(''); @@ -130,6 +134,41 @@ export default function CreateEditSite() { return true; } + useEffect(() => { + if (mode === 'edit') { + const urlParams = new URLSearchParams(window.location.search); + const siteParam = urlParams.get('site'); + if (siteParam) { + try { + const siteData = JSON.parse(decodeURIComponent(siteParam)); + setName(siteData.name); + setLatitude(siteData.latitude.toString()); + setLongitude(siteData.longitude.toString()); + setStatus(siteData.status); + setAddress(siteData.address); + setCells(siteData.cell_id.map((cellId: string) => ({ + id: Date.now().toString() + cellId, + cellId: cellId + }))); + if (siteData.color) { + setColorEnabled(true); + setColorValue(siteData.color); + } + if (siteData.boundary) { + setBoundaryEnabled(true); + setBoundaryPoints(siteData.boundary.map((point: [number, number]) => ({ + id: Date.now().toString() + point.join(','), + lat: point[0].toString(), + lng: point[1].toString() + }))); + } + } catch (error) { + console.error('Failed to parse site data from URL:', error); + } + } + } + }, [mode]); + return ( diff --git a/src/admin/ListSites.tsx b/src/admin/ListSites.tsx index 92d2bd8..5fae033 100644 --- a/src/admin/ListSites.tsx +++ b/src/admin/ListSites.tsx @@ -46,7 +46,11 @@ export default function ListSites() { const [sites, setSites] = useState([]); const handleEdit = (siteName: string) => { console.log(`Edit site with ID: ${siteName}`); - window.open('/admin/new-edit-site', '_self'); + const site = sites.find(s => s.name === siteName); + if (site) { + const siteData = encodeURIComponent(JSON.stringify(site)); + window.open(`/admin/new-edit-site?site=${siteData}`, '_self'); + } }; const handleDelete = (siteName: string) => { From 2d6aa54eb353d752b6e9a9bfe6ecb360fa2019c4 Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Sun, 3 Aug 2025 16:30:37 -0700 Subject: [PATCH 09/21] feat: connect API calls to save button in CreateEditSite --- src/admin/CreateEditSite.tsx | 54 +++++++++++++++++++++++++++++++----- src/admin/ListSites.tsx | 53 +---------------------------------- src/utils/siteUtils.ts | 22 +++++++++++++++ 3 files changed, 70 insertions(+), 59 deletions(-) create mode 100644 src/utils/siteUtils.ts diff --git a/src/admin/CreateEditSite.tsx b/src/admin/CreateEditSite.tsx index e157015..1aa14b6 100644 --- a/src/admin/CreateEditSite.tsx +++ b/src/admin/CreateEditSite.tsx @@ -21,6 +21,8 @@ import { Delete as DeleteIcon, } from '@mui/icons-material'; import ColorPicker from 'react-pick-color'; +import { apiClient } from '@/utils/fetch'; +import { siteToSchema } from '@/utils/siteUtils'; interface CellEntry { id: string; @@ -49,6 +51,37 @@ export default function CreateEditSite({ mode }: CreateEditSiteProps) { const [boundaryEnabled, setBoundaryEnabled] = useState(true); const [boundaryPoints, setBoundaryPoints] = useState([]); + const editSite = (site: Site) => { + apiClient.PUT('/api/secure-site', { + body: siteToSchema(site) + }).then(res => { + const { data, error } = res; + if (error) { + console.error(`Failed to edit site: ${error}`); + return; + } + console.log(`Successfully edited site: ${site.name}`); + + }).catch(err => { + console.error(`Error editing site: ${err}`); + }); + }; + + const createSite = (site: Site) => { + apiClient.POST('/api/secure-site', { + body: siteToSchema(site) + }).then(res => { + const { data, error } = res; + if (error) { + console.error(`Failed to create site: ${error}`); + return; + } + console.log(`Successfully created site: ${site.name}`); + }).catch(err => { + console.error(`Error creating site: ${err}`); + }); + }; + const handleBack = () => { console.log('Navigate back'); @@ -68,6 +101,11 @@ export default function CreateEditSite({ mode }: CreateEditSiteProps) { color: colorEnabled ? colorValue : undefined, boundary: boundaryEnabled ? boundaryPoints.map(point => [parseFloat(point.lat), parseFloat(point.lng)]) : undefined, }; + if (mode === 'edit') { + editSite(site); + } else { + createSite(site); + } } }; @@ -156,11 +194,13 @@ export default function CreateEditSite({ mode }: CreateEditSiteProps) { } if (siteData.boundary) { setBoundaryEnabled(true); - setBoundaryPoints(siteData.boundary.map((point: [number, number]) => ({ - id: Date.now().toString() + point.join(','), - lat: point[0].toString(), - lng: point[1].toString() - }))); + setBoundaryPoints(siteData.boundary + .filter((point: [number, number]) => point && point[0] !== null && point[1] !== null) + .map((point: [number, number], index: number) => ({ + id: Date.now().toString() + index, + lat: point[0].toString(), + lng: point[1].toString() + }))); } } catch (error) { console.error('Failed to parse site data from URL:', error); @@ -221,8 +261,8 @@ export default function CreateEditSite({ mode }: CreateEditSiteProps) { label="Status" > Active - Inactive - Maintenance + Confirmed + In Conversation diff --git a/src/admin/ListSites.tsx b/src/admin/ListSites.tsx index 5fae033..a3aab33 100644 --- a/src/admin/ListSites.tsx +++ b/src/admin/ListSites.tsx @@ -12,7 +12,7 @@ import { } from '@mui/material'; import { Add as AddIcon } from '@mui/icons-material'; import { apiClient } from '@/utils/fetch'; -import { components } from '@/types/api'; +import { siteToSchema } from '@/utils/siteUtils'; const parseSitesFromJSON = (jsonString: string): Site[] => { try { @@ -85,27 +85,6 @@ export default function ListSites() { reloadSites(); }); - const siteToSchema = (site: Site): components['schemas']['Site'] => { - return { - name: site.name, - latitude: site.latitude, - longitude: site.longitude, - status: siteStatusToSchema(site.status), - address: site.address, - cell_id: site.cell_id, - color: site.color, - boundary: site.boundary - }; - } - - const siteStatusToSchema = (siteStatus: SiteStatus): components['parameters']['SiteStatus'] => { - if (siteStatus === 'unknown') { - throw new Error(`Invalid site status: ${siteStatus}`); - } else { - return siteStatus as components['parameters']['SiteStatus']; - } - } - const deleteSite = (site: Site) => { apiClient.DELETE('/api/secure-site', { body: siteToSchema(site) @@ -122,37 +101,7 @@ export default function ListSites() { }); }; - const editSite = (site: Site) => { - apiClient.PUT('/api/secure-site', { - body: siteToSchema(site) - }).then(res => { - const { data, error } = res; - if (error) { - console.error(`Failed to edit site: ${error}`); - return; - } - console.log(`Successfully edited site: ${site.name}`); - reloadSites(); - }).catch(err => { - console.error(`Error editing site: ${err}`); - }); - }; - const createSite = (site: Site) => { - apiClient.POST('/api/secure-site', { - body: siteToSchema(site) - }).then(res => { - const { data, error } = res; - if (error) { - console.error(`Failed to create site: ${error}`); - return; - } - console.log(`Successfully created site: ${site.name}`); - reloadSites(); - }).catch(err => { - console.error(`Error creating site: ${err}`); - }); - }; return ( diff --git a/src/utils/siteUtils.ts b/src/utils/siteUtils.ts new file mode 100644 index 0000000..86d5730 --- /dev/null +++ b/src/utils/siteUtils.ts @@ -0,0 +1,22 @@ +import { components } from '@/types/api'; + +export const siteToSchema = (site: Site): components['schemas']['Site'] => { + return { + name: site.name, + latitude: site.latitude, + longitude: site.longitude, + status: siteStatusToSchema(site.status), + address: site.address, + cell_id: site.cell_id, + color: site.color, + boundary: site.boundary + }; +}; + +export const siteStatusToSchema = (siteStatus: SiteStatus): components['parameters']['SiteStatus'] => { + if (siteStatus === 'unknown') { + throw new Error(`Invalid site status: ${siteStatus}`); + } else { + return siteStatus as components['parameters']['SiteStatus']; + } +}; From 9a19eab4b2e407e0ba4f4528dddf01abddc6cd13 Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Sun, 3 Aug 2025 16:36:26 -0700 Subject: [PATCH 10/21] refactor: import for color picker --- package-lock.json | 31 +++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 32 insertions(+) diff --git a/package-lock.json b/package-lock.json index 6f6909d..a6f3811 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "qrcode.react": "^4.2.0", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-pick-color": "^2.0.0", "react-router": "^7.3.0", "react-router-dom": "^7.3.0", "react-select": "^5.10.1", @@ -9105,6 +9106,18 @@ "react": "^19.0.0" } }, + "node_modules/react-pick-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/react-pick-color/-/react-pick-color-2.0.0.tgz", + "integrity": "sha512-GLYyUN1k60cxkrizqRDqfmCBNP6vJZDam5TfCMMxgxPjNul9zmunAZAJ8x9wy1yMb1NqMa/MI2np7oDQLCEbDg==", + "dependencies": { + "tinycolor2": "^1.4.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/react-router": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.3.0.tgz", @@ -10414,6 +10427,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -16862,6 +16880,14 @@ "scheduler": "^0.25.0" } }, + "react-pick-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/react-pick-color/-/react-pick-color-2.0.0.tgz", + "integrity": "sha512-GLYyUN1k60cxkrizqRDqfmCBNP6vJZDam5TfCMMxgxPjNul9zmunAZAJ8x9wy1yMb1NqMa/MI2np7oDQLCEbDg==", + "requires": { + "tinycolor2": "^1.4.1" + } + }, "react-router": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.3.0.tgz", @@ -17727,6 +17753,11 @@ "convert-hrtime": "^5.0.0" } }, + "tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==" + }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/package.json b/package.json index e2f889a..7c0db0b 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "qrcode.react": "^4.2.0", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-pick-color": "^2.0.0", "react-router": "^7.3.0", "react-router-dom": "^7.3.0", "react-select": "^5.10.1", From 915ae955f1ecd82da948e40a396ab27933efdab8 Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Thu, 7 Aug 2025 19:07:56 -0700 Subject: [PATCH 11/21] fix: change how sites are parsed from JSON to fix boundary loading --- src/admin/ListSites.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/admin/ListSites.tsx b/src/admin/ListSites.tsx index a3aab33..83df70f 100644 --- a/src/admin/ListSites.tsx +++ b/src/admin/ListSites.tsx @@ -31,7 +31,7 @@ const parseSitesFromJSON = (jsonString: string): Site[] => { address: site.address, cell_id: site.cell_id, color: site.color, - boundary: site.boundary?.map((point: any) => [point.lat, point.lng] as [number, number]) ?? undefined, + boundary: site.boundary?.map((point: any) => [point[0], point[1]] as [number, number]) ?? undefined, }; }); From 071aea19841a3274e091201c9b1fa07f80d1bdb1 Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Thu, 7 Aug 2025 19:08:47 -0700 Subject: [PATCH 12/21] refactor: run npm format --- src/ListItems.tsx | 4 +- src/admin/CreateEditSite.tsx | 234 +++++++++++++++++++++-------------- src/admin/ListSites.tsx | 37 +++--- src/types.d.ts | 10 +- src/utils/siteUtils.ts | 6 +- 5 files changed, 171 insertions(+), 120 deletions(-) diff --git a/src/ListItems.tsx b/src/ListItems.tsx index 4a1b485..972e3ee 100644 --- a/src/ListItems.tsx +++ b/src/ListItems.tsx @@ -31,9 +31,7 @@ export const mainListItems = (
- window.open('/admin/list-sites', '_self')} - > + window.open('/admin/list-sites', '_self')}> diff --git a/src/admin/CreateEditSite.tsx b/src/admin/CreateEditSite.tsx index 1aa14b6..74f6188 100644 --- a/src/admin/CreateEditSite.tsx +++ b/src/admin/CreateEditSite.tsx @@ -51,37 +51,41 @@ export default function CreateEditSite({ mode }: CreateEditSiteProps) { const [boundaryEnabled, setBoundaryEnabled] = useState(true); const [boundaryPoints, setBoundaryPoints] = useState([]); - const editSite = (site: Site) => { - apiClient.PUT('/api/secure-site', { - body: siteToSchema(site) - }).then(res => { + const editSite = (site: Site) => { + apiClient + .PUT('/api/secure-site', { + body: siteToSchema(site), + }) + .then(res => { const { data, error } = res; if (error) { console.error(`Failed to edit site: ${error}`); return; } console.log(`Successfully edited site: ${site.name}`); - - }).catch(err => { + }) + .catch(err => { console.error(`Error editing site: ${err}`); }); - }; + }; - const createSite = (site: Site) => { - apiClient.POST('/api/secure-site', { - body: siteToSchema(site) - }).then(res => { + const createSite = (site: Site) => { + apiClient + .POST('/api/secure-site', { + body: siteToSchema(site), + }) + .then(res => { const { data, error } = res; if (error) { console.error(`Failed to create site: ${error}`); return; } console.log(`Successfully created site: ${site.name}`); - }).catch(err => { + }) + .catch(err => { console.error(`Error creating site: ${err}`); }); - }; - + }; const handleBack = () => { console.log('Navigate back'); @@ -99,7 +103,12 @@ export default function CreateEditSite({ mode }: CreateEditSiteProps) { address, cell_id: cells.map(cell => cell.cellId), color: colorEnabled ? colorValue : undefined, - boundary: boundaryEnabled ? boundaryPoints.map(point => [parseFloat(point.lat), parseFloat(point.lng)]) : undefined, + boundary: boundaryEnabled + ? boundaryPoints.map(point => [ + parseFloat(point.lat), + parseFloat(point.lng), + ]) + : undefined, }; if (mode === 'edit') { editSite(site); @@ -112,7 +121,7 @@ export default function CreateEditSite({ mode }: CreateEditSiteProps) { const addCell = () => { const newCell: CellEntry = { id: Date.now().toString(), - cellId: '' + cellId: '', }; setCells([...cells, newCell]); }; @@ -122,14 +131,14 @@ export default function CreateEditSite({ mode }: CreateEditSiteProps) { }; const updateCellId = (id: string, cellId: string) => { - setCells(cells.map(cell => cell.id === id ? { ...cell, cellId } : cell)); + setCells(cells.map(cell => (cell.id === id ? { ...cell, cellId } : cell))); }; const addBoundaryPoint = () => { const newPoint: BoundaryPoint = { id: Date.now().toString(), lat: '', - lng: '' + lng: '', }; setBoundaryPoints([...boundaryPoints, newPoint]); }; @@ -138,13 +147,19 @@ export default function CreateEditSite({ mode }: CreateEditSiteProps) { setBoundaryPoints(boundaryPoints.filter(point => point.id !== id)); }; - const updateBoundaryPoint = (id: string, field: 'lat' | 'lng', value: string) => { - setBoundaryPoints(boundaryPoints.map(point => - point.id === id ? { ...point, [field]: value } : point - )); + const updateBoundaryPoint = ( + id: string, + field: 'lat' | 'lng', + value: string, + ) => { + setBoundaryPoints( + boundaryPoints.map(point => + point.id === id ? { ...point, [field]: value } : point, + ), + ); }; - const validateSite = () : boolean => { + const validateSite = (): boolean => { if (name === '') { alert('Name is required'); return false; @@ -170,7 +185,7 @@ export default function CreateEditSite({ mode }: CreateEditSiteProps) { return false; } return true; - } + }; useEffect(() => { if (mode === 'edit') { @@ -184,23 +199,30 @@ export default function CreateEditSite({ mode }: CreateEditSiteProps) { setLongitude(siteData.longitude.toString()); setStatus(siteData.status); setAddress(siteData.address); - setCells(siteData.cell_id.map((cellId: string) => ({ - id: Date.now().toString() + cellId, - cellId: cellId - }))); + setCells( + siteData.cell_id.map((cellId: string) => ({ + id: Date.now().toString() + cellId, + cellId: cellId, + })), + ); if (siteData.color) { setColorEnabled(true); setColorValue(siteData.color); } if (siteData.boundary) { setBoundaryEnabled(true); - setBoundaryPoints(siteData.boundary - .filter((point: [number, number]) => point && point[0] !== null && point[1] !== null) - .map((point: [number, number], index: number) => ({ - id: Date.now().toString() + index, - lat: point[0].toString(), - lng: point[1].toString() - }))); + setBoundaryPoints( + siteData.boundary + .filter( + (point: [number, number]) => + point && point[0] !== null && point[1] !== null, + ) + .map((point: [number, number], index: number) => ({ + id: Date.now().toString() + index, + lat: point[0].toString(), + lng: point[1].toString(), + })), + ); } } catch (error) { console.error('Failed to parse site data from URL:', error); @@ -210,14 +232,14 @@ export default function CreateEditSite({ mode }: CreateEditSiteProps) { }, [mode]); return ( - + - - ))} + + (Lat, Long) + + + updateBoundaryPoint(point.id, 'lat', e.target.value) + } + sx={{ flexGrow: 1 }} + /> + + updateBoundaryPoint(point.id, 'lng', e.target.value) + } + sx={{ flexGrow: 1 }} + /> + + + ))} diff --git a/src/admin/ListSites.tsx b/src/admin/ListSites.tsx index 83df70f..99f0756 100644 --- a/src/admin/ListSites.tsx +++ b/src/admin/ListSites.tsx @@ -31,7 +31,10 @@ const parseSitesFromJSON = (jsonString: string): Site[] => { address: site.address, cell_id: site.cell_id, color: site.color, - boundary: site.boundary?.map((point: any) => [point[0], point[1]] as [number, number]) ?? undefined, + boundary: + site.boundary?.map( + (point: any) => [point[0], point[1]] as [number, number], + ) ?? undefined, }; }); @@ -63,7 +66,7 @@ export default function ListSites() { const handleAdd = () => { console.log('Add new site'); - window.open('/admin/create-site', '_self') + window.open('/admin/create-site', '_self'); }; const reloadSites = () => { @@ -86,22 +89,24 @@ export default function ListSites() { }); const deleteSite = (site: Site) => { - apiClient.DELETE('/api/secure-site', { - body: siteToSchema(site) - }).then(res => { - const { data, error } = res; - if (error) { - console.error(`Failed to delete site: ${error}`); - return; - } - console.log(`Successfully deleted site: ${site.name}`); - reloadSites(); - }).catch(err => { - console.error(`Error deleting site: ${err}`); - }); + apiClient + .DELETE('/api/secure-site', { + body: siteToSchema(site), + }) + .then(res => { + const { data, error } = res; + if (error) { + console.error(`Failed to delete site: ${error}`); + return; + } + console.log(`Successfully deleted site: ${site.name}`); + reloadSites(); + }) + .catch(err => { + console.error(`Error deleting site: ${err}`); + }); }; - return ( diff --git a/src/types.d.ts b/src/types.d.ts index 569d7e7..bd2f21a 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -24,7 +24,13 @@ type DisplayOption = { type SiteStatus = 'active' | 'confirmed' | 'in-conversation' | 'unknown'; -type AdminPage = 'users' | 'edit-site' | 'edit-data' | 'list-sites' | 'create-site' | 'new-edit-site'; +type AdminPage = + | 'users' + | 'edit-site' + | 'edit-data' + | 'list-sites' + | 'create-site' + | 'new-edit-site'; type UserRow = { identity: string; @@ -80,5 +86,3 @@ type Measurement = { site: string; device_id: number; }; - - diff --git a/src/utils/siteUtils.ts b/src/utils/siteUtils.ts index 86d5730..b27d7ed 100644 --- a/src/utils/siteUtils.ts +++ b/src/utils/siteUtils.ts @@ -9,11 +9,13 @@ export const siteToSchema = (site: Site): components['schemas']['Site'] => { address: site.address, cell_id: site.cell_id, color: site.color, - boundary: site.boundary + boundary: site.boundary, }; }; -export const siteStatusToSchema = (siteStatus: SiteStatus): components['parameters']['SiteStatus'] => { +export const siteStatusToSchema = ( + siteStatus: SiteStatus, +): components['parameters']['SiteStatus'] => { if (siteStatus === 'unknown') { throw new Error(`Invalid site status: ${siteStatus}`); } else { From 80ba9765e295c020b11b90c633fb145947654358 Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Fri, 8 Aug 2025 16:09:24 -0700 Subject: [PATCH 13/21] feat: add additional validation to all lat/long data entry in CreateEditSites.tsx --- src/admin/CreateEditSite.tsx | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/src/admin/CreateEditSite.tsx b/src/admin/CreateEditSite.tsx index 74f6188..4c628bb 100644 --- a/src/admin/CreateEditSite.tsx +++ b/src/admin/CreateEditSite.tsx @@ -164,11 +164,21 @@ export default function CreateEditSite({ mode }: CreateEditSiteProps) { alert('Name is required'); return false; } - if (longitude === '' || isNaN(Number(longitude))) { + if ( + longitude === '' || + isNaN(Number(longitude)) || + Number(longitude) < -180 || + Number(longitude) > 180 + ) { alert('Valid Longitude is required'); return false; } - if (latitude === '' || isNaN(Number(latitude))) { + if ( + latitude === '' || + isNaN(Number(latitude)) || + Number(latitude) < -90 || + Number(latitude) > 90 + ) { alert('Valid Latitude is required'); return false; } @@ -184,6 +194,28 @@ export default function CreateEditSite({ mode }: CreateEditSiteProps) { alert('At least one Cell ID is required'); return false; } + if (boundaryEnabled) { + for (const point of boundaryPoints) { + if ( + point.lat === '' || + isNaN(Number(point.lat)) || + Number(point.lat) < -90 || + Number(point.lat) > 90 + ) { + alert('Valid Latitude for Boundary Point is required'); + return false; + } + if ( + point.lng === '' || + isNaN(Number(point.lng)) || + Number(point.lng) < -180 || + Number(point.lng) > 180 + ) { + alert('Valid Longitude for Boundary Point is required'); + return false; + } + } + } return true; }; From 5a449713fb133011861e403afa3d2b6159751fa4 Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Fri, 8 Aug 2025 16:12:03 -0700 Subject: [PATCH 14/21] refactor: replace old edit-site routing with new UI --- src/ListItems.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/ListItems.tsx b/src/ListItems.tsx index 972e3ee..de1add4 100644 --- a/src/ListItems.tsx +++ b/src/ListItems.tsx @@ -19,7 +19,7 @@ export const mainListItems = ( - window.open('/admin/edit-site', '_self')}> + window.open('/admin/list-sites', '_self')}> @@ -31,12 +31,7 @@ export const mainListItems = ( - window.open('/admin/list-sites', '_self')}> - - - - - + ); From 04bb588984674b44ccce74a5120f2eba0af839bf Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Sat, 9 Aug 2025 15:52:43 -0700 Subject: [PATCH 15/21] refactor: replace api/sites with api/public-sites --- src/admin/EditSite.tsx | 2 +- src/vis/Vis.tsx | 22 ++++++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/admin/EditSite.tsx b/src/admin/EditSite.tsx index b0469d7..40452ba 100644 --- a/src/admin/EditSite.tsx +++ b/src/admin/EditSite.tsx @@ -90,7 +90,7 @@ export default function EditSite() { }; const reloadSites = () => { apiClient - .GET('/api/sites') + .GET('/api/public-sites') .then(res => { const { data, error } = res; if (error || !data) { diff --git a/src/vis/Vis.tsx b/src/vis/Vis.tsx index c496878..3e0263c 100644 --- a/src/vis/Vis.tsx +++ b/src/vis/Vis.tsx @@ -149,20 +149,22 @@ export default function Vis() { useEffect(() => { (async () => { try { - const { data, error } = await apiClient.GET('/api/sites'); + const { data, error } = await apiClient.GET('/api/public-sites'); if (!data || error) { console.log(`Cannot fetch site info: ${error}`); return; } - - const siteOptions = data.map(({ name, status }) => ({ - label: name, - value: name, - status: status, - })); - setSites(data); - setSiteOptions(siteOptions); - setSelectedSites(siteOptions); + if (data.sites) { + const siteOptions = data.sites.map(({ name, status }) => ({ + label: name, + value: name, + status: status, + })); + setSites(data.sites); + setSiteOptions(siteOptions); + setSelectedSites(siteOptions); + } + } catch (error) { console.error(`Error while fetching site data: ${error}`); return; From fdce863c0456ee29bef17df7860b11b88489985c Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Sat, 9 Aug 2025 16:00:05 -0700 Subject: [PATCH 16/21] Revert "refactor: replace api/sites with api/public-sites" This reverts commit 04bb588984674b44ccce74a5120f2eba0af839bf. --- src/admin/EditSite.tsx | 2 +- src/vis/Vis.tsx | 22 ++++++++++------------ 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/admin/EditSite.tsx b/src/admin/EditSite.tsx index 40452ba..b0469d7 100644 --- a/src/admin/EditSite.tsx +++ b/src/admin/EditSite.tsx @@ -90,7 +90,7 @@ export default function EditSite() { }; const reloadSites = () => { apiClient - .GET('/api/public-sites') + .GET('/api/sites') .then(res => { const { data, error } = res; if (error || !data) { diff --git a/src/vis/Vis.tsx b/src/vis/Vis.tsx index 3e0263c..c496878 100644 --- a/src/vis/Vis.tsx +++ b/src/vis/Vis.tsx @@ -149,22 +149,20 @@ export default function Vis() { useEffect(() => { (async () => { try { - const { data, error } = await apiClient.GET('/api/public-sites'); + const { data, error } = await apiClient.GET('/api/sites'); if (!data || error) { console.log(`Cannot fetch site info: ${error}`); return; } - if (data.sites) { - const siteOptions = data.sites.map(({ name, status }) => ({ - label: name, - value: name, - status: status, - })); - setSites(data.sites); - setSiteOptions(siteOptions); - setSelectedSites(siteOptions); - } - + + const siteOptions = data.map(({ name, status }) => ({ + label: name, + value: name, + status: status, + })); + setSites(data); + setSiteOptions(siteOptions); + setSelectedSites(siteOptions); } catch (error) { console.error(`Error while fetching site data: ${error}`); return; From afa04c01f9bf11be7bf0f013da51427a33eb4f35 Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Sat, 9 Aug 2025 16:32:27 -0700 Subject: [PATCH 17/21] refactor: replace api/public-sites with api/sites and make necessary changes to preserve functionality --- src/ListItems.tsx | 1 - src/admin/ListSites.tsx | 8 +++--- src/types/api.d.ts | 63 +++++++++++++++++------------------------ 3 files changed, 30 insertions(+), 42 deletions(-) diff --git a/src/ListItems.tsx b/src/ListItems.tsx index de1add4..1fce4d0 100644 --- a/src/ListItems.tsx +++ b/src/ListItems.tsx @@ -31,7 +31,6 @@ export const mainListItems = ( - ); diff --git a/src/admin/ListSites.tsx b/src/admin/ListSites.tsx index 99f0756..3c12241 100644 --- a/src/admin/ListSites.tsx +++ b/src/admin/ListSites.tsx @@ -18,11 +18,11 @@ const parseSitesFromJSON = (jsonString: string): Site[] => { try { const parsed = JSON.parse(jsonString); - if (!Array.isArray(parsed.sites)) { - throw new Error("Invalid format: 'sites' should be an array"); + if (!Array.isArray(parsed)) { + throw new Error('Invalid format: response should be an array of sites'); } - const sites: Site[] = parsed.sites.map((site: any): Site => { + const sites: Site[] = parsed.map((site: any): Site => { return { name: site.name, latitude: site.latitude, @@ -71,7 +71,7 @@ export default function ListSites() { const reloadSites = () => { apiClient - .GET('/api/public-sites') + .GET('/api/sites') .then(res => { const { data, error } = res; if (error || !data) { diff --git a/src/types/api.d.ts b/src/types/api.d.ts index 0e54a52..83174b5 100644 --- a/src/types/api.d.ts +++ b/src/types/api.d.ts @@ -415,7 +415,7 @@ export interface paths { patch?: never; trace?: never; }; - '/api/sites': { + '/api/old-sites': { parameters: { query?: never; header?: never; @@ -426,7 +426,7 @@ export interface paths { * Get all sites * @description Returns a list of all available sites with their location and status information */ - get: operations['getSites']; + get: operations['getSitesOld']; put?: never; post?: never; delete?: never; @@ -435,7 +435,7 @@ export interface paths { patch?: never; trace?: never; }; - '/api/public-sites': { + '/api/sites': { parameters: { query?: never; header?: never; @@ -443,41 +443,10 @@ export interface paths { cookie?: never; }; /** - * Get sites list - * @description Returns a list of public sites + * Get all sites + * @description Returns a list of all available sites with their location and status information */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description List of public sites */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @description List of sites */ - sites?: components['schemas']['Site'][]; - }; - }; - }; - /** @description Server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'text/plain': string; - }; - }; - }; - }; + get: operations['getSites']; put?: never; post?: never; delete?: never; @@ -1940,6 +1909,26 @@ export interface operations { }; }; }; + getSitesOld: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of sites */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Site'][]; + }; + }; + }; + }; getSites: { parameters: { query?: never; From 9305d5386ed2d7fc3db7d44d7606838edd10ce03 Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Tue, 19 Aug 2025 16:06:24 -0700 Subject: [PATCH 18/21] feat: add confirmation dialog to site deletion plus edit handleSave so it goes back after site is saved --- src/admin/CreateEditSite.tsx | 24 +++++++++++++++--------- src/admin/ListSites.tsx | 8 ++++++-- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/admin/CreateEditSite.tsx b/src/admin/CreateEditSite.tsx index 4c628bb..e5d715c 100644 --- a/src/admin/CreateEditSite.tsx +++ b/src/admin/CreateEditSite.tsx @@ -52,7 +52,7 @@ export default function CreateEditSite({ mode }: CreateEditSiteProps) { const [boundaryPoints, setBoundaryPoints] = useState([]); const editSite = (site: Site) => { - apiClient + return apiClient .PUT('/api/secure-site', { body: siteToSchema(site), }) @@ -60,17 +60,20 @@ export default function CreateEditSite({ mode }: CreateEditSiteProps) { const { data, error } = res; if (error) { console.error(`Failed to edit site: ${error}`); - return; + return Promise.reject(error); } console.log(`Successfully edited site: ${site.name}`); + return data; }) .catch(err => { console.error(`Error editing site: ${err}`); + return Promise.reject(err); }); }; + const createSite = (site: Site) => { - apiClient + return apiClient .POST('/api/secure-site', { body: siteToSchema(site), }) @@ -78,12 +81,14 @@ export default function CreateEditSite({ mode }: CreateEditSiteProps) { const { data, error } = res; if (error) { console.error(`Failed to create site: ${error}`); - return; + return Promise.reject(error); } console.log(`Successfully created site: ${site.name}`); + return data; }) .catch(err => { console.error(`Error creating site: ${err}`); + return Promise.reject(err); }); }; @@ -110,11 +115,12 @@ export default function CreateEditSite({ mode }: CreateEditSiteProps) { ]) : undefined, }; - if (mode === 'edit') { - editSite(site); - } else { - createSite(site); - } + const savePromise = + mode === 'edit' ? editSite(site) : createSite(site); + + savePromise.then(() => { + handleBack(); + }); } }; diff --git a/src/admin/ListSites.tsx b/src/admin/ListSites.tsx index 3c12241..3902980 100644 --- a/src/admin/ListSites.tsx +++ b/src/admin/ListSites.tsx @@ -59,11 +59,15 @@ export default function ListSites() { const handleDelete = (siteName: string) => { const site = sites.find(s => s.name === siteName); if (site) { - deleteSite(site); - reloadSites(); + const confirmed = window.confirm(`Are you sure you want to delete "${site.name}"?`); + if (confirmed) { + deleteSite(site); + reloadSites(); + } } }; + const handleAdd = () => { console.log('Add new site'); window.open('/admin/create-site', '_self'); From a4e9f7d61eedd165809fdccb6be1d7c171130052 Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Tue, 19 Aug 2025 16:07:05 -0700 Subject: [PATCH 19/21] refactor: run npm format --- src/admin/CreateEditSite.tsx | 4 +--- src/admin/ListSites.tsx | 5 +++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/admin/CreateEditSite.tsx b/src/admin/CreateEditSite.tsx index e5d715c..d074faf 100644 --- a/src/admin/CreateEditSite.tsx +++ b/src/admin/CreateEditSite.tsx @@ -71,7 +71,6 @@ export default function CreateEditSite({ mode }: CreateEditSiteProps) { }); }; - const createSite = (site: Site) => { return apiClient .POST('/api/secure-site', { @@ -115,8 +114,7 @@ export default function CreateEditSite({ mode }: CreateEditSiteProps) { ]) : undefined, }; - const savePromise = - mode === 'edit' ? editSite(site) : createSite(site); + const savePromise = mode === 'edit' ? editSite(site) : createSite(site); savePromise.then(() => { handleBack(); diff --git a/src/admin/ListSites.tsx b/src/admin/ListSites.tsx index 3902980..3f6f275 100644 --- a/src/admin/ListSites.tsx +++ b/src/admin/ListSites.tsx @@ -59,7 +59,9 @@ export default function ListSites() { const handleDelete = (siteName: string) => { const site = sites.find(s => s.name === siteName); if (site) { - const confirmed = window.confirm(`Are you sure you want to delete "${site.name}"?`); + const confirmed = window.confirm( + `Are you sure you want to delete "${site.name}"?`, + ); if (confirmed) { deleteSite(site); reloadSites(); @@ -67,7 +69,6 @@ export default function ListSites() { } }; - const handleAdd = () => { console.log('Add new site'); window.open('/admin/create-site', '_self'); From c47f4764379dd8aba98485fed414695df489e094 Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Wed, 27 Aug 2025 20:34:16 -0700 Subject: [PATCH 20/21] refactor: remove old-sites --- src/types/api.d.ts | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/types/api.d.ts b/src/types/api.d.ts index 83174b5..c9b4368 100644 --- a/src/types/api.d.ts +++ b/src/types/api.d.ts @@ -415,26 +415,6 @@ export interface paths { patch?: never; trace?: never; }; - '/api/old-sites': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get all sites - * @description Returns a list of all available sites with their location and status information - */ - get: operations['getSitesOld']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; '/api/sites': { parameters: { query?: never; From f78de4a38ab2b82294f6c1851a0281b9671d8308 Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Thu, 28 Aug 2025 12:07:22 -0700 Subject: [PATCH 21/21] fix: put edit-sites behind secure --- src/admin/CreateEditSite.tsx | 4 ++-- src/admin/ListSites.tsx | 2 +- src/types/api.d.ts | 22 +--------------------- 3 files changed, 4 insertions(+), 24 deletions(-) diff --git a/src/admin/CreateEditSite.tsx b/src/admin/CreateEditSite.tsx index d074faf..7d42add 100644 --- a/src/admin/CreateEditSite.tsx +++ b/src/admin/CreateEditSite.tsx @@ -53,7 +53,7 @@ export default function CreateEditSite({ mode }: CreateEditSiteProps) { const editSite = (site: Site) => { return apiClient - .PUT('/api/secure-site', { + .PUT('/secure/edit-sites', { body: siteToSchema(site), }) .then(res => { @@ -73,7 +73,7 @@ export default function CreateEditSite({ mode }: CreateEditSiteProps) { const createSite = (site: Site) => { return apiClient - .POST('/api/secure-site', { + .POST('/secure/edit-sites', { body: siteToSchema(site), }) .then(res => { diff --git a/src/admin/ListSites.tsx b/src/admin/ListSites.tsx index 3f6f275..028f61c 100644 --- a/src/admin/ListSites.tsx +++ b/src/admin/ListSites.tsx @@ -95,7 +95,7 @@ export default function ListSites() { const deleteSite = (site: Site) => { apiClient - .DELETE('/api/secure-site', { + .DELETE('/secure/edit-sites', { body: siteToSchema(site), }) .then(res => { diff --git a/src/types/api.d.ts b/src/types/api.d.ts index c9b4368..454747e 100644 --- a/src/types/api.d.ts +++ b/src/types/api.d.ts @@ -1075,7 +1075,7 @@ export interface paths { patch?: never; trace?: never; }; - '/api/secure-site': { + '/secure/edit-sites': { parameters: { query?: never; header?: never; @@ -1889,26 +1889,6 @@ export interface operations { }; }; }; - getSitesOld: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description List of sites */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Site'][]; - }; - }; - }; - }; getSites: { parameters: { query?: never;