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 && (
-
- )}
- {
- 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}
- />
-
- {
- 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 && (