fix: Delete ai comments
This commit is contained in:
@@ -11,8 +11,8 @@ import {
|
||||
devicesStore,
|
||||
Modal,
|
||||
snapshotStore,
|
||||
vehicleStore, // Not directly used in this component's rendering logic anymore
|
||||
} from "@shared"; // Assuming @shared exports these
|
||||
vehicleStore,
|
||||
} from "@shared";
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Button, Checkbox, Typography } from "@mui/material";
|
||||
@@ -23,12 +23,10 @@ import { useNavigate } from "react-router-dom";
|
||||
export type ConnectedDevice = string;
|
||||
|
||||
interface Snapshot {
|
||||
ID: string; // Assuming ID is string based on usage
|
||||
ID: string;
|
||||
Name: string;
|
||||
// Add other snapshot properties if needed
|
||||
}
|
||||
|
||||
// --- HELPER FUNCTIONS ---
|
||||
const formatDate = (dateString: string | null) => {
|
||||
if (!dateString) return "Нет данных";
|
||||
try {
|
||||
@@ -76,12 +74,7 @@ function createData(
|
||||
};
|
||||
}
|
||||
|
||||
// This function transforms the raw device data (which includes vehicle and device_status)
|
||||
// into the format expected by the table. It now filters for devices that have a UUID.
|
||||
const transformDevicesToRows = (
|
||||
vehicles: Vehicle[]
|
||||
// devices: ConnectedDevice[]
|
||||
): TableRowData[] => {
|
||||
const transformDevicesToRows = (vehicles: Vehicle[]): TableRowData[] => {
|
||||
return vehicles.map((vehicle) => {
|
||||
const uuid = vehicle.vehicle.uuid;
|
||||
if (!uuid)
|
||||
@@ -115,26 +108,21 @@ export const DevicesTable = observer(() => {
|
||||
} = devicesStore;
|
||||
|
||||
const { snapshots, getSnapshots } = snapshotStore;
|
||||
const { getVehicles, vehicles } = vehicleStore; // Removed as devicesStore.devices should be the source of truth
|
||||
const { getVehicles, vehicles } = vehicleStore;
|
||||
const { devices } = devicesStore;
|
||||
const navigate = useNavigate();
|
||||
const [selectedDeviceUuids, setSelectedDeviceUuids] = useState<string[]>([]);
|
||||
|
||||
// Transform the raw devices data into rows suitable for the table
|
||||
// This will also filter out devices without a UUID, as those cannot be acted upon.
|
||||
const currentTableRows = transformDevicesToRows(
|
||||
vehicles.data as Vehicle[]
|
||||
// devices as ConnectedDevice[]
|
||||
);
|
||||
const currentTableRows = transformDevicesToRows(vehicles.data as Vehicle[]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
await getVehicles(); // Not strictly needed if devicesStore.devices is populated correctly by getDevices
|
||||
await getDevices(); // This should fetch the combined vehicle/device_status data
|
||||
await getVehicles();
|
||||
await getDevices();
|
||||
await getSnapshots();
|
||||
};
|
||||
fetchData();
|
||||
}, [getDevices, getSnapshots]); // Added dependencies
|
||||
}, [getDevices, getSnapshots]);
|
||||
|
||||
const isAllSelected =
|
||||
currentTableRows.length > 0 &&
|
||||
@@ -144,7 +132,6 @@ export const DevicesTable = observer(() => {
|
||||
if (isAllSelected) {
|
||||
setSelectedDeviceUuids([]);
|
||||
} else {
|
||||
// Select all device UUIDs from the *currently visible and selectable* rows
|
||||
setSelectedDeviceUuids(
|
||||
currentTableRows.map((row) => row.device_uuid ?? "")
|
||||
);
|
||||
@@ -171,14 +158,13 @@ export const DevicesTable = observer(() => {
|
||||
};
|
||||
|
||||
const handleReloadStatus = async (uuid: string) => {
|
||||
setSelectedDevice(uuid); // Sets the device in the store, useful for context elsewhere
|
||||
setSelectedDevice(uuid);
|
||||
try {
|
||||
await authInstance.post(`/devices/${uuid}/request-status`);
|
||||
await getVehicles();
|
||||
await getDevices(); // Refresh devices to show updated status
|
||||
await getDevices();
|
||||
} catch (error) {
|
||||
console.error(`Error requesting status for device ${uuid}:`, error);
|
||||
// Optionally: show a user-facing error message
|
||||
}
|
||||
};
|
||||
|
||||
@@ -200,22 +186,16 @@ export const DevicesTable = observer(() => {
|
||||
}
|
||||
};
|
||||
try {
|
||||
// Create an array of promises for all snapshot requests
|
||||
const snapshotPromises = selectedDeviceUuids.map((deviceUuid) => {
|
||||
return send(deviceUuid);
|
||||
});
|
||||
|
||||
// Wait for all promises to settle (either resolve or reject)
|
||||
await Promise.allSettled(snapshotPromises);
|
||||
|
||||
// After all requests are attempted
|
||||
await getDevices(); // Refresh the device list
|
||||
setSelectedDeviceUuids([]); // Clear the selection
|
||||
toggleSendSnapshotModal(); // Close the modal
|
||||
await getDevices();
|
||||
setSelectedDeviceUuids([]);
|
||||
toggleSendSnapshotModal();
|
||||
} catch (error) {
|
||||
// This catch block might not be hit if Promise.allSettled is used,
|
||||
// as it doesn't reject on individual promise failures.
|
||||
// Individual errors should be handled if needed within the .map or by checking results.
|
||||
console.error("Error in snapshot sending process:", error);
|
||||
}
|
||||
};
|
||||
@@ -235,7 +215,7 @@ export const DevicesTable = observer(() => {
|
||||
</div>
|
||||
<div className="flex justify-end p-3 gap-2">
|
||||
<Button
|
||||
variant="outlined" // Changed to outlined for distinction
|
||||
variant="outlined"
|
||||
onClick={handleSelectAllDevices}
|
||||
size="small"
|
||||
>
|
||||
@@ -286,7 +266,6 @@ export const DevicesTable = observer(() => {
|
||||
)}
|
||||
selected={selectedDeviceUuids.includes(row.device_uuid ?? "")}
|
||||
onClick={(event) => {
|
||||
// Allow clicking row to toggle checkbox, if not clicking on button
|
||||
if (
|
||||
(event.target as HTMLElement).closest("button") === null &&
|
||||
(event.target as HTMLElement).closest(
|
||||
@@ -308,7 +287,6 @@ export const DevicesTable = observer(() => {
|
||||
}
|
||||
}
|
||||
|
||||
// Only toggle checkbox if Shift key is not pressed
|
||||
if (!event.shiftKey) {
|
||||
handleSelectDevice(
|
||||
{
|
||||
@@ -317,7 +295,7 @@ export const DevicesTable = observer(() => {
|
||||
row.device_uuid ?? ""
|
||||
),
|
||||
},
|
||||
} as React.ChangeEvent<HTMLInputElement>, // Simulate event
|
||||
} as React.ChangeEvent<HTMLInputElement>,
|
||||
row.device_uuid ?? ""
|
||||
);
|
||||
}
|
||||
@@ -445,7 +423,7 @@ export const DevicesTable = observer(() => {
|
||||
</strong>
|
||||
</Typography>
|
||||
<div className="mt-2 flex flex-col gap-2 max-h-[300px] overflow-y-auto pr-2">
|
||||
{snapshots && (snapshots as Snapshot[]).length > 0 ? ( // Cast snapshots
|
||||
{snapshots && (snapshots as Snapshot[]).length > 0 ? (
|
||||
(snapshots as Snapshot[]).map((snapshot) => (
|
||||
<Button
|
||||
variant="outlined"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useRef, DragEvent } from "react";
|
||||
import { Paper, Box, Typography, Button, Tooltip } from "@mui/material";
|
||||
import { X, Info, Plus } from "lucide-react"; // Assuming lucide-react for icons
|
||||
import { X, Info, Plus } from "lucide-react";
|
||||
import { editSightStore } from "@shared";
|
||||
import { toast } from "react-toastify";
|
||||
interface ImageUploadCardProps {
|
||||
@@ -50,14 +50,14 @@ export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
|
||||
toast.error("Пожалуйста, выберите изображение");
|
||||
}
|
||||
}
|
||||
// Reset the input value so selecting the same file again triggers change
|
||||
|
||||
event.target.value = "";
|
||||
};
|
||||
|
||||
const token = localStorage.getItem("token");
|
||||
// --- Drag and Drop Handlers ---
|
||||
|
||||
const handleDragOver = (event: DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault(); // Crucial to allow a drop
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
@@ -67,7 +67,7 @@ export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
|
||||
};
|
||||
|
||||
const handleDrop = async (event: DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault(); // Crucial to allow a drop
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const files = event.dataTransfer.files;
|
||||
@@ -120,7 +120,6 @@ export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
|
||||
cursor: imageUrl ? "pointer" : "default",
|
||||
}}
|
||||
onClick={onImageClick}
|
||||
// Removed onClick on the main Box to avoid conflicts
|
||||
>
|
||||
{imageUrl && (
|
||||
<button
|
||||
@@ -153,7 +152,7 @@ export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
|
||||
borderRadius: 1,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={handleZoneClick} // Click handler for the zone
|
||||
onClick={handleZoneClick}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
@@ -167,8 +166,8 @@ export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
|
||||
color="primary"
|
||||
startIcon={<Plus color="white" size={18} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // Prevent `handleZoneClick` from firing
|
||||
onSelectFileClick(); // This button might trigger a different modal
|
||||
e.stopPropagation();
|
||||
onSelectFileClick();
|
||||
}}
|
||||
>
|
||||
Выбрать файл
|
||||
@@ -179,7 +178,7 @@ export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileInputChange}
|
||||
style={{ display: "none" }}
|
||||
accept="image/*" // Accept only image files
|
||||
accept="image/*"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -3,11 +3,9 @@ import { OrbitControls, Stage, useGLTF } from "@react-three/drei";
|
||||
import { useEffect, Suspense } from "react";
|
||||
import { Box, CircularProgress, Typography } from "@mui/material";
|
||||
|
||||
// Утилита для очистки кеша GLTF
|
||||
const clearGLTFCache = (url?: string) => {
|
||||
try {
|
||||
if (url) {
|
||||
// Если это blob URL, очищаем его из кеша
|
||||
if (url.startsWith("blob:")) {
|
||||
useGLTF.clear(url);
|
||||
} else {
|
||||
@@ -19,29 +17,23 @@ const clearGLTFCache = (url?: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Утилита для проверки типа файла
|
||||
const isValid3DFile = (url: string): boolean => {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
const pathname = urlObj.pathname.toLowerCase();
|
||||
const searchParams = urlObj.searchParams;
|
||||
|
||||
// Проверяем расширение файла в пути
|
||||
const validExtensions = [".glb", ".gltf"];
|
||||
const hasValidExtension = validExtensions.some((ext) =>
|
||||
pathname.endsWith(ext)
|
||||
);
|
||||
|
||||
// Проверяем параметры запроса на наличие типа файла
|
||||
const fileType = searchParams.get("type") || searchParams.get("format");
|
||||
const hasValidType =
|
||||
fileType && ["glb", "gltf"].includes(fileType.toLowerCase());
|
||||
|
||||
// Если это blob URL, считаем его валидным (пользователь выбрал файл)
|
||||
const isBlobUrl = url.startsWith("blob:");
|
||||
|
||||
// Если это URL с токеном и нет явного расширения, считаем валидным
|
||||
// (предполагаем что сервер вернет правильный файл)
|
||||
const hasToken = searchParams.has("token");
|
||||
const isServerUrl = hasToken && !hasValidExtension;
|
||||
|
||||
@@ -51,7 +43,7 @@ const isValid3DFile = (url: string): boolean => {
|
||||
return isValid;
|
||||
} catch (error) {
|
||||
console.warn("⚠️ isValid3DFile: Ошибка при проверке URL", error);
|
||||
// В случае ошибки парсинга URL, считаем валидным (пусть useGLTF сам разберется)
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
@@ -63,13 +55,10 @@ type ModelViewerProps = {
|
||||
};
|
||||
|
||||
const Model = ({ fileUrl }: { fileUrl: string }) => {
|
||||
// Очищаем кеш перед загрузкой новой модели
|
||||
useEffect(() => {
|
||||
// Очищаем кеш для текущего URL
|
||||
clearGLTFCache(fileUrl);
|
||||
}, [fileUrl]);
|
||||
|
||||
// Проверяем валидность файла перед загрузкой (только для blob URL)
|
||||
if (fileUrl.startsWith("blob:") && !isValid3DFile(fileUrl)) {
|
||||
console.warn("⚠️ Model: Попытка загрузить невалидный 3D файл", { fileUrl });
|
||||
throw new Error(`Файл не является корректной 3D моделью: ${fileUrl}`);
|
||||
@@ -114,16 +103,13 @@ export const ThreeView = ({
|
||||
height = "100%",
|
||||
width = "100%",
|
||||
}: ModelViewerProps) => {
|
||||
// Проверяем валидность файла (только для blob URL)
|
||||
useEffect(() => {
|
||||
if (fileUrl.startsWith("blob:") && !isValid3DFile(fileUrl)) {
|
||||
console.warn("⚠️ ThreeView: Невалидный 3D файл", { fileUrl });
|
||||
}
|
||||
}, [fileUrl]);
|
||||
|
||||
// Очищаем кеш при размонтировании и при смене URL
|
||||
useEffect(() => {
|
||||
// Очищаем кеш сразу при монтировании компонента
|
||||
clearGLTFCache(fileUrl);
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -35,7 +35,6 @@ export class ThreeViewErrorBoundary extends Component<Props, State> {
|
||||
props: Props,
|
||||
state: State
|
||||
): Partial<State> | null {
|
||||
// Сбрасываем ошибку ТОЛЬКО при смене медиа (когда меняется ID в resetKey)
|
||||
if (
|
||||
props.resetKey !== state.lastResetKey &&
|
||||
state.lastResetKey !== undefined
|
||||
@@ -43,7 +42,6 @@ export class ThreeViewErrorBoundary extends Component<Props, State> {
|
||||
const oldMediaId = String(state.lastResetKey).split("-")[0];
|
||||
const newMediaId = String(props.resetKey).split("-")[0];
|
||||
|
||||
// Сбрасываем ошибку только если изменился ID медиа (пользователь выбрал другую модель)
|
||||
if (oldMediaId !== newMediaId) {
|
||||
return {
|
||||
hasError: false,
|
||||
@@ -52,9 +50,6 @@ export class ThreeViewErrorBoundary extends Component<Props, State> {
|
||||
};
|
||||
}
|
||||
|
||||
// Если изменился только счетчик (нажата кнопка "Попробовать снова"), обновляем lastResetKey
|
||||
// но не сбрасываем ошибку автоматически - ждем результата загрузки
|
||||
|
||||
return {
|
||||
lastResetKey: props.resetKey,
|
||||
};
|
||||
@@ -127,15 +122,12 @@ export class ThreeViewErrorBoundary extends Component<Props, State> {
|
||||
};
|
||||
|
||||
handleReset = () => {
|
||||
// Сначала сбрасываем состояние ошибки
|
||||
this.setState(
|
||||
{
|
||||
hasError: false,
|
||||
error: null,
|
||||
},
|
||||
() => {
|
||||
// После того как состояние обновилось, вызываем callback для изменения resetKey
|
||||
// Это приведет к пересозданию компонента и новой попытке загрузки
|
||||
this.props.onReset?.();
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
// import { Box, Button, Paper, Typography } from "@mui/material";
|
||||
// import { X, Upload } from "lucide-react";
|
||||
// import { useCallback, useState } from "react";
|
||||
// import { useDropzone } from "react-dropzone";
|
||||
// import { UploadMediaDialog } from "@shared";
|
||||
// import { createSightStore } from "@shared";
|
||||
|
||||
// interface MediaUploadBoxProps {
|
||||
// title: string;
|
||||
// tooltip?: string;
|
||||
// mediaId: string | null;
|
||||
// onMediaSelect: (mediaId: string) => void;
|
||||
// onMediaRemove: () => void;
|
||||
// onPreviewClick: (mediaId: string) => void;
|
||||
// token: string;
|
||||
// type: "thumbnail" | "watermark_lu" | "watermark_rd";
|
||||
// }
|
||||
|
||||
// export const MediaUploadBox = ({
|
||||
// title,
|
||||
// tooltip,
|
||||
// mediaId,
|
||||
// onMediaSelect,
|
||||
// onMediaRemove,
|
||||
// onPreviewClick,
|
||||
// token,
|
||||
// type,
|
||||
// }: MediaUploadBoxProps) => {
|
||||
// const [uploadMediaOpen, setUploadMediaOpen] = useState(false);
|
||||
// const [fileToUpload, setFileToUpload] = useState<File | null>(null);
|
||||
|
||||
// const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||
// if (acceptedFiles.length > 0) {
|
||||
// setFileToUpload(acceptedFiles[0]);
|
||||
// setUploadMediaOpen(true);
|
||||
// }
|
||||
// }, []);
|
||||
|
||||
// const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
// onDrop,
|
||||
// accept: {
|
||||
// "image/*": [".png", ".jpg", ".jpeg", ".gif"],
|
||||
// },
|
||||
// multiple: false,
|
||||
// });
|
||||
|
||||
// const handleUploadComplete = async (media: {
|
||||
// id: string;
|
||||
// filename: string;
|
||||
// media_name?: string;
|
||||
// media_type: number;
|
||||
// }) => {
|
||||
// onMediaSelect(media.id);
|
||||
// };
|
||||
|
||||
// return (
|
||||
// <>
|
||||
// <Paper
|
||||
// elevation={2}
|
||||
// sx={{
|
||||
// padding: 2,
|
||||
// display: "flex",
|
||||
// flexDirection: "column",
|
||||
// alignItems: "center",
|
||||
// gap: 1,
|
||||
// flex: 1,
|
||||
// minWidth: 150,
|
||||
// }}
|
||||
// >
|
||||
// <Box sx={{ display: "flex", alignItems: "center" }}>
|
||||
// <Typography variant="subtitle2" gutterBottom sx={{ mb: 0, mr: 0.5 }}>
|
||||
// {title}
|
||||
// </Typography>
|
||||
// </Box>
|
||||
// <Box
|
||||
// {...getRootProps()}
|
||||
// sx={{
|
||||
// position: "relative",
|
||||
// width: "200px",
|
||||
// height: "200px",
|
||||
// display: "flex",
|
||||
// alignItems: "center",
|
||||
// justifyContent: "center",
|
||||
// borderRadius: 1,
|
||||
// mb: 1,
|
||||
// cursor: mediaId ? "pointer" : "default",
|
||||
// border: isDragActive ? "2px dashed #1976d2" : "none",
|
||||
// backgroundColor: isDragActive
|
||||
// ? "rgba(25, 118, 210, 0.04)"
|
||||
// : "transparent",
|
||||
// transition: "all 0.2s ease",
|
||||
// }}
|
||||
// >
|
||||
// <input {...getInputProps()} />
|
||||
// {mediaId && (
|
||||
// <button
|
||||
// className="absolute top-2 right-2 z-10"
|
||||
// onClick={(e) => {
|
||||
// e.stopPropagation();
|
||||
// onMediaRemove();
|
||||
// }}
|
||||
// >
|
||||
// <X color="red" />
|
||||
// </button>
|
||||
// )}
|
||||
// {mediaId ? (
|
||||
// <img
|
||||
// src={`${
|
||||
// import.meta.env.VITE_KRBL_MEDIA
|
||||
// }${mediaId}/download?token=${token}`}
|
||||
// alt={title}
|
||||
// style={{ maxWidth: "100%", maxHeight: "100%" }}
|
||||
// onClick={(e) => {
|
||||
// e.stopPropagation();
|
||||
// onPreviewClick(mediaId);
|
||||
// }}
|
||||
// />
|
||||
// ) : (
|
||||
// <div className="w-full flex flex-col items-center justify-center gap-3">
|
||||
// <div
|
||||
// className={`w-full h-20 border rounded-md flex flex-col items-center transition-all duration-300 justify-center border-dashed ${
|
||||
// isDragActive
|
||||
// ? "border-blue-500 bg-blue-50"
|
||||
// : "border-gray-300"
|
||||
// } cursor-pointer hover:bg-gray-100`}
|
||||
// >
|
||||
// <Upload size={24} className="mb-2" />
|
||||
// <p>
|
||||
// {isDragActive ? "Отпустите файл здесь" : "Перетащите файл"}
|
||||
// </p>
|
||||
// </div>
|
||||
// <p>или</p>
|
||||
// <Button
|
||||
// variant="contained"
|
||||
// color="primary"
|
||||
// onClick={(e) => {
|
||||
// e.stopPropagation();
|
||||
// onMediaSelect("");
|
||||
// }}
|
||||
// >
|
||||
// Выбрать файл
|
||||
// </Button>
|
||||
// </div>
|
||||
// )}
|
||||
// </Box>
|
||||
// </Paper>
|
||||
|
||||
// <UploadMediaDialog
|
||||
// open={uploadMediaOpen}
|
||||
// onClose={() => {
|
||||
// setUploadMediaOpen(false);
|
||||
// setFileToUpload(null);
|
||||
// }}
|
||||
// afterUpload={handleUploadComplete}
|
||||
// />
|
||||
// </>
|
||||
// );
|
||||
// };
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @widgets/LeftWidgetTab.tsx
|
||||
import { Box, Button, TextField, Paper, Typography } from "@mui/material";
|
||||
import {
|
||||
BackButton,
|
||||
@@ -50,17 +49,6 @@ export const CreateLeftTab = observer(
|
||||
const [isSelectMediaDialogOpen, setIsSelectMediaDialogOpen] =
|
||||
useState(false);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
// const handleMediaSelected = useCallback(() => {
|
||||
// // При выборе медиа, обновляем данные для ТЕКУЩЕГО ЯЗЫКА
|
||||
// // сохраняя текущие heading и body.
|
||||
// updateSightInfo(language, {
|
||||
// left: {
|
||||
// heading: data.left.heading,
|
||||
// body: data.left.body,
|
||||
// },
|
||||
// });
|
||||
// setIsSelectMediaDialogOpen(false);
|
||||
// }, [language, data.left.heading, data.left.body]);
|
||||
|
||||
const handleCloseArticleDialog = useCallback(() => {
|
||||
setIsSelectArticleDialogOpen(false);
|
||||
|
||||
@@ -13,28 +13,27 @@ import {
|
||||
languageStore,
|
||||
SelectArticleModal,
|
||||
TabPanel,
|
||||
SelectMediaDialog, // Import
|
||||
SelectMediaDialog,
|
||||
UploadMediaDialog,
|
||||
Media, // Import
|
||||
Media,
|
||||
} from "@shared";
|
||||
import {
|
||||
LanguageSwitcher,
|
||||
MediaArea, // Import
|
||||
MediaAreaForSight, // Import
|
||||
MediaArea,
|
||||
MediaAreaForSight,
|
||||
ReactMarkdownComponent,
|
||||
ReactMarkdownEditor,
|
||||
DeleteModal,
|
||||
} from "@widgets";
|
||||
import { ImagePlus, Plus, Save, Trash2, Unlink, X } from "lucide-react"; // Import X
|
||||
import { ImagePlus, Plus, Save, Trash2, Unlink, X } from "lucide-react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useState, useEffect } from "react"; // Added useEffect
|
||||
import { useState, useEffect } from "react";
|
||||
import { MediaViewer } from "../../MediaViewer/index";
|
||||
import { toast } from "react-toastify";
|
||||
import { authInstance } from "@shared";
|
||||
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";
|
||||
|
||||
type MediaItemShared = {
|
||||
// Define if not already available from @shared
|
||||
id: string;
|
||||
filename: string;
|
||||
media_name?: string;
|
||||
@@ -52,14 +51,14 @@ export const CreateRightTab = observer(
|
||||
unlinkPreviewMedia,
|
||||
createLinkWithRightArticle,
|
||||
deleteRightArticleMedia,
|
||||
setFileToUpload, // From store
|
||||
setUploadMediaOpen, // From store
|
||||
uploadMediaOpen, // From store
|
||||
unlinkRightAritcle, // Corrected spelling
|
||||
setFileToUpload,
|
||||
setUploadMediaOpen,
|
||||
uploadMediaOpen,
|
||||
unlinkRightAritcle,
|
||||
deleteRightArticle,
|
||||
linkExistingRightArticle,
|
||||
createSight,
|
||||
clearCreateSight, // For resetting form
|
||||
clearCreateSight,
|
||||
updateRightArticles,
|
||||
} = createSightStore;
|
||||
const { language } = languageStore;
|
||||
@@ -78,7 +77,7 @@ export const CreateRightTab = observer(
|
||||
>(null);
|
||||
|
||||
const [previewMedia, setPreviewMedia] = useState<Media | null>(null);
|
||||
// Reset activeArticleIndex if language changes and index is out of bounds
|
||||
|
||||
useEffect(() => {
|
||||
if (sight.preview_media) {
|
||||
const fetchMedia = async () => {
|
||||
@@ -97,7 +96,7 @@ export const CreateRightTab = observer(
|
||||
activeArticleIndex >= sight[language].right.length
|
||||
) {
|
||||
setActiveArticleIndex(null);
|
||||
setType("media"); // Default back to media preview if selected article disappears
|
||||
setType("media");
|
||||
}
|
||||
}, [language, sight[language].right, activeArticleIndex]);
|
||||
|
||||
@@ -113,10 +112,9 @@ export const CreateRightTab = observer(
|
||||
try {
|
||||
await createSight(language);
|
||||
toast.success("Достопримечательность успешно создана!");
|
||||
clearCreateSight(); // Reset form
|
||||
clearCreateSight();
|
||||
setActiveArticleIndex(null);
|
||||
setType("media");
|
||||
// Potentially navigate away: history.push('/sights-list');
|
||||
} catch (error) {
|
||||
console.error("Failed to save sight:", error);
|
||||
toast.error("Ошибка при создании достопримечательности.");
|
||||
@@ -132,7 +130,7 @@ export const CreateRightTab = observer(
|
||||
handleCloseMenu();
|
||||
try {
|
||||
const newArticleId = await createNewRightArticle();
|
||||
// Automatically select the new article if ID is returned
|
||||
|
||||
const newIndex = sight[language].right.findIndex(
|
||||
(a) => a.id === newArticleId
|
||||
);
|
||||
@@ -140,7 +138,6 @@ export const CreateRightTab = observer(
|
||||
setActiveArticleIndex(newIndex);
|
||||
setType("article");
|
||||
} else {
|
||||
// Fallback if findIndex fails (should not happen if store updates correctly)
|
||||
setActiveArticleIndex(sight[language].right.length - 1);
|
||||
setType("article");
|
||||
}
|
||||
@@ -156,7 +153,7 @@ export const CreateRightTab = observer(
|
||||
const linkedArticleId = await linkExistingRightArticle(
|
||||
selectedArticleId
|
||||
);
|
||||
setSelectArticleDialogOpen(false); // Close dialog
|
||||
setSelectArticleDialogOpen(false);
|
||||
const newIndex = sight[language].right.findIndex(
|
||||
(a) => a.id === linkedArticleId
|
||||
);
|
||||
@@ -174,7 +171,6 @@ export const CreateRightTab = observer(
|
||||
? sight[language].right[activeArticleIndex]
|
||||
: null;
|
||||
|
||||
// Media Handling for Dialogs
|
||||
const handleOpenUploadMedia = () => {
|
||||
setUploadMediaOpen(true);
|
||||
};
|
||||
@@ -203,7 +199,6 @@ export const CreateRightTab = observer(
|
||||
};
|
||||
|
||||
const handleMediaUploaded = async (media: MediaItemShared) => {
|
||||
// After UploadMediaDialog finishes
|
||||
setUploadMediaOpen(false);
|
||||
setFileToUpload(null);
|
||||
if (mediaTarget === "sightPreview") {
|
||||
@@ -211,36 +206,25 @@ export const CreateRightTab = observer(
|
||||
} else if (mediaTarget === "rightArticle" && currentRightArticle) {
|
||||
await createLinkWithRightArticle(media, currentRightArticle.id);
|
||||
}
|
||||
setMediaTarget(null); // Reset target
|
||||
setMediaTarget(null);
|
||||
};
|
||||
|
||||
const handleDragEnd = (result: any) => {
|
||||
const { source, destination } = result;
|
||||
|
||||
// 1. Guard clause: If dropped outside any droppable area, do nothing.
|
||||
if (!destination) return;
|
||||
|
||||
// Extract source and destination indices
|
||||
const sourceIndex = source.index;
|
||||
const destinationIndex = destination.index;
|
||||
|
||||
// 2. Guard clause: If dropped in the same position, do nothing.
|
||||
if (sourceIndex === destinationIndex) return;
|
||||
|
||||
// 3. Create a new array with reordered articles:
|
||||
// - Create a shallow copy of the current articles array.
|
||||
// This is important for immutability and triggering re-renders.
|
||||
const newRightArticles = [...sight[language].right];
|
||||
|
||||
// - Remove the dragged article from its original position.
|
||||
// `splice` returns an array of removed items, so we destructure the first (and only) one.
|
||||
const [movedArticle] = newRightArticles.splice(sourceIndex, 1);
|
||||
|
||||
// - Insert the moved article into its new position.
|
||||
newRightArticles.splice(destinationIndex, 0, movedArticle);
|
||||
|
||||
// 4. Update the store with the new order:
|
||||
// This will typically trigger a re-render of the component with the updated list.
|
||||
updateRightArticles(newRightArticles);
|
||||
};
|
||||
|
||||
@@ -254,7 +238,7 @@ export const CreateRightTab = observer(
|
||||
height: "100%",
|
||||
minHeight: "calc(100vh - 200px)",
|
||||
gap: 2,
|
||||
paddingBottom: "70px", // Space for the save button
|
||||
paddingBottom: "70px",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
@@ -264,7 +248,6 @@ export const CreateRightTab = observer(
|
||||
</div>
|
||||
|
||||
<Box sx={{ display: "flex", flexGrow: 1, gap: 2.5 }}>
|
||||
{/* Left Column: Navigation & Article List */}
|
||||
<Box className="flex flex-col w-[75%] gap-2">
|
||||
<Box className="w-full flex gap-2 ">
|
||||
<Box className="relative w-[20%] h-[70vh] flex flex-col rounded-2xl overflow-y-auto gap-3 border border-gray-300 p-3">
|
||||
@@ -272,7 +255,6 @@ export const CreateRightTab = observer(
|
||||
<Box
|
||||
onClick={() => {
|
||||
setType("media");
|
||||
// setActiveArticleIndex(null); // Optional: deselect article when switching to general media view
|
||||
}}
|
||||
className={`w-full p-4 rounded-2xl cursor-pointer text-sm hover:bg-gray-300 transition-all duration-300 ${
|
||||
type === "media"
|
||||
@@ -364,7 +346,6 @@ export const CreateRightTab = observer(
|
||||
</Menu>
|
||||
</Box>
|
||||
|
||||
{/* Main content area: Article Editor or Sight Media Preview */}
|
||||
{type === "article" && currentRightArticle ? (
|
||||
<Box className="w-[80%] border border-gray-300 p-3 flex flex-col gap-2 overflow-hidden">
|
||||
<Box className="flex justify-end gap-2 mb-1 flex-shrink-0">
|
||||
@@ -375,7 +356,7 @@ export const CreateRightTab = observer(
|
||||
startIcon={<Unlink color="white" size={18} />}
|
||||
onClick={() => {
|
||||
if (currentRightArticle) {
|
||||
unlinkRightAritcle(currentRightArticle.id); // Corrected function name
|
||||
unlinkRightAritcle(currentRightArticle.id);
|
||||
setActiveArticleIndex(null);
|
||||
setType("media");
|
||||
}
|
||||
@@ -435,7 +416,7 @@ export const CreateRightTab = observer(
|
||||
/>
|
||||
</Box>
|
||||
<MediaArea
|
||||
articleId={currentRightArticle.id} // Needs a real ID
|
||||
articleId={currentRightArticle.id}
|
||||
mediaIds={currentRightArticle.media || []}
|
||||
onFilesDrop={(files) => {
|
||||
if (files.length > 0) {
|
||||
@@ -507,7 +488,6 @@ export const CreateRightTab = observer(
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Right Column: Live Preview */}
|
||||
<Box className="w-[25%] mr-10">
|
||||
{type === "article" && activeArticleIndex !== null && (
|
||||
<Paper
|
||||
@@ -662,12 +642,11 @@ export const CreateRightTab = observer(
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Sticky Save Button Footer */}
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: "-20px",
|
||||
left: 0, // ensure it spans from left
|
||||
left: 0,
|
||||
right: 0,
|
||||
padding: 2,
|
||||
backgroundColor: "background.paper",
|
||||
@@ -689,19 +668,17 @@ export const CreateRightTab = observer(
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Modals */}
|
||||
<SelectArticleModal
|
||||
open={selectArticleDialogOpen}
|
||||
onClose={() => setSelectArticleDialogOpen(false)}
|
||||
onSelectArticle={handleSelectExistingArticleAndLink}
|
||||
// Pass IDs of already linked/added right articles to exclude them from selection
|
||||
linkedArticleIds={sight[language].right.map((article) => article.id)}
|
||||
/>
|
||||
<UploadMediaDialog
|
||||
open={uploadMediaOpen} // From store
|
||||
open={uploadMediaOpen}
|
||||
onClose={() => {
|
||||
setUploadMediaOpen(false);
|
||||
setFileToUpload(null); // Clear file if dialog is closed without upload
|
||||
setFileToUpload(null);
|
||||
setMediaTarget(null);
|
||||
}}
|
||||
contextObjectName={sight[language].name}
|
||||
@@ -712,7 +689,7 @@ export const CreateRightTab = observer(
|
||||
? sight[language].right[activeArticleIndex].heading
|
||||
: undefined
|
||||
}
|
||||
afterUpload={handleMediaUploaded} // This will use the mediaTarget
|
||||
afterUpload={handleMediaUploaded}
|
||||
/>
|
||||
<SelectMediaDialog
|
||||
open={isSelectMediaDialogOpen}
|
||||
|
||||
@@ -118,7 +118,7 @@ export const RightWidgetTab = observer(
|
||||
try {
|
||||
const newArticleId = await createNewRightArticle();
|
||||
handleClose();
|
||||
// Automatically select the newly created article
|
||||
|
||||
const newIndex = sight[language].right.findIndex(
|
||||
(article) => article.id === newArticleId
|
||||
);
|
||||
@@ -144,7 +144,7 @@ export const RightWidgetTab = observer(
|
||||
try {
|
||||
const linkedArticleId = await linkArticle(id);
|
||||
handleCloseSelectModal();
|
||||
// Automatically select the newly linked article
|
||||
|
||||
const newIndex = sight[language].right.findIndex(
|
||||
(article) => article.id === linkedArticleId
|
||||
);
|
||||
@@ -177,30 +177,19 @@ export const RightWidgetTab = observer(
|
||||
const handleDragEnd = (result: DropResult) => {
|
||||
const { source, destination } = result;
|
||||
|
||||
// 1. Guard clause: If dropped outside any droppable area, do nothing.
|
||||
if (!destination) return;
|
||||
|
||||
// Extract source and destination indices
|
||||
const sourceIndex = source.index;
|
||||
const destinationIndex = destination.index;
|
||||
|
||||
// 2. Guard clause: If dropped in the same position, do nothing.
|
||||
if (sourceIndex === destinationIndex) return;
|
||||
|
||||
// 3. Create a new array with reordered articles:
|
||||
// - Create a shallow copy of the current articles array.
|
||||
// This is important for immutability and triggering re-renders.
|
||||
const newRightArticles = [...sight[language].right];
|
||||
|
||||
// - Remove the dragged article from its original position.
|
||||
// `splice` returns an array of removed items, so we destructure the first (and only) one.
|
||||
const [movedArticle] = newRightArticles.splice(sourceIndex, 1);
|
||||
|
||||
// - Insert the moved article into its new position.
|
||||
newRightArticles.splice(destinationIndex, 0, movedArticle);
|
||||
|
||||
// 4. Update the store with the new order:
|
||||
// This will typically trigger a re-render of the component with the updated list.
|
||||
updateRightArticles(newRightArticles);
|
||||
};
|
||||
|
||||
|
||||
@@ -28,9 +28,8 @@ export const VideoPreviewCard: React.FC<VideoPreviewCardProps> = ({
|
||||
const token = localStorage.getItem("token");
|
||||
|
||||
useEffect(() => {}, [isDragOver]);
|
||||
// --- Click to select file ---
|
||||
|
||||
const handleZoneClick = () => {
|
||||
// Trigger the hidden file input click
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
@@ -40,19 +39,17 @@ export const VideoPreviewCard: React.FC<VideoPreviewCardProps> = ({
|
||||
const file = event.target.files?.[0];
|
||||
if (file) {
|
||||
if (file.type.startsWith("video/")) {
|
||||
// Открываем диалог загрузки медиа с файлом видео
|
||||
onSelectVideoClick(file);
|
||||
} else {
|
||||
toast.error("Пожалуйста, выберите видео файл");
|
||||
}
|
||||
}
|
||||
// Reset the input value so selecting the same file again triggers change
|
||||
|
||||
event.target.value = "";
|
||||
};
|
||||
|
||||
// --- Drag and Drop Handlers ---
|
||||
const handleDragOver = (event: DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault(); // Crucial to allow a drop
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setIsDragOver(true);
|
||||
};
|
||||
@@ -64,7 +61,7 @@ export const VideoPreviewCard: React.FC<VideoPreviewCardProps> = ({
|
||||
};
|
||||
|
||||
const handleDrop = async (event: DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault(); // Crucial to allow a drop
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setIsDragOver(false);
|
||||
|
||||
@@ -72,7 +69,6 @@ export const VideoPreviewCard: React.FC<VideoPreviewCardProps> = ({
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0];
|
||||
if (file.type.startsWith("video/")) {
|
||||
// Открываем диалог загрузки медиа с файлом видео
|
||||
onSelectVideoClick(file);
|
||||
} else {
|
||||
toast.error("Пожалуйста, выберите видео файл");
|
||||
@@ -175,7 +171,7 @@ export const VideoPreviewCard: React.FC<VideoPreviewCardProps> = ({
|
||||
borderRadius: 1,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={handleZoneClick} // Click handler for the zone
|
||||
onClick={handleZoneClick}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
@@ -189,8 +185,8 @@ export const VideoPreviewCard: React.FC<VideoPreviewCardProps> = ({
|
||||
color="primary"
|
||||
startIcon={<Plus color="white" size={18} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // Prevent `handleZoneClick` from firing
|
||||
onSelectVideoClick(); // This button triggers the media selection dialog
|
||||
e.stopPropagation();
|
||||
onSelectVideoClick();
|
||||
}}
|
||||
>
|
||||
Выбрать файл
|
||||
@@ -201,7 +197,7 @@ export const VideoPreviewCard: React.FC<VideoPreviewCardProps> = ({
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileInputChange}
|
||||
style={{ display: "none" }}
|
||||
accept="video/*" // Accept only video files
|
||||
accept="video/*"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user