diff --git a/resq/frontend/package.json b/resq/frontend/package.json index d7d8f370..f5572041 100644 --- a/resq/frontend/package.json +++ b/resq/frontend/package.json @@ -10,11 +10,13 @@ "@mui/material": "^5.14.14", "@mui/styled-engine": "^5.14.14", "@mui/x-data-grid": "^6.18.1", + "@mui/x-date-pickers": "^6.18.2", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "axios": "^1.6.0", "bootstrap": "^5.3.2", + "dayjs": "^1.11.10", "next": "^14.0.0", "npm": "^10.2.1", "pigeon-maps": "^0.21.3", diff --git a/resq/frontend/src/App.js b/resq/frontend/src/App.js index 835bbd17..ad52daf0 100644 --- a/resq/frontend/src/App.js +++ b/resq/frontend/src/App.js @@ -9,6 +9,8 @@ import Account from "./pages/Account"; import RoleRequest from "./pages/RoleRequest"; import LogoutIcon from '@mui/icons-material/Logout'; import Request from "./pages/RequestCreation"; +import {LocalizationProvider} from "@mui/x-date-pickers"; +import {AdapterDayjs} from "@mui/x-date-pickers/AdapterDayjs"; const SmallRedCircle = () => function App() { const [token, _setToken] = useState(localStorage.getItem("token")) const [role, setRole] = useState("") - // eslint-disable-next-line no-unused-vars - const [width, setWidth] = useState(window.innerWidth); const [height, setHeight] = useState(window.innerHeight); const updateDimensions = () => { - setWidth(window.innerWidth); setHeight(window.innerHeight); } useEffect(() => { @@ -63,81 +62,83 @@ function App() { const ref = useRef(null) return ( - -
- - - - - ResQ - - - - - - - - -
- - {navLinks.map(({path, component}) => ( - - ))} - }/> - - { - token ? <> - - - - : <> - - - - } - -
-
- + } + + + + + ); } diff --git a/resq/frontend/src/components/DisasterMap.js b/resq/frontend/src/components/DisasterMap.js index 12b2f7e0..7447897f 100644 --- a/resq/frontend/src/components/DisasterMap.js +++ b/resq/frontend/src/components/DisasterMap.js @@ -1,5 +1,5 @@ import * as React from 'react'; -import {useEffect, useState} from 'react'; +import {useState} from 'react'; import {Map, Marker, ZoomControl} from 'pigeon-maps'; import {type_colors} from "../Colors"; import {AnnotationIcon, MarkerIcon} from "./MapIcons"; @@ -17,13 +17,8 @@ function mapboxProvider(x, y, z, dpr) { const marker_order = ["Annotation", "Request", "Resource"] -export default function DisasterMap({onPointSelected, markers = [], center}) { +export default function DisasterMap({onPointSelected, markers = [], mapCenter, setMapCenter, onBoundsChanged}) { const [zoom, setZoom] = useState(6.5); - const [mapCenter, setMapCenter] = useState([39, 34.5]) - - useEffect(() => { - center && setMapCenter(center); - }, [center]) const renderMarker = (marker) => { return ( @@ -36,7 +31,8 @@ export default function DisasterMap({onPointSelected, markers = [], center}) { event.preventDefault() }} > - {marker.type==="Annotation" ? : } + {marker.type === "Annotation" ? : + } ); }; @@ -56,13 +52,14 @@ export default function DisasterMap({onPointSelected, markers = [], center}) { onPointSelected(null); event.preventDefault() }} - onBoundsChanged={({center, zoom}) => { + onBoundsChanged={({center, zoom, bounds}) => { setMapCenter(center) setZoom(zoom) + onBoundsChanged(bounds) }}> {markers - .sort(({type})=> -marker_order.indexOf(type)) + .sort(({type}) => -marker_order.indexOf(type)) .map(renderMarker)} diff --git a/resq/frontend/src/pages/ListCards.js b/resq/frontend/src/components/ListCards.js similarity index 95% rename from resq/frontend/src/pages/ListCards.js rename to resq/frontend/src/components/ListCards.js index 1636f947..a89cb5ce 100644 --- a/resq/frontend/src/pages/ListCards.js +++ b/resq/frontend/src/components/ListCards.js @@ -7,7 +7,7 @@ import {type_colors} from "../Colors"; import Typography from "@mui/material/Typography"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import styled from "styled-components"; -import {AnnotationIcon} from "../components/MapIcons"; +import {AnnotationIcon} from "./MapIcons"; const ExpandMore = styled(IconButton)` transform: ${({expand}) => !expand ? 'rotate(0deg)' : 'rotate(180deg)'}; @@ -34,7 +34,7 @@ async function getAddress(latitude, longitude) { } -export const AnnotationCard = ({item: {title, short_description, long_description, latitude, longitude, category}}) => { +export const AnnotationCard = ({item: {title, short_description, long_description, latitude, longitude, category, date}}) => { const [expanded, setExpanded] = useState(false); const [locationName, setLocationName] = useState(''); @@ -61,12 +61,6 @@ export const AnnotationCard = ({item: {title, short_description, long_descriptio - {/* - - - - - */} setExpanded(!expanded)} @@ -78,6 +72,8 @@ export const AnnotationCard = ({item: {title, short_description, long_descriptio + Added on: {date} +
{long_description}
diff --git a/resq/frontend/src/components/MapIcons.js b/resq/frontend/src/components/MapIcons.js index 8016c36b..8d13d09f 100644 --- a/resq/frontend/src/components/MapIcons.js +++ b/resq/frontend/src/components/MapIcons.js @@ -3,9 +3,9 @@ import * as React from "react"; export const AnnotationIcon = ({icon, color}) => ({ - fire: , - health: , - closed: + Fire: , + Health: , + "Road Closure": })[icon] export const MarkerIcon = ({color}) => ( { + const theme = useTheme(); + + const ITEM_HEIGHT = 48; + const ITEM_PADDING_TOP = 8; + const MenuProps = { + PaperProps: { + style: { + maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, + width: 250, + }, + }, + }; + + const [currentChoices, setCurrentChoices] = React.useState([]); + const label_id = useId(); + const select_id = useId(); + const input_id = useId(); + + + const handleChange = (event) => { + const { + target: {value}, + } = event; + setCurrentChoices( + typeof value === 'string' ? value.split(',') : value, + ); + }; + + useEffect(() => { + onChosenChanged && onChosenChanged(currentChoices) + }, [onChosenChanged, currentChoices]) + + return + {name} + + +} + +export const AmountSelector = ({name, onChosenChanged}) => { + const [open, setOpen] = React.useState(false); + + const [min, setMin] = useState(null) + const [max, setMax] = useState(null) + + const handleClose = () => { + setOpen(false); + setCurrentChoices([`${min}-${max}`]) + }; + + + const choices = ['All', '1-9', '10-99', '100-999', '1000+', "Custom"] + const theme = useTheme(); + + const ITEM_HEIGHT = 48; + const ITEM_PADDING_TOP = 8; + const MenuProps = { + PaperProps: { + style: { + maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, + width: 250, + }, + }, + }; + + const [currentChoices, setCurrentChoices] = React.useState(["All"]); + const label_id = useId(); + const select_id = useId(); + const input_id = useId(); + + + const handleChange = (event) => { + const { + target: {value}, + } = event; + if (value === "Custom") + setOpen(true); + else + setCurrentChoices( + typeof value === 'string' ? value.split(',') : value, + ); + }; + + useEffect(() => { + onChosenChanged && onChosenChanged(currentChoices) + }, [onChosenChanged, currentChoices]) + + return <> + + + + setMin(e.target.value)} + label="Min" + type="number" + variant="standard" + /> + setMax(e.target.value)} + label="Max" + type="number" + variant="standard" + /> + + + + + + + + {name} +
{value}
+ ))} + + )} + MenuProps={MenuProps} + > + {choices.map((name) => ( + + {name} + + ))} + + + +} \ No newline at end of file diff --git a/resq/frontend/src/pages/MapDemo.js b/resq/frontend/src/pages/MapDemo.js index 6680724e..76492fa2 100644 --- a/resq/frontend/src/pages/MapDemo.js +++ b/resq/frontend/src/pages/MapDemo.js @@ -1,3 +1,5 @@ +// noinspection JSUnusedLocalSymbols + import * as React from 'react'; import {useEffect, useState} from 'react'; import CssBaseline from '@mui/material/CssBaseline'; @@ -5,8 +7,10 @@ import Box from '@mui/material/Box'; import {createTheme, ThemeProvider} from '@mui/material/styles'; import Container from '@mui/material/Container'; import DisasterMap from "../components/DisasterMap"; -import {cards} from "./ListCards"; -import {MultiCheckbox} from "./MultiCheckbox"; +import {cards} from "../components/ListCards"; +import {AmountSelector, MultiCheckbox} from "../components/MultiCheckbox"; +import {DatePicker} from "@mui/x-date-pickers"; +import dayjs from "dayjs"; const customTheme = createTheme({ @@ -23,14 +27,15 @@ const mock_markers = [ type: "Annotation", latitude: 41.089, longitude: 29.053, - category: "health", + category: "Health", title: "First Aid Clinic", short_description: "First aid clinic and emergency wound care. Open 24 hours.", long_description: "Welcome to our First Aid Clinic, a dedicated facility committed to providing immediate and " + "compassionate healthcare 24 hours a day. Our experienced team of healthcare professionals specializes in " + "emergency wound care and first aid assistance, ensuring you receive prompt attention when you need it most. " + "From minor cuts to more serious injuries, our clinic is equipped to handle a range of medical concerns, " + - "promoting healing and preventing complications." + "promoting healing and preventing complications.", + date: "26/11/2023" }, { type: "Request", @@ -97,8 +102,20 @@ const mock_markers = [ quantity: 500, }, ], - status: "READY" }, + ...[...Array(20).keys()].map(i => + [...Array(20).keys()].map(j => ( + { + type: "Request", + latitude: 37 + 0.5 * i, + longitude: 31 + 0.5 * j, + requester: { + name: "Müslüm", + surname: "Ertürk" + }, + urgency: "HIGH", + needs: [] + }))).flat() ] function getAllCategories(item) { @@ -114,48 +131,91 @@ function getAllCategories(item) { } } -const makeFilterByCategory = categories => { - if (categories.length === 0) - return () => true - return item => { +const applyFilterTo = (predicate) => + item => { switch (item.type) { case "Annotation": - return categories.indexOf(item?.category) !== -1 + return predicate(item) case "Resource": - return !item.resources.every(resource => categories.indexOf(resource?.category) === -1) + return !item.resources.every(resource => !predicate(resource)) case "Request": - return !item.needs.every(need => categories.indexOf(need?.category) === -1) + return !item.needs.every(need => !predicate(need)) default: return false } } + +const makeFilterByCategory = categories => { + if (categories.length === 0) + return () => true + return applyFilterTo( + function (item) { + return categories.indexOf(item?.category) !== -1; + } + ) }; const makeFilterByType = (typeFilter) => item => typeFilter.length === 0 || typeFilter.indexOf(item.type) !== -1 +const makeFilterByAmount = ([amount]) => { + if (typeof amount !== "string" || amount.indexOf("-") === -1) + return () => true + const [min, max] = amount.split("-").map(i => parseInt(i)) + return applyFilterTo(function (item) { + return item.quantity && item.quantity >= min && max >= item.quantity; + }) +}; + +const makeFilterByDateFrom = (dateFrom) => item => dateFrom === null || !dateFrom.isValid() || !(dateFrom > dayjs(item.date)) +const makeFilterByDateTo = (dateTo) => item => dateTo === null || !dateTo.isValid() || !(dateTo < dayjs(item.date)) + +const makeFilterByBounds = ({ne: [ne_lat, ne_lng], sw: [sw_lat, sw_lng]}) => + function (item) { + return item.latitude <= ne_lat && + item.longitude <= ne_lng && + item.latitude >= sw_lat && + item.longitude >= sw_lng; + } + + export default function MapDemo() { + // eslint-disable-next-line no-unused-vars const [allMarkers, setAllMarkers] = useState(mock_markers) const [shownMarkers, setShownMarkers] = useState(allMarkers) const [selectedPoint, setSelectedPoint] = useState(null) + const [mapCenter, setMapCenter] = useState([39, 34.5]) const [typeFilter, setTypeFilter] = useState([]) - const [timeFilter, setTimeFilter] = useState([]) + const [dateFromFilter, setDateFromFilter] = useState(null) + const [dateToFilter, setDateToFilter] = useState(null) const [amountFilter, setAmountFilter] = useState([]) const [categoryFilter, setCategoryFilter] = useState([]) - const [mapBounds, setMapBounds] = useState([]) + const [mapBounds, setMapBounds] = useState({ne: [0, 0], sw: [0, 0]}) + + useEffect(() => { + if (selectedPoint) + setMapCenter([selectedPoint.latitude, selectedPoint.longitude]) + }, [selectedPoint]) useEffect(() => setShownMarkers( allMarkers .filter(makeFilterByCategory(categoryFilter)) .filter(makeFilterByType(typeFilter)) - ), [allMarkers, categoryFilter, typeFilter]) + .filter(makeFilterByAmount(amountFilter)) + .filter(makeFilterByDateFrom(dateFromFilter)) + .filter(makeFilterByDateTo(dateToFilter)) + .filter(makeFilterByBounds(mapBounds)) + ), [allMarkers, amountFilter, categoryFilter, dateFromFilter, dateToFilter, mapBounds, typeFilter]) // noinspection JSValidateTypes return ( - + v && array.indexOf(v) === i)} onChosenChanged={setCategoryFilter}/> - - + + setDateFromFilter(e)} + /> + setDateToFilter(e)} + /> - {shownMarkers.map((marker) => { - const SelectedCard = cards[marker.type] - return < SelectedCard item={marker} onClick={() => setSelectedPoint(marker)}/> - })} + + {shownMarkers.map((marker) => { + const SelectedCard = cards[marker.type] + return < SelectedCard item={marker} onClick={() => setSelectedPoint(marker)}/> + })} + + mapCenter={mapCenter} + setMapCenter={setMapCenter} + onPointSelected={setSelectedPoint} + onBoundsChanged={setMapBounds} + /> diff --git a/resq/frontend/src/pages/MultiCheckbox.js b/resq/frontend/src/pages/MultiCheckbox.js deleted file mode 100644 index eab5b426..00000000 --- a/resq/frontend/src/pages/MultiCheckbox.js +++ /dev/null @@ -1,78 +0,0 @@ -import {Chip, FormControl, InputLabel, MenuItem, OutlinedInput, Select, useTheme} from "@mui/material"; -import * as React from "react"; -import {useEffect, useId} from "react"; -import Box from "@mui/material/Box"; - -function getDropDownStyles(name, personName, theme) { - return { - fontWeight: - personName.indexOf(name) === -1 - ? theme.typography.fontWeightRegular - : theme.typography.fontWeightMedium, - }; -} - -export const MultiCheckbox = ({name, choices, onChosenChanged}) => { - const theme = useTheme(); - - const ITEM_HEIGHT = 48; - const ITEM_PADDING_TOP = 8; - const MenuProps = { - PaperProps: { - style: { - maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, - width: 250, - }, - }, - }; - - const [currentChoices, setCurrentChoices] = React.useState([]); - const label_id = useId(); - const select_id = useId(); - const input_id = useId(); - - - const handleChange = (event) => { - const { - target: {value}, - } = event; - setCurrentChoices( - typeof value === 'string' ? value.split(',') : value, - ); - }; - - useEffect(() => { - onChosenChanged && onChosenChanged(currentChoices) - }, [onChosenChanged, currentChoices]) - - return - {name} - - -} \ No newline at end of file