diff --git a/package.json b/package.json index 55ede4f..8408a43 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "react-simple-maps": "^3.0.0", "react-simplemde-editor": "^5.2.0", "react-swipeable": "^7.0.2", + "react-toastify": "^11.0.5", "rehype-raw": "^7.0.0", "three": "^0.175.0", "vite-plugin-svgr": "^4.3.0" diff --git a/src/components/CreateSightArticle.tsx b/src/components/CreateSightArticle.tsx index d66f10d..cda29fa 100644 --- a/src/components/CreateSightArticle.tsx +++ b/src/components/CreateSightArticle.tsx @@ -19,6 +19,7 @@ import { ALLOWED_VIDEO_TYPES, } from "../components/media/MediaFormUtils"; import { EVERY_LANGUAGE, Languages } from "@stores"; +import { useNotification } from "@refinedev/core"; const MemoizedSimpleMDE = React.memo(MarkdownEditor); @@ -30,14 +31,16 @@ type MediaFile = { }; type Props = { - parentId: string | number; + parentId?: string | number; parentResource: string; childResource: string; title: string; left?: boolean; - language: Languages, - setHeadingParent?: (heading: string) => void, - setBodyParent?: (body: string) => void, + language: Languages; + setHeadingParent?: (heading: string) => void; + setBodyParent?: (body: string) => void; + onSave?: (something: any) => void; + noReset?: boolean; }; export const CreateSightArticle = ({ @@ -48,8 +51,11 @@ export const CreateSightArticle = ({ left, language, setHeadingParent, - setBodyParent + setBodyParent, + onSave, + noReset, }: Props) => { + const notification = useNotification(); const theme = useTheme(); const [mediaFiles, setMediaFiles] = useState([]); const [workingLanguage, setWorkingLanguage] = useState(language); @@ -69,10 +75,9 @@ export const CreateSightArticle = ({ }, }); - const [articleData, setArticleData] = useState({ heading: EVERY_LANGUAGE(""), - body: EVERY_LANGUAGE("") + body: EVERY_LANGUAGE(""), }); function updateTranslations() { @@ -85,8 +90,8 @@ export const CreateSightArticle = ({ body: { ...articleData.body, [workingLanguage]: watch("body") ?? "", - } - } + }, + }; setArticleData(newArticleData); return newArticleData; } @@ -126,8 +131,12 @@ export const CreateSightArticle = ({ const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, accept: { - "image/*": ALLOWED_IMAGE_TYPES, - "video/*": ALLOWED_VIDEO_TYPES, + "image/jpeg": [".jpeg", ".jpg"], + "image/png": [".png"], + "image/webp": [".webp"], + "video/mp4": [".mp4"], + "video/webm": [".webm"], + "video/ogg": [".ogg"], }, multiple: true, }); @@ -153,42 +162,48 @@ 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() + translations: updateTranslations(), } ); const itemId = response.data.id; - // Получаем существующие статьи для определения порядкового номера - const existingItemsResponse = await axiosInstance.get( - `${import.meta.env.VITE_KRBL_API}/${parentResource}/${parentId}/${childResource}` - ); - const existingItems = existingItemsResponse.data ?? []; - const nextPageNum = existingItems.length + 1; + if (parentId) { + // Получаем существующие статьи для определения порядкового номера + const existingItemsResponse = await axiosInstance.get( + `${ + import.meta.env.VITE_KRBL_API + }/${parentResource}/${parentId}/${childResource}` + ); + const existingItems = existingItemsResponse.data ?? []; + const nextPageNum = existingItems.length + 1; - if (!left) { - // Привязываем статью к достопримечательности если она не левая - await axiosInstance.post( - `${import.meta.env.VITE_KRBL_API}/${parentResource}/${parentId}/${childResource}/`, - { - [`${childResource}_id`]: itemId, - page_num: nextPageNum, - } - ); - } else { - const response = await axiosInstance.get( - `${import.meta.env.VITE_KRBL_API}/${parentResource}/${parentId}/` - ); - const data = response.data; - if(data) { - await axiosInstance.patch( - `${import.meta.env.VITE_KRBL_API}/${parentResource}/${parentId}/`, + if (!left) { + await axiosInstance.post( + `${ + import.meta.env.VITE_KRBL_API + }/${parentResource}/${parentId}/${childResource}/`, { - ...data, - left_article: itemId + [`${childResource}_id`]: itemId, + page_num: nextPageNum, } ); + } else { + const response = await axiosInstance.get( + `${import.meta.env.VITE_KRBL_API}/${parentResource}/${parentId}/` + ); + const data = response.data; + if (data) { + await axiosInstance.patch( + `${import.meta.env.VITE_KRBL_API}/${parentResource}/${parentId}/`, + { + ...data, + left_article: itemId, + } + ); + } } } @@ -211,10 +226,21 @@ export const CreateSightArticle = ({ ) ) ); - - resetItem(); - setMediaFiles([]); - window.location.reload(); + if (noReset) { + setValue("heading", ""); + setValue("body", ""); + } else { + resetItem(); + } + if (onSave) { + onSave(response.data); + notification.open({ + message: "Статья успешно создана", + type: "success", + }); + } else { + window.location.reload(); + } } catch (err: any) { console.error("Error creating item:", err); } @@ -239,7 +265,7 @@ export const CreateSightArticle = ({ helperText={(itemErrors as any)?.heading?.message} margin="normal" fullWidth - slotProps={{inputLabel: {shrink: true}}} + slotProps={{ inputLabel: { shrink: true } }} type="text" sx={{ backgroundColor: theme.palette.background.paper, @@ -345,7 +371,12 @@ export const CreateSightArticle = ({ - - ) : ( - <> - - Создать и прикрепить новую статью: - - { - //console.log("Updating", heading) - setCreatingArticleHeading(heading); - }} - setBodyParent={(body) => { - setCreatingArticleBody(body); - }} + + + ( + 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) => ( + + )} + /> + )} /> - - - // - // - // - )} + + + ( + option.id === field.value + ) || null + } + onChange={(_, value) => { + field.onChange(value?.id || ""); + setLeftArticleData(undefined); + }} + getOptionLabel={(item) => { + return item ? item.heading : ""; + }} + isOptionEqualToValue={(option, value) => { + return option.id === value?.id; + }} + filterOptions={(options, { inputValue }) => { + return options.filter((option) => + option.heading + .toLowerCase() + .includes(inputValue.toLowerCase()) + ); + }} + renderInput={(params) => ( + + )} + /> + )} + /> + + {leftArticleId ? ( + + ) : ( + <> + + Создать и прикрепить новую статью: + + { + //console.log("Updating", heading) + setCreatingArticleHeading(heading); + }} + setBodyParent={(body) => { + setCreatingArticleBody(body); + }} + /> + + + // + // + // + )} + - - { mb: 3, }} > - {leftArticleId ? leftArticleData?.heading : creatingArticleHeading} + {name} {/* Адрес */} @@ -786,752 +827,841 @@ export const SightEdit = observer(() => { - {/* Текст статьи */} - - theme.palette.mode === "dark" ? "grey.300" : "grey.800", - }} - > + }, + "& a": { + color: "primary.main", + textDecoration: "none", + "&:hover": { + textDecoration: "underline", + }, + }, + "& blockquote": { + borderLeft: "4px solid", + borderColor: "primary.main", + pl: 2, + my: 2, + color: "text.secondary", + }, + "& code": { + bgcolor: (theme) => + theme.palette.mode === "dark" ? "grey.900" : "grey.100", + p: 0.5, + borderRadius: 0.5, + color: "primary.main", + }, + }} + > + {leftArticleId ? leftArticleData?.body : creatingArticleBody} - - + + - - - - - - ( - option.id === field.value - ) || null - } - onChange={(_, value) => { - console.log(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); + sx: { bottom: 0, left: 0 }, + }} + > + + + + ( + option.id === field.value + ) || null + } + onChange={(_, value) => { + console.log(value, _); + field.onChange(value?.id || ""); }} - label="Медиа-предпросмотр" - margin="normal" - variant="outlined" - error={!!errors.arms} - helperText={(errors as any)?.arms?.message} + 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) => ( + { + setPreviewSelected(true); + //setSelectedMediaIndex(-1); + }} + label="Медиа-предпросмотр" + margin="normal" + variant="outlined" + error={!!errors.arms} + helperText={(errors as any)?.arms?.message} + /> + )} /> )} /> - )} - /> - - - - - - setArticleAdditionMode("attaching")} - > - Добавить существующую статью - - setArticleAdditionMode("creating")} - > - Создать и привязать новую статью + + + + + + setArticleAdditionMode("attaching")} + > + Добавить существующую статью + + setArticleAdditionMode("creating")} + > + Создать и привязать новую статью + - - {articleAdditionMode === "attaching" && ( - - option.id === selectedItemId - ) || null - } - onChange={(_, value) => { - setSelectedItemId(value?.id || ""); - setLeftArticleData(undefined); - }} - getOptionLabel={(item) => { - return item ? item.heading : ""; - }} - isOptionEqualToValue={(option, value) => { - return option.id === value?.id; - }} - filterOptions={(options, { inputValue }) => { - return options.filter((option) => - option.heading - .toLowerCase() - .includes(inputValue.toLowerCase()) - ); - }} - renderInput={(params) => ( - + option.id === selectedItemId + ) || null + } + onChange={(_, value) => { + setSelectedItemId(value?.id || ""); + setLeftArticleData(undefined); + }} + getOptionLabel={(item) => { + return item ? item.heading : ""; + }} + isOptionEqualToValue={(option, value) => { + return option.id === value?.id; + }} + filterOptions={(options, { inputValue }) => { + return options.filter((option) => + option.heading + .toLowerCase() + .includes(inputValue.toLowerCase()) + ); + }} + renderInput={(params) => ( + + )} + /> + + + + + + )} + {articleAdditionMode === "creating" && ( + { + console.log("Updating", heading); + setCreatingArticleHeading(heading); + }} + setBodyParent={(body) => { + setCreatingArticleBody(body); + }} /> - - - - - - )} - {articleAdditionMode === "creating" && ( - + Привязанные статьи + + + + type="edit" + disableCreation parentId={sightId!} + dragAllowed={true} + setItemsParent={setLinkedArticles} parentResource="sight" + fields={articleFields} childResource="article" - title="статью" - //left - setHeadingParent={(heading) => { - console.log("Updating", heading) - setCreatingArticleHeading(heading); - }} - setBodyParent={(body) => { - setCreatingArticleBody(body); - }} + title="статьи" + updatedLinkedItems={updatedLinkedArticles} /> - )} - - Привязанные статьи - - - - type="edit" - disableCreation - parentId={sightId!} - dragAllowed={true} - setItemsParent={setLinkedArticles} - parentResource="sight" - fields={articleFields} - childResource="article" - title="статьи" - updatedLinkedItems={updatedLinkedArticles} - /> - + - - {/* Предпросмотр */} - - - - {mediaFile && ( - - )} - - - { + {previewSelected && previewMediaFile && ( + + )} + {mediaFile && !previewSelected && ( + + )} + - {!previewSelected && articleAdditionMode !== "creating" && ( - - {selectedArticle && ( + { + + {!previewSelected && articleAdditionMode !== "creating" && ( + + {selectedArticle && ( + + {selectedArticle.heading} + + )} + + {selectedArticle && ( + + theme.palette.mode === "dark" + ? "grey.300" + : "grey.800", + }, + "& a": { + color: "primary.main", + textDecoration: "none", + "&:hover": { + textDecoration: "underline", + }, + }, + "& blockquote": { + borderLeft: "4px solid", + borderColor: "primary.main", + pl: 2, + my: 2, + color: "text.secondary", + }, + "& code": { + bgcolor: (theme) => + theme.palette.mode === "dark" + ? "grey.900" + : "grey.100", + p: 0.5, + borderRadius: 0.5, + color: "primary.main", + }, + }} + > + + {selectedArticle.body} + + + )} + + )} + + {articleAdditionMode === "creating" && ( + - {selectedArticle.heading} + {creatingArticleHeading} - )} - {selectedArticle && ( - {selectedArticle.body} + {creatingArticleBody} - )} - - )} - - {articleAdditionMode === "creating" && ( - - - {creatingArticleHeading} - - - - {creatingArticleBody} - - - )} - - + + )} + - {linkedArticles.map((article, index) => ( - { - setSelectedArticleIndex(index); - setPreviewSelected(false); - }} - sx={{ - cursor: "pointer", - bgcolor: "transparent", - color: "inherit", - textDecoration: - selectedArticleIndex === index ? - "underline" : "none", - p: 1, - borderRadius: 1, - }} - > - - {article.heading} - - - ))} + + {linkedArticles.map((article, index) => ( + { + setSelectedArticleIndex(index); + setPreviewSelected(false); + }} + sx={{ + cursor: "pointer", + bgcolor: "transparent", + color: "inherit", + textDecoration: + selectedArticleIndex === index + ? "underline" + : "none", + p: 1, + borderRadius: 1, + }} + > + + {article.heading} + + + ))} + - - } - - - - - - - - - - - - - - - ( - option.id === field.value - ) || null - } - onChange={(_, value) => { - field.onChange(value?.id || ""); - }} - getOptionLabel={(item) => { - return item ? item.name : ""; - }} - isOptionEqualToValue={(option, value) => { - console.log(cityAutocompleteProps.options) - return option.id === value?.id; - }} - filterOptions={(options, { inputValue }) => { - return options.filter((option) => - option.name - .toLowerCase() - .includes(inputValue.toLowerCase()) - ); - }} - renderInput={(params) => ( - - )} - /> - )} - /> - - ( - 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()) && - option.media_type === 3 - ); - }} - renderInput={(params) => ( - - )} - /> - )} - /> - - ( - 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()) && - option.media_type === 4 - ); - }} - renderInput={(params) => ( - - )} - /> - )} - /> - - ( - 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()) && - option.media_type === 4 - ); - }} - renderInput={(params) => ( - - )} - /> - )} - /> - - - - - {/* Предпросмотр */} - - theme.palette.mode === "dark" ? "background.paper" : "#fff", - }} - > - - Предпросмотр - - - {thumbnailPreview && ( - - - - Адрес:{" "} - - - theme.palette.mode === "dark" ? "grey.300" : "grey.800", - }} - > - {`${addressContent}`} - - - - Логотип достопримечательности: - - - - )} - - {/* Водяные знаки */} - - - Водяные знаки: - - - {watermarkLUPreview && ( - - - Левый верхний: - - - - )} - {watermarkRDPreview && ( - - - Правый верхний: - - - - )} - - {/* Координаты */} - - - Координаты:{" "} - - - theme.palette.mode === "dark" ? "grey.300" : "grey.800", - }} - > - {`${coordinatesPreview.latitude}, ${coordinatesPreview.longitude}`} - - + } + + + + + + + + + + + ( + option.id === field.value + ) || null + } + onChange={(_, value) => { + field.onChange(value?.id || ""); + }} + getOptionLabel={(item) => { + return item ? item.name : ""; + }} + isOptionEqualToValue={(option, value) => { + console.log(cityAutocompleteProps.options); + return option.id === value?.id; + }} + filterOptions={(options, { inputValue }) => { + return options.filter((option) => + option.name + .toLowerCase() + .includes(inputValue.toLowerCase()) + ); + }} + renderInput={(params) => ( + + )} + /> + )} + /> + + ( + 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()) && + option.media_type === 3 + ); + }} + renderInput={(params) => ( + + )} + /> + )} + /> + + ( + 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()) && + option.media_type === 4 + ); + }} + renderInput={(params) => ( + + )} + /> + )} + /> + + ( + 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()) && + option.media_type === 4 + ); + }} + renderInput={(params) => ( + + )} + /> + )} + /> + + + + + {/* Предпросмотр */} + + theme.palette.mode === "dark" ? "background.paper" : "#fff", + }} + > + + Предпросмотр + + + {thumbnailPreview && ( + + + + Адрес:{" "} + + + theme.palette.mode === "dark" + ? "grey.300" + : "grey.800", + }} + > + {`${addressContent}`} + + + + Логотип достопримечательности: + + + + )} + + {/* Водяные знаки */} + + + Водяные знаки: + + + {watermarkLUPreview && ( + + + Левый верхний: + + + + )} + {watermarkRDPreview && ( + + + Правый верхний: + + + + )} + + {/* Координаты */} + + + Координаты:{" "} + + + theme.palette.mode === "dark" + ? "grey.300" + : "grey.800", + }} + > + {`${coordinatesPreview.latitude}, ${coordinatesPreview.longitude}`} + + + + + diff --git a/src/pages/snapshot/list.tsx b/src/pages/snapshot/list.tsx index aeb5461..a0cb5f1 100644 --- a/src/pages/snapshot/list.tsx +++ b/src/pages/snapshot/list.tsx @@ -12,8 +12,14 @@ import { CustomDataGrid } from "@components"; import { localeText } from "../../locales/ru/localeText"; import { observer } from "mobx-react-lite"; import { useMany } from "@refinedev/core"; +import { DatabaseBackup } from "lucide-react"; +import axios from "axios"; +import { TOKEN_KEY } from "../../providers/authProvider"; +import { toast } from "react-toastify"; +import { useNotification } from "@refinedev/core"; export const SnapshotList = observer(() => { + const notification = useNotification(); const { dataGridProps } = useDataGrid({ resource: "snapshots", hasPagination: false, @@ -47,6 +53,30 @@ export const SnapshotList = observer(() => { return map; }, [parentsData]); + const handleBackup = async (id: number) => { + try { + const response = await axios.post( + `${import.meta.env.VITE_KRBL_API}/snapshots/${id}/restore`, + {}, + { + headers: { + Authorization: `Bearer ${localStorage.getItem(TOKEN_KEY)}`, + }, + } + ); + + notification?.open({ + message: "Cнапшот восстановлен", + type: "success", + }); + } catch (error) { + notification?.open({ + message: "Ошибка при восстановлении снимка", + type: "error", + }); + } + }; + const columns = React.useMemo( () => [ { @@ -80,6 +110,12 @@ export const SnapshotList = observer(() => { renderCell: function render({ row }) { return ( <> + { config.headers["X-Language"] = config.headers["Accept-Language"]; - console.log("Request headers:", config.headers); - return config; }); diff --git a/yarn.lock b/yarn.lock index fb2e746..5bc75d3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5703,6 +5703,13 @@ react-swipeable@^7.0.2: resolved "https://registry.npmjs.org/react-swipeable/-/react-swipeable-7.0.2.tgz" integrity sha512-v1Qx1l+aC2fdxKa9aKJiaU/ZxmJ5o98RMoFwUqAAzVWUcxgfHFXDDruCKXhw6zIYXm6V64JiHgP9f6mlME5l8w== +react-toastify@^11.0.5: + version "11.0.5" + resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-11.0.5.tgz#ce4c42d10eeb433988ab2264d3e445c4e9d13313" + integrity sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA== + dependencies: + clsx "^2.1.1" + react-transition-group@^4.4.5: version "4.4.5" resolved "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz"