feat: Add translation on 3 languages for sight page
This commit is contained in:
parent
0d9bbb140f
commit
87386c6a73
@ -1,7 +1,7 @@
|
||||
import List from "@mui/material/List";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import { NAVIGATION_ITEMS } from "@shared";
|
||||
import { NavigationItemComponent } from "@entities";
|
||||
import { NavigationItem, NavigationItemComponent } from "@entities";
|
||||
|
||||
export const NavigationList = ({ open }: { open: boolean }) => {
|
||||
const primaryItems = NAVIGATION_ITEMS.primary;
|
||||
@ -11,13 +11,21 @@ export const NavigationList = ({ open }: { open: boolean }) => {
|
||||
<>
|
||||
<List>
|
||||
{primaryItems.map((item) => (
|
||||
<NavigationItemComponent key={item.id} item={item} open={open} />
|
||||
<NavigationItemComponent
|
||||
key={item.id}
|
||||
item={item as NavigationItem}
|
||||
open={open}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
<Divider />
|
||||
<List>
|
||||
{secondaryItems.map((item) => (
|
||||
<NavigationItemComponent key={item.id} item={item} open={open} />
|
||||
<NavigationItemComponent
|
||||
key={item.id}
|
||||
item={item as NavigationItem}
|
||||
open={open}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
</>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Box, Tab, Tabs } from "@mui/material";
|
||||
import { articlesStore, cityStore } from "@shared";
|
||||
import { articlesStore, cityStore, languageStore } from "@shared";
|
||||
import { InformationTab, RightWidgetTab } from "@widgets";
|
||||
import { LeftWidgetTab } from "@widgets";
|
||||
import { useEffect, useState } from "react";
|
||||
@ -22,7 +22,7 @@ export const CreateSightPage = observer(() => {
|
||||
|
||||
useEffect(() => {
|
||||
getCities();
|
||||
getArticles();
|
||||
getArticles(languageStore.language);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
@ -3,7 +3,12 @@ import { InformationTab, RightWidgetTab } from "@widgets";
|
||||
import { LeftWidgetTab } from "@widgets";
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { articlesStore, languageStore, sightsStore } from "@shared";
|
||||
import {
|
||||
articlesStore,
|
||||
cityStore,
|
||||
editSightStore,
|
||||
languageStore,
|
||||
} from "@shared";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
function a11yProps(index: number) {
|
||||
@ -15,10 +20,11 @@ function a11yProps(index: number) {
|
||||
|
||||
export const EditSightPage = observer(() => {
|
||||
const [value, setValue] = useState(0);
|
||||
const { sight, getSight } = sightsStore;
|
||||
const { articles, getArticles } = articlesStore;
|
||||
const { getSightInfo } = editSightStore;
|
||||
const { getArticles } = articlesStore;
|
||||
const { language } = languageStore;
|
||||
const { id } = useParams();
|
||||
const { getCities } = cityStore;
|
||||
|
||||
const handleChange = (_: React.SyntheticEvent, newValue: number) => {
|
||||
setValue(newValue);
|
||||
@ -27,55 +33,53 @@ export const EditSightPage = observer(() => {
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
if (id) {
|
||||
await getSight(Number(id));
|
||||
await getSightInfo(+id, language);
|
||||
await getArticles(language);
|
||||
await getCities();
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, [id, language]);
|
||||
|
||||
return (
|
||||
articles &&
|
||||
sight && (
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
minHeight: "100vh",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
borderBottom: 1,
|
||||
borderColor: "divider",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
minHeight: "100vh",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
<Tabs
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
aria-label="sight tabs"
|
||||
sx={{
|
||||
borderBottom: 1,
|
||||
borderColor: "divider",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
width: "100%",
|
||||
"& .MuiTabs-flexContainer": {
|
||||
justifyContent: "center",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Tabs
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
aria-label="sight tabs"
|
||||
sx={{
|
||||
width: "100%",
|
||||
"& .MuiTabs-flexContainer": {
|
||||
justifyContent: "center",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Tab sx={{ flex: 1 }} label="Общая информация" {...a11yProps(0)} />
|
||||
<Tab sx={{ flex: 1 }} label="Левый виджет" {...a11yProps(1)} />
|
||||
<Tab sx={{ flex: 1 }} label="Правый виджет" {...a11yProps(2)} />
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
<div className="flex-1">
|
||||
<InformationTab value={value} index={0} />
|
||||
<LeftWidgetTab value={value} index={1} />
|
||||
<RightWidgetTab value={value} index={2} />
|
||||
</div>
|
||||
<Tab sx={{ flex: 1 }} label="Общая информация" {...a11yProps(0)} />
|
||||
<Tab sx={{ flex: 1 }} label="Левый виджет" {...a11yProps(1)} />
|
||||
<Tab sx={{ flex: 1 }} label="Правый виджет" {...a11yProps(2)} />
|
||||
</Tabs>
|
||||
</Box>
|
||||
)
|
||||
|
||||
<div className="flex-1">
|
||||
<InformationTab value={value} index={0} />
|
||||
<LeftWidgetTab value={value} index={1} />
|
||||
<RightWidgetTab value={value} index={2} />
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
@ -1,16 +1,28 @@
|
||||
import { languageStore, Language } from "@shared";
|
||||
import axios from "axios";
|
||||
import axios, { AxiosError, InternalAxiosRequestConfig } from "axios";
|
||||
|
||||
const authInstance = axios.create({
|
||||
baseURL: "https://wn.krbl.ru",
|
||||
});
|
||||
|
||||
authInstance.interceptors.request.use((config) => {
|
||||
authInstance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
||||
console.log(config);
|
||||
config.headers.Authorization = `Bearer ${localStorage.getItem("token")}`;
|
||||
config.headers["X-Language"] = languageStore.language ?? "ru";
|
||||
return config;
|
||||
});
|
||||
|
||||
authInstance.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error: AxiosError) => {
|
||||
if (error.response?.status === 401) {
|
||||
localStorage.removeItem("token");
|
||||
window.location.href = "/login";
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
const languageInstance = (language: Language) => {
|
||||
const instance = axios.create({
|
||||
baseURL: "https://wn.krbl.ru",
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { authStore } from "@shared";
|
||||
import { Power, LucideIcon, Building2, MonitorSmartphone } from "lucide-react";
|
||||
export const DRAWER_WIDTH = 300;
|
||||
|
||||
@ -5,7 +6,8 @@ interface NavigationItem {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: LucideIcon;
|
||||
path: string;
|
||||
path?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export const NAVIGATION_ITEMS: {
|
||||
@ -31,7 +33,9 @@ export const NAVIGATION_ITEMS: {
|
||||
id: "logout",
|
||||
label: "Выйти",
|
||||
icon: Power,
|
||||
path: "/logout",
|
||||
onClick: () => {
|
||||
authStore.logout();
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -1,13 +1,6 @@
|
||||
import {
|
||||
articlesStore,
|
||||
authStore,
|
||||
Language,
|
||||
mediaStore,
|
||||
MEDIA_TYPE_LABELS,
|
||||
API_URL,
|
||||
} from "@shared";
|
||||
import { mediaStore, MEDIA_TYPE_LABELS } from "@shared";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useEffect, useRef, useState, useCallback } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
@ -17,13 +10,12 @@ import {
|
||||
TextField,
|
||||
Paper,
|
||||
Box,
|
||||
Typography,
|
||||
CircularProgress,
|
||||
Alert,
|
||||
Snackbar,
|
||||
} from "@mui/material";
|
||||
import { Download, Save } from "lucide-react";
|
||||
import { ReactMarkdownComponent, MediaViewer } from "@widgets";
|
||||
import { MediaViewer } from "@widgets";
|
||||
import { authInstance } from "@shared";
|
||||
|
||||
interface PreviewMediaDialogProps {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { articlesStore, authStore, Language } from "@shared";
|
||||
import { articlesStore } from "@shared";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
@ -73,7 +73,10 @@ export const SelectArticleModal = observer(
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await Promise.all([getArticle(articleId), getArticleMedia(articleId)]);
|
||||
await Promise.all([
|
||||
getArticle(Number(articleId)),
|
||||
getArticleMedia(Number(articleId)),
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch article data:", error);
|
||||
// Reset article data on error
|
||||
@ -83,9 +86,11 @@ export const SelectArticleModal = observer(
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
const filteredArticles = articles
|
||||
// @ts-ignore
|
||||
.filter((article) => !linkedArticleIds.includes(article.id))
|
||||
// @ts-ignore
|
||||
.filter((article) =>
|
||||
article.service_name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
@ -140,6 +145,7 @@ export const SelectArticleModal = observer(
|
||||
{searchQuery ? "Статьи не найдены" : "Нет доступных статей"}
|
||||
</Typography>
|
||||
) : (
|
||||
// @ts-ignore
|
||||
filteredArticles.map((article) => (
|
||||
<ListItemButton
|
||||
key={article.id}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { articlesStore, authStore, Language, mediaStore } from "@shared";
|
||||
import { mediaStore } from "@shared";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
@ -12,12 +12,11 @@ import {
|
||||
ListItemButton,
|
||||
ListItemText,
|
||||
Paper,
|
||||
Box,
|
||||
Typography,
|
||||
InputAdornment,
|
||||
} from "@mui/material";
|
||||
import { ImagePlus, Search } from "lucide-react";
|
||||
import { ReactMarkdownComponent, MediaViewer } from "@widgets";
|
||||
import { Search } from "lucide-react";
|
||||
import { MediaViewer } from "@widgets";
|
||||
|
||||
interface SelectMediaDialogProps {
|
||||
open: boolean; // Corrected prop name
|
||||
|
@ -53,7 +53,7 @@ class ArticlesStore {
|
||||
const response = await authInstance.get(`/sight/${id}/article`);
|
||||
|
||||
runInAction(() => {
|
||||
editSightStore.sightInfo[languageStore.language].right = response.data;
|
||||
editSightStore.sight[languageStore.language].right = response.data;
|
||||
});
|
||||
};
|
||||
|
||||
@ -66,10 +66,14 @@ class ArticlesStore {
|
||||
};
|
||||
|
||||
getArticleByArticleId = computed(() => {
|
||||
if (editSightStore.sightInfo.left_article) {
|
||||
return this.articles[languageStore.language].find(
|
||||
(a) => a.id == editSightStore.sightInfo.left_article
|
||||
if (editSightStore.sight.common.left_article) {
|
||||
const language = languageStore.language;
|
||||
const foundArticle = this.articles[language].find(
|
||||
(a) => a.id == editSightStore.sight.common.left_article
|
||||
);
|
||||
editSightStore.sight[language].left.heading = foundArticle?.heading ?? "";
|
||||
editSightStore.sight[language].left.body = foundArticle?.body ?? "";
|
||||
return foundArticle;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
@ -79,7 +79,7 @@ class AuthStore {
|
||||
};
|
||||
|
||||
get isAuthenticated() {
|
||||
return this.payload?.token !== null;
|
||||
return this.payload !== null;
|
||||
}
|
||||
|
||||
get user() {
|
||||
|
@ -1,15 +1,16 @@
|
||||
// @shared/stores/editSightStore.ts
|
||||
import { Language } from "@shared";
|
||||
import { makeAutoObservable } from "mobx";
|
||||
import { authInstance, Language } from "@shared";
|
||||
import { makeAutoObservable, runInAction } from "mobx";
|
||||
|
||||
export interface MediaObject {
|
||||
id: string;
|
||||
filename: string;
|
||||
media_type: number;
|
||||
}
|
||||
|
||||
type SightBaseInfo = {
|
||||
export type SightLanguageInfo = {
|
||||
id: number;
|
||||
name: string;
|
||||
address: string;
|
||||
left: { heading: string; body: string };
|
||||
right: { heading: string; body: string }[];
|
||||
};
|
||||
|
||||
export type SightCommonInfo = {
|
||||
city_id: number;
|
||||
city: string;
|
||||
latitude: number;
|
||||
@ -22,103 +23,14 @@ type SightBaseInfo = {
|
||||
video_preview: string;
|
||||
};
|
||||
|
||||
export interface RightArticleBlock {
|
||||
id: string;
|
||||
type: "article" | "preview_media";
|
||||
name: string;
|
||||
linkedArticleId?: string;
|
||||
heading: string;
|
||||
body: string;
|
||||
media: MediaObject | null;
|
||||
}
|
||||
|
||||
type SightInfo = SightBaseInfo & {
|
||||
[key in Language]: {
|
||||
info: {
|
||||
name: string;
|
||||
address: string;
|
||||
};
|
||||
left: {
|
||||
loaded: boolean; // Означает, что данные для этого языка были инициализированы/загружены
|
||||
heading: string;
|
||||
body: string;
|
||||
media: MediaObject | null;
|
||||
};
|
||||
right: RightArticleBlock[];
|
||||
};
|
||||
export type SightBaseInfo = {
|
||||
common: SightCommonInfo;
|
||||
} & {
|
||||
[key in Language]: SightLanguageInfo;
|
||||
};
|
||||
|
||||
class EditSightStore {
|
||||
sightInfo: SightInfo = {
|
||||
id: 0,
|
||||
city_id: 0,
|
||||
city: "",
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
thumbnail: "",
|
||||
watermark_lu: "",
|
||||
watermark_rd: "",
|
||||
left_article: 0,
|
||||
preview_media: "",
|
||||
video_preview: "",
|
||||
ru: {
|
||||
info: { name: "", address: "" },
|
||||
left: { loaded: false, heading: "", body: "", media: null },
|
||||
right: [],
|
||||
},
|
||||
en: {
|
||||
info: { name: "", address: "" },
|
||||
left: { loaded: false, heading: "", body: "", media: null },
|
||||
right: [],
|
||||
},
|
||||
zh: {
|
||||
info: { name: "", address: "" },
|
||||
left: { loaded: false, heading: "", body: "", media: null },
|
||||
right: [],
|
||||
},
|
||||
};
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
// loadSightInfo: Используется для первоначальной загрузки данных для ЯЗЫКА.
|
||||
// Она устанавливает loaded: true, чтобы в будущем не перезатирать данные.
|
||||
loadSightInfo = (
|
||||
language: Language,
|
||||
heading: string,
|
||||
body: string,
|
||||
media: MediaObject | null
|
||||
) => {
|
||||
// Важно: если данные уже были загружены или изменены, не перезаписывайте их.
|
||||
// Это предотвращает потерю пользовательского ввода при переключении языков.
|
||||
// Если хотите принудительную загрузку, добавьте другой метод или параметр.
|
||||
if (!this.sightInfo[language].left.loaded) {
|
||||
// <--- Только если еще не загружено
|
||||
this.sightInfo[language].left.heading = heading;
|
||||
this.sightInfo[language].left.body = body;
|
||||
this.sightInfo[language].left.media = media;
|
||||
this.sightInfo[language].left.loaded = true; // <--- Устанавливаем loaded только при загрузке
|
||||
}
|
||||
};
|
||||
|
||||
// updateSightInfo: Используется для сохранения ЛЮБЫХ пользовательских изменений.
|
||||
// Она НЕ должна влиять на флаг 'loaded', который управляется 'loadSightInfo'.
|
||||
updateSightInfo = (
|
||||
language: Language,
|
||||
heading: string,
|
||||
body: string,
|
||||
media: MediaObject | null
|
||||
) => {
|
||||
this.sightInfo[language].left.heading = heading;
|
||||
this.sightInfo[language].left.body = body;
|
||||
this.sightInfo[language].left.media = media;
|
||||
// this.sightInfo[language].left.loaded = true; // <-- УДАЛИТЕ эту строку
|
||||
};
|
||||
|
||||
clearSightInfo = () => {
|
||||
this.sightInfo = {
|
||||
id: 0,
|
||||
sight: SightBaseInfo = {
|
||||
common: {
|
||||
city_id: 0,
|
||||
city: "",
|
||||
latitude: 0,
|
||||
@ -129,23 +41,123 @@ class EditSightStore {
|
||||
left_article: 0,
|
||||
preview_media: "",
|
||||
video_preview: "",
|
||||
},
|
||||
ru: {
|
||||
id: 0,
|
||||
name: "",
|
||||
address: "",
|
||||
left: { heading: "", body: "" },
|
||||
right: [],
|
||||
},
|
||||
en: {
|
||||
id: 0,
|
||||
name: "",
|
||||
address: "",
|
||||
left: { heading: "", body: "" },
|
||||
right: [],
|
||||
},
|
||||
zh: {
|
||||
id: 0,
|
||||
name: "",
|
||||
address: "",
|
||||
left: { heading: "", body: "" },
|
||||
right: [],
|
||||
},
|
||||
};
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
hasLoadedCommon = false;
|
||||
getSightInfo = async (id: number, language: Language) => {
|
||||
if (this.sight[language].id === id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await authInstance.get(`/sight/${id}`);
|
||||
const data = response.data;
|
||||
|
||||
runInAction(() => {
|
||||
// Обновляем языковую часть
|
||||
this.sight[language] = {
|
||||
...this.sight[language],
|
||||
...data,
|
||||
};
|
||||
|
||||
// Только при первом запросе обновляем общую часть
|
||||
if (!this.hasLoadedCommon) {
|
||||
this.sight.common = {
|
||||
...this.sight.common,
|
||||
...data,
|
||||
};
|
||||
this.hasLoadedCommon = true;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
updateLeftInfo = (language: Language, heading: string, body: string) => {
|
||||
this.sight[language].left.heading = heading;
|
||||
this.sight[language].left.body = body;
|
||||
};
|
||||
|
||||
clearSightInfo = () => {
|
||||
this.sight = {
|
||||
common: {
|
||||
city_id: 0,
|
||||
city: "",
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
thumbnail: "",
|
||||
watermark_lu: "",
|
||||
watermark_rd: "",
|
||||
left_article: 0,
|
||||
preview_media: "",
|
||||
video_preview: "",
|
||||
},
|
||||
ru: {
|
||||
info: { name: "", address: "" },
|
||||
left: { loaded: false, heading: "", body: "", media: null },
|
||||
id: 0,
|
||||
name: "",
|
||||
address: "",
|
||||
left: { heading: "", body: "" },
|
||||
right: [],
|
||||
},
|
||||
|
||||
en: {
|
||||
info: { name: "", address: "" },
|
||||
left: { loaded: false, heading: "", body: "", media: null },
|
||||
id: 0,
|
||||
name: "",
|
||||
address: "",
|
||||
left: { heading: "", body: "" },
|
||||
right: [],
|
||||
},
|
||||
|
||||
zh: {
|
||||
info: { name: "", address: "" },
|
||||
left: { loaded: false, heading: "", body: "", media: null },
|
||||
id: 0,
|
||||
name: "",
|
||||
address: "",
|
||||
left: { heading: "", body: "" },
|
||||
right: [],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
updateSightInfo = (
|
||||
language: Language,
|
||||
content: Partial<SightLanguageInfo | SightCommonInfo>,
|
||||
common: boolean = false
|
||||
) => {
|
||||
if (common) {
|
||||
this.sight.common = {
|
||||
...this.sight.common,
|
||||
...content,
|
||||
};
|
||||
} else {
|
||||
this.sight[language] = {
|
||||
...this.sight[language],
|
||||
...content,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const editSightStore = new EditSightStore();
|
||||
|
@ -1,9 +1,8 @@
|
||||
import {
|
||||
articlesStore,
|
||||
authInstance,
|
||||
languageInstance,
|
||||
languageStore,
|
||||
editSightStore,
|
||||
SightBaseInfo,
|
||||
} from "@shared";
|
||||
import { computed, makeAutoObservable, runInAction } from "mobx";
|
||||
|
||||
@ -59,48 +58,40 @@ class SightsStore {
|
||||
});
|
||||
};
|
||||
|
||||
getSight = async (id: number) => {
|
||||
const response = await authInstance.get(`/sight/${id}`);
|
||||
// getSight = async (id: number) => {
|
||||
// const response = await authInstance.get(`/sight/${id}`);
|
||||
|
||||
runInAction(() => {
|
||||
this.sight = response.data;
|
||||
editSightStore.sightInfo = {
|
||||
...editSightStore.sightInfo,
|
||||
id: response.data.id,
|
||||
city_id: response.data.city_id,
|
||||
city: response.data.city,
|
||||
latitude: response.data.latitude,
|
||||
longitude: response.data.longitude,
|
||||
thumbnail: response.data.thumbnail,
|
||||
watermark_lu: response.data.watermark_lu,
|
||||
watermark_rd: response.data.watermark_rd,
|
||||
left_article: response.data.left_article,
|
||||
preview_media: response.data.preview_media,
|
||||
video_preview: response.data.video_preview,
|
||||
[languageStore.language]: {
|
||||
info: {
|
||||
name: response.data.name,
|
||||
address: response.data.address,
|
||||
description: response.data.description,
|
||||
},
|
||||
left: {
|
||||
heading: editSightStore.sightInfo[languageStore.language].left
|
||||
.loaded
|
||||
? editSightStore.sightInfo[languageStore.language].left.heading
|
||||
: articlesStore.articles[languageStore.language].find(
|
||||
(article) => article.id === response.data.left_article
|
||||
)?.heading,
|
||||
body: editSightStore.sightInfo[languageStore.language].left.loaded
|
||||
? editSightStore.sightInfo[languageStore.language].left.body
|
||||
: articlesStore.articles[languageStore.language].find(
|
||||
(article) => article.id === response.data.left_article
|
||||
)?.body,
|
||||
},
|
||||
},
|
||||
};
|
||||
console.log(editSightStore.sightInfo);
|
||||
});
|
||||
};
|
||||
// runInAction(() => {
|
||||
// this.sight = response.data;
|
||||
// editSightStore.sightInfo = {
|
||||
// ...editSightStore.sightInfo,
|
||||
// id: response.data.id,
|
||||
// city_id: response.data.city_id,
|
||||
// city: response.data.city,
|
||||
// latitude: response.data.latitude,
|
||||
// longitude: response.data.longitude,
|
||||
// thumbnail: response.data.thumbnail,
|
||||
// watermark_lu: response.data.watermark_lu,
|
||||
// watermark_rd: response.data.watermark_rd,
|
||||
// left_article: response.data.left_article,
|
||||
// preview_media: response.data.preview_media,
|
||||
// video_preview: response.data.video_preview,
|
||||
|
||||
// [languageStore.language]: {
|
||||
// info: {
|
||||
// name: response.data.name,
|
||||
// address: response.data.address,
|
||||
// },
|
||||
// left: {
|
||||
// heading: articlesStore.articles[languageStore.language].find(
|
||||
// (article) => article.id === response.data.left_article
|
||||
// )?.heading,
|
||||
// body: articlesStore.articles[languageStore.language].find(
|
||||
// },
|
||||
// },
|
||||
// };
|
||||
// });
|
||||
// };
|
||||
|
||||
createSightAction = async (
|
||||
city: number,
|
||||
@ -160,12 +151,32 @@ class SightsStore {
|
||||
language: Language,
|
||||
content: Partial<CreateSight[Language]>
|
||||
) => {
|
||||
runInAction(() => {
|
||||
this.createSight[language] = {
|
||||
...this.createSight[language],
|
||||
this.createSight[language] = {
|
||||
...this.createSight[language],
|
||||
...content,
|
||||
};
|
||||
};
|
||||
|
||||
updateSight = (
|
||||
language: Language,
|
||||
content: Partial<SightBaseInfo[Language]>,
|
||||
common: boolean
|
||||
) => {
|
||||
if (common) {
|
||||
// @ts-ignore
|
||||
this.sight!.common = {
|
||||
// @ts-ignore
|
||||
...this.sight!.common,
|
||||
...content,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// @ts-ignore
|
||||
this.sight![language] = {
|
||||
// @ts-ignore
|
||||
...this.sight![language],
|
||||
...content,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
clearCreateSight = () => {
|
||||
|
@ -23,7 +23,7 @@ export const CoordinatesInput = ({
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<TextField
|
||||
label="Координаты"
|
||||
value={inputValue}
|
||||
value={inputValue ?? ""}
|
||||
onChange={(e) => {
|
||||
setInputValue(e.target.value);
|
||||
}}
|
||||
|
@ -5,7 +5,7 @@ import TableContainer from "@mui/material/TableContainer";
|
||||
import TableHead from "@mui/material/TableHead";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import { Check, RotateCcw, Send, X } from "lucide-react";
|
||||
import { Check, RotateCcw, X } from "lucide-react";
|
||||
import {
|
||||
authInstance,
|
||||
devicesStore,
|
||||
@ -49,7 +49,7 @@ function createData(
|
||||
}
|
||||
|
||||
// Keep the rows function as you provided it, without additional filters
|
||||
const rows = (devices: any[], vehicles: any[]) => {
|
||||
const rows = (vehicles: any[]) => {
|
||||
return vehicles.map((vehicle) => {
|
||||
return createData(
|
||||
vehicle?.vehicle?.tail_number ?? "1243000", // Using tail_number as UUID, as in your original code
|
||||
@ -72,11 +72,11 @@ export const DevicesTable = observer(() => {
|
||||
toggleSendSnapshotModal,
|
||||
} = devicesStore;
|
||||
const { snapshots, getSnapshots } = snapshotStore;
|
||||
const { vehicles, getVehicles } = vehicleStore;
|
||||
const { getVehicles } = vehicleStore;
|
||||
const [selectedDevices, setSelectedDevices] = useState<string[]>([]);
|
||||
|
||||
// Get the current list of rows displayed in the table
|
||||
const currentRows = rows(devices, vehicles);
|
||||
const currentRows = rows(devices);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
|
@ -2,7 +2,6 @@ import * as React from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { Menu, ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { AppBar } from "./ui/AppBar";
|
||||
|
@ -6,46 +6,45 @@ import {
|
||||
Typography,
|
||||
Paper,
|
||||
Tooltip,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
MenuItem,
|
||||
Menu as MuiMenu,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
BackButton,
|
||||
sightsStore,
|
||||
TabPanel,
|
||||
languageStore,
|
||||
CreateSight,
|
||||
Language,
|
||||
cityStore,
|
||||
CoordinatesInput,
|
||||
editSightStore,
|
||||
SelectMediaDialog,
|
||||
PreviewMediaDialog,
|
||||
SightLanguageInfo,
|
||||
SightCommonInfo,
|
||||
} from "@shared";
|
||||
import { LanguageSwitcher } from "@widgets";
|
||||
import { Info, ImagePlus } from "lucide-react";
|
||||
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRef, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
// Мокап для всплывающей подсказки
|
||||
|
||||
export const InformationTab = observer(
|
||||
({ value, index }: { value: number; index: number }) => {
|
||||
const { cities } = cityStore;
|
||||
const [isMediaModalOpen, setIsMediaModalOpen] = useState(false);
|
||||
const [, setIsMediaModalOpen] = useState(false);
|
||||
const [mediaId, setMediaId] = useState<string>("");
|
||||
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
|
||||
const { createSight, updateCreateSight, createSightAction } = sightsStore;
|
||||
const { sightInfo } = editSightStore;
|
||||
const [city, setCity] = useState<number>(sightInfo.city_id ?? 0);
|
||||
const [coordinates, setCoordinates] = useState({
|
||||
latitude: sightInfo.latitude ?? 0,
|
||||
longitude: sightInfo.longitude ?? 0,
|
||||
});
|
||||
|
||||
const { language } = languageStore;
|
||||
const { sight, updateSightInfo } = editSightStore;
|
||||
|
||||
const data = sight[language];
|
||||
const common = sight.common;
|
||||
|
||||
const [, setCity] = useState<number>(common.city_id ?? 0);
|
||||
const [coordinates, setCoordinates] = useState<string>(`0 0`);
|
||||
|
||||
const token = localStorage.getItem("token");
|
||||
|
||||
// Menu state for each media button
|
||||
@ -63,6 +62,14 @@ export const InformationTab = observer(
|
||||
setActiveMenuType(type);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Показывать только при инициализации (не менять при ошибках пользователя)
|
||||
if (common.latitude !== 0 || common.longitude !== 0) {
|
||||
setCoordinates(`${common.latitude} ${common.longitude}`);
|
||||
}
|
||||
// если координаты обнулились — оставить поле как есть
|
||||
}, [common.latitude, common.longitude]);
|
||||
|
||||
const handleMenuClose = () => {
|
||||
setMenuAnchorEl(null);
|
||||
setActiveMenuType(null);
|
||||
@ -77,7 +84,7 @@ export const InformationTab = observer(
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleMediaSelect = (selectedMediaId: string) => {
|
||||
const handleMediaSelect = () => {
|
||||
if (!activeMenuType) return;
|
||||
|
||||
// Close the dialog
|
||||
@ -87,17 +94,10 @@ export const InformationTab = observer(
|
||||
|
||||
const handleChange = (
|
||||
language: Language,
|
||||
content: Partial<CreateSight[Language]>
|
||||
content: Partial<SightLanguageInfo | SightCommonInfo>,
|
||||
common: boolean = false
|
||||
) => {
|
||||
updateCreateSight(language, content);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
await createSightAction(city, coordinates);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
updateSightInfo(language, content, common);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -135,7 +135,7 @@ export const InformationTab = observer(
|
||||
>
|
||||
<TextField
|
||||
label={`Название (${language.toUpperCase()})`}
|
||||
value={sightInfo[language]?.info?.name ?? ""}
|
||||
value={data.name}
|
||||
onChange={(e) => {
|
||||
handleChange(language as Language, {
|
||||
name: e.target.value,
|
||||
@ -147,7 +147,7 @@ export const InformationTab = observer(
|
||||
|
||||
<TextField
|
||||
label="Адрес"
|
||||
value={sightInfo[language]?.info?.address ?? ""}
|
||||
value={data.address}
|
||||
onChange={(e) => {
|
||||
handleChange(language as Language, {
|
||||
address: e.target.value,
|
||||
@ -158,20 +158,57 @@ export const InformationTab = observer(
|
||||
/>
|
||||
|
||||
<Autocomplete
|
||||
options={cities}
|
||||
value={cities.find((city) => city.id === sightInfo.city_id)}
|
||||
options={cities ?? []}
|
||||
value={
|
||||
cities.find((city) => city.id === common.city_id) ?? null
|
||||
}
|
||||
getOptionLabel={(option) => option.name}
|
||||
onChange={(_, value) => {
|
||||
setCity(value?.id ?? 0);
|
||||
handleChange(
|
||||
language as Language,
|
||||
{
|
||||
city_id: value?.id ?? 0,
|
||||
},
|
||||
true
|
||||
);
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField {...params} label="Город" />
|
||||
)}
|
||||
/>
|
||||
|
||||
<CoordinatesInput
|
||||
initialValue={coordinates}
|
||||
setValue={setCoordinates}
|
||||
<TextField
|
||||
label="Координаты"
|
||||
value={coordinates}
|
||||
onChange={(e) => {
|
||||
const input = e.target.value;
|
||||
setCoordinates(input); // показываем как есть
|
||||
|
||||
const [latStr, lonStr] = input.split(/\s+/); // учитываем любые пробелы
|
||||
|
||||
const lat = parseFloat(latStr);
|
||||
const lon = parseFloat(lonStr);
|
||||
|
||||
// Проверка, что обе координаты валидные числа
|
||||
const isValidLat = !isNaN(lat);
|
||||
const isValidLon = !isNaN(lon);
|
||||
|
||||
if (isValidLat && isValidLon) {
|
||||
handleChange(language as Language, {
|
||||
latitude: lat,
|
||||
longitude: lon,
|
||||
});
|
||||
} else {
|
||||
handleChange(language as Language, {
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
});
|
||||
}
|
||||
}}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
placeholder="Введите координаты в формате: широта долгота"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@ -222,11 +259,9 @@ export const InformationTab = observer(
|
||||
justifyContent: "center",
|
||||
borderRadius: 1,
|
||||
mb: 1,
|
||||
cursor: editSightStore.sightInfo?.thumbnail
|
||||
? "pointer"
|
||||
: "default", // Only clickable if there's an image
|
||||
cursor: common.thumbnail ? "pointer" : "default",
|
||||
"&:hover": {
|
||||
backgroundColor: editSightStore.sightInfo?.thumbnail
|
||||
backgroundColor: common.thumbnail
|
||||
? "red.300"
|
||||
: "grey.200",
|
||||
},
|
||||
@ -235,16 +270,16 @@ export const InformationTab = observer(
|
||||
setIsMediaModalOpen(true);
|
||||
}}
|
||||
>
|
||||
{editSightStore.sightInfo?.thumbnail ? (
|
||||
{common.thumbnail ? (
|
||||
<img
|
||||
src={`${import.meta.env.VITE_KRBL_MEDIA}${
|
||||
editSightStore.sightInfo?.thumbnail
|
||||
common.thumbnail
|
||||
}/download?token=${token}`}
|
||||
alt="Логотип"
|
||||
style={{ maxWidth: "100%", maxHeight: "100%" }}
|
||||
onClick={() => {
|
||||
setIsPreviewMediaOpen(true);
|
||||
setMediaId(editSightStore.sightInfo?.thumbnail);
|
||||
setMediaId(common.thumbnail);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
@ -297,31 +332,28 @@ export const InformationTab = observer(
|
||||
justifyContent: "center",
|
||||
borderRadius: 1,
|
||||
mb: 1,
|
||||
cursor: editSightStore.sightInfo?.watermark_lu
|
||||
? "pointer"
|
||||
: "default", // Only clickable if there's an image
|
||||
cursor: common.watermark_lu ? "pointer" : "default",
|
||||
"&:hover": {
|
||||
backgroundColor: editSightStore.sightInfo
|
||||
?.watermark_lu
|
||||
backgroundColor: common.watermark_lu
|
||||
? "grey.300"
|
||||
: "grey.200",
|
||||
},
|
||||
}}
|
||||
onClick={() => {
|
||||
setIsPreviewMediaOpen(true);
|
||||
setMediaId(editSightStore.sightInfo?.watermark_lu);
|
||||
setMediaId(common.watermark_lu);
|
||||
}}
|
||||
>
|
||||
{editSightStore.sightInfo?.watermark_lu ? (
|
||||
{common.watermark_lu ? (
|
||||
<img
|
||||
src={`${import.meta.env.VITE_KRBL_MEDIA}${
|
||||
editSightStore.sightInfo?.watermark_lu
|
||||
common.watermark_lu
|
||||
}/download?token=${token}`}
|
||||
alt="Знак л.в"
|
||||
style={{ maxWidth: "100%", maxHeight: "100%" }}
|
||||
onClick={() => {
|
||||
setIsMediaModalOpen(true);
|
||||
setMediaId(editSightStore.sightInfo?.watermark_lu);
|
||||
setMediaId(common.watermark_lu);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
@ -375,28 +407,28 @@ export const InformationTab = observer(
|
||||
justifyContent: "center",
|
||||
borderRadius: 1,
|
||||
mb: 1,
|
||||
cursor: editSightStore.sightInfo?.watermark_rd
|
||||
? "pointer"
|
||||
: "default", // Only clickable if there's an image
|
||||
cursor: common.watermark_rd ? "pointer" : "default",
|
||||
"&:hover": {
|
||||
backgroundColor: editSightStore.sightInfo
|
||||
?.watermark_rd
|
||||
backgroundColor: common.watermark_rd
|
||||
? "grey.300"
|
||||
: "grey.200",
|
||||
},
|
||||
}}
|
||||
onClick={() => editSightStore.sightInfo?.watermark_rd}
|
||||
onClick={() => {
|
||||
setIsMediaModalOpen(true);
|
||||
setMediaId(common.watermark_rd);
|
||||
}}
|
||||
>
|
||||
{editSightStore.sightInfo?.watermark_rd ? (
|
||||
{common.watermark_rd ? (
|
||||
<img
|
||||
src={`${import.meta.env.VITE_KRBL_MEDIA}${
|
||||
editSightStore.sightInfo?.watermark_rd
|
||||
common.watermark_rd
|
||||
}/download?token=${token}`}
|
||||
alt="Знак п.в"
|
||||
style={{ maxWidth: "100%", maxHeight: "100%" }}
|
||||
onClick={() => {
|
||||
setIsPreviewMediaOpen(true);
|
||||
setMediaId(editSightStore.sightInfo?.watermark_rd);
|
||||
setMediaId(common.watermark_rd);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
@ -432,7 +464,13 @@ export const InformationTab = observer(
|
||||
justifyContent: "flex-end", // Align to the right
|
||||
}}
|
||||
>
|
||||
<Button variant="contained" color="success" onClick={handleSave}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="success"
|
||||
onClick={() => {
|
||||
console.log(sight);
|
||||
}}
|
||||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
</Box>
|
||||
@ -463,7 +501,7 @@ export const InformationTab = observer(
|
||||
setIsAddMediaOpen(false);
|
||||
setActiveMenuType(null);
|
||||
}}
|
||||
onSelectArticle={handleMediaSelect}
|
||||
onSelectMedia={handleMediaSelect}
|
||||
/>
|
||||
|
||||
<PreviewMediaDialog
|
||||
|
@ -14,79 +14,44 @@ import {
|
||||
ReactMarkdownEditor,
|
||||
} from "@widgets";
|
||||
import { Unlink, Trash2, ImagePlus } from "lucide-react";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useState, useCallback } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
export const LeftWidgetTab = observer(
|
||||
({ value, index }: { value: number; index: number }) => {
|
||||
const { sightInfo, updateSightInfo, loadSightInfo } = editSightStore;
|
||||
const { articleLoading, getArticleByArticleId } = articlesStore;
|
||||
const { language } = languageStore;
|
||||
const { sight, updateSightInfo } = editSightStore;
|
||||
const { getArticleByArticleId } = articlesStore;
|
||||
|
||||
const linkedArticle = getArticleByArticleId.get(); // Получаем связанную статью
|
||||
const data = sightInfo[languageStore.language]; // Получаем данные для текущего языка
|
||||
|
||||
useEffect(() => {
|
||||
// Этот useEffect должен загружать данные ИЗ СВЯЗАННОЙ СТАТЬИ
|
||||
// ТОЛЬКО ЕСЛИ данные для ТЕКУЩЕГО ЯЗЫКА еще не были загружены
|
||||
// или если sightInfo.left_article изменился (т.е. привязали новую статью).
|
||||
|
||||
// Мы также должны учитывать, что linkedArticle может измениться (т.е. новую статью привязали)
|
||||
// или language изменился.
|
||||
// Если для текущего языка данные еще не "загружены" (`loaded: false`),
|
||||
// тогда мы берем их из `linkedArticle` и инициализируем.
|
||||
console.log("data.left.loaded", data.left.loaded);
|
||||
if (!data.left.loaded) {
|
||||
// <--- КЛЮЧЕВОЕ УСЛОВИЕ
|
||||
if (linkedArticle && !articleLoading) {
|
||||
console.log("loadSightInfo", linkedArticle, language);
|
||||
loadSightInfo(
|
||||
languageStore.language,
|
||||
linkedArticle.heading,
|
||||
linkedArticle.body || "",
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
||||
// Зависимости: linkedArticle (для реакции на изменение привязанной статьи),
|
||||
// languageStore.language (для реакции на изменение языка),
|
||||
// loadSightInfo (чтобы useEffect знал об изменениях в функции),
|
||||
// data.left.loaded (чтобы useEffect перепроверил условие, когда этот флаг изменится).
|
||||
// Важно: если data.left.loaded становится true, то этот эффект не будет
|
||||
// перезапускаться для того же языка.
|
||||
}, [
|
||||
linkedArticle?.heading,
|
||||
language,
|
||||
loadSightInfo,
|
||||
data.left.loaded,
|
||||
articleLoading,
|
||||
]);
|
||||
const data = sight[languageStore.language]; // Получаем данные для текущего языка
|
||||
|
||||
const [isSelectMediaDialogOpen, setIsSelectMediaDialogOpen] =
|
||||
useState(false);
|
||||
|
||||
const handleOpenMediaDialog = useCallback(() => {
|
||||
setIsSelectMediaDialogOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleMediaSelected = useCallback(
|
||||
(selectedMedia: any) => {
|
||||
// При выборе медиа, обновляем данные для ТЕКУЩЕГО ЯЗЫКА
|
||||
// сохраняя текущие heading и body.
|
||||
updateSightInfo(
|
||||
languageStore.language,
|
||||
data.left.heading,
|
||||
data.left.body,
|
||||
selectedMedia
|
||||
);
|
||||
setIsSelectMediaDialogOpen(false);
|
||||
},
|
||||
[
|
||||
const handleMediaSelected = useCallback(() => {
|
||||
// При выборе медиа, обновляем данные для ТЕКУЩЕГО ЯЗЫКА
|
||||
// сохраняя текущие heading и body.
|
||||
updateSightInfo(
|
||||
languageStore.language,
|
||||
data.left.heading,
|
||||
data.left.body,
|
||||
updateSightInfo,
|
||||
]
|
||||
);
|
||||
{
|
||||
left: {
|
||||
heading: data.left.heading,
|
||||
body: data.left.body,
|
||||
},
|
||||
},
|
||||
false
|
||||
);
|
||||
setIsSelectMediaDialogOpen(false);
|
||||
}, [
|
||||
languageStore.language,
|
||||
{
|
||||
left: {
|
||||
heading: data.left.heading,
|
||||
body: data.left.body,
|
||||
},
|
||||
},
|
||||
false,
|
||||
]);
|
||||
|
||||
const handleCloseMediaDialog = useCallback(() => {
|
||||
setIsSelectMediaDialogOpen(false);
|
||||
@ -150,13 +115,17 @@ export const LeftWidgetTab = observer(
|
||||
>
|
||||
<TextField
|
||||
label="Название информации"
|
||||
value={data.left.heading}
|
||||
value={data?.left?.heading}
|
||||
onChange={(e) =>
|
||||
updateSightInfo(
|
||||
languageStore.language,
|
||||
e.target.value,
|
||||
data.left.body,
|
||||
data.left.media
|
||||
{
|
||||
left: {
|
||||
heading: e.target.value,
|
||||
body: data.left.body,
|
||||
},
|
||||
},
|
||||
false
|
||||
)
|
||||
}
|
||||
variant="outlined"
|
||||
@ -164,19 +133,23 @@ export const LeftWidgetTab = observer(
|
||||
/>
|
||||
|
||||
<ReactMarkdownEditor
|
||||
value={data.left.body}
|
||||
value={data?.left?.body}
|
||||
onChange={(value) =>
|
||||
updateSightInfo(
|
||||
languageStore.language,
|
||||
data.left.heading,
|
||||
value,
|
||||
data.left.media
|
||||
{
|
||||
left: {
|
||||
heading: data.left.heading,
|
||||
body: value,
|
||||
},
|
||||
},
|
||||
false
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Блок МЕДИА для статьи */}
|
||||
<Paper elevation={1} sx={{ padding: 2, mt: 1 }}>
|
||||
{/* <Paper elevation={1} sx={{ padding: 2, mt: 1 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
МЕДИА
|
||||
</Typography>
|
||||
@ -226,16 +199,21 @@ export const LeftWidgetTab = observer(
|
||||
onClick={() =>
|
||||
updateSightInfo(
|
||||
languageStore.language,
|
||||
data.left.heading,
|
||||
data.left.body,
|
||||
null
|
||||
{
|
||||
left: {
|
||||
heading: data.left.heading,
|
||||
body: data.left.body,
|
||||
media: null,
|
||||
},
|
||||
},
|
||||
false
|
||||
)
|
||||
}
|
||||
>
|
||||
Удалить медиа
|
||||
</Button>
|
||||
)}
|
||||
</Paper>
|
||||
</Paper> */}
|
||||
</Box>
|
||||
|
||||
{/* Правая колонка: Предпросмотр */}
|
||||
@ -247,7 +225,6 @@ export const LeftWidgetTab = observer(
|
||||
gap: 1.5,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6">Предпросмотр</Typography>
|
||||
<Paper
|
||||
elevation={3}
|
||||
sx={{
|
||||
@ -263,8 +240,7 @@ export const LeftWidgetTab = observer(
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
{/* Медиа в превью (если есть) */}
|
||||
{data.left.media ? (
|
||||
{/* {data.left.media?.filename ? (
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
@ -276,7 +252,7 @@ export const LeftWidgetTab = observer(
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={data.left.media.filename}
|
||||
src={data.left.media?.filename ?? ""}
|
||||
alt="Превью медиа"
|
||||
style={{
|
||||
objectFit: "cover",
|
||||
@ -286,19 +262,21 @@ export const LeftWidgetTab = observer(
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: 200,
|
||||
backgroundColor: "grey.300",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<ImagePlus size={48} color="grey" />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
)} */}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: 200,
|
||||
backgroundColor: "grey.300",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<ImagePlus size={48} color="grey" />
|
||||
</Box>
|
||||
|
||||
{/* Заголовок в превью */}
|
||||
<Box
|
||||
@ -313,7 +291,7 @@ export const LeftWidgetTab = observer(
|
||||
component="h2"
|
||||
sx={{ wordBreak: "break-word" }}
|
||||
>
|
||||
{data.left.heading || "Название информации"}
|
||||
{data?.left?.heading || "Название информации"}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
@ -324,7 +302,7 @@ export const LeftWidgetTab = observer(
|
||||
flexGrow: 1,
|
||||
}}
|
||||
>
|
||||
<ReactMarkdownComponent value={data.left.body} />
|
||||
<ReactMarkdownComponent value={data?.left?.body} />
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
@ -1,404 +1,371 @@
|
||||
// RightWidgetTab.tsx
|
||||
import { Box, Button, Paper, TextField, Typography } from "@mui/material";
|
||||
import {
|
||||
TabPanel,
|
||||
Box,
|
||||
Button,
|
||||
List,
|
||||
ListItemButton,
|
||||
ListItemText,
|
||||
Paper,
|
||||
Typography,
|
||||
Menu,
|
||||
MenuItem,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
articlesStore,
|
||||
BackButton,
|
||||
languageStore, // Предполагаем, что он есть в @shared
|
||||
Language, // Предполагаем, что он есть в @shared
|
||||
// SelectArticleModal, // Добавим позже
|
||||
// articlesStore, // Добавим позже
|
||||
SelectArticleModal,
|
||||
TabPanel,
|
||||
} from "@shared";
|
||||
import { LanguageSwitcher } from "@widgets"; // Предполагаем, что LanguageSwitcher у вас есть
|
||||
import { SightEdit } from "@widgets";
|
||||
import { Plus } from "lucide-react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useState, useMemo, useEffect } from "react";
|
||||
import { editSightStore, BlockItem } from "@shared"; // Путь к вашему стору
|
||||
import { useState } from "react";
|
||||
|
||||
// Импортируем сюда же определения 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" },
|
||||
// --- Mock Data (can be moved to a separate file or fetched from an API) ---
|
||||
const mockRightWidgetBlocks = [
|
||||
{ id: "preview_media", name: "Превью-медиа", type: "special" },
|
||||
{ id: "article_1", name: "1. История", type: "article" },
|
||||
{ id: "article_2", name: "2. Факты", type: "article" },
|
||||
{
|
||||
id: "article_3",
|
||||
name: "3. Блокада (Пример длинного названия)",
|
||||
type: "article",
|
||||
},
|
||||
];
|
||||
|
||||
interface RightWidgetTabProps {
|
||||
value: number;
|
||||
index: number;
|
||||
const mockSelectedBlockData = {
|
||||
id: "article_1",
|
||||
heading: "История основания Санкт-Петербурга",
|
||||
body: "## Начало\nГород был основан 27 мая 1703 года Петром I...",
|
||||
media: [],
|
||||
};
|
||||
|
||||
// --- ArticleListSidebar Component ---
|
||||
interface ArticleBlock {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
linkedArticleId?: string; // Added for linked articles
|
||||
}
|
||||
|
||||
export const RightWidgetTab = observer(
|
||||
({ value, index }: RightWidgetTabProps) => {
|
||||
const { language } = languageStore; // Текущий язык
|
||||
const { sightInfo } = editSightStore; // Данные достопримечательности
|
||||
interface ArticleListSidebarProps {
|
||||
blocks: ArticleBlock[];
|
||||
selectedBlockId: string | null;
|
||||
onSelectBlock: (blockId: string) => void;
|
||||
onCreateNew: () => void;
|
||||
onSelectExisting: () => void;
|
||||
}
|
||||
|
||||
// 1. Структура блоков: порядок, тип, связи (не сам контент)
|
||||
// Имена nameForSidebar будут динамически браться из sightInfo или articlesStore
|
||||
const [blockItemsStructure, setBlockItemsStructure] = useState<
|
||||
Omit<BlockItem, "nameForSidebar">[]
|
||||
>(initialBlockStructures);
|
||||
const ArticleListSidebar = ({
|
||||
blocks,
|
||||
selectedBlockId,
|
||||
onSelectBlock,
|
||||
onCreateNew,
|
||||
onSelectExisting,
|
||||
}: ArticleListSidebarProps) => {
|
||||
const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null);
|
||||
|
||||
// 2. ID выбранного блока для редактирования
|
||||
const [selectedBlockId, setSelectedBlockId] = useState<string | null>(
|
||||
() => {
|
||||
// По умолчанию выбираем первый блок, если он есть
|
||||
return initialBlockStructures.length > 0
|
||||
? initialBlockStructures[0].id
|
||||
: null;
|
||||
}
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setMenuAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleMenuClose = () => {
|
||||
setMenuAnchorEl(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper
|
||||
elevation={2}
|
||||
sx={{
|
||||
width: 260,
|
||||
minWidth: 240,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
padding: 1.5,
|
||||
borderRadius: 2,
|
||||
border: "1px solid",
|
||||
borderColor: "divider",
|
||||
}}
|
||||
>
|
||||
<List
|
||||
dense
|
||||
sx={{
|
||||
overflowY: "auto",
|
||||
flexGrow: 1,
|
||||
maxHeight: "calc(100% - 60px)",
|
||||
}}
|
||||
>
|
||||
{blocks.map((block) => (
|
||||
<ListItemButton
|
||||
key={block.id}
|
||||
selected={selectedBlockId === block.id}
|
||||
onClick={() => onSelectBlock(block.id)}
|
||||
sx={{
|
||||
borderRadius: 1,
|
||||
mb: 0.5,
|
||||
backgroundColor:
|
||||
selectedBlockId === block.id ? "primary.light" : "transparent",
|
||||
"&.Mui-selected": {
|
||||
backgroundColor: "primary.main",
|
||||
color: "primary.contrastText",
|
||||
"&:hover": {
|
||||
backgroundColor: "primary.dark",
|
||||
},
|
||||
},
|
||||
"&:hover": {
|
||||
backgroundColor:
|
||||
selectedBlockId !== block.id ? "action.hover" : undefined,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
primary={block.name}
|
||||
primaryTypographyProps={{
|
||||
fontWeight: selectedBlockId === block.id ? "bold" : "normal",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
/>
|
||||
</ListItemButton>
|
||||
))}
|
||||
</List>
|
||||
|
||||
<button
|
||||
className="w-8 h-8 rounded-full bg-blue-500 text-white flex items-center justify-center hover:bg-blue-600 transition-colors"
|
||||
onClick={handleMenuOpen}
|
||||
>
|
||||
<Plus color="white" />
|
||||
</button>
|
||||
<Menu
|
||||
anchorEl={menuAnchorEl}
|
||||
open={Boolean(menuAnchorEl)}
|
||||
onClose={handleMenuClose}
|
||||
anchorOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "right",
|
||||
}}
|
||||
>
|
||||
<MenuItem onClick={onCreateNew}>Создать новую</MenuItem>
|
||||
<MenuItem onClick={onSelectExisting}>Выбрать существующую</MenuItem>
|
||||
</Menu>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
// --- ArticleEditorPane Component ---
|
||||
interface ArticleData {
|
||||
id: string;
|
||||
heading: string;
|
||||
body: string;
|
||||
media: any[]; // Define a proper type for media if available
|
||||
}
|
||||
|
||||
interface ArticleEditorPaneProps {
|
||||
articleData: ArticleData | null;
|
||||
}
|
||||
|
||||
const ArticleEditorPane = ({ articleData }: ArticleEditorPaneProps) => {
|
||||
if (!articleData) {
|
||||
return (
|
||||
<Paper
|
||||
elevation={2}
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
padding: 2.5,
|
||||
borderRadius: 2,
|
||||
border: "1px solid",
|
||||
borderColor: "divider",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" color="text.secondary">
|
||||
Выберите блок для редактирования
|
||||
</Typography>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Состояние для модального окна выбора существующей статьи (добавим позже)
|
||||
// const [isSelectModalOpen, setIsSelectModalOpen] = useState(false);
|
||||
return (
|
||||
<Paper
|
||||
elevation={2}
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
padding: 2.5,
|
||||
borderRadius: 2,
|
||||
border: "1px solid",
|
||||
borderColor: "divider",
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
<SightEdit />
|
||||
<Paper elevation={1} sx={{ padding: 2, mt: 1, width: "75%" }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
МЕДИА
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: 100,
|
||||
backgroundColor: "grey.100",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: 1,
|
||||
mb: 1,
|
||||
border: "2px dashed",
|
||||
borderColor: "grey.300",
|
||||
}}
|
||||
>
|
||||
<Typography color="text.secondary">Нет медиа</Typography>
|
||||
</Box>
|
||||
<Button variant="contained">Выбрать/Загрузить медиа</Button>
|
||||
</Paper>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
// --- Производные данные (Derived State) ---
|
||||
// --- RightWidgetTab (Parent) Component ---
|
||||
export const RightWidgetTab = observer(
|
||||
({ value, index }: { value: number; index: number }) => {
|
||||
const [rightWidgetBlocks, setRightWidgetBlocks] = useState<ArticleBlock[]>(
|
||||
mockRightWidgetBlocks
|
||||
);
|
||||
const [selectedBlockId, setSelectedBlockId] = useState<string | null>(
|
||||
mockRightWidgetBlocks[1]?.id || null
|
||||
);
|
||||
const [isSelectModalOpen, setIsSelectModalOpen] = useState(false);
|
||||
|
||||
// Блоки для отображения в сайдбаре (с локализованными именами)
|
||||
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);
|
||||
console.log("Selected block:", 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()})...`,
|
||||
});
|
||||
}
|
||||
});
|
||||
const handleCreateNew = () => {
|
||||
const newBlockId = `article_${Date.now()}`;
|
||||
setRightWidgetBlocks((prevBlocks) => [
|
||||
...prevBlocks,
|
||||
{
|
||||
id: newBlockId,
|
||||
name: `${
|
||||
prevBlocks.filter((b) => b.type === "article").length + 1
|
||||
}. Новый блок`,
|
||||
type: "article",
|
||||
},
|
||||
]);
|
||||
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 handleSelectExisting = () => {
|
||||
setIsSelectModalOpen(true);
|
||||
};
|
||||
|
||||
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 handleCloseSelectModal = () => {
|
||||
setIsSelectModalOpen(false);
|
||||
};
|
||||
|
||||
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 handleSelectArticle = (articleId: string) => {
|
||||
// @ts-ignore
|
||||
const article = articlesStore.articles.find((a) => a.id === articleId);
|
||||
if (article) {
|
||||
const newBlockId = `article_linked_${article.id}_${Date.now()}`;
|
||||
setRightWidgetBlocks((prevBlocks) => [
|
||||
...prevBlocks,
|
||||
{
|
||||
id: newBlockId,
|
||||
name: `${
|
||||
prevBlocks.filter((b) => b.type === "article").length + 1
|
||||
}. ${article.service_name}`,
|
||||
type: "article",
|
||||
linkedArticleId: article.id,
|
||||
},
|
||||
]);
|
||||
setSelectedBlockId(newBlockId);
|
||||
}
|
||||
handleCloseSelectModal();
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
console.log(
|
||||
"Сохранение Right Widget:",
|
||||
JSON.stringify(editSightStore.sightInfo, null, 2)
|
||||
);
|
||||
// Здесь будет логика отправки editSightStore.sightInfo на сервер
|
||||
alert("Данные для сохранения (см. консоль)");
|
||||
console.log("Saving right widget...");
|
||||
// Implement save logic here, e.g., send data to an API
|
||||
};
|
||||
|
||||
// --- Инициализация контента в сторе для 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()}.`,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}, []); // Запускается один раз при монтировании
|
||||
// Determine the current block data to pass to the editor pane
|
||||
const currentBlockToEdit = selectedBlockId
|
||||
? selectedBlockId === mockSelectedBlockData.id
|
||||
? mockSelectedBlockData
|
||||
: {
|
||||
id: selectedBlockId,
|
||||
heading:
|
||||
rightWidgetBlocks.find((b) => b.id === selectedBlockId)?.name ||
|
||||
"Заголовок...",
|
||||
body: "Содержимое...",
|
||||
media: [],
|
||||
}
|
||||
: null;
|
||||
|
||||
// Get list of already linked article IDs
|
||||
const linkedArticleIds = rightWidgetBlocks
|
||||
.filter((block) => block.linkedArticleId)
|
||||
.map((block) => block.linkedArticleId as string);
|
||||
|
||||
return (
|
||||
<TabPanel value={value} index={index}>
|
||||
<LanguageSwitcher />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "100%",
|
||||
minHeight: "calc(100vh - 200px)",
|
||||
minHeight: "calc(100vh - 200px)", // Adjust as needed
|
||||
gap: 2,
|
||||
paddingBottom: "70px",
|
||||
paddingBottom: "70px", // Space for the save button
|
||||
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>
|
||||
<Box sx={{ display: "flex", flexGrow: 1, gap: 2.5 }}>
|
||||
<ArticleListSidebar
|
||||
blocks={rightWidgetBlocks}
|
||||
selectedBlockId={selectedBlockId}
|
||||
onSelectBlock={handleSelectBlock}
|
||||
onCreateNew={handleCreateNew}
|
||||
onSelectExisting={handleSelectExisting}
|
||||
/>
|
||||
|
||||
{/* Компонент редактора выбранного блока */}
|
||||
<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>
|
||||
<ArticleEditorPane articleData={currentBlockToEdit} />
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
padding: 2,
|
||||
backgroundColor: "background.paper",
|
||||
borderTop: "1px solid",
|
||||
borderColor: "divider",
|
||||
zIndex: 10,
|
||||
backgroundColor: "background.paper", // Ensure button is visible
|
||||
width: "100%", // Cover the full width to make it a sticky footer
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="success"
|
||||
onClick={handleSave}
|
||||
size="large"
|
||||
>
|
||||
<Button variant="contained" color="success" onClick={handleSave}>
|
||||
Сохранить изменения
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
{/* <SelectArticleModal open={isSelectModalOpen} ... /> */}
|
||||
|
||||
<SelectArticleModal
|
||||
open={isSelectModalOpen}
|
||||
onClose={handleCloseSelectModal}
|
||||
onSelectArticle={handleSelectArticle}
|
||||
linkedArticleIds={linkedArticleIds}
|
||||
/>
|
||||
</TabPanel>
|
||||
);
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import Paper from "@mui/material/Paper";
|
||||
import { authInstance, cityStore, languageStore, sightsStore } from "@shared";
|
||||
import { useEffect } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Button, Checkbox } from "@mui/material";
|
||||
import { Button } from "@mui/material";
|
||||
import { LanguageSwitcher } from "@widgets";
|
||||
import { Pencil, Trash2 } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { articlesStore } from "@shared";
|
||||
import { articlesStore, languageStore } from "@shared";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
@ -44,7 +44,7 @@ export const SelectArticleModal = observer(
|
||||
useEffect(() => {
|
||||
if (hoveredArticleId) {
|
||||
hoverTimerRef.current = setTimeout(() => {
|
||||
getArticle(hoveredArticleId);
|
||||
getArticle(Number(hoveredArticleId));
|
||||
}, 200);
|
||||
}
|
||||
|
||||
@ -66,7 +66,8 @@ export const SelectArticleModal = observer(
|
||||
}
|
||||
};
|
||||
|
||||
const filteredArticles = articles
|
||||
const filteredArticles = articles[languageStore.language]
|
||||
// @ts-ignore
|
||||
.filter((article) => !linkedArticleIds.includes(article.id))
|
||||
.filter((article) =>
|
||||
article.service_name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
@ -96,11 +97,12 @@ export const SelectArticleModal = observer(
|
||||
}}
|
||||
/>
|
||||
<List sx={{ flexGrow: 1, overflowY: "auto" }}>
|
||||
{/* @ts-ignore */}
|
||||
{filteredArticles.map((article) => (
|
||||
<ListItemButton
|
||||
key={article.id}
|
||||
onClick={() => onSelectArticle(article.id)}
|
||||
onMouseEnter={() => handleArticleHover(article.id)}
|
||||
onClick={() => onSelectArticle(article.id.toString())}
|
||||
onMouseEnter={() => handleArticleHover(article.id.toString())}
|
||||
onMouseLeave={handleArticleLeave}
|
||||
sx={{
|
||||
borderRadius: 1,
|
||||
|
@ -25,6 +25,5 @@
|
||||
"@app": ["src/app"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
"include": ["src"]
|
||||
}
|
||||
|
1
tsconfig.tsbuildinfo
Normal file
1
tsconfig.tsbuildinfo
Normal file
@ -0,0 +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/createsightpage/index.tsx","./src/pages/devicespage/index.tsx","./src/pages/editsightpage/index.tsx","./src/pages/loginpage/index.tsx","./src/pages/mainpage/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/store/index.ts","./src/shared/store/articlesstore/index.tsx","./src/shared/store/authstore/index.tsx","./src/shared/store/citystore/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/mediaviewer/threeview.tsx","./src/widgets/mediaviewer/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/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"],"errors":true,"version":"5.8.3"}
|
Loading…
Reference in New Issue
Block a user