fix: Update map with tables fixes

This commit is contained in:
2025-07-09 18:56:18 +03:00
parent 78800ee2ae
commit e2547cb571
87 changed files with 5392 additions and 1410 deletions

View File

@ -3,21 +3,25 @@ import { Button } from "@mui/material";
export const DeleteModal = ({
onDelete,
onCancel,
edit = false,
open,
}: {
onDelete: () => void;
onCancel: () => void;
edit?: boolean;
open: boolean;
}) => {
return (
<div
className={`fixed top-0 left-0 w-screen h-screen flex justify-center items-center z-10000 bg-black/30 transition-all duration-300 ${
className={`fixed top-0 left-0 w-screen h-screen flex justify-center items-center z-1000000000000 bg-black/30 transition-all duration-300 ${
open ? "block" : "hidden"
}`}
>
<div className="bg-white p-4 w-100 rounded-lg flex flex-col gap-4 items-center">
<p className="text-black w-100 text-center">
Вы уверены, что хотите удалить этот элемент?
{`Вы уверены, что хотите ${
edit ? "убрать" : "удалить"
} этот элемент?`}
</p>
<div className="flex gap-4 justify-center">
<Button variant="contained" color="error" onClick={onDelete}>

View File

@ -18,6 +18,7 @@ import { observer } from "mobx-react-lite";
import { Button, Checkbox, Typography } from "@mui/material";
import { Vehicle } from "@shared";
import { toast } from "react-toastify";
import { useNavigate } from "react-router-dom";
export type ConnectedDevice = string;
@ -116,6 +117,7 @@ export const DevicesTable = observer(() => {
const { snapshots, getSnapshots } = snapshotStore;
const { getVehicles, vehicles } = vehicleStore; // Removed as devicesStore.devices should be the source of truth
const { devices } = devicesStore;
const navigate = useNavigate();
const [selectedDeviceUuids, setSelectedDeviceUuids] = useState<string[]>([]);
// Transform the raw devices data into rows suitable for the table
@ -182,13 +184,25 @@ export const DevicesTable = observer(() => {
const handleSendSnapshotAction = async (snapshotId: string) => {
if (selectedDeviceUuids.length === 0) return;
const send = async (deviceUuid: string) => {
try {
await authInstance.post(
`/devices/${deviceUuid}/force-snapshot-update`,
{
snapshot_id: snapshotId,
}
);
toast.success(`Снапшот отправлен на устройство `);
} catch (error) {
console.error(`Error sending snapshot to device ${deviceUuid}:`, error);
toast.error(`Не удалось отправить снапшот на устройство`);
}
};
try {
// Create an array of promises for all snapshot requests
const snapshotPromises = selectedDeviceUuids.map((deviceUuid) => {
console.log(`Sending snapshot ${snapshotId} to device ${deviceUuid}`);
return authInstance.post(`/devices/${deviceUuid}/force-snapshot`, {
snapshot_id: snapshotId,
});
return send(deviceUuid);
});
// Wait for all promises to settle (either resolve or reject)
@ -209,6 +223,16 @@ export const DevicesTable = observer(() => {
return (
<>
<TableContainer component={Paper} sx={{ mt: 2 }}>
<div className="flex justify-end p-3 gap-2 ">
<Button
variant="contained"
color="primary"
size="small"
onClick={() => navigate("/vehicle/create")}
>
Добавить устройство
</Button>
</div>
<div className="flex justify-end p-3 gap-2">
<Button
variant="outlined" // Changed to outlined for distinction
@ -269,16 +293,34 @@ export const DevicesTable = observer(() => {
'input[type="checkbox"]'
) === null
) {
handleSelectDevice(
{
target: {
checked: !selectedDeviceUuids.includes(
row.device_uuid ?? ""
),
},
} as React.ChangeEvent<HTMLInputElement>, // Simulate event
row.device_uuid ?? ""
);
if (event.shiftKey) {
if (row.device_uuid) {
navigator.clipboard
.writeText(row.device_uuid)
.then(() => {
toast.success(`UUID скопирован`);
})
.catch(() => {
toast.error("Не удалось скопировать UUID");
});
} else {
toast.warning("Устройство не имеет UUID");
}
}
// Only toggle checkbox if Shift key is not pressed
if (!event.shiftKey) {
handleSelectDevice(
{
target: {
checked: !selectedDeviceUuids.includes(
row.device_uuid ?? ""
),
},
} as React.ChangeEvent<HTMLInputElement>, // Simulate event
row.device_uuid ?? ""
);
}
}
}}
sx={{

View File

@ -46,10 +46,16 @@ export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
) => {
const file = event.target.files?.[0];
if (file) {
setFileToUpload(file);
setUploadMediaOpen(true);
if (imageKey && setHardcodeType) {
setHardcodeType(imageKey);
if (file.type.startsWith("image/") && file.type !== "image/gif") {
setFileToUpload(file);
setUploadMediaOpen(true);
if (imageKey && setHardcodeType) {
setHardcodeType(imageKey);
}
} else if (file.type === "image/gif") {
toast.error("GIF файлы не поддерживаются");
} else {
toast.error("Пожалуйста, выберите изображение");
}
}
// Reset the input value so selecting the same file again triggers change
@ -78,9 +84,11 @@ export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
const files = event.dataTransfer.files;
if (files && files.length > 0) {
const file = files[0];
if (file.type.startsWith("image/")) {
if (file.type.startsWith("image/") && file.type !== "image/gif") {
setFileToUpload(file);
setUploadMediaOpen(true);
} else if (file.type === "image/gif") {
toast.error("GIF файлы не поддерживаются");
} else {
toast.error("Пожалуйста, выберите изображение");
}

View File

@ -44,14 +44,14 @@ export const LanguageSwitcher = observer(() => {
};
return (
<div className="fixed bottom-0 left-1/2 -translate-x-1/2 flex gap-2 p-4 z-100000000">
<div className="fixed top-0 left-1/2 -translate-x-1/2 flex gap-2 p-4 z-100000">
{/* Added some styling for better visualization */}
{LANGUAGES.map((lang) => (
<Button
key={lang}
onClick={() => handleLanguageChange(lang)}
variant={language === lang ? "contained" : "outlined"} // Highlight the active language
color="primary"
variant={"contained"} // Highlight the active language
color={language === lang ? "primary" : "secondary"}
sx={{ minWidth: "60px" }} // Give buttons a consistent width
>
{getLanguageLabel(lang)}

View File

@ -2,20 +2,32 @@ import * as React from "react";
import Box from "@mui/material/Box";
import Toolbar from "@mui/material/Toolbar";
import IconButton from "@mui/material/IconButton";
import { Menu, ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
import { Menu, ChevronLeftIcon, ChevronRightIcon, User } from "lucide-react";
import { useTheme } from "@mui/material/styles";
import { AppBar } from "./ui/AppBar";
import { Drawer } from "./ui/Drawer";
import { DrawerHeader } from "./ui/DrawerHeader";
import { NavigationList } from "@features";
import { authStore, userStore } from "@shared";
import { observer } from "mobx-react-lite";
import { useEffect } from "react";
import { Typography } from "@mui/material";
interface LayoutProps {
children: React.ReactNode;
}
export const Layout: React.FC<LayoutProps> = ({ children }) => {
export const Layout: React.FC<LayoutProps> = observer(({ children }) => {
const theme = useTheme();
const [open, setOpen] = React.useState(false);
const [open, setOpen] = React.useState(true);
const { getUsers, users } = userStore;
useEffect(() => {
const fetchUsers = async () => {
await getUsers();
};
fetchUsers();
}, []);
const handleDrawerOpen = () => {
setOpen(true);
@ -28,7 +40,7 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
return (
<Box sx={{ display: "flex" }}>
<AppBar position="fixed" open={open}>
<Toolbar>
<Toolbar className="flex justify-between">
<IconButton
color="inherit"
aria-label="open drawer"
@ -43,17 +55,78 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
>
<Menu />
</IconButton>
<div></div>
<div className="flex gap-2 items-center">
<div className="flex flex-col gap-1">
{(() => {
console.log(authStore.payload);
return (
<>
<p className=" text-white">
{
users?.data?.find(
// @ts-ignore
(user) => user.id === authStore.payload?.user_id
)?.name
}
</p>
<div
className="text-center text-xs"
style={{
backgroundColor: "#877361",
borderRadius: "4px",
color: "white",
padding: "2px 10px",
}}
>
{/* @ts-ignore */}
{authStore.payload?.is_admin
? "Администратор"
: "Режим пользователя"}
</div>
</>
);
})()}
</div>
<div className="w-10 h-10 bg-gray-600 rounded-full flex items-center justify-center">
<User />
</div>
</div>
</Toolbar>
</AppBar>
<Drawer variant="permanent" open={open}>
<DrawerHeader>
<IconButton onClick={handleDrawerClose}>
{theme.direction === "rtl" ? (
<ChevronRightIcon />
) : (
<ChevronLeftIcon />
)}
</IconButton>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: 2,
cursor: "pointer",
}}
onClick={() => {
setOpen(!open);
}}
>
<img
src="/favicon_ship.png"
alt="logo"
width={40}
height={40}
style={{ filter: "brightness(0)", marginLeft: "-5px" }}
/>
<Typography variant="h6" component="h1">
Белые ночи
</Typography>
</Box>
{open && (
<IconButton onClick={handleDrawerClose}>
{theme.direction === "rtl" ? (
<ChevronRightIcon />
) : (
<ChevronLeftIcon />
)}
</IconButton>
)}
</DrawerHeader>
<NavigationList open={open} onDrawerOpen={handleDrawerOpen} />
</Drawer>
@ -67,10 +140,9 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
maxWidth: "100vw",
}}
>
<DrawerHeader />
<div className="mt-16"></div>
{children}
</Box>
</Box>
);
};
});

View File

@ -24,4 +24,12 @@ export const AppBar = styled(MuiAppBar, {
duration: theme.transitions.duration.enteringScreen,
}),
}),
...(!open && {
marginLeft: theme.spacing(7),
width: `calc(100% - ${theme.spacing(7)})`,
[theme.breakpoints.up("sm")]: {
marginLeft: theme.spacing(8),
width: `calc(100% - ${theme.spacing(8)})`,
},
}),
}));

View File

@ -1,10 +1,12 @@
import { styled } from "@mui/material/styles";
import type { Theme } from "@mui/material/styles";
import Box from "@mui/material/Box";
export const DrawerHeader = styled("div")(({ theme }: { theme: Theme }) => ({
export const DrawerHeader = styled(Box)(({ theme }: { theme: Theme }) => ({
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
padding: theme.spacing(0, 1),
justifyContent: "space-between",
padding: theme.spacing(2),
...theme.mixins.toolbar,
borderBottom: "1px solid rgba(0, 0, 0, 0.12)",
}));

View File

@ -8,9 +8,23 @@ export const MediaAreaForSight = observer(
({
onFilesDrop, // 👈 Проп для обработки загруженных файлов
onFinishUpload,
contextObjectName,
contextType,
isArticle,
articleName,
}: {
onFilesDrop?: (files: File[]) => void;
onFinishUpload?: (mediaId: string) => void;
contextObjectName?: string;
contextType?:
| "sight"
| "city"
| "carrier"
| "country"
| "vehicle"
| "station";
isArticle?: boolean;
articleName?: string;
}) => {
const [selectMediaDialogOpen, setSelectMediaDialogOpen] = useState(false);
const [uploadMediaDialogOpen, setUploadMediaDialogOpen] = useState(false);
@ -94,6 +108,10 @@ export const MediaAreaForSight = observer(
<UploadMediaDialog
open={uploadMediaDialogOpen}
onClose={() => setUploadMediaDialogOpen(false)}
contextObjectName={contextObjectName}
contextType={contextType}
isArticle={isArticle}
articleName={articleName}
afterUploadSight={onFinishUpload}
/>
<SelectMediaDialog

View File

@ -36,6 +36,11 @@ export function MediaViewer({
style={{
height: fullHeight ? "100%" : "auto",
width: fullWidth ? "100%" : "auto",
...(media?.filename?.toLowerCase().endsWith(".webp") && {
maxWidth: "300px",
maxHeight: "300px",
objectFit: "contain",
}),
}}
/>
)}

View File

@ -5,7 +5,7 @@ import rehypeRaw from "rehype-raw";
export const ReactMarkdownComponent = ({ value }: { value: string }) => {
return (
<Box
className="prose prose-sm prose-invert"
className="prose prose-sm prose-invert w-full"
sx={{
"& img": {
maxWidth: "100%",

View File

@ -38,6 +38,12 @@ const StyledMarkdownEditor = styled("div")(({ theme }) => ({
maxHeight: "500px",
overflowY: "auto",
overflowX: "hidden",
"&::-webkit-scrollbar": {
display: "none",
},
"&": {
scrollbarWidth: "none",
},
wordBreak: "break-word", // ✅ добавлено
},
"& .CodeMirror-selected": {

View File

@ -31,7 +31,7 @@ import { toast } from "react-toastify";
export const CreateInformationTab = observer(
({ value, index }: { value: number; index: number }) => {
const { ruCities } = cityStore;
const { cities } = cityStore;
const [mediaId, setMediaId] = useState<string>("");
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false);
@ -175,10 +175,11 @@ export const CreateInformationTab = observer(
/>
<Autocomplete
options={ruCities.data ?? []}
options={cities["ru"]?.data ?? []}
value={
ruCities.data.find((city) => city.id === sight.city_id) ??
null
cities["ru"]?.data?.find(
(city) => city.id === sight.city_id
) ?? null
}
getOptionLabel={(option) => option.name}
onChange={(_, value) => {
@ -246,7 +247,7 @@ export const CreateInformationTab = observer(
}}
>
<ImageUploadCard
title="Логотип"
title="Иконка"
imageKey="thumbnail"
imageUrl={sight.thumbnail}
onImageClick={() => {
@ -266,11 +267,10 @@ export const CreateInformationTab = observer(
setUploadMediaOpen={() => {
setIsUploadMediaOpen(true);
setActiveMenuType("thumbnail");
setHardcodeType("thumbnail");
}}
setHardcodeType={(type) => {
setHardcodeType(
type as "thumbnail" | "watermark_lu" | "watermark_rd"
);
setHardcodeType={() => {
setHardcodeType("thumbnail");
}}
/>
@ -295,16 +295,15 @@ export const CreateInformationTab = observer(
setUploadMediaOpen={() => {
setIsUploadMediaOpen(true);
setActiveMenuType("watermark_lu");
setHardcodeType("watermark_lu");
}}
setHardcodeType={(type) => {
setHardcodeType(
type as "thumbnail" | "watermark_lu" | "watermark_rd"
);
setHardcodeType={() => {
setHardcodeType("watermark_lu");
}}
/>
<ImageUploadCard
title="Водяной знак (правый нижний)"
title="Водяной знак (правый верхний)"
imageKey="watermark_rd"
imageUrl={sight.watermark_rd}
onImageClick={() => {
@ -324,11 +323,10 @@ export const CreateInformationTab = observer(
setUploadMediaOpen={() => {
setIsUploadMediaOpen(true);
setActiveMenuType("watermark_rd");
setHardcodeType("watermark_rd");
}}
setHardcodeType={(type) => {
setHardcodeType(
type as "thumbnail" | "watermark_lu" | "watermark_rd"
);
setHardcodeType={() => {
setHardcodeType("watermark_rd");
}}
/>
</Box>
@ -412,6 +410,8 @@ export const CreateInformationTab = observer(
<UploadMediaDialog
open={isUploadMediaOpen}
onClose={() => setIsUploadMediaOpen(false)}
contextObjectName={sight[language].name}
contextType="sight"
afterUpload={(media) => {
handleChange({
[activeMenuType ?? "thumbnail"]: media.id,

View File

@ -44,7 +44,7 @@ export const CreateLeftTab = observer(
} = editSightStore;
const { language } = languageStore;
const token = localStorage.getItem("token");
const [isSelectArticleDialogOpen, setIsSelectArticleDialogOpen] =
useState(false);
const [isSelectMediaDialogOpen, setIsSelectMediaDialogOpen] =
@ -326,6 +326,7 @@ export const CreateLeftTab = observer(
<Box
sx={{
overflow: "hidden",
position: "relative",
width: "100%",
minHeight: 100,
padding: "3px",
@ -343,15 +344,45 @@ export const CreateLeftTab = observer(
}}
>
{sight[language].left.media.length > 0 ? (
<MediaViewer
media={{
id: sight[language].left.media[0].id,
media_type:
sight[language].left.media[0].media_type,
filename: sight[language].left.media[0].filename,
}}
fullWidth
/>
<>
<MediaViewer
media={{
id: sight[language].left.media[0].id,
media_type:
sight[language].left.media[0].media_type,
filename: sight[language].left.media[0].filename,
}}
fullWidth
/>
{sight.watermark_lu && (
<img
src={`${import.meta.env.VITE_KRBL_MEDIA}${
sight.watermark_lu
}/download?token=${token}`}
alt="preview"
className="absolute top-4 left-4 z-10"
style={{
width: "30px",
height: "30px",
objectFit: "contain",
}}
/>
)}
{sight.watermark_rd && (
<img
src={`${import.meta.env.VITE_KRBL_MEDIA}${
sight.watermark_rd
}/download?token=${token}`}
alt="preview"
className="absolute bottom-4 right-4 z-10"
style={{
width: "30px",
height: "30px",
objectFit: "contain",
}}
/>
)}
</>
) : (
<ImagePlus size={48} color="white" />
)}
@ -400,7 +431,13 @@ export const CreateLeftTab = observer(
sx={{
padding: 1,
maxHeight: "300px",
overflowY: "scroll",
overflowY: "auto",
"&::-webkit-scrollbar": {
display: "none",
},
"&": {
scrollbarWidth: "none",
},
background:
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
flexGrow: 1,
@ -451,6 +488,10 @@ export const CreateLeftTab = observer(
<UploadMediaDialog
open={uploadMediaOpen}
onClose={() => setUploadMediaOpen(false)}
contextObjectName={sight[language].name}
contextType="sight"
isArticle={true}
articleName={sight[language].left.heading || "Левая статья"}
afterUpload={async (media) => {
setUploadMediaOpen(false);
setFileToUpload(null);
@ -466,6 +507,7 @@ export const CreateLeftTab = observer(
open={isDeleteModalOpen}
onDelete={() => {
deleteLeftArticle(sight.left_article);
setIsDeleteModalOpen(false);
toast.success("Статья откреплена");
}}
onCancel={() => setIsDeleteModalOpen(false)}

View File

@ -153,10 +153,12 @@ export const CreateRightTab = observer(
selectedArticleId: number
) => {
try {
await linkExistingRightArticle(selectedArticleId);
const linkedArticleId = await linkExistingRightArticle(
selectedArticleId
);
setSelectArticleDialogOpen(false); // Close dialog
const newIndex = sight[language].right.findIndex(
(a) => a.id === selectedArticleId
(a) => a.id === linkedArticleId
);
if (newIndex > -1) {
setActiveArticleIndex(newIndex);
@ -483,6 +485,9 @@ export const CreateRightTab = observer(
linkPreviewMedia(mediaId);
}}
onFilesDrop={() => {}}
contextObjectName={sight[language].name}
contextType="sight"
isArticle={false}
/>
)}
</Box>
@ -495,6 +500,9 @@ export const CreateRightTab = observer(
linkPreviewMedia(mediaId);
}}
onFilesDrop={() => {}}
contextObjectName={sight[language].name}
contextType="sight"
isArticle={false}
/>
)}
</Box>
@ -597,7 +605,13 @@ export const CreateRightTab = observer(
padding: 1,
minHeight: "200px",
maxHeight: "300px",
overflowY: "scroll",
overflowY: "auto",
"&::-webkit-scrollbar": {
display: "none",
},
"&": {
scrollbarWidth: "none",
},
background:
"rgba(179, 165, 152, 0.4), linear-gradient(180deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.2) 100%)",
@ -698,6 +712,14 @@ export const CreateRightTab = observer(
setFileToUpload(null); // Clear file if dialog is closed without upload
setMediaTarget(null);
}}
contextObjectName={sight[language].name}
contextType="sight"
isArticle={mediaTarget === "rightArticle"}
articleName={
mediaTarget === "rightArticle" && activeArticleIndex !== null
? sight[language].right[activeArticleIndex].heading
: undefined
}
afterUpload={handleMediaUploaded} // This will use the mediaTarget
/>
<SelectMediaDialog

View File

@ -32,8 +32,6 @@ import { toast } from "react-toastify";
export const InformationTab = observer(
({ value, index }: { value: number; index: number }) => {
const { ruCities } = cityStore;
const [mediaId, setMediaId] = useState<string>("");
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false);
@ -53,6 +51,7 @@ export const InformationTab = observer(
const [hardcodeType, setHardcodeType] = useState<
"thumbnail" | "watermark_lu" | "watermark_rd" | null
>(null);
const { cities } = cityStore;
useEffect(() => {
// Показывать только при инициализации (не менять при ошибках пользователя)
if (sight.common.latitude !== 0 || sight.common.longitude !== 0) {
@ -89,7 +88,7 @@ export const InformationTab = observer(
},
true
);
setActiveMenuType(null);
setIsUploadMediaOpen(false);
};
@ -163,9 +162,9 @@ export const InformationTab = observer(
/>
<Autocomplete
options={ruCities?.data ?? []}
options={cities["ru"]?.data ?? []}
value={
ruCities?.data?.find(
cities["ru"]?.data?.find(
(city) => city.id === sight.common.city_id
) ?? null
}
@ -246,7 +245,7 @@ export const InformationTab = observer(
}}
>
<ImageUploadCard
title="Логотип"
title="Иконка"
imageKey="thumbnail"
imageUrl={sight.common.thumbnail}
onImageClick={() => {
@ -270,11 +269,10 @@ export const InformationTab = observer(
setUploadMediaOpen={() => {
setIsUploadMediaOpen(true);
setActiveMenuType("thumbnail");
setHardcodeType("thumbnail");
}}
setHardcodeType={(type) => {
setHardcodeType(
type as "thumbnail" | "watermark_lu" | "watermark_rd"
);
setHardcodeType={() => {
setHardcodeType("thumbnail");
}}
/>
<ImageUploadCard
@ -302,15 +300,14 @@ export const InformationTab = observer(
setUploadMediaOpen={() => {
setIsUploadMediaOpen(true);
setActiveMenuType("watermark_lu");
setHardcodeType("watermark_lu");
}}
setHardcodeType={(type) => {
setHardcodeType(
type as "thumbnail" | "watermark_lu" | "watermark_rd"
);
setHardcodeType={() => {
setHardcodeType("watermark_lu");
}}
/>
<ImageUploadCard
title="Водяной знак (правый нижний)"
title="Водяной знак (правый верхний)"
imageKey="watermark_rd"
imageUrl={sight.common.watermark_rd}
onImageClick={() => {
@ -334,11 +331,10 @@ export const InformationTab = observer(
setUploadMediaOpen={() => {
setIsUploadMediaOpen(true);
setActiveMenuType("watermark_rd");
setHardcodeType("watermark_rd");
}}
setHardcodeType={(type) => {
setHardcodeType(
type as "thumbnail" | "watermark_lu" | "watermark_rd"
);
setHardcodeType={() => {
setHardcodeType("watermark_rd");
}}
/>
</Box>
@ -399,7 +395,6 @@ export const InformationTab = observer(
open={isAddMediaOpen}
onClose={() => {
setIsAddMediaOpen(false);
setActiveMenuType(null);
}}
onSelectMedia={handleMediaSelect}
mediaType={
@ -414,6 +409,8 @@ export const InformationTab = observer(
<UploadMediaDialog
open={isUploadMediaOpen}
onClose={() => setIsUploadMediaOpen(false)}
contextObjectName={sight[language].name}
contextType="sight"
afterUpload={(media) => {
handleChange(
language as Language,

View File

@ -9,6 +9,7 @@ import {
SelectArticleModal,
UploadMediaDialog,
Language,
articlesStore,
} from "@shared";
import {
LanguageSwitcher,
@ -43,7 +44,7 @@ export const LeftWidgetTab = observer(
const { language } = languageStore;
const data = sight[language];
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const token = localStorage.getItem("token");
const [isSelectMediaDialogOpen, setIsSelectMediaDialogOpen] =
useState(false);
const [isSelectArticleDialogOpen, setIsSelectArticleDialogOpen] =
@ -76,20 +77,40 @@ export const LeftWidgetTab = observer(
}, []);
const handleSelectArticle = useCallback(
(
articleId: number,
heading: string,
body: string,
media: { id: string; media_type: number; filename: string }[]
async (
articleId: number
// heading: string,
// body: string,
// media: { id: string; media_type: number; filename: string }[]
) => {
setIsSelectArticleDialogOpen(false);
updateSightInfo(languageStore.language, {
const ruArticle = await articlesStore.getArticle(articleId, "ru");
const enArticle = await articlesStore.getArticle(articleId, "en");
const zhArticle = await articlesStore.getArticle(articleId, "zh");
updateSightInfo("ru", {
left: {
heading,
body,
media,
heading: ruArticle.data.heading,
body: ruArticle.data.body,
media: ruArticle.data.media || [],
},
});
updateSightInfo("en", {
left: {
heading: enArticle.data.heading,
body: enArticle.data.body,
media: enArticle.data.media || [],
},
});
updateSightInfo("zh", {
left: {
heading: zhArticle.data.heading,
body: zhArticle.data.body,
media: zhArticle.data.media || [],
},
});
updateSightInfo(
languageStore.language,
{
@ -271,6 +292,7 @@ export const LeftWidgetTab = observer(
width: "100%",
minHeight: 100,
padding: "3px",
position: "relative",
display: "flex",
alignItems: "center",
justifyContent: "center",
@ -285,14 +307,41 @@ export const LeftWidgetTab = observer(
}}
>
{data.left.media.length > 0 ? (
<MediaViewer
media={{
id: data.left.media[0].id,
media_type: data.left.media[0].media_type,
filename: data.left.media[0].filename,
}}
fullWidth
/>
<>
<MediaViewer
media={{
id: data.left.media[0].id,
media_type: data.left.media[0].media_type,
filename: data.left.media[0].filename,
}}
fullWidth
/>
<img
src={`${import.meta.env.VITE_KRBL_MEDIA}${
sight.common.watermark_lu
}/download?token=${token}`}
alt="preview"
className="absolute top-4 left-4 z-10"
style={{
width: "30px",
height: "30px",
objectFit: "contain",
}}
/>
<img
src={`${import.meta.env.VITE_KRBL_MEDIA}${
sight.common.watermark_rd
}/download?token=${token}`}
alt="preview"
className="absolute bottom-4 right-4 z-10"
style={{
width: "30px",
height: "30px",
objectFit: "contain",
}}
/>
</>
) : (
<ImagePlus size={48} color="white" />
)}
@ -341,7 +390,14 @@ export const LeftWidgetTab = observer(
sx={{
padding: 1,
maxHeight: "300px",
overflowY: "scroll",
overflowY: "auto",
width: "100%",
"&::-webkit-scrollbar": {
display: "none",
},
"&": {
scrollbarWidth: "none",
},
background:
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
flexGrow: 1,
@ -373,6 +429,12 @@ export const LeftWidgetTab = observer(
<UploadMediaDialog
open={uploadMediaOpen}
onClose={() => setUploadMediaOpen(false)}
contextObjectName={sight[languageStore.language].name}
contextType="sight"
isArticle={true}
articleName={
sight[languageStore.language].left.heading || "Левая статья"
}
afterUpload={async (media) => {
setUploadMediaOpen(false);
setFileToUpload(null);

View File

@ -115,9 +115,21 @@ export const RightWidgetTab = observer(
setActiveArticleIndex(index);
};
const handleCreateNew = () => {
createNewRightArticle();
handleClose();
const handleCreateNew = async () => {
try {
const newArticleId = await createNewRightArticle();
handleClose();
// Automatically select the newly created article
const newIndex = sight[language].right.findIndex(
(article) => article.id === newArticleId
);
if (newIndex > -1) {
setActiveArticleIndex(newIndex);
setType("article");
}
} catch (error) {
console.error("Error creating new article:", error);
}
};
const handleSelectExisting = () => {
@ -129,9 +141,21 @@ export const RightWidgetTab = observer(
setIsSelectModalOpen(false);
};
const handleArticleSelect = (id: number) => {
linkArticle(id);
handleCloseSelectModal();
const handleArticleSelect = async (id: number) => {
try {
const linkedArticleId = await linkArticle(id);
handleCloseSelectModal();
// Automatically select the newly linked article
const newIndex = sight[language].right.findIndex(
(article) => article.id === linkedArticleId
);
if (newIndex > -1) {
setActiveArticleIndex(newIndex);
setType("article");
}
} catch (error) {
console.error("Error linking article:", error);
}
};
const handleMediaSelected = async (media: {
@ -417,6 +441,9 @@ export const RightWidgetTab = observer(
linkPreviewMedia(mediaId);
}}
onFilesDrop={() => {}}
contextObjectName={sight[language].name}
contextType="sight"
isArticle={false}
/>
)}
</Box>
@ -441,6 +468,9 @@ export const RightWidgetTab = observer(
linkPreviewMedia(mediaId);
}}
onFilesDrop={() => {}}
contextObjectName={sight[language].name}
contextType="sight"
isArticle={false}
/>
</Box>
</Box>
@ -539,7 +569,15 @@ export const RightWidgetTab = observer(
padding: 1,
minHeight: "200px",
maxHeight: "300px",
overflowY: "scroll",
overflowY: "auto",
width: "100%",
"&::-webkit-scrollbar": {
display: "none",
},
"&": {
scrollbarWidth: "none",
},
background:
"rgba(179, 165, 152, 0.4), linear-gradient(180deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.2) 100%)",
@ -565,13 +603,13 @@ export const RightWidgetTab = observer(
sx={{
p: 2,
display: "flex",
justifyContent: "space-between",
justifyContent: "center",
fontSize: "24px",
fontWeight: 700,
lineHeight: "120%",
flexWrap: "wrap",
gap: 1,
gap: "34px",
backdropFilter: "blur(12px)",
boxShadow:
"inset 4px 4px 12px 0 rgba(255,255,255,0.12)",
@ -627,6 +665,14 @@ export const RightWidgetTab = observer(
<UploadMediaDialog
open={uploadMediaOpen}
onClose={() => setUploadMediaOpen(false)}
contextObjectName={sight[language].name}
contextType="sight"
isArticle={true}
articleName={
activeArticleIndex !== null
? sight[language].right[activeArticleIndex].heading
: "Правая статья"
}
afterUpload={async (media) => {
setUploadMediaOpen(false);
setFileToUpload(null);

View File

@ -16,3 +16,4 @@ export * from "./LeaveAgree";
export * from "./DeleteModal";
export * from "./SnapshotRestore";
export * from "./CreateButton";
export * from "./modals";

View File

@ -0,0 +1,140 @@
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
TextField,
Typography,
IconButton,
Box,
} from "@mui/material";
import { routeStore } from "@shared";
import { observer } from "mobx-react-lite";
import { ArrowLeft } from "lucide-react";
import { useParams } from "react-router-dom";
import { toast } from "react-toastify";
interface EditStationModalProps {
open: boolean;
onClose: () => void;
}
const transferFields = [
{ key: "bus", label: "Автобус" },
{ key: "metro_blue", label: "Метро (синяя)" },
{ key: "metro_green", label: "Метро (зеленая)" },
{ key: "metro_orange", label: "Метро (оранжевая)" },
{ key: "metro_purple", label: "Метро (фиолетовая)" },
{ key: "metro_red", label: "Метро (красная)" },
{ key: "train", label: "Электричка" },
{ key: "tram", label: "Трамвай" },
{ key: "trolleybus", label: "Троллейбус" },
];
export const EditStationModal = observer(
({ open, onClose }: EditStationModalProps) => {
const { id: routeId } = useParams<{ id: string }>();
const {
selectedStationId,
setRouteStations,
saveRouteStations,
routeStations,
} = routeStore;
const handleSave = async () => {
console.log(routeId, selectedStationId);
await saveRouteStations(Number(routeId), selectedStationId);
toast.success("Успешно сохранено");
onClose();
};
const station = routeStations[Number(routeId)]?.find(
(station: any) => station.id === selectedStationId
);
return (
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
<DialogTitle>
<Box display="flex" alignItems="center" gap={2}>
<IconButton onClick={onClose}>
<ArrowLeft />
</IconButton>
<Box>
<Typography variant="caption" color="text.secondary">
Маршруты / Редактировать
</Typography>
<Typography variant="h6">Редактирование остановки</Typography>
</Box>
</Box>
</DialogTitle>
<DialogContent>
<Box sx={{ mt: 2, display: "flex", gap: 2, flexDirection: "column" }}>
<TextField
label="Смещение (X)"
name="offset_x"
type="number"
fullWidth
onChange={(e) => {
setRouteStations(Number(routeId), selectedStationId, {
offset_x: Number(e.target.value),
});
}}
defaultValue={station?.offset_x}
/>
<TextField
label="Смещение (Y)"
name="offset_y"
type="number"
fullWidth
onChange={(e) => {
setRouteStations(Number(routeId), selectedStationId, {
offset_y: Number(e.target.value),
});
}}
defaultValue={station?.offset_y}
/>
</Box>
<Box sx={{ mt: 4 }}>
<Typography variant="h6" gutterBottom>
Пересадки
</Typography>
<Box
sx={{
display: "grid",
gridTemplateColumns: "repeat(3, 1fr)",
gap: 2,
}}
>
{transferFields.map(({ key, label }) => (
<TextField
key={key}
label={label}
name={key}
fullWidth
defaultValue={station?.transfers?.[key]}
onChange={(e) => {
setRouteStations(Number(routeId), selectedStationId, {
...station,
transfers: {
...station?.transfers,
[key]: e.target.value,
},
});
}}
/>
))}
</Box>
</Box>
</DialogContent>
<DialogActions sx={{ p: 3, justifyContent: "flex-end" }}>
<Button onClick={handleSave} variant="contained" color="primary">
Сохранить
</Button>
</DialogActions>
</Dialog>
);
}
);

View File

@ -1 +1,2 @@
export * from "./SelectArticleDialog";
export * from "./EditStationModal";