feature/webgl-preview-update #21

Merged
Kerblif merged 78 commits from feature/webgl-preview-update into main 2026-10-13 16:49:19 +00:00
18 changed files with 365 additions and 599 deletions
Showing only changes of commit de03847187 - Show all commits

View File

@@ -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}
/>
) : (
<div>

View File

@@ -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;

View File

@@ -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" }}

View File

@@ -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",
}}
>
<img
className="side-menu-crest"
src={designData?.creastPath || defaultCrest}
alt="Герб"
/>
{carrier?.slogan && (
<div className="side-menu-label">{carrier.slogan}</div>
)}
{shouldRenderSights && (
<SightsList
ref={sightsListRef}
@@ -606,19 +582,16 @@ const SideMenu = observer(({ onMenuToggle }) => {
: "#EveryoneOnTheWay"} */}
</div>
<div className="side-menu-bottom-section">
<div className="side-menu-carrier-block">
{designData?.carrierPath && (
<img
className="carrier-logo"
src={designData?.carrierPath}
alt="ГЭТ - Электротранспорт Санкт-Петербурга"
/>
)}
{carrier?.full_name && (
<div className="side-menu-slogan">{carrier.short_name}</div>
)}
</div>
<img className="side-menu-bottom-photo" src={sideMenuPhoto} alt="" />
<img
className="side-menu-bottom-photo"
src={
carrier?.preview_media && !isPreviewMediaError
? getMediaUrl(carrier.preview_media)
: sideMenuPhoto
}
onError={() => setIsPreviewMediaError(true)}
alt=""
/>
</div>
</div>
<div className="side-menu-control-panel">

View File

@@ -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;

View File

@@ -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<string | null>(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 (
<Paper className="w-full h-full p-3 flex flex-col gap-10">
<div className="w-full h-full p-3 flex flex-col gap-10">
<LanguageSwitcher />
<div className="flex items-center gap-4">
<button
@@ -212,8 +222,6 @@ export const CarrierCreatePage = observer(() => {
createCarrierData[language].full_name,
createCarrierData[language].short_name,
e.target.value as number,
createCarrierData[language].slogan,
selectedMediaId || "",
language,
)
}
@@ -236,8 +244,6 @@ export const CarrierCreatePage = observer(() => {
e.target.value,
createCarrierData[language].short_name,
createCarrierData.city_id,
createCarrierData[language].slogan,
selectedMediaId || "",
language,
)
}
@@ -253,24 +259,6 @@ export const CarrierCreatePage = observer(() => {
createCarrierData[language].full_name,
e.target.value,
createCarrierData.city_id,
createCarrierData[language].slogan,
selectedMediaId || "",
language,
)
}
/>
<TextField
fullWidth
label="Слоган"
value={createCarrierData[language].slogan}
onChange={(e) =>
setCreateCarrierData(
createCarrierData[language].full_name,
createCarrierData[language].short_name,
createCarrierData.city_id,
e.target.value,
selectedMediaId || "",
language,
)
}
@@ -286,8 +274,6 @@ export const CarrierCreatePage = observer(() => {
createCarrierData[language].full_name,
createCarrierData[language].short_name,
createCarrierData.city_id,
createCarrierData[language].slogan,
selectedMediaId || "",
language,
{ ...colorFields(createCarrierData), main_color: val }
)
@@ -306,8 +292,6 @@ export const CarrierCreatePage = observer(() => {
createCarrierData[language].full_name,
createCarrierData[language].short_name,
createCarrierData.city_id,
createCarrierData[language].slogan,
selectedMediaId || "",
language,
{ ...colorFields(createCarrierData), left_color: val }
)
@@ -326,8 +310,6 @@ export const CarrierCreatePage = observer(() => {
createCarrierData[language].full_name,
createCarrierData[language].short_name,
createCarrierData.city_id,
createCarrierData[language].slogan,
selectedMediaId || "",
language,
{ ...colorFields(createCarrierData), right_color: val }
)
@@ -339,36 +321,57 @@ export const CarrierCreatePage = observer(() => {
</div>
</div>
<div className="w-full flex flex-col gap-4 max-w-[300px] mx-auto">
<ImageUploadCard
title="Логотип перевозчика"
imageKey="thumbnail"
imageUrl={effectiveLogoUrl}
onImageClick={() => {
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");
}}
/>
<div className="w-full flex justify-center items-start gap-10 flex-wrap">
<div className="w-full max-w-[300px]">
<ImageUploadCard
title="Изображение сайд-меню"
imageKey="image"
imageUrl={effectivePreviewMediaUrl}
onImageClick={() => {
setIsPreviewMediaOpen(true);
setMediaId(effectivePreviewMediaUrl ?? "");
}}
onDeleteImageClick={() =>
setCreateCarrierMedia("preview_media", "")
}
onSelectFileClick={() => {
setActiveMediaField("preview_media");
setActiveMenuType("image");
setIsSelectMediaOpen(true);
}}
setUploadMediaOpen={() => {
setActiveMediaField("preview_media");
setActiveMenuType("image");
setIsUploadMediaOpen(true);
}}
/>
</div>
<div className="w-full max-w-[300px]">
<VideoPreviewCard
title="Видео загрузки"
videoId={effectiveVideoPreviewId}
onVideoClick={() => {
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"
/>
</div>
</div>
<Button
@@ -395,7 +398,7 @@ export const CarrierCreatePage = observer(() => {
open={isSelectMediaOpen}
onClose={() => setIsSelectMediaOpen(false)}
onSelectMedia={handleMediaSelect}
mediaType={1}
mediaType={activeMediaField === "video_preview" ? 2 : 1}
/>
<UploadMediaDialog
@@ -412,6 +415,6 @@ export const CarrierCreatePage = observer(() => {
onClose={() => setIsPreviewMediaOpen(false)}
mediaId={mediaId}
/>
</Paper>
</div>
);
});

View File

@@ -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 (
<Paper className="w-full h-full p-3 flex flex-col gap-10">
<div className="w-full h-full p-3 flex flex-col gap-10">
<LanguageSwitcher />
<div className="flex items-center gap-4">
<button
@@ -275,8 +289,6 @@ export const CarrierEditPage = observer(() => {
editCarrierData[language].full_name,
editCarrierData[language].short_name,
Number(e.target.value),
editCarrierData[language].slogan,
editCarrierData.logo,
language,
)
}
@@ -299,8 +311,6 @@ export const CarrierEditPage = observer(() => {
e.target.value,
editCarrierData[language].short_name,
editCarrierData.city_id,
editCarrierData[language].slogan,
editCarrierData.logo,
language,
)
}
@@ -316,24 +326,6 @@ export const CarrierEditPage = observer(() => {
editCarrierData[language].full_name,
e.target.value,
editCarrierData.city_id,
editCarrierData[language].slogan,
editCarrierData.logo,
language,
)
}
/>
<TextField
fullWidth
label="Слоган"
value={editCarrierData[language].slogan}
onChange={(e) =>
setEditCarrierData(
editCarrierData[language].full_name,
editCarrierData[language].short_name,
editCarrierData.city_id,
e.target.value,
editCarrierData.logo,
language,
)
}
@@ -349,8 +341,6 @@ export const CarrierEditPage = observer(() => {
editCarrierData[language].full_name,
editCarrierData[language].short_name,
editCarrierData.city_id,
editCarrierData[language].slogan,
editCarrierData.logo,
language,
{ ...colorFields(editCarrierData), main_color: val },
)
@@ -370,8 +360,6 @@ export const CarrierEditPage = observer(() => {
editCarrierData[language].full_name,
editCarrierData[language].short_name,
editCarrierData.city_id,
editCarrierData[language].slogan,
editCarrierData.logo,
language,
{ ...colorFields(editCarrierData), left_color: val },
)
@@ -391,8 +379,6 @@ export const CarrierEditPage = observer(() => {
editCarrierData[language].full_name,
editCarrierData[language].short_name,
editCarrierData.city_id,
editCarrierData[language].slogan,
editCarrierData.logo,
language,
{ ...colorFields(editCarrierData), right_color: val },
)
@@ -405,27 +391,57 @@ export const CarrierEditPage = observer(() => {
</div>
</div>
<div className="w-full flex flex-col gap-4 max-w-[300px] mx-auto">
<ImageUploadCard
title="Логотип перевозчика"
imageKey="thumbnail"
imageUrl={effectiveLogoUrl}
onImageClick={() => {
setIsPreviewMediaOpen(true);
setMediaId(effectiveLogoUrl ?? "");
}}
onDeleteImageClick={() => {
setIsDeleteLogoModalOpen(true);
}}
onSelectFileClick={() => {
setActiveMenuType("image");
setIsSelectMediaOpen(true);
}}
setUploadMediaOpen={() => {
setIsUploadMediaOpen(true);
setActiveMenuType("image");
}}
/>
<div className="w-full flex justify-center items-start gap-10 flex-wrap">
<div className="w-full max-w-[300px]">
<ImageUploadCard
title="Изображение сайд-меню"
imageKey="image"
imageUrl={effectivePreviewMediaUrl}
onImageClick={() => {
setIsPreviewMediaOpen(true);
setMediaId(effectivePreviewMediaUrl ?? "");
}}
onDeleteImageClick={() =>
setEditCarrierMedia("preview_media", "")
}
onSelectFileClick={() => {
setActiveMediaField("preview_media");
setActiveMenuType("image");
setIsSelectMediaOpen(true);
}}
setUploadMediaOpen={() => {
setActiveMediaField("preview_media");
setActiveMenuType("image");
setIsUploadMediaOpen(true);
}}
/>
</div>
<div className="w-full max-w-[300px]">
<VideoPreviewCard
title="Видео загрузки"
videoId={effectiveVideoPreviewId}
onVideoClick={() => {
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"
/>
</div>
</div>
<Button
@@ -451,7 +467,7 @@ export const CarrierEditPage = observer(() => {
open={isSelectMediaOpen}
onClose={() => setIsSelectMediaOpen(false)}
onSelectMedia={handleMediaSelect}
mediaType={1}
mediaType={activeMediaField === "video_preview" ? 2 : 1}
/>
<UploadMediaDialog
@@ -469,22 +485,6 @@ export const CarrierEditPage = observer(() => {
mediaId={mediaId}
/>
<DeleteModal
open={isDeleteLogoModalOpen}
onDelete={() => {
setEditCarrierData(
editCarrierData[language].full_name,
editCarrierData[language].short_name,
editCarrierData.city_id,
editCarrierData[language].slogan,
"",
language,
);
setIsDeleteLogoModalOpen(false);
}}
onCancel={() => setIsDeleteLogoModalOpen(false)}
edit
/>
</Paper>
</div>
);
});

View File

@@ -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<string>("");
const [icon, setIcon] = useState<string>("");
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<File | null>(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(() => {
}}
/>
</div>
<div className="flex flex-col gap-4 max-w-[300px]">
<VideoPreviewCard
title="Видеозаставка"
videoId={effectiveVideoId}
onVideoClick={handleVideoPreviewClick}
onDeleteVideoClick={() => setVideoPreview("")}
onSelectVideoClick={handleVideoFileSelect}
className="w-full"
/>
</div>
</Box>
<FormControl fullWidth required>
@@ -578,7 +519,7 @@ export const RouteCreatePage = observer(() => {
/>
<TextField
className="w-full"
label="Таймер видео заставки (сек)"
label="Таймер показа видео при бездействии (сек)"
type="number"
value={videoTimer}
onChange={(e) => {
@@ -611,51 +552,6 @@ export const RouteCreatePage = observer(() => {
onClose={() => setIsSelectArticleDialogOpen(false)}
onSelectArticle={handleArticleSelect}
/>
<SelectMediaDialog
open={isSelectVideoDialogOpen}
onClose={() => setIsSelectVideoDialogOpen(false)}
onSelectMedia={handleVideoSelect}
mediaType={2}
/>
{effectiveVideoId && (
<Dialog
open={isVideoPreviewOpen}
onClose={() => setIsVideoPreviewOpen(false)}
maxWidth="md"
fullWidth
>
<DialogTitle>Предпросмотр видео</DialogTitle>
<DialogContent>
<Box className="flex justify-center items-center p-4">
<MediaViewer
media={{
id: effectiveVideoId,
media_type: 2,
filename: "video_preview",
}}
/>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={() => setIsVideoPreviewOpen(false)}>
Закрыть
</Button>
</DialogActions>
</Dialog>
)}
<UploadMediaDialog
open={isUploadVideoDialogOpen}
onClose={() => {
setIsUploadVideoDialogOpen(false);
setFileToUpload(null);
}}
hardcodeType="video_preview"
contextObjectName={routeName || "Маршрут"}
contextType="sight"
initialFile={fileToUpload || undefined}
afterUpload={handleVideoUpload}
/>
<SelectMediaDialog
open={isSelectIconDialogOpen}
onClose={() => setIsSelectIconDialogOpen(false)}

View File

@@ -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<File | null>(null);
const { language } = languageStore;
const [coordinates, setCoordinates] = useState<string>("");
@@ -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(() => {
/>
<TextField
className="w-full"
label="Таймер видео заставки (сек)"
label="Таймер показа видео при бездействии (сек)"
type="number"
value={editRouteData.video_timer ?? 420}
onChange={(e) => {
@@ -646,18 +593,6 @@ export const RouteEditPage = observer(() => {
}}
/>
</div>
<div className="flex flex-col gap-4 max-w-[300px]">
<VideoPreviewCard
title="Видеозаставка"
videoId={effectiveVideoId}
onVideoClick={handleVideoPreviewClick}
onDeleteVideoClick={() => {
routeStore.setEditRouteData({ video_preview: "" });
}}
onSelectVideoClick={handleVideoFileSelect}
className="w-full"
/>
</div>
</Box>
</Box>
@@ -703,49 +638,6 @@ export const RouteEditPage = observer(() => {
onClose={() => setIsSelectArticleDialogOpen(false)}
onSelectArticle={handleArticleSelect}
/>
<SelectMediaDialog
open={isSelectVideoDialogOpen}
onClose={() => setIsSelectVideoDialogOpen(false)}
onSelectMedia={handleVideoSelect}
mediaType={2}
/>
<Dialog
open={isVideoPreviewOpen}
onClose={() => setIsVideoPreviewOpen(false)}
maxWidth="md"
fullWidth
>
<DialogTitle>Предпросмотр видео</DialogTitle>
<DialogContent>
<Box className="flex justify-center items-center p-4">
{effectiveVideoId && (
<MediaViewer
media={{
id: effectiveVideoId,
media_type: 2,
filename: "video_preview",
}}
/>
)}
</Box>
</DialogContent>
<DialogActions>
<Button onClick={() => setIsVideoPreviewOpen(false)}>Закрыть</Button>
</DialogActions>
</Dialog>
<UploadMediaDialog
open={isUploadVideoDialogOpen}
onClose={() => {
setIsUploadVideoDialogOpen(false);
setFileToUpload(null);
}}
hardcodeType="video_preview"
contextObjectName={editRouteData.route_name || "Маршрут"}
contextType="sight"
initialFile={fileToUpload || undefined}
afterUpload={handleVideoUpload}
/>
<SelectMediaDialog
open={isSelectIconDialogOpen}
onClose={() => setIsSelectIconDialogOpen(false)}

View File

@@ -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<string | null>(null);
const [carrierSlogan, setCarrierSlogan] = useState<string | null>(null);
const [carrierShortName, setCarrierShortName] = useState<string | null>(null);
const [previewMedia, setPreviewMedia] = useState<string | null>(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 */}
<div
style={{
width: 170,
height: 170,
alignSelf: "flex-start",
marginLeft: 20,
backgroundColor: "rgba(255,255,255,0.15)",
borderRadius: 8,
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "rgba(255,255,255,0.5)",
fontSize: 14,
fontWeight: 500,
}}
>
Герб
</div>
{/* Слоган — .side-menu-label */}
{carrierSlogan && (
<div
style={{
marginTop: 10,
textAlign: "left",
fontSize: 15,
padding: "0 20px",
alignSelf: "flex-start",
fontWeight: 400,
lineHeight: "150%",
}}
>
{carrierSlogan}
</div>
)}
{/* Кнопки — .side-menu-buttons */}
<div
style={{
@@ -175,39 +135,14 @@ export const LeftSidebar = observer(({ open, onToggle }: LeftSidebarProps) => {
flexDirection: "column",
}}
>
{/* .side-menu-carrier-block */}
<div style={{ padding: "0 20px" }}>
{carrierLogo && !isMediaIdEmpty(carrierLogo) && (
<div style={{ width: 170 }}>
<MediaViewer
media={{
id: carrierLogo,
media_type: 1,
filename: "carrier_logo",
}}
fullWidth
/>
</div>
)}
{carrierShortName && (
<div
style={{
marginTop: 4,
textAlign: "left",
fontSize: 16,
fontWeight: 700,
lineHeight: "150%",
color: "#fff",
}}
>
{carrierShortName}
</div>
)}
</div>
{/* .side-menu-bottom-photo */}
<img
src="/side-menu-photo.png"
src={
previewMedia && !isPreviewMediaError
? `${import.meta.env.VITE_KRBL_MEDIA}${previewMedia}/download?token=${localStorage.getItem("token") ?? ""}`
: "/side-menu-photo.png"
}
onError={() => setIsPreviewMediaError(true)}
alt=""
style={{
width: "288px",

View File

@@ -131,7 +131,7 @@ export const SnapshotListPage = observer(() => {
},
{
field: "created_at",
headerName: "Дата создания",
headerName: "Дата создания (UTC+03:00, MSK)",
flex: 1,
renderCell: (params: GridRenderCellParams) => {
return <div>{params.value ? params.value : "-"}</div>;

View File

@@ -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<string | null>(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) && (
<PreviewBackgroundToggle
dark={isPreviewBackgroundDark}
onToggle={() =>
setIsPreviewBackgroundDark((value) => !value)
}
/>
)}
<MediaViewer
media={{
id: mediaId,

View File

@@ -6,6 +6,7 @@ import {
clearBlobAndGLTFCache,
authStore,
snapshotStore,
PreviewBackgroundToggle,
} from "@shared";
import { observer } from "mobx-react-lite";
import { useEffect, useState, useRef } from "react";
@@ -90,6 +91,8 @@ export const UploadMediaDialog = observer(
[]
);
const [isPreviewLoaded, setIsPreviewLoaded] = useState(false);
const [isPreviewBackgroundDark, setIsPreviewBackgroundDark] =
useState(false);
const previousMediaUrlRef = useRef<string | null>(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 && (
<PreviewBackgroundToggle
dark={isPreviewBackgroundDark}
onToggle={() =>
setIsPreviewBackgroundDark((value) => !value)
}
/>
)}
{!isPreviewLoaded && mediaUrl && (
<Box
sx={{

View File

@@ -12,10 +12,10 @@ export type Carrier = {
id: number;
short_name: string;
full_name: string;
slogan: string;
city: string;
city_id: number;
logo: string;
preview_media?: string;
video_preview?: string;
main_color: string;
left_color: string;
right_color: string;
@@ -111,38 +111,40 @@ class CarrierStore {
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: "",
},
};
setCreateCarrierMedia = (
field: "preview_media" | "video_preview",
mediaId: string
) => {
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 } : {}),

View File

@@ -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

View File

@@ -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) => (
<Tooltip title={dark ? "Сделать фон белым" : "Сделать фон чёрным"}>
<IconButton
size="small"
aria-label={dark ? "Сделать фон белым" : "Сделать фон чёрным"}
onClick={(event) => {
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)",
},
}}
>
<Contrast size={18} />
</IconButton>
</Tooltip>
);

View File

@@ -6,3 +6,4 @@ export * from "./AnimatedCircleButton";
export * from "./LoadingSpinner";
export * from "./MultiSelect";
export * from "./SearchInput";
export * from "./PreviewBackgroundToggle";

View File

@@ -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<ImageUploadCardProps> = ({
tooltipText,
}) => {
const fileInputRef = useRef<HTMLInputElement>(null);
const [isPreviewBackgroundDark, setIsPreviewBackgroundDark] = useState(false);
const { setFileToUpload } = editSightStore;
const handleZoneClick = () => {
@@ -124,9 +125,21 @@ export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
borderRadius: 1,
mb: 1,
cursor: imageUrl ? "pointer" : "default",
backgroundColor: imageUrl
? isPreviewBackgroundDark
? "#000"
: "#fff"
: "transparent",
transition: "background-color 150ms ease",
}}
onClick={onImageClick}
>
{imageUrl && (
<PreviewBackgroundToggle
dark={isPreviewBackgroundDark}
onToggle={() => setIsPreviewBackgroundDark((value) => !value)}
/>
)}
{imageUrl && (
<button
className="absolute top-2 right-2"