Skip to content

Commit 4d60372

Browse files
authored
UI: map filters, map change actions (#1164)
* Map filter checkboxes, map change actions
1 parent 41363dd commit 4d60372

File tree

4 files changed

+159
-77
lines changed

4 files changed

+159
-77
lines changed

rcongui/src/pages/settings/maps/MapFilter.jsx

Lines changed: 91 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
11
import {
2-
Box,
3-
FormControl,
4-
InputLabel,
5-
Select,
6-
MenuItem,
72
Stack,
83
Checkbox,
9-
ListItemText,
10-
OutlinedInput,
4+
FormControlLabel,
5+
Typography,
116
} from "@mui/material";
127
import { DebouncedSearchInput } from "@/components/shared/DebouncedSearchInput";
138
import { useState, useEffect, useMemo } from "react";
@@ -44,12 +39,14 @@ const MenuProps = {
4439
*/
4540
export const MapFilter = ({ maps, onFilterChange }) => {
4641
const [searchTerm, setSearchTerm] = useState("");
47-
const [selectedModes, setSelectedModes] = useState([]);
42+
const [selectedModes, setSelectedModes] = useState(["warfare"]);
4843
const [selectedWeathers, setSelectedWeathers] = useState([]);
4944

5045
// Compute unique modes and weathers for dropdowns
5146
const allModes = useMemo(() => {
52-
return Array.from(new Set(maps.map((map) => unifiedGamemodeName(map.game_mode)))).sort();
47+
return Array.from(
48+
new Set(maps.map((map) => unifiedGamemodeName(map.game_mode)))
49+
).sort();
5350
}, [maps]);
5451

5552
const allWeather = useMemo(() => {
@@ -59,9 +56,15 @@ export const MapFilter = ({ maps, onFilterChange }) => {
5956
// Compute filtered maps based on filter criteria
6057
const filteredMaps = useMemo(() => {
6158
return maps.filter((mapLayer) => {
62-
const matchesSearch = mapLayer.map.pretty_name.toLowerCase().includes(searchTerm.toLowerCase());
63-
const matchesModes = !selectedModes.length || selectedModes.includes(unifiedGamemodeName(mapLayer.game_mode));
64-
const matchesWeather = !selectedWeathers.length || selectedWeathers.includes(mapLayer.environment);
59+
const matchesSearch = mapLayer.map.pretty_name
60+
.toLowerCase()
61+
.includes(searchTerm.toLowerCase());
62+
const matchesModes =
63+
!selectedModes.length ||
64+
selectedModes.includes(unifiedGamemodeName(mapLayer.game_mode));
65+
const matchesWeather =
66+
!selectedWeathers.length ||
67+
selectedWeathers.includes(mapLayer.environment);
6568
return matchesSearch && matchesModes && matchesWeather;
6669
});
6770
}, [maps, searchTerm, selectedModes, selectedWeathers]);
@@ -71,62 +74,102 @@ export const MapFilter = ({ maps, onFilterChange }) => {
7174
onFilterChange(filteredMaps);
7275
}, [filteredMaps, onFilterChange]);
7376

77+
const handleModeChange = (mode) => {
78+
setSelectedModes((prev) =>
79+
prev.includes(mode) ? prev.filter((m) => m !== mode) : [...prev, mode]
80+
);
81+
};
82+
83+
const handleWeatherChange = (weather) => {
84+
setSelectedWeathers((prev) =>
85+
prev.includes(weather)
86+
? prev.filter((w) => w !== weather)
87+
: [...prev, weather]
88+
);
89+
};
90+
7491
return (
7592
<Stack
7693
alignItems={"center"}
7794
flexWrap={"wrap"}
7895
gap={1}
7996
direction={"row"}
80-
sx={{ py: 1, gap: 1 }}
97+
sx={{
98+
py: 1,
99+
gap: 1,
100+
// Card-like styles
101+
borderRadius: 1,
102+
boxShadow: 1,
103+
border: '1px solid',
104+
borderColor: 'divider',
105+
p: 2,
106+
mb: 2
107+
}}
81108
>
82109
<DebouncedSearchInput
83110
size="small"
84111
placeholder="Search maps..."
85112
onChange={setSearchTerm}
86113
/>
87114

88-
<Stack direction={"row"} sx={{ gap: 1, mb: 2, width: "100%" }}>
89-
<FormControl size="small" sx={{ width: { xs: "100%", md: "50%" } }}>
90-
<InputLabel id={"game-mode"}>Game Mode</InputLabel>
91-
<Select
92-
labelId={"game-mode"}
93-
value={selectedModes}
94-
onChange={(e) => setSelectedModes(e.target.value)}
95-
multiple
96-
renderValue={(selected) => selected.join(", ")}
97-
input={<OutlinedInput label="Game Mode" />}
98-
MenuProps={MenuProps}
99-
SelectDisplayProps={{ style: { display: "block", textTransform: "uppercase" } }}
115+
<Stack direction={"column"} sx={{ width: "100%", px: 1 }}>
116+
<Stack direction={"row"} gap={1} alignItems={"center"}>
117+
<Typography
118+
variant="subtitle2"
119+
fontSize={10}
120+
sx={{ textTransform: "uppercase", width: 70 }}
100121
>
122+
Game Mode
123+
</Typography>
124+
<Stack direction={"row"} flexWrap={"wrap"}>
101125
{allModes.map((mode) => (
102-
<MenuItem key={mode} value={mode}>
103-
<Checkbox checked={selectedModes.includes(mode)} />
104-
<ListItemText primary={unifiedGamemodeName(mode)} />
105-
</MenuItem>
126+
<FormControlLabel
127+
key={mode}
128+
control={
129+
<Checkbox
130+
checked={selectedModes.includes(mode)}
131+
onChange={() => handleModeChange(mode)}
132+
size="small"
133+
sx={{
134+
"& .MuiSvgIcon-root": { fontSize: 14 },
135+
}}
136+
/>
137+
}
138+
label={unifiedGamemodeName(mode)}
139+
sx={{ textTransform: "uppercase", '& .MuiFormControlLabel-label': { fontSize: 10 } }}
140+
/>
106141
))}
107-
</Select>
108-
</FormControl>
142+
</Stack>
143+
</Stack>
109144

110-
<FormControl size="small" sx={{ width: { xs: "100%", md: "50%" } }}>
111-
<InputLabel id={"game-weather"}>Weather</InputLabel>
112-
<Select
113-
labelId={"game-weather"}
114-
value={selectedWeathers}
115-
onChange={(e) => setSelectedWeathers(e.target.value)}
116-
multiple
117-
renderValue={(selected) => selected.join(", ")}
118-
input={<OutlinedInput label="Weather" />}
119-
MenuProps={MenuProps}
120-
SelectDisplayProps={{ style: { display: "block", textTransform: "uppercase" } }}
145+
<Stack direction={"row"} gap={1} alignItems={"center"}>
146+
<Typography
147+
variant="subtitle2"
148+
fontSize={10}
149+
sx={{ textTransform: "uppercase", width: 70 }}
121150
>
151+
Weather
152+
</Typography>
153+
<Stack direction={"row"} flexWrap={"wrap"}>
122154
{allWeather.map((weather) => (
123-
<MenuItem key={weather} value={weather}>
124-
<Checkbox checked={selectedWeathers.includes(weather)} />
125-
<ListItemText primary={weather} />
126-
</MenuItem>
155+
<FormControlLabel
156+
key={weather}
157+
control={
158+
<Checkbox
159+
checked={selectedWeathers.includes(weather)}
160+
onChange={() => handleWeatherChange(weather)}
161+
size="small"
162+
sx={{
163+
"& .MuiSvgIcon-root": { fontSize: 14 },
164+
}}
165+
/>
166+
}
167+
label={weather}
168+
sx={{ textTransform: "uppercase", '& .MuiFormControlLabel-label': { fontSize: 10 } }}
169+
/>
127170
))}
128-
</Select>
129-
</FormControl>
171+
</Stack>
172+
</Stack>
130173
</Stack>
131174
</Stack>
132175
);

rcongui/src/pages/settings/maps/MapListBuilder.jsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,17 @@ export function MapListBuilder({
4040
exclusive = false,
4141
actions,
4242
}) {
43-
const [mapSelection, setMapSelection] = useState(selectedMaps.map(withSelectionId));
44-
43+
const [mapSelection, setMapSelection] = useState(
44+
selectedMaps.map(withSelectionId)
45+
);
46+
4547
const mapOptions = useMemo(() => {
4648
if (!exclusive) return allMaps;
4749
const selectedIds = new Set(mapSelection.map((m) => m.id));
4850
const exclusiveOptions = allMaps.filter((m) => !selectedIds.has(m.id));
4951
return exclusiveOptions;
5052
}, [exclusive, allMaps, mapSelection]);
51-
53+
5254
const [filteredMapOptions, setFilteredMapOptions] = useState(mapOptions);
5355

5456
const theme = useTheme();
@@ -138,11 +140,7 @@ export function MapListBuilder({
138140
{isMapListVisible && (
139141
<Grid size={{ xs: 12, md: 6 }}>
140142
<Box sx={{ mb: 3 }}>
141-
<Box
142-
sx={{
143-
pb: 2,
144-
}}
145-
>
143+
<Box>
146144
<MapFilter
147145
maps={mapOptions}
148146
onFilterChange={handleFilterChange}

rcongui/src/pages/settings/maps/MapListItem.jsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,12 @@ export function MapListItem({ mapLayer }) {
4141
return <MapListItemBase mapLayer={mapLayer} />;
4242
}
4343

44-
export function MapChangeListItem({ mapLayer, onClick }) {
44+
export function MapChangeListItem({ mapLayer, onClick, icon, title }) {
4545
return (
4646
<MapListItemBase
4747
mapLayer={mapLayer}
4848
renderActions={(mapLayer) => (
49-
<Tooltip title="Set as current map">
49+
<Tooltip title={title ?? "Set as current map"}>
5050
<span>
5151
<IconButton
5252
sx={{
@@ -60,7 +60,7 @@ export function MapChangeListItem({ mapLayer, onClick }) {
6060
size="small"
6161
onClick={() => onClick(mapLayer)}
6262
>
63-
<PlayArrowIcon />
63+
{icon ?? <PlayArrowIcon />}
6464
</IconButton>
6565
</span>
6666
</Tooltip>

rcongui/src/pages/settings/maps/list/index.jsx

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import {
88
DialogActions,
99
Button,
1010
Box,
11+
Stack,
12+
styled,
13+
Typography,
1114
} from "@mui/material";
1215
import { MapList } from "../MapList";
1316
import { MapChangeListItem } from "../MapListItem";
@@ -18,13 +21,35 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
1821
import { mapsManagerMutationOptions, mapsManagerQueryKeys } from "../queries";
1922
import { toast } from "react-toastify";
2023
import { MapFilter } from "../MapFilter";
24+
import RestartAltIcon from '@mui/icons-material/RestartAlt';
25+
26+
const MapListContainer = styled(Box)(({ theme }) => ({
27+
width: "100%",
28+
[theme.breakpoints.up("md")]: {
29+
width: "50%",
30+
},
31+
}));
32+
33+
const ActionsContainer = styled(Box)(({ theme }) => ({
34+
width: "100%",
35+
paddingTop: theme.spacing(1),
36+
paddingBottom: theme.spacing(1),
37+
borderBottom: `1px solid ${theme.palette.divider}`,
38+
[theme.breakpoints.up("md")]: {
39+
padding: theme.spacing(1),
40+
width: "50%",
41+
borderLeft: `1px solid ${theme.palette.divider}`,
42+
borderBottom: "none",
43+
},
44+
}));
2145

2246
const MapListPage = () => {
2347
const { maps } = useRouteLoaderData("maps");
2448
const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
2549
const [mapToConfirm, setMapToConfirm] = useState(null);
2650
const [filteredMapOptions, setFilteredMapOptions] = useState(maps);
2751
const serverState = useGlobalStore((state) => state.serverState);
52+
const gameState = useGlobalStore((state) => state.gameState);
2853
const queryClient = useQueryClient();
2954

3055
const { mutate: changeMap } = useMutation({
@@ -70,25 +95,41 @@ const MapListPage = () => {
7095

7196
return (
7297
<>
73-
<Box
74-
sx={{
75-
height: "fit-content",
76-
py: 2,
77-
maxWidth: theme => theme.breakpoints.values.md,
78-
}}
79-
>
80-
<MapFilter maps={maps} onFilterChange={handleFilterChange} />
81-
<MapList
82-
maps={filteredMapOptions}
83-
renderItem={(mapLayer) => (
84-
<MapChangeListItem
85-
mapLayer={mapLayer}
86-
key={mapLayer.id}
87-
onClick={handleChangeMapClick}
88-
/>
98+
<Stack direction={{ xs: "column-reverse", md: "row" }} spacing={1}>
99+
<MapListContainer>
100+
<MapFilter maps={maps} onFilterChange={handleFilterChange} />
101+
<MapList
102+
maps={filteredMapOptions}
103+
renderItem={(mapLayer) => (
104+
<MapChangeListItem
105+
mapLayer={mapLayer}
106+
key={mapLayer.id}
107+
onClick={handleChangeMapClick}
108+
/>
109+
)}
110+
/>
111+
</MapListContainer>
112+
<ActionsContainer>
113+
{gameState && (
114+
<>
115+
<Typography variant="subtitle2" sx={{ mb: 1 }}>Current map</Typography>
116+
<MapChangeListItem
117+
mapLayer={gameState.current_map}
118+
key={gameState.current_map.id}
119+
onClick={handleChangeMapClick}
120+
icon={<RestartAltIcon />}
121+
title={"Restart the current map"}
122+
/>
123+
<Typography variant="subtitle2" sx={{ mb: 1 }}>Next map</Typography>
124+
<MapChangeListItem
125+
mapLayer={gameState.next_map}
126+
key={gameState.next_map.id}
127+
onClick={handleChangeMapClick}
128+
/>
129+
</>
89130
)}
90-
/>
91-
</Box>
131+
</ActionsContainer>
132+
</Stack>
92133
{/* Confirmation Dialog */}
93134
<Dialog
94135
open={confirmDialogOpen}

0 commit comments

Comments
 (0)