last changes, possibly

This commit is contained in:
Spynder 2025-05-15 04:32:23 +03:00
parent 042b53e6a4
commit ab1fd6b22a
17 changed files with 1044 additions and 1354 deletions

View File

@ -18,10 +18,7 @@ import {
ALLOWED_IMAGE_TYPES,
ALLOWED_VIDEO_TYPES,
} from "../components/media/MediaFormUtils";
import { LinkedItems } from "./LinkedItems";
import { mediaFields, MediaItem } from "../pages/article/types";
import { LanguageSelector } from "@ui";
import { EVERY_LANGUAGE, Languages, languageStore } from "@stores";
import { EVERY_LANGUAGE, Languages } from "@stores";
const MemoizedSimpleMDE = React.memo(MarkdownEditor);
@ -38,6 +35,9 @@ type Props = {
childResource: string;
title: string;
left?: boolean;
language: Languages,
setHeadingParent?: (heading: string) => void,
setBodyParent?: (body: string) => void,
};
export const CreateSightArticle = ({
@ -46,10 +46,13 @@ export const CreateSightArticle = ({
childResource,
title,
left,
language,
setHeadingParent,
setBodyParent
}: Props) => {
const theme = useTheme();
const [mediaFiles, setMediaFiles] = useState<MediaFile[]>([]);
const { language, setLanguageAction } = languageStore;
const [workingLanguage, setWorkingLanguage] = useState<Languages>(language);
const {
register: registerItem,
@ -66,6 +69,7 @@ export const CreateSightArticle = ({
},
});
const [articleData, setArticleData] = useState({
heading: EVERY_LANGUAGE(""),
body: EVERY_LANGUAGE("")
@ -76,34 +80,31 @@ export const CreateSightArticle = ({
...articleData,
heading: {
...articleData.heading,
[language]: watch("heading") ?? "",
[workingLanguage]: watch("heading") ?? "",
},
body: {
...articleData.body,
[language]: watch("body") ?? "",
[workingLanguage]: watch("body") ?? "",
}
}
setArticleData(newArticleData);
return newArticleData;
}
// const handleFormSubmit = handleSubmit((values: FieldValues) => {
// const newTranslations = updateTranslations();
// console.log(newTranslations);
// return onFinish({
// translations: newTranslations
// });
// });
useEffect(() => {
setValue("heading", articleData.heading[workingLanguage] ?? "");
setValue("body", articleData.body[workingLanguage] ?? "");
}, [workingLanguage, articleData, setValue]);
useEffect(() => {
setValue("heading", articleData.heading[language] ?? "");
setValue("body", articleData.body[language] ?? "");
}, [language, articleData, setValue]);
const handleLanguageChange = (lang: Languages) => {
updateTranslations();
setLanguageAction(lang);
};
setWorkingLanguage(language);
}, [language]);
useEffect(() => {
setHeadingParent?.(watch("heading"));
setBodyParent?.(watch("body"));
}, [watch("heading"), watch("body"), setHeadingParent, setBodyParent]);
const simpleMDEOptions = React.useMemo(
() => ({
@ -152,8 +153,7 @@ export const CreateSightArticle = ({
try {
// Создаем статью
const response = await axiosInstance.post(
`${import.meta.env.VITE_KRBL_API}/${childResource}`,
{
`${import.meta.env.VITE_KRBL_API}/${childResource}`, {
...data,
translations: updateTranslations()
}
@ -220,154 +220,135 @@ export const CreateSightArticle = ({
};
return (
<Accordion>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
<Box component="form" onSubmit={handleSubmitItem(handleCreate)}>
<TextField
{...registerItem("heading", {
required: "Это поле является обязательным",
})}
error={!!(itemErrors as any)?.heading}
helperText={(itemErrors as any)?.heading?.message}
margin="normal"
fullWidth
slotProps={{inputLabel: {shrink: true}}}
type="text"
sx={{
marginTop: 2,
background: theme.palette.background.paper,
borderBottom: `1px solid ${theme.palette.divider}`,
zIndex: 2000,
backgroundColor: theme.palette.background.paper,
}}
>
<Typography variant="subtitle1" fontWeight="bold">
Создать {title}
</Typography>
</AccordionSummary>
<AccordionDetails sx={{ background: theme.palette.background.paper }}>
<Box component="form" onSubmit={handleSubmitItem(handleCreate)}>
<LanguageSelector action={handleLanguageChange} />
<TextField
{...registerItem("heading", {
required: "Это поле является обязательным",
})}
error={!!(itemErrors as any)?.heading}
helperText={(itemErrors as any)?.heading?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="text"
sx={{
zIndex: 2000,
backgroundColor: theme.palette.background.paper,
}}
label="Заголовок *"
/>
label="Заголовок *"
/>
<Controller
control={controlItem}
name="body"
rules={{ required: "Это поле является обязательным" }}
defaultValue=""
render={({ field: { onChange, value } }) => (
<MemoizedSimpleMDE
value={value}
onChange={onChange}
options={simpleMDEOptions}
className="my-markdown-editor"
/>
)}
<Controller
control={controlItem}
name="body"
rules={{ required: "Это поле является обязательным" }}
defaultValue=""
render={({ field: { onChange, value } }) => (
<MemoizedSimpleMDE
value={value}
onChange={onChange}
options={simpleMDEOptions}
className="my-markdown-editor"
/>
)}
/>
{/* Dropzone для медиа файлов */}
<Box sx={{ mt: 2, mb: 2 }}>
{/* Dropzone для медиа файлов */}
<Box sx={{ mt: 2, mb: 2 }}>
<Box
{...getRootProps()}
sx={{
border: "2px dashed",
borderColor: isDragActive ? "primary.main" : "grey.300",
borderRadius: 1,
p: 2,
textAlign: "center",
cursor: "pointer",
"&:hover": {
borderColor: "primary.main",
},
}}
>
<input {...getInputProps()} />
<Typography>
{isDragActive
? "Перетащите файлы сюда..."
: "Перетащите файлы сюда или кликните для выбора"}
</Typography>
</Box>
{/* Превью загруженных файлов */}
<Box sx={{ mt: 2, display: "flex", flexWrap: "wrap", gap: 1 }}>
{mediaFiles.map((mediaFile, index) => (
<Box
{...getRootProps()}
key={mediaFile.preview}
sx={{
border: "2px dashed",
borderColor: isDragActive ? "primary.main" : "grey.300",
borderRadius: 1,
p: 2,
textAlign: "center",
cursor: "pointer",
"&:hover": {
borderColor: "primary.main",
},
position: "relative",
width: 100,
height: 100,
}}
>
<input {...getInputProps()} />
<Typography>
{isDragActive
? "Перетащите файлы сюда..."
: "Перетащите файлы сюда или кликните для выбора"}
</Typography>
</Box>
{/* Превью загруженных файлов */}
<Box sx={{ mt: 2, display: "flex", flexWrap: "wrap", gap: 1 }}>
{mediaFiles.map((mediaFile, index) => (
{mediaFile.file.type.startsWith("image/") ? (
<img
src={mediaFile.preview}
alt={mediaFile.file.name}
style={{
width: "100%",
height: "100%",
objectFit: "cover",
}}
/>
) : (
<Box
key={mediaFile.preview}
sx={{
position: "relative",
width: 100,
height: 100,
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
bgcolor: "grey.200",
}}
>
{mediaFile.file.type.startsWith("image/") ? (
<img
src={mediaFile.preview}
alt={mediaFile.file.name}
style={{
width: "100%",
height: "100%",
objectFit: "cover",
}}
/>
) : (
<Box
sx={{
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
bgcolor: "grey.200",
}}
>
<Typography variant="caption">
{mediaFile.file.name}
</Typography>
</Box>
)}
<Button
size="small"
color="error"
onClick={() => removeMedia(index)}
sx={{
position: "absolute",
top: 0,
right: 0,
minWidth: "auto",
width: 20,
height: 20,
p: 0,
}}
>
×
</Button>
<Typography variant="caption">
{mediaFile.file.name}
</Typography>
</Box>
))}
)}
<Button
size="small"
color="error"
onClick={() => removeMedia(index)}
sx={{
position: "absolute",
top: 0,
right: 0,
minWidth: "auto",
width: 20,
height: 20,
p: 0,
}}
>
×
</Button>
</Box>
</Box>
<Box sx={{ mt: 2, display: "flex", gap: 2 }}>
<Button variant="contained" color="primary" type="submit">
Создать
</Button>
<Button
variant="outlined"
onClick={() => {
resetItem();
mediaFiles.forEach((file) => URL.revokeObjectURL(file.preview));
setMediaFiles([]);
}}
>
Очистить
</Button>
</Box>
))}
</Box>
</AccordionDetails>
</Accordion>
</Box>
<Box sx={{ mt: 2, display: "flex", gap: 2 }}>
<Button variant="contained" color="primary" type="submit">
Создать
</Button>
<Button
variant="outlined"
onClick={() => {
resetItem();
mediaFiles.forEach((file) => URL.revokeObjectURL(file.preview));
setMediaFiles([]);
}}
>
Очистить
</Button>
</Box>
</Box>
);
};

View File

@ -5,8 +5,6 @@ import {
Typography,
Button,
FormControl,
Grid,
Box,
Accordion,
AccordionSummary,
AccordionDetails,
@ -21,8 +19,6 @@ import {
Paper,
TableBody,
IconButton,
Collapse,
Modal,
} from "@mui/material";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import DragIndicatorIcon from "@mui/icons-material/DragIndicator";
@ -71,6 +67,8 @@ type LinkedItemsProps<T> = {
onSave?: (items: T[]) => void;
onUpdate?: () => void;
dontRecurse?: boolean;
disableCreation?: boolean;
updatedLinkedItems?: T[];
};
const reorder = (list: any[], startIndex: number, endIndex: number) => {
@ -80,7 +78,44 @@ const reorder = (list: any[], startIndex: number, endIndex: number) => {
return result;
};
export const LinkedItems = <T extends { id: number; [key: string]: any }>({
export const LinkedItems = <T extends { id: number; [key: string]: any }>(
props: LinkedItemsProps<T>
) => {
const theme = useTheme();
return (
<>
<Accordion>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
sx={{
background: theme.palette.background.paper,
borderBottom: `1px solid ${theme.palette.divider}`,
}}
>
<Typography variant="subtitle1" fontWeight="bold">
Привязанные {props.title}
</Typography>
</AccordionSummary>
<AccordionDetails sx={{ background: theme.palette.background.paper }}>
<Stack gap={2}>
<LinkedItemsContents {...props} />
</Stack>
</AccordionDetails>
</Accordion>
{!props.dontRecurse &&
<>
<ArticleEditModal />
<StationEditModal />
</>
}
</>
);
}
export const LinkedItemsContents = <T extends { id: number; [key: string]: any }>({
parentId,
parentResource,
childResource,
@ -89,9 +124,9 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
title,
dragAllowed = false,
type,
onSave,
onUpdate,
dontRecurse = false,
disableCreation = false,
updatedLinkedItems
}: LinkedItemsProps<T>) => {
const { language } = languageStore;
const { setArticleModalOpenAction, setArticleIdAction } = articleStore;
@ -104,7 +139,6 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
const [pageNum, setPageNum] = useState<number>(1);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [mediaOrder, setMediaOrder] = useState<number>(1);
const theme = useTheme();
let availableItems = items.filter(
(item) => !linkedItems.some((linked) => linked.id === item.id)
@ -118,9 +152,12 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
}, [childResource, availableItems]);
useEffect(() => {
if (setItemsParent) {
setItemsParent(linkedItems);
}
if(!updatedLinkedItems?.length) return;
setLinkedItems(updatedLinkedItems);
}, [updatedLinkedItems]);
useEffect(() => {
setItemsParent?.(linkedItems);
}, [linkedItems, setItemsParent]);
const onDragEnd = (result: any) => {
@ -272,254 +309,230 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
return (
<>
<Accordion>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
sx={{
background: theme.palette.background.paper,
borderBottom: `1px solid ${theme.palette.divider}`,
}}
>
<Typography variant="subtitle1" fontWeight="bold">
Привязанные {title}
</Typography>
</AccordionSummary>
{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>
))}
<AccordionDetails sx={{ background: theme.palette.background.paper }}>
<Stack gap={2}>
{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>
))}
{type === "edit" && (
<TableCell width="120px">Действие</TableCell>
)}
</TableRow>
</TableHead>
{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>
))}
<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",
{type === "edit" && (
<TableCell>
<Button
variant="outlined"
color="error"
size="small"
onClick={(e) => {
e.stopPropagation();
deleteItem(item.id);
}}
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>
))}
Отвязать
</Button>
</TableCell>
)}
</TableRow>
)}
</Draggable>
))}
{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}>
{title} не найдены
</Typography>
)}
{type === "edit" && (
<Stack gap={2} mt={2}>
<Typography variant="subtitle1">Добавить {title}</Typography>
<Autocomplete
fullWidth
value={
availableItems?.find(
(item) => item.id === selectedItemId
) || null
}
onChange={(_, newValue) =>
setSelectedItemId(newValue?.id || null)
}
options={availableItems}
getOptionLabel={(item) => String(item[fields[0].data])}
renderInput={(params) => (
<TextField
{...params}
label={`Выберите ${title}`}
fullWidth
/>
)}
isOptionEqualToValue={(option, value) =>
option.id === value?.id
}
filterOptions={(options, { inputValue }) => {
const searchWords = inputValue
.toLowerCase()
.split(" ")
.filter((word) => word.length > 0);
return options.filter((option) => {
const optionWords = String(option[fields[0].data])
.toLowerCase()
.split(" ");
return searchWords.every((searchWord) =>
optionWords.some((word) => word.startsWith(searchWord))
);
});
}}
renderOption={(props, option) => (
<li {...props} key={option.id}>
{String(option[fields[0].data])}
</li>
)}
/>
{/* {childResource === "article" && (
<FormControl fullWidth>
<TextField
type="number"
label="Позиция добавляемой статьи"
name="page_num"
value={pageNum}
onChange={(e) => {
const newValue = Number(e.target.value);
const minValue = linkedItems.length + 1;
setPageNum(newValue < minValue ? minValue : newValue);
}}
fullWidth
InputLabelProps={{ shrink: true }}
/>
</FormControl>
)} */}
{childResource === "media" && (
<FormControl fullWidth>
<TextField
type="number"
label="Порядок отображения медиа"
value={mediaOrder}
onChange={(e) => {
const newValue = Number(e.target.value);
const maxValue = linkedItems.length + 1;
const value = Math.max(1, Math.min(newValue, maxValue));
setMediaOrder(value);
}}
fullWidth
InputLabelProps={{ shrink: true }}
/>
</FormControl>
{provided.placeholder}
</TableBody>
)}
</Droppable>
</Table>
</TableContainer>
</DragDropContext>
)}
<Button
variant="contained"
onClick={linkItem}
disabled={!selectedItemId}
sx={{ alignSelf: "flex-start" }}
>
Добавить
</Button>
{childResource == "station" && (
<TextField
type="text"
label="Позиция добавляемой остановки к маршруту"
value={position}
onChange={(e) => {
const newValue = Number(e.target.value);
setPosition(
newValue > linkedItems.length + 1
? linkedItems.length + 1
: newValue
);
}}
></TextField>
)}
</Stack>
{linkedItems.length === 0 && !isLoading && (
<Typography color="textSecondary" textAlign="center" py={2}>
{title} не найдены
</Typography>
)}
{type === "edit" && !disableCreation && (
<Stack gap={2} mt={2}>
<Typography variant="subtitle1">Добавить {title}</Typography>
<Autocomplete
fullWidth
value={
availableItems?.find(
(item) => item.id === selectedItemId
) || null
}
onChange={(_, newValue) =>
setSelectedItemId(newValue?.id || null)
}
options={availableItems}
getOptionLabel={(item) => String(item[fields[0].data])}
renderInput={(params) => (
<TextField
{...params}
label={`Выберите ${title}`}
fullWidth
/>
)}
</Stack>
</AccordionDetails>
</Accordion>
{!dontRecurse &&
<>
<ArticleEditModal />
<StationEditModal />
</>
}
isOptionEqualToValue={(option, value) =>
option.id === value?.id
}
filterOptions={(options, { inputValue }) => {
const searchWords = inputValue
.toLowerCase()
.split(" ")
.filter((word) => word.length > 0);
return options.filter((option) => {
const optionWords = String(option[fields[0].data])
.toLowerCase()
.split(" ");
return searchWords.every((searchWord) =>
optionWords.some((word) => word.startsWith(searchWord))
);
});
}}
renderOption={(props, option) => (
<li {...props} key={option.id}>
{String(option[fields[0].data])}
</li>
)}
/>
{/* {childResource === "article" && (
<FormControl fullWidth>
<TextField
type="number"
label="Позиция добавляемой статьи"
name="page_num"
value={pageNum}
onChange={(e) => {
const newValue = Number(e.target.value);
const minValue = linkedItems.length + 1;
setPageNum(newValue < minValue ? minValue : newValue);
}}
fullWidth
InputLabelProps={{ shrink: true }}
/>
</FormControl>
)} */}
{childResource === "media" && (
<FormControl fullWidth>
<TextField
type="number"
label="Порядок отображения медиа"
value={mediaOrder}
onChange={(e) => {
const newValue = Number(e.target.value);
const maxValue = linkedItems.length + 1;
const value = Math.max(1, Math.min(newValue, maxValue));
setMediaOrder(value);
}}
fullWidth
slotProps={{inputLabel: {shrink: true}}}
/>
</FormControl>
)}
<Button
variant="contained"
onClick={linkItem}
disabled={!selectedItemId}
sx={{ alignSelf: "flex-start" }}
>
Добавить
</Button>
{childResource == "station" && (
<TextField
type="text"
label="Позиция добавляемой остановки к маршруту"
value={position}
onChange={(e) => {
const newValue = Number(e.target.value);
setPosition(
newValue > linkedItems.length + 1
? linkedItems.length + 1
: newValue
);
}}
></TextField>
)}
</Stack>
)}
</>
);
};

