diff --git a/index.html b/index.html index e4b78ea..486e7ff 100644 --- a/index.html +++ b/index.html @@ -1,10 +1,10 @@ - + - + - Vite + React + TS + Белые ночи
diff --git a/src/app/router/index.tsx b/src/app/router/index.tsx index bc25682..7aeadc7 100644 --- a/src/app/router/index.tsx +++ b/src/app/router/index.tsx @@ -1,4 +1,11 @@ -import { DevicesPage, LoginPage, MainPage, SightPage } from "@pages"; +import { + CreateSightPage, + DevicesPage, + EditSightPage, + LoginPage, + MainPage, + SightPage, +} from "@pages"; import { authStore } from "@shared"; import { Layout } from "@widgets"; @@ -23,7 +30,9 @@ export const Router = () => { } > } /> - } /> + } /> + } /> + } /> } /> diff --git a/src/assets/favicon_ship.png b/src/assets/favicon_ship.png new file mode 100644 index 0000000..3487dd6 Binary files /dev/null and b/src/assets/favicon_ship.png differ diff --git a/src/pages/CreateSightPage/index.tsx b/src/pages/CreateSightPage/index.tsx new file mode 100644 index 0000000..0d1e3ac --- /dev/null +++ b/src/pages/CreateSightPage/index.tsx @@ -0,0 +1,61 @@ +import { Box, Tab, Tabs } from "@mui/material"; +import { InformationTab, RightWidgetTab } from "@widgets"; +import { LeftWidgetTab } from "@widgets"; +import { useState } from "react"; + +function a11yProps(index: number) { + return { + id: `sight-tab-${index}`, + "aria-controls": `sight-tabpanel-${index}`, + }; +} + +export const CreateSightPage = () => { + const [value, setValue] = useState(0); + + const handleChange = (_: React.SyntheticEvent, newValue: number) => { + setValue(newValue); + }; + + return ( + + + + + + + + + +
+ + + +
+
+ ); +}; diff --git a/src/pages/EditSightPage/index.tsx b/src/pages/EditSightPage/index.tsx new file mode 100644 index 0000000..de008a0 --- /dev/null +++ b/src/pages/EditSightPage/index.tsx @@ -0,0 +1,78 @@ +import { Box, Tab, Tabs } from "@mui/material"; +import { InformationTab, RightWidgetTab } from "@widgets"; +import { LeftWidgetTab } from "@widgets"; +import { useEffect, useState } from "react"; +import { observer } from "mobx-react-lite"; +import { languageStore, sightsStore } from "@shared"; +import { useParams } from "react-router-dom"; + +function a11yProps(index: number) { + return { + id: `sight-tab-${index}`, + "aria-controls": `sight-tabpanel-${index}`, + }; +} + +export const EditSightPage = observer(() => { + const [value, setValue] = useState(0); + const { sight, getSight } = sightsStore; + const { language } = languageStore; + const { id } = useParams(); + + const handleChange = (_: React.SyntheticEvent, newValue: number) => { + setValue(newValue); + }; + + useEffect(() => { + const fetchData = async () => { + if (id) { + await getSight(Number(id)); + } + }; + fetchData(); + }, [id, language]); + + return ( + + + + + + + + + + {sight && ( +
+ + + +
+ )} +
+ ); +}); diff --git a/src/pages/LoginPage/index.tsx b/src/pages/LoginPage/index.tsx index 928e22e..77fa0eb 100644 --- a/src/pages/LoginPage/index.tsx +++ b/src/pages/LoginPage/index.tsx @@ -26,7 +26,7 @@ export const LoginPage = () => { try { await login(email, password); - navigate("/sights"); + navigate("/sight"); toast.success("Вход в систему выполнен успешно"); } catch (err) { setError( diff --git a/src/pages/SightPage/index.tsx b/src/pages/SightPage/index.tsx index a0c064e..6f8d05f 100644 --- a/src/pages/SightPage/index.tsx +++ b/src/pages/SightPage/index.tsx @@ -1,61 +1,9 @@ -import { Box, Tab, Tabs } from "@mui/material"; -import { InformationTab, RightWidgetTab } from "@widgets"; -import { LeftWidgetTab } from "@widgets"; -import { useState } from "react"; - -function a11yProps(index: number) { - return { - id: `sight-tab-${index}`, - "aria-controls": `sight-tabpanel-${index}`, - }; -} +import { SightsTable } from "@widgets"; export const SightPage = () => { - const [value, setValue] = useState(0); - - const handleChange = (_: React.SyntheticEvent, newValue: number) => { - setValue(newValue); - }; - return ( - - - - - - - - - -
- - - -
-
+ <> + + ); }; diff --git a/src/pages/index.ts b/src/pages/index.ts index c036111..838ea4c 100644 --- a/src/pages/index.ts +++ b/src/pages/index.ts @@ -1,4 +1,6 @@ export * from "./MainPage"; -export * from "./SightPage"; +export * from "./EditSightPage"; export * from "./LoginPage"; export * from "./DevicesPage"; +export * from "./SightPage"; +export * from "./CreateSightPage"; diff --git a/src/shared/api/index.tsx b/src/shared/api/index.tsx index 2830636..cb215e4 100644 --- a/src/shared/api/index.tsx +++ b/src/shared/api/index.tsx @@ -1,3 +1,4 @@ +import { languageStore } from "@shared"; import axios from "axios"; const authInstance = axios.create({ @@ -6,6 +7,7 @@ const authInstance = axios.create({ authInstance.interceptors.request.use((config) => { config.headers.Authorization = `Bearer ${localStorage.getItem("token")}`; + config.headers["X-Language"] = languageStore.language ?? "ru"; return config; }); diff --git a/src/shared/config/constants.tsx b/src/shared/config/constants.tsx index 86a5ed4..125514b 100644 --- a/src/shared/config/constants.tsx +++ b/src/shared/config/constants.tsx @@ -17,7 +17,7 @@ export const NAVIGATION_ITEMS: { id: "attractions", label: "Достопримечательности", icon: Building2, - path: "/sights", + path: "/sight", }, { id: "devices", diff --git a/src/shared/store/CityStore/index.tsx b/src/shared/store/CityStore/index.tsx new file mode 100644 index 0000000..91c46b2 --- /dev/null +++ b/src/shared/store/CityStore/index.tsx @@ -0,0 +1,25 @@ +import { authInstance } from "@shared"; +import { makeAutoObservable } from "mobx"; + +type City = { + id: number; + name: string; + country_code: string; + country: string; + arms?: string; +}; + +class CityStore { + cities: City[] = []; + + constructor() { + makeAutoObservable(this); + } + + getCities = async () => { + const response = await authInstance.get("/city"); + this.cities = response.data; + }; +} + +export const cityStore = new CityStore(); diff --git a/src/shared/store/DevicesStore/index.tsx b/src/shared/store/DevicesStore/index.tsx index 049a95e..d695479 100644 --- a/src/shared/store/DevicesStore/index.tsx +++ b/src/shared/store/DevicesStore/index.tsx @@ -2,7 +2,7 @@ import { API_URL, authInstance } from "@shared"; import { makeAutoObservable } from "mobx"; class DevicesStore { - devices: any[] = []; + devices: string[] = []; uuid: string | null = null; sendSnapshotModalOpen = false; @@ -12,7 +12,6 @@ class DevicesStore { getDevices = async () => { const response = await authInstance.get(`${API_URL}/devices/connected`); - console.log(response.data); this.devices = response.data; }; diff --git a/src/shared/store/SightsStore/index.tsx b/src/shared/store/SightsStore/index.tsx new file mode 100644 index 0000000..272818e --- /dev/null +++ b/src/shared/store/SightsStore/index.tsx @@ -0,0 +1,87 @@ +import { authInstance } from "@shared"; +import { makeAutoObservable, runInAction } from "mobx"; + +export type Language = "ru" | "en" | "zh"; + +export type MultilingualContent = { + [key in Language]: { + name: string; + description: string; + address: string; + // Add other fields that need to be multilingual + }; +}; + +export type Sight = { + id: number; + name: string; + city_id: number; + city: string; + address: string; + latitude: number; + longitude: number; + thumbnail: string | null; + watermark_lu: string | null; + watermark_rd: string | null; + left_article: number; + preview_media: string; + video_preview: string | null; +}; + +class SightsStore { + sights: Sight[] = []; + sight: Sight | null = null; + cachedMultilingualContent: MultilingualContent | null = null; + + constructor() { + makeAutoObservable(this); + } + + getSights = async () => { + const response = await authInstance.get("/sight"); + runInAction(() => { + this.sights = response.data; + }); + }; + + getSight = async (id: number) => { + const response = await authInstance.get(`/sight/${id}`); + + runInAction(() => { + this.sight = response.data; + }); + }; + + setCachedMultilingualContent = (content: MultilingualContent) => { + runInAction(() => { + this.cachedMultilingualContent = content; + }); + }; + + updateCachedLanguageContent = ( + language: Language, + content: Partial + ) => { + runInAction(() => { + if (!this.cachedMultilingualContent) { + this.cachedMultilingualContent = { + ru: { name: "", description: "", address: "" }, + en: { name: "", description: "", address: "" }, + zh: { name: "", description: "", address: "" }, + }; + } + this.cachedMultilingualContent[language] = { + ...this.cachedMultilingualContent[language], + ...content, + }; + }); + }; + + clearCachedMultilingualContent = () => { + runInAction(() => { + this.cachedMultilingualContent = null; + }); + }; +} + +export const sightsStore = new SightsStore(); diff --git a/src/shared/store/SnapshotStore/index.ts b/src/shared/store/SnapshotStore/index.ts index 0824485..4768390 100644 --- a/src/shared/store/SnapshotStore/index.ts +++ b/src/shared/store/SnapshotStore/index.ts @@ -2,8 +2,15 @@ import { authInstance } from "@shared"; import { API_URL } from "@shared"; import { makeAutoObservable } from "mobx"; +type Snapshot = { + ID: string; + Name: string; + ParentID: string; + CreationTime: string; +}; + class SnapshotStore { - snapshots: any[] = []; + snapshots: Snapshot[] = []; constructor() { makeAutoObservable(this); diff --git a/src/shared/store/VehicleStore/index.ts b/src/shared/store/VehicleStore/index.ts index e32a51f..01e2c2a 100644 --- a/src/shared/store/VehicleStore/index.ts +++ b/src/shared/store/VehicleStore/index.ts @@ -1,8 +1,27 @@ import { API_URL, authInstance } from "@shared"; import { makeAutoObservable } from "mobx"; +type Vehicle = { + vehicle: { + id: number; + tail_number: number; + type: number; + carrier_id: number; + carrier: string; + uuid?: string; + }; + device_status?: { + device_uuid: string; + online: boolean; + gps_ok: boolean; + media_service_ok: boolean; + last_update: string; + is_connected: boolean; + }; +}; + class VehicleStore { - vehicles: any[] = []; + vehicles: Vehicle[] = []; constructor() { makeAutoObservable(this); diff --git a/src/shared/store/index.ts b/src/shared/store/index.ts index 62cccd8..9dc3f47 100644 --- a/src/shared/store/index.ts +++ b/src/shared/store/index.ts @@ -3,3 +3,5 @@ export * from "./LanguageStore"; export * from "./DevicesStore"; export * from "./VehicleStore"; export * from "./SnapshotStore"; +export * from "./SightsStore"; +export * from "./CityStore"; diff --git a/src/widgets/DevicesTable/index.tsx b/src/widgets/DevicesTable/index.tsx index 0ef98d7..386b513 100644 --- a/src/widgets/DevicesTable/index.tsx +++ b/src/widgets/DevicesTable/index.tsx @@ -6,7 +6,13 @@ 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 { devicesStore, Modal, snapshotStore, vehicleStore } from "@shared"; +import { + authInstance, + devicesStore, + Modal, + snapshotStore, + vehicleStore, +} from "@shared"; import { useEffect, useState } from "react"; import { observer } from "mobx-react-lite"; import { Button, Checkbox } from "@mui/material"; @@ -82,13 +88,15 @@ export const DevicesTable = observer(() => { fetchData(); }, []); - const handleSendSnapshot = (uuid: string[]) => { + const handleSendSnapshot = (uuid: string) => { setSelectedDevice(uuid); toggleSendSnapshotModal(); }; - const handleReloadStatus = (uuid: string) => { + const handleReloadStatus = async (uuid: string) => { setSelectedDevice(uuid); + await authInstance.post(`/devices/${uuid}/request-status`); + await getDevices(); }; const handleSelectDevice = (event: React.ChangeEvent) => { @@ -101,6 +109,13 @@ export const DevicesTable = observer(() => { } }; + const handleSendSnapshotAction = async (uuid: string, snapshotId: string) => { + await authInstance.post(`/devices/${uuid}/force-snapshot`, { + snapshot_id: snapshotId, + }); + await getDevices(); + }; + return ( <> @@ -113,7 +128,7 @@ export const DevicesTable = observer(() => { color="primary" disabled={selectedDevices.length === 0} className="ml-auto" - onClick={() => handleSendSnapshot(selectedDevices)} + onClick={() => handleSendSnapshot(uuid ?? "")} > Отправить снапшот @@ -195,7 +210,11 @@ export const DevicesTable = observer(() => {
{snapshots && snapshots.map((snapshot) => ( - ))} diff --git a/src/widgets/LanguageSwitcher/index.tsx b/src/widgets/LanguageSwitcher/index.tsx index 40d28a8..0d34162 100644 --- a/src/widgets/LanguageSwitcher/index.tsx +++ b/src/widgets/LanguageSwitcher/index.tsx @@ -1,29 +1,63 @@ import { languageStore } from "@shared"; +import { Button } from "@mui/material"; // Only Button is needed +import { useEffect, useCallback } from "react"; import { observer } from "mobx-react-lite"; +const LANGUAGES = ["ru", "en", "zh"] as const; +type Language = (typeof LANGUAGES)[number]; + export const LanguageSwitcher = observer(() => { const { language, setLanguage } = languageStore; + // Memoize getLanguageLabel for consistent rendering + const getLanguageLabel = useCallback((lang: Language) => { + switch (lang) { + case "ru": + return "RU"; + case "en": + return "EN"; + case "zh": + return "ZH"; + default: + return ""; + } + }, []); + + useEffect(() => { + const handleKeyPress = (event: KeyboardEvent) => { + // Keep Ctrl+S for language cycling + if (event.key.toLowerCase() === "f3") { + event.preventDefault(); // Prevent browser save dialog + const currentIndex = LANGUAGES.indexOf(language as Language); + const nextIndex = (currentIndex + 1) % LANGUAGES.length; + setLanguage(LANGUAGES[nextIndex]); + } + }; + + window.addEventListener("keydown", handleKeyPress); + return () => { + window.removeEventListener("keydown", handleKeyPress); + }; + }, [language, setLanguage]); // Only language is a dependency now + + const handleLanguageChange = (lang: Language) => { + setLanguage(lang); + }; + return ( -
- - - +
+ {/* Added some styling for better visualization */} + {LANGUAGES.map((lang) => ( + + ))}
); }); diff --git a/src/widgets/ReactMarkdown/index.tsx b/src/widgets/ReactMarkdown/index.tsx index 68eddfd..81493fa 100644 --- a/src/widgets/ReactMarkdown/index.tsx +++ b/src/widgets/ReactMarkdown/index.tsx @@ -12,17 +12,16 @@ export const ReactMarkdownComponent = ({ value }: { value: string }) => { borderRadius: 1, }, "& h1, & h2, & h3, & h4, & h5, & h6": { - color: "primary.main", + color: "white", mt: 2, mb: 1, }, "& p": { mb: 2, - color: (theme) => - theme.palette.mode === "dark" ? "grey.300" : "grey.800", + color: "white", }, "& a": { - color: "primary.main", + color: "white", textDecoration: "none", "&:hover": { textDecoration: "underline", diff --git a/src/widgets/SightEdit/index.tsx b/src/widgets/SightEdit/index.tsx index 890a440..c31568a 100644 --- a/src/widgets/SightEdit/index.tsx +++ b/src/widgets/SightEdit/index.tsx @@ -1,7 +1,7 @@ -import { Unlink } from "lucide-react"; +import { ImagePlus, Unlink } from "lucide-react"; import { Trash2 } from "lucide-react"; -import { TextField } from "@mui/material"; +import { Box, TextField } from "@mui/material"; import { ReactMarkdownEditor } from "@widgets"; export const SightEdit = () => { @@ -24,9 +24,52 @@ export const SightEdit = () => {
-

Превью

+

Превью

+ 1
+ background: "#877361", + borderColor: "grey.300", + }} + > + {!false && ( + + + + )} + + + +
); diff --git a/src/widgets/SightTabs/InformationTab/index.tsx b/src/widgets/SightTabs/InformationTab/index.tsx index 80f2ca7..de6deea 100644 --- a/src/widgets/SightTabs/InformationTab/index.tsx +++ b/src/widgets/SightTabs/InformationTab/index.tsx @@ -1,48 +1,484 @@ -import { TextField } from "@mui/material"; -import { BackButton, TabPanel } from "@shared"; +import { + Button, + TextField, + Box, + Typography, + IconButton, + Paper, + Tooltip, +} from "@mui/material"; +import { BackButton, Sight, sightsStore, TabPanel, Language } from "@shared"; import { LanguageSwitcher } from "@widgets"; +import { ImagePlus, Info } from "lucide-react"; +import { observer } from "mobx-react-lite"; +import { useState, useEffect } from "react"; +import { useLocation } from "react-router-dom"; -export const InformationTab = ({ - value, - index, -}: { - value: number; - index: number; -}) => { - return ( - -
-
- -
- - - - - -
-
-

Логотип

- -
-
-

Водяной знак (л.в)

- -
-
-

Водяной знак (п.в)

- -
-
-
-
- -
- -
-
-
- ); +// Мокап данных для отображения, потом это будет приходить из store/props +// Keeping this mock for demonstration, but in a real app, +// this would come from the MobX store's 'sight' object. +const mockSightData = { + name: "Эрмитаж", + address: "Дворцовая площадь, 2", + city: "Санкт-Петербург", // или city_id, если будет Select + coordinates: "59.9398, 30.3146", + logo: null, // null или URL/ID медиа + watermark_lu: null, + watermark_rd: null, }; + +// Мокап для всплывающей подсказки +const watermarkTooltipText = "При наведении открывается просмотр в поп-апе"; +const logoTooltipText = "При наведении открывается просмотр логотипа в поп-апе"; + +export const InformationTab = observer( + ({ value, index }: { value: number; index: number }) => { + const { + sight, + cachedMultilingualContent, + updateCachedLanguageContent, + clearCachedMultilingualContent, + // Assuming you'll have an action to update the main sight object + updateSight, + } = sightsStore; + + // Initialize local states with data from the MobX store's 'sight' + const [address, setAddress] = useState(sight?.address ?? ""); + const [city, setCity] = useState(sight?.city ?? ""); + const [coordinates, setCoordinates] = useState( + sight?.latitude && sight?.longitude + ? `${sight.latitude}, ${sight.longitude}` + : "" + ); + const [currentLanguage, setCurrentLanguage] = useState("ru"); + const pathname = useLocation().pathname; + + // Effect to initialize local states when `sight` data becomes available or changes + useEffect(() => { + if (sight) { + setAddress(sight.address ?? ""); + setCity(sight.city ?? ""); + setCoordinates( + sight.latitude && sight.longitude + ? `${sight.latitude}, ${sight.longitude}` + : "" + ); + // Initialize cached content if not already set + if (!cachedMultilingualContent) { + sightsStore.setCachedMultilingualContent({ + ru: { name: sight.name, description: "", address: sight.address }, + en: { name: "", description: "", address: "" }, + zh: { name: "", description: "", address: "" }, + }); + } + } + }, [sight, cachedMultilingualContent]); // Add cachedMultilingualContent to dependencies + + // Effect to clear cached content when the route changes + useEffect(() => { + clearCachedMultilingualContent(); + }, [pathname, clearCachedMultilingualContent]); + + const handleLanguageChange = (lang: Language) => { + setCurrentLanguage(lang); + }; + + const handleSelectMedia = ( + type: "logo" | "watermark_lu" | "watermark_rd" + ) => { + // Here will be logic for opening modal window for media selection + console.log("Select media for:", type); + // In a real application, you might open a dialog here + // and update the sight object with the selected media URL/ID. + }; + + const handleSave = () => { + // Parse coordinates back to latitude and longitude + let latitude: number | undefined; + let longitude: number | undefined; + const coordsArray = coordinates + .split(",") + .map((coord) => parseFloat(coord.trim())); + if ( + coordsArray.length === 2 && + !isNaN(coordsArray[0]) && + !isNaN(coordsArray[1]) + ) { + latitude = coordsArray[0]; + longitude = coordsArray[1]; + } + + // Prepare the updated sight data + const updatedSightData = { + ...sight, // Keep existing sight data + address: address, + city: city, + latitude: latitude, + longitude: longitude, + // Assuming logo and watermark updates would happen via handleSelectMedia + // and then be reflected in the sight object in the store. + }; + + // Here we would save both the sight data and the multilingual content + console.log("Saving general information and multilingual content...", { + updatedSightData, + multilingualContent: cachedMultilingualContent, + }); + + // Call an action from your store to save the data + // For example: + // sightsStore.saveSight({ ...updatedSightData, multilingualContent: cachedMultilingualContent }); + // You might have a specific action in your store for saving all this data. + }; + + return ( + + + + + + {/* Left column with main fields */} + + { + updateCachedLanguageContent(currentLanguage, { + name: e.target.value, + }); + }} + fullWidth + variant="outlined" + /> + { + updateCachedLanguageContent(currentLanguage, { + description: e.target.value, + }); + }} + fullWidth + variant="outlined" + multiline + rows={4} + /> + { + setAddress(e.target.value); + }} + fullWidth + variant="outlined" + /> + { + setCity(e.target.value); + }} + fullWidth + variant="outlined" + /> + + { + setCoordinates(e.target.value); + }} + fullWidth + variant="outlined" + helperText="Формат: широта, долгота (например, 59.9398, 30.3146)" + /> + + + + {/* Правая колонка для логотипа и водяных знаков */} + + {/* Водяные знаки */} + + + + + Логотип + + + + + + + mockSightData.watermark_lu && + handleSelectMedia("watermark_lu") + } + > + {mockSightData.watermark_lu ? ( + Знак л.в + ) : ( + + )} + + + + + + + Водяной знак (л.в) + + + + + + + mockSightData.watermark_lu && + handleSelectMedia("watermark_lu") + } + > + {mockSightData.watermark_lu ? ( + Знак л.в + ) : ( + + )} + + + + + + + + Водяной знак (п.в) + + + + + + + mockSightData.watermark_rd && + handleSelectMedia("watermark_rd") + } + > + {mockSightData.watermark_rd ? ( + Знак п.в + ) : ( + + )} + + + + + + + + {/* LanguageSwitcher positioned at the top right */} + + + + + {/* Save Button fixed at the bottom right */} + + + + + + ); + } +); diff --git a/src/widgets/SightTabs/LeftWidgetTab/index.tsx b/src/widgets/SightTabs/LeftWidgetTab/index.tsx index ace860b..db0cea7 100644 --- a/src/widgets/SightTabs/LeftWidgetTab/index.tsx +++ b/src/widgets/SightTabs/LeftWidgetTab/index.tsx @@ -1,60 +1,269 @@ -import { TextField } from "@mui/material"; -import { BackButton, TabPanel } from "@shared"; +import { Box, Button, TextField, Paper, Typography } from "@mui/material"; +import { BackButton, Sight, TabPanel } from "@shared"; import { ReactMarkdownComponent, ReactMarkdownEditor } from "@widgets"; -import { Trash2 } from "lucide-react"; -import { Unlink } from "lucide-react"; +import { Unlink, Trash2, ImagePlus } from "lucide-react"; import { useState } from "react"; +// Мокап данных для левой статьи +const mockLeftArticle = { + title: "История основания", + markdownContent: `## Заголовок статьи H2 + +Какой-то **текст** для левой статьи. +Можно использовать *markdown*. + +- Список 1 +- Список 2 + +[Ссылка на Яндекс](https://ya.ru) + `, + media: null, // null или URL/ID медиа +}; + export const LeftWidgetTab = ({ value, index, + data, }: { value: number; index: number; + data?: Sight; }) => { - const [leftArticleData, setLeftArticleData] = useState(" "); + const [articleTitle, setArticleTitle] = useState(mockLeftArticle.title); + const [markdownContent, setMarkdownContent] = useState( + mockLeftArticle.markdownContent + ); + const [articleMedia, setArticleMedia] = useState( + mockLeftArticle.media + ); // Для превью медиа + + const handleSelectMediaForArticle = () => { + // Логика открытия модального окна для выбора медиа для статьи + console.log("Select media fo r left article"); + // Для примера, установим моковое изображение + // setArticleMedia("https://via.placeholder.com/350x200.png?text=Article+Media"); + }; + + const handleUnlinkArticle = () => { + console.log("Unlink left article"); + }; + + const handleDeleteArticle = () => { + console.log("Delete left article"); + }; + + const handleSave = () => { + console.log("Saving left widget..."); + }; + return ( -
+ -
-

Левая статья

-
- - + -
-
-
-
- + + + - + {/* Левая колонка: Редактирование */} + + setArticleTitle(e.target.value)} + variant="outlined" + sx={{ width: "100%" }} // Примерная ширина как на макете /> -
-
-

Предпросмотр

-
-
-
-
- -
-
-
-
- -
+ + {/* Редактор Markdown */} + + Текст + + + + {/* Блок МЕДИА для статьи */} + + + МЕДИА + + {/* Здесь будет UI для управления медиа статьи */} + {articleMedia ? ( + + Article media + + ) : ( + + Нет медиа + + )} + + + + + {/* Правая колонка: Предпросмотр */} + + Предпросмотр + + {/* Медиа в превью (если есть) */} + {articleMedia && ( + + Превью медиа + + )} + {!articleMedia && ( + + + + )} + + {/* Заголовок в превью */} + + + {articleTitle || "Название информации"} + + + + {/* Текст статьи в превью */} + + + + + + + + + + +
); }; diff --git a/src/widgets/SightTabs/RightWidgetTab/index.tsx b/src/widgets/SightTabs/RightWidgetTab/index.tsx index 12f00f1..27478e9 100644 --- a/src/widgets/SightTabs/RightWidgetTab/index.tsx +++ b/src/widgets/SightTabs/RightWidgetTab/index.tsx @@ -1,73 +1,264 @@ -import { BackButton, TabPanel } from "@shared"; +import { + Box, + Button, + List, + ListItemButton, + ListItemText, + Paper, + Typography, +} from "@mui/material"; +import { BackButton, Sight, TabPanel } from "@shared"; import { SightEdit } from "@widgets"; import { Plus } from "lucide-react"; +import { useState } from "react"; + +// Мокап данных для списка блоков правого виджета +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", + }, +]; + +// Мокап данных для выбранного блока для редактирования +// В реальности это будет объект Article из API +const mockSelectedBlockData = { + id: "article_1", + heading: "История основания Санкт-Петербурга", + body: "## Начало\nГород был основан 27 мая 1703 года Петром I...", + media: [ + // Предполагаем, что у статьи может быть несколько медиа + // { id: "media_1", url: "https://via.placeholder.com/300x200.png?text=History+Image+1", type: "image" } + ], +}; export const RightWidgetTab = ({ value, index, + data, }: { value: number; index: number; + data?: Sight; }) => { + const [rightWidgetBlocks, setRightWidgetBlocks] = useState( + mockRightWidgetBlocks + ); + const [selectedBlockId, setSelectedBlockId] = useState( + mockRightWidgetBlocks[1]?.id || null + ); // Выбираем первый "article" по умолчанию + + const handleSelectBlock = (blockId: string) => { + setSelectedBlockId(blockId); + // Здесь будет логика загрузки данных для выбранного блока, если они не загружены + console.log("Selected block:", blockId); + }; + + const handleAddBlock = () => { + // Логика открытия модала/формы для создания нового блока/статьи + // или выбора существующей статьи для привязки + console.log("Add new block"); + const newBlockId = `article_${Date.now()}`; + setRightWidgetBlocks([ + ...rightWidgetBlocks, + { + id: newBlockId, + name: `${ + rightWidgetBlocks.filter((b) => b.type === "article").length + 1 + }. Новый блок`, + type: "article", + }, + ]); + setSelectedBlockId(newBlockId); + }; + + const handleSave = () => { + console.log("Saving right widget..."); + }; + + // Находим данные для редактирования на основе selectedBlockId + // В реальном приложении эти данные будут приходить из store или загружаться по API + const currentBlockToEdit = + selectedBlockId === mockSelectedBlockData.id + ? mockSelectedBlockData + : selectedBlockId + ? { + id: selectedBlockId, + heading: + rightWidgetBlocks.find((b) => b.id === selectedBlockId)?.name || + "Заголовок...", + body: "Содержимое...", + media: [], + } + : null; + return ( - {/* Ensure the main container takes full height and uses flexbox for layout */} -
- {/* Content area with back button and main layout */} -
- {" "} - {/* Added padding for better spacing */} - -
- {" "} - {/* flex-1 allows this div to take remaining height */} - {/* Left sidebar */} -
- {" "} - {/* Added background and padding */} -
-
- {" "} - {/* Adjusted background and added shadow */} - Превью медиа -
-
- {" "} - {/* Adjusted background and added shadow */}1 История -
-
- {" "} - {/* Adjusted background and added shadow */}2 Факты -
-
- -
- {/* Main content area */} -
- {" "} - {/* Added shadow for depth */} - {/* Content within the main area */} - - {/* Replaced '1' with more descriptive content */} -
-
-
+ + - {/* Save button at the bottom, aligned to the right */} -
- {" "} - {/* Wrapper for save button, added padding */} - + + + + {/* Правая колонка: Редактор выбранного блока (SightEdit) */} + + {currentBlockToEdit ? ( + <> + console.log("Unlink block:", selectedBlockId)} + onDelete={() => { + console.log("Delete block:", selectedBlockId); + setRightWidgetBlocks((blocks) => + blocks.filter((b) => b.id !== selectedBlockId) + ); + setSelectedBlockId(null); + }} + /> + + + МЕДИА + + {/* Здесь будет UI для управления медиа статьи */} + + Нет медиа + + + + + + ) : ( + <> + )} + + + + {/* Блок МЕДИА для статьи */} + + + -
-
+ + +
); }; diff --git a/src/widgets/SightsTable/index.tsx b/src/widgets/SightsTable/index.tsx new file mode 100644 index 0000000..cc5f262 --- /dev/null +++ b/src/widgets/SightsTable/index.tsx @@ -0,0 +1,108 @@ +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +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 { authInstance, cityStore, languageStore, sightsStore } from "@shared"; +import { useEffect } from "react"; +import { observer } from "mobx-react-lite"; +import { Button, Checkbox } from "@mui/material"; +import { LanguageSwitcher } from "@widgets"; +import { Pencil, Trash2 } from "lucide-react"; +import { useNavigate } from "react-router-dom"; + +function createData(id: number, name: string, city: string) { + return { id, name, city }; +} + +const rows = (sights: any[], cities: any[]) => { + return sights.map((sight) => { + const city = cities.find((city) => city?.id === sight?.city_id); + return createData(sight?.id, sight?.name, city?.name ?? "Нет данных"); + }); +}; + +export const SightsTable = observer(() => { + const navigate = useNavigate(); + const { language } = languageStore; + const { sights, getSights } = sightsStore; + const { cities, getCities } = cityStore; + + useEffect(() => { + const fetchData = async () => { + await getSights(); + await getCities(); + }; + fetchData(); + }, [language, getSights, getCities]); + + const handleDelete = async (id: number) => { + await authInstance.delete(`/sight/${id}`); + await getSights(); + }; + + return ( + <> + + +
+ +
+ + + + Название + Город + Действия + + + + {rows(sights, cities)?.map((row) => ( + + + {row?.name} + + + {row?.city} + + +
+ + +
+
+
+ ))} +
+
+
+ + ); +}); diff --git a/src/widgets/index.ts b/src/widgets/index.ts index cddfb06..c21b92f 100644 --- a/src/widgets/index.ts +++ b/src/widgets/index.ts @@ -6,3 +6,4 @@ export * from "./ReactMarkdownEditor"; export * from "./SightEdit"; export * from "./LanguageSwitcher"; export * from "./DevicesTable"; +export * from "./SightsTable";