Compare commits
No commits in common. "078f051e8a52f19042290e7bae796136585cc166" and "5814e65953f42b02db1af85afb72a3367ae6f2c9" have entirely different histories.
078f051e8a
...
5814e65953
@ -9,26 +9,18 @@ import { useNavigate } from "react-router-dom";
|
|||||||
interface NavigationItemProps {
|
interface NavigationItemProps {
|
||||||
item: NavigationItem;
|
item: NavigationItem;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClick?: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NavigationItemComponent: React.FC<NavigationItemProps> = ({
|
export const NavigationItemComponent: React.FC<NavigationItemProps> = ({
|
||||||
item,
|
item,
|
||||||
open,
|
open,
|
||||||
onClick,
|
|
||||||
}) => {
|
}) => {
|
||||||
const Icon = item.icon;
|
const Icon = item.icon;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem
|
<ListItem
|
||||||
onClick={() => {
|
onClick={() => navigate(item.path)}
|
||||||
if (onClick) {
|
|
||||||
onClick();
|
|
||||||
} else {
|
|
||||||
navigate(item.path);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disablePadding
|
disablePadding
|
||||||
sx={{ display: "block" }}
|
sx={{ display: "block" }}
|
||||||
>
|
>
|
||||||
|
@ -25,7 +25,6 @@ export const NavigationList = ({ open }: { open: boolean }) => {
|
|||||||
key={item.id}
|
key={item.id}
|
||||||
item={item as NavigationItem}
|
item={item as NavigationItem}
|
||||||
open={open}
|
open={open}
|
||||||
onClick={item.onClick ? item.onClick : undefined}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</List>
|
</List>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { TableBody } from "@mui/material";
|
import { Button, TableBody } from "@mui/material";
|
||||||
import { TableRow, TableCell } from "@mui/material";
|
import { TableRow, TableCell } from "@mui/material";
|
||||||
import { Table, TableHead } from "@mui/material";
|
import { Table, TableHead } from "@mui/material";
|
||||||
import { mediaStore, MEDIA_TYPE_LABELS } from "@shared";
|
import { mediaStore, MEDIA_TYPE_LABELS } from "@shared";
|
||||||
@ -27,6 +27,15 @@ export const MediaListPage = observer(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<div className="flex justify-end p-3 gap-5">
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => navigate("/sight/create")}
|
||||||
|
>
|
||||||
|
Создать
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<Table sx={{ minWidth: 650 }} aria-label="simple table">
|
<Table sx={{ minWidth: 650 }} aria-label="simple table">
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
@ -6,6 +6,7 @@ const authInstance = axios.create({
|
|||||||
});
|
});
|
||||||
|
|
||||||
authInstance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
authInstance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
||||||
|
console.log(config);
|
||||||
config.headers.Authorization = `Bearer ${localStorage.getItem("token")}`;
|
config.headers.Authorization = `Bearer ${localStorage.getItem("token")}`;
|
||||||
config.headers["X-Language"] = languageStore.language ?? "ru";
|
config.headers["X-Language"] = languageStore.language ?? "ru";
|
||||||
return config;
|
return config;
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
MonitorSmartphone,
|
MonitorSmartphone,
|
||||||
Map,
|
Map,
|
||||||
BookImage,
|
BookImage,
|
||||||
|
Newspaper,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
export const DRAWER_WIDTH = 300;
|
export const DRAWER_WIDTH = 300;
|
||||||
|
|
||||||
@ -46,12 +47,12 @@ export const NAVIGATION_ITEMS: {
|
|||||||
icon: BookImage,
|
icon: BookImage,
|
||||||
path: "/media",
|
path: "/media",
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// id: "articles",
|
id: "articles",
|
||||||
// label: "Статьи",
|
label: "Статьи",
|
||||||
// icon: Newspaper,
|
icon: Newspaper,
|
||||||
// path: "/articles",
|
path: "/articles",
|
||||||
// },
|
},
|
||||||
],
|
],
|
||||||
secondary: [
|
secondary: [
|
||||||
{
|
{
|
||||||
@ -60,7 +61,6 @@ export const NAVIGATION_ITEMS: {
|
|||||||
icon: Power,
|
icon: Power,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
authStore.logout();
|
authStore.logout();
|
||||||
window.location.href = "/login";
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -21,13 +21,12 @@ import { MediaViewer } from "@widgets";
|
|||||||
interface SelectMediaDialogProps {
|
interface SelectMediaDialogProps {
|
||||||
open: boolean; // Corrected prop name
|
open: boolean; // Corrected prop name
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSelectMedia?: (media: {
|
onSelectMedia: (media: {
|
||||||
id: string;
|
id: string;
|
||||||
filename: string;
|
filename: string;
|
||||||
media_name?: string;
|
media_name?: string;
|
||||||
media_type: number;
|
media_type: number;
|
||||||
}) => void; // Renamed from onSelectArticle
|
}) => void; // Renamed from onSelectArticle
|
||||||
onSelectForSightMedia?: (mediaId: string) => void;
|
|
||||||
linkedMediaIds?: string[]; // Renamed from linkedArticleIds, assuming it refers to media already in use
|
linkedMediaIds?: string[]; // Renamed from linkedArticleIds, assuming it refers to media already in use
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,7 +35,6 @@ export const SelectMediaDialog = observer(
|
|||||||
open, // Corrected prop name
|
open, // Corrected prop name
|
||||||
onClose,
|
onClose,
|
||||||
onSelectMedia, // Renamed prop
|
onSelectMedia, // Renamed prop
|
||||||
onSelectForSightMedia,
|
|
||||||
linkedMediaIds = [], // Default to empty array if not provided, renamed
|
linkedMediaIds = [], // Default to empty array if not provided, renamed
|
||||||
}: SelectMediaDialogProps) => {
|
}: SelectMediaDialogProps) => {
|
||||||
const { media, getMedia } = mediaStore;
|
const { media, getMedia } = mediaStore;
|
||||||
@ -57,11 +55,7 @@ export const SelectMediaDialog = observer(
|
|||||||
if (hoveredMediaId) {
|
if (hoveredMediaId) {
|
||||||
const mediaItem = media.find((m) => m.id === hoveredMediaId);
|
const mediaItem = media.find((m) => m.id === hoveredMediaId);
|
||||||
if (mediaItem) {
|
if (mediaItem) {
|
||||||
if (onSelectForSightMedia) {
|
onSelectMedia(mediaItem);
|
||||||
onSelectForSightMedia(mediaItem.id);
|
|
||||||
} else if (onSelectMedia) {
|
|
||||||
onSelectMedia(mediaItem);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
@ -120,11 +114,7 @@ export const SelectMediaDialog = observer(
|
|||||||
key={mediaItem.id}
|
key={mediaItem.id}
|
||||||
onClick={() => setHoveredMediaId(mediaItem.id)} // Call onSelectMedia
|
onClick={() => setHoveredMediaId(mediaItem.id)} // Call onSelectMedia
|
||||||
onDoubleClick={() => {
|
onDoubleClick={() => {
|
||||||
if (onSelectForSightMedia) {
|
onSelectMedia(mediaItem);
|
||||||
onSelectForSightMedia(mediaItem.id);
|
|
||||||
} else if (onSelectMedia) {
|
|
||||||
onSelectMedia(mediaItem);
|
|
||||||
}
|
|
||||||
onClose();
|
onClose();
|
||||||
}}
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
|
@ -24,22 +24,16 @@ import { ModelViewer3D } from "@widgets";
|
|||||||
interface UploadMediaDialogProps {
|
interface UploadMediaDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
afterUpload?: (media: {
|
afterUpload: (media: {
|
||||||
id: string;
|
id: string;
|
||||||
filename: string;
|
filename: string;
|
||||||
media_name?: string;
|
media_name?: string;
|
||||||
media_type: number;
|
media_type: number;
|
||||||
}) => void;
|
}) => void;
|
||||||
afterUploadSight?: (id: string) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UploadMediaDialog = observer(
|
export const UploadMediaDialog = observer(
|
||||||
({
|
({ open, onClose, afterUpload }: UploadMediaDialogProps) => {
|
||||||
open,
|
|
||||||
onClose,
|
|
||||||
afterUpload,
|
|
||||||
afterUploadSight,
|
|
||||||
}: 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);
|
||||||
const [success, setSuccess] = useState(false);
|
const [success, setSuccess] = useState(false);
|
||||||
@ -109,11 +103,7 @@ export const UploadMediaDialog = observer(
|
|||||||
mediaName
|
mediaName
|
||||||
);
|
);
|
||||||
if (media) {
|
if (media) {
|
||||||
if (afterUploadSight) {
|
await afterUpload(media);
|
||||||
await afterUploadSight(media.id);
|
|
||||||
} else if (afterUpload) {
|
|
||||||
await afterUpload(media);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
setSuccess(true);
|
setSuccess(true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
// @shared/stores/createSightStore.ts
|
// @shared/stores/createSightStore.ts
|
||||||
import { Language, authInstance, languageInstance, mediaStore } from "@shared";
|
import {
|
||||||
import { makeAutoObservable, runInAction } from "mobx";
|
Language,
|
||||||
|
authInstance,
|
||||||
type MediaItem = {
|
languageInstance,
|
||||||
id: string;
|
articlesStore,
|
||||||
filename: string;
|
languageStore,
|
||||||
media_name?: string;
|
mediaStore,
|
||||||
media_type: number;
|
} from "@shared";
|
||||||
};
|
import { makeAutoObservable } from "mobx";
|
||||||
|
|
||||||
type SightLanguageInfo = {
|
type SightLanguageInfo = {
|
||||||
name: string;
|
name: string;
|
||||||
@ -15,13 +15,18 @@ type SightLanguageInfo = {
|
|||||||
left: {
|
left: {
|
||||||
heading: string;
|
heading: string;
|
||||||
body: string;
|
body: string;
|
||||||
media: MediaItem[];
|
media: {
|
||||||
|
id: string;
|
||||||
|
filename: string;
|
||||||
|
media_name?: string;
|
||||||
|
media_type: number;
|
||||||
|
}[];
|
||||||
};
|
};
|
||||||
right: { id: number; heading: string; body: string; media: MediaItem[] }[];
|
right: { heading: string; body: string }[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type SightCommonInfo = {
|
type SightCommonInfo = {
|
||||||
// id: number; // ID is 0 until created
|
id: number;
|
||||||
city_id: number;
|
city_id: number;
|
||||||
city: string;
|
city: string;
|
||||||
latitude: number;
|
latitude: number;
|
||||||
@ -29,50 +34,48 @@ 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;
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialSightState: SightBaseInfo = {
|
|
||||||
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: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
class CreateSightStore {
|
class CreateSightStore {
|
||||||
sight: SightBaseInfo = JSON.parse(JSON.stringify(initialSightState)); // Deep copy for reset
|
sight: SightBaseInfo = {
|
||||||
|
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: {
|
||||||
|
name: "",
|
||||||
|
address: "",
|
||||||
|
left: { heading: "", body: "", media: [] },
|
||||||
|
right: [],
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
name: "",
|
||||||
|
address: "",
|
||||||
|
left: { heading: "", body: "", media: [] },
|
||||||
|
right: [],
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
name: "",
|
||||||
|
address: "",
|
||||||
|
left: { heading: "", body: "", media: [] },
|
||||||
|
right: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
uploadMediaOpen = false;
|
uploadMediaOpen = false;
|
||||||
setUploadMediaOpen = (open: boolean) => {
|
setUploadMediaOpen = (open: boolean) => {
|
||||||
@ -87,504 +90,359 @@ class CreateSightStore {
|
|||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Right Article Management ---
|
createNewRightArticle = () => {
|
||||||
createNewRightArticle = async () => {
|
this.sight.ru.right.push({
|
||||||
// Create article in DB for all languages
|
heading: "Введите русский заголовок",
|
||||||
const articleRuData = {
|
body: "Введите русский текст",
|
||||||
heading: "Новый заголовок (RU)",
|
});
|
||||||
body: "Новый текст (RU)",
|
this.sight.en.right.push({
|
||||||
|
heading: "Enter the English heading",
|
||||||
|
body: "Enter the English text",
|
||||||
|
});
|
||||||
|
this.sight.zh.right.push({
|
||||||
|
heading: "Введите китайский заголовок",
|
||||||
|
body: "Введите китайский текст",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
updateLeftInfo = (language: Language, heading: string, body: string) => {
|
||||||
|
this.sight[language].left.heading = heading;
|
||||||
|
this.sight[language].left.body = body;
|
||||||
|
};
|
||||||
|
|
||||||
|
clearCreateSight = () => {
|
||||||
|
this.sight = {
|
||||||
|
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: {
|
||||||
|
name: "",
|
||||||
|
address: "",
|
||||||
|
left: { heading: "", body: "", media: [] },
|
||||||
|
right: [],
|
||||||
|
},
|
||||||
|
|
||||||
|
en: {
|
||||||
|
name: "",
|
||||||
|
address: "",
|
||||||
|
left: { heading: "", body: "", media: [] },
|
||||||
|
right: [],
|
||||||
|
},
|
||||||
|
|
||||||
|
zh: {
|
||||||
|
name: "",
|
||||||
|
address: "",
|
||||||
|
left: { heading: "", body: "", media: [] },
|
||||||
|
right: [],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
const articleEnData = {
|
};
|
||||||
heading: "New Heading (EN)",
|
|
||||||
body: "New Text (EN)",
|
|
||||||
};
|
|
||||||
const articleZhData = { heading: "新标题 (ZH)", body: "新文本 (ZH)" };
|
|
||||||
|
|
||||||
try {
|
updateSightInfo = (
|
||||||
const articleRes = await languageInstance("ru").post(
|
content: Partial<SightLanguageInfo | SightCommonInfo>,
|
||||||
"/article",
|
language?: Language
|
||||||
articleRuData
|
) => {
|
||||||
);
|
if (language) {
|
||||||
const { id } = articleRes.data; // New article's ID
|
this.sight[language] = {
|
||||||
|
...this.sight[language],
|
||||||
await languageInstance("en").patch(`/article/${id}`, articleEnData);
|
...content,
|
||||||
await languageInstance("zh").patch(`/article/${id}`, articleZhData);
|
};
|
||||||
|
} else {
|
||||||
runInAction(() => {
|
this.sight = {
|
||||||
const newArticleEntry = { id, media: [] };
|
...this.sight,
|
||||||
this.sight.ru.right.push({ ...newArticleEntry, ...articleRuData });
|
...content,
|
||||||
this.sight.en.right.push({ ...newArticleEntry, ...articleEnData });
|
};
|
||||||
this.sight.zh.right.push({ ...newArticleEntry, ...articleZhData });
|
|
||||||
});
|
|
||||||
return id; // Return ID for potential immediate use
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error creating new right article:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
linkExistingRightArticle = async (articleId: number) => {
|
unlinkLeftArticle = () => {
|
||||||
try {
|
this.sight.left_article = 0;
|
||||||
const ruData = await languageInstance("ru").get(`/article/${articleId}`);
|
this.sight.ru.left.heading = "";
|
||||||
const enData = await languageInstance("en").get(`/article/${articleId}`);
|
this.sight.en.left.heading = "";
|
||||||
const zhData = await languageInstance("zh").get(`/article/${articleId}`);
|
this.sight.zh.left.heading = "";
|
||||||
const mediaRes = await authInstance.get(`/article/${articleId}/media`);
|
this.sight.ru.left.body = "";
|
||||||
const mediaData: MediaItem[] = mediaRes.data || [];
|
this.sight.en.left.body = "";
|
||||||
|
this.sight.zh.left.body = "";
|
||||||
|
};
|
||||||
|
|
||||||
runInAction(() => {
|
updateLeftArticle = async (articleId: number) => {
|
||||||
this.sight.ru.right.push({
|
this.sight.left_article = articleId;
|
||||||
id: articleId,
|
|
||||||
heading: ruData.data.heading,
|
if (articleId) {
|
||||||
body: ruData.data.body,
|
const ruArticleData = await languageInstance("ru").get(
|
||||||
media: mediaData,
|
`/article/${articleId}`
|
||||||
});
|
);
|
||||||
this.sight.en.right.push({
|
const enArticleData = await languageInstance("en").get(
|
||||||
id: articleId,
|
`/article/${articleId}`
|
||||||
heading: enData.data.heading,
|
);
|
||||||
body: enData.data.body,
|
const zhArticleData = await languageInstance("zh").get(
|
||||||
media: mediaData,
|
`/article/${articleId}`
|
||||||
});
|
);
|
||||||
this.sight.zh.right.push({
|
|
||||||
id: articleId,
|
this.sight.ru.left.heading = ruArticleData.data.heading;
|
||||||
heading: zhData.data.heading,
|
this.sight.en.left.heading = enArticleData.data.heading;
|
||||||
body: zhData.data.body,
|
this.sight.zh.left.heading = zhArticleData.data.heading;
|
||||||
media: mediaData,
|
|
||||||
});
|
this.sight.ru.left.body = ruArticleData.data.body;
|
||||||
});
|
this.sight.en.left.body = enArticleData.data.body;
|
||||||
} catch (error) {
|
this.sight.zh.left.body = zhArticleData.data.body;
|
||||||
console.error("Error linking existing right article:", error);
|
} else {
|
||||||
throw error;
|
this.sight.left_article = 0;
|
||||||
|
this.sight.ru.left.heading = "";
|
||||||
|
this.sight.en.left.heading = "";
|
||||||
|
this.sight.zh.left.heading = "";
|
||||||
|
this.sight.ru.left.body = "";
|
||||||
|
this.sight.en.left.body = "";
|
||||||
|
this.sight.zh.left.body = "";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
deleteLeftArticle = async (articleId: number) => {
|
||||||
|
await authInstance.delete(`/article/${articleId}`);
|
||||||
|
articlesStore.getArticles(languageStore.language);
|
||||||
|
this.sight.left_article = 0;
|
||||||
|
this.sight.ru.left.heading = "";
|
||||||
|
this.sight.en.left.heading = "";
|
||||||
|
this.sight.zh.left.heading = "";
|
||||||
|
this.sight.ru.left.body = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
createLeftArticle = async () => {
|
||||||
|
const response = await languageInstance("ru").post("/article", {
|
||||||
|
heading: "Новая статья",
|
||||||
|
body: "Заполните статью контентом",
|
||||||
|
});
|
||||||
|
|
||||||
|
this.sight.left_article = response.data.id;
|
||||||
|
|
||||||
|
this.sight.ru.left.heading = "Новая статья ";
|
||||||
|
this.sight.en.left.heading = "";
|
||||||
|
this.sight.zh.left.heading = "";
|
||||||
|
this.sight.ru.left.body = "Заполните статью контентом";
|
||||||
|
this.sight.en.left.body = "";
|
||||||
|
this.sight.zh.left.body = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
createSight = async (language: Language) => {
|
||||||
|
const rightArticles: number[] = [];
|
||||||
|
|
||||||
|
if (this.sight.left_article !== 0) {
|
||||||
|
if (this.sight.left_article == 10000000) {
|
||||||
|
const response = await languageInstance("ru").post("/article", {
|
||||||
|
heading: this.sight.ru.left.heading,
|
||||||
|
body: this.sight.ru.left.body,
|
||||||
|
});
|
||||||
|
const { id } = response.data;
|
||||||
|
await languageInstance("en").patch(`/article/${id}`, {
|
||||||
|
heading: this.sight.en.left.heading,
|
||||||
|
body: this.sight.en.left.body,
|
||||||
|
});
|
||||||
|
|
||||||
|
await languageInstance("zh").patch(`/article/${id}`, {
|
||||||
|
heading: this.sight.zh.left.heading,
|
||||||
|
body: this.sight.zh.left.body,
|
||||||
|
});
|
||||||
|
this.sight.left_article = id;
|
||||||
|
} else {
|
||||||
|
await languageInstance("ru").patch(
|
||||||
|
`/article/${this.sight.left_article}`,
|
||||||
|
{
|
||||||
|
heading: this.sight.ru.left.heading,
|
||||||
|
body: this.sight.ru.left.body,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await languageInstance("en").patch(
|
||||||
|
`/article/${this.sight.left_article}`,
|
||||||
|
{
|
||||||
|
heading: this.sight.en.left.heading,
|
||||||
|
body: this.sight.en.left.body,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await languageInstance("zh").patch(
|
||||||
|
`/article/${this.sight.left_article}`,
|
||||||
|
{
|
||||||
|
heading: this.sight.zh.left.heading,
|
||||||
|
body: this.sight.zh.left.body,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sight[language].right.map(async (article, index) => {
|
||||||
|
try {
|
||||||
|
const response = await languageInstance(language).post("/article", {
|
||||||
|
heading: article.heading,
|
||||||
|
body: article.body,
|
||||||
|
});
|
||||||
|
const { id } = response.data;
|
||||||
|
const anotherLanguages = ["en", "zh", "ru"].filter(
|
||||||
|
(lang) => lang !== language
|
||||||
|
);
|
||||||
|
await languageInstance(anotherLanguages[0] as Language).patch(
|
||||||
|
`/article/${id}`,
|
||||||
|
{
|
||||||
|
heading:
|
||||||
|
this.sight[anotherLanguages[0] as Language].right[index].heading,
|
||||||
|
body: this.sight[anotherLanguages[0] as Language].right[index].body,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await languageInstance(anotherLanguages[1] as Language).patch(
|
||||||
|
`/article/${id}`,
|
||||||
|
{
|
||||||
|
heading:
|
||||||
|
this.sight[anotherLanguages[1] as Language].right[index].heading,
|
||||||
|
body: this.sight[anotherLanguages[1] as Language].right[index].body,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
rightArticles.push(id);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const response = await languageInstance(language).post("/sight", {
|
||||||
|
city_id: this.sight.city_id,
|
||||||
|
city: this.sight.city,
|
||||||
|
latitude: this.sight.latitude,
|
||||||
|
longitude: this.sight.longitude,
|
||||||
|
name: this.sight[language].name,
|
||||||
|
address: this.sight[language].address,
|
||||||
|
thumbnail: this.sight.thumbnail ?? null,
|
||||||
|
watermark_lu: this.sight.watermark_lu,
|
||||||
|
watermark_rd: this.sight.watermark_rd,
|
||||||
|
left_article: this.sight.left_article,
|
||||||
|
preview_media: this.sight.preview_media,
|
||||||
|
video_preview: this.sight.video_preview,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { id } = response.data;
|
||||||
|
const anotherLanguages = ["en", "zh", "ru"].filter(
|
||||||
|
(lang) => lang !== language
|
||||||
|
);
|
||||||
|
|
||||||
|
await languageInstance(anotherLanguages[0] as Language).patch(
|
||||||
|
`/sight/${id}`,
|
||||||
|
{
|
||||||
|
city_id: this.sight.city_id,
|
||||||
|
city: this.sight.city,
|
||||||
|
latitude: this.sight.latitude,
|
||||||
|
longitude: this.sight.longitude,
|
||||||
|
name: this.sight[anotherLanguages[0] as Language as Language].name,
|
||||||
|
address:
|
||||||
|
this.sight[anotherLanguages[0] as Language as Language].address,
|
||||||
|
thumbnail: this.sight.thumbnail ?? null,
|
||||||
|
watermark_lu: this.sight.watermark_lu,
|
||||||
|
watermark_rd: this.sight.watermark_rd,
|
||||||
|
left_article: this.sight.left_article,
|
||||||
|
preview_media: this.sight.preview_media,
|
||||||
|
video_preview: this.sight.video_preview,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await languageInstance(anotherLanguages[1] as Language).patch(
|
||||||
|
`/sight/${id}`,
|
||||||
|
{
|
||||||
|
city_id: this.sight.city_id,
|
||||||
|
city: this.sight.city,
|
||||||
|
latitude: this.sight.latitude,
|
||||||
|
longitude: this.sight.longitude,
|
||||||
|
name: this.sight[anotherLanguages[1] as Language].name,
|
||||||
|
address: this.sight[anotherLanguages[1] as Language].address,
|
||||||
|
thumbnail: this.sight.thumbnail ?? null,
|
||||||
|
watermark_lu: this.sight.watermark_lu,
|
||||||
|
watermark_rd: this.sight.watermark_rd,
|
||||||
|
left_article: this.sight.left_article,
|
||||||
|
preview_media: this.sight.preview_media,
|
||||||
|
video_preview: this.sight.video_preview,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
rightArticles.map(async (article, index) => {
|
||||||
|
await authInstance.post(`/sight/${id}/article`, {
|
||||||
|
article_id: article,
|
||||||
|
page_num: index + 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
console.log("created");
|
||||||
|
};
|
||||||
|
|
||||||
updateRightArticleInfo = (
|
updateRightArticleInfo = (
|
||||||
index: number,
|
index: number,
|
||||||
language: Language,
|
language: Language,
|
||||||
heading: string,
|
heading: string,
|
||||||
body: string
|
body: string
|
||||||
) => {
|
) => {
|
||||||
if (this.sight[language].right[index]) {
|
this.sight[language].right[index].heading = heading;
|
||||||
this.sight[language].right[index].heading = heading;
|
this.sight[language].right[index].body = body;
|
||||||
this.sight[language].right[index].body = body;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// "Unlink" in create mode means just removing from the list to be created with the sight
|
|
||||||
unlinkRightAritcle = (articleId: number) => {
|
|
||||||
// Changed from 'unlinkRightAritcle' spelling
|
|
||||||
runInAction(() => {
|
|
||||||
this.sight.ru.right = this.sight.ru.right.filter(
|
|
||||||
(article) => article.id !== articleId
|
|
||||||
);
|
|
||||||
this.sight.en.right = this.sight.en.right.filter(
|
|
||||||
(article) => article.id !== articleId
|
|
||||||
);
|
|
||||||
this.sight.zh.right = this.sight.zh.right.filter(
|
|
||||||
(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) => {
|
|
||||||
try {
|
|
||||||
await authInstance.delete(`/article/${articleId}`); // Delete from backend
|
|
||||||
runInAction(() => {
|
|
||||||
// Remove from local store for all languages
|
|
||||||
this.sight.ru.right = this.sight.ru.right.filter(
|
|
||||||
(article) => article.id !== articleId
|
|
||||||
);
|
|
||||||
this.sight.en.right = this.sight.en.right.filter(
|
|
||||||
(article) => article.id !== articleId
|
|
||||||
);
|
|
||||||
this.sight.zh.right = this.sight.zh.right.filter(
|
|
||||||
(article) => article.id !== articleId
|
|
||||||
);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error deleting right article:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Right Article Media Management ---
|
|
||||||
createLinkWithRightArticle = async (media: MediaItem, articleId: number) => {
|
|
||||||
try {
|
|
||||||
await authInstance.post(`/article/${articleId}/media`, {
|
|
||||||
media_id: media.id,
|
|
||||||
media_order: 1, // Or calculate based on existing media.length + 1
|
|
||||||
});
|
|
||||||
runInAction(() => {
|
|
||||||
(["ru", "en", "zh"] as Language[]).forEach((lang) => {
|
|
||||||
const article = this.sight[lang].right.find(
|
|
||||||
(a) => a.id === articleId
|
|
||||||
);
|
|
||||||
if (article) {
|
|
||||||
if (!article.media) article.media = [];
|
|
||||||
article.media.unshift(media); // Add to the beginning
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error linking media to right article:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
deleteRightArticleMedia = async (articleId: number, mediaId: string) => {
|
|
||||||
try {
|
|
||||||
await authInstance.delete(`/article/${articleId}/media`, {
|
|
||||||
data: { media_id: mediaId },
|
|
||||||
});
|
|
||||||
runInAction(() => {
|
|
||||||
(["ru", "en", "zh"] as Language[]).forEach((lang) => {
|
|
||||||
const article = this.sight[lang].right.find(
|
|
||||||
(a) => a.id === articleId
|
|
||||||
);
|
|
||||||
if (article && article.media) {
|
|
||||||
article.media = article.media.filter((m) => m.id !== mediaId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error deleting media from right article:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Left Article Management (largely unchanged from your provided store) ---
|
|
||||||
updateLeftInfo = (language: Language, heading: string, body: string) => {
|
|
||||||
this.sight[language].left.heading = heading;
|
|
||||||
this.sight[language].left.body = body;
|
|
||||||
};
|
|
||||||
|
|
||||||
unlinkLeftArticle = () => {
|
|
||||||
/* ... your existing logic ... */
|
|
||||||
this.sight.left_article = 0;
|
|
||||||
this.sight.ru.left = { heading: "", body: "", media: [] };
|
|
||||||
this.sight.en.left = { heading: "", body: "", media: [] };
|
|
||||||
this.sight.zh.left = { heading: "", body: "", media: [] };
|
|
||||||
};
|
|
||||||
|
|
||||||
updateLeftArticle = async (articleId: number) => {
|
|
||||||
/* ... your existing logic ... */
|
|
||||||
this.sight.left_article = articleId;
|
|
||||||
if (articleId) {
|
|
||||||
const [ruArticleData, enArticleData, zhArticleData, mediaData] =
|
|
||||||
await Promise.all([
|
|
||||||
languageInstance("ru").get(`/article/${articleId}`),
|
|
||||||
languageInstance("en").get(`/article/${articleId}`),
|
|
||||||
languageInstance("zh").get(`/article/${articleId}`),
|
|
||||||
authInstance.get(`/article/${articleId}/media`),
|
|
||||||
]);
|
|
||||||
runInAction(() => {
|
|
||||||
this.sight.ru.left = {
|
|
||||||
heading: ruArticleData.data.heading,
|
|
||||||
body: ruArticleData.data.body,
|
|
||||||
media: mediaData.data || [],
|
|
||||||
};
|
|
||||||
this.sight.en.left = {
|
|
||||||
heading: enArticleData.data.heading,
|
|
||||||
body: enArticleData.data.body,
|
|
||||||
media: mediaData.data || [],
|
|
||||||
};
|
|
||||||
this.sight.zh.left = {
|
|
||||||
heading: zhArticleData.data.heading,
|
|
||||||
body: zhArticleData.data.body,
|
|
||||||
media: mediaData.data || [],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.unlinkLeftArticle();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
deleteLeftArticle = async (articleId: number) => {
|
|
||||||
/* ... your existing logic ... */
|
|
||||||
await authInstance.delete(`/article/${articleId}`);
|
|
||||||
// articlesStore.getArticles(languageStore.language); // If still needed
|
|
||||||
this.unlinkLeftArticle();
|
|
||||||
};
|
|
||||||
|
|
||||||
createLeftArticle = async () => {
|
|
||||||
/* ... your existing logic to create a new left article (placeholder or DB) ... */
|
|
||||||
const response = await languageInstance("ru").post("/article", {
|
|
||||||
heading: "Новая левая статья",
|
|
||||||
body: "Заполните контентом",
|
|
||||||
});
|
|
||||||
const newLeftArticleId = response.data.id;
|
|
||||||
await languageInstance("en").patch(`/article/${newLeftArticleId}`, {
|
|
||||||
heading: "New Left Article",
|
|
||||||
body: "Fill with content",
|
|
||||||
});
|
|
||||||
await languageInstance("zh").patch(`/article/${newLeftArticleId}`, {
|
|
||||||
heading: "新的左侧文章",
|
|
||||||
body: "填写内容",
|
|
||||||
});
|
|
||||||
|
|
||||||
runInAction(() => {
|
|
||||||
this.sight.left_article = newLeftArticleId; // Store the actual ID
|
|
||||||
this.sight.ru.left = {
|
|
||||||
heading: "Новая левая статья",
|
|
||||||
body: "Заполните контентом",
|
|
||||||
media: [],
|
|
||||||
};
|
|
||||||
this.sight.en.left = {
|
|
||||||
heading: "New Left Article",
|
|
||||||
body: "Fill with content",
|
|
||||||
media: [],
|
|
||||||
};
|
|
||||||
this.sight.zh.left = {
|
|
||||||
heading: "新的左侧文章",
|
|
||||||
body: "填写内容",
|
|
||||||
media: [],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return newLeftArticleId;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Placeholder for a "new" unsaved left article
|
|
||||||
setNewLeftArticlePlaceholder = () => {
|
|
||||||
this.sight.left_article = 10000000; // Special placeholder ID
|
|
||||||
this.sight.ru.left = {
|
|
||||||
heading: "Новая левая статья",
|
|
||||||
body: "Заполните контентом",
|
|
||||||
media: [],
|
|
||||||
};
|
|
||||||
this.sight.en.left = {
|
|
||||||
heading: "New Left Article",
|
|
||||||
body: "Fill with content",
|
|
||||||
media: [],
|
|
||||||
};
|
|
||||||
this.sight.zh.left = {
|
|
||||||
heading: "新的左侧文章",
|
|
||||||
body: "填写内容",
|
|
||||||
media: [],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Sight Preview Media ---
|
|
||||||
linkPreviewMedia = (mediaId: string) => {
|
|
||||||
this.sight.preview_media = mediaId;
|
|
||||||
};
|
|
||||||
|
|
||||||
unlinkPreviewMedia = () => {
|
|
||||||
this.sight.preview_media = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- General Store Methods ---
|
|
||||||
clearCreateSight = () => {
|
|
||||||
this.sight = JSON.parse(JSON.stringify(initialSightState)); // Reset to initial
|
|
||||||
};
|
|
||||||
|
|
||||||
updateSightInfo = (
|
|
||||||
content: Partial<SightLanguageInfo | SightCommonInfo>, // Corrected types
|
|
||||||
language?: Language
|
|
||||||
) => {
|
|
||||||
if (language) {
|
|
||||||
this.sight[language] = { ...this.sight[language], ...content };
|
|
||||||
} else {
|
|
||||||
// Assuming content here is for SightCommonInfo
|
|
||||||
this.sight = { ...this.sight, ...(content as Partial<SightCommonInfo>) };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Main Sight Creation Logic ---
|
|
||||||
createSight = async (primaryLanguage: Language) => {
|
|
||||||
let finalLeftArticleId = this.sight.left_article;
|
|
||||||
|
|
||||||
// 1. Handle Left Article (Create if new, or use existing ID)
|
|
||||||
if (this.sight.left_article === 10000000) {
|
|
||||||
// Placeholder for new
|
|
||||||
const res = await languageInstance("ru").post("/article", {
|
|
||||||
heading: this.sight.ru.left.heading,
|
|
||||||
body: this.sight.ru.left.body,
|
|
||||||
});
|
|
||||||
finalLeftArticleId = res.data.id;
|
|
||||||
await languageInstance("en").patch(`/article/${finalLeftArticleId}`, {
|
|
||||||
heading: this.sight.en.left.heading,
|
|
||||||
body: this.sight.en.left.body,
|
|
||||||
});
|
|
||||||
await languageInstance("zh").patch(`/article/${finalLeftArticleId}`, {
|
|
||||||
heading: this.sight.zh.left.heading,
|
|
||||||
body: this.sight.zh.left.body,
|
|
||||||
});
|
|
||||||
} else if (
|
|
||||||
this.sight.left_article !== 0 &&
|
|
||||||
this.sight.left_article !== null
|
|
||||||
) {
|
|
||||||
// Existing, ensure it's up-to-date
|
|
||||||
await languageInstance("ru").patch(
|
|
||||||
`/article/${this.sight.left_article}`,
|
|
||||||
{ heading: this.sight.ru.left.heading, body: this.sight.ru.left.body }
|
|
||||||
);
|
|
||||||
await languageInstance("en").patch(
|
|
||||||
`/article/${this.sight.left_article}`,
|
|
||||||
{ heading: this.sight.en.left.heading, body: this.sight.en.left.body }
|
|
||||||
);
|
|
||||||
await languageInstance("zh").patch(
|
|
||||||
`/article/${this.sight.left_article}`,
|
|
||||||
{ 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 article of this.sight[lang].right) {
|
|
||||||
if (article.id == 0 || article.id == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
await languageInstance(lang).patch(`/article/${article.id}`, {
|
|
||||||
heading: article.heading,
|
|
||||||
body: article.body,
|
|
||||||
});
|
|
||||||
// Media for these articles are already linked via createLinkWithRightArticle
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const rightArticleIdsForLink = this.sight[primaryLanguage].right.map(
|
|
||||||
(a) => a.id
|
|
||||||
);
|
|
||||||
|
|
||||||
// 3. Create Sight object in DB
|
|
||||||
const sightPayload = {
|
|
||||||
city_id: this.sight.city_id,
|
|
||||||
city: this.sight.city,
|
|
||||||
latitude: this.sight.latitude,
|
|
||||||
longitude: this.sight.longitude,
|
|
||||||
name: this.sight[primaryLanguage].name,
|
|
||||||
address: this.sight[primaryLanguage].address,
|
|
||||||
thumbnail: this.sight.thumbnail,
|
|
||||||
watermark_lu: this.sight.watermark_lu,
|
|
||||||
watermark_rd: this.sight.watermark_rd,
|
|
||||||
left_article: finalLeftArticleId === 0 ? null : finalLeftArticleId,
|
|
||||||
preview_media: this.sight.preview_media,
|
|
||||||
video_preview: this.sight.video_preview,
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await languageInstance(primaryLanguage).post(
|
|
||||||
"/sight",
|
|
||||||
sightPayload
|
|
||||||
);
|
|
||||||
const newSightId = response.data.id; // ID of the newly created sight
|
|
||||||
|
|
||||||
// 4. Update other languages for the sight
|
|
||||||
const otherLanguages = (["ru", "en", "zh"] as Language[]).filter(
|
|
||||||
(l) => l !== primaryLanguage
|
|
||||||
);
|
|
||||||
for (const lang of otherLanguages) {
|
|
||||||
await languageInstance(lang).patch(`/sight/${newSightId}`, {
|
|
||||||
city_id: this.sight.city_id,
|
|
||||||
city: this.sight.city,
|
|
||||||
latitude: this.sight.latitude,
|
|
||||||
longitude: this.sight.longitude,
|
|
||||||
name: this.sight[lang].name,
|
|
||||||
address: this.sight[lang].address,
|
|
||||||
thumbnail: this.sight.thumbnail,
|
|
||||||
watermark_lu: this.sight.watermark_lu,
|
|
||||||
watermark_rd: this.sight.watermark_rd,
|
|
||||||
left_article: finalLeftArticleId === 0 ? null : finalLeftArticleId,
|
|
||||||
preview_media: this.sight.preview_media,
|
|
||||||
video_preview: this.sight.video_preview,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Link Right Articles to the new Sight
|
|
||||||
for (let i = 0; i < rightArticleIdsForLink.length; i++) {
|
|
||||||
await authInstance.post(`/sight/${newSightId}/article`, {
|
|
||||||
article_id: rightArticleIdsForLink[i],
|
|
||||||
page_num: i + 1, // Or other logic for page_num
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Sight created with ID:", newSightId);
|
|
||||||
// Optionally: this.clearCreateSight(); // To reset form after successful creation
|
|
||||||
return newSightId;
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Media Upload (Generic, used by dialogs) ---
|
|
||||||
uploadMedia = async (
|
uploadMedia = async (
|
||||||
filename: string,
|
filename: string,
|
||||||
type: number,
|
type: number,
|
||||||
file: File,
|
file: File,
|
||||||
media_name?: string
|
media_name?: string
|
||||||
): Promise<MediaItem> => {
|
) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", file);
|
formData.append("file", file);
|
||||||
formData.append("filename", filename);
|
formData.append("filename", filename);
|
||||||
if (media_name) formData.append("media_name", media_name);
|
if (media_name) {
|
||||||
|
formData.append("media_name", media_name);
|
||||||
|
}
|
||||||
formData.append("type", type.toString());
|
formData.append("type", type.toString());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await authInstance.post(`/media`, formData);
|
const response = await authInstance.post(`/media`, formData);
|
||||||
runInAction(() => {
|
this.fileToUpload = null;
|
||||||
this.fileToUpload = null;
|
this.uploadMediaOpen = false;
|
||||||
this.uploadMediaOpen = false;
|
mediaStore.getMedia();
|
||||||
});
|
|
||||||
mediaStore.getMedia(); // Refresh global media list
|
|
||||||
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.log(error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// For Left Article Media
|
createLinkWithArticle = async (media: {
|
||||||
createLinkWithLeftArticle = async (media: MediaItem) => {
|
id: string;
|
||||||
if (!this.sight.left_article || this.sight.left_article === 10000000) {
|
filename: string;
|
||||||
console.warn(
|
media_name?: string;
|
||||||
"Left article not selected or is a placeholder. Cannot link media yet."
|
media_type: number;
|
||||||
);
|
}) => {
|
||||||
// If it's a placeholder, we could store the media temporarily and link it after the article is created.
|
await authInstance.post(`/article/${this.sight.left_article}/media`, {
|
||||||
// For simplicity, we'll assume the article must exist.
|
media_id: media.id,
|
||||||
// A more robust solution might involve creating the article first if it's a placeholder.
|
media_order: 1,
|
||||||
return;
|
});
|
||||||
}
|
|
||||||
try {
|
|
||||||
await authInstance.post(`/article/${this.sight.left_article}/media`, {
|
|
||||||
media_id: media.id,
|
|
||||||
media_order: (this.sight.ru.left.media?.length || 0) + 1,
|
|
||||||
});
|
|
||||||
runInAction(() => {
|
|
||||||
(["ru", "en", "zh"] as Language[]).forEach((lang) => {
|
|
||||||
if (!this.sight[lang].left.media) this.sight[lang].left.media = [];
|
|
||||||
this.sight[lang].left.media.unshift(media);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error linking media to left article:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
deleteLeftArticleMedia = async (mediaId: string) => {
|
this.sight.ru.left.media.unshift({
|
||||||
if (!this.sight.left_article || this.sight.left_article === 10000000)
|
id: media.id,
|
||||||
return;
|
media_type: media.media_type,
|
||||||
try {
|
filename: media.filename,
|
||||||
await authInstance.delete(`/article/${this.sight.left_article}/media`, {
|
});
|
||||||
data: { media_id: mediaId },
|
|
||||||
});
|
this.sight.en.left.media.unshift({
|
||||||
runInAction(() => {
|
id: media.id,
|
||||||
(["ru", "en", "zh"] as Language[]).forEach((lang) => {
|
media_type: media.media_type,
|
||||||
if (this.sight[lang].left.media) {
|
filename: media.filename,
|
||||||
this.sight[lang].left.media = this.sight[lang].left.media.filter(
|
});
|
||||||
(m) => m.id !== mediaId
|
|
||||||
);
|
this.sight.zh.left.media.unshift({
|
||||||
}
|
id: media.id,
|
||||||
});
|
media_type: media.media_type,
|
||||||
});
|
filename: media.filename,
|
||||||
} catch (error) {
|
});
|
||||||
console.error("Error deleting media from left article:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,12 +11,7 @@ export type SightLanguageInfo = {
|
|||||||
body: string;
|
body: string;
|
||||||
media: { id: string; media_type: number; filename: string }[];
|
media: { id: string; media_type: number; filename: string }[];
|
||||||
};
|
};
|
||||||
right: {
|
right: { heading: string; body: string }[];
|
||||||
id: number;
|
|
||||||
heading: string;
|
|
||||||
body: string;
|
|
||||||
media: { id: string; media_type: number; filename: string }[];
|
|
||||||
}[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SightCommonInfo = {
|
export type SightCommonInfo = {
|
||||||
@ -88,8 +83,7 @@ class EditSightStore {
|
|||||||
|
|
||||||
const response = await authInstance.get(`/sight/${id}`);
|
const response = await authInstance.get(`/sight/${id}`);
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
|
if (data.left_article != 0) {
|
||||||
if (data.left_article != 0 && data.left_article != null) {
|
|
||||||
await this.getLeftArticle(data.left_article);
|
await this.getLeftArticle(data.left_article);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,42 +111,21 @@ class EditSightStore {
|
|||||||
};
|
};
|
||||||
|
|
||||||
getRightArticles = async (id: number) => {
|
getRightArticles = async (id: number) => {
|
||||||
let responseRu = await languageInstance("ru").get(`/sight/${id}/article`);
|
const responseRu = await languageInstance("ru").get(`/sight/${id}/article`);
|
||||||
let responseEn = await languageInstance("en").get(`/sight/${id}/article`);
|
const responseEn = await languageInstance("en").get(`/sight/${id}/article`);
|
||||||
let responseZh = await languageInstance("zh").get(`/sight/${id}/article`);
|
const responseZh = await languageInstance("zh").get(`/sight/${id}/article`);
|
||||||
|
|
||||||
// Function to fetch media for a given set of articles
|
|
||||||
const fetchMediaForArticles = async (articles: any[]) => {
|
|
||||||
const articlesWithMedia = [];
|
|
||||||
for (const article of articles) {
|
|
||||||
const responseMedia = await authInstance.get(
|
|
||||||
`/article/${article.id}/media`
|
|
||||||
);
|
|
||||||
articlesWithMedia.push({
|
|
||||||
...article,
|
|
||||||
media: responseMedia.data,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return articlesWithMedia;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Fetch media for articles in each language
|
|
||||||
const ruArticlesWithMedia = await fetchMediaForArticles(responseRu.data);
|
|
||||||
const enArticlesWithMedia = await fetchMediaForArticles(responseEn.data);
|
|
||||||
const zhArticlesWithMedia = await fetchMediaForArticles(responseZh.data);
|
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
ru: {
|
ru: {
|
||||||
right: ruArticlesWithMedia,
|
right: responseRu.data,
|
||||||
},
|
},
|
||||||
en: {
|
en: {
|
||||||
right: enArticlesWithMedia,
|
right: responseEn.data,
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
right: zhArticlesWithMedia,
|
right: responseZh.data,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.sight = {
|
this.sight = {
|
||||||
...this.sight,
|
...this.sight,
|
||||||
@ -164,6 +137,7 @@ class EditSightStore {
|
|||||||
...this.sight.en,
|
...this.sight.en,
|
||||||
right: data.en.right,
|
right: data.en.right,
|
||||||
},
|
},
|
||||||
|
|
||||||
zh: {
|
zh: {
|
||||||
...this.sight.zh,
|
...this.sight.zh,
|
||||||
right: data.zh.right,
|
right: data.zh.right,
|
||||||
@ -261,10 +235,7 @@ class EditSightStore {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.sight.common.left_article = createdLeftArticleId;
|
this.sight.common.left_article = createdLeftArticleId;
|
||||||
} else if (
|
} else if (this.sight.common.left_article != 0) {
|
||||||
this.sight.common.left_article != 0 &&
|
|
||||||
this.sight.common.left_article != null
|
|
||||||
) {
|
|
||||||
await languageInstance("ru").patch(
|
await languageInstance("ru").patch(
|
||||||
`/article/${this.sight.common.left_article}`,
|
`/article/${this.sight.common.left_article}`,
|
||||||
{
|
{
|
||||||
@ -308,16 +279,8 @@ class EditSightStore {
|
|||||||
left_article: createdLeftArticleId,
|
left_article: createdLeftArticleId,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const language of ["ru", "en", "zh"] as Language[]) {
|
if (this.sight.common.left_article == 0) {
|
||||||
for (const article of this.sight[language].right) {
|
return;
|
||||||
if (article.id == 0 || article.id == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
await languageInstance(language).patch(`/article/${article.id}`, {
|
|
||||||
heading: article.heading,
|
|
||||||
body: article.body,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// await languageInstance("ru").patch(
|
// await languageInstance("ru").patch(
|
||||||
@ -412,38 +375,6 @@ class EditSightStore {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
unlinkRightArticle = async (article_id: number) => {
|
|
||||||
await authInstance.delete(`/sight/${this.sight.common.id}/article`, {
|
|
||||||
data: {
|
|
||||||
article_id: article_id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.sight.ru.right = this.sight.ru.right.filter(
|
|
||||||
(article) => article.id !== article_id
|
|
||||||
);
|
|
||||||
this.sight.en.right = this.sight.en.right.filter(
|
|
||||||
(article) => article.id !== article_id
|
|
||||||
);
|
|
||||||
this.sight.zh.right = this.sight.zh.right.filter(
|
|
||||||
(article) => article.id !== article_id
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
deleteRightArticle = async (article_id: number) => {
|
|
||||||
this.sight.ru.right = this.sight.ru.right.filter(
|
|
||||||
(article) => article.id !== article_id
|
|
||||||
);
|
|
||||||
this.sight.en.right = this.sight.en.right.filter(
|
|
||||||
(article) => article.id !== article_id
|
|
||||||
);
|
|
||||||
this.sight.zh.right = this.sight.zh.right.filter(
|
|
||||||
(article) => article.id !== article_id
|
|
||||||
);
|
|
||||||
|
|
||||||
await authInstance.delete(`/article/${article_id}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
uploadMediaOpen = false;
|
uploadMediaOpen = false;
|
||||||
setUploadMediaOpen = (open: boolean) => {
|
setUploadMediaOpen = (open: boolean) => {
|
||||||
this.uploadMediaOpen = open;
|
this.uploadMediaOpen = open;
|
||||||
@ -511,168 +442,6 @@ class EditSightStore {
|
|||||||
filename: media.filename,
|
filename: media.filename,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
unlinkPreviewMedia = async () => {
|
|
||||||
this.sight.common.preview_media = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
linkPreviewMedia = async (mediaId: string) => {
|
|
||||||
this.sight.common.preview_media = mediaId;
|
|
||||||
};
|
|
||||||
|
|
||||||
linkArticle = async (article_id: number) => {
|
|
||||||
const response = await languageInstance("ru").get(`/article/${article_id}`);
|
|
||||||
const responseEn = await languageInstance("en").get(
|
|
||||||
`/article/${article_id}`
|
|
||||||
);
|
|
||||||
const responseZh = await languageInstance("zh").get(
|
|
||||||
`/article/${article_id}`
|
|
||||||
);
|
|
||||||
const mediaIds = await authInstance.get(`/article/${article_id}/media`);
|
|
||||||
runInAction(() => {
|
|
||||||
this.sight.ru.right.push({
|
|
||||||
id: article_id,
|
|
||||||
heading: response.data.heading,
|
|
||||||
body: response.data.body,
|
|
||||||
media: mediaIds.data,
|
|
||||||
});
|
|
||||||
this.sight.en.right.push({
|
|
||||||
id: article_id,
|
|
||||||
heading: responseEn.data.heading,
|
|
||||||
body: responseEn.data.body,
|
|
||||||
media: mediaIds.data,
|
|
||||||
});
|
|
||||||
this.sight.zh.right.push({
|
|
||||||
id: article_id,
|
|
||||||
heading: responseZh.data.heading,
|
|
||||||
body: responseZh.data.body,
|
|
||||||
media: mediaIds.data,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
deleteRightArticleMedia = async (article_id: number, media_id: string) => {
|
|
||||||
await authInstance.delete(`/article/${article_id}/media`, {
|
|
||||||
data: {
|
|
||||||
media_id: media_id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.sight.ru.right = this.sight.ru.right.map((article) => {
|
|
||||||
if (article.id === article_id) {
|
|
||||||
article.media = article.media.filter((media) => media.id !== media_id);
|
|
||||||
}
|
|
||||||
return article;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.sight.en.right = this.sight.en.right.map((article) => {
|
|
||||||
if (article.id === article_id) {
|
|
||||||
article.media = article.media.filter((media) => media.id !== media_id);
|
|
||||||
}
|
|
||||||
return article;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.sight.zh.right = this.sight.zh.right.map((article) => {
|
|
||||||
if (article.id === article_id) {
|
|
||||||
article.media = article.media.filter((media) => media.id !== media_id);
|
|
||||||
}
|
|
||||||
return article;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
createNewRightArticle = async () => {
|
|
||||||
const articleId = await languageInstance("ru").post("/article", {
|
|
||||||
heading: "Введите русский заголовок",
|
|
||||||
body: "Введите русский текст",
|
|
||||||
});
|
|
||||||
const { id } = articleId.data;
|
|
||||||
await languageInstance("en").patch(`/article/${id}`, {
|
|
||||||
heading: "Enter the English heading",
|
|
||||||
body: "Enter the English text",
|
|
||||||
});
|
|
||||||
await languageInstance("zh").patch(`/article/${id}`, {
|
|
||||||
heading: "Введите китайский заголовок",
|
|
||||||
body: "Введите китайский текст",
|
|
||||||
});
|
|
||||||
await authInstance.post(`/sight/${this.sight.common.id}/article`, {
|
|
||||||
article_id: id,
|
|
||||||
page_num: this.sight.ru.right.length + 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.sight.ru.right.push({
|
|
||||||
id: id,
|
|
||||||
heading: "Введите русский заголовок",
|
|
||||||
body: "Введите русский текст",
|
|
||||||
media: [],
|
|
||||||
});
|
|
||||||
this.sight.en.right.push({
|
|
||||||
id: id,
|
|
||||||
heading: "Enter the English heading",
|
|
||||||
body: "Enter the English text",
|
|
||||||
media: [],
|
|
||||||
});
|
|
||||||
this.sight.zh.right.push({
|
|
||||||
id: id,
|
|
||||||
heading: "Введите китайский заголовок",
|
|
||||||
body: "Введите китайский текст",
|
|
||||||
media: [],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
createLinkWithRightArticle = async (
|
|
||||||
media: {
|
|
||||||
id: string;
|
|
||||||
filename: string;
|
|
||||||
media_name?: string;
|
|
||||||
media_type: number;
|
|
||||||
},
|
|
||||||
article_id: number
|
|
||||||
) => {
|
|
||||||
await authInstance.post(`/article/${article_id}/media`, {
|
|
||||||
media_id: media.id,
|
|
||||||
media_order: 1,
|
|
||||||
});
|
|
||||||
this.sight.ru.right = this.sight.ru.right.map((article) => {
|
|
||||||
if (article.id === article_id) {
|
|
||||||
article.media.unshift({
|
|
||||||
id: media.id,
|
|
||||||
media_type: media.media_type,
|
|
||||||
filename: media.filename,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return article;
|
|
||||||
});
|
|
||||||
this.sight.en.right = this.sight.en.right.map((article) => {
|
|
||||||
if (article.id === article_id) {
|
|
||||||
article.media.unshift({
|
|
||||||
id: media.id,
|
|
||||||
media_type: media.media_type,
|
|
||||||
filename: media.filename,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return article;
|
|
||||||
});
|
|
||||||
this.sight.zh.right = this.sight.zh.right.map((article) => {
|
|
||||||
if (article.id === article_id) {
|
|
||||||
article.media.unshift({
|
|
||||||
id: media.id,
|
|
||||||
media_type: media.media_type,
|
|
||||||
filename: media.filename,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return article;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
updateRightArticleInfo = (
|
|
||||||
index: number,
|
|
||||||
language: Language,
|
|
||||||
heading: string,
|
|
||||||
body: string
|
|
||||||
) => {
|
|
||||||
this.sight[language].right[index].heading = heading;
|
|
||||||
this.sight[language].right[index].body = body;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const editSightStore = new EditSightStore();
|
export const editSightStore = new EditSightStore();
|
||||||
|
@ -1,182 +0,0 @@
|
|||||||
import React, { useRef, useState, DragEvent, useEffect } from "react";
|
|
||||||
import { Paper, Box, Typography, Button, Tooltip } from "@mui/material";
|
|
||||||
import { X, Info } from "lucide-react"; // Assuming lucide-react for icons
|
|
||||||
import { editSightStore } from "@shared";
|
|
||||||
|
|
||||||
interface ImageUploadCardProps {
|
|
||||||
title: string;
|
|
||||||
imageKey?: "thumbnail" | "watermark_lu" | "watermark_rd";
|
|
||||||
imageUrl: string | null | undefined;
|
|
||||||
onImageClick: () => void;
|
|
||||||
onDeleteImageClick: () => void;
|
|
||||||
onSelectFileClick: () => void;
|
|
||||||
setUploadMediaOpen: (open: boolean) => void;
|
|
||||||
tooltipText?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
|
|
||||||
title,
|
|
||||||
|
|
||||||
imageUrl,
|
|
||||||
onImageClick,
|
|
||||||
onDeleteImageClick,
|
|
||||||
onSelectFileClick,
|
|
||||||
setUploadMediaOpen,
|
|
||||||
tooltipText,
|
|
||||||
}) => {
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
const [isDragOver, setIsDragOver] = useState(false);
|
|
||||||
const { setFileToUpload } = editSightStore;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isDragOver) {
|
|
||||||
console.log("isDragOver");
|
|
||||||
}
|
|
||||||
}, [isDragOver]);
|
|
||||||
// --- Click to select file ---
|
|
||||||
const handleZoneClick = () => {
|
|
||||||
// Trigger the hidden file input click
|
|
||||||
fileInputRef.current?.click();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFileInputChange = async (
|
|
||||||
event: React.ChangeEvent<HTMLInputElement>
|
|
||||||
) => {
|
|
||||||
const file = event.target.files?.[0];
|
|
||||||
if (file) {
|
|
||||||
setFileToUpload(file);
|
|
||||||
setUploadMediaOpen(true);
|
|
||||||
}
|
|
||||||
// Reset the input value so selecting the same file again triggers change
|
|
||||||
event.target.value = "";
|
|
||||||
};
|
|
||||||
|
|
||||||
const token = localStorage.getItem("token");
|
|
||||||
// --- Drag and Drop Handlers ---
|
|
||||||
const handleDragOver = (event: DragEvent<HTMLDivElement>) => {
|
|
||||||
event.preventDefault(); // Crucial to allow a drop
|
|
||||||
event.stopPropagation();
|
|
||||||
setIsDragOver(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDragLeave = (event: DragEvent<HTMLDivElement>) => {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
setIsDragOver(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDrop = async (event: DragEvent<HTMLDivElement>) => {
|
|
||||||
event.preventDefault(); // Crucial to allow a drop
|
|
||||||
event.stopPropagation();
|
|
||||||
setIsDragOver(false);
|
|
||||||
|
|
||||||
const files = event.dataTransfer.files;
|
|
||||||
if (files && files.length > 0) {
|
|
||||||
const file = files[0];
|
|
||||||
setFileToUpload(file);
|
|
||||||
setUploadMediaOpen(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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>
|
|
||||||
{tooltipText && (
|
|
||||||
<Tooltip title={tooltipText}>
|
|
||||||
<Info size={16} color="gray" style={{ cursor: "pointer" }} />
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
position: "relative",
|
|
||||||
width: "200px",
|
|
||||||
height: "200px",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
borderRadius: 1,
|
|
||||||
mb: 1,
|
|
||||||
cursor: imageUrl ? "pointer" : "default",
|
|
||||||
}}
|
|
||||||
onClick={onImageClick}
|
|
||||||
// Removed onClick on the main Box to avoid conflicts
|
|
||||||
>
|
|
||||||
{imageUrl && (
|
|
||||||
<button
|
|
||||||
className="absolute top-2 right-2"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onDeleteImageClick();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<X color="red" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{imageUrl ? (
|
|
||||||
<img
|
|
||||||
src={`${
|
|
||||||
import.meta.env.VITE_KRBL_MEDIA
|
|
||||||
}${imageUrl}/download?token=${token}`}
|
|
||||||
alt={title}
|
|
||||||
style={{ maxWidth: "100%", maxHeight: "100%" }}
|
|
||||||
onClick={onImageClick}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className={`w-full flex flex-col items-center justify-center gap-3 `}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="flex flex-col p-5 items-center justify-center gap-3"
|
|
||||||
style={{
|
|
||||||
border: "2px dashed #ccc",
|
|
||||||
borderRadius: 1,
|
|
||||||
cursor: "pointer",
|
|
||||||
}}
|
|
||||||
onClick={handleZoneClick} // Click handler for the zone
|
|
||||||
onDragOver={handleDragOver}
|
|
||||||
onDragLeave={handleDragLeave}
|
|
||||||
onDrop={handleDrop}
|
|
||||||
>
|
|
||||||
<p>Перетащите файл</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>или</p>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation(); // Prevent `handleZoneClick` from firing
|
|
||||||
onSelectFileClick(); // This button might trigger a different modal
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Выбрать файл
|
|
||||||
</Button>
|
|
||||||
{/* Hidden file input */}
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
ref={fileInputRef}
|
|
||||||
onChange={handleFileInputChange}
|
|
||||||
style={{ display: "none" }}
|
|
||||||
accept="image/*" // Accept only image files
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,107 +0,0 @@
|
|||||||
import { Box, Button } from "@mui/material";
|
|
||||||
import { editSightStore, SelectMediaDialog, UploadMediaDialog } from "@shared";
|
|
||||||
import { Upload } from "lucide-react";
|
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
import { useState, DragEvent, useRef } from "react";
|
|
||||||
|
|
||||||
export const MediaAreaForSight = observer(
|
|
||||||
({
|
|
||||||
onFilesDrop, // 👈 Проп для обработки загруженных файлов
|
|
||||||
onFinishUpload,
|
|
||||||
}: {
|
|
||||||
onFilesDrop?: (files: File[]) => void;
|
|
||||||
onFinishUpload?: (mediaId: string) => void;
|
|
||||||
}) => {
|
|
||||||
const [selectMediaDialogOpen, setSelectMediaDialogOpen] = useState(false);
|
|
||||||
const [uploadMediaDialogOpen, setUploadMediaDialogOpen] = useState(false);
|
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
const { setFileToUpload } = editSightStore;
|
|
||||||
|
|
||||||
const handleDrop = (e: DragEvent<HTMLDivElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsDragging(false);
|
|
||||||
|
|
||||||
const files = Array.from(e.dataTransfer.files);
|
|
||||||
if (files.length && onFilesDrop) {
|
|
||||||
setFileToUpload(files[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
setUploadMediaDialogOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDragOver = (e: DragEvent<HTMLDivElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setIsDragging(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDragLeave = () => {
|
|
||||||
setIsDragging(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClick = () => {
|
|
||||||
fileInputRef.current?.click();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const files = Array.from(event.target.files || []);
|
|
||||||
if (files.length && onFilesDrop) {
|
|
||||||
setFileToUpload(files[0]);
|
|
||||||
onFilesDrop(files);
|
|
||||||
setUploadMediaDialogOpen(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Сбрасываем значение input, чтобы можно было выбрать тот же файл снова
|
|
||||||
event.target.value = "";
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
ref={fileInputRef}
|
|
||||||
onChange={handleFileSelect}
|
|
||||||
accept="image/*,video/*,.glb,.gltf"
|
|
||||||
multiple
|
|
||||||
style={{ display: "none" }}
|
|
||||||
/>
|
|
||||||
<Box className="w-full flex flex-col items-center justify-center border rounded-md p-4">
|
|
||||||
<div className="w-full flex flex-col items-center justify-center">
|
|
||||||
<div
|
|
||||||
className={`w-full h-40 flex flex-col justify-center items-center text-gray-400 border-dashed border-2 rounded-md border-gray-400 p-4 cursor-pointer hover:bg-gray-50 ${
|
|
||||||
isDragging ? "bg-blue-100 border-blue-400" : ""
|
|
||||||
}`}
|
|
||||||
onDrop={handleDrop}
|
|
||||||
onDragOver={handleDragOver}
|
|
||||||
onDragLeave={handleDragLeave}
|
|
||||||
onClick={handleClick}
|
|
||||||
>
|
|
||||||
<Upload size={32} className="mb-2" />
|
|
||||||
Перетащите медиа файлы сюда или нажмите для выбора
|
|
||||||
</div>
|
|
||||||
<div>или</div>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
onClick={() => setSelectMediaDialogOpen(true)}
|
|
||||||
>
|
|
||||||
Выбрать существующие медиа файлы
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<UploadMediaDialog
|
|
||||||
open={uploadMediaDialogOpen}
|
|
||||||
onClose={() => setUploadMediaDialogOpen(false)}
|
|
||||||
afterUploadSight={onFinishUpload}
|
|
||||||
/>
|
|
||||||
<SelectMediaDialog
|
|
||||||
open={selectMediaDialogOpen}
|
|
||||||
onClose={() => setSelectMediaDialogOpen(false)}
|
|
||||||
onSelectForSightMedia={onFinishUpload}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
@ -3,6 +3,9 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
Box,
|
Box,
|
||||||
Autocomplete,
|
Autocomplete,
|
||||||
|
Typography,
|
||||||
|
Paper,
|
||||||
|
Tooltip,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Menu as MuiMenu,
|
Menu as MuiMenu,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
@ -17,9 +20,9 @@ import {
|
|||||||
SightLanguageInfo,
|
SightLanguageInfo,
|
||||||
SightCommonInfo,
|
SightCommonInfo,
|
||||||
createSightStore,
|
createSightStore,
|
||||||
UploadMediaDialog,
|
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
import { ImageUploadCard, LanguageSwitcher } from "@widgets";
|
import { LanguageSwitcher } from "@widgets";
|
||||||
|
import { Info, X } from "lucide-react";
|
||||||
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
@ -30,16 +33,20 @@ import { toast } from "react-toastify";
|
|||||||
export const CreateInformationTab = observer(
|
export const CreateInformationTab = observer(
|
||||||
({ value, index }: { value: number; index: number }) => {
|
({ value, index }: { value: number; index: number }) => {
|
||||||
const { cities } = cityStore;
|
const { cities } = cityStore;
|
||||||
|
const [, setIsMediaModalOpen] = useState(false);
|
||||||
const [mediaId, setMediaId] = useState<string>("");
|
const [mediaId, setMediaId] = useState<string>("");
|
||||||
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
|
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
|
||||||
const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false);
|
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
const { sight, updateSightInfo, createSight } = createSightStore;
|
const { sight, updateSightInfo, createSight } = createSightStore;
|
||||||
|
|
||||||
const data = sight[language];
|
const data = sight[language];
|
||||||
|
|
||||||
const [, setCity] = useState<number>(sight.city_id ?? 0);
|
const [, setCity] = useState<number>(sight.city_id ?? 0);
|
||||||
const [coordinates, setCoordinates] = useState<string>(`0 0`);
|
const [coordinates, setCoordinates] = useState<string>(`0 0`);
|
||||||
|
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
// Menu state for each media button
|
// Menu state for each media button
|
||||||
const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null);
|
const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
const [activeMenuType, setActiveMenuType] = useState<
|
const [activeMenuType, setActiveMenuType] = useState<
|
||||||
@ -102,7 +109,6 @@ export const CreateInformationTab = observer(
|
|||||||
});
|
});
|
||||||
setActiveMenuType(null);
|
setActiveMenuType(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TabPanel value={value} index={index}>
|
<TabPanel value={value} index={index}>
|
||||||
@ -236,77 +242,271 @@ export const CreateInformationTab = observer(
|
|||||||
flexDirection: { xs: "column", sm: "row" }, // Stack on extra small, side-by-side on small and up
|
flexDirection: { xs: "column", sm: "row" }, // Stack on extra small, side-by-side on small and up
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ImageUploadCard
|
<Paper
|
||||||
title="Логотип"
|
elevation={2}
|
||||||
imageKey="thumbnail"
|
sx={{
|
||||||
imageUrl={sight.thumbnail}
|
padding: 2,
|
||||||
onImageClick={() => {
|
display: "flex",
|
||||||
setIsPreviewMediaOpen(true);
|
flexDirection: "column",
|
||||||
setMediaId(sight.thumbnail ?? "");
|
alignItems: "center",
|
||||||
|
gap: 1,
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 150, // Ensure a minimum width
|
||||||
}}
|
}}
|
||||||
onDeleteImageClick={() => {
|
>
|
||||||
handleChange({
|
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||||
thumbnail: null,
|
<Typography
|
||||||
});
|
variant="subtitle2"
|
||||||
setActiveMenuType(null);
|
gutterBottom
|
||||||
}}
|
sx={{ mb: 0, mr: 0.5 }}
|
||||||
onSelectFileClick={() => {
|
>
|
||||||
setActiveMenuType("thumbnail");
|
Логотип
|
||||||
setIsAddMediaOpen(true);
|
</Typography>
|
||||||
}}
|
</Box>
|
||||||
setUploadMediaOpen={() => {
|
<Box
|
||||||
setIsUploadMediaOpen(true);
|
sx={{
|
||||||
setActiveMenuType("thumbnail");
|
position: "relative",
|
||||||
}}
|
width: "200px",
|
||||||
/>
|
height: "200px",
|
||||||
|
|
||||||
<ImageUploadCard
|
display: "flex",
|
||||||
title="Водяной знак (л.в)"
|
alignItems: "center",
|
||||||
imageKey="watermark_lu"
|
justifyContent: "center",
|
||||||
imageUrl={sight.watermark_lu}
|
borderRadius: 1,
|
||||||
onImageClick={() => {
|
mb: 1,
|
||||||
setIsPreviewMediaOpen(true);
|
cursor: sight.thumbnail ? "pointer" : "default",
|
||||||
setMediaId(sight.watermark_lu ?? "");
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setIsMediaModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{sight.thumbnail && (
|
||||||
|
<button
|
||||||
|
className="absolute top-2 right-2"
|
||||||
|
onClick={() => {
|
||||||
|
handleChange({
|
||||||
|
thumbnail: null,
|
||||||
|
});
|
||||||
|
setActiveMenuType(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X color="red" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{sight.thumbnail ? (
|
||||||
|
<img
|
||||||
|
src={`${import.meta.env.VITE_KRBL_MEDIA}${
|
||||||
|
sight.thumbnail
|
||||||
|
}/download?token=${token}`}
|
||||||
|
alt="Логотип"
|
||||||
|
style={{ maxWidth: "100%", maxHeight: "100%" }}
|
||||||
|
onClick={() => {
|
||||||
|
setIsPreviewMediaOpen(true);
|
||||||
|
setMediaId(sight.thumbnail ?? "");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<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 border-gray-300 cursor-pointer hover:bg-gray-100">
|
||||||
|
<p>Перетащите файл</p>
|
||||||
|
</div>
|
||||||
|
<p>или</p>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
setIsAddMediaOpen(true);
|
||||||
|
setActiveMenuType("thumbnail");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Выбрать файл
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
<Paper
|
||||||
|
elevation={2}
|
||||||
|
sx={{
|
||||||
|
padding: 2,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 1,
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 150, // Ensure a minimum width
|
||||||
}}
|
}}
|
||||||
onDeleteImageClick={() => {
|
>
|
||||||
handleChange({
|
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||||
watermark_lu: null,
|
<Typography
|
||||||
});
|
variant="subtitle2"
|
||||||
setActiveMenuType(null);
|
gutterBottom
|
||||||
}}
|
sx={{ mb: 0, mr: 0.5 }}
|
||||||
onSelectFileClick={() => {
|
>
|
||||||
setActiveMenuType("watermark_lu");
|
Водяной знак (л.в)
|
||||||
setIsAddMediaOpen(true);
|
</Typography>
|
||||||
}}
|
<Tooltip title={"asf"}>
|
||||||
setUploadMediaOpen={() => {
|
<Info
|
||||||
setIsUploadMediaOpen(true);
|
size={16}
|
||||||
setActiveMenuType("watermark_lu");
|
color="gray"
|
||||||
}}
|
style={{ cursor: "pointer" }}
|
||||||
/>
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<ImageUploadCard
|
<Box
|
||||||
title="Водяной знак (п.в)"
|
sx={{
|
||||||
imageKey="watermark_rd"
|
position: "relative",
|
||||||
imageUrl={sight.watermark_rd}
|
width: "200px",
|
||||||
onImageClick={() => {
|
height: "200px",
|
||||||
setIsPreviewMediaOpen(true);
|
|
||||||
setMediaId(sight.watermark_rd ?? "");
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
borderRadius: 1,
|
||||||
|
mb: 1,
|
||||||
|
cursor: sight.watermark_lu ? "pointer" : "default",
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setIsMediaModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{sight.watermark_lu && (
|
||||||
|
<button
|
||||||
|
className="absolute top-2 right-2"
|
||||||
|
onClick={() => {
|
||||||
|
handleChange({
|
||||||
|
watermark_lu: null,
|
||||||
|
});
|
||||||
|
setActiveMenuType(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X color="red" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{sight.watermark_lu ? (
|
||||||
|
<img
|
||||||
|
src={`${import.meta.env.VITE_KRBL_MEDIA}${
|
||||||
|
sight.watermark_lu
|
||||||
|
}/download?token=${token}`}
|
||||||
|
alt="Логотип"
|
||||||
|
style={{ maxWidth: "100%", maxHeight: "100%" }}
|
||||||
|
onClick={() => {
|
||||||
|
setIsPreviewMediaOpen(true);
|
||||||
|
setMediaId(sight.watermark_lu ?? "");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<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 border-gray-300 cursor-pointer hover:bg-gray-100">
|
||||||
|
<p>Перетащите файл</p>
|
||||||
|
</div>
|
||||||
|
<p>или</p>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
setActiveMenuType("watermark_lu");
|
||||||
|
setIsAddMediaOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Выбрать файл
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
elevation={2}
|
||||||
|
sx={{
|
||||||
|
padding: 2,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 1,
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 150, // Ensure a minimum width
|
||||||
}}
|
}}
|
||||||
onDeleteImageClick={() => {
|
>
|
||||||
handleChange({
|
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||||
watermark_rd: null,
|
<Typography
|
||||||
});
|
variant="subtitle2"
|
||||||
setActiveMenuType(null);
|
gutterBottom
|
||||||
}}
|
sx={{ mb: 0, mr: 0.5 }}
|
||||||
onSelectFileClick={() => {
|
>
|
||||||
setActiveMenuType("watermark_rd");
|
Водяной знак (п.в)
|
||||||
setIsAddMediaOpen(true);
|
</Typography>
|
||||||
}}
|
<Tooltip title={"asfaf"}>
|
||||||
setUploadMediaOpen={() => {
|
<Info
|
||||||
setIsUploadMediaOpen(true);
|
size={16}
|
||||||
setActiveMenuType("watermark_rd");
|
color="gray"
|
||||||
}}
|
style={{ cursor: "pointer" }}
|
||||||
/>
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "relative",
|
||||||
|
width: "200px",
|
||||||
|
height: "200px",
|
||||||
|
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
borderRadius: 1,
|
||||||
|
mb: 1,
|
||||||
|
cursor: sight.watermark_rd ? "pointer" : "default",
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setIsMediaModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{sight.watermark_rd && (
|
||||||
|
<button
|
||||||
|
className="absolute top-2 right-2"
|
||||||
|
onClick={() => {
|
||||||
|
handleChange({
|
||||||
|
watermark_rd: null,
|
||||||
|
});
|
||||||
|
setActiveMenuType(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X color="red" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{sight.watermark_rd ? (
|
||||||
|
<img
|
||||||
|
src={`${import.meta.env.VITE_KRBL_MEDIA}${
|
||||||
|
sight.watermark_rd
|
||||||
|
}/download?token=${token}`}
|
||||||
|
alt="Логотип"
|
||||||
|
style={{ maxWidth: "100%", maxHeight: "100%" }}
|
||||||
|
onClick={() => {
|
||||||
|
setIsPreviewMediaOpen(true);
|
||||||
|
setMediaId(sight.watermark_rd ?? "");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<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 border-gray-300 cursor-pointer hover:bg-gray-100">
|
||||||
|
<p>Перетащите файл</p>
|
||||||
|
</div>
|
||||||
|
<p>или</p>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
setActiveMenuType("watermark_rd");
|
||||||
|
setIsAddMediaOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Выбрать файл
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@ -376,18 +576,6 @@ export const CreateInformationTab = observer(
|
|||||||
onClose={() => setIsPreviewMediaOpen(false)}
|
onClose={() => setIsPreviewMediaOpen(false)}
|
||||||
mediaId={mediaId}
|
mediaId={mediaId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UploadMediaDialog
|
|
||||||
open={isUploadMediaOpen}
|
|
||||||
onClose={() => setIsUploadMediaOpen(false)}
|
|
||||||
afterUpload={(media) => {
|
|
||||||
handleChange({
|
|
||||||
[activeMenuType ?? "thumbnail"]: media.id,
|
|
||||||
});
|
|
||||||
setActiveMenuType(null);
|
|
||||||
setIsUploadMediaOpen(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@ export const CreateLeftTab = observer(
|
|||||||
deleteLeftArticle,
|
deleteLeftArticle,
|
||||||
createLeftArticle,
|
createLeftArticle,
|
||||||
unlinkLeftArticle,
|
unlinkLeftArticle,
|
||||||
createLinkWithLeftArticle,
|
createLinkWithArticle,
|
||||||
} = createSightStore;
|
} = createSightStore;
|
||||||
const {
|
const {
|
||||||
deleteMedia,
|
deleteMedia,
|
||||||
@ -75,10 +75,10 @@ export const CreateLeftTab = observer(
|
|||||||
media_name?: string;
|
media_name?: string;
|
||||||
media_type: number;
|
media_type: number;
|
||||||
}) => {
|
}) => {
|
||||||
await createLinkWithLeftArticle(media);
|
await createLinkWithArticle(media);
|
||||||
setIsSelectMediaDialogOpen(false);
|
setIsSelectMediaDialogOpen(false);
|
||||||
},
|
},
|
||||||
[createLinkWithLeftArticle]
|
[createLinkWithArticle]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleArticleSelect = useCallback(
|
const handleArticleSelect = useCallback(
|
||||||
@ -437,7 +437,7 @@ export const CreateLeftTab = observer(
|
|||||||
afterUpload={async (media) => {
|
afterUpload={async (media) => {
|
||||||
setUploadMediaOpen(false);
|
setUploadMediaOpen(false);
|
||||||
setFileToUpload(null);
|
setFileToUpload(null);
|
||||||
await createLinkWithLeftArticle(media);
|
await createLinkWithArticle(media);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<SelectArticleModal
|
<SelectArticleModal
|
||||||
|
@ -7,185 +7,40 @@ import {
|
|||||||
MenuItem,
|
MenuItem,
|
||||||
TextField,
|
TextField,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import {
|
import { BackButton, createSightStore, languageStore, TabPanel } from "@shared";
|
||||||
BackButton,
|
|
||||||
createSightStore,
|
|
||||||
languageStore,
|
|
||||||
SelectArticleModal,
|
|
||||||
TabPanel,
|
|
||||||
SelectMediaDialog, // Import
|
|
||||||
UploadMediaDialog, // Import
|
|
||||||
} from "@shared";
|
|
||||||
import {
|
import {
|
||||||
LanguageSwitcher,
|
LanguageSwitcher,
|
||||||
MediaArea, // Import
|
|
||||||
MediaAreaForSight, // Import
|
|
||||||
ReactMarkdownComponent,
|
ReactMarkdownComponent,
|
||||||
ReactMarkdownEditor,
|
ReactMarkdownEditor,
|
||||||
} from "@widgets";
|
} from "@widgets";
|
||||||
import { ImagePlus, Plus, X } from "lucide-react"; // Import X
|
import { ImagePlus, Plus } from "lucide-react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useState, useEffect } from "react"; // Added useEffect
|
import { useState } from "react";
|
||||||
import { MediaViewer } from "../../MediaViewer/index";
|
|
||||||
import { toast } from "react-toastify";
|
|
||||||
|
|
||||||
type MediaItemShared = {
|
|
||||||
// Define if not already available from @shared
|
|
||||||
id: string;
|
|
||||||
filename: string;
|
|
||||||
media_name?: string;
|
|
||||||
media_type: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// --- RightWidgetTab (Parent) Component ---
|
||||||
export const CreateRightTab = observer(
|
export const CreateRightTab = observer(
|
||||||
({ value, index }: { value: number; index: number }) => {
|
({ value, index }: { value: number; index: number }) => {
|
||||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
const {
|
const { sight, createNewRightArticle, updateRightArticleInfo } =
|
||||||
sight,
|
createSightStore;
|
||||||
createNewRightArticle,
|
|
||||||
updateRightArticleInfo,
|
|
||||||
linkPreviewMedia,
|
|
||||||
unlinkPreviewMedia,
|
|
||||||
createLinkWithRightArticle,
|
|
||||||
deleteRightArticleMedia,
|
|
||||||
setFileToUpload, // From store
|
|
||||||
setUploadMediaOpen, // From store
|
|
||||||
uploadMediaOpen, // From store
|
|
||||||
unlinkRightAritcle, // Corrected spelling
|
|
||||||
deleteRightArticle,
|
|
||||||
linkExistingRightArticle,
|
|
||||||
createSight,
|
|
||||||
clearCreateSight, // For resetting form
|
|
||||||
} = createSightStore;
|
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
|
|
||||||
const [selectArticleDialogOpen, setSelectArticleDialogOpen] =
|
|
||||||
useState(false);
|
|
||||||
const [activeArticleIndex, setActiveArticleIndex] = useState<number | null>(
|
const [activeArticleIndex, setActiveArticleIndex] = useState<number | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
const [type, setType] = useState<"article" | "media">("media");
|
const open = Boolean(anchorEl);
|
||||||
|
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
const [isSelectMediaDialogOpen, setIsSelectMediaDialogOpen] =
|
|
||||||
useState(false);
|
|
||||||
const [mediaTarget, setMediaTarget] = useState<
|
|
||||||
"sightPreview" | "rightArticle" | null
|
|
||||||
>(null);
|
|
||||||
|
|
||||||
// Reset activeArticleIndex if language changes and index is out of bounds
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
activeArticleIndex !== null &&
|
|
||||||
activeArticleIndex >= sight[language].right.length
|
|
||||||
) {
|
|
||||||
setActiveArticleIndex(null);
|
|
||||||
setType("media"); // Default back to media preview if selected article disappears
|
|
||||||
}
|
|
||||||
}, [language, sight[language].right, activeArticleIndex]);
|
|
||||||
|
|
||||||
const openMenu = Boolean(anchorEl);
|
|
||||||
const handleClickMenu = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
||||||
setAnchorEl(event.currentTarget);
|
setAnchorEl(event.currentTarget);
|
||||||
};
|
};
|
||||||
const handleCloseMenu = () => {
|
const handleClose = () => {
|
||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
};
|
};
|
||||||
|
const handleSave = () => {
|
||||||
const handleSave = async () => {
|
console.log("Saving right widget...");
|
||||||
try {
|
|
||||||
await createSight(language);
|
|
||||||
toast.success("Достопримечательность успешно создана!");
|
|
||||||
clearCreateSight(); // Reset form
|
|
||||||
setActiveArticleIndex(null);
|
|
||||||
setType("media");
|
|
||||||
// Potentially navigate away: history.push('/sights-list');
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to save sight:", error);
|
|
||||||
toast.error("Ошибка при создании достопримечательности.");
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDisplayArticleFromList = (idx: number) => {
|
const handleSelectArticle = (index: number) => {
|
||||||
setActiveArticleIndex(idx);
|
setActiveArticleIndex(index);
|
||||||
setType("article");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCreateNewLocalArticle = async () => {
|
|
||||||
handleCloseMenu();
|
|
||||||
try {
|
|
||||||
const newArticleId = await createNewRightArticle();
|
|
||||||
// Automatically select the new article if ID is returned
|
|
||||||
const newIndex = sight[language].right.findIndex(
|
|
||||||
(a) => a.id === newArticleId
|
|
||||||
);
|
|
||||||
if (newIndex > -1) {
|
|
||||||
setActiveArticleIndex(newIndex);
|
|
||||||
setType("article");
|
|
||||||
} else {
|
|
||||||
// Fallback if findIndex fails (should not happen if store updates correctly)
|
|
||||||
setActiveArticleIndex(sight[language].right.length - 1);
|
|
||||||
setType("article");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
toast.error("Не удалось создать новую статью.");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSelectExistingArticleAndLink = async (
|
|
||||||
selectedArticleId: number
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
await linkExistingRightArticle(selectedArticleId);
|
|
||||||
setSelectArticleDialogOpen(false); // Close dialog
|
|
||||||
const newIndex = sight[language].right.findIndex(
|
|
||||||
(a) => a.id === selectedArticleId
|
|
||||||
);
|
|
||||||
if (newIndex > -1) {
|
|
||||||
setActiveArticleIndex(newIndex);
|
|
||||||
setType("article");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
toast.error("Не удалось привязать существующую статью.");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const currentRightArticle =
|
|
||||||
activeArticleIndex !== null && sight[language].right[activeArticleIndex]
|
|
||||||
? sight[language].right[activeArticleIndex]
|
|
||||||
: null;
|
|
||||||
|
|
||||||
// Media Handling for Dialogs
|
|
||||||
const handleOpenUploadMedia = () => {
|
|
||||||
setUploadMediaOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOpenSelectMediaDialog = (
|
|
||||||
target: "sightPreview" | "rightArticle"
|
|
||||||
) => {
|
|
||||||
setMediaTarget(target);
|
|
||||||
setIsSelectMediaDialogOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMediaSelectedFromDialog = async (media: MediaItemShared) => {
|
|
||||||
setIsSelectMediaDialogOpen(false);
|
|
||||||
if (mediaTarget === "sightPreview") {
|
|
||||||
await linkPreviewMedia(media.id);
|
|
||||||
} else if (mediaTarget === "rightArticle" && currentRightArticle) {
|
|
||||||
await createLinkWithRightArticle(media, currentRightArticle.id);
|
|
||||||
}
|
|
||||||
setMediaTarget(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMediaUploaded = async (media: MediaItemShared) => {
|
|
||||||
// After UploadMediaDialog finishes
|
|
||||||
setUploadMediaOpen(false);
|
|
||||||
setFileToUpload(null);
|
|
||||||
if (mediaTarget === "sightPreview") {
|
|
||||||
linkPreviewMedia(media.id);
|
|
||||||
} else if (mediaTarget === "rightArticle" && currentRightArticle) {
|
|
||||||
await createLinkWithRightArticle(media, currentRightArticle.id);
|
|
||||||
}
|
|
||||||
setMediaTarget(null); // Reset target
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -196,7 +51,7 @@ export const CreateRightTab = observer(
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
minHeight: "calc(100vh - 200px)",
|
minHeight: "calc(100vh - 200px)", // Adjust as needed
|
||||||
gap: 2,
|
gap: 2,
|
||||||
paddingBottom: "70px", // Space for the save button
|
paddingBottom: "70px", // Space for the save button
|
||||||
position: "relative",
|
position: "relative",
|
||||||
@ -205,389 +60,314 @@ export const CreateRightTab = observer(
|
|||||||
<BackButton />
|
<BackButton />
|
||||||
|
|
||||||
<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">
|
||||||
<Box className="flex flex-col gap-3 max-h-[60vh] overflow-y-auto">
|
<Box className="flex flex-col gap-3 max-h-[60vh] overflow-y-auto">
|
||||||
<Box
|
<Box
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setType("media");
|
// setMediaType("preview");
|
||||||
// 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 bg-gray-200 p-4 rounded-2xl cursor-pointer text-sm hover:bg-gray-300 transition-all duration-300"
|
||||||
type === "media"
|
|
||||||
? "bg-green-300 font-semibold"
|
|
||||||
: "bg-green-200"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<Typography>Предпросмотр медиа</Typography>
|
<Typography>Предпросмотр медиа</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{sight[language].right.map((article, artIdx) => (
|
{sight[language].right.map((article, index) => (
|
||||||
<Box
|
<Box
|
||||||
key={article.id || artIdx} // article.id should be preferred
|
key={index}
|
||||||
className={`w-full p-4 rounded-2xl text-sm cursor-pointer hover:bg-gray-300 transition-all duration-300 ${
|
className="w-full bg-gray-200 p-4 rounded-2xl text-sm cursor-pointer hover:bg-gray-300 transition-all duration-300"
|
||||||
type === "article" && activeArticleIndex === artIdx
|
onClick={() => {
|
||||||
? "bg-blue-300 font-semibold"
|
handleSelectArticle(index);
|
||||||
: "bg-gray-200"
|
}}
|
||||||
}`}
|
|
||||||
onClick={() => handleDisplayArticleFromList(artIdx)}
|
|
||||||
>
|
>
|
||||||
<Typography noWrap title={article.heading}>
|
<Typography>{article.heading}</Typography>
|
||||||
{article.heading || "Без названия"}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
<button
|
<button
|
||||||
className="w-10 h-10 bg-blue-500 rounded-full absolute bottom-5 left-5 flex items-center justify-center hover:bg-blue-600"
|
className="w-10 h-10 bg-blue-500 rounded-full absolute bottom-5 left-5 flex items-center justify-center"
|
||||||
onClick={handleClickMenu}
|
onClick={handleClick}
|
||||||
aria-controls={openMenu ? "add-article-menu" : undefined}
|
|
||||||
aria-haspopup="true"
|
|
||||||
aria-expanded={openMenu ? "true" : undefined}
|
|
||||||
>
|
>
|
||||||
<Plus size={20} color="white" />
|
<Plus size={20} color="white" />
|
||||||
</button>
|
</button>
|
||||||
<Menu
|
<Menu
|
||||||
id="add-article-menu"
|
id="basic-menu"
|
||||||
anchorEl={anchorEl}
|
anchorEl={anchorEl}
|
||||||
open={openMenu}
|
open={open}
|
||||||
onClose={handleCloseMenu}
|
onClose={handleClose}
|
||||||
MenuListProps={{ "aria-labelledby": "basic-button" }}
|
MenuListProps={{
|
||||||
sx={{ mt: 1 }}
|
"aria-labelledby": "basic-button",
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
mt: 1,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<MenuItem onClick={handleCreateNewLocalArticle}>
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
createNewRightArticle();
|
||||||
|
handleClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Typography>Создать новую</Typography>
|
<Typography>Создать новую</Typography>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectArticleDialogOpen(true);
|
handleClose();
|
||||||
handleCloseMenu();
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography>Выбрать существующую статью</Typography>
|
<Typography>Выбрать существующую статью</Typography>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
</Box>
|
</Box>
|
||||||
|
<Box className="w-[80%] h-[70vh] border border-gray-300 rounded-2xl p-3">
|
||||||
|
{activeArticleIndex !== null && (
|
||||||
|
<>
|
||||||
|
<Box className="flex justify-end gap-2 mb-3">
|
||||||
|
<Button variant="contained" color="primary">
|
||||||
|
Открепить
|
||||||
|
</Button>
|
||||||
|
|
||||||
{/* Main content area: Article Editor or Sight Media Preview */}
|
<Button variant="contained" color="success">
|
||||||
{type === "article" && currentRightArticle ? (
|
Удалить
|
||||||
<Box className="w-[80%] border border-gray-300 rounded-2xl p-3 flex flex-col gap-2 overflow-hidden">
|
</Button>
|
||||||
<Box className="flex justify-end gap-2 mb-1 flex-shrink-0">
|
</Box>
|
||||||
<Button
|
<Box sx={{ display: "flex", gap: 3, flexGrow: 1 }}>
|
||||||
variant="outlined"
|
{/* Левая колонка: Редактирование */}
|
||||||
color="warning"
|
|
||||||
size="small"
|
<Box
|
||||||
onClick={() => {
|
sx={{
|
||||||
if (currentRightArticle) {
|
flex: 2,
|
||||||
unlinkRightAritcle(currentRightArticle.id); // Corrected function name
|
display: "flex",
|
||||||
setActiveArticleIndex(null);
|
flexDirection: "column",
|
||||||
setType("media");
|
gap: 2,
|
||||||
}
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<TextField
|
||||||
Открепить
|
label="Название информации"
|
||||||
</Button>
|
value={
|
||||||
<Button
|
sight[language].right[activeArticleIndex].heading
|
||||||
variant="contained"
|
|
||||||
color="error"
|
|
||||||
size="small"
|
|
||||||
onClick={async () => {
|
|
||||||
if (
|
|
||||||
currentRightArticle &&
|
|
||||||
window.confirm(
|
|
||||||
`Удалить статью "${currentRightArticle.heading}" окончательно?`
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
await deleteRightArticle(currentRightArticle.id);
|
|
||||||
setActiveArticleIndex(null);
|
|
||||||
setType("media");
|
|
||||||
toast.success("Статья удалена");
|
|
||||||
} catch {
|
|
||||||
toast.error("Не удалось удалить статью");
|
|
||||||
}
|
}
|
||||||
}
|
onChange={(e) =>
|
||||||
}}
|
updateRightArticleInfo(
|
||||||
>
|
activeArticleIndex,
|
||||||
Удалить
|
language,
|
||||||
</Button>
|
e.target.value,
|
||||||
</Box>
|
sight[language].right[activeArticleIndex].body
|
||||||
<Box
|
)
|
||||||
sx={{
|
}
|
||||||
flexGrow: 1,
|
variant="outlined"
|
||||||
display: "flex",
|
fullWidth
|
||||||
flexDirection: "column",
|
/>
|
||||||
gap: 2,
|
|
||||||
overflowY: "auto",
|
<ReactMarkdownEditor
|
||||||
pt: 7,
|
value={
|
||||||
}}
|
sight[language].right[activeArticleIndex].body
|
||||||
>
|
}
|
||||||
<TextField
|
onChange={(value) =>
|
||||||
label="Название информации (правый виджет)"
|
updateRightArticleInfo(
|
||||||
value={currentRightArticle.heading}
|
activeArticleIndex,
|
||||||
onChange={(e) =>
|
language,
|
||||||
activeArticleIndex !== null &&
|
sight[language].right[activeArticleIndex]
|
||||||
updateRightArticleInfo(
|
.heading,
|
||||||
activeArticleIndex,
|
value
|
||||||
language,
|
)
|
||||||
e.target.value,
|
}
|
||||||
currentRightArticle.body
|
/>
|
||||||
)
|
</Box>
|
||||||
}
|
{/* Блок МЕДИА для статьи */}
|
||||||
variant="outlined"
|
{/* <Paper elevation={1} sx={{ padding: 2, mt: 1 }}>
|
||||||
fullWidth
|
<Typography variant="h6" gutterBottom>
|
||||||
/>
|
МЕДИА
|
||||||
<Box sx={{ minHeight: 200, flexGrow: 1 }}>
|
</Typography>
|
||||||
<ReactMarkdownEditor
|
{data.left.media ? (
|
||||||
value={currentRightArticle.body}
|
<Box sx={{ mb: 1 }}>
|
||||||
onChange={(mdValue) =>
|
<img
|
||||||
activeArticleIndex !== null &&
|
src={data.left.media.filename}
|
||||||
updateRightArticleInfo(
|
alt="Selected media"
|
||||||
activeArticleIndex,
|
style={{
|
||||||
language,
|
maxWidth: "100%",
|
||||||
currentRightArticle.heading,
|
maxHeight: "150px",
|
||||||
mdValue || ""
|
objectFit: "contain",
|
||||||
)
|
}}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<MediaArea
|
) : (
|
||||||
articleId={currentRightArticle.id} // Needs a real ID
|
<Box
|
||||||
mediaIds={currentRightArticle.media || []}
|
sx={{
|
||||||
onFilesDrop={(files) => {
|
width: "100%",
|
||||||
if (files.length > 0) {
|
height: 100,
|
||||||
setFileToUpload(files[0]);
|
backgroundColor: "grey.100",
|
||||||
setMediaTarget("rightArticle");
|
display: "flex",
|
||||||
handleOpenUploadMedia();
|
alignItems: "center",
|
||||||
}
|
justifyContent: "center",
|
||||||
|
borderRadius: 1,
|
||||||
|
mb: 1,
|
||||||
|
border: "2px dashed",
|
||||||
|
borderColor: "grey.300",
|
||||||
}}
|
}}
|
||||||
deleteMedia={deleteRightArticleMedia}
|
>
|
||||||
setSelectMediaDialogOpen={() =>
|
<Typography color="text.secondary">Нет медиа</Typography>
|
||||||
handleOpenSelectMediaDialog("rightArticle")
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
) : type === "media" ? (
|
|
||||||
<Box className="w-[80%] h-[70vh] border border-gray-300 rounded-2xl p-3 relative flex justify-center items-center">
|
|
||||||
{type === "media" && (
|
|
||||||
<Box className="w-[80%] border border-gray-300 rounded-2xl relative">
|
|
||||||
{sight.preview_media && (
|
|
||||||
<>
|
|
||||||
<Box className="absolute top-4 right-4">
|
|
||||||
<button
|
|
||||||
className="w-10 h-10 flex items-center justify-center"
|
|
||||||
onClick={unlinkPreviewMedia}
|
|
||||||
>
|
|
||||||
<X size={20} color="red" />
|
|
||||||
</button>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<MediaViewer
|
|
||||||
media={{
|
|
||||||
id: sight.preview_media || "",
|
|
||||||
media_type: 1,
|
|
||||||
filename: sight.preview_media || "",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!sight.preview_media && (
|
|
||||||
<MediaAreaForSight
|
|
||||||
onFinishUpload={(mediaId) => {
|
|
||||||
linkPreviewMedia(mediaId);
|
|
||||||
}}
|
|
||||||
onFilesDrop={() => {}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
<Button
|
||||||
) : (
|
variant="contained"
|
||||||
<Box className="w-[80%] h-[70vh] border border-gray-300 rounded-2xl p-3 flex justify-center items-center">
|
startIcon={<ImagePlus size={18} />}
|
||||||
<Typography variant="h6" color="text.secondary">
|
onClick={handleOpenMediaDialog}
|
||||||
Выберите статью слева или секцию "Предпросмотр медиа"
|
>
|
||||||
</Typography>
|
Выбрать/Загрузить медиа
|
||||||
</Box>
|
</Button>
|
||||||
)}
|
{data.left.media && (
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
|
size="small"
|
||||||
|
sx={{ ml: 1 }}
|
||||||
|
onClick={() =>
|
||||||
|
updateSightInfo(
|
||||||
|
languageStore.language,
|
||||||
|
{
|
||||||
|
left: {
|
||||||
|
heading: data.left.heading,
|
||||||
|
body: data.left.body,
|
||||||
|
media: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Удалить медиа
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Paper> */}
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Right Column: Live Preview */}
|
|
||||||
<Box className="w-[25%] mr-10">
|
<Box className="w-[25%] mr-10">
|
||||||
{type === "article" && currentRightArticle && (
|
{activeArticleIndex !== null && (
|
||||||
<Paper
|
<Paper
|
||||||
className="flex-1 flex flex-col rounded-2xl"
|
className="flex-1 flex flex-col rounded-2xl"
|
||||||
elevation={2}
|
elevation={2}
|
||||||
sx={{ height: "75vh", overflow: "hidden" }}
|
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
className="rounded-2xl overflow-hidden"
|
className="rounded-2xl overflow-hidden"
|
||||||
sx={{
|
sx={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "75vh",
|
||||||
background: "#877361", // Theme background
|
background: "#877361",
|
||||||
|
borderColor: "grey.300",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{currentRightArticle.media &&
|
{false ? (
|
||||||
currentRightArticle.media.length > 0 ? (
|
|
||||||
<MediaViewer media={currentRightArticle.media[0]} />
|
|
||||||
) : (
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: 200,
|
height: "100%",
|
||||||
flexShrink: 0,
|
|
||||||
backgroundColor: "rgba(0,0,0,0.1)",
|
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ImagePlus size={48} color="white" />
|
<Typography color="white">Загрузка...</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
) : (
|
||||||
<Box
|
<>
|
||||||
sx={{
|
<Box
|
||||||
width: "100%",
|
sx={{
|
||||||
minHeight: "70px", // Fixed height for heading container
|
width: "100%",
|
||||||
background: "#877361", // Consistent with theme
|
height: 200,
|
||||||
display: "flex",
|
flexShrink: 0,
|
||||||
flexShrink: 0,
|
backgroundColor: "rgba(0,0,0,0.1)",
|
||||||
flex: 1,
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
borderBottom: "1px solid rgba(255,255,255,0.1)",
|
justifyContent: "center",
|
||||||
px: 2,
|
}}
|
||||||
py: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
variant="h6"
|
|
||||||
color="white"
|
|
||||||
noWrap
|
|
||||||
title={currentRightArticle.heading}
|
|
||||||
>
|
|
||||||
{currentRightArticle.heading || "Заголовок"}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
px: 2,
|
|
||||||
py: 1,
|
|
||||||
flexGrow: 1,
|
|
||||||
overflowY: "auto",
|
|
||||||
backgroundColor: "#877361",
|
|
||||||
color: "white",
|
|
||||||
"&::-webkit-scrollbar": { width: "8px" },
|
|
||||||
"&::-webkit-scrollbar-thumb": {
|
|
||||||
backgroundColor: "rgba(255,255,255,0.3)",
|
|
||||||
borderRadius: "4px",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{currentRightArticle.body ? (
|
|
||||||
<ReactMarkdownComponent
|
|
||||||
value={currentRightArticle.body}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Typography
|
|
||||||
color="rgba(255,255,255,0.7)"
|
|
||||||
sx={{ textAlign: "center", mt: 4 }}
|
|
||||||
>
|
>
|
||||||
Содержимое статьи...
|
<ImagePlus size={48} color="white" />
|
||||||
</Typography>
|
</Box>
|
||||||
)}
|
|
||||||
</Box>
|
<Box
|
||||||
</Box>
|
sx={{
|
||||||
</Paper>
|
width: "100%",
|
||||||
)}
|
minHeight: "70px",
|
||||||
{/* Optional: Preview for sight.preview_media when type === "media" */}
|
background: "#877361",
|
||||||
{type === "media" && sight.preview_media && (
|
display: "flex",
|
||||||
<Paper
|
flexShrink: 0,
|
||||||
className="flex-1 flex flex-col rounded-2xl"
|
alignItems: "center",
|
||||||
elevation={2}
|
borderBottom: "1px solid rgba(255,255,255,0.1)",
|
||||||
sx={{ height: "75vh", overflow: "hidden" }}
|
px: 2,
|
||||||
>
|
}}
|
||||||
<Box
|
>
|
||||||
sx={{
|
<Typography variant="h6" color="white">
|
||||||
width: "100%",
|
{sight[language].right[activeArticleIndex]
|
||||||
height: "100%",
|
.heading || "Выберите статью"}
|
||||||
background: "#877361",
|
</Typography>
|
||||||
display: "flex",
|
</Box>
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
<Box
|
||||||
}}
|
sx={{
|
||||||
>
|
px: 2,
|
||||||
<MediaViewer
|
flexGrow: 1,
|
||||||
media={{
|
|
||||||
id: sight.preview_media,
|
overflowY: "auto",
|
||||||
filename: sight.preview_media,
|
backgroundColor: "#877361",
|
||||||
media_type: 1,
|
color: "white",
|
||||||
}}
|
py: 1,
|
||||||
/>
|
}}
|
||||||
|
>
|
||||||
|
{sight[language].right[activeArticleIndex].body ? (
|
||||||
|
<ReactMarkdownComponent
|
||||||
|
value={
|
||||||
|
sight[language].right[activeArticleIndex].body
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Typography
|
||||||
|
color="rgba(255,255,255,0.7)"
|
||||||
|
sx={{ textAlign: "center", mt: 4 }}
|
||||||
|
>
|
||||||
|
Предпросмотр статьи появится здесь
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Sticky Save Button Footer */}
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
left: 0, // ensure it spans from left
|
|
||||||
right: 0,
|
right: 0,
|
||||||
padding: 2,
|
padding: 2,
|
||||||
backgroundColor: "background.paper",
|
backgroundColor: "background.paper", // Ensure button is visible
|
||||||
borderTop: "1px solid", // Add a subtle top border
|
width: "100%", // Cover the full width to make it a sticky footer
|
||||||
borderColor: "divider", // Use theme's divider color
|
|
||||||
width: "100%",
|
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "flex-end",
|
justifyContent: "flex-end",
|
||||||
boxShadow: "0 -2px 5px rgba(0,0,0,0.1)", // Optional shadow
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button variant="contained" color="success" onClick={handleSave}>
|
||||||
variant="contained"
|
Сохранить изменения
|
||||||
color="success"
|
|
||||||
onClick={handleSave}
|
|
||||||
size="large"
|
|
||||||
>
|
|
||||||
Сохранить достопримечательность
|
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
{/*
|
||||||
{/* Modals */}
|
|
||||||
<SelectArticleModal
|
<SelectArticleModal
|
||||||
open={selectArticleDialogOpen}
|
open={openedType === "article"}
|
||||||
onClose={() => setSelectArticleDialogOpen(false)}
|
onClose={handleCloseSelectModal}
|
||||||
onSelectArticle={handleSelectExistingArticleAndLink}
|
onSelectArticle={handleSelectArticle}
|
||||||
// Pass IDs of already linked/added right articles to exclude them from selection
|
linkedArticleIds={linkedArticleIds}
|
||||||
linkedArticleIds={sight[language].right.map((article) => article.id)}
|
/> */}
|
||||||
/>
|
|
||||||
<UploadMediaDialog
|
|
||||||
open={uploadMediaOpen} // From store
|
|
||||||
onClose={() => {
|
|
||||||
setUploadMediaOpen(false);
|
|
||||||
setFileToUpload(null); // Clear file if dialog is closed without upload
|
|
||||||
setMediaTarget(null);
|
|
||||||
}}
|
|
||||||
afterUpload={handleMediaUploaded} // This will use the mediaTarget
|
|
||||||
/>
|
|
||||||
<SelectMediaDialog
|
|
||||||
open={isSelectMediaDialogOpen}
|
|
||||||
onClose={() => {
|
|
||||||
setIsSelectMediaDialogOpen(false);
|
|
||||||
setMediaTarget(null);
|
|
||||||
}}
|
|
||||||
onSelectMedia={handleMediaSelectedFromDialog}
|
|
||||||
/>
|
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,9 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
Box,
|
Box,
|
||||||
Autocomplete,
|
Autocomplete,
|
||||||
|
Typography,
|
||||||
|
Paper,
|
||||||
|
Tooltip,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Menu as MuiMenu,
|
Menu as MuiMenu,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
@ -17,9 +20,9 @@ import {
|
|||||||
PreviewMediaDialog,
|
PreviewMediaDialog,
|
||||||
SightLanguageInfo,
|
SightLanguageInfo,
|
||||||
SightCommonInfo,
|
SightCommonInfo,
|
||||||
UploadMediaDialog,
|
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
import { ImageUploadCard, LanguageSwitcher } from "@widgets";
|
import { LanguageSwitcher } from "@widgets";
|
||||||
|
import { Info, ImagePlus } from "lucide-react";
|
||||||
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
@ -31,10 +34,10 @@ import { toast } from "react-toastify";
|
|||||||
export const InformationTab = observer(
|
export const InformationTab = observer(
|
||||||
({ value, index }: { value: number; index: number }) => {
|
({ value, index }: { value: number; index: number }) => {
|
||||||
const { cities } = cityStore;
|
const { cities } = cityStore;
|
||||||
|
const [, setIsMediaModalOpen] = useState(false);
|
||||||
const [mediaId, setMediaId] = useState<string>("");
|
const [mediaId, setMediaId] = useState<string>("");
|
||||||
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
|
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
|
||||||
const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false);
|
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
|
|
||||||
const { sight, updateSightInfo, updateSight } = editSightStore;
|
const { sight, updateSightInfo, updateSight } = editSightStore;
|
||||||
@ -42,6 +45,8 @@ export const InformationTab = observer(
|
|||||||
const [, setCity] = useState<number>(sight.common.city_id ?? 0);
|
const [, setCity] = useState<number>(sight.common.city_id ?? 0);
|
||||||
const [coordinates, setCoordinates] = useState<string>(`0 0`);
|
const [coordinates, setCoordinates] = useState<string>(`0 0`);
|
||||||
|
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
// Menu state for each media button
|
// Menu state for each media button
|
||||||
const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null);
|
const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
const [activeMenuType, setActiveMenuType] = useState<
|
const [activeMenuType, setActiveMenuType] = useState<
|
||||||
@ -71,22 +76,12 @@ export const InformationTab = observer(
|
|||||||
handleMenuClose();
|
handleMenuClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMediaSelect = (media: {
|
const handleMediaSelect = () => {
|
||||||
id: string;
|
|
||||||
filename: string;
|
|
||||||
media_name?: string;
|
|
||||||
media_type: number;
|
|
||||||
}) => {
|
|
||||||
if (!activeMenuType) return;
|
if (!activeMenuType) return;
|
||||||
handleChange(
|
|
||||||
language as Language,
|
// Close the dialog
|
||||||
{
|
setIsAddMediaOpen(false);
|
||||||
[activeMenuType ?? "thumbnail"]: media.id,
|
|
||||||
},
|
|
||||||
true
|
|
||||||
);
|
|
||||||
setActiveMenuType(null);
|
setActiveMenuType(null);
|
||||||
setIsUploadMediaOpen(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (
|
const handleChange = (
|
||||||
@ -230,87 +225,207 @@ export const InformationTab = observer(
|
|||||||
flexDirection: { xs: "column", sm: "row" }, // Stack on extra small, side-by-side on small and up
|
flexDirection: { xs: "column", sm: "row" }, // Stack on extra small, side-by-side on small and up
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ImageUploadCard
|
<Paper
|
||||||
title="Логотип"
|
elevation={2}
|
||||||
imageKey="thumbnail"
|
sx={{
|
||||||
imageUrl={sight.common.thumbnail}
|
padding: 2,
|
||||||
onImageClick={() => {
|
display: "flex",
|
||||||
setIsPreviewMediaOpen(true);
|
flexDirection: "column",
|
||||||
setMediaId(sight.common.thumbnail ?? "");
|
alignItems: "center",
|
||||||
|
gap: 1,
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 150, // Ensure a minimum width
|
||||||
}}
|
}}
|
||||||
onDeleteImageClick={() => {
|
>
|
||||||
handleChange(
|
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||||
language as Language,
|
<Typography
|
||||||
{
|
variant="subtitle2"
|
||||||
thumbnail: null,
|
gutterBottom
|
||||||
|
sx={{ mb: 0, mr: 0.5 }}
|
||||||
|
>
|
||||||
|
Логотип
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "relative",
|
||||||
|
width: "200px",
|
||||||
|
height: "200px",
|
||||||
|
backgroundColor: "grey.200",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
borderRadius: 1,
|
||||||
|
mb: 1,
|
||||||
|
cursor: sight.common.thumbnail ? "pointer" : "default",
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: sight.common.thumbnail
|
||||||
|
? "red.300"
|
||||||
|
: "grey.200",
|
||||||
},
|
},
|
||||||
true
|
}}
|
||||||
);
|
onClick={() => {
|
||||||
setActiveMenuType(null);
|
setIsMediaModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{sight.common.thumbnail ? (
|
||||||
|
<img
|
||||||
|
src={`${import.meta.env.VITE_KRBL_MEDIA}${
|
||||||
|
sight.common.thumbnail
|
||||||
|
}/download?token=${token}`}
|
||||||
|
alt="Логотип"
|
||||||
|
style={{ maxWidth: "100%", maxHeight: "100%" }}
|
||||||
|
onClick={() => {
|
||||||
|
setIsPreviewMediaOpen(true);
|
||||||
|
setMediaId(sight.common.thumbnail ?? "");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ImagePlus size={24} color="grey" />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
<Paper
|
||||||
|
elevation={2}
|
||||||
|
sx={{
|
||||||
|
padding: 2,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 1,
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 150, // Ensure a minimum width
|
||||||
}}
|
}}
|
||||||
onSelectFileClick={() => {
|
>
|
||||||
setActiveMenuType("thumbnail");
|
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||||
setIsAddMediaOpen(true);
|
<Typography
|
||||||
}}
|
variant="subtitle2"
|
||||||
setUploadMediaOpen={() => {
|
gutterBottom
|
||||||
setIsUploadMediaOpen(true);
|
sx={{ mb: 0, mr: 0.5 }}
|
||||||
setActiveMenuType("thumbnail");
|
>
|
||||||
}}
|
Водяной знак (л.в)
|
||||||
/>
|
</Typography>
|
||||||
<ImageUploadCard
|
<Tooltip title={"asf"}>
|
||||||
title="Водяной знак (л.в)"
|
<Info
|
||||||
imageKey="watermark_lu"
|
size={16}
|
||||||
imageUrl={sight.common.watermark_lu}
|
color="gray"
|
||||||
onImageClick={() => {
|
style={{ cursor: "pointer" }}
|
||||||
setIsPreviewMediaOpen(true);
|
/>
|
||||||
setMediaId(sight.common.watermark_lu ?? "");
|
</Tooltip>
|
||||||
}}
|
</Box>
|
||||||
onDeleteImageClick={() => {
|
<Box
|
||||||
handleChange(
|
sx={{
|
||||||
language as Language,
|
position: "relative",
|
||||||
{
|
width: "200px",
|
||||||
watermark_lu: null,
|
height: "200px",
|
||||||
|
backgroundColor: "grey.200",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
borderRadius: 1,
|
||||||
|
mb: 1,
|
||||||
|
cursor: sight.common.watermark_lu
|
||||||
|
? "pointer"
|
||||||
|
: "default",
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: sight.common.watermark_lu
|
||||||
|
? "grey.300"
|
||||||
|
: "grey.200",
|
||||||
},
|
},
|
||||||
true
|
}}
|
||||||
);
|
onClick={() => {
|
||||||
setActiveMenuType(null);
|
setIsPreviewMediaOpen(true);
|
||||||
|
setMediaId(sight.common.watermark_lu ?? "");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{sight.common.watermark_lu ? (
|
||||||
|
<img
|
||||||
|
src={`${import.meta.env.VITE_KRBL_MEDIA}${
|
||||||
|
sight.common.watermark_lu
|
||||||
|
}/download?token=${token}`}
|
||||||
|
alt="Знак л.в"
|
||||||
|
style={{ maxWidth: "100%", maxHeight: "100%" }}
|
||||||
|
onClick={() => {
|
||||||
|
setIsMediaModalOpen(true);
|
||||||
|
setMediaId(sight.common.watermark_lu ?? "");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ImagePlus size={24} color="grey" />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
elevation={2}
|
||||||
|
sx={{
|
||||||
|
padding: 2,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 1,
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 150, // Ensure a minimum width
|
||||||
}}
|
}}
|
||||||
onSelectFileClick={() => {
|
>
|
||||||
setActiveMenuType("watermark_lu");
|
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||||
setIsAddMediaOpen(true);
|
<Typography
|
||||||
}}
|
variant="subtitle2"
|
||||||
setUploadMediaOpen={() => {
|
gutterBottom
|
||||||
setIsUploadMediaOpen(true);
|
sx={{ mb: 0, mr: 0.5 }}
|
||||||
setActiveMenuType("watermark_lu");
|
>
|
||||||
}}
|
Водяной знак (п.в)
|
||||||
/>
|
</Typography>
|
||||||
<ImageUploadCard
|
<Tooltip title={"asfaf"}>
|
||||||
title="Водяной знак (п.в)"
|
<Info
|
||||||
imageKey="watermark_rd"
|
size={16}
|
||||||
imageUrl={sight.common.watermark_rd}
|
color="gray"
|
||||||
onImageClick={() => {
|
style={{ cursor: "pointer" }}
|
||||||
setIsPreviewMediaOpen(true);
|
/>
|
||||||
setMediaId(sight.common.watermark_rd ?? "");
|
</Tooltip>
|
||||||
}}
|
</Box>
|
||||||
onDeleteImageClick={() => {
|
<Box
|
||||||
handleChange(
|
sx={{
|
||||||
language as Language,
|
position: "relative",
|
||||||
{
|
width: "200px",
|
||||||
watermark_rd: null,
|
height: "200px",
|
||||||
|
backgroundColor: "grey.200",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
borderRadius: 1,
|
||||||
|
mb: 1,
|
||||||
|
cursor: sight.common.watermark_rd
|
||||||
|
? "pointer"
|
||||||
|
: "default",
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: sight.common.watermark_rd
|
||||||
|
? "grey.300"
|
||||||
|
: "grey.200",
|
||||||
},
|
},
|
||||||
true
|
}}
|
||||||
);
|
onClick={() => {
|
||||||
setActiveMenuType(null);
|
setIsMediaModalOpen(true);
|
||||||
}}
|
setMediaId(sight.common.watermark_rd ?? "");
|
||||||
onSelectFileClick={() => {
|
}}
|
||||||
setActiveMenuType("watermark_rd");
|
>
|
||||||
setIsAddMediaOpen(true);
|
{sight.common.watermark_rd ? (
|
||||||
}}
|
<img
|
||||||
setUploadMediaOpen={() => {
|
src={`${import.meta.env.VITE_KRBL_MEDIA}${
|
||||||
setIsUploadMediaOpen(true);
|
sight.common.watermark_rd
|
||||||
setActiveMenuType("watermark_rd");
|
}/download?token=${token}`}
|
||||||
}}
|
alt="Знак п.в"
|
||||||
/>
|
style={{ maxWidth: "100%", maxHeight: "100%" }}
|
||||||
|
onClick={() => {
|
||||||
|
setIsPreviewMediaOpen(true);
|
||||||
|
setMediaId(sight.common.watermark_rd ?? "");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ImagePlus size={24} color="grey" />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@ -373,21 +488,6 @@ export const InformationTab = observer(
|
|||||||
onSelectMedia={handleMediaSelect}
|
onSelectMedia={handleMediaSelect}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UploadMediaDialog
|
|
||||||
open={isUploadMediaOpen}
|
|
||||||
onClose={() => setIsUploadMediaOpen(false)}
|
|
||||||
afterUpload={(media) => {
|
|
||||||
handleChange(
|
|
||||||
language as Language,
|
|
||||||
{
|
|
||||||
[activeMenuType ?? "thumbnail"]: media.id,
|
|
||||||
},
|
|
||||||
true
|
|
||||||
);
|
|
||||||
setActiveMenuType(null);
|
|
||||||
setIsUploadMediaOpen(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<PreviewMediaDialog
|
<PreviewMediaDialog
|
||||||
open={isPreviewMediaOpen}
|
open={isPreviewMediaOpen}
|
||||||
onClose={() => setIsPreviewMediaOpen(false)}
|
onClose={() => setIsPreviewMediaOpen(false)}
|
||||||
|
@ -9,66 +9,41 @@ import {
|
|||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import {
|
import {
|
||||||
BackButton,
|
BackButton,
|
||||||
|
createSightStore,
|
||||||
editSightStore,
|
editSightStore,
|
||||||
languageStore,
|
languageStore,
|
||||||
SelectArticleModal,
|
SelectArticleModal,
|
||||||
SelectMediaDialog,
|
|
||||||
TabPanel,
|
TabPanel,
|
||||||
UploadMediaDialog,
|
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
import {
|
import {
|
||||||
LanguageSwitcher,
|
LanguageSwitcher,
|
||||||
MediaArea,
|
|
||||||
MediaAreaForSight,
|
|
||||||
ReactMarkdownComponent,
|
ReactMarkdownComponent,
|
||||||
ReactMarkdownEditor,
|
ReactMarkdownEditor,
|
||||||
} from "@widgets";
|
} from "@widgets";
|
||||||
import { ImagePlus, Plus, X } from "lucide-react";
|
import { ImagePlus, Plus } from "lucide-react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { MediaViewer } from "../../MediaViewer/index";
|
|
||||||
|
|
||||||
export const RightWidgetTab = observer(
|
export const RightWidgetTab = observer(
|
||||||
({ value, index }: { value: number; index: number }) => {
|
({ value, index }: { value: number; index: number }) => {
|
||||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
|
const { createNewRightArticle, updateRightArticleInfo } = createSightStore;
|
||||||
const {
|
const { sight, getRightArticles, updateSight } = editSightStore;
|
||||||
sight,
|
|
||||||
updateRightArticleInfo,
|
|
||||||
getRightArticles,
|
|
||||||
updateSight,
|
|
||||||
unlinkPreviewMedia,
|
|
||||||
linkPreviewMedia,
|
|
||||||
unlinkRightArticle,
|
|
||||||
deleteRightArticle,
|
|
||||||
linkArticle,
|
|
||||||
deleteRightArticleMedia,
|
|
||||||
createLinkWithRightArticle,
|
|
||||||
setFileToUpload,
|
|
||||||
createNewRightArticle,
|
|
||||||
} = editSightStore;
|
|
||||||
|
|
||||||
const [uploadMediaOpen, setUploadMediaOpen] = useState(false);
|
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
const [type, setType] = useState<"article" | "media">("media");
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
if (sight.common.id) {
|
||||||
if (sight.common.id) {
|
getRightArticles(sight.common.id);
|
||||||
await getRightArticles(sight.common.id);
|
}
|
||||||
}
|
|
||||||
};
|
|
||||||
fetchData();
|
|
||||||
console.log(sight[language].right);
|
|
||||||
}, [sight.common.id]);
|
}, [sight.common.id]);
|
||||||
|
|
||||||
const [activeArticleIndex, setActiveArticleIndex] = useState<number | null>(
|
const [activeArticleIndex, setActiveArticleIndex] = useState<number | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
const [isSelectModalOpen, setIsSelectModalOpen] = useState(false);
|
const [isSelectModalOpen, setIsSelectModalOpen] = useState(false);
|
||||||
const [isSelectMediaModalOpen, setIsSelectMediaModalOpen] = useState(false);
|
|
||||||
const open = Boolean(anchorEl);
|
|
||||||
|
|
||||||
|
const open = Boolean(anchorEl);
|
||||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
setAnchorEl(event.currentTarget);
|
setAnchorEl(event.currentTarget);
|
||||||
};
|
};
|
||||||
@ -94,32 +69,16 @@ export const RightWidgetTab = observer(
|
|||||||
setIsSelectModalOpen(false);
|
setIsSelectModalOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleArticleSelect = (id: number) => {
|
const handleArticleSelect = () => {
|
||||||
linkArticle(id);
|
// TODO: Implement article selection logic
|
||||||
handleCloseSelectModal();
|
handleCloseSelectModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMediaSelected = async (media: {
|
|
||||||
id: string;
|
|
||||||
filename: string;
|
|
||||||
media_name?: string;
|
|
||||||
media_type: number;
|
|
||||||
}) => {
|
|
||||||
await createLinkWithRightArticle(
|
|
||||||
media,
|
|
||||||
sight[language].right[activeArticleIndex || 0].id
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
await updateSight();
|
await updateSight();
|
||||||
toast.success("Достопримечательность сохранена");
|
toast.success("Достопримечательность сохранена");
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log(sight[language].right);
|
|
||||||
}, [sight[language].right]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabPanel value={value} index={index}>
|
<TabPanel value={value} index={index}>
|
||||||
<LanguageSwitcher />
|
<LanguageSwitcher />
|
||||||
@ -140,26 +99,23 @@ export const RightWidgetTab = observer(
|
|||||||
<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">
|
||||||
<Box className="flex flex-col gap-3 max-h-[60vh] overflow-y-auto ">
|
<Box className="flex flex-col gap-3 max-h-[60vh] overflow-y-auto">
|
||||||
<Box
|
<Box
|
||||||
onClick={() => setType("media")}
|
// onClick={() => setMediaType("preview")}
|
||||||
className="w-full bg-green-200 p-4 rounded-2xl cursor-pointer text-sm hover:bg-gray-300 transition-all duration-300"
|
className="w-full bg-gray-200 p-4 rounded-2xl cursor-pointer text-sm hover:bg-gray-300 transition-all duration-300"
|
||||||
>
|
>
|
||||||
<Typography>Предпросмотр медиа</Typography>
|
<Typography>Предпросмотр медиа</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
{sight[language].right.length > 0 &&
|
|
||||||
sight[language].right.map((article, index) => (
|
{sight[language].right.map((article, index) => (
|
||||||
<Box
|
<Box
|
||||||
key={index}
|
key={index}
|
||||||
className="w-full bg-gray-200 p-4 rounded-2xl text-sm cursor-pointer hover:bg-gray-300 transition-all duration-300"
|
className="w-full bg-gray-200 p-4 rounded-2xl text-sm cursor-pointer hover:bg-gray-300 transition-all duration-300"
|
||||||
onClick={() => {
|
onClick={() => handleSelectArticle(index)}
|
||||||
handleSelectArticle(index);
|
>
|
||||||
setType("article");
|
<Typography>{article.heading}</Typography>
|
||||||
}}
|
</Box>
|
||||||
>
|
))}
|
||||||
<Typography>{article.heading}</Typography>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</Box>
|
</Box>
|
||||||
<button
|
<button
|
||||||
className="w-10 h-10 bg-blue-500 rounded-full absolute bottom-5 left-5 flex items-center justify-center"
|
className="w-10 h-10 bg-blue-500 rounded-full absolute bottom-5 left-5 flex items-center justify-center"
|
||||||
@ -186,229 +142,147 @@ export const RightWidgetTab = observer(
|
|||||||
</Menu>
|
</Menu>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{type === "article" && (
|
<Box className="w-[80%] border border-gray-300 rounded-2xl p-3">
|
||||||
<Box className="w-[80%] border border-gray-300 rounded-2xl p-3">
|
{activeArticleIndex !== null && (
|
||||||
{activeArticleIndex !== null && (
|
<>
|
||||||
<>
|
<Box className="flex justify-end gap-2 mb-3">
|
||||||
<Box className="flex justify-end gap-2 mb-3">
|
<Button variant="contained" color="primary">
|
||||||
<Button
|
Открепить
|
||||||
variant="contained"
|
</Button>
|
||||||
color="primary"
|
<Button variant="contained" color="success">
|
||||||
onClick={() => {
|
Удалить
|
||||||
unlinkRightArticle(
|
</Button>
|
||||||
sight[language].right[activeArticleIndex].id
|
</Box>
|
||||||
);
|
<Box sx={{ display: "flex", gap: 3, flexGrow: 1 }}>
|
||||||
setActiveArticleIndex(null);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Открепить
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="success"
|
|
||||||
onClick={() => {
|
|
||||||
deleteRightArticle(
|
|
||||||
sight[language].right[activeArticleIndex].id
|
|
||||||
);
|
|
||||||
setActiveArticleIndex(null);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Удалить
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
<Box sx={{ display: "flex", gap: 3, flexGrow: 1 }}>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
flex: 2,
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: 2,
|
|
||||||
maxHeight: "70%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TextField
|
|
||||||
label="Название информации"
|
|
||||||
value={
|
|
||||||
sight[language].right[activeArticleIndex]
|
|
||||||
.heading
|
|
||||||
}
|
|
||||||
onChange={(e) =>
|
|
||||||
updateRightArticleInfo(
|
|
||||||
activeArticleIndex,
|
|
||||||
language,
|
|
||||||
e.target.value,
|
|
||||||
sight[language].right[activeArticleIndex].body
|
|
||||||
)
|
|
||||||
}
|
|
||||||
variant="outlined"
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ReactMarkdownEditor
|
|
||||||
value={
|
|
||||||
sight[language].right[activeArticleIndex].body
|
|
||||||
}
|
|
||||||
onChange={(value) =>
|
|
||||||
updateRightArticleInfo(
|
|
||||||
activeArticleIndex,
|
|
||||||
language,
|
|
||||||
sight[language].right[activeArticleIndex]
|
|
||||||
.heading,
|
|
||||||
value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MediaArea
|
|
||||||
articleId={
|
|
||||||
sight[language].right[activeArticleIndex].id
|
|
||||||
}
|
|
||||||
mediaIds={
|
|
||||||
sight[language].right[activeArticleIndex].media
|
|
||||||
}
|
|
||||||
onFilesDrop={(files) => {
|
|
||||||
setFileToUpload(files[0]);
|
|
||||||
setUploadMediaOpen(true);
|
|
||||||
}}
|
|
||||||
deleteMedia={deleteRightArticleMedia}
|
|
||||||
setSelectMediaDialogOpen={() => {
|
|
||||||
setIsSelectMediaModalOpen(true);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
{type === "media" && (
|
|
||||||
<Box className="w-[80%] border border-gray-300 rounded-2xl relative">
|
|
||||||
{sight.common.preview_media && (
|
|
||||||
<>
|
|
||||||
<Box className="absolute top-4 right-4">
|
|
||||||
<button
|
|
||||||
className="w-10 h-10 flex items-center justify-center"
|
|
||||||
onClick={unlinkPreviewMedia}
|
|
||||||
>
|
|
||||||
<X size={20} color="red" />
|
|
||||||
</button>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<MediaViewer
|
|
||||||
media={{
|
|
||||||
id: sight.common.preview_media || "",
|
|
||||||
media_type: 1,
|
|
||||||
filename: sight.common.preview_media || "",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!sight.common.preview_media && (
|
|
||||||
<MediaAreaForSight
|
|
||||||
onFinishUpload={(mediaId) => {
|
|
||||||
linkPreviewMedia(mediaId);
|
|
||||||
}}
|
|
||||||
onFilesDrop={() => {}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{type === "article" && (
|
|
||||||
<Box className="w-[25%] mr-10">
|
|
||||||
{activeArticleIndex !== null && (
|
|
||||||
<Paper
|
|
||||||
className="flex-1 flex flex-col rounded-2xl"
|
|
||||||
elevation={2}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
className="rounded-2xl overflow-hidden"
|
|
||||||
sx={{
|
|
||||||
width: "100%",
|
|
||||||
height: "75vh",
|
|
||||||
background: "#877361",
|
|
||||||
borderColor: "grey.300",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{sight[language].right[activeArticleIndex].media.length >
|
|
||||||
0 ? (
|
|
||||||
<MediaViewer
|
|
||||||
media={
|
|
||||||
sight[language].right[activeArticleIndex].media[0]
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
width: "100%",
|
flex: 2,
|
||||||
height: 200,
|
|
||||||
flexShrink: 0,
|
|
||||||
|
|
||||||
backgroundColor: "rgba(0,0,0,0.1)",
|
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
flexDirection: "column",
|
||||||
justifyContent: "center",
|
gap: 2,
|
||||||
|
maxHeight: "70%",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ImagePlus size={48} color="white" />
|
<TextField
|
||||||
</Box>
|
label="Название информации"
|
||||||
)}
|
value={
|
||||||
|
sight[language].right[activeArticleIndex].heading
|
||||||
|
}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateRightArticleInfo(
|
||||||
|
activeArticleIndex,
|
||||||
|
language,
|
||||||
|
e.target.value,
|
||||||
|
sight[language].right[activeArticleIndex].body
|
||||||
|
)
|
||||||
|
}
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
|
||||||
<Box
|
<ReactMarkdownEditor
|
||||||
sx={{
|
|
||||||
width: "100%",
|
|
||||||
height: "70px",
|
|
||||||
background: "#877361",
|
|
||||||
display: "flex",
|
|
||||||
flexShrink: 0,
|
|
||||||
|
|
||||||
alignItems: "center",
|
|
||||||
borderBottom: "1px solid rgba(255,255,255,0.1)",
|
|
||||||
px: 2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="h6" color="white">
|
|
||||||
{sight[language].right[activeArticleIndex].heading ||
|
|
||||||
"Выберите статью"}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
px: 2,
|
|
||||||
flexGrow: 1,
|
|
||||||
flex: 1,
|
|
||||||
minHeight: "300px",
|
|
||||||
overflowY: "auto",
|
|
||||||
backgroundColor: "#877361",
|
|
||||||
color: "white",
|
|
||||||
py: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{sight[language].right[activeArticleIndex].body ? (
|
|
||||||
<ReactMarkdownComponent
|
|
||||||
value={
|
value={
|
||||||
sight[language].right[activeArticleIndex].body
|
sight[language].right[activeArticleIndex].body
|
||||||
}
|
}
|
||||||
|
onChange={(value) =>
|
||||||
|
updateRightArticleInfo(
|
||||||
|
activeArticleIndex,
|
||||||
|
language,
|
||||||
|
sight[language].right[activeArticleIndex]
|
||||||
|
.heading,
|
||||||
|
value
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
{/* <MediaArea
|
||||||
<Typography
|
articleId={1}
|
||||||
color="rgba(255,255,255,0.7)"
|
mediaIds={[]}
|
||||||
sx={{ textAlign: "center", mt: 4 }}
|
deleteMedia={() => {}}
|
||||||
>
|
/> */}
|
||||||
Предпросмотр статьи появится здесь
|
</Box>
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</>
|
||||||
</Paper>
|
)}
|
||||||
)}
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
</Box>
|
||||||
|
|
||||||
|
<Box className="w-[25%] mr-10">
|
||||||
|
{activeArticleIndex !== null && (
|
||||||
|
<Paper
|
||||||
|
className="flex-1 flex flex-col rounded-2xl"
|
||||||
|
elevation={2}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
className="rounded-2xl overflow-hidden"
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
height: "75vh",
|
||||||
|
background: "#877361",
|
||||||
|
borderColor: "grey.300",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
height: 200,
|
||||||
|
flexShrink: 0,
|
||||||
|
backgroundColor: "rgba(0,0,0,0.1)",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ImagePlus size={48} color="white" />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
minHeight: "70px",
|
||||||
|
background: "#877361",
|
||||||
|
display: "flex",
|
||||||
|
flexShrink: 0,
|
||||||
|
alignItems: "center",
|
||||||
|
borderBottom: "1px solid rgba(255,255,255,0.1)",
|
||||||
|
px: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h6" color="white">
|
||||||
|
{sight[language].right[activeArticleIndex].heading ||
|
||||||
|
"Выберите статью"}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
px: 2,
|
||||||
|
flexGrow: 1,
|
||||||
|
overflowY: "auto",
|
||||||
|
backgroundColor: "#877361",
|
||||||
|
color: "white",
|
||||||
|
py: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{sight[language].right[activeArticleIndex].body ? (
|
||||||
|
<ReactMarkdownComponent
|
||||||
|
value={sight[language].right[activeArticleIndex].body}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Typography
|
||||||
|
color="rgba(255,255,255,0.7)"
|
||||||
|
sx={{ textAlign: "center", mt: 4 }}
|
||||||
|
>
|
||||||
|
Предпросмотр статьи появится здесь
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
@ -429,29 +303,11 @@ export const RightWidgetTab = observer(
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<UploadMediaDialog
|
|
||||||
open={uploadMediaOpen}
|
|
||||||
onClose={() => setUploadMediaOpen(false)}
|
|
||||||
afterUpload={async (media) => {
|
|
||||||
setUploadMediaOpen(false);
|
|
||||||
setFileToUpload(null);
|
|
||||||
await createLinkWithRightArticle(
|
|
||||||
media,
|
|
||||||
sight[language].right[activeArticleIndex || 0].id
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SelectArticleModal
|
<SelectArticleModal
|
||||||
open={isSelectModalOpen}
|
open={isSelectModalOpen}
|
||||||
onClose={handleCloseSelectModal}
|
onClose={handleCloseSelectModal}
|
||||||
onSelectArticle={handleArticleSelect}
|
onSelectArticle={handleArticleSelect}
|
||||||
/>
|
/>
|
||||||
<SelectMediaDialog
|
|
||||||
open={isSelectMediaModalOpen}
|
|
||||||
onClose={() => setIsSelectMediaModalOpen(false)}
|
|
||||||
onSelectMedia={handleMediaSelected}
|
|
||||||
/>
|
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -10,5 +10,3 @@ export * from "./SightsTable";
|
|||||||
export * from "./MediaViewer";
|
export * from "./MediaViewer";
|
||||||
export * from "./MediaArea";
|
export * from "./MediaArea";
|
||||||
export * from "./ModelViewer3D";
|
export * from "./ModelViewer3D";
|
||||||
export * from "./MediaAreaForSight";
|
|
||||||
export * from "./ImageUploadCard";
|
|
||||||
|
@ -1 +1 @@
|
|||||||
{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/app/index.tsx","./src/app/router/index.tsx","./src/entities/index.ts","./src/entities/navigation/index.ts","./src/entities/navigation/model/index.ts","./src/entities/navigation/ui/index.tsx","./src/features/index.ts","./src/features/navigation/index.ts","./src/features/navigation/ui/index.tsx","./src/pages/index.ts","./src/pages/createmediapage/index.tsx","./src/pages/createsightpage/index.tsx","./src/pages/devicespage/index.tsx","./src/pages/editmediapage/index.tsx","./src/pages/editsightpage/index.tsx","./src/pages/loginpage/index.tsx","./src/pages/mainpage/index.tsx","./src/pages/mappage/index.tsx","./src/pages/medialistpage/index.tsx","./src/pages/previewmediapage/index.tsx","./src/pages/sightpage/index.tsx","./src/shared/index.tsx","./src/shared/api/index.tsx","./src/shared/config/constants.tsx","./src/shared/config/index.ts","./src/shared/const/index.ts","./src/shared/lib/index.ts","./src/shared/lib/decodejwt/index.ts","./src/shared/lib/mui/theme.ts","./src/shared/modals/index.ts","./src/shared/modals/previewmediadialog/index.tsx","./src/shared/modals/selectarticledialog/index.tsx","./src/shared/modals/selectmediadialog/index.tsx","./src/shared/modals/uploadmediadialog/index.tsx","./src/shared/store/index.ts","./src/shared/store/articlesstore/index.tsx","./src/shared/store/authstore/index.tsx","./src/shared/store/citystore/index.tsx","./src/shared/store/createsightstore/index.tsx","./src/shared/store/devicesstore/index.tsx","./src/shared/store/editsightstore/index.tsx","./src/shared/store/languagestore/index.tsx","./src/shared/store/mediastore/index.tsx","./src/shared/store/sightsstore/index.tsx","./src/shared/store/snapshotstore/index.ts","./src/shared/store/vehiclestore/index.ts","./src/shared/ui/index.ts","./src/shared/ui/backbutton/index.tsx","./src/shared/ui/coordinatesinput/index.tsx","./src/shared/ui/input/index.tsx","./src/shared/ui/modal/index.tsx","./src/shared/ui/tabpanel/index.tsx","./src/widgets/index.ts","./src/widgets/devicestable/index.tsx","./src/widgets/imageuploadcard/index.tsx","./src/widgets/languageswitcher/index.tsx","./src/widgets/layout/index.tsx","./src/widgets/layout/ui/appbar.tsx","./src/widgets/layout/ui/drawer.tsx","./src/widgets/layout/ui/drawerheader.tsx","./src/widgets/mediaarea/index.tsx","./src/widgets/mediaareaforsight/index.tsx","./src/widgets/mediaviewer/threeview.tsx","./src/widgets/mediaviewer/index.tsx","./src/widgets/modelviewer3d/index.tsx","./src/widgets/reactmarkdown/index.tsx","./src/widgets/reactmarkdowneditor/index.tsx","./src/widgets/sightedit/index.tsx","./src/widgets/sightheader/index.ts","./src/widgets/sightheader/ui/index.tsx","./src/widgets/sighttabs/index.ts","./src/widgets/sighttabs/createinformationtab/mediauploadbox.tsx","./src/widgets/sighttabs/createinformationtab/index.tsx","./src/widgets/sighttabs/createlefttab/index.tsx","./src/widgets/sighttabs/createrighttab/index.tsx","./src/widgets/sighttabs/informationtab/index.tsx","./src/widgets/sighttabs/leftwidgettab/index.tsx","./src/widgets/sighttabs/rightwidgettab/index.tsx","./src/widgets/sightstable/index.tsx","./src/widgets/modals/index.ts","./src/widgets/modals/selectarticledialog/index.tsx"],"version":"5.8.3"}
|
{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/app/index.tsx","./src/app/router/index.tsx","./src/entities/index.ts","./src/entities/navigation/index.ts","./src/entities/navigation/model/index.ts","./src/entities/navigation/ui/index.tsx","./src/features/index.ts","./src/features/navigation/index.ts","./src/features/navigation/ui/index.tsx","./src/pages/index.ts","./src/pages/createmediapage/index.tsx","./src/pages/createsightpage/index.tsx","./src/pages/devicespage/index.tsx","./src/pages/editmediapage/index.tsx","./src/pages/editsightpage/index.tsx","./src/pages/loginpage/index.tsx","./src/pages/mainpage/index.tsx","./src/pages/mappage/index.tsx","./src/pages/medialistpage/index.tsx","./src/pages/previewmediapage/index.tsx","./src/pages/sightpage/index.tsx","./src/shared/index.tsx","./src/shared/api/index.tsx","./src/shared/config/constants.tsx","./src/shared/config/index.ts","./src/shared/const/index.ts","./src/shared/lib/index.ts","./src/shared/lib/decodejwt/index.ts","./src/shared/lib/mui/theme.ts","./src/shared/modals/index.ts","./src/shared/modals/previewmediadialog/index.tsx","./src/shared/modals/selectarticledialog/index.tsx","./src/shared/modals/selectmediadialog/index.tsx","./src/shared/modals/uploadmediadialog/index.tsx","./src/shared/store/index.ts","./src/shared/store/articlesstore/index.tsx","./src/shared/store/authstore/index.tsx","./src/shared/store/citystore/index.tsx","./src/shared/store/createsightstore/index.tsx","./src/shared/store/devicesstore/index.tsx","./src/shared/store/editsightstore/index.tsx","./src/shared/store/languagestore/index.tsx","./src/shared/store/mediastore/index.tsx","./src/shared/store/sightsstore/index.tsx","./src/shared/store/snapshotstore/index.ts","./src/shared/store/vehiclestore/index.ts","./src/shared/ui/index.ts","./src/shared/ui/backbutton/index.tsx","./src/shared/ui/coordinatesinput/index.tsx","./src/shared/ui/input/index.tsx","./src/shared/ui/modal/index.tsx","./src/shared/ui/tabpanel/index.tsx","./src/widgets/index.ts","./src/widgets/devicestable/index.tsx","./src/widgets/languageswitcher/index.tsx","./src/widgets/layout/index.tsx","./src/widgets/layout/ui/appbar.tsx","./src/widgets/layout/ui/drawer.tsx","./src/widgets/layout/ui/drawerheader.tsx","./src/widgets/mediaarea/index.tsx","./src/widgets/mediaviewer/threeview.tsx","./src/widgets/mediaviewer/index.tsx","./src/widgets/modelviewer3d/index.tsx","./src/widgets/reactmarkdown/index.tsx","./src/widgets/reactmarkdowneditor/index.tsx","./src/widgets/sightedit/index.tsx","./src/widgets/sightheader/index.ts","./src/widgets/sightheader/ui/index.tsx","./src/widgets/sighttabs/index.ts","./src/widgets/sighttabs/createinformationtab/mediauploadbox.tsx","./src/widgets/sighttabs/createinformationtab/index.tsx","./src/widgets/sighttabs/createlefttab/index.tsx","./src/widgets/sighttabs/createrighttab/index.tsx","./src/widgets/sighttabs/informationtab/index.tsx","./src/widgets/sighttabs/leftwidgettab/index.tsx","./src/widgets/sighttabs/rightwidgettab/index.tsx","./src/widgets/sightstable/index.tsx","./src/widgets/modals/index.ts","./src/widgets/modals/selectarticledialog/index.tsx"],"version":"5.8.3"}
|
Loading…
Reference in New Issue
Block a user