5
src/components/index.ts Normal file
View File

@ -0,0 +1,5 @@
export * from './AdminOnly'
export * from './CreateSightArticle'
export * from './CustomDataGrid'
export * from './LinkedItems'
export * from './MarkdownEditor'

View File

@ -4,17 +4,16 @@ import { ReactPhotoSphereViewer } from "react-photo-sphere-viewer";
import { ModelViewer } from "./ModelViewer";
export interface MediaData {
filename?: string;
id: string | number;
media_name?: string;
media_type: number;
filename?: string;
}
export function MediaView({media} : Readonly<{media?: MediaData}>) {
const token = localStorage.getItem(TOKEN_KEY);
return (
<Box
sx={{maxHeight: "50vh", height: "100%", width: "100%", display: "flex", justifyContent: "center"}}
sx={{maxHeight: "300px", width: "100%", display: "flex", justifyContent: "center"}}
>
{media?.media_type === 1 && (
<img
@ -23,10 +22,10 @@ export function MediaView({media} : Readonly<{media?: MediaData}>) {
}/download?token=${token}`}
alt={media?.filename}
style={{
maxWidth: "100%",
height: "100%",
objectFit: "contain",
borderRadius: 8,
maxWidth: "100%",
height: "auto",
objectFit: "contain",
borderRadius: 8,
}}
/>
)}
@ -37,9 +36,10 @@ export function MediaView({media} : Readonly<{media?: MediaData}>) {
media?.id
}/download?token=${token}`}
style={{
objectFit: "contain",
borderRadius: 30,
maxWidth: "100%",
height: "100%",
objectFit: "contain",
borderRadius: 30,
}}
controls
autoPlay

View File

@ -9,7 +9,6 @@ import "easymde/dist/easymde.min.css";
import { LanguageSelector } from "@ui";
import { observer } from "mobx-react-lite";
import { EVERY_LANGUAGE, Languages, languageStore, META_LANGUAGE } from "@stores";
import { axiosInstance } from "@/providers/data";
const MemoizedSimpleMDE = React.memo(MarkdownEditor);
@ -33,15 +32,14 @@ export const ArticleCreate = observer(() => {
refineCoreProps: {
resource: "article",
...META_LANGUAGE(language)
},
warnWhenUnsavedChanges: false
}
});
// Следим за изменениями в полях body и heading
const bodyContent = watch("body");
const headingContent = watch("heading");
function updateTranslations() {
function updateTranslations(update: boolean = true) {
const newArticleData = {
...articleData,
heading: {
@ -53,13 +51,12 @@ export const ArticleCreate = observer(() => {
[language]: watch("body") ?? "",
}
}
setArticleData(newArticleData);
if(update) setArticleData(newArticleData);
return newArticleData;
}
const handleFormSubmit = handleSubmit((values: FieldValues) => {
const newTranslations = updateTranslations();
console.log(newTranslations);
const handleFormSubmit = handleSubmit((values) => {
const newTranslations = updateTranslations(false);
return onFinish({
translations: newTranslations
});
@ -80,7 +77,6 @@ export const ArticleCreate = observer(() => {
const [preview, setPreview] = useState("");
const [headingPreview, setHeadingPreview] = useState("");
useEffect(() => {
setPreview(bodyContent ?? "");
}, [bodyContent]);
@ -89,17 +85,13 @@ export const ArticleCreate = observer(() => {
setHeadingPreview(headingContent ?? "");
}, [headingContent]);
const simpleMDEOptions = React.useMemo(
() => ({
placeholder: "Введите контент в формате Markdown...",
spellChecker: false,
}),
[]
);
const simpleMDEOptions = React.useMemo(() => ({
placeholder: "Введите контент в формате Markdown...",
spellChecker: false,
}), []);
return (
<Create isLoading={formLoading} saveButtonProps={{
//...saveButtonProps,
onClick: handleFormSubmit
}}>
<Box sx={{ display: "flex", flex: 1, gap: 2 }}>
@ -119,7 +111,7 @@ export const ArticleCreate = observer(() => {
helperText={(errors as any)?.heading?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
slotProps={{inputLabel: {shrink: true}}}
type="text"
label="Заголовок *"
name="heading"
@ -128,7 +120,7 @@ export const ArticleCreate = observer(() => {
<Controller
control={control}
name="body"
rules={{ required: "Это поле является обязательным" }}
//rules={{ required: "Это поле является обязательным" }}
defaultValue=""
render={({ field: { onChange, value } }) => (
<MemoizedSimpleMDE

View File

@ -7,10 +7,8 @@ import React, { useState, useEffect, useMemo } from "react";
import ReactMarkdown from "react-markdown";
import { useList } from "@refinedev/core";
import { MarkdownEditor } from "../../components/MarkdownEditor";
import { LinkedItems } from "../../components/LinkedItems";
import { MarkdownEditor, LinkedItems } from "@components";
import { MediaItem, mediaFields } from "./types";
import { TOKEN_KEY } from "@providers";
import "easymde/dist/easymde.min.css";
import { EVERY_LANGUAGE, Languages, languageStore, META_LANGUAGE } from "@stores";
import { observer } from "mobx-react-lite";
@ -58,7 +56,7 @@ export const ArticleEdit = observer(() => {
setPreview(articleData.body[language] ?? "");
}, [language, articleData, setValue]);
function updateTranslations() {
function updateTranslations(update: boolean = true) {
const newArticleData = {
...articleData,
heading: {
@ -70,7 +68,7 @@ export const ArticleEdit = observer(() => {
[language]: watch("body") ?? "",
}
}
setArticleData(newArticleData);
if(update) setArticleData(newArticleData);
return newArticleData;
}
@ -80,7 +78,7 @@ export const ArticleEdit = observer(() => {
};
const handleFormSubmit = handleSubmit((values: FieldValues) => {
const newTranslations = updateTranslations();
const newTranslations = updateTranslations(false);
console.log(newTranslations);
return onFinish({
translations: newTranslations
@ -113,7 +111,6 @@ export const ArticleEdit = observer(() => {
>
<Box sx={{ display: "flex", gap: 2 }}>
{/* Форма редактирования */}
{/* Форма создания */}
<Box sx={{ display: "flex", flex: 1, flexDirection: "column", gap: 2 }}>
<LanguageSelector action={handleLanguageChange} />
@ -130,7 +127,7 @@ export const ArticleEdit = observer(() => {
helperText={errors?.heading?.message as string}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
slotProps={{inputLabel: {shrink: true}}}
type="text"
label="Заголовок *"
name="heading"
@ -139,7 +136,7 @@ export const ArticleEdit = observer(() => {
<Controller
control={control}
name="body"
rules={{ required: "Это поле является обязательным" }}
//rules={{ required: "Это поле является обязательным" }}
defaultValue=""
render={({ field: { onChange, value } }) => (
<MemoizedSimpleMDE

View File

@ -7,8 +7,7 @@ import {
ShowButton,
useDataGrid,
} from "@refinedev/mui";
import React, { useEffect } from "react";
import { useDelete } from "@refinedev/core";
import React from "react";
import { localeText } from "../../locales/ru/localeText";
import { observer } from "mobx-react-lite";

View File

@ -62,10 +62,10 @@ export const CarrierCreate = observer(() => {
value={
cityAutocompleteProps.options.find(
(option) => option.id === field.value
) || null
) ?? null
}
onChange={(_, value) => {
field.onChange(value?.id || "");
field.onChange(value?.id ?? "");
}}
getOptionLabel={(item) => {
return item ? item.name : "";
@ -101,7 +101,7 @@ export const CarrierCreate = observer(() => {
helperText={(errors as any)?.full_name?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
slotProps={{inputLabel: {shrink: true}}}
type="text"
label={"Полное имя *"}
name="full_name"
@ -109,82 +109,89 @@ export const CarrierCreate = observer(() => {
<TextField
{...register("short_name", {
required: "Это поле является обязательным",
//required: "Это поле является обязательным",
})}
error={!!(errors as any)?.short_name}
helperText={(errors as any)?.short_name?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
slotProps={{inputLabel: {shrink: true}}}
type="text"
label={"Короткое имя *"}
label={"Короткое имя"}
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
slotProps={{inputLabel: {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
slotProps={{inputLabel: {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
slotProps={{inputLabel: {shrink: true}}}
type="color"
label={"Цвет правого виджета"}
name="right_color"
sx={{
"& input": {
height: "50px",
paddingBlock: "14px",
paddingInline: "14px",
cursor: "pointer",
},
}}
/>
</Box>
<TextField
{...register("slogan", {
@ -194,7 +201,7 @@ export const CarrierCreate = observer(() => {
helperText={(errors as any)?.slogan?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
slotProps={{inputLabel: {shrink: true}}}
type="text"
label={"Слоган"}
name="slogan"
@ -211,10 +218,10 @@ export const CarrierCreate = observer(() => {
value={
mediaAutocompleteProps.options.find(
(option) => option.id === field.value
) || null
) ?? null
}
onChange={(_, value) => {
field.onChange(value?.id || "");
field.onChange(value?.id ?? "");
}}
getOptionLabel={(item) => {
return item ? item.media_name : "";

View File

@ -2,7 +2,7 @@ import { Autocomplete, Box, TextField } from "@mui/material";
import { Edit, useAutocomplete } from "@refinedev/mui";
import { useForm } from "@refinedev/react-hook-form";
import { languageStore, META_LANGUAGE } from "@stores";
import { LanguageSelector, MediaData, MediaView } from "@ui";
import { LanguageSelector, MediaView } from "@ui";
import { observer } from "mobx-react-lite";
import { Controller } from "react-hook-form";
@ -42,6 +42,7 @@ export const CarrierEdit = observer(() => {
...META_LANGUAGE(language)
});
return (
<Edit saveButtonProps={saveButtonProps}>
<Box
@ -61,10 +62,10 @@ export const CarrierEdit = observer(() => {
value={
cityAutocompleteProps.options.find(
(option) => option.id === field.value
) || null
) ?? null
}
onChange={(_, value) => {
field.onChange(value?.id || "");
field.onChange(value?.id ?? "");
}}
getOptionLabel={(item) => {
return item ? item.name : "";
@ -100,7 +101,7 @@ export const CarrierEdit = observer(() => {
helperText={(errors as any)?.full_name?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
slotProps={{inputLabel: {shrink: true}}}
type="text"
label={"Полное имя *"}
name="full_name"
@ -108,21 +109,21 @@ export const CarrierEdit = observer(() => {
<TextField
{...register("short_name", {
required: "Это поле является обязательным",
//required: "Это поле является обязательным",
})}
error={!!(errors as any)?.short_name}
helperText={(errors as any)?.short_name?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
slotProps={{inputLabel: {shrink: true}}}
type="text"
label={"Короткое имя *"}
label={"Короткое имя"}
name="short_name"
/>
<Box component="form"
sx={{ display: "flex" }}
autoComplete="off"
sx={{ display: "flex" }}
autoComplete="off"
>
<TextField
{...register("main_color", {
@ -132,7 +133,7 @@ export const CarrierEdit = observer(() => {
helperText={(errors as any)?.main_color?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
slotProps={{inputLabel: {shrink: true}}}
type="color"
label={"Основной цвет"}
name="main_color"
@ -154,7 +155,7 @@ export const CarrierEdit = observer(() => {
helperText={(errors as any)?.left_color?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
slotProps={{inputLabel: {shrink: true}}}
type="color"
label={"Цвет левого виджета"}
name="left_color"
@ -177,7 +178,7 @@ export const CarrierEdit = observer(() => {
helperText={(errors as any)?.right_color?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
slotProps={{inputLabel: {shrink: true}}}
type="color"
label={"Цвет правого виджета"}
name="right_color"
@ -200,7 +201,7 @@ export const CarrierEdit = observer(() => {
helperText={(errors as any)?.slogan?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
slotProps={{inputLabel: {shrink: true}}}
type="text"
label={"Слоган"}
name="slogan"
@ -217,10 +218,10 @@ export const CarrierEdit = observer(() => {
value={
mediaAutocompleteProps.options.find(
(option) => option.id === field.value
) || null
) ?? null
}
onChange={(_, value) => {
field.onChange(value?.id || "");
field.onChange(value?.id ?? "");
}}
getOptionLabel={(item) => {
return item ? item.media_name : "";

View File

@ -1,7 +1,7 @@
import { useCustom, useApiUrl } from "@refinedev/core";
import { useParams } from "react-router";
import { createContext, ReactNode, useContext, useEffect, useMemo, useState } from "react";
import { RouteData, SightData, StationData, StationPatchData } from "./types";
import { RouteData, SightData, SightPatchData, StationData, StationPatchData } from "./types";
import { axiosInstance } from "../../providers/data";
const MapDataContext = createContext<{
@ -19,6 +19,7 @@ const MapDataContext = createContext<{
setMapRotation: (rotation: number) => void,
setMapCenter: (x: number, y: number) => void,
setStationOffset: (stationId: number, x: number, y: number) => void,
setSightCoordinates: (sightId: number, latitude: number, longitude: number) => void,
saveChanges: () => void,
}>({
originalRouteData: undefined,
@ -35,6 +36,7 @@ const MapDataContext = createContext<{
setMapRotation: () => {},
setMapCenter: () => {},
setStationOffset: () => {},
setSightCoordinates: () => {},
saveChanges: () => {},
});
@ -52,14 +54,13 @@ export function MapDataProvider({ children }: Readonly<{ children: ReactNode }>)
const [routeChanges, setRouteChanges] = useState<RouteData>({} as RouteData);
const [stationChanges, setStationChanges] = useState<StationPatchData[]>([]);
const [sightChanges, setSightChanges] = useState<SightData[]>([]);
const [sightChanges, setSightChanges] = useState<SightPatchData[]>([]);
const { data: routeQuery, isLoading: isRouteLoading } = useCustom({
url: `${apiUrl}/route/${routeId}`,
method: 'get',
});
const { data: stationQuery, isLoading: isStationLoading } = useCustom({
url: `${apiUrl}/route/${routeId}/station`,
method: 'get'
@ -110,17 +111,25 @@ export function MapDataProvider({ children }: Readonly<{ children: ReactNode }>)
async function saveChanges() {
await axiosInstance.patch(`/route/${routeId}`, routeData);
saveStationChanges();
await saveStationChanges();
await saveSightChanges();
}
async function saveStationChanges() {
console.log("saveStationChanges", stationChanges);
for(const station of stationChanges) {
const response = await axiosInstance.patch(`/route/${routeId}/station`, station);
console.log("response", response);
}
}
async function saveSightChanges() {
console.log("sightChanges", sightChanges);
for(const sight of sightChanges) {
const response = await axiosInstance.patch(`/route/${routeId}/sight`, sight);
console.log("response", response);
}
}
function setStationOffset(stationId: number, x: number, y: number) {
setStationChanges((prev) => {
let found = prev.find((station) => station.station_id === stationId);
@ -148,9 +157,36 @@ export function MapDataProvider({ children }: Readonly<{ children: ReactNode }>)
});
}
function setSightCoordinates(sightId: number, latitude: number, longitude: number) {
setSightChanges((prev) => {
let found = prev.find((sight) => sight.sight_id === sightId);
if(found) {
found.latitude = latitude;
found.longitude = longitude;
return prev.map((sight) => {
if(sight.sight_id === sightId) {
return found;
}
return sight;
});
} else {
const foundSight = sightData?.find((sight) => sight.id === sightId);
if(foundSight) {
return [...prev, {
sight_id: sightId,
latitude,
longitude
}];
}
return prev;
}
});
}
useEffect(() => {
console.log("stationChanges", stationChanges);
}, [stationChanges]);
console.log("sightChanges", sightChanges);
}, [sightChanges]);
const value = useMemo(() => ({
originalRouteData: originalRouteData,
@ -167,6 +203,7 @@ export function MapDataProvider({ children }: Readonly<{ children: ReactNode }>)
setMapCenter,
saveChanges,
setStationOffset,
setSightCoordinates
}), [originalRouteData, originalStationData, originalSightData, routeData, stationData, sightData, isRouteLoading, isStationLoading, isSightLoading]);
return (

View File

@ -4,7 +4,8 @@ import { SightData } from "./types";
import { Assets, FederatedMouseEvent, Graphics, Texture } from "pixi.js";
import { COLORS } from "../../contexts/color-mode/theme";
import { SIGHT_SIZE, UP_SCALE } from "./Constants";
import { coordinatesToLocal } from "./utils";
import { coordinatesToLocal, localToCoordinates } from "./utils";
import { useMapData } from "./MapDataContext";
interface SightProps {
sight: SightData;
@ -15,8 +16,9 @@ export function Sight({
sight, id
}: Readonly<SightProps>) {
const { rotation, scale } = useTransform();
const { setSightCoordinates } = useMapData();
const [position, setPosition] = useState({ x: 0, y: 0 });
const [position, setPosition] = useState(coordinatesToLocal(sight.latitude, sight.longitude));
const [isDragging, setIsDragging] = useState(false);
const [startPosition, setStartPosition] = useState({ x: 0, y: 0 });
const [startMousePosition, setStartMousePosition] = useState({ x: 0, y: 0 });
@ -36,8 +38,8 @@ export function Sight({
};
const handlePointerMove = (e: FederatedMouseEvent) => {
if (!isDragging) return;
const dx = (e.globalX - startMousePosition.x) / scale;
const dy = (e.globalY - startMousePosition.y) / scale;
const dx = (e.globalX - startMousePosition.x) / scale / UP_SCALE;
const dy = (e.globalY - startMousePosition.y) / scale / UP_SCALE;
const cos = Math.cos(rotation);
const sin = Math.sin(rotation);
const newPosition = {
@ -45,6 +47,8 @@ export function Sight({
y: startPosition.y - dx * sin + dy * cos
};
setPosition(newPosition);
const coordinates = localToCoordinates(newPosition.x, newPosition.y);
setSightCoordinates(sight.id, coordinates.latitude, coordinates.longitude);
e.stopPropagation();
};
@ -85,8 +89,8 @@ export function Sight({
onGlobalPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
onPointerUpOutside={handlePointerUp}
x={coordinates.x * UP_SCALE - SIGHT_SIZE/2 + position.x} // Offset by half width to center
y={coordinates.y * UP_SCALE - SIGHT_SIZE/2 + position.y} // Offset by half height to center
x={position.x * UP_SCALE - SIGHT_SIZE/2} // Offset by half width to center
y={position.y * UP_SCALE - SIGHT_SIZE/2} // Offset by half height to center
>
<pixiSprite
texture={texture}

View File

@ -62,11 +62,9 @@ export function StationLabel({
if (!isDragging) return;
const dx = (e.globalX - startMousePosition.x);
const dy = (e.globalY - startMousePosition.y);
const cos = Math.cos(rotation);
const sin = Math.sin(rotation);
const newPosition = {
x: startPosition.x + dx * cos + dy * sin,
y: startPosition.y - dx * sin + dy * cos
x: startPosition.x + dx,
y: startPosition.y + dy
};
setPosition(newPosition);
setStationOffset(station.id, newPosition.x, newPosition.y);

View File

@ -47,6 +47,12 @@ export interface StationPatchData {
transfers: StationTransferData;
}
export interface SightPatchData {
sight_id: number;
latitude: number;
longitude: number;
}
export interface SightData {
address: string;
city: string;

View File

@ -19,7 +19,7 @@ export const RouteCreate = () => {
formState: { errors },
} = useForm({
refineCoreProps: {
resource: "route/",
resource: "route",
},
});

View File

@ -1,66 +1,70 @@
import { Autocomplete, Box, TextField, Typography, Paper } from "@mui/material";
import { Create, useAutocomplete } from "@refinedev/mui";
import { useForm } from "@refinedev/react-hook-form";
import { Controller } from "react-hook-form";
import { Controller, FieldValues } from "react-hook-form";
import React, { useState, useEffect } from "react";
import { TOKEN_KEY } from "@providers";
import { observer } from "mobx-react-lite";
import { Languages, languageStore, cityStore } from "@stores";
import { EVERY_LANGUAGE, Languages, languageStore, cityStore } from "@stores";
import { LanguageSelector } from "@ui";
export const SightCreate = observer(() => {
const { language, setLanguageAction } = languageStore;
const [sightData, setSightData] = useState({
ru: {
name: "",
address: "",
},
en: {
name: "",
address: "",
},
zh: {
name: "",
address: "",
},
name: EVERY_LANGUAGE(""),
address: EVERY_LANGUAGE("")
});
// Состояния для предпросмотра
const handleLanguageChange = (lang: Languages) => {
setSightData((prevData) => ({
...prevData,
[language]: {
name: watch("name") ?? "",
address: watch("address") ?? "",
},
}));
setLanguageAction(lang);
};
const {
saveButtonProps,
refineCore: { formLoading },
refineCore: { formLoading, onFinish },
register,
control,
watch,
setValue,
formState: { errors },
handleSubmit,
} = useForm({
refineCoreProps: {
resource: "sight/",
resource: "sight",
},
});
const { city_id } = cityStore;
useEffect(() => {
if (sightData[language as keyof typeof sightData]?.name) {
setValue("name", sightData[language as keyof typeof sightData]?.name);
}
if (sightData[language as keyof typeof sightData]?.address) {
setValue(
"address",
sightData[language as keyof typeof sightData]?.address
);
}
setValue("name", sightData.name[language]);
setValue("address", sightData.address[language]);
}, [sightData, language, setValue]);
function updateTranslations(update: boolean = true) {
const newSightData = {
...sightData,
name: {
...sightData.name,
[language]: watch("name") ?? "",
},
address: {
...sightData.address,
[language]: watch("address") ?? "",
}
}
if(update) setSightData(newSightData);
return newSightData;
}
const handleLanguageChange = (lang: Languages) => {
updateTranslations();
setLanguageAction(lang);
};
const handleFormSubmit = handleSubmit((values: FieldValues) => {
const newTranslations = updateTranslations(false);
console.log(newTranslations);
return onFinish({
...values,
translations: newTranslations
});
});
const [namePreview, setNamePreview] = useState("");
const [coordinatesPreview, setCoordinatesPreview] = useState({
latitude: "",
@ -140,7 +144,7 @@ export const SightCreate = observer(() => {
// Обновляем состояния при изменении полей
useEffect(() => {
setNamePreview(nameContent || "");
setNamePreview(nameContent ?? "");
}, [nameContent]);
useEffect(() => {
@ -154,7 +158,7 @@ export const SightCreate = observer(() => {
const selectedCity = cityAutocompleteProps.options.find(
(option) => option.id === cityContent
);
setCityPreview(selectedCity?.name || "");
setCityPreview(selectedCity?.name ?? "");
}, [cityContent, cityAutocompleteProps.options]);
useEffect(() => {
@ -206,74 +210,25 @@ export const SightCreate = observer(() => {
const selectedLeftArticle = articleAutocompleteProps.options.find(
(option) => option.id === leftArticleContent
);
setLeftArticlePreview(selectedLeftArticle?.heading || "");
setLeftArticlePreview(selectedLeftArticle?.heading ?? "");
}, [leftArticleContent, articleAutocompleteProps.options]);
useEffect(() => {
const selectedPreviewArticle = articleAutocompleteProps.options.find(
(option) => option.id === previewArticleContent
);
setPreviewArticlePreview(selectedPreviewArticle?.heading || "");
setPreviewArticlePreview(selectedPreviewArticle?.heading ?? "");
}, [previewArticleContent, articleAutocompleteProps.options]);
return (
<Create isLoading={formLoading} saveButtonProps={saveButtonProps}>
<Create isLoading={formLoading} saveButtonProps={{
...saveButtonProps,
onClick: handleFormSubmit
}}>
<Box sx={{ display: "flex", gap: 2 }}>
<Box sx={{ display: "flex", flexDirection: "column", flex: 1, 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",
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>
<LanguageSelector action={handleLanguageChange} />
<Box
component="form"
@ -288,7 +243,7 @@ export const SightCreate = observer(() => {
helperText={(errors as any)?.name?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
slotProps={{inputLabel: {shrink: true}}}
type="text"
label={"Название *"}
name="name"
@ -301,7 +256,7 @@ export const SightCreate = observer(() => {
helperText={(errors as any)?.latitude?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
slotProps={{inputLabel: {shrink: true}}}
type="text"
label={"Координаты *"}
/>
@ -324,15 +279,15 @@ export const SightCreate = observer(() => {
<TextField
{...register("address", {
required: "Это поле является обязательным",
//required: "Это поле является обязательным",
})}
error={!!(errors as any)?.address}
helperText={(errors as any)?.address?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
slotProps={{inputLabel: {shrink: true}}}
type="text"
label={"Адрес *"}
label={"Адрес"}
name="address"
/>
@ -346,10 +301,10 @@ export const SightCreate = observer(() => {
value={
cityAutocompleteProps.options.find(
(option) => option.id === field.value
) || null
) ?? null
}
onChange={(_, value) => {
field.onChange(value?.id || "");
field.onChange(value?.id ?? "");
}}
getOptionLabel={(item) => {
return item ? item.name : "";
@ -372,7 +327,6 @@ export const SightCreate = observer(() => {
variant="outlined"
error={!!errors.city_id}
helperText={(errors as any)?.city_id?.message}
required
/>
)}
/>
@ -389,10 +343,10 @@ export const SightCreate = observer(() => {
value={
mediaAutocompleteProps.options.find(
(option) => option.id === field.value
) || null
) ?? null
}
onChange={(_, value) => {
field.onChange(value?.id || "");
field.onChange(value?.id ?? "");
}}
getOptionLabel={(item) => {
return item ? item.media_name : "";
@ -415,7 +369,7 @@ export const SightCreate = observer(() => {
variant="outlined"
error={!!errors.thumbnail}
helperText={(errors as any)?.thumbnail?.message}
required
// required
/>
)}
/>
@ -432,10 +386,10 @@ export const SightCreate = observer(() => {
value={
mediaAutocompleteProps.options.find(
(option) => option.id === field.value
) || null
) ?? null
}
onChange={(_, value) => {
field.onChange(value?.id || "");
field.onChange(value?.id ?? "");
}}
getOptionLabel={(item) => {
return item ? item.media_name : "";
@ -474,10 +428,10 @@ export const SightCreate = observer(() => {
value={
mediaAutocompleteProps.options.find(
(option) => option.id === field.value
) || null
) ?? null
}
onChange={(_, value) => {
field.onChange(value?.id || "");
field.onChange(value?.id ?? "");
}}
getOptionLabel={(item) => {
return item ? item.media_name : "";
@ -516,10 +470,10 @@ export const SightCreate = observer(() => {
value={
articleAutocompleteProps.options.find(
(option) => option.id === field.value
) || null
) ?? null
}
onChange={(_, value) => {
field.onChange(value?.id || "");
field.onChange(value?.id ?? "");
}}
getOptionLabel={(item) => {
return item ? item.heading : "";
@ -558,10 +512,10 @@ export const SightCreate = observer(() => {
value={
articleAutocompleteProps.options.find(
(option) => option.id === field.value
) || null
) ?? null
}
onChange={(_, value) => {
field.onChange(value?.id || "");
field.onChange(value?.id ?? "");
}}
getOptionLabel={(item) => {
return item ? item.heading : "";

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
import React, { useEffect } from "react";
import React from "react";
import { type GridColDef } from "@mui/x-data-grid";
import {
DeleteButton,
@ -8,11 +8,10 @@ import {
useDataGrid,
} from "@refinedev/mui";
import { Stack } from "@mui/material";
import { CustomDataGrid } from "../../components/CustomDataGrid";
import { CustomDataGrid } from "@components";
import { localeText } from "../../locales/ru/localeText";
import { cityStore } from "../../store/CityStore";
import { cityStore, languageStore } from "@stores";
import { observer } from "mobx-react-lite";
import { languageStore } from "../../store/LanguageStore";
export const SightList = observer(() => {
const { city_id } = cityStore;