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, loadingStatus,
loadingProgress, loadingProgress,
context, context,
carrier,
setIsLoading, setIsLoading,
setLoadingStatus, setLoadingStatus,
setLoadingProgress, setLoadingProgress,
@@ -172,6 +173,7 @@ const App = observer(() => {
loadingStatus={loadingStatus} loadingStatus={loadingStatus}
loadingProgress={loadingProgress} loadingProgress={loadingProgress}
onToggleDebug={() => setIsDebugVisible((prev) => !prev)} onToggleDebug={() => setIsDebugVisible((prev) => !prev)}
videoId={carrier?.video_preview}
/> />
) : ( ) : (
<div> <div>

View File

@@ -64,7 +64,6 @@ export type GetRouteResponse = {
route_sys_number: string; route_sys_number: string;
scale_max: number; scale_max: number;
scale_min: number; scale_min: number;
video_preview: string;
video_timer?: number; video_timer?: number;
icon?: string; icon?: string;
icon_size?: number; icon_size?: number;
@@ -77,6 +76,8 @@ export type GetCarrierResponse = {
full_name: string; full_name: string;
id: number; id: number;
logo: string; logo: string;
preview_media?: string;
video_preview?: string;
short_name: string; short_name: string;
slogan: string; slogan: string;
main_color?: string; main_color?: string;

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import "../styles/Loader.css"; import "../styles/Loader.css";
import { getMediaUrl } from "../api/apiConfig";
const loaderVideoPath = new URL("../assets/video/taganai.mp4", import.meta.url) const loaderVideoPath = new URL("../assets/video/taganai.mp4", import.meta.url)
.href; .href;
@@ -9,8 +10,14 @@ const Loader = ({
loadingStatus = null, loadingStatus = null,
loadingProgress = null, loadingProgress = null,
onToggleDebug, onToggleDebug,
videoId = null,
}) => { }) => {
const [showLoadingInfo, setShowLoadingInfo] = useState(false); const [showLoadingInfo, setShowLoadingInfo] = useState(false);
const [isCustomVideoError, setIsCustomVideoError] = useState(false);
useEffect(() => {
setIsCustomVideoError(false);
}, [videoId]);
useEffect(() => { useEffect(() => {
const handleKeyDown = (event) => { const handleKeyDown = (event) => {
@@ -55,7 +62,12 @@ const Loader = ({
loop loop
muted muted
playsInline playsInline
src={loaderVideoPath} src={
videoId && !isCustomVideoError
? getMediaUrl(videoId)
: loaderVideoPath
}
onError={() => setIsCustomVideoError(true)}
alt="Loading" alt="Loading"
className="error-gif" className="error-gif"
style={{ width: "100%", height: "100%", objectFit: "cover" }} style={{ width: "100%", height: "100%", objectFit: "cover" }}

View File

@@ -13,12 +13,10 @@ import StationsList from "./StationsList";
import LeftWidget from "./LeftWidget"; import LeftWidget from "./LeftWidget";
import { apiStore } from "../../api/ApiStore/store"; import { apiStore } from "../../api/ApiStore/store";
import { getMediaUrl } from "../../api/apiConfig"; import { getMediaUrl } from "../../api/apiConfig";
import defaultCrest from "../../assets/images/Герб.png";
const SideMenu = observer(({ onMenuToggle }) => { const SideMenu = observer(({ onMenuToggle }) => {
const { const {
carrier, carrier,
city,
route, route,
getArticle, getArticle,
sightArticles, sightArticles,
@@ -33,6 +31,7 @@ const SideMenu = observer(({ onMenuToggle }) => {
const [isStationOpen, openStation] = useState(false); const [isStationOpen, openStation] = useState(false);
const [isWidgetOpen, openWidget] = useState(false); const [isWidgetOpen, openWidget] = useState(false);
const [isWeatherVisible, setIsWeatherVisible] = useState(true); const [isWeatherVisible, setIsWeatherVisible] = useState(true);
const [isPreviewMediaError, setIsPreviewMediaError] = useState(false);
const [isLeftWidgetVisible, setIsLeftWidgetVisible] = useState(false); const [isLeftWidgetVisible, setIsLeftWidgetVisible] = useState(false);
const [localSelectedSightId, setLocalSelectedSightId] = useState(null); const [localSelectedSightId, setLocalSelectedSightId] = useState(null);
const [isSightsAnimating, setIsSightsAnimating] = useState(false); const [isSightsAnimating, setIsSightsAnimating] = useState(false);
@@ -186,27 +185,12 @@ const SideMenu = observer(({ onMenuToggle }) => {
} }
}, [isLeftWidgetVisible]); }, [isLeftWidgetVisible]);
const [designData, setDesignData] = useState(null);
const [routeData, setRouteData] = useState(null); const [routeData, setRouteData] = useState(null);
const [isLangMenuOpen, setIsLangMenuOpen] = useState(false); const [isLangMenuOpen, setIsLangMenuOpen] = useState(false);
useEffect(() => { useEffect(() => {
async function fetchCarrierAndCrest() { setIsPreviewMediaError(false);
try { }, [carrier?.preview_media]);
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]);
useEffect(() => { useEffect(() => {
if (isGeolocationLoading || !contextData?.routeId) { 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", "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 && ( {shouldRenderSights && (
<SightsList <SightsList
ref={sightsListRef} ref={sightsListRef}
@@ -606,19 +582,16 @@ const SideMenu = observer(({ onMenuToggle }) => {
: "#EveryoneOnTheWay"} */} : "#EveryoneOnTheWay"} */}
</div> </div>
<div className="side-menu-bottom-section"> <div className="side-menu-bottom-section">
<div className="side-menu-carrier-block">
{designData?.carrierPath && (
<img <img
className="carrier-logo" className="side-menu-bottom-photo"
src={designData?.carrierPath} src={
alt="ГЭТ - Электротранспорт Санкт-Петербурга" carrier?.preview_media && !isPreviewMediaError
? getMediaUrl(carrier.preview_media)
: sideMenuPhoto
}
onError={() => setIsPreviewMediaError(true)}
alt=""
/> />
)}
{carrier?.full_name && (
<div className="side-menu-slogan">{carrier.short_name}</div>
)}
</div>
<img className="side-menu-bottom-photo" src={sideMenuPhoto} alt="" />
</div> </div>
</div> </div>
<div className="side-menu-control-panel"> <div className="side-menu-control-panel">

View File

@@ -16,17 +16,6 @@
var(--carrier-left, #006f3a); 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 { .appeal-button {
color: black; color: black;
padding: 8px 16px; padding: 8px 16px;
@@ -66,24 +55,6 @@
flex-direction: column; 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 { .side-menu-bottom-photo {
width: 100%; width: 100%;
margin-top: 32px; margin-top: 32px;
@@ -284,12 +255,6 @@
} }
} }
.side-menu-crest {
width: 170px;
align-self: flex-start;
margin-left: 20px;
}
.side-menu-control-lang-open { .side-menu-control-lang-open {
gap: 10px; gap: 10px;
display: inline-flex; display: inline-flex;

View File

@@ -1,6 +1,5 @@
import { import {
Button, Button,
Paper,
TextField, TextField,
Select, Select,
MenuItem, MenuItem,
@@ -24,9 +23,10 @@ import {
SelectMediaDialog, SelectMediaDialog,
UploadMediaDialog, UploadMediaDialog,
PreviewMediaDialog, PreviewMediaDialog,
editSightStore,
} from "@shared"; } from "@shared";
import { useState, useEffect } from "react"; 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 }; type ColorFields = { main_color: string; left_color: string; right_color: string };
@@ -80,18 +80,29 @@ const ColorPickerField = ({
export const CarrierCreatePage = observer(() => { export const CarrierCreatePage = observer(() => {
const navigate = useNavigate(); const navigate = useNavigate();
const { createCarrierData, setCreateCarrierData } = carrierStore; const {
createCarrierData,
setCreateCarrierData,
setCreateCarrierMedia,
} = carrierStore;
const { language } = languageStore; const { language } = languageStore;
const canReadCities = authStore.canRead("cities"); const canReadCities = authStore.canRead("cities");
const { selectedCityId, selectedCity } = useSelectedCity(); const { selectedCityId, selectedCity } = useSelectedCity();
const [selectedMediaId, setSelectedMediaId] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false); const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false);
const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false); const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false);
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false); const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
const [mediaId, setMediaId] = useState(""); const [mediaId, setMediaId] = useState("");
const [activeMediaField, setActiveMediaField] = useState<
"preview_media" | "video_preview"
>("preview_media");
const [activeMenuType, setActiveMenuType] = useState< const [activeMenuType, setActiveMenuType] = useState<
"thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null | "thumbnail"
| "watermark_lu"
| "watermark_rd"
| "image"
| "video_preview"
| null
>(null); >(null);
useEffect(() => { useEffect(() => {
@@ -137,12 +148,16 @@ export const CarrierCreatePage = observer(() => {
createCarrierData[language].full_name, createCarrierData[language].full_name,
createCarrierData[language].short_name, createCarrierData[language].short_name,
selectedCityId, selectedCityId,
createCarrierData[language].slogan,
selectedMediaId || "",
language, language,
); );
} }
}, [selectedCityId, createCarrierData.city_id]); }, [
selectedCityId,
createCarrierData,
createCarrierData.city_id,
language,
setCreateCarrierData,
]);
const handleCreate = async () => { const handleCreate = async () => {
try { try {
@@ -151,7 +166,7 @@ export const CarrierCreatePage = observer(() => {
toast.success("Перевозчик успешно создан"); toast.success("Перевозчик успешно создан");
navigate("/carrier"); navigate("/carrier");
} catch (error) { } catch {
toast.error("Ошибка при создании перевозчика"); toast.error("Ошибка при создании перевозчика");
} finally { } finally {
setIsLoading(false); setIsLoading(false);
@@ -164,27 +179,22 @@ export const CarrierCreatePage = observer(() => {
media_name?: string; media_name?: string;
media_type: number; media_type: number;
}) => { }) => {
setSelectedMediaId(media.id); setCreateCarrierMedia(activeMediaField, media.id);
setCreateCarrierData(
createCarrierData[language].full_name,
createCarrierData[language].short_name,
createCarrierData.city_id,
createCarrierData[language].slogan,
media.id,
language,
);
}; };
const selectedMedia = const effectivePreviewMediaUrl = isMediaIdEmpty(
selectedMediaId && !isMediaIdEmpty(selectedMediaId) createCarrierData.preview_media,
? mediaStore.media.find((m) => m.id === selectedMediaId) )
: null;
const effectiveLogoUrl = isMediaIdEmpty(selectedMediaId)
? null ? null
: selectedMedia?.id ?? selectedMediaId ?? null; : createCarrierData.preview_media;
const effectiveVideoPreviewId = isMediaIdEmpty(
createCarrierData.video_preview,
)
? null
: createCarrierData.video_preview;
return ( 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 /> <LanguageSwitcher />
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<button <button
@@ -212,8 +222,6 @@ export const CarrierCreatePage = observer(() => {
createCarrierData[language].full_name, createCarrierData[language].full_name,
createCarrierData[language].short_name, createCarrierData[language].short_name,
e.target.value as number, e.target.value as number,
createCarrierData[language].slogan,
selectedMediaId || "",
language, language,
) )
} }
@@ -236,8 +244,6 @@ export const CarrierCreatePage = observer(() => {
e.target.value, e.target.value,
createCarrierData[language].short_name, createCarrierData[language].short_name,
createCarrierData.city_id, createCarrierData.city_id,
createCarrierData[language].slogan,
selectedMediaId || "",
language, language,
) )
} }
@@ -253,24 +259,6 @@ export const CarrierCreatePage = observer(() => {
createCarrierData[language].full_name, createCarrierData[language].full_name,
e.target.value, e.target.value,
createCarrierData.city_id, 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, language,
) )
} }
@@ -286,8 +274,6 @@ export const CarrierCreatePage = observer(() => {
createCarrierData[language].full_name, createCarrierData[language].full_name,
createCarrierData[language].short_name, createCarrierData[language].short_name,
createCarrierData.city_id, createCarrierData.city_id,
createCarrierData[language].slogan,
selectedMediaId || "",
language, language,
{ ...colorFields(createCarrierData), main_color: val } { ...colorFields(createCarrierData), main_color: val }
) )
@@ -306,8 +292,6 @@ export const CarrierCreatePage = observer(() => {
createCarrierData[language].full_name, createCarrierData[language].full_name,
createCarrierData[language].short_name, createCarrierData[language].short_name,
createCarrierData.city_id, createCarrierData.city_id,
createCarrierData[language].slogan,
selectedMediaId || "",
language, language,
{ ...colorFields(createCarrierData), left_color: val } { ...colorFields(createCarrierData), left_color: val }
) )
@@ -326,8 +310,6 @@ export const CarrierCreatePage = observer(() => {
createCarrierData[language].full_name, createCarrierData[language].full_name,
createCarrierData[language].short_name, createCarrierData[language].short_name,
createCarrierData.city_id, createCarrierData.city_id,
createCarrierData[language].slogan,
selectedMediaId || "",
language, language,
{ ...colorFields(createCarrierData), right_color: val } { ...colorFields(createCarrierData), right_color: val }
) )
@@ -339,37 +321,58 @@ export const CarrierCreatePage = observer(() => {
</div> </div>
</div> </div>
<div className="w-full flex flex-col gap-4 max-w-[300px] mx-auto"> <div className="w-full flex justify-center items-start gap-10 flex-wrap">
<div className="w-full max-w-[300px]">
<ImageUploadCard <ImageUploadCard
title="Логотип перевозчика" title="Изображение сайд-меню"
imageKey="thumbnail" imageKey="image"
imageUrl={effectiveLogoUrl} imageUrl={effectivePreviewMediaUrl}
onImageClick={() => { onImageClick={() => {
setIsPreviewMediaOpen(true); setIsPreviewMediaOpen(true);
setMediaId(effectiveLogoUrl ?? ""); setMediaId(effectivePreviewMediaUrl ?? "");
}}
onDeleteImageClick={() => {
setSelectedMediaId(null);
setActiveMenuType(null);
setCreateCarrierData(
createCarrierData[language].full_name,
createCarrierData[language].short_name,
createCarrierData.city_id,
createCarrierData[language].slogan,
"",
language,
);
}} }}
onDeleteImageClick={() =>
setCreateCarrierMedia("preview_media", "")
}
onSelectFileClick={() => { onSelectFileClick={() => {
setActiveMediaField("preview_media");
setActiveMenuType("image"); setActiveMenuType("image");
setIsSelectMediaOpen(true); setIsSelectMediaOpen(true);
}} }}
setUploadMediaOpen={() => { setUploadMediaOpen={() => {
setIsUploadMediaOpen(true); setActiveMediaField("preview_media");
setActiveMenuType("image"); setActiveMenuType("image");
setIsUploadMediaOpen(true);
}} }}
/> />
</div> </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 <Button
variant="contained" variant="contained"
@@ -395,7 +398,7 @@ export const CarrierCreatePage = observer(() => {
open={isSelectMediaOpen} open={isSelectMediaOpen}
onClose={() => setIsSelectMediaOpen(false)} onClose={() => setIsSelectMediaOpen(false)}
onSelectMedia={handleMediaSelect} onSelectMedia={handleMediaSelect}
mediaType={1} mediaType={activeMediaField === "video_preview" ? 2 : 1}
/> />
<UploadMediaDialog <UploadMediaDialog
@@ -412,6 +415,6 @@ export const CarrierCreatePage = observer(() => {
onClose={() => setIsPreviewMediaOpen(false)} onClose={() => setIsPreviewMediaOpen(false)}
mediaId={mediaId} mediaId={mediaId}
/> />
</Paper> </div>
); );
}); });

View File

@@ -1,6 +1,5 @@
import { import {
Button, Button,
Paper,
TextField, TextField,
Select, Select,
MenuItem, MenuItem,
@@ -22,9 +21,14 @@ import {
isMediaIdEmpty, isMediaIdEmpty,
LoadingSpinner, LoadingSpinner,
selectedCityStore, selectedCityStore,
editSightStore,
} from "@shared"; } from "@shared";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { ImageUploadCard, LanguageSwitcher, DeleteModal } from "@widgets"; import {
ImageUploadCard,
LanguageSwitcher,
VideoPreviewCard,
} from "@widgets";
import { import {
SelectMediaDialog, SelectMediaDialog,
PreviewMediaDialog, PreviewMediaDialog,
@@ -88,7 +92,12 @@ const ColorPickerField = ({
export const CarrierEditPage = observer(() => { export const CarrierEditPage = observer(() => {
const navigate = useNavigate(); const navigate = useNavigate();
const { id } = useParams(); const { id } = useParams();
const { getCarrier, setEditCarrierData, editCarrierData } = carrierStore; const {
getCarrier,
setEditCarrierData,
setEditCarrierMedia,
editCarrierData,
} = carrierStore;
const { language } = languageStore; const { language } = languageStore;
const canReadCities = authStore.canRead("cities"); const canReadCities = authStore.canRead("cities");
@@ -98,10 +107,17 @@ export const CarrierEditPage = observer(() => {
const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false); const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false);
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false); const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
const [mediaId, setMediaId] = useState(""); const [mediaId, setMediaId] = useState("");
const [isDeleteLogoModalOpen, setIsDeleteLogoModalOpen] = useState(false);
const [initialCityName, setInitialCityName] = useState(""); const [initialCityName, setInitialCityName] = useState("");
const [activeMediaField, setActiveMediaField] = useState<
"preview_media" | "video_preview"
>("preview_media");
const [activeMenuType, setActiveMenuType] = useState< const [activeMenuType, setActiveMenuType] = useState<
"thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null | "thumbnail"
| "watermark_lu"
| "watermark_rd"
| "image"
| "video_preview"
| null
>(null); >(null);
useEffect(() => { useEffect(() => {
@@ -137,8 +153,6 @@ export const CarrierEditPage = observer(() => {
carrierData.ru?.full_name || "", carrierData.ru?.full_name || "",
carrierData.ru?.short_name || "", carrierData.ru?.short_name || "",
carrierData.ru?.city_id || 0, carrierData.ru?.city_id || 0,
carrierData.ru?.slogan || "",
carrierData.ru?.logo || "",
"ru", "ru",
colors, colors,
); );
@@ -146,18 +160,22 @@ export const CarrierEditPage = observer(() => {
carrierData.en?.full_name || "", carrierData.en?.full_name || "",
carrierData.en?.short_name || "", carrierData.en?.short_name || "",
carrierData.en?.city_id || 0, carrierData.en?.city_id || 0,
carrierData.en?.slogan || "",
carrierData.en?.logo || "",
"en", "en",
); );
setEditCarrierData( setEditCarrierData(
carrierData.zh?.full_name || "", carrierData.zh?.full_name || "",
carrierData.zh?.short_name || "", carrierData.zh?.short_name || "",
carrierData.zh?.city_id || 0, carrierData.zh?.city_id || 0,
carrierData.zh?.slogan || "",
carrierData.zh?.logo || "",
"zh", "zh",
); );
setEditCarrierMedia(
"video_preview",
carrierData.ru?.video_preview || "",
);
setEditCarrierMedia(
"preview_media",
carrierData.ru?.preview_media || "",
);
setInitialCityName(carrierData.ru?.city || ""); setInitialCityName(carrierData.ru?.city || "");
} }
@@ -168,7 +186,7 @@ export const CarrierEditPage = observer(() => {
})(); })();
languageStore.setLanguage("ru"); languageStore.setLanguage("ru");
}, [id]); }, [id, getCarrier, setEditCarrierData, setEditCarrierMedia]);
const handleEdit = async () => { const handleEdit = async () => {
try { try {
@@ -176,7 +194,7 @@ export const CarrierEditPage = observer(() => {
await carrierStore.editCarrier(Number(id)); await carrierStore.editCarrier(Number(id));
toast.success("Перевозчик успешно обновлен"); toast.success("Перевозчик успешно обновлен");
navigate("/carrier"); navigate("/carrier");
} catch (error) { } catch {
toast.error("Ошибка при обновлении перевозчика"); toast.error("Ошибка при обновлении перевозчика");
} finally { } finally {
setIsLoading(false); setIsLoading(false);
@@ -189,23 +207,19 @@ export const CarrierEditPage = observer(() => {
media_name?: string; media_name?: string;
media_type: number; media_type: number;
}) => { }) => {
setEditCarrierData( setEditCarrierMedia(activeMediaField, media.id);
editCarrierData[language].full_name,
editCarrierData[language].short_name,
editCarrierData.city_id,
editCarrierData[language].slogan,
media.id,
language,
);
}; };
const selectedMedia = const effectivePreviewMediaUrl = isMediaIdEmpty(
editCarrierData.logo && !isMediaIdEmpty(editCarrierData.logo) editCarrierData.preview_media,
? mediaStore.media.find((m) => m.id === editCarrierData.logo) )
: null;
const effectiveLogoUrl = isMediaIdEmpty(editCarrierData.logo)
? null ? null
: (selectedMedia?.id ?? editCarrierData.logo); : editCarrierData.preview_media;
const effectiveVideoPreviewId = isMediaIdEmpty(
editCarrierData.video_preview,
)
? null
: editCarrierData.video_preview;
const baseCities = canReadCities const baseCities = canReadCities
? cityStore.cities["ru"].data ? cityStore.cities["ru"].data
@@ -248,7 +262,7 @@ export const CarrierEditPage = observer(() => {
} }
return ( 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 /> <LanguageSwitcher />
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<button <button
@@ -275,8 +289,6 @@ export const CarrierEditPage = observer(() => {
editCarrierData[language].full_name, editCarrierData[language].full_name,
editCarrierData[language].short_name, editCarrierData[language].short_name,
Number(e.target.value), Number(e.target.value),
editCarrierData[language].slogan,
editCarrierData.logo,
language, language,
) )
} }
@@ -299,8 +311,6 @@ export const CarrierEditPage = observer(() => {
e.target.value, e.target.value,
editCarrierData[language].short_name, editCarrierData[language].short_name,
editCarrierData.city_id, editCarrierData.city_id,
editCarrierData[language].slogan,
editCarrierData.logo,
language, language,
) )
} }
@@ -316,24 +326,6 @@ export const CarrierEditPage = observer(() => {
editCarrierData[language].full_name, editCarrierData[language].full_name,
e.target.value, e.target.value,
editCarrierData.city_id, 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, language,
) )
} }
@@ -349,8 +341,6 @@ export const CarrierEditPage = observer(() => {
editCarrierData[language].full_name, editCarrierData[language].full_name,
editCarrierData[language].short_name, editCarrierData[language].short_name,
editCarrierData.city_id, editCarrierData.city_id,
editCarrierData[language].slogan,
editCarrierData.logo,
language, language,
{ ...colorFields(editCarrierData), main_color: val }, { ...colorFields(editCarrierData), main_color: val },
) )
@@ -370,8 +360,6 @@ export const CarrierEditPage = observer(() => {
editCarrierData[language].full_name, editCarrierData[language].full_name,
editCarrierData[language].short_name, editCarrierData[language].short_name,
editCarrierData.city_id, editCarrierData.city_id,
editCarrierData[language].slogan,
editCarrierData.logo,
language, language,
{ ...colorFields(editCarrierData), left_color: val }, { ...colorFields(editCarrierData), left_color: val },
) )
@@ -391,8 +379,6 @@ export const CarrierEditPage = observer(() => {
editCarrierData[language].full_name, editCarrierData[language].full_name,
editCarrierData[language].short_name, editCarrierData[language].short_name,
editCarrierData.city_id, editCarrierData.city_id,
editCarrierData[language].slogan,
editCarrierData.logo,
language, language,
{ ...colorFields(editCarrierData), right_color: val }, { ...colorFields(editCarrierData), right_color: val },
) )
@@ -405,28 +391,58 @@ export const CarrierEditPage = observer(() => {
</div> </div>
</div> </div>
<div className="w-full flex flex-col gap-4 max-w-[300px] mx-auto"> <div className="w-full flex justify-center items-start gap-10 flex-wrap">
<div className="w-full max-w-[300px]">
<ImageUploadCard <ImageUploadCard
title="Логотип перевозчика" title="Изображение сайд-меню"
imageKey="thumbnail" imageKey="image"
imageUrl={effectiveLogoUrl} imageUrl={effectivePreviewMediaUrl}
onImageClick={() => { onImageClick={() => {
setIsPreviewMediaOpen(true); setIsPreviewMediaOpen(true);
setMediaId(effectiveLogoUrl ?? ""); setMediaId(effectivePreviewMediaUrl ?? "");
}}
onDeleteImageClick={() => {
setIsDeleteLogoModalOpen(true);
}} }}
onDeleteImageClick={() =>
setEditCarrierMedia("preview_media", "")
}
onSelectFileClick={() => { onSelectFileClick={() => {
setActiveMediaField("preview_media");
setActiveMenuType("image"); setActiveMenuType("image");
setIsSelectMediaOpen(true); setIsSelectMediaOpen(true);
}} }}
setUploadMediaOpen={() => { setUploadMediaOpen={() => {
setIsUploadMediaOpen(true); setActiveMediaField("preview_media");
setActiveMenuType("image"); setActiveMenuType("image");
setIsUploadMediaOpen(true);
}} }}
/> />
</div> </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 <Button
variant="contained" variant="contained"
@@ -451,7 +467,7 @@ export const CarrierEditPage = observer(() => {
open={isSelectMediaOpen} open={isSelectMediaOpen}
onClose={() => setIsSelectMediaOpen(false)} onClose={() => setIsSelectMediaOpen(false)}
onSelectMedia={handleMediaSelect} onSelectMedia={handleMediaSelect}
mediaType={1} mediaType={activeMediaField === "video_preview" ? 2 : 1}
/> />
<UploadMediaDialog <UploadMediaDialog
@@ -469,22 +485,6 @@ export const CarrierEditPage = observer(() => {
mediaId={mediaId} mediaId={mediaId}
/> />
<DeleteModal </div>
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>
); );
}); });

View File

@@ -7,16 +7,8 @@ import {
InputLabel, InputLabel,
Typography, Typography,
Box, Box,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
} from "@mui/material"; } from "@mui/material";
import { import { ImageUploadCard } from "@widgets";
MediaViewer,
VideoPreviewCard,
ImageUploadCard,
} from "@widgets";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { ArrowLeft, Loader2, Save, Plus, X } from "lucide-react"; import { ArrowLeft, Loader2, Save, Plus, X } from "lucide-react";
import { useEffect, useState, useMemo } from "react"; import { useEffect, useState, useMemo } from "react";
@@ -59,14 +51,10 @@ export const RouteCreatePage = observer(() => {
const [centerLat, setCenterLat] = useState(""); const [centerLat, setCenterLat] = useState("");
const [centerLng, setCenterLng] = useState(""); const [centerLng, setCenterLng] = useState("");
const [videoTimer, setVideoTimer] = useState(420); const [videoTimer, setVideoTimer] = useState(420);
const [videoPreview, setVideoPreview] = useState<string>("");
const [icon, setIcon] = useState<string>(""); const [icon, setIcon] = useState<string>("");
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isSelectArticleDialogOpen, setIsSelectArticleDialogOpen] = const [isSelectArticleDialogOpen, setIsSelectArticleDialogOpen] =
useState(false); useState(false);
const [isSelectVideoDialogOpen, setIsSelectVideoDialogOpen] = useState(false);
const [isVideoPreviewOpen, setIsVideoPreviewOpen] = useState(false);
const [isUploadVideoDialogOpen, setIsUploadVideoDialogOpen] = useState(false);
const [isSelectIconDialogOpen, setIsSelectIconDialogOpen] = useState(false); const [isSelectIconDialogOpen, setIsSelectIconDialogOpen] = useState(false);
const [isUploadIconDialogOpen, setIsUploadIconDialogOpen] = useState(false); const [isUploadIconDialogOpen, setIsUploadIconDialogOpen] = useState(false);
const [isPreviewIconOpen, setIsPreviewIconOpen] = useState(false); const [isPreviewIconOpen, setIsPreviewIconOpen] = useState(false);
@@ -74,7 +62,6 @@ export const RouteCreatePage = observer(() => {
const [activeIconMenuType, setActiveIconMenuType] = useState< const [activeIconMenuType, setActiveIconMenuType] = useState<
"thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null "thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null
>(null); >(null);
const [fileToUpload, setFileToUpload] = useState<File | null>(null);
const { language } = languageStore; const { language } = languageStore;
useEffect(() => { useEffect(() => {
@@ -140,40 +127,6 @@ export const RouteCreatePage = observer(() => {
articlesStore.getArticleList(); 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: { const handleIconSelect = (media: {
id: string; id: string;
filename: string; filename: string;
@@ -189,7 +142,6 @@ export const RouteCreatePage = observer(() => {
? mediaStore.media.find((m) => m.id === icon) ? mediaStore.media.find((m) => m.id === icon)
: null; : null;
const effectiveIconUrl = isMediaIdEmpty(icon) ? null : selectedIconMedia?.id ?? icon; const effectiveIconUrl = isMediaIdEmpty(icon) ? null : selectedIconMedia?.id ?? icon;
const effectiveVideoId = isMediaIdEmpty(videoPreview) ? null : videoPreview;
const handleCreateRoute = async () => { const handleCreateRoute = async () => {
try { try {
@@ -284,7 +236,6 @@ export const RouteCreatePage = observer(() => {
center_latitude, center_latitude,
center_longitude, center_longitude,
path, path,
video_preview: !isMediaIdEmpty(videoPreview) ? videoPreview : undefined,
icon: !isMediaIdEmpty(icon) ? icon : undefined, icon: !isMediaIdEmpty(icon) ? icon : undefined,
video_timer: videoTimer, video_timer: videoTimer,
}; };
@@ -485,16 +436,6 @@ export const RouteCreatePage = observer(() => {
}} }}
/> />
</div> </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> </Box>
<FormControl fullWidth required> <FormControl fullWidth required>
@@ -578,7 +519,7 @@ export const RouteCreatePage = observer(() => {
/> />
<TextField <TextField
className="w-full" className="w-full"
label="Таймер видео заставки (сек)" label="Таймер показа видео при бездействии (сек)"
type="number" type="number"
value={videoTimer} value={videoTimer}
onChange={(e) => { onChange={(e) => {
@@ -611,51 +552,6 @@ export const RouteCreatePage = observer(() => {
onClose={() => setIsSelectArticleDialogOpen(false)} onClose={() => setIsSelectArticleDialogOpen(false)}
onSelectArticle={handleArticleSelect} 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 <SelectMediaDialog
open={isSelectIconDialogOpen} open={isSelectIconDialogOpen}
onClose={() => setIsSelectIconDialogOpen(false)} onClose={() => setIsSelectIconDialogOpen(false)}

View File

@@ -7,14 +7,8 @@ import {
InputLabel, InputLabel,
Typography, Typography,
Box, Box,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
} from "@mui/material"; } from "@mui/material";
import { import {
MediaViewer,
VideoPreviewCard,
ImageUploadCard, ImageUploadCard,
DeleteModal, DeleteModal,
} from "@widgets"; } from "@widgets";
@@ -54,9 +48,6 @@ export const RouteEditPage = observer(() => {
const [isLoadingData, setIsLoadingData] = useState(true); const [isLoadingData, setIsLoadingData] = useState(true);
const [isSelectArticleDialogOpen, setIsSelectArticleDialogOpen] = const [isSelectArticleDialogOpen, setIsSelectArticleDialogOpen] =
useState(false); useState(false);
const [isSelectVideoDialogOpen, setIsSelectVideoDialogOpen] = useState(false);
const [isVideoPreviewOpen, setIsVideoPreviewOpen] = useState(false);
const [isUploadVideoDialogOpen, setIsUploadVideoDialogOpen] = useState(false);
const [isSelectIconDialogOpen, setIsSelectIconDialogOpen] = useState(false); const [isSelectIconDialogOpen, setIsSelectIconDialogOpen] = useState(false);
const [isUploadIconDialogOpen, setIsUploadIconDialogOpen] = useState(false); const [isUploadIconDialogOpen, setIsUploadIconDialogOpen] = useState(false);
const [isPreviewIconOpen, setIsPreviewIconOpen] = useState(false); const [isPreviewIconOpen, setIsPreviewIconOpen] = useState(false);
@@ -65,7 +56,6 @@ export const RouteEditPage = observer(() => {
const [activeIconMenuType, setActiveIconMenuType] = useState< const [activeIconMenuType, setActiveIconMenuType] = useState<
"thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null "thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null
>(null); >(null);
const [fileToUpload, setFileToUpload] = useState<File | null>(null);
const { language } = languageStore; const { language } = languageStore;
const [coordinates, setCoordinates] = useState<string>(""); const [coordinates, setCoordinates] = useState<string>("");
@@ -114,9 +104,6 @@ export const RouteEditPage = observer(() => {
const effectiveIconUrl = isMediaIdEmpty(editRouteData.icon) const effectiveIconUrl = isMediaIdEmpty(editRouteData.icon)
? null ? null
: (selectedIconMedia?.id ?? editRouteData.icon); : (selectedIconMedia?.id ?? editRouteData.icon);
const effectiveVideoId = isMediaIdEmpty(editRouteData.video_preview)
? null
: editRouteData.video_preview;
useEffect(() => { useEffect(() => {
if (editRouteData.path && editRouteData.path.length > 0) { if (editRouteData.path && editRouteData.path.length > 0) {
@@ -238,46 +225,6 @@ export const RouteEditPage = observer(() => {
articlesStore.getArticleList(); 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( const selectedArticle = articlesStore.articleList.ru.data.find(
(article) => article.id === editRouteData.governor_appeal (article) => article.id === editRouteData.governor_appeal
@@ -555,7 +502,7 @@ export const RouteEditPage = observer(() => {
/> />
<TextField <TextField
className="w-full" className="w-full"
label="Таймер видео заставки (сек)" label="Таймер показа видео при бездействии (сек)"
type="number" type="number"
value={editRouteData.video_timer ?? 420} value={editRouteData.video_timer ?? 420}
onChange={(e) => { onChange={(e) => {
@@ -646,18 +593,6 @@ export const RouteEditPage = observer(() => {
}} }}
/> />
</div> </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>
</Box> </Box>
@@ -703,49 +638,6 @@ export const RouteEditPage = observer(() => {
onClose={() => setIsSelectArticleDialogOpen(false)} onClose={() => setIsSelectArticleDialogOpen(false)}
onSelectArticle={handleArticleSelect} 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 <SelectMediaDialog
open={isSelectIconDialogOpen} open={isSelectIconDialogOpen}
onClose={() => setIsSelectIconDialogOpen(false)} onClose={() => setIsSelectIconDialogOpen(false)}

View File

@@ -1,10 +1,9 @@
import { Button } from "@mui/material"; import { Button } from "@mui/material";
import { useNavigate, useNavigationType } from "react-router"; import { useNavigate, useNavigationType } from "react-router";
import { MediaViewer } from "@widgets";
import { useMapData } from "./MapDataContext"; import { useMapData } from "./MapDataContext";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { authInstance, isMediaIdEmpty } from "@shared"; import { authInstance } from "@shared";
import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import LanguageSelector from "./web-gl/LanguageSelector"; import LanguageSelector from "./web-gl/LanguageSelector";
@@ -17,9 +16,8 @@ export const LeftSidebar = observer(({ open, onToggle }: LeftSidebarProps) => {
const navigate = useNavigate(); const navigate = useNavigate();
const navigationType = useNavigationType(); const navigationType = useNavigationType();
const { routeData } = useMapData(); const { routeData } = useMapData();
const [carrierLogo, setCarrierLogo] = useState<string | null>(null); const [previewMedia, setPreviewMedia] = useState<string | null>(null);
const [carrierSlogan, setCarrierSlogan] = useState<string | null>(null); const [isPreviewMediaError, setIsPreviewMediaError] = useState(false);
const [carrierShortName, setCarrierShortName] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
async function fetchCarrierData() { async function fetchCarrierData() {
@@ -27,9 +25,8 @@ export const LeftSidebar = observer(({ open, onToggle }: LeftSidebarProps) => {
const carrier = ( const carrier = (
await authInstance.get(`/carrier/${routeData.carrier_id}`) await authInstance.get(`/carrier/${routeData.carrier_id}`)
).data; ).data;
setCarrierLogo(carrier.logo); setPreviewMedia(carrier.preview_media ?? null);
setCarrierSlogan(carrier.slogan ?? null); setIsPreviewMediaError(false);
setCarrierShortName(carrier.short_name ?? null);
} }
} }
fetchCarrierData(); fetchCarrierData();
@@ -94,43 +91,6 @@ export const LeftSidebar = observer(({ open, onToggle }: LeftSidebarProps) => {
pointerEvents: open ? "auto" : "none", 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 */} {/* Кнопки — .side-menu-buttons */}
<div <div
style={{ style={{
@@ -175,39 +135,14 @@ export const LeftSidebar = observer(({ open, onToggle }: LeftSidebarProps) => {
flexDirection: "column", 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 */} {/* .side-menu-bottom-photo */}
<img <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="" alt=""
style={{ style={{
width: "288px", width: "288px",

View File

@@ -131,7 +131,7 @@ export const SnapshotListPage = observer(() => {
}, },
{ {
field: "created_at", field: "created_at",
headerName: "Дата создания", headerName: "Дата создания (UTC+03:00, MSK)",
flex: 1, flex: 1,
renderCell: (params: GridRenderCellParams) => { renderCell: (params: GridRenderCellParams) => {
return <div>{params.value ? params.value : "-"}</div>; 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 { observer } from "mobx-react-lite";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { import {
@@ -29,6 +33,8 @@ export const PreviewMediaDialog = observer(
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false); const [success, setSuccess] = useState(false);
const [isPreviewBackgroundDark, setIsPreviewBackgroundDark] =
useState(false);
const media = mediaId const media = mediaId
? mediaStore.media.find((m) => m.id === mediaId) ? mediaStore.media.find((m) => m.id === mediaId)
: null; : null;
@@ -40,6 +46,7 @@ export const PreviewMediaDialog = observer(
if (media) { if (media) {
setMediaName(media.media_name); setMediaName(media.media_name);
setMediaFilename(media.filename); setMediaFilename(media.filename);
setIsPreviewBackgroundDark(false);
} }
}, [media]); }, [media]);
@@ -141,8 +148,19 @@ export const PreviewMediaDialog = observer(
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "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 <MediaViewer
media={{ media={{
id: mediaId, id: mediaId,

View File

@@ -6,6 +6,7 @@ import {
clearBlobAndGLTFCache, clearBlobAndGLTFCache,
authStore, authStore,
snapshotStore, snapshotStore,
PreviewBackgroundToggle,
} from "@shared"; } from "@shared";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useEffect, useState, useRef } from "react"; import { useEffect, useState, useRef } from "react";
@@ -90,6 +91,8 @@ export const UploadMediaDialog = observer(
[] []
); );
const [isPreviewLoaded, setIsPreviewLoaded] = useState(false); const [isPreviewLoaded, setIsPreviewLoaded] = useState(false);
const [isPreviewBackgroundDark, setIsPreviewBackgroundDark] =
useState(false);
const previousMediaUrlRef = useRef<string | null>(null); const previousMediaUrlRef = useRef<string | null>(null);
useEffect(() => { useEffect(() => {
@@ -347,6 +350,7 @@ export const UploadMediaDialog = observer(
setMediaUrl(null); setMediaUrl(null);
setMediaFile(null); setMediaFile(null);
setIsPreviewLoaded(false); setIsPreviewLoaded(false);
setIsPreviewBackgroundDark(false);
previousMediaUrlRef.current = null; previousMediaUrlRef.current = null;
onClose(); onClose();
}; };
@@ -416,8 +420,18 @@ export const UploadMediaDialog = observer(
justifyContent: "center", justifyContent: "center",
height: "100%", height: "100%",
position: "relative", 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 && ( {!isPreviewLoaded && mediaUrl && (
<Box <Box
sx={{ sx={{

View File

@@ -12,10 +12,10 @@ export type Carrier = {
id: number; id: number;
short_name: string; short_name: string;
full_name: string; full_name: string;
slogan: string;
city: string; city: string;
city_id: number; city_id: number;
logo: string; preview_media?: string;
video_preview?: string;
main_color: string; main_color: string;
left_color: string; left_color: string;
right_color: string; right_color: string;
@@ -111,38 +111,40 @@ class CarrierStore {
createCarrierData = { createCarrierData = {
city_id: 0, city_id: 0,
logo: "", preview_media: "",
video_preview: "",
main_color: "", main_color: "",
left_color: "", left_color: "",
right_color: "", right_color: "",
ru: { ru: {
full_name: "", full_name: "",
short_name: "", short_name: "",
slogan: "",
}, },
en: { en: {
full_name: "", full_name: "",
short_name: "", short_name: "",
slogan: "",
}, },
zh: { zh: {
full_name: "", full_name: "",
short_name: "", short_name: "",
slogan: "",
}, },
}; };
setCreateCarrierMedia = (
field: "preview_media" | "video_preview",
mediaId: string
) => {
this.createCarrierData[field] = mediaId;
};
setCreateCarrierData = ( setCreateCarrierData = (
fullName: string, fullName: string,
shortName: string, shortName: string,
cityId: number, cityId: number,
slogan: string,
logoId: string,
language: Language, language: Language,
colors?: { main_color?: string; left_color?: string; right_color?: string } colors?: { main_color?: string; left_color?: string; right_color?: string }
) => { ) => {
this.createCarrierData.city_id = cityId; this.createCarrierData.city_id = cityId;
this.createCarrierData.logo = logoId;
if (colors) { if (colors) {
if (colors.main_color !== undefined) this.createCarrierData.main_color = colors.main_color; if (colors.main_color !== undefined) this.createCarrierData.main_color = colors.main_color;
if (colors.left_color !== undefined) this.createCarrierData.left_color = colors.left_color; if (colors.left_color !== undefined) this.createCarrierData.left_color = colors.left_color;
@@ -151,7 +153,6 @@ class CarrierStore {
this.createCarrierData[language] = { this.createCarrierData[language] = {
full_name: fullName, full_name: fullName,
short_name: shortName, short_name: shortName,
slogan: slogan,
}; };
}; };
@@ -206,8 +207,12 @@ class CarrierStore {
short_name: (this.createCarrierData[language].short_name || "").trim(), short_name: (this.createCarrierData[language].short_name || "").trim(),
city: cityName, city: cityName,
city_id: this.createCarrierData.city_id, city_id: this.createCarrierData.city_id,
slogan: (this.createCarrierData[language].slogan || "").trim(), ...(this.createCarrierData.preview_media
...(this.createCarrierData.logo ? { logo: this.createCarrierData.logo } : {}), ? { 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.main_color ? { main_color: this.createCarrierData.main_color } : {}),
...(this.createCarrierData.left_color ? { left_color: this.createCarrierData.left_color } : {}), ...(this.createCarrierData.left_color ? { left_color: this.createCarrierData.left_color } : {}),
...(this.createCarrierData.right_color ? { right_color: this.createCarrierData.right_color } : {}), ...(this.createCarrierData.right_color ? { right_color: this.createCarrierData.right_color } : {}),
@@ -221,55 +226,55 @@ class CarrierStore {
this.carriers[language].data.push(response.data); 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( const cityNameForLang = this.resolveCityName(
this.createCarrierData.city_id, this.createCarrierData.city_id,
lang as Language lang
); );
const localizedData = this.createCarrierData[lang];
const patchPayload = { const patchPayload = {
// @ts-ignore full_name: (localizedData.full_name || "").trim(),
full_name: ((this.createCarrierData[lang as any].full_name as string) || "").trim(), short_name: (localizedData.short_name || "").trim(),
// @ts-ignore
short_name: ((this.createCarrierData[lang as any].short_name as string) || "").trim(),
city: cityNameForLang || cityName, city: cityNameForLang || cityName,
city_id: this.createCarrierData.city_id, city_id: this.createCarrierData.city_id,
// @ts-ignore ...(this.createCarrierData.preview_media
slogan: ((this.createCarrierData[lang as any].slogan as string) || "").trim(), ? { preview_media: this.createCarrierData.preview_media }
...(this.createCarrierData.logo : {}),
? { logo: this.createCarrierData.logo } ...(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}`, `/carrier/${carrierId}`,
patchPayload patchPayload
); );
runInAction(() => { runInAction(() => {
this.carriers[lang as keyof Carriers].data.push(response.data); this.carriers[lang].data.push(response.data);
}); });
} }
this.createCarrierData = { this.createCarrierData = {
city_id: 0, city_id: 0,
logo: "", preview_media: "",
video_preview: "",
main_color: "", main_color: "",
left_color: "", left_color: "",
right_color: "", right_color: "",
ru: { ru: {
full_name: "", full_name: "",
short_name: "", short_name: "",
slogan: "",
}, },
en: { en: {
full_name: "", full_name: "",
short_name: "", short_name: "",
slogan: "",
}, },
zh: { zh: {
full_name: "", full_name: "",
short_name: "", short_name: "",
slogan: "",
}, },
}; };
}; };
@@ -278,36 +283,38 @@ class CarrierStore {
ru: { ru: {
full_name: "", full_name: "",
short_name: "", short_name: "",
slogan: "",
}, },
en: { en: {
full_name: "", full_name: "",
short_name: "", short_name: "",
slogan: "",
}, },
zh: { zh: {
full_name: "", full_name: "",
short_name: "", short_name: "",
slogan: "",
}, },
city_id: 0, city_id: 0,
logo: "", preview_media: "",
video_preview: "",
main_color: "", main_color: "",
left_color: "", left_color: "",
right_color: "", right_color: "",
}; };
setEditCarrierMedia = (
field: "preview_media" | "video_preview",
mediaId: string
) => {
this.editCarrierData[field] = mediaId;
};
setEditCarrierData = ( setEditCarrierData = (
fullName: string, fullName: string,
shortName: string, shortName: string,
cityId: number, cityId: number,
slogan: string,
logoId: string,
language: Language, language: Language,
colors?: { main_color?: string; left_color?: string; right_color?: string } colors?: { main_color?: string; left_color?: string; right_color?: string }
) => { ) => {
this.editCarrierData.city_id = cityId; this.editCarrierData.city_id = cityId;
this.editCarrierData.logo = logoId;
if (colors) { if (colors) {
if (colors.main_color !== undefined) this.editCarrierData.main_color = colors.main_color; if (colors.main_color !== undefined) this.editCarrierData.main_color = colors.main_color;
if (colors.left_color !== undefined) this.editCarrierData.left_color = colors.left_color; if (colors.left_color !== undefined) this.editCarrierData.left_color = colors.left_color;
@@ -316,7 +323,6 @@ class CarrierStore {
this.editCarrierData[language] = { this.editCarrierData[language] = {
full_name: fullName, full_name: fullName,
short_name: shortName, short_name: shortName,
slogan: slogan,
}; };
}; };
@@ -327,10 +333,10 @@ class CarrierStore {
...this.editCarrierData[lang], ...this.editCarrierData[lang],
full_name: (this.editCarrierData[lang].full_name || "").trim(), full_name: (this.editCarrierData[lang].full_name || "").trim(),
short_name: (this.editCarrierData[lang].short_name || "").trim(), short_name: (this.editCarrierData[lang].short_name || "").trim(),
slogan: (this.editCarrierData[lang].slogan || "").trim(),
city: cityName, city: cityName,
city_id: this.editCarrierData.city_id, 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.main_color ? { main_color: this.editCarrierData.main_color } : {}),
...(this.editCarrierData.left_color ? { left_color: this.editCarrierData.left_color } : {}), ...(this.editCarrierData.left_color ? { left_color: this.editCarrierData.left_color } : {}),
...(this.editCarrierData.right_color ? { right_color: this.editCarrierData.right_color } : {}), ...(this.editCarrierData.right_color ? { right_color: this.editCarrierData.right_color } : {}),

View File

@@ -23,7 +23,6 @@ export type Route = {
route_sys_number: string; route_sys_number: string;
scale_max: number; scale_max: number;
scale_min: number; scale_min: number;
video_preview: string;
video_timer: number; video_timer: number;
}; };
@@ -154,21 +153,16 @@ class RouteStore {
route_sys_number: "", route_sys_number: "",
scale_max: 0, scale_max: 0,
scale_min: 0, scale_min: 0,
video_preview: "" as string | undefined,
video_timer: 420, video_timer: 420,
}; };
setEditRouteData = (data: any) => { setEditRouteData = (data: any) => {
this.editRouteData = { ...this.editRouteData, ...data }; const routeData = { ...data };
delete routeData.video_preview;
this.editRouteData = { ...this.editRouteData, ...routeData };
}; };
editRoute = async (id: number) => { 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)) { if (!this.editRouteData.icon || isMediaIdEmpty(this.editRouteData.icon)) {
delete (this.editRouteData as any).icon; delete (this.editRouteData as any).icon;
} }
@@ -180,6 +174,7 @@ class RouteStore {
center_latitude: parseFloat(this.editRouteData.center_latitude), center_latitude: parseFloat(this.editRouteData.center_latitude),
center_longitude: parseFloat(this.editRouteData.center_longitude), center_longitude: parseFloat(this.editRouteData.center_longitude),
}; };
delete dataToSend.video_preview;
if ( if (
this.editRouteData.governor_appeal === 0 || this.editRouteData.governor_appeal === 0 ||
!this.editRouteData.governor_appeal !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 "./LoadingSpinner";
export * from "./MultiSelect"; export * from "./MultiSelect";
export * from "./SearchInput"; 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 { Paper, Box, Typography, Button, Tooltip } from "@mui/material";
import { X, Info, Plus } from "lucide-react"; import { X, Info, Plus } from "lucide-react";
import { editSightStore } from "@shared"; import { editSightStore, PreviewBackgroundToggle } from "@shared";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
interface ImageUploadCardProps { interface ImageUploadCardProps {
title: string; title: string;
@@ -33,6 +33,7 @@ export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
tooltipText, tooltipText,
}) => { }) => {
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const [isPreviewBackgroundDark, setIsPreviewBackgroundDark] = useState(false);
const { setFileToUpload } = editSightStore; const { setFileToUpload } = editSightStore;
const handleZoneClick = () => { const handleZoneClick = () => {
@@ -124,9 +125,21 @@ export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
borderRadius: 1, borderRadius: 1,
mb: 1, mb: 1,
cursor: imageUrl ? "pointer" : "default", cursor: imageUrl ? "pointer" : "default",
backgroundColor: imageUrl
? isPreviewBackgroundDark
? "#000"
: "#fff"
: "transparent",
transition: "background-color 150ms ease",
}} }}
onClick={onImageClick} onClick={onImageClick}
> >
{imageUrl && (
<PreviewBackgroundToggle
dark={isPreviewBackgroundDark}
onToggle={() => setIsPreviewBackgroundDark((value) => !value)}
/>
)}
{imageUrl && ( {imageUrl && (
<button <button
className="absolute top-2 right-2" className="absolute top-2 right-2"