From 300ff262ceb2e7448125a621c7c5f940ce13cb3d Mon Sep 17 00:00:00 2001 From: itoshi Date: Thu, 12 Jun 2025 22:50:43 +0300 Subject: [PATCH] fix: Fix `Map` page --- src/app/router/index.tsx | 24 +- src/entities/navigation/ui/index.tsx | 7 +- src/pages/Article/ArticleCreatePage/index.tsx | 28 + src/pages/Article/ArticleEditPage/index.tsx | 44 ++ src/pages/Article/ArticleListPage/index.tsx | 66 +- src/pages/Carrier/CarrierCreatePage/index.tsx | 160 ++--- src/pages/Carrier/CarrierEditPage/index.tsx | 239 +++---- src/pages/Carrier/CarrierListPage/index.tsx | 75 +- .../Carrier/CarrierPreviewPage/index.tsx | 32 +- src/pages/City/CityCreatePage/index.tsx | 113 +-- src/pages/City/CityEditPage/index.tsx | 128 ++-- src/pages/City/CityListPage/index.tsx | 30 +- src/pages/City/CityPreviewPage/index.tsx | 20 +- src/pages/Country/CountryCreatePage/index.tsx | 3 + src/pages/Country/CountryEditPage/index.tsx | 16 +- src/pages/Country/CountryListPage/index.tsx | 34 +- .../Country/CountryPreviewPage/index.tsx | 13 +- src/pages/EditSightPage/index.tsx | 10 +- src/pages/MapPage/index.tsx | 660 ++++++++++++++---- src/pages/MapPage/mapStore.ts | 23 +- src/pages/Media/MediaListPage/index.tsx | 58 +- src/pages/Route/RouteCreatePage/index.tsx | 241 +++---- src/pages/Route/RouteEditPage/index.tsx | 311 +++++---- src/pages/Route/RouteListPage/index.tsx | 62 +- src/pages/Sight/SightListPage/index.tsx | 60 +- src/pages/Station/StationCreatePage/index.tsx | 8 +- src/pages/Station/StationEditPage/index.tsx | 13 +- src/pages/Station/StationListPage/index.tsx | 61 +- src/pages/User/UserListPage/index.tsx | 60 +- src/pages/Vehicle/VehicleListPage/index.tsx | 80 ++- src/shared/config/CarrierSvg.tsx | 62 ++ src/shared/config/constants.tsx | 96 +-- .../modals/PreviewMediaDialog/index.tsx | 6 +- src/shared/modals/UploadMediaDialog/index.tsx | 4 +- src/shared/store/ArticlesStore/index.tsx | 58 +- src/shared/store/CarrierStore/index.tsx | 65 +- src/shared/store/CityStore/index.ts | 189 +++-- src/shared/store/CountryStore/index.ts | 63 +- src/widgets/MediaViewer/ThreeView.tsx | 9 +- src/widgets/MediaViewer/index.tsx | 23 +- .../SightTabs/InformationTab/index.tsx | 17 +- 41 files changed, 2216 insertions(+), 1055 deletions(-) create mode 100644 src/pages/Article/ArticleCreatePage/index.tsx create mode 100644 src/pages/Article/ArticleEditPage/index.tsx create mode 100644 src/shared/config/CarrierSvg.tsx diff --git a/src/app/router/index.tsx b/src/app/router/index.tsx index da3525a..5167e3f 100644 --- a/src/app/router/index.tsx +++ b/src/app/router/index.tsx @@ -16,22 +16,22 @@ import { SnapshotListPage, CarrierListPage, StationListPage, - VehicleListPage, + // VehicleListPage, ArticleListPage, CityPreviewPage, - CountryPreviewPage, - VehiclePreviewPage, - CarrierPreviewPage, + // CountryPreviewPage, + // VehiclePreviewPage, + // CarrierPreviewPage, SnapshotCreatePage, CountryCreatePage, CityCreatePage, CarrierCreatePage, - VehicleCreatePage, + // VehicleCreatePage, CountryEditPage, CityEditPage, UserCreatePage, UserEditPage, - VehicleEditPage, + // VehicleEditPage, CarrierEditPage, StationCreatePage, StationPreviewPage, @@ -133,7 +133,7 @@ const router = createBrowserRouter([ // Country { path: "country", element: }, { path: "country/create", element: }, - { path: "country/:id", element: }, + // { path: "country/:id", element: }, { path: "country/:id/edit", element: }, // City { path: "city", element: }, @@ -156,7 +156,7 @@ const router = createBrowserRouter([ // Carrier { path: "carrier", element: }, { path: "carrier/create", element: }, - { path: "carrier/:id", element: }, + // { path: "carrier/:id", element: }, { path: "carrier/:id/edit", element: }, // Station { path: "station", element: }, @@ -164,10 +164,10 @@ const router = createBrowserRouter([ { path: "station/:id", element: }, { path: "station/:id/edit", element: }, // Vehicle - { path: "vehicle", element: }, - { path: "vehicle/create", element: }, - { path: "vehicle/:id", element: }, - { path: "vehicle/:id/edit", element: }, + // { path: "vehicle", element: }, + // { path: "vehicle/create", element: }, + // { path: "vehicle/:id", element: }, + // { path: "vehicle/:id/edit", element: }, // Article { path: "article", element: }, // { path: "article/:id", element: }, diff --git a/src/entities/navigation/ui/index.tsx b/src/entities/navigation/ui/index.tsx index 0091637..f90052a 100644 --- a/src/entities/navigation/ui/index.tsx +++ b/src/entities/navigation/ui/index.tsx @@ -9,6 +9,7 @@ import ExpandLessIcon from "@mui/icons-material/ExpandLess"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import type { NavigationItem } from "../model"; import { useNavigate, useLocation } from "react-router-dom"; +import { Plus } from "lucide-react"; interface NavigationItemProps { item: NavigationItem; @@ -58,7 +59,7 @@ export const NavigationItemComponent: React.FC = ({ justifyContent: "center", }, isNested && { - pl: 4, + pl: open ? 4 : 2.5, }, isActive && { backgroundColor: "rgba(0, 0, 0, 0.08)", @@ -84,7 +85,7 @@ export const NavigationItemComponent: React.FC = ({ }, ]} > - + {Icon ? : } = ({ {item.nestedItems && ( - + {item.nestedItems.map((nestedItem) => ( { + const navigate = useNavigate(); + + return ( +
+
+

Создание статьи

+
+ +
+ +
+
+ ); +}; + +export default ArticleCreatePage; diff --git a/src/pages/Article/ArticleEditPage/index.tsx b/src/pages/Article/ArticleEditPage/index.tsx new file mode 100644 index 0000000..2ab1749 --- /dev/null +++ b/src/pages/Article/ArticleEditPage/index.tsx @@ -0,0 +1,44 @@ +import React, { useEffect, useState } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { ArrowLeft } from "lucide-react"; +import { LanguageSwitcher } from "@widgets"; +import { articlesStore, languageStore } from "@shared"; +import { observer } from "mobx-react-lite"; + +const ArticleEditPage: React.FC = observer(() => { + const navigate = useNavigate(); + const { id } = useParams(); + const { language } = languageStore; + const { articleData, getArticle } = articlesStore; + + useEffect(() => { + if (id) { + // Fetch data for all languages + getArticle(parseInt(id), "ru"); + getArticle(parseInt(id), "en"); + getArticle(parseInt(id), "zh"); + } + }, [id]); + + return ( +
+
+

+ {articleData?.ru?.heading || "Редактирование статьи"} +

+
+ +
+ +
+
+ ); +}); + +export default ArticleEditPage; diff --git a/src/pages/Article/ArticleListPage/index.tsx b/src/pages/Article/ArticleListPage/index.tsx index 965eaeb..fd410a1 100644 --- a/src/pages/Article/ArticleListPage/index.tsx +++ b/src/pages/Article/ArticleListPage/index.tsx @@ -2,16 +2,18 @@ import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; import { articlesStore, languageStore } from "@shared"; import { useEffect, useState } from "react"; import { observer } from "mobx-react-lite"; -import { Trash2, Eye } from "lucide-react"; +import { Trash2, Eye, Minus } from "lucide-react"; import { useNavigate } from "react-router-dom"; import { DeleteModal, LanguageSwitcher } from "@widgets"; export const ArticleListPage = observer(() => { - const { articleList, getArticleList } = articlesStore; + const { articleList, getArticleList, deleteArticles } = articlesStore; const navigate = useNavigate(); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [rowId, setRowId] = useState(null); // Lifted state + const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false); + const [rowId, setRowId] = useState(null); const { language } = languageStore; + const [ids, setIds] = useState([]); useEffect(() => { getArticleList(); @@ -22,6 +24,15 @@ export const ArticleListPage = observer(() => { field: "heading", headerName: "Название", flex: 1, + renderCell: (params: GridRenderCellParams) => { + return params.value ? ( + params.value + ) : ( +
+ +
+ ); + }, }, { @@ -59,18 +70,42 @@ export const ArticleListPage = observer(() => {
- +
+

Статьи

+
+ +
0 ? 1 : 0 }} + > + +
+ +
+ { + setIds(Array.from(newSelection.ids) as number[]); + }} + hideFooter + /> +
{ if (rowId) { + await deleteArticles([parseInt(rowId)]); getArticleList(); } setIsDeleteModalOpen(false); @@ -81,6 +116,19 @@ export const ArticleListPage = observer(() => { setRowId(null); }} /> + + { + await deleteArticles(ids); + getArticleList(); + setIsBulkDeleteModalOpen(false); + setIds([]); + }} + onCancel={() => { + setIsBulkDeleteModalOpen(false); + }} + /> ); }); diff --git a/src/pages/Carrier/CarrierCreatePage/index.tsx b/src/pages/Carrier/CarrierCreatePage/index.tsx index 33569ec..93ae0c1 100644 --- a/src/pages/Carrier/CarrierCreatePage/index.tsx +++ b/src/pages/Carrier/CarrierCreatePage/index.tsx @@ -12,21 +12,31 @@ import { ArrowLeft, Save } from "lucide-react"; import { Loader2 } from "lucide-react"; import { useNavigate } from "react-router-dom"; import { toast } from "react-toastify"; -import { carrierStore, cityStore, mediaStore } from "@shared"; +import { carrierStore, cityStore, mediaStore, languageStore } from "@shared"; import { useState, useEffect } from "react"; -import { MediaViewer } from "@widgets"; +import { MediaViewer, ImageUploadCard, LanguageSwitcher } from "@widgets"; +import { + SelectMediaDialog, + UploadMediaDialog, + PreviewMediaDialog, +} from "@shared"; export const CarrierCreatePage = observer(() => { const navigate = useNavigate(); + const { language } = languageStore; const [fullName, setFullName] = useState(""); const [shortName, setShortName] = useState(""); const [cityId, setCityId] = useState(null); - const [main_color, setMainColor] = useState("#000000"); - const [left_color, setLeftColor] = useState("#ffffff"); - const [right_color, setRightColor] = useState("#ff0000"); const [slogan, setSlogan] = useState(""); const [selectedMediaId, setSelectedMediaId] = useState(null); const [isLoading, setIsLoading] = useState(false); + const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false); + const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false); + const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false); + const [mediaId, setMediaId] = useState(""); + const [activeMenuType, setActiveMenuType] = useState< + "thumbnail" | "watermark_lu" | "watermark_rd" | null + >(null); useEffect(() => { cityStore.getCities("ru"); @@ -39,11 +49,7 @@ export const CarrierCreatePage = observer(() => { await carrierStore.createCarrier( fullName, shortName, - cityStore.cities.ru.find((c) => c.id === cityId)?.name!, cityId!, - main_color, - left_color, - right_color, slogan, selectedMediaId! ); @@ -56,8 +62,22 @@ export const CarrierCreatePage = observer(() => { } }; + const handleMediaSelect = (media: { + id: string; + filename: string; + media_name?: string; + media_type: number; + }) => { + setSelectedMediaId(media.id); + }; + + const selectedMedia = selectedMediaId + ? mediaStore.media.find((m) => m.id === selectedMediaId) + : null; + return ( +
+
+

Создание перевозчика

+
+ Город setSelectedMediaId(e.target.value as string)} - > - {mediaStore.media - .filter((media) => media.media_type === 3) - .map((media) => ( - - {media.media_name || media.filename} - - ))} - - - {selectedMediaId && ( -
- -
- )} +
+ { + setIsPreviewMediaOpen(true); + setMediaId(selectedMedia?.id ?? ""); + }} + onDeleteImageClick={() => { + setSelectedMediaId(null); + setActiveMenuType(null); + }} + onSelectFileClick={() => { + setActiveMenuType("thumbnail"); + setIsSelectMediaOpen(true); + }} + setUploadMediaOpen={() => { + setIsUploadMediaOpen(true); + setActiveMenuType("thumbnail"); + }} + />
+ + setIsSelectMediaOpen(false)} + onSelectMedia={handleMediaSelect} + mediaType={3} + /> + + setIsUploadMediaOpen(false)} + afterUpload={handleMediaSelect} + hardcodeType={activeMenuType} + /> + + setIsPreviewMediaOpen(false)} + mediaId={mediaId} + />
); }); diff --git a/src/pages/Carrier/CarrierEditPage/index.tsx b/src/pages/Carrier/CarrierEditPage/index.tsx index 2e49d7d..eb40c2e 100644 --- a/src/pages/Carrier/CarrierEditPage/index.tsx +++ b/src/pages/Carrier/CarrierEditPage/index.tsx @@ -14,7 +14,12 @@ import { useNavigate, useParams } from "react-router-dom"; import { toast } from "react-toastify"; import { carrierStore, cityStore, mediaStore } from "@shared"; import { useState, useEffect } from "react"; -import { MediaViewer } from "@widgets"; +import { MediaViewer, ImageUploadCard } from "@widgets"; +import { + SelectMediaDialog, + UploadMediaDialog, + PreviewMediaDialog, +} from "@shared"; export const CarrierEditPage = observer(() => { const navigate = useNavigate(); @@ -23,22 +28,30 @@ export const CarrierEditPage = observer(() => { carrierStore; const [isLoading, setIsLoading] = useState(false); + const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false); + const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false); + const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false); + const [mediaId, setMediaId] = useState(""); + const [activeMenuType, setActiveMenuType] = useState< + "thumbnail" | "watermark_lu" | "watermark_rd" | null + >(null); useEffect(() => { (async () => { + await cityStore.getCities("ru"); + await cityStore.getCities("en"); + await cityStore.getCities("zh"); await getCarrier(Number(id)); + setEditCarrierData( carrier?.[Number(id)]?.full_name as string, carrier?.[Number(id)]?.short_name as string, - carrier?.[Number(id)]?.city as string, + carrier?.[Number(id)]?.city_id as number, - carrier?.[Number(id)]?.main_color as string, - carrier?.[Number(id)]?.left_color as string, - carrier?.[Number(id)]?.right_color as string, carrier?.[Number(id)]?.slogan as string, carrier?.[Number(id)]?.logo as string ); - cityStore.getCities("ru"); + mediaStore.getMedia(); })(); }, [id]); @@ -56,6 +69,26 @@ export const CarrierEditPage = observer(() => { } }; + const handleMediaSelect = (media: { + id: string; + filename: string; + media_name?: string; + media_type: number; + }) => { + setEditCarrierData( + editCarrierData.full_name, + editCarrierData.short_name, + + editCarrierData.city_id, + editCarrierData.slogan, + media.id + ); + }; + + const selectedMedia = editCarrierData.logo + ? mediaStore.media.find((m) => m.id === editCarrierData.logo) + : null; + return (
@@ -79,17 +112,13 @@ export const CarrierEditPage = observer(() => { setEditCarrierData( editCarrierData.full_name, editCarrierData.short_name, - editCarrierData.city, Number(e.target.value), - editCarrierData.main_color, - editCarrierData.left_color, - editCarrierData.right_color, editCarrierData.slogan, editCarrierData.logo ) } > - {cityStore.cities.ru.map((city) => ( + {cityStore.cities.ru.data?.map((city) => ( {city.name} @@ -106,11 +135,8 @@ export const CarrierEditPage = observer(() => { setEditCarrierData( e.target.value, editCarrierData.short_name, - editCarrierData.city, + editCarrierData.city_id, - editCarrierData.main_color, - editCarrierData.left_color, - editCarrierData.right_color, editCarrierData.slogan, editCarrierData.logo ) @@ -126,104 +152,14 @@ export const CarrierEditPage = observer(() => { setEditCarrierData( editCarrierData.full_name, e.target.value, - editCarrierData.city, + editCarrierData.city_id, - editCarrierData.main_color, - editCarrierData.left_color, - editCarrierData.right_color, editCarrierData.slogan, editCarrierData.logo ) } /> -
- - setEditCarrierData( - editCarrierData.full_name, - editCarrierData.short_name, - editCarrierData.city, - editCarrierData.city_id, - e.target.value, - editCarrierData.left_color, - editCarrierData.right_color, - editCarrierData.slogan, - editCarrierData.logo - ) - } - type="color" - sx={{ - "& input": { - height: "50px", - paddingBlock: "14px", - paddingInline: "14px", - cursor: "pointer", - }, - }} - /> - - setEditCarrierData( - editCarrierData.full_name, - editCarrierData.short_name, - editCarrierData.city, - editCarrierData.city_id, - editCarrierData.main_color, - e.target.value, - editCarrierData.right_color, - editCarrierData.slogan, - editCarrierData.logo - ) - } - type="color" - sx={{ - "& input": { - height: "50px", - paddingBlock: "14px", - paddingInline: "14px", - cursor: "pointer", - }, - }} - /> - - setEditCarrierData( - editCarrierData.full_name, - editCarrierData.short_name, - editCarrierData.city, - editCarrierData.city_id, - editCarrierData.main_color, - editCarrierData.left_color, - e.target.value, - editCarrierData.slogan, - editCarrierData.logo - ) - } - type="color" - sx={{ - "& input": { - height: "50px", - paddingBlock: "14px", - paddingInline: "14px", - cursor: "pointer", - }, - }} - /> -
- { setEditCarrierData( editCarrierData.full_name, editCarrierData.short_name, - editCarrierData.city, + editCarrierData.city_id, - editCarrierData.main_color, - editCarrierData.left_color, - editCarrierData.right_color, e.target.value, editCarrierData.logo ) } /> -
- - Логотип - - - {editCarrierData.logo && ( -
- -
- )} +
+ { + setIsPreviewMediaOpen(true); + setMediaId(selectedMedia?.id ?? ""); + }} + onDeleteImageClick={() => { + setEditCarrierData( + editCarrierData.full_name, + editCarrierData.short_name, + + editCarrierData.city_id, + editCarrierData.slogan, + "" + ); + setActiveMenuType(null); + }} + onSelectFileClick={() => { + setActiveMenuType("thumbnail"); + setIsSelectMediaOpen(true); + }} + setUploadMediaOpen={() => { + setIsUploadMediaOpen(true); + setActiveMenuType("thumbnail"); + }} + />
+ + setIsSelectMediaOpen(false)} + onSelectMedia={handleMediaSelect} + mediaType={3} + /> + + setIsUploadMediaOpen(false)} + afterUpload={handleMediaSelect} + hardcodeType={activeMenuType} + /> + + setIsPreviewMediaOpen(false)} + mediaId={mediaId} + /> ); }); diff --git a/src/pages/Carrier/CarrierListPage/index.tsx b/src/pages/Carrier/CarrierListPage/index.tsx index cf5df03..e2a93ad 100644 --- a/src/pages/Carrier/CarrierListPage/index.tsx +++ b/src/pages/Carrier/CarrierListPage/index.tsx @@ -2,7 +2,7 @@ import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; import { carrierStore } from "@shared"; import { useEffect, useState } from "react"; import { observer } from "mobx-react-lite"; -import { Eye, Pencil, Trash2 } from "lucide-react"; +import { Eye, Pencil, Trash2, Minus } from "lucide-react"; import { useNavigate } from "react-router-dom"; import { CreateButton, DeleteModal } from "@widgets"; @@ -10,7 +10,9 @@ export const CarrierListPage = observer(() => { const { carriers, getCarriers, deleteCarrier } = carrierStore; const navigate = useNavigate(); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [rowId, setRowId] = useState(null); // Lifted state + const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false); + const [rowId, setRowId] = useState(null); + const [ids, setIds] = useState([]); useEffect(() => { getCarriers(); @@ -20,18 +22,50 @@ export const CarrierListPage = observer(() => { { field: "full_name", headerName: "Полное имя", - width: 300, + renderCell: (params: GridRenderCellParams) => { + return ( +
+ {params.value ? ( + params.value + ) : ( + + )} +
+ ); + }, }, { field: "short_name", headerName: "Короткое имя", width: 200, + renderCell: (params: GridRenderCellParams) => { + return ( +
+ {params.value ? ( + params.value + ) : ( + + )} +
+ ); + }, }, { field: "city", headerName: "Город", flex: 1, + renderCell: (params: GridRenderCellParams) => { + return ( +
+ {params.value ? ( + params.value + ) : ( + + )} +
+ ); + }, }, { field: "actions", @@ -45,9 +79,9 @@ export const CarrierListPage = observer(() => { - + */}
+ +
0 ? 1 : 0 }} + > + +
+ { + setIds(Array.from(newSelection.ids) as number[]); + }} hideFooter /> @@ -98,6 +150,19 @@ export const CarrierListPage = observer(() => { setRowId(null); }} /> + + { + await Promise.all(ids.map((id) => deleteCarrier(id))); + getCarriers(); + setIsBulkDeleteModalOpen(false); + setIds([]); + }} + onCancel={() => { + setIsBulkDeleteModalOpen(false); + }} + /> ); }); diff --git a/src/pages/Carrier/CarrierPreviewPage/index.tsx b/src/pages/Carrier/CarrierPreviewPage/index.tsx index ac5814f..82f98e9 100644 --- a/src/pages/Carrier/CarrierPreviewPage/index.tsx +++ b/src/pages/Carrier/CarrierPreviewPage/index.tsx @@ -20,9 +20,9 @@ export const CarrierPreviewPage = observer(() => { carrierResponse?.short_name as string, carrierResponse?.city as string, carrierResponse?.city_id as number, - carrierResponse?.main_color as string, - carrierResponse?.left_color as string, - carrierResponse?.right_color as string, + // carrierResponse?.main_color as string, + // carrierResponse?.left_color as string, + // carrierResponse?.right_color as string, carrierResponse?.slogan as string, carrierResponse?.logo as string ); @@ -58,7 +58,7 @@ export const CarrierPreviewPage = observer(() => {

Город

{carrier[Number(id)]?.city}

-
+ {/*

Основной цвет

{ > {carrier[Number(id)]?.right_color}
-
+
*/}

Краткое имя

{carrier[Number(id)]?.short_name}

-
-

Логотип

+ {oneMedia && ( +
+

Логотип

- -
+ +
+ )} )} diff --git a/src/pages/City/CityCreatePage/index.tsx b/src/pages/City/CityCreatePage/index.tsx index 3f25b1a..45ccac5 100644 --- a/src/pages/City/CityCreatePage/index.tsx +++ b/src/pages/City/CityCreatePage/index.tsx @@ -9,14 +9,18 @@ import { Box, } from "@mui/material"; import { observer } from "mobx-react-lite"; -import { ArrowLeft, Save, ImagePlus } from "lucide-react"; +import { ArrowLeft, Save, ImagePlus, Minus } from "lucide-react"; import { Loader2 } from "lucide-react"; import { useNavigate } from "react-router-dom"; import { toast } from "react-toastify"; import { cityStore, countryStore, languageStore, mediaStore } from "@shared"; import { useState, useEffect } from "react"; -import { LanguageSwitcher, MediaViewer } from "@widgets"; -import { SelectMediaDialog } from "@shared"; +import { LanguageSwitcher, MediaViewer, ImageUploadCard } from "@widgets"; +import { + SelectMediaDialog, + UploadMediaDialog, + PreviewMediaDialog, +} from "@shared"; export const CityCreatePage = observer(() => { const navigate = useNavigate(); @@ -24,12 +28,20 @@ export const CityCreatePage = observer(() => { const { createCityData, setCreateCityData } = cityStore; const [isLoading, setIsLoading] = useState(false); const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false); + const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false); + const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false); + const [mediaId, setMediaId] = useState(""); + const [activeMenuType, setActiveMenuType] = useState< + "thumbnail" | "watermark_lu" | "watermark_rd" | null + >(null); const { getCountries } = countryStore; const { getMedia } = mediaStore; useEffect(() => { (async () => { - await getCountries(language); + await getCountries("ru"); + await getCountries("en"); + await getCountries("zh"); await getMedia(); })(); }, [language]); @@ -55,7 +67,6 @@ export const CityCreatePage = observer(() => { }) => { setCreateCityData( createCityData[language].name, - createCityData.country, createCityData.country_code, media.id, language @@ -80,6 +91,9 @@ export const CityCreatePage = observer(() => {
+
+

Создание города

+
{ onChange={(e) => setCreateCityData( e.target.value, - createCityData.country, createCityData.country_code, createCityData.arms, language @@ -103,19 +116,15 @@ export const CityCreatePage = observer(() => { label="Страна" required onChange={(e) => { - const selectedCountry = countryStore.countries[language]?.find( - (country) => country.code === e.target.value - ); setCreateCityData( createCityData[language].name, - selectedCountry?.name || "", e.target.value, createCityData.arms, language ); }} > - {countryStore.countries[language].map((country) => ( + {countryStore.countries["ru"]?.data?.map((country) => ( {country.name} @@ -123,44 +132,39 @@ export const CityCreatePage = observer(() => { -
- -
- - {selectedMedia && ( - - {selectedMedia.media_name || selectedMedia.filename} - - )} -
- {selectedMedia && ( - - - +
+ {!selectedMedia && ( +
+ + Герб города не выбран +
)} + { + setIsPreviewMediaOpen(true); + setMediaId(selectedMedia?.id ?? ""); + }} + onDeleteImageClick={() => { + setCreateCityData( + createCityData[language].name, + createCityData.country_code, + "", + language + ); + setActiveMenuType(null); + }} + onSelectFileClick={() => { + setActiveMenuType("thumbnail"); + setIsSelectMediaOpen(true); + }} + setUploadMediaOpen={() => { + setIsUploadMediaOpen(true); + setActiveMenuType("thumbnail"); + }} + />
+
+

{editCityData.ru.name}

+
{ onChange={(e) => setEditCityData( e.target.value, - editCityData.country, editCityData.country_code, editCityData.arms, language @@ -120,19 +133,15 @@ export const CityEditPage = observer(() => { label="Страна" required onChange={(e) => { - const selectedCountry = countryStore.countries[language]?.find( - (country) => country.code === e.target.value - ); setEditCityData( - editCityData[language as keyof CashedCities]?.name || "", - selectedCountry?.name || "", + editCityData[language].name, e.target.value, editCityData.arms, language ); }} > - {countryStore.countries[language].map((country) => ( + {countryStore.countries[language].data.map((country) => ( {country.name} @@ -140,44 +149,33 @@ export const CityEditPage = observer(() => { -
- -
- - {selectedMedia && ( - - {selectedMedia.media_name || selectedMedia.filename} - - )} -
- {selectedMedia && ( - - - - )} +
+ { + setIsPreviewMediaOpen(true); + setMediaId(selectedMedia?.id ?? ""); + }} + onDeleteImageClick={() => { + setEditCityData( + editCityData[language].name, + editCityData.country_code, + "", + language + ); + setActiveMenuType(null); + }} + onSelectFileClick={() => { + setActiveMenuType("thumbnail"); + setIsSelectMediaOpen(true); + }} + setUploadMediaOpen={() => { + setIsUploadMediaOpen(true); + setActiveMenuType("thumbnail"); + }} + />
+
+

Создание страны

+
{ useEffect(() => { (async () => { if (id) { - const data = await getCountry(id as string, language); - setEditCountryData(data.name, language); + // Fetch data for all languages + const ruData = await getCountry(id as string, "ru"); + const enData = await getCountry(id as string, "en"); + const zhData = await getCountry(id as string, "zh"); + + // Set data for each language + setEditCountryData(ruData.name, "ru"); + setEditCountryData(enData.name, "en"); + setEditCountryData(zhData.name, "zh"); } })(); - }, [id, language]); + }, [id]); return ( @@ -51,6 +58,9 @@ export const CountryEditPage = observer(() => {
+
+

{editCountryData.ru.name}

+
{ - const { countries, getCountries } = countryStore; + const { countries, getCountries, deleteCountry } = countryStore; const navigate = useNavigate(); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [rowId, setRowId] = useState(null); // Lifted state + const [rowId, setRowId] = useState(null); const { language } = languageStore; useEffect(() => { @@ -22,6 +22,17 @@ export const CountryListPage = observer(() => { field: "name", headerName: "Название", flex: 1, + renderCell: (params: GridRenderCellParams) => { + return ( +
+ {params.value ? ( + params.value + ) : ( + + )} +
+ ); + }, }, { field: "actions", @@ -37,9 +48,9 @@ export const CountryListPage = observer(() => { > - + */} - ))} + {controls.map((c) => { + // --- ИЗМЕНЕНО --- Определяем классы в зависимости от состояния + const isActive = + c.isActive !== undefined ? c.isActive : activeMode === c.mode; + const isDisabled = c.disabled; + + const buttonClasses = `flex items-center px-3 py-2 rounded-md transition-all duration-200 text-xs sm:text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-blue-500 focus:ring-opacity-75 ${ + isDisabled + ? "bg-gray-200 text-gray-400 cursor-not-allowed" + : isActive + ? "bg-blue-600 text-white shadow-md hover:bg-blue-700" + : "bg-gray-100 text-gray-700 hover:bg-blue-100 hover:text-blue-700" + }`; + + return ( + + ); + })}
); }; @@ -1145,24 +1321,34 @@ interface MapSightbarProps { mapService: MapService | null; mapFeatures: Feature[]; selectedFeature: Feature | null; + selectedIds: Set; + setSelectedIds: (ids: Set) => void; + activeSection: string | null; + setActiveSection: (section: string | null) => void; } const MapSightbar: React.FC = ({ mapService, mapFeatures, selectedFeature, + selectedIds, + setSelectedIds, + activeSection, + setActiveSection, }) => { - const [activeSection, setActiveSection] = useState("layers"); const navigate = useNavigate(); const [isLoading, setIsLoading] = useState(false); - // --- НОВОЕ --- - // Состояние для хранения ID объектов, выбранных для удаления - const [selectedForDeletion, setSelectedForDeletion] = useState< - Set - >(new Set()); + const [searchQuery, setSearchQuery] = useState(""); - const toggleSection = (id: string) => - setActiveSection(activeSection === id ? null : id); + const filteredFeatures = useMemo(() => { + if (!searchQuery.trim()) { + return mapFeatures; + } + return mapFeatures.filter((feature) => { + const name = (feature.get("name") as string) || ""; + return name.toLowerCase().includes(searchQuery.toLowerCase()); + }); + }, [mapFeatures, searchQuery]); const handleFeatureClick = useCallback( (id: string | number | undefined) => { @@ -1184,39 +1370,33 @@ const MapSightbar: React.FC = ({ [mapService] ); - // --- НОВОЕ --- - // Обработчик изменения состояния чекбокса const handleCheckboxChange = useCallback( (id: string | number | undefined) => { if (id === undefined) return; - setSelectedForDeletion((prev) => { - const newSet = new Set(prev); - if (newSet.has(id)) { - newSet.delete(id); - } else { - newSet.add(id); - } - return newSet; - }); + const newSet = new Set(selectedIds); + if (newSet.has(id)) { + newSet.delete(id); + } else { + newSet.add(id); + } + setSelectedIds(newSet); }, - [] + [selectedIds, setSelectedIds] ); - // --- НОВОЕ --- - // Обработчик для запуска множественного удаления const handleBulkDelete = useCallback(() => { - if (!mapService || selectedForDeletion.size === 0) return; + if (!mapService || selectedIds.size === 0) return; if ( window.confirm( - `Вы уверены, что хотите удалить ${selectedForDeletion.size} объект(ов)? Это действие нельзя отменить.` + `Вы уверены, что хотите удалить ${selectedIds.size} объект(ов)? Это действие нельзя отменить.` ) ) { - const idsToDelete = Array.from(selectedForDeletion); + const idsToDelete = Array.from(selectedIds); mapService.deleteMultipleFeatures(idsToDelete); - setSelectedForDeletion(new Set()); // Очищаем выбор после удаления + setSelectedIds(new Set()); } - }, [mapService, selectedForDeletion]); + }, [mapService, selectedIds, setSelectedIds]); const handleEditFeature = useCallback( (featureType: string | undefined, fullId: string | number | undefined) => { @@ -1243,51 +1423,83 @@ const MapSightbar: React.FC = ({ setIsLoading(false); }, [mapService]); - const stations = mapFeatures.filter( + function sortFeatures( + features: Feature[], + selectedIds: Set, + selectedFeature: Feature | null + ) { + const selectedId = selectedFeature?.getId(); + return features.slice().sort((a, b) => { + const aId = a.getId(); + const bId = b.getId(); + if (selectedId && aId === selectedId) return -1; + if (selectedId && bId === selectedId) return 1; + const aSelected = selectedIds.has(aId!); + const bSelected = selectedIds.has(bId!); + if (aSelected && !bSelected) return -1; + if (!aSelected && bSelected) return 1; + const aName = (a.get("name") as string) || ""; + const bName = (b.get("name") as string) || ""; + return aName.localeCompare(bName, "ru"); + }); + } + + const toggleSection = (id: string) => + setActiveSection(activeSection === id ? null : id); + + const stations = (filteredFeatures || []).filter( (f) => f.get("featureType") === "station" || (f.getGeometry()?.getType() === "Point" && !f.get("featureType")) ); - const lines = mapFeatures.filter( + const lines = (filteredFeatures || []).filter( (f) => f.get("featureType") === "route" || (f.getGeometry()?.getType() === "LineString" && !f.get("featureType")) ); - const sights = mapFeatures.filter((f) => f.get("featureType") === "sight"); + const sights = (filteredFeatures || []).filter( + (f) => f.get("featureType") === "sight" + ); + + const sortedStations = sortFeatures(stations, selectedIds, selectedFeature); + const sortedLines = sortFeatures(lines, selectedIds, selectedFeature); + const sortedSights = sortFeatures(sights, selectedIds, selectedFeature); interface SidebarSection { id: string; title: string; icon: ReactNode; content: ReactNode; + count: number; } const sections: SidebarSection[] = [ { id: "layers", - title: `Остановки (${stations.length})`, + title: `Остановки (${sortedStations.length})`, icon: , + count: sortedStations.length, content: (
- {stations.length > 0 ? ( - stations.map((s) => { + {sortedStations.length > 0 ? ( + sortedStations.map((s) => { const sId = s.getId(); const sName = (s.get("name") as string) || "Без названия"; const isSelected = selectedFeature?.getId() === sId; - // --- ИЗМЕНЕНИЕ --- const isCheckedForDeletion = - sId !== undefined && selectedForDeletion.has(sId); + sId !== undefined && selectedIds.has(sId); return (
- {/* --- НОВОЕ: Чекбокс для множественного выбора --- */}
= ({ }, { id: "lines", - title: `Маршруты (${lines.length})`, + title: `Маршруты (${sortedLines.length})`, icon: , + count: sortedLines.length, content: (
- {lines.length > 0 ? ( - lines.map((l) => { + {sortedLines.length > 0 ? ( + sortedLines.map((l) => { const lId = l.getId(); const lName = (l.get("name") as string) || "Без названия"; const isSelected = selectedFeature?.getId() === lId; const isCheckedForDeletion = - lId !== undefined && selectedForDeletion.has(lId); + lId !== undefined && selectedIds.has(lId); const lGeom = l.getGeometry(); let lineLengthText: string | null = null; if (lGeom instanceof LineString) { @@ -1378,6 +1591,8 @@ const MapSightbar: React.FC = ({ return (
= ({ }, { id: "sights", - title: `Достопримечательности (${sights.length})`, + title: `Достопримечательности (${sortedSights.length})`, icon: , + count: sortedSights.length, content: (
- {sights.length > 0 ? ( - sights.map((s) => { + {sortedSights.length > 0 ? ( + sortedSights.map((s) => { const sId = s.getId(); const sName = (s.get("name") as string) || "Без названия"; const isSelected = selectedFeature?.getId() === sId; const isCheckedForDeletion = - sId !== undefined && selectedForDeletion.has(sId); + sId !== undefined && selectedIds.has(sId); return (
= ({ } return ( - // --- ИЗМЕНЕНИЕ: Реструктуризация для футера с кнопками --- -
+

Панель управления

+ +
+ setSearchQuery(e.target.value)} + className="w-full px-3 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+
- {sections.map((s) => ( -
- -
-
- {s.content} -
-
+ {filteredFeatures.length === 0 && searchQuery ? ( +
+ Ничего не найдено.
- ))} + ) : ( + sections.map( + (s) => + (s.count > 0 || !searchQuery) && ( +
+ +
+
+ {s.content} +
+
+
+ ) + ) + )}
- {/* --- НОВОЕ: Футер сайдбара с кнопками действий --- */}
- {selectedForDeletion.size > 0 && ( + {selectedIds.size > 0 && ( )}
)} + {isLassoActive && ( +
+ Режим выделения области. Нарисуйте многоугольник для выбора + объектов. +
+ )}
{showContent && ( )} + + {/* Help button */} + + + {/* Help popup */} + {showHelp && ( +
+

Горячие клавиши:

+
    +
  • + + Shift + {" "} + - Режим выделения области (лассо) +
  • +
  • + + Ctrl + клик + {" "} + - Добавить объект к выбранным +
  • +
  • + Esc{" "} + - Отменить выделение +
  • +
  • + + Ctrl+Z + {" "} + - Отменить последнее действие +
  • +
  • + + Ctrl+Y + {" "} + - Повторить отменённое действие +
  • +
+ +
+ )}
{showContent && ( )}
diff --git a/src/pages/MapPage/mapStore.ts b/src/pages/MapPage/mapStore.ts index 430d574..aef8893 100644 --- a/src/pages/MapPage/mapStore.ts +++ b/src/pages/MapPage/mapStore.ts @@ -54,20 +54,15 @@ class MapStore { sights: ApiSight[] = []; getRoutes = async () => { - const routes = await languageInstance("ru").get("/route"); - const routedIds = routes.data.map((route: any) => route.id); - const mappedRoutes: ApiRoute[] = []; - for (const routeId of routedIds) { - const responseSoloRoute = await languageInstance("ru").get( - `/route/${routeId}` - ); - const route = responseSoloRoute.data; - mappedRoutes.push({ - id: route.id, - route_number: route.route_number, - path: route.path, - }); - } + // ИСПРАВЛЕНО: Проблема N+1. + // Вместо цикла и множества запросов теперь выполняется один. + // Бэкенд по эндпоинту `/route` должен возвращать массив полных объектов маршрутов. + const response = await languageInstance("ru").get("/route"); + const mappedRoutes: ApiRoute[] = response.data.map((route: any) => ({ + id: route.id, + route_number: route.route_number, + path: route.path, + })); this.routes = mappedRoutes.sort((a, b) => a.route_number.localeCompare(b.route_number) ); diff --git a/src/pages/Media/MediaListPage/index.tsx b/src/pages/Media/MediaListPage/index.tsx index af81a21..cf3b3e6 100644 --- a/src/pages/Media/MediaListPage/index.tsx +++ b/src/pages/Media/MediaListPage/index.tsx @@ -2,7 +2,7 @@ import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; import { languageStore, MEDIA_TYPE_LABELS, mediaStore } from "@shared"; import { useEffect, useState } from "react"; import { observer } from "mobx-react-lite"; -import { Eye, Trash2 } from "lucide-react"; +import { Eye, Trash2, Minus } from "lucide-react"; import { useNavigate } from "react-router-dom"; import { CreateButton, DeleteModal } from "@widgets"; @@ -10,7 +10,9 @@ export const MediaListPage = observer(() => { const { media, getMedia, deleteMedia } = mediaStore; const navigate = useNavigate(); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [rowId, setRowId] = useState(null); // Lifted state + const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false); + const [rowId, setRowId] = useState(null); + const [ids, setIds] = useState([]); const { language } = languageStore; useEffect(() => { @@ -22,6 +24,17 @@ export const MediaListPage = observer(() => { field: "media_name", headerName: "Название", flex: 1, + renderCell: (params: GridRenderCellParams) => { + return ( +
+ {params.value ? ( + params.value + ) : ( + + )} +
+ ); + }, }, { field: "media_type", @@ -30,13 +43,15 @@ export const MediaListPage = observer(() => { flex: 1, renderCell: (params: GridRenderCellParams) => { return ( -

- { +

+ {params.value ? ( MEDIA_TYPE_LABELS[ params.row.media_type as keyof typeof MEDIA_TYPE_LABELS ] - } -

+ ) : ( + + )} +
); }, }, @@ -80,10 +95,28 @@ export const MediaListPage = observer(() => {

Медиа

+ +
0 ? 1 : 0 }} + > + +
+ { + setIds(Array.from(newSelection.ids) as string[]); + }} hideFooter />
@@ -103,6 +136,19 @@ export const MediaListPage = observer(() => { setRowId(null); }} /> + + { + await Promise.all(ids.map((id) => deleteMedia(id))); + getMedia(); + setIsBulkDeleteModalOpen(false); + setIds([]); + }} + onCancel={() => { + setIsBulkDeleteModalOpen(false); + }} + /> ); }); diff --git a/src/pages/Route/RouteCreatePage/index.tsx b/src/pages/Route/RouteCreatePage/index.tsx index b77ed92..6d8c599 100644 --- a/src/pages/Route/RouteCreatePage/index.tsx +++ b/src/pages/Route/RouteCreatePage/index.tsx @@ -9,7 +9,7 @@ import { Typography, Box, } from "@mui/material"; - +import { LanguageSwitcher } from "@widgets"; import { observer } from "mobx-react-lite"; import { ArrowLeft, Loader2, Save } from "lucide-react"; import { useEffect, useState } from "react"; @@ -93,134 +93,135 @@ export const RouteCreatePage = observer(() => { return ( -
+ +
- - Создать маршрут - - - - Выберите перевозчика - setCarrier(e.target.value as string)} + disabled={carrierStore.carriers.data.length === 0} + > + Не выбрано + {carrierStore.carriers.data.map( + (c: (typeof carrierStore.carriers.data)[number]) => ( + + {c.full_name} + + ) + )} + + + setRouteNumber(e.target.value)} + /> + setRouteCoords(e.target.value)} + /> + setGovRouteNumber(e.target.value)} + /> + + Обращение губернатора + + + + Прямой/обратный маршрут + + + setScaleMin(e.target.value)} + /> + setScaleMax(e.target.value)} + /> + setTurn(e.target.value)} + /> + setCenterLat(e.target.value)} + /> + setCenterLng(e.target.value)} + /> + +
+ + +
); diff --git a/src/pages/Route/RouteEditPage/index.tsx b/src/pages/Route/RouteEditPage/index.tsx index 887e010..19794c3 100644 --- a/src/pages/Route/RouteEditPage/index.tsx +++ b/src/pages/Route/RouteEditPage/index.tsx @@ -9,7 +9,7 @@ import { Typography, Box, } from "@mui/material"; - +import { LanguageSwitcher } from "@widgets"; import { observer } from "mobx-react-lite"; import { ArrowLeft, Save } from "lucide-react"; import { useEffect, useState } from "react"; @@ -45,180 +45,181 @@ export const RouteEditPage = observer(() => { return ( -
+ +
- - Редактировать маршрут - - - - Выберите перевозчика - + routeStore.setEditRouteData({ + carrier_id: Number(e.target.value), + carrier: + carrierStore.carriers.data.find( + (c) => c.id === Number(e.target.value) + )?.full_name || "", + }) + } + disabled={carrierStore.carriers.data.length === 0} + > + Не выбрано + {carrierStore.carriers.data.map( + (c: (typeof carrierStore.carriers.data)[number]) => ( + + {c.full_name} + + ) + )} + + + routeStore.setEditRouteData({ - carrier_id: Number(e.target.value), - carrier: - carrierStore.carriers.data.find( - (c) => c.id === Number(e.target.value) - )?.full_name || "", + route_number: e.target.value, }) } - disabled={carrierStore.carriers.data.length === 0} - > - Не выбрано - {carrierStore.carriers.data.map( - (c: (typeof carrierStore.carriers.data)[number]) => ( - - {c.full_name} - - ) - )} - - - - routeStore.setEditRouteData({ - route_number: e.target.value, - }) - } - /> - p.join(" ")).join("\n") || ""} - onChange={(e) => - routeStore.setEditRouteData({ - path: e.target.value - .split("\n") - .map((line) => line.split(" ").map(Number)), - }) - } - /> - - routeStore.setEditRouteData({ - route_sys_number: e.target.value, - }) - } - /> - - Обращение губернатора - - - - Прямой/обратный маршрут - + routeStore.setEditRouteData({ + governor_appeal: Number(e.target.value), + }) + } + disabled={articlesStore.articleList.ru.data.length === 0} + > + Не выбрано + {articlesStore.articleList.ru.data.map( + (a: (typeof articlesStore.articleList.ru.data)[number]) => ( + + {a.heading} + + ) + )} + + + + Прямой/обратный маршрут + + + + routeStore.setEditRouteData({ + scale_min: Number(e.target.value), + }) + } + /> + + routeStore.setEditRouteData({ + scale_max: Number(e.target.value), + }) + } + /> + + routeStore.setEditRouteData({ + rotate: Number(e.target.value), + }) + } + /> + + routeStore.setEditRouteData({ + center_latitude: Number(e.target.value), + }) + } + /> + + routeStore.setEditRouteData({ + center_longitude: Number(e.target.value), + }) + } + /> + +
+ + Сохранить + +
); diff --git a/src/pages/Route/RouteListPage/index.tsx b/src/pages/Route/RouteListPage/index.tsx index c0f4e2c..50fc58d 100644 --- a/src/pages/Route/RouteListPage/index.tsx +++ b/src/pages/Route/RouteListPage/index.tsx @@ -2,15 +2,18 @@ import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; import { languageStore, routeStore } from "@shared"; import { useEffect, useState } from "react"; import { observer } from "mobx-react-lite"; -import { Map, Pencil, Trash2 } from "lucide-react"; +import { Map, Pencil, Trash2, Minus } from "lucide-react"; import { useNavigate } from "react-router-dom"; import { CreateButton, DeleteModal } from "@widgets"; +import { LanguageSwitcher } from "@widgets"; export const RouteListPage = observer(() => { const { routes, getRoutes, deleteRoute } = routeStore; const navigate = useNavigate(); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [rowId, setRowId] = useState(null); // Lifted state + const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false); + const [rowId, setRowId] = useState(null); + const [ids, setIds] = useState([]); const { language } = languageStore; useEffect(() => { @@ -22,11 +25,33 @@ export const RouteListPage = observer(() => { field: "carrier", headerName: "Перевозчик", width: 250, + renderCell: (params: GridRenderCellParams) => { + return ( +
+ {params.value ? ( + params.value + ) : ( + + )} +
+ ); + }, }, { field: "route_number", headerName: "Номер маршрута", flex: 1, + renderCell: (params: GridRenderCellParams) => { + return ( +
+ {params.value ? ( + params.value + ) : ( + + )} +
+ ); + }, }, { field: "route_direction", @@ -87,15 +112,35 @@ export const RouteListPage = observer(() => { return ( <> + +

Маршруты

+ +
0 ? 1 : 0 }} + > + +
+ { + setIds(Array.from(newSelection.ids) as number[]); + }} hideFooter />
@@ -114,6 +159,19 @@ export const RouteListPage = observer(() => { setRowId(null); }} /> + + { + await Promise.all(ids.map((id) => deleteRoute(id))); + getRoutes(); + setIsBulkDeleteModalOpen(false); + setIds([]); + }} + onCancel={() => { + setIsBulkDeleteModalOpen(false); + }} + /> ); }); diff --git a/src/pages/Sight/SightListPage/index.tsx b/src/pages/Sight/SightListPage/index.tsx index bdd928f..486bd13 100644 --- a/src/pages/Sight/SightListPage/index.tsx +++ b/src/pages/Sight/SightListPage/index.tsx @@ -2,7 +2,7 @@ import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; import { languageStore, sightsStore } from "@shared"; import { useEffect, useState } from "react"; import { observer } from "mobx-react-lite"; -import { Pencil, Trash2 } from "lucide-react"; +import { Pencil, Trash2, Minus } from "lucide-react"; import { useNavigate } from "react-router-dom"; import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets"; @@ -10,7 +10,9 @@ export const SightListPage = observer(() => { const { sights, getSights, deleteListSight } = sightsStore; const navigate = useNavigate(); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [rowId, setRowId] = useState(null); // Lifted state + const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false); + const [rowId, setRowId] = useState(null); + const [ids, setIds] = useState([]); const { language } = languageStore; useEffect(() => { @@ -22,13 +24,34 @@ export const SightListPage = observer(() => { field: "name", headerName: "Имя", flex: 1, + renderCell: (params: GridRenderCellParams) => { + return ( +
+ {params.value ? ( + params.value + ) : ( + + )} +
+ ); + }, }, { field: "city", headerName: "Город", flex: 1, + renderCell: (params: GridRenderCellParams) => { + return ( +
+ {params.value ? ( + params.value + ) : ( + + )} +
+ ); + }, }, - { field: "actions", headerName: "Действия", @@ -76,10 +99,28 @@ export const SightListPage = observer(() => { path="/sight/create" />
+ +
0 ? 1 : 0 }} + > + +
+ { + setIds(Array.from(newSelection.ids) as number[]); + }} hideFooter />
@@ -98,6 +139,19 @@ export const SightListPage = observer(() => { setRowId(null); }} /> + + { + await Promise.all(ids.map((id) => deleteListSight(id))); + getSights(); + setIsBulkDeleteModalOpen(false); + setIds([]); + }} + onCancel={() => { + setIsBulkDeleteModalOpen(false); + }} + /> ); }); diff --git a/src/pages/Station/StationCreatePage/index.tsx b/src/pages/Station/StationCreatePage/index.tsx index e2db4e4..890f231 100644 --- a/src/pages/Station/StationCreatePage/index.tsx +++ b/src/pages/Station/StationCreatePage/index.tsx @@ -13,6 +13,7 @@ import { useNavigate } from "react-router-dom"; import { toast } from "react-toastify"; import { stationsStore } from "@shared"; import { useState } from "react"; +import { LanguageSwitcher } from "@widgets"; export const StationCreatePage = observer(() => { const navigate = useNavigate(); @@ -36,7 +37,8 @@ export const StationCreatePage = observer(() => { return ( -
+ +
-

Создание станции

+
+

Создание станции

+
{ const stationId = Number(id); await getEditStation(stationId); - await getCities(language); + await getCities("ru"); + await getCities("en"); + await getCities("zh"); }; fetchAndSetStationData(); - }, [id, language]); + }, [id]); return ( @@ -69,6 +71,9 @@ export const StationEditPage = observer(() => {
+
+

{editStationData.ru.name}

+
{ value={editStationData.common.city_id || ""} label="Город" onChange={(e) => { - const selectedCity = cities[language].find( + const selectedCity = cities[language].data.find( (city) => city.id === e.target.value ); setEditCommonData({ @@ -150,7 +155,7 @@ export const StationEditPage = observer(() => { }); }} > - {cities[language].map((city) => ( + {cities[language].data.map((city) => ( {city.name} diff --git a/src/pages/Station/StationListPage/index.tsx b/src/pages/Station/StationListPage/index.tsx index 8bb3f31..149209f 100644 --- a/src/pages/Station/StationListPage/index.tsx +++ b/src/pages/Station/StationListPage/index.tsx @@ -2,7 +2,7 @@ import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; import { languageStore, stationsStore } from "@shared"; import { useEffect, useState } from "react"; import { observer } from "mobx-react-lite"; -import { Eye, Pencil, Trash2 } from "lucide-react"; +import { Eye, Pencil, Trash2, Minus } from "lucide-react"; import { useNavigate } from "react-router-dom"; import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets"; @@ -10,7 +10,9 @@ export const StationListPage = observer(() => { const { stationLists, getStationList, deleteStation } = stationsStore; const navigate = useNavigate(); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [rowId, setRowId] = useState(null); // Lifted state + const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false); + const [rowId, setRowId] = useState(null); + const [ids, setIds] = useState([]); const { language } = languageStore; useEffect(() => { @@ -22,11 +24,33 @@ export const StationListPage = observer(() => { field: "name", headerName: "Название", flex: 1, + renderCell: (params: GridRenderCellParams) => { + return ( +
+ {params.value ? ( + params.value + ) : ( + + )} +
+ ); + }, }, { field: "system_name", headerName: "Системное название", flex: 1, + renderCell: (params: GridRenderCellParams) => { + return ( +
+ {params.value ? ( + params.value + ) : ( + + )} +
+ ); + }, }, { field: "direction", @@ -88,15 +112,33 @@ export const StationListPage = observer(() => { <> -
+

Станции

+ +
0 ? 1 : 0 }} + > + +
+ { + setIds(Array.from(newSelection.ids) as number[]); + }} hideFooter />
@@ -115,6 +157,19 @@ export const StationListPage = observer(() => { setRowId(null); }} /> + + { + await Promise.all(ids.map((id) => deleteStation(id))); + getStationList(); + setIsBulkDeleteModalOpen(false); + setIds([]); + }} + onCancel={() => { + setIsBulkDeleteModalOpen(false); + }} + /> ); }); diff --git a/src/pages/User/UserListPage/index.tsx b/src/pages/User/UserListPage/index.tsx index 9dc0579..307cc48 100644 --- a/src/pages/User/UserListPage/index.tsx +++ b/src/pages/User/UserListPage/index.tsx @@ -2,7 +2,7 @@ import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; import { userStore } from "@shared"; import { useEffect, useState } from "react"; import { observer } from "mobx-react-lite"; -import { Pencil, Trash2 } from "lucide-react"; +import { Pencil, Trash2, Minus } from "lucide-react"; import { useNavigate } from "react-router-dom"; import { CreateButton, DeleteModal } from "@widgets"; @@ -11,7 +11,9 @@ export const UserListPage = observer(() => { const { users, getUsers, deleteUser } = userStore; const navigate = useNavigate(); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [rowId, setRowId] = useState(null); // Lifted state + const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false); + const [rowId, setRowId] = useState(null); + const [ids, setIds] = useState([]); useEffect(() => { getUsers(); @@ -22,11 +24,33 @@ export const UserListPage = observer(() => { field: "name", headerName: "Имя", width: 400, + renderCell: (params: GridRenderCellParams) => { + return ( +
+ {params.value ? ( + params.value + ) : ( + + )} +
+ ); + }, }, { field: "email", headerName: "Email", width: 400, + renderCell: (params: GridRenderCellParams) => { + return ( +
+ {params.value ? ( + params.value + ) : ( + + )} +
+ ); + }, }, { field: "is_admin", @@ -93,10 +117,28 @@ export const UserListPage = observer(() => {

Пользователи

+ +
0 ? 1 : 0 }} + > + +
+ { + setIds(Array.from(newSelection.ids) as number[]); + }} hideFooter />
@@ -107,7 +149,6 @@ export const UserListPage = observer(() => { if (rowId) { await deleteUser(rowId); } - setIsDeleteModalOpen(false); setRowId(null); }} @@ -116,6 +157,19 @@ export const UserListPage = observer(() => { setRowId(null); }} /> + + { + await Promise.all(ids.map((id) => deleteUser(id))); + getUsers(); + setIsBulkDeleteModalOpen(false); + setIds([]); + }} + onCancel={() => { + setIsBulkDeleteModalOpen(false); + }} + /> ); }); diff --git a/src/pages/Vehicle/VehicleListPage/index.tsx b/src/pages/Vehicle/VehicleListPage/index.tsx index 8e4589c..1d56929 100644 --- a/src/pages/Vehicle/VehicleListPage/index.tsx +++ b/src/pages/Vehicle/VehicleListPage/index.tsx @@ -2,7 +2,7 @@ import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; import { carrierStore, languageStore, vehicleStore } from "@shared"; import { useEffect, useState } from "react"; import { observer } from "mobx-react-lite"; -import { Eye, Pencil, Trash2 } from "lucide-react"; +import { Eye, Pencil, Trash2, Minus } from "lucide-react"; import { useNavigate } from "react-router-dom"; import { CreateButton, DeleteModal } from "@widgets"; import { VEHICLE_TYPES } from "@shared"; @@ -12,7 +12,9 @@ export const VehicleListPage = observer(() => { const { carriers, getCarriers } = carrierStore; const navigate = useNavigate(); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false); const [rowId, setRowId] = useState(null); + const [ids, setIds] = useState([]); const { language } = languageStore; useEffect(() => { @@ -25,17 +27,31 @@ export const VehicleListPage = observer(() => { field: "tail_number", headerName: "Бортовой номер", flex: 1, + renderCell: (params: GridRenderCellParams) => { + return ( +
+ {params.value ? ( + params.value + ) : ( + + )} +
+ ); + }, }, { field: "type", headerName: "Тип", flex: 1, - renderCell: (params: GridRenderCellParams) => { return ( -
- {VEHICLE_TYPES.find((type) => type.value === params.row.type) - ?.label || params.row.type} +
+ {params.value ? ( + VEHICLE_TYPES.find((type) => type.value === params.row.type) + ?.label || params.row.type + ) : ( + + )}
); }, @@ -44,13 +60,34 @@ export const VehicleListPage = observer(() => { field: "carrier", headerName: "Перевозчик", flex: 1, + renderCell: (params: GridRenderCellParams) => { + return ( +
+ {params.value ? ( + params.value + ) : ( + + )} +
+ ); + }, }, { field: "city", headerName: "Город", flex: 1, + renderCell: (params: GridRenderCellParams) => { + return ( +
+ {params.value ? ( + params.value + ) : ( + + )} +
+ ); + }, }, - { field: "actions", headerName: "Действия", @@ -101,10 +138,28 @@ export const VehicleListPage = observer(() => { path="/vehicle/create" />
+ +
0 ? 1 : 0 }} + > + +
+ { + setIds(Array.from(newSelection.ids) as number[]); + }} hideFooter />
@@ -123,6 +178,19 @@ export const VehicleListPage = observer(() => { setRowId(null); }} /> + + { + await Promise.all(ids.map((id) => deleteVehicle(id))); + getVehicles(); + setIsBulkDeleteModalOpen(false); + setIds([]); + }} + onCancel={() => { + setIsBulkDeleteModalOpen(false); + }} + /> ); }); diff --git a/src/shared/config/CarrierSvg.tsx b/src/shared/config/CarrierSvg.tsx new file mode 100644 index 0000000..6d4facc --- /dev/null +++ b/src/shared/config/CarrierSvg.tsx @@ -0,0 +1,62 @@ +export const CarrierSvg = () => { + return ( + + + + + + + + + + ); +}; diff --git a/src/shared/config/constants.tsx b/src/shared/config/constants.tsx index e92abb6..938ba7c 100644 --- a/src/shared/config/constants.tsx +++ b/src/shared/config/constants.tsx @@ -7,22 +7,24 @@ import { Users, Earth, Landmark, - BusFront, GitBranch, Car, Table, + Notebook, Split, Newspaper, PersonStanding, Cpu, BookImage, } from "lucide-react"; +import { CarrierSvg } from "./CarrierSvg"; + export const DRAWER_WIDTH = 300; interface NavigationItem { id: string; label: string; - icon: LucideIcon; + icon?: LucideIcon | React.ReactNode; path?: string; onClick?: () => void; nestedItems?: NavigationItem[]; @@ -34,43 +36,6 @@ export const NAVIGATION_ITEMS: { secondary: NavigationItem[]; } = { primary: [ - { - id: "countries", - label: "Страны", - icon: Earth, - path: "/country", - }, - { - id: "cities", - label: "Города", - icon: Building2, - path: "/city", - }, - { - id: "carriers", - label: "Перевозчики", - icon: BusFront, - path: "/carrier", - }, - - { - id: "snapshots", - label: "Снапшоты", - icon: GitBranch, - path: "/snapshot", - }, - { - id: "map", - label: "Карта", - icon: Map, - path: "/map", - }, - { - id: "devices", - label: "Устройства", - icon: Cpu, - path: "/devices", - }, { id: "all", label: "Все сущности", @@ -106,15 +71,58 @@ export const NAVIGATION_ITEMS: { icon: Split, path: "/route", }, + { + id: "reference", + label: "Справочник", + icon: Notebook, + nestedItems: [ + { + id: "countries", + label: "Страны", + icon: Earth, + path: "/country", + }, + { + id: "cities", + label: "Города", + icon: Building2, + path: "/city", + }, + { + id: "carriers", + label: "Перевозчики", + // @ts-ignore + icon: CarrierSvg, + path: "/carrier", + }, + ], + }, ], }, - { - id: "vehicles", - label: "Транспорт", - icon: Car, - path: "/vehicle", + id: "snapshots", + label: "Снапшоты", + icon: GitBranch, + path: "/snapshot", }, + { + id: "map", + label: "Карта", + icon: Map, + path: "/map", + }, + { + id: "devices", + label: "Устройства", + icon: Cpu, + path: "/devices", + }, + // { + // id: "vehicles", + // label: "Транспорт", + // icon: Car, + // path: "/vehicle", + // }, { id: "users", label: "Пользователи", diff --git a/src/shared/modals/PreviewMediaDialog/index.tsx b/src/shared/modals/PreviewMediaDialog/index.tsx index a7f96b7..955fe96 100644 --- a/src/shared/modals/PreviewMediaDialog/index.tsx +++ b/src/shared/modals/PreviewMediaDialog/index.tsx @@ -120,7 +120,6 @@ export const PreviewMediaDialog = observer( disabled={isLoading} /> - - + diff --git a/src/shared/modals/UploadMediaDialog/index.tsx b/src/shared/modals/UploadMediaDialog/index.tsx index b5678ab..ed58828 100644 --- a/src/shared/modals/UploadMediaDialog/index.tsx +++ b/src/shared/modals/UploadMediaDialog/index.tsx @@ -188,7 +188,7 @@ export const UploadMediaDialog = observer( - + {/* { + getArticle = async (id: number, language?: Language) => { this.articleLoading = true; - const response = await authInstance.get(`/article/${id}`); - - runInAction(() => { - this.articleData = response.data; - }); + if (language) { + const response = await languageInstance(language).get(`/article/${id}`); + runInAction(() => { + if (!this.articleData) { + this.articleData = { id, heading: "", body: "", service_name: "" }; + } + this.articleData[language] = { + heading: response.data.heading, + body: response.data.body, + }; + }); + } else { + const response = await authInstance.get(`/article/${id}`); + runInAction(() => { + this.articleData = response.data; + }); + } this.articleLoading = false; }; @@ -137,6 +167,20 @@ class ArticlesStore { } return null; }); + + deleteArticles = async (ids: number[]) => { + for (const id of ids) { + await authInstance.delete(`/article/${id}`); + } + + for (const id of ["ru", "en", "zh"] as Language[]) { + runInAction(() => { + this.articleList[id].data = this.articleList[id].data.filter( + (article) => !ids.includes(article.id) + ); + }); + } + }; } export const articlesStore = new ArticlesStore(); diff --git a/src/shared/store/CarrierStore/index.tsx b/src/shared/store/CarrierStore/index.tsx index 5131af4..982cb1c 100644 --- a/src/shared/store/CarrierStore/index.tsx +++ b/src/shared/store/CarrierStore/index.tsx @@ -1,4 +1,4 @@ -import { authInstance } from "@shared"; +import { authInstance, cityStore, languageStore } from "@shared"; import { makeAutoObservable, runInAction } from "mobx"; export type Carrier = { @@ -9,9 +9,9 @@ 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; }; type Carriers = { @@ -68,9 +68,9 @@ class CarrierStore { city: "", city_id: 0, logo: "", - main_color: "", - left_color: "", - right_color: "", + // main_color: "", + // left_color: "", + // right_color: "", }; } this.carrier[id] = response.data; @@ -81,22 +81,22 @@ class CarrierStore { createCarrier = async ( fullName: string, shortName: string, - city: string, + cityId: number, - main_color: string, - left_color: string, - right_color: string, + // main_color: string, + // left_color: string, + // right_color: string, slogan: string, logoId: string ) => { const response = await authInstance.post("/carrier", { full_name: fullName, short_name: shortName, - city, + city: "", city_id: cityId, - main_color, - left_color, - right_color, + // main_color, + // left_color, + // right_color, slogan, logo: logoId, }); @@ -108,11 +108,11 @@ class CarrierStore { editCarrierData = { full_name: "", short_name: "", - city: "", + city_id: 0, - main_color: "", - left_color: "", - right_color: "", + // main_color: "", + // left_color: "", + // right_color: "", slogan: "", logo: "", }; @@ -120,32 +120,35 @@ class CarrierStore { setEditCarrierData = ( fullName: string, shortName: string, - city: string, + cityId: number, - main_color: string, - left_color: string, - right_color: string, + // main_color: string, + // left_color: string, + // right_color: string, slogan: string, logoId: string ) => { this.editCarrierData = { full_name: fullName, short_name: shortName, - city, + city_id: cityId, - main_color: main_color, - left_color: left_color, - right_color: right_color, + // main_color: main_color, + // left_color: left_color, + // right_color: right_color, slogan: slogan, logo: logoId, }; }; editCarrier = async (id: number) => { - const response = await authInstance.patch( - `/carrier/${id}`, - this.editCarrierData - ); + const { language } = languageStore; + const response = await authInstance.patch(`/carrier/${id}`, { + ...this.editCarrierData, + city: cityStore.cities[language].data.find( + (city) => city.id === this.editCarrierData.city_id + )?.name, + }); runInAction(() => { this.carriers.data = this.carriers.data.map((carrier) => diff --git a/src/shared/store/CityStore/index.ts b/src/shared/store/CityStore/index.ts index 5bce254..27ef73b 100644 --- a/src/shared/store/CityStore/index.ts +++ b/src/shared/store/CityStore/index.ts @@ -4,6 +4,7 @@ import { Language, languageStore, countryStore, + CashedCountries, } from "@shared"; import { makeAutoObservable, runInAction } from "mobx"; @@ -16,9 +17,18 @@ export type City = { }; export type CashedCities = { - ru: City[]; - en: City[]; - zh: City[]; + ru: { + data: City[]; + loaded: boolean; + }; + en: { + data: City[]; + loaded: boolean; + }; + zh: { + data: City[]; + loaded: boolean; + }; }; export type CashedCity = { @@ -29,9 +39,18 @@ export type CashedCity = { class CityStore { cities: CashedCities = { - ru: [], - en: [], - zh: [], + ru: { + data: [], + loaded: false, + }, + en: { + data: [], + loaded: false, + }, + zh: { + data: [], + loaded: false, + }, }; city: Record = {}; @@ -40,25 +59,37 @@ class CityStore { makeAutoObservable(this); } - ruCities: City[] = []; + ruCities: { + data: City[]; + loaded: boolean; + } = { + data: [], + loaded: false, + }; getRuCities = async () => { + if (this.ruCities.loaded) { + return; + } + const response = await languageInstance("ru").get(`/city`); runInAction(() => { - this.ruCities = response.data; + this.ruCities.data = response.data; + this.ruCities.loaded = true; }); }; getCities = async (language: keyof CashedCities) => { - if (this.cities[language] && this.cities[language].length > 0) { + if (this.cities[language].loaded) { return; } const response = await authInstance.get(`/city`); runInAction(() => { - this.cities[language] = response.data; + this.cities[language].data = response.data; + this.cities[language].loaded = true; }); }; @@ -83,19 +114,22 @@ class CityStore { return response.data; }; - deleteCity = async (code: string, language: keyof CashedCities) => { + deleteCity = async (code: string) => { await authInstance.delete(`/city/${code}`); runInAction(() => { - this.cities[language] = this.cities[language].filter( - (city) => city.country_code !== code - ); - this.city[code][language] = null; + for (const secondaryLanguage of ["ru", "en", "zh"] as Language[]) { + this.cities[secondaryLanguage].data = this.cities[ + secondaryLanguage + ].data.filter((city) => city.id !== Number(code)); + if (this.city[code]) { + this.city[code][secondaryLanguage] = null; + } + } }); }; createCityData = { - country: "", country_code: "", arms: "", ru: { @@ -111,14 +145,12 @@ class CityStore { setCreateCityData = ( name: string, - country: string, country_code: string, arms: string, language: keyof CashedCities ) => { this.createCityData = { ...this.createCityData, - country: country, country_code: country_code, arms: arms, [language]: { @@ -127,73 +159,84 @@ class CityStore { }; }; - createCity = async () => { - const { language } = languageStore; - const { country, country_code, arms } = this.createCityData; - const { name } = this.createCityData[language as keyof CashedCities]; + async createCity() { + const language = languageStore.language as Language; + const { country_code, arms } = this.createCityData; + const { name } = this.createCityData[language]; - if (name && country && country_code && arms) { - const cityResponse = await languageInstance(language as Language).post( - "/city", - { - name: name, - country: country, - country_code: country_code, - arms: arms, - } - ); + if (!name || !country_code) { + return; + } - runInAction(() => { - this.cities[language as keyof CashedCities] = [ - ...this.cities[language as keyof CashedCities], - cityResponse.data, - ]; + try { + // Create city in primary language + const cityResponse = await languageInstance(language).post("/city", { + name, + country: + countryStore.countries[language as keyof CashedCountries]?.data.find( + (c) => c.code === country_code + )?.name || "", + country_code, + arms: arms || "", }); - for (const secondaryLanguage of ["ru", "en", "zh"].filter( + const cityId = cityResponse.data.id; + + // Create/update other language versions + for (const secondaryLanguage of (["ru", "en", "zh"] as Language[]).filter( (l) => l !== language )) { - const { name } = - this.createCityData[secondaryLanguage as keyof CashedCities]; + const { name: secondaryName } = this.createCityData[secondaryLanguage]; - const patchResponse = await languageInstance( - secondaryLanguage as Language - ).patch(`/city/${cityResponse.data.id}`, { - name: name, - country: country, - country_code: country_code, - arms: arms, - }); + // Get country name in secondary language + const countryName = + countryStore.countries[secondaryLanguage]?.data.find( + (c) => c.code === country_code + )?.name || ""; + + const patchResponse = await languageInstance(secondaryLanguage).patch( + `/city/${cityId}`, + { + name: secondaryName || "", + country: countryName, + country_code: country_code || "", + arms: arms || "", + } + ); runInAction(() => { - this.cities[secondaryLanguage as keyof CashedCities] = [ - ...this.cities[secondaryLanguage as keyof CashedCities], + this.cities[secondaryLanguage].data = [ + ...this.cities[secondaryLanguage].data, patchResponse.data, ]; }); } - } - runInAction(() => { - this.createCityData = { - country: "", - country_code: "", - arms: "", - ru: { - name: "", - }, - en: { - name: "", - }, - zh: { - name: "", - }, - }; - }); - }; + // Update primary language data + runInAction(() => { + this.cities[language].data = [ + ...this.cities[language].data, + cityResponse.data, + ]; + }); + + // Reset form data + runInAction(() => { + this.createCityData = { + country_code: "", + arms: "", + ru: { name: "" }, + en: { name: "" }, + zh: { name: "" }, + }; + }); + } catch (error) { + console.error("Error creating city:", error); + throw error; + } + } editCityData = { - country: "", country_code: "", arms: "", ru: { @@ -209,14 +252,12 @@ class CityStore { setEditCityData = ( name: string, - country: string, country_code: string, arms: string, language: keyof CashedCities ) => { this.editCityData = { ...this.editCityData, - country: country, country_code: country_code, arms: arms, @@ -232,7 +273,7 @@ class CityStore { const { name } = this.editCityData[language as keyof CashedCities]; const { countries } = countryStore; - const country = countries[language as keyof CashedCities].find( + const country = countries[language as keyof CashedCities].data.find( (country) => country.code === country_code ); @@ -255,9 +296,9 @@ class CityStore { } if (this.cities[language as keyof CashedCities]) { - this.cities[language as keyof CashedCities] = this.cities[ + this.cities[language as keyof CashedCities].data = this.cities[ language as keyof CashedCities - ].map((city) => + ].data.map((city) => city.id === Number(code) ? { id: city.id, diff --git a/src/shared/store/CountryStore/index.ts b/src/shared/store/CountryStore/index.ts index bf7fa01..116c1a6 100644 --- a/src/shared/store/CountryStore/index.ts +++ b/src/shared/store/CountryStore/index.ts @@ -12,9 +12,18 @@ export type Country = { }; export type CashedCountries = { - ru: Country[]; - en: Country[]; - zh: Country[]; + ru: { + data: Country[]; + loaded: boolean; + }; + en: { + data: Country[]; + loaded: boolean; + }; + zh: { + data: Country[]; + loaded: boolean; + }; }; export type CashedCountry = { @@ -25,9 +34,18 @@ export type CashedCountry = { class CountryStore { countries: CashedCountries = { - ru: [], - en: [], - zh: [], + ru: { + data: [], + loaded: false, + }, + en: { + data: [], + loaded: false, + }, + zh: { + data: [], + loaded: false, + }, }; country: Record = {}; @@ -37,14 +55,15 @@ class CountryStore { } getCountries = async (language: keyof CashedCountries) => { - if (this.countries[language] && this.countries[language].length > 0) { + if (this.countries[language].loaded) { return; } - const response = await authInstance.get(`/country`); + const response = await languageInstance(language).get(`/country`); runInAction(() => { - this.countries[language] = response.data; + this.countries[language].data = response.data; + this.countries[language].loaded = true; }); }; @@ -76,10 +95,15 @@ class CountryStore { await authInstance.delete(`/country/${code}`); runInAction(() => { - this.countries[language] = this.countries[language].filter( + this.countries[language].data = this.countries[language].data.filter( (country) => country.code !== code ); - this.country[code][language] = null; + this.countries[language].loaded = true; + this.country[code] = { + ru: null, + en: null, + zh: null, + }; }); }; @@ -121,8 +145,8 @@ class CountryStore { }); runInAction(() => { - this.countries[language as keyof CashedCountries] = [ - ...this.countries[language as keyof CashedCountries], + this.countries[language as keyof CashedCountries].data = [ + ...this.countries[language as keyof CashedCountries].data, { code: code, name: name }, ]; }); @@ -142,8 +166,8 @@ class CountryStore { ); } runInAction(() => { - this.countries[secondaryLanguage as keyof CashedCountries] = [ - ...this.countries[secondaryLanguage as keyof CashedCountries], + this.countries[secondaryLanguage as keyof CashedCountries].data = [ + ...this.countries[secondaryLanguage as keyof CashedCountries].data, { code: code, name: name }, ]; }); @@ -204,11 +228,10 @@ class CountryStore { }; } if (this.countries[language as keyof CashedCountries]) { - this.countries[language as keyof CashedCountries] = this.countries[ - language as keyof CashedCountries - ].map((country) => - country.code === code ? { code, name } : country - ); + this.countries[language as keyof CashedCountries].data = + this.countries[language as keyof CashedCountries].data.map( + (country) => (country.code === code ? { code, name } : country) + ); } }); } diff --git a/src/widgets/MediaViewer/ThreeView.tsx b/src/widgets/MediaViewer/ThreeView.tsx index 12ab158..023a563 100644 --- a/src/widgets/MediaViewer/ThreeView.tsx +++ b/src/widgets/MediaViewer/ThreeView.tsx @@ -2,15 +2,20 @@ import { Canvas } from "@react-three/fiber"; import { OrbitControls, Stage, useGLTF } from "@react-three/drei"; type ModelViewerProps = { + width?: string; fileUrl: string; height?: string; }; -export const ThreeView = ({ fileUrl, height = "100%" }: ModelViewerProps) => { +export const ThreeView = ({ + fileUrl, + height = "100%", + width = "100%", +}: ModelViewerProps) => { const { scene } = useGLTF(fileUrl); return ( - + diff --git a/src/widgets/MediaViewer/index.tsx b/src/widgets/MediaViewer/index.tsx index 4db2b60..0d72487 100644 --- a/src/widgets/MediaViewer/index.tsx +++ b/src/widgets/MediaViewer/index.tsx @@ -13,16 +13,30 @@ export function MediaViewer({ media, className, fullWidth, -}: Readonly<{ media?: MediaData; className?: string; fullWidth?: boolean }>) { + fullHeight, +}: Readonly<{ + media?: MediaData; + className?: string; + fullWidth?: boolean; + fullHeight?: boolean; +}>) { const token = localStorage.getItem("token"); return ( - + {media?.media_type === 1 && ( {media?.filename} )} @@ -48,6 +62,10 @@ export function MediaViewer({ media?.id }/download?token=${token}`} alt={media?.filename} + style={{ + height: fullHeight ? "100%" : "auto", + width: fullWidth ? "100%" : "auto", + }} /> )} {media?.media_type === 4 && ( @@ -78,6 +96,7 @@ export function MediaViewer({ media?.id }/download?token=${token}`} height="100%" + width="1000px" /> )} diff --git a/src/widgets/SightTabs/InformationTab/index.tsx b/src/widgets/SightTabs/InformationTab/index.tsx index 1063158..a43e7fe 100644 --- a/src/widgets/SightTabs/InformationTab/index.tsx +++ b/src/widgets/SightTabs/InformationTab/index.tsx @@ -163,17 +163,22 @@ export const InformationTab = observer( /> city.id === sight.common.city_id) ?? - null + ruCities?.data?.find( + (city) => city.id === sight.common.city_id + ) ?? null } getOptionLabel={(option) => option.name} onChange={(_, value) => { setCity(value?.id ?? 0); - handleChange(language as Language, { - city_id: value?.id ?? 0, - }); + handleChange( + language as Language, + { + city_id: value?.id ?? 0, + }, + true + ); }} renderInput={(params) => (