From 6af95bb449b8c1d66aab319e43a297ccfc1b3568 Mon Sep 17 00:00:00 2001 From: itoshi Date: Tue, 5 May 2026 15:07:18 +0300 Subject: [PATCH] feat: update color carrier --- .env | 14 +- .../components/ListOfSights/SightFrame.jsx | 42 +++++ .../src/components/side-menu/SideMenu.jsx | 13 +- src/client/src/stores/ColorStore.ts | 2 +- src/pages/Carrier/CarrierCreatePage/index.tsx | 162 ++++++++++++++++ src/pages/Carrier/CarrierEditPage/index.tsx | 176 +++++++++++++++++- src/pages/City/CityCreatePage/index.tsx | 7 + src/pages/City/CityEditPage/index.tsx | 7 + src/pages/City/CityListPage/index.tsx | 27 ++- src/pages/Country/CountryAddPage/index.tsx | 9 +- src/pages/Country/CountryCreatePage/index.tsx | 10 +- src/pages/Country/CountryEditPage/index.tsx | 8 +- src/pages/Country/CountryListPage/index.tsx | 23 ++- src/pages/CreateSightPage/index.tsx | 6 + src/pages/EditSightPage/index.tsx | 6 + src/pages/Route/RouteCreatePage/index.tsx | 8 +- src/pages/Route/RouteEditPage/index.tsx | 9 +- src/pages/Route/RouteListPage/index.tsx | 17 +- src/pages/Sight/SightListPage/index.tsx | 12 +- src/pages/Station/StationCreatePage/index.tsx | 7 + src/pages/Station/StationEditPage/index.tsx | 7 + src/pages/Station/StationListPage/index.tsx | 19 +- src/shared/store/CarrierStore/index.tsx | 81 ++++---- src/shared/store/SelectedCityStore/index.ts | 7 + src/widgets/CitySelector/index.tsx | 21 ++- 25 files changed, 620 insertions(+), 80 deletions(-) diff --git a/.env b/.env index c554530..c8ded82 100644 --- a/.env +++ b/.env @@ -1,8 +1,8 @@ -VITE_API_URL='https://wn.st.unprism.ru' -VITE_REACT_APP ='https://wn.st.unprism.ru/' -VITE_KRBL_MEDIA='https://wn.st.unprism.ru/media/' -VITE_NEED_AUTH='true' -# VITE_API_URL='https://wn.krbl.ru' -# VITE_REACT_APP ='https://wn.krbl.ru/' -# VITE_KRBL_MEDIA='https://wn.krbl.ru/media/' +# VITE_API_URL='https://wn.st.unprism.ru' +# VITE_REACT_APP ='https://wn.st.unprism.ru/' +# VITE_KRBL_MEDIA='https://wn.st.unprism.ru/media/' # VITE_NEED_AUTH='true' +VITE_API_URL='https://wn.krbl.ru' +VITE_REACT_APP ='https://wn.krbl.ru/' +VITE_KRBL_MEDIA='https://wn.krbl.ru/media/' +VITE_NEED_AUTH='true' diff --git a/src/client/src/components/ListOfSights/SightFrame.jsx b/src/client/src/components/ListOfSights/SightFrame.jsx index 073befc..2ae027e 100644 --- a/src/client/src/components/ListOfSights/SightFrame.jsx +++ b/src/client/src/components/ListOfSights/SightFrame.jsx @@ -43,9 +43,51 @@ const SightFrame = observer(({ media, sight_id, sight_name }) => { const [threeViewResetKey, setThreeViewResetKey] = useState(0); const threeViewControlRef = useRef(null); const mediaCache = useRef({}); + const idleTimerRef = useRef(null); const textWrapperRef = useRef(null); + // Автозакрытие fullscreen 3D при бездействии (45 сек) + useEffect(() => { + if (!isFullscreen3D) { + if (idleTimerRef.current) { + clearInterval(idleTimerRef.current); + idleTimerRef.current = null; + } + return; + } + + let idleSeconds = 0; + + const checkIdle = () => { + idleSeconds += 1; + if (idleSeconds >= 45) { + setIsFullscreen3D(false); + } + }; + + idleTimerRef.current = setInterval(checkIdle, 1000); + + const resetIdle = () => { + idleSeconds = 0; + }; + + const events = ["mousedown", "mousemove", "keypress", "scroll", "touchstart", "click"]; + events.forEach((event) => { + window.addEventListener(event, resetIdle, { passive: true }); + }); + + return () => { + if (idleTimerRef.current) { + clearInterval(idleTimerRef.current); + idleTimerRef.current = null; + } + events.forEach((event) => { + window.removeEventListener(event, resetIdle); + }); + }; + }, [isFullscreen3D]); + const { routeSights, routeSightsEn, diff --git a/src/client/src/components/side-menu/SideMenu.jsx b/src/client/src/components/side-menu/SideMenu.jsx index 0d38baf..c92a564 100644 --- a/src/client/src/components/side-menu/SideMenu.jsx +++ b/src/client/src/components/side-menu/SideMenu.jsx @@ -13,6 +13,7 @@ import StationsList from "./StationsList"; import LeftWidget from "./LeftWidget"; import { apiStore } from "../../api/ApiStore/store"; import { getMediaUrl } from "../../api/apiConfig"; +import defaultCrest from "../../assets/images/Герб.png"; const SideMenu = observer(({ onMenuToggle }) => { const { @@ -369,13 +370,11 @@ const SideMenu = observer(({ onMenuToggle }) => { "background 0.3s ease, backdrop-filter 0.3s ease, box-shadow 0.3s ease", }} > - {designData?.creastPath && ( - Герб - )} + Герб {carrier?.slogan && (
{carrier.slogan}
)} diff --git a/src/client/src/stores/ColorStore.ts b/src/client/src/stores/ColorStore.ts index 75ce2a0..f24723a 100644 --- a/src/client/src/stores/ColorStore.ts +++ b/src/client/src/stores/ColorStore.ts @@ -68,4 +68,4 @@ class ColorStore implements ColorStore { } export const colorStore = new ColorStore(); -export { ColorStore }; \ No newline at end of file +export { ColorStore }; diff --git a/src/pages/Carrier/CarrierCreatePage/index.tsx b/src/pages/Carrier/CarrierCreatePage/index.tsx index ba87efc..40b1c47 100644 --- a/src/pages/Carrier/CarrierCreatePage/index.tsx +++ b/src/pages/Carrier/CarrierCreatePage/index.tsx @@ -6,6 +6,8 @@ import { MenuItem, FormControl, InputLabel, + ToggleButtonGroup, + ToggleButton, } from "@mui/material"; import { observer } from "mobx-react-lite"; import { ArrowLeft, Loader2, Save } from "lucide-react"; @@ -27,7 +29,59 @@ import { import { useState, useEffect } from "react"; import { ImageUploadCard, LanguageSwitcher } from "@widgets"; +type ColorFields = { main_color: string; left_color: string; right_color: string; rgb_color: string }; + +const colorFields = (data: ColorFields) => ({ + main_color: data.main_color, + left_color: data.left_color, + right_color: data.right_color, + rgb_color: data.rgb_color, +}); + +const ColorPickerField = ({ + label, + value, + onChange, +}: { + label: string; + value: string; + onChange: (val: string) => void; +}) => ( +
+
+ onChange(e.target.value)} + className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" + /> +
+ onChange(e.target.value)} + InputProps={{ + endAdornment: value ? ( + + ) : undefined, + }} + /> +
+); + export const CarrierCreatePage = observer(() => { + const [colorMode, setColorMode] = useState<"rgb" | "three">("three"); const navigate = useNavigate(); const { createCarrierData, setCreateCarrierData } = carrierStore; const { language } = languageStore; @@ -220,6 +274,114 @@ export const CarrierCreatePage = observer(() => { } /> +
+
+
+ Режим цвета: + { + if (!val) return; + setColorMode(val); + if (val === "rgb") { + setCreateCarrierData( + createCarrierData[language].full_name, + createCarrierData[language].short_name, + createCarrierData.city_id, + createCarrierData[language].slogan, + selectedMediaId || "", + language, + { main_color: "", left_color: "", right_color: "", rgb_color: createCarrierData.rgb_color } + ); + } else { + setCreateCarrierData( + createCarrierData[language].full_name, + createCarrierData[language].short_name, + createCarrierData.city_id, + createCarrierData[language].slogan, + selectedMediaId || "", + language, + { main_color: createCarrierData.main_color, left_color: createCarrierData.left_color, right_color: createCarrierData.right_color, rgb_color: "" } + ); + } + }} + > + Один цвет + Три цвета + +
+ * при переключении цвет сбрасывается +
+ + {colorMode === "rgb" ? ( + + setCreateCarrierData( + createCarrierData[language].full_name, + createCarrierData[language].short_name, + createCarrierData.city_id, + createCarrierData[language].slogan, + selectedMediaId || "", + language, + { ...colorFields(createCarrierData), rgb_color: val } + ) + } + /> + ) : ( + <> + + setCreateCarrierData( + createCarrierData[language].full_name, + createCarrierData[language].short_name, + createCarrierData.city_id, + createCarrierData[language].slogan, + selectedMediaId || "", + language, + { ...colorFields(createCarrierData), main_color: val } + ) + } + /> + + setCreateCarrierData( + createCarrierData[language].full_name, + createCarrierData[language].short_name, + createCarrierData.city_id, + createCarrierData[language].slogan, + selectedMediaId || "", + language, + { ...colorFields(createCarrierData), left_color: val } + ) + } + /> + + setCreateCarrierData( + createCarrierData[language].full_name, + createCarrierData[language].short_name, + createCarrierData.city_id, + createCarrierData[language].slogan, + selectedMediaId || "", + language, + { ...colorFields(createCarrierData), right_color: val } + ) + } + /> + + )} +
+
({ + main_color: data.main_color, + left_color: data.left_color, + right_color: data.right_color, + rgb_color: data.rgb_color, +}); + +const ColorPickerField = ({ + label, + value, + onChange, +}: { + label: string; + value: string; + onChange: (val: string) => void; +}) => ( +
+
+ onChange(e.target.value)} + className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" + /> +
+ onChange(e.target.value)} + InputProps={{ + endAdornment: value ? ( + + ) : undefined, + }} + /> +
+); + export const CarrierEditPage = observer(() => { const navigate = useNavigate(); const { id } = useParams(); @@ -37,6 +90,7 @@ export const CarrierEditPage = observer(() => { const { language } = languageStore; const canReadCities = authStore.canRead("cities"); + const [colorMode, setColorMode] = useState<"rgb" | "three">("rgb"); const [isLoading, setIsLoading] = useState(false); const [isLoadingData, setIsLoadingData] = useState(true); const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false); @@ -68,13 +122,25 @@ export const CarrierEditPage = observer(() => { const carrierData = await getCarrier(Number(id)); if (carrierData) { + const colors = { + main_color: carrierData.ru?.main_color || "", + left_color: carrierData.ru?.left_color || "", + right_color: carrierData.ru?.right_color || "", + rgb_color: carrierData.ru?.rgb_color || "", + }; + if (colors.rgb_color) { + setColorMode("rgb"); + } else { + setColorMode("three"); + } setEditCarrierData( carrierData.ru?.full_name || "", carrierData.ru?.short_name || "", carrierData.ru?.city_id || 0, carrierData.ru?.slogan || "", carrierData.ru?.logo || "", - "ru" + "ru", + colors ); setEditCarrierData( carrierData.en?.full_name || "", @@ -273,6 +339,114 @@ export const CarrierEditPage = observer(() => { } /> +
+
+
+ Режим цвета: + { + if (!val) return; + setColorMode(val); + if (val === "rgb") { + setEditCarrierData( + editCarrierData[language].full_name, + editCarrierData[language].short_name, + editCarrierData.city_id, + editCarrierData[language].slogan, + editCarrierData.logo, + language, + { main_color: "", left_color: "", right_color: "", rgb_color: editCarrierData.rgb_color } + ); + } else { + setEditCarrierData( + editCarrierData[language].full_name, + editCarrierData[language].short_name, + editCarrierData.city_id, + editCarrierData[language].slogan, + editCarrierData.logo, + language, + { main_color: editCarrierData.main_color, left_color: editCarrierData.left_color, right_color: editCarrierData.right_color, rgb_color: "" } + ); + } + }} + > + Один цвет + Три цвета + +
+ * при переключении цвет сбрасывается +
+ + {colorMode === "rgb" ? ( + + setEditCarrierData( + editCarrierData[language].full_name, + editCarrierData[language].short_name, + editCarrierData.city_id, + editCarrierData[language].slogan, + editCarrierData.logo, + language, + { ...colorFields(editCarrierData), rgb_color: val } + ) + } + /> + ) : ( + <> + + setEditCarrierData( + editCarrierData[language].full_name, + editCarrierData[language].short_name, + editCarrierData.city_id, + editCarrierData[language].slogan, + editCarrierData.logo, + language, + { ...colorFields(editCarrierData), main_color: val } + ) + } + /> + + setEditCarrierData( + editCarrierData[language].full_name, + editCarrierData[language].short_name, + editCarrierData.city_id, + editCarrierData[language].slogan, + editCarrierData.logo, + language, + { ...colorFields(editCarrierData), left_color: val } + ) + } + /> + + setEditCarrierData( + editCarrierData[language].full_name, + editCarrierData[language].short_name, + editCarrierData.city_id, + editCarrierData[language].slogan, + editCarrierData.logo, + language, + { ...colorFields(editCarrierData), right_color: val } + ) + } + /> + + )} +
+
{ const navigate = useNavigate(); + + useEffect(() => { + selectedCityStore.setIsLocked(true); + return () => selectedCityStore.setIsLocked(false); + }, []); + const { language } = languageStore; const { createCityData, setCreateCityData, setCreateCityWeatherCode } = cityStore; diff --git a/src/pages/City/CityEditPage/index.tsx b/src/pages/City/CityEditPage/index.tsx index 1090841..f765aba 100644 --- a/src/pages/City/CityEditPage/index.tsx +++ b/src/pages/City/CityEditPage/index.tsx @@ -23,12 +23,19 @@ import { SelectMediaDialog, UploadMediaDialog, PreviewMediaDialog, + selectedCityStore, } from "@shared"; import { useEffect, useState } from "react"; import { LanguageSwitcher, ImageUploadCard } from "@widgets"; export const CityEditPage = observer(() => { const navigate = useNavigate(); + + useEffect(() => { + selectedCityStore.setIsLocked(true); + return () => selectedCityStore.setIsLocked(false); + }, []); + const [isLoading, setIsLoading] = useState(false); const [isLoadingData, setIsLoadingData] = useState(true); const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false); diff --git a/src/pages/City/CityListPage/index.tsx b/src/pages/City/CityListPage/index.tsx index b1349dc..afbab5f 100644 --- a/src/pages/City/CityListPage/index.tsx +++ b/src/pages/City/CityListPage/index.tsx @@ -1,6 +1,6 @@ import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; import { ruRU } from "@mui/x-data-grid/locales"; -import { authStore, languageStore, cityStore, countryStore, SearchInput } from "@shared"; +import { authStore, languageStore, cityStore, countryStore, selectedCityStore, SearchInput } from "@shared"; import { useEffect, useState, useMemo } from "react"; import { observer } from "mobx-react-lite"; import { Pencil, Trash2, Minus } from "lucide-react"; @@ -59,16 +59,21 @@ export const CityListPage = observer(() => { }, [cities, countryStore.countries, language, isLoading]); const filteredRows = useMemo(() => { + const { selectedCityId } = selectedCityStore; + if (!selectedCityId) return []; + const query = searchQuery.trim().toLowerCase(); - if (!query) return rows; - return rows.filter((row) => { + const result = rows.filter((row) => row.id === selectedCityId); + + if (!query) return result; + return result.filter((row) => { const cityName = (row.name ?? "").toLowerCase(); const countryName = ( countryStore.countries[language]?.data?.find((c) => c.code === row.country)?.name ?? "" ).toLowerCase(); return cityName.includes(query) || countryName.includes(query); }); - }, [rows, searchQuery, countryStore.countries, language]); + }, [rows, searchQuery, countryStore.countries, language, selectedCityStore.selectedCityId]); const columns: GridColDef[] = [ { @@ -139,7 +144,11 @@ export const CityListPage = observer(() => {

Города

{canWriteCities && ( - + )}
@@ -195,7 +204,13 @@ export const CityListPage = observer(() => { slots={{ noRowsOverlay: () => ( - {isLoading ? : "Нет городов"} + {isLoading ? ( + + ) : !selectedCityStore.selectedCityId ? ( + "Выберите город" + ) : ( + "Нет городов" + )} ), }} diff --git a/src/pages/Country/CountryAddPage/index.tsx b/src/pages/Country/CountryAddPage/index.tsx index d6ef176..b7bcea1 100644 --- a/src/pages/Country/CountryAddPage/index.tsx +++ b/src/pages/Country/CountryAddPage/index.tsx @@ -15,11 +15,18 @@ import { RU_COUNTRIES, EN_COUNTRIES, ZH_COUNTRIES, + selectedCityStore, } from "@shared"; -import { useState } from "react"; +import { useState, useEffect } from "react"; export const CountryAddPage = observer(() => { const navigate = useNavigate(); + + useEffect(() => { + selectedCityStore.setIsLocked(true); + return () => selectedCityStore.setIsLocked(false); + }, []); + const [isLoading, setIsLoading] = useState(false); const { createCountryData, setCountryData, createCountry } = countryStore; diff --git a/src/pages/Country/CountryCreatePage/index.tsx b/src/pages/Country/CountryCreatePage/index.tsx index 4601465..3de4d13 100644 --- a/src/pages/Country/CountryCreatePage/index.tsx +++ b/src/pages/Country/CountryCreatePage/index.tsx @@ -4,12 +4,18 @@ import { ArrowLeft, Save } from "lucide-react"; import { Loader2 } from "lucide-react"; import { useNavigate } from "react-router-dom"; import { toast } from "react-toastify"; -import { countryStore, languageStore } from "@shared"; -import { useState } from "react"; +import { countryStore, languageStore, selectedCityStore } from "@shared"; +import { useState, useEffect } from "react"; import { LanguageSwitcher } from "@widgets"; export const CountryCreatePage = observer(() => { const navigate = useNavigate(); + + useEffect(() => { + selectedCityStore.setIsLocked(true); + return () => selectedCityStore.setIsLocked(false); + }, []); + const [isLoading, setIsLoading] = useState(false); const { language } = languageStore; const { createCountryData, setCountryData, createCountry } = countryStore; diff --git a/src/pages/Country/CountryEditPage/index.tsx b/src/pages/Country/CountryEditPage/index.tsx index fbfda84..b20ab8e 100644 --- a/src/pages/Country/CountryEditPage/index.tsx +++ b/src/pages/Country/CountryEditPage/index.tsx @@ -4,12 +4,18 @@ import { ArrowLeft, Save } from "lucide-react"; import { Loader2 } from "lucide-react"; import { useNavigate, useParams } from "react-router-dom"; import { toast } from "react-toastify"; -import { countryStore, languageStore, LoadingSpinner } from "@shared"; +import { countryStore, languageStore, LoadingSpinner, selectedCityStore } from "@shared"; import { useEffect, useState } from "react"; import { LanguageSwitcher } from "@widgets"; export const CountryEditPage = observer(() => { const navigate = useNavigate(); + + useEffect(() => { + selectedCityStore.setIsLocked(true); + return () => selectedCityStore.setIsLocked(false); + }, []); + const [isLoading, setIsLoading] = useState(false); const [isLoadingData, setIsLoadingData] = useState(true); const { language } = languageStore; diff --git a/src/pages/Country/CountryListPage/index.tsx b/src/pages/Country/CountryListPage/index.tsx index efdfd9c..92555f4 100644 --- a/src/pages/Country/CountryListPage/index.tsx +++ b/src/pages/Country/CountryListPage/index.tsx @@ -1,6 +1,6 @@ import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; import { ruRU } from "@mui/x-data-grid/locales"; -import { authStore, countryStore, languageStore, SearchInput } from "@shared"; +import { authStore, countryStore, languageStore, selectedCityStore, SearchInput } from "@shared"; import { useEffect, useState, useMemo } from "react"; import { observer } from "mobx-react-lite"; import { Trash2, Minus } from "lucide-react"; @@ -74,15 +74,20 @@ export const CountryListPage = observer(() => { ]; const rows = useMemo(() => { + const { selectedCity } = selectedCityStore; + if (!selectedCity) { + return []; + } const query = searchQuery.trim().toLowerCase(); return (countries[language]?.data ?? []) + .filter((country) => country.code === selectedCity.country_code) .filter((country) => !query || (country.name ?? "").toLowerCase().includes(query)) .map((country) => ({ id: country.code, code: country.code, name: country.name, })); - }, [countries[language]?.data, searchQuery]); + }, [countries[language]?.data, searchQuery, selectedCityStore.selectedCity]); return ( <> @@ -92,7 +97,11 @@ export const CountryListPage = observer(() => {

Страны

{canWriteCountries && ( - + )}
@@ -148,7 +157,13 @@ export const CountryListPage = observer(() => { slots={{ noRowsOverlay: () => ( - {isLoading ? : "Нет стран"} + {isLoading ? ( + + ) : !selectedCityStore.selectedCityId ? ( + "Выберите город" + ) : ( + "Нет стран" + )} ), }} diff --git a/src/pages/CreateSightPage/index.tsx b/src/pages/CreateSightPage/index.tsx index f0e34da..7f6729f 100644 --- a/src/pages/CreateSightPage/index.tsx +++ b/src/pages/CreateSightPage/index.tsx @@ -5,6 +5,7 @@ import { cityStore, createSightStore, languageStore, + selectedCityStore, } from "@shared"; import { CreateInformationTab, @@ -30,6 +31,11 @@ export const CreateSightPage = observer(() => { const { getArticles } = articlesStore; const needLeave = createSightStore.needLeaveAgree; + useEffect(() => { + selectedCityStore.setIsLocked(true); + return () => selectedCityStore.setIsLocked(false); + }, []); + const handleChange = (_: React.SyntheticEvent, newValue: number) => { setValue(newValue); }; diff --git a/src/pages/EditSightPage/index.tsx b/src/pages/EditSightPage/index.tsx index 7ca464e..6c94a6c 100644 --- a/src/pages/EditSightPage/index.tsx +++ b/src/pages/EditSightPage/index.tsx @@ -9,6 +9,7 @@ import { cityStore, editSightStore, LoadingSpinner, + selectedCityStore, } from "@shared"; import { useBlocker, useParams } from "react-router-dom"; @@ -25,6 +26,11 @@ export const EditSightPage = observer(() => { const { sight, getSightInfo, needLeaveAgree, getRightArticles } = editSightStore; const { getArticles } = articlesStore; + useEffect(() => { + selectedCityStore.setIsLocked(true); + return () => selectedCityStore.setIsLocked(false); + }, []); + const { id } = useParams(); const { getCities } = cityStore; diff --git a/src/pages/Route/RouteCreatePage/index.tsx b/src/pages/Route/RouteCreatePage/index.tsx index 908f4de..769a550 100644 --- a/src/pages/Route/RouteCreatePage/index.tsx +++ b/src/pages/Route/RouteCreatePage/index.tsx @@ -39,6 +39,12 @@ import type { Route } from "@shared"; export const RouteCreatePage = observer(() => { const navigate = useNavigate(); + + useEffect(() => { + selectedCityStore.setIsLocked(true); + return () => selectedCityStore.setIsLocked(false); + }, []); + const [carrier, setCarrier] = useState(""); const [routeNumber, setRouteNumber] = useState(""); const [routeCoords, setRouteCoords] = useState(""); @@ -555,7 +561,7 @@ export const RouteCreatePage = observer(() => { /> { diff --git a/src/pages/Route/RouteEditPage/index.tsx b/src/pages/Route/RouteEditPage/index.tsx index 4c3fe9c..c70c7ac 100644 --- a/src/pages/Route/RouteEditPage/index.tsx +++ b/src/pages/Route/RouteEditPage/index.tsx @@ -36,11 +36,18 @@ import { UploadMediaDialog, PreviewMediaDialog, LoadingSpinner, + selectedCityStore, } from "@shared"; import { LinkedItems } from "../LinekedStations"; export const RouteEditPage = observer(() => { const navigate = useNavigate(); + + useEffect(() => { + selectedCityStore.setIsLocked(true); + return () => selectedCityStore.setIsLocked(false); + }, []); + const { id } = useParams(); const { editRouteData, copyRouteAction } = routeStore; const [isLoading, setIsLoading] = useState(false); @@ -548,7 +555,7 @@ export const RouteEditPage = observer(() => { /> { diff --git a/src/pages/Route/RouteListPage/index.tsx b/src/pages/Route/RouteListPage/index.tsx index d2390d4..9635780 100644 --- a/src/pages/Route/RouteListPage/index.tsx +++ b/src/pages/Route/RouteListPage/index.tsx @@ -210,6 +210,9 @@ export const RouteListPage = observer(() => { const rows = useMemo(() => { const { selectedCityId } = selectedCityStore; + if (!selectedCityId) { + return []; + } const query = searchQuery.trim().toLowerCase(); let filtered = routes.data; if (selectedCityId) { @@ -247,7 +250,11 @@ export const RouteListPage = observer(() => {

Маршруты

{canWriteRoutes && ( - + )}
@@ -304,7 +311,13 @@ export const RouteListPage = observer(() => { slots={{ noRowsOverlay: () => ( - {isLoading ? : "Нет маршрутов"} + {isLoading ? ( + + ) : !selectedCityStore.selectedCityId ? ( + "Выберите город" + ) : ( + "Нет маршрутов" + )} ), }} diff --git a/src/pages/Sight/SightListPage/index.tsx b/src/pages/Sight/SightListPage/index.tsx index f647ca9..ec54204 100644 --- a/src/pages/Sight/SightListPage/index.tsx +++ b/src/pages/Sight/SightListPage/index.tsx @@ -121,8 +121,11 @@ export const SightListPage = observer(() => { }] : []), ]; + const { selectedCityId } = selectedCityStore; const filteredSights = useMemo(() => { - const { selectedCityId } = selectedCityStore; + if (!selectedCityId) { + return []; + } const allowedCityIds = canReadCities ? null : authStore.meCities["ru"].map((c) => c.city_id); @@ -131,12 +134,12 @@ export const SightListPage = observer(() => { if (allowedCityIds && !allowedCityIds.includes(sight.city_id)) { return false; } - if (selectedCityId && sight.city_id !== selectedCityId) { + if (sight.city_id !== selectedCityId) { return false; } return true; }); - }, [sights, selectedCityStore.selectedCityId, canReadCities, authStore.meCities]); + }, [sights, selectedCityId, canReadCities, authStore.meCities]); const query = searchQuery.trim().toLowerCase(); const rows = filteredSights @@ -161,6 +164,7 @@ export const SightListPage = observer(() => { )}
@@ -216,6 +220,8 @@ export const SightListPage = observer(() => { {isLoading ? ( + ) : !selectedCityId ? ( + "Выберите город" ) : ( "Нет достопримечательностей" )} diff --git a/src/pages/Station/StationCreatePage/index.tsx b/src/pages/Station/StationCreatePage/index.tsx index 8a9732f..2cb2fa6 100644 --- a/src/pages/Station/StationCreatePage/index.tsx +++ b/src/pages/Station/StationCreatePage/index.tsx @@ -19,6 +19,7 @@ import { mediaStore, isMediaIdEmpty, useSelectedCity, + selectedCityStore, SelectMediaDialog, UploadMediaDialog, PreviewMediaDialog, @@ -32,6 +33,12 @@ import { export const StationCreatePage = observer(() => { const navigate = useNavigate(); + + useEffect(() => { + selectedCityStore.setIsLocked(true); + return () => selectedCityStore.setIsLocked(false); + }, []); + const [isLoading, setIsLoading] = useState(false); const { language } = languageStore; const { diff --git a/src/pages/Station/StationEditPage/index.tsx b/src/pages/Station/StationEditPage/index.tsx index bf4e82b..5ef875a 100644 --- a/src/pages/Station/StationEditPage/index.tsx +++ b/src/pages/Station/StationEditPage/index.tsx @@ -19,6 +19,7 @@ import { mediaStore, isMediaIdEmpty, LoadingSpinner, + selectedCityStore, SelectMediaDialog, PreviewMediaDialog, UploadMediaDialog, @@ -34,6 +35,12 @@ import { LinkedSights } from "../LinkedSights"; export const StationEditPage = observer(() => { const navigate = useNavigate(); + + useEffect(() => { + selectedCityStore.setIsLocked(true); + return () => selectedCityStore.setIsLocked(false); + }, []); + const [isLoading, setIsLoading] = useState(false); const [isLoadingData, setIsLoadingData] = useState(true); const { language } = languageStore; diff --git a/src/pages/Station/StationListPage/index.tsx b/src/pages/Station/StationListPage/index.tsx index c27768a..445460b 100644 --- a/src/pages/Station/StationListPage/index.tsx +++ b/src/pages/Station/StationListPage/index.tsx @@ -174,9 +174,12 @@ export const StationListPage = observer(() => { const rows = useMemo(() => { const { selectedCityId } = selectedCityStore; + if (!selectedCityId) { + return []; + } const query = searchQuery.trim().toLowerCase(); return stationLists[language].data - .filter((station: any) => !selectedCityId || station.city_id === selectedCityId) + .filter((station: any) => station.city_id === selectedCityId) .filter( (station: any) => !query || @@ -202,7 +205,11 @@ export const StationListPage = observer(() => {

Остановки

{canWriteStations && ( - + )}
@@ -277,7 +284,13 @@ export const StationListPage = observer(() => { slots={{ noRowsOverlay: () => ( - {isLoading ? : "Нет остановок"} + {isLoading ? ( + + ) : !selectedCityStore.selectedCityId ? ( + "Выберите город" + ) : ( + "Нет остановок" + )} ), }} diff --git a/src/shared/store/CarrierStore/index.tsx b/src/shared/store/CarrierStore/index.tsx index 88967cf..db1898c 100644 --- a/src/shared/store/CarrierStore/index.tsx +++ b/src/shared/store/CarrierStore/index.tsx @@ -16,9 +16,10 @@ export type Carrier = { city: string; city_id: number; logo: string; - // main_color: string; - // left_color: string; - // right_color: string; + main_color: string; + left_color: string; + right_color: string; + rgb_color: string; }; type CarrierData = { @@ -112,6 +113,10 @@ class CarrierStore { createCarrierData = { city_id: 0, logo: "", + main_color: "", + left_color: "", + right_color: "", + rgb_color: "", ru: { full_name: "", short_name: "", @@ -135,10 +140,17 @@ class CarrierStore { cityId: number, slogan: string, logoId: string, - language: Language + language: Language, + colors?: { main_color?: string; left_color?: string; right_color?: string; rgb_color?: string } ) => { this.createCarrierData.city_id = cityId; this.createCarrierData.logo = logoId; + if (colors) { + if (colors.main_color !== undefined) this.createCarrierData.main_color = colors.main_color; + if (colors.left_color !== undefined) this.createCarrierData.left_color = colors.left_color; + if (colors.right_color !== undefined) this.createCarrierData.right_color = colors.right_color; + if (colors.rgb_color !== undefined) this.createCarrierData.rgb_color = colors.rgb_color; + } this.createCarrierData[language] = { full_name: fullName, short_name: shortName, @@ -198,9 +210,11 @@ class CarrierStore { city: cityName, city_id: this.createCarrierData.city_id, slogan: (this.createCarrierData[language].slogan || "").trim(), - ...(this.createCarrierData.logo - ? { logo: this.createCarrierData.logo } - : {}), + ...(this.createCarrierData.logo ? { logo: this.createCarrierData.logo } : {}), + ...(this.createCarrierData.rgb_color ? { rgb_color: this.createCarrierData.rgb_color } : {}), + ...(this.createCarrierData.main_color ? { main_color: this.createCarrierData.main_color } : {}), + ...(this.createCarrierData.left_color ? { left_color: this.createCarrierData.left_color } : {}), + ...(this.createCarrierData.right_color ? { right_color: this.createCarrierData.right_color } : {}), }; const response = await languageInstance(language).post("/carrier", payload); @@ -243,6 +257,10 @@ class CarrierStore { this.createCarrierData = { city_id: 0, logo: "", + main_color: "", + left_color: "", + right_color: "", + rgb_color: "", ru: { full_name: "", short_name: "", @@ -265,53 +283,46 @@ class CarrierStore { ru: { full_name: "", short_name: "", - - // main_color: "", - // left_color: "", - // right_color: "", slogan: "", }, en: { full_name: "", short_name: "", - - // main_color: "", - // left_color: "", - // right_color: "", + slogan: "", + }, + zh: { + full_name: "", + short_name: "", slogan: "", }, city_id: 0, logo: "", - zh: { - full_name: "", - short_name: "", - - // main_color: "", - // left_color: "", - // right_color: "", - slogan: "", - }, + main_color: "", + left_color: "", + right_color: "", + rgb_color: "", }; setEditCarrierData = ( fullName: string, shortName: string, cityId: number, - // main_color: string, - // left_color: string, - // right_color: string, slogan: string, logoId: string, - language: Language + language: Language, + colors?: { main_color?: string; left_color?: string; right_color?: string; rgb_color?: string } ) => { this.editCarrierData.city_id = cityId; this.editCarrierData.logo = logoId; + if (colors) { + if (colors.main_color !== undefined) this.editCarrierData.main_color = colors.main_color; + if (colors.left_color !== undefined) this.editCarrierData.left_color = colors.left_color; + if (colors.right_color !== undefined) this.editCarrierData.right_color = colors.right_color; + if (colors.rgb_color !== undefined) this.editCarrierData.rgb_color = colors.rgb_color; + } this.editCarrierData[language] = { full_name: fullName, short_name: shortName, - // main_color: main_color, - // left_color: left_color, - // right_color: right_color, slogan: slogan, }; }; @@ -326,9 +337,11 @@ class CarrierStore { slogan: (this.editCarrierData[lang].slogan || "").trim(), city: cityName, city_id: this.editCarrierData.city_id, - ...(this.editCarrierData.logo - ? { logo: this.editCarrierData.logo } - : {}), + ...(this.editCarrierData.logo ? { logo: this.editCarrierData.logo } : {}), + ...(this.editCarrierData.rgb_color ? { rgb_color: this.editCarrierData.rgb_color } : {}), + ...(this.editCarrierData.main_color ? { main_color: this.editCarrierData.main_color } : {}), + ...(this.editCarrierData.left_color ? { left_color: this.editCarrierData.left_color } : {}), + ...(this.editCarrierData.right_color ? { right_color: this.editCarrierData.right_color } : {}), }); runInAction(() => { diff --git a/src/shared/store/SelectedCityStore/index.ts b/src/shared/store/SelectedCityStore/index.ts index 270cc20..534996a 100644 --- a/src/shared/store/SelectedCityStore/index.ts +++ b/src/shared/store/SelectedCityStore/index.ts @@ -3,6 +3,7 @@ import { City } from "../CityStore"; class SelectedCityStore { selectedCity: City | null = null; + isLocked: boolean = false; constructor() { makeAutoObservable(this); @@ -32,6 +33,12 @@ class SelectedCityStore { }); }; + setIsLocked = (locked: boolean) => { + runInAction(() => { + this.isLocked = locked; + }); + }; + clearSelectedCity = () => { this.setSelectedCity(null); }; diff --git a/src/widgets/CitySelector/index.tsx b/src/widgets/CitySelector/index.tsx index 12f6165..d73654c 100644 --- a/src/widgets/CitySelector/index.tsx +++ b/src/widgets/CitySelector/index.tsx @@ -12,7 +12,7 @@ import { authStore, cityStore, selectedCityStore, type City } from "@shared"; import { MapPin } from "lucide-react"; export const CitySelector: React.FC = observer(() => { - const { selectedCity, setSelectedCity } = selectedCityStore; + const { selectedCity, setSelectedCity, isLocked } = selectedCityStore; const canLoadAllCities = authStore.isAdmin && authStore.canRead("cities"); useEffect(() => { @@ -58,26 +58,35 @@ export const CitySelector: React.FC = observer(() => { return ( - +