[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": "^5.0.0",
|
||||||
"react-leaflet-markercluster": "^5.0.0-rc.0",
|
"react-leaflet-markercluster": "^5.0.0-rc.0",
|
||||||
"react-router-dom": "^7.3.0",
|
"react-router-dom": "^7.3.0",
|
||||||
|
"react-toastify": "^11.0.5",
|
||||||
"recharts": "^2.15.1",
|
"recharts": "^2.15.1",
|
||||||
"tailwindcss": "^4.0.9"
|
"tailwindcss": "^4.0.9"
|
||||||
},
|
},
|
||||||
@ -4459,6 +4460,19 @@
|
|||||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"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": {
|
"node_modules/react-transition-group": {
|
||||||
"version": "4.4.5",
|
"version": "4.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
"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": "^5.0.0",
|
||||||
"react-leaflet-markercluster": "^5.0.0-rc.0",
|
"react-leaflet-markercluster": "^5.0.0-rc.0",
|
||||||
"react-router-dom": "^7.3.0",
|
"react-router-dom": "^7.3.0",
|
||||||
|
"react-toastify": "^11.0.5",
|
||||||
"recharts": "^2.15.1",
|
"recharts": "^2.15.1",
|
||||||
"tailwindcss": "^4.0.9"
|
"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 { BrowserRouter as Router, Route, Routes } from "react-router-dom";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
|
|
||||||
|
import { ToastContainer } from 'react-toastify';
|
||||||
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
|
|
||||||
import Navbar from "./components/Navbar";
|
import Navbar from "./components/Navbar";
|
||||||
import Statistics from "./components/Statistics.jsx";
|
import Statistics from "./components/Statistics.jsx";
|
||||||
import Help from "./components/Help.jsx";
|
import Help from "./components/Help.jsx";
|
||||||
@ -441,62 +444,65 @@ function App() {
|
|||||||
}, [memoizedFilteredMarkers]);
|
}, [memoizedFilteredMarkers]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Router>
|
<>
|
||||||
<Navbar />
|
<Router>
|
||||||
<Routes>
|
<Navbar />
|
||||||
<Route
|
<Routes>
|
||||||
path="/"
|
<Route
|
||||||
element={
|
path="/"
|
||||||
<div style={{ height: "100vh", width: "100vw", display: "flex", position: "relative", paddingTop: "5vh" }}>
|
element={
|
||||||
{loading && <LoadingOverlay message={"Loading data..."} />}
|
<div style={{ height: "100vh", width: "100vw", display: "flex", position: "relative", paddingTop: "5vh" }}>
|
||||||
<div
|
{loading && <LoadingOverlay message={"Loading data..."} />}
|
||||||
style={{
|
<div
|
||||||
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..."
|
|
||||||
style={{
|
style={{
|
||||||
width: "250px", fontSize: "16px",
|
position: "absolute",
|
||||||
top: "6vh", marginTop: "5vh",
|
top: "1vh",
|
||||||
padding: "10px", background: "rgba(255, 255, 255, 0.9)", color: "black",
|
height: "5vh",
|
||||||
borderRadius: "10px", overflow: "hidden"
|
width: "250px", minWidth: "50px",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
zIndex: 1000,
|
||||||
|
...(window.innerWidth < 800 ? { top: "auto", bottom: "10vh" } : {})
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
</div>
|
<input
|
||||||
<Sidebar
|
type="text"
|
||||||
selectedSources={selectedSources}
|
onChange={(e) => handleSearchChange(e)}
|
||||||
setSelectedSources={setSelectedSources}
|
placeholder="Search..."
|
||||||
clusteringEnabled={clusteringEnabled}
|
style={{
|
||||||
setClusteringEnabled={setClusteringEnabled}
|
width: "250px", fontSize: "16px",
|
||||||
fetchData={fetchData}
|
top: "6vh", marginTop: "5vh",
|
||||||
userLocationAvailable={userLocationAvailable}
|
padding: "10px", background: "rgba(255, 255, 255, 0.9)", color: "black",
|
||||||
showFavouritesOnly={showFaovouritesOnly}
|
borderRadius: "10px", overflow: "hidden"
|
||||||
setShowFavouritesOnly={setShowFavouritesOnly}
|
}}
|
||||||
/>
|
|
||||||
<div style={{ flex: 1 }}>
|
|
||||||
<MapComponent
|
|
||||||
markers={filteredMarkers}
|
|
||||||
clusteringEnabled={clusteringEnabled}
|
|
||||||
userLocationAvailable={userLocationAvailable}
|
|
||||||
/>
|
/>
|
||||||
|
</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>
|
||||||
</div>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
<Route path="/statistics" element={<Statistics />} />
|
||||||
<Route path="/statistics" element={<Statistics />} />
|
<Route path="/help" element={<Help />} />
|
||||||
<Route path="/help" element={<Help />} />
|
</Routes>
|
||||||
</Routes>
|
</Router>
|
||||||
</Router>
|
<ToastContainer position="bottom-right"/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export default App;
|
export default App;
|
@ -1,6 +1,7 @@
|
|||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
const menuData = [
|
const menuData = [
|
||||||
{
|
{
|
||||||
@ -19,7 +20,7 @@ const menuData = [
|
|||||||
{ id: "terminated", name: "Terminated", endsec: true },
|
{ id: "terminated", name: "Terminated", endsec: true },
|
||||||
{ id: "early", name: "Early" },
|
{ id: "early", name: "Early" },
|
||||||
{ id: "on-time", name: "On-time" },
|
{ id: "on-time", name: "On-time" },
|
||||||
{ id: "late", name: "Late" },
|
{ id: "late", name: "Late", endsec: true },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ id: "irish-rail-stations", name: "Irish Rail Stations" },
|
{ id: "irish-rail-stations", name: "Irish Rail Stations" },
|
||||||
@ -42,11 +43,22 @@ const menuData = [
|
|||||||
{ id: "enabled", name: "Enabled" },
|
{ id: "enabled", name: "Enabled" },
|
||||||
{ id: "disabled", name: "Disabled", endsec: true },
|
{ id: "disabled", name: "Disabled", endsec: true },
|
||||||
{ id: "park-and-ride", name: "Must be Park & Ride" },
|
{ 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 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) => {
|
const getAllDefaultCheckedIds = (data) => {
|
||||||
@ -66,14 +78,26 @@ const getAllDefaultCheckedIds = (data) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const CheckboxItem = ({ item, selectedSources, setSelectedSources, enabledSources, setEnabledSources, level = 0, parentChecked = true }) => {
|
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 isChecked = selectedSources.includes(item.id);
|
||||||
const isDisabled = !parentChecked; // Disable if any parent is not checked
|
const isDisabled = !parentChecked; // Disable if any parent is not checked
|
||||||
const isEnabled = isChecked && parentChecked; // Only enabled if checked and parent is checked
|
const isEnabled = isChecked && parentChecked; // Only enabled if checked and parent is checked
|
||||||
|
|
||||||
const handleCheckboxChange = () => {
|
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) =>
|
setSelectedSources((prev) =>
|
||||||
isChecked
|
isChecked
|
||||||
? prev.filter((id) => id !== item.id)
|
? prev.filter((id) => id !== item.id)
|
||||||
|
Reference in New Issue
Block a user