diff --git a/src/client/src/App.jsx b/src/client/src/App.jsx index 113b06a..af597f2 100644 --- a/src/client/src/App.jsx +++ b/src/client/src/App.jsx @@ -23,6 +23,7 @@ const App = observer(() => { loadingStatus, loadingProgress, context, + carrier, setIsLoading, setLoadingStatus, setLoadingProgress, @@ -172,6 +173,7 @@ const App = observer(() => { loadingStatus={loadingStatus} loadingProgress={loadingProgress} onToggleDebug={() => setIsDebugVisible((prev) => !prev)} + videoId={carrier?.video_preview} /> ) : (
diff --git a/src/client/src/api/ApiStore/types.ts b/src/client/src/api/ApiStore/types.ts index d5d1962..1b331e8 100644 --- a/src/client/src/api/ApiStore/types.ts +++ b/src/client/src/api/ApiStore/types.ts @@ -64,7 +64,6 @@ export type GetRouteResponse = { route_sys_number: string; scale_max: number; scale_min: number; - video_preview: string; video_timer?: number; icon?: string; icon_size?: number; @@ -77,6 +76,8 @@ export type GetCarrierResponse = { full_name: string; id: number; logo: string; + preview_media?: string; + video_preview?: string; short_name: string; slogan: string; main_color?: string; diff --git a/src/client/src/components/Loader.jsx b/src/client/src/components/Loader.jsx index 8f1ebb8..765df0b 100644 --- a/src/client/src/components/Loader.jsx +++ b/src/client/src/components/Loader.jsx @@ -1,6 +1,7 @@ import React, { useState, useEffect } from "react"; import { createPortal } from "react-dom"; import "../styles/Loader.css"; +import { getMediaUrl } from "../api/apiConfig"; const loaderVideoPath = new URL("../assets/video/taganai.mp4", import.meta.url) .href; @@ -9,8 +10,14 @@ const Loader = ({ loadingStatus = null, loadingProgress = null, onToggleDebug, + videoId = null, }) => { const [showLoadingInfo, setShowLoadingInfo] = useState(false); + const [isCustomVideoError, setIsCustomVideoError] = useState(false); + + useEffect(() => { + setIsCustomVideoError(false); + }, [videoId]); useEffect(() => { const handleKeyDown = (event) => { @@ -55,7 +62,12 @@ const Loader = ({ loop muted playsInline - src={loaderVideoPath} + src={ + videoId && !isCustomVideoError + ? getMediaUrl(videoId) + : loaderVideoPath + } + onError={() => setIsCustomVideoError(true)} alt="Loading" className="error-gif" style={{ width: "100%", height: "100%", objectFit: "cover" }} diff --git a/src/client/src/components/side-menu/SideMenu.jsx b/src/client/src/components/side-menu/SideMenu.jsx index e9e7af2..437cb69 100644 --- a/src/client/src/components/side-menu/SideMenu.jsx +++ b/src/client/src/components/side-menu/SideMenu.jsx @@ -13,12 +13,10 @@ 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 { carrier, - city, route, getArticle, sightArticles, @@ -33,6 +31,7 @@ const SideMenu = observer(({ onMenuToggle }) => { const [isStationOpen, openStation] = useState(false); const [isWidgetOpen, openWidget] = useState(false); const [isWeatherVisible, setIsWeatherVisible] = useState(true); + const [isPreviewMediaError, setIsPreviewMediaError] = useState(false); const [isLeftWidgetVisible, setIsLeftWidgetVisible] = useState(false); const [localSelectedSightId, setLocalSelectedSightId] = useState(null); const [isSightsAnimating, setIsSightsAnimating] = useState(false); @@ -186,27 +185,12 @@ const SideMenu = observer(({ onMenuToggle }) => { } }, [isLeftWidgetVisible]); - const [designData, setDesignData] = useState(null); const [routeData, setRouteData] = useState(null); const [isLangMenuOpen, setIsLangMenuOpen] = useState(false); useEffect(() => { - async function fetchCarrierAndCrest() { - try { - const carrierPath = carrier.logo ? getMediaUrl(carrier.logo) : null; - const cityPath = city.arms ? getMediaUrl(city.arms) : null; - - const response = { - carrierPath: carrierPath, - creastPath: cityPath, - }; - setDesignData(response); - } catch (err) { - console.error("Ошибка в получении данных герба и перевозчика:", err); - } - } - fetchCarrierAndCrest(); - }, [contextData?.routeId, isGeolocationLoading, selectedLanguage]); + setIsPreviewMediaError(false); + }, [carrier?.preview_media]); useEffect(() => { if (isGeolocationLoading || !contextData?.routeId) { @@ -415,14 +399,6 @@ const SideMenu = observer(({ onMenuToggle }) => { "background 0.3s ease, backdrop-filter 0.3s ease, box-shadow 0.3s ease", }} > - Герб - {carrier?.slogan && ( -
{carrier.slogan}
- )} {shouldRenderSights && ( { : "#EveryoneOnTheWay"} */}
-
- {designData?.carrierPath && ( - ГЭТ - Электротранспорт Санкт-Петербурга - )} - {carrier?.full_name && ( -
{carrier.short_name}
- )} -
- + setIsPreviewMediaError(true)} + alt="" + />
diff --git a/src/client/src/styles/SideMenu.css b/src/client/src/styles/SideMenu.css index 9322f20..e40e864 100644 --- a/src/client/src/styles/SideMenu.css +++ b/src/client/src/styles/SideMenu.css @@ -16,17 +16,6 @@ var(--carrier-left, #006f3a); } -.side-menu-label { - margin-top: 10px; - text-align: left; - font-size: 15px; - padding: 0 20px; - align-self: flex-start; - font-style: normal; - font-weight: 400; - line-height: 150%; -} - .appeal-button { color: black; padding: 8px 16px; @@ -66,24 +55,6 @@ flex-direction: column; } -.side-menu-carrier-block { - padding: 0 20px; -} - -.carrier-logo { - width: 170px; -} - -.side-menu-slogan { - margin-top: 4px; - text-align: left; - font-size: 16px; - font-style: normal; - font-weight: 700; - line-height: 150%; - color: #fff; -} - .side-menu-bottom-photo { width: 100%; margin-top: 32px; @@ -284,12 +255,6 @@ } } -.side-menu-crest { - width: 170px; - align-self: flex-start; - margin-left: 20px; -} - .side-menu-control-lang-open { gap: 10px; display: inline-flex; diff --git a/src/pages/Carrier/CarrierCreatePage/index.tsx b/src/pages/Carrier/CarrierCreatePage/index.tsx index fc8c5b5..fb055e1 100644 --- a/src/pages/Carrier/CarrierCreatePage/index.tsx +++ b/src/pages/Carrier/CarrierCreatePage/index.tsx @@ -1,6 +1,5 @@ import { Button, - Paper, TextField, Select, MenuItem, @@ -24,9 +23,10 @@ import { SelectMediaDialog, UploadMediaDialog, PreviewMediaDialog, + editSightStore, } from "@shared"; import { useState, useEffect } from "react"; -import { ImageUploadCard, LanguageSwitcher } from "@widgets"; +import { ImageUploadCard, LanguageSwitcher, VideoPreviewCard } from "@widgets"; type ColorFields = { main_color: string; left_color: string; right_color: string }; @@ -80,18 +80,29 @@ const ColorPickerField = ({ export const CarrierCreatePage = observer(() => { const navigate = useNavigate(); - const { createCarrierData, setCreateCarrierData } = carrierStore; + const { + createCarrierData, + setCreateCarrierData, + setCreateCarrierMedia, + } = carrierStore; const { language } = languageStore; const canReadCities = authStore.canRead("cities"); const { selectedCityId, selectedCity } = useSelectedCity(); - 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 [activeMediaField, setActiveMediaField] = useState< + "preview_media" | "video_preview" + >("preview_media"); const [activeMenuType, setActiveMenuType] = useState< - "thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null + | "thumbnail" + | "watermark_lu" + | "watermark_rd" + | "image" + | "video_preview" + | null >(null); useEffect(() => { @@ -137,12 +148,16 @@ export const CarrierCreatePage = observer(() => { createCarrierData[language].full_name, createCarrierData[language].short_name, selectedCityId, - createCarrierData[language].slogan, - selectedMediaId || "", language, ); } - }, [selectedCityId, createCarrierData.city_id]); + }, [ + selectedCityId, + createCarrierData, + createCarrierData.city_id, + language, + setCreateCarrierData, + ]); const handleCreate = async () => { try { @@ -151,7 +166,7 @@ export const CarrierCreatePage = observer(() => { toast.success("Перевозчик успешно создан"); navigate("/carrier"); - } catch (error) { + } catch { toast.error("Ошибка при создании перевозчика"); } finally { setIsLoading(false); @@ -164,27 +179,22 @@ export const CarrierCreatePage = observer(() => { media_name?: string; media_type: number; }) => { - setSelectedMediaId(media.id); - setCreateCarrierData( - createCarrierData[language].full_name, - createCarrierData[language].short_name, - createCarrierData.city_id, - createCarrierData[language].slogan, - media.id, - language, - ); + setCreateCarrierMedia(activeMediaField, media.id); }; - const selectedMedia = - selectedMediaId && !isMediaIdEmpty(selectedMediaId) - ? mediaStore.media.find((m) => m.id === selectedMediaId) - : null; - const effectiveLogoUrl = isMediaIdEmpty(selectedMediaId) + const effectivePreviewMediaUrl = isMediaIdEmpty( + createCarrierData.preview_media, + ) ? null - : selectedMedia?.id ?? selectedMediaId ?? null; + : createCarrierData.preview_media; + const effectiveVideoPreviewId = isMediaIdEmpty( + createCarrierData.video_preview, + ) + ? null + : createCarrierData.video_preview; return ( - +
-
- { - setIsPreviewMediaOpen(true); - setMediaId(effectiveLogoUrl ?? ""); - }} - onDeleteImageClick={() => { - setSelectedMediaId(null); - setActiveMenuType(null); - setCreateCarrierData( - createCarrierData[language].full_name, - createCarrierData[language].short_name, - createCarrierData.city_id, - createCarrierData[language].slogan, - "", - language, - ); - }} - onSelectFileClick={() => { - setActiveMenuType("image"); - setIsSelectMediaOpen(true); - }} - setUploadMediaOpen={() => { - setIsUploadMediaOpen(true); - setActiveMenuType("image"); - }} - /> +
+
+ { + setIsPreviewMediaOpen(true); + setMediaId(effectivePreviewMediaUrl ?? ""); + }} + onDeleteImageClick={() => + setCreateCarrierMedia("preview_media", "") + } + onSelectFileClick={() => { + setActiveMediaField("preview_media"); + setActiveMenuType("image"); + setIsSelectMediaOpen(true); + }} + setUploadMediaOpen={() => { + setActiveMediaField("preview_media"); + setActiveMenuType("image"); + setIsUploadMediaOpen(true); + }} + /> +
+
+ { + if (effectiveVideoPreviewId) { + setMediaId(effectiveVideoPreviewId); + setIsPreviewMediaOpen(true); + } + }} + onDeleteVideoClick={() => + setCreateCarrierMedia("video_preview", "") + } + onSelectVideoClick={(file) => { + setActiveMediaField("video_preview"); + setActiveMenuType("video_preview"); + if (file) { + editSightStore.setFileToUpload(file); + setIsUploadMediaOpen(true); + } else { + setIsSelectMediaOpen(true); + } + }} + className="w-full" + /> +
); }); diff --git a/src/pages/Carrier/CarrierEditPage/index.tsx b/src/pages/Carrier/CarrierEditPage/index.tsx index f4bbe8d..792faab 100644 --- a/src/pages/Carrier/CarrierEditPage/index.tsx +++ b/src/pages/Carrier/CarrierEditPage/index.tsx @@ -1,6 +1,5 @@ import { Button, - Paper, TextField, Select, MenuItem, @@ -22,9 +21,14 @@ import { isMediaIdEmpty, LoadingSpinner, selectedCityStore, + editSightStore, } from "@shared"; import { useState, useEffect } from "react"; -import { ImageUploadCard, LanguageSwitcher, DeleteModal } from "@widgets"; +import { + ImageUploadCard, + LanguageSwitcher, + VideoPreviewCard, +} from "@widgets"; import { SelectMediaDialog, PreviewMediaDialog, @@ -88,7 +92,12 @@ const ColorPickerField = ({ export const CarrierEditPage = observer(() => { const navigate = useNavigate(); const { id } = useParams(); - const { getCarrier, setEditCarrierData, editCarrierData } = carrierStore; + const { + getCarrier, + setEditCarrierData, + setEditCarrierMedia, + editCarrierData, + } = carrierStore; const { language } = languageStore; const canReadCities = authStore.canRead("cities"); @@ -98,10 +107,17 @@ export const CarrierEditPage = observer(() => { const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false); const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false); const [mediaId, setMediaId] = useState(""); - const [isDeleteLogoModalOpen, setIsDeleteLogoModalOpen] = useState(false); const [initialCityName, setInitialCityName] = useState(""); + const [activeMediaField, setActiveMediaField] = useState< + "preview_media" | "video_preview" + >("preview_media"); const [activeMenuType, setActiveMenuType] = useState< - "thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null + | "thumbnail" + | "watermark_lu" + | "watermark_rd" + | "image" + | "video_preview" + | null >(null); useEffect(() => { @@ -137,8 +153,6 @@ export const CarrierEditPage = observer(() => { carrierData.ru?.full_name || "", carrierData.ru?.short_name || "", carrierData.ru?.city_id || 0, - carrierData.ru?.slogan || "", - carrierData.ru?.logo || "", "ru", colors, ); @@ -146,18 +160,22 @@ export const CarrierEditPage = observer(() => { carrierData.en?.full_name || "", carrierData.en?.short_name || "", carrierData.en?.city_id || 0, - carrierData.en?.slogan || "", - carrierData.en?.logo || "", "en", ); setEditCarrierData( carrierData.zh?.full_name || "", carrierData.zh?.short_name || "", carrierData.zh?.city_id || 0, - carrierData.zh?.slogan || "", - carrierData.zh?.logo || "", "zh", ); + setEditCarrierMedia( + "video_preview", + carrierData.ru?.video_preview || "", + ); + setEditCarrierMedia( + "preview_media", + carrierData.ru?.preview_media || "", + ); setInitialCityName(carrierData.ru?.city || ""); } @@ -168,7 +186,7 @@ export const CarrierEditPage = observer(() => { })(); languageStore.setLanguage("ru"); - }, [id]); + }, [id, getCarrier, setEditCarrierData, setEditCarrierMedia]); const handleEdit = async () => { try { @@ -176,7 +194,7 @@ export const CarrierEditPage = observer(() => { await carrierStore.editCarrier(Number(id)); toast.success("Перевозчик успешно обновлен"); navigate("/carrier"); - } catch (error) { + } catch { toast.error("Ошибка при обновлении перевозчика"); } finally { setIsLoading(false); @@ -189,23 +207,19 @@ export const CarrierEditPage = observer(() => { media_name?: string; media_type: number; }) => { - setEditCarrierData( - editCarrierData[language].full_name, - editCarrierData[language].short_name, - editCarrierData.city_id, - editCarrierData[language].slogan, - media.id, - language, - ); + setEditCarrierMedia(activeMediaField, media.id); }; - const selectedMedia = - editCarrierData.logo && !isMediaIdEmpty(editCarrierData.logo) - ? mediaStore.media.find((m) => m.id === editCarrierData.logo) - : null; - const effectiveLogoUrl = isMediaIdEmpty(editCarrierData.logo) + const effectivePreviewMediaUrl = isMediaIdEmpty( + editCarrierData.preview_media, + ) ? null - : (selectedMedia?.id ?? editCarrierData.logo); + : editCarrierData.preview_media; + const effectiveVideoPreviewId = isMediaIdEmpty( + editCarrierData.video_preview, + ) + ? null + : editCarrierData.video_preview; const baseCities = canReadCities ? cityStore.cities["ru"].data @@ -248,7 +262,7 @@ export const CarrierEditPage = observer(() => { } return ( - +
-
- { - setIsPreviewMediaOpen(true); - setMediaId(effectiveLogoUrl ?? ""); - }} - onDeleteImageClick={() => { - setIsDeleteLogoModalOpen(true); - }} - onSelectFileClick={() => { - setActiveMenuType("image"); - setIsSelectMediaOpen(true); - }} - setUploadMediaOpen={() => { - setIsUploadMediaOpen(true); - setActiveMenuType("image"); - }} - /> +
+
+ { + setIsPreviewMediaOpen(true); + setMediaId(effectivePreviewMediaUrl ?? ""); + }} + onDeleteImageClick={() => + setEditCarrierMedia("preview_media", "") + } + onSelectFileClick={() => { + setActiveMediaField("preview_media"); + setActiveMenuType("image"); + setIsSelectMediaOpen(true); + }} + setUploadMediaOpen={() => { + setActiveMediaField("preview_media"); + setActiveMenuType("image"); + setIsUploadMediaOpen(true); + }} + /> +
+
+ { + if (effectiveVideoPreviewId) { + setMediaId(effectiveVideoPreviewId); + setIsPreviewMediaOpen(true); + } + }} + onDeleteVideoClick={() => + setEditCarrierMedia("video_preview", "") + } + onSelectVideoClick={(file) => { + setActiveMediaField("video_preview"); + setActiveMenuType("video_preview"); + if (file) { + editSightStore.setFileToUpload(file); + setIsUploadMediaOpen(true); + } else { + setIsSelectMediaOpen(true); + } + }} + className="w-full" + /> +
); }); diff --git a/src/pages/Route/RouteCreatePage/index.tsx b/src/pages/Route/RouteCreatePage/index.tsx index ae2c4f2..92a666b 100644 --- a/src/pages/Route/RouteCreatePage/index.tsx +++ b/src/pages/Route/RouteCreatePage/index.tsx @@ -7,16 +7,8 @@ import { InputLabel, Typography, Box, - Dialog, - DialogTitle, - DialogContent, - DialogActions, } from "@mui/material"; -import { - MediaViewer, - VideoPreviewCard, - ImageUploadCard, -} from "@widgets"; +import { ImageUploadCard } from "@widgets"; import { observer } from "mobx-react-lite"; import { ArrowLeft, Loader2, Save, Plus, X } from "lucide-react"; import { useEffect, useState, useMemo } from "react"; @@ -59,14 +51,10 @@ export const RouteCreatePage = observer(() => { const [centerLat, setCenterLat] = useState(""); const [centerLng, setCenterLng] = useState(""); const [videoTimer, setVideoTimer] = useState(420); - const [videoPreview, setVideoPreview] = useState(""); const [icon, setIcon] = useState(""); const [isLoading, setIsLoading] = useState(false); const [isSelectArticleDialogOpen, setIsSelectArticleDialogOpen] = useState(false); - const [isSelectVideoDialogOpen, setIsSelectVideoDialogOpen] = useState(false); - const [isVideoPreviewOpen, setIsVideoPreviewOpen] = useState(false); - const [isUploadVideoDialogOpen, setIsUploadVideoDialogOpen] = useState(false); const [isSelectIconDialogOpen, setIsSelectIconDialogOpen] = useState(false); const [isUploadIconDialogOpen, setIsUploadIconDialogOpen] = useState(false); const [isPreviewIconOpen, setIsPreviewIconOpen] = useState(false); @@ -74,7 +62,6 @@ export const RouteCreatePage = observer(() => { const [activeIconMenuType, setActiveIconMenuType] = useState< "thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null >(null); - const [fileToUpload, setFileToUpload] = useState(null); const { language } = languageStore; useEffect(() => { @@ -140,40 +127,6 @@ export const RouteCreatePage = observer(() => { articlesStore.getArticleList(); }; - const handleVideoSelect = (media: { - id: string; - filename: string; - media_name?: string; - media_type: number; - }) => { - setVideoPreview(media.id); - setIsSelectVideoDialogOpen(false); - }; - - const handleVideoFileSelect = (file?: File) => { - if (file) { - setFileToUpload(file); - setIsUploadVideoDialogOpen(true); - } else { - setIsSelectVideoDialogOpen(true); - } - }; - - const handleVideoUpload = (media: { - id: string; - filename: string; - media_name?: string; - media_type: number; - }) => { - setVideoPreview(media.id); - setIsUploadVideoDialogOpen(false); - setFileToUpload(null); - }; - - const handleVideoPreviewClick = () => { - setIsVideoPreviewOpen(true); - }; - const handleIconSelect = (media: { id: string; filename: string; @@ -189,7 +142,6 @@ export const RouteCreatePage = observer(() => { ? mediaStore.media.find((m) => m.id === icon) : null; const effectiveIconUrl = isMediaIdEmpty(icon) ? null : selectedIconMedia?.id ?? icon; - const effectiveVideoId = isMediaIdEmpty(videoPreview) ? null : videoPreview; const handleCreateRoute = async () => { try { @@ -284,7 +236,6 @@ export const RouteCreatePage = observer(() => { center_latitude, center_longitude, path, - video_preview: !isMediaIdEmpty(videoPreview) ? videoPreview : undefined, icon: !isMediaIdEmpty(icon) ? icon : undefined, video_timer: videoTimer, }; @@ -485,16 +436,6 @@ export const RouteCreatePage = observer(() => { }} />
-
- setVideoPreview("")} - onSelectVideoClick={handleVideoFileSelect} - className="w-full" - /> -
@@ -578,7 +519,7 @@ export const RouteCreatePage = observer(() => { /> { @@ -611,51 +552,6 @@ export const RouteCreatePage = observer(() => { onClose={() => setIsSelectArticleDialogOpen(false)} onSelectArticle={handleArticleSelect} /> - setIsSelectVideoDialogOpen(false)} - onSelectMedia={handleVideoSelect} - mediaType={2} - /> - {effectiveVideoId && ( - setIsVideoPreviewOpen(false)} - maxWidth="md" - fullWidth - > - Предпросмотр видео - - - - - - - - - - )} - { - setIsUploadVideoDialogOpen(false); - setFileToUpload(null); - }} - hardcodeType="video_preview" - contextObjectName={routeName || "Маршрут"} - contextType="sight" - initialFile={fileToUpload || undefined} - afterUpload={handleVideoUpload} - /> - setIsSelectIconDialogOpen(false)} diff --git a/src/pages/Route/RouteEditPage/index.tsx b/src/pages/Route/RouteEditPage/index.tsx index 9043765..1eca200 100644 --- a/src/pages/Route/RouteEditPage/index.tsx +++ b/src/pages/Route/RouteEditPage/index.tsx @@ -7,14 +7,8 @@ import { InputLabel, Typography, Box, - Dialog, - DialogTitle, - DialogContent, - DialogActions, } from "@mui/material"; import { - MediaViewer, - VideoPreviewCard, ImageUploadCard, DeleteModal, } from "@widgets"; @@ -54,9 +48,6 @@ export const RouteEditPage = observer(() => { const [isLoadingData, setIsLoadingData] = useState(true); const [isSelectArticleDialogOpen, setIsSelectArticleDialogOpen] = useState(false); - const [isSelectVideoDialogOpen, setIsSelectVideoDialogOpen] = useState(false); - const [isVideoPreviewOpen, setIsVideoPreviewOpen] = useState(false); - const [isUploadVideoDialogOpen, setIsUploadVideoDialogOpen] = useState(false); const [isSelectIconDialogOpen, setIsSelectIconDialogOpen] = useState(false); const [isUploadIconDialogOpen, setIsUploadIconDialogOpen] = useState(false); const [isPreviewIconOpen, setIsPreviewIconOpen] = useState(false); @@ -65,7 +56,6 @@ export const RouteEditPage = observer(() => { const [activeIconMenuType, setActiveIconMenuType] = useState< "thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null >(null); - const [fileToUpload, setFileToUpload] = useState(null); const { language } = languageStore; const [coordinates, setCoordinates] = useState(""); @@ -114,9 +104,6 @@ export const RouteEditPage = observer(() => { const effectiveIconUrl = isMediaIdEmpty(editRouteData.icon) ? null : (selectedIconMedia?.id ?? editRouteData.icon); - const effectiveVideoId = isMediaIdEmpty(editRouteData.video_preview) - ? null - : editRouteData.video_preview; useEffect(() => { if (editRouteData.path && editRouteData.path.length > 0) { @@ -238,46 +225,6 @@ export const RouteEditPage = observer(() => { articlesStore.getArticleList(); }; - const handleVideoSelect = (media: { - id: string; - filename: string; - media_name?: string; - media_type: number; - }) => { - routeStore.setEditRouteData({ - video_preview: media.id, - }); - setIsSelectVideoDialogOpen(false); - }; - - const handleVideoFileSelect = (file?: File) => { - if (file) { - setFileToUpload(file); - setIsUploadVideoDialogOpen(true); - } else { - setIsSelectVideoDialogOpen(true); - } - }; - - const handleVideoUpload = (media: { - id: string; - filename: string; - media_name?: string; - media_type: number; - }) => { - routeStore.setEditRouteData({ - video_preview: media.id, - }); - setIsUploadVideoDialogOpen(false); - setFileToUpload(null); - }; - - const handleVideoPreviewClick = () => { - if (editRouteData.video_preview && editRouteData.video_preview !== "") { - setIsVideoPreviewOpen(true); - } - }; - // Получаем название выбранной статьи для отображения const selectedArticle = articlesStore.articleList.ru.data.find( (article) => article.id === editRouteData.governor_appeal @@ -555,7 +502,7 @@ export const RouteEditPage = observer(() => { /> { @@ -646,18 +593,6 @@ export const RouteEditPage = observer(() => { }} /> -
- { - routeStore.setEditRouteData({ video_preview: "" }); - }} - onSelectVideoClick={handleVideoFileSelect} - className="w-full" - /> -
@@ -703,49 +638,6 @@ export const RouteEditPage = observer(() => { onClose={() => setIsSelectArticleDialogOpen(false)} onSelectArticle={handleArticleSelect} /> - setIsSelectVideoDialogOpen(false)} - onSelectMedia={handleVideoSelect} - mediaType={2} - /> - setIsVideoPreviewOpen(false)} - maxWidth="md" - fullWidth - > - Предпросмотр видео - - - {effectiveVideoId && ( - - )} - - - - - - - { - setIsUploadVideoDialogOpen(false); - setFileToUpload(null); - }} - hardcodeType="video_preview" - contextObjectName={editRouteData.route_name || "Маршрут"} - contextType="sight" - initialFile={fileToUpload || undefined} - afterUpload={handleVideoUpload} - /> - setIsSelectIconDialogOpen(false)} diff --git a/src/pages/Route/route-preview/LeftSidebar.tsx b/src/pages/Route/route-preview/LeftSidebar.tsx index 6cf0b2e..cb16e17 100644 --- a/src/pages/Route/route-preview/LeftSidebar.tsx +++ b/src/pages/Route/route-preview/LeftSidebar.tsx @@ -1,10 +1,9 @@ import { Button } from "@mui/material"; import { useNavigate, useNavigationType } from "react-router"; -import { MediaViewer } from "@widgets"; import { useMapData } from "./MapDataContext"; import { observer } from "mobx-react-lite"; import { useEffect, useState } from "react"; -import { authInstance, isMediaIdEmpty } from "@shared"; +import { authInstance } from "@shared"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import LanguageSelector from "./web-gl/LanguageSelector"; @@ -17,9 +16,8 @@ export const LeftSidebar = observer(({ open, onToggle }: LeftSidebarProps) => { const navigate = useNavigate(); const navigationType = useNavigationType(); const { routeData } = useMapData(); - const [carrierLogo, setCarrierLogo] = useState(null); - const [carrierSlogan, setCarrierSlogan] = useState(null); - const [carrierShortName, setCarrierShortName] = useState(null); + const [previewMedia, setPreviewMedia] = useState(null); + const [isPreviewMediaError, setIsPreviewMediaError] = useState(false); useEffect(() => { async function fetchCarrierData() { @@ -27,9 +25,8 @@ export const LeftSidebar = observer(({ open, onToggle }: LeftSidebarProps) => { const carrier = ( await authInstance.get(`/carrier/${routeData.carrier_id}`) ).data; - setCarrierLogo(carrier.logo); - setCarrierSlogan(carrier.slogan ?? null); - setCarrierShortName(carrier.short_name ?? null); + setPreviewMedia(carrier.preview_media ?? null); + setIsPreviewMediaError(false); } } fetchCarrierData(); @@ -94,43 +91,6 @@ export const LeftSidebar = observer(({ open, onToggle }: LeftSidebarProps) => { pointerEvents: open ? "auto" : "none", }} > - {/* Герб — .side-menu-crest */} -
- Герб -
- - {/* Слоган — .side-menu-label */} - {carrierSlogan && ( -
- {carrierSlogan} -
- )} - {/* Кнопки — .side-menu-buttons */}
{ flexDirection: "column", }} > - {/* .side-menu-carrier-block */} -
- {carrierLogo && !isMediaIdEmpty(carrierLogo) && ( -
- -
- )} - {carrierShortName && ( -
- {carrierShortName} -
- )} -
- {/* .side-menu-bottom-photo */} setIsPreviewMediaError(true)} alt="" style={{ width: "288px", diff --git a/src/pages/Snapshot/SnapshotListPage/index.tsx b/src/pages/Snapshot/SnapshotListPage/index.tsx index 14c6c08..1503534 100644 --- a/src/pages/Snapshot/SnapshotListPage/index.tsx +++ b/src/pages/Snapshot/SnapshotListPage/index.tsx @@ -131,7 +131,7 @@ export const SnapshotListPage = observer(() => { }, { field: "created_at", - headerName: "Дата создания", + headerName: "Дата создания (UTC+03:00, MSK)", flex: 1, renderCell: (params: GridRenderCellParams) => { return
{params.value ? params.value : "-"}
; diff --git a/src/shared/modals/PreviewMediaDialog/index.tsx b/src/shared/modals/PreviewMediaDialog/index.tsx index 82fe7c2..85f7ef8 100644 --- a/src/shared/modals/PreviewMediaDialog/index.tsx +++ b/src/shared/modals/PreviewMediaDialog/index.tsx @@ -1,4 +1,8 @@ -import { mediaStore, MEDIA_TYPE_LABELS } from "@shared"; +import { + mediaStore, + MEDIA_TYPE_LABELS, + PreviewBackgroundToggle, +} from "@shared"; import { observer } from "mobx-react-lite"; import { useEffect, useState } from "react"; import { @@ -29,6 +33,8 @@ export const PreviewMediaDialog = observer( const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(false); + const [isPreviewBackgroundDark, setIsPreviewBackgroundDark] = + useState(false); const media = mediaId ? mediaStore.media.find((m) => m.id === mediaId) : null; @@ -40,6 +46,7 @@ export const PreviewMediaDialog = observer( if (media) { setMediaName(media.media_name); setMediaFilename(media.filename); + setIsPreviewBackgroundDark(false); } }, [media]); @@ -141,8 +148,19 @@ export const PreviewMediaDialog = observer( display: "flex", alignItems: "center", justifyContent: "center", + position: "relative", + backgroundColor: isPreviewBackgroundDark ? "#000" : "#fff", + transition: "background-color 150ms ease", }} > + {[1, 3, 4, 5].includes(media.media_type) && ( + + setIsPreviewBackgroundDark((value) => !value) + } + /> + )} (null); useEffect(() => { @@ -347,6 +350,7 @@ export const UploadMediaDialog = observer( setMediaUrl(null); setMediaFile(null); setIsPreviewLoaded(false); + setIsPreviewBackgroundDark(false); previousMediaUrlRef.current = null; onClose(); }; @@ -416,8 +420,18 @@ export const UploadMediaDialog = observer( justifyContent: "center", height: "100%", position: "relative", + backgroundColor: isPreviewBackgroundDark ? "#000" : "#fff", + transition: "background-color 150ms ease", }} > + {mediaUrl && mediaType !== 2 && mediaType !== 6 && ( + + setIsPreviewBackgroundDark((value) => !value) + } + /> + )} {!isPreviewLoaded && mediaUrl && ( { + this.createCarrierData[field] = mediaId; + }; + setCreateCarrierData = ( fullName: string, shortName: string, cityId: number, - slogan: string, - logoId: string, language: Language, colors?: { main_color?: string; left_color?: string; right_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; @@ -151,7 +153,6 @@ class CarrierStore { this.createCarrierData[language] = { full_name: fullName, short_name: shortName, - slogan: slogan, }; }; @@ -206,8 +207,12 @@ class CarrierStore { short_name: (this.createCarrierData[language].short_name || "").trim(), city: cityName, city_id: this.createCarrierData.city_id, - slogan: (this.createCarrierData[language].slogan || "").trim(), - ...(this.createCarrierData.logo ? { logo: this.createCarrierData.logo } : {}), + ...(this.createCarrierData.preview_media + ? { preview_media: this.createCarrierData.preview_media } + : {}), + ...(this.createCarrierData.video_preview + ? { video_preview: this.createCarrierData.video_preview } + : {}), ...(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 } : {}), @@ -221,55 +226,55 @@ class CarrierStore { this.carriers[language].data.push(response.data); }); - for (const lang of ["ru", "en", "zh"].filter((l) => l !== language)) { + for (const lang of (["ru", "en", "zh"] as const).filter( + (item) => item !== language + )) { const cityNameForLang = this.resolveCityName( this.createCarrierData.city_id, - lang as Language + lang ); + const localizedData = this.createCarrierData[lang]; const patchPayload = { - // @ts-ignore - full_name: ((this.createCarrierData[lang as any].full_name as string) || "").trim(), - // @ts-ignore - short_name: ((this.createCarrierData[lang as any].short_name as string) || "").trim(), + full_name: (localizedData.full_name || "").trim(), + short_name: (localizedData.short_name || "").trim(), city: cityNameForLang || cityName, city_id: this.createCarrierData.city_id, - // @ts-ignore - slogan: ((this.createCarrierData[lang as any].slogan as string) || "").trim(), - ...(this.createCarrierData.logo - ? { logo: this.createCarrierData.logo } + ...(this.createCarrierData.preview_media + ? { preview_media: this.createCarrierData.preview_media } + : {}), + ...(this.createCarrierData.video_preview + ? { video_preview: this.createCarrierData.video_preview } : {}), }; - const response = await languageInstance(lang as Language).patch( + const response = await languageInstance(lang).patch( `/carrier/${carrierId}`, patchPayload ); runInAction(() => { - this.carriers[lang as keyof Carriers].data.push(response.data); + this.carriers[lang].data.push(response.data); }); } this.createCarrierData = { city_id: 0, - logo: "", + preview_media: "", + video_preview: "", main_color: "", left_color: "", right_color: "", ru: { full_name: "", short_name: "", - slogan: "", }, en: { full_name: "", short_name: "", - slogan: "", }, zh: { full_name: "", short_name: "", - slogan: "", }, }; }; @@ -278,36 +283,38 @@ class CarrierStore { ru: { full_name: "", short_name: "", - slogan: "", }, en: { full_name: "", short_name: "", - slogan: "", }, zh: { full_name: "", short_name: "", - slogan: "", }, city_id: 0, - logo: "", + preview_media: "", + video_preview: "", main_color: "", left_color: "", right_color: "", }; + setEditCarrierMedia = ( + field: "preview_media" | "video_preview", + mediaId: string + ) => { + this.editCarrierData[field] = mediaId; + }; + setEditCarrierData = ( fullName: string, shortName: string, cityId: number, - slogan: string, - logoId: string, language: Language, colors?: { main_color?: string; left_color?: string; right_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; @@ -316,7 +323,6 @@ class CarrierStore { this.editCarrierData[language] = { full_name: fullName, short_name: shortName, - slogan: slogan, }; }; @@ -327,10 +333,10 @@ class CarrierStore { ...this.editCarrierData[lang], full_name: (this.editCarrierData[lang].full_name || "").trim(), short_name: (this.editCarrierData[lang].short_name || "").trim(), - slogan: (this.editCarrierData[lang].slogan || "").trim(), city: cityName, city_id: this.editCarrierData.city_id, - ...(this.editCarrierData.logo ? { logo: this.editCarrierData.logo } : {}), + preview_media: this.editCarrierData.preview_media || null, + video_preview: this.editCarrierData.video_preview || null, ...(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 } : {}), diff --git a/src/shared/store/RouteStore/index.ts b/src/shared/store/RouteStore/index.ts index edb2bbb..dd56536 100644 --- a/src/shared/store/RouteStore/index.ts +++ b/src/shared/store/RouteStore/index.ts @@ -23,7 +23,6 @@ export type Route = { route_sys_number: string; scale_max: number; scale_min: number; - video_preview: string; video_timer: number; }; @@ -154,21 +153,16 @@ class RouteStore { route_sys_number: "", scale_max: 0, scale_min: 0, - video_preview: "" as string | undefined, video_timer: 420, }; setEditRouteData = (data: any) => { - this.editRouteData = { ...this.editRouteData, ...data }; + const routeData = { ...data }; + delete routeData.video_preview; + this.editRouteData = { ...this.editRouteData, ...routeData }; }; editRoute = async (id: number) => { - if ( - !this.editRouteData.video_preview || - isMediaIdEmpty(this.editRouteData.video_preview) - ) { - delete this.editRouteData.video_preview; - } if (!this.editRouteData.icon || isMediaIdEmpty(this.editRouteData.icon)) { delete (this.editRouteData as any).icon; } @@ -180,6 +174,7 @@ class RouteStore { center_latitude: parseFloat(this.editRouteData.center_latitude), center_longitude: parseFloat(this.editRouteData.center_longitude), }; + delete dataToSend.video_preview; if ( this.editRouteData.governor_appeal === 0 || !this.editRouteData.governor_appeal diff --git a/src/shared/ui/PreviewBackgroundToggle/index.tsx b/src/shared/ui/PreviewBackgroundToggle/index.tsx new file mode 100644 index 0000000..864dae0 --- /dev/null +++ b/src/shared/ui/PreviewBackgroundToggle/index.tsx @@ -0,0 +1,40 @@ +import { IconButton, Tooltip } from "@mui/material"; +import { Contrast } from "lucide-react"; + +interface PreviewBackgroundToggleProps { + dark: boolean; + onToggle: () => void; +} + +export const PreviewBackgroundToggle = ({ + dark, + onToggle, +}: PreviewBackgroundToggleProps) => ( + + { + event.stopPropagation(); + onToggle(); + }} + sx={{ + position: "absolute", + top: 8, + left: 8, + zIndex: 2, + color: dark ? "#fff" : "#000", + backgroundColor: dark + ? "rgba(255, 255, 255, 0.16)" + : "rgba(255, 255, 255, 0.8)", + "&:hover": { + backgroundColor: dark + ? "rgba(255, 255, 255, 0.28)" + : "rgba(255, 255, 255, 1)", + }, + }} + > + + + +); diff --git a/src/shared/ui/index.ts b/src/shared/ui/index.ts index ce77238..aef6a71 100644 --- a/src/shared/ui/index.ts +++ b/src/shared/ui/index.ts @@ -6,3 +6,4 @@ export * from "./AnimatedCircleButton"; export * from "./LoadingSpinner"; export * from "./MultiSelect"; export * from "./SearchInput"; +export * from "./PreviewBackgroundToggle"; diff --git a/src/widgets/ImageUploadCard/index.tsx b/src/widgets/ImageUploadCard/index.tsx index 6c48cad..22d81e5 100644 --- a/src/widgets/ImageUploadCard/index.tsx +++ b/src/widgets/ImageUploadCard/index.tsx @@ -1,7 +1,7 @@ -import React, { useRef, DragEvent } from "react"; +import React, { useRef, useState, DragEvent } from "react"; import { Paper, Box, Typography, Button, Tooltip } from "@mui/material"; import { X, Info, Plus } from "lucide-react"; -import { editSightStore } from "@shared"; +import { editSightStore, PreviewBackgroundToggle } from "@shared"; import { toast } from "react-toastify"; interface ImageUploadCardProps { title: string; @@ -33,6 +33,7 @@ export const ImageUploadCard: React.FC = ({ tooltipText, }) => { const fileInputRef = useRef(null); + const [isPreviewBackgroundDark, setIsPreviewBackgroundDark] = useState(false); const { setFileToUpload } = editSightStore; const handleZoneClick = () => { @@ -124,9 +125,21 @@ export const ImageUploadCard: React.FC = ({ borderRadius: 1, mb: 1, cursor: imageUrl ? "pointer" : "default", + backgroundColor: imageUrl + ? isPreviewBackgroundDark + ? "#000" + : "#fff" + : "transparent", + transition: "background-color 150ms ease", }} onClick={onImageClick} > + {imageUrl && ( + setIsPreviewBackgroundDark((value) => !value)} + /> + )} {imageUrl && (