Compare commits

..

No commits in common. "078f051e8a52f19042290e7bae796136585cc166" and "5814e65953f42b02db1af85afb72a3367ae6f2c9" have entirely different histories.

18 changed files with 1277 additions and 2036 deletions

View File

@ -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" }}
> >

View File

@ -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>

View File

@ -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>

View File

@ -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;

View File

@ -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";
}, },
}, },
], ],

View File

@ -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={{

View File

@ -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) {

View File

@ -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;
}
}; };
} }

View File

@ -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();

View File

@ -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>
);
};

View File

@ -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}
/>
</>
);
}
);

View File

@ -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);
}}
/>
</> </>
); );
} }

View File

@ -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

View File

@ -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>
); );
} }

View File

@ -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)}

View File

@ -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>
); );
} }

View File

@ -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";

View File

@ -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"}