Compare commits
5 Commits
bf117ef048
...
react
Author | SHA1 | Date | |
---|---|---|---|
34ba3c1db0
|
|||
4f038551a2 | |||
470a58a3fa | |||
89d7fc2748 | |||
97f95fc394 |
32
Dockerfile
Normal file
32
Dockerfile
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# Stage 1: Build the application
|
||||||
|
FROM node:20-alpine AS build
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package.json and yarn.lock
|
||||||
|
COPY package.json yarn.lock ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN yarn install --frozen-lockfile
|
||||||
|
|
||||||
|
# Copy the rest of the application code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
RUN yarn build
|
||||||
|
|
||||||
|
# Stage 2: Serve the application with Nginx
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
# Copy the built application from the build stage
|
||||||
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Copy nginx configuration (optional, can be added later if needed)
|
||||||
|
# COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
# Expose port 80
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# Start Nginx server
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
38
Makefile
Normal file
38
Makefile
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# Variables
|
||||||
|
IMAGE_NAME = white-nights-admin-panel
|
||||||
|
IMAGE_TAG = latest
|
||||||
|
FULL_IMAGE_NAME = $(IMAGE_NAME):$(IMAGE_TAG)
|
||||||
|
ARCHIVE_NAME = white-nights-admin-panel-image.zip
|
||||||
|
|
||||||
|
# Default target
|
||||||
|
.PHONY: help
|
||||||
|
help:
|
||||||
|
@echo "Available commands:"
|
||||||
|
@echo " make build-image - Build Docker image"
|
||||||
|
@echo " make export-image - Build Docker image and export it to a zip archive"
|
||||||
|
@echo " make clean - Remove Docker image and zip archive"
|
||||||
|
@echo " make help - Show this help message"
|
||||||
|
|
||||||
|
# Build Docker image
|
||||||
|
.PHONY: build-image
|
||||||
|
build-image:
|
||||||
|
@echo "Building Docker image: $(FULL_IMAGE_NAME)"
|
||||||
|
docker build -t $(FULL_IMAGE_NAME) .
|
||||||
|
|
||||||
|
# Export Docker image to zip archive
|
||||||
|
.PHONY: export-image
|
||||||
|
export-image: build-image
|
||||||
|
@echo "Exporting Docker image to $(ARCHIVE_NAME)"
|
||||||
|
docker save $(FULL_IMAGE_NAME) | gzip > $(ARCHIVE_NAME)
|
||||||
|
@echo "Image exported successfully to $(ARCHIVE_NAME)"
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
.PHONY: clean
|
||||||
|
clean:
|
||||||
|
@echo "Removing Docker image and zip archive"
|
||||||
|
-docker rmi $(FULL_IMAGE_NAME) 2>/dev/null || true
|
||||||
|
-rm -f $(ARCHIVE_NAME) 2>/dev/null || true
|
||||||
|
@echo "Clean up completed"
|
||||||
|
|
||||||
|
# Default target when no arguments provided
|
||||||
|
.DEFAULT_GOAL := help
|
File diff suppressed because it is too large
Load Diff
@ -45,10 +45,10 @@ export const MediaEditPage = observer(() => {
|
|||||||
if (id) {
|
if (id) {
|
||||||
mediaStore.getOneMedia(id);
|
mediaStore.getOneMedia(id);
|
||||||
}
|
}
|
||||||
console.log(newFile);
|
|
||||||
console.log(uploadDialogOpen);
|
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
|
useEffect(() => {}, [newFile, uploadDialogOpen]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (media) {
|
if (media) {
|
||||||
setMediaName(media.media_name);
|
setMediaName(media.media_name);
|
||||||
|
@ -140,9 +140,7 @@ export const LinkedItemsContents = <
|
|||||||
const [activeTab, setActiveTab] = useState(0);
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {}, [error]);
|
||||||
console.log(error);
|
|
||||||
}, [error]);
|
|
||||||
|
|
||||||
const parentResource = "route";
|
const parentResource = "route";
|
||||||
const childResource = "station";
|
const childResource = "station";
|
||||||
@ -227,7 +225,7 @@ export const LinkedItemsContents = <
|
|||||||
if (type === "edit") {
|
if (type === "edit") {
|
||||||
setError(null);
|
setError(null);
|
||||||
authInstance
|
authInstance
|
||||||
.get(`/${childResource}/`)
|
.get(`/${childResource}`)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
setAllItems(response?.data || []);
|
setAllItems(response?.data || []);
|
||||||
})
|
})
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
export const UP_SCALE = 30000;
|
export const UP_SCALE = 10000;
|
||||||
export const PATH_WIDTH = 15;
|
export const PATH_WIDTH = 5;
|
||||||
export const STATION_RADIUS = 20;
|
export const STATION_RADIUS = 8;
|
||||||
export const STATION_OUTLINE_WIDTH = 10;
|
export const STATION_OUTLINE_WIDTH = 4;
|
||||||
export const SIGHT_SIZE = 40;
|
export const SIGHT_SIZE = 40;
|
||||||
export const SCALE_FACTOR = 50;
|
export const SCALE_FACTOR = 50;
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { useTransform } from "./TransformContext";
|
import { useTransform } from "./TransformContext";
|
||||||
import { coordinatesToLocal, localToCoordinates } from "./utils";
|
import { coordinatesToLocal, localToCoordinates } from "./utils";
|
||||||
import { SCALE_FACTOR } from "./Constants";
|
import { SCALE_FACTOR } from "./Constants";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
export function RightSidebar() {
|
export function RightSidebar() {
|
||||||
const {
|
const {
|
||||||
@ -360,8 +361,14 @@ export function RightSidebar() {
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
sx={{ mt: 2 }}
|
sx={{ mt: 2 }}
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
saveChanges();
|
try {
|
||||||
|
await saveChanges();
|
||||||
|
toast.success("Изменения сохранены");
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
toast.error("Ошибка при сохранении изменений");
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Сохранить изменения
|
Сохранить изменения
|
||||||
|
@ -82,11 +82,7 @@ export const Sight = ({ sight, id }: Readonly<SightProps>) => {
|
|||||||
Assets.load("/SightIcon.png").then(setTexture);
|
Assets.load("/SightIcon.png").then(setTexture);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {}, [id, sight.latitude, sight.longitude]);
|
||||||
console.log(
|
|
||||||
`Rendering Sight ${id + 1} at [${sight.latitude}, ${sight.longitude}]`
|
|
||||||
);
|
|
||||||
}, [id, sight.latitude, sight.longitude]);
|
|
||||||
|
|
||||||
if (!sight) {
|
if (!sight) {
|
||||||
console.error("sight is null");
|
console.error("sight is null");
|
||||||
|
@ -57,8 +57,8 @@ export function Widgets() {
|
|||||||
mb: 1,
|
mb: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}>
|
<Box sx={{ display: "flex", gap: 0.5 }}>
|
||||||
<Landmark size={16} />
|
<Landmark size={16} className="shrink-0" />
|
||||||
<Typography
|
<Typography
|
||||||
variant="subtitle2"
|
variant="subtitle2"
|
||||||
sx={{ color: "#fff", fontWeight: "bold" }}
|
sx={{ color: "#fff", fontWeight: "bold" }}
|
||||||
|
@ -26,6 +26,7 @@ import { Sight } from "./Sight";
|
|||||||
import { SightData } from "./types";
|
import { SightData } from "./types";
|
||||||
import { Station } from "./Station";
|
import { Station } from "./Station";
|
||||||
import { UP_SCALE } from "./Constants";
|
import { UP_SCALE } from "./Constants";
|
||||||
|
import CircularProgress from "@mui/material/CircularProgress";
|
||||||
|
|
||||||
extend({
|
extend({
|
||||||
Container,
|
Container,
|
||||||
@ -36,13 +37,27 @@ extend({
|
|||||||
Text,
|
Text,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const Loading = () => {
|
||||||
|
const { isRouteLoading, isStationLoading, isSightLoading } = useMapData();
|
||||||
|
|
||||||
|
if (isRouteLoading || isStationLoading || isSightLoading) {
|
||||||
|
return (
|
||||||
|
<div className="fixed flex z-1 items-center justify-center h-screen w-screen bg-[#111]">
|
||||||
|
<CircularProgress />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
export const RoutePreview = () => {
|
export const RoutePreview = () => {
|
||||||
|
const { routeData, stationData, sightData } = useMapData();
|
||||||
return (
|
return (
|
||||||
<MapDataProvider>
|
<MapDataProvider>
|
||||||
<TransformProvider>
|
<TransformProvider>
|
||||||
<Stack direction="row" height="100vh" width="100vw" overflow="hidden">
|
<Stack direction="row" height="100vh" width="100vw" overflow="hidden">
|
||||||
<LanguageSwitcher />
|
{routeData && stationData && sightData ? <LanguageSwitcher /> : null}
|
||||||
|
<Loading />
|
||||||
<LeftSidebar />
|
<LeftSidebar />
|
||||||
<Stack direction="row" flex={1} position="relative" height="100%">
|
<Stack direction="row" flex={1} position="relative" height="100%">
|
||||||
<RouteMap />
|
<RouteMap />
|
||||||
@ -145,12 +160,12 @@ export const RouteMap = observer(() => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if (!routeData || !stationData || !sightData) {
|
if (!routeData || !stationData || !sightData) {
|
||||||
console.error("routeData, stationData or sightData is null");
|
return null;
|
||||||
return <div>Loading...</div>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width: "100%", height: "100%" }} ref={parentRef}>
|
<div style={{ width: "100%", height: "100%" }} ref={parentRef}>
|
||||||
|
<LanguageSwitcher />
|
||||||
<Application resizeTo={parentRef} background="#fff">
|
<Application resizeTo={parentRef} background="#fff">
|
||||||
<InfiniteCanvas>
|
<InfiniteCanvas>
|
||||||
<TravelPath points={points} />
|
<TravelPath points={points} />
|
||||||
|
@ -132,7 +132,6 @@ export const SightListPage = observer(() => {
|
|||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
localeText={ruRU.components.MuiDataGrid.defaultProps.localeText}
|
localeText={ruRU.components.MuiDataGrid.defaultProps.localeText}
|
||||||
onRowSelectionModelChange={(newSelection) => {
|
onRowSelectionModelChange={(newSelection) => {
|
||||||
console.log(newSelection);
|
|
||||||
setIds(Array.from(newSelection.ids as unknown as number[]));
|
setIds(Array.from(newSelection.ids as unknown as number[]));
|
||||||
}}
|
}}
|
||||||
slots={{
|
slots={{
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Button, Paper, TextField } from "@mui/material";
|
import { Button, TextField } from "@mui/material";
|
||||||
import { snapshotStore } from "@shared";
|
import { snapshotStore } from "@shared";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { ArrowLeft, Loader2, Save } from "lucide-react";
|
import { ArrowLeft, Loader2, Save } from "lucide-react";
|
||||||
@ -20,7 +20,8 @@ export const SnapshotCreatePage = observer(() => {
|
|||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
<div className="w-full h-[400px] flex justify-center items-center">
|
||||||
|
<div className="w-full h-full p-3 flex flex-col gap-10">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
@ -51,11 +52,15 @@ export const SnapshotCreatePage = observer(() => {
|
|||||||
await createSnapshot(name);
|
await createSnapshot(name);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
toast.success("Снапшот успешно создан");
|
toast.success("Снапшот успешно создан");
|
||||||
|
navigate(-1);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
toast.error("Ошибка при создании снапшота");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={isLoading}
|
disabled={isLoading || !name.trim()}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Loader2 size={20} className="animate-spin" />
|
<Loader2 size={20} className="animate-spin" />
|
||||||
@ -64,6 +69,7 @@ export const SnapshotCreatePage = observer(() => {
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Paper>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -117,12 +117,15 @@ export const SnapshotListPage = observer(() => {
|
|||||||
|
|
||||||
<SnapshotRestore
|
<SnapshotRestore
|
||||||
open={isRestoreModalOpen}
|
open={isRestoreModalOpen}
|
||||||
|
loading={isLoading}
|
||||||
onDelete={async () => {
|
onDelete={async () => {
|
||||||
|
setIsLoading(true);
|
||||||
if (rowId) {
|
if (rowId) {
|
||||||
await restoreSnapshot(rowId);
|
await restoreSnapshot(rowId);
|
||||||
}
|
}
|
||||||
setIsRestoreModalOpen(false);
|
setIsRestoreModalOpen(false);
|
||||||
setRowId(null);
|
setRowId(null);
|
||||||
|
setIsLoading(false);
|
||||||
}}
|
}}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
setIsRestoreModalOpen(false);
|
setIsRestoreModalOpen(false);
|
||||||
|
@ -1,3 +1,2 @@
|
|||||||
export * from "./SnapshotListPage";
|
export * from "./SnapshotListPage";
|
||||||
|
|
||||||
export * from "./SnapshotCreatePage";
|
export * from "./SnapshotCreatePage";
|
||||||
|
@ -93,9 +93,7 @@ export const LinkedSightsContents = <
|
|||||||
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);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {}, [error]);
|
||||||
console.log(error);
|
|
||||||
}, [error]);
|
|
||||||
|
|
||||||
const parentResource = "station";
|
const parentResource = "station";
|
||||||
const childResource = "sight";
|
const childResource = "sight";
|
||||||
@ -178,7 +176,7 @@ export const LinkedSightsContents = <
|
|||||||
if (type === "edit") {
|
if (type === "edit") {
|
||||||
setError(null);
|
setError(null);
|
||||||
authInstance
|
authInstance
|
||||||
.get(`/${childResource}/`)
|
.get(`/${childResource}`)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
setAllItems(response?.data || []);
|
setAllItems(response?.data || []);
|
||||||
})
|
})
|
||||||
|
@ -81,6 +81,7 @@ export const UploadMediaDialog = observer(
|
|||||||
const [availableMediaTypes, setAvailableMediaTypes] = useState<number[]>(
|
const [availableMediaTypes, setAvailableMediaTypes] = useState<number[]>(
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
const [isPreviewLoaded, setIsPreviewLoaded] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialFile) {
|
if (initialFile) {
|
||||||
@ -207,6 +208,7 @@ export const UploadMediaDialog = observer(
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mediaFile) {
|
if (mediaFile) {
|
||||||
setMediaUrl(URL.createObjectURL(mediaFile as Blob));
|
setMediaUrl(URL.createObjectURL(mediaFile as Blob));
|
||||||
|
setIsPreviewLoaded(false); // Сбрасываем состояние загрузки при смене файла
|
||||||
}
|
}
|
||||||
}, [mediaFile]);
|
}, [mediaFile]);
|
||||||
|
|
||||||
@ -326,8 +328,22 @@ export const UploadMediaDialog = observer(
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
|
position: "relative",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{!isPreviewLoaded && mediaUrl && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "50%",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translate(-50%, -50%)",
|
||||||
|
zIndex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CircularProgress />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
{mediaType == 2 && mediaUrl && (
|
{mediaType == 2 && mediaUrl && (
|
||||||
<video
|
<video
|
||||||
src={mediaUrl}
|
src={mediaUrl}
|
||||||
@ -336,10 +352,16 @@ export const UploadMediaDialog = observer(
|
|||||||
loop
|
loop
|
||||||
controls
|
controls
|
||||||
style={{ maxWidth: "100%", maxHeight: "100%" }}
|
style={{ maxWidth: "100%", maxHeight: "100%" }}
|
||||||
|
onLoadedData={() => setIsPreviewLoaded(true)}
|
||||||
|
onError={() => setIsPreviewLoaded(true)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{mediaType === 6 && mediaUrl && (
|
{mediaType === 6 && mediaUrl && (
|
||||||
<ModelViewer3D fileUrl={mediaUrl} height="100%" />
|
<ModelViewer3D
|
||||||
|
fileUrl={mediaUrl}
|
||||||
|
height="100%"
|
||||||
|
onLoad={() => setIsPreviewLoaded(true)}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{mediaType !== 6 && mediaType !== 2 && mediaUrl && (
|
{mediaType !== 6 && mediaType !== 2 && mediaUrl && (
|
||||||
<img
|
<img
|
||||||
@ -349,6 +371,8 @@ export const UploadMediaDialog = observer(
|
|||||||
height: "100%",
|
height: "100%",
|
||||||
objectFit: "contain",
|
objectFit: "contain",
|
||||||
}}
|
}}
|
||||||
|
onLoad={() => setIsPreviewLoaded(true)}
|
||||||
|
onError={() => setIsPreviewLoaded(true)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Paper>
|
</Paper>
|
||||||
@ -370,9 +394,17 @@ export const UploadMediaDialog = observer(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={isLoading || (!mediaName && !mediaFilename)}
|
disabled={
|
||||||
|
isLoading ||
|
||||||
|
(!mediaName && !mediaFilename) ||
|
||||||
|
!isPreviewLoaded
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{isLoading ? "Сохранение..." : "Сохранить"}
|
{isLoading
|
||||||
|
? "Сохранение..."
|
||||||
|
: !isPreviewLoaded
|
||||||
|
? "Загрузка превью..."
|
||||||
|
: "Сохранить"}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -497,9 +497,7 @@ class EditSightStore {
|
|||||||
media_name: media_name,
|
media_name: media_name,
|
||||||
media_type: type,
|
media_type: type,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {}
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
createLinkWithArticle = async (media: {
|
createLinkWithArticle = async (media: {
|
||||||
|
@ -82,11 +82,6 @@ class RouteStore {
|
|||||||
};
|
};
|
||||||
|
|
||||||
setRouteStations = (routeId: number, stationId: number, data: any) => {
|
setRouteStations = (routeId: number, stationId: number, data: any) => {
|
||||||
console.log(
|
|
||||||
this.routeStations[routeId],
|
|
||||||
stationId,
|
|
||||||
this.routeStations[routeId].find((station) => station.id === stationId)
|
|
||||||
);
|
|
||||||
this.routeStations[routeId] = this.routeStations[routeId]?.map((station) =>
|
this.routeStations[routeId] = this.routeStations[routeId]?.map((station) =>
|
||||||
station.id === stationId ? { ...station, ...data } : station
|
station.id === stationId ? { ...station, ...data } : station
|
||||||
);
|
);
|
||||||
|
@ -97,15 +97,19 @@ class SightsStore {
|
|||||||
city: number,
|
city: number,
|
||||||
coordinates: { latitude: number; longitude: number }
|
coordinates: { latitude: number; longitude: number }
|
||||||
) => {
|
) => {
|
||||||
const id = (
|
const response = await authInstance.post("/sight", {
|
||||||
await authInstance.post("/sight", {
|
|
||||||
name: this.createSight[languageStore.language].name,
|
name: this.createSight[languageStore.language].name,
|
||||||
address: this.createSight[languageStore.language].address,
|
address: this.createSight[languageStore.language].address,
|
||||||
city_id: city,
|
city_id: city,
|
||||||
latitude: coordinates.latitude,
|
latitude: coordinates.latitude,
|
||||||
longitude: coordinates.longitude,
|
longitude: coordinates.longitude,
|
||||||
})
|
});
|
||||||
).data.id;
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.sights.push(response.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
const id = response.data.id;
|
||||||
|
|
||||||
const anotherLanguages = ["ru", "en", "zh"].filter(
|
const anotherLanguages = ["ru", "en", "zh"].filter(
|
||||||
(language) => language !== languageStore.language
|
(language) => language !== languageStore.language
|
||||||
|
@ -1,6 +1,24 @@
|
|||||||
import { authInstance } from "@shared";
|
import { authInstance } from "@shared";
|
||||||
|
|
||||||
import { makeAutoObservable, runInAction } from "mobx";
|
import { makeAutoObservable, runInAction } from "mobx";
|
||||||
|
// Импорт функции сброса кешей карты
|
||||||
|
// import { clearMapCaches } from "../../pages/MapPage";
|
||||||
|
import {
|
||||||
|
articlesStore,
|
||||||
|
cityStore,
|
||||||
|
countryStore,
|
||||||
|
carrierStore,
|
||||||
|
stationsStore,
|
||||||
|
sightsStore,
|
||||||
|
routeStore,
|
||||||
|
vehicleStore,
|
||||||
|
userStore,
|
||||||
|
mediaStore,
|
||||||
|
createSightStore,
|
||||||
|
editSightStore,
|
||||||
|
devicesStore,
|
||||||
|
authStore,
|
||||||
|
} from "@shared";
|
||||||
|
|
||||||
type Snapshot = {
|
type Snapshot = {
|
||||||
ID: string;
|
ID: string;
|
||||||
@ -17,6 +35,248 @@ class SnapshotStore {
|
|||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Функция для сброса всех кешей в приложении
|
||||||
|
private clearAllCaches = () => {
|
||||||
|
// Сброс кешей статей
|
||||||
|
articlesStore.articleList = {
|
||||||
|
ru: { data: [], loaded: false },
|
||||||
|
en: { data: [], loaded: false },
|
||||||
|
zh: { data: [], loaded: false },
|
||||||
|
};
|
||||||
|
articlesStore.articlePreview = {};
|
||||||
|
articlesStore.articleData = null;
|
||||||
|
articlesStore.articleMedia = null;
|
||||||
|
|
||||||
|
// Сброс кешей городов
|
||||||
|
cityStore.cities = {
|
||||||
|
ru: { data: [], loaded: false },
|
||||||
|
en: { data: [], loaded: false },
|
||||||
|
zh: { data: [], loaded: false },
|
||||||
|
};
|
||||||
|
cityStore.ruCities = { data: [], loaded: false };
|
||||||
|
cityStore.city = {};
|
||||||
|
|
||||||
|
// Сброс кешей стран
|
||||||
|
countryStore.countries = {
|
||||||
|
ru: { data: [], loaded: false },
|
||||||
|
en: { data: [], loaded: false },
|
||||||
|
zh: { data: [], loaded: false },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Сброс кешей перевозчиков
|
||||||
|
carrierStore.carriers = {
|
||||||
|
ru: { data: [], loaded: false },
|
||||||
|
en: { data: [], loaded: false },
|
||||||
|
zh: { data: [], loaded: false },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Сброс кешей станций
|
||||||
|
stationsStore.stationLists = {
|
||||||
|
ru: { data: [], loaded: false },
|
||||||
|
en: { data: [], loaded: false },
|
||||||
|
zh: { data: [], loaded: false },
|
||||||
|
};
|
||||||
|
stationsStore.stationPreview = {};
|
||||||
|
|
||||||
|
// Сброс кешей достопримечательностей
|
||||||
|
sightsStore.sights = [];
|
||||||
|
sightsStore.sight = null;
|
||||||
|
|
||||||
|
// Сброс кешей маршрутов
|
||||||
|
routeStore.routes = { data: [], loaded: false };
|
||||||
|
|
||||||
|
// Сброс кешей транспорта
|
||||||
|
vehicleStore.vehicles = { data: [], loaded: false };
|
||||||
|
|
||||||
|
// Сброс кешей пользователей
|
||||||
|
userStore.users = { data: [], loaded: false };
|
||||||
|
|
||||||
|
// Сброс кешей медиа
|
||||||
|
mediaStore.media = [];
|
||||||
|
mediaStore.oneMedia = null;
|
||||||
|
|
||||||
|
// Сброс кешей создания и редактирования достопримечательностей
|
||||||
|
createSightStore.sight = JSON.parse(
|
||||||
|
JSON.stringify({
|
||||||
|
city_id: 0,
|
||||||
|
city: "",
|
||||||
|
latitude: 0,
|
||||||
|
longitude: 0,
|
||||||
|
thumbnail: null,
|
||||||
|
watermark_lu: null,
|
||||||
|
watermark_rd: null,
|
||||||
|
left_article: 0,
|
||||||
|
preview_media: null,
|
||||||
|
video_preview: null,
|
||||||
|
ru: {
|
||||||
|
name: "",
|
||||||
|
address: "",
|
||||||
|
left: { heading: "", body: "", media: [] },
|
||||||
|
right: [],
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
name: "",
|
||||||
|
address: "",
|
||||||
|
left: { heading: "", body: "", media: [] },
|
||||||
|
right: [],
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
name: "",
|
||||||
|
address: "",
|
||||||
|
left: { heading: "", body: "", media: [] },
|
||||||
|
right: [],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
createSightStore.uploadMediaOpen = false;
|
||||||
|
createSightStore.fileToUpload = null;
|
||||||
|
createSightStore.needLeaveAgree = false;
|
||||||
|
|
||||||
|
editSightStore.sight = {
|
||||||
|
common: {
|
||||||
|
id: 0,
|
||||||
|
city_id: 0,
|
||||||
|
city: "",
|
||||||
|
latitude: 0,
|
||||||
|
longitude: 0,
|
||||||
|
thumbnail: null,
|
||||||
|
watermark_lu: null,
|
||||||
|
watermark_rd: null,
|
||||||
|
left_article: 0,
|
||||||
|
preview_media: null,
|
||||||
|
video_preview: null,
|
||||||
|
},
|
||||||
|
ru: {
|
||||||
|
id: 0,
|
||||||
|
name: "",
|
||||||
|
address: "",
|
||||||
|
left: { heading: "", body: "", media: [] },
|
||||||
|
right: [],
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
id: 0,
|
||||||
|
name: "",
|
||||||
|
address: "",
|
||||||
|
left: { heading: "", body: "", media: [] },
|
||||||
|
right: [],
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
id: 0,
|
||||||
|
name: "",
|
||||||
|
address: "",
|
||||||
|
left: { heading: "", body: "", media: [] },
|
||||||
|
right: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
editSightStore.hasLoadedCommon = false;
|
||||||
|
editSightStore.uploadMediaOpen = false;
|
||||||
|
editSightStore.fileToUpload = null;
|
||||||
|
editSightStore.needLeaveAgree = false;
|
||||||
|
|
||||||
|
// Сброс кешей устройств
|
||||||
|
devicesStore.devices = [];
|
||||||
|
devicesStore.uuid = null;
|
||||||
|
devicesStore.sendSnapshotModalOpen = false;
|
||||||
|
|
||||||
|
// Сброс кешей авторизации (кроме токена)
|
||||||
|
authStore.payload = null;
|
||||||
|
authStore.error = null;
|
||||||
|
authStore.isLoading = false;
|
||||||
|
|
||||||
|
// Сброс кешей карты (если они загружены)
|
||||||
|
try {
|
||||||
|
// Сбрасываем кеши mapStore если он доступен
|
||||||
|
if (typeof window !== "undefined" && (window as any).mapStore) {
|
||||||
|
(window as any).mapStore.routes = [];
|
||||||
|
(window as any).mapStore.stations = [];
|
||||||
|
(window as any).mapStore.sights = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сбрасываем кеши MapService если он доступен
|
||||||
|
if (typeof window !== "undefined" && (window as any).mapServiceInstance) {
|
||||||
|
(window as any).mapServiceInstance.clearCaches();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Не удалось сбросить кеши карты:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сброс localStorage кешей (кроме токена авторизации)
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
const rememberedEmail = localStorage.getItem("rememberedEmail");
|
||||||
|
const rememberedPassword = localStorage.getItem("rememberedPassword");
|
||||||
|
|
||||||
|
localStorage.clear();
|
||||||
|
sessionStorage.clear();
|
||||||
|
|
||||||
|
// Восстанавливаем важные данные
|
||||||
|
if (token) localStorage.setItem("token", token);
|
||||||
|
if (rememberedEmail)
|
||||||
|
localStorage.setItem("rememberedEmail", rememberedEmail);
|
||||||
|
if (rememberedPassword)
|
||||||
|
localStorage.setItem("rememberedPassword", rememberedPassword);
|
||||||
|
|
||||||
|
// Сброс кешей карты (если они есть)
|
||||||
|
const mapPositionKey = "mapPosition";
|
||||||
|
const activeSectionKey = "mapActiveSection";
|
||||||
|
if (localStorage.getItem(mapPositionKey)) {
|
||||||
|
localStorage.removeItem(mapPositionKey);
|
||||||
|
}
|
||||||
|
if (localStorage.getItem(activeSectionKey)) {
|
||||||
|
localStorage.removeItem(activeSectionKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Попытка очистить кеш браузера (если поддерживается)
|
||||||
|
if ("caches" in window) {
|
||||||
|
try {
|
||||||
|
caches
|
||||||
|
.keys()
|
||||||
|
.then((cacheNames) => {
|
||||||
|
return Promise.all(
|
||||||
|
cacheNames.map((cacheName) => {
|
||||||
|
return caches.delete(cacheName);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
console.log("Кеш браузера очищен");
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.warn("Не удалось очистить кеш браузера:", error);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Кеш браузера не поддерживается:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Попытка очистить IndexedDB (если поддерживается)
|
||||||
|
if ("indexedDB" in window) {
|
||||||
|
try {
|
||||||
|
indexedDB
|
||||||
|
.databases()
|
||||||
|
.then((databases) => {
|
||||||
|
return Promise.all(
|
||||||
|
databases.map((db) => {
|
||||||
|
if (db.name) {
|
||||||
|
return indexedDB.deleteDatabase(db.name);
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
console.log("IndexedDB очищен");
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.warn("Не удалось очистить IndexedDB:", error);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("IndexedDB не поддерживается:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Все кеши приложения сброшены");
|
||||||
|
};
|
||||||
|
|
||||||
getSnapshots = async () => {
|
getSnapshots = async () => {
|
||||||
const response = await authInstance.get(`/snapshots`);
|
const response = await authInstance.get(`/snapshots`);
|
||||||
|
|
||||||
@ -42,6 +302,10 @@ class SnapshotStore {
|
|||||||
};
|
};
|
||||||
|
|
||||||
restoreSnapshot = async (id: string) => {
|
restoreSnapshot = async (id: string) => {
|
||||||
|
// Сначала сбрасываем все кеши
|
||||||
|
this.clearAllCaches();
|
||||||
|
|
||||||
|
// Затем восстанавливаем снапшот
|
||||||
await authInstance.post(`/snapshots/${id}/restore`);
|
await authInstance.post(`/snapshots/${id}/restore`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -202,7 +202,6 @@ export const DevicesTable = observer(() => {
|
|||||||
try {
|
try {
|
||||||
// Create an array of promises for all snapshot requests
|
// Create an array of promises for all snapshot requests
|
||||||
const snapshotPromises = selectedDeviceUuids.map((deviceUuid) => {
|
const snapshotPromises = selectedDeviceUuids.map((deviceUuid) => {
|
||||||
console.log(`Sending snapshot ${snapshotId} to device ${deviceUuid}`);
|
|
||||||
return send(deviceUuid);
|
return send(deviceUuid);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -51,7 +51,7 @@ export const LanguageSwitcher = observer(() => {
|
|||||||
key={lang}
|
key={lang}
|
||||||
onClick={() => handleLanguageChange(lang)}
|
onClick={() => handleLanguageChange(lang)}
|
||||||
variant={"contained"} // Highlight the active language
|
variant={"contained"} // Highlight the active language
|
||||||
color={language === lang ? "primary" : "secondary"}
|
color={language === lang ? "primary" : "inherit"}
|
||||||
sx={{ minWidth: "60px" }} // Give buttons a consistent width
|
sx={{ minWidth: "60px" }} // Give buttons a consistent width
|
||||||
>
|
>
|
||||||
{getLanguageLabel(lang)}
|
{getLanguageLabel(lang)}
|
||||||
|
@ -109,6 +109,7 @@ export const MediaArea = observer(
|
|||||||
media_type: m.media_type,
|
media_type: m.media_type,
|
||||||
filename: m.filename,
|
filename: m.filename,
|
||||||
}}
|
}}
|
||||||
|
height="40px"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
className="absolute top-2 right-2"
|
className="absolute top-2 right-2"
|
||||||
|
@ -12,11 +12,15 @@ export interface MediaData {
|
|||||||
export function MediaViewer({
|
export function MediaViewer({
|
||||||
media,
|
media,
|
||||||
className,
|
className,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
fullWidth,
|
fullWidth,
|
||||||
fullHeight,
|
fullHeight,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
media?: MediaData;
|
media?: MediaData;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
height?: string;
|
||||||
|
width?: string;
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
fullHeight?: boolean;
|
fullHeight?: boolean;
|
||||||
}>) {
|
}>) {
|
||||||
@ -42,8 +46,8 @@ export function MediaViewer({
|
|||||||
}/download?token=${token}`}
|
}/download?token=${token}`}
|
||||||
alt={media?.filename}
|
alt={media?.filename}
|
||||||
style={{
|
style={{
|
||||||
height: fullHeight ? "100%" : "auto",
|
height: fullHeight ? "100%" : height ? height : "auto",
|
||||||
width: fullWidth ? "100%" : "auto",
|
width: fullWidth ? "100%" : width ? width : "auto",
|
||||||
...(media?.filename?.toLowerCase().endsWith(".webp") && {
|
...(media?.filename?.toLowerCase().endsWith(".webp") && {
|
||||||
maxWidth: "300px",
|
maxWidth: "300px",
|
||||||
maxHeight: "300px",
|
maxHeight: "300px",
|
||||||
@ -59,8 +63,8 @@ export function MediaViewer({
|
|||||||
media?.id
|
media?.id
|
||||||
}/download?token=${token}`}
|
}/download?token=${token}`}
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: width ? width : "100%",
|
||||||
height: "100%",
|
height: height ? height : "100%",
|
||||||
objectFit: "cover",
|
objectFit: "cover",
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
}}
|
}}
|
||||||
@ -76,8 +80,8 @@ export function MediaViewer({
|
|||||||
}/download?token=${token}`}
|
}/download?token=${token}`}
|
||||||
alt={media?.filename}
|
alt={media?.filename}
|
||||||
style={{
|
style={{
|
||||||
height: fullHeight ? "100%" : "auto",
|
height: fullHeight ? "100%" : height ? height : "auto",
|
||||||
width: fullWidth ? "100%" : "auto",
|
width: fullWidth ? "100%" : width ? width : "auto",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -98,8 +102,8 @@ export function MediaViewer({
|
|||||||
src={`${import.meta.env.VITE_KRBL_MEDIA}${
|
src={`${import.meta.env.VITE_KRBL_MEDIA}${
|
||||||
media?.id
|
media?.id
|
||||||
}/download?token=${token}`}
|
}/download?token=${token}`}
|
||||||
width={"100%"}
|
width={width ? width : "500px"}
|
||||||
height={"100%"}
|
height={height ? height : "300px"}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -108,8 +112,8 @@ export function MediaViewer({
|
|||||||
fileUrl={`${import.meta.env.VITE_KRBL_MEDIA}${
|
fileUrl={`${import.meta.env.VITE_KRBL_MEDIA}${
|
||||||
media?.id
|
media?.id
|
||||||
}/download?token=${token}`}
|
}/download?token=${token}`}
|
||||||
height="500px"
|
height={height ? height : "500px"}
|
||||||
width="500px"
|
width={width ? width : "500px"}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import React from "react";
|
||||||
import { Stage, useGLTF } from "@react-three/drei";
|
import { Stage, useGLTF } from "@react-three/drei";
|
||||||
import { Canvas } from "@react-three/fiber";
|
import { Canvas } from "@react-three/fiber";
|
||||||
import { OrbitControls } from "@react-three/drei";
|
import { OrbitControls } from "@react-three/drei";
|
||||||
@ -5,12 +6,21 @@ import { OrbitControls } from "@react-three/drei";
|
|||||||
export const ModelViewer3D = ({
|
export const ModelViewer3D = ({
|
||||||
fileUrl,
|
fileUrl,
|
||||||
height = "100%",
|
height = "100%",
|
||||||
|
onLoad,
|
||||||
}: {
|
}: {
|
||||||
fileUrl: string;
|
fileUrl: string;
|
||||||
height: string;
|
height: string;
|
||||||
|
onLoad?: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { scene } = useGLTF(fileUrl);
|
const { scene } = useGLTF(fileUrl);
|
||||||
|
|
||||||
|
// Вызываем onLoad когда модель загружена
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (onLoad) {
|
||||||
|
onLoad();
|
||||||
|
}
|
||||||
|
}, [scene, onLoad]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Canvas style={{ width: "100%", height: height }}>
|
<Canvas style={{ width: "100%", height: height }}>
|
||||||
<ambientLight />
|
<ambientLight />
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
import { Button } from "@mui/material";
|
import { Button, CircularProgress } from "@mui/material";
|
||||||
|
|
||||||
export const SnapshotRestore = ({
|
export const SnapshotRestore = ({
|
||||||
onDelete,
|
onDelete,
|
||||||
onCancel,
|
onCancel,
|
||||||
open,
|
open,
|
||||||
|
loading = false,
|
||||||
}: {
|
}: {
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
loading?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -23,10 +25,22 @@ export const SnapshotRestore = ({
|
|||||||
Это действие нельзя будет отменить.
|
Это действие нельзя будет отменить.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-4 justify-center">
|
<div className="flex gap-4 justify-center">
|
||||||
<Button variant="contained" color="primary" onClick={onDelete}>
|
<Button
|
||||||
Да
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={onDelete}
|
||||||
|
disabled={loading}
|
||||||
|
startIcon={
|
||||||
|
loading ? (
|
||||||
|
<CircularProgress size={16} color="inherit" />
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{loading ? "Восстановление..." : "Да"}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={onCancel} disabled={loading}>
|
||||||
|
Нет
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={onCancel}>Нет</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user