diff --git a/src/components/routes/Router.js b/src/components/routes/Router.js index 89e838e0f3105da7e32cca27afbcbc7d67274deb..048891bf250972c7b31ef8b3036557d1cec65968 100644 --- a/src/components/routes/Router.js +++ b/src/components/routes/Router.js @@ -3,6 +3,7 @@ import React from "react"; import { BrowserRouter as Router, Route, Routes, Link } from "react-router-dom"; import LoginPage from "../../pages/auth/login/LoginPage"; import HomePage from "../../pages/home/HomePage"; +import AdminPage from "../../pages/admin/admin"; import BookingsPage from "../../pages/bookings/bookings"; import NotificationsPage from "../../pages/notifications/notifications"; import ParkingAreasPage from "../../pages/parking_areas/parking_areas"; @@ -61,6 +62,13 @@ const MainRoutes = [ component: ParkingAreasPage, protected: true }, + { + name: "Admin", + path: "/admin", + exact: true, + component: < AdminPage />, + protected: false + }, ]; diff --git a/src/pages/admin/admin.js b/src/pages/admin/admin.js new file mode 100644 index 0000000000000000000000000000000000000000..d1c1b55242493ef26946fb3d0609450a22a1285a --- /dev/null +++ b/src/pages/admin/admin.js @@ -0,0 +1,340 @@ +import React, { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import { + Box, + Button, + Container, + Heading, + VStack, + Text, + useDisclosure, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalCloseButton, + ModalBody, + ModalFooter, + FormControl, + FormLabel, + Input, + NumberInput, + NumberInputField, + NumberInputStepper, + NumberIncrementStepper, + NumberDecrementStepper, + useToast, +} from "@chakra-ui/react"; + +import { CloseIcon } from "@chakra-ui/icons"; + +const AdminPage = () => { + const token = localStorage.getItem('token'); + const navigate = useNavigate(); + const [isLoading, setLoading] = useState(true); + + const isAdmin = async () => { + const response = await fetch(`${baseUrl}/admin`); + + setLoading(false); + if (!response.ok) { + navigate('/'); + } + } + + const baseUrl = (process.env.REACT_APP_AUTH_SERVICE_ENDPOINT); + useEffect(() => { + isAdmin(); + }, []); + + return isLoading ? <Text>Loading</Text> : <AdminPageView /> +} + +const AdminPageView = () => { + const toast = useToast(); + const navigate = useNavigate(); + const [locations, setLocations] = useState([]); + const { isOpen, onOpen, onClose } = useDisclosure(); + const [newLocation, setNewLocation] = useState({ + title: "", + address: "", + latitude: 0, + longitude: 0, + spaces: 0, + }); + + // Load locations when the page is loaded + useEffect(() => { + loadLocations(); + }, []); + + // Fetch locations from the server + const loadLocations = async () => { + // const response = await fetch("/api/locations"); + // const data = await response.json(); + const data = [ + { + _id: 1, + title: "sd", + address: "sd", + latitude: 0, + longitude: 0, + spaces: 0, + }, + { + _id: 2, + title: "sd", + address: "sd", + latitude: 0, + longitude: 0, + spaces: 0, + } + ]; + setLocations(data); + }; + + // Add a new location + const addLocation = async (location) => { + + const { title, address, latitude, longitude, spaces } = location; + + console.log(location); + + try { + const response = await fetch("/api/locations", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(newLocation), + }); + if (!response.ok) { + throw new Error("Failed to add location"); + } + setNewLocation({ + title: title, + address: address, + latitude: latitude, + longitude: longitude, + spaces: spaces, + }); + toast({ + title: "Added new location.", + status: "success", + duration: 2000, + isClosable: true, + }); + } catch (error) { + toast({ + title: error.message ?? "Failed to add location.", + status: "error", + duration: 2000, + isClosable: true, + }); + } + loadLocations(); + onClose(); + }; + + // Delete a location + const deleteLocation = async (id) => { + try { + const response = await fetch(`/api/locations/${id}`, { + method: 'DELETE', + headers: { + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + }); + if (!response.ok) { + throw new Error("Failed to delete location"); + } + setLocations((prevLocations) => + prevLocations.filter( + (location) => location._id !== id + ) + ); + toast({ + title: "Location deleted.", + status: "success", + duration: 2000, + isClosable: true, + }); + } catch (error) { + toast({ + title: "Failed to delete location.", + status: "error", + duration: 2000, + isClosable: true, + }); + } + loadLocations(); + }; + + return ( + <Box> + <Box bg="gray.100" py="4" px="2" textAlign="center"> + <Heading as="h1" size="xl"> + Admin Panel + </Heading> + <Button onClick={() => { + localStorage.removeItem('token'); + navigate("/"); + }}> + Sign Out + </Button> + </Box> + <br /> + <Container p={4} align={"center"} borderWidth={1} > + <VStack spacing="6"> + <Box w="100%"> + <Text fontSize="2xl" fontWeight="semibold"> + Location Management + </Text> + <Button colorScheme="blue" size="sm" onClick={onOpen}> + Add New Location + </Button> + </Box> + <Box minW={"50vw"}> + {locations.map((location) => ( + <Box + key={`parent${location._id}`} + py={1}> + <Box + key={location._id} + p={4} + display={"flex"} + flexDir={"row"} + justifyContent={"space-between"} + borderWidth={1} + borderRadius={4} + > + <div> + <Text mr={4} fontSize={18}> + <strong></strong> + <strong>Title:</strong> {location.title} + </Text> + <Text flex="1"> + <strong>Address:</strong> {location.address} + </Text> + <Text flex="1"> + <strong>Longitude:</strong> {location.longitude} + </Text> + <Text flex="1"> + <strong>Latitude:</strong> {location.latitude} + </Text> + <Text flex="1"> + <strong>Spaces:</strong> {location.spaces} + </Text> + </div> + <Button + key={`delete:${location._id}`} + rightIcon={<CloseIcon />} + aria-label="Delete location" + onClick={() => deleteLocation(location._id)} + > + Delete + </Button> + </Box> + </Box> + ))} + </Box> + </VStack> + </Container> + + <Modal + isOpen={isOpen} + onClose={onClose} + size="md" + closeOnOverlayClick={false} + > + <ModalOverlay /> + <ModalContent> + <ModalHeader>Add Location / Parking Area</ModalHeader> + <ModalCloseButton /> + <ModalBody> + <VStack spacing="4"> + <FormControl id="title"> + <FormLabel>Title</FormLabel> + <Input + type="text" + value={newLocation.title} + onChange={(e) => + setNewLocation({ ...newLocation, title: e.target.value }) + } + /> + </FormControl> + <FormControl id="address"> + <FormLabel>Address</FormLabel> + <Input + type="text" + value={newLocation.address} + onChange={(e) => + setNewLocation({ ...newLocation, address: e.target.value }) + } + /> + </FormControl> + <FormControl id="latitude"> + <FormLabel>Latitude</FormLabel> + <Input + type="number" + step="0.000001" + value={newLocation.latitude} + onChange={(e) => + setNewLocation({ + ...newLocation, + latitude: parseFloat(e.target.value), + }) + } + /> + </FormControl> + <FormControl id="longitude"> + <FormLabel>Longitude</FormLabel> + <Input + type="number" + step="0.000001" + value={newLocation.longitude} + onChange={(e) => + setNewLocation({ + ...newLocation, + longitude: parseFloat(e.target.value), + }) + } + /> + </FormControl> + <FormControl id="spaces"> + <FormLabel>Spaces</FormLabel> + <NumberInput + value={newLocation.spaces} + onChange={(value) => { + const intValue = parseInt(value); + if (!isNaN(intValue) && intValue > 0) { + setNewLocation({ + ...newLocation, + spaces: intValue, + }); + } + } + } + > + <NumberInputField /> + <NumberInputStepper> + <NumberIncrementStepper /> + <NumberDecrementStepper /> + </NumberInputStepper> + </NumberInput> + </FormControl> + </VStack> + </ModalBody> + <ModalFooter> + <Button variant="ghost" mr="2" onClick={onClose}> + Cancel + </Button> + <Button colorScheme="blue" onClick={() => addLocation(newLocation)}> + Add Location + </Button> + </ModalFooter> + </ModalContent> + </Modal> + </Box> + ); +}; + +export default AdminPage; \ No newline at end of file