WhiteNightsAdminPanel/src/widgets/SightTabs/RightWidgetTab/index.tsx
2025-05-31 21:17:27 +03:00

406 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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