Compare commits
7 Commits
#14
...
0a6192c7da
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a6192c7da | |||
| b1ba3b4cd5 | |||
| 1917b2cf5a | |||
| 5298fb9f60 | |||
| c95a6517e9 | |||
| 79f523e9cb | |||
| 90f3d66b22 |
@@ -16,12 +16,7 @@ import {
|
|||||||
SnapshotListPage,
|
SnapshotListPage,
|
||||||
CarrierListPage,
|
CarrierListPage,
|
||||||
StationListPage,
|
StationListPage,
|
||||||
// VehicleListPage,
|
|
||||||
ArticleListPage,
|
ArticleListPage,
|
||||||
|
|
||||||
// CountryPreviewPage,
|
|
||||||
// VehiclePreviewPage,
|
|
||||||
// CarrierPreviewPage,
|
|
||||||
SnapshotCreatePage,
|
SnapshotCreatePage,
|
||||||
CountryCreatePage,
|
CountryCreatePage,
|
||||||
CityCreatePage,
|
CityCreatePage,
|
||||||
@@ -31,7 +26,6 @@ import {
|
|||||||
CityEditPage,
|
CityEditPage,
|
||||||
UserCreatePage,
|
UserCreatePage,
|
||||||
UserEditPage,
|
UserEditPage,
|
||||||
// VehicleEditPage,
|
|
||||||
CarrierEditPage,
|
CarrierEditPage,
|
||||||
StationCreatePage,
|
StationCreatePage,
|
||||||
StationPreviewPage,
|
StationPreviewPage,
|
||||||
@@ -75,7 +69,6 @@ const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
|
|||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Чтобы очистка сторов происходила при смене локации
|
|
||||||
const ClearStoresWrapper: React.FC<{ children: React.ReactNode }> = ({
|
const ClearStoresWrapper: React.FC<{ children: React.ReactNode }> = ({
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -116,65 +109,45 @@ const router = createBrowserRouter([
|
|||||||
children: [
|
children: [
|
||||||
{ index: true, element: <MainPage /> },
|
{ index: true, element: <MainPage /> },
|
||||||
|
|
||||||
// Sight
|
|
||||||
{ path: "sight", element: <SightListPage /> },
|
{ path: "sight", element: <SightListPage /> },
|
||||||
{ path: "sight/create", element: <CreateSightPage /> },
|
{ path: "sight/create", element: <CreateSightPage /> },
|
||||||
{ path: "sight/:id/edit", element: <EditSightPage /> },
|
{ path: "sight/:id/edit", element: <EditSightPage /> },
|
||||||
|
|
||||||
// Device
|
|
||||||
{ path: "devices", element: <DevicesPage /> },
|
{ path: "devices", element: <DevicesPage /> },
|
||||||
|
|
||||||
// Map
|
|
||||||
{ path: "map", element: <MapPage /> },
|
{ path: "map", element: <MapPage /> },
|
||||||
|
|
||||||
// Media
|
|
||||||
{ path: "media", element: <MediaListPage /> },
|
{ path: "media", element: <MediaListPage /> },
|
||||||
{ path: "media/:id", element: <MediaPreviewPage /> },
|
{ path: "media/:id", element: <MediaPreviewPage /> },
|
||||||
{ path: "media/:id/edit", element: <MediaEditPage /> },
|
{ path: "media/:id/edit", element: <MediaEditPage /> },
|
||||||
|
|
||||||
// Country
|
|
||||||
{ path: "country", element: <CountryListPage /> },
|
{ path: "country", element: <CountryListPage /> },
|
||||||
{ path: "country/create", element: <CountryCreatePage /> },
|
{ path: "country/create", element: <CountryCreatePage /> },
|
||||||
{ path: "country/add", element: <CountryAddPage /> },
|
{ path: "country/add", element: <CountryAddPage /> },
|
||||||
// { path: "country/:id", element: <CountryPreviewPage /> },
|
|
||||||
{ path: "country/:id/edit", element: <CountryEditPage /> },
|
{ path: "country/:id/edit", element: <CountryEditPage /> },
|
||||||
// City
|
|
||||||
{ path: "city", element: <CityListPage /> },
|
{ path: "city", element: <CityListPage /> },
|
||||||
{ path: "city/create", element: <CityCreatePage /> },
|
{ path: "city/create", element: <CityCreatePage /> },
|
||||||
// { path: "city/:id", element: <CityPreviewPage /> },
|
|
||||||
{ path: "city/:id/edit", element: <CityEditPage /> },
|
{ path: "city/:id/edit", element: <CityEditPage /> },
|
||||||
// Route
|
|
||||||
{ path: "route", element: <RouteListPage /> },
|
{ path: "route", element: <RouteListPage /> },
|
||||||
{ path: "route/create", element: <RouteCreatePage /> },
|
{ path: "route/create", element: <RouteCreatePage /> },
|
||||||
{ path: "route/:id/edit", element: <RouteEditPage /> },
|
{ path: "route/:id/edit", element: <RouteEditPage /> },
|
||||||
|
|
||||||
// User
|
|
||||||
{ path: "user", element: <UserListPage /> },
|
{ path: "user", element: <UserListPage /> },
|
||||||
{ path: "user/create", element: <UserCreatePage /> },
|
{ path: "user/create", element: <UserCreatePage /> },
|
||||||
{ path: "user/:id/edit", element: <UserEditPage /> },
|
{ path: "user/:id/edit", element: <UserEditPage /> },
|
||||||
// Snapshot
|
|
||||||
{ path: "snapshot", element: <SnapshotListPage /> },
|
{ path: "snapshot", element: <SnapshotListPage /> },
|
||||||
{ path: "snapshot/create", element: <SnapshotCreatePage /> },
|
{ path: "snapshot/create", element: <SnapshotCreatePage /> },
|
||||||
|
|
||||||
// Carrier
|
|
||||||
{ path: "carrier", element: <CarrierListPage /> },
|
{ path: "carrier", element: <CarrierListPage /> },
|
||||||
{ path: "carrier/create", element: <CarrierCreatePage /> },
|
{ path: "carrier/create", element: <CarrierCreatePage /> },
|
||||||
// { path: "carrier/:id", element: <CarrierPreviewPage /> },
|
|
||||||
{ path: "carrier/:id/edit", element: <CarrierEditPage /> },
|
{ path: "carrier/:id/edit", element: <CarrierEditPage /> },
|
||||||
// Station
|
|
||||||
{ path: "station", element: <StationListPage /> },
|
{ path: "station", element: <StationListPage /> },
|
||||||
{ path: "station/create", element: <StationCreatePage /> },
|
{ path: "station/create", element: <StationCreatePage /> },
|
||||||
{ path: "station/:id", element: <StationPreviewPage /> },
|
{ path: "station/:id", element: <StationPreviewPage /> },
|
||||||
{ path: "station/:id/edit", element: <StationEditPage /> },
|
{ path: "station/:id/edit", element: <StationEditPage /> },
|
||||||
// Vehicle
|
|
||||||
// { path: "vehicle", element: <VehicleListPage /> },
|
|
||||||
{ path: "vehicle/create", element: <VehicleCreatePage /> },
|
{ path: "vehicle/create", element: <VehicleCreatePage /> },
|
||||||
// { path: "vehicle/:id", element: <VehiclePreviewPage /> },
|
|
||||||
// { path: "vehicle/:id/edit", element: <VehicleEditPage /> },
|
|
||||||
// Article
|
|
||||||
{ path: "article", element: <ArticleListPage /> },
|
{ path: "article", element: <ArticleListPage /> },
|
||||||
{ path: "article/:id", element: <ArticlePreviewPage /> },
|
{ path: "article/:id", element: <ArticlePreviewPage /> },
|
||||||
// { path: "media/create", element: <CreateMediaPage /> },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ export const CarrierCreatePage = observer(() => {
|
|||||||
languageStore.setLanguage("ru");
|
languageStore.setLanguage("ru");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Автоматически устанавливаем выбранный город при загрузке страницы
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedCityId && !createCarrierData.city_id) {
|
if (selectedCityId && !createCarrierData.city_id) {
|
||||||
setCreateCarrierData(
|
setCreateCarrierData(
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ export const CarrierEditPage = observer(() => {
|
|||||||
mediaStore.getMedia();
|
mediaStore.getMedia();
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// Устанавливаем русский язык при загрузке страницы
|
|
||||||
languageStore.setLanguage("ru");
|
languageStore.setLanguage("ru");
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ export const CityEditPage = observer(() => {
|
|||||||
const { getMedia, getOneMedia } = mediaStore;
|
const { getMedia, getOneMedia } = mediaStore;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Устанавливаем русский язык при загрузке страницы
|
|
||||||
languageStore.setLanguage("ru");
|
languageStore.setLanguage("ru");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -64,12 +63,11 @@ export const CityEditPage = observer(() => {
|
|||||||
(async () => {
|
(async () => {
|
||||||
if (id) {
|
if (id) {
|
||||||
await getCountries("ru");
|
await getCountries("ru");
|
||||||
// Fetch data for all languages
|
|
||||||
const ruData = await getCity(id as string, "ru");
|
const ruData = await getCity(id as string, "ru");
|
||||||
const enData = await getCity(id as string, "en");
|
const enData = await getCity(id as string, "en");
|
||||||
const zhData = await getCity(id as string, "zh");
|
const zhData = await getCity(id as string, "zh");
|
||||||
|
|
||||||
// Set data for each language
|
|
||||||
setEditCityData(ruData.name, ruData.country_code, ruData.arms, "ru");
|
setEditCityData(ruData.name, ruData.country_code, ruData.arms, "ru");
|
||||||
setEditCityData(enData.name, enData.country_code, enData.arms, "en");
|
setEditCityData(enData.name, enData.country_code, enData.arms, "en");
|
||||||
setEditCityData(zhData.name, zhData.country_code, zhData.arms, "zh");
|
setEditCityData(zhData.name, zhData.country_code, zhData.arms, "zh");
|
||||||
@@ -207,7 +205,7 @@ export const CityEditPage = observer(() => {
|
|||||||
open={isSelectMediaOpen}
|
open={isSelectMediaOpen}
|
||||||
onClose={() => setIsSelectMediaOpen(false)}
|
onClose={() => setIsSelectMediaOpen(false)}
|
||||||
onSelectMedia={handleMediaSelect}
|
onSelectMedia={handleMediaSelect}
|
||||||
mediaType={1} // Тип медиа для иконок
|
mediaType={1}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UploadMediaDialog
|
<UploadMediaDialog
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ export const CountryEditPage = observer(() => {
|
|||||||
countryStore;
|
countryStore;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Устанавливаем русский язык при загрузке страницы
|
|
||||||
languageStore.setLanguage("ru");
|
languageStore.setLanguage("ru");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -36,12 +35,10 @@ export const CountryEditPage = observer(() => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
if (id) {
|
if (id) {
|
||||||
// Fetch data for all languages
|
|
||||||
const ruData = await getCountry(id as string, "ru");
|
const ruData = await getCountry(id as string, "ru");
|
||||||
const enData = await getCountry(id as string, "en");
|
const enData = await getCountry(id as string, "en");
|
||||||
const zhData = await getCountry(id as string, "zh");
|
const zhData = await getCountry(id as string, "zh");
|
||||||
|
|
||||||
// Set data for each language
|
|
||||||
setEditCountryData(ruData.name, "ru");
|
setEditCountryData(ruData.name, "ru");
|
||||||
setEditCountryData(enData.name, "en");
|
setEditCountryData(enData.name, "en");
|
||||||
setEditCountryData(zhData.name, "zh");
|
setEditCountryData(zhData.name, "zh");
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ export const LoginPage = () => {
|
|||||||
const { login } = authStore;
|
const { login } = authStore;
|
||||||
const { getUsers } = userStore;
|
const { getUsers } = userStore;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Load saved credentials if they exist
|
|
||||||
const savedEmail = localStorage.getItem("rememberedEmail");
|
const savedEmail = localStorage.getItem("rememberedEmail");
|
||||||
const savedPassword = localStorage.getItem("rememberedPassword");
|
const savedPassword = localStorage.getItem("rememberedPassword");
|
||||||
if (savedEmail && savedPassword) {
|
if (savedEmail && savedPassword) {
|
||||||
@@ -42,7 +41,6 @@ export const LoginPage = () => {
|
|||||||
try {
|
try {
|
||||||
await login(email, password);
|
await login(email, password);
|
||||||
|
|
||||||
// Save or clear credentials based on remember me checkbox
|
|
||||||
if (rememberMe) {
|
if (rememberMe) {
|
||||||
localStorage.setItem("rememberedEmail", email);
|
localStorage.setItem("rememberedEmail", email);
|
||||||
localStorage.setItem("rememberedPassword", password);
|
localStorage.setItem("rememberedPassword", password);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -22,10 +22,8 @@ interface ApiSight {
|
|||||||
longitude: number;
|
longitude: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Допуск для сравнения координат, чтобы избежать ошибок с точностью чисел.
|
|
||||||
const COORDINATE_PRECISION_TOLERANCE = 1e-9;
|
const COORDINATE_PRECISION_TOLERANCE = 1e-9;
|
||||||
|
|
||||||
// Вспомогательная функция, обновленная для сравнения с допуском.
|
|
||||||
const arePathsEqual = (
|
const arePathsEqual = (
|
||||||
path1: [number, number][],
|
path1: [number, number][],
|
||||||
path2: [number, number][]
|
path2: [number, number][]
|
||||||
@@ -136,7 +134,6 @@ class MapStore {
|
|||||||
longitude: geometry.coordinates[0],
|
longitude: geometry.coordinates[0],
|
||||||
};
|
};
|
||||||
|
|
||||||
// ИЗМЕНЕНИЕ: Сравнение координат с допуском
|
|
||||||
if (
|
if (
|
||||||
originalStation.name !== currentStation.name ||
|
originalStation.name !== currentStation.name ||
|
||||||
Math.abs(originalStation.latitude - currentStation.latitude) >
|
Math.abs(originalStation.latitude - currentStation.latitude) >
|
||||||
@@ -155,7 +152,6 @@ class MapStore {
|
|||||||
path: geometry.coordinates,
|
path: geometry.coordinates,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ИЗМЕНЕНИЕ: Используется новая функция arePathsEqual с допуском
|
|
||||||
if (
|
if (
|
||||||
originalRoute.route_number !== currentRoute.route_number ||
|
originalRoute.route_number !== currentRoute.route_number ||
|
||||||
!arePathsEqual(originalRoute.path, currentRoute.path)
|
!arePathsEqual(originalRoute.path, currentRoute.path)
|
||||||
@@ -173,7 +169,6 @@ class MapStore {
|
|||||||
longitude: geometry.coordinates[0],
|
longitude: geometry.coordinates[0],
|
||||||
};
|
};
|
||||||
|
|
||||||
// ИЗМЕНЕНИЕ: Сравнение координат с допуском
|
|
||||||
if (
|
if (
|
||||||
originalSight.name !== currentSight.name ||
|
originalSight.name !== currentSight.name ||
|
||||||
originalSight.description !== currentSight.description ||
|
originalSight.description !== currentSight.description ||
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export const MediaEditPage = observer(() => {
|
|||||||
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [newFile, setNewFile] = useState<File | null>(null);
|
const [newFile, setNewFile] = useState<File | null>(null);
|
||||||
const [uploadDialogOpen, setUploadDialogOpen] = useState(false); // State for the upload dialog
|
const [uploadDialogOpen, setUploadDialogOpen] = useState(false);
|
||||||
|
|
||||||
const media = id ? mediaStore.media.find((m) => m.id === id) : null;
|
const media = id ? mediaStore.media.find((m) => m.id === id) : null;
|
||||||
const [mediaName, setMediaName] = useState(media?.media_name ?? "");
|
const [mediaName, setMediaName] = useState(media?.media_name ?? "");
|
||||||
@@ -55,51 +55,27 @@ export const MediaEditPage = observer(() => {
|
|||||||
setMediaFilename(media.filename);
|
setMediaFilename(media.filename);
|
||||||
setMediaType(media.media_type);
|
setMediaType(media.media_type);
|
||||||
|
|
||||||
// Set available media types based on current file extension
|
|
||||||
const extension = media.filename.split(".").pop()?.toLowerCase();
|
const extension = media.filename.split(".").pop()?.toLowerCase();
|
||||||
if (extension) {
|
if (extension) {
|
||||||
if (["glb", "gltf"].includes(extension)) {
|
if (["glb", "gltf"].includes(extension)) {
|
||||||
setAvailableMediaTypes([6]); // 3D model
|
setAvailableMediaTypes([6]);
|
||||||
} else if (
|
} else if (
|
||||||
["jpg", "jpeg", "png", "gif", "svg", "webp", "bmp"].includes(
|
["jpg", "jpeg", "png", "gif", "svg", "webp", "bmp"].includes(
|
||||||
extension
|
extension
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
setAvailableMediaTypes([1, 3, 4, 5]); // Photo, Icon, Watermark, Panorama
|
setAvailableMediaTypes([1, 3, 4, 5]);
|
||||||
} else if (["mp4", "webm", "mov"].includes(extension)) {
|
} else if (["mp4", "webm", "mov"].includes(extension)) {
|
||||||
setAvailableMediaTypes([2]); // Video
|
setAvailableMediaTypes([2]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [media]);
|
}, [media]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Устанавливаем русский язык при загрузке страницы
|
|
||||||
languageStore.setLanguage("ru");
|
languageStore.setLanguage("ru");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// const handleDrop = (e: DragEvent<HTMLDivElement>) => {
|
|
||||||
// e.preventDefault();
|
|
||||||
// e.stopPropagation();
|
|
||||||
// setIsDragging(false);
|
|
||||||
|
|
||||||
// const files = Array.from(e.dataTransfer.files);
|
|
||||||
// if (files.length > 0) {
|
|
||||||
// setNewFile(files[0]);
|
|
||||||
// setMediaFilename(files[0].name);
|
|
||||||
// setUploadDialogOpen(true); // Open dialog on file drop
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const handleDragOver = (e: DragEvent<HTMLDivElement>) => {
|
|
||||||
// e.preventDefault();
|
|
||||||
// setIsDragging(true);
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const handleDragLeave = () => {
|
|
||||||
// setIsDragging(false);
|
|
||||||
// };
|
|
||||||
|
|
||||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const files = e.target.files;
|
const files = e.target.files;
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
@@ -107,26 +83,25 @@ export const MediaEditPage = observer(() => {
|
|||||||
setNewFile(file);
|
setNewFile(file);
|
||||||
setMediaFilename(file.name);
|
setMediaFilename(file.name);
|
||||||
|
|
||||||
// Determine media type based on file extension
|
|
||||||
const extension = file.name.split(".").pop()?.toLowerCase();
|
const extension = file.name.split(".").pop()?.toLowerCase();
|
||||||
if (extension) {
|
if (extension) {
|
||||||
if (["glb", "gltf"].includes(extension)) {
|
if (["glb", "gltf"].includes(extension)) {
|
||||||
setAvailableMediaTypes([6]); // 3D model
|
setAvailableMediaTypes([6]);
|
||||||
setMediaType(6);
|
setMediaType(6);
|
||||||
} else if (
|
} else if (
|
||||||
["jpg", "jpeg", "png", "gif", "svg", "webp", "bmp"].includes(
|
["jpg", "jpeg", "png", "gif", "svg", "webp", "bmp"].includes(
|
||||||
extension
|
extension
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
setAvailableMediaTypes([1, 3, 4, 5]); // Photo, Icon, Watermark, Panorama
|
setAvailableMediaTypes([1, 3, 4, 5]);
|
||||||
setMediaType(1); // Default to Photo
|
setMediaType(1);
|
||||||
} else if (["mp4", "webm", "mov"].includes(extension)) {
|
} else if (["mp4", "webm", "mov"].includes(extension)) {
|
||||||
setAvailableMediaTypes([2]); // Video
|
setAvailableMediaTypes([2]);
|
||||||
setMediaType(2);
|
setMediaType(2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setUploadDialogOpen(true); // Open dialog on file selection
|
setUploadDialogOpen(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -143,11 +118,6 @@ export const MediaEditPage = observer(() => {
|
|||||||
type: mediaType,
|
type: mediaType,
|
||||||
});
|
});
|
||||||
|
|
||||||
// If a new file was selected, the actual file upload will happen
|
|
||||||
// via the UploadMediaDialog. We just need to make sure the metadata
|
|
||||||
// is updated correctly before or after.
|
|
||||||
// Since the dialog handles the actual upload, we don't call updateMediaFile here.
|
|
||||||
|
|
||||||
setSuccess(true);
|
setSuccess(true);
|
||||||
handleUploadSuccess();
|
handleUploadSuccess();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -158,17 +128,15 @@ export const MediaEditPage = observer(() => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleUploadSuccess = () => {
|
const handleUploadSuccess = () => {
|
||||||
// After successful upload in the dialog, refresh media data if needed
|
|
||||||
if (id) {
|
if (id) {
|
||||||
mediaStore.getOneMedia(id);
|
mediaStore.getOneMedia(id);
|
||||||
}
|
}
|
||||||
setNewFile(null); // Clear the new file state after successful upload
|
setNewFile(null);
|
||||||
setUploadDialogOpen(false);
|
setUploadDialogOpen(false);
|
||||||
setSuccess(true);
|
setSuccess(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!media && id) {
|
if (!media && id) {
|
||||||
// Only show loading if an ID is present and media is not yet loaded
|
|
||||||
return (
|
return (
|
||||||
<Box className="flex justify-center items-center h-screen">
|
<Box className="flex justify-center items-center h-screen">
|
||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useState, useEffect } from "react";
|
|||||||
import {
|
import {
|
||||||
Stack,
|
Stack,
|
||||||
Typography,
|
Typography,
|
||||||
Button,
|
|
||||||
FormControl,
|
FormControl,
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionSummary,
|
AccordionSummary,
|
||||||
@@ -35,6 +34,7 @@ import {
|
|||||||
} from "@hello-pangea/dnd";
|
} from "@hello-pangea/dnd";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
AnimatedCircleButton,
|
||||||
authInstance,
|
authInstance,
|
||||||
languageStore,
|
languageStore,
|
||||||
routeStore,
|
routeStore,
|
||||||
@@ -42,7 +42,6 @@ import {
|
|||||||
} from "@shared";
|
} from "@shared";
|
||||||
import { EditStationModal } from "../../widgets/modals/EditStationModal";
|
import { EditStationModal } from "../../widgets/modals/EditStationModal";
|
||||||
|
|
||||||
// Helper function to insert an item at a specific position (1-based index)
|
|
||||||
function insertAtPosition<T>(arr: T[], pos: number, value: T): T[] {
|
function insertAtPosition<T>(arr: T[], pos: number, value: T): T[] {
|
||||||
const index = pos - 1;
|
const index = pos - 1;
|
||||||
const result = [...arr];
|
const result = [...arr];
|
||||||
@@ -54,7 +53,6 @@ function insertAtPosition<T>(arr: T[], pos: number, value: T): T[] {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to reorder items after drag and drop
|
|
||||||
const reorder = <T,>(list: T[], startIndex: number, endIndex: number): T[] => {
|
const reorder = <T,>(list: T[], startIndex: number, endIndex: number): T[] => {
|
||||||
const result = Array.from(list);
|
const result = Array.from(list);
|
||||||
const [removed] = result.splice(startIndex, 1);
|
const [removed] = result.splice(startIndex, 1);
|
||||||
@@ -143,6 +141,9 @@ const LinkedItemsContentsInner = <
|
|||||||
const [selectedItems, setSelectedItems] = useState<Set<number>>(new Set());
|
const [selectedItems, setSelectedItems] = useState<Set<number>>(new Set());
|
||||||
const [activeTab, setActiveTab] = useState(0);
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [isLinkingSingle, setIsLinkingSingle] = useState(false);
|
||||||
|
const [isLinkingBulk, setIsLinkingBulk] = useState(false);
|
||||||
|
const [detachingIds, setDetachingIds] = useState<Set<number>>(new Set());
|
||||||
|
|
||||||
useEffect(() => {}, [error]);
|
useEffect(() => {}, [error]);
|
||||||
|
|
||||||
@@ -152,13 +153,11 @@ const LinkedItemsContentsInner = <
|
|||||||
const availableItems = allItems
|
const availableItems = allItems
|
||||||
.filter((item) => !linkedItems.some((linked) => linked.id === item.id))
|
.filter((item) => !linkedItems.some((linked) => linked.id === item.id))
|
||||||
.filter((item) => {
|
.filter((item) => {
|
||||||
// Если направление маршрута не указано, показываем все станции
|
|
||||||
if (routeDirection === undefined) return true;
|
if (routeDirection === undefined) return true;
|
||||||
// Фильтруем станции по направлению маршрута
|
|
||||||
return item.direction === routeDirection;
|
return item.direction === routeDirection;
|
||||||
})
|
})
|
||||||
.filter((item) => {
|
.filter((item) => {
|
||||||
// Фильтруем по городу из навбара
|
|
||||||
const selectedCityId = selectedCityStore.selectedCityId;
|
const selectedCityId = selectedCityStore.selectedCityId;
|
||||||
if (selectedCityId && "city_id" in item) {
|
if (selectedCityId && "city_id" in item) {
|
||||||
return item.city_id === selectedCityId;
|
return item.city_id === selectedCityId;
|
||||||
@@ -167,7 +166,6 @@ const LinkedItemsContentsInner = <
|
|||||||
})
|
})
|
||||||
.sort((a, b) => a.name.localeCompare(b.name));
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
// Фильтрация по поиску для массового режима
|
|
||||||
const filteredAvailableItems = availableItems.filter((item) => {
|
const filteredAvailableItems = availableItems.filter((item) => {
|
||||||
if (!searchQuery.trim()) return true;
|
if (!searchQuery.trim()) return true;
|
||||||
return String(item.name).toLowerCase().includes(searchQuery.toLowerCase());
|
return String(item.name).toLowerCase().includes(searchQuery.toLowerCase());
|
||||||
@@ -255,6 +253,7 @@ const LinkedItemsContentsInner = <
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
setIsLinkingSingle(true);
|
||||||
authInstance
|
authInstance
|
||||||
.post(`/${parentResource}/${parentId}/${childResource}`, requestData)
|
.post(`/${parentResource}/${parentId}/${childResource}`, requestData)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -273,12 +272,20 @@ const LinkedItemsContentsInner = <
|
|||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("Error linking item:", error);
|
console.error("Error linking item:", error);
|
||||||
setError("Failed to link station");
|
setError("Failed to link station");
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLinkingSingle(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteItem = (itemId: number) => {
|
const deleteItem = (itemId: number) => {
|
||||||
setError(null);
|
setError(null);
|
||||||
|
setDetachingIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.add(itemId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
authInstance
|
authInstance
|
||||||
.delete(`/${parentResource}/${parentId}/${childResource}`, {
|
.delete(`/${parentResource}/${parentId}/${childResource}`, {
|
||||||
data: { [`${childResource}_id`]: itemId },
|
data: { [`${childResource}_id`]: itemId },
|
||||||
@@ -290,6 +297,13 @@ const LinkedItemsContentsInner = <
|
|||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("Error deleting item:", error);
|
console.error("Error deleting item:", error);
|
||||||
setError("Failed to delete station");
|
setError("Failed to delete station");
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setDetachingIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.delete(itemId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -316,6 +330,7 @@ const LinkedItemsContentsInner = <
|
|||||||
if (selectedItems.size === 0) return;
|
if (selectedItems.size === 0) return;
|
||||||
|
|
||||||
setError(null);
|
setError(null);
|
||||||
|
setIsLinkingBulk(true);
|
||||||
const selectedStations = Array.from(selectedItems).map((id) => ({ id }));
|
const selectedStations = Array.from(selectedItems).map((id) => ({ id }));
|
||||||
const requestData = {
|
const requestData = {
|
||||||
stations: [
|
stations: [
|
||||||
@@ -335,6 +350,9 @@ const LinkedItemsContentsInner = <
|
|||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("Error linking items:", error);
|
console.error("Error linking items:", error);
|
||||||
setError("Failed to link stations");
|
setError("Failed to link stations");
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLinkingBulk(false);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -404,7 +422,7 @@ const LinkedItemsContentsInner = <
|
|||||||
))}
|
))}
|
||||||
{type === "edit" && (
|
{type === "edit" && (
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Button
|
<AnimatedCircleButton
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="error"
|
color="error"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -412,9 +430,11 @@ const LinkedItemsContentsInner = <
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
deleteItem(item.id);
|
deleteItem(item.id);
|
||||||
}}
|
}}
|
||||||
|
disabled={detachingIds.has(item.id)}
|
||||||
|
loading={detachingIds.has(item.id)}
|
||||||
>
|
>
|
||||||
Отвязать
|
Отвязать
|
||||||
</Button>
|
</AnimatedCircleButton>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -525,14 +545,15 @@ const LinkedItemsContentsInner = <
|
|||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<Button
|
<AnimatedCircleButton
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={linkItem}
|
onClick={linkItem}
|
||||||
disabled={!selectedItemId}
|
disabled={!selectedItemId || isLinkingSingle}
|
||||||
|
loading={isLinkingSingle}
|
||||||
sx={{ alignSelf: "flex-start" }}
|
sx={{ alignSelf: "flex-start" }}
|
||||||
>
|
>
|
||||||
Добавить
|
Добавить
|
||||||
</Button>
|
</AnimatedCircleButton>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -562,7 +583,14 @@ const LinkedItemsContentsInner = <
|
|||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label={String(item.name)}
|
label={
|
||||||
|
<div className="flex justify-between items-center w-full gap-10">
|
||||||
|
<p>{String(item.name)}</p>
|
||||||
|
<p className="text-xs text-gray-500 max-w-[300px] truncate text-ellipsis">
|
||||||
|
{String(item.description)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
sx={{
|
sx={{
|
||||||
margin: 0,
|
margin: 0,
|
||||||
"& .MuiFormControlLabel-label": {
|
"& .MuiFormControlLabel-label": {
|
||||||
@@ -585,14 +613,15 @@ const LinkedItemsContentsInner = <
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
<Button
|
<AnimatedCircleButton
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={handleBulkLink}
|
onClick={handleBulkLink}
|
||||||
disabled={selectedItems.size === 0}
|
disabled={selectedItems.size === 0 || isLinkingBulk}
|
||||||
|
loading={isLinkingBulk}
|
||||||
sx={{ alignSelf: "flex-start" }}
|
sx={{ alignSelf: "flex-start" }}
|
||||||
>
|
>
|
||||||
Добавить выбранные ({selectedItems.size})
|
Добавить выбранные ({selectedItems.size})
|
||||||
</Button>
|
</AnimatedCircleButton>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
DialogContent,
|
DialogContent,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { MediaViewer } from "@widgets";
|
import { MediaViewer, VideoPreviewCard } from "@widgets";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { ArrowLeft, Loader2, Save, Plus } from "lucide-react";
|
import { ArrowLeft, Loader2, Save, Plus } from "lucide-react";
|
||||||
import { useEffect, useState, useMemo } from "react";
|
import { useEffect, useState, useMemo } from "react";
|
||||||
@@ -24,9 +24,10 @@ import { articlesStore } from "../../../shared/store/ArticlesStore";
|
|||||||
import { Route, routeStore } from "../../../shared/store/RouteStore";
|
import { Route, routeStore } from "../../../shared/store/RouteStore";
|
||||||
import {
|
import {
|
||||||
languageStore,
|
languageStore,
|
||||||
SelectArticleModal,
|
ArticleSelectOrCreateDialog,
|
||||||
SelectMediaDialog,
|
SelectMediaDialog,
|
||||||
selectedCityStore,
|
selectedCityStore,
|
||||||
|
UploadMediaDialog,
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
|
|
||||||
export const RouteCreatePage = observer(() => {
|
export const RouteCreatePage = observer(() => {
|
||||||
@@ -37,8 +38,9 @@ export const RouteCreatePage = observer(() => {
|
|||||||
const [govRouteNumber, setGovRouteNumber] = useState("");
|
const [govRouteNumber, setGovRouteNumber] = useState("");
|
||||||
const [governorAppeal, setGovernorAppeal] = useState<string>("");
|
const [governorAppeal, setGovernorAppeal] = useState<string>("");
|
||||||
const [direction, setDirection] = useState("backward");
|
const [direction, setDirection] = useState("backward");
|
||||||
const [scaleMin, setScaleMin] = useState("");
|
const [scaleMin, setScaleMin] = useState("10");
|
||||||
const [scaleMax, setScaleMax] = useState("");
|
const [scaleMax, setScaleMax] = useState("100");
|
||||||
|
const [routeName, setRouteName] = useState("");
|
||||||
const [turn, setTurn] = useState("");
|
const [turn, setTurn] = useState("");
|
||||||
const [centerLat, setCenterLat] = useState("");
|
const [centerLat, setCenterLat] = useState("");
|
||||||
const [centerLng, setCenterLng] = useState("");
|
const [centerLng, setCenterLng] = useState("");
|
||||||
@@ -48,6 +50,8 @@ export const RouteCreatePage = observer(() => {
|
|||||||
useState(false);
|
useState(false);
|
||||||
const [isSelectVideoDialogOpen, setIsSelectVideoDialogOpen] = useState(false);
|
const [isSelectVideoDialogOpen, setIsSelectVideoDialogOpen] = useState(false);
|
||||||
const [isVideoPreviewOpen, setIsVideoPreviewOpen] = useState(false);
|
const [isVideoPreviewOpen, setIsVideoPreviewOpen] = useState(false);
|
||||||
|
const [isUploadVideoDialogOpen, setIsUploadVideoDialogOpen] = useState(false);
|
||||||
|
const [fileToUpload, setFileToUpload] = useState<File | null>(null);
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -55,7 +59,6 @@ export const RouteCreatePage = observer(() => {
|
|||||||
articlesStore.getArticleList();
|
articlesStore.getArticleList();
|
||||||
}, [language]);
|
}, [language]);
|
||||||
|
|
||||||
// Фильтруем перевозчиков только из выбранного города
|
|
||||||
const filteredCarriers = useMemo(() => {
|
const filteredCarriers = useMemo(() => {
|
||||||
const carriers =
|
const carriers =
|
||||||
carrierStore.carriers[language as keyof typeof carrierStore.carriers]
|
carrierStore.carriers[language as keyof typeof carrierStore.carriers]
|
||||||
@@ -110,6 +113,7 @@ export const RouteCreatePage = observer(() => {
|
|||||||
const handleArticleSelect = (articleId: number) => {
|
const handleArticleSelect = (articleId: number) => {
|
||||||
setGovernorAppeal(articleId.toString());
|
setGovernorAppeal(articleId.toString());
|
||||||
setIsSelectArticleDialogOpen(false);
|
setIsSelectArticleDialogOpen(false);
|
||||||
|
articlesStore.getArticleList();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleVideoSelect = (media: {
|
const handleVideoSelect = (media: {
|
||||||
@@ -122,6 +126,26 @@ export const RouteCreatePage = observer(() => {
|
|||||||
setIsSelectVideoDialogOpen(false);
|
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 = () => {
|
const handleVideoPreviewClick = () => {
|
||||||
setIsVideoPreviewOpen(true);
|
setIsVideoPreviewOpen(true);
|
||||||
};
|
};
|
||||||
@@ -129,23 +153,72 @@ export const RouteCreatePage = observer(() => {
|
|||||||
const handleCreateRoute = async () => {
|
const handleCreateRoute = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
// Преобразуем значения в нужные типы
|
|
||||||
|
if (!routeName.trim()) {
|
||||||
|
toast.error("Заполните название маршрута");
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!carrier) {
|
||||||
|
toast.error("Выберите перевозчика");
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!routeNumber.trim()) {
|
||||||
|
toast.error("Заполните номер маршрута");
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!govRouteNumber.trim()) {
|
||||||
|
toast.error("Заполните номер маршрута в Говорящем Городе");
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!governorAppeal) {
|
||||||
|
toast.error("Выберите статью для обращения к пассажирам");
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validationResult = validateCoordinates(routeCoords);
|
||||||
|
if (validationResult !== true) {
|
||||||
|
toast.error(validationResult);
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scale_min = scaleMin ? Number(scaleMin) : null;
|
||||||
|
const scale_max = scaleMax ? Number(scaleMax) : null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
scale_min === 0 ||
|
||||||
|
scale_max === 0 ||
|
||||||
|
scale_min === null ||
|
||||||
|
scale_max === null
|
||||||
|
) {
|
||||||
|
toast.error("Масштабы не могут быть равны 0");
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
scale_min !== null &&
|
||||||
|
scale_max !== null &&
|
||||||
|
scale_max !== undefined &&
|
||||||
|
scale_min > scale_max
|
||||||
|
) {
|
||||||
|
toast.error("Максимальный масштаб не может быть меньше минимального");
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const carrier_id = Number(carrier);
|
const carrier_id = Number(carrier);
|
||||||
const governor_appeal = Number(governorAppeal);
|
const governor_appeal = Number(governorAppeal);
|
||||||
const scale_min = scaleMin ? Number(scaleMin) : undefined;
|
|
||||||
const scale_max = scaleMax ? Number(scaleMax) : undefined;
|
|
||||||
const rotate = turn ? Number(turn) : undefined;
|
const rotate = turn ? Number(turn) : undefined;
|
||||||
const center_latitude = centerLat ? Number(centerLat) : undefined;
|
const center_latitude = centerLat ? Number(centerLat) : undefined;
|
||||||
const center_longitude = centerLng ? Number(centerLng) : undefined;
|
const center_longitude = centerLng ? Number(centerLng) : undefined;
|
||||||
const route_direction = direction === "forward";
|
const route_direction = direction === "forward";
|
||||||
|
|
||||||
const validationResult = validateCoordinates(routeCoords);
|
|
||||||
if (validationResult !== true) {
|
|
||||||
toast.error(validationResult);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Координаты маршрута как массив массивов чисел
|
|
||||||
const path = routeCoords
|
const path = routeCoords
|
||||||
.trim()
|
.trim()
|
||||||
.split("\n")
|
.split("\n")
|
||||||
@@ -157,7 +230,6 @@ export const RouteCreatePage = observer(() => {
|
|||||||
return [lat, lon];
|
return [lat, lon];
|
||||||
});
|
});
|
||||||
|
|
||||||
// Собираем объект маршрута
|
|
||||||
const newRoute: Partial<Route> = {
|
const newRoute: Partial<Route> = {
|
||||||
carrier:
|
carrier:
|
||||||
carrierStore.carriers[
|
carrierStore.carriers[
|
||||||
@@ -167,9 +239,10 @@ export const RouteCreatePage = observer(() => {
|
|||||||
route_number: routeNumber,
|
route_number: routeNumber,
|
||||||
route_sys_number: govRouteNumber,
|
route_sys_number: govRouteNumber,
|
||||||
governor_appeal,
|
governor_appeal,
|
||||||
|
route_name: routeName,
|
||||||
route_direction,
|
route_direction,
|
||||||
scale_min,
|
scale_min: scale_min !== null ? scale_min : 0,
|
||||||
scale_max,
|
scale_max: scale_max !== null ? scale_max : 0,
|
||||||
rotate,
|
rotate,
|
||||||
center_latitude,
|
center_latitude,
|
||||||
center_longitude,
|
center_longitude,
|
||||||
@@ -189,7 +262,6 @@ export const RouteCreatePage = observer(() => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Получаем название выбранной статьи для отображения
|
|
||||||
const selectedArticle = articlesStore.articleList.ru.data.find(
|
const selectedArticle = articlesStore.articleList.ru.data.find(
|
||||||
(article) => article.id === Number(governorAppeal)
|
(article) => article.id === Number(governorAppeal)
|
||||||
);
|
);
|
||||||
@@ -208,6 +280,13 @@ export const RouteCreatePage = observer(() => {
|
|||||||
|
|
||||||
<div className="flex flex-col gap-10 w-full items-end">
|
<div className="flex flex-col gap-10 w-full items-end">
|
||||||
<Box className="flex flex-col gap-6 w-full">
|
<Box className="flex flex-col gap-6 w-full">
|
||||||
|
<TextField
|
||||||
|
className="w-full"
|
||||||
|
label="Название маршрута"
|
||||||
|
required
|
||||||
|
value={routeName}
|
||||||
|
onChange={(e) => setRouteName(e.target.value)}
|
||||||
|
/>
|
||||||
<FormControl fullWidth required>
|
<FormControl fullWidth required>
|
||||||
<InputLabel>Выберите перевозчика</InputLabel>
|
<InputLabel>Выберите перевозчика</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
@@ -247,7 +326,6 @@ export const RouteCreatePage = observer(() => {
|
|||||||
const lines = routeCoords.split("\n");
|
const lines = routeCoords.split("\n");
|
||||||
const lastLine = lines[lines.length - 1];
|
const lastLine = lines[lines.length - 1];
|
||||||
|
|
||||||
// Если мы на последней строке и она не пустая
|
|
||||||
if (lastLine && lastLine.trim()) {
|
if (lastLine && lastLine.trim()) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const newValue = routeCoords + "\n";
|
const newValue = routeCoords + "\n";
|
||||||
@@ -279,6 +357,7 @@ export const RouteCreatePage = observer(() => {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
className="w-full"
|
className="w-full"
|
||||||
label="Номер маршрута в Говорящем Городе"
|
label="Номер маршрута в Говорящем Городе"
|
||||||
@@ -287,99 +366,42 @@ export const RouteCreatePage = observer(() => {
|
|||||||
onChange={(e) => setGovRouteNumber(e.target.value)}
|
onChange={(e) => setGovRouteNumber(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Заменяем Select на кнопку для выбора статьи */}
|
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
|
||||||
<Box className="flex flex-col gap-2">
|
Обращение к пассажирам
|
||||||
<label className="text-sm font-medium text-gray-700">
|
</Typography>
|
||||||
Обращение к пассажирам
|
<Box className="flex gap-2">
|
||||||
</label>
|
<TextField
|
||||||
<Box className="flex gap-2">
|
className="flex-1"
|
||||||
<TextField
|
value={selectedArticle?.heading || "Статья не выбрана"}
|
||||||
className="flex-1"
|
placeholder="Выберите статью"
|
||||||
value={selectedArticle?.heading || "Статья не выбрана"}
|
disabled
|
||||||
placeholder="Выберите статью"
|
fullWidth
|
||||||
disabled
|
sx={{
|
||||||
sx={{
|
"& .MuiInputBase-input": {
|
||||||
"& .MuiInputBase-input": {
|
color: selectedArticle ? "inherit" : "#999",
|
||||||
color: selectedArticle ? "inherit" : "#999",
|
},
|
||||||
},
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
<Button
|
||||||
<Button
|
variant="outlined"
|
||||||
variant="outlined"
|
onClick={() => setIsSelectArticleDialogOpen(true)}
|
||||||
onClick={() => setIsSelectArticleDialogOpen(true)}
|
startIcon={<Plus size={16} />}
|
||||||
startIcon={<Plus size={16} />}
|
sx={{ minWidth: "auto", px: 2 }}
|
||||||
sx={{ minWidth: "auto", px: 2 }}
|
>
|
||||||
>
|
Выбрать
|
||||||
Выбрать
|
</Button>
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Селектор видеозаставки */}
|
<VideoPreviewCard
|
||||||
<Box className="flex flex-col gap-2">
|
title="Видеозаставка"
|
||||||
<label className="text-sm font-medium text-gray-700">
|
videoId={videoPreview}
|
||||||
Видеозаставка
|
onVideoClick={handleVideoPreviewClick}
|
||||||
</label>
|
onDeleteVideoClick={() => {
|
||||||
<Box className="flex gap-2">
|
setVideoPreview("");
|
||||||
<Box
|
}}
|
||||||
className="flex-1"
|
onSelectVideoClick={handleVideoFileSelect}
|
||||||
onClick={handleVideoPreviewClick}
|
className="w-full"
|
||||||
sx={{
|
/>
|
||||||
cursor:
|
|
||||||
videoPreview && videoPreview !== "" ? "pointer" : "default",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
className="w-full h-[50px] border border-gray-400 rounded-sm flex items-center justify-between px-4"
|
|
||||||
sx={{
|
|
||||||
"& .MuiInputBase-input": {
|
|
||||||
color:
|
|
||||||
videoPreview && videoPreview !== ""
|
|
||||||
? "inherit"
|
|
||||||
: "#999",
|
|
||||||
cursor:
|
|
||||||
videoPreview && videoPreview !== ""
|
|
||||||
? "pointer"
|
|
||||||
: "default",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="body1" className="text-sm">
|
|
||||||
{videoPreview && videoPreview !== ""
|
|
||||||
? "Видео выбрано"
|
|
||||||
: "Видео не выбрано"}
|
|
||||||
</Typography>
|
|
||||||
{videoPreview && videoPreview !== "" && (
|
|
||||||
<Box
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setVideoPreview("");
|
|
||||||
}}
|
|
||||||
sx={{
|
|
||||||
cursor: "pointer",
|
|
||||||
color: "#999",
|
|
||||||
"&:hover": {
|
|
||||||
color: "#666",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="body1" className="text-lg font-bold">
|
|
||||||
×
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
onClick={() => setIsSelectVideoDialogOpen(true)}
|
|
||||||
startIcon={<Plus size={16} />}
|
|
||||||
sx={{ minWidth: "auto", px: 2 }}
|
|
||||||
>
|
|
||||||
Выбрать
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<FormControl fullWidth required>
|
<FormControl fullWidth required>
|
||||||
<InputLabel>Прямой/обратный маршрут</InputLabel>
|
<InputLabel>Прямой/обратный маршрут</InputLabel>
|
||||||
@@ -395,15 +417,53 @@ export const RouteCreatePage = observer(() => {
|
|||||||
<TextField
|
<TextField
|
||||||
className="w-full"
|
className="w-full"
|
||||||
label="Масштаб (мин)"
|
label="Масштаб (мин)"
|
||||||
|
type="number"
|
||||||
value={scaleMin}
|
value={scaleMin}
|
||||||
onChange={(e) => setScaleMin(e.target.value)}
|
onChange={(e) => {
|
||||||
|
let value = e.target.value;
|
||||||
|
if (Number(value) > 297) {
|
||||||
|
value = "297";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number(value) < 10) {
|
||||||
|
value = "10";
|
||||||
|
}
|
||||||
|
|
||||||
|
setScaleMin(value);
|
||||||
|
if (value && scaleMax && Number(value) > Number(scaleMax)) {
|
||||||
|
setScaleMax(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
error={
|
||||||
|
scaleMin !== "" &&
|
||||||
|
scaleMax !== "" &&
|
||||||
|
Number(scaleMin) > Number(scaleMax)
|
||||||
|
}
|
||||||
|
required
|
||||||
|
helperText={
|
||||||
|
scaleMin !== "" &&
|
||||||
|
scaleMax !== "" &&
|
||||||
|
Number(scaleMin) > Number(scaleMax)
|
||||||
|
? "Минимальный масштаб не может быть больше максимального"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
className="w-full"
|
className="w-full"
|
||||||
label="Масштаб (макс)"
|
label="Масштаб (макс)"
|
||||||
|
type="number"
|
||||||
value={scaleMax}
|
value={scaleMax}
|
||||||
onChange={(e) => setScaleMax(e.target.value)}
|
required
|
||||||
|
onChange={(e) => {
|
||||||
|
if (Number(e.target.value) > 300) {
|
||||||
|
e.target.value = "300";
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = e.target.value;
|
||||||
|
setScaleMax(value);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
className="w-full"
|
className="w-full"
|
||||||
label="Поворот"
|
label="Поворот"
|
||||||
@@ -440,23 +500,17 @@ export const RouteCreatePage = observer(() => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ArticleSelectOrCreateDialog
|
||||||
{/* Модальное окно выбора статьи */}
|
|
||||||
<SelectArticleModal
|
|
||||||
open={isSelectArticleDialogOpen}
|
open={isSelectArticleDialogOpen}
|
||||||
onClose={() => setIsSelectArticleDialogOpen(false)}
|
onClose={() => setIsSelectArticleDialogOpen(false)}
|
||||||
onSelectArticle={handleArticleSelect}
|
onSelectArticle={handleArticleSelect}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Модальное окно выбора видео */}
|
|
||||||
<SelectMediaDialog
|
<SelectMediaDialog
|
||||||
open={isSelectVideoDialogOpen}
|
open={isSelectVideoDialogOpen}
|
||||||
onClose={() => setIsSelectVideoDialogOpen(false)}
|
onClose={() => setIsSelectVideoDialogOpen(false)}
|
||||||
onSelectMedia={handleVideoSelect}
|
onSelectMedia={handleVideoSelect}
|
||||||
mediaType={2}
|
mediaType={2}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Модальное окно предпросмотра видео */}
|
|
||||||
{videoPreview && videoPreview !== "" && (
|
{videoPreview && videoPreview !== "" && (
|
||||||
<Dialog
|
<Dialog
|
||||||
open={isVideoPreviewOpen}
|
open={isVideoPreviewOpen}
|
||||||
@@ -483,6 +537,18 @@ export const RouteCreatePage = observer(() => {
|
|||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)}
|
)}
|
||||||
|
<UploadMediaDialog
|
||||||
|
open={isUploadVideoDialogOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIsUploadVideoDialogOpen(false);
|
||||||
|
setFileToUpload(null);
|
||||||
|
}}
|
||||||
|
hardcodeType="video_preview"
|
||||||
|
contextObjectName={routeName || "Маршрут"}
|
||||||
|
contextType="sight"
|
||||||
|
initialFile={fileToUpload || undefined}
|
||||||
|
afterUpload={handleVideoUpload}
|
||||||
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
DialogContent,
|
DialogContent,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { MediaViewer } from "@widgets";
|
import { MediaViewer, VideoPreviewCard } from "@widgets";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { ArrowLeft, Copy, Save, Plus } from "lucide-react";
|
import { ArrowLeft, Copy, Save, Plus } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
@@ -24,8 +24,9 @@ import { articlesStore } from "../../../shared/store/ArticlesStore";
|
|||||||
import {
|
import {
|
||||||
routeStore,
|
routeStore,
|
||||||
languageStore,
|
languageStore,
|
||||||
SelectArticleModal,
|
ArticleSelectOrCreateDialog,
|
||||||
SelectMediaDialog,
|
SelectMediaDialog,
|
||||||
|
UploadMediaDialog,
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { stationsStore } from "@shared";
|
import { stationsStore } from "@shared";
|
||||||
@@ -40,12 +41,13 @@ export const RouteEditPage = observer(() => {
|
|||||||
useState(false);
|
useState(false);
|
||||||
const [isSelectVideoDialogOpen, setIsSelectVideoDialogOpen] = useState(false);
|
const [isSelectVideoDialogOpen, setIsSelectVideoDialogOpen] = useState(false);
|
||||||
const [isVideoPreviewOpen, setIsVideoPreviewOpen] = useState(false);
|
const [isVideoPreviewOpen, setIsVideoPreviewOpen] = useState(false);
|
||||||
|
const [isUploadVideoDialogOpen, setIsUploadVideoDialogOpen] = useState(false);
|
||||||
|
const [fileToUpload, setFileToUpload] = useState<File | null>(null);
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
const [coordinates, setCoordinates] = useState<string>("");
|
const [coordinates, setCoordinates] = useState<string>("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
// Устанавливаем русский язык при загрузке страницы
|
|
||||||
const response = await routeStore.getRoute(Number(id));
|
const response = await routeStore.getRoute(Number(id));
|
||||||
routeStore.setEditRouteData(response);
|
routeStore.setEditRouteData(response);
|
||||||
languageStore.setLanguage("ru");
|
languageStore.setLanguage("ru");
|
||||||
@@ -72,10 +74,67 @@ export const RouteEditPage = observer(() => {
|
|||||||
}, [editRouteData.path]);
|
}, [editRouteData.path]);
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
|
// Валидация обязательных полей
|
||||||
|
if (!editRouteData.route_name?.trim()) {
|
||||||
|
toast.error("Заполните название маршрута");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!editRouteData.carrier_id) {
|
||||||
|
toast.error("Выберите перевозчика");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!editRouteData.route_number?.trim()) {
|
||||||
|
toast.error("Заполните номер маршрута");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!editRouteData.route_sys_number?.trim()) {
|
||||||
|
toast.error("Заполните номер маршрута в Говорящем Городе");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!editRouteData.governor_appeal) {
|
||||||
|
toast.error("Выберите статью для обращения к пассажирам");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validationResult = validateCoordinates(coordinates);
|
||||||
|
if (validationResult !== true) {
|
||||||
|
toast.error(validationResult);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Валидация масштабов
|
||||||
|
if (
|
||||||
|
editRouteData.scale_min !== null &&
|
||||||
|
editRouteData.scale_min !== undefined &&
|
||||||
|
editRouteData.scale_max !== null &&
|
||||||
|
editRouteData.scale_max !== undefined &&
|
||||||
|
editRouteData.scale_min > editRouteData.scale_max
|
||||||
|
) {
|
||||||
|
toast.error("Максимальный масштаб не может быть меньше минимального");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
editRouteData.scale_min === 0 ||
|
||||||
|
editRouteData.scale_max === 0 ||
|
||||||
|
editRouteData.scale_min === null ||
|
||||||
|
editRouteData.scale_max === null
|
||||||
|
) {
|
||||||
|
toast.error("Масштабы не могут быть равны 0");
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
await routeStore.editRoute(Number(id));
|
try {
|
||||||
toast.success("Маршрут успешно сохранен");
|
await routeStore.editRoute(Number(id));
|
||||||
setIsLoading(false);
|
toast.success("Маршрут успешно сохранен");
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
toast.error("Произошла ошибка при сохранении маршрута");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateCoordinates = (value: string) => {
|
const validateCoordinates = (value: string) => {
|
||||||
@@ -125,6 +184,8 @@ export const RouteEditPage = observer(() => {
|
|||||||
governor_appeal: articleId,
|
governor_appeal: articleId,
|
||||||
});
|
});
|
||||||
setIsSelectArticleDialogOpen(false);
|
setIsSelectArticleDialogOpen(false);
|
||||||
|
// Обновляем список статей после создания новой
|
||||||
|
articlesStore.getArticleList();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleVideoSelect = (media: {
|
const handleVideoSelect = (media: {
|
||||||
@@ -139,6 +200,28 @@ export const RouteEditPage = observer(() => {
|
|||||||
setIsSelectVideoDialogOpen(false);
|
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 = () => {
|
const handleVideoPreviewClick = () => {
|
||||||
if (editRouteData.video_preview && editRouteData.video_preview !== "") {
|
if (editRouteData.video_preview && editRouteData.video_preview !== "") {
|
||||||
setIsVideoPreviewOpen(true);
|
setIsVideoPreviewOpen(true);
|
||||||
@@ -164,6 +247,17 @@ export const RouteEditPage = observer(() => {
|
|||||||
|
|
||||||
<div className="flex flex-col gap-10 w-full items-end">
|
<div className="flex flex-col gap-10 w-full items-end">
|
||||||
<Box className="flex flex-col gap-6 w-full">
|
<Box className="flex flex-col gap-6 w-full">
|
||||||
|
<TextField
|
||||||
|
className="w-full"
|
||||||
|
label="Название маршрута"
|
||||||
|
required
|
||||||
|
value={editRouteData.route_name || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
routeStore.setEditRouteData({
|
||||||
|
route_name: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
<FormControl fullWidth required>
|
<FormControl fullWidth required>
|
||||||
<InputLabel>Выберите перевозчика</InputLabel>
|
<InputLabel>Выберите перевозчика</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
@@ -235,7 +329,6 @@ export const RouteEditPage = observer(() => {
|
|||||||
const lines = coordinates.split("\n");
|
const lines = coordinates.split("\n");
|
||||||
const lastLine = lines[lines.length - 1];
|
const lastLine = lines[lines.length - 1];
|
||||||
|
|
||||||
// Если мы на последней строке и она не пустая
|
|
||||||
if (lastLine && lastLine.trim()) {
|
if (lastLine && lastLine.trim()) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const newValue = coordinates + "\n";
|
const newValue = coordinates + "\n";
|
||||||
@@ -279,110 +372,6 @@ export const RouteEditPage = observer(() => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Заменяем Select на кнопку для выбора статьи */}
|
|
||||||
<Box className="flex flex-col gap-2">
|
|
||||||
<label className="text-sm font-medium text-gray-700">
|
|
||||||
Обращение к пассажирам
|
|
||||||
</label>
|
|
||||||
<Box className="flex gap-2">
|
|
||||||
<TextField
|
|
||||||
className="flex-1"
|
|
||||||
value={selectedArticle?.heading || "Статья не выбрана"}
|
|
||||||
placeholder="Выберите статью"
|
|
||||||
disabled
|
|
||||||
sx={{
|
|
||||||
"& .MuiInputBase-input": {
|
|
||||||
color: selectedArticle ? "inherit" : "#999",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
onClick={() => setIsSelectArticleDialogOpen(true)}
|
|
||||||
startIcon={<Plus size={16} />}
|
|
||||||
sx={{ minWidth: "auto", px: 2 }}
|
|
||||||
>
|
|
||||||
Выбрать
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Селектор видеозаставки */}
|
|
||||||
<Box className="flex flex-col gap-2">
|
|
||||||
<label className="text-sm font-medium text-gray-700">
|
|
||||||
Видеозаставка
|
|
||||||
</label>
|
|
||||||
<Box className="flex gap-2">
|
|
||||||
<Box
|
|
||||||
className="flex-1"
|
|
||||||
onClick={handleVideoPreviewClick}
|
|
||||||
sx={{
|
|
||||||
cursor:
|
|
||||||
editRouteData.video_preview &&
|
|
||||||
editRouteData.video_preview !== ""
|
|
||||||
? "pointer"
|
|
||||||
: "default",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
className="w-full h-[50px] border border-gray-400 rounded-sm flex items-center justify-between px-4"
|
|
||||||
sx={{
|
|
||||||
"& .MuiInputBase-input": {
|
|
||||||
color:
|
|
||||||
editRouteData.video_preview &&
|
|
||||||
editRouteData.video_preview !== ""
|
|
||||||
? "inherit"
|
|
||||||
: "#999",
|
|
||||||
cursor:
|
|
||||||
editRouteData.video_preview &&
|
|
||||||
editRouteData.video_preview !== ""
|
|
||||||
? "pointer"
|
|
||||||
: "default",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="body1" className="text-sm">
|
|
||||||
{editRouteData.video_preview &&
|
|
||||||
editRouteData.video_preview !== ""
|
|
||||||
? "Видео выбрано"
|
|
||||||
: "Видео не выбрано"}
|
|
||||||
</Typography>
|
|
||||||
{editRouteData.video_preview &&
|
|
||||||
editRouteData.video_preview !== "" && (
|
|
||||||
<Box
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
routeStore.setEditRouteData({ video_preview: "" });
|
|
||||||
}}
|
|
||||||
sx={{
|
|
||||||
cursor: "pointer",
|
|
||||||
color: "#999",
|
|
||||||
"&:hover": {
|
|
||||||
color: "#666",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
variant="body1"
|
|
||||||
className="text-lg font-bold"
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
onClick={() => setIsSelectVideoDialogOpen(true)}
|
|
||||||
startIcon={<Plus size={16} />}
|
|
||||||
sx={{ minWidth: "auto", px: 2 }}
|
|
||||||
>
|
|
||||||
Выбрать
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<FormControl fullWidth required>
|
<FormControl fullWidth required>
|
||||||
<InputLabel>Прямой/обратный маршрут</InputLabel>
|
<InputLabel>Прямой/обратный маршрут</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
@@ -401,23 +390,69 @@ export const RouteEditPage = observer(() => {
|
|||||||
<TextField
|
<TextField
|
||||||
className="w-full"
|
className="w-full"
|
||||||
label="Масштаб (мин)"
|
label="Масштаб (мин)"
|
||||||
|
type="number"
|
||||||
value={editRouteData.scale_min ?? ""}
|
value={editRouteData.scale_min ?? ""}
|
||||||
onChange={(e) =>
|
onChange={(e) => {
|
||||||
|
let value = e.target.value === "" ? null : e.target.value;
|
||||||
|
|
||||||
|
if (value && Number(value) > 297) {
|
||||||
|
value = "297";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value && Number(value) < 10) {
|
||||||
|
value = "10";
|
||||||
|
}
|
||||||
|
|
||||||
routeStore.setEditRouteData({
|
routeStore.setEditRouteData({
|
||||||
scale_min:
|
scale_min: value ? Number(value) : null,
|
||||||
e.target.value === "" ? null : parseFloat(e.target.value),
|
});
|
||||||
})
|
// Если максимальный масштаб стал меньше минимального, обновляем его
|
||||||
}
|
if (
|
||||||
|
value !== null &&
|
||||||
|
editRouteData.scale_max !== null &&
|
||||||
|
editRouteData.scale_max !== undefined &&
|
||||||
|
value &&
|
||||||
|
Number(value) > (editRouteData.scale_max ?? 0)
|
||||||
|
) {
|
||||||
|
routeStore.setEditRouteData({
|
||||||
|
scale_max: value ? Number(value) : null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
className="w-full"
|
className="w-full"
|
||||||
|
required
|
||||||
label="Масштаб (макс)"
|
label="Масштаб (макс)"
|
||||||
|
type="number"
|
||||||
value={editRouteData.scale_max ?? ""}
|
value={editRouteData.scale_max ?? ""}
|
||||||
onChange={(e) =>
|
onChange={(e) => {
|
||||||
|
let value = e.target.value;
|
||||||
|
|
||||||
|
if (Number(value) > 300) {
|
||||||
|
value = "300";
|
||||||
|
}
|
||||||
|
|
||||||
routeStore.setEditRouteData({
|
routeStore.setEditRouteData({
|
||||||
scale_max:
|
scale_max: value === "" ? null : parseFloat(value),
|
||||||
e.target.value === "" ? null : parseFloat(e.target.value),
|
});
|
||||||
})
|
}}
|
||||||
|
error={
|
||||||
|
editRouteData.scale_min !== null &&
|
||||||
|
editRouteData.scale_min !== undefined &&
|
||||||
|
editRouteData.scale_max !== null &&
|
||||||
|
editRouteData.scale_max !== undefined &&
|
||||||
|
editRouteData.scale_max < editRouteData.scale_min
|
||||||
|
}
|
||||||
|
helperText={
|
||||||
|
editRouteData.scale_min !== null &&
|
||||||
|
editRouteData.scale_min !== undefined &&
|
||||||
|
editRouteData.scale_max !== null &&
|
||||||
|
editRouteData.scale_max !== undefined &&
|
||||||
|
editRouteData.scale_max < editRouteData.scale_min
|
||||||
|
? "Максимальный масштаб не может быть меньше минимального"
|
||||||
|
: ""
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
@@ -453,6 +488,43 @@ export const RouteEditPage = observer(() => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
|
||||||
|
Обращение к пассажирам
|
||||||
|
</Typography>
|
||||||
|
<Box className="flex gap-2">
|
||||||
|
<TextField
|
||||||
|
className="flex-1"
|
||||||
|
value={selectedArticle?.heading || "Статья не выбрана"}
|
||||||
|
placeholder="Выберите статью"
|
||||||
|
disabled
|
||||||
|
fullWidth
|
||||||
|
sx={{
|
||||||
|
"& .MuiInputBase-input": {
|
||||||
|
color: selectedArticle ? "inherit" : "#999",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => setIsSelectArticleDialogOpen(true)}
|
||||||
|
startIcon={<Plus size={16} />}
|
||||||
|
sx={{ minWidth: "auto", px: 2 }}
|
||||||
|
>
|
||||||
|
Выбрать
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<VideoPreviewCard
|
||||||
|
title="Видеозаставка"
|
||||||
|
videoId={editRouteData.video_preview}
|
||||||
|
onVideoClick={handleVideoPreviewClick}
|
||||||
|
onDeleteVideoClick={() => {
|
||||||
|
routeStore.setEditRouteData({ video_preview: "" });
|
||||||
|
}}
|
||||||
|
onSelectVideoClick={handleVideoFileSelect}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<LinkedItems
|
<LinkedItems
|
||||||
@@ -493,23 +565,17 @@ export const RouteEditPage = observer(() => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ArticleSelectOrCreateDialog
|
||||||
{/* Модальное окно выбора статьи */}
|
|
||||||
<SelectArticleModal
|
|
||||||
open={isSelectArticleDialogOpen}
|
open={isSelectArticleDialogOpen}
|
||||||
onClose={() => setIsSelectArticleDialogOpen(false)}
|
onClose={() => setIsSelectArticleDialogOpen(false)}
|
||||||
onSelectArticle={handleArticleSelect}
|
onSelectArticle={handleArticleSelect}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Модальное окно выбора видео */}
|
|
||||||
<SelectMediaDialog
|
<SelectMediaDialog
|
||||||
open={isSelectVideoDialogOpen}
|
open={isSelectVideoDialogOpen}
|
||||||
onClose={() => setIsSelectVideoDialogOpen(false)}
|
onClose={() => setIsSelectVideoDialogOpen(false)}
|
||||||
onSelectMedia={handleVideoSelect}
|
onSelectMedia={handleVideoSelect}
|
||||||
mediaType={2}
|
mediaType={2}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Модальное окно предпросмотра видео */}
|
|
||||||
<Dialog
|
<Dialog
|
||||||
open={isVideoPreviewOpen}
|
open={isVideoPreviewOpen}
|
||||||
onClose={() => setIsVideoPreviewOpen(false)}
|
onClose={() => setIsVideoPreviewOpen(false)}
|
||||||
@@ -519,19 +585,33 @@ export const RouteEditPage = observer(() => {
|
|||||||
<DialogTitle>Предпросмотр видео</DialogTitle>
|
<DialogTitle>Предпросмотр видео</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<Box className="flex justify-center items-center p-4">
|
<Box className="flex justify-center items-center p-4">
|
||||||
<MediaViewer
|
{editRouteData.video_preview && (
|
||||||
media={{
|
<MediaViewer
|
||||||
id: editRouteData.video_preview,
|
media={{
|
||||||
media_type: 2,
|
id: editRouteData.video_preview,
|
||||||
filename: "video_preview",
|
media_type: 2,
|
||||||
}}
|
filename: "video_preview",
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={() => setIsVideoPreviewOpen(false)}>Закрыть</Button>
|
<Button onClick={() => setIsVideoPreviewOpen(false)}>Закрыть</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
<UploadMediaDialog
|
||||||
|
open={isUploadVideoDialogOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIsUploadVideoDialogOpen(false);
|
||||||
|
setFileToUpload(null);
|
||||||
|
}}
|
||||||
|
hardcodeType="video_preview"
|
||||||
|
contextObjectName={editRouteData.route_name || "Маршрут"}
|
||||||
|
contextType="sight"
|
||||||
|
initialFile={fileToUpload || undefined}
|
||||||
|
afterUpload={handleVideoUpload}
|
||||||
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -50,6 +50,22 @@ export const RouteListPage = observer(() => {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
field: "route_name",
|
||||||
|
headerName: "Название маршрута",
|
||||||
|
flex: 1,
|
||||||
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex items-center">
|
||||||
|
{params.value ? (
|
||||||
|
params.value
|
||||||
|
) : (
|
||||||
|
<Minus size={20} className="text-red-500" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
field: "route_number",
|
field: "route_number",
|
||||||
headerName: "Номер маршрута",
|
headerName: "Номер маршрута",
|
||||||
@@ -100,9 +116,7 @@ export const RouteListPage = observer(() => {
|
|||||||
<button onClick={() => navigate(`/route-preview/${params.row.id}`)}>
|
<button onClick={() => navigate(`/route-preview/${params.row.id}`)}>
|
||||||
<Map size={20} className="text-purple-500" />
|
<Map size={20} className="text-purple-500" />
|
||||||
</button>
|
</button>
|
||||||
{/* <button onClick={() => navigate(`/route/${params.row.id}`)}>
|
|
||||||
<Eye size={20} className="text-green-500" />
|
|
||||||
</button> */}
|
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
@@ -122,6 +136,7 @@ export const RouteListPage = observer(() => {
|
|||||||
carrier_id: route.carrier_id,
|
carrier_id: route.carrier_id,
|
||||||
route_number: route.route_number,
|
route_number: route.route_number,
|
||||||
route_direction: route.route_direction ? "Прямой" : "Обратный",
|
route_direction: route.route_direction ? "Прямой" : "Обратный",
|
||||||
|
route_name: route.route_name,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -47,10 +47,8 @@ export function InfiniteCanvas({
|
|||||||
const [startPosition, setStartPosition] = useState({ x: 0, y: 0 });
|
const [startPosition, setStartPosition] = useState({ x: 0, y: 0 });
|
||||||
const [isPointerDown, setIsPointerDown] = useState(false);
|
const [isPointerDown, setIsPointerDown] = useState(false);
|
||||||
|
|
||||||
// Флаг для предотвращения конфликта между пользовательским вводом и данными маршрута
|
|
||||||
const [isUserInteracting, setIsUserInteracting] = useState(false);
|
const [isUserInteracting, setIsUserInteracting] = useState(false);
|
||||||
|
|
||||||
// Реф для отслеживания последнего значения originalRouteData?.rotate
|
|
||||||
const lastOriginalRotation = useRef<number | undefined>(undefined);
|
const lastOriginalRotation = useRef<number | undefined>(undefined);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -68,7 +66,7 @@ export function InfiniteCanvas({
|
|||||||
const handlePointerDown = (e: FederatedMouseEvent) => {
|
const handlePointerDown = (e: FederatedMouseEvent) => {
|
||||||
setIsPointerDown(true);
|
setIsPointerDown(true);
|
||||||
setIsDragging(false);
|
setIsDragging(false);
|
||||||
setIsUserInteracting(true); // Устанавливаем флаг взаимодействия пользователя
|
setIsUserInteracting(true);
|
||||||
setStartPosition({
|
setStartPosition({
|
||||||
x: position.x,
|
x: position.x,
|
||||||
y: position.y,
|
y: position.y,
|
||||||
@@ -81,13 +79,9 @@ export function InfiniteCanvas({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Устанавливаем rotation только при изменении originalRouteData и отсутствии взаимодействия пользователя
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newRotation = originalRouteData?.rotate ?? 0;
|
const newRotation = originalRouteData?.rotate ?? 0;
|
||||||
|
|
||||||
// Обновляем rotation только если:
|
|
||||||
// 1. Пользователь не взаимодействует с канвасом
|
|
||||||
// 2. Значение действительно изменилось
|
|
||||||
if (!isUserInteracting && lastOriginalRotation.current !== newRotation) {
|
if (!isUserInteracting && lastOriginalRotation.current !== newRotation) {
|
||||||
setRotation((newRotation * Math.PI) / 180);
|
setRotation((newRotation * Math.PI) / 180);
|
||||||
lastOriginalRotation.current = newRotation;
|
lastOriginalRotation.current = newRotation;
|
||||||
@@ -97,7 +91,6 @@ export function InfiniteCanvas({
|
|||||||
const handlePointerMove = (e: FederatedMouseEvent) => {
|
const handlePointerMove = (e: FederatedMouseEvent) => {
|
||||||
if (!isPointerDown) return;
|
if (!isPointerDown) return;
|
||||||
|
|
||||||
// Проверяем, началось ли перетаскивание
|
|
||||||
if (!isDragging) {
|
if (!isDragging) {
|
||||||
const dx = e.globalX - startMousePosition.x;
|
const dx = e.globalX - startMousePosition.x;
|
||||||
const dy = e.globalY - startMousePosition.y;
|
const dy = e.globalY - startMousePosition.y;
|
||||||
@@ -119,10 +112,8 @@ export function InfiniteCanvas({
|
|||||||
e.globalX - center.x
|
e.globalX - center.x
|
||||||
);
|
);
|
||||||
|
|
||||||
// Calculate rotation difference in radians
|
|
||||||
const rotationDiff = currentAngle - startAngle;
|
const rotationDiff = currentAngle - startAngle;
|
||||||
|
|
||||||
// Update rotation
|
|
||||||
setRotation(startRotation + rotationDiff);
|
setRotation(startRotation + rotationDiff);
|
||||||
|
|
||||||
const cosDelta = Math.cos(rotationDiff);
|
const cosDelta = Math.cos(rotationDiff);
|
||||||
@@ -149,15 +140,13 @@ export function InfiniteCanvas({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handlePointerUp = (e: FederatedMouseEvent) => {
|
const handlePointerUp = (e: FederatedMouseEvent) => {
|
||||||
// Если не было перетаскивания, то это простой клик - закрываем виджет
|
|
||||||
if (!isDragging) {
|
if (!isDragging) {
|
||||||
setSelectedSight(undefined);
|
setSelectedSight(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsPointerDown(false);
|
setIsPointerDown(false);
|
||||||
setIsDragging(false);
|
setIsDragging(false);
|
||||||
// Сбрасываем флаг взаимодействия через небольшую задержку
|
|
||||||
// чтобы избежать немедленного срабатывания useEffect
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIsUserInteracting(false);
|
setIsUserInteracting(false);
|
||||||
}, 100);
|
}, 100);
|
||||||
@@ -166,29 +155,25 @@ export function InfiniteCanvas({
|
|||||||
|
|
||||||
const handleWheel = (e: FederatedWheelEvent) => {
|
const handleWheel = (e: FederatedWheelEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setIsUserInteracting(true); // Устанавливаем флаг при зуме
|
setIsUserInteracting(true);
|
||||||
|
|
||||||
// Get mouse position relative to canvas
|
|
||||||
const mouseX = e.globalX - position.x;
|
const mouseX = e.globalX - position.x;
|
||||||
const mouseY = e.globalY - position.y;
|
const mouseY = e.globalY - position.y;
|
||||||
|
|
||||||
// Calculate new scale
|
|
||||||
const scaleMin = (routeData?.scale_min ?? 10) / SCALE_FACTOR;
|
const scaleMin = (routeData?.scale_min ?? 10) / SCALE_FACTOR;
|
||||||
const scaleMax = (routeData?.scale_max ?? 20) / SCALE_FACTOR;
|
const scaleMax = (routeData?.scale_max ?? 20) / SCALE_FACTOR;
|
||||||
|
|
||||||
const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1; // Zoom out/in
|
const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1;
|
||||||
const newScale = Math.max(scaleMin, Math.min(scaleMax, scale * zoomFactor));
|
const newScale = Math.max(scaleMin, Math.min(scaleMax, scale * zoomFactor));
|
||||||
const actualZoomFactor = newScale / scale;
|
const actualZoomFactor = newScale / scale;
|
||||||
|
|
||||||
if (scale === newScale) {
|
if (scale === newScale) {
|
||||||
// Сбрасываем флаг, если зум не изменился
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIsUserInteracting(false);
|
setIsUserInteracting(false);
|
||||||
}, 100);
|
}, 100);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update position to zoom towards mouse cursor
|
|
||||||
setPosition({
|
setPosition({
|
||||||
x: position.x + mouseX * (1 - actualZoomFactor),
|
x: position.x + mouseX * (1 - actualZoomFactor),
|
||||||
y: position.y + mouseY * (1 - actualZoomFactor),
|
y: position.y + mouseY * (1 - actualZoomFactor),
|
||||||
@@ -196,7 +181,6 @@ export function InfiniteCanvas({
|
|||||||
|
|
||||||
setScale(newScale);
|
setScale(newScale);
|
||||||
|
|
||||||
// Сбрасываем флаг взаимодействия через задержку
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIsUserInteracting(false);
|
setIsUserInteracting(false);
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
import { Stack, Typography, Button } from "@mui/material";
|
import { Box, Stack, Typography, Button } from "@mui/material";
|
||||||
import { useNavigate, useNavigationType } from "react-router";
|
import { useNavigate, useNavigationType } from "react-router";
|
||||||
import { MediaViewer } from "@widgets";
|
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 } from "@shared";
|
import { authInstance } from "@shared";
|
||||||
|
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||||
|
import LanguageSelector from "./web-gl/LanguageSelector";
|
||||||
|
|
||||||
export const LeftSidebar = observer(() => {
|
type LeftSidebarProps = {
|
||||||
|
open: boolean;
|
||||||
|
onToggle: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LeftSidebar = observer(({ open, onToggle }: LeftSidebarProps) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const navigationType = useNavigationType(); // PUSH, POP, REPLACE
|
const navigationType = useNavigationType(); // PUSH, POP, REPLACE
|
||||||
const { routeData } = useMapData();
|
const { routeData } = useMapData();
|
||||||
@@ -35,101 +42,146 @@ export const LeftSidebar = observer(() => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack direction="column" width="300px" p={2} bgcolor="primary.main">
|
<Box
|
||||||
<button
|
sx={{
|
||||||
onClick={handleBack}
|
position: "relative",
|
||||||
type="button"
|
height: "100%",
|
||||||
style={{
|
color: "#fff",
|
||||||
display: "flex",
|
transition: "padding 0.3s ease",
|
||||||
justifyContent: "center",
|
p: open ? 2 : 0,
|
||||||
alignItems: "center",
|
display: "flex",
|
||||||
gap: 10,
|
flexDirection: "column",
|
||||||
color: "#fff",
|
alignItems: "stretch",
|
||||||
backgroundColor: "#222",
|
justifyContent: "flex-start",
|
||||||
borderRadius: 10,
|
}}
|
||||||
height: 40,
|
>
|
||||||
width: "100%",
|
|
||||||
border: "none",
|
|
||||||
cursor: "pointer",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<p>Назад</p>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<Stack
|
<Stack
|
||||||
direction="column"
|
direction="column"
|
||||||
alignItems="center"
|
height="100%"
|
||||||
justifyContent="center"
|
width="100%"
|
||||||
my={10}
|
spacing={4}
|
||||||
|
alignItems="stretch"
|
||||||
|
sx={{
|
||||||
|
opacity: open ? 1 : 0,
|
||||||
|
transition: "opacity 0.25s ease",
|
||||||
|
pointerEvents: open ? "auto" : "none",
|
||||||
|
display: open ? "flex" : "none",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<Button
|
||||||
style={{
|
onClick={handleBack}
|
||||||
maxWidth: 200,
|
variant="contained"
|
||||||
display: "flex",
|
color="primary"
|
||||||
flexDirection: "column",
|
sx={{
|
||||||
alignItems: "center",
|
backgroundColor: "#222",
|
||||||
gap: 10,
|
color: "#fff",
|
||||||
|
borderRadius: 1.5,
|
||||||
|
px: 2,
|
||||||
|
py: 1,
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: "#2d2d2d",
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
|
fullWidth
|
||||||
|
startIcon={<ArrowBackIcon />}
|
||||||
>
|
>
|
||||||
{carrierThumbnail && (
|
Назад
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Stack
|
||||||
|
direction="column"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
spacing={3}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
maxWidth: 200,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{carrierThumbnail && (
|
||||||
|
<MediaViewer
|
||||||
|
media={{
|
||||||
|
id: carrierThumbnail,
|
||||||
|
media_type: 1, // Тип "Фото" для логотипа
|
||||||
|
filename: "route_thumbnail",
|
||||||
|
}}
|
||||||
|
fullWidth
|
||||||
|
fullHeight
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Typography sx={{ color: "#fff" }} textAlign="center">
|
||||||
|
При поддержке Правительства
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack
|
||||||
|
direction="column"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
spacing={2}
|
||||||
|
>
|
||||||
|
<Button variant="outlined" color="warning" fullWidth>
|
||||||
|
Достопримечательности
|
||||||
|
</Button>
|
||||||
|
<Button variant="outlined" color="warning" fullWidth>
|
||||||
|
Остановки
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack
|
||||||
|
direction="column"
|
||||||
|
alignItems="center"
|
||||||
|
maxHeight={150}
|
||||||
|
justifyContent="center"
|
||||||
|
flexGrow={1}
|
||||||
|
>
|
||||||
|
{carrierLogo && (
|
||||||
<MediaViewer
|
<MediaViewer
|
||||||
media={{
|
media={{
|
||||||
id: carrierThumbnail,
|
id: carrierLogo,
|
||||||
media_type: 1, // Тип "Фото" для логотипа
|
media_type: 1, // Тип "Фото" для логотипа
|
||||||
filename: "route_thumbnail",
|
filename: "route_thumbnail_logo",
|
||||||
}}
|
}}
|
||||||
fullWidth
|
|
||||||
fullHeight
|
fullHeight
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Typography sx={{ mb: 2, color: "#fff" }} textAlign="center">
|
</Stack>
|
||||||
При поддержке Правительства
|
|
||||||
</Typography>{" "}
|
<Typography variant="h6" textAlign="center" sx={{ color: "#fff" }}>
|
||||||
</div>
|
#ВсемПоПути
|
||||||
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Stack
|
{!open && (
|
||||||
direction="column"
|
<Typography
|
||||||
alignItems="center"
|
variant="caption"
|
||||||
justifyContent="center"
|
sx={{
|
||||||
my={10}
|
color: "rgba(255,255,255,0.6)",
|
||||||
spacing={2}
|
writingMode: "vertical-rl",
|
||||||
>
|
transform: "rotate(180deg)",
|
||||||
<Button variant="outlined" color="warning" fullWidth>
|
letterSpacing: 4,
|
||||||
Достопримечательности
|
position: "absolute",
|
||||||
</Button>
|
top: "50%",
|
||||||
<Button variant="outlined" color="warning" fullWidth>
|
left: "50%",
|
||||||
Остановки
|
transformOrigin: "center",
|
||||||
</Button>
|
translate: "-50% -50%",
|
||||||
</Stack>
|
opacity: 0.6,
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
#ВсемПоПути
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
<Stack
|
<div className="absolute bottom-[20px] -right-[520px] z-10">
|
||||||
direction="column"
|
<LanguageSelector onBack={onToggle} isSidebarOpen={open} />
|
||||||
alignItems="center"
|
</div>
|
||||||
maxHeight={150}
|
</Box>
|
||||||
justifyContent="center"
|
|
||||||
my={10}
|
|
||||||
>
|
|
||||||
{carrierLogo && (
|
|
||||||
<MediaViewer
|
|
||||||
media={{
|
|
||||||
id: carrierLogo,
|
|
||||||
media_type: 1, // Тип "Фото" для логотипа
|
|
||||||
filename: "route_thumbnail_logo",
|
|
||||||
}}
|
|
||||||
fullHeight
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Typography
|
|
||||||
variant="h6"
|
|
||||||
textAlign="center"
|
|
||||||
mt="auto"
|
|
||||||
sx={{ color: "#fff" }}
|
|
||||||
>
|
|
||||||
#ВсемПоПути
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ const MapDataContext = createContext<{
|
|||||||
latitude: number,
|
latitude: number,
|
||||||
longitude: number
|
longitude: number
|
||||||
) => void;
|
) => void;
|
||||||
|
setIconSize: (size: number) => void;
|
||||||
|
setFontSize: (size: number) => void;
|
||||||
saveChanges: () => void;
|
saveChanges: () => void;
|
||||||
}>({
|
}>({
|
||||||
originalRouteData: undefined,
|
originalRouteData: undefined,
|
||||||
@@ -61,6 +63,8 @@ const MapDataContext = createContext<{
|
|||||||
setStationOffset: () => {},
|
setStationOffset: () => {},
|
||||||
setStationAlign: () => {},
|
setStationAlign: () => {},
|
||||||
setSightCoordinates: () => {},
|
setSightCoordinates: () => {},
|
||||||
|
setIconSize: () => {},
|
||||||
|
setFontSize: () => {},
|
||||||
saveChanges: () => {},
|
saveChanges: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -141,7 +145,6 @@ export const MapDataProvider = observer(
|
|||||||
}, [routeId]);
|
}, [routeId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// combine changes with original data
|
|
||||||
if (originalRouteData)
|
if (originalRouteData)
|
||||||
setRouteData({ ...originalRouteData, ...routeChanges });
|
setRouteData({ ...originalRouteData, ...routeChanges });
|
||||||
if (originalSightData) setSightData(originalSightData);
|
if (originalSightData) setSightData(originalSightData);
|
||||||
@@ -165,9 +168,57 @@ export const MapDataProvider = observer(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setMapCenter(x: number, y: number) {
|
function setIconSize(size: number) {
|
||||||
|
const clamped = Math.max(50, Math.min(300, size));
|
||||||
setRouteChanges((prev) => {
|
setRouteChanges((prev) => {
|
||||||
return { ...prev, center_latitude: x, center_longitude: y };
|
if (prev.icon_size === clamped) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
return { ...prev, icon_size: clamped };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFontSize(size: number) {
|
||||||
|
const clamped = Math.max(50, Math.min(300, size));
|
||||||
|
setRouteChanges((prev) => {
|
||||||
|
if (prev.font_size === clamped) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
return { ...prev, font_size: clamped };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMapCenter(latitude: number, longitude: number) {
|
||||||
|
const epsilon = 1e-6;
|
||||||
|
|
||||||
|
setRouteChanges((prev) => {
|
||||||
|
const prevLat = prev.center_latitude;
|
||||||
|
const prevLon = prev.center_longitude;
|
||||||
|
|
||||||
|
if (
|
||||||
|
prevLat !== undefined &&
|
||||||
|
prevLon !== undefined &&
|
||||||
|
Math.abs(prevLat - latitude) < epsilon &&
|
||||||
|
Math.abs(prevLon - longitude) < epsilon
|
||||||
|
) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
center_latitude: latitude,
|
||||||
|
center_longitude: longitude,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
setRouteData((routePrev) => {
|
||||||
|
if (!routePrev) return routePrev;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...routePrev,
|
||||||
|
center_latitude: latitude,
|
||||||
|
center_longitude: longitude,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,12 +231,42 @@ export const MapDataProvider = observer(
|
|||||||
async function saveStationChanges() {
|
async function saveStationChanges() {
|
||||||
for (const station of stationChanges) {
|
for (const station of stationChanges) {
|
||||||
await authInstance.patch(`/route/${routeId}/station`, station);
|
await authInstance.patch(`/route/${routeId}/station`, station);
|
||||||
|
|
||||||
|
setStationData((prev) => {
|
||||||
|
const updated = { ...prev };
|
||||||
|
Object.keys(updated).forEach((lang) => {
|
||||||
|
updated[lang] = updated[lang].map((s) =>
|
||||||
|
s.id === station.station_id
|
||||||
|
? {
|
||||||
|
...s,
|
||||||
|
offset_x: station.offset_x,
|
||||||
|
offset_y: station.offset_y,
|
||||||
|
}
|
||||||
|
: s
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveSightChanges() {
|
async function saveSightChanges() {
|
||||||
for (const sight of sightChanges) {
|
for (const sight of sightChanges) {
|
||||||
await authInstance.patch(`/route/${routeId}/sight`, sight);
|
await authInstance.patch(`/route/${routeId}/sight`, sight);
|
||||||
|
|
||||||
|
setSightData((prev) =>
|
||||||
|
prev
|
||||||
|
? prev.map((s) =>
|
||||||
|
s.id === sight.sight_id
|
||||||
|
? {
|
||||||
|
...s,
|
||||||
|
latitude: sight.latitude,
|
||||||
|
longitude: sight.longitude,
|
||||||
|
}
|
||||||
|
: s
|
||||||
|
)
|
||||||
|
: prev
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,6 +402,14 @@ export const MapDataProvider = observer(
|
|||||||
latitude: number,
|
latitude: number,
|
||||||
longitude: number
|
longitude: number
|
||||||
) {
|
) {
|
||||||
|
setSightData((prev) =>
|
||||||
|
prev
|
||||||
|
? prev.map((sight) =>
|
||||||
|
sight.id === sightId ? { ...sight, latitude, longitude } : sight
|
||||||
|
)
|
||||||
|
: prev
|
||||||
|
);
|
||||||
|
|
||||||
setSightChanges((prev) => {
|
setSightChanges((prev) => {
|
||||||
const existingIndex = prev.findIndex(
|
const existingIndex = prev.findIndex(
|
||||||
(sight) => sight.sight_id === sightId
|
(sight) => sight.sight_id === sightId
|
||||||
@@ -376,6 +465,8 @@ export const MapDataProvider = observer(
|
|||||||
setStationOffset,
|
setStationOffset,
|
||||||
setStationAlign,
|
setStationAlign,
|
||||||
setSightCoordinates,
|
setSightCoordinates,
|
||||||
|
setIconSize,
|
||||||
|
setFontSize,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
originalRouteData,
|
originalRouteData,
|
||||||
@@ -388,6 +479,8 @@ export const MapDataProvider = observer(
|
|||||||
isStationLoading,
|
isStationLoading,
|
||||||
isSightLoading,
|
isSightLoading,
|
||||||
selectedSight,
|
selectedSight,
|
||||||
|
setIconSize,
|
||||||
|
setFontSize,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { Button, Stack, TextField, Typography, Slider } from "@mui/material";
|
|||||||
import { useMapData } from "./MapDataContext";
|
import { useMapData } from "./MapDataContext";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTransform } from "./TransformContext";
|
import { useTransform } from "./TransformContext";
|
||||||
import { coordinatesToLocal, localToCoordinates } from "./utils";
|
|
||||||
import { SCALE_FACTOR } from "./Constants";
|
import { SCALE_FACTOR } from "./Constants";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
@@ -13,18 +12,10 @@ export function RightSidebar() {
|
|||||||
saveChanges,
|
saveChanges,
|
||||||
originalRouteData,
|
originalRouteData,
|
||||||
setMapRotation,
|
setMapRotation,
|
||||||
setMapCenter,
|
setIconSize: updateIconSize,
|
||||||
|
setFontSize: updateFontSize,
|
||||||
} = useMapData();
|
} = useMapData();
|
||||||
const {
|
const { rotation, rotateToAngle, scale, setScaleAtCenter } = useTransform();
|
||||||
rotation,
|
|
||||||
position,
|
|
||||||
screenToLocal,
|
|
||||||
screenCenter,
|
|
||||||
rotateToAngle,
|
|
||||||
setTransform,
|
|
||||||
scale,
|
|
||||||
setScaleAtCenter,
|
|
||||||
} = useTransform();
|
|
||||||
|
|
||||||
const [minScale, setMinScale] = useState<number>(1);
|
const [minScale, setMinScale] = useState<number>(1);
|
||||||
const [maxScale, setMaxScale] = useState<number>(5);
|
const [maxScale, setMaxScale] = useState<number>(5);
|
||||||
@@ -34,14 +25,14 @@ export function RightSidebar() {
|
|||||||
});
|
});
|
||||||
const [rotationDegrees, setRotationDegrees] = useState<number>(0);
|
const [rotationDegrees, setRotationDegrees] = useState<number>(0);
|
||||||
const [isUserEditing, setIsUserEditing] = useState<boolean>(false);
|
const [isUserEditing, setIsUserEditing] = useState<boolean>(false);
|
||||||
|
const [iconSize, setIconSize] = useState<number>(100);
|
||||||
|
const [fontSize, setFontSize] = useState<number>(100);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (originalRouteData) {
|
if (originalRouteData) {
|
||||||
// Проверяем и сбрасываем минимальный масштаб если нужно
|
|
||||||
const originalMinScale = originalRouteData.scale_min ?? 1;
|
const originalMinScale = originalRouteData.scale_min ?? 1;
|
||||||
const resetMinScale = originalMinScale < 1 ? 1 : originalMinScale;
|
const resetMinScale = originalMinScale < 1 ? 1 : originalMinScale;
|
||||||
|
|
||||||
// Проверяем и сбрасываем максимальный масштаб если нужно
|
|
||||||
const originalMaxScale = originalRouteData.scale_max ?? 5;
|
const originalMaxScale = originalRouteData.scale_max ?? 5;
|
||||||
const resetMaxScale = originalMaxScale < 3 ? 3 : originalMaxScale;
|
const resetMaxScale = originalMaxScale < 3 ? 3 : originalMaxScale;
|
||||||
|
|
||||||
@@ -52,6 +43,8 @@ export function RightSidebar() {
|
|||||||
x: originalRouteData.center_latitude ?? 0,
|
x: originalRouteData.center_latitude ?? 0,
|
||||||
y: originalRouteData.center_longitude ?? 0,
|
y: originalRouteData.center_longitude ?? 0,
|
||||||
});
|
});
|
||||||
|
setIconSize(originalRouteData.icon_size ?? 100);
|
||||||
|
setFontSize(originalRouteData.font_size ?? 100);
|
||||||
}
|
}
|
||||||
}, [originalRouteData]);
|
}, [originalRouteData]);
|
||||||
|
|
||||||
@@ -72,33 +65,55 @@ export function RightSidebar() {
|
|||||||
}, [rotationDegrees]);
|
}, [rotationDegrees]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isUserEditing) {
|
if (isUserEditing) {
|
||||||
const center = screenCenter ?? { x: 0, y: 0 };
|
return;
|
||||||
const localCenter = screenToLocal(center.x, center.y);
|
|
||||||
const coordinates = localToCoordinates(localCenter.x, localCenter.y);
|
|
||||||
setLocalCenter({ x: coordinates.latitude, y: coordinates.longitude });
|
|
||||||
}
|
}
|
||||||
}, [
|
|
||||||
position,
|
|
||||||
screenCenter,
|
|
||||||
screenToLocal,
|
|
||||||
localToCoordinates,
|
|
||||||
setLocalCenter,
|
|
||||||
isUserEditing,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const latitude = routeData?.center_latitude ?? 0;
|
||||||
setMapCenter(localCenter.x, localCenter.y);
|
const longitude = routeData?.center_longitude ?? 0;
|
||||||
}, [localCenter]);
|
|
||||||
|
setLocalCenter((prev) => {
|
||||||
|
if (
|
||||||
|
Math.abs(prev.x - latitude) < 1e-6 &&
|
||||||
|
Math.abs(prev.y - longitude) < 1e-6
|
||||||
|
) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
return { x: latitude, y: longitude };
|
||||||
|
});
|
||||||
|
}, [isUserEditing, routeData?.center_latitude, routeData?.center_longitude]);
|
||||||
|
|
||||||
function setRotationFromDegrees(degrees: number) {
|
function setRotationFromDegrees(degrees: number) {
|
||||||
rotateToAngle((degrees * Math.PI) / 180);
|
rotateToAngle((degrees * Math.PI) / 180);
|
||||||
}
|
}
|
||||||
|
|
||||||
function pan({ x, y }: { x: number; y: number }) {
|
const handleIconSizeChange = (value: number) => {
|
||||||
const coordinates = coordinatesToLocal(x, y);
|
if (!Number.isFinite(value)) {
|
||||||
setTransform(coordinates.x, coordinates.y);
|
return;
|
||||||
}
|
}
|
||||||
|
const clamped = Math.max(50, Math.min(300, Math.round(value)));
|
||||||
|
setIconSize(clamped);
|
||||||
|
updateIconSize(clamped);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFontSizeChange = (value: number) => {
|
||||||
|
if (!Number.isFinite(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const clamped = Math.max(50, Math.min(300, Math.round(value)));
|
||||||
|
setFontSize(clamped);
|
||||||
|
updateFontSize(clamped);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const next = routeData?.icon_size ?? originalRouteData?.icon_size ?? 100;
|
||||||
|
setIconSize((prev) => (Math.abs(prev - next) > 0.5 ? next : prev));
|
||||||
|
}, [routeData?.icon_size, originalRouteData?.icon_size]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const next = routeData?.font_size ?? originalRouteData?.font_size ?? 100;
|
||||||
|
setFontSize((prev) => (Math.abs(prev - next) > 0.5 ? next : prev));
|
||||||
|
}, [routeData?.font_size, originalRouteData?.font_size]);
|
||||||
|
|
||||||
if (!routeData) {
|
if (!routeData) {
|
||||||
return null;
|
return null;
|
||||||
@@ -118,7 +133,7 @@ export function RightSidebar() {
|
|||||||
borderRadius={2}
|
borderRadius={2}
|
||||||
>
|
>
|
||||||
<Typography variant="h6" sx={{ mb: 2, color: "#fff" }} textAlign="center">
|
<Typography variant="h6" sx={{ mb: 2, color: "#fff" }} textAlign="center">
|
||||||
Детали о достопримечательностях
|
Настройка маршрута
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Stack spacing={2} direction="row" alignItems="center">
|
<Stack spacing={2} direction="row" alignItems="center">
|
||||||
@@ -130,19 +145,18 @@ export function RightSidebar() {
|
|||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
let newMinScale = Number(e.target.value);
|
let newMinScale = Number(e.target.value);
|
||||||
|
|
||||||
// Сбрасываем к 1 если меньше
|
if (newMinScale < 10) {
|
||||||
if (newMinScale < 1) {
|
newMinScale = 10;
|
||||||
newMinScale = 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setMinScale(newMinScale);
|
setMinScale(newMinScale);
|
||||||
|
|
||||||
if (maxScale - newMinScale < 2) {
|
if (maxScale - newMinScale < 2) {
|
||||||
let newMaxScale = newMinScale + 2;
|
let newMaxScale = newMinScale + 2;
|
||||||
// Сбрасываем максимальный к 3 если меньше минимального
|
|
||||||
if (newMaxScale < 3) {
|
if (newMaxScale < 3) {
|
||||||
newMaxScale = 3;
|
newMaxScale = 3;
|
||||||
setMinScale(1); // Сбрасываем минимальный к 1
|
setMinScale(1);
|
||||||
}
|
}
|
||||||
setMaxScale(newMaxScale);
|
setMaxScale(newMaxScale);
|
||||||
}
|
}
|
||||||
@@ -175,19 +189,22 @@ export function RightSidebar() {
|
|||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
let newMaxScale = Number(e.target.value);
|
let newMaxScale = Number(e.target.value);
|
||||||
|
|
||||||
// Сбрасываем к 3 если меньше минимального
|
if (newMaxScale < 13) {
|
||||||
if (newMaxScale < 3) {
|
newMaxScale = 13;
|
||||||
newMaxScale = 3;
|
}
|
||||||
|
|
||||||
|
if (newMaxScale > 300) {
|
||||||
|
newMaxScale = 300;
|
||||||
}
|
}
|
||||||
|
|
||||||
setMaxScale(newMaxScale);
|
setMaxScale(newMaxScale);
|
||||||
|
|
||||||
if (newMaxScale - minScale < 2) {
|
if (newMaxScale - minScale < 2) {
|
||||||
let newMinScale = newMaxScale - 2;
|
let newMinScale = newMaxScale - 2;
|
||||||
// Сбрасываем минимальный к 1 если меньше
|
|
||||||
if (newMinScale < 1) {
|
if (newMinScale < 1) {
|
||||||
newMinScale = 1;
|
newMinScale = 1;
|
||||||
setMaxScale(3); // Сбрасываем максимальный к минимальному значению
|
setMaxScale(3);
|
||||||
}
|
}
|
||||||
setMinScale(newMinScale);
|
setMinScale(newMinScale);
|
||||||
}
|
}
|
||||||
@@ -208,7 +225,7 @@ export function RightSidebar() {
|
|||||||
slotProps={{
|
slotProps={{
|
||||||
input: {
|
input: {
|
||||||
min: 3,
|
min: 3,
|
||||||
max: 10,
|
max: 300,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -272,6 +289,62 @@ export function RightSidebar() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Typography variant="body2" sx={{ color: "#fff", textAlign: "center" }}>
|
||||||
|
Размер иконок: {iconSize}%
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Slider
|
||||||
|
value={iconSize}
|
||||||
|
onChange={(_, value) => {
|
||||||
|
if (typeof value === "number") {
|
||||||
|
handleIconSizeChange(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
min={50}
|
||||||
|
max={300}
|
||||||
|
step={1}
|
||||||
|
sx={{
|
||||||
|
color: "#fff",
|
||||||
|
"& .MuiSlider-thumb": {
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
},
|
||||||
|
"& .MuiSlider-track": {
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
},
|
||||||
|
"& .MuiSlider-rail": {
|
||||||
|
backgroundColor: "#666",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Typography variant="body2" sx={{ color: "#fff", textAlign: "center" }}>
|
||||||
|
Размер шрифта: {fontSize}%
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Slider
|
||||||
|
value={fontSize}
|
||||||
|
onChange={(_, value) => {
|
||||||
|
if (typeof value === "number") {
|
||||||
|
handleFontSizeChange(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
min={50}
|
||||||
|
max={300}
|
||||||
|
step={1}
|
||||||
|
sx={{
|
||||||
|
color: "#fff",
|
||||||
|
"& .MuiSlider-thumb": {
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
},
|
||||||
|
"& .MuiSlider-track": {
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
},
|
||||||
|
"& .MuiSlider-rail": {
|
||||||
|
backgroundColor: "#666",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
type="number"
|
type="number"
|
||||||
label="Поворот (в градусах)"
|
label="Поворот (в градусах)"
|
||||||
@@ -314,9 +387,10 @@ export function RightSidebar() {
|
|||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setIsUserEditing(true);
|
setIsUserEditing(true);
|
||||||
setLocalCenter((prev) => ({ ...prev, x: Number(e.target.value) }));
|
setLocalCenter((prev) => ({ ...prev, x: Number(e.target.value) }));
|
||||||
pan({ x: Number(e.target.value), y: localCenter.y });
|
|
||||||
}}
|
}}
|
||||||
onBlur={() => setIsUserEditing(false)}
|
onBlur={() => {
|
||||||
|
setIsUserEditing(false);
|
||||||
|
}}
|
||||||
style={{ backgroundColor: "#222", borderRadius: 4 }}
|
style={{ backgroundColor: "#222", borderRadius: 4 }}
|
||||||
sx={{
|
sx={{
|
||||||
"& .MuiInputLabel-root": {
|
"& .MuiInputLabel-root": {
|
||||||
@@ -338,9 +412,10 @@ export function RightSidebar() {
|
|||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setIsUserEditing(true);
|
setIsUserEditing(true);
|
||||||
setLocalCenter((prev) => ({ ...prev, y: Number(e.target.value) }));
|
setLocalCenter((prev) => ({ ...prev, y: Number(e.target.value) }));
|
||||||
pan({ x: localCenter.x, y: Number(e.target.value) });
|
|
||||||
}}
|
}}
|
||||||
onBlur={() => setIsUserEditing(false)}
|
onBlur={() => {
|
||||||
|
setIsUserEditing(false);
|
||||||
|
}}
|
||||||
style={{ backgroundColor: "#222", borderRadius: 4 }}
|
style={{ backgroundColor: "#222", borderRadius: 4 }}
|
||||||
sx={{
|
sx={{
|
||||||
"& .MuiInputLabel-root": {
|
"& .MuiInputLabel-root": {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { FederatedMouseEvent, Graphics } from "pixi.js";
|
|||||||
import { useCallback, useState, useEffect, useRef, FC, useMemo } from "react";
|
import { useCallback, useState, useEffect, useRef, FC, useMemo } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
// --- Заглушки для зависимостей (замените на ваши реальные импорты) ---
|
|
||||||
import {
|
import {
|
||||||
BACKGROUND_COLOR,
|
BACKGROUND_COLOR,
|
||||||
PATH_COLOR,
|
PATH_COLOR,
|
||||||
@@ -15,22 +14,16 @@ import { StationData } from "./types";
|
|||||||
import { useMapData } from "./MapDataContext";
|
import { useMapData } from "./MapDataContext";
|
||||||
import { coordinatesToLocal } from "./utils";
|
import { coordinatesToLocal } from "./utils";
|
||||||
import { languageStore } from "@shared";
|
import { languageStore } from "@shared";
|
||||||
// --- Конец заглушек ---
|
|
||||||
|
|
||||||
// --- Декларации для react-pixi ---
|
|
||||||
// (В реальном проекте типы должны быть предоставлены библиотекой @pixi/react-pixi)
|
|
||||||
declare const pixiContainer: any;
|
declare const pixiContainer: any;
|
||||||
declare const pixiGraphics: any;
|
declare const pixiGraphics: any;
|
||||||
declare const pixiText: any;
|
declare const pixiText: any;
|
||||||
|
|
||||||
// --- Типы ---
|
|
||||||
type HorizontalAlign = "left" | "center" | "right";
|
type HorizontalAlign = "left" | "center" | "right";
|
||||||
type VerticalAlign = "top" | "center" | "bottom";
|
type VerticalAlign = "top" | "center" | "bottom";
|
||||||
type TextAlign = HorizontalAlign | `${HorizontalAlign} ${VerticalAlign}`;
|
type TextAlign = HorizontalAlign | `${HorizontalAlign} ${VerticalAlign}`;
|
||||||
type LabelAlign = "left" | "center" | "right";
|
type LabelAlign = "left" | "center" | "right";
|
||||||
|
|
||||||
// --- Утилиты ---
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Преобразует текстовое позиционирование в anchor координаты.
|
* Преобразует текстовое позиционирование в anchor координаты.
|
||||||
*/
|
*/
|
||||||
@@ -39,8 +32,6 @@ type LabelAlign = "left" | "center" | "right";
|
|||||||
* Получает координату anchor.x из типа выравнивания.
|
* Получает координату anchor.x из типа выравнивания.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// --- Интерфейсы пропсов ---
|
|
||||||
|
|
||||||
interface StationProps {
|
interface StationProps {
|
||||||
station: StationData;
|
station: StationData;
|
||||||
ruLabel: string | null;
|
ruLabel: string | null;
|
||||||
@@ -83,10 +74,6 @@ const getAnchorFromOffset = (
|
|||||||
return { x: (1 - nx) / 2, y: (1 - ny) / 2 };
|
return { x: (1 - nx) / 2, y: (1 - ny) / 2 };
|
||||||
};
|
};
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// Компонент: Панель управления выравниванием в стиле УрФУ
|
|
||||||
// =========================================================================
|
|
||||||
|
|
||||||
const LabelAlignmentControl: FC<LabelAlignmentControlProps> = ({
|
const LabelAlignmentControl: FC<LabelAlignmentControlProps> = ({
|
||||||
scale,
|
scale,
|
||||||
currentAlign,
|
currentAlign,
|
||||||
@@ -107,7 +94,6 @@ const LabelAlignmentControl: FC<LabelAlignmentControlProps> = ({
|
|||||||
(g: Graphics) => {
|
(g: Graphics) => {
|
||||||
g.clear();
|
g.clear();
|
||||||
|
|
||||||
// Основной фон с градиентом
|
|
||||||
g.roundRect(
|
g.roundRect(
|
||||||
-controlWidth / 2,
|
-controlWidth / 2,
|
||||||
0,
|
0,
|
||||||
@@ -115,9 +101,8 @@ const LabelAlignmentControl: FC<LabelAlignmentControlProps> = ({
|
|||||||
controlHeight,
|
controlHeight,
|
||||||
borderRadius
|
borderRadius
|
||||||
);
|
);
|
||||||
g.fill({ color: "#1a1a1a" }); // Темный фон как у УрФУ
|
g.fill({ color: "#1a1a1a" });
|
||||||
|
|
||||||
// Тонкая рамка
|
|
||||||
g.roundRect(
|
g.roundRect(
|
||||||
-controlWidth / 2,
|
-controlWidth / 2,
|
||||||
0,
|
0,
|
||||||
@@ -127,7 +112,6 @@ const LabelAlignmentControl: FC<LabelAlignmentControlProps> = ({
|
|||||||
);
|
);
|
||||||
g.stroke({ color: "#333333", width: strokeWidth });
|
g.stroke({ color: "#333333", width: strokeWidth });
|
||||||
|
|
||||||
// Разделители между кнопками
|
|
||||||
for (let i = 1; i < 3; i++) {
|
for (let i = 1; i < 3; i++) {
|
||||||
const x = -controlWidth / 2 + buttonWidth * i;
|
const x = -controlWidth / 2 + buttonWidth * i;
|
||||||
g.moveTo(x, strokeWidth);
|
g.moveTo(x, strokeWidth);
|
||||||
@@ -151,7 +135,7 @@ const LabelAlignmentControl: FC<LabelAlignmentControlProps> = ({
|
|||||||
controlHeight - strokeWidth * 2,
|
controlHeight - strokeWidth * 2,
|
||||||
borderRadius / 2
|
borderRadius / 2
|
||||||
);
|
);
|
||||||
g.fill({ color: "#0066cc", alpha: 0.8 }); // Синий акцент УрФУ
|
g.fill({ color: "#0066cc", alpha: 0.8 });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[controlWidth, controlHeight, buttonWidth, strokeWidth, borderRadius]
|
[controlWidth, controlHeight, buttonWidth, strokeWidth, borderRadius]
|
||||||
@@ -230,10 +214,6 @@ const LabelAlignmentControl: FC<LabelAlignmentControlProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// Компонент: Метка Станции (с логикой)
|
|
||||||
// =========================================================================
|
|
||||||
|
|
||||||
const StationLabel = observer(
|
const StationLabel = observer(
|
||||||
({
|
({
|
||||||
station,
|
station,
|
||||||
@@ -274,48 +254,45 @@ const StationLabel = observer(
|
|||||||
hideTimer.current = null;
|
hideTimer.current = null;
|
||||||
}
|
}
|
||||||
setIsHovered(true);
|
setIsHovered(true);
|
||||||
onTextHover?.(true); // Call the callback to indicate text is hovered
|
onTextHover?.(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleControlPointerEnter = () => {
|
const handleControlPointerEnter = () => {
|
||||||
// Дополнительная обработка для панели управления
|
|
||||||
if (hideTimer.current) {
|
if (hideTimer.current) {
|
||||||
clearTimeout(hideTimer.current);
|
clearTimeout(hideTimer.current);
|
||||||
hideTimer.current = null;
|
hideTimer.current = null;
|
||||||
}
|
}
|
||||||
setIsControlHovered(true);
|
setIsControlHovered(true);
|
||||||
setIsHovered(true);
|
setIsHovered(true);
|
||||||
onTextHover?.(true); // Call the callback to indicate text/control is hovered
|
onTextHover?.(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleControlPointerLeave = () => {
|
const handleControlPointerLeave = () => {
|
||||||
setIsControlHovered(false);
|
setIsControlHovered(false);
|
||||||
// Если курсор не над основным контейнером, скрываем панель через некоторое время
|
|
||||||
if (!isHovered) {
|
if (!isHovered) {
|
||||||
hideTimer.current = setTimeout(() => {
|
hideTimer.current = setTimeout(() => {
|
||||||
setIsHovered(false);
|
setIsHovered(false);
|
||||||
onTextHover?.(false); // Call the callback to indicate neither text nor control is hovered
|
onTextHover?.(false);
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePointerLeave = () => {
|
const handlePointerLeave = () => {
|
||||||
// Увеличиваем время до скрытия панели и добавляем проверку
|
|
||||||
hideTimer.current = setTimeout(() => {
|
hideTimer.current = setTimeout(() => {
|
||||||
setIsHovered(false);
|
setIsHovered(false);
|
||||||
// Если курсор не над панелью управления, скрываем и её
|
|
||||||
if (!isControlHovered) {
|
if (!isControlHovered) {
|
||||||
setIsControlHovered(false);
|
setIsControlHovered(false);
|
||||||
}
|
}
|
||||||
onTextHover?.(false); // Call the callback to indicate text is no longer hovered
|
onTextHover?.(false);
|
||||||
}, 100); // Увеличиваем время до скрытия панели
|
}, 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPosition({ x: station.offset_x ?? 0, y: station.offset_y ?? 0 });
|
setPosition({ x: station.offset_x ?? 0, y: station.offset_y ?? 0 });
|
||||||
}, [station.offset_x, station.offset_y, station.id]);
|
}, [station.offset_x, station.offset_y, station.id]);
|
||||||
|
|
||||||
// Функция для конвертации числового align в строковый
|
|
||||||
const convertNumericAlign = (align: number): LabelAlign => {
|
const convertNumericAlign = (align: number): LabelAlign => {
|
||||||
switch (align) {
|
switch (align) {
|
||||||
case 0:
|
case 0:
|
||||||
@@ -329,7 +306,6 @@ const StationLabel = observer(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Функция для конвертации строкового align в числовой
|
|
||||||
const convertStringAlign = (align: LabelAlign): number => {
|
const convertStringAlign = (align: LabelAlign): number => {
|
||||||
switch (align) {
|
switch (align) {
|
||||||
case "left":
|
case "left":
|
||||||
@@ -353,7 +329,6 @@ const StationLabel = observer(
|
|||||||
const compensatedRuFontSize = (26 * 0.75) / scale;
|
const compensatedRuFontSize = (26 * 0.75) / scale;
|
||||||
const compensatedNameFontSize = (16 * 0.75) / scale;
|
const compensatedNameFontSize = (16 * 0.75) / scale;
|
||||||
|
|
||||||
// Измеряем ширину верхнего лейбла
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ruLabelRef.current && ruLabel) {
|
if (ruLabelRef.current && ruLabel) {
|
||||||
setRuLabelWidth(ruLabelRef.current.width);
|
setRuLabelWidth(ruLabelRef.current.width);
|
||||||
@@ -386,7 +361,6 @@ const StationLabel = observer(
|
|||||||
y: dragStartPos.current.y + dy_screen,
|
y: dragStartPos.current.y + dy_screen,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Проверяем, изменилась ли позиция
|
|
||||||
if (
|
if (
|
||||||
Math.abs(newPosition.x - position.x) > 0.01 ||
|
Math.abs(newPosition.x - position.x) > 0.01 ||
|
||||||
Math.abs(newPosition.y - position.y) > 0.01
|
Math.abs(newPosition.y - position.y) > 0.01
|
||||||
@@ -406,7 +380,7 @@ const StationLabel = observer(
|
|||||||
const handleAlignChange = async (align: LabelAlign) => {
|
const handleAlignChange = async (align: LabelAlign) => {
|
||||||
setCurrentLabelAlign(align);
|
setCurrentLabelAlign(align);
|
||||||
onLabelAlignChange?.(align);
|
onLabelAlignChange?.(align);
|
||||||
// Сохраняем в стор
|
|
||||||
const numericAlign = convertStringAlign(align);
|
const numericAlign = convertStringAlign(align);
|
||||||
setStationAlign(station.id, numericAlign);
|
setStationAlign(station.id, numericAlign);
|
||||||
};
|
};
|
||||||
@@ -416,34 +390,29 @@ const StationLabel = observer(
|
|||||||
[position.x, position.y]
|
[position.x, position.y]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Функция для расчета позиции нижнего лейбла относительно ширины верхнего
|
|
||||||
const getSecondLabelPosition = (): number => {
|
const getSecondLabelPosition = (): number => {
|
||||||
if (!ruLabelWidth) return 0;
|
if (!ruLabelWidth) return 0;
|
||||||
|
|
||||||
switch (currentLabelAlign) {
|
switch (currentLabelAlign) {
|
||||||
case "left":
|
case "left":
|
||||||
// Позиционируем относительно левого края верхнего текста
|
|
||||||
return -ruLabelWidth / 2;
|
return -ruLabelWidth / 2;
|
||||||
case "center":
|
case "center":
|
||||||
// Центрируем относительно центра верхнего текста
|
|
||||||
return 0;
|
return 0;
|
||||||
case "right":
|
case "right":
|
||||||
// Позиционируем относительно правого края верхнего текста
|
|
||||||
return ruLabelWidth / 2;
|
return ruLabelWidth / 2;
|
||||||
default:
|
default:
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Функция для расчета anchor нижнего лейбла
|
|
||||||
const getSecondLabelAnchor = (): number => {
|
const getSecondLabelAnchor = (): number => {
|
||||||
switch (currentLabelAlign) {
|
switch (currentLabelAlign) {
|
||||||
case "left":
|
case "left":
|
||||||
return 0; // anchor.x = 0 (левый край)
|
return 0;
|
||||||
case "center":
|
case "center":
|
||||||
return 0.5; // anchor.x = 0.5 (центр)
|
return 0.5;
|
||||||
case "right":
|
case "right":
|
||||||
return 1; // anchor.x = 1 (правый край)
|
return 1;
|
||||||
default:
|
default:
|
||||||
return 0.5;
|
return 0.5;
|
||||||
}
|
}
|
||||||
@@ -522,10 +491,6 @@ const StationLabel = observer(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// Главный экспортируемый компонент: Станция
|
|
||||||
// =========================================================================
|
|
||||||
|
|
||||||
export const Station = ({
|
export const Station = ({
|
||||||
station,
|
station,
|
||||||
ruLabel,
|
ruLabel,
|
||||||
@@ -548,10 +513,9 @@ export const Station = ({
|
|||||||
|
|
||||||
g.circle(coordinates.x * UP_SCALE, coordinates.y * UP_SCALE, radius);
|
g.circle(coordinates.x * UP_SCALE, coordinates.y * UP_SCALE, radius);
|
||||||
|
|
||||||
// Change fill color when text is hovered
|
|
||||||
if (isTextHovered) {
|
if (isTextHovered) {
|
||||||
g.fill({ color: 0x00aaff }); // Highlight color when hovered
|
g.fill({ color: 0x00aaff });
|
||||||
g.stroke({ color: 0xffffff, width: strokeWidth + 1 }); // Brighter outline when hovered
|
g.stroke({ color: 0xffffff, width: strokeWidth + 1 });
|
||||||
} else {
|
} else {
|
||||||
g.fill({ color: PATH_COLOR });
|
g.fill({ color: PATH_COLOR });
|
||||||
g.stroke({ color: BACKGROUND_COLOR, width: strokeWidth });
|
g.stroke({ color: BACKGROUND_COLOR, width: strokeWidth });
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ const TransformContext = createContext<{
|
|||||||
setScaleAtCenter: () => {},
|
setScaleAtCenter: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Provider component
|
|
||||||
export const TransformProvider = ({ children }: { children: ReactNode }) => {
|
export const TransformProvider = ({ children }: { children: ReactNode }) => {
|
||||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||||
const [scale, setScale] = useState(1);
|
const [scale, setScale] = useState(1);
|
||||||
@@ -59,12 +58,10 @@ export const TransformProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
|
|
||||||
const screenToLocal = useCallback(
|
const screenToLocal = useCallback(
|
||||||
(screenX: number, screenY: number) => {
|
(screenX: number, screenY: number) => {
|
||||||
// Translate point relative to current pan position
|
|
||||||
const translatedX = (screenX - position.x) / scale;
|
const translatedX = (screenX - position.x) / scale;
|
||||||
const translatedY = (screenY - position.y) / scale;
|
const translatedY = (screenY - position.y) / scale;
|
||||||
|
|
||||||
// Rotate point around center
|
const cosRotation = Math.cos(-rotation);
|
||||||
const cosRotation = Math.cos(-rotation); // Negative rotation to reverse transform
|
|
||||||
const sinRotation = Math.sin(-rotation);
|
const sinRotation = Math.sin(-rotation);
|
||||||
const rotatedX = translatedX * cosRotation - translatedY * sinRotation;
|
const rotatedX = translatedX * cosRotation - translatedY * sinRotation;
|
||||||
const rotatedY = translatedX * sinRotation + translatedY * cosRotation;
|
const rotatedY = translatedX * sinRotation + translatedY * cosRotation;
|
||||||
@@ -77,7 +74,6 @@ export const TransformProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
[position.x, position.y, scale, rotation]
|
[position.x, position.y, scale, rotation]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Inverse of screenToLocal
|
|
||||||
const localToScreen = useCallback(
|
const localToScreen = useCallback(
|
||||||
(localX: number, localY: number) => {
|
(localX: number, localY: number) => {
|
||||||
const upscaledX = localX * UP_SCALE;
|
const upscaledX = localX * UP_SCALE;
|
||||||
@@ -120,7 +116,6 @@ export const TransformProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
(currentFromPosition.x - center.x) * sinDelta,
|
(currentFromPosition.x - center.x) * sinDelta,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update both rotation and position in a single batch to avoid stale closure
|
|
||||||
setRotation(to);
|
setRotation(to);
|
||||||
setPosition(newPosition);
|
setPosition(newPosition);
|
||||||
},
|
},
|
||||||
@@ -150,13 +145,11 @@ export const TransformProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
const cosRot = Math.cos(selectedRotation);
|
const cosRot = Math.cos(selectedRotation);
|
||||||
const sinRot = Math.sin(selectedRotation);
|
const sinRot = Math.sin(selectedRotation);
|
||||||
|
|
||||||
// Translate point relative to center, rotate, then translate back
|
|
||||||
const dx = newPosition.x;
|
const dx = newPosition.x;
|
||||||
const dy = newPosition.y;
|
const dy = newPosition.y;
|
||||||
newPosition.x = dx * cosRot - dy * sinRot + center.x;
|
newPosition.x = dx * cosRot - dy * sinRot + center.x;
|
||||||
newPosition.y = dx * sinRot + dy * cosRot + center.y;
|
newPosition.y = dx * sinRot + dy * cosRot + center.y;
|
||||||
|
|
||||||
// Batch state updates to avoid intermediate renders
|
|
||||||
setPosition(newPosition);
|
setPosition(newPosition);
|
||||||
setRotation(selectedRotation);
|
setRotation(selectedRotation);
|
||||||
setScale(selectedScale);
|
setScale(selectedScale);
|
||||||
@@ -184,7 +177,6 @@ export const TransformProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const setScaleOnly = useCallback((newScale: number) => {
|
const setScaleOnly = useCallback((newScale: number) => {
|
||||||
// Изменяем только масштаб, не трогая позицию и поворот
|
|
||||||
setScale(newScale);
|
setScale(newScale);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -237,7 +229,6 @@ export const TransformProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Custom hook for easy access to transform values
|
|
||||||
export const useTransform = () => {
|
export const useTransform = () => {
|
||||||
const context = useContext(TransformContext);
|
const context = useContext(TransformContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useRef, useEffect, useState } from "react";
|
import { useRef, useEffect, useState } from "react";
|
||||||
import { Widgets } from "./Widgets";
|
import { Widgets } from "./Widgets";
|
||||||
import { Application, extend } from "@pixi/react";
|
import { extend } from "@pixi/react";
|
||||||
import {
|
import {
|
||||||
Container,
|
Container,
|
||||||
Graphics,
|
Graphics,
|
||||||
@@ -9,24 +9,18 @@ import {
|
|||||||
TilingSprite,
|
TilingSprite,
|
||||||
Text,
|
Text,
|
||||||
} from "pixi.js";
|
} from "pixi.js";
|
||||||
import { Stack } from "@mui/material";
|
import { Box, Stack } from "@mui/material";
|
||||||
import { MapDataProvider, useMapData } from "./MapDataContext";
|
import { MapDataProvider, useMapData } from "./MapDataContext";
|
||||||
import { TransformProvider, useTransform } from "./TransformContext";
|
import { TransformProvider, useTransform } from "./TransformContext";
|
||||||
import { InfiniteCanvas } from "./InfiniteCanvas";
|
|
||||||
|
|
||||||
import { TravelPath } from "./TravelPath";
|
|
||||||
import { LeftSidebar } from "./LeftSidebar";
|
import { LeftSidebar } from "./LeftSidebar";
|
||||||
import { RightSidebar } from "./RightSidebar";
|
import { RightSidebar } from "./RightSidebar";
|
||||||
|
|
||||||
import { coordinatesToLocal } from "./utils";
|
import { coordinatesToLocal } from "./utils";
|
||||||
import { LanguageSwitcher } from "@widgets";
|
|
||||||
import { languageStore } from "@shared";
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Sight } from "./Sight";
|
|
||||||
import { SightData } from "./types";
|
|
||||||
import { Station } from "./Station";
|
|
||||||
import { UP_SCALE } from "./Constants";
|
import { UP_SCALE } from "./Constants";
|
||||||
import CircularProgress from "@mui/material/CircularProgress";
|
import { WebGLRouteMapPrototype } from "./webgl-prototype/WebGLRouteMapPrototype";
|
||||||
|
import { CircularProgress } from "@mui/material";
|
||||||
|
|
||||||
extend({
|
extend({
|
||||||
Container,
|
Container,
|
||||||
@@ -42,7 +36,7 @@ const Loading = () => {
|
|||||||
|
|
||||||
if (isRouteLoading || isStationLoading || isSightLoading) {
|
if (isRouteLoading || isStationLoading || isSightLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="fixed flex z-1 items-center justify-center h-screen w-screen bg-[#111]">
|
<div className="fixed flex z-1000000000 items-center justify-center h-screen w-screen bg-[#111]">
|
||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -51,14 +45,33 @@ const Loading = () => {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
export const RoutePreview = () => {
|
export const RoutePreview = () => {
|
||||||
const { routeData, stationData, sightData } = useMapData();
|
const [isLeftSidebarOpen, setIsLeftSidebarOpen] = useState(true);
|
||||||
return (
|
return (
|
||||||
<MapDataProvider>
|
<MapDataProvider>
|
||||||
<TransformProvider>
|
<TransformProvider>
|
||||||
<Stack direction="row" height="100vh" width="100vw" overflow="hidden">
|
<Stack direction="row" height="100vh" width="100vw" overflow="hidden">
|
||||||
{routeData && stationData && sightData ? <LanguageSwitcher /> : null}
|
|
||||||
<Loading />
|
<Loading />
|
||||||
<LeftSidebar />
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "relative",
|
||||||
|
width: isLeftSidebarOpen ? 300 : 0,
|
||||||
|
transition: "width 0.3s ease",
|
||||||
|
overflow: "visible",
|
||||||
|
height: "100%",
|
||||||
|
bgcolor: "primary.main",
|
||||||
|
borderRight: isLeftSidebarOpen
|
||||||
|
? "1px solid rgba(255,255,255,0.08)"
|
||||||
|
: "none",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LeftSidebar
|
||||||
|
open={isLeftSidebarOpen}
|
||||||
|
onToggle={() => setIsLeftSidebarOpen((prev) => !prev)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
<Stack direction="row" flex={1} position="relative" height="100%">
|
<Stack direction="row" flex={1} position="relative" height="100%">
|
||||||
<RouteMap />
|
<RouteMap />
|
||||||
<Widgets />
|
<Widgets />
|
||||||
@@ -71,15 +84,8 @@ export const RoutePreview = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const RouteMap = observer(() => {
|
export const RouteMap = observer(() => {
|
||||||
const { language } = languageStore;
|
|
||||||
const { setPosition, setTransform, screenCenter } = useTransform();
|
const { setPosition, setTransform, screenCenter } = useTransform();
|
||||||
const {
|
const { routeData, stationData, sightData, originalRouteData } = useMapData();
|
||||||
routeData,
|
|
||||||
stationData,
|
|
||||||
sightData,
|
|
||||||
originalRouteData,
|
|
||||||
originalSightData,
|
|
||||||
} = useMapData();
|
|
||||||
|
|
||||||
const [points, setPoints] = useState<{ x: number; y: number }[]>([]);
|
const [points, setPoints] = useState<{ x: number; y: number }[]>([]);
|
||||||
const [isSetup, setIsSetup] = useState(false);
|
const [isSetup, setIsSetup] = useState(false);
|
||||||
@@ -165,8 +171,7 @@ export const RouteMap = observer(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width: "100%", height: "100%" }} ref={parentRef}>
|
<div style={{ width: "100%", height: "100%" }} ref={parentRef}>
|
||||||
<LanguageSwitcher />
|
{/* <Application resizeTo={parentRef} background="#000" preference="webgl">
|
||||||
<Application resizeTo={parentRef} background="#fff" preference="webgl">
|
|
||||||
<InfiniteCanvas>
|
<InfiniteCanvas>
|
||||||
<TravelPath points={points} />
|
<TravelPath points={points} />
|
||||||
{stationData[language].map((obj, index) => (
|
{stationData[language].map((obj, index) => (
|
||||||
@@ -184,7 +189,8 @@ export const RouteMap = observer(() => {
|
|||||||
return <Sight sight={sight} id={index} key={sight.id} />;
|
return <Sight sight={sight} id={index} key={sight.id} />;
|
||||||
})}
|
})}
|
||||||
</InfiniteCanvas>
|
</InfiniteCanvas>
|
||||||
</Application>
|
</Application> */}
|
||||||
|
<WebGLRouteMapPrototype />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ export interface RouteData {
|
|||||||
carrier_id: number;
|
carrier_id: number;
|
||||||
center_latitude: number;
|
center_latitude: number;
|
||||||
center_longitude: number;
|
center_longitude: number;
|
||||||
|
icon_size: number;
|
||||||
|
font_size: number;
|
||||||
governor_appeal: number;
|
governor_appeal: number;
|
||||||
id: number;
|
id: number;
|
||||||
path: [number, number][];
|
path: [number, number][];
|
||||||
|
|||||||
203
src/pages/Route/route-preview/web-gl/LanguageSelector.tsx
Normal file
203
src/pages/Route/route-preview/web-gl/LanguageSelector.tsx
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
import { useEffect, useRef, useState, type ReactElement } from "react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { languageStore } from "@shared";
|
||||||
|
|
||||||
|
const LANGUAGES = ["ru", "zh", "en"] as const;
|
||||||
|
type Language = (typeof LANGUAGES)[number];
|
||||||
|
|
||||||
|
type LanguageSelectorProps = {
|
||||||
|
onBack?: () => void;
|
||||||
|
isSidebarOpen?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderLanguageIcon = (lang: Language): ReactElement => {
|
||||||
|
switch (lang) {
|
||||||
|
case "ru":
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
className="h-12 w-12 cursor-pointer"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<rect x="3" y="3" width="42" height="42" rx="21" fill="black" />
|
||||||
|
<path
|
||||||
|
d="M24 0C10.75 0 0 10.75 0 24C0 37.25 10.75 48 24 48C37.25 48 48 37.25 48 24C48 10.75 37.25 0 24 0ZM24.2 33.55H19.92L16.29 26.46H13.11V33.55H9.12V14.18H16.32C18.61 14.18 20.37 14.69 21.62 15.71C22.87 16.73 23.48 18.17 23.48 20.03C23.48 21.35 23.19 22.45 22.62 23.34C22.05 24.22 21.18 24.93 20.02 25.45L24.21 33.37V33.56L24.2 33.55ZM40.3 26.94C40.3 29.06 39.64 30.74 38.31 31.97C36.98 33.2 35.17 33.82 32.87 33.82C30.57 33.82 28.81 33.22 27.48 32.02C26.15 30.82 25.47 29.18 25.44 27.08V14.18H29.43V26.97C29.43 28.24 29.73 29.16 30.34 29.74C30.95 30.32 31.79 30.61 32.86 30.61C35.1 30.61 36.24 29.43 36.28 27.07V14.18H40.28V26.94H40.3Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M16.3086 17.4099H13.0986V23.2199H16.3186C17.3186 23.2199 18.0986 22.9599 18.6486 22.4499C19.1986 21.9399 19.4686 21.2399 19.4686 20.3399C19.4686 19.4399 19.2086 18.7099 18.6886 18.1799C18.1686 17.6499 17.3686 17.3899 16.2986 17.3899L16.3086 17.4099Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
case "zh":
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
className="h-12 w-12 cursor-pointer"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<rect x="3" y="3" width="42" height="42" rx="21" fill="black" />
|
||||||
|
<path d="M10.287 20.382H6.291V24.147H10.287V20.382Z" fill={"white"} />
|
||||||
|
<path
|
||||||
|
d="M13.704 24.147H17.721V20.382H13.704V24.147Z"
|
||||||
|
fill={"white"}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M36.1254 20.046H29.8575C30.6606 21.9406 31.7187 23.6442 33.0513 25.1217C34.3105 23.6927 35.3315 22.0126 36.1254 20.046Z"
|
||||||
|
fill={"white"}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M24 48C37.2548 48 48 37.2548 48 24C48 10.7452 37.2548 0 24 0C10.7452 0 0 10.7452 0 24C0 37.2548 10.7452 48 24 48ZM10.287 13.5H13.704V17.1541H21.117V28.446H17.721V27.375H13.704V33.969H10.287V27.375H6.291V28.5511H3V17.1541H10.287V13.5ZM31.35 13.5H34.704V16.8181H43.083V20.046H39.8887C38.804 22.9506 37.3746 25.3834 35.581 27.4065C37.6488 28.9237 40.1651 30.0542 43.1682 30.7291L43.8469 30.8817L43.3465 31.3649C42.7753 31.9162 41.9777 33.0771 41.5886 33.7939L41.4484 34.0521L41.1642 33.9778C37.8385 33.1088 35.1249 31.7521 32.8974 29.9253C30.6296 31.6954 27.9389 33.0335 24.802 34.015L24.4889 34.1129L24.3502 33.8156C24.0724 33.2203 23.2933 32.029 22.8051 31.439L22.4307 30.9868L22.9986 30.8373C25.936 30.0648 28.4025 28.9702 30.4373 27.4935C28.6775 25.4061 27.319 22.9142 26.2412 20.046H23.097V16.8181H31.35V13.5Z"
|
||||||
|
fill={"white"}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
case "en":
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
className="h-12 w-12 cursor-pointer"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<rect x="3" y="3" width="42" height="42" rx="21" fill="black" />
|
||||||
|
<path
|
||||||
|
d="M24 0C10.75 0 0 10.75 0 24C0 37.25 10.75 48 24 48C37.25 48 48 37.25 48 24C48 10.75 37.25 0 24 0ZM21.57 33.79H8.41V14.15H21.55V17.43H12.45V22.11H20.22V25.28H12.45V30.54H21.57V33.79ZM39.54 33.79H35.49L27.61 20.87V33.79H23.56V14.15H27.61L35.5 27.1V14.15H39.53V33.79H39.54Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const CollapsedIcon = () => (
|
||||||
|
<svg
|
||||||
|
className="h-12 w-12 cursor-pointer"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<rect x="4" y="3" width="39" height="42" rx="19.5" fill="black" />
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M0 24C0 10.75 10.75 0 24 0C37.25 0 48 10.75 48 24C48 37.25 37.25 48 24 48C10.75 48 0 37.25 0 24ZM36.05 19.41L38.27 25.47L40.47 19.39L39.17 19.77C39.16 19.75 39.16 19.35 39.16 19.32C38.58 12.41 32.99 7.15 25.94 7.15C25.42 7.15 24.99 7.58 24.99 8.1C24.99 8.62 25.42 9.05 25.94 9.05C31.92 9.05 36.68 13.47 37.26 19.31C37.26 19.325 37.2625 19.435 37.265 19.545C37.2675 19.655 37.27 19.765 37.27 19.78L36.05 19.41ZM8.90375 27.5369C11.3535 26.9568 13.6332 25.8438 15.5701 24.2838L15.5709 24.2846C17.2124 25.8526 19.2497 26.9784 21.4812 27.5521C21.5875 27.5649 21.6945 27.5649 21.8007 27.5521C22.3194 27.6074 22.8298 27.3911 23.1385 26.9848C23.448 26.5786 23.5086 26.0441 23.2986 25.5826C23.0879 25.1211 22.6389 24.803 22.1202 24.7477C20.4804 24.3182 18.9808 23.4937 17.7634 22.3495C20.2489 20.0131 22.1036 17.1245 23.1659 13.9363C23.2729 13.5077 23.165 13.0566 22.8754 12.716C22.6016 12.3627 22.1709 12.1552 21.7136 12.1552H16.6307V10.402C16.6307 9.90121 16.3535 9.43889 15.9045 9.1881C15.4556 8.9373 14.9012 8.9373 14.4522 9.1881C14.0033 9.43809 13.7261 9.90121 13.7261 10.402V12.1824H8.64317C8.12367 12.1824 7.64484 12.45 7.38509 12.8835C7.12534 13.317 7.12534 13.8522 7.38509 14.2857C7.64484 14.7192 8.1245 14.9868 8.64317 14.9868H19.6655C18.6804 16.9971 17.3443 18.828 15.7153 20.3993C14.8373 19.3977 14.0688 18.3128 13.4207 17.1598C13.2747 16.7896 12.9734 16.4972 12.5909 16.3529C12.2083 16.2087 11.7809 16.2279 11.4141 16.405C11.0473 16.5821 10.7751 16.901 10.6647 17.2824C10.5544 17.6638 10.6166 18.0724 10.8357 18.4074C11.6058 19.7423 12.5153 20.997 13.551 22.1516C12.0207 23.3728 10.2307 24.2533 8.30873 24.7317C7.92284 24.7709 7.56932 24.956 7.32617 25.2469C7.08219 25.5369 6.96767 25.9095 7.00833 26.2813C7.04899 26.6539 7.24069 26.9952 7.54193 27.23C7.84235 27.4656 8.22824 27.5761 8.61329 27.5369C8.70956 27.5513 8.80748 27.5513 8.90375 27.5369ZM34.9002 38.8803C35.2512 39.0301 35.6487 39.0397 36.0072 38.9067C36.3815 38.7865 36.6886 38.5237 36.8587 38.18C37.0288 37.8362 37.0462 37.4404 36.9077 37.0839L30.3434 20.7767C30.238 20.5131 30.0529 20.2863 29.8115 20.1261C29.57 19.9658 29.2845 19.8793 28.9924 19.8785C28.7019 19.8785 28.4173 19.9626 28.1766 20.1196C27.9359 20.2775 27.7501 20.501 27.6422 20.7623L21.136 36.523C20.9443 36.9885 21.024 37.5181 21.346 37.9116C21.668 38.305 22.1825 38.5029 22.6962 38.4308C23.2107 38.3579 23.6455 38.0269 23.8372 37.5606L25.4057 33.6489H32.3185L34.1342 38.1079C34.2737 38.4524 34.5492 38.7304 34.9002 38.8803ZM28.9052 25.1652L31.1857 30.8445H31.1849H26.5667L28.9052 25.1652Z"
|
||||||
|
fill="#FFFFFF"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ArrowIcon = ({ rotation }: { rotation: number }) => (
|
||||||
|
<svg
|
||||||
|
style={{
|
||||||
|
transform: `rotate(${rotation}deg)`,
|
||||||
|
transition: "transform 0.15s ease",
|
||||||
|
}}
|
||||||
|
className="h-12 w-12"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<rect x="8" y="7" width="31" height="33" fill="black" />
|
||||||
|
<path
|
||||||
|
d="M24.0001 0C10.7501 0 0.00012207 10.75 0.00012207 24C0.00012207 37.25 10.7501 48 24.0001 48C37.2501 48 48.0001 37.25 48.0001 24C48.0001 10.75 37.2501 0 24.0001 0ZM37.5401 25.84C37.5401 26.4 37.0901 26.85 36.5301 26.85H20.5901C20.1401 26.85 19.9201 27.39 20.2301 27.71L27.6801 35.16C28.0801 35.56 28.0801 36.2 27.6801 36.59L25.0801 39.19C24.6801 39.59 24.0401 39.59 23.6501 39.19L12.4901 28.03L9.17012 24.71C8.77012 24.31 8.77012 23.67 9.17012 23.28L12.4901 19.96L23.6501 8.8C24.0501 8.4 24.6901 8.4 25.0801 8.8L27.6801 11.4C28.0801 11.8 28.0801 12.44 27.6801 12.83L20.2301 20.28C19.9101 20.6 20.1401 21.14 20.5901 21.14H36.5301C37.0901 21.14 37.5401 21.59 37.5401 22.15V25.82V25.84Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const LanguageSelector = observer(
|
||||||
|
({ onBack, isSidebarOpen = true }: LanguageSelectorProps) => {
|
||||||
|
const { setLanguage } = languageStore;
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOutside = (event: PointerEvent) => {
|
||||||
|
if (
|
||||||
|
containerRef.current &&
|
||||||
|
!containerRef.current.contains(event.target as Node)
|
||||||
|
) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("pointerdown", handleOutside);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("pointerdown", handleOutside);
|
||||||
|
};
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const handleSelect = (code: Language) => {
|
||||||
|
setLanguage(code);
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggle = () => setIsOpen((prev) => !prev);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="pointer-events-auto"
|
||||||
|
style={{
|
||||||
|
width: "500px",
|
||||||
|
transition: "width 0.25s ease",
|
||||||
|
flex: "0 0 auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex items-center gap-2 ">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
onBack?.();
|
||||||
|
}}
|
||||||
|
className="flex h-12 w-12 items-center justify-center"
|
||||||
|
aria-label={
|
||||||
|
isOpen ? "Скрыть выбор языка" : "Показать выбор языка"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ArrowIcon rotation={isSidebarOpen ? 0 : 180} />
|
||||||
|
</button>
|
||||||
|
{isOpen ? (
|
||||||
|
LANGUAGES.map((lang) => (
|
||||||
|
<button
|
||||||
|
key={lang}
|
||||||
|
type="button"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
handleSelect(lang);
|
||||||
|
}}
|
||||||
|
className="flex h-12 w-12 items-center justify-center"
|
||||||
|
aria-label={`Переключить язык на ${lang.toUpperCase()}`}
|
||||||
|
>
|
||||||
|
{renderLanguageIcon(lang)}
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="flex h-12 w-12 items-center justify-center"
|
||||||
|
onClick={toggle}
|
||||||
|
>
|
||||||
|
<CollapsedIcon />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default LanguageSelector;
|
||||||
File diff suppressed because it is too large
Load Diff
627
src/pages/Sight/LinkedStations.tsx
Normal file
627
src/pages/Sight/LinkedStations.tsx
Normal file
@@ -0,0 +1,627 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import {
|
||||||
|
Stack,
|
||||||
|
Typography,
|
||||||
|
Accordion,
|
||||||
|
AccordionSummary,
|
||||||
|
AccordionDetails,
|
||||||
|
useTheme,
|
||||||
|
TextField,
|
||||||
|
Autocomplete,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
Table,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
Paper,
|
||||||
|
TableBody,
|
||||||
|
Checkbox,
|
||||||
|
FormControlLabel,
|
||||||
|
Tabs,
|
||||||
|
Tab,
|
||||||
|
Box,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AnimatedCircleButton,
|
||||||
|
authInstance,
|
||||||
|
languageStore,
|
||||||
|
selectedCityStore,
|
||||||
|
} from "@shared";
|
||||||
|
|
||||||
|
type Field<T> = {
|
||||||
|
label: string;
|
||||||
|
data: keyof T;
|
||||||
|
render?: (value: any) => React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
type LinkedStationsProps<T> = {
|
||||||
|
parentId: string | number;
|
||||||
|
fields: Field<T>[];
|
||||||
|
setItemsParent?: (items: T[]) => void;
|
||||||
|
type: "show" | "edit";
|
||||||
|
onUpdate?: () => void;
|
||||||
|
disableCreation?: boolean;
|
||||||
|
updatedLinkedItems?: T[];
|
||||||
|
refresh?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LinkedStations = <
|
||||||
|
T extends { id: number; name: string; [key: string]: any }
|
||||||
|
>(
|
||||||
|
props: LinkedStationsProps<T>
|
||||||
|
) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Accordion sx={{ width: "100%" }}>
|
||||||
|
<AccordionSummary
|
||||||
|
expandIcon={<ExpandMoreIcon />}
|
||||||
|
sx={{
|
||||||
|
background: theme.palette.background.paper,
|
||||||
|
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="subtitle1" fontWeight="bold">
|
||||||
|
Привязанные остановки
|
||||||
|
</Typography>
|
||||||
|
</AccordionSummary>
|
||||||
|
|
||||||
|
<AccordionDetails
|
||||||
|
sx={{ background: theme.palette.background.paper, width: "100%" }}
|
||||||
|
>
|
||||||
|
<Stack gap={2} width="100%">
|
||||||
|
<LinkedStationsContents {...props} />
|
||||||
|
</Stack>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const LinkedStationsContentsInner = <
|
||||||
|
T extends { id: number; name: string; [key: string]: any }
|
||||||
|
>({
|
||||||
|
parentId,
|
||||||
|
setItemsParent,
|
||||||
|
fields,
|
||||||
|
type,
|
||||||
|
onUpdate,
|
||||||
|
disableCreation = false,
|
||||||
|
updatedLinkedItems,
|
||||||
|
refresh,
|
||||||
|
}: LinkedStationsProps<T>) => {
|
||||||
|
const { language } = languageStore;
|
||||||
|
|
||||||
|
const [allItems, setAllItems] = useState<T[]>([]);
|
||||||
|
const [linkedItems, setLinkedItems] = useState<T[]>([]);
|
||||||
|
const [selectedItemId, setSelectedItemId] = useState<number | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
|
const [selectedItems, setSelectedItems] = useState<Set<number>>(new Set());
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [selectedToDetach, setSelectedToDetach] = useState<Set<number>>(
|
||||||
|
new Set()
|
||||||
|
);
|
||||||
|
const [isLinkingSingle, setIsLinkingSingle] = useState(false);
|
||||||
|
const [isLinkingBulk, setIsLinkingBulk] = useState(false);
|
||||||
|
const [isBulkDetaching, setIsBulkDetaching] = useState(false);
|
||||||
|
const [detachingIds, setDetachingIds] = useState<Set<number>>(new Set());
|
||||||
|
|
||||||
|
useEffect(() => {}, [error]);
|
||||||
|
|
||||||
|
const parentResource = "sight";
|
||||||
|
const childResource = "station";
|
||||||
|
|
||||||
|
const availableItems = allItems
|
||||||
|
.filter((item) => !linkedItems.some((linked) => linked.id === item.id))
|
||||||
|
.filter((item) => {
|
||||||
|
const selectedCityId = selectedCityStore.selectedCityId;
|
||||||
|
if (selectedCityId && "city_id" in item) {
|
||||||
|
return item.city_id === selectedCityId;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
|
const filteredAvailableItems = availableItems.filter((item) => {
|
||||||
|
if (!searchQuery.trim()) return true;
|
||||||
|
return String(item.name).toLowerCase().includes(searchQuery.toLowerCase());
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (updatedLinkedItems) {
|
||||||
|
setLinkedItems(updatedLinkedItems);
|
||||||
|
}
|
||||||
|
}, [updatedLinkedItems]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setItemsParent?.(linkedItems);
|
||||||
|
}, [linkedItems, setItemsParent]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedToDetach((prev) => {
|
||||||
|
const updated = new Set<number>();
|
||||||
|
linkedItems.forEach((item) => {
|
||||||
|
if (prev.has(item.id)) {
|
||||||
|
updated.add(item.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
}, [linkedItems]);
|
||||||
|
|
||||||
|
const linkItem = () => {
|
||||||
|
if (selectedItemId !== null) {
|
||||||
|
setError(null);
|
||||||
|
const requestData = {
|
||||||
|
station_id: selectedItemId,
|
||||||
|
};
|
||||||
|
|
||||||
|
setIsLinkingSingle(true);
|
||||||
|
authInstance
|
||||||
|
.post(`/${parentResource}/${parentId}/${childResource}`, requestData)
|
||||||
|
.then(() => {
|
||||||
|
const newItem = allItems.find((item) => item.id === selectedItemId);
|
||||||
|
if (newItem) {
|
||||||
|
setLinkedItems([...linkedItems, newItem]);
|
||||||
|
}
|
||||||
|
setSelectedItemId(null);
|
||||||
|
onUpdate?.();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error linking station:", error);
|
||||||
|
setError("Failed to link station");
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLinkingSingle(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteItem = (itemId: number) => {
|
||||||
|
setError(null);
|
||||||
|
setDetachingIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.add(itemId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
authInstance
|
||||||
|
.delete(`/${parentResource}/${parentId}/${childResource}`, {
|
||||||
|
data: { [`${childResource}_id`]: itemId },
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
setLinkedItems(linkedItems.filter((item) => item.id !== itemId));
|
||||||
|
onUpdate?.();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error deleting station:", error);
|
||||||
|
setError("Failed to delete station");
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setDetachingIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.delete(itemId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCheckboxChange = (itemId: number) => {
|
||||||
|
const updated = new Set(selectedItems);
|
||||||
|
if (updated.has(itemId)) {
|
||||||
|
updated.delete(itemId);
|
||||||
|
} else {
|
||||||
|
updated.add(itemId);
|
||||||
|
}
|
||||||
|
setSelectedItems(updated);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBulkLink = async () => {
|
||||||
|
if (selectedItems.size === 0) return;
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
setIsLinkingBulk(true);
|
||||||
|
const idsToLink = Array.from(selectedItems);
|
||||||
|
const linkedIds: number[] = [];
|
||||||
|
const failedIds: number[] = [];
|
||||||
|
|
||||||
|
for (const id of idsToLink) {
|
||||||
|
try {
|
||||||
|
await authInstance.post(`/${parentResource}/${parentId}/${childResource}`, {
|
||||||
|
station_id: id,
|
||||||
|
});
|
||||||
|
linkedIds.push(id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error linking station:", error);
|
||||||
|
failedIds.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (linkedIds.length > 0) {
|
||||||
|
const newItems = allItems.filter((item) => linkedIds.includes(item.id));
|
||||||
|
setLinkedItems((prev) => {
|
||||||
|
const existingIds = new Set(prev.map((item) => item.id));
|
||||||
|
const additions = newItems.filter((item) => !existingIds.has(item.id));
|
||||||
|
return [...prev, ...additions];
|
||||||
|
});
|
||||||
|
onUpdate?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedItems((prev) => {
|
||||||
|
if (linkedIds.length === 0) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
const remaining = new Set(prev);
|
||||||
|
linkedIds.forEach((id) => remaining.delete(id));
|
||||||
|
return failedIds.length > 0 ? remaining : new Set();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (failedIds.length > 0) {
|
||||||
|
setError(
|
||||||
|
failedIds.length === idsToLink.length
|
||||||
|
? "Failed to link stations"
|
||||||
|
: "Some stations failed to link"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLinkingBulk(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleDetachSelection = (itemId: number) => {
|
||||||
|
const updated = new Set(selectedToDetach);
|
||||||
|
if (updated.has(itemId)) {
|
||||||
|
updated.delete(itemId);
|
||||||
|
} else {
|
||||||
|
updated.add(itemId);
|
||||||
|
}
|
||||||
|
setSelectedToDetach(updated);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleAllDetach = (checked: boolean) => {
|
||||||
|
if (!checked) {
|
||||||
|
setSelectedToDetach(new Set());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSelectedToDetach(new Set(linkedItems.map((item) => item.id)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBulkDetach = async () => {
|
||||||
|
const idsToDetach = Array.from(selectedToDetach);
|
||||||
|
if (idsToDetach.length === 0) return;
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
setIsBulkDetaching(true);
|
||||||
|
setDetachingIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
idsToDetach.forEach((id) => next.add(id));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
|
||||||
|
const detachedIds: number[] = [];
|
||||||
|
const failedIds: number[] = [];
|
||||||
|
|
||||||
|
for (const itemId of idsToDetach) {
|
||||||
|
try {
|
||||||
|
await authInstance.delete(`/${parentResource}/${parentId}/${childResource}`, {
|
||||||
|
data: { [`${childResource}_id`]: itemId },
|
||||||
|
});
|
||||||
|
detachedIds.push(itemId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting station:", error);
|
||||||
|
failedIds.push(itemId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (detachedIds.length > 0) {
|
||||||
|
setLinkedItems((prev) =>
|
||||||
|
prev.filter((item) => !detachedIds.includes(item.id))
|
||||||
|
);
|
||||||
|
setSelectedToDetach((prev) => {
|
||||||
|
const remaining = new Set(prev);
|
||||||
|
detachedIds.forEach((id) => remaining.delete(id));
|
||||||
|
return failedIds.length > 0 ? remaining : new Set();
|
||||||
|
});
|
||||||
|
onUpdate?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failedIds.length > 0) {
|
||||||
|
setError(
|
||||||
|
failedIds.length === idsToDetach.length
|
||||||
|
? "Failed to delete stations"
|
||||||
|
: "Some stations failed to delete"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setDetachingIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
idsToDetach.forEach((id) => next.delete(id));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
setIsBulkDetaching(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const allSelectedForDetach =
|
||||||
|
linkedItems.length > 0 &&
|
||||||
|
linkedItems.every((item) => selectedToDetach.has(item.id));
|
||||||
|
const isIndeterminateDetach =
|
||||||
|
selectedToDetach.size > 0 && !allSelectedForDetach;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (parentId) {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
authInstance
|
||||||
|
.get(`/${parentResource}/${parentId}/${childResource}`)
|
||||||
|
.then((response) => {
|
||||||
|
setLinkedItems(response?.data || []);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error fetching linked stations:", error);
|
||||||
|
setError("Failed to load linked stations");
|
||||||
|
setLinkedItems([]);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [parentId, language, refresh]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (type === "edit") {
|
||||||
|
setError(null);
|
||||||
|
authInstance
|
||||||
|
.get(`/${childResource}`)
|
||||||
|
.then((response) => {
|
||||||
|
setAllItems(response?.data || []);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error fetching all stations:", error);
|
||||||
|
setError("Failed to load available stations");
|
||||||
|
setAllItems([]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [type]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{linkedItems?.length > 0 && (
|
||||||
|
<TableContainer component={Paper} sx={{ width: "100%" }}>
|
||||||
|
<Table sx={{ width: "100%" }}>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
{type === "edit" && (
|
||||||
|
<TableCell width="50px">
|
||||||
|
<Checkbox
|
||||||
|
size="small"
|
||||||
|
checked={allSelectedForDetach}
|
||||||
|
indeterminate={isIndeterminateDetach}
|
||||||
|
onChange={(e) => handleToggleAllDetach(e.target.checked)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
)}
|
||||||
|
<TableCell key="id" width="60px">
|
||||||
|
№
|
||||||
|
</TableCell>
|
||||||
|
{fields.map((field) => (
|
||||||
|
<TableCell key={String(field.data)}>{field.label}</TableCell>
|
||||||
|
))}
|
||||||
|
{type === "edit" && (
|
||||||
|
<TableCell width="120px">Действие</TableCell>
|
||||||
|
)}
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
|
||||||
|
<TableBody>
|
||||||
|
{linkedItems.map((item, index) => (
|
||||||
|
<TableRow key={item.id} hover>
|
||||||
|
{type === "edit" && (
|
||||||
|
<TableCell>
|
||||||
|
<Checkbox
|
||||||
|
size="small"
|
||||||
|
checked={selectedToDetach.has(item.id)}
|
||||||
|
onChange={() => toggleDetachSelection(item.id)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
)}
|
||||||
|
<TableCell>{index + 1}</TableCell>
|
||||||
|
{fields.map((field, idx) => (
|
||||||
|
<TableCell key={String(field.data) + String(idx)}>
|
||||||
|
{field.render
|
||||||
|
? field.render(item[field.data])
|
||||||
|
: item[field.data]}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
{type === "edit" && (
|
||||||
|
<TableCell>
|
||||||
|
<AnimatedCircleButton
|
||||||
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
|
size="small"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
deleteItem(item.id);
|
||||||
|
}}
|
||||||
|
disabled={detachingIds.has(item.id)}
|
||||||
|
loading={detachingIds.has(item.id)}
|
||||||
|
>
|
||||||
|
Отвязать
|
||||||
|
</AnimatedCircleButton>
|
||||||
|
</TableCell>
|
||||||
|
)}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{type === "edit" && linkedItems.length > 0 && (
|
||||||
|
<Stack direction="row" gap={2} mt={2}>
|
||||||
|
<AnimatedCircleButton
|
||||||
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
|
onClick={handleBulkDetach}
|
||||||
|
disabled={selectedToDetach.size === 0 || isBulkDetaching}
|
||||||
|
loading={isBulkDetaching}
|
||||||
|
>
|
||||||
|
Отвязать выбранные ({selectedToDetach.size})
|
||||||
|
</AnimatedCircleButton>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{linkedItems.length === 0 && !isLoading && (
|
||||||
|
<Typography color="textSecondary" textAlign="center" py={2}>
|
||||||
|
Остановки не найдены
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{type === "edit" && !disableCreation && (
|
||||||
|
<Stack gap={2} mt={2}>
|
||||||
|
<Typography variant="subtitle1">Добавить остановки</Typography>
|
||||||
|
<Tabs
|
||||||
|
value={activeTab}
|
||||||
|
onChange={(_, value) => setActiveTab(value)}
|
||||||
|
variant="fullWidth"
|
||||||
|
>
|
||||||
|
<Tab label="По одной" />
|
||||||
|
<Tab label="Массово" />
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<Box sx={{ mt: 1 }}>
|
||||||
|
{activeTab === 0 && (
|
||||||
|
<Stack gap={2}>
|
||||||
|
<Autocomplete
|
||||||
|
fullWidth
|
||||||
|
value={
|
||||||
|
availableItems?.find((item) => item.id === selectedItemId) ||
|
||||||
|
null
|
||||||
|
}
|
||||||
|
onChange={(_, newValue) =>
|
||||||
|
setSelectedItemId(newValue?.id || null)
|
||||||
|
}
|
||||||
|
options={availableItems}
|
||||||
|
getOptionLabel={(item) => String(item.name)}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField {...params} label="Выберите остановку" fullWidth />
|
||||||
|
)}
|
||||||
|
isOptionEqualToValue={(option, value) =>
|
||||||
|
option.id === value?.id
|
||||||
|
}
|
||||||
|
filterOptions={(options, { inputValue }) => {
|
||||||
|
const searchWords = inputValue
|
||||||
|
.toLowerCase()
|
||||||
|
.split(" ")
|
||||||
|
.filter(Boolean);
|
||||||
|
return options.filter((option) => {
|
||||||
|
const optionWords = String(option.name)
|
||||||
|
.toLowerCase()
|
||||||
|
.split(" ");
|
||||||
|
return searchWords.every((searchWord) =>
|
||||||
|
optionWords.some((word) => word.startsWith(searchWord))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
renderOption={(props, option) => (
|
||||||
|
<li {...props} key={option.id}>
|
||||||
|
{String(option.name)}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AnimatedCircleButton
|
||||||
|
variant="contained"
|
||||||
|
onClick={linkItem}
|
||||||
|
disabled={!selectedItemId || isLinkingSingle}
|
||||||
|
loading={isLinkingSingle}
|
||||||
|
sx={{ alignSelf: "flex-start" }}
|
||||||
|
>
|
||||||
|
Добавить
|
||||||
|
</AnimatedCircleButton>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 1 && (
|
||||||
|
<Stack gap={2}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Поиск остановок"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
placeholder="Введите название остановки..."
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Paper sx={{ maxHeight: 300, overflow: "auto", p: 2 }}>
|
||||||
|
<Stack gap={1}>
|
||||||
|
{filteredAvailableItems.map((item) => (
|
||||||
|
<FormControlLabel
|
||||||
|
key={item.id}
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedItems.has(item.id)}
|
||||||
|
onChange={() => handleCheckboxChange(item.id)}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={String(item.name)}
|
||||||
|
sx={{
|
||||||
|
margin: 0,
|
||||||
|
"& .MuiFormControlLabel-label": {
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{filteredAvailableItems.length === 0 && (
|
||||||
|
<Typography
|
||||||
|
color="textSecondary"
|
||||||
|
textAlign="center"
|
||||||
|
py={1}
|
||||||
|
>
|
||||||
|
{searchQuery.trim()
|
||||||
|
? "Остановки не найдены"
|
||||||
|
: "Нет доступных остановок"}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<AnimatedCircleButton
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleBulkLink}
|
||||||
|
disabled={selectedItems.size === 0 || isLinkingBulk}
|
||||||
|
loading={isLinkingBulk}
|
||||||
|
sx={{ alignSelf: "flex-start" }}
|
||||||
|
>
|
||||||
|
Добавить выбранные ({selectedItems.size})
|
||||||
|
</AnimatedCircleButton>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isLoading && (
|
||||||
|
<Typography color="textSecondary" textAlign="center" py={2}>
|
||||||
|
Загрузка...
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Typography color="error" textAlign="center" py={2}>
|
||||||
|
{error}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LinkedStationsContents = observer(
|
||||||
|
LinkedStationsContentsInner
|
||||||
|
) as typeof LinkedStationsContentsInner;
|
||||||
@@ -1 +1,2 @@
|
|||||||
export * from "./SightListPage";
|
export * from "./SightListPage";
|
||||||
|
export { LinkedStations } from "./LinkedStations";
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useState, useEffect } from "react";
|
|||||||
import {
|
import {
|
||||||
Stack,
|
Stack,
|
||||||
Typography,
|
Typography,
|
||||||
Button,
|
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionSummary,
|
AccordionSummary,
|
||||||
AccordionDetails,
|
AccordionDetails,
|
||||||
@@ -16,11 +15,21 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
Paper,
|
Paper,
|
||||||
TableBody,
|
TableBody,
|
||||||
|
Checkbox,
|
||||||
|
FormControlLabel,
|
||||||
|
Tabs,
|
||||||
|
Tab,
|
||||||
|
Box,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||||
|
|
||||||
import { authInstance, languageStore, selectedCityStore } from "@shared";
|
import {
|
||||||
|
AnimatedCircleButton,
|
||||||
|
authInstance,
|
||||||
|
languageStore,
|
||||||
|
selectedCityStore,
|
||||||
|
} from "@shared";
|
||||||
|
|
||||||
type Field<T> = {
|
type Field<T> = {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -93,6 +102,16 @@ const LinkedSightsContentsInner = <
|
|||||||
const [selectedItemId, setSelectedItemId] = useState<number | null>(null);
|
const [selectedItemId, setSelectedItemId] = useState<number | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
|
const [selectedItems, setSelectedItems] = useState<Set<number>>(new Set());
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [selectedToDetach, setSelectedToDetach] = useState<Set<number>>(
|
||||||
|
new Set()
|
||||||
|
);
|
||||||
|
const [isLinkingSingle, setIsLinkingSingle] = useState(false);
|
||||||
|
const [isLinkingBulk, setIsLinkingBulk] = useState(false);
|
||||||
|
const [isBulkDetaching, setIsBulkDetaching] = useState(false);
|
||||||
|
const [detachingIds, setDetachingIds] = useState<Set<number>>(new Set());
|
||||||
|
|
||||||
useEffect(() => {}, [error]);
|
useEffect(() => {}, [error]);
|
||||||
|
|
||||||
@@ -111,6 +130,11 @@ const LinkedSightsContentsInner = <
|
|||||||
})
|
})
|
||||||
.sort((a, b) => a.name.localeCompare(b.name));
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
|
const filteredAvailableItems = availableItems.filter((item) => {
|
||||||
|
if (!searchQuery.trim()) return true;
|
||||||
|
return String(item.name).toLowerCase().includes(searchQuery.toLowerCase());
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (updatedLinkedItems) {
|
if (updatedLinkedItems) {
|
||||||
setLinkedItems(updatedLinkedItems);
|
setLinkedItems(updatedLinkedItems);
|
||||||
@@ -121,6 +145,18 @@ const LinkedSightsContentsInner = <
|
|||||||
setItemsParent?.(linkedItems);
|
setItemsParent?.(linkedItems);
|
||||||
}, [linkedItems, setItemsParent]);
|
}, [linkedItems, setItemsParent]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedToDetach((prev) => {
|
||||||
|
const updated = new Set<number>();
|
||||||
|
linkedItems.forEach((item) => {
|
||||||
|
if (prev.has(item.id)) {
|
||||||
|
updated.add(item.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
}, [linkedItems]);
|
||||||
|
|
||||||
const linkItem = () => {
|
const linkItem = () => {
|
||||||
if (selectedItemId !== null) {
|
if (selectedItemId !== null) {
|
||||||
setError(null);
|
setError(null);
|
||||||
@@ -128,6 +164,7 @@ const LinkedSightsContentsInner = <
|
|||||||
sight_id: selectedItemId,
|
sight_id: selectedItemId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
setIsLinkingSingle(true);
|
||||||
authInstance
|
authInstance
|
||||||
.post(`/${parentResource}/${parentId}/${childResource}`, requestData)
|
.post(`/${parentResource}/${parentId}/${childResource}`, requestData)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -141,12 +178,20 @@ const LinkedSightsContentsInner = <
|
|||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("Error linking sight:", error);
|
console.error("Error linking sight:", error);
|
||||||
setError("Failed to link sight");
|
setError("Failed to link sight");
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLinkingSingle(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteItem = (itemId: number) => {
|
const deleteItem = (itemId: number) => {
|
||||||
setError(null);
|
setError(null);
|
||||||
|
setDetachingIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.add(itemId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
authInstance
|
authInstance
|
||||||
.delete(`/${parentResource}/${parentId}/${childResource}`, {
|
.delete(`/${parentResource}/${parentId}/${childResource}`, {
|
||||||
data: { [`${childResource}_id`]: itemId },
|
data: { [`${childResource}_id`]: itemId },
|
||||||
@@ -158,9 +203,162 @@ const LinkedSightsContentsInner = <
|
|||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("Error deleting sight:", error);
|
console.error("Error deleting sight:", error);
|
||||||
setError("Failed to delete sight");
|
setError("Failed to delete sight");
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setDetachingIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.delete(itemId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCheckboxChange = (itemId: number) => {
|
||||||
|
const updated = new Set(selectedItems);
|
||||||
|
if (updated.has(itemId)) {
|
||||||
|
updated.delete(itemId);
|
||||||
|
} else {
|
||||||
|
updated.add(itemId);
|
||||||
|
}
|
||||||
|
setSelectedItems(updated);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBulkLink = async () => {
|
||||||
|
if (selectedItems.size === 0) return;
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
setIsLinkingBulk(true);
|
||||||
|
const idsToLink = Array.from(selectedItems);
|
||||||
|
const linkedIds: number[] = [];
|
||||||
|
const failedIds: number[] = [];
|
||||||
|
|
||||||
|
for (const id of idsToLink) {
|
||||||
|
try {
|
||||||
|
await authInstance.post(
|
||||||
|
`/${parentResource}/${parentId}/${childResource}`,
|
||||||
|
{
|
||||||
|
sight_id: id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
linkedIds.push(id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error linking sight:", error);
|
||||||
|
failedIds.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (linkedIds.length > 0) {
|
||||||
|
const newItems = allItems.filter((item) => linkedIds.includes(item.id));
|
||||||
|
setLinkedItems((prev) => {
|
||||||
|
const existingIds = new Set(prev.map((item) => item.id));
|
||||||
|
const additions = newItems.filter((item) => !existingIds.has(item.id));
|
||||||
|
return [...prev, ...additions];
|
||||||
|
});
|
||||||
|
onUpdate?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedItems((prev) => {
|
||||||
|
if (linkedIds.length === 0) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
const remaining = new Set(prev);
|
||||||
|
linkedIds.forEach((id) => remaining.delete(id));
|
||||||
|
return failedIds.length > 0 ? remaining : new Set();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (failedIds.length > 0) {
|
||||||
|
setError(
|
||||||
|
failedIds.length === idsToLink.length
|
||||||
|
? "Failed to link sights"
|
||||||
|
: "Some sights failed to link"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLinkingBulk(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleDetachSelection = (itemId: number) => {
|
||||||
|
const updated = new Set(selectedToDetach);
|
||||||
|
if (updated.has(itemId)) {
|
||||||
|
updated.delete(itemId);
|
||||||
|
} else {
|
||||||
|
updated.add(itemId);
|
||||||
|
}
|
||||||
|
setSelectedToDetach(updated);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleAllDetach = (checked: boolean) => {
|
||||||
|
if (!checked) {
|
||||||
|
setSelectedToDetach(new Set());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSelectedToDetach(new Set(linkedItems.map((item) => item.id)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBulkDetach = async () => {
|
||||||
|
const idsToDetach = Array.from(selectedToDetach);
|
||||||
|
if (idsToDetach.length === 0) return;
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
setIsBulkDetaching(true);
|
||||||
|
setDetachingIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
idsToDetach.forEach((id) => next.add(id));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
|
||||||
|
const detachedIds: number[] = [];
|
||||||
|
const failedIds: number[] = [];
|
||||||
|
|
||||||
|
for (const itemId of idsToDetach) {
|
||||||
|
try {
|
||||||
|
await authInstance.delete(
|
||||||
|
`/${parentResource}/${parentId}/${childResource}`,
|
||||||
|
{
|
||||||
|
data: { [`${childResource}_id`]: itemId },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
detachedIds.push(itemId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting sight:", error);
|
||||||
|
failedIds.push(itemId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (detachedIds.length > 0) {
|
||||||
|
setLinkedItems((prev) =>
|
||||||
|
prev.filter((item) => !detachedIds.includes(item.id))
|
||||||
|
);
|
||||||
|
setSelectedToDetach((prev) => {
|
||||||
|
const remaining = new Set(prev);
|
||||||
|
detachedIds.forEach((id) => remaining.delete(id));
|
||||||
|
return failedIds.length > 0 ? remaining : new Set();
|
||||||
|
});
|
||||||
|
onUpdate?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failedIds.length > 0) {
|
||||||
|
setError(
|
||||||
|
failedIds.length === idsToDetach.length
|
||||||
|
? "Failed to delete sights"
|
||||||
|
: "Some sights failed to delete"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setDetachingIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
idsToDetach.forEach((id) => next.delete(id));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
setIsBulkDetaching(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const allSelectedForDetach =
|
||||||
|
linkedItems.length > 0 &&
|
||||||
|
linkedItems.every((item) => selectedToDetach.has(item.id));
|
||||||
|
const isIndeterminateDetach =
|
||||||
|
selectedToDetach.size > 0 && !allSelectedForDetach;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (parentId) {
|
if (parentId) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@@ -204,6 +402,16 @@ const LinkedSightsContentsInner = <
|
|||||||
<Table sx={{ width: "100%" }}>
|
<Table sx={{ width: "100%" }}>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
{type === "edit" && (
|
||||||
|
<TableCell width="50px">
|
||||||
|
<Checkbox
|
||||||
|
size="small"
|
||||||
|
checked={allSelectedForDetach}
|
||||||
|
indeterminate={isIndeterminateDetach}
|
||||||
|
onChange={(e) => handleToggleAllDetach(e.target.checked)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
)}
|
||||||
<TableCell key="id" width="60px">
|
<TableCell key="id" width="60px">
|
||||||
№
|
№
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -219,6 +427,15 @@ const LinkedSightsContentsInner = <
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{linkedItems.map((item, index) => (
|
{linkedItems.map((item, index) => (
|
||||||
<TableRow key={item.id} hover>
|
<TableRow key={item.id} hover>
|
||||||
|
{type === "edit" && (
|
||||||
|
<TableCell>
|
||||||
|
<Checkbox
|
||||||
|
size="small"
|
||||||
|
checked={selectedToDetach.has(item.id)}
|
||||||
|
onChange={() => toggleDetachSelection(item.id)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
)}
|
||||||
<TableCell>{index + 1}</TableCell>
|
<TableCell>{index + 1}</TableCell>
|
||||||
{fields.map((field, idx) => (
|
{fields.map((field, idx) => (
|
||||||
<TableCell key={String(field.data) + String(idx)}>
|
<TableCell key={String(field.data) + String(idx)}>
|
||||||
@@ -229,7 +446,7 @@ const LinkedSightsContentsInner = <
|
|||||||
))}
|
))}
|
||||||
{type === "edit" && (
|
{type === "edit" && (
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Button
|
<AnimatedCircleButton
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="error"
|
color="error"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -237,9 +454,11 @@ const LinkedSightsContentsInner = <
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
deleteItem(item.id);
|
deleteItem(item.id);
|
||||||
}}
|
}}
|
||||||
|
disabled={detachingIds.has(item.id)}
|
||||||
|
loading={detachingIds.has(item.id)}
|
||||||
>
|
>
|
||||||
Отвязать
|
Отвязать
|
||||||
</Button>
|
</AnimatedCircleButton>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -249,6 +468,20 @@ const LinkedSightsContentsInner = <
|
|||||||
</TableContainer>
|
</TableContainer>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{type === "edit" && linkedItems.length > 0 && (
|
||||||
|
<Stack direction="row" gap={2} mt={2}>
|
||||||
|
<AnimatedCircleButton
|
||||||
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
|
onClick={handleBulkDetach}
|
||||||
|
disabled={selectedToDetach.size === 0 || isBulkDetaching}
|
||||||
|
loading={isBulkDetaching}
|
||||||
|
>
|
||||||
|
Отвязать выбранные ({selectedToDetach.size})
|
||||||
|
</AnimatedCircleButton>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
{linkedItems.length === 0 && !isLoading && (
|
{linkedItems.length === 0 && !isLoading && (
|
||||||
<Typography color="textSecondary" textAlign="center" py={2}>
|
<Typography color="textSecondary" textAlign="center" py={2}>
|
||||||
Достопримечательности не найдены
|
Достопримечательности не найдены
|
||||||
@@ -258,53 +491,133 @@ const LinkedSightsContentsInner = <
|
|||||||
{type === "edit" && !disableCreation && (
|
{type === "edit" && !disableCreation && (
|
||||||
<Stack gap={2} mt={2}>
|
<Stack gap={2} mt={2}>
|
||||||
<Typography variant="subtitle1">
|
<Typography variant="subtitle1">
|
||||||
Добавить достопримечательность
|
Добавить достопримечательности
|
||||||
</Typography>
|
</Typography>
|
||||||
<Autocomplete
|
<Tabs
|
||||||
fullWidth
|
value={activeTab}
|
||||||
value={
|
onChange={(_, value) => setActiveTab(value)}
|
||||||
availableItems?.find((item) => item.id === selectedItemId) || null
|
variant="fullWidth"
|
||||||
}
|
|
||||||
onChange={(_, newValue) => setSelectedItemId(newValue?.id || null)}
|
|
||||||
options={availableItems}
|
|
||||||
getOptionLabel={(item) => String(item.name)}
|
|
||||||
renderInput={(params) => (
|
|
||||||
<TextField
|
|
||||||
{...params}
|
|
||||||
label="Выберите достопримечательность"
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
isOptionEqualToValue={(option, value) => option.id === value?.id}
|
|
||||||
filterOptions={(options, { inputValue }) => {
|
|
||||||
const searchWords = inputValue
|
|
||||||
.toLowerCase()
|
|
||||||
.split(" ")
|
|
||||||
.filter(Boolean);
|
|
||||||
return options.filter((option) => {
|
|
||||||
const optionWords = String(option.name)
|
|
||||||
.toLowerCase()
|
|
||||||
.split(" ");
|
|
||||||
return searchWords.every((searchWord) =>
|
|
||||||
optionWords.some((word) => word.startsWith(searchWord))
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
renderOption={(props, option) => (
|
|
||||||
<li {...props} key={option.id}>
|
|
||||||
{String(option.name)}
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
onClick={linkItem}
|
|
||||||
disabled={!selectedItemId}
|
|
||||||
sx={{ alignSelf: "flex-start" }}
|
|
||||||
>
|
>
|
||||||
Добавить
|
<Tab label="По одной" />
|
||||||
</Button>
|
<Tab label="Массово" />
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<Box sx={{ mt: 1 }}>
|
||||||
|
{activeTab === 0 && (
|
||||||
|
<Stack gap={2}>
|
||||||
|
<Autocomplete
|
||||||
|
fullWidth
|
||||||
|
value={
|
||||||
|
availableItems?.find(
|
||||||
|
(item) => item.id === selectedItemId
|
||||||
|
) || null
|
||||||
|
}
|
||||||
|
onChange={(_, newValue) =>
|
||||||
|
setSelectedItemId(newValue?.id || null)
|
||||||
|
}
|
||||||
|
options={availableItems}
|
||||||
|
getOptionLabel={(item) => String(item.name)}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
label="Выберите достопримечательность"
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
isOptionEqualToValue={(option, value) =>
|
||||||
|
option.id === value?.id
|
||||||
|
}
|
||||||
|
filterOptions={(options, { inputValue }) => {
|
||||||
|
const searchWords = inputValue
|
||||||
|
.toLowerCase()
|
||||||
|
.split(" ")
|
||||||
|
.filter(Boolean);
|
||||||
|
return options.filter((option) => {
|
||||||
|
const optionWords = String(option.name)
|
||||||
|
.toLowerCase()
|
||||||
|
.split(" ");
|
||||||
|
return searchWords.every((searchWord) =>
|
||||||
|
optionWords.some((word) => word.startsWith(searchWord))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
renderOption={(props, option) => (
|
||||||
|
<li {...props} key={option.id}>
|
||||||
|
{String(option.name)}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AnimatedCircleButton
|
||||||
|
variant="contained"
|
||||||
|
onClick={linkItem}
|
||||||
|
disabled={!selectedItemId || isLinkingSingle}
|
||||||
|
loading={isLinkingSingle}
|
||||||
|
sx={{ alignSelf: "flex-start" }}
|
||||||
|
>
|
||||||
|
Добавить
|
||||||
|
</AnimatedCircleButton>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 1 && (
|
||||||
|
<Stack gap={2}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Поиск достопримечательностей"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
placeholder="Введите название..."
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Paper sx={{ maxHeight: 300, overflow: "auto", p: 2 }}>
|
||||||
|
<Stack gap={1}>
|
||||||
|
{filteredAvailableItems.map((item) => (
|
||||||
|
<FormControlLabel
|
||||||
|
key={item.id}
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedItems.has(item.id)}
|
||||||
|
onChange={() => handleCheckboxChange(item.id)}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={String(item.name)}
|
||||||
|
sx={{
|
||||||
|
margin: 0,
|
||||||
|
"& .MuiFormControlLabel-label": {
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{filteredAvailableItems.length === 0 && (
|
||||||
|
<Typography
|
||||||
|
color="textSecondary"
|
||||||
|
textAlign="center"
|
||||||
|
py={1}
|
||||||
|
>
|
||||||
|
{searchQuery.trim()
|
||||||
|
? "Достопримечательности не найдены"
|
||||||
|
: "Нет доступных достопримечательностей"}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<AnimatedCircleButton
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleBulkLink}
|
||||||
|
disabled={selectedItems.size === 0 || isLinkingBulk}
|
||||||
|
loading={isLinkingBulk}
|
||||||
|
sx={{ alignSelf: "flex-start" }}
|
||||||
|
>
|
||||||
|
Добавить выбранные ({selectedItems.size})
|
||||||
|
</AnimatedCircleButton>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export const StationCreatePage = observer(() => {
|
|||||||
const { cities, getCities } = cityStore;
|
const { cities, getCities } = cityStore;
|
||||||
const { selectedCityId, selectedCity } = useSelectedCity();
|
const { selectedCityId, selectedCity } = useSelectedCity();
|
||||||
const [coordinates, setCoordinates] = useState<string>("");
|
const [coordinates, setCoordinates] = useState<string>("");
|
||||||
// НОВОЕ СОСТОЯНИЕ ДЛЯ ПРЕДУПРЕЖДАЮЩЕГО ОКНА
|
|
||||||
const [isSaveWarningOpen, setIsSaveWarningOpen] = useState(false);
|
const [isSaveWarningOpen, setIsSaveWarningOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -49,7 +49,6 @@ export const StationCreatePage = observer(() => {
|
|||||||
}
|
}
|
||||||
}, [createStationData.common.latitude, createStationData.common.longitude]);
|
}, [createStationData.common.latitude, createStationData.common.longitude]);
|
||||||
|
|
||||||
// НОВАЯ ФУНКЦИЯ: Фактическое создание (вызывается после подтверждения)
|
|
||||||
const executeCreate = async () => {
|
const executeCreate = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@@ -64,11 +63,13 @@ export const StationCreatePage = observer(() => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ОБНОВЛЕННАЯ ФУНКЦИЯ: Проверка и вызов окна или создания
|
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
const isCityMissing = !createStationData.common.city_id;
|
const isCityMissing = !createStationData.common.city_id;
|
||||||
// Проверяем названия на всех языках
|
|
||||||
const isNameMissing = !createStationData.ru.name || !createStationData.en.name || !createStationData.zh.name;
|
const isNameMissing =
|
||||||
|
!createStationData.ru.name ||
|
||||||
|
!createStationData.en.name ||
|
||||||
|
!createStationData.zh.name;
|
||||||
|
|
||||||
if (isCityMissing || isNameMissing) {
|
if (isCityMissing || isNameMissing) {
|
||||||
setIsSaveWarningOpen(true);
|
setIsSaveWarningOpen(true);
|
||||||
@@ -78,13 +79,11 @@ export const StationCreatePage = observer(() => {
|
|||||||
await executeCreate();
|
await executeCreate();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Обработчик "Да" в предупреждающем окне
|
|
||||||
const handleConfirmCreate = async () => {
|
const handleConfirmCreate = async () => {
|
||||||
setIsSaveWarningOpen(false);
|
setIsSaveWarningOpen(false);
|
||||||
await executeCreate();
|
await executeCreate();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Обработчик "Нет" в предупреждающем окне
|
|
||||||
const handleCancelCreate = () => {
|
const handleCancelCreate = () => {
|
||||||
setIsSaveWarningOpen(false);
|
setIsSaveWarningOpen(false);
|
||||||
};
|
};
|
||||||
@@ -99,7 +98,6 @@ export const StationCreatePage = observer(() => {
|
|||||||
fetchCities();
|
fetchCities();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Автоматически устанавливаем выбранный город при загрузке страницы
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedCityId && selectedCity && !createStationData.common.city_id) {
|
if (selectedCityId && selectedCity && !createStationData.common.city_id) {
|
||||||
setCreateCommonData({
|
setCreateCommonData({
|
||||||
@@ -237,7 +235,7 @@ export const StationCreatePage = observer(() => {
|
|||||||
className="w-min flex gap-2 items-center"
|
className="w-min flex gap-2 items-center"
|
||||||
startIcon={<Save size={20} />}
|
startIcon={<Save size={20} />}
|
||||||
onClick={handleCreate}
|
onClick={handleCreate}
|
||||||
disabled={isLoading} // Убрал проверку name/city_id отсюда, чтобы ее обрабатывал handleCreate
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Loader2 size={20} className="animate-spin" />
|
<Loader2 size={20} className="animate-spin" />
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export const StationEditPage = observer(() => {
|
|||||||
} = stationsStore;
|
} = stationsStore;
|
||||||
const { cities, getCities } = cityStore;
|
const { cities, getCities } = cityStore;
|
||||||
const [coordinates, setCoordinates] = useState<string>("");
|
const [coordinates, setCoordinates] = useState<string>("");
|
||||||
// НОВОЕ СОСТОЯНИЕ ДЛЯ ПРЕДУПРЕЖДАЮЩЕГО ОКНА
|
|
||||||
const [isSaveWarningOpen, setIsSaveWarningOpen] = useState(false);
|
const [isSaveWarningOpen, setIsSaveWarningOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -50,7 +50,6 @@ export const StationEditPage = observer(() => {
|
|||||||
}
|
}
|
||||||
}, [editStationData.common.latitude, editStationData.common.longitude]);
|
}, [editStationData.common.latitude, editStationData.common.longitude]);
|
||||||
|
|
||||||
// НОВАЯ ФУНКЦИЯ: Фактическое редактирование (вызывается после подтверждения)
|
|
||||||
const executeEdit = async () => {
|
const executeEdit = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@@ -64,10 +63,9 @@ export const StationEditPage = observer(() => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ОБНОВЛЕННАЯ ФУНКЦИЯ: Проверка и вызов окна или редактирования
|
|
||||||
const handleEdit = async () => {
|
const handleEdit = async () => {
|
||||||
const isCityMissing = !editStationData.common.city_id;
|
const isCityMissing = !editStationData.common.city_id;
|
||||||
// Проверяем названия на всех языках
|
|
||||||
const isNameMissing =
|
const isNameMissing =
|
||||||
!editStationData.ru.name ||
|
!editStationData.ru.name ||
|
||||||
!editStationData.en.name ||
|
!editStationData.en.name ||
|
||||||
@@ -81,13 +79,11 @@ export const StationEditPage = observer(() => {
|
|||||||
await executeEdit();
|
await executeEdit();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Обработчик "Да" в предупреждающем окне
|
|
||||||
const handleConfirmEdit = async () => {
|
const handleConfirmEdit = async () => {
|
||||||
setIsSaveWarningOpen(false);
|
setIsSaveWarningOpen(false);
|
||||||
await executeEdit();
|
await executeEdit();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Обработчик "Нет" в предупреждающем окне
|
|
||||||
const handleCancelEdit = () => {
|
const handleCancelEdit = () => {
|
||||||
setIsSaveWarningOpen(false);
|
setIsSaveWarningOpen(false);
|
||||||
};
|
};
|
||||||
@@ -243,7 +239,7 @@ export const StationEditPage = observer(() => {
|
|||||||
className="w-min flex gap-2 items-center"
|
className="w-min flex gap-2 items-center"
|
||||||
startIcon={<Save size={20} />}
|
startIcon={<Save size={20} />}
|
||||||
onClick={handleEdit}
|
onClick={handleEdit}
|
||||||
disabled={isLoading} // Убрал проверку name/city_id отсюда, чтобы ее обрабатывал handleEdit
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Loader2 size={20} className="animate-spin" />
|
<Loader2 size={20} className="animate-spin" />
|
||||||
|
|||||||
@@ -8,13 +8,10 @@ import {
|
|||||||
Earth,
|
Earth,
|
||||||
Landmark,
|
Landmark,
|
||||||
GitBranch,
|
GitBranch,
|
||||||
// Car,
|
|
||||||
Table,
|
Table,
|
||||||
Split,
|
Split,
|
||||||
// Newspaper,
|
|
||||||
PersonStanding,
|
PersonStanding,
|
||||||
Cpu,
|
Cpu,
|
||||||
// BookImage,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import carrierIcon from "./carrier.svg";
|
import carrierIcon from "./carrier.svg";
|
||||||
@@ -57,12 +54,6 @@ export const NAVIGATION_ITEMS: {
|
|||||||
path: "/devices",
|
path: "/devices",
|
||||||
for_admin: true,
|
for_admin: true,
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// id: "vehicles",
|
|
||||||
// label: "Транспорт",
|
|
||||||
// icon: Car,
|
|
||||||
// path: "/vehicle",
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
id: "users",
|
id: "users",
|
||||||
label: "Пользователи",
|
label: "Пользователи",
|
||||||
@@ -75,18 +66,6 @@ export const NAVIGATION_ITEMS: {
|
|||||||
label: "Справочник",
|
label: "Справочник",
|
||||||
icon: Table,
|
icon: Table,
|
||||||
nestedItems: [
|
nestedItems: [
|
||||||
// {
|
|
||||||
// id: "media",
|
|
||||||
// label: "Медиа",
|
|
||||||
// icon: BookImage,
|
|
||||||
// path: "/media",
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// id: "articles",
|
|
||||||
// label: "Статьи",
|
|
||||||
// icon: Newspaper,
|
|
||||||
// path: "/article",
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
id: "attractions",
|
id: "attractions",
|
||||||
label: "Достопримечательности",
|
label: "Достопримечательности",
|
||||||
@@ -124,7 +103,7 @@ export const NAVIGATION_ITEMS: {
|
|||||||
id: "carriers",
|
id: "carriers",
|
||||||
label: "Перевозчики",
|
label: "Перевозчики",
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: () => <img src={carrierIcon} alt="Перевозчики"/>,
|
icon: () => <img src={carrierIcon} alt="Перевозчики" />,
|
||||||
path: "/carrier",
|
path: "/carrier",
|
||||||
for_admin: true,
|
for_admin: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
/**
|
|
||||||
* Утилита для управления кешем GLTF и blob URL
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Динамический импорт useGLTF для избежания проблем с SSR
|
|
||||||
let useGLTF: any = null;
|
let useGLTF: any = null;
|
||||||
|
|
||||||
const initializeUseGLTF = async () => {
|
const initializeUseGLTF = async () => {
|
||||||
@@ -20,9 +15,6 @@ const initializeUseGLTF = async () => {
|
|||||||
return useGLTF;
|
return useGLTF;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Очищает кеш GLTF для конкретного URL
|
|
||||||
*/
|
|
||||||
export const clearGLTFCacheForUrl = async (url: string) => {
|
export const clearGLTFCacheForUrl = async (url: string) => {
|
||||||
try {
|
try {
|
||||||
const gltf = await initializeUseGLTF();
|
const gltf = await initializeUseGLTF();
|
||||||
@@ -32,9 +24,6 @@ export const clearGLTFCacheForUrl = async (url: string) => {
|
|||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Очищает весь кеш GLTF
|
|
||||||
*/
|
|
||||||
export const clearAllGLTFCache = async () => {
|
export const clearAllGLTFCache = async () => {
|
||||||
try {
|
try {
|
||||||
const gltf = await initializeUseGLTF();
|
const gltf = await initializeUseGLTF();
|
||||||
@@ -44,9 +33,6 @@ export const clearAllGLTFCache = async () => {
|
|||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Очищает blob URL из памяти браузера
|
|
||||||
*/
|
|
||||||
export const revokeBlobURL = (url: string) => {
|
export const revokeBlobURL = (url: string) => {
|
||||||
if (url && url.startsWith("blob:")) {
|
if (url && url.startsWith("blob:")) {
|
||||||
try {
|
try {
|
||||||
@@ -55,27 +41,16 @@ export const revokeBlobURL = (url: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Комплексная очистка: blob URL + кеш GLTF
|
|
||||||
*/
|
|
||||||
export const clearBlobAndGLTFCache = async (url: string) => {
|
export const clearBlobAndGLTFCache = async (url: string) => {
|
||||||
// Сначала отзываем blob URL
|
|
||||||
revokeBlobURL(url);
|
revokeBlobURL(url);
|
||||||
|
|
||||||
// Затем очищаем кеш GLTF
|
|
||||||
await clearGLTFCacheForUrl(url);
|
await clearGLTFCacheForUrl(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Очистка при смене медиа (для предотвращения конфликтов)
|
|
||||||
*/
|
|
||||||
export const clearMediaTransitionCache = async (
|
export const clearMediaTransitionCache = async (
|
||||||
previousMediaId: string | number | null,
|
previousMediaId: string | number | null,
|
||||||
newMediaId: string | number | null,
|
|
||||||
newMediaType?: number
|
newMediaType?: number
|
||||||
) => {
|
) => {
|
||||||
console.log(newMediaId, newMediaType);
|
|
||||||
// Если переключаемся с/на 3D модель, очищаем весь кеш
|
|
||||||
if (newMediaType === 6 || previousMediaId) {
|
if (newMediaType === 6 || previousMediaId) {
|
||||||
await clearAllGLTFCache();
|
await clearAllGLTFCache();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,34 +2,17 @@ export * from "./mui/theme";
|
|||||||
export * from "./DecodeJWT";
|
export * from "./DecodeJWT";
|
||||||
export * from "./gltfCacheManager";
|
export * from "./gltfCacheManager";
|
||||||
|
|
||||||
/**
|
|
||||||
* Генерирует название медиа по умолчанию в разных форматах
|
|
||||||
*
|
|
||||||
* Примеры использования:
|
|
||||||
* - Для достопримечательности с названием: "Михайловский замок_mikhail-zamok_Фото"
|
|
||||||
* - Для достопримечательности без названия: "Название_mikhail-zamok_Фото"
|
|
||||||
* - Для статьи: "Михайловский замок_mikhail-zamok_Факты" (где "Факты" - название статьи)
|
|
||||||
*
|
|
||||||
* @param objectName - Название объекта (достопримечательности, города и т.д.)
|
|
||||||
* @param fileName - Название файла
|
|
||||||
* @param mediaType - Тип медиа (число) или название статьи
|
|
||||||
* @param isArticle - Флаг, указывающий что медиа добавляется к статье
|
|
||||||
* @returns Строка в нужном формате
|
|
||||||
*/
|
|
||||||
export const generateDefaultMediaName = (
|
export const generateDefaultMediaName = (
|
||||||
objectName: string,
|
objectName: string,
|
||||||
fileName: string,
|
fileName: string,
|
||||||
mediaType: number | string,
|
mediaType: number | string,
|
||||||
isArticle: boolean = false
|
isArticle: boolean = false
|
||||||
): string => {
|
): string => {
|
||||||
// Убираем расширение из названия файла
|
|
||||||
const fileNameWithoutExtension = fileName.split(".").slice(0, -1).join(".");
|
const fileNameWithoutExtension = fileName.split(".").slice(0, -1).join(".");
|
||||||
|
|
||||||
if (isArticle && typeof mediaType === "string") {
|
if (isArticle && typeof mediaType === "string") {
|
||||||
// Для статей: "Название достопримечательности_название файла_название статьи"
|
|
||||||
return `${objectName}_${fileNameWithoutExtension}_${mediaType}`;
|
return `${objectName}_${fileNameWithoutExtension}_${mediaType}`;
|
||||||
} else if (typeof mediaType === "number") {
|
} else if (typeof mediaType === "number") {
|
||||||
// Получаем название типа медиа
|
|
||||||
const mediaTypeLabels: Record<number, string> = {
|
const mediaTypeLabels: Record<number, string> = {
|
||||||
1: "Фото",
|
1: "Фото",
|
||||||
2: "Видео",
|
2: "Видео",
|
||||||
@@ -42,14 +25,11 @@ export const generateDefaultMediaName = (
|
|||||||
const mediaTypeLabel = mediaTypeLabels[mediaType] || "Медиа";
|
const mediaTypeLabel = mediaTypeLabels[mediaType] || "Медиа";
|
||||||
|
|
||||||
if (objectName && objectName.trim() !== "") {
|
if (objectName && objectName.trim() !== "") {
|
||||||
// Если есть название объекта: "Название объекта_название файла_тип медиа"
|
|
||||||
return `${objectName}_${fileNameWithoutExtension}_${mediaTypeLabel}`;
|
return `${objectName}_${fileNameWithoutExtension}_${mediaTypeLabel}`;
|
||||||
} else {
|
} else {
|
||||||
// Если нет названия объекта: "Название_название файла_тип медиа"
|
|
||||||
return `Название_${fileNameWithoutExtension}_${mediaTypeLabel}`;
|
return `Название_${fileNameWithoutExtension}_${mediaTypeLabel}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback
|
|
||||||
return `${objectName || "Название"}_${fileNameWithoutExtension}_Медиа`;
|
return `${objectName || "Название"}_${fileNameWithoutExtension}_Медиа`;
|
||||||
};
|
};
|
||||||
|
|||||||
1067
src/shared/modals/ArticleSelectOrCreateDialog/index.tsx
Normal file
1067
src/shared/modals/ArticleSelectOrCreateDialog/index.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -54,7 +54,7 @@ interface UploadMediaDialogProps {
|
|||||||
| "station";
|
| "station";
|
||||||
isArticle?: boolean;
|
isArticle?: boolean;
|
||||||
articleName?: string;
|
articleName?: string;
|
||||||
initialFile?: File; // <--- добавлено
|
initialFile?: File;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UploadMediaDialog = observer(
|
export const UploadMediaDialog = observer(
|
||||||
@@ -68,7 +68,7 @@ export const UploadMediaDialog = observer(
|
|||||||
|
|
||||||
isArticle,
|
isArticle,
|
||||||
articleName,
|
articleName,
|
||||||
initialFile, // <--- добавлено
|
initialFile,
|
||||||
}: UploadMediaDialogProps) => {
|
}: UploadMediaDialogProps) => {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -87,7 +87,6 @@ export const UploadMediaDialog = observer(
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialFile) {
|
if (initialFile) {
|
||||||
// Очищаем предыдущий blob URL если он существует
|
|
||||||
if (
|
if (
|
||||||
previousMediaUrlRef.current &&
|
previousMediaUrlRef.current &&
|
||||||
previousMediaUrlRef.current.startsWith("blob:")
|
previousMediaUrlRef.current.startsWith("blob:")
|
||||||
@@ -106,7 +105,6 @@ export const UploadMediaDialog = observer(
|
|||||||
}
|
}
|
||||||
}, [initialFile]);
|
}, [initialFile]);
|
||||||
|
|
||||||
// Очистка blob URL при размонтировании компонента
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (
|
if (
|
||||||
@@ -116,13 +114,13 @@ export const UploadMediaDialog = observer(
|
|||||||
clearBlobAndGLTFCache(previousMediaUrlRef.current);
|
clearBlobAndGLTFCache(previousMediaUrlRef.current);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, []); // Пустой массив зависимостей - выполняется только при размонтировании
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (fileToUpload) {
|
if (fileToUpload) {
|
||||||
setMediaFile(fileToUpload);
|
setMediaFile(fileToUpload);
|
||||||
setMediaFilename(fileToUpload.name);
|
setMediaFilename(fileToUpload.name);
|
||||||
// Try to determine media type from file extension
|
|
||||||
const extension = fileToUpload.name.split(".").pop()?.toLowerCase();
|
const extension = fileToUpload.name.split(".").pop()?.toLowerCase();
|
||||||
if (extension) {
|
if (extension) {
|
||||||
if (["glb", "gltf"].includes(extension)) {
|
if (["glb", "gltf"].includes(extension)) {
|
||||||
@@ -134,22 +132,18 @@ export const UploadMediaDialog = observer(
|
|||||||
extension
|
extension
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
// Для изображений доступны все типы кроме видео
|
setAvailableMediaTypes([1, 3, 4, 5]);
|
||||||
setAvailableMediaTypes([1, 3, 4, 5]); // Фото, Иконка, Водяной знак, Панорама, 3Д-модель
|
setMediaType(1);
|
||||||
setMediaType(1); // По умолчанию Фото
|
|
||||||
} else if (["mp4", "webm", "mov"].includes(extension)) {
|
} else if (["mp4", "webm", "mov"].includes(extension)) {
|
||||||
// Для видео только тип Видео
|
|
||||||
setAvailableMediaTypes([2]);
|
setAvailableMediaTypes([2]);
|
||||||
setMediaType(2);
|
setMediaType(2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Генерируем название по умолчанию если есть контекст
|
|
||||||
if (fileToUpload.name) {
|
if (fileToUpload.name) {
|
||||||
let defaultName = "";
|
let defaultName = "";
|
||||||
|
|
||||||
if (isArticle && articleName && contextObjectName) {
|
if (isArticle && articleName && contextObjectName) {
|
||||||
// Для статей: "Название достопримечательности_название файла_название статьи"
|
|
||||||
defaultName = generateDefaultMediaName(
|
defaultName = generateDefaultMediaName(
|
||||||
contextObjectName,
|
contextObjectName,
|
||||||
fileToUpload.name,
|
fileToUpload.name,
|
||||||
@@ -157,10 +151,9 @@ export const UploadMediaDialog = observer(
|
|||||||
true
|
true
|
||||||
);
|
);
|
||||||
} else if (contextObjectName && contextObjectName.trim() !== "") {
|
} else if (contextObjectName && contextObjectName.trim() !== "") {
|
||||||
// Для обычных медиа с названием объекта
|
|
||||||
const currentMediaType = hardcodeType
|
const currentMediaType = hardcodeType
|
||||||
? MEDIA_TYPE_VALUES[hardcodeType]
|
? MEDIA_TYPE_VALUES[hardcodeType]
|
||||||
: 1; // По умолчанию фото
|
: 1;
|
||||||
defaultName = generateDefaultMediaName(
|
defaultName = generateDefaultMediaName(
|
||||||
contextObjectName,
|
contextObjectName,
|
||||||
fileToUpload.name,
|
fileToUpload.name,
|
||||||
@@ -168,10 +161,9 @@ export const UploadMediaDialog = observer(
|
|||||||
false
|
false
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Для медиа без названия объекта
|
|
||||||
const currentMediaType = hardcodeType
|
const currentMediaType = hardcodeType
|
||||||
? MEDIA_TYPE_VALUES[hardcodeType]
|
? MEDIA_TYPE_VALUES[hardcodeType]
|
||||||
: 1; // По умолчанию фото
|
: 1;
|
||||||
defaultName = generateDefaultMediaName(
|
defaultName = generateDefaultMediaName(
|
||||||
"",
|
"",
|
||||||
fileToUpload.name,
|
fileToUpload.name,
|
||||||
@@ -185,13 +177,11 @@ export const UploadMediaDialog = observer(
|
|||||||
}
|
}
|
||||||
}, [fileToUpload, contextObjectName, hardcodeType, isArticle, articleName]);
|
}, [fileToUpload, contextObjectName, hardcodeType, isArticle, articleName]);
|
||||||
|
|
||||||
// Обновляем название при изменении типа медиа
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mediaFilename && mediaType > 0) {
|
if (mediaFilename && mediaType > 0) {
|
||||||
let defaultName = "";
|
let defaultName = "";
|
||||||
|
|
||||||
if (isArticle && articleName && contextObjectName) {
|
if (isArticle && articleName && contextObjectName) {
|
||||||
// Для статей: "Название достопримечательности_название файла_название статьи"
|
|
||||||
defaultName = generateDefaultMediaName(
|
defaultName = generateDefaultMediaName(
|
||||||
contextObjectName,
|
contextObjectName,
|
||||||
mediaFilename,
|
mediaFilename,
|
||||||
@@ -199,7 +189,6 @@ export const UploadMediaDialog = observer(
|
|||||||
true
|
true
|
||||||
);
|
);
|
||||||
} else if (contextObjectName && contextObjectName.trim() !== "") {
|
} else if (contextObjectName && contextObjectName.trim() !== "") {
|
||||||
// Для обычных медиа с названием объекта
|
|
||||||
const currentMediaType = hardcodeType
|
const currentMediaType = hardcodeType
|
||||||
? MEDIA_TYPE_VALUES[hardcodeType]
|
? MEDIA_TYPE_VALUES[hardcodeType]
|
||||||
: mediaType;
|
: mediaType;
|
||||||
@@ -210,7 +199,6 @@ export const UploadMediaDialog = observer(
|
|||||||
false
|
false
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Для медиа без названия объекта
|
|
||||||
const currentMediaType = hardcodeType
|
const currentMediaType = hardcodeType
|
||||||
? MEDIA_TYPE_VALUES[hardcodeType]
|
? MEDIA_TYPE_VALUES[hardcodeType]
|
||||||
: mediaType;
|
: mediaType;
|
||||||
@@ -235,7 +223,6 @@ export const UploadMediaDialog = observer(
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mediaFile) {
|
if (mediaFile) {
|
||||||
// Очищаем предыдущий blob URL и кеш GLTF если он существует
|
|
||||||
if (
|
if (
|
||||||
previousMediaUrlRef.current &&
|
previousMediaUrlRef.current &&
|
||||||
previousMediaUrlRef.current.startsWith("blob:")
|
previousMediaUrlRef.current.startsWith("blob:")
|
||||||
@@ -245,22 +232,10 @@ export const UploadMediaDialog = observer(
|
|||||||
|
|
||||||
const newBlobUrl = URL.createObjectURL(mediaFile as Blob);
|
const newBlobUrl = URL.createObjectURL(mediaFile as Blob);
|
||||||
setMediaUrl(newBlobUrl);
|
setMediaUrl(newBlobUrl);
|
||||||
previousMediaUrlRef.current = newBlobUrl; // Сохраняем новый URL в ref
|
previousMediaUrlRef.current = newBlobUrl;
|
||||||
setIsPreviewLoaded(false); // Сбрасываем состояние загрузки при смене файла
|
setIsPreviewLoaded(false);
|
||||||
}
|
}
|
||||||
}, [mediaFile]); // Убираем mediaUrl из зависимостей чтобы избежать зацикливания
|
}, [mediaFile]);
|
||||||
|
|
||||||
// const fileFormat = useEffect(() => {
|
|
||||||
// const handleKeyPress = (event: KeyboardEvent) => {
|
|
||||||
// if (event.key.toLowerCase() === "enter" && !event.ctrlKey) {
|
|
||||||
// event.preventDefault();
|
|
||||||
// onClose();
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// window.addEventListener("keydown", handleKeyPress);
|
|
||||||
// return () => window.removeEventListener("keydown", handleKeyPress);
|
|
||||||
// }, [onClose]);
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (!mediaFile) return;
|
if (!mediaFile) return;
|
||||||
@@ -285,10 +260,10 @@ export const UploadMediaDialog = observer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
setSuccess(true);
|
setSuccess(true);
|
||||||
// Закрываем модальное окно после успешного сохранения
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
handleClose();
|
handleClose();
|
||||||
}, 1000); // Небольшая задержка, чтобы пользователь увидел сообщение об успехе
|
}, 1000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Failed to save media");
|
setError(err instanceof Error ? err.message : "Failed to save media");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -297,7 +272,6 @@ export const UploadMediaDialog = observer(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
// Очищаем blob URL и кеш GLTF при закрытии диалога
|
|
||||||
if (
|
if (
|
||||||
previousMediaUrlRef.current &&
|
previousMediaUrlRef.current &&
|
||||||
previousMediaUrlRef.current.startsWith("blob:")
|
previousMediaUrlRef.current.startsWith("blob:")
|
||||||
@@ -310,7 +284,7 @@ export const UploadMediaDialog = observer(
|
|||||||
setMediaUrl(null);
|
setMediaUrl(null);
|
||||||
setMediaFile(null);
|
setMediaFile(null);
|
||||||
setIsPreviewLoaded(false);
|
setIsPreviewLoaded(false);
|
||||||
previousMediaUrlRef.current = null; // Очищаем ref
|
previousMediaUrlRef.current = null;
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ export * from "./SelectArticleDialog";
|
|||||||
export * from "./SelectMediaDialog";
|
export * from "./SelectMediaDialog";
|
||||||
export * from "./PreviewMediaDialog";
|
export * from "./PreviewMediaDialog";
|
||||||
export * from "./UploadMediaDialog";
|
export * from "./UploadMediaDialog";
|
||||||
|
export * from "./ArticleSelectOrCreateDialog";
|
||||||
|
|||||||
@@ -171,7 +171,6 @@ class CarrierStore {
|
|||||||
this.carriers[language].data.push(response.data);
|
this.carriers[language].data.push(response.data);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create translations for other languages
|
|
||||||
for (const lang of ["ru", "en", "zh"].filter((l) => l !== language)) {
|
for (const lang of ["ru", "en", "zh"].filter((l) => l !== language)) {
|
||||||
const patchPayload = {
|
const patchPayload = {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @shared/stores/createSightStore.ts
|
|
||||||
import {
|
import {
|
||||||
articlesStore,
|
articlesStore,
|
||||||
Language,
|
Language,
|
||||||
@@ -27,7 +26,6 @@ type SightLanguageInfo = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type SightCommonInfo = {
|
type SightCommonInfo = {
|
||||||
// id: number; // ID is 0 until created
|
|
||||||
city_id: number;
|
city_id: number;
|
||||||
city: string;
|
city: string;
|
||||||
latitude: number;
|
latitude: number;
|
||||||
@@ -35,13 +33,11 @@ type SightCommonInfo = {
|
|||||||
thumbnail: string | null;
|
thumbnail: string | null;
|
||||||
watermark_lu: string | null;
|
watermark_lu: string | null;
|
||||||
watermark_rd: string | null;
|
watermark_rd: string | null;
|
||||||
left_article: number; // Can be 0 or a real ID, or placeholder like 10000000
|
left_article: number;
|
||||||
preview_media: string | null;
|
preview_media: string | null;
|
||||||
video_preview: string | null;
|
video_preview: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// SightBaseInfo combines common info with language-specific info
|
|
||||||
// The 'id' for the sight itself will be assigned upon creation by the backend.
|
|
||||||
type SightBaseInfo = SightCommonInfo & {
|
type SightBaseInfo = SightCommonInfo & {
|
||||||
[key in Language]: SightLanguageInfo;
|
[key in Language]: SightLanguageInfo;
|
||||||
};
|
};
|
||||||
@@ -78,7 +74,7 @@ const initialSightState: SightBaseInfo = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class CreateSightStore {
|
class CreateSightStore {
|
||||||
sight: SightBaseInfo = JSON.parse(JSON.stringify(initialSightState)); // Deep copy for reset
|
sight: SightBaseInfo = JSON.parse(JSON.stringify(initialSightState));
|
||||||
|
|
||||||
uploadMediaOpen = false;
|
uploadMediaOpen = false;
|
||||||
setUploadMediaOpen = (open: boolean) => {
|
setUploadMediaOpen = (open: boolean) => {
|
||||||
@@ -93,9 +89,7 @@ class CreateSightStore {
|
|||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Right Article Management ---
|
|
||||||
createNewRightArticle = async () => {
|
createNewRightArticle = async () => {
|
||||||
// Create article in DB for all languages
|
|
||||||
const articleRuData = {
|
const articleRuData = {
|
||||||
heading: "Новый заголовок (RU)",
|
heading: "Новый заголовок (RU)",
|
||||||
body: "Новый текст (RU)",
|
body: "Новый текст (RU)",
|
||||||
@@ -125,7 +119,7 @@ class CreateSightStore {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const { id } = articleRes.data; // New article's ID
|
const { id } = articleRes.data;
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
const newArticleEntry = { id, media: [] };
|
const newArticleEntry = { id, media: [] };
|
||||||
@@ -133,7 +127,7 @@ class CreateSightStore {
|
|||||||
this.sight.en.right.push({ ...newArticleEntry, ...articleEnData });
|
this.sight.en.right.push({ ...newArticleEntry, ...articleEnData });
|
||||||
this.sight.zh.right.push({ ...newArticleEntry, ...articleZhData });
|
this.sight.zh.right.push({ ...newArticleEntry, ...articleZhData });
|
||||||
});
|
});
|
||||||
return id; // Return ID for potential immediate use
|
return id;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating new right article:", error);
|
console.error("Error creating new right article:", error);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -169,7 +163,7 @@ class CreateSightStore {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return articleId; // Return the linked article ID
|
return articleId;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error linking existing right article:", error);
|
console.error("Error linking existing right article:", error);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -188,9 +182,7 @@ class CreateSightStore {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// "Unlink" in create mode means just removing from the list to be created with the sight
|
|
||||||
unlinkRightAritcle = (articleId: number) => {
|
unlinkRightAritcle = (articleId: number) => {
|
||||||
// Changed from 'unlinkRightAritcle' spelling
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.sight.ru.right = this.sight.ru.right.filter(
|
this.sight.ru.right = this.sight.ru.right.filter(
|
||||||
(article) => article.id !== articleId
|
(article) => article.id !== articleId
|
||||||
@@ -202,16 +194,12 @@ class CreateSightStore {
|
|||||||
(article) => article.id !== articleId
|
(article) => article.id !== articleId
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
// Note: If this article was created via createNewRightArticle, it still exists in the DB.
|
|
||||||
// Consider if an orphaned article should be deleted here or managed separately.
|
|
||||||
// For now, it just removes it from the list associated with *this specific sight creation process*.
|
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteRightArticle = async (articleId: number) => {
|
deleteRightArticle = async (articleId: number) => {
|
||||||
try {
|
try {
|
||||||
await authInstance.delete(`/article/${articleId}`); // Delete from backend
|
await authInstance.delete(`/article/${articleId}`);
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
// Remove from local store for all languages
|
|
||||||
this.sight.ru.right = this.sight.ru.right.filter(
|
this.sight.ru.right = this.sight.ru.right.filter(
|
||||||
(article) => article.id !== articleId
|
(article) => article.id !== articleId
|
||||||
);
|
);
|
||||||
@@ -228,12 +216,11 @@ class CreateSightStore {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Right Article Media Management ---
|
|
||||||
createLinkWithRightArticle = async (media: MediaItem, articleId: number) => {
|
createLinkWithRightArticle = async (media: MediaItem, articleId: number) => {
|
||||||
try {
|
try {
|
||||||
await authInstance.post(`/article/${articleId}/media`, {
|
await authInstance.post(`/article/${articleId}/media`, {
|
||||||
media_id: media.id,
|
media_id: media.id,
|
||||||
media_order: 1, // Or calculate based on existing media.length + 1
|
media_order: 1,
|
||||||
});
|
});
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
(["ru", "en", "zh"] as Language[]).forEach((lang) => {
|
(["ru", "en", "zh"] as Language[]).forEach((lang) => {
|
||||||
@@ -242,7 +229,7 @@ class CreateSightStore {
|
|||||||
);
|
);
|
||||||
if (article) {
|
if (article) {
|
||||||
if (!article.media) article.media = [];
|
if (!article.media) article.media = [];
|
||||||
article.media.unshift(media); // Add to the beginning
|
article.media.unshift(media);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -273,7 +260,6 @@ class CreateSightStore {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Left Article Management (largely unchanged from your provided store) ---
|
|
||||||
updateLeftInfo = (language: Language, heading: string, body: string) => {
|
updateLeftInfo = (language: Language, heading: string, body: string) => {
|
||||||
this.sight[language].left.heading = heading;
|
this.sight[language].left.heading = heading;
|
||||||
this.sight[language].left.body = body;
|
this.sight[language].left.body = body;
|
||||||
@@ -323,7 +309,7 @@ class CreateSightStore {
|
|||||||
deleteLeftArticle = async (articleId: number) => {
|
deleteLeftArticle = async (articleId: number) => {
|
||||||
/* ... your existing logic ... */
|
/* ... your existing logic ... */
|
||||||
await authInstance.delete(`/article/${articleId}`);
|
await authInstance.delete(`/article/${articleId}`);
|
||||||
// articlesStore.getArticles(languageStore.language); // If still neede
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
articlesStore.articles.ru = articlesStore.articles.ru.filter(
|
articlesStore.articles.ru = articlesStore.articles.ru.filter(
|
||||||
(article) => article.id !== articleId
|
(article) => article.id !== articleId
|
||||||
@@ -340,63 +326,69 @@ class CreateSightStore {
|
|||||||
|
|
||||||
createLeftArticle = async () => {
|
createLeftArticle = async () => {
|
||||||
/* ... your existing logic to create a new left article (placeholder or DB) ... */
|
/* ... your existing logic to create a new left article (placeholder or DB) ... */
|
||||||
|
const ruName = (this.sight.ru.name || "").trim();
|
||||||
|
const enName = (this.sight.en.name || "").trim();
|
||||||
|
const zhName = (this.sight.zh.name || "").trim();
|
||||||
|
|
||||||
|
const hasAnyName = !!(ruName || enName || zhName);
|
||||||
|
|
||||||
const response = await languageInstance("ru").post("/article", {
|
const response = await languageInstance("ru").post("/article", {
|
||||||
heading: "Новая левая статья",
|
heading: hasAnyName ? ruName : "",
|
||||||
body: "Заполните контентом",
|
body: "",
|
||||||
});
|
});
|
||||||
const newLeftArticleId = response.data.id;
|
const newLeftArticleId = response.data.id;
|
||||||
|
|
||||||
await languageInstance("en").patch(`/article/${newLeftArticleId}`, {
|
await languageInstance("en").patch(`/article/${newLeftArticleId}`, {
|
||||||
heading: "New Left Article",
|
heading: hasAnyName ? enName : "",
|
||||||
body: "Fill with content",
|
body: "",
|
||||||
});
|
});
|
||||||
await languageInstance("zh").patch(`/article/${newLeftArticleId}`, {
|
await languageInstance("zh").patch(`/article/${newLeftArticleId}`, {
|
||||||
heading: "新的左侧文章",
|
heading: hasAnyName ? zhName : "",
|
||||||
body: "填写内容",
|
body: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.sight.left_article = newLeftArticleId; // Store the actual ID
|
this.sight.left_article = newLeftArticleId;
|
||||||
this.sight.ru.left = {
|
this.sight.ru.left = {
|
||||||
heading: "Новая левая статья",
|
heading: hasAnyName ? ruName : "",
|
||||||
body: "Заполните контентом",
|
body: "",
|
||||||
media: [],
|
media: [],
|
||||||
};
|
};
|
||||||
this.sight.en.left = {
|
this.sight.en.left = {
|
||||||
heading: "New Left Article",
|
heading: hasAnyName ? enName : "",
|
||||||
body: "Fill with content",
|
body: "",
|
||||||
media: [],
|
media: [],
|
||||||
};
|
};
|
||||||
this.sight.zh.left = {
|
this.sight.zh.left = {
|
||||||
heading: "新的左侧文章",
|
heading: hasAnyName ? zhName : "",
|
||||||
body: "填写内容",
|
body: "",
|
||||||
media: [],
|
media: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
articlesStore.articles.ru.push({
|
articlesStore.articles.ru.push({
|
||||||
id: newLeftArticleId,
|
id: newLeftArticleId,
|
||||||
heading: "Новая левая статья",
|
heading: hasAnyName ? ruName : "",
|
||||||
body: "Заполните контентом",
|
body: "",
|
||||||
service_name: "Новая левая статья",
|
service_name: hasAnyName ? ruName : "",
|
||||||
});
|
});
|
||||||
articlesStore.articles.en.push({
|
articlesStore.articles.en.push({
|
||||||
id: newLeftArticleId,
|
id: newLeftArticleId,
|
||||||
heading: "New Left Article",
|
heading: hasAnyName ? enName : "",
|
||||||
body: "Fill with content",
|
body: "",
|
||||||
service_name: "New Left Article",
|
service_name: hasAnyName ? enName : "",
|
||||||
});
|
});
|
||||||
articlesStore.articles.zh.push({
|
articlesStore.articles.zh.push({
|
||||||
id: newLeftArticleId,
|
id: newLeftArticleId,
|
||||||
heading: "新的左侧文章",
|
heading: hasAnyName ? zhName : "",
|
||||||
body: "填写内容",
|
body: "",
|
||||||
service_name: "新的左侧文章",
|
service_name: hasAnyName ? zhName : "",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return newLeftArticleId;
|
return newLeftArticleId;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Placeholder for a "new" unsaved left article
|
|
||||||
setNewLeftArticlePlaceholder = () => {
|
setNewLeftArticlePlaceholder = () => {
|
||||||
this.sight.left_article = 10000000; // Special placeholder ID
|
this.sight.left_article = 10000000;
|
||||||
this.sight.ru.left = {
|
this.sight.ru.left = {
|
||||||
heading: "Новая левая статья",
|
heading: "Новая левая статья",
|
||||||
body: "Заполните контентом",
|
body: "Заполните контентом",
|
||||||
@@ -414,7 +406,6 @@ class CreateSightStore {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Sight Preview Media ---
|
|
||||||
linkPreviewMedia = (mediaId: string) => {
|
linkPreviewMedia = (mediaId: string) => {
|
||||||
this.sight.preview_media = mediaId;
|
this.sight.preview_media = mediaId;
|
||||||
};
|
};
|
||||||
@@ -423,32 +414,27 @@ class CreateSightStore {
|
|||||||
this.sight.preview_media = null;
|
this.sight.preview_media = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- General Store Methods ---
|
|
||||||
clearCreateSight = () => {
|
clearCreateSight = () => {
|
||||||
this.needLeaveAgree = false;
|
this.needLeaveAgree = false;
|
||||||
this.sight = JSON.parse(JSON.stringify(initialSightState)); // Reset to initial
|
this.sight = JSON.parse(JSON.stringify(initialSightState));
|
||||||
};
|
};
|
||||||
|
|
||||||
updateSightInfo = (
|
updateSightInfo = (
|
||||||
content: Partial<SightLanguageInfo | SightCommonInfo>, // Corrected types
|
content: Partial<SightLanguageInfo | SightCommonInfo>,
|
||||||
language?: Language
|
language?: Language
|
||||||
) => {
|
) => {
|
||||||
this.needLeaveAgree = true;
|
this.needLeaveAgree = true;
|
||||||
if (language) {
|
if (language) {
|
||||||
this.sight[language] = { ...this.sight[language], ...content };
|
this.sight[language] = { ...this.sight[language], ...content };
|
||||||
} else {
|
} else {
|
||||||
// Assuming content here is for SightCommonInfo
|
|
||||||
this.sight = { ...this.sight, ...(content as Partial<SightCommonInfo>) };
|
this.sight = { ...this.sight, ...(content as Partial<SightCommonInfo>) };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Main Sight Creation Logic ---
|
|
||||||
createSight = async (primaryLanguage: Language) => {
|
createSight = async (primaryLanguage: Language) => {
|
||||||
let finalLeftArticleId = this.sight.left_article;
|
let finalLeftArticleId = this.sight.left_article;
|
||||||
|
|
||||||
// 1. Handle Left Article (Create if new, or use existing ID)
|
|
||||||
if (this.sight.left_article === 10000000) {
|
if (this.sight.left_article === 10000000) {
|
||||||
// Placeholder for new
|
|
||||||
const res = await languageInstance("ru").post("/article", {
|
const res = await languageInstance("ru").post("/article", {
|
||||||
heading: this.sight.ru.left.heading,
|
heading: this.sight.ru.left.heading,
|
||||||
body: this.sight.ru.left.body,
|
body: this.sight.ru.left.body,
|
||||||
@@ -466,7 +452,6 @@ class CreateSightStore {
|
|||||||
this.sight.left_article !== 0 &&
|
this.sight.left_article !== 0 &&
|
||||||
this.sight.left_article !== null
|
this.sight.left_article !== null
|
||||||
) {
|
) {
|
||||||
// Existing, ensure it's up-to-date
|
|
||||||
await languageInstance("ru").patch(
|
await languageInstance("ru").patch(
|
||||||
`/article/${this.sight.left_article}`,
|
`/article/${this.sight.left_article}`,
|
||||||
{ heading: this.sight.ru.left.heading, body: this.sight.ru.left.body }
|
{ heading: this.sight.ru.left.heading, body: this.sight.ru.left.body }
|
||||||
@@ -480,10 +465,7 @@ class CreateSightStore {
|
|||||||
{ heading: this.sight.zh.left.heading, body: this.sight.zh.left.body }
|
{ heading: this.sight.zh.left.heading, body: this.sight.zh.left.body }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// else: left_article is 0, so no left article
|
|
||||||
|
|
||||||
// 2. Right articles are already created in DB and their IDs are in this.sight[lang].right.
|
|
||||||
// We just need to update their content if changed before saving the sight.
|
|
||||||
for (const lang of ["ru", "en", "zh"] as Language[]) {
|
for (const lang of ["ru", "en", "zh"] as Language[]) {
|
||||||
for (const article of this.sight[lang].right) {
|
for (const article of this.sight[lang].right) {
|
||||||
if (article.id == 0 || article.id == null) {
|
if (article.id == 0 || article.id == null) {
|
||||||
@@ -493,14 +475,12 @@ class CreateSightStore {
|
|||||||
heading: article.heading,
|
heading: article.heading,
|
||||||
body: article.body,
|
body: article.body,
|
||||||
});
|
});
|
||||||
// Media for these articles are already linked via createLinkWithRightArticle
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const rightArticleIdsForLink = this.sight[primaryLanguage].right.map(
|
const rightArticleIdsForLink = this.sight[primaryLanguage].right.map(
|
||||||
(a) => a.id
|
(a) => a.id
|
||||||
);
|
);
|
||||||
|
|
||||||
// 3. Create Sight object in DB
|
|
||||||
const sightPayload = {
|
const sightPayload = {
|
||||||
city_id: this.sight.city_id,
|
city_id: this.sight.city_id,
|
||||||
city: this.sight.city,
|
city: this.sight.city,
|
||||||
@@ -520,9 +500,8 @@ class CreateSightStore {
|
|||||||
"/sight",
|
"/sight",
|
||||||
sightPayload
|
sightPayload
|
||||||
);
|
);
|
||||||
const newSightId = response.data.id; // ID of the newly created sight
|
const newSightId = response.data.id;
|
||||||
|
|
||||||
// 4. Update other languages for the sight
|
|
||||||
const otherLanguages = (["ru", "en", "zh"] as Language[]).filter(
|
const otherLanguages = (["ru", "en", "zh"] as Language[]).filter(
|
||||||
(l) => l !== primaryLanguage
|
(l) => l !== primaryLanguage
|
||||||
);
|
);
|
||||||
@@ -543,20 +522,17 @@ class CreateSightStore {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Link Right Articles to the new Sight
|
|
||||||
for (let i = 0; i < rightArticleIdsForLink.length; i++) {
|
for (let i = 0; i < rightArticleIdsForLink.length; i++) {
|
||||||
await authInstance.post(`/sight/${newSightId}/article`, {
|
await authInstance.post(`/sight/${newSightId}/article`, {
|
||||||
article_id: rightArticleIdsForLink[i],
|
article_id: rightArticleIdsForLink[i],
|
||||||
page_num: i + 1, // Or other logic for page_num
|
page_num: i + 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optionally: this.clearCreateSight(); // To reset form after successful creation
|
|
||||||
this.needLeaveAgree = false;
|
this.needLeaveAgree = false;
|
||||||
return newSightId;
|
return newSightId;
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Media Upload (Generic, used by dialogs) ---
|
|
||||||
uploadMedia = async (
|
uploadMedia = async (
|
||||||
filename: string,
|
filename: string,
|
||||||
type: number,
|
type: number,
|
||||||
@@ -575,12 +551,12 @@ class CreateSightStore {
|
|||||||
this.fileToUpload = null;
|
this.fileToUpload = null;
|
||||||
this.uploadMediaOpen = false;
|
this.uploadMediaOpen = false;
|
||||||
});
|
});
|
||||||
mediaStore.getMedia(); // Refresh global media list
|
mediaStore.getMedia();
|
||||||
return {
|
return {
|
||||||
id: response.data.id,
|
id: response.data.id,
|
||||||
filename: filename, // Or response.data.filename if backend returns it
|
filename: filename,
|
||||||
media_name: media_name, // Or response.data.media_name
|
media_name: media_name,
|
||||||
media_type: type, // Or response.data.type
|
media_type: type,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error uploading media:", error);
|
console.error("Error uploading media:", error);
|
||||||
@@ -588,15 +564,12 @@ class CreateSightStore {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// For Left Article Media
|
|
||||||
createLinkWithLeftArticle = async (media: MediaItem) => {
|
createLinkWithLeftArticle = async (media: MediaItem) => {
|
||||||
if (!this.sight.left_article || this.sight.left_article === 10000000) {
|
if (!this.sight.left_article || this.sight.left_article === 10000000) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"Left article not selected or is a placeholder. Cannot link media yet."
|
"Left article not selected or is a placeholder. Cannot link media yet."
|
||||||
);
|
);
|
||||||
// If it's a placeholder, we could store the media temporarily and link it after the article is created.
|
|
||||||
// For simplicity, we'll assume the article must exist.
|
|
||||||
// A more robust solution might involve creating the article first if it's a placeholder.
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -655,7 +628,7 @@ class CreateSightStore {
|
|||||||
|
|
||||||
this.sight.ru.right = sortArticles(this.sight.ru.right);
|
this.sight.ru.right = sortArticles(this.sight.ru.right);
|
||||||
this.sight.en.right = sortArticles(this.sight.en.right);
|
this.sight.en.right = sortArticles(this.sight.en.right);
|
||||||
this.sight.zh.right = sortArticles(this.sight.zh.right); // теперь zh тоже сортируется одинаково
|
this.sight.zh.right = sortArticles(this.sight.zh.right);
|
||||||
|
|
||||||
this.needLeaveAgree = true;
|
this.needLeaveAgree = true;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @shared/stores/editSightStore.ts
|
|
||||||
import {
|
import {
|
||||||
articlesStore,
|
articlesStore,
|
||||||
authInstance,
|
authInstance,
|
||||||
@@ -96,13 +95,11 @@ class EditSightStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
// Обновляем языковую часть
|
|
||||||
this.sight[language] = {
|
this.sight[language] = {
|
||||||
...this.sight[language],
|
...this.sight[language],
|
||||||
...data,
|
...data,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Только при первом запросе обновляем общую часть
|
|
||||||
if (!this.hasLoadedCommon) {
|
if (!this.hasLoadedCommon) {
|
||||||
this.sight.common = {
|
this.sight.common = {
|
||||||
...this.sight.common,
|
...this.sight.common,
|
||||||
@@ -123,7 +120,6 @@ class EditSightStore {
|
|||||||
let responseEn = await languageInstance("en").get(`/sight/${id}/article`);
|
let responseEn = await languageInstance("en").get(`/sight/${id}/article`);
|
||||||
let responseZh = await languageInstance("zh").get(`/sight/${id}/article`);
|
let responseZh = await languageInstance("zh").get(`/sight/${id}/article`);
|
||||||
|
|
||||||
// Create a map of article IDs to their media
|
|
||||||
const mediaMap = new Map();
|
const mediaMap = new Map();
|
||||||
for (const article of responseRu.data) {
|
for (const article of responseRu.data) {
|
||||||
const responseMedia = await authInstance.get(
|
const responseMedia = await authInstance.get(
|
||||||
@@ -132,7 +128,6 @@ class EditSightStore {
|
|||||||
mediaMap.set(article.id, responseMedia.data);
|
mediaMap.set(article.id, responseMedia.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to add media to articles
|
|
||||||
const addMediaToArticles = (articles: any[]) => {
|
const addMediaToArticles = (articles: any[]) => {
|
||||||
return articles.map((article) => ({
|
return articles.map((article) => ({
|
||||||
...article,
|
...article,
|
||||||
@@ -327,28 +322,6 @@ class EditSightStore {
|
|||||||
articles: articleIdsInObject,
|
articles: articleIdsInObject,
|
||||||
});
|
});
|
||||||
|
|
||||||
// await languageInstance("ru").patch(
|
|
||||||
// `/sight/${this.sight.common.left_article}/article`,
|
|
||||||
// {
|
|
||||||
// heading: this.sight.ru.left.heading,
|
|
||||||
// body: this.sight.ru.left.body,
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
// await languageInstance("en").patch(
|
|
||||||
// `/sight/${this.sight.common.left_article}/article`,
|
|
||||||
// {
|
|
||||||
// heading: this.sight.en.left.heading,
|
|
||||||
// body: this.sight.en.left.body,
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
// await languageInstance("zh").patch(
|
|
||||||
// `/sight/${this.sight.common.left_article}/article`,
|
|
||||||
// {
|
|
||||||
// heading: this.sight.zh.left.heading,
|
|
||||||
// body: this.sight.zh.left.body,
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
|
|
||||||
this.needLeaveAgree = false;
|
this.needLeaveAgree = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -400,16 +373,36 @@ class EditSightStore {
|
|||||||
};
|
};
|
||||||
|
|
||||||
createLeftArticle = async () => {
|
createLeftArticle = async () => {
|
||||||
|
const ruName = (this.sight.ru.name || "").trim();
|
||||||
|
const enName = (this.sight.en.name || "").trim();
|
||||||
|
const zhName = (this.sight.zh.name || "").trim();
|
||||||
|
const hasAnyName = !!(ruName || enName || zhName);
|
||||||
|
|
||||||
const response = await languageInstance("ru").post(`/article`, {
|
const response = await languageInstance("ru").post(`/article`, {
|
||||||
heading: "",
|
heading: hasAnyName ? ruName : "",
|
||||||
body: "",
|
body: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
this.sight.common.left_article = response.data.id;
|
this.sight.common.left_article = response.data.id;
|
||||||
|
|
||||||
this.sight.ru.left.heading = "";
|
await languageInstance("en").patch(
|
||||||
this.sight.en.left.heading = "";
|
`/article/${this.sight.common.left_article}`,
|
||||||
this.sight.zh.left.heading = "";
|
{
|
||||||
|
heading: hasAnyName ? enName : "",
|
||||||
|
body: "",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await languageInstance("zh").patch(
|
||||||
|
`/article/${this.sight.common.left_article}`,
|
||||||
|
{
|
||||||
|
heading: hasAnyName ? zhName : "",
|
||||||
|
body: "",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.sight.ru.left.heading = hasAnyName ? ruName : "";
|
||||||
|
this.sight.en.left.heading = hasAnyName ? enName : "";
|
||||||
|
this.sight.zh.left.heading = hasAnyName ? zhName : "";
|
||||||
this.sight.ru.left.body = "";
|
this.sight.ru.left.body = "";
|
||||||
this.sight.en.left.body = "";
|
this.sight.en.left.body = "";
|
||||||
this.sight.zh.left.body = "";
|
this.sight.zh.left.body = "";
|
||||||
@@ -569,7 +562,7 @@ class EditSightStore {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return article_id; // Return the linked article ID
|
return article_id;
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteRightArticleMedia = async (article_id: number, media_id: string) => {
|
deleteRightArticleMedia = async (article_id: number, media_id: string) => {
|
||||||
@@ -675,7 +668,7 @@ class EditSightStore {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return id; // Return the ID of the newly created article
|
return id;
|
||||||
};
|
};
|
||||||
|
|
||||||
createLinkWithRightArticle = async (
|
createLinkWithRightArticle = async (
|
||||||
@@ -750,7 +743,7 @@ class EditSightStore {
|
|||||||
|
|
||||||
this.sight.ru.right = sortArticles(this.sight.ru.right);
|
this.sight.ru.right = sortArticles(this.sight.ru.right);
|
||||||
this.sight.en.right = sortArticles(this.sight.en.right);
|
this.sight.en.right = sortArticles(this.sight.en.right);
|
||||||
this.sight.zh.right = sortArticles(this.sight.zh.right); // теперь zh тоже сортируется одинаково
|
this.sight.zh.right = sortArticles(this.sight.zh.right);
|
||||||
|
|
||||||
this.needLeaveAgree = true;
|
this.needLeaveAgree = true;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,10 +6,24 @@ class LanguageStore {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
|
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const storedLanguage = window.localStorage.getItem("appLanguage");
|
||||||
|
if (
|
||||||
|
storedLanguage &&
|
||||||
|
["ru", "en", "zh"].includes(storedLanguage.toLowerCase())
|
||||||
|
) {
|
||||||
|
this.language = storedLanguage.toLowerCase() as Language;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setLanguage = (language: Language) => {
|
setLanguage = (language: Language) => {
|
||||||
this.language = language;
|
this.language = language;
|
||||||
|
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
window.localStorage.setItem("appLanguage", language);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,12 +39,11 @@ class MediaStore {
|
|||||||
updateMedia = async (id: string, data: Partial<Media>) => {
|
updateMedia = async (id: string, data: Partial<Media>) => {
|
||||||
const response = await authInstance.patch(`/media/${id}`, data);
|
const response = await authInstance.patch(`/media/${id}`, data);
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
// Update in media array
|
|
||||||
const index = this.media.findIndex((m) => m.id === id);
|
const index = this.media.findIndex((m) => m.id === id);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
this.media[index] = { ...this.media[index], ...response.data };
|
this.media[index] = { ...this.media[index], ...response.data };
|
||||||
}
|
}
|
||||||
// Update oneMedia if it's the current media being viewed
|
|
||||||
if (this.oneMedia?.id === id) {
|
if (this.oneMedia?.id === id) {
|
||||||
this.oneMedia = { ...this.oneMedia, ...response.data };
|
this.oneMedia = { ...this.oneMedia, ...response.data };
|
||||||
}
|
}
|
||||||
@@ -64,12 +63,11 @@ class MediaStore {
|
|||||||
});
|
});
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
// Update in media array
|
|
||||||
const index = this.media.findIndex((m) => m.id === id);
|
const index = this.media.findIndex((m) => m.id === id);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
this.media[index] = { ...this.media[index], ...response.data };
|
this.media[index] = { ...this.media[index], ...response.data };
|
||||||
}
|
}
|
||||||
// Update oneMedia if it's the current media being viewed
|
|
||||||
if (this.oneMedia?.id === id) {
|
if (this.oneMedia?.id === id) {
|
||||||
this.oneMedia = { ...this.oneMedia, ...response.data };
|
this.oneMedia = { ...this.oneMedia, ...response.data };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ class ModelLoadingStore {
|
|||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Начать отслеживание загрузки модели
|
|
||||||
startLoading(modelId: string) {
|
startLoading(modelId: string) {
|
||||||
this.loadingStates.set(modelId, {
|
this.loadingStates.set(modelId, {
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
@@ -25,7 +24,6 @@ class ModelLoadingStore {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновить прогресс загрузки
|
|
||||||
updateProgress(modelId: string, progress: number) {
|
updateProgress(modelId: string, progress: number) {
|
||||||
const state = this.loadingStates.get(modelId);
|
const state = this.loadingStates.get(modelId);
|
||||||
if (state) {
|
if (state) {
|
||||||
@@ -33,7 +31,6 @@ class ModelLoadingStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Завершить загрузку модели
|
|
||||||
finishLoading(modelId: string) {
|
finishLoading(modelId: string) {
|
||||||
const state = this.loadingStates.get(modelId);
|
const state = this.loadingStates.get(modelId);
|
||||||
if (state) {
|
if (state) {
|
||||||
@@ -42,12 +39,10 @@ class ModelLoadingStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Остановить загрузку (в случае ошибки)
|
|
||||||
stopLoading(modelId: string) {
|
stopLoading(modelId: string) {
|
||||||
this.loadingStates.delete(modelId);
|
this.loadingStates.delete(modelId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обработать ошибку загрузки
|
|
||||||
handleError(modelId: string, error?: string) {
|
handleError(modelId: string, error?: string) {
|
||||||
const state = this.loadingStates.get(modelId);
|
const state = this.loadingStates.get(modelId);
|
||||||
if (state) {
|
if (state) {
|
||||||
@@ -56,26 +51,22 @@ class ModelLoadingStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Получить состояние загрузки для конкретной модели
|
|
||||||
getLoadingState(modelId: string): ModelLoadingState | undefined {
|
getLoadingState(modelId: string): ModelLoadingState | undefined {
|
||||||
return this.loadingStates.get(modelId);
|
return this.loadingStates.get(modelId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверить, загружается ли какая-либо модель
|
|
||||||
get isAnyModelLoading(): boolean {
|
get isAnyModelLoading(): boolean {
|
||||||
return Array.from(this.loadingStates.values()).some(
|
return Array.from(this.loadingStates.values()).some(
|
||||||
(state) => state.isLoading
|
(state) => state.isLoading
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Получить все загружающиеся модели
|
|
||||||
get loadingModels(): ModelLoadingState[] {
|
get loadingModels(): ModelLoadingState[] {
|
||||||
return Array.from(this.loadingStates.values()).filter(
|
return Array.from(this.loadingStates.values()).filter(
|
||||||
(state) => state.isLoading
|
(state) => state.isLoading
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Получить общий прогресс всех загружающихся моделей
|
|
||||||
get overallProgress(): number {
|
get overallProgress(): number {
|
||||||
const loadingModels = this.loadingModels;
|
const loadingModels = this.loadingModels;
|
||||||
if (loadingModels.length === 0) return 100;
|
if (loadingModels.length === 0) return 100;
|
||||||
@@ -87,12 +78,10 @@ class ModelLoadingStore {
|
|||||||
return Math.round(totalProgress / loadingModels.length);
|
return Math.round(totalProgress / loadingModels.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверить, заблокировано ли сохранение (есть ли загружающиеся модели)
|
|
||||||
get isSaveBlocked(): boolean {
|
get isSaveBlocked(): boolean {
|
||||||
return this.isAnyModelLoading;
|
return this.isAnyModelLoading;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Очистить все состояния загрузки
|
|
||||||
clearAll() {
|
clearAll() {
|
||||||
this.loadingStates.clear();
|
this.loadingStates.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { makeAutoObservable, runInAction } from "mobx";
|
|||||||
import { authInstance } from "@shared";
|
import { authInstance } from "@shared";
|
||||||
|
|
||||||
export type Route = {
|
export type Route = {
|
||||||
|
route_name: string;
|
||||||
carrier: string;
|
carrier: string;
|
||||||
carrier_id: number;
|
carrier_id: number;
|
||||||
center_latitude: number;
|
center_latitude: number;
|
||||||
@@ -97,6 +98,7 @@ class RouteStore {
|
|||||||
};
|
};
|
||||||
|
|
||||||
editRouteData = {
|
editRouteData = {
|
||||||
|
route_name: "",
|
||||||
carrier: "",
|
carrier: "",
|
||||||
carrier_id: 0,
|
carrier_id: 0,
|
||||||
center_latitude: "",
|
center_latitude: "",
|
||||||
@@ -110,7 +112,7 @@ class RouteStore {
|
|||||||
route_sys_number: "",
|
route_sys_number: "",
|
||||||
scale_max: 0,
|
scale_max: 0,
|
||||||
scale_min: 0,
|
scale_min: 0,
|
||||||
video_preview: "",
|
video_preview: "" as string | undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
setEditRouteData = (data: any) => {
|
setEditRouteData = (data: any) => {
|
||||||
@@ -118,6 +120,9 @@ class RouteStore {
|
|||||||
};
|
};
|
||||||
|
|
||||||
editRoute = async (id: number) => {
|
editRoute = async (id: number) => {
|
||||||
|
if (!this.editRouteData.video_preview) {
|
||||||
|
delete this.editRouteData.video_preview;
|
||||||
|
}
|
||||||
const response = await authInstance.patch(`/route/${id}`, {
|
const response = await authInstance.patch(`/route/${id}`, {
|
||||||
...this.editRouteData,
|
...this.editRouteData,
|
||||||
center_latitude: parseFloat(this.editRouteData.center_latitude),
|
center_latitude: parseFloat(this.editRouteData.center_latitude),
|
||||||
|
|||||||
@@ -58,41 +58,6 @@ class SightsStore {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// getSight = async (id: number) => {
|
|
||||||
// const response = await authInstance.get(`/sight/${id}`);
|
|
||||||
|
|
||||||
// runInAction(() => {
|
|
||||||
// this.sight = response.data;
|
|
||||||
// editSightStore.sightInfo = {
|
|
||||||
// ...editSightStore.sightInfo,
|
|
||||||
// id: response.data.id,
|
|
||||||
// city_id: response.data.city_id,
|
|
||||||
// city: response.data.city,
|
|
||||||
// latitude: response.data.latitude,
|
|
||||||
// longitude: response.data.longitude,
|
|
||||||
// thumbnail: response.data.thumbnail,
|
|
||||||
// watermark_lu: response.data.watermark_lu,
|
|
||||||
// watermark_rd: response.data.watermark_rd,
|
|
||||||
// left_article: response.data.left_article,
|
|
||||||
// preview_media: response.data.preview_media,
|
|
||||||
// video_preview: response.data.video_preview,
|
|
||||||
|
|
||||||
// [languageStore.language]: {
|
|
||||||
// info: {
|
|
||||||
// name: response.data.name,
|
|
||||||
// address: response.data.address,
|
|
||||||
// },
|
|
||||||
// left: {
|
|
||||||
// heading: articlesStore.articles[languageStore.language].find(
|
|
||||||
// (article) => article.id === response.data.left_article
|
|
||||||
// )?.heading,
|
|
||||||
// body: articlesStore.articles[languageStore.language].find(
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// };
|
|
||||||
// });
|
|
||||||
// };
|
|
||||||
|
|
||||||
createSightAction = async (
|
createSightAction = async (
|
||||||
city: number,
|
city: number,
|
||||||
coordinates: { latitude: number; longitude: number }
|
coordinates: { latitude: number; longitude: number }
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { authInstance } from "@shared";
|
import { authInstance } from "@shared";
|
||||||
|
|
||||||
import { makeAutoObservable, runInAction } from "mobx";
|
import { makeAutoObservable, runInAction } from "mobx";
|
||||||
// Импорт функции сброса кешей карты
|
|
||||||
// import { clearMapCaches } from "../../pages/MapPage";
|
|
||||||
import {
|
import {
|
||||||
articlesStore,
|
articlesStore,
|
||||||
cityStore,
|
cityStore,
|
||||||
@@ -35,9 +33,7 @@ class SnapshotStore {
|
|||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Функция для сброса всех кешей в приложении
|
|
||||||
private clearAllCaches = () => {
|
private clearAllCaches = () => {
|
||||||
// Сброс кешей статей
|
|
||||||
articlesStore.articleList = {
|
articlesStore.articleList = {
|
||||||
ru: { data: [], loaded: false },
|
ru: { data: [], loaded: false },
|
||||||
en: { data: [], loaded: false },
|
en: { data: [], loaded: false },
|
||||||
@@ -47,7 +43,6 @@ class SnapshotStore {
|
|||||||
articlesStore.articleData = null;
|
articlesStore.articleData = null;
|
||||||
articlesStore.articleMedia = null;
|
articlesStore.articleMedia = null;
|
||||||
|
|
||||||
// Сброс кешей городов
|
|
||||||
cityStore.cities = {
|
cityStore.cities = {
|
||||||
ru: { data: [], loaded: false },
|
ru: { data: [], loaded: false },
|
||||||
en: { data: [], loaded: false },
|
en: { data: [], loaded: false },
|
||||||
@@ -56,21 +51,18 @@ class SnapshotStore {
|
|||||||
cityStore.ruCities = { data: [], loaded: false };
|
cityStore.ruCities = { data: [], loaded: false };
|
||||||
cityStore.city = {};
|
cityStore.city = {};
|
||||||
|
|
||||||
// Сброс кешей стран
|
|
||||||
countryStore.countries = {
|
countryStore.countries = {
|
||||||
ru: { data: [], loaded: false },
|
ru: { data: [], loaded: false },
|
||||||
en: { data: [], loaded: false },
|
en: { data: [], loaded: false },
|
||||||
zh: { data: [], loaded: false },
|
zh: { data: [], loaded: false },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Сброс кешей перевозчиков
|
|
||||||
carrierStore.carriers = {
|
carrierStore.carriers = {
|
||||||
ru: { data: [], loaded: false },
|
ru: { data: [], loaded: false },
|
||||||
en: { data: [], loaded: false },
|
en: { data: [], loaded: false },
|
||||||
zh: { data: [], loaded: false },
|
zh: { data: [], loaded: false },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Сброс кешей станций
|
|
||||||
stationsStore.stationLists = {
|
stationsStore.stationLists = {
|
||||||
ru: { data: [], loaded: false },
|
ru: { data: [], loaded: false },
|
||||||
en: { data: [], loaded: false },
|
en: { data: [], loaded: false },
|
||||||
@@ -78,24 +70,18 @@ class SnapshotStore {
|
|||||||
};
|
};
|
||||||
stationsStore.stationPreview = {};
|
stationsStore.stationPreview = {};
|
||||||
|
|
||||||
// Сброс кешей достопримечательностей
|
|
||||||
sightsStore.sights = [];
|
sightsStore.sights = [];
|
||||||
sightsStore.sight = null;
|
sightsStore.sight = null;
|
||||||
|
|
||||||
// Сброс кешей маршрутов
|
|
||||||
routeStore.routes = { data: [], loaded: false };
|
routeStore.routes = { data: [], loaded: false };
|
||||||
|
|
||||||
// Сброс кешей транспорта
|
|
||||||
vehicleStore.vehicles = { data: [], loaded: false };
|
vehicleStore.vehicles = { data: [], loaded: false };
|
||||||
|
|
||||||
// Сброс кешей пользователей
|
|
||||||
userStore.users = { data: [], loaded: false };
|
userStore.users = { data: [], loaded: false };
|
||||||
|
|
||||||
// Сброс кешей медиа
|
|
||||||
mediaStore.media = [];
|
mediaStore.media = [];
|
||||||
mediaStore.oneMedia = null;
|
mediaStore.oneMedia = null;
|
||||||
|
|
||||||
// Сброс кешей создания и редактирования достопримечательностей
|
|
||||||
createSightStore.sight = JSON.parse(
|
createSightStore.sight = JSON.parse(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
city_id: 0,
|
city_id: 0,
|
||||||
@@ -173,26 +159,21 @@ class SnapshotStore {
|
|||||||
editSightStore.fileToUpload = null;
|
editSightStore.fileToUpload = null;
|
||||||
editSightStore.needLeaveAgree = false;
|
editSightStore.needLeaveAgree = false;
|
||||||
|
|
||||||
// Сброс кешей устройств
|
|
||||||
devicesStore.devices = [];
|
devicesStore.devices = [];
|
||||||
devicesStore.uuid = null;
|
devicesStore.uuid = null;
|
||||||
devicesStore.sendSnapshotModalOpen = false;
|
devicesStore.sendSnapshotModalOpen = false;
|
||||||
|
|
||||||
// Сброс кешей авторизации (кроме токена)
|
|
||||||
authStore.payload = null;
|
authStore.payload = null;
|
||||||
authStore.error = null;
|
authStore.error = null;
|
||||||
authStore.isLoading = false;
|
authStore.isLoading = false;
|
||||||
|
|
||||||
// Сброс кешей карты (если они загружены)
|
|
||||||
try {
|
try {
|
||||||
// Сбрасываем кеши mapStore если он доступен
|
|
||||||
if (typeof window !== "undefined" && (window as any).mapStore) {
|
if (typeof window !== "undefined" && (window as any).mapStore) {
|
||||||
(window as any).mapStore.routes = [];
|
(window as any).mapStore.routes = [];
|
||||||
(window as any).mapStore.stations = [];
|
(window as any).mapStore.stations = [];
|
||||||
(window as any).mapStore.sights = [];
|
(window as any).mapStore.sights = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сбрасываем кеши MapService если он доступен
|
|
||||||
if (typeof window !== "undefined" && (window as any).mapServiceInstance) {
|
if (typeof window !== "undefined" && (window as any).mapServiceInstance) {
|
||||||
(window as any).mapServiceInstance.clearCaches();
|
(window as any).mapServiceInstance.clearCaches();
|
||||||
}
|
}
|
||||||
@@ -200,7 +181,6 @@ class SnapshotStore {
|
|||||||
console.warn("Не удалось сбросить кеши карты:", error);
|
console.warn("Не удалось сбросить кеши карты:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сброс localStorage кешей (кроме токена авторизации)
|
|
||||||
const token = localStorage.getItem("token");
|
const token = localStorage.getItem("token");
|
||||||
const rememberedEmail = localStorage.getItem("rememberedEmail");
|
const rememberedEmail = localStorage.getItem("rememberedEmail");
|
||||||
const rememberedPassword = localStorage.getItem("rememberedPassword");
|
const rememberedPassword = localStorage.getItem("rememberedPassword");
|
||||||
@@ -208,14 +188,12 @@ class SnapshotStore {
|
|||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
sessionStorage.clear();
|
sessionStorage.clear();
|
||||||
|
|
||||||
// Восстанавливаем важные данные
|
|
||||||
if (token) localStorage.setItem("token", token);
|
if (token) localStorage.setItem("token", token);
|
||||||
if (rememberedEmail)
|
if (rememberedEmail)
|
||||||
localStorage.setItem("rememberedEmail", rememberedEmail);
|
localStorage.setItem("rememberedEmail", rememberedEmail);
|
||||||
if (rememberedPassword)
|
if (rememberedPassword)
|
||||||
localStorage.setItem("rememberedPassword", rememberedPassword);
|
localStorage.setItem("rememberedPassword", rememberedPassword);
|
||||||
|
|
||||||
// Сброс кешей карты (если они есть)
|
|
||||||
const mapPositionKey = "mapPosition";
|
const mapPositionKey = "mapPosition";
|
||||||
const activeSectionKey = "mapActiveSection";
|
const activeSectionKey = "mapActiveSection";
|
||||||
if (localStorage.getItem(mapPositionKey)) {
|
if (localStorage.getItem(mapPositionKey)) {
|
||||||
@@ -225,7 +203,6 @@ class SnapshotStore {
|
|||||||
localStorage.removeItem(activeSectionKey);
|
localStorage.removeItem(activeSectionKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Попытка очистить кеш браузера (если поддерживается)
|
|
||||||
if ("caches" in window) {
|
if ("caches" in window) {
|
||||||
try {
|
try {
|
||||||
caches.keys().then((cacheNames) => {
|
caches.keys().then((cacheNames) => {
|
||||||
@@ -240,7 +217,6 @@ class SnapshotStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Попытка очистить IndexedDB (если поддерживается)
|
|
||||||
if ("indexedDB" in window) {
|
if ("indexedDB" in window) {
|
||||||
try {
|
try {
|
||||||
indexedDB.databases().then((databases) => {
|
indexedDB.databases().then((databases) => {
|
||||||
@@ -284,10 +260,8 @@ class SnapshotStore {
|
|||||||
};
|
};
|
||||||
|
|
||||||
restoreSnapshot = async (id: string) => {
|
restoreSnapshot = async (id: string) => {
|
||||||
// Сначала сбрасываем все кеши
|
|
||||||
this.clearAllCaches();
|
this.clearAllCaches();
|
||||||
|
|
||||||
// Затем восстанавливаем снапшот
|
|
||||||
await authInstance.post(`/snapshots/${id}/restore`);
|
await authInstance.post(`/snapshots/${id}/restore`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ type StationLanguageData = {
|
|||||||
name: string;
|
name: string;
|
||||||
system_name: string;
|
system_name: string;
|
||||||
address: string;
|
address: string;
|
||||||
loaded: boolean; // Indicates if this language's data has been loaded/modified
|
loaded: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StationCommonData = {
|
type StationCommonData = {
|
||||||
@@ -92,7 +92,6 @@ class StationsStore {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// This will store the full station data, keyed by ID and then by language
|
|
||||||
stationPreview: Record<
|
stationPreview: Record<
|
||||||
string,
|
string,
|
||||||
Record<string, { loaded: boolean; data: Station }>
|
Record<string, { loaded: boolean; data: Station }>
|
||||||
@@ -264,7 +263,6 @@ class StationsStore {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sets language-specific station data
|
|
||||||
setLanguageEditStationData = (
|
setLanguageEditStationData = (
|
||||||
language: Language,
|
language: Language,
|
||||||
data: Partial<StationLanguageData>
|
data: Partial<StationLanguageData>
|
||||||
@@ -295,7 +293,7 @@ class StationsStore {
|
|||||||
`/station/${id}`,
|
`/station/${id}`,
|
||||||
{
|
{
|
||||||
name: name || "",
|
name: name || "",
|
||||||
system_name: name || "", // system_name is often derived from name
|
system_name: name || "",
|
||||||
description: description || "",
|
description: description || "",
|
||||||
address: address || "",
|
address: address || "",
|
||||||
...commonDataPayload,
|
...commonDataPayload,
|
||||||
@@ -303,7 +301,6 @@ class StationsStore {
|
|||||||
);
|
);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
// Update the cached preview data and station lists after successful patch
|
|
||||||
if (this.stationPreview[id]) {
|
if (this.stationPreview[id]) {
|
||||||
this.stationPreview[id][language] = {
|
this.stationPreview[id][language] = {
|
||||||
loaded: true,
|
loaded: true,
|
||||||
@@ -343,11 +340,11 @@ class StationsStore {
|
|||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.stations = this.stations.filter((station) => station.id !== id);
|
this.stations = this.stations.filter((station) => station.id !== id);
|
||||||
// Also clear from stationPreview cache
|
|
||||||
if (this.stationPreview[id]) {
|
if (this.stationPreview[id]) {
|
||||||
delete this.stationPreview[id];
|
delete this.stationPreview[id];
|
||||||
}
|
}
|
||||||
// Clear from stationLists as well for all languages
|
|
||||||
for (const lang of ["ru", "en", "zh"] as const) {
|
for (const lang of ["ru", "en", "zh"] as const) {
|
||||||
if (this.stationLists[lang].data) {
|
if (this.stationLists[lang].data) {
|
||||||
this.stationLists[lang].data = this.stationLists[lang].data.filter(
|
this.stationLists[lang].data = this.stationLists[lang].data.filter(
|
||||||
@@ -421,12 +418,11 @@ class StationsStore {
|
|||||||
delete commonDataPayload.icon;
|
delete commonDataPayload.icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
// First create station in Russian
|
|
||||||
const { name, address } = this.createStationData[language];
|
const { name, address } = this.createStationData[language];
|
||||||
const description = this.createStationData.common.description;
|
const description = this.createStationData.common.description;
|
||||||
const response = await languageInstance(language).post("/station", {
|
const response = await languageInstance(language).post("/station", {
|
||||||
name: name || "",
|
name: name || "",
|
||||||
system_name: name || "", // system_name is often derived from name
|
system_name: name || "",
|
||||||
description: description || "",
|
description: description || "",
|
||||||
address: address || "",
|
address: address || "",
|
||||||
...commonDataPayload,
|
...commonDataPayload,
|
||||||
@@ -438,7 +434,6 @@ class StationsStore {
|
|||||||
|
|
||||||
const stationId = response.data.id;
|
const stationId = response.data.id;
|
||||||
|
|
||||||
// Then update for other languages
|
|
||||||
for (const lang of ["ru", "en", "zh"].filter(
|
for (const lang of ["ru", "en", "zh"].filter(
|
||||||
(lang) => lang !== language
|
(lang) => lang !== language
|
||||||
) as Language[]) {
|
) as Language[]) {
|
||||||
@@ -448,7 +443,7 @@ class StationsStore {
|
|||||||
`/station/${stationId}`,
|
`/station/${stationId}`,
|
||||||
{
|
{
|
||||||
name: name || "",
|
name: name || "",
|
||||||
system_name: name || "", // system_name is often derived from name
|
system_name: name || "",
|
||||||
description: description || "",
|
description: description || "",
|
||||||
address: address || "",
|
address: address || "",
|
||||||
...commonDataPayload,
|
...commonDataPayload,
|
||||||
@@ -507,7 +502,6 @@ class StationsStore {
|
|||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reset editStationData when navigating away or after saving
|
|
||||||
resetEditStationData = () => {
|
resetEditStationData = () => {
|
||||||
this.editStationData = {
|
this.editStationData = {
|
||||||
ru: {
|
ru: {
|
||||||
|
|||||||
171
src/shared/ui/AnimatedCircleButton.tsx
Normal file
171
src/shared/ui/AnimatedCircleButton.tsx
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import { forwardRef } from "react";
|
||||||
|
import { Button, ButtonProps, CircularProgress } from "@mui/material";
|
||||||
|
import { alpha, keyframes, styled } from "@mui/material/styles";
|
||||||
|
import type { Theme } from "@mui/material/styles";
|
||||||
|
|
||||||
|
type AnimatedCircleButtonProps = ButtonProps & {
|
||||||
|
disableAnimation?: boolean;
|
||||||
|
loading?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StyledButtonProps = AnimatedCircleButtonProps & { theme: Theme };
|
||||||
|
|
||||||
|
const loadingPulse = keyframes`
|
||||||
|
0% {
|
||||||
|
transform: translate(-50%, -50%) scale(0.6);
|
||||||
|
opacity: 0.35;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translate(-50%, -50%) scale(1.45);
|
||||||
|
opacity: 0.15;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(-50%, -50%) scale(0.6);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledButton = styled(Button, {
|
||||||
|
shouldForwardProp: (prop) =>
|
||||||
|
prop !== "disableAnimation" && prop !== "loading",
|
||||||
|
})<AnimatedCircleButtonProps>((props: StyledButtonProps) => {
|
||||||
|
const {
|
||||||
|
theme,
|
||||||
|
disableAnimation = false,
|
||||||
|
color,
|
||||||
|
variant = "text",
|
||||||
|
disabled = false,
|
||||||
|
loading = false,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const shouldAnimate = !disableAnimation && (!disabled || loading);
|
||||||
|
const pointerBlocked = loading;
|
||||||
|
|
||||||
|
const paletteMainMap: Record<string, string> = {
|
||||||
|
primary: theme.palette.primary.main,
|
||||||
|
secondary: theme.palette.secondary.main,
|
||||||
|
error: theme.palette.error.main,
|
||||||
|
warning: theme.palette.warning.main,
|
||||||
|
info: theme.palette.info.main,
|
||||||
|
success: theme.palette.success.main,
|
||||||
|
inherit: theme.palette.primary.main,
|
||||||
|
};
|
||||||
|
|
||||||
|
const paletteMain =
|
||||||
|
(color && paletteMainMap[String(color)]) ?? theme.palette.primary.main;
|
||||||
|
|
||||||
|
const pulseColor =
|
||||||
|
variant === "outlined" || variant === "text"
|
||||||
|
? alpha(paletteMain, 0.18)
|
||||||
|
: alpha(paletteMain, 0.3);
|
||||||
|
|
||||||
|
return {
|
||||||
|
position: "relative",
|
||||||
|
overflow: "hidden",
|
||||||
|
borderRadius: 5,
|
||||||
|
zIndex: 0,
|
||||||
|
transition: "transform 0.2s ease, box-shadow 0.2s ease",
|
||||||
|
pointerEvents: pointerBlocked ? "none" : undefined,
|
||||||
|
"&::after": shouldAnimate
|
||||||
|
? {
|
||||||
|
content: '""',
|
||||||
|
position: "absolute",
|
||||||
|
width: "12px",
|
||||||
|
height: "12px",
|
||||||
|
backgroundColor: pulseColor,
|
||||||
|
borderRadius: "50%",
|
||||||
|
top: "50%",
|
||||||
|
left: "50%",
|
||||||
|
pointerEvents: "none",
|
||||||
|
zIndex: 0,
|
||||||
|
...(loading
|
||||||
|
? {
|
||||||
|
opacity: 0.35,
|
||||||
|
transform: "translate(-50%, -50%) scale(0.6)",
|
||||||
|
animation: `${loadingPulse} 1.2s ease-in-out infinite`,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
opacity: 0,
|
||||||
|
transform: "translate(-50%, -50%) scale(0)",
|
||||||
|
transition: "transform 0.45s ease, opacity 0.45s ease",
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
...(loading
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
"&:hover": {
|
||||||
|
transform: "translateY(-1px)",
|
||||||
|
boxShadow: theme.shadows[4],
|
||||||
|
},
|
||||||
|
"&:hover::after": shouldAnimate
|
||||||
|
? {
|
||||||
|
transform: "translate(-50%, -50%) scale(15)",
|
||||||
|
opacity: 1,
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
"&:active": {
|
||||||
|
transform: "translateY(0)",
|
||||||
|
boxShadow: theme.shadows[2],
|
||||||
|
},
|
||||||
|
"&:active::after": shouldAnimate
|
||||||
|
? {
|
||||||
|
transform: "translate(-50%, -50%) scale(18)",
|
||||||
|
opacity: 0.4,
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
}),
|
||||||
|
"&.Mui-disabled": {
|
||||||
|
boxShadow: "none",
|
||||||
|
transform: "none",
|
||||||
|
...(loading && shouldAnimate
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
"&::after": {
|
||||||
|
opacity: 0,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
...(disabled && {
|
||||||
|
boxShadow: "none",
|
||||||
|
transform: "none",
|
||||||
|
}),
|
||||||
|
"& > *": {
|
||||||
|
position: "relative",
|
||||||
|
zIndex: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AnimatedCircleButton = forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
AnimatedCircleButtonProps
|
||||||
|
>((props, ref) => {
|
||||||
|
const {
|
||||||
|
loading = false,
|
||||||
|
disabled,
|
||||||
|
children,
|
||||||
|
startIcon,
|
||||||
|
endIcon,
|
||||||
|
...rest
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const effectiveStartIcon = loading ? (
|
||||||
|
<CircularProgress size={16} color="inherit" />
|
||||||
|
) : (
|
||||||
|
startIcon
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledButton
|
||||||
|
ref={ref}
|
||||||
|
loading={loading}
|
||||||
|
disabled={loading ? true : disabled}
|
||||||
|
startIcon={effectiveStartIcon}
|
||||||
|
endIcon={loading ? undefined : endIcon}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</StyledButton>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -2,3 +2,4 @@ export * from "./TabPanel";
|
|||||||
export * from "./BackButton";
|
export * from "./BackButton";
|
||||||
export * from "./Modal";
|
export * from "./Modal";
|
||||||
export * from "./CoordinatesInput";
|
export * from "./CoordinatesInput";
|
||||||
|
export * from "./AnimatedCircleButton";
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import {
|
|||||||
devicesStore,
|
devicesStore,
|
||||||
Modal,
|
Modal,
|
||||||
snapshotStore,
|
snapshotStore,
|
||||||
vehicleStore, // Not directly used in this component's rendering logic anymore
|
vehicleStore,
|
||||||
} from "@shared"; // Assuming @shared exports these
|
} from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Button, Checkbox, Typography } from "@mui/material";
|
import { Button, Checkbox, Typography } from "@mui/material";
|
||||||
@@ -23,12 +23,10 @@ import { useNavigate } from "react-router-dom";
|
|||||||
export type ConnectedDevice = string;
|
export type ConnectedDevice = string;
|
||||||
|
|
||||||
interface Snapshot {
|
interface Snapshot {
|
||||||
ID: string; // Assuming ID is string based on usage
|
ID: string;
|
||||||
Name: string;
|
Name: string;
|
||||||
// Add other snapshot properties if needed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- HELPER FUNCTIONS ---
|
|
||||||
const formatDate = (dateString: string | null) => {
|
const formatDate = (dateString: string | null) => {
|
||||||
if (!dateString) return "Нет данных";
|
if (!dateString) return "Нет данных";
|
||||||
try {
|
try {
|
||||||
@@ -76,12 +74,7 @@ function createData(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function transforms the raw device data (which includes vehicle and device_status)
|
const transformDevicesToRows = (vehicles: Vehicle[]): TableRowData[] => {
|
||||||
// into the format expected by the table. It now filters for devices that have a UUID.
|
|
||||||
const transformDevicesToRows = (
|
|
||||||
vehicles: Vehicle[]
|
|
||||||
// devices: ConnectedDevice[]
|
|
||||||
): TableRowData[] => {
|
|
||||||
return vehicles.map((vehicle) => {
|
return vehicles.map((vehicle) => {
|
||||||
const uuid = vehicle.vehicle.uuid;
|
const uuid = vehicle.vehicle.uuid;
|
||||||
if (!uuid)
|
if (!uuid)
|
||||||
@@ -115,26 +108,21 @@ export const DevicesTable = observer(() => {
|
|||||||
} = devicesStore;
|
} = devicesStore;
|
||||||
|
|
||||||
const { snapshots, getSnapshots } = snapshotStore;
|
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 { devices } = devicesStore;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [selectedDeviceUuids, setSelectedDeviceUuids] = useState<string[]>([]);
|
const [selectedDeviceUuids, setSelectedDeviceUuids] = useState<string[]>([]);
|
||||||
|
|
||||||
// Transform the raw devices data into rows suitable for the table
|
const currentTableRows = transformDevicesToRows(vehicles.data as Vehicle[]);
|
||||||
// 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[]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
await getVehicles(); // Not strictly needed if devicesStore.devices is populated correctly by getDevices
|
await getVehicles();
|
||||||
await getDevices(); // This should fetch the combined vehicle/device_status data
|
await getDevices();
|
||||||
await getSnapshots();
|
await getSnapshots();
|
||||||
};
|
};
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [getDevices, getSnapshots]); // Added dependencies
|
}, [getDevices, getSnapshots]);
|
||||||
|
|
||||||
const isAllSelected =
|
const isAllSelected =
|
||||||
currentTableRows.length > 0 &&
|
currentTableRows.length > 0 &&
|
||||||
@@ -144,7 +132,6 @@ export const DevicesTable = observer(() => {
|
|||||||
if (isAllSelected) {
|
if (isAllSelected) {
|
||||||
setSelectedDeviceUuids([]);
|
setSelectedDeviceUuids([]);
|
||||||
} else {
|
} else {
|
||||||
// Select all device UUIDs from the *currently visible and selectable* rows
|
|
||||||
setSelectedDeviceUuids(
|
setSelectedDeviceUuids(
|
||||||
currentTableRows.map((row) => row.device_uuid ?? "")
|
currentTableRows.map((row) => row.device_uuid ?? "")
|
||||||
);
|
);
|
||||||
@@ -171,14 +158,13 @@ export const DevicesTable = observer(() => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleReloadStatus = async (uuid: string) => {
|
const handleReloadStatus = async (uuid: string) => {
|
||||||
setSelectedDevice(uuid); // Sets the device in the store, useful for context elsewhere
|
setSelectedDevice(uuid);
|
||||||
try {
|
try {
|
||||||
await authInstance.post(`/devices/${uuid}/request-status`);
|
await authInstance.post(`/devices/${uuid}/request-status`);
|
||||||
await getVehicles();
|
await getVehicles();
|
||||||
await getDevices(); // Refresh devices to show updated status
|
await getDevices();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error requesting status for device ${uuid}:`, 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 {
|
try {
|
||||||
// Create an array of promises for all snapshot requests
|
|
||||||
const snapshotPromises = selectedDeviceUuids.map((deviceUuid) => {
|
const snapshotPromises = selectedDeviceUuids.map((deviceUuid) => {
|
||||||
return send(deviceUuid);
|
return send(deviceUuid);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wait for all promises to settle (either resolve or reject)
|
|
||||||
await Promise.allSettled(snapshotPromises);
|
await Promise.allSettled(snapshotPromises);
|
||||||
|
|
||||||
// After all requests are attempted
|
await getDevices();
|
||||||
await getDevices(); // Refresh the device list
|
setSelectedDeviceUuids([]);
|
||||||
setSelectedDeviceUuids([]); // Clear the selection
|
toggleSendSnapshotModal();
|
||||||
toggleSendSnapshotModal(); // Close the modal
|
|
||||||
} catch (error) {
|
} 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);
|
console.error("Error in snapshot sending process:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -235,7 +215,7 @@ export const DevicesTable = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end p-3 gap-2">
|
<div className="flex justify-end p-3 gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outlined" // Changed to outlined for distinction
|
variant="outlined"
|
||||||
onClick={handleSelectAllDevices}
|
onClick={handleSelectAllDevices}
|
||||||
size="small"
|
size="small"
|
||||||
>
|
>
|
||||||
@@ -286,7 +266,6 @@ export const DevicesTable = observer(() => {
|
|||||||
)}
|
)}
|
||||||
selected={selectedDeviceUuids.includes(row.device_uuid ?? "")}
|
selected={selectedDeviceUuids.includes(row.device_uuid ?? "")}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
// Allow clicking row to toggle checkbox, if not clicking on button
|
|
||||||
if (
|
if (
|
||||||
(event.target as HTMLElement).closest("button") === null &&
|
(event.target as HTMLElement).closest("button") === null &&
|
||||||
(event.target as HTMLElement).closest(
|
(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) {
|
if (!event.shiftKey) {
|
||||||
handleSelectDevice(
|
handleSelectDevice(
|
||||||
{
|
{
|
||||||
@@ -317,7 +295,7 @@ export const DevicesTable = observer(() => {
|
|||||||
row.device_uuid ?? ""
|
row.device_uuid ?? ""
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
} as React.ChangeEvent<HTMLInputElement>, // Simulate event
|
} as React.ChangeEvent<HTMLInputElement>,
|
||||||
row.device_uuid ?? ""
|
row.device_uuid ?? ""
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -445,7 +423,7 @@ export const DevicesTable = observer(() => {
|
|||||||
</strong>
|
</strong>
|
||||||
</Typography>
|
</Typography>
|
||||||
<div className="mt-2 flex flex-col gap-2 max-h-[300px] overflow-y-auto pr-2">
|
<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) => (
|
(snapshots as Snapshot[]).map((snapshot) => (
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useRef, useState, DragEvent, useEffect } from "react";
|
import React, { useRef, 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"; // Assuming lucide-react for icons
|
import { X, Info, Plus } from "lucide-react";
|
||||||
import { editSightStore } from "@shared";
|
import { editSightStore } from "@shared";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
interface ImageUploadCardProps {
|
interface ImageUploadCardProps {
|
||||||
@@ -27,18 +27,9 @@ export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
|
|||||||
tooltipText,
|
tooltipText,
|
||||||
}) => {
|
}) => {
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [isDragOver, setIsDragOver] = useState(false);
|
|
||||||
const { setFileToUpload } = editSightStore;
|
const { setFileToUpload } = editSightStore;
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isDragOver) {
|
|
||||||
console.log("isDragOver");
|
|
||||||
}
|
|
||||||
}, [isDragOver]);
|
|
||||||
|
|
||||||
// --- Click to select file ---
|
|
||||||
const handleZoneClick = () => {
|
const handleZoneClick = () => {
|
||||||
// Trigger the hidden file input click
|
|
||||||
fileInputRef.current?.click();
|
fileInputRef.current?.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -59,28 +50,25 @@ export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
|
|||||||
toast.error("Пожалуйста, выберите изображение");
|
toast.error("Пожалуйста, выберите изображение");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Reset the input value so selecting the same file again triggers change
|
|
||||||
event.target.value = "";
|
event.target.value = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
const token = localStorage.getItem("token");
|
const token = localStorage.getItem("token");
|
||||||
// --- Drag and Drop Handlers ---
|
|
||||||
const handleDragOver = (event: DragEvent<HTMLDivElement>) => {
|
const handleDragOver = (event: DragEvent<HTMLDivElement>) => {
|
||||||
event.preventDefault(); // Crucial to allow a drop
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
setIsDragOver(true);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDragLeave = (event: DragEvent<HTMLDivElement>) => {
|
const handleDragLeave = (event: DragEvent<HTMLDivElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
setIsDragOver(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDrop = async (event: DragEvent<HTMLDivElement>) => {
|
const handleDrop = async (event: DragEvent<HTMLDivElement>) => {
|
||||||
event.preventDefault(); // Crucial to allow a drop
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
setIsDragOver(false);
|
|
||||||
|
|
||||||
const files = event.dataTransfer.files;
|
const files = event.dataTransfer.files;
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
@@ -132,7 +120,6 @@ export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
|
|||||||
cursor: imageUrl ? "pointer" : "default",
|
cursor: imageUrl ? "pointer" : "default",
|
||||||
}}
|
}}
|
||||||
onClick={onImageClick}
|
onClick={onImageClick}
|
||||||
// Removed onClick on the main Box to avoid conflicts
|
|
||||||
>
|
>
|
||||||
{imageUrl && (
|
{imageUrl && (
|
||||||
<button
|
<button
|
||||||
@@ -165,7 +152,7 @@ export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
|
|||||||
borderRadius: 1,
|
borderRadius: 1,
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
}}
|
}}
|
||||||
onClick={handleZoneClick} // Click handler for the zone
|
onClick={handleZoneClick}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
@@ -179,8 +166,8 @@ export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
|
|||||||
color="primary"
|
color="primary"
|
||||||
startIcon={<Plus color="white" size={18} />}
|
startIcon={<Plus color="white" size={18} />}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation(); // Prevent `handleZoneClick` from firing
|
e.stopPropagation();
|
||||||
onSelectFileClick(); // This button might trigger a different modal
|
onSelectFileClick();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Выбрать файл
|
Выбрать файл
|
||||||
@@ -191,7 +178,7 @@ export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
|
|||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
onChange={handleFileInputChange}
|
onChange={handleFileInputChange}
|
||||||
style={{ display: "none" }}
|
style={{ display: "none" }}
|
||||||
accept="image/*" // Accept only image files
|
accept="image/*"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,11 +3,9 @@ import { OrbitControls, Stage, useGLTF } from "@react-three/drei";
|
|||||||
import { useEffect, Suspense } from "react";
|
import { useEffect, Suspense } from "react";
|
||||||
import { Box, CircularProgress, Typography } from "@mui/material";
|
import { Box, CircularProgress, Typography } from "@mui/material";
|
||||||
|
|
||||||
// Утилита для очистки кеша GLTF
|
|
||||||
const clearGLTFCache = (url?: string) => {
|
const clearGLTFCache = (url?: string) => {
|
||||||
try {
|
try {
|
||||||
if (url) {
|
if (url) {
|
||||||
// Если это blob URL, очищаем его из кеша
|
|
||||||
if (url.startsWith("blob:")) {
|
if (url.startsWith("blob:")) {
|
||||||
useGLTF.clear(url);
|
useGLTF.clear(url);
|
||||||
} else {
|
} else {
|
||||||
@@ -19,29 +17,23 @@ const clearGLTFCache = (url?: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Утилита для проверки типа файла
|
|
||||||
const isValid3DFile = (url: string): boolean => {
|
const isValid3DFile = (url: string): boolean => {
|
||||||
try {
|
try {
|
||||||
const urlObj = new URL(url);
|
const urlObj = new URL(url);
|
||||||
const pathname = urlObj.pathname.toLowerCase();
|
const pathname = urlObj.pathname.toLowerCase();
|
||||||
const searchParams = urlObj.searchParams;
|
const searchParams = urlObj.searchParams;
|
||||||
|
|
||||||
// Проверяем расширение файла в пути
|
|
||||||
const validExtensions = [".glb", ".gltf"];
|
const validExtensions = [".glb", ".gltf"];
|
||||||
const hasValidExtension = validExtensions.some((ext) =>
|
const hasValidExtension = validExtensions.some((ext) =>
|
||||||
pathname.endsWith(ext)
|
pathname.endsWith(ext)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Проверяем параметры запроса на наличие типа файла
|
|
||||||
const fileType = searchParams.get("type") || searchParams.get("format");
|
const fileType = searchParams.get("type") || searchParams.get("format");
|
||||||
const hasValidType =
|
const hasValidType =
|
||||||
fileType && ["glb", "gltf"].includes(fileType.toLowerCase());
|
fileType && ["glb", "gltf"].includes(fileType.toLowerCase());
|
||||||
|
|
||||||
// Если это blob URL, считаем его валидным (пользователь выбрал файл)
|
|
||||||
const isBlobUrl = url.startsWith("blob:");
|
const isBlobUrl = url.startsWith("blob:");
|
||||||
|
|
||||||
// Если это URL с токеном и нет явного расширения, считаем валидным
|
|
||||||
// (предполагаем что сервер вернет правильный файл)
|
|
||||||
const hasToken = searchParams.has("token");
|
const hasToken = searchParams.has("token");
|
||||||
const isServerUrl = hasToken && !hasValidExtension;
|
const isServerUrl = hasToken && !hasValidExtension;
|
||||||
|
|
||||||
@@ -51,7 +43,7 @@ const isValid3DFile = (url: string): boolean => {
|
|||||||
return isValid;
|
return isValid;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("⚠️ isValid3DFile: Ошибка при проверке URL", error);
|
console.warn("⚠️ isValid3DFile: Ошибка при проверке URL", error);
|
||||||
// В случае ошибки парсинга URL, считаем валидным (пусть useGLTF сам разберется)
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -63,13 +55,10 @@ type ModelViewerProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Model = ({ fileUrl }: { fileUrl: string }) => {
|
const Model = ({ fileUrl }: { fileUrl: string }) => {
|
||||||
// Очищаем кеш перед загрузкой новой модели
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Очищаем кеш для текущего URL
|
|
||||||
clearGLTFCache(fileUrl);
|
clearGLTFCache(fileUrl);
|
||||||
}, [fileUrl]);
|
}, [fileUrl]);
|
||||||
|
|
||||||
// Проверяем валидность файла перед загрузкой (только для blob URL)
|
|
||||||
if (fileUrl.startsWith("blob:") && !isValid3DFile(fileUrl)) {
|
if (fileUrl.startsWith("blob:") && !isValid3DFile(fileUrl)) {
|
||||||
console.warn("⚠️ Model: Попытка загрузить невалидный 3D файл", { fileUrl });
|
console.warn("⚠️ Model: Попытка загрузить невалидный 3D файл", { fileUrl });
|
||||||
throw new Error(`Файл не является корректной 3D моделью: ${fileUrl}`);
|
throw new Error(`Файл не является корректной 3D моделью: ${fileUrl}`);
|
||||||
@@ -114,16 +103,13 @@ export const ThreeView = ({
|
|||||||
height = "100%",
|
height = "100%",
|
||||||
width = "100%",
|
width = "100%",
|
||||||
}: ModelViewerProps) => {
|
}: ModelViewerProps) => {
|
||||||
// Проверяем валидность файла (только для blob URL)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (fileUrl.startsWith("blob:") && !isValid3DFile(fileUrl)) {
|
if (fileUrl.startsWith("blob:") && !isValid3DFile(fileUrl)) {
|
||||||
console.warn("⚠️ ThreeView: Невалидный 3D файл", { fileUrl });
|
console.warn("⚠️ ThreeView: Невалидный 3D файл", { fileUrl });
|
||||||
}
|
}
|
||||||
}, [fileUrl]);
|
}, [fileUrl]);
|
||||||
|
|
||||||
// Очищаем кеш при размонтировании и при смене URL
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Очищаем кеш сразу при монтировании компонента
|
|
||||||
clearGLTFCache(fileUrl);
|
clearGLTFCache(fileUrl);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ export class ThreeViewErrorBoundary extends Component<Props, State> {
|
|||||||
props: Props,
|
props: Props,
|
||||||
state: State
|
state: State
|
||||||
): Partial<State> | null {
|
): Partial<State> | null {
|
||||||
// Сбрасываем ошибку ТОЛЬКО при смене медиа (когда меняется ID в resetKey)
|
|
||||||
if (
|
if (
|
||||||
props.resetKey !== state.lastResetKey &&
|
props.resetKey !== state.lastResetKey &&
|
||||||
state.lastResetKey !== undefined
|
state.lastResetKey !== undefined
|
||||||
@@ -43,7 +42,6 @@ export class ThreeViewErrorBoundary extends Component<Props, State> {
|
|||||||
const oldMediaId = String(state.lastResetKey).split("-")[0];
|
const oldMediaId = String(state.lastResetKey).split("-")[0];
|
||||||
const newMediaId = String(props.resetKey).split("-")[0];
|
const newMediaId = String(props.resetKey).split("-")[0];
|
||||||
|
|
||||||
// Сбрасываем ошибку только если изменился ID медиа (пользователь выбрал другую модель)
|
|
||||||
if (oldMediaId !== newMediaId) {
|
if (oldMediaId !== newMediaId) {
|
||||||
return {
|
return {
|
||||||
hasError: false,
|
hasError: false,
|
||||||
@@ -52,9 +50,6 @@ export class ThreeViewErrorBoundary extends Component<Props, State> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если изменился только счетчик (нажата кнопка "Попробовать снова"), обновляем lastResetKey
|
|
||||||
// но не сбрасываем ошибку автоматически - ждем результата загрузки
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
lastResetKey: props.resetKey,
|
lastResetKey: props.resetKey,
|
||||||
};
|
};
|
||||||
@@ -127,15 +122,12 @@ export class ThreeViewErrorBoundary extends Component<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleReset = () => {
|
handleReset = () => {
|
||||||
// Сначала сбрасываем состояние ошибки
|
|
||||||
this.setState(
|
this.setState(
|
||||||
{
|
{
|
||||||
hasError: false,
|
hasError: false,
|
||||||
error: null,
|
error: null,
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
// После того как состояние обновилось, вызываем callback для изменения resetKey
|
|
||||||
// Это приведет к пересозданию компонента и новой попытке загрузки
|
|
||||||
this.props.onReset?.();
|
this.props.onReset?.();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export function MediaViewer({
|
|||||||
// Используем новый cache manager для очистки кеша
|
// Используем новый cache manager для очистки кеша
|
||||||
clearMediaTransitionCache(
|
clearMediaTransitionCache(
|
||||||
previousMediaId,
|
previousMediaId,
|
||||||
media?.id || null,
|
|
||||||
media?.media_type
|
media?.media_type
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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 { Box, Button, TextField, Paper, Typography } from "@mui/material";
|
||||||
import {
|
import {
|
||||||
BackButton,
|
BackButton,
|
||||||
@@ -50,17 +49,6 @@ export const CreateLeftTab = observer(
|
|||||||
const [isSelectMediaDialogOpen, setIsSelectMediaDialogOpen] =
|
const [isSelectMediaDialogOpen, setIsSelectMediaDialogOpen] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = 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(() => {
|
const handleCloseArticleDialog = useCallback(() => {
|
||||||
setIsSelectArticleDialogOpen(false);
|
setIsSelectArticleDialogOpen(false);
|
||||||
|
|||||||
@@ -13,28 +13,27 @@ import {
|
|||||||
languageStore,
|
languageStore,
|
||||||
SelectArticleModal,
|
SelectArticleModal,
|
||||||
TabPanel,
|
TabPanel,
|
||||||
SelectMediaDialog, // Import
|
SelectMediaDialog,
|
||||||
UploadMediaDialog,
|
UploadMediaDialog,
|
||||||
Media, // Import
|
Media,
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
import {
|
import {
|
||||||
LanguageSwitcher,
|
LanguageSwitcher,
|
||||||
MediaArea, // Import
|
MediaArea,
|
||||||
MediaAreaForSight, // Import
|
MediaAreaForSight,
|
||||||
ReactMarkdownComponent,
|
ReactMarkdownComponent,
|
||||||
ReactMarkdownEditor,
|
ReactMarkdownEditor,
|
||||||
DeleteModal,
|
DeleteModal,
|
||||||
} from "@widgets";
|
} 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 { observer } from "mobx-react-lite";
|
||||||
import { useState, useEffect } from "react"; // Added useEffect
|
import { useState, useEffect } from "react";
|
||||||
import { MediaViewer } from "../../MediaViewer/index";
|
import { MediaViewer } from "../../MediaViewer/index";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { authInstance } from "@shared";
|
import { authInstance } from "@shared";
|
||||||
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";
|
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";
|
||||||
|
|
||||||
type MediaItemShared = {
|
type MediaItemShared = {
|
||||||
// Define if not already available from @shared
|
|
||||||
id: string;
|
id: string;
|
||||||
filename: string;
|
filename: string;
|
||||||
media_name?: string;
|
media_name?: string;
|
||||||
@@ -52,14 +51,14 @@ export const CreateRightTab = observer(
|
|||||||
unlinkPreviewMedia,
|
unlinkPreviewMedia,
|
||||||
createLinkWithRightArticle,
|
createLinkWithRightArticle,
|
||||||
deleteRightArticleMedia,
|
deleteRightArticleMedia,
|
||||||
setFileToUpload, // From store
|
setFileToUpload,
|
||||||
setUploadMediaOpen, // From store
|
setUploadMediaOpen,
|
||||||
uploadMediaOpen, // From store
|
uploadMediaOpen,
|
||||||
unlinkRightAritcle, // Corrected spelling
|
unlinkRightAritcle,
|
||||||
deleteRightArticle,
|
deleteRightArticle,
|
||||||
linkExistingRightArticle,
|
linkExistingRightArticle,
|
||||||
createSight,
|
createSight,
|
||||||
clearCreateSight, // For resetting form
|
clearCreateSight,
|
||||||
updateRightArticles,
|
updateRightArticles,
|
||||||
} = createSightStore;
|
} = createSightStore;
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
@@ -78,7 +77,7 @@ export const CreateRightTab = observer(
|
|||||||
>(null);
|
>(null);
|
||||||
|
|
||||||
const [previewMedia, setPreviewMedia] = useState<Media | null>(null);
|
const [previewMedia, setPreviewMedia] = useState<Media | null>(null);
|
||||||
// Reset activeArticleIndex if language changes and index is out of bounds
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sight.preview_media) {
|
if (sight.preview_media) {
|
||||||
const fetchMedia = async () => {
|
const fetchMedia = async () => {
|
||||||
@@ -97,7 +96,7 @@ export const CreateRightTab = observer(
|
|||||||
activeArticleIndex >= sight[language].right.length
|
activeArticleIndex >= sight[language].right.length
|
||||||
) {
|
) {
|
||||||
setActiveArticleIndex(null);
|
setActiveArticleIndex(null);
|
||||||
setType("media"); // Default back to media preview if selected article disappears
|
setType("media");
|
||||||
}
|
}
|
||||||
}, [language, sight[language].right, activeArticleIndex]);
|
}, [language, sight[language].right, activeArticleIndex]);
|
||||||
|
|
||||||
@@ -113,10 +112,9 @@ export const CreateRightTab = observer(
|
|||||||
try {
|
try {
|
||||||
await createSight(language);
|
await createSight(language);
|
||||||
toast.success("Достопримечательность успешно создана!");
|
toast.success("Достопримечательность успешно создана!");
|
||||||
clearCreateSight(); // Reset form
|
clearCreateSight();
|
||||||
setActiveArticleIndex(null);
|
setActiveArticleIndex(null);
|
||||||
setType("media");
|
setType("media");
|
||||||
// Potentially navigate away: history.push('/sights-list');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to save sight:", error);
|
console.error("Failed to save sight:", error);
|
||||||
toast.error("Ошибка при создании достопримечательности.");
|
toast.error("Ошибка при создании достопримечательности.");
|
||||||
@@ -132,7 +130,7 @@ export const CreateRightTab = observer(
|
|||||||
handleCloseMenu();
|
handleCloseMenu();
|
||||||
try {
|
try {
|
||||||
const newArticleId = await createNewRightArticle();
|
const newArticleId = await createNewRightArticle();
|
||||||
// Automatically select the new article if ID is returned
|
|
||||||
const newIndex = sight[language].right.findIndex(
|
const newIndex = sight[language].right.findIndex(
|
||||||
(a) => a.id === newArticleId
|
(a) => a.id === newArticleId
|
||||||
);
|
);
|
||||||
@@ -140,7 +138,6 @@ export const CreateRightTab = observer(
|
|||||||
setActiveArticleIndex(newIndex);
|
setActiveArticleIndex(newIndex);
|
||||||
setType("article");
|
setType("article");
|
||||||
} else {
|
} else {
|
||||||
// Fallback if findIndex fails (should not happen if store updates correctly)
|
|
||||||
setActiveArticleIndex(sight[language].right.length - 1);
|
setActiveArticleIndex(sight[language].right.length - 1);
|
||||||
setType("article");
|
setType("article");
|
||||||
}
|
}
|
||||||
@@ -156,7 +153,7 @@ export const CreateRightTab = observer(
|
|||||||
const linkedArticleId = await linkExistingRightArticle(
|
const linkedArticleId = await linkExistingRightArticle(
|
||||||
selectedArticleId
|
selectedArticleId
|
||||||
);
|
);
|
||||||
setSelectArticleDialogOpen(false); // Close dialog
|
setSelectArticleDialogOpen(false);
|
||||||
const newIndex = sight[language].right.findIndex(
|
const newIndex = sight[language].right.findIndex(
|
||||||
(a) => a.id === linkedArticleId
|
(a) => a.id === linkedArticleId
|
||||||
);
|
);
|
||||||
@@ -174,7 +171,6 @@ export const CreateRightTab = observer(
|
|||||||
? sight[language].right[activeArticleIndex]
|
? sight[language].right[activeArticleIndex]
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// Media Handling for Dialogs
|
|
||||||
const handleOpenUploadMedia = () => {
|
const handleOpenUploadMedia = () => {
|
||||||
setUploadMediaOpen(true);
|
setUploadMediaOpen(true);
|
||||||
};
|
};
|
||||||
@@ -203,7 +199,6 @@ export const CreateRightTab = observer(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleMediaUploaded = async (media: MediaItemShared) => {
|
const handleMediaUploaded = async (media: MediaItemShared) => {
|
||||||
// After UploadMediaDialog finishes
|
|
||||||
setUploadMediaOpen(false);
|
setUploadMediaOpen(false);
|
||||||
setFileToUpload(null);
|
setFileToUpload(null);
|
||||||
if (mediaTarget === "sightPreview") {
|
if (mediaTarget === "sightPreview") {
|
||||||
@@ -211,36 +206,25 @@ export const CreateRightTab = observer(
|
|||||||
} else if (mediaTarget === "rightArticle" && currentRightArticle) {
|
} else if (mediaTarget === "rightArticle" && currentRightArticle) {
|
||||||
await createLinkWithRightArticle(media, currentRightArticle.id);
|
await createLinkWithRightArticle(media, currentRightArticle.id);
|
||||||
}
|
}
|
||||||
setMediaTarget(null); // Reset target
|
setMediaTarget(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDragEnd = (result: any) => {
|
const handleDragEnd = (result: any) => {
|
||||||
const { source, destination } = result;
|
const { source, destination } = result;
|
||||||
|
|
||||||
// 1. Guard clause: If dropped outside any droppable area, do nothing.
|
|
||||||
if (!destination) return;
|
if (!destination) return;
|
||||||
|
|
||||||
// Extract source and destination indices
|
|
||||||
const sourceIndex = source.index;
|
const sourceIndex = source.index;
|
||||||
const destinationIndex = destination.index;
|
const destinationIndex = destination.index;
|
||||||
|
|
||||||
// 2. Guard clause: If dropped in the same position, do nothing.
|
|
||||||
if (sourceIndex === destinationIndex) return;
|
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];
|
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);
|
const [movedArticle] = newRightArticles.splice(sourceIndex, 1);
|
||||||
|
|
||||||
// - Insert the moved article into its new position.
|
|
||||||
newRightArticles.splice(destinationIndex, 0, movedArticle);
|
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);
|
updateRightArticles(newRightArticles);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -254,7 +238,7 @@ export const CreateRightTab = observer(
|
|||||||
height: "100%",
|
height: "100%",
|
||||||
minHeight: "calc(100vh - 200px)",
|
minHeight: "calc(100vh - 200px)",
|
||||||
gap: 2,
|
gap: 2,
|
||||||
paddingBottom: "70px", // Space for the save button
|
paddingBottom: "70px",
|
||||||
position: "relative",
|
position: "relative",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -264,7 +248,6 @@ export const CreateRightTab = observer(
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Box sx={{ display: "flex", flexGrow: 1, gap: 2.5 }}>
|
<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="flex flex-col w-[75%] gap-2">
|
||||||
<Box className="w-full flex 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">
|
<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
|
<Box
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setType("media");
|
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 ${
|
className={`w-full p-4 rounded-2xl cursor-pointer text-sm hover:bg-gray-300 transition-all duration-300 ${
|
||||||
type === "media"
|
type === "media"
|
||||||
@@ -364,7 +346,6 @@ export const CreateRightTab = observer(
|
|||||||
</Menu>
|
</Menu>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Main content area: Article Editor or Sight Media Preview */}
|
|
||||||
{type === "article" && currentRightArticle ? (
|
{type === "article" && currentRightArticle ? (
|
||||||
<Box className="w-[80%] border border-gray-300 p-3 flex flex-col gap-2 overflow-hidden">
|
<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">
|
<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} />}
|
startIcon={<Unlink color="white" size={18} />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (currentRightArticle) {
|
if (currentRightArticle) {
|
||||||
unlinkRightAritcle(currentRightArticle.id); // Corrected function name
|
unlinkRightAritcle(currentRightArticle.id);
|
||||||
setActiveArticleIndex(null);
|
setActiveArticleIndex(null);
|
||||||
setType("media");
|
setType("media");
|
||||||
}
|
}
|
||||||
@@ -435,7 +416,7 @@ export const CreateRightTab = observer(
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<MediaArea
|
<MediaArea
|
||||||
articleId={currentRightArticle.id} // Needs a real ID
|
articleId={currentRightArticle.id}
|
||||||
mediaIds={currentRightArticle.media || []}
|
mediaIds={currentRightArticle.media || []}
|
||||||
onFilesDrop={(files) => {
|
onFilesDrop={(files) => {
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
@@ -507,7 +488,6 @@ export const CreateRightTab = observer(
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Right Column: Live Preview */}
|
|
||||||
<Box className="w-[25%] mr-10">
|
<Box className="w-[25%] mr-10">
|
||||||
{type === "article" && activeArticleIndex !== null && (
|
{type === "article" && activeArticleIndex !== null && (
|
||||||
<Paper
|
<Paper
|
||||||
@@ -662,12 +642,11 @@ export const CreateRightTab = observer(
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Sticky Save Button Footer */}
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
bottom: "-20px",
|
bottom: "-20px",
|
||||||
left: 0, // ensure it spans from left
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
padding: 2,
|
padding: 2,
|
||||||
backgroundColor: "background.paper",
|
backgroundColor: "background.paper",
|
||||||
@@ -689,19 +668,17 @@ export const CreateRightTab = observer(
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Modals */}
|
|
||||||
<SelectArticleModal
|
<SelectArticleModal
|
||||||
open={selectArticleDialogOpen}
|
open={selectArticleDialogOpen}
|
||||||
onClose={() => setSelectArticleDialogOpen(false)}
|
onClose={() => setSelectArticleDialogOpen(false)}
|
||||||
onSelectArticle={handleSelectExistingArticleAndLink}
|
onSelectArticle={handleSelectExistingArticleAndLink}
|
||||||
// Pass IDs of already linked/added right articles to exclude them from selection
|
|
||||||
linkedArticleIds={sight[language].right.map((article) => article.id)}
|
linkedArticleIds={sight[language].right.map((article) => article.id)}
|
||||||
/>
|
/>
|
||||||
<UploadMediaDialog
|
<UploadMediaDialog
|
||||||
open={uploadMediaOpen} // From store
|
open={uploadMediaOpen}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setUploadMediaOpen(false);
|
setUploadMediaOpen(false);
|
||||||
setFileToUpload(null); // Clear file if dialog is closed without upload
|
setFileToUpload(null);
|
||||||
setMediaTarget(null);
|
setMediaTarget(null);
|
||||||
}}
|
}}
|
||||||
contextObjectName={sight[language].name}
|
contextObjectName={sight[language].name}
|
||||||
@@ -712,7 +689,7 @@ export const CreateRightTab = observer(
|
|||||||
? sight[language].right[activeArticleIndex].heading
|
? sight[language].right[activeArticleIndex].heading
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
afterUpload={handleMediaUploaded} // This will use the mediaTarget
|
afterUpload={handleMediaUploaded}
|
||||||
/>
|
/>
|
||||||
<SelectMediaDialog
|
<SelectMediaDialog
|
||||||
open={isSelectMediaDialogOpen}
|
open={isSelectMediaDialogOpen}
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ import { useEffect, useState } from "react";
|
|||||||
|
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
// Компонент предупреждающего окна (перенесен сюда)
|
|
||||||
import { SaveWithoutCityAgree } from "@widgets";
|
import { SaveWithoutCityAgree } from "@widgets";
|
||||||
|
import { LinkedStations } from "@pages";
|
||||||
|
|
||||||
export const InformationTab = observer(
|
export const InformationTab = observer(
|
||||||
({ value, index }: { value: number; index: number }) => {
|
({ value, index }: { value: number; index: number }) => {
|
||||||
@@ -62,7 +62,7 @@ export const InformationTab = observer(
|
|||||||
"thumbnail" | "watermark_lu" | "watermark_rd" | "video_preview" | null
|
"thumbnail" | "watermark_lu" | "watermark_rd" | "video_preview" | null
|
||||||
>(null);
|
>(null);
|
||||||
const { cities } = cityStore;
|
const { cities } = cityStore;
|
||||||
// НОВОЕ СОСТОЯНИЕ ДЛЯ ПРЕДУПРЕЖДАЮЩЕГО ОКНА
|
|
||||||
const [isSaveWarningOpen, setIsSaveWarningOpen] = useState(false);
|
const [isSaveWarningOpen, setIsSaveWarningOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {}, [hardcodeType]);
|
useEffect(() => {}, [hardcodeType]);
|
||||||
@@ -119,16 +119,14 @@ export const InformationTab = observer(
|
|||||||
updateSightInfo(language, content, common);
|
updateSightInfo(language, content, common);
|
||||||
};
|
};
|
||||||
|
|
||||||
// НОВАЯ ФУНКЦИЯ: Фактическое сохранение (вызывается после подтверждения)
|
|
||||||
const executeSave = async () => {
|
const executeSave = async () => {
|
||||||
await updateSight();
|
await updateSight();
|
||||||
toast.success("Достопримечательность сохранена");
|
toast.success("Достопримечательность сохранена");
|
||||||
};
|
};
|
||||||
|
|
||||||
// ОБНОВЛЕННАЯ ФУНКЦИЯ: Проверка и вызов окна или сохранения
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
const isCityMissing = !sight.common.city_id;
|
const isCityMissing = !sight.common.city_id;
|
||||||
// Проверяем названия на всех языках
|
|
||||||
const isNameMissing = !sight.ru.name || !sight.en.name || !sight.zh.name;
|
const isNameMissing = !sight.ru.name || !sight.en.name || !sight.zh.name;
|
||||||
|
|
||||||
if (isCityMissing || isNameMissing) {
|
if (isCityMissing || isNameMissing) {
|
||||||
@@ -139,13 +137,11 @@ export const InformationTab = observer(
|
|||||||
await executeSave();
|
await executeSave();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Обработчик "Да" в предупреждающем окне
|
|
||||||
const handleConfirmSave = async () => {
|
const handleConfirmSave = async () => {
|
||||||
setIsSaveWarningOpen(false);
|
setIsSaveWarningOpen(false);
|
||||||
await executeSave();
|
await executeSave();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Обработчик "Нет" в предупреждающем окне
|
|
||||||
const handleCancelSave = () => {
|
const handleCancelSave = () => {
|
||||||
setIsSaveWarningOpen(false);
|
setIsSaveWarningOpen(false);
|
||||||
};
|
};
|
||||||
@@ -275,6 +271,16 @@ export const InformationTab = observer(
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ width: "80%" }}>
|
||||||
|
{sight.common.id !== 0 && (
|
||||||
|
<LinkedStations
|
||||||
|
parentId={sight.common.id}
|
||||||
|
fields={[{ label: "Название", data: "name" }]}
|
||||||
|
type="edit"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -431,7 +437,7 @@ export const InformationTab = observer(
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
color="success"
|
color="success"
|
||||||
startIcon={<Save color="white" size={18} />}
|
startIcon={<Save color="white" size={18} />}
|
||||||
onClick={handleSave} // Используем новую функцию-обработчик
|
onClick={handleSave}
|
||||||
>
|
>
|
||||||
Сохранить
|
Сохранить
|
||||||
</Button>
|
</Button>
|
||||||
@@ -538,7 +544,6 @@ export const InformationTab = observer(
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ИНТЕГРИРОВАННОЕ ПРЕДУПРЕЖДАЮЩЕЕ ОКНО */}
|
|
||||||
{isSaveWarningOpen && (
|
{isSaveWarningOpen && (
|
||||||
<SaveWithoutCityAgree
|
<SaveWithoutCityAgree
|
||||||
blocker={{
|
blocker={{
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ export const RightWidgetTab = observer(
|
|||||||
try {
|
try {
|
||||||
const newArticleId = await createNewRightArticle();
|
const newArticleId = await createNewRightArticle();
|
||||||
handleClose();
|
handleClose();
|
||||||
// Automatically select the newly created article
|
|
||||||
const newIndex = sight[language].right.findIndex(
|
const newIndex = sight[language].right.findIndex(
|
||||||
(article) => article.id === newArticleId
|
(article) => article.id === newArticleId
|
||||||
);
|
);
|
||||||
@@ -144,7 +144,7 @@ export const RightWidgetTab = observer(
|
|||||||
try {
|
try {
|
||||||
const linkedArticleId = await linkArticle(id);
|
const linkedArticleId = await linkArticle(id);
|
||||||
handleCloseSelectModal();
|
handleCloseSelectModal();
|
||||||
// Automatically select the newly linked article
|
|
||||||
const newIndex = sight[language].right.findIndex(
|
const newIndex = sight[language].right.findIndex(
|
||||||
(article) => article.id === linkedArticleId
|
(article) => article.id === linkedArticleId
|
||||||
);
|
);
|
||||||
@@ -177,30 +177,19 @@ export const RightWidgetTab = observer(
|
|||||||
const handleDragEnd = (result: DropResult) => {
|
const handleDragEnd = (result: DropResult) => {
|
||||||
const { source, destination } = result;
|
const { source, destination } = result;
|
||||||
|
|
||||||
// 1. Guard clause: If dropped outside any droppable area, do nothing.
|
|
||||||
if (!destination) return;
|
if (!destination) return;
|
||||||
|
|
||||||
// Extract source and destination indices
|
|
||||||
const sourceIndex = source.index;
|
const sourceIndex = source.index;
|
||||||
const destinationIndex = destination.index;
|
const destinationIndex = destination.index;
|
||||||
|
|
||||||
// 2. Guard clause: If dropped in the same position, do nothing.
|
|
||||||
if (sourceIndex === destinationIndex) return;
|
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];
|
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);
|
const [movedArticle] = newRightArticles.splice(sourceIndex, 1);
|
||||||
|
|
||||||
// - Insert the moved article into its new position.
|
|
||||||
newRightArticles.splice(destinationIndex, 0, movedArticle);
|
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);
|
updateRightArticles(newRightArticles);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ interface VideoPreviewCardProps {
|
|||||||
onDeleteVideoClick: () => void;
|
onDeleteVideoClick: () => void;
|
||||||
onSelectVideoClick: (file?: File) => void;
|
onSelectVideoClick: (file?: File) => void;
|
||||||
tooltipText?: string;
|
tooltipText?: string;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VideoPreviewCard: React.FC<VideoPreviewCardProps> = ({
|
export const VideoPreviewCard: React.FC<VideoPreviewCardProps> = ({
|
||||||
@@ -20,15 +21,15 @@ export const VideoPreviewCard: React.FC<VideoPreviewCardProps> = ({
|
|||||||
onDeleteVideoClick,
|
onDeleteVideoClick,
|
||||||
onSelectVideoClick,
|
onSelectVideoClick,
|
||||||
tooltipText,
|
tooltipText,
|
||||||
|
className,
|
||||||
}) => {
|
}) => {
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [isDragOver, setIsDragOver] = useState(false);
|
const [isDragOver, setIsDragOver] = useState(false);
|
||||||
const token = localStorage.getItem("token");
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
useEffect(() => {}, [isDragOver]);
|
useEffect(() => {}, [isDragOver]);
|
||||||
// --- Click to select file ---
|
|
||||||
const handleZoneClick = () => {
|
const handleZoneClick = () => {
|
||||||
// Trigger the hidden file input click
|
|
||||||
fileInputRef.current?.click();
|
fileInputRef.current?.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -38,19 +39,17 @@ export const VideoPreviewCard: React.FC<VideoPreviewCardProps> = ({
|
|||||||
const file = event.target.files?.[0];
|
const file = event.target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
if (file.type.startsWith("video/")) {
|
if (file.type.startsWith("video/")) {
|
||||||
// Открываем диалог загрузки медиа с файлом видео
|
|
||||||
onSelectVideoClick(file);
|
onSelectVideoClick(file);
|
||||||
} else {
|
} else {
|
||||||
toast.error("Пожалуйста, выберите видео файл");
|
toast.error("Пожалуйста, выберите видео файл");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Reset the input value so selecting the same file again triggers change
|
|
||||||
event.target.value = "";
|
event.target.value = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Drag and Drop Handlers ---
|
|
||||||
const handleDragOver = (event: DragEvent<HTMLDivElement>) => {
|
const handleDragOver = (event: DragEvent<HTMLDivElement>) => {
|
||||||
event.preventDefault(); // Crucial to allow a drop
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
setIsDragOver(true);
|
setIsDragOver(true);
|
||||||
};
|
};
|
||||||
@@ -62,7 +61,7 @@ export const VideoPreviewCard: React.FC<VideoPreviewCardProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDrop = async (event: DragEvent<HTMLDivElement>) => {
|
const handleDrop = async (event: DragEvent<HTMLDivElement>) => {
|
||||||
event.preventDefault(); // Crucial to allow a drop
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
setIsDragOver(false);
|
setIsDragOver(false);
|
||||||
|
|
||||||
@@ -70,7 +69,6 @@ export const VideoPreviewCard: React.FC<VideoPreviewCardProps> = ({
|
|||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
if (file.type.startsWith("video/")) {
|
if (file.type.startsWith("video/")) {
|
||||||
// Открываем диалог загрузки медиа с файлом видео
|
|
||||||
onSelectVideoClick(file);
|
onSelectVideoClick(file);
|
||||||
} else {
|
} else {
|
||||||
toast.error("Пожалуйста, выберите видео файл");
|
toast.error("Пожалуйста, выберите видео файл");
|
||||||
@@ -89,7 +87,10 @@ export const VideoPreviewCard: React.FC<VideoPreviewCardProps> = ({
|
|||||||
gap: 1,
|
gap: 1,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
|
width: "min-content",
|
||||||
|
mx: "auto",
|
||||||
}}
|
}}
|
||||||
|
className={className}
|
||||||
>
|
>
|
||||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||||
<Typography variant="subtitle2" gutterBottom sx={{ mb: 0, mr: 0.5 }}>
|
<Typography variant="subtitle2" gutterBottom sx={{ mb: 0, mr: 0.5 }}>
|
||||||
@@ -127,7 +128,10 @@ export const VideoPreviewCard: React.FC<VideoPreviewCardProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{videoId ? (
|
{videoId ? (
|
||||||
<Box sx={{ position: "relative", width: "100%", height: "100%" }}>
|
<Box
|
||||||
|
sx={{ position: "relative", width: "100%", height: "100%" }}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
<video
|
<video
|
||||||
src={`${
|
src={`${
|
||||||
import.meta.env.VITE_KRBL_MEDIA
|
import.meta.env.VITE_KRBL_MEDIA
|
||||||
@@ -167,7 +171,7 @@ export const VideoPreviewCard: React.FC<VideoPreviewCardProps> = ({
|
|||||||
borderRadius: 1,
|
borderRadius: 1,
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
}}
|
}}
|
||||||
onClick={handleZoneClick} // Click handler for the zone
|
onClick={handleZoneClick}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
@@ -181,8 +185,8 @@ export const VideoPreviewCard: React.FC<VideoPreviewCardProps> = ({
|
|||||||
color="primary"
|
color="primary"
|
||||||
startIcon={<Plus color="white" size={18} />}
|
startIcon={<Plus color="white" size={18} />}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation(); // Prevent `handleZoneClick` from firing
|
e.stopPropagation();
|
||||||
onSelectVideoClick(); // This button triggers the media selection dialog
|
onSelectVideoClick();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Выбрать файл
|
Выбрать файл
|
||||||
@@ -193,7 +197,7 @@ export const VideoPreviewCard: React.FC<VideoPreviewCardProps> = ({
|
|||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
onChange={handleFileInputChange}
|
onChange={handleFileInputChange}
|
||||||
style={{ display: "none" }}
|
style={{ display: "none" }}
|
||||||
accept="video/*" // Accept only video files
|
accept="video/*"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -3,56 +3,6 @@ import react from "@vitejs/plugin-react";
|
|||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
type ManualChunksFn = (id: string, api: { getModuleIds: () => Iterable<string> }) => string | undefined;
|
|
||||||
|
|
||||||
const manualChunks: ManualChunksFn = (id) => {
|
|
||||||
if (id.includes('node_modules')) {
|
|
||||||
|
|
||||||
if (
|
|
||||||
id.includes('three.') ||
|
|
||||||
id.includes('@react-three') ||
|
|
||||||
id.includes('ol/') ||
|
|
||||||
id.includes('mapbox-gl') ||
|
|
||||||
id.includes('@babel/runtime')
|
|
||||||
) {
|
|
||||||
return 'vendor-3d-maps';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id.includes('codemirror') || id.includes('react-codemirror2')) {
|
|
||||||
return 'vendor-codemirror';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id.includes('hls.js')) {
|
|
||||||
return 'vendor-hls';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id.includes('pixi.js')) {
|
|
||||||
return 'vendor-pixijs';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id.includes('@mui/material') || id.includes('@mui/icons-material') || id.includes('@mui/x-data-grid')) {
|
|
||||||
return 'vendor-mui-core';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id.includes('/react/') || id.includes('/react-dom/')) {
|
|
||||||
return 'vendor-react-core';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id.includes('react-router') || id.includes('history')) {
|
|
||||||
return 'vendor-router';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'vendor-common-remainder';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id.includes('src/pages/')) {
|
|
||||||
const pathParts = id.split('src/pages/');
|
|
||||||
if (pathParts.length > 1) {
|
|
||||||
return 'page-' + pathParts[1].split('/')[0].toLowerCase();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
react(),
|
react(),
|
||||||
@@ -70,16 +20,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
|
|
||||||
build: {
|
build: {
|
||||||
chunkSizeWarningLimit: 2000,
|
chunkSizeWarningLimit: 5000,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
rollupOptions: {
|
|
||||||
output: {
|
|
||||||
manualChunks,
|
|
||||||
|
|
||||||
entryFileNames: `assets/[name]-[hash].js`,
|
|
||||||
chunkFileNames: `assets/[name]-[hash].js`,
|
|
||||||
assetFileNames: `assets/[name]-[hash].[ext]`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) as UserConfigExport;
|
|
||||||
474
yarn.lock
474
yarn.lock
@@ -16,7 +16,7 @@
|
|||||||
resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz"
|
resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz"
|
||||||
integrity sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==
|
integrity sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==
|
||||||
|
|
||||||
"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.21.3", "@babel/core@^7.28.0":
|
"@babel/core@^7.21.3", "@babel/core@^7.28.0":
|
||||||
version "7.28.5"
|
version "7.28.5"
|
||||||
resolved "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz"
|
resolved "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz"
|
||||||
integrity sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==
|
integrity sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==
|
||||||
@@ -170,6 +170,28 @@
|
|||||||
resolved "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz"
|
resolved "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz"
|
||||||
integrity sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==
|
integrity sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==
|
||||||
|
|
||||||
|
"@emnapi/core@^1.5.0":
|
||||||
|
version "1.7.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.7.0.tgz#135de4e8858763989112281bdf38ca02439db7c3"
|
||||||
|
integrity sha512-pJdKGq/1iquWYtv1RRSljZklxHCOCAJFJrImO5ZLKPJVJlVUcs8yFwNQlqS0Lo8xT1VAXXTCZocF9n26FWEKsw==
|
||||||
|
dependencies:
|
||||||
|
"@emnapi/wasi-threads" "1.1.0"
|
||||||
|
tslib "^2.4.0"
|
||||||
|
|
||||||
|
"@emnapi/runtime@^1.5.0":
|
||||||
|
version "1.7.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.7.0.tgz#d7ef3832df8564fe5903bf0567aedbd19538ecbe"
|
||||||
|
integrity sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q==
|
||||||
|
dependencies:
|
||||||
|
tslib "^2.4.0"
|
||||||
|
|
||||||
|
"@emnapi/wasi-threads@1.1.0", "@emnapi/wasi-threads@^1.1.0":
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz#60b2102fddc9ccb78607e4a3cf8403ea69be41bf"
|
||||||
|
integrity sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==
|
||||||
|
dependencies:
|
||||||
|
tslib "^2.4.0"
|
||||||
|
|
||||||
"@emotion/babel-plugin@^11.13.5":
|
"@emotion/babel-plugin@^11.13.5":
|
||||||
version "11.13.5"
|
version "11.13.5"
|
||||||
resolved "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz"
|
resolved "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz"
|
||||||
@@ -215,7 +237,7 @@
|
|||||||
resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz"
|
resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz"
|
||||||
integrity sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==
|
integrity sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==
|
||||||
|
|
||||||
"@emotion/react@^11.0.0-rc.0", "@emotion/react@^11.14.0", "@emotion/react@^11.4.1", "@emotion/react@^11.5.0", "@emotion/react@^11.9.0":
|
"@emotion/react@^11.14.0":
|
||||||
version "11.14.0"
|
version "11.14.0"
|
||||||
resolved "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz"
|
resolved "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz"
|
||||||
integrity sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==
|
integrity sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==
|
||||||
@@ -245,7 +267,7 @@
|
|||||||
resolved "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz"
|
resolved "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz"
|
||||||
integrity sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==
|
integrity sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==
|
||||||
|
|
||||||
"@emotion/styled@^11.14.0", "@emotion/styled@^11.3.0", "@emotion/styled@^11.8.1":
|
"@emotion/styled@^11.14.0":
|
||||||
version "11.14.1"
|
version "11.14.1"
|
||||||
resolved "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz"
|
resolved "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz"
|
||||||
integrity sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==
|
integrity sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==
|
||||||
@@ -277,11 +299,136 @@
|
|||||||
resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz"
|
resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz"
|
||||||
integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==
|
integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==
|
||||||
|
|
||||||
|
"@esbuild/aix-ppc64@0.25.11":
|
||||||
|
version "0.25.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz#2ae33300598132cc4cf580dbbb28d30fed3c5c49"
|
||||||
|
integrity sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==
|
||||||
|
|
||||||
|
"@esbuild/android-arm64@0.25.11":
|
||||||
|
version "0.25.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz#927708b3db5d739d6cb7709136924cc81bec9b03"
|
||||||
|
integrity sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==
|
||||||
|
|
||||||
|
"@esbuild/android-arm@0.25.11":
|
||||||
|
version "0.25.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.11.tgz#571f94e7f4068957ec4c2cfb907deae3d01b55ae"
|
||||||
|
integrity sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==
|
||||||
|
|
||||||
|
"@esbuild/android-x64@0.25.11":
|
||||||
|
version "0.25.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.11.tgz#8a3bf5cae6c560c7ececa3150b2bde76e0fb81e6"
|
||||||
|
integrity sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==
|
||||||
|
|
||||||
"@esbuild/darwin-arm64@0.25.11":
|
"@esbuild/darwin-arm64@0.25.11":
|
||||||
version "0.25.11"
|
version "0.25.11"
|
||||||
resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz"
|
resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz"
|
||||||
integrity sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==
|
integrity sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==
|
||||||
|
|
||||||
|
"@esbuild/darwin-x64@0.25.11":
|
||||||
|
version "0.25.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz#70f5e925a30c8309f1294d407a5e5e002e0315fe"
|
||||||
|
integrity sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==
|
||||||
|
|
||||||
|
"@esbuild/freebsd-arm64@0.25.11":
|
||||||
|
version "0.25.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz#4ec1db687c5b2b78b44148025da9632397553e8a"
|
||||||
|
integrity sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==
|
||||||
|
|
||||||
|
"@esbuild/freebsd-x64@0.25.11":
|
||||||
|
version "0.25.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz#4c81abd1b142f1e9acfef8c5153d438ca53f44bb"
|
||||||
|
integrity sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==
|
||||||
|
|
||||||
|
"@esbuild/linux-arm64@0.25.11":
|
||||||
|
version "0.25.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz#69517a111acfc2b93aa0fb5eaeb834c0202ccda5"
|
||||||
|
integrity sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==
|
||||||
|
|
||||||
|
"@esbuild/linux-arm@0.25.11":
|
||||||
|
version "0.25.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz#58dac26eae2dba0fac5405052b9002dac088d38f"
|
||||||
|
integrity sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==
|
||||||
|
|
||||||
|
"@esbuild/linux-ia32@0.25.11":
|
||||||
|
version "0.25.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz#b89d4efe9bdad46ba944f0f3b8ddd40834268c2b"
|
||||||
|
integrity sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==
|
||||||
|
|
||||||
|
"@esbuild/linux-loong64@0.25.11":
|
||||||
|
version "0.25.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz#11f603cb60ad14392c3f5c94d64b3cc8b630fbeb"
|
||||||
|
integrity sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==
|
||||||
|
|
||||||
|
"@esbuild/linux-mips64el@0.25.11":
|
||||||
|
version "0.25.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz#b7d447ff0676b8ab247d69dac40a5cf08e5eeaf5"
|
||||||
|
integrity sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==
|
||||||
|
|
||||||
|
"@esbuild/linux-ppc64@0.25.11":
|
||||||
|
version "0.25.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz#b3a28ed7cc252a61b07ff7c8fd8a984ffd3a2f74"
|
||||||
|
integrity sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==
|
||||||
|
|
||||||
|
"@esbuild/linux-riscv64@0.25.11":
|
||||||
|
version "0.25.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz#ce75b08f7d871a75edcf4d2125f50b21dc9dc273"
|
||||||
|
integrity sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==
|
||||||
|
|
||||||
|
"@esbuild/linux-s390x@0.25.11":
|
||||||
|
version "0.25.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz#cd08f6c73b6b6ff9ccdaabbd3ff6ad3dca99c263"
|
||||||
|
integrity sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==
|
||||||
|
|
||||||
|
"@esbuild/linux-x64@0.25.11":
|
||||||
|
version "0.25.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz#3c3718af31a95d8946ebd3c32bb1e699bdf74910"
|
||||||
|
integrity sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==
|
||||||
|
|
||||||
|
"@esbuild/netbsd-arm64@0.25.11":
|
||||||
|
version "0.25.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz#b4c767082401e3a4e8595fe53c47cd7f097c8077"
|
||||||
|
integrity sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==
|
||||||
|
|
||||||
|
"@esbuild/netbsd-x64@0.25.11":
|
||||||
|
version "0.25.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz#f2a930458ed2941d1f11ebc34b9c7d61f7a4d034"
|
||||||
|
integrity sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==
|
||||||
|
|
||||||
|
"@esbuild/openbsd-arm64@0.25.11":
|
||||||
|
version "0.25.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz#b4ae93c75aec48bc1e8a0154957a05f0641f2dad"
|
||||||
|
integrity sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==
|
||||||
|
|
||||||
|
"@esbuild/openbsd-x64@0.25.11":
|
||||||
|
version "0.25.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz#b42863959c8dcf9b01581522e40012d2c70045e2"
|
||||||
|
integrity sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==
|
||||||
|
|
||||||
|
"@esbuild/openharmony-arm64@0.25.11":
|
||||||
|
version "0.25.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz#b2e717141c8fdf6bddd4010f0912e6b39e1640f1"
|
||||||
|
integrity sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==
|
||||||
|
|
||||||
|
"@esbuild/sunos-x64@0.25.11":
|
||||||
|
version "0.25.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz#9fbea1febe8778927804828883ec0f6dd80eb244"
|
||||||
|
integrity sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==
|
||||||
|
|
||||||
|
"@esbuild/win32-arm64@0.25.11":
|
||||||
|
version "0.25.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz#501539cedb24468336073383989a7323005a8935"
|
||||||
|
integrity sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==
|
||||||
|
|
||||||
|
"@esbuild/win32-ia32@0.25.11":
|
||||||
|
version "0.25.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz#8ac7229aa82cef8f16ffb58f1176a973a7a15343"
|
||||||
|
integrity sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==
|
||||||
|
|
||||||
|
"@esbuild/win32-x64@0.25.11":
|
||||||
|
version "0.25.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz#5ecda6f3fe138b7e456f4e429edde33c823f392f"
|
||||||
|
integrity sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==
|
||||||
|
|
||||||
"@eslint-community/eslint-utils@^4.7.0", "@eslint-community/eslint-utils@^4.8.0":
|
"@eslint-community/eslint-utils@^4.7.0", "@eslint-community/eslint-utils@^4.8.0":
|
||||||
version "4.9.0"
|
version "4.9.0"
|
||||||
resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz"
|
resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz"
|
||||||
@@ -332,7 +479,7 @@
|
|||||||
minimatch "^3.1.2"
|
minimatch "^3.1.2"
|
||||||
strip-json-comments "^3.1.1"
|
strip-json-comments "^3.1.1"
|
||||||
|
|
||||||
"@eslint/js@^9.25.0", "@eslint/js@9.38.0":
|
"@eslint/js@9.38.0", "@eslint/js@^9.25.0":
|
||||||
version "9.38.0"
|
version "9.38.0"
|
||||||
resolved "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz"
|
resolved "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz"
|
||||||
integrity sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==
|
integrity sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==
|
||||||
@@ -442,7 +589,7 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.28.4"
|
"@babel/runtime" "^7.28.4"
|
||||||
|
|
||||||
"@mui/material@^5.15.14 || ^6.0.0 || ^7.0.0", "@mui/material@^7.1.0", "@mui/material@^7.3.4":
|
"@mui/material@^7.1.0":
|
||||||
version "7.3.4"
|
version "7.3.4"
|
||||||
resolved "https://registry.npmjs.org/@mui/material/-/material-7.3.4.tgz"
|
resolved "https://registry.npmjs.org/@mui/material/-/material-7.3.4.tgz"
|
||||||
integrity sha512-gEQL9pbJZZHT7lYJBKQCS723v1MGys2IFc94COXbUIyCTWa+qC77a7hUax4Yjd5ggEm35dk4AyYABpKKWC4MLw==
|
integrity sha512-gEQL9pbJZZHT7lYJBKQCS723v1MGys2IFc94COXbUIyCTWa+qC77a7hUax4Yjd5ggEm35dk4AyYABpKKWC4MLw==
|
||||||
@@ -481,7 +628,7 @@
|
|||||||
csstype "^3.1.3"
|
csstype "^3.1.3"
|
||||||
prop-types "^15.8.1"
|
prop-types "^15.8.1"
|
||||||
|
|
||||||
"@mui/system@^5.15.14 || ^6.0.0 || ^7.0.0", "@mui/system@^7.3.3":
|
"@mui/system@^7.3.3":
|
||||||
version "7.3.3"
|
version "7.3.3"
|
||||||
resolved "https://registry.npmjs.org/@mui/system/-/system-7.3.3.tgz"
|
resolved "https://registry.npmjs.org/@mui/system/-/system-7.3.3.tgz"
|
||||||
integrity sha512-Lqq3emZr5IzRLKaHPuMaLBDVaGvxoh6z7HMWd1RPKawBM5uMRaQ4ImsmmgXWtwJdfZux5eugfDhXJUo2mliS8Q==
|
integrity sha512-Lqq3emZr5IzRLKaHPuMaLBDVaGvxoh6z7HMWd1RPKawBM5uMRaQ4ImsmmgXWtwJdfZux5eugfDhXJUo2mliS8Q==
|
||||||
@@ -546,6 +693,15 @@
|
|||||||
"@mui/utils" "^7.3.3"
|
"@mui/utils" "^7.3.3"
|
||||||
"@mui/x-internals" "8.14.0"
|
"@mui/x-internals" "8.14.0"
|
||||||
|
|
||||||
|
"@napi-rs/wasm-runtime@^1.0.7":
|
||||||
|
version "1.0.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz#dcfea99a75f06209a235f3d941e3460a51e9b14c"
|
||||||
|
integrity sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==
|
||||||
|
dependencies:
|
||||||
|
"@emnapi/core" "^1.5.0"
|
||||||
|
"@emnapi/runtime" "^1.5.0"
|
||||||
|
"@tybys/wasm-util" "^0.10.1"
|
||||||
|
|
||||||
"@nodelib/fs.scandir@2.1.5":
|
"@nodelib/fs.scandir@2.1.5":
|
||||||
version "2.1.5"
|
version "2.1.5"
|
||||||
resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"
|
resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"
|
||||||
@@ -554,7 +710,7 @@
|
|||||||
"@nodelib/fs.stat" "2.0.5"
|
"@nodelib/fs.stat" "2.0.5"
|
||||||
run-parallel "^1.1.9"
|
run-parallel "^1.1.9"
|
||||||
|
|
||||||
"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5":
|
"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
|
||||||
version "2.0.5"
|
version "2.0.5"
|
||||||
resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz"
|
resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz"
|
||||||
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
|
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
|
||||||
@@ -572,7 +728,7 @@
|
|||||||
resolved "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.3.tgz"
|
resolved "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.3.tgz"
|
||||||
integrity sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==
|
integrity sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==
|
||||||
|
|
||||||
"@photo-sphere-viewer/core@^5.13.2", "@photo-sphere-viewer/core@>=5.13.1":
|
"@photo-sphere-viewer/core@^5.13.2":
|
||||||
version "5.14.0"
|
version "5.14.0"
|
||||||
resolved "https://registry.npmjs.org/@photo-sphere-viewer/core/-/core-5.14.0.tgz"
|
resolved "https://registry.npmjs.org/@photo-sphere-viewer/core/-/core-5.14.0.tgz"
|
||||||
integrity sha512-V0JeDSB1D2Q60Zqn7+0FPjq8gqbKEwuxMzNdTLydefkQugVztLvdZykO+4k5XTpweZ2QAWPH/QOI1xZbsdvR9A==
|
integrity sha512-V0JeDSB1D2Q60Zqn7+0FPjq8gqbKEwuxMzNdTLydefkQugVztLvdZykO+4k5XTpweZ2QAWPH/QOI1xZbsdvR9A==
|
||||||
@@ -624,7 +780,7 @@
|
|||||||
utility-types "^3.11.0"
|
utility-types "^3.11.0"
|
||||||
zustand "^5.0.1"
|
zustand "^5.0.1"
|
||||||
|
|
||||||
"@react-three/fiber@^9.0.0", "@react-three/fiber@^9.1.2":
|
"@react-three/fiber@^9.1.2":
|
||||||
version "9.4.0"
|
version "9.4.0"
|
||||||
resolved "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.4.0.tgz"
|
resolved "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.4.0.tgz"
|
||||||
integrity sha512-k4iu1R6e5D54918V4sqmISUkI5OgTw3v7/sDRKEC632Wd5g2WBtUS5gyG63X0GJO/HZUj1tsjSXfyzwrUHZl1g==
|
integrity sha512-k4iu1R6e5D54918V4sqmISUkI5OgTw3v7/sDRKEC632Wd5g2WBtUS5gyG63X0GJO/HZUj1tsjSXfyzwrUHZl1g==
|
||||||
@@ -656,11 +812,116 @@
|
|||||||
estree-walker "^2.0.2"
|
estree-walker "^2.0.2"
|
||||||
picomatch "^4.0.2"
|
picomatch "^4.0.2"
|
||||||
|
|
||||||
|
"@rollup/rollup-android-arm-eabi@4.52.5":
|
||||||
|
version "4.52.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz#0f44a2f8668ed87b040b6fe659358ac9239da4db"
|
||||||
|
integrity sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==
|
||||||
|
|
||||||
|
"@rollup/rollup-android-arm64@4.52.5":
|
||||||
|
version "4.52.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz#25b9a01deef6518a948431564c987bcb205274f5"
|
||||||
|
integrity sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==
|
||||||
|
|
||||||
"@rollup/rollup-darwin-arm64@4.52.5":
|
"@rollup/rollup-darwin-arm64@4.52.5":
|
||||||
version "4.52.5"
|
version "4.52.5"
|
||||||
resolved "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz"
|
resolved "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz"
|
||||||
integrity sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==
|
integrity sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==
|
||||||
|
|
||||||
|
"@rollup/rollup-darwin-x64@4.52.5":
|
||||||
|
version "4.52.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz#8e526417cd6f54daf1d0c04cf361160216581956"
|
||||||
|
integrity sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==
|
||||||
|
|
||||||
|
"@rollup/rollup-freebsd-arm64@4.52.5":
|
||||||
|
version "4.52.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz#0e7027054493f3409b1f219a3eac5efd128ef899"
|
||||||
|
integrity sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==
|
||||||
|
|
||||||
|
"@rollup/rollup-freebsd-x64@4.52.5":
|
||||||
|
version "4.52.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz#72b204a920139e9ec3d331bd9cfd9a0c248ccb10"
|
||||||
|
integrity sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm-gnueabihf@4.52.5":
|
||||||
|
version "4.52.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz#ab1b522ebe5b7e06c99504cc38f6cd8b808ba41c"
|
||||||
|
integrity sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm-musleabihf@4.52.5":
|
||||||
|
version "4.52.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz#f8cc30b638f1ee7e3d18eac24af47ea29d9beb00"
|
||||||
|
integrity sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm64-gnu@4.52.5":
|
||||||
|
version "4.52.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz#7af37a9e85f25db59dc8214172907b7e146c12cc"
|
||||||
|
integrity sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm64-musl@4.52.5":
|
||||||
|
version "4.52.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz#a623eb0d3617c03b7a73716eb85c6e37b776f7e0"
|
||||||
|
integrity sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-loong64-gnu@4.52.5":
|
||||||
|
version "4.52.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz#76ea038b549c5c6c5f0d062942627c4066642ee2"
|
||||||
|
integrity sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-ppc64-gnu@4.52.5":
|
||||||
|
version "4.52.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz#d9a4c3f0a3492bc78f6fdfe8131ac61c7359ccd5"
|
||||||
|
integrity sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-riscv64-gnu@4.52.5":
|
||||||
|
version "4.52.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz#87ab033eebd1a9a1dd7b60509f6333ec1f82d994"
|
||||||
|
integrity sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-riscv64-musl@4.52.5":
|
||||||
|
version "4.52.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz#bda3eb67e1c993c1ba12bc9c2f694e7703958d9f"
|
||||||
|
integrity sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-s390x-gnu@4.52.5":
|
||||||
|
version "4.52.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz#f7bc10fbe096ab44694233dc42a2291ed5453d4b"
|
||||||
|
integrity sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-gnu@4.52.5":
|
||||||
|
version "4.52.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz#a151cb1234cc9b2cf5e8cfc02aa91436b8f9e278"
|
||||||
|
integrity sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-musl@4.52.5":
|
||||||
|
version "4.52.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz#7859e196501cc3b3062d45d2776cfb4d2f3a9350"
|
||||||
|
integrity sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==
|
||||||
|
|
||||||
|
"@rollup/rollup-openharmony-arm64@4.52.5":
|
||||||
|
version "4.52.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz#85d0df7233734df31e547c1e647d2a5300b3bf30"
|
||||||
|
integrity sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-arm64-msvc@4.52.5":
|
||||||
|
version "4.52.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz#e62357d00458db17277b88adbf690bb855cac937"
|
||||||
|
integrity sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-ia32-msvc@4.52.5":
|
||||||
|
version "4.52.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz#fc7cd40f44834a703c1f1c3fe8bcc27ce476cd50"
|
||||||
|
integrity sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-x64-gnu@4.52.5":
|
||||||
|
version "4.52.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz#1a22acfc93c64a64a48c42672e857ee51774d0d3"
|
||||||
|
integrity sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-x64-msvc@4.52.5":
|
||||||
|
version "4.52.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz#1657f56326bbe0ac80eedc9f9c18fc1ddd24e107"
|
||||||
|
integrity sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==
|
||||||
|
|
||||||
"@svgr/babel-plugin-add-jsx-attribute@8.0.0":
|
"@svgr/babel-plugin-add-jsx-attribute@8.0.0":
|
||||||
version "8.0.0"
|
version "8.0.0"
|
||||||
resolved "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz"
|
resolved "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz"
|
||||||
@@ -715,7 +976,7 @@
|
|||||||
"@svgr/babel-plugin-transform-react-native-svg" "8.1.0"
|
"@svgr/babel-plugin-transform-react-native-svg" "8.1.0"
|
||||||
"@svgr/babel-plugin-transform-svg-component" "8.0.0"
|
"@svgr/babel-plugin-transform-svg-component" "8.0.0"
|
||||||
|
|
||||||
"@svgr/core@*", "@svgr/core@^8.1.0":
|
"@svgr/core@^8.1.0":
|
||||||
version "8.1.0"
|
version "8.1.0"
|
||||||
resolved "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz"
|
resolved "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz"
|
||||||
integrity sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==
|
integrity sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==
|
||||||
@@ -757,11 +1018,73 @@
|
|||||||
source-map-js "^1.2.1"
|
source-map-js "^1.2.1"
|
||||||
tailwindcss "4.1.16"
|
tailwindcss "4.1.16"
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-android-arm64@4.1.16":
|
||||||
|
version "4.1.16"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.16.tgz#9bd16c0a08db20d7c93907a9bd1564e0255307eb"
|
||||||
|
integrity sha512-8+ctzkjHgwDJ5caq9IqRSgsP70xhdhJvm+oueS/yhD5ixLhqTw9fSL1OurzMUhBwE5zK26FXLCz2f/RtkISqHA==
|
||||||
|
|
||||||
"@tailwindcss/oxide-darwin-arm64@4.1.16":
|
"@tailwindcss/oxide-darwin-arm64@4.1.16":
|
||||||
version "4.1.16"
|
version "4.1.16"
|
||||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.16.tgz"
|
resolved "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.16.tgz"
|
||||||
integrity sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA==
|
integrity sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA==
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-darwin-x64@4.1.16":
|
||||||
|
version "4.1.16"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.16.tgz#6193bafbb1a885795702f12bbef9cc5eb4cc550b"
|
||||||
|
integrity sha512-vjrl/1Ub9+JwU6BP0emgipGjowzYZMjbWCDqwA2Z4vCa+HBSpP4v6U2ddejcHsolsYxwL5r4bPNoamlV0xDdLg==
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-freebsd-x64@4.1.16":
|
||||||
|
version "4.1.16"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.16.tgz#0e2b064d71ba87a9001ac963be2752a8ddb64349"
|
||||||
|
integrity sha512-TSMpPYpQLm+aR1wW5rKuUuEruc/oOX3C7H0BTnPDn7W/eMw8W+MRMpiypKMkXZfwH8wqPIRKppuZoedTtNj2tg==
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm-gnueabihf@4.1.16":
|
||||||
|
version "4.1.16"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.16.tgz#8e80c959eeda81a08ed955e23eb6d228287b9672"
|
||||||
|
integrity sha512-p0GGfRg/w0sdsFKBjMYvvKIiKy/LNWLWgV/plR4lUgrsxFAoQBFrXkZ4C0w8IOXfslB9vHK/JGASWD2IefIpvw==
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm64-gnu@4.1.16":
|
||||||
|
version "4.1.16"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.16.tgz#d5f54910920fc5808122515f5208c5ecc1a40545"
|
||||||
|
integrity sha512-DoixyMmTNO19rwRPdqviTrG1rYzpxgyYJl8RgQvdAQUzxC1ToLRqtNJpU/ATURSKgIg6uerPw2feW0aS8SNr/w==
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm64-musl@4.1.16":
|
||||||
|
version "4.1.16"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.16.tgz#67cdb932230ac47bf3bf5415ccc92417b27020ee"
|
||||||
|
integrity sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ==
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-x64-gnu@4.1.16":
|
||||||
|
version "4.1.16"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.16.tgz#80ae0cfd8ebc970f239060ecdfdd07f6f6b14dce"
|
||||||
|
integrity sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew==
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-x64-musl@4.1.16":
|
||||||
|
version "4.1.16"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.16.tgz#524e5b87e8e79a712de3d9bbb94d2fc2fa44391c"
|
||||||
|
integrity sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw==
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi@4.1.16":
|
||||||
|
version "4.1.16"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.16.tgz#dc31d6bc1f6c1e8119a335ae3f28deb4d7c560f2"
|
||||||
|
integrity sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q==
|
||||||
|
dependencies:
|
||||||
|
"@emnapi/core" "^1.5.0"
|
||||||
|
"@emnapi/runtime" "^1.5.0"
|
||||||
|
"@emnapi/wasi-threads" "^1.1.0"
|
||||||
|
"@napi-rs/wasm-runtime" "^1.0.7"
|
||||||
|
"@tybys/wasm-util" "^0.10.1"
|
||||||
|
tslib "^2.4.0"
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-win32-arm64-msvc@4.1.16":
|
||||||
|
version "4.1.16"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.16.tgz#f1f810cdb49dae8071d5edf0db5cc0da2ec6a7e8"
|
||||||
|
integrity sha512-zX+Q8sSkGj6HKRTMJXuPvOcP8XfYON24zJBRPlszcH1Np7xuHXhWn8qfFjIujVzvH3BHU+16jBXwgpl20i+v9A==
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-win32-x64-msvc@4.1.16":
|
||||||
|
version "4.1.16"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.16.tgz#76dcda613578f06569c0a6015f39f12746a24dce"
|
||||||
|
integrity sha512-m5dDFJUEejbFqP+UXVstd4W/wnxA4F61q8SoL+mqTypId2T2ZpuxosNSgowiCnLp2+Z+rivdU0AqpfgiD7yCBg==
|
||||||
|
|
||||||
"@tailwindcss/oxide@4.1.16":
|
"@tailwindcss/oxide@4.1.16":
|
||||||
version "4.1.16"
|
version "4.1.16"
|
||||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.16.tgz"
|
resolved "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.16.tgz"
|
||||||
@@ -801,6 +1124,13 @@
|
|||||||
resolved "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz"
|
resolved "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz"
|
||||||
integrity sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==
|
integrity sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==
|
||||||
|
|
||||||
|
"@tybys/wasm-util@^0.10.1":
|
||||||
|
version "0.10.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz#ecddd3205cf1e2d5274649ff0eedd2991ed7f414"
|
||||||
|
integrity sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==
|
||||||
|
dependencies:
|
||||||
|
tslib "^2.4.0"
|
||||||
|
|
||||||
"@types/babel__core@^7.20.5":
|
"@types/babel__core@^7.20.5":
|
||||||
version "7.20.5"
|
version "7.20.5"
|
||||||
resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz"
|
resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz"
|
||||||
@@ -870,7 +1200,7 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/estree" "*"
|
"@types/estree" "*"
|
||||||
|
|
||||||
"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.6", "@types/estree@1.0.8":
|
"@types/estree@*", "@types/estree@1.0.8", "@types/estree@^1.0.0", "@types/estree@^1.0.6":
|
||||||
version "1.0.8"
|
version "1.0.8"
|
||||||
resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz"
|
resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz"
|
||||||
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
|
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
|
||||||
@@ -904,7 +1234,7 @@
|
|||||||
resolved "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz"
|
resolved "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz"
|
||||||
integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==
|
integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==
|
||||||
|
|
||||||
"@types/node@^18.0.0 || ^20.0.0 || >=22.0.0", "@types/node@^22.15.24":
|
"@types/node@^22.15.24":
|
||||||
version "22.18.13"
|
version "22.18.13"
|
||||||
resolved "https://registry.npmjs.org/@types/node/-/node-22.18.13.tgz"
|
resolved "https://registry.npmjs.org/@types/node/-/node-22.18.13.tgz"
|
||||||
integrity sha512-Bo45YKIjnmFtv6I1TuC8AaHBbqXtIo+Om5fE4QiU1Tj8QR/qt+8O3BAtOimG5IFmwaWiPmB3Mv3jtYzBA4Us2A==
|
integrity sha512-Bo45YKIjnmFtv6I1TuC8AaHBbqXtIo+Om5fE4QiU1Tj8QR/qt+8O3BAtOimG5IFmwaWiPmB3Mv3jtYzBA4Us2A==
|
||||||
@@ -951,7 +1281,7 @@
|
|||||||
resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz"
|
resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz"
|
||||||
integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==
|
integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==
|
||||||
|
|
||||||
"@types/react@*", "@types/react@^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react@^18.2.25 || ^19", "@types/react@^19.1.2", "@types/react@^19.2.0", "@types/react@>=16.8", "@types/react@>=18", "@types/react@>=18.0.0":
|
"@types/react@^19.1.2":
|
||||||
version "19.2.2"
|
version "19.2.2"
|
||||||
resolved "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz"
|
resolved "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz"
|
||||||
integrity sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==
|
integrity sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==
|
||||||
@@ -970,7 +1300,7 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/estree" "*"
|
"@types/estree" "*"
|
||||||
|
|
||||||
"@types/three@*", "@types/three@>=0.134.0":
|
"@types/three@*":
|
||||||
version "0.180.0"
|
version "0.180.0"
|
||||||
resolved "https://registry.npmjs.org/@types/three/-/three-0.180.0.tgz"
|
resolved "https://registry.npmjs.org/@types/three/-/three-0.180.0.tgz"
|
||||||
integrity sha512-ykFtgCqNnY0IPvDro7h+9ZeLY+qjgUWv+qEvUt84grhenO60Hqd4hScHE7VTB9nOQ/3QM8lkbNE+4vKjEpUxKg==
|
integrity sha512-ykFtgCqNnY0IPvDro7h+9ZeLY+qjgUWv+qEvUt84grhenO60Hqd4hScHE7VTB9nOQ/3QM8lkbNE+4vKjEpUxKg==
|
||||||
@@ -1018,7 +1348,7 @@
|
|||||||
natural-compare "^1.4.0"
|
natural-compare "^1.4.0"
|
||||||
ts-api-utils "^2.1.0"
|
ts-api-utils "^2.1.0"
|
||||||
|
|
||||||
"@typescript-eslint/parser@^8.46.2", "@typescript-eslint/parser@8.46.2":
|
"@typescript-eslint/parser@8.46.2":
|
||||||
version "8.46.2"
|
version "8.46.2"
|
||||||
resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.2.tgz"
|
resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.2.tgz"
|
||||||
integrity sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==
|
integrity sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==
|
||||||
@@ -1046,7 +1376,7 @@
|
|||||||
"@typescript-eslint/types" "8.46.2"
|
"@typescript-eslint/types" "8.46.2"
|
||||||
"@typescript-eslint/visitor-keys" "8.46.2"
|
"@typescript-eslint/visitor-keys" "8.46.2"
|
||||||
|
|
||||||
"@typescript-eslint/tsconfig-utils@^8.46.2", "@typescript-eslint/tsconfig-utils@8.46.2":
|
"@typescript-eslint/tsconfig-utils@8.46.2", "@typescript-eslint/tsconfig-utils@^8.46.2":
|
||||||
version "8.46.2"
|
version "8.46.2"
|
||||||
resolved "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz"
|
resolved "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz"
|
||||||
integrity sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==
|
integrity sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==
|
||||||
@@ -1062,7 +1392,7 @@
|
|||||||
debug "^4.3.4"
|
debug "^4.3.4"
|
||||||
ts-api-utils "^2.1.0"
|
ts-api-utils "^2.1.0"
|
||||||
|
|
||||||
"@typescript-eslint/types@^8.46.2", "@typescript-eslint/types@8.46.2":
|
"@typescript-eslint/types@8.46.2", "@typescript-eslint/types@^8.46.2":
|
||||||
version "8.46.2"
|
version "8.46.2"
|
||||||
resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.2.tgz"
|
resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.2.tgz"
|
||||||
integrity sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==
|
integrity sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==
|
||||||
@@ -1145,7 +1475,7 @@ acorn-jsx@^5.3.2:
|
|||||||
resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
|
resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
|
||||||
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
|
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
|
||||||
|
|
||||||
"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.15.0:
|
acorn@^8.15.0:
|
||||||
version "8.15.0"
|
version "8.15.0"
|
||||||
resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz"
|
resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz"
|
||||||
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
|
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
|
||||||
@@ -1249,7 +1579,7 @@ braces@^3.0.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
fill-range "^7.1.1"
|
fill-range "^7.1.1"
|
||||||
|
|
||||||
browserslist@^4.24.0, "browserslist@>= 4.21.0":
|
browserslist@^4.24.0:
|
||||||
version "4.27.0"
|
version "4.27.0"
|
||||||
resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz"
|
resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz"
|
||||||
integrity sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==
|
integrity sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==
|
||||||
@@ -1546,7 +1876,7 @@ earcut@^3.0.0, earcut@^3.0.2:
|
|||||||
resolved "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz"
|
resolved "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz"
|
||||||
integrity sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==
|
integrity sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==
|
||||||
|
|
||||||
easymde@^2.20.0, "easymde@>= 2.0.0 < 3.0.0":
|
easymde@^2.20.0:
|
||||||
version "2.20.0"
|
version "2.20.0"
|
||||||
resolved "https://registry.npmjs.org/easymde/-/easymde-2.20.0.tgz"
|
resolved "https://registry.npmjs.org/easymde/-/easymde-2.20.0.tgz"
|
||||||
integrity sha512-V1Z5f92TfR42Na852OWnIZMbM7zotWQYTddNaLYZFVKj7APBbyZ3FYJ27gBw2grMW3R6Qdv9J8n5Ij7XRSIgXQ==
|
integrity sha512-V1Z5f92TfR42Na852OWnIZMbM7zotWQYTddNaLYZFVKj7APBbyZ3FYJ27gBw2grMW3R6Qdv9J8n5Ij7XRSIgXQ==
|
||||||
@@ -1689,7 +2019,7 @@ eslint-visitor-keys@^4.2.1:
|
|||||||
resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz"
|
resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz"
|
||||||
integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==
|
integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==
|
||||||
|
|
||||||
"eslint@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0", "eslint@^6.0.0 || ^7.0.0 || >=8.0.0", "eslint@^8.57.0 || ^9.0.0", eslint@^9.25.0, eslint@>=8.40:
|
eslint@^9.25.0:
|
||||||
version "9.38.0"
|
version "9.38.0"
|
||||||
resolved "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz"
|
resolved "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz"
|
||||||
integrity sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==
|
integrity sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==
|
||||||
@@ -1815,12 +2145,7 @@ fastq@^1.6.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
reusify "^1.0.4"
|
reusify "^1.0.4"
|
||||||
|
|
||||||
fdir@^6.4.4:
|
fdir@^6.4.4, fdir@^6.5.0:
|
||||||
version "6.5.0"
|
|
||||||
resolved "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz"
|
|
||||||
integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==
|
|
||||||
|
|
||||||
fdir@^6.5.0:
|
|
||||||
version "6.5.0"
|
version "6.5.0"
|
||||||
resolved "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz"
|
resolved "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz"
|
||||||
integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==
|
integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==
|
||||||
@@ -2284,7 +2609,7 @@ its-fine@^2.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/react-reconciler" "^0.28.9"
|
"@types/react-reconciler" "^0.28.9"
|
||||||
|
|
||||||
jiti@*, jiti@^2.6.1, jiti@>=1.21.0:
|
jiti@^2.6.1:
|
||||||
version "2.6.1"
|
version "2.6.1"
|
||||||
resolved "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz"
|
resolved "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz"
|
||||||
integrity sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==
|
integrity sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==
|
||||||
@@ -2363,12 +2688,62 @@ lie@^3.0.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
immediate "~3.0.5"
|
immediate "~3.0.5"
|
||||||
|
|
||||||
|
lightningcss-android-arm64@1.30.2:
|
||||||
|
version "1.30.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz#6966b7024d39c94994008b548b71ab360eb3a307"
|
||||||
|
integrity sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==
|
||||||
|
|
||||||
lightningcss-darwin-arm64@1.30.2:
|
lightningcss-darwin-arm64@1.30.2:
|
||||||
version "1.30.2"
|
version "1.30.2"
|
||||||
resolved "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz"
|
resolved "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz"
|
||||||
integrity sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==
|
integrity sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==
|
||||||
|
|
||||||
lightningcss@^1.21.0, lightningcss@1.30.2:
|
lightningcss-darwin-x64@1.30.2:
|
||||||
|
version "1.30.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz#5ce87e9cd7c4f2dcc1b713f5e8ee185c88d9b7cd"
|
||||||
|
integrity sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==
|
||||||
|
|
||||||
|
lightningcss-freebsd-x64@1.30.2:
|
||||||
|
version "1.30.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz#6ae1d5e773c97961df5cff57b851807ef33692a5"
|
||||||
|
integrity sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==
|
||||||
|
|
||||||
|
lightningcss-linux-arm-gnueabihf@1.30.2:
|
||||||
|
version "1.30.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz#62c489610c0424151a6121fa99d77731536cdaeb"
|
||||||
|
integrity sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==
|
||||||
|
|
||||||
|
lightningcss-linux-arm64-gnu@1.30.2:
|
||||||
|
version "1.30.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz#2a3661b56fe95a0cafae90be026fe0590d089298"
|
||||||
|
integrity sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==
|
||||||
|
|
||||||
|
lightningcss-linux-arm64-musl@1.30.2:
|
||||||
|
version "1.30.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz#d7ddd6b26959245e026bc1ad9eb6aa983aa90e6b"
|
||||||
|
integrity sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==
|
||||||
|
|
||||||
|
lightningcss-linux-x64-gnu@1.30.2:
|
||||||
|
version "1.30.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz#5a89814c8e63213a5965c3d166dff83c36152b1a"
|
||||||
|
integrity sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==
|
||||||
|
|
||||||
|
lightningcss-linux-x64-musl@1.30.2:
|
||||||
|
version "1.30.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz#808c2e91ce0bf5d0af0e867c6152e5378c049728"
|
||||||
|
integrity sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==
|
||||||
|
|
||||||
|
lightningcss-win32-arm64-msvc@1.30.2:
|
||||||
|
version "1.30.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz#ab4a8a8a2e6a82a4531e8bbb6bf0ff161ee6625a"
|
||||||
|
integrity sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==
|
||||||
|
|
||||||
|
lightningcss-win32-x64-msvc@1.30.2:
|
||||||
|
version "1.30.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz#f01f382c8e0a27e1c018b0bee316d210eac43b6e"
|
||||||
|
integrity sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==
|
||||||
|
|
||||||
|
lightningcss@1.30.2:
|
||||||
version "1.30.2"
|
version "1.30.2"
|
||||||
resolved "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz"
|
resolved "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz"
|
||||||
integrity sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==
|
integrity sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==
|
||||||
@@ -2812,7 +3187,7 @@ mobx-react-lite@^4.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
use-sync-external-store "^1.4.0"
|
use-sync-external-store "^1.4.0"
|
||||||
|
|
||||||
mobx@^6.13.7, mobx@^6.9.0:
|
mobx@^6.13.7:
|
||||||
version "6.15.0"
|
version "6.15.0"
|
||||||
resolved "https://registry.npmjs.org/mobx/-/mobx-6.15.0.tgz"
|
resolved "https://registry.npmjs.org/mobx/-/mobx-6.15.0.tgz"
|
||||||
integrity sha512-UczzB+0nnwGotYSgllfARAqWCJ5e/skuV2K/l+Zyck/H6pJIhLXuBnz+6vn2i211o7DtbE78HQtsYEKICHGI+g==
|
integrity sha512-UczzB+0nnwGotYSgllfARAqWCJ5e/skuV2K/l+Zyck/H6pJIhLXuBnz+6vn2i211o7DtbE78HQtsYEKICHGI+g==
|
||||||
@@ -2993,12 +3368,12 @@ picomatch@^2.3.1:
|
|||||||
resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz"
|
resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz"
|
||||||
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
||||||
|
|
||||||
"picomatch@^3 || ^4", picomatch@^4.0.2, picomatch@^4.0.3:
|
picomatch@^4.0.2, picomatch@^4.0.3:
|
||||||
version "4.0.3"
|
version "4.0.3"
|
||||||
resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz"
|
resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz"
|
||||||
integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
|
integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
|
||||||
|
|
||||||
pixi.js@^8.10.1, pixi.js@^8.2.6:
|
pixi.js@^8.10.1:
|
||||||
version "8.14.0"
|
version "8.14.0"
|
||||||
resolved "https://registry.npmjs.org/pixi.js/-/pixi.js-8.14.0.tgz"
|
resolved "https://registry.npmjs.org/pixi.js/-/pixi.js-8.14.0.tgz"
|
||||||
integrity sha512-ituDiEBb1Oqx56RYwTtC6MjPUhPfF/i15fpUv5oEqmzC/ce3SaSumulJcOjKG7+y0J0Ekl9Rl4XTxaUw+MVFZw==
|
integrity sha512-ituDiEBb1Oqx56RYwTtC6MjPUhPfF/i15fpUv5oEqmzC/ce3SaSumulJcOjKG7+y0J0Ekl9Rl4XTxaUw+MVFZw==
|
||||||
@@ -3055,7 +3430,7 @@ promise-worker-transferable@^1.0.4:
|
|||||||
is-promise "^2.1.0"
|
is-promise "^2.1.0"
|
||||||
lie "^3.0.2"
|
lie "^3.0.2"
|
||||||
|
|
||||||
prop-types@^15.5.4, prop-types@^15.6.2, prop-types@^15.8.1:
|
prop-types@^15.6.2, prop-types@^15.8.1:
|
||||||
version "15.8.1"
|
version "15.8.1"
|
||||||
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz"
|
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz"
|
||||||
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
||||||
@@ -3116,19 +3491,14 @@ rbush@^4.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
quickselect "^3.0.0"
|
quickselect "^3.0.0"
|
||||||
|
|
||||||
"react-dom@^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom@^18 || ^19", "react-dom@^18.0.0 || ^19.0.0", react-dom@^19, react-dom@^19.0.0, react-dom@^19.1.0, react-dom@>=16.0.0, react-dom@>=16.13, react-dom@>=16.6.0, react-dom@>=16.8.2, react-dom@>=18:
|
react-dom@^19.1.0:
|
||||||
version "19.2.0"
|
version "19.2.0"
|
||||||
resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz"
|
resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz"
|
||||||
integrity sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==
|
integrity sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
scheduler "^0.27.0"
|
scheduler "^0.27.0"
|
||||||
|
|
||||||
react-is@^16.13.1:
|
react-is@^16.13.1, react-is@^16.7.0:
|
||||||
version "16.13.1"
|
|
||||||
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
|
|
||||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
|
||||||
|
|
||||||
react-is@^16.7.0:
|
|
||||||
version "16.13.1"
|
version "16.13.1"
|
||||||
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
|
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
|
||||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||||
@@ -3162,7 +3532,7 @@ react-photo-sphere-viewer@^6.2.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
eventemitter3 "^5.0.1"
|
eventemitter3 "^5.0.1"
|
||||||
|
|
||||||
react-reconciler@^0.31.0, react-reconciler@0.31.0:
|
react-reconciler@0.31.0, react-reconciler@^0.31.0:
|
||||||
version "0.31.0"
|
version "0.31.0"
|
||||||
resolved "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.31.0.tgz"
|
resolved "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.31.0.tgz"
|
||||||
integrity sha512-7Ob7Z+URmesIsIVRjnLoDGwBEG/tVitidU0nMsqX/eeJaLY89RISO/10ERe0MqmzuKUUB1rmY+h1itMbUHg9BQ==
|
integrity sha512-7Ob7Z+URmesIsIVRjnLoDGwBEG/tVitidU0nMsqX/eeJaLY89RISO/10ERe0MqmzuKUUB1rmY+h1itMbUHg9BQ==
|
||||||
@@ -3189,7 +3559,7 @@ react-router-dom@^7.6.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
react-router "7.9.4"
|
react-router "7.9.4"
|
||||||
|
|
||||||
react-router@^7.9.4, react-router@7.9.4:
|
react-router@7.9.4, react-router@^7.9.4:
|
||||||
version "7.9.4"
|
version "7.9.4"
|
||||||
resolved "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz"
|
resolved "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz"
|
||||||
integrity sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA==
|
integrity sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA==
|
||||||
@@ -3226,12 +3596,12 @@ react-use-measure@^2.1.7:
|
|||||||
resolved "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz"
|
resolved "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz"
|
||||||
integrity sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==
|
integrity sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==
|
||||||
|
|
||||||
"react@^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17 || ^18 || ^19", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^17.0.0 || ^18.0.0 || ^19.0.0", "react@^18 || ^19", "react@^18.0 || ^19", "react@^18.0.0 || ^19.0.0", react@^19, react@^19.0.0, react@^19.1.0, react@^19.2.0, "react@>= 16.8.0", react@>=16.0.0, react@>=16.13, react@>=16.6.0, react@>=16.8, react@>=16.8.0, react@>=16.8.2, react@>=17.0, react@>=18, react@>=18.0.0, react@>=19.0.0:
|
react@^19.1.0:
|
||||||
version "19.2.0"
|
version "19.2.0"
|
||||||
resolved "https://registry.npmjs.org/react/-/react-19.2.0.tgz"
|
resolved "https://registry.npmjs.org/react/-/react-19.2.0.tgz"
|
||||||
integrity sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==
|
integrity sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==
|
||||||
|
|
||||||
redux@^5.0.0, redux@^5.0.1:
|
redux@^5.0.1:
|
||||||
version "5.0.1"
|
version "5.0.1"
|
||||||
resolved "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz"
|
resolved "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz"
|
||||||
integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==
|
integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==
|
||||||
@@ -3317,7 +3687,7 @@ rollup-plugin-visualizer@^6.0.5:
|
|||||||
source-map "^0.7.4"
|
source-map "^0.7.4"
|
||||||
yargs "^17.5.1"
|
yargs "^17.5.1"
|
||||||
|
|
||||||
rollup@^1.20.0||^2.0.0||^3.0.0||^4.0.0, rollup@^4.34.9, "rollup@2.x || 3.x || 4.x":
|
rollup@^4.34.9:
|
||||||
version "4.52.5"
|
version "4.52.5"
|
||||||
resolved "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz"
|
resolved "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz"
|
||||||
integrity sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==
|
integrity sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==
|
||||||
@@ -3503,7 +3873,7 @@ svg-parser@^2.0.4:
|
|||||||
resolved "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz"
|
resolved "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz"
|
||||||
integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==
|
integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==
|
||||||
|
|
||||||
tailwindcss@^4.1.8, "tailwindcss@>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1", tailwindcss@4.1.16:
|
tailwindcss@4.1.16, tailwindcss@^4.1.8:
|
||||||
version "4.1.16"
|
version "4.1.16"
|
||||||
resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz"
|
resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz"
|
||||||
integrity sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==
|
integrity sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==
|
||||||
@@ -3535,7 +3905,7 @@ three@^0.170.0:
|
|||||||
resolved "https://registry.npmjs.org/three/-/three-0.170.0.tgz"
|
resolved "https://registry.npmjs.org/three/-/three-0.170.0.tgz"
|
||||||
integrity sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==
|
integrity sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==
|
||||||
|
|
||||||
three@^0.177.0, "three@>= 0.159.0", three@>=0.125.0, three@>=0.126.1, three@>=0.128.0, three@>=0.134.0, three@>=0.137, three@>=0.156, three@>=0.159:
|
three@^0.177.0:
|
||||||
version "0.177.0"
|
version "0.177.0"
|
||||||
resolved "https://registry.npmjs.org/three/-/three-0.177.0.tgz"
|
resolved "https://registry.npmjs.org/three/-/three-0.177.0.tgz"
|
||||||
integrity sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg==
|
integrity sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg==
|
||||||
@@ -3605,9 +3975,9 @@ ts-api-utils@^2.1.0:
|
|||||||
resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz"
|
resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz"
|
||||||
integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==
|
integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==
|
||||||
|
|
||||||
tslib@^2.0.3:
|
tslib@^2.0.3, tslib@^2.4.0:
|
||||||
version "2.8.1"
|
version "2.8.1"
|
||||||
resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
|
||||||
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
||||||
|
|
||||||
tunnel-rat@^0.1.2:
|
tunnel-rat@^0.1.2:
|
||||||
@@ -3634,7 +4004,7 @@ typescript-eslint@^8.30.1:
|
|||||||
"@typescript-eslint/typescript-estree" "8.46.2"
|
"@typescript-eslint/typescript-estree" "8.46.2"
|
||||||
"@typescript-eslint/utils" "8.46.2"
|
"@typescript-eslint/utils" "8.46.2"
|
||||||
|
|
||||||
typescript@>=4.8.4, "typescript@>=4.8.4 <6.0.0", typescript@>=4.9.5, typescript@~5.8.3:
|
typescript@~5.8.3:
|
||||||
version "5.8.3"
|
version "5.8.3"
|
||||||
resolved "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz"
|
resolved "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz"
|
||||||
integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==
|
integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==
|
||||||
@@ -3715,7 +4085,7 @@ uri-js@^4.2.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
punycode "^2.1.0"
|
punycode "^2.1.0"
|
||||||
|
|
||||||
use-sync-external-store@^1.2.2, use-sync-external-store@^1.4.0, use-sync-external-store@^1.6.0, use-sync-external-store@>=1.2.0:
|
use-sync-external-store@^1.2.2, use-sync-external-store@^1.4.0, use-sync-external-store@^1.6.0:
|
||||||
version "1.6.0"
|
version "1.6.0"
|
||||||
resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz"
|
resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz"
|
||||||
integrity sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==
|
integrity sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==
|
||||||
@@ -3770,7 +4140,7 @@ vite-plugin-svgr@^4.5.0:
|
|||||||
"@svgr/core" "^8.1.0"
|
"@svgr/core" "^8.1.0"
|
||||||
"@svgr/plugin-jsx" "^8.1.0"
|
"@svgr/plugin-jsx" "^8.1.0"
|
||||||
|
|
||||||
"vite@^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", "vite@^5.2.0 || ^6 || ^7", vite@^6.3.5, vite@>=2.6.0:
|
vite@^6.3.5:
|
||||||
version "6.4.1"
|
version "6.4.1"
|
||||||
resolved "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz"
|
resolved "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz"
|
||||||
integrity sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==
|
integrity sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==
|
||||||
|
|||||||
Reference in New Issue
Block a user