last changes
This commit is contained in:
@ -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,
|
||||
|
@ -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: "Это поле является обязательным",
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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";
|
||||
|
||||
|
71
src/components/ui/LanguageSelector.tsx
Normal file
71
src/components/ui/LanguageSelector.tsx
Normal 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>
|
||||
);
|
||||
});
|
97
src/components/ui/MediaView.tsx
Normal file
97
src/components/ui/MediaView.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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 (
|
5
src/components/ui/index.ts
Normal file
5
src/components/ui/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from './Icons';
|
||||
export * from './LanguageSelector';
|
||||
export * from './SidebarTitle';
|
||||
export * from './MediaView';
|
||||
export * from './ModelViewer';
|
@ -10,4 +10,4 @@ export const MEDIA_TYPES = [
|
||||
export const VEHICLE_TYPES = [
|
||||
{ label: "Трамвай", value: 1 },
|
||||
{ label: "Троллейбус", value: 2 },
|
||||
];
|
||||
];
|
1
src/lib/index.ts
Normal file
1
src/lib/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { MEDIA_TYPES, VEHICLE_TYPES } from './constants'
|
@ -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>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
@ -4,7 +4,7 @@ export type MediaItem = {
|
||||
id: number
|
||||
filename: string
|
||||
media_name: string
|
||||
media_type: string
|
||||
media_type: number
|
||||
media_order?: number
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -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>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
})
|
@ -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({});
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
});
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -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="достопримечательности"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
@ -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" },
|
||||
{
|
||||
|
@ -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]: {
|
||||
|
@ -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} />
|
||||
)}
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -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";
|
||||
|
@ -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
3
src/providers/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './data'
|
||||
export * from './authProvider'
|
||||
export * from './i18nProvider'
|
@ -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
4
src/store/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from './ArticleStore';
|
||||
export * from './CityStore';
|
||||
export * from './LanguageStore';
|
||||
export * from './StationStore';
|
Reference in New Issue
Block a user