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) {
|
||||
mediaStore.getOneMedia(id);
|
||||
}
|
||||
console.log(newFile);
|
||||
console.log(uploadDialogOpen);
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {}, [newFile, uploadDialogOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (media) {
|
||||
setMediaName(media.media_name);
|
||||
|
@ -140,9 +140,7 @@ export const LinkedItemsContents = <
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
console.log(error);
|
||||
}, [error]);
|
||||
useEffect(() => {}, [error]);
|
||||
|
||||
const parentResource = "route";
|
||||
const childResource = "station";
|
||||
@ -227,7 +225,7 @@ export const LinkedItemsContents = <
|
||||
if (type === "edit") {
|
||||
setError(null);
|
||||
authInstance
|
||||
.get(`/${childResource}/`)
|
||||
.get(`/${childResource}`)
|
||||
.then((response) => {
|
||||
setAllItems(response?.data || []);
|
||||
})
|
||||
|
@ -1,7 +1,7 @@
|
||||
export const UP_SCALE = 30000;
|
||||
export const PATH_WIDTH = 15;
|
||||
export const STATION_RADIUS = 20;
|
||||
export const STATION_OUTLINE_WIDTH = 10;
|
||||
export const UP_SCALE = 10000;
|
||||
export const PATH_WIDTH = 5;
|
||||
export const STATION_RADIUS = 8;
|
||||
export const STATION_OUTLINE_WIDTH = 4;
|
||||
export const SIGHT_SIZE = 40;
|
||||
export const SCALE_FACTOR = 50;
|
||||
|
||||
|
@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
|
||||
import { useTransform } from "./TransformContext";
|
||||
import { coordinatesToLocal, localToCoordinates } from "./utils";
|
||||
import { SCALE_FACTOR } from "./Constants";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export function RightSidebar() {
|
||||
const {
|
||||
@ -360,8 +361,14 @@ export function RightSidebar() {
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
sx={{ mt: 2 }}
|
||||
onClick={() => {
|
||||
saveChanges();
|
||||
onClick={async () => {
|
||||
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);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(
|
||||
`Rendering Sight ${id + 1} at [${sight.latitude}, ${sight.longitude}]`
|
||||
);
|
||||
}, [id, sight.latitude, sight.longitude]);
|
||||
useEffect(() => {}, [id, sight.latitude, sight.longitude]);
|
||||
|
||||
if (!sight) {
|
||||
console.error("sight is null");
|
||||
|
@ -57,8 +57,8 @@ export function Widgets() {
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}>
|
||||
<Landmark size={16} />
|
||||
<Box sx={{ display: "flex", gap: 0.5 }}>
|
||||
<Landmark size={16} className="shrink-0" />
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
sx={{ color: "#fff", fontWeight: "bold" }}
|
||||
|
@ -26,6 +26,7 @@ import { Sight } from "./Sight";
|
||||
import { SightData } from "./types";
|
||||
import { Station } from "./Station";
|
||||
import { UP_SCALE } from "./Constants";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
|
||||
extend({
|
||||
Container,
|
||||
@ -36,13 +37,27 @@ extend({
|
||||
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 = () => {
|
||||
const { routeData, stationData, sightData } = useMapData();
|
||||
return (
|
||||
<MapDataProvider>
|
||||
<TransformProvider>
|
||||
<Stack direction="row" height="100vh" width="100vw" overflow="hidden">
|
||||
<LanguageSwitcher />
|
||||
|
||||
{routeData && stationData && sightData ? <LanguageSwitcher /> : null}
|
||||
<Loading />
|
||||
<LeftSidebar />
|
||||
<Stack direction="row" flex={1} position="relative" height="100%">
|
||||
<RouteMap />
|
||||
@ -145,12 +160,12 @@ export const RouteMap = observer(() => {
|
||||
]);
|
||||
|
||||
if (!routeData || !stationData || !sightData) {
|
||||
console.error("routeData, stationData or sightData is null");
|
||||
return <div>Loading...</div>;
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ width: "100%", height: "100%" }} ref={parentRef}>
|
||||
<LanguageSwitcher />
|
||||
<Application resizeTo={parentRef} background="#fff">
|
||||
<InfiniteCanvas>
|
||||
<TravelPath points={points} />
|
||||
|
@ -132,7 +132,6 @@ export const SightListPage = observer(() => {
|
||||
loading={isLoading}
|
||||
localeText={ruRU.components.MuiDataGrid.defaultProps.localeText}
|
||||
onRowSelectionModelChange={(newSelection) => {
|
||||
console.log(newSelection);
|
||||
setIds(Array.from(newSelection.ids as unknown as number[]));
|
||||
}}
|
||||
slots={{
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Button, Paper, TextField } from "@mui/material";
|
||||
import { Button, TextField } from "@mui/material";
|
||||
import { snapshotStore } from "@shared";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ArrowLeft, Loader2, Save } from "lucide-react";
|
||||
@ -20,7 +20,8 @@ export const SnapshotCreatePage = observer(() => {
|
||||
}, [id]);
|
||||
|
||||
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">
|
||||
<button
|
||||
className="flex items-center gap-2"
|
||||
@ -51,11 +52,15 @@ export const SnapshotCreatePage = observer(() => {
|
||||
await createSnapshot(name);
|
||||
setIsLoading(false);
|
||||
toast.success("Снапшот успешно создан");
|
||||
navigate(-1);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error("Ошибка при создании снапшота");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}}
|
||||
disabled={isLoading}
|
||||
disabled={isLoading || !name.trim()}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 size={20} className="animate-spin" />
|
||||
@ -64,6 +69,7 @@ export const SnapshotCreatePage = observer(() => {
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</Paper>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -117,12 +117,15 @@ export const SnapshotListPage = observer(() => {
|
||||
|
||||
<SnapshotRestore
|
||||
open={isRestoreModalOpen}
|
||||
loading={isLoading}
|
||||
onDelete={async () => {
|
||||
setIsLoading(true);
|
||||
if (rowId) {
|
||||
await restoreSnapshot(rowId);
|
||||
}
|
||||
setIsRestoreModalOpen(false);
|
||||
setRowId(null);
|
||||
setIsLoading(false);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setIsRestoreModalOpen(false);
|
||||
|
@ -1,3 +1,2 @@
|
||||
export * from "./SnapshotListPage";
|
||||
|
||||
export * from "./SnapshotCreatePage";
|
||||
|
@ -93,9 +93,7 @@ export const LinkedSightsContents = <
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(error);
|
||||
}, [error]);
|
||||
useEffect(() => {}, [error]);
|
||||
|
||||
const parentResource = "station";
|
||||
const childResource = "sight";
|
||||
@ -178,7 +176,7 @@ export const LinkedSightsContents = <
|
||||
if (type === "edit") {
|
||||
setError(null);
|
||||
authInstance
|
||||
.get(`/${childResource}/`)
|
||||
.get(`/${childResource}`)
|
||||
.then((response) => {
|
||||
setAllItems(response?.data || []);
|
||||
})
|
||||
|
@ -81,6 +81,7 @@ export const UploadMediaDialog = observer(
|
||||
const [availableMediaTypes, setAvailableMediaTypes] = useState<number[]>(
|
||||
[]
|
||||
);
|
||||
const [isPreviewLoaded, setIsPreviewLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialFile) {
|
||||
@ -207,6 +208,7 @@ export const UploadMediaDialog = observer(
|
||||
useEffect(() => {
|
||||
if (mediaFile) {
|
||||
setMediaUrl(URL.createObjectURL(mediaFile as Blob));
|
||||
setIsPreviewLoaded(false); // Сбрасываем состояние загрузки при смене файла
|
||||
}
|
||||
}, [mediaFile]);
|
||||
|
||||
@ -326,8 +328,22 @@ export const UploadMediaDialog = observer(
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
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 && (
|
||||
<video
|
||||
src={mediaUrl}
|
||||
@ -336,10 +352,16 @@ export const UploadMediaDialog = observer(
|
||||
loop
|
||||
controls
|
||||
style={{ maxWidth: "100%", maxHeight: "100%" }}
|
||||
onLoadedData={() => setIsPreviewLoaded(true)}
|
||||
onError={() => setIsPreviewLoaded(true)}
|
||||
/>
|
||||
)}
|
||||
{mediaType === 6 && mediaUrl && (
|
||||
<ModelViewer3D fileUrl={mediaUrl} height="100%" />
|
||||
<ModelViewer3D
|
||||
fileUrl={mediaUrl}
|
||||
height="100%"
|
||||
onLoad={() => setIsPreviewLoaded(true)}
|
||||
/>
|
||||
)}
|
||||
{mediaType !== 6 && mediaType !== 2 && mediaUrl && (
|
||||
<img
|
||||
@ -349,6 +371,8 @@ export const UploadMediaDialog = observer(
|
||||
height: "100%",
|
||||
objectFit: "contain",
|
||||
}}
|
||||
onLoad={() => setIsPreviewLoaded(true)}
|
||||
onError={() => setIsPreviewLoaded(true)}
|
||||
/>
|
||||
)}
|
||||
</Paper>
|
||||
@ -370,9 +394,17 @@ export const UploadMediaDialog = observer(
|
||||
)
|
||||
}
|
||||
onClick={handleSave}
|
||||
disabled={isLoading || (!mediaName && !mediaFilename)}
|
||||
disabled={
|
||||
isLoading ||
|
||||
(!mediaName && !mediaFilename) ||
|
||||
!isPreviewLoaded
|
||||
}
|
||||
>
|
||||
{isLoading ? "Сохранение..." : "Сохранить"}
|
||||
{isLoading
|
||||
? "Сохранение..."
|
||||
: !isPreviewLoaded
|
||||
? "Загрузка превью..."
|
||||
: "Сохранить"}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
@ -497,9 +497,7 @@ class EditSightStore {
|
||||
media_name: media_name,
|
||||
media_type: type,
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
createLinkWithArticle = async (media: {
|
||||
|
@ -82,11 +82,6 @@ class RouteStore {
|
||||
};
|
||||
|
||||
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) =>
|
||||
station.id === stationId ? { ...station, ...data } : station
|
||||
);
|
||||
|
@ -97,15 +97,19 @@ class SightsStore {
|
||||
city: number,
|
||||
coordinates: { latitude: number; longitude: number }
|
||||
) => {
|
||||
const id = (
|
||||
await authInstance.post("/sight", {
|
||||
const response = await authInstance.post("/sight", {
|
||||
name: this.createSight[languageStore.language].name,
|
||||
address: this.createSight[languageStore.language].address,
|
||||
city_id: city,
|
||||
latitude: coordinates.latitude,
|
||||
longitude: coordinates.longitude,
|
||||
})
|
||||
).data.id;
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
this.sights.push(response.data);
|
||||
});
|
||||
|
||||
const id = response.data.id;
|
||||
|
||||
const anotherLanguages = ["ru", "en", "zh"].filter(
|
||||
(language) => language !== languageStore.language
|
||||
|
@ -1,6 +1,24 @@
|
||||
import { authInstance } from "@shared";
|
||||
|
||||
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 = {
|
||||
ID: string;
|
||||
@ -17,6 +35,248 @@ class SnapshotStore {
|
||||
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 () => {
|
||||
const response = await authInstance.get(`/snapshots`);
|
||||
|
||||
@ -42,6 +302,10 @@ class SnapshotStore {
|
||||
};
|
||||
|
||||
restoreSnapshot = async (id: string) => {
|
||||
// Сначала сбрасываем все кеши
|
||||
this.clearAllCaches();
|
||||
|
||||
// Затем восстанавливаем снапшот
|
||||
await authInstance.post(`/snapshots/${id}/restore`);
|
||||
};
|
||||
|
||||
|
@ -202,7 +202,6 @@ export const DevicesTable = observer(() => {
|
||||
try {
|
||||
// Create an array of promises for all snapshot requests
|
||||
const snapshotPromises = selectedDeviceUuids.map((deviceUuid) => {
|
||||
console.log(`Sending snapshot ${snapshotId} to device ${deviceUuid}`);
|
||||
return send(deviceUuid);
|
||||
});
|
||||
|
||||
|
@ -51,7 +51,7 @@ export const LanguageSwitcher = observer(() => {
|
||||
key={lang}
|
||||
onClick={() => handleLanguageChange(lang)}
|
||||
variant={"contained"} // Highlight the active language
|
||||
color={language === lang ? "primary" : "secondary"}
|
||||
color={language === lang ? "primary" : "inherit"}
|
||||
sx={{ minWidth: "60px" }} // Give buttons a consistent width
|
||||
>
|
||||
{getLanguageLabel(lang)}
|
||||
|
@ -109,6 +109,7 @@ export const MediaArea = observer(
|
||||
media_type: m.media_type,
|
||||
filename: m.filename,
|
||||
}}
|
||||
height="40px"
|
||||
/>
|
||||
<button
|
||||
className="absolute top-2 right-2"
|
||||
|
@ -12,11 +12,15 @@ export interface MediaData {
|
||||
export function MediaViewer({
|
||||
media,
|
||||
className,
|
||||
height,
|
||||
width,
|
||||
fullWidth,
|
||||
fullHeight,
|
||||
}: Readonly<{
|
||||
media?: MediaData;
|
||||
className?: string;
|
||||
height?: string;
|
||||
width?: string;
|
||||
fullWidth?: boolean;
|
||||
fullHeight?: boolean;
|
||||
}>) {
|
||||
@ -42,8 +46,8 @@ export function MediaViewer({
|
||||
}/download?token=${token}`}
|
||||
alt={media?.filename}
|
||||
style={{
|
||||
height: fullHeight ? "100%" : "auto",
|
||||
width: fullWidth ? "100%" : "auto",
|
||||
height: fullHeight ? "100%" : height ? height : "auto",
|
||||
width: fullWidth ? "100%" : width ? width : "auto",
|
||||
...(media?.filename?.toLowerCase().endsWith(".webp") && {
|
||||
maxWidth: "300px",
|
||||
maxHeight: "300px",
|
||||
@ -59,8 +63,8 @@ export function MediaViewer({
|
||||
media?.id
|
||||
}/download?token=${token}`}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
width: width ? width : "100%",
|
||||
height: height ? height : "100%",
|
||||
objectFit: "cover",
|
||||
borderRadius: 8,
|
||||
}}
|
||||
@ -76,8 +80,8 @@ export function MediaViewer({
|
||||
}/download?token=${token}`}
|
||||
alt={media?.filename}
|
||||
style={{
|
||||
height: fullHeight ? "100%" : "auto",
|
||||
width: fullWidth ? "100%" : "auto",
|
||||
height: fullHeight ? "100%" : height ? height : "auto",
|
||||
width: fullWidth ? "100%" : width ? width : "auto",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@ -98,8 +102,8 @@ export function MediaViewer({
|
||||
src={`${import.meta.env.VITE_KRBL_MEDIA}${
|
||||
media?.id
|
||||
}/download?token=${token}`}
|
||||
width={"100%"}
|
||||
height={"100%"}
|
||||
width={width ? width : "500px"}
|
||||
height={height ? height : "300px"}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -108,8 +112,8 @@ export function MediaViewer({
|
||||
fileUrl={`${import.meta.env.VITE_KRBL_MEDIA}${
|
||||
media?.id
|
||||
}/download?token=${token}`}
|
||||
height="500px"
|
||||
width="500px"
|
||||
height={height ? height : "500px"}
|
||||
width={width ? width : "500px"}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
@ -1,3 +1,4 @@
|
||||
import React from "react";
|
||||
import { Stage, useGLTF } from "@react-three/drei";
|
||||
import { Canvas } from "@react-three/fiber";
|
||||
import { OrbitControls } from "@react-three/drei";
|
||||
@ -5,12 +6,21 @@ import { OrbitControls } from "@react-three/drei";
|
||||
export const ModelViewer3D = ({
|
||||
fileUrl,
|
||||
height = "100%",
|
||||
onLoad,
|
||||
}: {
|
||||
fileUrl: string;
|
||||
height: string;
|
||||
onLoad?: () => void;
|
||||
}) => {
|
||||
const { scene } = useGLTF(fileUrl);
|
||||
|
||||
// Вызываем onLoad когда модель загружена
|
||||
React.useEffect(() => {
|
||||
if (onLoad) {
|
||||
onLoad();
|
||||
}
|
||||
}, [scene, onLoad]);
|
||||
|
||||
return (
|
||||
<Canvas style={{ width: "100%", height: height }}>
|
||||
<ambientLight />
|
||||
|
@ -1,13 +1,15 @@
|
||||
import { Button } from "@mui/material";
|
||||
import { Button, CircularProgress } from "@mui/material";
|
||||
|
||||
export const SnapshotRestore = ({
|
||||
onDelete,
|
||||
onCancel,
|
||||
open,
|
||||
loading = false,
|
||||
}: {
|
||||
onDelete: () => void;
|
||||
onCancel: () => void;
|
||||
open: boolean;
|
||||
loading?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
@ -23,10 +25,22 @@ export const SnapshotRestore = ({
|
||||
Это действие нельзя будет отменить.
|
||||
</p>
|
||||
<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 onClick={onCancel}>Нет</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user