last changes

This commit is contained in:
Spynder 2025-05-14 14:42:45 +03:00
parent 177653d84a
commit 042b53e6a4
41 changed files with 14316 additions and 900 deletions

13089
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -21,8 +21,7 @@ import routerBindings, {
import { ColorModeContextProvider } from "./contexts/color-mode";
import { Header } from "./components/header";
import { Login } from "./pages/login";
import { authProvider } from "./authProvider";
import { i18nProvider } from "./i18nProvider";
import { authProvider, i18nProvider } from "@providers";
import {
CountryList,

View File

@ -12,7 +12,7 @@ import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import { axiosInstance } from "../providers/data";
import { useForm, Controller } from "react-hook-form";
import { MarkdownEditor } from "./MarkdownEditor";
import React, { useState, useCallback } from "react";
import React, { useState, useCallback, useEffect } from "react";
import { useDropzone } from "react-dropzone";
import {
ALLOWED_IMAGE_TYPES,
@ -20,6 +20,8 @@ import {
} 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";
const MemoizedSimpleMDE = React.memo(MarkdownEditor);
@ -47,12 +49,15 @@ export const CreateSightArticle = ({
}: Props) => {
const theme = useTheme();
const [mediaFiles, setMediaFiles] = useState<MediaFile[]>([]);
const { language, setLanguageAction } = languageStore;
const {
register: registerItem,
watch,
control: controlItem,
handleSubmit: handleSubmitItem,
reset: resetItem,
setValue,
formState: { errors: itemErrors },
} = useForm({
defaultValues: {
@ -61,6 +66,45 @@ export const CreateSightArticle = ({
},
});
const [articleData, setArticleData] = useState({
heading: EVERY_LANGUAGE(""),
body: EVERY_LANGUAGE("")
});
function updateTranslations() {
const newArticleData = {
...articleData,
heading: {
...articleData.heading,
[language]: watch("heading") ?? "",
},
body: {
...articleData.body,
[language]: 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[language] ?? "");
setValue("body", articleData.body[language] ?? "");
}, [language, articleData, setValue]);
const handleLanguageChange = (lang: Languages) => {
updateTranslations();
setLanguageAction(lang);
};
const simpleMDEOptions = React.useMemo(
() => ({
placeholder: "Введите контент в формате Markdown...",
@ -109,7 +153,10 @@ export const CreateSightArticle = ({
// Создаем статью
const response = await axiosInstance.post(
`${import.meta.env.VITE_KRBL_API}/${childResource}`,
data
{
...data,
translations: updateTranslations()
}
);
const itemId = response.data.id;
@ -119,7 +166,7 @@ export const CreateSightArticle = ({
import.meta.env.VITE_KRBL_API
}/${parentResource}/${parentId}/${childResource}`
);
const existingItems = existingItemsResponse.data || [];
const existingItems = existingItemsResponse.data ?? [];
const nextPageNum = existingItems.length + 1;
if (!left) {
@ -189,6 +236,7 @@ export const CreateSightArticle = ({
</AccordionSummary>
<AccordionDetails sx={{ background: theme.palette.background.paper }}>
<Box component="form" onSubmit={handleSubmitItem(handleCreate)}>
<LanguageSelector action={handleLanguageChange} />
<TextField
{...registerItem("heading", {
required: "Это поле является обязательным",

View File

@ -1,14 +1,12 @@
import { Box } from "@mui/material";
import { languageStore } from "../../store/LanguageStore";
import { Languages, languageStore } from "../../store/LanguageStore";
import { observer } from "mobx-react-lite";
export const LanguageSwitch = observer(({ action }: any) => {
const { language, setLanguageAction } = languageStore;
const handleLanguageChange = (lang: string) => {
if (action) {
action();
}
const handleLanguageChange = (lang: Languages) => {
action?.();
setLanguageAction(lang);
};

View File

@ -287,114 +287,116 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
<AccordionDetails sx={{ background: theme.palette.background.paper }}>
<Stack gap={2}>
<DragDropContext onDragEnd={onDragEnd}>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
{type === "edit" && dragAllowed && (
<TableCell width="40px"></TableCell>
)}
<TableCell key="id"></TableCell>
{fields.map((field) => (
<TableCell key={String(field.data)}>
{field.label}
</TableCell>
))}
{type === "edit" && (
<TableCell width="120px">Действие</TableCell>
)}
</TableRow>
</TableHead>
<Droppable
droppableId="droppable"
isDropDisabled={type !== "edit" || !dragAllowed}
>
{(provided) => (
<TableBody
ref={provided.innerRef}
{...provided.droppableProps}
>
{linkedItems.map((item, index) => (
<Draggable
key={item.id}
draggableId={"q" + String(item.id)}
index={index}
isDragDisabled={type !== "edit" || !dragAllowed}
>
{(provided) => (
<TableRow
sx={{
cursor:
childResource === "article"
? "pointer"
: "default",
}}
onClick={() => {
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 && (
<TableCell {...provided.dragHandleProps}>
<IconButton size="small">
<DragIndicatorIcon />
</IconButton>
</TableCell>
)}
<TableCell key={String(item.id)}>
{index + 1}
</TableCell>
{fields.map((field, index) => (
<TableCell
key={String(field.data) + String(index)}
>
{field.render
? field.render(item[field.data])
: item[field.data]}
</TableCell>
))}
{type === "edit" && (
<TableCell>
<Button
variant="outlined"
color="error"
size="small"
onClick={(e) => {
e.stopPropagation();
deleteItem(item.id);
}}
>
Отвязать
</Button>
</TableCell>
)}
</TableRow>
)}
</Draggable>
{linkedItems?.length > 0 && (
<DragDropContext onDragEnd={onDragEnd}>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
{type === "edit" && dragAllowed && (
<TableCell width="40px"></TableCell>
)}
<TableCell key="id"></TableCell>
{fields.map((field) => (
<TableCell key={String(field.data)}>
{field.label}
</TableCell>
))}
{provided.placeholder}
</TableBody>
)}
</Droppable>
</Table>
</TableContainer>
</DragDropContext>
{type === "edit" && (
<TableCell width="120px">Действие</TableCell>
)}
</TableRow>
</TableHead>
<Droppable
droppableId="droppable"
isDropDisabled={type !== "edit" || !dragAllowed}
>
{(provided) => (
<TableBody
ref={provided.innerRef}
{...provided.droppableProps}
>
{linkedItems.map((item, index) => (
<Draggable
key={item.id}
draggableId={"q" + String(item.id)}
index={index}
isDragDisabled={type !== "edit" || !dragAllowed}
>
{(provided) => (
<TableRow
sx={{
cursor:
childResource === "article"
? "pointer"
: "default",
}}
onClick={() => {
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 && (
<TableCell {...provided.dragHandleProps}>
<IconButton size="small">
<DragIndicatorIcon />
</IconButton>
</TableCell>
)}
<TableCell key={String(item.id)}>
{index + 1}
</TableCell>
{fields.map((field, index) => (
<TableCell
key={String(field.data) + String(index)}
>
{field.render
? field.render(item[field.data])
: item[field.data]}
</TableCell>
))}
{type === "edit" && (
<TableCell>
<Button
variant="outlined"
color="error"
size="small"
onClick={(e) => {
e.stopPropagation();
deleteItem(item.id);
}}
>
Отвязать
</Button>
</TableCell>
)}
</TableRow>
)}
</Draggable>
))}
{provided.placeholder}
</TableBody>
)}
</Droppable>
</Table>
</TableContainer>
</DragDropContext>
)}
{linkedItems.length === 0 && !isLoading && (
<Typography color="textSecondary" textAlign="center" py={2}>
@ -448,7 +450,7 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
)}
/>
{childResource === "article" && (
{/* {childResource === "article" && (
<FormControl fullWidth>
<TextField
type="number"
@ -464,7 +466,7 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
InputLabelProps={{ shrink: true }}
/>
</FormControl>
)}
)} */}
{childResource === "media" && (
<FormControl fullWidth>

View File

@ -4,20 +4,17 @@ import { observer } from "mobx-react-lite";
import { useForm } from "@refinedev/react-hook-form";
import { Controller } from "react-hook-form";
import "easymde/dist/easymde.min.css";
import { memo, useMemo, useEffect, useCallback } from "react";
import { memo, useMemo, useEffect, useCallback, useState } from "react";
import { MarkdownEditor } from "../../MarkdownEditor";
import { Edit } from "@refinedev/mui";
import { languageStore } from "../../../store/LanguageStore";
import { LanguageSwitch } from "../../LanguageSwitch/index";
import { useNavigate } from "react-router";
import { useState } from "react";
import { useDropzone } from "react-dropzone";
import {
ALLOWED_IMAGE_TYPES,
ALLOWED_VIDEO_TYPES,
} from "../../media/MediaFormUtils";
import { axiosInstance } from "../../../providers/data";
import { TOKEN_KEY } from "../../../authProvider";
import { TOKEN_KEY, axiosInstance } from "@providers";
import { LinkedItems } from "../../../components/LinkedItems";
import { mediaFields, MediaItem } from "../../../pages/article/types";

View File

@ -0,0 +1,71 @@
import { Box } from "@mui/material";
import { Languages, languageStore } from "@stores";
import { observer } from "mobx-react-lite";
export const LanguageSelector = observer(({
action
}: {action?: (lang: Languages) => void}) => {
const { language, setLanguageAction } = languageStore;
function handleLanguageChange(language: Languages) {
if(action) action(language);
else setLanguageAction(language);
}
return (
<Box
sx={{
display: "flex",
gap: 2,
height: "min-content"
}}
>
<Box
sx={{
cursor: "pointer",
flex: 1,
display: "flex",
justifyContent: "center",
bgcolor: language === "ru" ? "primary.main" : "transparent",
color: language === "ru" ? "white" : "inherit",
borderRadius: 1,
p: 1,
}}
onClick={() => handleLanguageChange("ru")}
>
RU
</Box>
<Box
sx={{
cursor: "pointer",
flex: 1,
display: "flex",
justifyContent: "center",
bgcolor: language === "en" ? "primary.main" : "transparent",
color: language === "en" ? "white" : "inherit",
borderRadius: 1,
p: 1,
}}
onClick={() => handleLanguageChange("en")}
>
EN
</Box>
<Box
sx={{
cursor: "pointer",
flex: 1,
display: "flex",
justifyContent: "center",
bgcolor: language === "zh" ? "primary.main" : "transparent",
color: language === "zh" ? "white" : "inherit",
borderRadius: 1,
p: 1,
}}
onClick={() => handleLanguageChange("zh")}
>
ZH
</Box>
</Box>
);
});

View File

@ -0,0 +1,97 @@
import { Box } from "@mui/material";
import { TOKEN_KEY } from "@providers";
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;
}
export function MediaView({media} : Readonly<{media?: MediaData}>) {
const token = localStorage.getItem(TOKEN_KEY);
return (
<Box
sx={{maxHeight: "50vh", height: "100%", width: "100%", display: "flex", justifyContent: "center"}}
>
{media?.media_type === 1 && (
<img
src={`${import.meta.env.VITE_KRBL_MEDIA}${
media?.id
}/download?token=${token}`}
alt={media?.filename}
style={{
maxWidth: "100%",
height: "100%",
objectFit: "contain",
borderRadius: 8,
}}
/>
)}
{media?.media_type === 2 && (
<video
src={`${import.meta.env.VITE_KRBL_MEDIA}${
media?.id
}/download?token=${token}`}
style={{
objectFit: "contain",
borderRadius: 30,
}}
controls
autoPlay
muted
/>
)}
{media?.media_type === 3 && (
<img
src={`${import.meta.env.VITE_KRBL_MEDIA}${
media?.id
}/download?token=${token}`}
alt={media?.filename}
style={{
maxWidth: "100%",
height: "100%",
objectFit: "contain",
borderRadius: 8,
}}
/>
)}
{media?.media_type === 4 && (
<img
src={`${import.meta.env.VITE_KRBL_MEDIA}${
media?.id
}/download?token=${token}`}
alt={media?.filename}
style={{
maxWidth: "100%",
height: "100%",
objectFit: "contain",
borderRadius: 8,
}}
/>
)}
{media?.media_type === 5 && (
<ReactPhotoSphereViewer
src={`${import.meta.env.VITE_KRBL_MEDIA}${
media?.id
}/download?token=${token}`}
width={"100%"}
height={"100%"}
/>
)}
{media?.media_type === 6 && (
<ModelViewer
fileUrl={`${import.meta.env.VITE_KRBL_MEDIA}${
media?.id
}/download?token=${token}`}
/>
)}
</Box>
)
}

View File

@ -6,7 +6,7 @@ type ModelViewerProps = {
height?: string;
};
export const ModelViewer = ({ fileUrl, height = "80vh" }: ModelViewerProps) => {
export const ModelViewer = ({ fileUrl, height = "100%" }: ModelViewerProps) => {
const { scene } = useGLTF(fileUrl);
return (

View File

@ -0,0 +1,5 @@
export * from './Icons';
export * from './LanguageSelector';
export * from './SidebarTitle';
export * from './MediaView';
export * from './ModelViewer';

1
src/lib/index.ts Normal file
View File

@ -0,0 +1 @@
export { MEDIA_TYPES, VEHICLE_TYPES } from './constants'

View File

@ -1,94 +1,92 @@
import { Box, TextField, Typography, Paper } from "@mui/material";
import { Create } 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 ReactMarkdown from "react-markdown";
import Cookies from "js-cookie";
import { MarkdownEditor } from "../../components/MarkdownEditor";
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);
export const ArticleCreate = () => {
const [language, setLanguage] = useState(Cookies.get("lang")!);
const [articleData, setArticleData] = useState<{
ru: { heading: string; body: string };
en: { heading: string; body: string };
zh: { heading: string; body: string };
}>({
ru: { heading: "", body: "" },
en: { heading: "", body: "" },
zh: { heading: "", body: "" },
export const ArticleCreate = observer(() => {
const { language, setLanguageAction } = languageStore;
const [articleData, setArticleData] = useState({
heading: EVERY_LANGUAGE(""),
body: EVERY_LANGUAGE("")
});
const {
saveButtonProps,
refineCore: { formLoading },
refineCore: { formLoading, onFinish },
register,
control,
watch,
formState: { errors },
setValue,
handleSubmit,
} = useForm({
refineCoreProps: {
resource: "article/",
meta: {
headers: {
"Accept-Language": language,
},
},
resource: "article",
...META_LANGUAGE(language)
},
warnWhenUnsavedChanges: false
});
useEffect(() => {
const lang = Cookies.get("lang")!;
Cookies.set("lang", language);
return () => {
Cookies.set("lang", lang);
};
}, [language]);
useEffect(() => {
setValue(
"heading",
articleData[language as keyof typeof articleData]?.heading || ""
);
setValue(
"body",
articleData[language as keyof typeof articleData]?.body || ""
);
setPreview(articleData[language as keyof typeof articleData]?.body || "");
setHeadingPreview(
articleData[language as keyof typeof articleData]?.heading || ""
);
}, [language, articleData, setValue]);
const handleLanguageChange = (lang: string) => {
setArticleData((prevData) => ({
...prevData,
[language]: {
heading: watch("heading") || "",
body: watch("body") || "",
},
}));
setLanguage(lang);
Cookies.set("lang", lang);
};
const [preview, setPreview] = useState("");
const [headingPreview, setHeadingPreview] = useState("");
// Следим за изменениями в полях body и heading
const bodyContent = watch("body");
const headingContent = watch("heading");
function updateTranslations() {
const newArticleData = {
...articleData,
heading: {
...articleData.heading,
[language]: watch("heading") ?? "",
},
body: {
...articleData.body,
[language]: watch("body") ?? "",
}
}
setArticleData(newArticleData);
return newArticleData;
}
const handleFormSubmit = handleSubmit((values: FieldValues) => {
const newTranslations = updateTranslations();
console.log(newTranslations);
return onFinish({
translations: newTranslations
});
});
useEffect(() => {
setPreview(bodyContent || "");
setValue("heading", articleData.heading[language] ?? "");
setValue("body", articleData.body[language] ?? "");
setPreview(articleData.body[language] ?? "");
setHeadingPreview(articleData.heading[language] ?? "");
}, [language, articleData, setValue]);
const handleLanguageChange = (lang: Languages) => {
updateTranslations();
setLanguageAction(lang);
};
const [preview, setPreview] = useState("");
const [headingPreview, setHeadingPreview] = useState("");
useEffect(() => {
setPreview(bodyContent ?? "");
}, [bodyContent]);
useEffect(() => {
setHeadingPreview(headingContent || "");
setHeadingPreview(headingContent ?? "");
}, [headingContent]);
const simpleMDEOptions = React.useMemo(
@ -100,63 +98,14 @@ export const ArticleCreate = () => {
);
return (
<Create isLoading={formLoading} saveButtonProps={saveButtonProps}>
<Create isLoading={formLoading} saveButtonProps={{
//...saveButtonProps,
onClick: handleFormSubmit
}}>
<Box sx={{ display: "flex", flex: 1, gap: 2 }}>
{/* Форма создания */}
<Box sx={{ display: "flex", flex: 1, flexDirection: "column", gap: 2 }}>
<Box
sx={{
flex: 1,
display: "flex",
gap: 2,
}}
>
<Box
sx={{
cursor: "pointer",
flex: 1,
display: "flex",
justifyContent: "center",
bgcolor: language === "ru" ? "primary.main" : "transparent",
color: language === "ru" ? "white" : "inherit",
p: 1,
borderRadius: 1,
}}
onClick={() => handleLanguageChange("ru")}
>
RU
</Box>
<Box
sx={{
cursor: "pointer",
flex: 1,
display: "flex",
justifyContent: "center",
bgcolor: language === "en" ? "primary.main" : "transparent",
color: language === "en" ? "white" : "inherit",
p: 1,
borderRadius: 1,
}}
onClick={() => handleLanguageChange("en")}
>
EN
</Box>
<Box
sx={{
cursor: "pointer",
flex: 1,
display: "flex",
justifyContent: "center",
bgcolor: language === "zh" ? "primary.main" : "transparent",
color: language === "zh" ? "white" : "inherit",
p: 1,
borderRadius: 1,
}}
onClick={() => handleLanguageChange("zh")}
>
ZH
</Box>
</Box>
<LanguageSelector action={handleLanguageChange} />
<Box
component="form"
sx={{ flex: 1, display: "flex", flexDirection: "column" }}
@ -273,4 +222,4 @@ export const ArticleCreate = () => {
</Box>
</Create>
);
};
});

View File

@ -1,7 +1,7 @@
import { Box, TextField, Typography, Paper } from "@mui/material";
import { Edit } from "@refinedev/mui";
import { useForm } from "@refinedev/react-hook-form";
import { Controller } from "react-hook-form";
import { Controller, FieldValues } from "react-hook-form";
import { useParams } from "react-router";
import React, { useState, useEffect, useMemo } from "react";
import ReactMarkdown from "react-markdown";
@ -10,23 +10,19 @@ import { useList } from "@refinedev/core";
import { MarkdownEditor } from "../../components/MarkdownEditor";
import { LinkedItems } from "../../components/LinkedItems";
import { MediaItem, mediaFields } from "./types";
import { TOKEN_KEY } from "../../authProvider";
import { TOKEN_KEY } from "@providers";
import "easymde/dist/easymde.min.css";
import { languageStore } from "../../store/LanguageStore";
import { EVERY_LANGUAGE, Languages, languageStore, META_LANGUAGE } from "@stores";
import { observer } from "mobx-react-lite";
import { LanguageSelector, MediaView } from "@ui";
const MemoizedSimpleMDE = React.memo(MarkdownEditor);
export const ArticleEdit = observer(() => {
const { language, setLanguageAction } = languageStore;
const [articleData, setArticleData] = useState<{
ru: { heading: string; body: string };
en: { heading: string; body: string };
zh: { heading: string; body: string };
}>({
ru: { heading: "", body: "" },
en: { heading: "", body: "" },
zh: { heading: "", body: "" },
const [articleData, setArticleData] = useState({
heading: EVERY_LANGUAGE(""),
body: EVERY_LANGUAGE("")
});
const { id: articleId } = useParams<{ id: string }>();
const [preview, setPreview] = useState("");
@ -41,6 +37,7 @@ export const ArticleEdit = observer(() => {
const {
saveButtonProps,
refineCore: { onFinish },
register,
control,
handleSubmit,
@ -48,54 +45,54 @@ export const ArticleEdit = observer(() => {
formState: { errors },
setValue,
} = useForm<{ heading: string; body: string }>({
refineCoreProps: {
meta: {
headers: {
"Accept-Language": language,
},
},
},
refineCoreProps: META_LANGUAGE(language)
});
useEffect(() => {
if (articleData[language as keyof typeof articleData]?.heading) {
setValue(
"heading",
articleData[language as keyof typeof articleData]?.heading
);
setHeadingPreview(
articleData[language as keyof typeof articleData]?.heading || ""
);
}
if (articleData[language as keyof typeof articleData]?.body) {
setValue(
"body",
articleData[language as keyof typeof articleData]?.body || ""
);
setPreview(articleData[language as keyof typeof articleData]?.body || "");
}
}, [language, articleData, setValue]);
const handleLanguageChange = (lang: string) => {
setArticleData((prevData) => ({
...prevData,
[language]: {
heading: watch("heading") ?? "",
body: watch("body") ?? "",
},
}));
setLanguageAction(lang);
};
const bodyContent = watch("body");
const headingContent = watch("heading");
useEffect(() => {
setPreview(bodyContent || "");
setValue("heading", articleData.heading[language] ?? "");
setHeadingPreview(articleData.heading[language] ?? "");
setValue("body", articleData.body[language] ?? "");
setPreview(articleData.body[language] ?? "");
}, [language, articleData, setValue]);
function updateTranslations() {
const newArticleData = {
...articleData,
heading: {
...articleData.heading,
[language]: watch("heading") ?? "",
},
body: {
...articleData.body,
[language]: watch("body") ?? "",
}
}
setArticleData(newArticleData);
return newArticleData;
}
const handleLanguageChange = (lang: Languages) => {
updateTranslations();
setLanguageAction(lang);
};
const handleFormSubmit = handleSubmit((values: FieldValues) => {
const newTranslations = updateTranslations();
console.log(newTranslations);
return onFinish({
translations: newTranslations
});
});
useEffect(() => {
setPreview(bodyContent ?? "");
}, [bodyContent]);
useEffect(() => {
setHeadingPreview(headingContent || "");
setHeadingPreview(headingContent ?? "");
}, [headingContent]);
const { data: mediaData } = useList<MediaItem>({
@ -109,64 +106,17 @@ export const ArticleEdit = observer(() => {
}, [setLanguageAction]);
return (
<Edit saveButtonProps={saveButtonProps}>
<Edit saveButtonProps={{
...saveButtonProps,
onClick: handleFormSubmit
}}
>
<Box sx={{ display: "flex", gap: 2 }}>
{/* Форма редактирования */}
{/* Форма создания */}
<Box sx={{ display: "flex", flex: 1, flexDirection: "column", gap: 2 }}>
<Box
sx={{
flex: 1,
display: "flex",
gap: 2,
}}
>
<Box
sx={{
cursor: "pointer",
flex: 1,
display: "flex",
justifyContent: "center",
bgcolor: language === "ru" ? "primary.main" : "transparent",
color: language === "ru" ? "white" : "inherit",
p: 1,
borderRadius: 1,
}}
onClick={() => handleLanguageChange("ru")}
>
RU
</Box>
<Box
sx={{
cursor: "pointer",
flex: 1,
display: "flex",
justifyContent: "center",
bgcolor: language === "en" ? "primary.main" : "transparent",
color: language === "en" ? "white" : "inherit",
p: 1,
borderRadius: 1,
}}
onClick={() => handleLanguageChange("en")}
>
EN
</Box>
<Box
sx={{
cursor: "pointer",
flex: 1,
display: "flex",
justifyContent: "center",
bgcolor: language === "zh" ? "primary.main" : "transparent",
color: language === "zh" ? "white" : "inherit",
p: 1,
borderRadius: 1,
}}
onClick={() => handleLanguageChange("zh")}
>
ZH
</Box>
</Box>
<LanguageSelector action={handleLanguageChange} />
<Box
component="form"
sx={{ flex: 1, display: "flex", flexDirection: "column" }}
@ -317,7 +267,8 @@ export const ArticleEdit = observer(() => {
borderColor: "primary.main",
}}
>
<img
<MediaView media={media} />
{/* <img
src={`${import.meta.env.VITE_KRBL_MEDIA}${
media.id
}/download?token=${localStorage.getItem(TOKEN_KEY)}`}
@ -327,7 +278,7 @@ export const ArticleEdit = observer(() => {
height: "100%",
objectFit: "cover",
}}
/>
/> */}
</Box>
))}
</Box>

View File

@ -8,21 +8,18 @@ import {
useDataGrid,
} from "@refinedev/mui";
import React, { useEffect } from "react";
import { useDelete } from "@refinedev/core";
import { localeText } from "../../locales/ru/localeText";
import { observer } from "mobx-react-lite";
import { languageStore } from "../../store/LanguageStore";
import { languageStore, META_LANGUAGE } from "@stores";
export const ArticleList = observer(() => {
const { language } = languageStore;
const { dataGridProps } = useDataGrid({
resource: "article/",
meta: {
headers: {
"Accept-Language": language,
},
},
resource: "article",
...META_LANGUAGE(language)
});
const columns = React.useMemo<GridColDef[]>(
@ -70,7 +67,10 @@ export const ArticleList = observer(() => {
<>
<EditButton hideText recordItemId={row.id} />
<ShowButton hideText recordItemId={row.id} />
<DeleteButton hideText recordItemId={row.id} />
<DeleteButton
hideText
recordItemId={row.id}
/>
</>
);
},

View File

@ -4,7 +4,7 @@ export type MediaItem = {
id: number
filename: string
media_name: string
media_type: string
media_type: number
media_order?: number
}

View File

@ -1,15 +1,22 @@
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 { observer } from "mobx-react-lite";
import { Controller } from "react-hook-form";
export const CarrierEdit = () => {
export const CarrierEdit = observer(() => {
const { language } = languageStore;
const {
saveButtonProps,
register,
control,
watch,
formState: { errors },
} = useForm();
} = useForm({
refineCoreProps: META_LANGUAGE(language)
});
const { autocompleteProps: cityAutocompleteProps } = useAutocomplete({
resource: "city",
@ -20,6 +27,7 @@ export const CarrierEdit = () => {
value,
},
],
...META_LANGUAGE("ru")
});
const { autocompleteProps: mediaAutocompleteProps } = useAutocomplete({
@ -31,6 +39,7 @@ export const CarrierEdit = () => {
value,
},
],
...META_LANGUAGE(language)
});
return (
@ -40,6 +49,7 @@ export const CarrierEdit = () => {
sx={{ display: "flex", flexDirection: "column" }}
autoComplete="off"
>
<LanguageSelector />
<Controller
control={control}
name="city_id"
@ -110,70 +120,77 @@ export const CarrierEdit = () => {
name="short_name"
/>
<TextField
{...register("main_color", {
// required: 'Это поле является обязательным',
})}
error={!!(errors as any)?.main_color}
helperText={(errors as any)?.main_color?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="color"
label={"Основной цвет"}
name="main_color"
sx={{
"& input": {
height: "50px",
paddingBlock: "14px",
paddingInline: "14px",
cursor: "pointer",
},
}}
/>
<Box component="form"
sx={{ display: "flex" }}
autoComplete="off"
>
<TextField
{...register("main_color", {
// required: 'Это поле является обязательным',
})}
error={!!(errors as any)?.main_color}
helperText={(errors as any)?.main_color?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="color"
label={"Основной цвет"}
name="main_color"
sx={{
"& input": {
height: "50px",
paddingBlock: "14px",
paddingInline: "14px",
cursor: "pointer",
},
}}
/>
<TextField
{...register("left_color", {
// required: 'Это поле является обязательным',
})}
error={!!(errors as any)?.left_color}
helperText={(errors as any)?.left_color?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="color"
label={"Цвет левого виджета"}
name="left_color"
sx={{
"& input": {
height: "50px",
paddingBlock: "14px",
paddingInline: "14px",
cursor: "pointer",
},
}}
/>
<TextField
{...register("right_color", {
// required: 'Это поле является обязательным',
})}
error={!!(errors as any)?.right_color}
helperText={(errors as any)?.right_color?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="color"
label={"Цвет правого виджета"}
name="right_color"
sx={{
"& input": {
height: "50px",
paddingBlock: "14px",
paddingInline: "14px",
cursor: "pointer",
},
}}
/>
<TextField
{...register("left_color", {
// required: 'Это поле является обязательным',
})}
error={!!(errors as any)?.left_color}
helperText={(errors as any)?.left_color?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="color"
label={"Цвет левого виджета"}
name="left_color"
sx={{
marginLeft: "16px",
marginRight: "16px",
"& input": {
height: "50px",
paddingBlock: "14px",
paddingInline: "14px",
cursor: "pointer",
},
}}
/>
<TextField
{...register("right_color", {
// required: 'Это поле является обязательным',
})}
error={!!(errors as any)?.right_color}
helperText={(errors as any)?.right_color?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="color"
label={"Цвет правого виджета"}
name="right_color"
sx={{
"& input": {
height: "50px",
paddingBlock: "14px",
paddingInline: "14px",
cursor: "pointer",
},
}}
/>
</Box>
<TextField
{...register("slogan", {
@ -231,7 +248,10 @@ export const CarrierEdit = () => {
/>
)}
/>
<Box height={150} sx={{display: "flex", justifyContent: "start"}}>
<MediaView media={{id: watch("logo"), media_type: 1}} />
</Box>
</Box>
</Edit>
);
};
});

View File

@ -1,7 +1,8 @@
import { Box, Stack, Typography } from "@mui/material";
import { useShow } from "@refinedev/core";
import { Show, TextFieldComponent as TextField } from "@refinedev/mui";
import { TOKEN_KEY } from "../../authProvider";
import { TOKEN_KEY } from "@providers";
import { MediaView } from "@ui";
export type FieldType = {
label: string;
@ -81,13 +82,9 @@ export const CarrierShow = () => {
label: "Логотип",
data: "logo",
render: (value: number) => (
<img
src={`${
import.meta.env.VITE_KRBL_MEDIA
}${value}/download?token=${localStorage.getItem(TOKEN_KEY)}`}
alt={String(value)}
style={{ maxWidth: "10%", objectFit: "contain", borderRadius: 8 }}
/>
<Box height={150} sx={{display: "flex", justifyContent: "start"}}>
<MediaView media={{id: value, media_type: 1}} />
</Box>
),
},
];

View File

@ -1,18 +1,26 @@
import {Autocomplete, Box, TextField} from '@mui/material'
import {Edit, useAutocomplete} from '@refinedev/mui'
import {useForm} from '@refinedev/react-hook-form'
import { EVERY_LANGUAGE, Languages, languageStore, META_LANGUAGE } from '@stores'
import { LanguageSelector } from '@ui'
import { observer } from 'mobx-react-lite'
import { useEffect, useState } from 'react'
import {Controller} from 'react-hook-form'
export const CityEdit = () => {
export const CityEdit = observer(() => {
const { language } = languageStore;
const {
saveButtonProps,
register,
control,
formState: {errors},
} = useForm({})
} = useForm({
refineCoreProps: META_LANGUAGE(language)
})
const {autocompleteProps: countryAutocompleteProps} = useAutocomplete({
resource: 'country',
...META_LANGUAGE(language)
})
const {autocompleteProps: mediaAutocompleteProps} = useAutocomplete({
@ -24,11 +32,14 @@ export const CityEdit = () => {
value,
},
],
})
...META_LANGUAGE(language)
});
return (
<Edit saveButtonProps={saveButtonProps}>
<Box component="form" sx={{display: 'flex', flexDirection: 'column'}} autoComplete="off">
<LanguageSelector/>
<Controller
control={control}
name="country_code"
@ -94,4 +105,4 @@ export const CityEdit = () => {
</Box>
</Edit>
)
}
})

View File

@ -1,7 +1,7 @@
import { Stack, Typography } from "@mui/material";
import { useShow } from "@refinedev/core";
import { Show, TextFieldComponent as TextField } from "@refinedev/mui";
import { TOKEN_KEY } from "../../authProvider";
import { TOKEN_KEY } from "@providers";
export const CityShow = () => {
const { query } = useShow({});

View File

@ -1,13 +1,19 @@
import { Box, TextField } from "@mui/material";
import { Edit } from "@refinedev/mui";
import { useForm } from "@refinedev/react-hook-form";
import { languageStore, META_LANGUAGE } from "@stores";
import { LanguageSelector } from "@ui";
import { observer } from "mobx-react-lite";
export const CountryEdit = () => {
export const CountryEdit = observer(() => {
const { language } = languageStore;
const {
saveButtonProps,
register,
formState: { errors },
} = useForm({});
} = useForm({
refineCoreProps: META_LANGUAGE(language)
});
return (
<Edit saveButtonProps={saveButtonProps}>
@ -16,6 +22,7 @@ export const CountryEdit = () => {
sx={{ display: "flex", flexDirection: "column" }}
autoComplete="off"
>
<LanguageSelector />
<TextField
{...register("code", {
required: "Это поле является обязательным",
@ -27,6 +34,7 @@ export const CountryEdit = () => {
InputLabelProps={{ shrink: true }}
type="text"
label={"Код *"}
disabled
name="code"
/>
<TextField
@ -45,4 +53,4 @@ export const CountryEdit = () => {
</Box>
</Edit>
);
};
});

View File

@ -20,7 +20,7 @@ import {
useMediaFileUpload,
} from "../../components/media/MediaFormUtils";
import { ReactPhotoSphereViewer } from "react-photo-sphere-viewer";
import { ModelViewer } from "./ModelViewer/index";
import { ModelViewer } from "@ui";
type MediaFormValues = {
media_name: string;

View File

@ -11,8 +11,8 @@ import { useEffect } from "react";
import { useShow } from "@refinedev/core";
import { Controller } from "react-hook-form";
import { TOKEN_KEY } from "../../authProvider";
import { MEDIA_TYPES } from "../../lib/constants";
import { TOKEN_KEY } from "@providers";
import { MEDIA_TYPES } from "@lib";
import {
ALLOWED_IMAGE_TYPES,
ALLOWED_VIDEO_TYPES,
@ -22,6 +22,9 @@ import {
ALLOWED_3D_MODEL_TYPES,
useMediaFileUpload,
} from "../../components/media/MediaFormUtils";
import { languageStore, META_LANGUAGE } from "@stores";
import { observer } from "mobx-react-lite";
import { LanguageSelector, MediaData, MediaView } from "@ui";
type MediaFormValues = {
media_name: string;
@ -29,7 +32,8 @@ type MediaFormValues = {
file?: File;
};
export const MediaEdit = () => {
export const MediaEdit = observer(() => {
const { language } = languageStore;
const {
saveButtonProps,
refineCore: { onFinish },
@ -47,6 +51,7 @@ export const MediaEdit = () => {
media_type: "",
file: undefined,
},
refineCoreProps: META_LANGUAGE(language)
});
const { query } = useShow();
@ -100,6 +105,7 @@ export const MediaEdit = () => {
sx={{ display: "flex", flexDirection: "column" }}
autoComplete="off"
>
<LanguageSelector />
<Controller
control={control}
name="media_type"
@ -177,7 +183,7 @@ export const MediaEdit = () => {
hidden
onChange={handleFileChange}
accept={
selectedMediaType === 1
selectedMediaType === 1
? ALLOWED_IMAGE_TYPES.join(",")
: selectedMediaType === 2
? ALLOWED_VIDEO_TYPES.join(",")
@ -207,7 +213,9 @@ export const MediaEdit = () => {
)}
</Box>
{previewUrl && selectedMediaType === 1 && (
<MediaView media={record as MediaData} />
{/* {previewUrl && selectedMediaType === 1 && (
<Box mt={2} display="flex" justifyContent="center">
<img
src={previewUrl}
@ -215,9 +223,9 @@ export const MediaEdit = () => {
style={{ maxWidth: "200px", borderRadius: 8 }}
/>
</Box>
)}
)} */}
</Box>
</Box>
</Edit>
);
};
});

View File

@ -3,11 +3,16 @@ import {CustomDataGrid} from '../../components/CustomDataGrid'
import {DeleteButton, EditButton, List, ShowButton, useDataGrid} from '@refinedev/mui'
import React from 'react'
import {MEDIA_TYPES} from '../../lib/constants'
import { observer } from "mobx-react-lite"
import {localeText} from '../../locales/ru/localeText'
import { languageStore, META_LANGUAGE } from '@stores'
export const MediaList = () => {
const {dataGridProps} = useDataGrid({})
export const MediaList = observer(() => {
const { language } = languageStore;
const {dataGridProps} = useDataGrid({
...META_LANGUAGE(language)
})
const columns = React.useMemo<GridColDef[]>(
() => [
@ -77,7 +82,7 @@ export const MediaList = () => {
return (
<List>
<CustomDataGrid {...dataGridProps} columns={columns} localeText={localeText} getRowId={(row: any) => row.id} />
<CustomDataGrid {...dataGridProps} columns={columns} localeText={localeText} getRowId={(row: any) => row.id} languageEnabled/>
</List>
)
}
});

View File

@ -1,11 +1,9 @@
import { Stack, Typography, Box, Button } from "@mui/material";
import { useShow } from "@refinedev/core";
import { Show, TextFieldComponent as TextField } from "@refinedev/mui";
import { ReactPhotoSphereViewer } from "react-photo-sphere-viewer";
import sky from "./12414.jpg";
import { MEDIA_TYPES } from "../../lib/constants";
import { TOKEN_KEY } from "../../authProvider";
import { ModelViewer } from "./ModelViewer/index";
import { MEDIA_TYPES } from "@lib";
import { TOKEN_KEY } from "@providers";
import { MediaData, MediaView } from "@ui";
export const MediaShow = () => {
const { query } = useShow({});
@ -29,83 +27,7 @@ export const MediaShow = () => {
return (
<Show isLoading={isLoading}>
<Stack gap={4}>
{record && record.media_type === 1 && (
<img
src={`${import.meta.env.VITE_KRBL_MEDIA}${
record?.id
}/download?token=${token}`}
alt={record?.filename}
style={{
maxWidth: "100%",
height: "40vh",
objectFit: "contain",
borderRadius: 8,
}}
/>
)}
{record && record.media_type === 2 && (
<video
src={`${import.meta.env.VITE_KRBL_MEDIA}${
record?.id
}/download?token=${token}`}
style={{
maxWidth: "50%",
objectFit: "contain",
borderRadius: 30,
}}
controls
autoPlay
muted
/>
)}
{record && record.media_type === 3 && (
<img
src={`${import.meta.env.VITE_KRBL_MEDIA}${
record?.id
}/download?token=${token}`}
alt={record?.filename}
style={{
maxWidth: "100%",
height: "40vh",
objectFit: "contain",
borderRadius: 8,
}}
/>
)}
{record && record.media_type === 4 && (
<img
src={`${import.meta.env.VITE_KRBL_MEDIA}${
record?.id
}/download?token=${token}`}
alt={record?.filename}
style={{
maxWidth: "100%",
height: "40vh",
objectFit: "contain",
borderRadius: 8,
}}
/>
)}
{record && record.media_type === 5 && (
<ReactPhotoSphereViewer
src={`${import.meta.env.VITE_KRBL_MEDIA}${
record?.id
}/download?token=${token}`}
width={"100%"}
height={"80vh"}
/>
)}
{record && record.media_type === 6 && (
<ModelViewer
fileUrl={`${import.meta.env.VITE_KRBL_MEDIA}${
record?.id
}/download?token=${token}`}
/>
)}
<MediaView media={record as MediaData} />
{fields.map(({ label, data, render }) => (
<Stack key={data} gap={1}>
<Typography variant="body1" fontWeight="bold">

View File

@ -5,11 +5,12 @@ import {
FormControlLabel,
Checkbox,
Typography,
Button,
} from "@mui/material";
import { Edit, useAutocomplete } from "@refinedev/mui";
import { useForm } from "@refinedev/react-hook-form";
import { Controller } from "react-hook-form";
import { useParams } from "react-router";
import { useNavigate, useParams } from "react-router";
import { LinkedItems } from "../../components/LinkedItems";
import {
StationItem,
@ -29,6 +30,7 @@ export const RouteEdit = () => {
setValue,
watch,
} = useForm({});
const navigate = useNavigate();
const { id: routeId } = useParams<{ id: string }>();
@ -53,38 +55,213 @@ export const RouteEdit = () => {
],
});
const { autocompleteProps: governorAppealAutocompleteProps } = useAutocomplete({
resource: "article",
onSearch: (value) => [
{
field: "heading",
operator: "contains",
value,
},
{
field: "media_type",
operator: "contains",
value,
},
]
});
return (
<Edit saveButtonProps={saveButtonProps}>
<Box
component="form"
sx={{ display: "flex", flexDirection: "column" }}
autoComplete="off"
sx={{display: "flex", flexDirection: "column", gap:1}}
>
<Controller
<Box
component="form"
sx={{ display: "flex", flexDirection: "column"}}
autoComplete="off"
>
<Controller
control={control}
name="carrier_id"
rules={{ required: "Это поле является обязательным" }}
defaultValue={null}
render={({ field }) => (
<Autocomplete
{...carrierAutocompleteProps}
value={
carrierAutocompleteProps.options.find(
(option) => option.id === field.value
) || null
}
onChange={(_, value) => {
field.onChange(value?.id || "");
}}
getOptionLabel={(item) => {
return item ? item.short_name : "";
}}
isOptionEqualToValue={(option, value) => {
return option.id === value?.id;
}}
filterOptions={(options, { inputValue }) => {
return options.filter((option) =>
option.short_name
.toLowerCase()
.includes(inputValue.toLowerCase())
);
}}
renderInput={(params) => (
<TextField
{...params}
label="Выберите перевозчика"
margin="normal"
variant="outlined"
error={!!errors.carrier_id}
helperText={(errors as any)?.carrier_id?.message}
required
/>
)}
/>
)}
/>
<TextField
{...register("route_number", {
required: "Это поле является обязательным",
setValueAs: (value) => String(value),
})}
error={!!(errors as any)?.route_number}
helperText={(errors as any)?.route_number?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="text"
label={"Номер маршрута"}
name="route_number"
/>
<Controller
name="route_direction" // boolean
control={control}
defaultValue={false}
render={({ field }: { field: any }) => (
<FormControlLabel
label="Прямой маршрут?"
control={
<Checkbox
{...field}
checked={field.value}
onChange={(e) => field.onChange(e.target.checked)}
/>
}
/>
)}
/>
<Typography
variant="caption"
color="textSecondary"
sx={{ mt: 0, mb: 1 }}
>
(Прямой / Обратный)
</Typography>
<TextField
{...register("path", {
required: "Это поле является обязательным",
setValueAs: (value: string) => {
try {
const lines = value.trim().split("\n");
return lines.map((line) => {
const [lat, lon] = line
.trim()
.split(/[\s,]+/)
.map(Number);
return [lat, lon];
});
} catch {
return [];
}
},
validate: (value: unknown) => {
if (!Array.isArray(value)) return "Неверный формат";
if (value.length === 0)
return "Введите хотя бы одну пару координат";
if (
!value.every(
(point: unknown) => Array.isArray(point) && point.length === 2
)
) {
return "Каждая строка должна содержать две координаты";
}
if (
!value.every((point: unknown[]) =>
point.every(
(coord: unknown) =>
!isNaN(Number(coord)) && typeof coord === "number"
)
)
) {
return "Координаты должны быть числами";
}
return true;
},
})}
error={!!(errors as any)?.path}
helperText={(errors as any)?.path?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="text"
label={"Координаты маршрута *"}
name="path"
placeholder="55.7558 37.6173
55.7539 37.6208"
multiline
rows={4}
sx={{
marginBottom: 2,
}}
/>
<TextField
{...register("route_sys_number", {
required: "Это поле является обязательным",
})}
error={!!(errors as any)?.route_sys_number}
helperText={(errors as any)?.route_sys_number?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="number"
label={"Системный номер маршрута *"}
name="route_sys_number"
/>
<Controller
control={control}
name="carrier_id"
rules={{ required: "Это поле является обязательным" }}
name="governor_appeal"
defaultValue={null}
render={({ field }) => (
<Autocomplete
{...carrierAutocompleteProps}
{...governorAppealAutocompleteProps}
value={
carrierAutocompleteProps.options.find(
governorAppealAutocompleteProps.options.find(
(option) => option.id === field.value
) || null
) ?? null
}
onChange={(_, value) => {
field.onChange(value?.id || "");
field.onChange(value?.id ?? "");
}}
getOptionLabel={(item) => {
return item ? item.short_name : "";
return item ? item.heading : "";
}}
isOptionEqualToValue={(option, value) => {
return option.id === value?.id;
}}
filterOptions={(options, { inputValue }) => {
return options.filter((option) =>
option.short_name
option.heading
.toLowerCase()
.includes(inputValue.toLowerCase())
);
@ -92,242 +269,128 @@ export const RouteEdit = () => {
renderInput={(params) => (
<TextField
{...params}
label="Выберите перевозчика"
label="Обращение губернатора"
margin="normal"
variant="outlined"
error={!!errors.carrier_id}
helperText={(errors as any)?.carrier_id?.message}
error={!!errors.arms}
helperText={(errors as any)?.arms?.message}
required
/>
)}
/>
)}
/>
/>
<TextField
{...register("route_number", {
required: "Это поле является обязательным",
setValueAs: (value) => String(value),
})}
error={!!(errors as any)?.route_number}
helperText={(errors as any)?.route_number?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="text"
label={"Номер маршрута"}
name="route_number"
/>
<Controller
name="route_direction" // boolean
control={control}
defaultValue={false}
render={({ field }: { field: any }) => (
<FormControlLabel
label="Прямой маршрут?"
control={
<Checkbox
{...field}
checked={field.value}
onChange={(e) => field.onChange(e.target.checked)}
/>
}
<TextField
{...register("scale_min", {
// required: 'Это поле является обязательным',
setValueAs: (value) => Number(value),
})}
error={!!(errors as any)?.scale_min}
helperText={(errors as any)?.scale_min?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="number"
label={"Масштаб (мин)"}
name="scale_min"
/>
<TextField
{...register("scale_max", {
// required: 'Это поле является обязательным',
setValueAs: (value) => Number(value),
})}
error={!!(errors as any)?.scale_max}
helperText={(errors as any)?.scale_max?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="number"
label={"Масштаб (макс)"}
name="scale_max"
/>
<TextField
{...register("rotate", {
// required: 'Это поле является обязательным',
setValueAs: (value) => Number(value),
})}
error={!!(errors as any)?.rotate}
helperText={(errors as any)?.rotate?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="number"
label={"Поворот"}
name="rotate"
/>
<TextField
{...register("center_latitude", {
// required: 'Это поле является обязательным',
setValueAs: (value) => Number(value),
})}
error={!!(errors as any)?.center_latitude}
helperText={(errors as any)?.center_latitude?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="number"
label={"Центр. широта"}
name="center_latitude"
/>
<TextField
{...register("center_longitude", {
// required: 'Это поле является обязательным',
setValueAs: (value) => Number(value),
})}
error={!!(errors as any)?.center_longitude}
helperText={(errors as any)?.center_longitude?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="number"
label={"Центр. долгота"}
name="center_longitude"
/>
</Box>
{routeId && (
<>
<LinkedItems<StationItem>
type="edit"
parentId={routeId}
parentResource="route"
childResource="station"
fields={stationFields}
title="станции"
dragAllowed={true}
/>
)}
/>
<Typography
variant="caption"
color="textSecondary"
sx={{ mt: 0, mb: 1 }}
>
(Прямой / Обратный)
</Typography>
<LinkedItems<VehicleItem>
type="edit"
parentId={routeId}
parentResource="route"
childResource="vehicle"
fields={vehicleFields}
title="транспортные средства"
/>
</>
)}
<TextField
{...register("path", {
required: "Это поле является обязательным",
setValueAs: (value: string) => {
try {
const lines = value.trim().split("\n");
return lines.map((line) => {
const [lat, lon] = line
.trim()
.split(/[\s,]+/)
.map(Number);
return [lat, lon];
});
} catch {
return [];
}
},
validate: (value: unknown) => {
if (!Array.isArray(value)) return "Неверный формат";
if (value.length === 0)
return "Введите хотя бы одну пару координат";
if (
!value.every(
(point: unknown) => Array.isArray(point) && point.length === 2
)
) {
return "Каждая строка должна содержать две координаты";
}
if (
!value.every((point: unknown[]) =>
point.every(
(coord: unknown) =>
!isNaN(Number(coord)) && typeof coord === "number"
)
)
) {
return "Координаты должны быть числами";
}
return true;
},
})}
error={!!(errors as any)?.path}
helperText={(errors as any)?.path?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="text"
label={"Координаты маршрута *"}
name="path"
placeholder="55.7558 37.6173
55.7539 37.6208"
multiline
rows={4}
sx={{
marginBottom: 2,
}}
/>
<TextField
{...register("route_sys_number", {
required: "Это поле является обязательным",
})}
error={!!(errors as any)?.route_sys_number}
helperText={(errors as any)?.route_sys_number?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="number"
label={"Системный номер маршрута *"}
name="route_sys_number"
/>
<TextField
{...register("governor_appeal", {
// required: 'Это поле является обязательным',
})}
error={!!(errors as any)?.governor_appeal}
helperText={(errors as any)?.governor_appeal?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="number"
label={"Обращение губернатора"}
name="governor_appeal"
/>
<TextField
{...register("scale_min", {
// required: 'Это поле является обязательным',
setValueAs: (value) => Number(value),
})}
error={!!(errors as any)?.scale_min}
helperText={(errors as any)?.scale_min?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="number"
label={"Масштаб (мин)"}
name="scale_min"
/>
<TextField
{...register("scale_max", {
// required: 'Это поле является обязательным',
setValueAs: (value) => Number(value),
})}
error={!!(errors as any)?.scale_max}
helperText={(errors as any)?.scale_max?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="number"
label={"Масштаб (макс)"}
name="scale_max"
/>
<TextField
{...register("rotate", {
// required: 'Это поле является обязательным',
setValueAs: (value) => Number(value),
})}
error={!!(errors as any)?.rotate}
helperText={(errors as any)?.rotate?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="number"
label={"Поворот"}
name="rotate"
/>
<TextField
{...register("center_latitude", {
// required: 'Это поле является обязательным',
setValueAs: (value) => Number(value),
})}
error={!!(errors as any)?.center_latitude}
helperText={(errors as any)?.center_latitude?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="number"
label={"Центр. широта"}
name="center_latitude"
/>
<TextField
{...register("center_longitude", {
// required: 'Это поле является обязательным',
setValueAs: (value) => Number(value),
})}
error={!!(errors as any)?.center_longitude}
helperText={(errors as any)?.center_longitude?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="number"
label={"Центр. долгота"}
name="center_longitude"
/>
<Box sx={{ display: 'flex', justifyContent: 'flex-start' }}>
<Button
variant="contained"
color="primary"
onClick={() => navigate(`/route-preview/${routeId}`)}
>
Предпросмотр маршрута
</Button>
</Box>
</Box>
{routeId && (
<>
<LinkedItems<StationItem>
type="edit"
parentId={routeId}
parentResource="route"
childResource="station"
fields={stationFields}
title="станции"
dragAllowed={true}
/>
<LinkedItems<VehicleItem>
type="edit"
parentId={routeId}
parentResource="route"
childResource="vehicle"
fields={vehicleFields}
title="транспортные средства"
/>
</>
)}
</Edit>
);
};

View File

@ -7,12 +7,15 @@ import {
ShowButton,
useDataGrid,
} from "@refinedev/mui";
import { Typography } from "@mui/material";
import { Button, Typography } from "@mui/material";
import React from "react";
import MapIcon from '@mui/icons-material/Map';
import { localeText } from "../../locales/ru/localeText";
import { useLink } from "@refinedev/core";
export const RouteList = () => {
const Link = useLink();
const { dataGridProps } = useDataGrid({
resource: "route/",
});
@ -123,10 +126,10 @@ export const RouteList = () => {
headerName: "Направление маршрута",
type: "boolean",
display: "flex",
flex: 1,
align: "left",
headerAlign: "left",
minWidth: 120,
flex: 1,
renderCell: ({ value }) => (
<Typography style={{ color: value ? "#48989f" : "#7f6b58" }}>
{value ? "прямое" : "обратное"}
@ -139,7 +142,7 @@ export const RouteList = () => {
cellClassName: "route-actions",
align: "right",
headerAlign: "center",
minWidth: 120,
minWidth: 160,
display: "flex",
sortable: false,
filterable: false,
@ -148,6 +151,11 @@ export const RouteList = () => {
return (
<>
<EditButton hideText recordItemId={row.id} />
<Link to={`/route-preview/${row.id}`}>
<Button sx={{minWidth: 0}} >
<MapIcon fontSize="small" />
</Button>
</Link>
<ShowButton hideText recordItemId={row.id} />
<DeleteButton
hideText

View File

@ -5,6 +5,8 @@ import { LinkedItems } from "../../components/LinkedItems";
import {
StationItem,
VehicleItem,
SightItem,
sightFields,
stationFields,
vehicleFields,
} from "./types";
@ -88,6 +90,15 @@ export const RouteShow = () => {
fields={vehicleFields}
title="транспортные средства"
/>
<LinkedItems<SightItem>
type="show"
parentId={record.id}
parentResource="route"
childResource="sight"
fields={sightFields}
title="достопримечательности"
/>
</>
)}

View File

@ -16,6 +16,15 @@ export type VehicleItem = {
[key: string]: string | number;
};
export type SightItem = {
id: number;
name: string;
city: string;
city_id: number;
address: string;
[key: string]: string | number;
};
export type FieldType<T> = {
label: string;
data: keyof T;
@ -27,6 +36,12 @@ export const stationFields: Array<FieldType<StationItem>> = [
{ label: "Описание", data: "description" },
];
export const sightFields: Array<FieldType<SightItem>> = [
{ label: "Название", data: "name" },
{ label: "Город", data: "city" },
{ label: "Адрес", data: "address" },
];
export const vehicleFields: Array<FieldType<VehicleItem>> = [
{ label: "Бортовой номер", data: "tail_number" },
{

View File

@ -2,13 +2,10 @@ 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 { cityStore } from "../../store/CityStore";
import React, { useState, useEffect } from "react";
import { TOKEN_KEY } from "../../authProvider";
import { TOKEN_KEY } from "@providers";
import { observer } from "mobx-react-lite";
import Cookies from "js-cookie";
import { useLocation } from "react-router";
import { languageStore } from "../../store/LanguageStore";
import { Languages, languageStore, cityStore } from "@stores";
export const SightCreate = observer(() => {
const { language, setLanguageAction } = languageStore;
const [sightData, setSightData] = useState({
@ -27,7 +24,7 @@ export const SightCreate = observer(() => {
});
// Состояния для предпросмотра
const handleLanguageChange = (lang: string) => {
const handleLanguageChange = (lang: Languages) => {
setSightData((prevData) => ({
...prevData,
[language]: {

View File

@ -16,15 +16,14 @@ import React, { useState, useEffect } from "react";
import { LinkedItems } from "../../components/LinkedItems";
import { CreateSightArticle } from "../../components/CreateSightArticle";
import { ArticleItem, articleFields } from "./types";
import { TOKEN_KEY } from "../../authProvider";
import { TOKEN_KEY } from "@providers";
import { observer } from "mobx-react-lite";
import { languageStore } from "../../store/LanguageStore";
import { Languages, languageStore, articleStore } from "@stores";
import axios from "axios";
import { LanguageSwitch } from "../../components/LanguageSwitch/index";
import { ReactPhotoSphereViewer } from "react-photo-sphere-viewer";
import { ModelViewer } from "../media/ModelViewer";
import { articleStore } from "../../store/ArticleStore";
import { ModelViewer } from "@ui";
import { ArticleEditModal } from "../../components/modals/ArticleEditModal/index";
function a11yProps(index: number) {
@ -132,12 +131,7 @@ export const SightEdit = observer(() => {
operator: "contains",
value,
},
],
meta: {
headers: {
"Accept-Language": language,
},
},
]
});
const { autocompleteProps: articleAutocompleteProps } = useAutocomplete({
@ -154,13 +148,7 @@ export const SightEdit = observer(() => {
operator: "contains",
value,
},
],
meta: {
headers: {
"Accept-Language": language,
},
},
]
});
useEffect(() => {
@ -428,7 +416,7 @@ export const SightEdit = observer(() => {
address: watch("address") ?? "",
},
}));
setLanguageAction(lang);
setLanguageAction(lang as Languages);
};
useEffect(() => {
return () => {
@ -444,8 +432,8 @@ export const SightEdit = observer(() => {
onChange={(_, newValue) => setTabValue(newValue)}
aria-label="basic tabs example"
>
<Tab label="Левая статья" {...a11yProps(1)} />
<Tab label="Правая статья" {...a11yProps(2)} />
<Tab label="Левый виджет" {...a11yProps(1)} />
<Tab label="Правый виджет" {...a11yProps(2)} />
<Tab label="Основная информация" {...a11yProps(3)} />
</Tabs>
</Box>
@ -1189,7 +1177,7 @@ export const SightEdit = observer(() => {
/>
)}
{mediaFile && mediaFile.src && mediaFile.media_type == 5 && (
{mediaFile?.src && mediaFile.media_type == 5 && (
<ReactPhotoSphereViewer
src={mediaFile.src}
height={"300px"}
@ -1217,9 +1205,8 @@ export const SightEdit = observer(() => {
}}
>
{previewSelected &&
previewMedia &&
previewMedia.src &&
previewMedia.media_type === 1 && (
previewMedia?.src &&
previewMedia?.media_type === 1 && (
<img
src={previewMedia.src}
alt={previewMedia.filename}
@ -1233,8 +1220,7 @@ export const SightEdit = observer(() => {
)}
{previewSelected &&
previewMedia &&
previewMedia.media_type === 2 && (
previewMedia?.media_type === 2 && (
<video
src={previewMedia.src}
style={{
@ -1249,8 +1235,7 @@ export const SightEdit = observer(() => {
/>
)}
{previewSelected &&
previewMedia &&
previewMedia.media_type === 3 && (
previewMedia?.media_type === 3 && (
<img
src={previewMedia.src}
alt={previewMedia.filename}
@ -1263,8 +1248,7 @@ export const SightEdit = observer(() => {
/>
)}
{previewSelected &&
previewMedia &&
previewMedia.media_type === 4 && (
previewMedia?.media_type === 4 && (
<img
src={previewMedia.src}
alt={previewMedia.filename}
@ -1278,9 +1262,8 @@ export const SightEdit = observer(() => {
)}
{previewSelected &&
previewMedia &&
previewMedia.src &&
previewMedia.media_type == 5 && (
previewMedia?.src &&
previewMedia?.media_type == 5 && (
<ReactPhotoSphereViewer
src={previewMedia.src}
height={"300px"}
@ -1289,8 +1272,7 @@ export const SightEdit = observer(() => {
)}
{previewSelected &&
previewMedia &&
previewMedia.media_type === 6 && (
previewMedia?.media_type === 6 && (
<ModelViewer height={"400px"} fileUrl={previewMedia.src} />
)}

View File

@ -4,6 +4,8 @@ import { useForm } from "@refinedev/react-hook-form";
import { Controller } from "react-hook-form";
import { VEHICLE_TYPES } from "../../lib/constants";
import { observer } from "mobx-react-lite";
import { languageStore, META_LANGUAGE } from "@stores";
type VehicleFormValues = {
tail_number: number;
@ -11,13 +13,16 @@ type VehicleFormValues = {
city_id: number;
};
export const VehicleEdit = () => {
export const VehicleEdit = observer(() => {
const { language } = languageStore;
const {
saveButtonProps,
register,
control,
formState: { errors },
} = useForm<VehicleFormValues>({});
} = useForm<VehicleFormValues>({
refineCoreProps: META_LANGUAGE(language)
});
const { autocompleteProps: carrierAutocompleteProps } = useAutocomplete({
resource: "carrier",
@ -136,4 +141,4 @@ export const VehicleEdit = () => {
</Box>
</Edit>
);
};
});

View File

@ -1,6 +1,6 @@
import dataProvider from "@refinedev/simple-rest";
import { TOKEN_KEY } from "../authProvider";
import { TOKEN_KEY } from "@providers";
import axios from "axios";
import { languageStore } from "../store/LanguageStore";

View File

@ -2,7 +2,7 @@ import i18n from 'i18next'
import {initReactI18next} from 'react-i18next'
import {I18nProvider} from '@refinedev/core'
import translationRU from './locales/ru/translation.json'
import translationRU from '../locales/ru/translation.json'
i18n.use(initReactI18next).init({
resources: {

3
src/providers/index.ts Normal file
View File

@ -0,0 +1,3 @@
export * from './data'
export * from './authProvider'
export * from './i18nProvider'

View File

@ -1,15 +1,34 @@
import { makeAutoObservable } from "mobx";
export type Languages = "en" | "ru" | "zh";
class LanguageStore {
language = "ru";
language: Languages = "ru";
constructor() {
makeAutoObservable(this);
}
setLanguageAction = (language: string) => {
setLanguageAction = (language: Languages) => {
this.language = language;
};
}
export const languageStore = new LanguageStore();
export const META_LANGUAGE = (language: Languages) => {
return {
meta: {
headers: {
"Accept-Language": language,
},
},
}
}
export const EVERY_LANGUAGE = (data: any) => {
return {
"en": data,
"ru": data,
"zh": data,
}
}

4
src/store/index.ts Normal file
View File

@ -0,0 +1,4 @@
export * from './ArticleStore';
export * from './CityStore';
export * from './LanguageStore';
export * from './StationStore';

View File

@ -17,11 +17,12 @@
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@mt/common-types": ["src/preview/types"],
"@mt/components": ["src/preview/components"],
"@mt/i18n": ["src/preview/i18n"],
"@mt/widgets": ["src/preview/widgets"],
"@mt/utils": ["src/preview/utils"]
"@stores": ["./src/store"],
"@ui": ["./src/components/ui"],
"@providers": ["./src/providers"],
"@lib": ["./src/lib"],
"@components": ["./src/components"],
"@/*": ["./src/*"]
}
},
"include": ["src", "svg.d.ts"],

View File

@ -7,11 +7,12 @@ export default defineConfig({
plugins: [svgr(), react()],
resolve: {
alias: {
"@mt/common-types": path.resolve(__dirname, "./src/preview/types"),
"@mt/components": path.resolve(__dirname, "./src/preview/components"),
"@mt/i18n": path.resolve(__dirname, "./src/preview/i18n"),
"@mt/widgets": path.resolve(__dirname, "./src/preview/widgets"),
"@mt/utils": path.resolve(__dirname, "./src/preview/utils"),
"@ui": path.resolve(__dirname, "./src/components/ui"),
"@stores": path.resolve(__dirname, "./src/store"),
"@providers": path.resolve(__dirname, "./src/providers"),
"@lib": path.resolve(__dirname, "./src/lib"),
"@components": path.resolve(__dirname, "./src/components"),
"@": path.resolve(__dirname, "./src/"),
},
},
});

273
yarn.lock
View File

@ -453,10 +453,110 @@
resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz"
integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==
"@esbuild/win32-x64@^0.25.3":
version "0.25.3"
resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz"
integrity sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==
"@esbuild/android-arm@0.18.20":
version "0.18.20"
resolved "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz"
integrity sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==
"@esbuild/android-arm64@0.18.20":
version "0.18.20"
resolved "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz"
integrity sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==
"@esbuild/android-x64@0.18.20":
version "0.18.20"
resolved "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz"
integrity sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==
"@esbuild/darwin-arm64@0.18.20":
version "0.18.20"
resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz"
integrity sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==
"@esbuild/darwin-x64@0.18.20":
version "0.18.20"
resolved "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz"
integrity sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==
"@esbuild/freebsd-arm64@0.18.20":
version "0.18.20"
resolved "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz"
integrity sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==
"@esbuild/freebsd-x64@0.18.20":
version "0.18.20"
resolved "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz"
integrity sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==
"@esbuild/linux-arm@0.18.20":
version "0.18.20"
resolved "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz"
integrity sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==
"@esbuild/linux-arm64@0.18.20":
version "0.18.20"
resolved "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz"
integrity sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==
"@esbuild/linux-ia32@0.18.20":
version "0.18.20"
resolved "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz"
integrity sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==
"@esbuild/linux-loong64@0.18.20":
version "0.18.20"
resolved "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz"
integrity sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==
"@esbuild/linux-mips64el@0.18.20":
version "0.18.20"
resolved "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz"
integrity sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==
"@esbuild/linux-ppc64@0.18.20":
version "0.18.20"
resolved "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz"
integrity sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==
"@esbuild/linux-riscv64@0.18.20":
version "0.18.20"
resolved "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz"
integrity sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==
"@esbuild/linux-s390x@0.18.20":
version "0.18.20"
resolved "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz"
integrity sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==
"@esbuild/linux-x64@0.18.20":
version "0.18.20"
resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz"
integrity sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==
"@esbuild/netbsd-x64@0.18.20":
version "0.18.20"
resolved "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz"
integrity sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==
"@esbuild/openbsd-x64@0.18.20":
version "0.18.20"
resolved "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz"
integrity sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==
"@esbuild/sunos-x64@0.18.20":
version "0.18.20"
resolved "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz"
integrity sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==
"@esbuild/win32-arm64@0.18.20":
version "0.18.20"
resolved "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz"
integrity sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==
"@esbuild/win32-ia32@0.18.20":
version "0.18.20"
resolved "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz"
integrity sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==
"@esbuild/win32-x64@0.18.20":
version "0.18.20"
@ -758,31 +858,12 @@
dependencies:
"@babel/runtime" "^7.27.0"
"@mui/types@^7.4.1":
version "7.4.1"
resolved "https://registry.npmjs.org/@mui/types/-/types-7.4.1.tgz"
integrity sha512-gUL8IIAI52CRXP/MixT1tJKt3SI6tVv4U/9soFsTtAsHzaJQptZ42ffdHZV3niX1ei0aUgMvOxBBN0KYqdG39g==
dependencies:
"@babel/runtime" "^7.27.0"
"@mui/types@~7.2.24":
version "7.2.24"
resolved "https://registry.npmjs.org/@mui/types/-/types-7.2.24.tgz"
integrity sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw==
"@mui/utils@^5.16.6 || ^6.0.0 || ^7.0.0":
version "7.0.2"
resolved "https://registry.npmjs.org/@mui/utils/-/utils-7.0.2.tgz"
integrity sha512-72gcuQjPzhj/MLmPHLCgZjy2VjOH4KniR/4qRtXTTXIEwbkgcN+Y5W/rC90rWtMmZbjt9svZev/z+QHUI4j74w==
dependencies:
"@babel/runtime" "^7.27.0"
"@mui/types" "^7.4.1"
"@types/prop-types" "^15.7.14"
clsx "^2.1.1"
prop-types "^15.8.1"
react-is "^19.1.0"
"@mui/utils@^6.0.0-alpha.1", "@mui/utils@^6.0.0-alpha.3", "@mui/utils@^6.4.9":
"@mui/utils@^5.16.6 || ^6.0.0 || ^7.0.0", "@mui/utils@^6.0.0-alpha.1", "@mui/utils@^6.0.0-alpha.3", "@mui/utils@^6.4.9":
version "6.4.9"
resolved "https://registry.npmjs.org/@mui/utils/-/utils-6.4.9.tgz"
integrity sha512-Y12Q9hbK9g+ZY0T3Rxrx9m2m10gaphDuUMgWxyV5kNJevVxXYCLclYUCC9vXaIk1/NdNDTcW2Yfr2OGvNFNmHg==
@ -1470,14 +1551,7 @@
resolved "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz"
integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==
"@types/node@*":
version "22.15.2"
resolved "https://registry.npmjs.org/@types/node/-/node-22.15.2.tgz"
integrity sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A==
dependencies:
undici-types "~6.21.0"
"@types/node@^18.16.2":
"@types/node@*", "@types/node@^18.16.2":
version "18.19.87"
resolved "https://registry.npmjs.org/@types/node/-/node-18.19.87.tgz"
integrity sha512-OIAAu6ypnVZHmsHCeJ+7CCSub38QNBS9uceMQeg7K5Ur0Jr+wG9wEOEvvMbhp09pxD5czIUy/jND7s7Tb6Nw7A==
@ -1541,14 +1615,7 @@
resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz"
integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==
"@types/react@*":
version "19.1.2"
resolved "https://registry.npmjs.org/@types/react/-/react-19.1.2.tgz"
integrity sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==
dependencies:
csstype "^3.0.2"
"@types/react@^18.0.0":
"@types/react@*", "@types/react@^18.0.0", "@types/react@16 || 17 || 18 || 19":
version "18.3.20"
resolved "https://registry.npmjs.org/@types/react/-/react-18.3.20.tgz"
integrity sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg==
@ -1556,13 +1623,6 @@
"@types/prop-types" "*"
csstype "^3.0.2"
"@types/react@16 || 17 || 18 || 19":
version "19.1.2"
resolved "https://registry.npmjs.org/@types/react/-/react-19.1.2.tgz"
integrity sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==
dependencies:
csstype "^3.0.2"
"@types/semver@^7.3.12":
version "7.7.0"
resolved "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz"
@ -1597,12 +1657,7 @@
resolved "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz"
integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==
"@types/unist@^2", "@types/unist@^2.0.0", "@types/unist@^2.0.3":
version "2.0.11"
resolved "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz"
integrity sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==
"@types/unist@^2.0.2":
"@types/unist@^2", "@types/unist@^2.0.0", "@types/unist@^2.0.2", "@types/unist@^2.0.3":
version "2.0.11"
resolved "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz"
integrity sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==
@ -2959,7 +3014,12 @@ estraverse@^4.1.1:
resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz"
integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
estraverse@^5.1.0, estraverse@^5.2.0:
estraverse@^5.1.0:
version "5.3.0"
resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz"
integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
estraverse@^5.2.0:
version "5.3.0"
resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz"
integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
@ -3282,6 +3342,11 @@ fs.realpath@^1.0.0:
resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
fsevents@~2.3.2:
version "2.3.3"
resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
function-bind@^1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"
@ -4023,7 +4088,12 @@ kind-of@^5.0.2:
resolved "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz"
integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==
kind-of@^6.0.0, kind-of@^6.0.2:
kind-of@^6.0.0:
version "6.0.3"
resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz"
integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
kind-of@^6.0.2:
version "6.0.3"
resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz"
integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
@ -4359,7 +4429,19 @@ mdast-util-to-hast@^13.0.0:
unist-util-visit "^5.0.0"
vfile "^6.0.0"
mdast-util-to-markdown@^0.6.0, mdast-util-to-markdown@^0.6.1, mdast-util-to-markdown@~0.6.0:
mdast-util-to-markdown@^0.6.0:
version "0.6.5"
resolved "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-0.6.5.tgz"
integrity sha512-XeV9sDE7ZlOQvs45C9UKMtfTcctcaj/pGwH8YLbMHoMOXNNCn2LsqVQOqrF1+/NU8lKDAqozme9SCXWyo9oAcQ==
dependencies:
"@types/unist" "^2.0.0"
longest-streak "^2.0.0"
mdast-util-to-string "^2.0.0"
parse-entities "^2.0.0"
repeat-string "^1.0.0"
zwitch "^1.0.0"
mdast-util-to-markdown@^0.6.1:
version "0.6.5"
resolved "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-0.6.5.tgz"
integrity sha512-XeV9sDE7ZlOQvs45C9UKMtfTcctcaj/pGwH8YLbMHoMOXNNCn2LsqVQOqrF1+/NU8lKDAqozme9SCXWyo9oAcQ==
@ -4386,6 +4468,18 @@ mdast-util-to-markdown@^2.0.0:
unist-util-visit "^5.0.0"
zwitch "^2.0.0"
mdast-util-to-markdown@~0.6.0:
version "0.6.5"
resolved "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-0.6.5.tgz"
integrity sha512-XeV9sDE7ZlOQvs45C9UKMtfTcctcaj/pGwH8YLbMHoMOXNNCn2LsqVQOqrF1+/NU8lKDAqozme9SCXWyo9oAcQ==
dependencies:
"@types/unist" "^2.0.0"
longest-streak "^2.0.0"
mdast-util-to-string "^2.0.0"
parse-entities "^2.0.0"
repeat-string "^1.0.0"
zwitch "^1.0.0"
mdast-util-to-string@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz"
@ -4659,7 +4753,7 @@ micromark-util-types@^2.0.0:
resolved "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz"
integrity sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==
micromark@^2.11.3, micromark@~2.11.0, micromark@~2.11.3:
micromark@^2.11.3:
version "2.11.4"
resolved "https://registry.npmjs.org/micromark/-/micromark-2.11.4.tgz"
integrity sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==
@ -4690,6 +4784,22 @@ micromark@^4.0.0:
micromark-util-symbol "^2.0.0"
micromark-util-types "^2.0.0"
micromark@~2.11.0:
version "2.11.4"
resolved "https://registry.npmjs.org/micromark/-/micromark-2.11.4.tgz"
integrity sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==
dependencies:
debug "^4.0.0"
parse-entities "^2.0.0"
micromark@~2.11.3:
version "2.11.4"
resolved "https://registry.npmjs.org/micromark/-/micromark-2.11.4.tgz"
integrity sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==
dependencies:
debug "^4.0.0"
parse-entities "^2.0.0"
micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.8:
version "4.0.8"
resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz"
@ -4978,7 +5088,14 @@ os-tmpdir@~1.0.2:
resolved "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz"
integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==
p-limit@^2.0.0, p-limit@^2.2.0:
p-limit@^2.0.0:
version "2.3.0"
resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz"
integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
dependencies:
p-try "^2.0.0"
p-limit@^2.2.0:
version "2.3.0"
resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz"
integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
@ -5456,7 +5573,7 @@ react-is@^17.0.2:
resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
react-is@^19.0.0, react-is@^19.1.0:
react-is@^19.0.0:
version "19.1.0"
resolved "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz"
integrity sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==
@ -5636,7 +5753,14 @@ redeyed@~2.1.0:
dependencies:
esprima "~4.0.0"
redux@^4.0.0, redux@^4.0.4:
redux@^4.0.0:
version "4.2.1"
resolved "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz"
integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==
dependencies:
"@babel/runtime" "^7.9.2"
redux@^4.0.4:
version "4.2.1"
resolved "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz"
integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==
@ -5857,18 +5981,18 @@ semver@^6.3.1:
resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
semver@^7.1.1, semver@^7.3.5, semver@^7.3.7, semver@^7.5.3:
version "7.7.1"
resolved "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz"
integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==
semver@7.5.2:
semver@^7.1.1, semver@^7.3.5, semver@^7.3.7, semver@7.5.2:
version "7.5.2"
resolved "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz"
integrity sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==
dependencies:
lru-cache "^6.0.0"
semver@^7.5.3:
version "7.7.1"
resolved "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz"
integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==
send@0.19.0:
version "0.19.0"
resolved "https://registry.npmjs.org/send/-/send-0.19.0.tgz"
@ -6015,7 +6139,17 @@ source-map@^0.5.7:
resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz"
integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==
source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
source-map@^0.6.0:
version "0.6.1"
resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
source-map@^0.6.1:
version "0.6.1"
resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
source-map@~0.6.1:
version "0.6.1"
resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
@ -6444,11 +6578,6 @@ undici-types@~5.26.4:
resolved "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz"
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
undici-types@~6.21.0:
version "6.21.0"
resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz"
integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==
unicode-emoji-modifier-base@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz"