feat: hide search input when list is empty and add animated city selector highlight

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-19 14:14:04 +03:00
parent fbf6b0dc9d
commit 7e539f550b
12 changed files with 111 additions and 50 deletions

View File

@@ -108,7 +108,9 @@ export const ArticleListPage = observer(() => {
</div> </div>
)} )}
{rows.length > 0 && (
<SearchInput value={searchQuery} onChange={setSearchQuery} /> <SearchInput value={searchQuery} onChange={setSearchQuery} />
)}
<div className="w-full"> <div className="w-full">
<DataGrid <DataGrid

View File

@@ -162,7 +162,9 @@ export const CarrierListPage = observer(() => {
</div> </div>
)} )}
{rows.length > 0 && (
<SearchInput value={searchQuery} onChange={setSearchQuery} /> <SearchInput value={searchQuery} onChange={setSearchQuery} />
)}
<DataGrid <DataGrid
rows={rows} rows={rows}

View File

@@ -165,7 +165,9 @@ export const CityListPage = observer(() => {
</div> </div>
)} )}
{rows.length > 0 && (
<SearchInput value={searchQuery} onChange={setSearchQuery} /> <SearchInput value={searchQuery} onChange={setSearchQuery} />
)}
<DataGrid <DataGrid
rows={filteredRows} rows={filteredRows}

View File

@@ -117,7 +117,9 @@ export const CountryListPage = observer(() => {
</div> </div>
)} )}
{rows.length > 0 && (
<SearchInput value={searchQuery} onChange={setSearchQuery} /> <SearchInput value={searchQuery} onChange={setSearchQuery} />
)}
<DataGrid <DataGrid
rows={rows} rows={rows}

View File

@@ -113,7 +113,9 @@ export const MediaListPage = observer(() => {
return ( return (
<> <>
<div className="w-full"> <div className="w-full">
{rows.length > 0 && (
<SearchInput value={searchQuery} onChange={setSearchQuery} /> <SearchInput value={searchQuery} onChange={setSearchQuery} />
)}
{canWriteMedia && ids.length > 0 && ( {canWriteMedia && ids.length > 0 && (
<div className="flex justify-end mb-5 duration-300"> <div className="flex justify-end mb-5 duration-300">

View File

@@ -271,7 +271,9 @@ export const RouteListPage = observer(() => {
</div> </div>
)} )}
{rows.length > 0 && (
<SearchInput value={searchQuery} onChange={setSearchQuery} /> <SearchInput value={searchQuery} onChange={setSearchQuery} />
)}
<DataGrid <DataGrid
rows={rows} rows={rows}

View File

@@ -182,7 +182,9 @@ export const SightListPage = observer(() => {
</div> </div>
)} )}
{rows.length > 0 && (
<SearchInput value={searchQuery} onChange={setSearchQuery} /> <SearchInput value={searchQuery} onChange={setSearchQuery} />
)}
<DataGrid <DataGrid
rows={rows} rows={rows}

View File

@@ -299,7 +299,9 @@ export const SnapshotListPage = observer(() => {
</Alert> </Alert>
)} )}
{rows.length > 0 && (
<SearchInput value={searchQuery} onChange={setSearchQuery} /> <SearchInput value={searchQuery} onChange={setSearchQuery} />
)}
<DataGrid <DataGrid
rows={rows} rows={rows}

View File

@@ -226,7 +226,9 @@ export const StationListPage = observer(() => {
</div> </div>
)} )}
{rows.length > 0 && (
<SearchInput value={searchQuery} onChange={setSearchQuery} /> <SearchInput value={searchQuery} onChange={setSearchQuery} />
)}
<DataGrid <DataGrid
rows={rows} rows={rows}

View File

@@ -147,7 +147,9 @@ export const UserListPage = observer(() => {
</div> </div>
)} )}
{rows.length > 0 && (
<SearchInput value={searchQuery} onChange={setSearchQuery} /> <SearchInput value={searchQuery} onChange={setSearchQuery} />
)}
<DataGrid <DataGrid
rows={rows} rows={rows}

View File

@@ -174,7 +174,9 @@ export const VehicleListPage = observer(() => {
/> />
</div> </div>
{rows.length > 0 && (
<SearchInput value={searchQuery} onChange={setSearchQuery} /> <SearchInput value={searchQuery} onChange={setSearchQuery} />
)}
{canWriteVehicles && ids.length > 0 && ( {canWriteVehicles && ids.length > 0 && (
<div className="flex justify-end mb-5 duration-300"> <div className="flex justify-end mb-5 duration-300">

View File

@@ -6,11 +6,21 @@ import {
SelectChangeEvent, SelectChangeEvent,
Typography, Typography,
Box, Box,
keyframes,
} from "@mui/material"; } from "@mui/material";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { authStore, cityStore, selectedCityStore, snapshotStore, type City } from "@shared"; import { authStore, cityStore, selectedCityStore, snapshotStore, type City } from "@shared";
import { MapPin } from "lucide-react"; import { MapPin } from "lucide-react";
const borderSpin = keyframes`
0% {
background-position: 0% 50%;
}
100% {
background-position: 200% 50%;
}
`;
export const CitySelector: React.FC = observer(() => { export const CitySelector: React.FC = observer(() => {
const { selectedCity, setSelectedCity, isLocked } = selectedCityStore; const { selectedCity, setSelectedCity, isLocked } = selectedCityStore;
const canLoadAllCities = authStore.isAdmin && authStore.canRead("cities"); const canLoadAllCities = authStore.isAdmin && authStore.canRead("cities");
@@ -43,6 +53,8 @@ export const CitySelector: React.FC = observer(() => {
})() })()
: baseCities; : baseCities;
const noCitySelected = !selectedCity?.id;
const handleCityChange = (event: SelectChangeEvent<string>) => { const handleCityChange = (event: SelectChangeEvent<string>) => {
const cityId = event.target.value; const cityId = event.target.value;
if (cityId === "") { if (cityId === "") {
@@ -58,10 +70,7 @@ export const CitySelector: React.FC = observer(() => {
} }
}; };
return ( const selectElement = (
<Box className="flex items-center gap-2">
<MapPin size={16} className={isLocked ? "text-gray-400" : "text-white"} />
<FormControl size="medium" sx={{ minWidth: 120 }}>
<Select <Select
value={selectedCity?.id?.toString() || ""} value={selectedCity?.id?.toString() || ""}
onChange={handleCityChange} onChange={handleCityChange}
@@ -70,15 +79,23 @@ export const CitySelector: React.FC = observer(() => {
sx={{ sx={{
height: "40px", height: "40px",
color: "white", color: "white",
"&.Mui-disabled": { borderRadius: "4px",
color: "rgba(255, 255, 255, 0.5)", ...(noCitySelected && !isLocked
WebkitTextFillColor: "rgba(255, 255, 255, 0.5)", ? {
}, backgroundColor: "#48989f",
"& .MuiOutlinedInput-notchedOutline": { border: "none" },
}
: {
"& .MuiOutlinedInput-notchedOutline": { "& .MuiOutlinedInput-notchedOutline": {
borderColor: isLocked borderColor: isLocked
? "rgba(255, 255, 255, 0.1)" ? "rgba(255, 255, 255, 0.1)"
: "rgba(255, 255, 255, 0.3)", : "rgba(255, 255, 255, 0.3)",
}, },
}),
"&.Mui-disabled": {
color: "rgba(255, 255, 255, 0.5)",
WebkitTextFillColor: "rgba(255, 255, 255, 0.5)",
},
"&:hover .MuiOutlinedInput-notchedOutline": { "&:hover .MuiOutlinedInput-notchedOutline": {
borderColor: isLocked borderColor: isLocked
? "rgba(255, 255, 255, 0.1)" ? "rgba(255, 255, 255, 0.1)"
@@ -101,6 +118,28 @@ export const CitySelector: React.FC = observer(() => {
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>
);
return (
<Box className="flex items-center gap-2">
<MapPin size={16} className={isLocked ? "text-gray-400" : "text-white"} />
<FormControl size="medium" sx={{ minWidth: 120 }}>
{noCitySelected && !isLocked ? (
<Box
sx={{
position: "relative",
borderRadius: "4px",
padding: "2px",
background: "linear-gradient(90deg, rgba(255,255,255,0.1), rgba(255,255,255,0.7), rgba(255,255,255,0.1), rgba(255,255,255,0.7), rgba(255,255,255,0.1))",
backgroundSize: "200% 100%",
animation: `${borderSpin} 2.5s linear infinite`,
}}
>
{selectElement}
</Box>
) : (
selectElement
)}
</FormControl> </FormControl>
</Box> </Box>
); );