Latest version #12

Merged
Kerblif merged 46 commits from preview into master 2025-05-29 10:12:00 +00:00
9 changed files with 1388 additions and 1117 deletions
Showing only changes of commit 16640cb116 - Show all commits

View File

@ -51,6 +51,7 @@
"react-simple-maps": "^3.0.0", "react-simple-maps": "^3.0.0",
"react-simplemde-editor": "^5.2.0", "react-simplemde-editor": "^5.2.0",
"react-swipeable": "^7.0.2", "react-swipeable": "^7.0.2",
"react-toastify": "^11.0.5",
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
"three": "^0.175.0", "three": "^0.175.0",
"vite-plugin-svgr": "^4.3.0" "vite-plugin-svgr": "^4.3.0"

View File

@ -19,6 +19,7 @@ import {
ALLOWED_VIDEO_TYPES, ALLOWED_VIDEO_TYPES,
} from "../components/media/MediaFormUtils"; } from "../components/media/MediaFormUtils";
import { EVERY_LANGUAGE, Languages } from "@stores"; import { EVERY_LANGUAGE, Languages } from "@stores";
import { useNotification } from "@refinedev/core";
const MemoizedSimpleMDE = React.memo(MarkdownEditor); const MemoizedSimpleMDE = React.memo(MarkdownEditor);
@ -30,14 +31,16 @@ type MediaFile = {
}; };
type Props = { type Props = {
parentId: string | number; parentId?: string | number;
parentResource: string; parentResource: string;
childResource: string; childResource: string;
title: string; title: string;
left?: boolean; left?: boolean;
language: Languages, language: Languages;
setHeadingParent?: (heading: string) => void, setHeadingParent?: (heading: string) => void;
setBodyParent?: (body: string) => void, setBodyParent?: (body: string) => void;
onSave?: (something: any) => void;
noReset?: boolean;
}; };
export const CreateSightArticle = ({ export const CreateSightArticle = ({
@ -48,8 +51,11 @@ export const CreateSightArticle = ({
left, left,
language, language,
setHeadingParent, setHeadingParent,
setBodyParent setBodyParent,
onSave,
noReset,
}: Props) => { }: Props) => {
const notification = useNotification();
const theme = useTheme(); const theme = useTheme();
const [mediaFiles, setMediaFiles] = useState<MediaFile[]>([]); const [mediaFiles, setMediaFiles] = useState<MediaFile[]>([]);
const [workingLanguage, setWorkingLanguage] = useState<Languages>(language); const [workingLanguage, setWorkingLanguage] = useState<Languages>(language);
@ -69,10 +75,9 @@ export const CreateSightArticle = ({
}, },
}); });
const [articleData, setArticleData] = useState({ const [articleData, setArticleData] = useState({
heading: EVERY_LANGUAGE(""), heading: EVERY_LANGUAGE(""),
body: EVERY_LANGUAGE("") body: EVERY_LANGUAGE(""),
}); });
function updateTranslations() { function updateTranslations() {
@ -85,8 +90,8 @@ export const CreateSightArticle = ({
body: { body: {
...articleData.body, ...articleData.body,
[workingLanguage]: watch("body") ?? "", [workingLanguage]: watch("body") ?? "",
} },
} };
setArticleData(newArticleData); setArticleData(newArticleData);
return newArticleData; return newArticleData;
} }
@ -126,8 +131,12 @@ export const CreateSightArticle = ({
const { getRootProps, getInputProps, isDragActive } = useDropzone({ const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop, onDrop,
accept: { accept: {
"image/*": ALLOWED_IMAGE_TYPES, "image/jpeg": [".jpeg", ".jpg"],
"video/*": ALLOWED_VIDEO_TYPES, "image/png": [".png"],
"image/webp": [".webp"],
"video/mp4": [".mp4"],
"video/webm": [".webm"],
"video/ogg": [".ogg"],
}, },
multiple: true, multiple: true,
}); });
@ -153,42 +162,48 @@ export const CreateSightArticle = ({
try { try {
// Создаем статью // Создаем статью
const response = await axiosInstance.post( const response = await axiosInstance.post(
`${import.meta.env.VITE_KRBL_API}/${childResource}`, { `${import.meta.env.VITE_KRBL_API}/${childResource}`,
{
...data, ...data,
translations: updateTranslations() translations: updateTranslations(),
} }
); );
const itemId = response.data.id; const itemId = response.data.id;
// Получаем существующие статьи для определения порядкового номера if (parentId) {
const existingItemsResponse = await axiosInstance.get( // Получаем существующие статьи для определения порядкового номера
`${import.meta.env.VITE_KRBL_API}/${parentResource}/${parentId}/${childResource}` const existingItemsResponse = await axiosInstance.get(
); `${
const existingItems = existingItemsResponse.data ?? []; import.meta.env.VITE_KRBL_API
const nextPageNum = existingItems.length + 1; }/${parentResource}/${parentId}/${childResource}`
);
const existingItems = existingItemsResponse.data ?? [];
const nextPageNum = existingItems.length + 1;
if (!left) { if (!left) {
// Привязываем статью к достопримечательности если она не левая await axiosInstance.post(
await axiosInstance.post( `${
`${import.meta.env.VITE_KRBL_API}/${parentResource}/${parentId}/${childResource}/`, 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}/`,
{ {
...data, [`${childResource}_id`]: itemId,
left_article: 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 = ({
) )
) )
); );
if (noReset) {
resetItem(); setValue("heading", "");
setMediaFiles([]); setValue("body", "");
window.location.reload(); } else {
resetItem();
}
if (onSave) {
onSave(response.data);
notification.open({
message: "Статья успешно создана",
type: "success",
});
} else {
window.location.reload();
}
} catch (err: any) { } catch (err: any) {
console.error("Error creating item:", err); console.error("Error creating item:", err);
} }
@ -239,7 +265,7 @@ export const CreateSightArticle = ({
helperText={(itemErrors as any)?.heading?.message} helperText={(itemErrors as any)?.heading?.message}
margin="normal" margin="normal"
fullWidth fullWidth
slotProps={{inputLabel: {shrink: true}}} slotProps={{ inputLabel: { shrink: true } }}
type="text" type="text"
sx={{ sx={{
backgroundColor: theme.palette.background.paper, backgroundColor: theme.palette.background.paper,
@ -345,7 +371,12 @@ export const CreateSightArticle = ({
</Box> </Box>
<Box sx={{ mt: 2, display: "flex", gap: 2 }}> <Box sx={{ mt: 2, display: "flex", gap: 2 }}>
<Button variant="contained" color="primary" onClick={handleSubmitItem(handleCreate)}> <Button
variant="contained"
color="primary"
type="submit"
onClick={handleSubmitItem(handleCreate)}
>
Создать Создать
</Button> </Button>
<Button <Button

View File

@ -38,7 +38,7 @@ const style = {
bgcolor: "background.paper", bgcolor: "background.paper",
border: "2px solid #000", border: "2px solid #000",
boxShadow: 24, boxShadow: 24,
p: 4 p: 4,
}; };
export const ArticleEditModal = observer(() => { export const ArticleEditModal = observer(() => {
@ -62,13 +62,11 @@ export const ArticleEditModal = observer(() => {
// Load existing media files when editing an article // Load existing media files when editing an article
const loadExistingMedia = async () => { const loadExistingMedia = async () => {
console.log("Called loadExistingMedia") console.log("Called loadExistingMedia");
if (selectedArticleId) { if (selectedArticleId) {
try { try {
const response = await axiosInstance.get( const response = await axiosInstance.get(
`${ `${import.meta.env.VITE_KRBL_API}/article/${selectedArticleId}/media`
import.meta.env.VITE_KRBL_API
}/article/${selectedArticleId}/media`
); );
const existingMedia = response.data; const existingMedia = response.data;
@ -95,7 +93,7 @@ export const ArticleEditModal = observer(() => {
); );
setMediaFiles(mediaFiles); setMediaFiles(mediaFiles);
setRefresh(refresh+1); setRefresh(refresh + 1);
} catch (error) { } catch (error) {
console.error("Error loading existing media:", error); console.error("Error loading existing media:", error);
} }
@ -125,7 +123,9 @@ export const ArticleEditModal = observer(() => {
try { try {
// Upload new media files // Upload new media files
const newMediaFiles = mediaFiles.filter((file) => !file.media_id); const newMediaFiles = mediaFiles.filter((file) => !file.media_id);
const existingMediaAmount = mediaFiles.filter((file) => file.media_id).length; const existingMediaAmount = mediaFiles.filter(
(file) => file.media_id
).length;
const mediaIds = await Promise.all( const mediaIds = await Promise.all(
newMediaFiles.map(async (mediaFile) => { newMediaFiles.map(async (mediaFile) => {
return await uploadMedia(mediaFile); return await uploadMedia(mediaFile);
@ -164,10 +164,10 @@ export const ArticleEditModal = observer(() => {
useEffect(() => { useEffect(() => {
if (articleData.heading[language]) { if (articleData.heading[language]) {
setValue("heading", articleData.heading[language]) setValue("heading", articleData.heading[language]);
} }
if (articleData.body[language]) { if (articleData.body[language]) {
setValue("body", articleData.body[language]) setValue("body", articleData.body[language]);
} }
}, [language, articleData, setValue]); }, [language, articleData, setValue]);
@ -176,12 +176,12 @@ export const ArticleEditModal = observer(() => {
...prevData, ...prevData,
heading: { heading: {
...prevData.heading, ...prevData.heading,
[language]: watch("heading") ?? "" [language]: watch("heading") ?? "",
}, },
body: { body: {
...prevData.body, ...prevData.body,
[language]: watch("body") ?? "" [language]: watch("body") ?? "",
} },
})); }));
}; };
@ -205,8 +205,12 @@ export const ArticleEditModal = observer(() => {
const { getRootProps, getInputProps, isDragActive } = useDropzone({ const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop, onDrop,
accept: { accept: {
"image/*": ALLOWED_IMAGE_TYPES, "image/jpeg": [".jpeg", ".jpg"],
"video/*": ALLOWED_VIDEO_TYPES, "image/png": [".png"],
"image/webp": [".webp"],
"video/mp4": [".mp4"],
"video/webm": [".webm"],
"video/ogg": [".ogg"],
}, },
multiple: true, multiple: true,
}); });
@ -258,7 +262,7 @@ export const ArticleEditModal = observer(() => {
onClose={() => setArticleModalOpenAction(false)} onClose={() => setArticleModalOpenAction(false)}
aria-labelledby="modal-modal-title" aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description" aria-describedby="modal-modal-description"
sx={{overflow: "auto"}} sx={{ overflow: "auto" }}
> >
<Box sx={style}> <Box sx={style}>
<Edit <Edit

View File

@ -1,5 +1,5 @@
@import './stylesheets/hidden-functionality.css'; @import "./stylesheets/hidden-functionality.css";
@import './stylesheets/roles-functionality.css'; @import "./stylesheets/roles-functionality.css";
.limited-text { .limited-text {
overflow: hidden; overflow: hidden;
@ -7,3 +7,19 @@
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
} }
.backup-button {
background-color: transparent;
border: none;
cursor: pointer;
padding: 0;
margin: 0;
width: 35px;
height: 35px;
color: #544044;
border-radius: 10%;
transition: all 0.3s ease;
}
.backup-button:hover {
background-color: rgba(84, 64, 68, 0.5);
}

View File

@ -1,4 +1,13 @@
import { Autocomplete, Box, TextField, Typography, Paper } from "@mui/material"; import {
Autocomplete,
Box,
TextField,
Typography,
Paper,
Accordion,
AccordionSummary,
AccordionDetails,
} from "@mui/material";
import { Create, useAutocomplete } from "@refinedev/mui"; import { Create, useAutocomplete } from "@refinedev/mui";
import { useForm } from "@refinedev/react-hook-form"; import { useForm } from "@refinedev/react-hook-form";
import { Controller, FieldValues } from "react-hook-form"; import { Controller, FieldValues } from "react-hook-form";
@ -8,11 +17,14 @@ import { TOKEN_KEY } from "@providers";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { EVERY_LANGUAGE, Languages, languageStore, cityStore } from "@stores"; import { EVERY_LANGUAGE, Languages, languageStore, cityStore } from "@stores";
import { LanguageSelector } from "@ui"; import { LanguageSelector } from "@ui";
import { CreateSightArticle } from "@components";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
export const SightCreate = observer(() => { export const SightCreate = observer(() => {
const { language, setLanguageAction } = languageStore; const { language, setLanguageAction } = languageStore;
const [sightData, setSightData] = useState({ const [sightData, setSightData] = useState({
name: EVERY_LANGUAGE(""), name: EVERY_LANGUAGE(""),
address: EVERY_LANGUAGE("") address: EVERY_LANGUAGE(""),
}); });
const { const {
@ -46,9 +58,9 @@ export const SightCreate = observer(() => {
address: { address: {
...sightData.address, ...sightData.address,
[language]: watch("address") ?? "", [language]: watch("address") ?? "",
} },
} };
if(update) setSightData(newSightData); if (update) setSightData(newSightData);
return newSightData; return newSightData;
} }
@ -62,7 +74,7 @@ export const SightCreate = observer(() => {
console.log(newTranslations); console.log(newTranslations);
return onFinish({ return onFinish({
...values, ...values,
translations: newTranslations translations: newTranslations,
}); });
}); });
@ -71,6 +83,11 @@ export const SightCreate = observer(() => {
latitude: "", latitude: "",
longitude: "", longitude: "",
}); });
const [creatingArticleHeading, setCreatingArticleHeading] =
useState<string>("");
const [creatingArticleBody, setCreatingArticleBody] = useState<string>("");
const [cityPreview, setCityPreview] = useState(""); const [cityPreview, setCityPreview] = useState("");
const [thumbnailPreview, setThumbnailPreview] = useState<string | null>(null); const [thumbnailPreview, setThumbnailPreview] = useState<string | null>(null);
const [watermarkLUPreview, setWatermarkLUPreview] = useState<string | null>( const [watermarkLUPreview, setWatermarkLUPreview] = useState<string | null>(
@ -80,6 +97,8 @@ export const SightCreate = observer(() => {
null null
); );
const [leftArticlePreview, setLeftArticlePreview] = useState(""); const [leftArticlePreview, setLeftArticlePreview] = useState("");
const [customOptions, setCustomOptions] = useState<any[]>([]);
const [previewArticlePreview, setPreviewArticlePreview] = useState(""); const [previewArticlePreview, setPreviewArticlePreview] = useState("");
const handleCoordinatesChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleCoordinatesChange = (e: React.ChangeEvent<HTMLInputElement>) => {
@ -122,6 +141,7 @@ export const SightCreate = observer(() => {
const { autocompleteProps: articleAutocompleteProps } = useAutocomplete({ const { autocompleteProps: articleAutocompleteProps } = useAutocomplete({
resource: "article", resource: "article",
onSearch: (value) => [ onSearch: (value) => [
{ {
field: "heading", field: "heading",
@ -131,6 +151,8 @@ export const SightCreate = observer(() => {
], ],
}); });
const mergedOptions = [...articleAutocompleteProps.options, ...customOptions];
// Следим за изменениями во всех полях // Следим за изменениями во всех полях
const nameContent = watch("name"); const nameContent = watch("name");
const addressContent = watch("address"); const addressContent = watch("address");
@ -222,10 +244,13 @@ export const SightCreate = observer(() => {
}, [previewArticleContent, articleAutocompleteProps.options]); }, [previewArticleContent, articleAutocompleteProps.options]);
return ( return (
<Create isLoading={formLoading} saveButtonProps={{ <Create
...saveButtonProps, isLoading={formLoading}
onClick: handleFormSubmit saveButtonProps={{
}}> ...saveButtonProps,
onClick: handleFormSubmit,
}}
>
<Box sx={{ display: "flex", gap: 2 }}> <Box sx={{ display: "flex", gap: 2 }}>
<Box sx={{ display: "flex", flexDirection: "column", flex: 1, gap: 2 }}> <Box sx={{ display: "flex", flexDirection: "column", flex: 1, gap: 2 }}>
{/* Форма создания */} {/* Форма создания */}
@ -244,7 +269,7 @@ export const SightCreate = observer(() => {
helperText={(errors as any)?.name?.message} helperText={(errors as any)?.name?.message}
margin="normal" margin="normal"
fullWidth fullWidth
slotProps={{inputLabel: {shrink: true}}} slotProps={{ inputLabel: { shrink: true } }}
type="text" type="text"
label={"Название *"} label={"Название *"}
name="name" name="name"
@ -257,7 +282,7 @@ export const SightCreate = observer(() => {
helperText={(errors as any)?.latitude?.message} helperText={(errors as any)?.latitude?.message}
margin="normal" margin="normal"
fullWidth fullWidth
slotProps={{inputLabel: {shrink: true}}} slotProps={{ inputLabel: { shrink: true } }}
type="text" type="text"
label={"Координаты *"} label={"Координаты *"}
/> />
@ -286,7 +311,7 @@ export const SightCreate = observer(() => {
helperText={(errors as any)?.address?.message} helperText={(errors as any)?.address?.message}
margin="normal" margin="normal"
fullWidth fullWidth
slotProps={{inputLabel: {shrink: true}}} slotProps={{ inputLabel: { shrink: true } }}
type="text" type="text"
label={"Адрес"} label={"Адрес"}
name="address" name="address"
@ -370,7 +395,7 @@ export const SightCreate = observer(() => {
variant="outlined" variant="outlined"
error={!!errors.thumbnail} error={!!errors.thumbnail}
helperText={(errors as any)?.thumbnail?.message} helperText={(errors as any)?.thumbnail?.message}
// required // required
/> />
)} )}
/> />
@ -468,27 +493,25 @@ export const SightCreate = observer(() => {
render={({ field }) => ( render={({ field }) => (
<Autocomplete <Autocomplete
{...articleAutocompleteProps} {...articleAutocompleteProps}
options={mergedOptions} // ← use merged options
value={ value={
articleAutocompleteProps.options.find( mergedOptions.find((option) => option.id === field.value) ??
(option) => option.id === field.value null
) ?? null
} }
onChange={(_, value) => { onChange={(_, value) => {
field.onChange(value?.id ?? ""); field.onChange(value?.id ?? "");
}} }}
getOptionLabel={(item) => { getOptionLabel={(item) => (item ? item.heading : "")}
return item ? item.heading : ""; isOptionEqualToValue={(option, value) =>
}} option.id === value?.id
isOptionEqualToValue={(option, value) => { }
return option.id === value?.id; filterOptions={(options, { inputValue }) =>
}} options.filter((option) =>
filterOptions={(options, { inputValue }) => {
return options.filter((option) =>
option.heading option.heading
.toLowerCase() .toLowerCase()
.includes(inputValue.toLowerCase()) .includes(inputValue.toLowerCase())
); )
}} }
renderInput={(params) => ( renderInput={(params) => (
<TextField <TextField
{...params} {...params}
@ -503,6 +526,31 @@ export const SightCreate = observer(() => {
)} )}
/> />
{!leftArticleContent && (
<Accordion>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="create-article-content"
id="create-article-header"
>
<Typography>Создать новую статью</Typography>
</AccordionSummary>
<AccordionDetails>
<CreateSightArticle
language={language}
parentResource="sight"
childResource="article"
title="статью"
noReset
left
onSave={(something: any) => {
setCustomOptions((prev) => [...prev, something]);
}}
/>
</AccordionDetails>
</Accordion>
)}
<Controller <Controller
control={control} control={control}
name="preview_media" name="preview_media"
@ -516,7 +564,7 @@ export const SightCreate = observer(() => {
) || null ) || null
} }
onChange={(_, value) => { onChange={(_, value) => {
console.log(value, _) console.log(value, _);
field.onChange(value?.id || ""); field.onChange(value?.id || "");
}} }}
getOptionLabel={(item) => { getOptionLabel={(item) => {
@ -531,7 +579,7 @@ export const SightCreate = observer(() => {
option.media_name option.media_name
.toLowerCase() .toLowerCase()
.includes(inputValue.toLowerCase()) && .includes(inputValue.toLowerCase()) &&
[1,2,5,6].includes(option.media_type) [1, 2, 3, 4, 5, 6].includes(option.media_type)
); );
}} }}
renderInput={(params) => ( renderInput={(params) => (

File diff suppressed because it is too large Load Diff

View File

@ -12,8 +12,14 @@ import { CustomDataGrid } from "@components";
import { localeText } from "../../locales/ru/localeText"; import { localeText } from "../../locales/ru/localeText";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useMany } from "@refinedev/core"; 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(() => { export const SnapshotList = observer(() => {
const notification = useNotification();
const { dataGridProps } = useDataGrid({ const { dataGridProps } = useDataGrid({
resource: "snapshots", resource: "snapshots",
hasPagination: false, hasPagination: false,
@ -47,6 +53,30 @@ export const SnapshotList = observer(() => {
return map; return map;
}, [parentsData]); }, [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<GridColDef[]>( const columns = React.useMemo<GridColDef[]>(
() => [ () => [
{ {
@ -80,6 +110,12 @@ export const SnapshotList = observer(() => {
renderCell: function render({ row }) { renderCell: function render({ row }) {
return ( return (
<> <>
<button
className="backup-button"
onClick={() => handleBackup(row.ID)}
>
<DatabaseBackup />
</button>
<ShowButton hideText recordItemId={row.ID} /> <ShowButton hideText recordItemId={row.ID} />
<DeleteButton <DeleteButton
hideText hideText

View File

@ -19,8 +19,6 @@ axiosInstance.interceptors.request.use((config) => {
config.headers["X-Language"] = config.headers["Accept-Language"]; config.headers["X-Language"] = config.headers["Accept-Language"];
console.log("Request headers:", config.headers);
return config; return config;
}); });

View File

@ -5703,6 +5703,13 @@ react-swipeable@^7.0.2:
resolved "https://registry.npmjs.org/react-swipeable/-/react-swipeable-7.0.2.tgz" resolved "https://registry.npmjs.org/react-swipeable/-/react-swipeable-7.0.2.tgz"
integrity sha512-v1Qx1l+aC2fdxKa9aKJiaU/ZxmJ5o98RMoFwUqAAzVWUcxgfHFXDDruCKXhw6zIYXm6V64JiHgP9f6mlME5l8w== 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: react-transition-group@^4.4.5:
version "4.4.5" version "4.4.5"
resolved "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz" resolved "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz"