diff --git a/src/components/CreateSightArticle.tsx b/src/components/CreateSightArticle.tsx index fa9af9c..65d18bd 100644 --- a/src/components/CreateSightArticle.tsx +++ b/src/components/CreateSightArticle.tsx @@ -18,10 +18,7 @@ import { ALLOWED_IMAGE_TYPES, ALLOWED_VIDEO_TYPES, } from "../components/media/MediaFormUtils"; -import { LinkedItems } from "./LinkedItems"; -import { mediaFields, MediaItem } from "../pages/article/types"; -import { LanguageSelector } from "@ui"; -import { EVERY_LANGUAGE, Languages, languageStore } from "@stores"; +import { EVERY_LANGUAGE, Languages } from "@stores"; const MemoizedSimpleMDE = React.memo(MarkdownEditor); @@ -38,6 +35,9 @@ type Props = { childResource: string; title: string; left?: boolean; + language: Languages, + setHeadingParent?: (heading: string) => void, + setBodyParent?: (body: string) => void, }; export const CreateSightArticle = ({ @@ -46,10 +46,13 @@ export const CreateSightArticle = ({ childResource, title, left, + language, + setHeadingParent, + setBodyParent }: Props) => { const theme = useTheme(); const [mediaFiles, setMediaFiles] = useState([]); - const { language, setLanguageAction } = languageStore; + const [workingLanguage, setWorkingLanguage] = useState(language); const { register: registerItem, @@ -66,6 +69,7 @@ export const CreateSightArticle = ({ }, }); + const [articleData, setArticleData] = useState({ heading: EVERY_LANGUAGE(""), body: EVERY_LANGUAGE("") @@ -76,34 +80,31 @@ export const CreateSightArticle = ({ ...articleData, heading: { ...articleData.heading, - [language]: watch("heading") ?? "", + [workingLanguage]: watch("heading") ?? "", }, body: { ...articleData.body, - [language]: watch("body") ?? "", + [workingLanguage]: watch("body") ?? "", } } setArticleData(newArticleData); return newArticleData; } - // const handleFormSubmit = handleSubmit((values: FieldValues) => { - // const newTranslations = updateTranslations(); - // console.log(newTranslations); - // return onFinish({ - // translations: newTranslations - // }); - // }); + useEffect(() => { + setValue("heading", articleData.heading[workingLanguage] ?? ""); + setValue("body", articleData.body[workingLanguage] ?? ""); + }, [workingLanguage, articleData, setValue]); useEffect(() => { - setValue("heading", articleData.heading[language] ?? ""); - setValue("body", articleData.body[language] ?? ""); - }, [language, articleData, setValue]); - - const handleLanguageChange = (lang: Languages) => { updateTranslations(); - setLanguageAction(lang); - }; + setWorkingLanguage(language); + }, [language]); + + useEffect(() => { + setHeadingParent?.(watch("heading")); + setBodyParent?.(watch("body")); + }, [watch("heading"), watch("body"), setHeadingParent, setBodyParent]); const simpleMDEOptions = React.useMemo( () => ({ @@ -152,8 +153,7 @@ export const CreateSightArticle = ({ try { // Создаем статью const response = await axiosInstance.post( - `${import.meta.env.VITE_KRBL_API}/${childResource}`, - { + `${import.meta.env.VITE_KRBL_API}/${childResource}`, { ...data, translations: updateTranslations() } @@ -220,154 +220,135 @@ export const CreateSightArticle = ({ }; return ( - - } + + - - Создать {title} - - - - - - + label="Заголовок *" + /> - ( - - )} + ( + + )} + /> - {/* Dropzone для медиа файлов */} - + {/* Dropzone для медиа файлов */} + + + + + {isDragActive + ? "Перетащите файлы сюда..." + : "Перетащите файлы сюда или кликните для выбора"} + + + + {/* Превью загруженных файлов */} + + {mediaFiles.map((mediaFile, index) => ( - - - {isDragActive - ? "Перетащите файлы сюда..." - : "Перетащите файлы сюда или кликните для выбора"} - - - - {/* Превью загруженных файлов */} - - {mediaFiles.map((mediaFile, index) => ( + {mediaFile.file.type.startsWith("image/") ? ( + {mediaFile.file.name} + ) : ( - {mediaFile.file.type.startsWith("image/") ? ( - {mediaFile.file.name} - ) : ( - - - {mediaFile.file.name} - - - )} - + + {mediaFile.file.name} + - ))} + )} + - - - - - - + ))} - - + + + + + + + ); }; diff --git a/src/components/LinkedItems.tsx b/src/components/LinkedItems.tsx index 3347f21..5312526 100644 --- a/src/components/LinkedItems.tsx +++ b/src/components/LinkedItems.tsx @@ -5,8 +5,6 @@ import { Typography, Button, FormControl, - Grid, - Box, Accordion, AccordionSummary, AccordionDetails, @@ -21,8 +19,6 @@ import { Paper, TableBody, IconButton, - Collapse, - Modal, } from "@mui/material"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import DragIndicatorIcon from "@mui/icons-material/DragIndicator"; @@ -71,6 +67,8 @@ type LinkedItemsProps = { onSave?: (items: T[]) => void; onUpdate?: () => void; dontRecurse?: boolean; + disableCreation?: boolean; + updatedLinkedItems?: T[]; }; const reorder = (list: any[], startIndex: number, endIndex: number) => { @@ -80,7 +78,44 @@ const reorder = (list: any[], startIndex: number, endIndex: number) => { return result; }; -export const LinkedItems = ({ +export const LinkedItems = ( + props: LinkedItemsProps +) => { + const theme = useTheme(); + + return ( + <> + + } + sx={{ + background: theme.palette.background.paper, + borderBottom: `1px solid ${theme.palette.divider}`, + }} + > + + Привязанные {props.title} + + + + + + + + + + + {!props.dontRecurse && + <> + + + + } + + ); +} + +export const LinkedItemsContents = ({ parentId, parentResource, childResource, @@ -89,9 +124,9 @@ export const LinkedItems = ({ title, dragAllowed = false, type, - onSave, onUpdate, - dontRecurse = false, + disableCreation = false, + updatedLinkedItems }: LinkedItemsProps) => { const { language } = languageStore; const { setArticleModalOpenAction, setArticleIdAction } = articleStore; @@ -104,7 +139,6 @@ export const LinkedItems = ({ const [pageNum, setPageNum] = useState(1); const [isLoading, setIsLoading] = useState(true); const [mediaOrder, setMediaOrder] = useState(1); - const theme = useTheme(); let availableItems = items.filter( (item) => !linkedItems.some((linked) => linked.id === item.id) @@ -118,9 +152,12 @@ export const LinkedItems = ({ }, [childResource, availableItems]); useEffect(() => { - if (setItemsParent) { - setItemsParent(linkedItems); - } + if(!updatedLinkedItems?.length) return; + setLinkedItems(updatedLinkedItems); + }, [updatedLinkedItems]); + + useEffect(() => { + setItemsParent?.(linkedItems); }, [linkedItems, setItemsParent]); const onDragEnd = (result: any) => { @@ -272,254 +309,230 @@ export const LinkedItems = ({ return ( <> - - } - sx={{ - background: theme.palette.background.paper, - borderBottom: `1px solid ${theme.palette.divider}`, - }} - > - - Привязанные {title} - - + {linkedItems?.length > 0 && ( + + + + + + {type === "edit" && dragAllowed && ( + + )} + + {fields.map((field) => ( + + {field.label} + + ))} - - - {linkedItems?.length > 0 && ( - - -
- - - {type === "edit" && dragAllowed && ( - - )} - - {fields.map((field) => ( - - {field.label} - - ))} + {type === "edit" && ( + Действие + )} + + - {type === "edit" && ( - Действие - )} - - + + {(provided) => ( + + {linkedItems.map((item, index) => ( + + {(provided) => ( + { + if (childResource === "article") { + setArticleModalOpenAction(true); + setArticleIdAction(item.id); + } + if (childResource === "station") { + setStationModalOpenAction(true); + setStationIdAction(item.id); + setRouteIdAction(Number(parentId)); + } + }} + ref={provided.innerRef} + {...provided.draggableProps} + {...provided.dragHandleProps} + hover + > + {type === "edit" && dragAllowed && ( + + + + + + )} + + {index + 1} + + {fields.map((field, index) => ( + + {field.render + ? field.render(item[field.data]) + : item[field.data]} + + ))} - - {(provided) => ( - - {linkedItems.map((item, index) => ( - - {(provided) => ( - + + + )} + + )} + + ))} - {type === "edit" && ( - - - - )} - - )} - - ))} - - {provided.placeholder} - - )} - -
-
-
- )} - - {linkedItems.length === 0 && !isLoading && ( - - {title} не найдены - - )} - - {type === "edit" && ( - - Добавить {title} - item.id === selectedItemId - ) || null - } - onChange={(_, newValue) => - setSelectedItemId(newValue?.id || null) - } - options={availableItems} - getOptionLabel={(item) => String(item[fields[0].data])} - renderInput={(params) => ( - - )} - isOptionEqualToValue={(option, value) => - option.id === value?.id - } - filterOptions={(options, { inputValue }) => { - const searchWords = inputValue - .toLowerCase() - .split(" ") - .filter((word) => word.length > 0); - return options.filter((option) => { - const optionWords = String(option[fields[0].data]) - .toLowerCase() - .split(" "); - return searchWords.every((searchWord) => - optionWords.some((word) => word.startsWith(searchWord)) - ); - }); - }} - renderOption={(props, option) => ( -
  • - {String(option[fields[0].data])} -
  • - )} - /> - - {/* {childResource === "article" && ( - - { - const newValue = Number(e.target.value); - const minValue = linkedItems.length + 1; - setPageNum(newValue < minValue ? minValue : newValue); - }} - fullWidth - InputLabelProps={{ shrink: true }} - /> - - )} */} - - {childResource === "media" && ( - - { - const newValue = Number(e.target.value); - const maxValue = linkedItems.length + 1; - const value = Math.max(1, Math.min(newValue, maxValue)); - setMediaOrder(value); - }} - fullWidth - InputLabelProps={{ shrink: true }} - /> - + {provided.placeholder} + )} + + + + + )} - - {childResource == "station" && ( - { - const newValue = Number(e.target.value); - setPosition( - newValue > linkedItems.length + 1 - ? linkedItems.length + 1 - : newValue - ); - }} - > - )} -
    + {linkedItems.length === 0 && !isLoading && ( + + {title} не найдены + + )} + + {type === "edit" && !disableCreation && ( + + Добавить {title} + item.id === selectedItemId + ) || null + } + onChange={(_, newValue) => + setSelectedItemId(newValue?.id || null) + } + options={availableItems} + getOptionLabel={(item) => String(item[fields[0].data])} + renderInput={(params) => ( + )} - - -
    - {!dontRecurse && - <> - - - - } + isOptionEqualToValue={(option, value) => + option.id === value?.id + } + filterOptions={(options, { inputValue }) => { + const searchWords = inputValue + .toLowerCase() + .split(" ") + .filter((word) => word.length > 0); + return options.filter((option) => { + const optionWords = String(option[fields[0].data]) + .toLowerCase() + .split(" "); + return searchWords.every((searchWord) => + optionWords.some((word) => word.startsWith(searchWord)) + ); + }); + }} + renderOption={(props, option) => ( +
  • + {String(option[fields[0].data])} +
  • + )} + /> + + {/* {childResource === "article" && ( + + { + const newValue = Number(e.target.value); + const minValue = linkedItems.length + 1; + setPageNum(newValue < minValue ? minValue : newValue); + }} + fullWidth + InputLabelProps={{ shrink: true }} + /> + + )} */} + + {childResource === "media" && ( + + { + const newValue = Number(e.target.value); + const maxValue = linkedItems.length + 1; + const value = Math.max(1, Math.min(newValue, maxValue)); + setMediaOrder(value); + }} + fullWidth + slotProps={{inputLabel: {shrink: true}}} + /> + + )} + + + {childResource == "station" && ( + { + const newValue = Number(e.target.value); + setPosition( + newValue > linkedItems.length + 1 + ? linkedItems.length + 1 + : newValue + ); + }} + > + )} + + )} ); }; diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 0000000..22dc77b --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1,5 @@ +export * from './AdminOnly' +export * from './CreateSightArticle' +export * from './CustomDataGrid' +export * from './LinkedItems' +export * from './MarkdownEditor' \ No newline at end of file diff --git a/src/components/ui/MediaView.tsx b/src/components/ui/MediaView.tsx index 2a4ae4c..854639d 100644 --- a/src/components/ui/MediaView.tsx +++ b/src/components/ui/MediaView.tsx @@ -4,17 +4,16 @@ import { ReactPhotoSphereViewer } from "react-photo-sphere-viewer"; import { ModelViewer } from "./ModelViewer"; export interface MediaData { - filename?: string; id: string | number; - media_name?: string; media_type: number; + filename?: string; } export function MediaView({media} : Readonly<{media?: MediaData}>) { const token = localStorage.getItem(TOKEN_KEY); return ( {media?.media_type === 1 && ( ) { }/download?token=${token}`} alt={media?.filename} style={{ - maxWidth: "100%", - height: "100%", - objectFit: "contain", - borderRadius: 8, + maxWidth: "100%", + height: "auto", + objectFit: "contain", + borderRadius: 8, }} /> )} @@ -37,9 +36,10 @@ export function MediaView({media} : Readonly<{media?: MediaData}>) { media?.id }/download?token=${token}`} style={{ - - objectFit: "contain", - borderRadius: 30, + maxWidth: "100%", + height: "100%", + objectFit: "contain", + borderRadius: 30, }} controls autoPlay diff --git a/src/pages/article/create.tsx b/src/pages/article/create.tsx index d004aa0..ec3c59f 100644 --- a/src/pages/article/create.tsx +++ b/src/pages/article/create.tsx @@ -9,7 +9,6 @@ import "easymde/dist/easymde.min.css"; import { LanguageSelector } from "@ui"; import { observer } from "mobx-react-lite"; import { EVERY_LANGUAGE, Languages, languageStore, META_LANGUAGE } from "@stores"; -import { axiosInstance } from "@/providers/data"; const MemoizedSimpleMDE = React.memo(MarkdownEditor); @@ -33,15 +32,14 @@ export const ArticleCreate = observer(() => { refineCoreProps: { resource: "article", ...META_LANGUAGE(language) - }, - warnWhenUnsavedChanges: false + } }); // Следим за изменениями в полях body и heading const bodyContent = watch("body"); const headingContent = watch("heading"); - function updateTranslations() { + function updateTranslations(update: boolean = true) { const newArticleData = { ...articleData, heading: { @@ -53,13 +51,12 @@ export const ArticleCreate = observer(() => { [language]: watch("body") ?? "", } } - setArticleData(newArticleData); + if(update) setArticleData(newArticleData); return newArticleData; } - const handleFormSubmit = handleSubmit((values: FieldValues) => { - const newTranslations = updateTranslations(); - console.log(newTranslations); + const handleFormSubmit = handleSubmit((values) => { + const newTranslations = updateTranslations(false); return onFinish({ translations: newTranslations }); @@ -80,7 +77,6 @@ export const ArticleCreate = observer(() => { const [preview, setPreview] = useState(""); const [headingPreview, setHeadingPreview] = useState(""); - useEffect(() => { setPreview(bodyContent ?? ""); }, [bodyContent]); @@ -89,17 +85,13 @@ export const ArticleCreate = observer(() => { setHeadingPreview(headingContent ?? ""); }, [headingContent]); - const simpleMDEOptions = React.useMemo( - () => ({ - placeholder: "Введите контент в формате Markdown...", - spellChecker: false, - }), - [] - ); + const simpleMDEOptions = React.useMemo(() => ({ + placeholder: "Введите контент в формате Markdown...", + spellChecker: false, + }), []); return ( @@ -119,7 +111,7 @@ export const ArticleCreate = observer(() => { helperText={(errors as any)?.heading?.message} margin="normal" fullWidth - InputLabelProps={{ shrink: true }} + slotProps={{inputLabel: {shrink: true}}} type="text" label="Заголовок *" name="heading" @@ -128,7 +120,7 @@ export const ArticleCreate = observer(() => { ( { setPreview(articleData.body[language] ?? ""); }, [language, articleData, setValue]); - function updateTranslations() { + function updateTranslations(update: boolean = true) { const newArticleData = { ...articleData, heading: { @@ -70,7 +68,7 @@ export const ArticleEdit = observer(() => { [language]: watch("body") ?? "", } } - setArticleData(newArticleData); + if(update) setArticleData(newArticleData); return newArticleData; } @@ -80,7 +78,7 @@ export const ArticleEdit = observer(() => { }; const handleFormSubmit = handleSubmit((values: FieldValues) => { - const newTranslations = updateTranslations(); + const newTranslations = updateTranslations(false); console.log(newTranslations); return onFinish({ translations: newTranslations @@ -113,7 +111,6 @@ export const ArticleEdit = observer(() => { > {/* Форма редактирования */} - {/* Форма создания */} @@ -130,7 +127,7 @@ export const ArticleEdit = observer(() => { helperText={errors?.heading?.message as string} margin="normal" fullWidth - InputLabelProps={{ shrink: true }} + slotProps={{inputLabel: {shrink: true}}} type="text" label="Заголовок *" name="heading" @@ -139,7 +136,7 @@ export const ArticleEdit = observer(() => { ( { value={ cityAutocompleteProps.options.find( (option) => option.id === field.value - ) || null + ) ?? null } onChange={(_, value) => { - field.onChange(value?.id || ""); + field.onChange(value?.id ?? ""); }} getOptionLabel={(item) => { return item ? item.name : ""; @@ -101,7 +101,7 @@ export const CarrierCreate = observer(() => { helperText={(errors as any)?.full_name?.message} margin="normal" fullWidth - InputLabelProps={{ shrink: true }} + slotProps={{inputLabel: {shrink: true}}} type="text" label={"Полное имя *"} name="full_name" @@ -109,82 +109,89 @@ export const CarrierCreate = observer(() => { - + + - - + + + { helperText={(errors as any)?.slogan?.message} margin="normal" fullWidth - InputLabelProps={{ shrink: true }} + slotProps={{inputLabel: {shrink: true}}} type="text" label={"Слоган"} name="slogan" @@ -211,10 +218,10 @@ export const CarrierCreate = observer(() => { value={ mediaAutocompleteProps.options.find( (option) => option.id === field.value - ) || null + ) ?? null } onChange={(_, value) => { - field.onChange(value?.id || ""); + field.onChange(value?.id ?? ""); }} getOptionLabel={(item) => { return item ? item.media_name : ""; diff --git a/src/pages/carrier/edit.tsx b/src/pages/carrier/edit.tsx index d8118c2..ed3b5f2 100644 --- a/src/pages/carrier/edit.tsx +++ b/src/pages/carrier/edit.tsx @@ -2,7 +2,7 @@ import { Autocomplete, Box, TextField } from "@mui/material"; import { Edit, useAutocomplete } from "@refinedev/mui"; import { useForm } from "@refinedev/react-hook-form"; import { languageStore, META_LANGUAGE } from "@stores"; -import { LanguageSelector, MediaData, MediaView } from "@ui"; +import { LanguageSelector, MediaView } from "@ui"; import { observer } from "mobx-react-lite"; import { Controller } from "react-hook-form"; @@ -42,6 +42,7 @@ export const CarrierEdit = observer(() => { ...META_LANGUAGE(language) }); + return ( { value={ cityAutocompleteProps.options.find( (option) => option.id === field.value - ) || null + ) ?? null } onChange={(_, value) => { - field.onChange(value?.id || ""); + field.onChange(value?.id ?? ""); }} getOptionLabel={(item) => { return item ? item.name : ""; @@ -100,7 +101,7 @@ export const CarrierEdit = observer(() => { helperText={(errors as any)?.full_name?.message} margin="normal" fullWidth - InputLabelProps={{ shrink: true }} + slotProps={{inputLabel: {shrink: true}}} type="text" label={"Полное имя *"} name="full_name" @@ -108,21 +109,21 @@ export const CarrierEdit = observer(() => { { helperText={(errors as any)?.main_color?.message} margin="normal" fullWidth - InputLabelProps={{ shrink: true }} + slotProps={{inputLabel: {shrink: true}}} type="color" label={"Основной цвет"} name="main_color" @@ -154,7 +155,7 @@ export const CarrierEdit = observer(() => { helperText={(errors as any)?.left_color?.message} margin="normal" fullWidth - InputLabelProps={{ shrink: true }} + slotProps={{inputLabel: {shrink: true}}} type="color" label={"Цвет левого виджета"} name="left_color" @@ -177,7 +178,7 @@ export const CarrierEdit = observer(() => { helperText={(errors as any)?.right_color?.message} margin="normal" fullWidth - InputLabelProps={{ shrink: true }} + slotProps={{inputLabel: {shrink: true}}} type="color" label={"Цвет правого виджета"} name="right_color" @@ -200,7 +201,7 @@ export const CarrierEdit = observer(() => { helperText={(errors as any)?.slogan?.message} margin="normal" fullWidth - InputLabelProps={{ shrink: true }} + slotProps={{inputLabel: {shrink: true}}} type="text" label={"Слоган"} name="slogan" @@ -217,10 +218,10 @@ export const CarrierEdit = observer(() => { value={ mediaAutocompleteProps.options.find( (option) => option.id === field.value - ) || null + ) ?? null } onChange={(_, value) => { - field.onChange(value?.id || ""); + field.onChange(value?.id ?? ""); }} getOptionLabel={(item) => { return item ? item.media_name : ""; diff --git a/src/pages/route-preview/MapDataContext.tsx b/src/pages/route-preview/MapDataContext.tsx index 91d654c..78ed1c8 100644 --- a/src/pages/route-preview/MapDataContext.tsx +++ b/src/pages/route-preview/MapDataContext.tsx @@ -1,7 +1,7 @@ import { useCustom, useApiUrl } from "@refinedev/core"; import { useParams } from "react-router"; import { createContext, ReactNode, useContext, useEffect, useMemo, useState } from "react"; -import { RouteData, SightData, StationData, StationPatchData } from "./types"; +import { RouteData, SightData, SightPatchData, StationData, StationPatchData } from "./types"; import { axiosInstance } from "../../providers/data"; const MapDataContext = createContext<{ @@ -19,6 +19,7 @@ const MapDataContext = createContext<{ setMapRotation: (rotation: number) => void, setMapCenter: (x: number, y: number) => void, setStationOffset: (stationId: number, x: number, y: number) => void, + setSightCoordinates: (sightId: number, latitude: number, longitude: number) => void, saveChanges: () => void, }>({ originalRouteData: undefined, @@ -35,6 +36,7 @@ const MapDataContext = createContext<{ setMapRotation: () => {}, setMapCenter: () => {}, setStationOffset: () => {}, + setSightCoordinates: () => {}, saveChanges: () => {}, }); @@ -52,14 +54,13 @@ export function MapDataProvider({ children }: Readonly<{ children: ReactNode }>) const [routeChanges, setRouteChanges] = useState({} as RouteData); const [stationChanges, setStationChanges] = useState([]); - const [sightChanges, setSightChanges] = useState([]); + const [sightChanges, setSightChanges] = useState([]); const { data: routeQuery, isLoading: isRouteLoading } = useCustom({ url: `${apiUrl}/route/${routeId}`, method: 'get', }); - const { data: stationQuery, isLoading: isStationLoading } = useCustom({ url: `${apiUrl}/route/${routeId}/station`, method: 'get' @@ -110,17 +111,25 @@ export function MapDataProvider({ children }: Readonly<{ children: ReactNode }>) async function saveChanges() { await axiosInstance.patch(`/route/${routeId}`, routeData); - saveStationChanges(); + await saveStationChanges(); + await saveSightChanges(); } async function saveStationChanges() { - console.log("saveStationChanges", stationChanges); for(const station of stationChanges) { const response = await axiosInstance.patch(`/route/${routeId}/station`, station); console.log("response", response); } } + async function saveSightChanges() { + console.log("sightChanges", sightChanges); + for(const sight of sightChanges) { + const response = await axiosInstance.patch(`/route/${routeId}/sight`, sight); + console.log("response", response); + } + } + function setStationOffset(stationId: number, x: number, y: number) { setStationChanges((prev) => { let found = prev.find((station) => station.station_id === stationId); @@ -148,9 +157,36 @@ export function MapDataProvider({ children }: Readonly<{ children: ReactNode }>) }); } + function setSightCoordinates(sightId: number, latitude: number, longitude: number) { + setSightChanges((prev) => { + let found = prev.find((sight) => sight.sight_id === sightId); + if(found) { + found.latitude = latitude; + found.longitude = longitude; + + return prev.map((sight) => { + if(sight.sight_id === sightId) { + return found; + } + return sight; + }); + } else { + const foundSight = sightData?.find((sight) => sight.id === sightId); + if(foundSight) { + return [...prev, { + sight_id: sightId, + latitude, + longitude + }]; + } + return prev; + } + }); + } + useEffect(() => { - console.log("stationChanges", stationChanges); - }, [stationChanges]); + console.log("sightChanges", sightChanges); + }, [sightChanges]); const value = useMemo(() => ({ originalRouteData: originalRouteData, @@ -167,6 +203,7 @@ export function MapDataProvider({ children }: Readonly<{ children: ReactNode }>) setMapCenter, saveChanges, setStationOffset, + setSightCoordinates }), [originalRouteData, originalStationData, originalSightData, routeData, stationData, sightData, isRouteLoading, isStationLoading, isSightLoading]); return ( diff --git a/src/pages/route-preview/Sight.tsx b/src/pages/route-preview/Sight.tsx index 340ae9b..039ecfa 100644 --- a/src/pages/route-preview/Sight.tsx +++ b/src/pages/route-preview/Sight.tsx @@ -4,7 +4,8 @@ import { SightData } from "./types"; import { Assets, FederatedMouseEvent, Graphics, Texture } from "pixi.js"; import { COLORS } from "../../contexts/color-mode/theme"; import { SIGHT_SIZE, UP_SCALE } from "./Constants"; -import { coordinatesToLocal } from "./utils"; +import { coordinatesToLocal, localToCoordinates } from "./utils"; +import { useMapData } from "./MapDataContext"; interface SightProps { sight: SightData; @@ -15,8 +16,9 @@ export function Sight({ sight, id }: Readonly) { const { rotation, scale } = useTransform(); + const { setSightCoordinates } = useMapData(); - const [position, setPosition] = useState({ x: 0, y: 0 }); + const [position, setPosition] = useState(coordinatesToLocal(sight.latitude, sight.longitude)); const [isDragging, setIsDragging] = useState(false); const [startPosition, setStartPosition] = useState({ x: 0, y: 0 }); const [startMousePosition, setStartMousePosition] = useState({ x: 0, y: 0 }); @@ -36,8 +38,8 @@ export function Sight({ }; const handlePointerMove = (e: FederatedMouseEvent) => { if (!isDragging) return; - const dx = (e.globalX - startMousePosition.x) / scale; - const dy = (e.globalY - startMousePosition.y) / scale; + const dx = (e.globalX - startMousePosition.x) / scale / UP_SCALE; + const dy = (e.globalY - startMousePosition.y) / scale / UP_SCALE; const cos = Math.cos(rotation); const sin = Math.sin(rotation); const newPosition = { @@ -45,6 +47,8 @@ export function Sight({ y: startPosition.y - dx * sin + dy * cos }; setPosition(newPosition); + const coordinates = localToCoordinates(newPosition.x, newPosition.y); + setSightCoordinates(sight.id, coordinates.latitude, coordinates.longitude); e.stopPropagation(); }; @@ -85,8 +89,8 @@ export function Sight({ onGlobalPointerMove={handlePointerMove} onPointerUp={handlePointerUp} onPointerUpOutside={handlePointerUp} - x={coordinates.x * UP_SCALE - SIGHT_SIZE/2 + position.x} // Offset by half width to center - y={coordinates.y * UP_SCALE - SIGHT_SIZE/2 + position.y} // Offset by half height to center + x={position.x * UP_SCALE - SIGHT_SIZE/2} // Offset by half width to center + y={position.y * UP_SCALE - SIGHT_SIZE/2} // Offset by half height to center > { formState: { errors }, } = useForm({ refineCoreProps: { - resource: "route/", + resource: "route", }, }); diff --git a/src/pages/sight/create.tsx b/src/pages/sight/create.tsx index d7b5455..ae445c9 100644 --- a/src/pages/sight/create.tsx +++ b/src/pages/sight/create.tsx @@ -1,66 +1,70 @@ import { Autocomplete, Box, TextField, Typography, Paper } from "@mui/material"; import { Create, useAutocomplete } from "@refinedev/mui"; import { useForm } from "@refinedev/react-hook-form"; -import { Controller } from "react-hook-form"; +import { Controller, FieldValues } from "react-hook-form"; import React, { useState, useEffect } from "react"; import { TOKEN_KEY } from "@providers"; import { observer } from "mobx-react-lite"; -import { Languages, languageStore, cityStore } from "@stores"; +import { EVERY_LANGUAGE, Languages, languageStore, cityStore } from "@stores"; +import { LanguageSelector } from "@ui"; export const SightCreate = observer(() => { const { language, setLanguageAction } = languageStore; const [sightData, setSightData] = useState({ - ru: { - name: "", - address: "", - }, - en: { - name: "", - address: "", - }, - zh: { - name: "", - address: "", - }, + name: EVERY_LANGUAGE(""), + address: EVERY_LANGUAGE("") }); - // Состояния для предпросмотра - const handleLanguageChange = (lang: Languages) => { - setSightData((prevData) => ({ - ...prevData, - [language]: { - name: watch("name") ?? "", - address: watch("address") ?? "", - }, - })); - setLanguageAction(lang); - }; - const { saveButtonProps, - refineCore: { formLoading }, + refineCore: { formLoading, onFinish }, register, control, watch, setValue, formState: { errors }, + handleSubmit, } = useForm({ refineCoreProps: { - resource: "sight/", + resource: "sight", }, }); const { city_id } = cityStore; useEffect(() => { - if (sightData[language as keyof typeof sightData]?.name) { - setValue("name", sightData[language as keyof typeof sightData]?.name); - } - if (sightData[language as keyof typeof sightData]?.address) { - setValue( - "address", - sightData[language as keyof typeof sightData]?.address - ); - } + setValue("name", sightData.name[language]); + setValue("address", sightData.address[language]); }, [sightData, language, setValue]); + + function updateTranslations(update: boolean = true) { + const newSightData = { + ...sightData, + name: { + ...sightData.name, + [language]: watch("name") ?? "", + }, + address: { + ...sightData.address, + [language]: watch("address") ?? "", + } + } + if(update) setSightData(newSightData); + return newSightData; + } + + const handleLanguageChange = (lang: Languages) => { + updateTranslations(); + setLanguageAction(lang); + }; + + const handleFormSubmit = handleSubmit((values: FieldValues) => { + const newTranslations = updateTranslations(false); + console.log(newTranslations); + return onFinish({ + ...values, + translations: newTranslations + }); + }); + const [namePreview, setNamePreview] = useState(""); const [coordinatesPreview, setCoordinatesPreview] = useState({ latitude: "", @@ -140,7 +144,7 @@ export const SightCreate = observer(() => { // Обновляем состояния при изменении полей useEffect(() => { - setNamePreview(nameContent || ""); + setNamePreview(nameContent ?? ""); }, [nameContent]); useEffect(() => { @@ -154,7 +158,7 @@ export const SightCreate = observer(() => { const selectedCity = cityAutocompleteProps.options.find( (option) => option.id === cityContent ); - setCityPreview(selectedCity?.name || ""); + setCityPreview(selectedCity?.name ?? ""); }, [cityContent, cityAutocompleteProps.options]); useEffect(() => { @@ -206,74 +210,25 @@ export const SightCreate = observer(() => { const selectedLeftArticle = articleAutocompleteProps.options.find( (option) => option.id === leftArticleContent ); - setLeftArticlePreview(selectedLeftArticle?.heading || ""); + setLeftArticlePreview(selectedLeftArticle?.heading ?? ""); }, [leftArticleContent, articleAutocompleteProps.options]); useEffect(() => { const selectedPreviewArticle = articleAutocompleteProps.options.find( (option) => option.id === previewArticleContent ); - setPreviewArticlePreview(selectedPreviewArticle?.heading || ""); + setPreviewArticlePreview(selectedPreviewArticle?.heading ?? ""); }, [previewArticleContent, articleAutocompleteProps.options]); return ( - + {/* Форма создания */} - - handleLanguageChange("ru")} - > - RU - - handleLanguageChange("en")} - > - EN - - handleLanguageChange("zh")} - > - ZH - - + { helperText={(errors as any)?.name?.message} margin="normal" fullWidth - InputLabelProps={{ shrink: true }} + slotProps={{inputLabel: {shrink: true}}} type="text" label={"Название *"} name="name" @@ -301,7 +256,7 @@ export const SightCreate = observer(() => { helperText={(errors as any)?.latitude?.message} margin="normal" fullWidth - InputLabelProps={{ shrink: true }} + slotProps={{inputLabel: {shrink: true}}} type="text" label={"Координаты *"} /> @@ -324,15 +279,15 @@ export const SightCreate = observer(() => { @@ -346,10 +301,10 @@ export const SightCreate = observer(() => { value={ cityAutocompleteProps.options.find( (option) => option.id === field.value - ) || null + ) ?? null } onChange={(_, value) => { - field.onChange(value?.id || ""); + field.onChange(value?.id ?? ""); }} getOptionLabel={(item) => { return item ? item.name : ""; @@ -372,7 +327,6 @@ export const SightCreate = observer(() => { variant="outlined" error={!!errors.city_id} helperText={(errors as any)?.city_id?.message} - required /> )} /> @@ -389,10 +343,10 @@ export const SightCreate = observer(() => { value={ mediaAutocompleteProps.options.find( (option) => option.id === field.value - ) || null + ) ?? null } onChange={(_, value) => { - field.onChange(value?.id || ""); + field.onChange(value?.id ?? ""); }} getOptionLabel={(item) => { return item ? item.media_name : ""; @@ -415,7 +369,7 @@ export const SightCreate = observer(() => { variant="outlined" error={!!errors.thumbnail} helperText={(errors as any)?.thumbnail?.message} - required + // required /> )} /> @@ -432,10 +386,10 @@ export const SightCreate = observer(() => { value={ mediaAutocompleteProps.options.find( (option) => option.id === field.value - ) || null + ) ?? null } onChange={(_, value) => { - field.onChange(value?.id || ""); + field.onChange(value?.id ?? ""); }} getOptionLabel={(item) => { return item ? item.media_name : ""; @@ -474,10 +428,10 @@ export const SightCreate = observer(() => { value={ mediaAutocompleteProps.options.find( (option) => option.id === field.value - ) || null + ) ?? null } onChange={(_, value) => { - field.onChange(value?.id || ""); + field.onChange(value?.id ?? ""); }} getOptionLabel={(item) => { return item ? item.media_name : ""; @@ -516,10 +470,10 @@ export const SightCreate = observer(() => { value={ articleAutocompleteProps.options.find( (option) => option.id === field.value - ) || null + ) ?? null } onChange={(_, value) => { - field.onChange(value?.id || ""); + field.onChange(value?.id ?? ""); }} getOptionLabel={(item) => { return item ? item.heading : ""; @@ -558,10 +512,10 @@ export const SightCreate = observer(() => { value={ articleAutocompleteProps.options.find( (option) => option.id === field.value - ) || null + ) ?? null } onChange={(_, value) => { - field.onChange(value?.id || ""); + field.onChange(value?.id ?? ""); }} getOptionLabel={(item) => { return item ? item.heading : ""; diff --git a/src/pages/sight/edit.tsx b/src/pages/sight/edit.tsx index 981dc00..ff69eb6 100644 --- a/src/pages/sight/edit.tsx +++ b/src/pages/sight/edit.tsx @@ -7,23 +7,21 @@ import { Tab, Tabs, Button, + Stack, } from "@mui/material"; import { Edit, useAutocomplete } from "@refinedev/mui"; import { useForm } from "@refinedev/react-hook-form"; -import { Controller } from "react-hook-form"; -import { useParams, Link } from "react-router"; +import { Controller, FieldValues } from "react-hook-form"; +import { Link, useParams } from "react-router"; import React, { useState, useEffect } from "react"; -import { LinkedItems } from "../../components/LinkedItems"; -import { CreateSightArticle } from "../../components/CreateSightArticle"; +import { CreateSightArticle, LinkedItemsContents } from "@components"; import { ArticleItem, articleFields } from "./types"; -import { TOKEN_KEY } from "@providers"; +import { axiosInstance, TOKEN_KEY } from "@providers"; import { observer } from "mobx-react-lite"; -import { Languages, languageStore, articleStore } from "@stores"; +import { Languages, languageStore, articleStore, META_LANGUAGE, EVERY_LANGUAGE } from "@stores"; import axios from "axios"; -import { LanguageSwitch } from "../../components/LanguageSwitch/index"; -import { ReactPhotoSphereViewer } from "react-photo-sphere-viewer"; -import { ModelViewer } from "@ui"; +import { LanguageSelector, MediaData, MediaView } from "@ui"; import { ArticleEditModal } from "../../components/modals/ArticleEditModal/index"; function a11yProps(index: number) { @@ -61,38 +59,43 @@ export const SightEdit = observer(() => { const [previewSelected, setPreviewSelected] = useState(true); const { setArticleModalOpenAction, setArticleIdAction } = articleStore; const [sightData, setSightData] = useState({ - ru: { - name: "", - address: "", - }, - en: { - name: "", - address: "", - }, - zh: { - name: "", - address: "", - }, + name: EVERY_LANGUAGE(""), + address: EVERY_LANGUAGE("") }); + const { saveButtonProps, register, + refineCore: {onFinish}, control, watch, getValues, setValue, + handleSubmit, formState: { errors }, } = useForm({ - refineCoreProps: { - meta: { - headers: { - "Accept-Language": language, - }, - }, - }, + refineCoreProps: META_LANGUAGE(language), }); + const getMedia = async (id?: string | number) => { + if(!id) return; + try { + const response = await axios.get( + `${import.meta.env.VITE_KRBL_API}/article/${id}/media`, + { + headers: { + Authorization: `Bearer ${localStorage.getItem(TOKEN_KEY)}`, + "Accept-Language": language, + }, + } + ); + return response.data[0]; + } catch { + return; + } + }; + useEffect(() => { setLanguageAction("ru"); }, []); @@ -106,21 +109,14 @@ export const SightEdit = observer(() => { value, }, ], - meta: { - headers: { - "Accept-Language": "ru", - }, - }, - }); - const [mediaFile, setMediaFile] = useState<{ - src: string; - media_type: number; - filename: string; - }>({ - src: "", - media_type: 1, - filename: "", + ...META_LANGUAGE("ru") }); + const [mediaFile, setMediaFile] = useState(); + const [leftArticleData, setLeftArticleData] = useState<{ + heading: string; + body: string; + media: MediaData; + }>(); const [tabValue, setTabValue] = useState(0); const { autocompleteProps: mediaAutocompleteProps } = useAutocomplete({ @@ -152,15 +148,10 @@ export const SightEdit = observer(() => { }); useEffect(() => { - if (sightData[language as keyof typeof sightData]?.name) { - setValue("name", sightData[language as keyof typeof sightData]?.name); - } - if (sightData[language as keyof typeof sightData]?.address) { - setValue( - "address", - sightData[language as keyof typeof sightData]?.address || "" - ); - } + if(sightData.name[language]) + setValue("name", sightData.name[language]); + if(sightData.address[language]) + setValue("address", sightData.address[language]); }, [language, sightData, setValue]); useEffect(() => { @@ -185,13 +176,15 @@ export const SightEdit = observer(() => { }; // Состояния для предпросмотра - const [namePreview, setNamePreview] = useState(""); + + const [creatingArticleHeading, setCreatingArticleHeading] = useState(""); + const [creatingArticleBody, setCreatingArticleBody] = useState(""); + const [coordinatesPreview, setCoordinatesPreview] = useState({ latitude: "", longitude: "", }); const [selectedArticleIndex, setSelectedArticleIndex] = useState(-1); - const [cityPreview, setCityPreview] = useState(""); const [thumbnailPreview, setThumbnailPreview] = useState(null); const [watermarkLUPreview, setWatermarkLUPreview] = useState( null @@ -203,139 +196,48 @@ export const SightEdit = observer(() => { const [linkedArticles, setLinkedArticles] = useState([]); // Следим за изменениями во всех полях const selectedArticle = linkedArticles[selectedArticleIndex]; - const [previewMedia, setPreviewMedia] = useState<{ - src: string; - media_type: number; - filename: string; - } | null>(null); - const [leftArticleMedia, setLeftArticleMedia] = useState<{ - src: string; - media_type: number; - filename: string; - } | null>(null); const previewMediaId = watch("preview_media"); const leftArticleId = watch("left_article"); useEffect(() => { if (previewMediaId) { - const getMedia = async () => { - try { - const response = await axios.get( - `${import.meta.env.VITE_KRBL_API}/article/${previewMediaId}/media`, - { - headers: { - Authorization: `Bearer ${localStorage.getItem(TOKEN_KEY)}`, - "Accept-Language": language, - }, - } - ); - const media = response.data[0]; - if (media) { - setPreviewMedia({ - src: `${import.meta.env.VITE_KRBL_MEDIA}${ - media.id - }/download?token=${localStorage.getItem(TOKEN_KEY)}`, - media_type: media.media_type, - filename: media.filename, - }); - } else { - setPreviewMedia({ - src: "", - media_type: 1, - filename: "", - }); // или другой дефолт - } - } catch (error) { - console.error("Error fetching media:", error); - setPreviewMedia({ - src: "", - media_type: 1, - filename: "", - }); // или другой дефолт - } - }; - getMedia(); + const selectedMedia = mediaAutocompleteProps.options.find( + (option) => option.id === previewMediaId + ); + if(!selectedMedia) return; + setMediaFile(selectedMedia); } }, [previewMediaId]); const addressContent = watch("address"); - const nameContent = watch("name"); const latitudeContent = watch("latitude"); const longitudeContent = watch("longitude"); - const cityContent = watch("city_id"); const thumbnailContent = watch("thumbnail"); const watermarkLUContent = watch("watermark_lu"); const watermarkRDContent = watch("watermark_rd"); - const leftArticleContent = watch("left_article"); - const previewArticleContent = watch("preview_article"); - // Обновляем состояния при изменении полей - useEffect(() => { - setNamePreview(nameContent || ""); - }, [nameContent]); - useEffect(() => { - return () => { - setLanguageAction("ru"); - }; - }, []); + // useEffect(() => { + // return () => { + // setLanguageAction("ru"); + // }; + // }, []); useEffect(() => { setCoordinatesPreview({ - latitude: latitudeContent || "", - longitude: longitudeContent || "", + latitude: latitudeContent ?? "", + longitude: longitudeContent ?? "", }); }, [latitudeContent, longitudeContent]); useEffect(() => { - const getMedia = async () => { - if (!linkedArticles[selectedArticleIndex]?.id) return; - try { - const response = await axios.get( - `${import.meta.env.VITE_KRBL_API}/article/${ - linkedArticles[selectedArticleIndex].id - }/media`, - { - headers: { - Authorization: `Bearer ${localStorage.getItem(TOKEN_KEY)}`, - "Accept-Language": language, - }, - } - ); - const media = response.data[0]; - if (media) { - setMediaFile({ - src: `${import.meta.env.VITE_KRBL_MEDIA}${ - media.id - }/download?token=${localStorage.getItem(TOKEN_KEY)}`, - media_type: media.media_type, - filename: media.filename, - }); - } else { - setMediaFile({ - src: "", - media_type: 1, - filename: "", - }); // или другой дефолт - } - } catch (error) { - setMediaFile({ - src: "", - media_type: 1, - filename: "", - }); // или обработка ошибки - } + if(linkedArticles[selectedArticleIndex]?.id) { + getMedia(linkedArticles[selectedArticleIndex].id).then((media) => { + setMediaFile(media); + }); }; - - getMedia(); }, [selectedArticleIndex, linkedArticles]); - useEffect(() => { - const selectedCity = cityAutocompleteProps.options.find( - (option) => option.id === cityContent - ); - setCityPreview(selectedCity?.name || ""); - }, [cityContent, cityAutocompleteProps.options]); useEffect(() => { const selectedThumbnail = mediaAutocompleteProps.options.find( @@ -377,53 +279,80 @@ export const SightEdit = observer(() => { }, [watermarkRDContent, mediaAutocompleteProps.options]); useEffect(() => { - const getMedia = async () => { - const selectedLeftArticle = articleAutocompleteProps.options.find( - (option) => option.id === leftArticleContent - ); - if (!selectedLeftArticle) return; - const response = await axios.get( - `${import.meta.env.VITE_KRBL_API}/article/${ - selectedLeftArticle?.id - }/media`, - { - headers: { - Authorization: `Bearer ${localStorage.getItem(TOKEN_KEY)}`, - "Accept-Language": language, - }, - } - ); - const media = response.data[0]; - if (media) { - setLeftArticleMedia({ - src: `${import.meta.env.VITE_KRBL_MEDIA}${ - media.id - }/download?token=${localStorage.getItem(TOKEN_KEY)}`, - media_type: media.media_type, - filename: media.filename, - }); - } - }; + const selectedLeftArticle = articleAutocompleteProps.options.find( + (option) => option.id === leftArticleId + ); + if (!selectedLeftArticle?.id) return; + getMedia(selectedLeftArticle.id).then((media) => { + setLeftArticleData({ + heading: selectedLeftArticle.heading, + body: selectedLeftArticle.body, + media, + }); + }); + }, [leftArticleId]); - getMedia(); - }, [leftArticleId, leftArticleContent]); - - const handleLanguageChange = (lang: string) => { - setSightData((prevData) => ({ - ...prevData, - [language]: { - name: watch("name") ?? "", - address: watch("address") ?? "", + function updateTranslations(update: boolean = true) { + const newSightData = { + ...sightData, + name: { + ...sightData.name, + [language]: watch("name") ?? "", }, - })); - setLanguageAction(lang as Languages); + address: { + ...sightData.address, + [language]: watch("address") ?? "", + } + } + if(update) setSightData(newSightData); + return newSightData; + } + + const handleLanguageChange = (lang: Languages) => { + updateTranslations(); + setLanguageAction(lang); }; + + const handleFormSubmit = handleSubmit(async (values: FieldValues) => { + const newTranslations = updateTranslations(false); + console.log(newTranslations); + await onFinish({ + ...values, + translations: newTranslations + }); + }); + useEffect(() => { return () => { setLanguageAction("ru"); }; }, [setLanguageAction]); + const [articleAdditionMode, setArticleAdditionMode] = useState<'attaching' | 'creating'>('attaching'); + const [selectedItemId, setSelectedItemId] = useState(); + const [updatedLinkedArticles, setUpdatedLinkedArticles] = useState([]); + + const linkItem = () => { + if (!selectedItemId) return; + const requestData = { + article_id: selectedItemId, + page_num: linkedArticles.length+1, + } + + axiosInstance + .post(`${import.meta.env.VITE_KRBL_API}/sight/${sightId}/article`, requestData) + .then(() => { + axiosInstance.get(`${import.meta.env.VITE_KRBL_API}/sight/${sightId}/article`) + .then((response) => { + setUpdatedLinkedArticles(response?.data || []); + setSelectedItemId(undefined); + }); + }) + .catch((error) => { + console.error("Error linking item:", error); + }); + }; + return ( @@ -440,7 +369,10 @@ export const SightEdit = observer(() => { { > - - {/* Language Selection */} - - - handleLanguageChange("ru")} - > - RU - - handleLanguageChange("en")} - > - EN - - handleLanguageChange("zh")} - > - ZH - - + + {/* Форма редактирования */} - + { })} /> - {/* - */} - - {/* */} - { /> - - ( - option.id === field.value - ) || null - } - onChange={(_, value) => { - field.onChange(value?.id || ""); - }} - getOptionLabel={(item) => { - return item ? item.media_name : ""; - }} - isOptionEqualToValue={(option, value) => { - return option.id === value?.id; - }} - filterOptions={(options, { inputValue }) => { - return options.filter((option) => - option.media_name - .toLowerCase() - .includes(inputValue.toLowerCase()) - ); - }} - renderInput={(params) => ( - - )} - /> - )} - /> - { } onChange={(_, value) => { field.onChange(value?.id || ""); - setLeftArticleMedia(null); + setLeftArticleData(undefined); }} getOptionLabel={(item) => { return item ? item.heading : ""; @@ -808,17 +599,43 @@ export const SightEdit = observer(() => { variant="outlined" error={!!errors.arms} helperText={(errors as any)?.arms?.message} - required /> )} /> )} /> + + {leftArticleId ? ( + + ) : ( + + + + )} - - {/* Блок предпросмотра */} - { gap: 2, }} > - {leftArticleMedia && - leftArticleMedia.src && - leftArticleMedia.media_type === 1 && ( - {leftArticleMedia.filename} - )} - - {leftArticleMedia && leftArticleMedia.media_type === 2 && ( - - {/* Название достопримечательности */} + + {/* Заголовок статьи */} { mb: 3, }} > - {namePreview} - - - {/* Город */} - - - Город:{" "} - - - theme.palette.mode === "dark" ? "grey.300" : "grey.800", - }} - > - {cityPreview} - + {leftArticleData?.heading} {/* Адрес */} @@ -963,77 +696,134 @@ export const SightEdit = observer(() => { {addressContent} + + {/* Текст статьи */} + + + theme.palette.mode === "dark" ? "grey.300" : "grey.800", + }} + > + {leftArticleData?.body} + + - {/* {!leftArticleId && ( - - )} */} - {leftArticleId && ( - - )} - + footerButtonProps={{ + sx: {bottom: 0, left: 0 }, + }}> - - - + + + ( option.id === field.value ) || null } - onClick={() => {}} onChange={(_, value) => { field.onChange(value?.id || ""); }} + getOptionLabel={(item) => { + return item ? item.media_name : ""; + }} + isOptionEqualToValue={(option, value) => { + return option.id === value?.id; + }} + filterOptions={(options, { inputValue }) => { + return options.filter( + (option) => + option.media_name + .toLowerCase() + .includes(inputValue.toLowerCase()) && + [1,2,5,6].includes(option.media_type) + ); + }} + renderInput={(params) => ( + { + //setPreviewSelected(true); + //setSelectedMediaIndex(-1); + }} + label="Медиа-предпросмотр" + margin="normal" + variant="outlined" + error={!!errors.arms} + helperText={(errors as any)?.arms?.message} + /> + )} + /> + )} + /> + + + + + + setArticleAdditionMode("attaching")} + > + Добавить существующую статью + + setArticleAdditionMode("creating")} + > + Создать новую статью + + + + {articleAdditionMode === "attaching" && ( + + option.id === selectedItemId + ) || null + } + onChange={(_, value) => { + setSelectedItemId(value?.id || ""); + setLeftArticleData(undefined); + }} getOptionLabel={(item) => { return item ? item.heading : ""; }} @@ -1050,26 +840,53 @@ export const SightEdit = observer(() => { renderInput={(params) => ( { - setPreviewSelected(true); - setSelectedArticleIndex(-1); - }} - label="Медиа-предпросмотр" + label="Добавляемая статья" margin="normal" variant="outlined" error={!!errors.arms} helperText={(errors as any)?.arms?.message} - required /> )} /> - )} - /> - - - - + + + + + + )} + {articleAdditionMode === "creating" && ( + { + console.log("Updating", heading) + setCreatingArticleHeading(heading); + }} + setBodyParent={(body) => { + setCreatingArticleBody(body); + }} + /> + )} + + Привязанные статьи + + + type="edit" + disableCreation parentId={sightId!} dragAllowed={true} setItemsParent={setLinkedArticles} @@ -1077,15 +894,9 @@ export const SightEdit = observer(() => { fields={articleFields} childResource="article" title="статьи" + updatedLinkedItems={updatedLinkedArticles} /> - {/* Предпросмотр */} @@ -1115,81 +926,21 @@ export const SightEdit = observer(() => { flexGrow: 1, }} > - {!previewSelected && ( - - {mediaFile && mediaFile.src && mediaFile.media_type === 1 && ( - {mediaFile.filename} - )} - - {mediaFile && mediaFile.media_type === 2 && ( - - )} + + + {mediaFile && ( + + )} + { { overflowY: "auto", }} > - {previewSelected && - previewMedia?.src && - previewMedia?.media_type === 1 && ( - {previewMedia.filename} - )} - {previewSelected && - previewMedia?.media_type === 2 && ( - - - - - + + + ( { variant="outlined" error={!!errors.city_id} helperText={(errors as any)?.city_id?.message} - required /> )} /> @@ -1493,7 +1193,6 @@ export const SightEdit = observer(() => { variant="outlined" error={!!errors.arms} helperText={(errors as any)?.arms?.message} - required /> )} /> @@ -1538,7 +1237,6 @@ export const SightEdit = observer(() => { variant="outlined" error={!!errors.arms} helperText={(errors as any)?.arms?.message} - required /> )} /> @@ -1583,7 +1281,6 @@ export const SightEdit = observer(() => { variant="outlined" error={!!errors.arms} helperText={(errors as any)?.arms?.message} - required /> )} /> @@ -1597,7 +1294,7 @@ export const SightEdit = observer(() => { helperText={(errors as any)?.latitude?.message} margin="normal" fullWidth - InputLabelProps={{ shrink: true }} + slotProps={{inputLabel: {shrink: true}}} type="text" label={"Координаты *"} /> diff --git a/src/pages/sight/list.tsx b/src/pages/sight/list.tsx index 6b50585..c309a41 100644 --- a/src/pages/sight/list.tsx +++ b/src/pages/sight/list.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from "react"; +import React from "react"; import { type GridColDef } from "@mui/x-data-grid"; import { DeleteButton, @@ -8,11 +8,10 @@ import { useDataGrid, } from "@refinedev/mui"; import { Stack } from "@mui/material"; -import { CustomDataGrid } from "../../components/CustomDataGrid"; +import { CustomDataGrid } from "@components"; import { localeText } from "../../locales/ru/localeText"; -import { cityStore } from "../../store/CityStore"; +import { cityStore, languageStore } from "@stores"; import { observer } from "mobx-react-lite"; -import { languageStore } from "../../store/LanguageStore"; export const SightList = observer(() => { const { city_id } = cityStore;