[frontend]: Prevent user from deselecting all filters in a group
This commit is contained in:
14
frontend/package-lock.json
generated
14
frontend/package-lock.json
generated
@ -23,6 +23,7 @@
|
||||
"react-leaflet": "^5.0.0",
|
||||
"react-leaflet-markercluster": "^5.0.0-rc.0",
|
||||
"react-router-dom": "^7.3.0",
|
||||
"react-toastify": "^11.0.5",
|
||||
"recharts": "^2.15.1",
|
||||
"tailwindcss": "^4.0.9"
|
||||
},
|
||||
@ -4459,6 +4460,19 @@
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-toastify": {
|
||||
"version": "11.0.5",
|
||||
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz",
|
||||
"integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19",
|
||||
"react-dom": "^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/react-transition-group": {
|
||||
"version": "4.4.5",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||
|
@ -25,6 +25,7 @@
|
||||
"react-leaflet": "^5.0.0",
|
||||
"react-leaflet-markercluster": "^5.0.0-rc.0",
|
||||
"react-router-dom": "^7.3.0",
|
||||
"react-toastify": "^11.0.5",
|
||||
"recharts": "^2.15.1",
|
||||
"tailwindcss": "^4.0.9"
|
||||
},
|
||||
|
@ -2,6 +2,9 @@ import React, { useState, useEffect, useMemo, useRef } from "react";
|
||||
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
|
||||
import Cookies from "js-cookie";
|
||||
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
|
||||
import Navbar from "./components/Navbar";
|
||||
import Statistics from "./components/Statistics.jsx";
|
||||
import Help from "./components/Help.jsx";
|
||||
@ -441,62 +444,65 @@ function App() {
|
||||
}, [memoizedFilteredMarkers]);
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<Navbar />
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<div style={{ height: "100vh", width: "100vw", display: "flex", position: "relative", paddingTop: "5vh" }}>
|
||||
{loading && <LoadingOverlay message={"Loading data..."} />}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "1vh",
|
||||
height: "5vh",
|
||||
width: "250px", minWidth: "50px",
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
zIndex: 1000,
|
||||
...(window.innerWidth < 800 ? { top: "auto", bottom: "10vh" } : {})
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
onChange={(e) => handleSearchChange(e)}
|
||||
placeholder="Search..."
|
||||
<>
|
||||
<Router>
|
||||
<Navbar />
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<div style={{ height: "100vh", width: "100vw", display: "flex", position: "relative", paddingTop: "5vh" }}>
|
||||
{loading && <LoadingOverlay message={"Loading data..."} />}
|
||||
<div
|
||||
style={{
|
||||
width: "250px", fontSize: "16px",
|
||||
top: "6vh", marginTop: "5vh",
|
||||
padding: "10px", background: "rgba(255, 255, 255, 0.9)", color: "black",
|
||||
borderRadius: "10px", overflow: "hidden"
|
||||
position: "absolute",
|
||||
top: "1vh",
|
||||
height: "5vh",
|
||||
width: "250px", minWidth: "50px",
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
zIndex: 1000,
|
||||
...(window.innerWidth < 800 ? { top: "auto", bottom: "10vh" } : {})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Sidebar
|
||||
selectedSources={selectedSources}
|
||||
setSelectedSources={setSelectedSources}
|
||||
clusteringEnabled={clusteringEnabled}
|
||||
setClusteringEnabled={setClusteringEnabled}
|
||||
fetchData={fetchData}
|
||||
userLocationAvailable={userLocationAvailable}
|
||||
showFavouritesOnly={showFaovouritesOnly}
|
||||
setShowFavouritesOnly={setShowFavouritesOnly}
|
||||
/>
|
||||
<div style={{ flex: 1 }}>
|
||||
<MapComponent
|
||||
markers={filteredMarkers}
|
||||
clusteringEnabled={clusteringEnabled}
|
||||
userLocationAvailable={userLocationAvailable}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
onChange={(e) => handleSearchChange(e)}
|
||||
placeholder="Search..."
|
||||
style={{
|
||||
width: "250px", fontSize: "16px",
|
||||
top: "6vh", marginTop: "5vh",
|
||||
padding: "10px", background: "rgba(255, 255, 255, 0.9)", color: "black",
|
||||
borderRadius: "10px", overflow: "hidden"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Sidebar
|
||||
selectedSources={selectedSources}
|
||||
setSelectedSources={setSelectedSources}
|
||||
clusteringEnabled={clusteringEnabled}
|
||||
setClusteringEnabled={setClusteringEnabled}
|
||||
fetchData={fetchData}
|
||||
userLocationAvailable={userLocationAvailable}
|
||||
showFavouritesOnly={showFaovouritesOnly}
|
||||
setShowFavouritesOnly={setShowFavouritesOnly}
|
||||
/>
|
||||
<div style={{ flex: 1 }}>
|
||||
<MapComponent
|
||||
markers={filteredMarkers}
|
||||
clusteringEnabled={clusteringEnabled}
|
||||
userLocationAvailable={userLocationAvailable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<Route path="/statistics" element={<Statistics />} />
|
||||
<Route path="/help" element={<Help />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
}
|
||||
/>
|
||||
<Route path="/statistics" element={<Statistics />} />
|
||||
<Route path="/help" element={<Help />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
<ToastContainer position="bottom-right"/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default App;
|
@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import PropTypes from 'prop-types';
|
||||
import Cookies from "js-cookie";
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
const menuData = [
|
||||
{
|
||||
@ -19,7 +20,7 @@ const menuData = [
|
||||
{ id: "terminated", name: "Terminated", endsec: true },
|
||||
{ id: "early", name: "Early" },
|
||||
{ id: "on-time", name: "On-time" },
|
||||
{ id: "late", name: "Late" },
|
||||
{ id: "late", name: "Late", endsec: true },
|
||||
],
|
||||
},
|
||||
{ id: "irish-rail-stations", name: "Irish Rail Stations" },
|
||||
@ -42,11 +43,22 @@ const menuData = [
|
||||
{ id: "enabled", name: "Enabled" },
|
||||
{ id: "disabled", name: "Disabled", endsec: true },
|
||||
{ id: "park-and-ride", name: "Must be Park & Ride" },
|
||||
{ id: "cycle-and-ride", name: "Must be Cycle & Ride" },
|
||||
{ id: "cycle-and-ride", name: "Must be Cycle & Ride", endsec: true },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const sectionGroups = [
|
||||
["irish-rail", "bus", "luas-stops"],
|
||||
["irish-rail-trains", "irish-rail-stations"],
|
||||
["mainline", "suburban", "dart"],
|
||||
["running", "not-yet-running", "terminated"],
|
||||
["early", "on-time", "late"],
|
||||
["buses", "bus-stops"],
|
||||
["red-line", "green-line"],
|
||||
["enabled", "disabled"],
|
||||
];
|
||||
|
||||
const customDefaultChecked = ["mainline","suburban","dart","running","not-yet-running","terminated","early","on-time","late","disabled","buses","irish-rail-trains","luas-stops","enabled","green-line","red-line","irish-rail","bus"]
|
||||
|
||||
const getAllDefaultCheckedIds = (data) => {
|
||||
@ -66,14 +78,26 @@ const getAllDefaultCheckedIds = (data) => {
|
||||
};
|
||||
|
||||
const CheckboxItem = ({ item, selectedSources, setSelectedSources, enabledSources, setEnabledSources, level = 0, parentChecked = true }) => {
|
||||
console.log("item id: " + item.id);
|
||||
console.log(selectedSources.includes(item.id));
|
||||
|
||||
const isChecked = selectedSources.includes(item.id);
|
||||
const isDisabled = !parentChecked; // Disable if any parent is not checked
|
||||
const isEnabled = isChecked && parentChecked; // Only enabled if checked and parent is checked
|
||||
|
||||
const handleCheckboxChange = () => {
|
||||
if (isChecked) {
|
||||
if (item.id != "park-and-ride" && item.id != "cycle-and-ride") {
|
||||
// Find which section this item is in
|
||||
const section = sectionGroups.find(group => group.includes(item.id));
|
||||
|
||||
if (section.length > 1) {
|
||||
const selectedInSection = section.filter(id => selectedSources.includes(id));
|
||||
if (selectedInSection.length === 1 && selectedInSection[0] === item.id) {
|
||||
toast.warn("At least one item in this section must be selected");
|
||||
return; // Don't allow unchecking the last one
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedSources((prev) =>
|
||||
isChecked
|
||||
? prev.filter((id) => id !== item.id)
|
||||
|
Reference in New Issue
Block a user