406 lines
16 KiB
TypeScript
406 lines
16 KiB
TypeScript
// RightWidgetTab.tsx
|
||
import { Box, Button, Paper, TextField, Typography } from "@mui/material";
|
||
import {
|
||
TabPanel,
|
||
BackButton,
|
||
languageStore, // Предполагаем, что он есть в @shared
|
||
Language, // Предполагаем, что он есть в @shared
|
||
// SelectArticleModal, // Добавим позже
|
||
// articlesStore, // Добавим позже
|
||
} from "@shared";
|
||
import { LanguageSwitcher } from "@widgets"; // Предполагаем, что LanguageSwitcher у вас есть
|
||
import { observer } from "mobx-react-lite";
|
||
import { useState, useMemo, useEffect } from "react";
|
||
import { editSightStore, BlockItem } from "@shared"; // Путь к вашему стору
|
||
|
||
// Импортируем сюда же определения BlockItem, если не выносим в types.ts
|
||
// export interface BlockItem { id: string; type: 'media' | 'article'; nameForSidebar: string; linkedArticleStoreId?: string; }
|
||
|
||
// --- Начальные данные для структуры блоков (позже это может загружаться) ---
|
||
// ID здесь должны быть уникальными для списка.
|
||
const initialBlockStructures: Omit<BlockItem, "nameForSidebar">[] = [
|
||
{ id: "preview_media_main", type: "media" },
|
||
{ id: "article_1_local", type: "article" }, // Эти статьи будут редактироваться локально
|
||
{ id: "article_2_local", type: "article" },
|
||
];
|
||
|
||
interface RightWidgetTabProps {
|
||
value: number;
|
||
index: number;
|
||
}
|
||
|
||
export const RightWidgetTab = observer(
|
||
({ value, index }: RightWidgetTabProps) => {
|
||
const { language } = languageStore; // Текущий язык
|
||
const { sightInfo } = editSightStore; // Данные достопримечательности
|
||
|
||
// 1. Структура блоков: порядок, тип, связи (не сам контент)
|
||
// Имена nameForSidebar будут динамически браться из sightInfo или articlesStore
|
||
const [blockItemsStructure, setBlockItemsStructure] = useState<
|
||
Omit<BlockItem, "nameForSidebar">[]
|
||
>(initialBlockStructures);
|
||
|
||
// 2. ID выбранного блока для редактирования
|
||
const [selectedBlockId, setSelectedBlockId] = useState<string | null>(
|
||
() => {
|
||
// По умолчанию выбираем первый блок, если он есть
|
||
return initialBlockStructures.length > 0
|
||
? initialBlockStructures[0].id
|
||
: null;
|
||
}
|
||
);
|
||
|
||
// 3. Состояние для модального окна выбора существующей статьи (добавим позже)
|
||
// const [isSelectModalOpen, setIsSelectModalOpen] = useState(false);
|
||
|
||
// --- Производные данные (Derived State) ---
|
||
|
||
// Блоки для отображения в сайдбаре (с локализованными именами)
|
||
const blocksForSidebar: BlockItem[] = useMemo(() => {
|
||
return blockItemsStructure.map((struct) => {
|
||
let name = `Блок ${struct.id}`; // Имя по умолчанию
|
||
|
||
if (struct.type === "media" && struct.id === "preview_media_main") {
|
||
name = "Превью-медиа"; // Фиксированное имя для этого блока
|
||
} else if (struct.type === "article") {
|
||
if (struct.linkedArticleStoreId) {
|
||
// TODO: Найти имя в articlesStore по struct.linkedArticleStoreId
|
||
name = `Связанная: ${struct.linkedArticleStoreId}`;
|
||
} else {
|
||
// Это локальная статья, берем заголовок из editSightStore
|
||
const articleContent = sightInfo[language]?.right?.find(
|
||
(a) => a.id === struct.id
|
||
);
|
||
name =
|
||
articleContent?.heading ||
|
||
`Статья ${struct.id.slice(-4)} (${language.toUpperCase()})`;
|
||
}
|
||
}
|
||
return { ...struct, nameForSidebar: name };
|
||
});
|
||
}, [blockItemsStructure, language, sightInfo]);
|
||
|
||
// Данные выбранного блока (структура + контент)
|
||
const selectedBlockData = useMemo(() => {
|
||
if (!selectedBlockId) return null;
|
||
const structure = blockItemsStructure.find(
|
||
(b) => b.id === selectedBlockId
|
||
);
|
||
if (!structure) return null;
|
||
|
||
if (structure.type === "article" && !structure.linkedArticleStoreId) {
|
||
const content = sightInfo[language]?.right?.find(
|
||
(a) => a.id === selectedBlockId
|
||
);
|
||
return {
|
||
structure,
|
||
content: content || { id: selectedBlockId, heading: "", body: "" }, // Заглушка, если нет контента
|
||
};
|
||
}
|
||
// Для media или связанных статей пока просто структура
|
||
return { structure, content: null };
|
||
}, [selectedBlockId, blockItemsStructure, language, sightInfo]);
|
||
|
||
// --- Обработчики событий ---
|
||
const handleSelectBlock = (blockId: string) => {
|
||
setSelectedBlockId(blockId);
|
||
};
|
||
|
||
const handleCreateNewArticle = () => {
|
||
const newBlockId = `article_local_${Date.now()}`;
|
||
const newBlockStructure: Omit<BlockItem, "nameForSidebar"> = {
|
||
id: newBlockId,
|
||
type: "article",
|
||
};
|
||
setBlockItemsStructure((prev) => [...prev, newBlockStructure]);
|
||
|
||
// Добавляем пустой контент для этой статьи во все языки в editSightStore
|
||
const baseName = `Новая статья ${
|
||
blockItemsStructure.filter((b) => b.type === "article").length + 1
|
||
}`;
|
||
["ru", "en", "zh"].forEach((lang) => {
|
||
const currentLang = lang as Language;
|
||
if (
|
||
editSightStore.sightInfo[currentLang] &&
|
||
!editSightStore.sightInfo[currentLang].right?.find(
|
||
(r) => r.id === newBlockId
|
||
)
|
||
) {
|
||
editSightStore.sightInfo[currentLang].right.push({
|
||
id: newBlockId,
|
||
heading: `${baseName} (${currentLang.toUpperCase()})`,
|
||
body: `Содержимое для ${baseName} (${currentLang.toUpperCase()})...`,
|
||
});
|
||
}
|
||
});
|
||
setSelectedBlockId(newBlockId);
|
||
};
|
||
|
||
const handleHeadingChange = (newHeading: string) => {
|
||
if (
|
||
selectedBlockData &&
|
||
selectedBlockData.structure.type === "article" &&
|
||
!selectedBlockData.structure.linkedArticleStoreId
|
||
) {
|
||
const blockId = selectedBlockData.structure.id;
|
||
const langData = editSightStore.sightInfo[language];
|
||
const article = langData?.right?.find((a) => a.id === blockId);
|
||
if (article) {
|
||
article.heading = newHeading;
|
||
} else if (langData) {
|
||
// Если статьи еще нет, добавляем
|
||
langData.right.push({ id: blockId, heading: newHeading, body: "" });
|
||
}
|
||
// Обновить имя в сайдбаре (т.к. blocksForSidebar пересчитается)
|
||
// Для этого достаточно, чтобы sightInfo был observable и blocksForSidebar от него зависел
|
||
}
|
||
};
|
||
|
||
const handleBodyChange = (newBody: string) => {
|
||
if (
|
||
selectedBlockData &&
|
||
selectedBlockData.structure.type === "article" &&
|
||
!selectedBlockData.structure.linkedArticleStoreId
|
||
) {
|
||
const blockId = selectedBlockData.structure.id;
|
||
const langData = editSightStore.sightInfo[language];
|
||
const article = langData?.right?.find((a) => a.id === blockId);
|
||
if (article) {
|
||
article.body = newBody;
|
||
} else if (langData) {
|
||
// Если статьи еще нет, добавляем
|
||
langData.right.push({ id: blockId, heading: "", body: newBody });
|
||
}
|
||
}
|
||
};
|
||
|
||
const handleDeleteBlock = (blockIdToDelete: string) => {
|
||
setBlockItemsStructure((prev) =>
|
||
prev.filter((b) => b.id !== blockIdToDelete)
|
||
);
|
||
// Удаляем контент из editSightStore для всех языков
|
||
["ru", "en", "zh"].forEach((lang) => {
|
||
const currentLang = lang as Language;
|
||
if (editSightStore.sightInfo[currentLang]) {
|
||
editSightStore.sightInfo[currentLang].right =
|
||
editSightStore.sightInfo[currentLang].right?.filter(
|
||
(r) => r.id !== blockIdToDelete
|
||
);
|
||
}
|
||
});
|
||
|
||
if (selectedBlockId === blockIdToDelete) {
|
||
setSelectedBlockId(
|
||
blockItemsStructure.length > 1
|
||
? blockItemsStructure.filter((b) => b.id !== blockIdToDelete)[0]?.id
|
||
: null
|
||
);
|
||
}
|
||
};
|
||
|
||
const handleSave = () => {
|
||
console.log(
|
||
"Сохранение Right Widget:",
|
||
JSON.stringify(editSightStore.sightInfo, null, 2)
|
||
);
|
||
// Здесь будет логика отправки editSightStore.sightInfo на сервер
|
||
alert("Данные для сохранения (см. консоль)");
|
||
};
|
||
|
||
// --- Инициализация контента в сторе для initialBlockStructures (если его там нет) ---
|
||
useEffect(() => {
|
||
initialBlockStructures.forEach((struct) => {
|
||
if (struct.type === "article" && !struct.linkedArticleStoreId) {
|
||
const baseName = `Статья ${struct.id.split("_")[1]}`; // Пример "История" или "Факты"
|
||
["ru", "en", "zh"].forEach((lang) => {
|
||
const currentLang = lang as Language;
|
||
if (
|
||
editSightStore.sightInfo[currentLang] &&
|
||
!editSightStore.sightInfo[currentLang].right?.find(
|
||
(r) => r.id === struct.id
|
||
)
|
||
) {
|
||
editSightStore.sightInfo[currentLang].right?.push({
|
||
id: struct.id,
|
||
heading: `${baseName} (${currentLang.toUpperCase()})`, // Например: "История (RU)"
|
||
body: `Начальное содержимое для ${baseName} на ${currentLang.toUpperCase()}.`,
|
||
});
|
||
}
|
||
});
|
||
}
|
||
});
|
||
}, []); // Запускается один раз при монтировании
|
||
|
||
return (
|
||
<TabPanel value={value} index={index}>
|
||
<LanguageSwitcher />
|
||
<Box
|
||
sx={{
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
height: "100%",
|
||
minHeight: "calc(100vh - 200px)",
|
||
gap: 2,
|
||
paddingBottom: "70px",
|
||
position: "relative",
|
||
}}
|
||
>
|
||
<BackButton />
|
||
|
||
<Box sx={{ display: "flex", flexGrow: 1, gap: 2.5, minHeight: 0 }}>
|
||
{/* Компонент сайдбара списка блоков */}
|
||
<Paper
|
||
elevation={1}
|
||
sx={{
|
||
width: 280,
|
||
padding: 1.5,
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
}}
|
||
>
|
||
<Typography variant="h6" gutterBottom>
|
||
Блоки
|
||
</Typography>
|
||
<Box sx={{ flexGrow: 1, overflowY: "auto" }}>
|
||
{blocksForSidebar.map((block) => (
|
||
<Button
|
||
key={block.id}
|
||
fullWidth
|
||
variant={
|
||
selectedBlockId === block.id ? "contained" : "outlined"
|
||
}
|
||
onClick={() => handleSelectBlock(block.id)}
|
||
sx={{
|
||
justifyContent: "flex-start",
|
||
mb: 0.5,
|
||
textTransform: "none",
|
||
}}
|
||
>
|
||
{block.nameForSidebar}
|
||
</Button>
|
||
))}
|
||
</Box>
|
||
<Button
|
||
variant="contained"
|
||
onClick={handleCreateNewArticle}
|
||
sx={{ mt: 1 }}
|
||
>
|
||
+ Новая статья
|
||
</Button>
|
||
{/* TODO: Кнопка "Выбрать существующую" */}
|
||
</Paper>
|
||
|
||
{/* Компонент редактора выбранного блока */}
|
||
<Paper
|
||
elevation={1}
|
||
sx={{ flexGrow: 1, padding: 2.5, overflowY: "auto" }}
|
||
>
|
||
<Typography variant="h6" gutterBottom>
|
||
Редактор блока ({language.toUpperCase()})
|
||
</Typography>
|
||
{selectedBlockData ? (
|
||
<Box>
|
||
<Typography variant="subtitle1">
|
||
ID: {selectedBlockData.structure.id}
|
||
</Typography>
|
||
<Typography variant="subtitle1">
|
||
Тип: {selectedBlockData.structure.type}
|
||
</Typography>
|
||
{selectedBlockData.structure.type === "media" && (
|
||
<Box
|
||
my={2}
|
||
p={2}
|
||
border="1px dashed grey"
|
||
height={150}
|
||
display="flex"
|
||
alignItems="center"
|
||
justifyContent="center"
|
||
>
|
||
<Typography color="textSecondary">
|
||
Загрузчик медиа для "{selectedBlockData.structure.id}"
|
||
</Typography>
|
||
</Box>
|
||
)}
|
||
{selectedBlockData.structure.type === "article" &&
|
||
!selectedBlockData.structure.linkedArticleStoreId &&
|
||
selectedBlockData.content && (
|
||
<Box mt={2}>
|
||
<TextField
|
||
fullWidth
|
||
label="Заголовок статьи"
|
||
value={selectedBlockData.content.heading}
|
||
onChange={(e) => handleHeadingChange(e.target.value)}
|
||
sx={{ mb: 2 }}
|
||
/>
|
||
<TextField
|
||
fullWidth
|
||
multiline
|
||
rows={8}
|
||
label="Текст статьи"
|
||
value={selectedBlockData.content.body}
|
||
onChange={(e) => handleBodyChange(e.target.value)}
|
||
sx={{ mb: 2 }}
|
||
// Здесь позже можно будет вставить SightEdit
|
||
/>
|
||
{/* TODO: Секция медиа для статьи */}
|
||
<Button
|
||
color="error"
|
||
variant="outlined"
|
||
onClick={() =>
|
||
handleDeleteBlock(selectedBlockData.structure.id)
|
||
}
|
||
>
|
||
Удалить эту статью
|
||
</Button>
|
||
</Box>
|
||
)}
|
||
{selectedBlockData.structure.type === "article" &&
|
||
selectedBlockData.structure.linkedArticleStoreId && (
|
||
<Box mt={2}>
|
||
<Typography>
|
||
Это связанная статья:{" "}
|
||
{selectedBlockData.structure.linkedArticleStoreId}
|
||
</Typography>
|
||
{/* TODO: Кнопки "Открепить", "Удалить из списка" */}
|
||
</Box>
|
||
)}
|
||
</Box>
|
||
) : (
|
||
<Typography color="textSecondary">
|
||
Выберите блок для редактирования
|
||
</Typography>
|
||
)}
|
||
</Paper>
|
||
</Box>
|
||
|
||
<Box
|
||
sx={{
|
||
position: "absolute",
|
||
bottom: 0,
|
||
left: 0,
|
||
right: 0,
|
||
padding: 2,
|
||
backgroundColor: "background.paper",
|
||
borderTop: "1px solid",
|
||
borderColor: "divider",
|
||
zIndex: 10,
|
||
display: "flex",
|
||
justifyContent: "flex-end",
|
||
}}
|
||
>
|
||
<Button
|
||
variant="contained"
|
||
color="success"
|
||
onClick={handleSave}
|
||
size="large"
|
||
>
|
||
Сохранить изменения
|
||
</Button>
|
||
</Box>
|
||
</Box>
|
||
{/* <SelectArticleModal open={isSelectModalOpen} ... /> */}
|
||
</TabPanel>
|
||
);
|
||
}
|
||
);
|