abstract urls
This commit is contained in:
parent
24a8bcad0a
commit
607012bd47
2
.env
Normal file
2
.env
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
VITE_KRBL_MEDIA = "https://wn.krbl.ru/media/"
|
||||||
|
VITE_KRBL_API = "https://wn.krbl.ru"
|
@ -1,174 +1,182 @@
|
|||||||
import type {AuthProvider} from '@refinedev/core'
|
import type { AuthProvider } from "@refinedev/core";
|
||||||
import axios, {AxiosError} from 'axios'
|
import axios, { AxiosError } from "axios";
|
||||||
import {BACKEND_URL} from './lib/constants'
|
|
||||||
import {jwtDecode} from 'jwt-decode'
|
|
||||||
|
|
||||||
export const TOKEN_KEY = 'refine-auth'
|
import { jwtDecode } from "jwt-decode";
|
||||||
|
|
||||||
|
export const TOKEN_KEY = "refine-auth";
|
||||||
|
|
||||||
interface AuthResponse {
|
interface AuthResponse {
|
||||||
token: string
|
token: string;
|
||||||
user: {
|
user: {
|
||||||
id: number
|
id: number;
|
||||||
name: string
|
name: string;
|
||||||
email: string
|
email: string;
|
||||||
is_admin: boolean
|
is_admin: boolean;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ErrorResponse {
|
interface ErrorResponse {
|
||||||
message: string
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class AuthError extends Error {
|
class AuthError extends Error {
|
||||||
constructor(message: string) {
|
constructor(message: string) {
|
||||||
super(message)
|
super(message);
|
||||||
this.name = 'AuthError'
|
this.name = "AuthError";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface JWTPayload {
|
interface JWTPayload {
|
||||||
user_id: number
|
user_id: number;
|
||||||
email: string
|
email: string;
|
||||||
is_admin: boolean
|
is_admin: boolean;
|
||||||
exp: number
|
exp: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const authProvider: AuthProvider = {
|
export const authProvider: AuthProvider = {
|
||||||
login: async ({email, password}) => {
|
login: async ({ email, password }) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post<AuthResponse>(`${BACKEND_URL}/auth/login`, {
|
const response = await axios.post<AuthResponse>(
|
||||||
|
`${import.meta.env.VITE_KRBL_API}/auth/login`,
|
||||||
|
{
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
})
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const {token, user} = response.data
|
const { token, user } = response.data;
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
localStorage.setItem(TOKEN_KEY, token)
|
localStorage.setItem(TOKEN_KEY, token);
|
||||||
localStorage.setItem('user', JSON.stringify(user))
|
localStorage.setItem("user", JSON.stringify(user));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
redirectTo: '/',
|
redirectTo: "/",
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new AuthError('Неверный email или пароль')
|
throw new AuthError("Неверный email или пароль");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = (error as AxiosError<ErrorResponse>)?.response?.data?.message || 'Неверный email или пароль'
|
const errorMessage =
|
||||||
|
(error as AxiosError<ErrorResponse>)?.response?.data?.message ||
|
||||||
|
"Неверный email или пароль";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: new AuthError(errorMessage),
|
error: new AuthError(errorMessage),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
logout: async () => {
|
logout: async () => {
|
||||||
try {
|
try {
|
||||||
await axios.post(
|
await axios.post(
|
||||||
`${BACKEND_URL}/auth/logout`,
|
`${import.meta.env.VITE_KRBL_API}/auth/logout`,
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${localStorage.getItem(TOKEN_KEY)}`,
|
Authorization: `Bearer ${localStorage.getItem(TOKEN_KEY)}`,
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
)
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при выходе:', error)
|
console.error("Ошибка при выходе:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
localStorage.removeItem(TOKEN_KEY)
|
localStorage.removeItem(TOKEN_KEY);
|
||||||
localStorage.removeItem('user')
|
localStorage.removeItem("user");
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
redirectTo: '/login',
|
redirectTo: "/login",
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
check: async () => {
|
check: async () => {
|
||||||
const token = localStorage.getItem(TOKEN_KEY)
|
const token = localStorage.getItem(TOKEN_KEY);
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return {
|
return {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
redirectTo: '/login',
|
redirectTo: "/login",
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`${BACKEND_URL}/auth/me`, {
|
const response = await axios.get(
|
||||||
|
`${import.meta.env.VITE_KRBL_API}/auth/me`,
|
||||||
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
// Обновляем информацию о пользователе
|
// Обновляем информацию о пользователе
|
||||||
localStorage.setItem('user', JSON.stringify(response.data))
|
localStorage.setItem("user", JSON.stringify(response.data));
|
||||||
return {
|
return {
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
localStorage.removeItem(TOKEN_KEY)
|
localStorage.removeItem(TOKEN_KEY);
|
||||||
localStorage.removeItem('user')
|
localStorage.removeItem("user");
|
||||||
return {
|
return {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
redirectTo: '/login',
|
redirectTo: "/login",
|
||||||
error: new AuthError('Сессия истекла, пожалуйста, войдите снова'),
|
error: new AuthError("Сессия истекла, пожалуйста, войдите снова"),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
redirectTo: '/login',
|
redirectTo: "/login",
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
getPermissions: async () => {
|
getPermissions: async () => {
|
||||||
const token = localStorage.getItem(TOKEN_KEY)
|
const token = localStorage.getItem(TOKEN_KEY);
|
||||||
if (!token) return null
|
if (!token) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const decoded = jwtDecode<JWTPayload>(token)
|
const decoded = jwtDecode<JWTPayload>(token);
|
||||||
if (decoded.is_admin) {
|
if (decoded.is_admin) {
|
||||||
document.body.classList.add('is-admin')
|
document.body.classList.add("is-admin");
|
||||||
} else {
|
} else {
|
||||||
document.body.classList.remove('is-admin')
|
document.body.classList.remove("is-admin");
|
||||||
}
|
}
|
||||||
return decoded.is_admin ? ['admin'] : ['user']
|
return decoded.is_admin ? ["admin"] : ["user"];
|
||||||
} catch {
|
} catch {
|
||||||
document.body.classList.remove('is-admin')
|
document.body.classList.remove("is-admin");
|
||||||
return ['user']
|
return ["user"];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getIdentity: async () => {
|
getIdentity: async () => {
|
||||||
const token = localStorage.getItem(TOKEN_KEY)
|
const token = localStorage.getItem(TOKEN_KEY);
|
||||||
const user = localStorage.getItem('user')
|
const user = localStorage.getItem("user");
|
||||||
|
|
||||||
if (!token || !user) return null
|
if (!token || !user) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const decoded = jwtDecode<JWTPayload>(token)
|
const decoded = jwtDecode<JWTPayload>(token);
|
||||||
const userData = JSON.parse(user)
|
const userData = JSON.parse(user);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...userData,
|
...userData,
|
||||||
is_admin: decoded.is_admin, // всегда используем значение из токена
|
is_admin: decoded.is_admin, // всегда используем значение из токена
|
||||||
}
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: async (error) => {
|
onError: async (error) => {
|
||||||
const status = (error as AxiosError)?.response?.status
|
const status = (error as AxiosError)?.response?.status;
|
||||||
if (status === 401 || status === 403) {
|
if (status === 401 || status === 403) {
|
||||||
localStorage.removeItem(TOKEN_KEY)
|
localStorage.removeItem(TOKEN_KEY);
|
||||||
localStorage.removeItem('user')
|
localStorage.removeItem("user");
|
||||||
return {
|
return {
|
||||||
logout: true,
|
logout: true,
|
||||||
redirectTo: '/login',
|
redirectTo: "/login",
|
||||||
error: new AuthError('Сессия истекла, пожалуйста, войдите снова'),
|
error: new AuthError("Сессия истекла, пожалуйста, войдите снова"),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
return { error };
|
||||||
return {error}
|
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
@ -1,133 +1,170 @@
|
|||||||
import {Typography, Button, Box, Accordion, AccordionSummary, AccordionDetails, useTheme, TextField} from '@mui/material'
|
import {
|
||||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
Typography,
|
||||||
import {axiosInstance} from '../providers/data'
|
Button,
|
||||||
import {BACKEND_URL} from '../lib/constants'
|
Box,
|
||||||
import {useForm, Controller} from 'react-hook-form'
|
Accordion,
|
||||||
import {MarkdownEditor} from './MarkdownEditor'
|
AccordionSummary,
|
||||||
import React, {useState, useCallback} from 'react'
|
AccordionDetails,
|
||||||
import {useDropzone} from 'react-dropzone'
|
useTheme,
|
||||||
import {ALLOWED_IMAGE_TYPES, ALLOWED_VIDEO_TYPES} from '../components/media/MediaFormUtils'
|
TextField,
|
||||||
|
} from "@mui/material";
|
||||||
|
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 { useDropzone } from "react-dropzone";
|
||||||
|
import {
|
||||||
|
ALLOWED_IMAGE_TYPES,
|
||||||
|
ALLOWED_VIDEO_TYPES,
|
||||||
|
} from "../components/media/MediaFormUtils";
|
||||||
|
|
||||||
const MemoizedSimpleMDE = React.memo(MarkdownEditor)
|
const MemoizedSimpleMDE = React.memo(MarkdownEditor);
|
||||||
|
|
||||||
type MediaFile = {
|
type MediaFile = {
|
||||||
file: File
|
file: File;
|
||||||
preview: string
|
preview: string;
|
||||||
uploading: boolean
|
uploading: boolean;
|
||||||
mediaId?: number
|
mediaId?: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
parentId: string | number
|
parentId: string | number;
|
||||||
parentResource: string
|
parentResource: string;
|
||||||
childResource: string
|
childResource: string;
|
||||||
title: string
|
title: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const CreateSightArticle = ({parentId, parentResource, childResource, title}: Props) => {
|
export const CreateSightArticle = ({
|
||||||
const theme = useTheme()
|
parentId,
|
||||||
const [mediaFiles, setMediaFiles] = useState<MediaFile[]>([])
|
parentResource,
|
||||||
|
childResource,
|
||||||
|
title,
|
||||||
|
}: Props) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const [mediaFiles, setMediaFiles] = useState<MediaFile[]>([]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register: registerItem,
|
register: registerItem,
|
||||||
control: controlItem,
|
control: controlItem,
|
||||||
handleSubmit: handleSubmitItem,
|
handleSubmit: handleSubmitItem,
|
||||||
reset: resetItem,
|
reset: resetItem,
|
||||||
formState: {errors: itemErrors},
|
formState: { errors: itemErrors },
|
||||||
} = useForm({
|
} = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
heading: '',
|
heading: "",
|
||||||
body: '',
|
body: "",
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
const simpleMDEOptions = React.useMemo(
|
const simpleMDEOptions = React.useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
placeholder: 'Введите контент в формате Markdown...',
|
placeholder: "Введите контент в формате Markdown...",
|
||||||
spellChecker: false,
|
spellChecker: false,
|
||||||
}),
|
}),
|
||||||
[],
|
[]
|
||||||
)
|
);
|
||||||
|
|
||||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||||
const newFiles = acceptedFiles.map((file) => ({
|
const newFiles = acceptedFiles.map((file) => ({
|
||||||
file,
|
file,
|
||||||
preview: URL.createObjectURL(file),
|
preview: URL.createObjectURL(file),
|
||||||
uploading: false,
|
uploading: false,
|
||||||
}))
|
}));
|
||||||
setMediaFiles((prev) => [...prev, ...newFiles])
|
setMediaFiles((prev) => [...prev, ...newFiles]);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
const {getRootProps, getInputProps, isDragActive} = useDropzone({
|
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||||
onDrop,
|
onDrop,
|
||||||
accept: {
|
accept: {
|
||||||
'image/*': ALLOWED_IMAGE_TYPES,
|
"image/*": ALLOWED_IMAGE_TYPES,
|
||||||
'video/*': ALLOWED_VIDEO_TYPES,
|
"video/*": ALLOWED_VIDEO_TYPES,
|
||||||
},
|
},
|
||||||
multiple: true,
|
multiple: true,
|
||||||
})
|
});
|
||||||
|
|
||||||
const uploadMedia = async (mediaFile: MediaFile) => {
|
const uploadMedia = async (mediaFile: MediaFile) => {
|
||||||
const formData = new FormData()
|
const formData = new FormData();
|
||||||
formData.append('media_name', mediaFile.file.name)
|
formData.append("media_name", mediaFile.file.name);
|
||||||
formData.append('filename', mediaFile.file.name)
|
formData.append("filename", mediaFile.file.name);
|
||||||
formData.append('type', mediaFile.file.type.startsWith('image/') ? '1' : '2')
|
formData.append(
|
||||||
formData.append('file', mediaFile.file)
|
"type",
|
||||||
|
mediaFile.file.type.startsWith("image/") ? "1" : "2"
|
||||||
|
);
|
||||||
|
formData.append("file", mediaFile.file);
|
||||||
|
|
||||||
const response = await axiosInstance.post(`${BACKEND_URL}/media`, formData)
|
const response = await axiosInstance.post(
|
||||||
return response.data.id
|
`${import.meta.env.VITE_KRBL_API}/media`,
|
||||||
}
|
formData
|
||||||
|
);
|
||||||
|
return response.data.id;
|
||||||
|
};
|
||||||
|
|
||||||
const handleCreate = async (data: {heading: string; body: string}) => {
|
const handleCreate = async (data: { heading: string; body: string }) => {
|
||||||
try {
|
try {
|
||||||
// Создаем статью
|
// Создаем статью
|
||||||
const response = await axiosInstance.post(`${BACKEND_URL}/${childResource}`, data)
|
const response = await axiosInstance.post(
|
||||||
const itemId = response.data.id
|
`${import.meta.env.VITE_KRBL_API}/${childResource}`,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
const itemId = response.data.id;
|
||||||
|
|
||||||
// Получаем существующие статьи для определения порядкового номера
|
// Получаем существующие статьи для определения порядкового номера
|
||||||
const existingItemsResponse = await axiosInstance.get(`${BACKEND_URL}/${parentResource}/${parentId}/${childResource}`)
|
const existingItemsResponse = await axiosInstance.get(
|
||||||
const existingItems = existingItemsResponse.data || []
|
`${
|
||||||
const nextPageNum = existingItems.length + 1
|
import.meta.env.VITE_KRBL_API
|
||||||
|
}/${parentResource}/${parentId}/${childResource}`
|
||||||
|
);
|
||||||
|
const existingItems = existingItemsResponse.data || [];
|
||||||
|
const nextPageNum = existingItems.length + 1;
|
||||||
|
|
||||||
// Привязываем статью к достопримечательности
|
// Привязываем статью к достопримечательности
|
||||||
await axiosInstance.post(`${BACKEND_URL}/${parentResource}/${parentId}/${childResource}/`, {
|
await axiosInstance.post(
|
||||||
|
`${
|
||||||
|
import.meta.env.VITE_KRBL_API
|
||||||
|
}/${parentResource}/${parentId}/${childResource}/`,
|
||||||
|
{
|
||||||
[`${childResource}_id`]: itemId,
|
[`${childResource}_id`]: itemId,
|
||||||
page_num: nextPageNum,
|
page_num: nextPageNum,
|
||||||
})
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Загружаем все медиа файлы и получаем их ID
|
// Загружаем все медиа файлы и получаем их ID
|
||||||
const mediaIds = await Promise.all(
|
const mediaIds = await Promise.all(
|
||||||
mediaFiles.map(async (mediaFile) => {
|
mediaFiles.map(async (mediaFile) => {
|
||||||
return await uploadMedia(mediaFile)
|
return await uploadMedia(mediaFile);
|
||||||
}),
|
})
|
||||||
)
|
);
|
||||||
|
|
||||||
// Привязываем все медиа к статье
|
// Привязываем все медиа к статье
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
mediaIds.map((mediaId, index) =>
|
mediaIds.map((mediaId, index) =>
|
||||||
axiosInstance.post(`${BACKEND_URL}/article/${itemId}/media/`, {
|
axiosInstance.post(
|
||||||
|
`${import.meta.env.VITE_KRBL_API}/article/${itemId}/media/`,
|
||||||
|
{
|
||||||
media_id: mediaId,
|
media_id: mediaId,
|
||||||
media_order: index + 1,
|
media_order: index + 1,
|
||||||
}),
|
}
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
resetItem()
|
resetItem();
|
||||||
setMediaFiles([])
|
setMediaFiles([]);
|
||||||
window.location.reload()
|
window.location.reload();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Error creating item:', err)
|
console.error("Error creating item:", err);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const removeMedia = (index: number) => {
|
const removeMedia = (index: number) => {
|
||||||
setMediaFiles((prev) => {
|
setMediaFiles((prev) => {
|
||||||
const newFiles = [...prev]
|
const newFiles = [...prev];
|
||||||
URL.revokeObjectURL(newFiles[index].preview)
|
URL.revokeObjectURL(newFiles[index].preview);
|
||||||
newFiles.splice(index, 1)
|
newFiles.splice(index, 1);
|
||||||
return newFiles
|
return newFiles;
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Accordion>
|
<Accordion>
|
||||||
@ -143,76 +180,95 @@ export const CreateSightArticle = ({parentId, parentResource, childResource, tit
|
|||||||
Создать {title}
|
Создать {title}
|
||||||
</Typography>
|
</Typography>
|
||||||
</AccordionSummary>
|
</AccordionSummary>
|
||||||
<AccordionDetails sx={{background: theme.palette.background.paper}}>
|
<AccordionDetails sx={{ background: theme.palette.background.paper }}>
|
||||||
<Box component="form" onSubmit={handleSubmitItem(handleCreate)}>
|
<Box component="form" onSubmit={handleSubmitItem(handleCreate)}>
|
||||||
<TextField
|
<TextField
|
||||||
{...registerItem('heading', {
|
{...registerItem("heading", {
|
||||||
required: 'Это поле является обязательным',
|
required: "Это поле является обязательным",
|
||||||
})}
|
})}
|
||||||
error={!!(itemErrors as any)?.heading}
|
error={!!(itemErrors as any)?.heading}
|
||||||
helperText={(itemErrors as any)?.heading?.message}
|
helperText={(itemErrors as any)?.heading?.message}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{shrink: true}}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="text"
|
type="text"
|
||||||
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 для медиа файлов */}
|
{/* Dropzone для медиа файлов */}
|
||||||
<Box sx={{mt: 2, mb: 2}}>
|
<Box sx={{ mt: 2, mb: 2 }}>
|
||||||
<Box
|
<Box
|
||||||
{...getRootProps()}
|
{...getRootProps()}
|
||||||
sx={{
|
sx={{
|
||||||
border: '2px dashed',
|
border: "2px dashed",
|
||||||
borderColor: isDragActive ? 'primary.main' : 'grey.300',
|
borderColor: isDragActive ? "primary.main" : "grey.300",
|
||||||
borderRadius: 1,
|
borderRadius: 1,
|
||||||
p: 2,
|
p: 2,
|
||||||
textAlign: 'center',
|
textAlign: "center",
|
||||||
cursor: 'pointer',
|
cursor: "pointer",
|
||||||
'&:hover': {
|
"&:hover": {
|
||||||
borderColor: 'primary.main',
|
borderColor: "primary.main",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<input {...getInputProps()} />
|
<input {...getInputProps()} />
|
||||||
<Typography>{isDragActive ? 'Перетащите файлы сюда...' : 'Перетащите файлы сюда или кликните для выбора'}</Typography>
|
<Typography>
|
||||||
|
{isDragActive
|
||||||
|
? "Перетащите файлы сюда..."
|
||||||
|
: "Перетащите файлы сюда или кликните для выбора"}
|
||||||
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Превью загруженных файлов */}
|
{/* Превью загруженных файлов */}
|
||||||
<Box sx={{mt: 2, display: 'flex', flexWrap: 'wrap', gap: 1}}>
|
<Box sx={{ mt: 2, display: "flex", flexWrap: "wrap", gap: 1 }}>
|
||||||
{mediaFiles.map((mediaFile, index) => (
|
{mediaFiles.map((mediaFile, index) => (
|
||||||
<Box
|
<Box
|
||||||
key={mediaFile.preview}
|
key={mediaFile.preview}
|
||||||
sx={{
|
sx={{
|
||||||
position: 'relative',
|
position: "relative",
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 100,
|
height: 100,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{mediaFile.file.type.startsWith('image/') ? (
|
{mediaFile.file.type.startsWith("image/") ? (
|
||||||
<img
|
<img
|
||||||
src={mediaFile.preview}
|
src={mediaFile.preview}
|
||||||
alt={mediaFile.file.name}
|
alt={mediaFile.file.name}
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: "100%",
|
||||||
height: '100%',
|
height: "100%",
|
||||||
objectFit: 'cover',
|
objectFit: "cover",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
width: '100%',
|
width: "100%",
|
||||||
height: '100%',
|
height: "100%",
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
alignItems: 'center',
|
alignItems: "center",
|
||||||
justifyContent: 'center',
|
justifyContent: "center",
|
||||||
bgcolor: 'grey.200',
|
bgcolor: "grey.200",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="caption">{mediaFile.file.name}</Typography>
|
<Typography variant="caption">
|
||||||
|
{mediaFile.file.name}
|
||||||
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
@ -220,10 +276,10 @@ export const CreateSightArticle = ({parentId, parentResource, childResource, tit
|
|||||||
color="error"
|
color="error"
|
||||||
onClick={() => removeMedia(index)}
|
onClick={() => removeMedia(index)}
|
||||||
sx={{
|
sx={{
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
top: 0,
|
top: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
minWidth: 'auto',
|
minWidth: "auto",
|
||||||
width: 20,
|
width: 20,
|
||||||
height: 20,
|
height: 20,
|
||||||
p: 0,
|
p: 0,
|
||||||
@ -236,16 +292,16 @@ export const CreateSightArticle = ({parentId, parentResource, childResource, tit
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box sx={{mt: 2, display: 'flex', gap: 2}}>
|
<Box sx={{ mt: 2, display: "flex", gap: 2 }}>
|
||||||
<Button variant="contained" color="primary" type="submit">
|
<Button variant="contained" color="primary" type="submit">
|
||||||
Создать
|
Создать
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
resetItem()
|
resetItem();
|
||||||
mediaFiles.forEach((file) => URL.revokeObjectURL(file.preview))
|
mediaFiles.forEach((file) => URL.revokeObjectURL(file.preview));
|
||||||
setMediaFiles([])
|
setMediaFiles([]);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Очистить
|
Очистить
|
||||||
@ -254,5 +310,5 @@ export const CreateSightArticle = ({parentId, parentResource, childResource, tit
|
|||||||
</Box>
|
</Box>
|
||||||
</AccordionDetails>
|
</AccordionDetails>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
@ -1,127 +1,169 @@
|
|||||||
import {useState, useEffect} from 'react'
|
import { useState, useEffect } from "react";
|
||||||
import {Stack, Typography, Button, FormControl, Grid, Box, Accordion, AccordionSummary, AccordionDetails, useTheme, TextField, Autocomplete} from '@mui/material'
|
import {
|
||||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
Stack,
|
||||||
import {axiosInstance} from '../providers/data'
|
Typography,
|
||||||
import {BACKEND_URL} from '../lib/constants'
|
Button,
|
||||||
import {Link} from 'react-router'
|
FormControl,
|
||||||
import {TOKEN_KEY} from '../authProvider'
|
Grid,
|
||||||
|
Box,
|
||||||
|
Accordion,
|
||||||
|
AccordionSummary,
|
||||||
|
AccordionDetails,
|
||||||
|
useTheme,
|
||||||
|
TextField,
|
||||||
|
Autocomplete,
|
||||||
|
} from "@mui/material";
|
||||||
|
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||||
|
import { axiosInstance } from "../providers/data";
|
||||||
|
|
||||||
|
import { Link } from "react-router";
|
||||||
|
import { TOKEN_KEY } from "../authProvider";
|
||||||
|
|
||||||
type Field<T> = {
|
type Field<T> = {
|
||||||
label: string
|
label: string;
|
||||||
data: keyof T
|
data: keyof T;
|
||||||
render?: (value: any) => React.ReactNode
|
render?: (value: any) => React.ReactNode;
|
||||||
}
|
};
|
||||||
|
|
||||||
type ExtraFieldConfig = {
|
type ExtraFieldConfig = {
|
||||||
type: 'number'
|
type: "number";
|
||||||
label: string
|
label: string;
|
||||||
minValue: number
|
minValue: number;
|
||||||
maxValue: (linkedItems: any[]) => number
|
maxValue: (linkedItems: any[]) => number;
|
||||||
}
|
};
|
||||||
|
|
||||||
type LinkedItemsProps<T> = {
|
type LinkedItemsProps<T> = {
|
||||||
parentId: string | number
|
parentId: string | number;
|
||||||
parentResource: string
|
parentResource: string;
|
||||||
childResource: string
|
childResource: string;
|
||||||
fields: Field<T>[]
|
fields: Field<T>[];
|
||||||
title: string
|
title: string;
|
||||||
type: 'show' | 'edit'
|
type: "show" | "edit";
|
||||||
extraField?: ExtraFieldConfig
|
extraField?: ExtraFieldConfig;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const LinkedItems = <T extends {id: number; [key: string]: any}>({parentId, parentResource, childResource, fields, title, type}: LinkedItemsProps<T>) => {
|
export const LinkedItems = <T extends { id: number; [key: string]: any }>({
|
||||||
const [items, setItems] = useState<T[]>([])
|
parentId,
|
||||||
const [linkedItems, setLinkedItems] = useState<T[]>([])
|
parentResource,
|
||||||
const [selectedItemId, setSelectedItemId] = useState<number | null>(null)
|
childResource,
|
||||||
const [pageNum, setPageNum] = useState<number>(1)
|
fields,
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true)
|
title,
|
||||||
const [mediaOrder, setMediaOrder] = useState<number>(1)
|
type,
|
||||||
const theme = useTheme()
|
}: LinkedItemsProps<T>) => {
|
||||||
|
const [items, setItems] = useState<T[]>([]);
|
||||||
|
const [linkedItems, setLinkedItems] = useState<T[]>([]);
|
||||||
|
const [selectedItemId, setSelectedItemId] = useState<number | null>(null);
|
||||||
|
const [pageNum, setPageNum] = useState<number>(1);
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
|
const [mediaOrder, setMediaOrder] = useState<number>(1);
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (parentId) {
|
if (parentId) {
|
||||||
axiosInstance
|
axiosInstance
|
||||||
.get(`${BACKEND_URL}/${parentResource}/${parentId}/${childResource}`)
|
.get(
|
||||||
|
`${
|
||||||
|
import.meta.env.VITE_KRBL_API
|
||||||
|
}/${parentResource}/${parentId}/${childResource}`
|
||||||
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
setLinkedItems(response?.data || [])
|
setLinkedItems(response?.data || []);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setLinkedItems([])
|
setLinkedItems([]);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}, [parentId, parentResource, childResource])
|
}, [parentId, parentResource, childResource]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (type === 'edit') {
|
if (type === "edit") {
|
||||||
axiosInstance
|
axiosInstance
|
||||||
.get(`${BACKEND_URL}/${childResource}/`)
|
.get(`${import.meta.env.VITE_KRBL_API}/${childResource}/`)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
setItems(response?.data || [])
|
setItems(response?.data || []);
|
||||||
setIsLoading(false)
|
setIsLoading(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setItems([])
|
setItems([]);
|
||||||
setIsLoading(false)
|
setIsLoading(false);
|
||||||
})
|
});
|
||||||
} else {
|
} else {
|
||||||
setIsLoading(false)
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [childResource, type])
|
}, [childResource, type]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (childResource === 'article' && parentResource === 'sight') {
|
if (childResource === "article" && parentResource === "sight") {
|
||||||
setPageNum(linkedItems.length + 1)
|
setPageNum(linkedItems.length + 1);
|
||||||
}
|
}
|
||||||
}, [linkedItems, childResource, parentResource])
|
}, [linkedItems, childResource, parentResource]);
|
||||||
|
|
||||||
const availableItems = items.filter((item) => !linkedItems.some((linked) => linked.id === item.id))
|
const availableItems = items.filter(
|
||||||
|
(item) => !linkedItems.some((linked) => linked.id === item.id)
|
||||||
|
);
|
||||||
|
|
||||||
const linkItem = () => {
|
const linkItem = () => {
|
||||||
if (selectedItemId !== null) {
|
if (selectedItemId !== null) {
|
||||||
const requestData =
|
const requestData =
|
||||||
childResource === 'article'
|
childResource === "article"
|
||||||
? {
|
? {
|
||||||
[`${childResource}_id`]: selectedItemId,
|
[`${childResource}_id`]: selectedItemId,
|
||||||
page_num: pageNum,
|
page_num: pageNum,
|
||||||
}
|
}
|
||||||
: childResource === 'media'
|
: childResource === "media"
|
||||||
? {
|
? {
|
||||||
[`${childResource}_id`]: selectedItemId,
|
[`${childResource}_id`]: selectedItemId,
|
||||||
media_order: mediaOrder,
|
media_order: mediaOrder,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
[`${childResource}_id`]: selectedItemId,
|
[`${childResource}_id`]: selectedItemId,
|
||||||
}
|
};
|
||||||
|
|
||||||
axiosInstance
|
axiosInstance
|
||||||
.post(`${BACKEND_URL}/${parentResource}/${parentId}/${childResource}`, requestData)
|
.post(
|
||||||
|
`${
|
||||||
|
import.meta.env.VITE_KRBL_API
|
||||||
|
}/${parentResource}/${parentId}/${childResource}`,
|
||||||
|
requestData
|
||||||
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
axiosInstance.get(`${BACKEND_URL}/${parentResource}/${parentId}/${childResource}`).then((response) => {
|
axiosInstance
|
||||||
setLinkedItems(response?.data || [])
|
.get(
|
||||||
setSelectedItemId(null)
|
`${
|
||||||
if (childResource === 'article') {
|
import.meta.env.VITE_KRBL_API
|
||||||
setPageNum(pageNum + 1)
|
}/${parentResource}/${parentId}/${childResource}`
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
setLinkedItems(response?.data || []);
|
||||||
|
setSelectedItemId(null);
|
||||||
|
if (childResource === "article") {
|
||||||
|
setPageNum(pageNum + 1);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Error linking item:', error)
|
console.error("Error linking item:", error);
|
||||||
})
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const deleteItem = (itemId: number) => {
|
const deleteItem = (itemId: number) => {
|
||||||
axiosInstance
|
axiosInstance
|
||||||
.delete(`${BACKEND_URL}/${parentResource}/${parentId}/${childResource}`, {
|
.delete(
|
||||||
data: {[`${childResource}_id`]: itemId},
|
`${
|
||||||
})
|
import.meta.env.VITE_KRBL_API
|
||||||
|
}/${parentResource}/${parentId}/${childResource}`,
|
||||||
|
{
|
||||||
|
data: { [`${childResource}_id`]: itemId },
|
||||||
|
}
|
||||||
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setLinkedItems((prev) => prev.filter((item) => item.id !== itemId))
|
setLinkedItems((prev) => prev.filter((item) => item.id !== itemId));
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Error unlinking item:', error)
|
console.error("Error unlinking item:", error);
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Accordion>
|
<Accordion>
|
||||||
@ -137,7 +179,7 @@ export const LinkedItems = <T extends {id: number; [key: string]: any}>({parentI
|
|||||||
</Typography>
|
</Typography>
|
||||||
</AccordionSummary>
|
</AccordionSummary>
|
||||||
|
|
||||||
<AccordionDetails sx={{background: theme.palette.background.paper}}>
|
<AccordionDetails sx={{ background: theme.palette.background.paper }}>
|
||||||
<Stack gap={2}>
|
<Stack gap={2}>
|
||||||
<Grid container gap={1.25}>
|
<Grid container gap={1.25}>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
@ -149,48 +191,55 @@ export const LinkedItems = <T extends {id: number; [key: string]: any}>({parentI
|
|||||||
to={`/${childResource}/show/${item.id}`}
|
to={`/${childResource}/show/${item.id}`}
|
||||||
key={index}
|
key={index}
|
||||||
sx={{
|
sx={{
|
||||||
marginTop: '8px',
|
marginTop: "8px",
|
||||||
padding: '14px',
|
padding: "14px",
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
border: `2px solid ${theme.palette.divider}`,
|
border: `2px solid ${theme.palette.divider}`,
|
||||||
width: childResource === 'article' ? '100%' : 'auto',
|
width: childResource === "article" ? "100%" : "auto",
|
||||||
textDecoration: 'none',
|
textDecoration: "none",
|
||||||
color: 'inherit',
|
color: "inherit",
|
||||||
display: 'block',
|
display: "block",
|
||||||
'&:hover': {
|
"&:hover": {
|
||||||
backgroundColor: theme.palette.action.hover,
|
backgroundColor: theme.palette.action.hover,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack gap={0.25}>
|
<Stack gap={0.25}>
|
||||||
{childResource === 'media' && item.id && (
|
{childResource === "media" && item.id && (
|
||||||
<img
|
<img
|
||||||
src={`https://wn.krbl.ru/media/${item.id}/download?token=${localStorage.getItem(TOKEN_KEY)}`}
|
src={`${import.meta.env.VITE_KRBL_MEDIA}/${
|
||||||
|
item.id
|
||||||
|
}/download?token=${localStorage.getItem(TOKEN_KEY)}`}
|
||||||
alt={String(item.media_name)}
|
alt={String(item.media_name)}
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: "100%",
|
||||||
height: '120px',
|
height: "120px",
|
||||||
objectFit: 'contain',
|
objectFit: "contain",
|
||||||
marginBottom: '8px',
|
marginBottom: "8px",
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{fields.map(({label, data, render}) => (
|
{fields.map(({ label, data, render }) => (
|
||||||
<Typography variant="body2" color="textSecondary" key={String(data)}>
|
<Typography
|
||||||
<strong>{label}:</strong> {render ? render(item[data]) : item[data]}
|
variant="body2"
|
||||||
|
color="textSecondary"
|
||||||
|
key={String(data)}
|
||||||
|
>
|
||||||
|
<strong>{label}:</strong>{" "}
|
||||||
|
{render ? render(item[data]) : item[data]}
|
||||||
</Typography>
|
</Typography>
|
||||||
))}
|
))}
|
||||||
{type === 'edit' && (
|
{type === "edit" && (
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="error"
|
color="error"
|
||||||
size="small"
|
size="small"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault()
|
e.preventDefault();
|
||||||
deleteItem(item.id)
|
deleteItem(item.id);
|
||||||
}}
|
}}
|
||||||
sx={{mt: 1.5}}
|
sx={{ mt: 1.5 }}
|
||||||
>
|
>
|
||||||
Отвязать
|
Отвязать
|
||||||
</Button>
|
</Button>
|
||||||
@ -203,31 +252,48 @@ export const LinkedItems = <T extends {id: number; [key: string]: any}>({parentI
|
|||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{type === 'edit' && (
|
{type === "edit" && (
|
||||||
<Stack gap={2}>
|
<Stack gap={2}>
|
||||||
<Typography variant="subtitle1">Добавить {title}</Typography>
|
<Typography variant="subtitle1">Добавить {title}</Typography>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
fullWidth
|
fullWidth
|
||||||
value={availableItems.find((item) => item.id === selectedItemId) || null}
|
value={
|
||||||
onChange={(_, newValue) => setSelectedItemId(newValue?.id || null)}
|
availableItems.find((item) => item.id === selectedItemId) ||
|
||||||
|
null
|
||||||
|
}
|
||||||
|
onChange={(_, newValue) =>
|
||||||
|
setSelectedItemId(newValue?.id || null)
|
||||||
|
}
|
||||||
options={availableItems}
|
options={availableItems}
|
||||||
getOptionLabel={(item) => String(item[fields[0].data])}
|
getOptionLabel={(item) => String(item[fields[0].data])}
|
||||||
renderInput={(params) => <TextField {...params} label={`Выберите ${title}`} fullWidth />}
|
renderInput={(params) => (
|
||||||
isOptionEqualToValue={(option, value) => option.id === value?.id}
|
<TextField
|
||||||
filterOptions={(options, {inputValue}) => {
|
{...params}
|
||||||
|
label={`Выберите ${title}`}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
isOptionEqualToValue={(option, value) =>
|
||||||
|
option.id === value?.id
|
||||||
|
}
|
||||||
|
filterOptions={(options, { inputValue }) => {
|
||||||
// return options.filter((option) => String(option[fields[0].data]).toLowerCase().includes(inputValue.toLowerCase()))
|
// return options.filter((option) => String(option[fields[0].data]).toLowerCase().includes(inputValue.toLowerCase()))
|
||||||
const searchWords = inputValue
|
const searchWords = inputValue
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.split(' ')
|
.split(" ")
|
||||||
.filter((word) => word.length > 0)
|
.filter((word) => word.length > 0);
|
||||||
return options.filter((option) => {
|
return options.filter((option) => {
|
||||||
const optionWords = String(option[fields[0].data]).toLowerCase().split(' ')
|
const optionWords = String(option[fields[0].data])
|
||||||
return searchWords.every((searchWord) => optionWords.some((word) => word.startsWith(searchWord)))
|
.toLowerCase()
|
||||||
})
|
.split(" ");
|
||||||
|
return searchWords.every((searchWord) =>
|
||||||
|
optionWords.some((word) => word.startsWith(searchWord))
|
||||||
|
);
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{childResource === 'article' && (
|
{childResource === "article" && (
|
||||||
<FormControl fullWidth>
|
<FormControl fullWidth>
|
||||||
<TextField
|
<TextField
|
||||||
type="number"
|
type="number"
|
||||||
@ -235,35 +301,39 @@ export const LinkedItems = <T extends {id: number; [key: string]: any}>({parentI
|
|||||||
name="page_num"
|
name="page_num"
|
||||||
value={pageNum}
|
value={pageNum}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newValue = Number(e.target.value)
|
const newValue = Number(e.target.value);
|
||||||
const minValue = linkedItems.length + 1 // page number on articles lenght
|
const minValue = linkedItems.length + 1; // page number on articles lenght
|
||||||
setPageNum(newValue < minValue ? minValue : newValue)
|
setPageNum(newValue < minValue ? minValue : newValue);
|
||||||
}}
|
}}
|
||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{shrink: true}}
|
InputLabelProps={{ shrink: true }}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{childResource === 'media' && type === 'edit' && (
|
{childResource === "media" && type === "edit" && (
|
||||||
<FormControl fullWidth>
|
<FormControl fullWidth>
|
||||||
<TextField
|
<TextField
|
||||||
type="number"
|
type="number"
|
||||||
label="Порядок отображения медиа"
|
label="Порядок отображения медиа"
|
||||||
value={mediaOrder}
|
value={mediaOrder}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newValue = Number(e.target.value)
|
const newValue = Number(e.target.value);
|
||||||
const maxValue = linkedItems.length + 1
|
const maxValue = linkedItems.length + 1;
|
||||||
const value = Math.max(1, Math.min(newValue, maxValue))
|
const value = Math.max(1, Math.min(newValue, maxValue));
|
||||||
setMediaOrder(value)
|
setMediaOrder(value);
|
||||||
}}
|
}}
|
||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{shrink: true}}
|
InputLabelProps={{ shrink: true }}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button variant="contained" onClick={linkItem} disabled={!selectedItemId}>
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={linkItem}
|
||||||
|
disabled={!selectedItemId}
|
||||||
|
>
|
||||||
Добавить
|
Добавить
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
@ -271,5 +341,5 @@ export const LinkedItems = <T extends {id: number; [key: string]: any}>({parentI
|
|||||||
</Stack>
|
</Stack>
|
||||||
</AccordionDetails>
|
</AccordionDetails>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
export const BACKEND_URL = 'https://wn.krbl.ru'
|
|
||||||
|
|
||||||
export const MEDIA_TYPES = [
|
export const MEDIA_TYPES = [
|
||||||
{label: 'Фото', value: 1},
|
{ label: "Фото", value: 1 },
|
||||||
{label: 'Видео', value: 2},
|
{ label: "Видео", value: 2 },
|
||||||
]
|
];
|
||||||
|
|
||||||
export const VEHICLE_TYPES = [
|
export const VEHICLE_TYPES = [
|
||||||
{label: 'Трамвай', value: 1},
|
{ label: "Трамвай", value: 1 },
|
||||||
{label: 'Троллейбус', value: 2},
|
{ label: "Троллейбус", value: 2 },
|
||||||
]
|
];
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
import {Box, TextField, Typography, Paper} from '@mui/material'
|
import { Box, TextField, Typography, Paper } from "@mui/material";
|
||||||
import {Edit} from '@refinedev/mui'
|
import { Edit } from "@refinedev/mui";
|
||||||
import {useForm} from '@refinedev/react-hook-form'
|
import { useForm } from "@refinedev/react-hook-form";
|
||||||
import {Controller} from 'react-hook-form'
|
import { Controller } from "react-hook-form";
|
||||||
import {useParams} from 'react-router'
|
import { useParams } from "react-router";
|
||||||
import React, {useState, useEffect} from 'react'
|
import React, { useState, useEffect } from "react";
|
||||||
import ReactMarkdown from 'react-markdown'
|
import ReactMarkdown from "react-markdown";
|
||||||
import {useList} from '@refinedev/core'
|
import { useList } from "@refinedev/core";
|
||||||
|
|
||||||
import {MarkdownEditor} from '../../components/MarkdownEditor'
|
import { MarkdownEditor } from "../../components/MarkdownEditor";
|
||||||
import {LinkedItems} from '../../components/LinkedItems'
|
import { LinkedItems } from "../../components/LinkedItems";
|
||||||
import {MediaItem, mediaFields} from './types'
|
import { MediaItem, mediaFields } from "./types";
|
||||||
import {TOKEN_KEY} from '../../authProvider'
|
import { TOKEN_KEY } from "../../authProvider";
|
||||||
import 'easymde/dist/easymde.min.css'
|
import "easymde/dist/easymde.min.css";
|
||||||
|
|
||||||
const MemoizedSimpleMDE = React.memo(MarkdownEditor)
|
const MemoizedSimpleMDE = React.memo(MarkdownEditor);
|
||||||
|
|
||||||
export const ArticleEdit = () => {
|
export const ArticleEdit = () => {
|
||||||
const {
|
const {
|
||||||
@ -21,55 +21,59 @@ export const ArticleEdit = () => {
|
|||||||
register,
|
register,
|
||||||
control,
|
control,
|
||||||
watch,
|
watch,
|
||||||
formState: {errors},
|
formState: { errors },
|
||||||
} = useForm()
|
} = useForm();
|
||||||
|
|
||||||
const {id: articleId} = useParams<{id: string}>()
|
const { id: articleId } = useParams<{ id: string }>();
|
||||||
const [preview, setPreview] = useState('')
|
const [preview, setPreview] = useState("");
|
||||||
const [headingPreview, setHeadingPreview] = useState('')
|
const [headingPreview, setHeadingPreview] = useState("");
|
||||||
|
|
||||||
// Получаем привязанные медиа
|
// Получаем привязанные медиа
|
||||||
const {data: mediaData} = useList<MediaItem>({
|
const { data: mediaData } = useList<MediaItem>({
|
||||||
resource: `article/${articleId}/media`,
|
resource: `article/${articleId}/media`,
|
||||||
queryOptions: {
|
queryOptions: {
|
||||||
enabled: !!articleId,
|
enabled: !!articleId,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
// Следим за изменениями в полях body и heading
|
// Следим за изменениями в полях body и heading
|
||||||
const bodyContent = watch('body')
|
const bodyContent = watch("body");
|
||||||
const headingContent = watch('heading')
|
const headingContent = watch("heading");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPreview(bodyContent || '')
|
setPreview(bodyContent || "");
|
||||||
}, [bodyContent])
|
}, [bodyContent]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setHeadingPreview(headingContent || '')
|
setHeadingPreview(headingContent || "");
|
||||||
}, [headingContent])
|
}, [headingContent]);
|
||||||
|
|
||||||
const simpleMDEOptions = React.useMemo(
|
const simpleMDEOptions = React.useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
placeholder: 'Введите контент в формате Markdown...',
|
placeholder: "Введите контент в формате Markdown...",
|
||||||
spellChecker: false,
|
spellChecker: false,
|
||||||
}),
|
}),
|
||||||
[],
|
[]
|
||||||
)
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Edit saveButtonProps={saveButtonProps}>
|
<Edit saveButtonProps={saveButtonProps}>
|
||||||
<Box sx={{display: 'flex', gap: 2}}>
|
<Box sx={{ display: "flex", gap: 2 }}>
|
||||||
{/* Форма редактирования */}
|
{/* Форма редактирования */}
|
||||||
<Box component="form" sx={{flex: 1, display: 'flex', flexDirection: 'column'}} autoComplete="off">
|
<Box
|
||||||
|
component="form"
|
||||||
|
sx={{ flex: 1, display: "flex", flexDirection: "column" }}
|
||||||
|
autoComplete="off"
|
||||||
|
>
|
||||||
<TextField
|
<TextField
|
||||||
{...register('heading', {
|
{...register("heading", {
|
||||||
required: 'Это поле является обязательным',
|
required: "Это поле является обязательным",
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.heading}
|
error={!!(errors as any)?.heading}
|
||||||
helperText={(errors as any)?.heading?.message}
|
helperText={(errors as any)?.heading?.message}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{shrink: true}}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="text"
|
type="text"
|
||||||
label="Заголовок *"
|
label="Заголовок *"
|
||||||
name="heading"
|
name="heading"
|
||||||
@ -78,9 +82,9 @@ export const ArticleEdit = () => {
|
|||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="body"
|
name="body"
|
||||||
rules={{required: 'Это поле является обязательным'}}
|
rules={{ required: "Это поле является обязательным" }}
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
render={({field: {onChange, value}}) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<MemoizedSimpleMDE
|
<MemoizedSimpleMDE
|
||||||
value={value} // markdown
|
value={value} // markdown
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
@ -90,7 +94,16 @@ export const ArticleEdit = () => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{articleId && <LinkedItems<MediaItem> type="edit" parentId={articleId} parentResource="article" childResource="media" fields={mediaFields} title="медиа" />}
|
{articleId && (
|
||||||
|
<LinkedItems<MediaItem>
|
||||||
|
type="edit"
|
||||||
|
parentId={articleId}
|
||||||
|
parentResource="article"
|
||||||
|
childResource="media"
|
||||||
|
fields={mediaFields}
|
||||||
|
title="медиа"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Блок предпросмотра */}
|
{/* Блок предпросмотра */}
|
||||||
@ -98,14 +111,15 @@ export const ArticleEdit = () => {
|
|||||||
sx={{
|
sx={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
p: 2,
|
p: 2,
|
||||||
maxHeight: 'calc(100vh - 200px)',
|
maxHeight: "calc(100vh - 200px)",
|
||||||
overflowY: 'auto',
|
overflowY: "auto",
|
||||||
position: 'sticky',
|
position: "sticky",
|
||||||
top: 16,
|
top: 16,
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
border: '1px solid',
|
border: "1px solid",
|
||||||
borderColor: 'primary.main',
|
borderColor: "primary.main",
|
||||||
bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'background.paper' : '#fff'),
|
bgcolor: (theme) =>
|
||||||
|
theme.palette.mode === "dark" ? "background.paper" : "#fff",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="h6" gutterBottom color="primary">
|
<Typography variant="h6" gutterBottom color="primary">
|
||||||
@ -117,7 +131,8 @@ export const ArticleEdit = () => {
|
|||||||
variant="h4"
|
variant="h4"
|
||||||
gutterBottom
|
gutterBottom
|
||||||
sx={{
|
sx={{
|
||||||
color: (theme) => (theme.palette.mode === 'dark' ? 'grey.300' : 'grey.800'),
|
color: (theme) =>
|
||||||
|
theme.palette.mode === "dark" ? "grey.300" : "grey.800",
|
||||||
mb: 3,
|
mb: 3,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -127,39 +142,41 @@ export const ArticleEdit = () => {
|
|||||||
{/* Markdown контент */}
|
{/* Markdown контент */}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
'& img': {
|
"& img": {
|
||||||
maxWidth: '100%',
|
maxWidth: "100%",
|
||||||
height: 'auto',
|
height: "auto",
|
||||||
borderRadius: 1,
|
borderRadius: 1,
|
||||||
},
|
},
|
||||||
'& h1, & h2, & h3, & h4, & h5, & h6': {
|
"& h1, & h2, & h3, & h4, & h5, & h6": {
|
||||||
color: 'primary.main',
|
color: "primary.main",
|
||||||
mt: 2,
|
mt: 2,
|
||||||
mb: 1,
|
mb: 1,
|
||||||
},
|
},
|
||||||
'& p': {
|
"& p": {
|
||||||
mb: 2,
|
mb: 2,
|
||||||
color: (theme) => (theme.palette.mode === 'dark' ? 'grey.300' : 'grey.800'),
|
color: (theme) =>
|
||||||
|
theme.palette.mode === "dark" ? "grey.300" : "grey.800",
|
||||||
},
|
},
|
||||||
'& a': {
|
"& a": {
|
||||||
color: 'primary.main',
|
color: "primary.main",
|
||||||
textDecoration: 'none',
|
textDecoration: "none",
|
||||||
'&:hover': {
|
"&:hover": {
|
||||||
textDecoration: 'underline',
|
textDecoration: "underline",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'& blockquote': {
|
"& blockquote": {
|
||||||
borderLeft: '4px solid',
|
borderLeft: "4px solid",
|
||||||
borderColor: 'primary.main',
|
borderColor: "primary.main",
|
||||||
pl: 2,
|
pl: 2,
|
||||||
my: 2,
|
my: 2,
|
||||||
color: 'text.secondary',
|
color: "text.secondary",
|
||||||
},
|
},
|
||||||
'& code': {
|
"& code": {
|
||||||
bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'grey.900' : 'grey.100'),
|
bgcolor: (theme) =>
|
||||||
|
theme.palette.mode === "dark" ? "grey.900" : "grey.100",
|
||||||
p: 0.5,
|
p: 0.5,
|
||||||
borderRadius: 0.5,
|
borderRadius: 0.5,
|
||||||
color: 'primary.main',
|
color: "primary.main",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -168,15 +185,15 @@ export const ArticleEdit = () => {
|
|||||||
|
|
||||||
{/* Привязанные медиа */}
|
{/* Привязанные медиа */}
|
||||||
{mediaData?.data && mediaData.data.length > 0 && (
|
{mediaData?.data && mediaData.data.length > 0 && (
|
||||||
<Box sx={{mb: 3}}>
|
<Box sx={{ mb: 3 }}>
|
||||||
<Typography variant="subtitle1" gutterBottom color="primary">
|
<Typography variant="subtitle1" gutterBottom color="primary">
|
||||||
Привязанные медиа:
|
Привязанные медиа:
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
gap: 1,
|
gap: 1,
|
||||||
flexWrap: 'wrap',
|
flexWrap: "wrap",
|
||||||
mb: 2,
|
mb: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -187,18 +204,20 @@ export const ArticleEdit = () => {
|
|||||||
width: 120,
|
width: 120,
|
||||||
height: 120,
|
height: 120,
|
||||||
borderRadius: 1,
|
borderRadius: 1,
|
||||||
overflow: 'hidden',
|
overflow: "hidden",
|
||||||
border: '1px solid',
|
border: "1px solid",
|
||||||
borderColor: 'primary.main',
|
borderColor: "primary.main",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={`https://wn.krbl.ru/media/${media.id}/download?token=${localStorage.getItem(TOKEN_KEY)}`}
|
src={`${import.meta.env.VITE_KRBL_MEDIA}${
|
||||||
|
media.id
|
||||||
|
}/download?token=${localStorage.getItem(TOKEN_KEY)}`}
|
||||||
alt={media.media_name}
|
alt={media.media_name}
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: "100%",
|
||||||
height: '100%',
|
height: "100%",
|
||||||
objectFit: 'cover',
|
objectFit: "cover",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
@ -209,5 +228,5 @@ export const ArticleEdit = () => {
|
|||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
</Edit>
|
</Edit>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
@ -1,60 +1,114 @@
|
|||||||
import {Box, Stack, Typography} from '@mui/material'
|
import { Box, Stack, Typography } from "@mui/material";
|
||||||
import {useShow} from '@refinedev/core'
|
import { useShow } from "@refinedev/core";
|
||||||
import {Show, TextFieldComponent as TextField} from '@refinedev/mui'
|
import { Show, TextFieldComponent as TextField } from "@refinedev/mui";
|
||||||
import {TOKEN_KEY} from '../../authProvider'
|
import { TOKEN_KEY } from "../../authProvider";
|
||||||
|
|
||||||
export type FieldType = {
|
export type FieldType = {
|
||||||
label: string
|
label: string;
|
||||||
data: any
|
data: any;
|
||||||
render?: (value: any) => React.ReactNode
|
render?: (value: any) => React.ReactNode;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const CarrierShow = () => {
|
export const CarrierShow = () => {
|
||||||
const {query} = useShow({})
|
const { query } = useShow({});
|
||||||
const {data, isLoading} = query
|
const { data, isLoading } = query;
|
||||||
|
|
||||||
const record = data?.data
|
const record = data?.data;
|
||||||
|
|
||||||
const fields: FieldType[] = [
|
const fields: FieldType[] = [
|
||||||
{label: 'Полное имя', data: 'full_name'},
|
{ label: "Полное имя", data: "full_name" },
|
||||||
{label: 'Короткое имя', data: 'short_name'},
|
{ label: "Короткое имя", data: "short_name" },
|
||||||
{label: 'Город', data: 'city'},
|
{ label: "Город", data: "city" },
|
||||||
{
|
{
|
||||||
label: 'Основной цвет',
|
label: "Основной цвет",
|
||||||
data: 'main_color',
|
data: "main_color",
|
||||||
render: (value: string) => <Box sx={{display: 'grid', placeItems: 'center', width: 'fit-content', paddingInline: '6px', height: '100%', backgroundColor: `${value}20`, borderRadius: 1}}>{value}</Box>,
|
render: (value: string) => (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "grid",
|
||||||
|
placeItems: "center",
|
||||||
|
width: "fit-content",
|
||||||
|
paddingInline: "6px",
|
||||||
|
height: "100%",
|
||||||
|
backgroundColor: `${value}20`,
|
||||||
|
borderRadius: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Цвет левого виджета',
|
label: "Цвет левого виджета",
|
||||||
data: 'left_color',
|
data: "left_color",
|
||||||
render: (value: string) => <Box sx={{display: 'grid', placeItems: 'center', width: 'fit-content', paddingInline: '6px', height: '100%', backgroundColor: `${value}20`, borderRadius: 1}}>{value}</Box>,
|
render: (value: string) => (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "grid",
|
||||||
|
placeItems: "center",
|
||||||
|
width: "fit-content",
|
||||||
|
paddingInline: "6px",
|
||||||
|
height: "100%",
|
||||||
|
backgroundColor: `${value}20`,
|
||||||
|
borderRadius: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Цвет правого виджета',
|
label: "Цвет правого виджета",
|
||||||
data: 'right_color',
|
data: "right_color",
|
||||||
render: (value: string) => <Box sx={{display: 'grid', placeItems: 'center', width: 'fit-content', paddingInline: '6px', height: '100%', backgroundColor: `${value}20`, borderRadius: 1}}>{value}</Box>,
|
render: (value: string) => (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "grid",
|
||||||
|
placeItems: "center",
|
||||||
|
width: "fit-content",
|
||||||
|
paddingInline: "6px",
|
||||||
|
height: "100%",
|
||||||
|
backgroundColor: `${value}20`,
|
||||||
|
borderRadius: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{label: 'Слоган', data: 'slogan'},
|
{ label: "Слоган", data: "slogan" },
|
||||||
{
|
{
|
||||||
label: 'Логотип',
|
label: "Логотип",
|
||||||
data: 'logo',
|
data: "logo",
|
||||||
render: (value: number) => <img src={`https://wn.krbl.ru/media/${value}/download?token=${localStorage.getItem(TOKEN_KEY)}`} alt={String(value)} style={{maxWidth: '10%', objectFit: 'contain', borderRadius: 8}} />,
|
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 }}
|
||||||
|
/>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
]
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show isLoading={isLoading}>
|
<Show isLoading={isLoading}>
|
||||||
<Stack gap={4}>
|
<Stack gap={4}>
|
||||||
{fields.map(({label, data, render}) => (
|
{fields.map(({ label, data, render }) => (
|
||||||
<Stack key={data} gap={1}>
|
<Stack key={data} gap={1}>
|
||||||
<Typography variant="body1" fontWeight="bold">
|
<Typography variant="body1" fontWeight="bold">
|
||||||
{label}
|
{label}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{render ? render(record?.[data]) : <TextField value={record?.[data]} />}
|
{render ? (
|
||||||
|
render(record?.[data])
|
||||||
|
) : (
|
||||||
|
<TextField value={record?.[data]} />
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
@ -1,35 +1,51 @@
|
|||||||
import {Stack, Typography} from '@mui/material'
|
import { Stack, Typography } from "@mui/material";
|
||||||
import {useShow} from '@refinedev/core'
|
import { useShow } from "@refinedev/core";
|
||||||
import {Show, TextFieldComponent as TextField} from '@refinedev/mui'
|
import { Show, TextFieldComponent as TextField } from "@refinedev/mui";
|
||||||
import {TOKEN_KEY} from '../../authProvider'
|
import { TOKEN_KEY } from "../../authProvider";
|
||||||
|
|
||||||
export const CityShow = () => {
|
export const CityShow = () => {
|
||||||
const {query} = useShow({})
|
const { query } = useShow({});
|
||||||
const {data, isLoading} = query
|
const { data, isLoading } = query;
|
||||||
|
|
||||||
const record = data?.data
|
const record = data?.data;
|
||||||
|
|
||||||
const fields = [
|
const fields = [
|
||||||
// {label: 'ID', data: 'id'},
|
// {label: 'ID', data: 'id'},
|
||||||
{label: 'Название', data: 'name'},
|
{ label: "Название", data: "name" },
|
||||||
// {label: 'Код страны', data: 'country_code'},
|
// {label: 'Код страны', data: 'country_code'},
|
||||||
{label: 'Страна', data: 'country'},
|
{ label: "Страна", data: "country" },
|
||||||
{label: 'Герб', data: 'arms', render: (value: number) => <img src={`https://wn.krbl.ru/media/${value}/download?token=${localStorage.getItem(TOKEN_KEY)}`} alt={String(value)} style={{maxWidth: '10%', objectFit: 'contain', borderRadius: 8}} />},
|
{
|
||||||
]
|
label: "Герб",
|
||||||
|
data: "arms",
|
||||||
|
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 }}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show isLoading={isLoading}>
|
<Show isLoading={isLoading}>
|
||||||
<Stack gap={4}>
|
<Stack gap={4}>
|
||||||
{fields.map(({label, data, render}) => (
|
{fields.map(({ label, data, render }) => (
|
||||||
<Stack key={data} gap={1}>
|
<Stack key={data} gap={1}>
|
||||||
<Typography variant="body1" fontWeight="bold">
|
<Typography variant="body1" fontWeight="bold">
|
||||||
{label}
|
{label}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{render ? render(record?.[data]) : <TextField value={record?.[data]} />}
|
{render ? (
|
||||||
|
render(record?.[data])
|
||||||
|
) : (
|
||||||
|
<TextField value={record?.[data]} />
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
@ -1,26 +1,36 @@
|
|||||||
import {Box, TextField, Button, Typography, Autocomplete} from '@mui/material'
|
import {
|
||||||
import {Edit} from '@refinedev/mui'
|
Box,
|
||||||
import {useForm} from '@refinedev/react-hook-form'
|
TextField,
|
||||||
import {useEffect} from 'react'
|
Button,
|
||||||
import {useShow} from '@refinedev/core'
|
Typography,
|
||||||
import {Controller} from 'react-hook-form'
|
Autocomplete,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { Edit } from "@refinedev/mui";
|
||||||
|
import { useForm } from "@refinedev/react-hook-form";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useShow } from "@refinedev/core";
|
||||||
|
import { Controller } from "react-hook-form";
|
||||||
|
|
||||||
import {MEDIA_TYPES} from '../../lib/constants'
|
import { MEDIA_TYPES } from "../../lib/constants";
|
||||||
import {ALLOWED_IMAGE_TYPES, ALLOWED_VIDEO_TYPES, useMediaFileUpload} from '../../components/media/MediaFormUtils'
|
import {
|
||||||
import {TOKEN_KEY} from '../../authProvider'
|
ALLOWED_IMAGE_TYPES,
|
||||||
|
ALLOWED_VIDEO_TYPES,
|
||||||
|
useMediaFileUpload,
|
||||||
|
} from "../../components/media/MediaFormUtils";
|
||||||
|
import { TOKEN_KEY } from "../../authProvider";
|
||||||
|
|
||||||
type MediaFormValues = {
|
type MediaFormValues = {
|
||||||
media_name: string
|
media_name: string;
|
||||||
media_type: number
|
media_type: number;
|
||||||
file?: File
|
file?: File;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const MediaEdit = () => {
|
export const MediaEdit = () => {
|
||||||
const {
|
const {
|
||||||
saveButtonProps,
|
saveButtonProps,
|
||||||
refineCore: {onFinish},
|
refineCore: { onFinish },
|
||||||
register,
|
register,
|
||||||
formState: {errors},
|
formState: { errors },
|
||||||
setValue,
|
setValue,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
watch,
|
watch,
|
||||||
@ -29,32 +39,42 @@ export const MediaEdit = () => {
|
|||||||
control,
|
control,
|
||||||
} = useForm<MediaFormValues>({
|
} = useForm<MediaFormValues>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
media_name: '',
|
media_name: "",
|
||||||
media_type: '',
|
media_type: "",
|
||||||
file: undefined,
|
file: undefined,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
const {query} = useShow()
|
const { query } = useShow();
|
||||||
const {data} = query
|
const { data } = query;
|
||||||
const record = data?.data
|
const record = data?.data;
|
||||||
|
|
||||||
const selectedMediaType = watch('media_type')
|
const selectedMediaType = watch("media_type");
|
||||||
|
|
||||||
const {selectedFile, previewUrl, setPreviewUrl, handleFileChange, handleMediaTypeChange} = useMediaFileUpload({
|
const {
|
||||||
|
selectedFile,
|
||||||
|
previewUrl,
|
||||||
|
setPreviewUrl,
|
||||||
|
handleFileChange,
|
||||||
|
handleMediaTypeChange,
|
||||||
|
} = useMediaFileUpload({
|
||||||
selectedMediaType,
|
selectedMediaType,
|
||||||
setError,
|
setError,
|
||||||
clearErrors,
|
clearErrors,
|
||||||
setValue,
|
setValue,
|
||||||
})
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (record?.id) {
|
if (record?.id) {
|
||||||
setPreviewUrl(`https://wn.krbl.ru/media/${record.id}/download?token=${localStorage.getItem(TOKEN_KEY)}`)
|
setPreviewUrl(
|
||||||
setValue('media_name', record?.media_name || '')
|
`${import.meta.env.VITE_KRBL_MEDIA}${
|
||||||
setValue('media_type', record?.media_type)
|
record.id
|
||||||
|
}/download?token=${localStorage.getItem(TOKEN_KEY)}`
|
||||||
|
);
|
||||||
|
setValue("media_name", record?.media_name || "");
|
||||||
|
setValue("media_type", record?.media_type);
|
||||||
}
|
}
|
||||||
}, [record, setValue, setPreviewUrl])
|
}, [record, setValue, setPreviewUrl]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Edit
|
<Edit
|
||||||
@ -66,57 +86,98 @@ export const MediaEdit = () => {
|
|||||||
media_name: data.media_name,
|
media_name: data.media_name,
|
||||||
filename: selectedFile?.name || record?.filename,
|
filename: selectedFile?.name || record?.filename,
|
||||||
type: Number(data.media_type),
|
type: Number(data.media_type),
|
||||||
}
|
};
|
||||||
onFinish(formData)
|
onFinish(formData);
|
||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box component="form" sx={{display: 'flex', flexDirection: 'column'}} autoComplete="off">
|
<Box
|
||||||
|
component="form"
|
||||||
|
sx={{ display: "flex", flexDirection: "column" }}
|
||||||
|
autoComplete="off"
|
||||||
|
>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="media_type"
|
name="media_type"
|
||||||
rules={{
|
rules={{
|
||||||
required: 'Это поле является обязательным',
|
required: "Это поле является обязательным",
|
||||||
}}
|
}}
|
||||||
defaultValue={null}
|
defaultValue={null}
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
options={MEDIA_TYPES}
|
options={MEDIA_TYPES}
|
||||||
value={MEDIA_TYPES.find((option) => option.value === field.value) || null}
|
value={
|
||||||
|
MEDIA_TYPES.find((option) => option.value === field.value) ||
|
||||||
|
null
|
||||||
|
}
|
||||||
onChange={(_, value) => {
|
onChange={(_, value) => {
|
||||||
field.onChange(value?.value || null)
|
field.onChange(value?.value || null);
|
||||||
handleMediaTypeChange(value?.value || null)
|
handleMediaTypeChange(value?.value || null);
|
||||||
}}
|
}}
|
||||||
getOptionLabel={(item) => {
|
getOptionLabel={(item) => {
|
||||||
return item ? item.label : ''
|
return item ? item.label : "";
|
||||||
}}
|
}}
|
||||||
isOptionEqualToValue={(option, value) => {
|
isOptionEqualToValue={(option, value) => {
|
||||||
return option.value === value?.value
|
return option.value === value?.value;
|
||||||
}}
|
}}
|
||||||
renderInput={(params) => <TextField {...params} label="Тип" margin="normal" variant="outlined" error={!!errors.media_type} helperText={(errors as any)?.media_type?.message} required />}
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
label="Тип"
|
||||||
|
margin="normal"
|
||||||
|
variant="outlined"
|
||||||
|
error={!!errors.media_type}
|
||||||
|
helperText={(errors as any)?.media_type?.message}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
{...register('media_name', {
|
{...register("media_name", {
|
||||||
required: 'Это поле является обязательным',
|
required: "Это поле является обязательным",
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.media_name}
|
error={!!(errors as any)?.media_name}
|
||||||
helperText={(errors as any)?.media_name?.message}
|
helperText={(errors as any)?.media_name?.message}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{shrink: true}}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="text"
|
type="text"
|
||||||
label="Название *"
|
label="Название *"
|
||||||
name="media_name"
|
name="media_name"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box display="flex" flexDirection="column-reverse" alignItems="center" gap={4} style={{marginTop: 10}}>
|
<Box
|
||||||
<Box display="flex" flexDirection="column" alignItems="center" gap={2}>
|
display="flex"
|
||||||
<Button variant="contained" component="label" disabled={!selectedMediaType}>
|
flexDirection="column-reverse"
|
||||||
{selectedFile ? 'Изменить файл' : 'Загрузить файл'}
|
alignItems="center"
|
||||||
<input type="file" hidden onChange={handleFileChange} accept={selectedMediaType === 1 ? ALLOWED_IMAGE_TYPES.join(',') : ALLOWED_VIDEO_TYPES.join(',')} />
|
gap={4}
|
||||||
|
style={{ marginTop: 10 }}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
display="flex"
|
||||||
|
flexDirection="column"
|
||||||
|
alignItems="center"
|
||||||
|
gap={2}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
component="label"
|
||||||
|
disabled={!selectedMediaType}
|
||||||
|
>
|
||||||
|
{selectedFile ? "Изменить файл" : "Загрузить файл"}
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
hidden
|
||||||
|
onChange={handleFileChange}
|
||||||
|
accept={
|
||||||
|
selectedMediaType === 1
|
||||||
|
? ALLOWED_IMAGE_TYPES.join(",")
|
||||||
|
: ALLOWED_VIDEO_TYPES.join(",")
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{selectedFile && (
|
{selectedFile && (
|
||||||
@ -134,11 +195,15 @@ export const MediaEdit = () => {
|
|||||||
|
|
||||||
{previewUrl && selectedMediaType === 1 && (
|
{previewUrl && selectedMediaType === 1 && (
|
||||||
<Box mt={2} display="flex" justifyContent="center">
|
<Box mt={2} display="flex" justifyContent="center">
|
||||||
<img src={previewUrl} alt="Preview" style={{maxWidth: '200px', borderRadius: 8}} />
|
<img
|
||||||
|
src={previewUrl}
|
||||||
|
alt="Preview"
|
||||||
|
style={{ maxWidth: "200px", borderRadius: 8 }}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Edit>
|
</Edit>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
@ -1,39 +1,42 @@
|
|||||||
import {Stack, Typography, Box, Button} from '@mui/material'
|
import { Stack, Typography, Box, Button } from "@mui/material";
|
||||||
import {useShow} from '@refinedev/core'
|
import { useShow } from "@refinedev/core";
|
||||||
import {Show, TextFieldComponent as TextField} from '@refinedev/mui'
|
import { Show, TextFieldComponent as TextField } from "@refinedev/mui";
|
||||||
|
|
||||||
import {MEDIA_TYPES} from '../../lib/constants'
|
import { MEDIA_TYPES } from "../../lib/constants";
|
||||||
import {TOKEN_KEY} from '../../authProvider'
|
import { TOKEN_KEY } from "../../authProvider";
|
||||||
|
|
||||||
export const MediaShow = () => {
|
export const MediaShow = () => {
|
||||||
const {query} = useShow({})
|
const { query } = useShow({});
|
||||||
const {data, isLoading} = query
|
const { data, isLoading } = query;
|
||||||
|
|
||||||
const record = data?.data
|
const record = data?.data;
|
||||||
const token = localStorage.getItem(TOKEN_KEY)
|
const token = localStorage.getItem(TOKEN_KEY);
|
||||||
|
|
||||||
const fields = [
|
const fields = [
|
||||||
// {label: 'Название файла', data: 'filename'},
|
// {label: 'Название файла', data: 'filename'},
|
||||||
{label: 'Название', data: 'media_name'},
|
{ label: "Название", data: "media_name" },
|
||||||
{
|
{
|
||||||
label: 'Тип',
|
label: "Тип",
|
||||||
data: 'media_type',
|
data: "media_type",
|
||||||
render: (value: number) => MEDIA_TYPES.find((type) => type.value === value)?.label || value,
|
render: (value: number) =>
|
||||||
|
MEDIA_TYPES.find((type) => type.value === value)?.label || value,
|
||||||
},
|
},
|
||||||
// {label: 'ID', data: 'id'},
|
// {label: 'ID', data: 'id'},
|
||||||
]
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show isLoading={isLoading}>
|
<Show isLoading={isLoading}>
|
||||||
<Stack gap={4}>
|
<Stack gap={4}>
|
||||||
{record && record.media_type === 1 && (
|
{record && record.media_type === 1 && (
|
||||||
<img
|
<img
|
||||||
src={`https://wn.krbl.ru/media/${record?.id}/download?token=${token}`}
|
src={`${import.meta.env.VITE_KRBL_MEDIA}${
|
||||||
|
record?.id
|
||||||
|
}/download?token=${token}`}
|
||||||
alt={record?.filename}
|
alt={record?.filename}
|
||||||
style={{
|
style={{
|
||||||
maxWidth: '100%',
|
maxWidth: "100%",
|
||||||
height: '40vh',
|
height: "40vh",
|
||||||
objectFit: 'contain',
|
objectFit: "contain",
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -43,36 +46,45 @@ export const MediaShow = () => {
|
|||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
p: 2,
|
p: 2,
|
||||||
border: '1px solid text.pimary',
|
border: "1px solid text.pimary",
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
bgcolor: 'primary.light',
|
bgcolor: "primary.light",
|
||||||
width: 'fit-content',
|
width: "fit-content",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography
|
<Typography
|
||||||
variant="body1"
|
variant="body1"
|
||||||
gutterBottom
|
gutterBottom
|
||||||
sx={{
|
sx={{
|
||||||
color: '#FFFFFF',
|
color: "#FFFFFF",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Видео доступно для скачивания по ссылке:
|
Видео доступно для скачивания по ссылке:
|
||||||
</Typography>
|
</Typography>
|
||||||
<Button variant="contained" href={`https://wn.krbl.ru/media/${record?.id}/download?token=${token}`} target="_blank" sx={{mt: 1, width: '100%'}}>
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
href={`${import.meta.env.VITE_KRBL_MEDIA}${
|
||||||
|
record?.id
|
||||||
|
}/download?token=${token}`}
|
||||||
|
target="_blank"
|
||||||
|
sx={{ mt: 1, width: "100%" }}
|
||||||
|
>
|
||||||
Скачать видео
|
Скачать видео
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{fields.map(({label, data, render}) => (
|
{fields.map(({ label, data, render }) => (
|
||||||
<Stack key={data} gap={1}>
|
<Stack key={data} gap={1}>
|
||||||
<Typography variant="body1" fontWeight="bold">
|
<Typography variant="body1" fontWeight="bold">
|
||||||
{label}
|
{label}
|
||||||
</Typography>
|
</Typography>
|
||||||
<TextField value={render ? render(record?.[data]) : record?.[data]} />
|
<TextField
|
||||||
|
value={render ? render(record?.[data]) : record?.[data]}
|
||||||
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
@ -1,73 +1,102 @@
|
|||||||
import {Autocomplete, Box, TextField, FormControlLabel, Checkbox, Typography} from '@mui/material'
|
import {
|
||||||
import {Create, useAutocomplete} from '@refinedev/mui'
|
Autocomplete,
|
||||||
import {useForm} from '@refinedev/react-hook-form'
|
Box,
|
||||||
import {Controller} from 'react-hook-form'
|
TextField,
|
||||||
|
FormControlLabel,
|
||||||
|
Checkbox,
|
||||||
|
Typography,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { Create, useAutocomplete } from "@refinedev/mui";
|
||||||
|
import { useForm } from "@refinedev/react-hook-form";
|
||||||
|
import { Controller } from "react-hook-form";
|
||||||
|
|
||||||
export const RouteCreate = () => {
|
export const RouteCreate = () => {
|
||||||
const {
|
const {
|
||||||
saveButtonProps,
|
saveButtonProps,
|
||||||
refineCore: {formLoading},
|
refineCore: { formLoading },
|
||||||
register,
|
register,
|
||||||
control,
|
control,
|
||||||
formState: {errors},
|
formState: { errors },
|
||||||
} = useForm({
|
} = useForm({
|
||||||
refineCoreProps: {
|
refineCoreProps: {
|
||||||
resource: 'route/',
|
resource: "route/",
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
const {autocompleteProps: carrierAutocompleteProps} = useAutocomplete({
|
const { autocompleteProps: carrierAutocompleteProps } = useAutocomplete({
|
||||||
resource: 'carrier',
|
resource: "carrier",
|
||||||
onSearch: (value) => [
|
onSearch: (value) => [
|
||||||
{
|
{
|
||||||
field: 'short_name',
|
field: "short_name",
|
||||||
operator: 'contains',
|
operator: "contains",
|
||||||
value,
|
value,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Create isLoading={formLoading} saveButtonProps={saveButtonProps}>
|
<Create isLoading={formLoading} saveButtonProps={saveButtonProps}>
|
||||||
<Box component="form" sx={{display: 'flex', flexDirection: 'column'}} autoComplete="off">
|
<Box
|
||||||
|
component="form"
|
||||||
|
sx={{ display: "flex", flexDirection: "column" }}
|
||||||
|
autoComplete="off"
|
||||||
|
>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="carrier_id"
|
name="carrier_id"
|
||||||
rules={{required: 'Это поле является обязательным'}}
|
rules={{ required: "Это поле является обязательным" }}
|
||||||
defaultValue={null}
|
defaultValue={null}
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
{...carrierAutocompleteProps}
|
{...carrierAutocompleteProps}
|
||||||
value={carrierAutocompleteProps.options.find((option) => option.id === field.value) || null}
|
value={
|
||||||
|
carrierAutocompleteProps.options.find(
|
||||||
|
(option) => option.id === field.value
|
||||||
|
) || null
|
||||||
|
}
|
||||||
onChange={(_, value) => {
|
onChange={(_, value) => {
|
||||||
field.onChange(value?.id || '')
|
field.onChange(value?.id || "");
|
||||||
}}
|
}}
|
||||||
getOptionLabel={(item) => {
|
getOptionLabel={(item) => {
|
||||||
return item ? item.short_name : ''
|
return item ? item.short_name : "";
|
||||||
}}
|
}}
|
||||||
isOptionEqualToValue={(option, value) => {
|
isOptionEqualToValue={(option, value) => {
|
||||||
return option.id === value?.id
|
return option.id === value?.id;
|
||||||
}}
|
}}
|
||||||
filterOptions={(options, {inputValue}) => {
|
filterOptions={(options, { inputValue }) => {
|
||||||
return options.filter((option) => option.short_name.toLowerCase().includes(inputValue.toLowerCase()))
|
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 />}
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
label="Выберите перевозчика"
|
||||||
|
margin="normal"
|
||||||
|
variant="outlined"
|
||||||
|
error={!!errors.carrier_id}
|
||||||
|
helperText={(errors as any)?.carrier_id?.message}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
{...register('route_number', {
|
{...register("route_number", {
|
||||||
required: 'Это поле является обязательным',
|
required: "Это поле является обязательным",
|
||||||
setValueAs: (value) => String(value),
|
setValueAs: (value) => String(value),
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.route_number}
|
error={!!(errors as any)?.route_number}
|
||||||
helperText={(errors as any)?.route_number?.message}
|
helperText={(errors as any)?.route_number?.message}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{shrink: true}}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="text"
|
type="text"
|
||||||
label={'Номер маршрута *'}
|
label={"Номер маршрута *"}
|
||||||
name="route_number"
|
name="route_number"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -75,143 +104,169 @@ export const RouteCreate = () => {
|
|||||||
name="route_direction" // boolean
|
name="route_direction" // boolean
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue={false}
|
defaultValue={false}
|
||||||
render={({field}: {field: any}) => <FormControlLabel label="Прямой маршрут? *" control={<Checkbox {...field} checked={field.value} onChange={(e) => field.onChange(e.target.checked)} />} />}
|
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
|
||||||
|
variant="caption"
|
||||||
|
color="textSecondary"
|
||||||
|
sx={{ mt: 0, mb: 1 }}
|
||||||
|
>
|
||||||
(Прямой / Обратный)
|
(Прямой / Обратный)
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
{...register('path', {
|
{...register("path", {
|
||||||
required: 'Это поле является обязательным',
|
required: "Это поле является обязательным",
|
||||||
setValueAs: (value: string) => {
|
setValueAs: (value: string) => {
|
||||||
try {
|
try {
|
||||||
// Парсим строку в массив массивов
|
// Парсим строку в массив массивов
|
||||||
return JSON.parse(value)
|
return JSON.parse(value);
|
||||||
} catch {
|
} catch {
|
||||||
return []
|
return [];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
validate: (value: unknown) => {
|
validate: (value: unknown) => {
|
||||||
if (!Array.isArray(value)) return 'Неверный формат'
|
if (!Array.isArray(value)) return "Неверный формат";
|
||||||
if (!value.every((point: unknown) => Array.isArray(point) && point.length === 2)) {
|
if (
|
||||||
return 'Каждая точка должна быть массивом из двух координат'
|
!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'))) {
|
if (
|
||||||
return 'Координаты должны быть числами'
|
!value.every((point: unknown[]) =>
|
||||||
|
point.every(
|
||||||
|
(coord: unknown) =>
|
||||||
|
!isNaN(Number(coord)) && typeof coord === "number"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return "Координаты должны быть числами";
|
||||||
}
|
}
|
||||||
return true
|
return true;
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.path}
|
error={!!(errors as any)?.path}
|
||||||
helperText={(errors as any)?.path?.message} // 'Формат: [[lat1,lon1], [lat2,lon2], ...]'
|
helperText={(errors as any)?.path?.message} // 'Формат: [[lat1,lon1], [lat2,lon2], ...]'
|
||||||
margin="normal"
|
margin="normal"
|
||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{shrink: true}}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="text"
|
type="text"
|
||||||
label={'Координаты маршрута *'}
|
label={"Координаты маршрута *"}
|
||||||
name="path"
|
name="path"
|
||||||
placeholder="[[1.1, 2.2], [2.1, 4.5]]"
|
placeholder="[[1.1, 2.2], [2.1, 4.5]]"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
{...register('route_sys_number', {
|
{...register("route_sys_number", {
|
||||||
required: 'Это поле является обязательным',
|
required: "Это поле является обязательным",
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.route_sys_number}
|
error={!!(errors as any)?.route_sys_number}
|
||||||
helperText={(errors as any)?.route_sys_number?.message}
|
helperText={(errors as any)?.route_sys_number?.message}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{shrink: true}}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="number"
|
type="number"
|
||||||
label={'Системный номер маршрута *'}
|
label={"Системный номер маршрута *"}
|
||||||
name="route_sys_number"
|
name="route_sys_number"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
{...register('governor_appeal', {
|
{...register("governor_appeal", {
|
||||||
// required: 'Это поле является обязательным',
|
// required: 'Это поле является обязательным',
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.governor_appeal}
|
error={!!(errors as any)?.governor_appeal}
|
||||||
helperText={(errors as any)?.governor_appeal?.message}
|
helperText={(errors as any)?.governor_appeal?.message}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{shrink: true}}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="number"
|
type="number"
|
||||||
label={'Обращение губернатора'}
|
label={"Обращение губернатора"}
|
||||||
name="governor_appeal"
|
name="governor_appeal"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
{...register('scale_min', {
|
{...register("scale_min", {
|
||||||
// required: 'Это поле является обязательным',
|
// required: 'Это поле является обязательным',
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.scale_min}
|
error={!!(errors as any)?.scale_min}
|
||||||
helperText={(errors as any)?.scale_min?.message}
|
helperText={(errors as any)?.scale_min?.message}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{shrink: true}}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="number"
|
type="number"
|
||||||
label={'Масштаб (мин)'}
|
label={"Масштаб (мин)"}
|
||||||
name="scale_min"
|
name="scale_min"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
{...register('scale_max', {
|
{...register("scale_max", {
|
||||||
// required: 'Это поле является обязательным',
|
// required: 'Это поле является обязательным',
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.scale_max}
|
error={!!(errors as any)?.scale_max}
|
||||||
helperText={(errors as any)?.scale_max?.message}
|
helperText={(errors as any)?.scale_max?.message}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{shrink: true}}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="number"
|
type="number"
|
||||||
label={'Масштаб (макс)'}
|
label={"Масштаб (макс)"}
|
||||||
name="scale_max"
|
name="scale_max"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
{...register('rotate', {
|
{...register("rotate", {
|
||||||
// required: 'Это поле является обязательным',
|
// required: 'Это поле является обязательным',
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.rotate}
|
error={!!(errors as any)?.rotate}
|
||||||
helperText={(errors as any)?.rotate?.message}
|
helperText={(errors as any)?.rotate?.message}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{shrink: true}}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="number"
|
type="number"
|
||||||
label={'Поворот'}
|
label={"Поворот"}
|
||||||
name="rotate"
|
name="rotate"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
{...register('center_latitude', {
|
{...register("center_latitude", {
|
||||||
// required: 'Это поле является обязательным',
|
// required: 'Это поле является обязательным',
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.center_latitude}
|
error={!!(errors as any)?.center_latitude}
|
||||||
helperText={(errors as any)?.center_latitude?.message}
|
helperText={(errors as any)?.center_latitude?.message}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{shrink: true}}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="number"
|
type="number"
|
||||||
label={'Центр. широта'}
|
label={"Центр. широта"}
|
||||||
name="center_latitude"
|
name="center_latitude"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
{...register('center_longitude', {
|
{...register("center_longitude", {
|
||||||
// required: 'Это поле является обязательным',
|
// required: 'Это поле является обязательным',
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.center_longitude}
|
error={!!(errors as any)?.center_longitude}
|
||||||
helperText={(errors as any)?.center_longitude?.message}
|
helperText={(errors as any)?.center_longitude?.message}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{shrink: true}}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="number"
|
type="number"
|
||||||
label={'Центр. долгота'}
|
label={"Центр. долгота"}
|
||||||
name="center_longitude"
|
name="center_longitude"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Create>
|
</Create>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
@ -1,191 +1,248 @@
|
|||||||
import {Autocomplete, Box, TextField, Typography, Paper} from '@mui/material'
|
import { Autocomplete, Box, TextField, Typography, Paper } from "@mui/material";
|
||||||
import {Create, useAutocomplete} from '@refinedev/mui'
|
import { Create, useAutocomplete } from "@refinedev/mui";
|
||||||
import {useForm} from '@refinedev/react-hook-form'
|
import { useForm } from "@refinedev/react-hook-form";
|
||||||
import {Controller} from 'react-hook-form'
|
import { Controller } from "react-hook-form";
|
||||||
import {Link} from 'react-router'
|
import { Link } from "react-router";
|
||||||
import React, {useState, useEffect} from 'react'
|
import React, { useState, useEffect } from "react";
|
||||||
import {TOKEN_KEY} from '../../authProvider'
|
import { TOKEN_KEY } from "../../authProvider";
|
||||||
|
|
||||||
export const SightCreate = () => {
|
export const SightCreate = () => {
|
||||||
const {
|
const {
|
||||||
saveButtonProps,
|
saveButtonProps,
|
||||||
refineCore: {formLoading},
|
refineCore: { formLoading },
|
||||||
register,
|
register,
|
||||||
control,
|
control,
|
||||||
watch,
|
watch,
|
||||||
formState: {errors},
|
formState: { errors },
|
||||||
} = useForm({
|
} = useForm({
|
||||||
refineCoreProps: {
|
refineCoreProps: {
|
||||||
resource: 'sight/',
|
resource: "sight/",
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
// Состояния для предпросмотра
|
// Состояния для предпросмотра
|
||||||
const [namePreview, setNamePreview] = useState('')
|
const [namePreview, setNamePreview] = useState("");
|
||||||
const [coordinatesPreview, setCoordinatesPreview] = useState({latitude: '', longitude: ''})
|
const [coordinatesPreview, setCoordinatesPreview] = useState({
|
||||||
const [cityPreview, setCityPreview] = useState('')
|
latitude: "",
|
||||||
const [thumbnailPreview, setThumbnailPreview] = useState<string | null>(null)
|
longitude: "",
|
||||||
const [watermarkLUPreview, setWatermarkLUPreview] = useState<string | null>(null)
|
});
|
||||||
const [watermarkRDPreview, setWatermarkRDPreview] = useState<string | null>(null)
|
const [cityPreview, setCityPreview] = useState("");
|
||||||
const [leftArticlePreview, setLeftArticlePreview] = useState('')
|
const [thumbnailPreview, setThumbnailPreview] = useState<string | null>(null);
|
||||||
const [previewArticlePreview, setPreviewArticlePreview] = useState('')
|
const [watermarkLUPreview, setWatermarkLUPreview] = useState<string | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [watermarkRDPreview, setWatermarkRDPreview] = useState<string | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [leftArticlePreview, setLeftArticlePreview] = useState("");
|
||||||
|
const [previewArticlePreview, setPreviewArticlePreview] = useState("");
|
||||||
|
|
||||||
// Автокомплиты
|
// Автокомплиты
|
||||||
const {autocompleteProps: cityAutocompleteProps} = useAutocomplete({
|
const { autocompleteProps: cityAutocompleteProps } = useAutocomplete({
|
||||||
resource: 'city',
|
resource: "city",
|
||||||
onSearch: (value) => [
|
onSearch: (value) => [
|
||||||
{
|
{
|
||||||
field: 'name',
|
field: "name",
|
||||||
operator: 'contains',
|
operator: "contains",
|
||||||
value,
|
value,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
});
|
||||||
|
|
||||||
const {autocompleteProps: mediaAutocompleteProps} = useAutocomplete({
|
const { autocompleteProps: mediaAutocompleteProps } = useAutocomplete({
|
||||||
resource: 'media',
|
resource: "media",
|
||||||
onSearch: (value) => [
|
onSearch: (value) => [
|
||||||
{
|
{
|
||||||
field: 'media_name',
|
field: "media_name",
|
||||||
operator: 'contains',
|
operator: "contains",
|
||||||
value,
|
value,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
});
|
||||||
|
|
||||||
const {autocompleteProps: articleAutocompleteProps} = useAutocomplete({
|
const { autocompleteProps: articleAutocompleteProps } = useAutocomplete({
|
||||||
resource: 'article',
|
resource: "article",
|
||||||
onSearch: (value) => [
|
onSearch: (value) => [
|
||||||
{
|
{
|
||||||
field: 'heading',
|
field: "heading",
|
||||||
operator: 'contains',
|
operator: "contains",
|
||||||
value,
|
value,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
});
|
||||||
|
|
||||||
// Следим за изменениями во всех полях
|
// Следим за изменениями во всех полях
|
||||||
const nameContent = watch('name')
|
const nameContent = watch("name");
|
||||||
const latitudeContent = watch('latitude')
|
const latitudeContent = watch("latitude");
|
||||||
const longitudeContent = watch('longitude')
|
const longitudeContent = watch("longitude");
|
||||||
const cityContent = watch('city_id')
|
const cityContent = watch("city_id");
|
||||||
const thumbnailContent = watch('thumbnail')
|
const thumbnailContent = watch("thumbnail");
|
||||||
const watermarkLUContent = watch('watermark_lu')
|
const watermarkLUContent = watch("watermark_lu");
|
||||||
const watermarkRDContent = watch('watermark_rd')
|
const watermarkRDContent = watch("watermark_rd");
|
||||||
const leftArticleContent = watch('left_article')
|
const leftArticleContent = watch("left_article");
|
||||||
const previewArticleContent = watch('preview_article')
|
const previewArticleContent = watch("preview_article");
|
||||||
|
|
||||||
// Обновляем состояния при изменении полей
|
// Обновляем состояния при изменении полей
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setNamePreview(nameContent || '')
|
setNamePreview(nameContent || "");
|
||||||
}, [nameContent])
|
}, [nameContent]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCoordinatesPreview({
|
setCoordinatesPreview({
|
||||||
latitude: latitudeContent || '',
|
latitude: latitudeContent || "",
|
||||||
longitude: longitudeContent || '',
|
longitude: longitudeContent || "",
|
||||||
})
|
});
|
||||||
}, [latitudeContent, longitudeContent])
|
}, [latitudeContent, longitudeContent]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const selectedCity = cityAutocompleteProps.options.find((option) => option.id === cityContent)
|
const selectedCity = cityAutocompleteProps.options.find(
|
||||||
setCityPreview(selectedCity?.name || '')
|
(option) => option.id === cityContent
|
||||||
}, [cityContent, cityAutocompleteProps.options])
|
);
|
||||||
|
setCityPreview(selectedCity?.name || "");
|
||||||
|
}, [cityContent, cityAutocompleteProps.options]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const selectedThumbnail = mediaAutocompleteProps.options.find((option) => option.id === thumbnailContent)
|
const selectedThumbnail = mediaAutocompleteProps.options.find(
|
||||||
setThumbnailPreview(selectedThumbnail ? `https://wn.krbl.ru/media/${selectedThumbnail.id}/download?token=${localStorage.getItem(TOKEN_KEY)}` : null)
|
(option) => option.id === thumbnailContent
|
||||||
}, [thumbnailContent, mediaAutocompleteProps.options])
|
);
|
||||||
|
setThumbnailPreview(
|
||||||
|
selectedThumbnail
|
||||||
|
? `${import.meta.env.VITE_KRBL_MEDIA}${
|
||||||
|
selectedThumbnail.id
|
||||||
|
}/download?token=${localStorage.getItem(TOKEN_KEY)}`
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
}, [thumbnailContent, mediaAutocompleteProps.options]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const selectedWatermarkLU = mediaAutocompleteProps.options.find((option) => option.id === watermarkLUContent)
|
const selectedWatermarkLU = mediaAutocompleteProps.options.find(
|
||||||
setWatermarkLUPreview(selectedWatermarkLU ? `https://wn.krbl.ru/media/${selectedWatermarkLU.id}/download?token=${localStorage.getItem(TOKEN_KEY)}` : null)
|
(option) => option.id === watermarkLUContent
|
||||||
}, [watermarkLUContent, mediaAutocompleteProps.options])
|
);
|
||||||
|
setWatermarkLUPreview(
|
||||||
|
selectedWatermarkLU
|
||||||
|
? `${import.meta.env.VITE_KRBL_MEDIA}${
|
||||||
|
selectedWatermarkLU.id
|
||||||
|
}/download?token=${localStorage.getItem(TOKEN_KEY)}`
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
}, [watermarkLUContent, mediaAutocompleteProps.options]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const selectedWatermarkRD = mediaAutocompleteProps.options.find((option) => option.id === watermarkRDContent)
|
const selectedWatermarkRD = mediaAutocompleteProps.options.find(
|
||||||
setWatermarkRDPreview(selectedWatermarkRD ? `https://wn.krbl.ru/media/${selectedWatermarkRD.id}/download?token=${localStorage.getItem(TOKEN_KEY)}` : null)
|
(option) => option.id === watermarkRDContent
|
||||||
}, [watermarkRDContent, mediaAutocompleteProps.options])
|
);
|
||||||
|
setWatermarkRDPreview(
|
||||||
|
selectedWatermarkRD
|
||||||
|
? `${import.meta.env.VITE_KRBL_MEDIA}${
|
||||||
|
selectedWatermarkRD.id
|
||||||
|
}/download?token=${localStorage.getItem(TOKEN_KEY)}`
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
}, [watermarkRDContent, mediaAutocompleteProps.options]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const selectedLeftArticle = articleAutocompleteProps.options.find((option) => option.id === leftArticleContent)
|
const selectedLeftArticle = articleAutocompleteProps.options.find(
|
||||||
setLeftArticlePreview(selectedLeftArticle?.heading || '')
|
(option) => option.id === leftArticleContent
|
||||||
}, [leftArticleContent, articleAutocompleteProps.options])
|
);
|
||||||
|
setLeftArticlePreview(selectedLeftArticle?.heading || "");
|
||||||
|
}, [leftArticleContent, articleAutocompleteProps.options]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const selectedPreviewArticle = articleAutocompleteProps.options.find((option) => option.id === previewArticleContent)
|
const selectedPreviewArticle = articleAutocompleteProps.options.find(
|
||||||
setPreviewArticlePreview(selectedPreviewArticle?.heading || '')
|
(option) => option.id === previewArticleContent
|
||||||
}, [previewArticleContent, articleAutocompleteProps.options])
|
);
|
||||||
|
setPreviewArticlePreview(selectedPreviewArticle?.heading || "");
|
||||||
|
}, [previewArticleContent, articleAutocompleteProps.options]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Create isLoading={formLoading} saveButtonProps={saveButtonProps}>
|
<Create isLoading={formLoading} saveButtonProps={saveButtonProps}>
|
||||||
<Box sx={{display: 'flex', gap: 2}}>
|
<Box sx={{ display: "flex", gap: 2 }}>
|
||||||
{/* Форма создания */}
|
{/* Форма создания */}
|
||||||
<Box component="form" sx={{flex: 1, display: 'flex', flexDirection: 'column'}} autoComplete="off">
|
<Box
|
||||||
|
component="form"
|
||||||
|
sx={{ flex: 1, display: "flex", flexDirection: "column" }}
|
||||||
|
autoComplete="off"
|
||||||
|
>
|
||||||
<TextField
|
<TextField
|
||||||
{...register('name', {
|
{...register("name", {
|
||||||
required: 'Это поле является обязательным',
|
required: "Это поле является обязательным",
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.name}
|
error={!!(errors as any)?.name}
|
||||||
helperText={(errors as any)?.name?.message}
|
helperText={(errors as any)?.name?.message}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{shrink: true}}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="text"
|
type="text"
|
||||||
label={'Название *'}
|
label={"Название *"}
|
||||||
name="name"
|
name="name"
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
{...register('latitude', {
|
{...register("latitude", {
|
||||||
required: 'Это поле является обязательным',
|
required: "Это поле является обязательным",
|
||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.latitude}
|
error={!!(errors as any)?.latitude}
|
||||||
helperText={(errors as any)?.latitude?.message}
|
helperText={(errors as any)?.latitude?.message}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{shrink: true}}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="number"
|
type="number"
|
||||||
label={'Широта *'}
|
label={"Широта *"}
|
||||||
name="latitude"
|
name="latitude"
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
{...register('longitude', {
|
{...register("longitude", {
|
||||||
required: 'Это поле является обязательным',
|
required: "Это поле является обязательным",
|
||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.longitude}
|
error={!!(errors as any)?.longitude}
|
||||||
helperText={(errors as any)?.longitude?.message}
|
helperText={(errors as any)?.longitude?.message}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{shrink: true}}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="number"
|
type="number"
|
||||||
label={'Долгота *'}
|
label={"Долгота *"}
|
||||||
name="longitude"
|
name="longitude"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="city_id"
|
name="city_id"
|
||||||
rules={{required: 'Это поле является обязательным'}}
|
rules={{ required: "Это поле является обязательным" }}
|
||||||
defaultValue={null}
|
defaultValue={null}
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
{...cityAutocompleteProps}
|
{...cityAutocompleteProps}
|
||||||
value={cityAutocompleteProps.options.find((option) => option.id === field.value) || null}
|
value={
|
||||||
|
cityAutocompleteProps.options.find(
|
||||||
|
(option) => option.id === field.value
|
||||||
|
) || null
|
||||||
|
}
|
||||||
onChange={(_, value) => {
|
onChange={(_, value) => {
|
||||||
field.onChange(value?.id || '')
|
field.onChange(value?.id || "");
|
||||||
}}
|
}}
|
||||||
getOptionLabel={(item) => {
|
getOptionLabel={(item) => {
|
||||||
return item ? item.name : ''
|
return item ? item.name : "";
|
||||||
}}
|
}}
|
||||||
isOptionEqualToValue={(option, value) => {
|
isOptionEqualToValue={(option, value) => {
|
||||||
return option.id === value?.id
|
return option.id === value?.id;
|
||||||
}}
|
}}
|
||||||
filterOptions={(options, {inputValue}) => {
|
filterOptions={(options, { inputValue }) => {
|
||||||
return options.filter((option) => option.name.toLowerCase().includes(inputValue.toLowerCase()))
|
return options.filter((option) =>
|
||||||
|
option.name.toLowerCase().includes(inputValue.toLowerCase())
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
renderInput={(params) => <TextField {...params} label="Выберите город" margin="normal" variant="outlined" error={!!errors.city_id} helperText={(errors as any)?.city_id?.message} required />}
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
label="Выберите город"
|
||||||
|
margin="normal"
|
||||||
|
variant="outlined"
|
||||||
|
error={!!errors.city_id}
|
||||||
|
helperText={(errors as any)?.city_id?.message}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -194,23 +251,41 @@ export const SightCreate = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
name="thumbnail"
|
name="thumbnail"
|
||||||
defaultValue={null}
|
defaultValue={null}
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
{...mediaAutocompleteProps}
|
{...mediaAutocompleteProps}
|
||||||
value={mediaAutocompleteProps.options.find((option) => option.id === field.value) || null}
|
value={
|
||||||
|
mediaAutocompleteProps.options.find(
|
||||||
|
(option) => option.id === field.value
|
||||||
|
) || null
|
||||||
|
}
|
||||||
onChange={(_, value) => {
|
onChange={(_, value) => {
|
||||||
field.onChange(value?.id || '')
|
field.onChange(value?.id || "");
|
||||||
}}
|
}}
|
||||||
getOptionLabel={(item) => {
|
getOptionLabel={(item) => {
|
||||||
return item ? item.media_name : ''
|
return item ? item.media_name : "";
|
||||||
}}
|
}}
|
||||||
isOptionEqualToValue={(option, value) => {
|
isOptionEqualToValue={(option, value) => {
|
||||||
return option.id === value?.id
|
return option.id === value?.id;
|
||||||
}}
|
}}
|
||||||
filterOptions={(options, {inputValue}) => {
|
filterOptions={(options, { inputValue }) => {
|
||||||
return options.filter((option) => option.media_name.toLowerCase().includes(inputValue.toLowerCase()))
|
return options.filter((option) =>
|
||||||
|
option.media_name
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(inputValue.toLowerCase())
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
renderInput={(params) => <TextField {...params} label="Выберите обложку" margin="normal" variant="outlined" error={!!errors.arms} helperText={(errors as any)?.arms?.message} required />}
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
label="Выберите обложку"
|
||||||
|
margin="normal"
|
||||||
|
variant="outlined"
|
||||||
|
error={!!errors.arms}
|
||||||
|
helperText={(errors as any)?.arms?.message}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -219,23 +294,41 @@ export const SightCreate = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
name="watermark_lu"
|
name="watermark_lu"
|
||||||
defaultValue={null}
|
defaultValue={null}
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
{...mediaAutocompleteProps}
|
{...mediaAutocompleteProps}
|
||||||
value={mediaAutocompleteProps.options.find((option) => option.id === field.value) || null}
|
value={
|
||||||
|
mediaAutocompleteProps.options.find(
|
||||||
|
(option) => option.id === field.value
|
||||||
|
) || null
|
||||||
|
}
|
||||||
onChange={(_, value) => {
|
onChange={(_, value) => {
|
||||||
field.onChange(value?.id || '')
|
field.onChange(value?.id || "");
|
||||||
}}
|
}}
|
||||||
getOptionLabel={(item) => {
|
getOptionLabel={(item) => {
|
||||||
return item ? item.media_name : ''
|
return item ? item.media_name : "";
|
||||||
}}
|
}}
|
||||||
isOptionEqualToValue={(option, value) => {
|
isOptionEqualToValue={(option, value) => {
|
||||||
return option.id === value?.id
|
return option.id === value?.id;
|
||||||
}}
|
}}
|
||||||
filterOptions={(options, {inputValue}) => {
|
filterOptions={(options, { inputValue }) => {
|
||||||
return options.filter((option) => option.media_name.toLowerCase().includes(inputValue.toLowerCase()))
|
return options.filter((option) =>
|
||||||
|
option.media_name
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(inputValue.toLowerCase())
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
renderInput={(params) => <TextField {...params} label="Выберите водный знак (Левый верх)" margin="normal" variant="outlined" error={!!errors.arms} helperText={(errors as any)?.arms?.message} required />}
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
label="Выберите водный знак (Левый верх)"
|
||||||
|
margin="normal"
|
||||||
|
variant="outlined"
|
||||||
|
error={!!errors.arms}
|
||||||
|
helperText={(errors as any)?.arms?.message}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -244,23 +337,41 @@ export const SightCreate = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
name="watermark_rd"
|
name="watermark_rd"
|
||||||
defaultValue={null}
|
defaultValue={null}
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
{...mediaAutocompleteProps}
|
{...mediaAutocompleteProps}
|
||||||
value={mediaAutocompleteProps.options.find((option) => option.id === field.value) || null}
|
value={
|
||||||
|
mediaAutocompleteProps.options.find(
|
||||||
|
(option) => option.id === field.value
|
||||||
|
) || null
|
||||||
|
}
|
||||||
onChange={(_, value) => {
|
onChange={(_, value) => {
|
||||||
field.onChange(value?.id || '')
|
field.onChange(value?.id || "");
|
||||||
}}
|
}}
|
||||||
getOptionLabel={(item) => {
|
getOptionLabel={(item) => {
|
||||||
return item ? item.media_name : ''
|
return item ? item.media_name : "";
|
||||||
}}
|
}}
|
||||||
isOptionEqualToValue={(option, value) => {
|
isOptionEqualToValue={(option, value) => {
|
||||||
return option.id === value?.id
|
return option.id === value?.id;
|
||||||
}}
|
}}
|
||||||
filterOptions={(options, {inputValue}) => {
|
filterOptions={(options, { inputValue }) => {
|
||||||
return options.filter((option) => option.media_name.toLowerCase().includes(inputValue.toLowerCase()))
|
return options.filter((option) =>
|
||||||
|
option.media_name
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(inputValue.toLowerCase())
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
renderInput={(params) => <TextField {...params} label="Выберите водный знак (Правый низ)" margin="normal" variant="outlined" error={!!errors.arms} helperText={(errors as any)?.arms?.message} required />}
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
label="Выберите водный знак (Правый низ)"
|
||||||
|
margin="normal"
|
||||||
|
variant="outlined"
|
||||||
|
error={!!errors.arms}
|
||||||
|
helperText={(errors as any)?.arms?.message}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -269,23 +380,41 @@ export const SightCreate = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
name="left_article"
|
name="left_article"
|
||||||
defaultValue={null}
|
defaultValue={null}
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
{...articleAutocompleteProps}
|
{...articleAutocompleteProps}
|
||||||
value={articleAutocompleteProps.options.find((option) => option.id === field.value) || null}
|
value={
|
||||||
|
articleAutocompleteProps.options.find(
|
||||||
|
(option) => option.id === field.value
|
||||||
|
) || null
|
||||||
|
}
|
||||||
onChange={(_, value) => {
|
onChange={(_, value) => {
|
||||||
field.onChange(value?.id || '')
|
field.onChange(value?.id || "");
|
||||||
}}
|
}}
|
||||||
getOptionLabel={(item) => {
|
getOptionLabel={(item) => {
|
||||||
return item ? item.heading : ''
|
return item ? item.heading : "";
|
||||||
}}
|
}}
|
||||||
isOptionEqualToValue={(option, value) => {
|
isOptionEqualToValue={(option, value) => {
|
||||||
return option.id === value?.id
|
return option.id === value?.id;
|
||||||
}}
|
}}
|
||||||
filterOptions={(options, {inputValue}) => {
|
filterOptions={(options, { inputValue }) => {
|
||||||
return options.filter((option) => option.heading.toLowerCase().includes(inputValue.toLowerCase()))
|
return options.filter((option) =>
|
||||||
|
option.heading
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(inputValue.toLowerCase())
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
renderInput={(params) => <TextField {...params} label="Левая статья" margin="normal" variant="outlined" error={!!errors.arms} helperText={(errors as any)?.arms?.message} required />}
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
label="Левая статья"
|
||||||
|
margin="normal"
|
||||||
|
variant="outlined"
|
||||||
|
error={!!errors.arms}
|
||||||
|
helperText={(errors as any)?.arms?.message}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -294,23 +423,41 @@ export const SightCreate = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
name="preview_article"
|
name="preview_article"
|
||||||
defaultValue={null}
|
defaultValue={null}
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
{...articleAutocompleteProps}
|
{...articleAutocompleteProps}
|
||||||
value={articleAutocompleteProps.options.find((option) => option.id === field.value) || null}
|
value={
|
||||||
|
articleAutocompleteProps.options.find(
|
||||||
|
(option) => option.id === field.value
|
||||||
|
) || null
|
||||||
|
}
|
||||||
onChange={(_, value) => {
|
onChange={(_, value) => {
|
||||||
field.onChange(value?.id || '')
|
field.onChange(value?.id || "");
|
||||||
}}
|
}}
|
||||||
getOptionLabel={(item) => {
|
getOptionLabel={(item) => {
|
||||||
return item ? item.heading : ''
|
return item ? item.heading : "";
|
||||||
}}
|
}}
|
||||||
isOptionEqualToValue={(option, value) => {
|
isOptionEqualToValue={(option, value) => {
|
||||||
return option.id === value?.id
|
return option.id === value?.id;
|
||||||
}}
|
}}
|
||||||
filterOptions={(options, {inputValue}) => {
|
filterOptions={(options, { inputValue }) => {
|
||||||
return options.filter((option) => option.heading.toLowerCase().includes(inputValue.toLowerCase()))
|
return options.filter((option) =>
|
||||||
|
option.heading
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(inputValue.toLowerCase())
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
renderInput={(params) => <TextField {...params} label="Cтатья-предпросмотр" margin="normal" variant="outlined" error={!!errors.arms} helperText={(errors as any)?.arms?.message} required />}
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
label="Cтатья-предпросмотр"
|
||||||
|
margin="normal"
|
||||||
|
variant="outlined"
|
||||||
|
error={!!errors.arms}
|
||||||
|
helperText={(errors as any)?.arms?.message}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -321,14 +468,15 @@ export const SightCreate = () => {
|
|||||||
sx={{
|
sx={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
p: 2,
|
p: 2,
|
||||||
maxHeight: 'calc(100vh - 200px)',
|
maxHeight: "calc(100vh - 200px)",
|
||||||
overflowY: 'auto',
|
overflowY: "auto",
|
||||||
position: 'sticky',
|
position: "sticky",
|
||||||
top: 16,
|
top: 16,
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
border: '1px solid',
|
border: "1px solid",
|
||||||
borderColor: 'primary.main',
|
borderColor: "primary.main",
|
||||||
bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'background.paper' : '#fff'),
|
bgcolor: (theme) =>
|
||||||
|
theme.palette.mode === "dark" ? "background.paper" : "#fff",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="h6" gutterBottom color="primary">
|
<Typography variant="h6" gutterBottom color="primary">
|
||||||
@ -336,34 +484,57 @@ export const SightCreate = () => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{/* Название */}
|
{/* Название */}
|
||||||
<Typography variant="h4" gutterBottom sx={{color: (theme) => (theme.palette.mode === 'dark' ? 'grey.300' : 'grey.800')}}>
|
<Typography
|
||||||
|
variant="h4"
|
||||||
|
gutterBottom
|
||||||
|
sx={{
|
||||||
|
color: (theme) =>
|
||||||
|
theme.palette.mode === "dark" ? "grey.300" : "grey.800",
|
||||||
|
}}
|
||||||
|
>
|
||||||
{namePreview}
|
{namePreview}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{/* Город */}
|
{/* Город */}
|
||||||
<Typography variant="body1" sx={{mb: 2}}>
|
<Typography variant="body1" sx={{ mb: 2 }}>
|
||||||
<Box component="span" sx={{color: 'text.secondary'}}>
|
<Box component="span" sx={{ color: "text.secondary" }}>
|
||||||
Город:{' '}
|
Город:{" "}
|
||||||
</Box>
|
</Box>
|
||||||
<Box component="span" sx={{color: (theme) => (theme.palette.mode === 'dark' ? 'grey.300' : 'grey.800')}}>
|
<Box
|
||||||
|
component="span"
|
||||||
|
sx={{
|
||||||
|
color: (theme) =>
|
||||||
|
theme.palette.mode === "dark" ? "grey.300" : "grey.800",
|
||||||
|
}}
|
||||||
|
>
|
||||||
{cityPreview}
|
{cityPreview}
|
||||||
</Box>
|
</Box>
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{/* Координаты */}
|
{/* Координаты */}
|
||||||
<Typography variant="body1" sx={{mb: 2}}>
|
<Typography variant="body1" sx={{ mb: 2 }}>
|
||||||
<Box component="span" sx={{color: 'text.secondary'}}>
|
<Box component="span" sx={{ color: "text.secondary" }}>
|
||||||
Координаты:{' '}
|
Координаты:{" "}
|
||||||
</Box>
|
</Box>
|
||||||
<Box component="span" sx={{color: (theme) => (theme.palette.mode === 'dark' ? 'grey.300' : 'grey.800')}}>
|
<Box
|
||||||
|
component="span"
|
||||||
|
sx={{
|
||||||
|
color: (theme) =>
|
||||||
|
theme.palette.mode === "dark" ? "grey.300" : "grey.800",
|
||||||
|
}}
|
||||||
|
>
|
||||||
{coordinatesPreview.latitude}, {coordinatesPreview.longitude}
|
{coordinatesPreview.latitude}, {coordinatesPreview.longitude}
|
||||||
</Box>
|
</Box>
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{/* Обложка */}
|
{/* Обложка */}
|
||||||
{thumbnailPreview && (
|
{thumbnailPreview && (
|
||||||
<Box sx={{mb: 2}}>
|
<Box sx={{ mb: 2 }}>
|
||||||
<Typography variant="body1" gutterBottom sx={{color: 'text.secondary'}}>
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
gutterBottom
|
||||||
|
sx={{ color: "text.secondary" }}
|
||||||
|
>
|
||||||
Обложка:
|
Обложка:
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box
|
<Box
|
||||||
@ -371,25 +542,33 @@ export const SightCreate = () => {
|
|||||||
src={thumbnailPreview}
|
src={thumbnailPreview}
|
||||||
alt="Обложка"
|
alt="Обложка"
|
||||||
sx={{
|
sx={{
|
||||||
maxWidth: '100%',
|
maxWidth: "100%",
|
||||||
height: 'auto',
|
height: "auto",
|
||||||
borderRadius: 1,
|
borderRadius: 1,
|
||||||
border: '1px solid',
|
border: "1px solid",
|
||||||
borderColor: 'primary.main',
|
borderColor: "primary.main",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Водяные знаки */}
|
{/* Водяные знаки */}
|
||||||
<Box sx={{mb: 2}}>
|
<Box sx={{ mb: 2 }}>
|
||||||
<Typography variant="body1" gutterBottom sx={{color: 'text.secondary'}}>
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
gutterBottom
|
||||||
|
sx={{ color: "text.secondary" }}
|
||||||
|
>
|
||||||
Водяные знаки:
|
Водяные знаки:
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box sx={{display: 'flex', gap: 2}}>
|
<Box sx={{ display: "flex", gap: 2 }}>
|
||||||
{watermarkLUPreview && (
|
{watermarkLUPreview && (
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="body2" gutterBottom sx={{color: 'text.secondary'}}>
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
gutterBottom
|
||||||
|
sx={{ color: "text.secondary" }}
|
||||||
|
>
|
||||||
Левый верхний:
|
Левый верхний:
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box
|
<Box
|
||||||
@ -399,17 +578,21 @@ export const SightCreate = () => {
|
|||||||
sx={{
|
sx={{
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 100,
|
height: 100,
|
||||||
objectFit: 'cover',
|
objectFit: "cover",
|
||||||
borderRadius: 1,
|
borderRadius: 1,
|
||||||
border: '1px solid',
|
border: "1px solid",
|
||||||
borderColor: 'primary.main',
|
borderColor: "primary.main",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
{watermarkRDPreview && (
|
{watermarkRDPreview && (
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="body2" gutterBottom sx={{color: 'text.secondary'}}>
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
gutterBottom
|
||||||
|
sx={{ color: "text.secondary" }}
|
||||||
|
>
|
||||||
Правый нижний:
|
Правый нижний:
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box
|
<Box
|
||||||
@ -419,10 +602,10 @@ export const SightCreate = () => {
|
|||||||
sx={{
|
sx={{
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 100,
|
height: 100,
|
||||||
objectFit: 'cover',
|
objectFit: "cover",
|
||||||
borderRadius: 1,
|
borderRadius: 1,
|
||||||
border: '1px solid',
|
border: "1px solid",
|
||||||
borderColor: 'primary.main',
|
borderColor: "primary.main",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
@ -432,22 +615,27 @@ export const SightCreate = () => {
|
|||||||
|
|
||||||
{/* Связанные статьи */}
|
{/* Связанные статьи */}
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="body1" gutterBottom sx={{color: 'text.secondary'}}>
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
gutterBottom
|
||||||
|
sx={{ color: "text.secondary" }}
|
||||||
|
>
|
||||||
Связанные статьи:
|
Связанные статьи:
|
||||||
</Typography>
|
</Typography>
|
||||||
{leftArticlePreview && (
|
{leftArticlePreview && (
|
||||||
<Typography variant="body1" gutterBottom>
|
<Typography variant="body1" gutterBottom>
|
||||||
<Box component="span" sx={{color: 'text.secondary'}}>
|
<Box component="span" sx={{ color: "text.secondary" }}>
|
||||||
Левая статья:{' '}
|
Левая статья:{" "}
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
component={Link}
|
component={Link}
|
||||||
to={`/article/show/${watch('left_article')}`}
|
to={`/article/show/${watch("left_article")}`}
|
||||||
sx={{
|
sx={{
|
||||||
color: (theme) => (theme.palette.mode === 'dark' ? 'grey.300' : 'grey.800'),
|
color: (theme) =>
|
||||||
textDecoration: 'none',
|
theme.palette.mode === "dark" ? "grey.300" : "grey.800",
|
||||||
'&:hover': {
|
textDecoration: "none",
|
||||||
textDecoration: 'underline',
|
"&:hover": {
|
||||||
|
textDecoration: "underline",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -457,17 +645,18 @@ export const SightCreate = () => {
|
|||||||
)}
|
)}
|
||||||
{previewArticlePreview && (
|
{previewArticlePreview && (
|
||||||
<Typography variant="body1" gutterBottom>
|
<Typography variant="body1" gutterBottom>
|
||||||
<Box component="span" sx={{color: 'text.secondary'}}>
|
<Box component="span" sx={{ color: "text.secondary" }}>
|
||||||
Статья-предпросмотр:{' '}
|
Статья-предпросмотр:{" "}
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
component={Link}
|
component={Link}
|
||||||
to={`/article/show/${watch('preview_article')}`}
|
to={`/article/show/${watch("preview_article")}`}
|
||||||
sx={{
|
sx={{
|
||||||
color: (theme) => (theme.palette.mode === 'dark' ? 'grey.300' : 'grey.800'),
|
color: (theme) =>
|
||||||
textDecoration: 'none',
|
theme.palette.mode === "dark" ? "grey.300" : "grey.800",
|
||||||
'&:hover': {
|
textDecoration: "none",
|
||||||
textDecoration: 'underline',
|
"&:hover": {
|
||||||
|
textDecoration: "underline",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -479,5 +668,5 @@ export const SightCreate = () => {
|
|||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
</Create>
|
</Create>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
@ -1,194 +1,248 @@
|
|||||||
import {Autocomplete, Box, TextField, Paper, Typography} from '@mui/material'
|
import { Autocomplete, Box, TextField, Paper, Typography } from "@mui/material";
|
||||||
import {Edit, useAutocomplete} from '@refinedev/mui'
|
import { Edit, useAutocomplete } from "@refinedev/mui";
|
||||||
import {useForm} from '@refinedev/react-hook-form'
|
import { useForm } from "@refinedev/react-hook-form";
|
||||||
import {Controller} from 'react-hook-form'
|
import { Controller } from "react-hook-form";
|
||||||
import {useParams} from 'react-router'
|
import { useParams } from "react-router";
|
||||||
import React, {useState, useEffect} from 'react'
|
import React, { useState, useEffect } from "react";
|
||||||
import {LinkedItems} from '../../components/LinkedItems'
|
import { LinkedItems } from "../../components/LinkedItems";
|
||||||
import {CreateSightArticle} from '../../components/CreateSightArticle'
|
import { CreateSightArticle } from "../../components/CreateSightArticle";
|
||||||
import {ArticleItem, articleFields} from './types'
|
import { ArticleItem, articleFields } from "./types";
|
||||||
import {TOKEN_KEY} from '../../authProvider'
|
import { TOKEN_KEY } from "../../authProvider";
|
||||||
import {Link} from 'react-router'
|
import { Link } from "react-router";
|
||||||
|
|
||||||
export const SightEdit = () => {
|
export const SightEdit = () => {
|
||||||
const {id: sightId} = useParams<{id: string}>()
|
const { id: sightId } = useParams<{ id: string }>();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
saveButtonProps,
|
saveButtonProps,
|
||||||
register,
|
register,
|
||||||
control,
|
control,
|
||||||
watch,
|
watch,
|
||||||
formState: {errors},
|
formState: { errors },
|
||||||
} = useForm({})
|
} = useForm({});
|
||||||
|
|
||||||
const {autocompleteProps: cityAutocompleteProps} = useAutocomplete({
|
const { autocompleteProps: cityAutocompleteProps } = useAutocomplete({
|
||||||
resource: 'city',
|
resource: "city",
|
||||||
onSearch: (value) => [
|
onSearch: (value) => [
|
||||||
{
|
{
|
||||||
field: 'name',
|
field: "name",
|
||||||
operator: 'contains',
|
operator: "contains",
|
||||||
value,
|
value,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
});
|
||||||
|
|
||||||
const {autocompleteProps: mediaAutocompleteProps} = useAutocomplete({
|
const { autocompleteProps: mediaAutocompleteProps } = useAutocomplete({
|
||||||
resource: 'media',
|
resource: "media",
|
||||||
onSearch: (value) => [
|
onSearch: (value) => [
|
||||||
{
|
{
|
||||||
field: 'media_name',
|
field: "media_name",
|
||||||
operator: 'contains',
|
operator: "contains",
|
||||||
value,
|
value,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
});
|
||||||
|
|
||||||
const {autocompleteProps: articleAutocompleteProps} = useAutocomplete({
|
const { autocompleteProps: articleAutocompleteProps } = useAutocomplete({
|
||||||
resource: 'article',
|
resource: "article",
|
||||||
onSearch: (value) => [
|
onSearch: (value) => [
|
||||||
{
|
{
|
||||||
field: 'heading',
|
field: "heading",
|
||||||
operator: 'contains',
|
operator: "contains",
|
||||||
value,
|
value,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
});
|
||||||
|
|
||||||
// Состояния для предпросмотра
|
// Состояния для предпросмотра
|
||||||
const [namePreview, setNamePreview] = useState('')
|
const [namePreview, setNamePreview] = useState("");
|
||||||
const [coordinatesPreview, setCoordinatesPreview] = useState({
|
const [coordinatesPreview, setCoordinatesPreview] = useState({
|
||||||
latitude: '',
|
latitude: "",
|
||||||
longitude: '',
|
longitude: "",
|
||||||
})
|
});
|
||||||
const [cityPreview, setCityPreview] = useState('')
|
const [cityPreview, setCityPreview] = useState("");
|
||||||
const [thumbnailPreview, setThumbnailPreview] = useState<string | null>(null)
|
const [thumbnailPreview, setThumbnailPreview] = useState<string | null>(null);
|
||||||
const [watermarkLUPreview, setWatermarkLUPreview] = useState<string | null>(null)
|
const [watermarkLUPreview, setWatermarkLUPreview] = useState<string | null>(
|
||||||
const [watermarkRDPreview, setWatermarkRDPreview] = useState<string | null>(null)
|
null
|
||||||
const [leftArticlePreview, setLeftArticlePreview] = useState('')
|
);
|
||||||
const [previewArticlePreview, setPreviewArticlePreview] = useState('')
|
const [watermarkRDPreview, setWatermarkRDPreview] = useState<string | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [leftArticlePreview, setLeftArticlePreview] = useState("");
|
||||||
|
const [previewArticlePreview, setPreviewArticlePreview] = useState("");
|
||||||
|
|
||||||
// Следим за изменениями во всех полях
|
// Следим за изменениями во всех полях
|
||||||
const nameContent = watch('name')
|
const nameContent = watch("name");
|
||||||
const latitudeContent = watch('latitude')
|
const latitudeContent = watch("latitude");
|
||||||
const longitudeContent = watch('longitude')
|
const longitudeContent = watch("longitude");
|
||||||
const cityContent = watch('city_id')
|
const cityContent = watch("city_id");
|
||||||
const thumbnailContent = watch('thumbnail')
|
const thumbnailContent = watch("thumbnail");
|
||||||
const watermarkLUContent = watch('watermark_lu')
|
const watermarkLUContent = watch("watermark_lu");
|
||||||
const watermarkRDContent = watch('watermark_rd')
|
const watermarkRDContent = watch("watermark_rd");
|
||||||
const leftArticleContent = watch('left_article')
|
const leftArticleContent = watch("left_article");
|
||||||
const previewArticleContent = watch('preview_article')
|
const previewArticleContent = watch("preview_article");
|
||||||
|
|
||||||
// Обновляем состояния при изменении полей
|
// Обновляем состояния при изменении полей
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setNamePreview(nameContent || '')
|
setNamePreview(nameContent || "");
|
||||||
}, [nameContent])
|
}, [nameContent]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCoordinatesPreview({
|
setCoordinatesPreview({
|
||||||
latitude: latitudeContent || '',
|
latitude: latitudeContent || "",
|
||||||
longitude: longitudeContent || '',
|
longitude: longitudeContent || "",
|
||||||
})
|
});
|
||||||
}, [latitudeContent, longitudeContent])
|
}, [latitudeContent, longitudeContent]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const selectedCity = cityAutocompleteProps.options.find((option) => option.id === cityContent)
|
const selectedCity = cityAutocompleteProps.options.find(
|
||||||
setCityPreview(selectedCity?.name || '')
|
(option) => option.id === cityContent
|
||||||
}, [cityContent, cityAutocompleteProps.options])
|
);
|
||||||
|
setCityPreview(selectedCity?.name || "");
|
||||||
|
}, [cityContent, cityAutocompleteProps.options]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const selectedThumbnail = mediaAutocompleteProps.options.find((option) => option.id === thumbnailContent)
|
const selectedThumbnail = mediaAutocompleteProps.options.find(
|
||||||
setThumbnailPreview(selectedThumbnail ? `https://wn.krbl.ru/media/${selectedThumbnail.id}/download?token=${localStorage.getItem(TOKEN_KEY)}` : null)
|
(option) => option.id === thumbnailContent
|
||||||
}, [thumbnailContent, mediaAutocompleteProps.options])
|
);
|
||||||
|
setThumbnailPreview(
|
||||||
|
selectedThumbnail
|
||||||
|
? `${import.meta.env.VITE_KRBL_MEDIA}${
|
||||||
|
selectedThumbnail.id
|
||||||
|
}/download?token=${localStorage.getItem(TOKEN_KEY)}`
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
}, [thumbnailContent, mediaAutocompleteProps.options]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const selectedWatermarkLU = mediaAutocompleteProps.options.find((option) => option.id === watermarkLUContent)
|
const selectedWatermarkLU = mediaAutocompleteProps.options.find(
|
||||||
setWatermarkLUPreview(selectedWatermarkLU ? `https://wn.krbl.ru/media/${selectedWatermarkLU.id}/download?token=${localStorage.getItem(TOKEN_KEY)}` : null)
|
(option) => option.id === watermarkLUContent
|
||||||
}, [watermarkLUContent, mediaAutocompleteProps.options])
|
);
|
||||||
|
setWatermarkLUPreview(
|
||||||
|
selectedWatermarkLU
|
||||||
|
? `${import.meta.env.VITE_KRBL_MEDIA}${
|
||||||
|
selectedWatermarkLU.id
|
||||||
|
}/download?token=${localStorage.getItem(TOKEN_KEY)}`
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
}, [watermarkLUContent, mediaAutocompleteProps.options]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const selectedWatermarkRD = mediaAutocompleteProps.options.find((option) => option.id === watermarkRDContent)
|
const selectedWatermarkRD = mediaAutocompleteProps.options.find(
|
||||||
setWatermarkRDPreview(selectedWatermarkRD ? `https://wn.krbl.ru/media/${selectedWatermarkRD.id}/download?token=${localStorage.getItem(TOKEN_KEY)}` : null)
|
(option) => option.id === watermarkRDContent
|
||||||
}, [watermarkRDContent, mediaAutocompleteProps.options])
|
);
|
||||||
|
setWatermarkRDPreview(
|
||||||
|
selectedWatermarkRD
|
||||||
|
? `${import.meta.env.VITE_KRBL_MEDIA}${
|
||||||
|
selectedWatermarkRD.id
|
||||||
|
}/download?token=${localStorage.getItem(TOKEN_KEY)}`
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
}, [watermarkRDContent, mediaAutocompleteProps.options]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const selectedLeftArticle = articleAutocompleteProps.options.find((option) => option.id === leftArticleContent)
|
const selectedLeftArticle = articleAutocompleteProps.options.find(
|
||||||
setLeftArticlePreview(selectedLeftArticle?.heading || '')
|
(option) => option.id === leftArticleContent
|
||||||
}, [leftArticleContent, articleAutocompleteProps.options])
|
);
|
||||||
|
setLeftArticlePreview(selectedLeftArticle?.heading || "");
|
||||||
|
}, [leftArticleContent, articleAutocompleteProps.options]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const selectedPreviewArticle = articleAutocompleteProps.options.find((option) => option.id === previewArticleContent)
|
const selectedPreviewArticle = articleAutocompleteProps.options.find(
|
||||||
setPreviewArticlePreview(selectedPreviewArticle?.heading || '')
|
(option) => option.id === previewArticleContent
|
||||||
}, [previewArticleContent, articleAutocompleteProps.options])
|
);
|
||||||
|
setPreviewArticlePreview(selectedPreviewArticle?.heading || "");
|
||||||
|
}, [previewArticleContent, articleAutocompleteProps.options]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Edit saveButtonProps={saveButtonProps}>
|
<Edit saveButtonProps={saveButtonProps}>
|
||||||
<Box sx={{display: 'flex', gap: 2}}>
|
<Box sx={{ display: "flex", gap: 2 }}>
|
||||||
{/* Форма редактирования */}
|
{/* Форма редактирования */}
|
||||||
<Box component="form" sx={{flex: 1, display: 'flex', flexDirection: 'column'}} autoComplete="off">
|
<Box
|
||||||
|
component="form"
|
||||||
|
sx={{ flex: 1, display: "flex", flexDirection: "column" }}
|
||||||
|
autoComplete="off"
|
||||||
|
>
|
||||||
<TextField
|
<TextField
|
||||||
{...register('name', {
|
{...register("name", {
|
||||||
required: 'Это поле является обязательным',
|
required: "Это поле является обязательным",
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.name}
|
error={!!(errors as any)?.name}
|
||||||
helperText={(errors as any)?.name?.message}
|
helperText={(errors as any)?.name?.message}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{shrink: true}}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="text"
|
type="text"
|
||||||
label={'Название *'}
|
label={"Название *"}
|
||||||
name="name"
|
name="name"
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
{...register('latitude', {
|
{...register("latitude", {
|
||||||
required: 'Это поле является обязательным',
|
required: "Это поле является обязательным",
|
||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.latitude}
|
error={!!(errors as any)?.latitude}
|
||||||
helperText={(errors as any)?.latitude?.message}
|
helperText={(errors as any)?.latitude?.message}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{shrink: true}}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="number"
|
type="number"
|
||||||
label={'Широта *'}
|
label={"Широта *"}
|
||||||
name="latitude"
|
name="latitude"
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
{...register('longitude', {
|
{...register("longitude", {
|
||||||
required: 'Это поле является обязательным',
|
required: "Это поле является обязательным",
|
||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.longitude}
|
error={!!(errors as any)?.longitude}
|
||||||
helperText={(errors as any)?.longitude?.message}
|
helperText={(errors as any)?.longitude?.message}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{shrink: true}}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="number"
|
type="number"
|
||||||
label={'Долгота *'}
|
label={"Долгота *"}
|
||||||
name="longitude"
|
name="longitude"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="city_id"
|
name="city_id"
|
||||||
rules={{required: 'Это поле является обязательным'}}
|
rules={{ required: "Это поле является обязательным" }}
|
||||||
defaultValue={null}
|
defaultValue={null}
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
{...cityAutocompleteProps}
|
{...cityAutocompleteProps}
|
||||||
value={cityAutocompleteProps.options.find((option) => option.id === field.value) || null}
|
value={
|
||||||
|
cityAutocompleteProps.options.find(
|
||||||
|
(option) => option.id === field.value
|
||||||
|
) || null
|
||||||
|
}
|
||||||
onChange={(_, value) => {
|
onChange={(_, value) => {
|
||||||
field.onChange(value?.id || '')
|
field.onChange(value?.id || "");
|
||||||
}}
|
}}
|
||||||
getOptionLabel={(item) => {
|
getOptionLabel={(item) => {
|
||||||
return item ? item.name : ''
|
return item ? item.name : "";
|
||||||
}}
|
}}
|
||||||
isOptionEqualToValue={(option, value) => {
|
isOptionEqualToValue={(option, value) => {
|
||||||
return option.id === value?.id
|
return option.id === value?.id;
|
||||||
}}
|
}}
|
||||||
filterOptions={(options, {inputValue}) => {
|
filterOptions={(options, { inputValue }) => {
|
||||||
return options.filter((option) => option.name.toLowerCase().includes(inputValue.toLowerCase()))
|
return options.filter((option) =>
|
||||||
|
option.name.toLowerCase().includes(inputValue.toLowerCase())
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
renderInput={(params) => <TextField {...params} label="Выберите город" margin="normal" variant="outlined" error={!!errors.city_id} helperText={(errors as any)?.city_id?.message} required />}
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
label="Выберите город"
|
||||||
|
margin="normal"
|
||||||
|
variant="outlined"
|
||||||
|
error={!!errors.city_id}
|
||||||
|
helperText={(errors as any)?.city_id?.message}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -197,23 +251,41 @@ export const SightEdit = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
name="thumbnail"
|
name="thumbnail"
|
||||||
defaultValue={null}
|
defaultValue={null}
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
{...mediaAutocompleteProps}
|
{...mediaAutocompleteProps}
|
||||||
value={mediaAutocompleteProps.options.find((option) => option.id === field.value) || null}
|
value={
|
||||||
|
mediaAutocompleteProps.options.find(
|
||||||
|
(option) => option.id === field.value
|
||||||
|
) || null
|
||||||
|
}
|
||||||
onChange={(_, value) => {
|
onChange={(_, value) => {
|
||||||
field.onChange(value?.id || '')
|
field.onChange(value?.id || "");
|
||||||
}}
|
}}
|
||||||
getOptionLabel={(item) => {
|
getOptionLabel={(item) => {
|
||||||
return item ? item.media_name : ''
|
return item ? item.media_name : "";
|
||||||
}}
|
}}
|
||||||
isOptionEqualToValue={(option, value) => {
|
isOptionEqualToValue={(option, value) => {
|
||||||
return option.id === value?.id
|
return option.id === value?.id;
|
||||||
}}
|
}}
|
||||||
filterOptions={(options, {inputValue}) => {
|
filterOptions={(options, { inputValue }) => {
|
||||||
return options.filter((option) => option.media_name.toLowerCase().includes(inputValue.toLowerCase()))
|
return options.filter((option) =>
|
||||||
|
option.media_name
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(inputValue.toLowerCase())
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
renderInput={(params) => <TextField {...params} label="Выберите обложку" margin="normal" variant="outlined" error={!!errors.arms} helperText={(errors as any)?.arms?.message} required />}
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
label="Выберите обложку"
|
||||||
|
margin="normal"
|
||||||
|
variant="outlined"
|
||||||
|
error={!!errors.arms}
|
||||||
|
helperText={(errors as any)?.arms?.message}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -222,23 +294,41 @@ export const SightEdit = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
name="watermark_lu"
|
name="watermark_lu"
|
||||||
defaultValue={null}
|
defaultValue={null}
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
{...mediaAutocompleteProps}
|
{...mediaAutocompleteProps}
|
||||||
value={mediaAutocompleteProps.options.find((option) => option.id === field.value) || null}
|
value={
|
||||||
|
mediaAutocompleteProps.options.find(
|
||||||
|
(option) => option.id === field.value
|
||||||
|
) || null
|
||||||
|
}
|
||||||
onChange={(_, value) => {
|
onChange={(_, value) => {
|
||||||
field.onChange(value?.id || '')
|
field.onChange(value?.id || "");
|
||||||
}}
|
}}
|
||||||
getOptionLabel={(item) => {
|
getOptionLabel={(item) => {
|
||||||
return item ? item.media_name : ''
|
return item ? item.media_name : "";
|
||||||
}}
|
}}
|
||||||
isOptionEqualToValue={(option, value) => {
|
isOptionEqualToValue={(option, value) => {
|
||||||
return option.id === value?.id
|
return option.id === value?.id;
|
||||||
}}
|
}}
|
||||||
filterOptions={(options, {inputValue}) => {
|
filterOptions={(options, { inputValue }) => {
|
||||||
return options.filter((option) => option.media_name.toLowerCase().includes(inputValue.toLowerCase()))
|
return options.filter((option) =>
|
||||||
|
option.media_name
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(inputValue.toLowerCase())
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
renderInput={(params) => <TextField {...params} label="Выберите водный знак (Левый верх)" margin="normal" variant="outlined" error={!!errors.arms} helperText={(errors as any)?.arms?.message} required />}
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
label="Выберите водный знак (Левый верх)"
|
||||||
|
margin="normal"
|
||||||
|
variant="outlined"
|
||||||
|
error={!!errors.arms}
|
||||||
|
helperText={(errors as any)?.arms?.message}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -247,23 +337,41 @@ export const SightEdit = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
name="watermark_rd"
|
name="watermark_rd"
|
||||||
defaultValue={null}
|
defaultValue={null}
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
{...mediaAutocompleteProps}
|
{...mediaAutocompleteProps}
|
||||||
value={mediaAutocompleteProps.options.find((option) => option.id === field.value) || null}
|
value={
|
||||||
|
mediaAutocompleteProps.options.find(
|
||||||
|
(option) => option.id === field.value
|
||||||
|
) || null
|
||||||
|
}
|
||||||
onChange={(_, value) => {
|
onChange={(_, value) => {
|
||||||
field.onChange(value?.id || '')
|
field.onChange(value?.id || "");
|
||||||
}}
|
}}
|
||||||
getOptionLabel={(item) => {
|
getOptionLabel={(item) => {
|
||||||
return item ? item.media_name : ''
|
return item ? item.media_name : "";
|
||||||
}}
|
}}
|
||||||
isOptionEqualToValue={(option, value) => {
|
isOptionEqualToValue={(option, value) => {
|
||||||
return option.id === value?.id
|
return option.id === value?.id;
|
||||||
}}
|
}}
|
||||||
filterOptions={(options, {inputValue}) => {
|
filterOptions={(options, { inputValue }) => {
|
||||||
return options.filter((option) => option.media_name.toLowerCase().includes(inputValue.toLowerCase()))
|
return options.filter((option) =>
|
||||||
|
option.media_name
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(inputValue.toLowerCase())
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
renderInput={(params) => <TextField {...params} label="Выберите водный знак (Правый низ)" margin="normal" variant="outlined" error={!!errors.arms} helperText={(errors as any)?.arms?.message} required />}
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
label="Выберите водный знак (Правый низ)"
|
||||||
|
margin="normal"
|
||||||
|
variant="outlined"
|
||||||
|
error={!!errors.arms}
|
||||||
|
helperText={(errors as any)?.arms?.message}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -272,23 +380,41 @@ export const SightEdit = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
name="left_article"
|
name="left_article"
|
||||||
defaultValue={null}
|
defaultValue={null}
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
{...articleAutocompleteProps}
|
{...articleAutocompleteProps}
|
||||||
value={articleAutocompleteProps.options.find((option) => option.id === field.value) || null}
|
value={
|
||||||
|
articleAutocompleteProps.options.find(
|
||||||
|
(option) => option.id === field.value
|
||||||
|
) || null
|
||||||
|
}
|
||||||
onChange={(_, value) => {
|
onChange={(_, value) => {
|
||||||
field.onChange(value?.id || '')
|
field.onChange(value?.id || "");
|
||||||
}}
|
}}
|
||||||
getOptionLabel={(item) => {
|
getOptionLabel={(item) => {
|
||||||
return item ? item.heading : ''
|
return item ? item.heading : "";
|
||||||
}}
|
}}
|
||||||
isOptionEqualToValue={(option, value) => {
|
isOptionEqualToValue={(option, value) => {
|
||||||
return option.id === value?.id
|
return option.id === value?.id;
|
||||||
}}
|
}}
|
||||||
filterOptions={(options, {inputValue}) => {
|
filterOptions={(options, { inputValue }) => {
|
||||||
return options.filter((option) => option.heading.toLowerCase().includes(inputValue.toLowerCase()))
|
return options.filter((option) =>
|
||||||
|
option.heading
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(inputValue.toLowerCase())
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
renderInput={(params) => <TextField {...params} label="Левая статья" margin="normal" variant="outlined" error={!!errors.arms} helperText={(errors as any)?.arms?.message} required />}
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
label="Левая статья"
|
||||||
|
margin="normal"
|
||||||
|
variant="outlined"
|
||||||
|
error={!!errors.arms}
|
||||||
|
helperText={(errors as any)?.arms?.message}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -297,23 +423,41 @@ export const SightEdit = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
name="preview_article"
|
name="preview_article"
|
||||||
defaultValue={null}
|
defaultValue={null}
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
{...articleAutocompleteProps}
|
{...articleAutocompleteProps}
|
||||||
value={articleAutocompleteProps.options.find((option) => option.id === field.value) || null}
|
value={
|
||||||
|
articleAutocompleteProps.options.find(
|
||||||
|
(option) => option.id === field.value
|
||||||
|
) || null
|
||||||
|
}
|
||||||
onChange={(_, value) => {
|
onChange={(_, value) => {
|
||||||
field.onChange(value?.id || '')
|
field.onChange(value?.id || "");
|
||||||
}}
|
}}
|
||||||
getOptionLabel={(item) => {
|
getOptionLabel={(item) => {
|
||||||
return item ? item.heading : ''
|
return item ? item.heading : "";
|
||||||
}}
|
}}
|
||||||
isOptionEqualToValue={(option, value) => {
|
isOptionEqualToValue={(option, value) => {
|
||||||
return option.id === value?.id
|
return option.id === value?.id;
|
||||||
}}
|
}}
|
||||||
filterOptions={(options, {inputValue}) => {
|
filterOptions={(options, { inputValue }) => {
|
||||||
return options.filter((option) => option.heading.toLowerCase().includes(inputValue.toLowerCase()))
|
return options.filter((option) =>
|
||||||
|
option.heading
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(inputValue.toLowerCase())
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
renderInput={(params) => <TextField {...params} label="Cтатья-предпросмотр" margin="normal" variant="outlined" error={!!errors.arms} helperText={(errors as any)?.arms?.message} required />}
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
label="Cтатья-предпросмотр"
|
||||||
|
margin="normal"
|
||||||
|
variant="outlined"
|
||||||
|
error={!!errors.arms}
|
||||||
|
helperText={(errors as any)?.arms?.message}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -324,14 +468,15 @@ export const SightEdit = () => {
|
|||||||
sx={{
|
sx={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
p: 2,
|
p: 2,
|
||||||
maxHeight: 'calc(100vh - 200px)',
|
maxHeight: "calc(100vh - 200px)",
|
||||||
overflowY: 'auto',
|
overflowY: "auto",
|
||||||
position: 'sticky',
|
position: "sticky",
|
||||||
top: 16,
|
top: 16,
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
border: '1px solid',
|
border: "1px solid",
|
||||||
borderColor: 'primary.main',
|
borderColor: "primary.main",
|
||||||
bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'background.paper' : '#fff'),
|
bgcolor: (theme) =>
|
||||||
|
theme.palette.mode === "dark" ? "background.paper" : "#fff",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="h6" gutterBottom color="primary">
|
<Typography variant="h6" gutterBottom color="primary">
|
||||||
@ -343,7 +488,8 @@ export const SightEdit = () => {
|
|||||||
variant="h4"
|
variant="h4"
|
||||||
gutterBottom
|
gutterBottom
|
||||||
sx={{
|
sx={{
|
||||||
color: (theme) => (theme.palette.mode === 'dark' ? 'grey.300' : 'grey.800'),
|
color: (theme) =>
|
||||||
|
theme.palette.mode === "dark" ? "grey.300" : "grey.800",
|
||||||
mb: 3,
|
mb: 3,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -351,29 +497,45 @@ export const SightEdit = () => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{/* Город */}
|
{/* Город */}
|
||||||
<Typography variant="body1" sx={{mb: 2}}>
|
<Typography variant="body1" sx={{ mb: 2 }}>
|
||||||
<Box component="span" sx={{color: 'text.secondary'}}>
|
<Box component="span" sx={{ color: "text.secondary" }}>
|
||||||
Город:{' '}
|
Город:{" "}
|
||||||
</Box>
|
</Box>
|
||||||
<Box component="span" sx={{color: (theme) => (theme.palette.mode === 'dark' ? 'grey.300' : 'grey.800')}}>
|
<Box
|
||||||
|
component="span"
|
||||||
|
sx={{
|
||||||
|
color: (theme) =>
|
||||||
|
theme.palette.mode === "dark" ? "grey.300" : "grey.800",
|
||||||
|
}}
|
||||||
|
>
|
||||||
{cityPreview}
|
{cityPreview}
|
||||||
</Box>
|
</Box>
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{/* Координаты */}
|
{/* Координаты */}
|
||||||
<Typography variant="body1" sx={{mb: 2}}>
|
<Typography variant="body1" sx={{ mb: 2 }}>
|
||||||
<Box component="span" sx={{color: 'text.secondary'}}>
|
<Box component="span" sx={{ color: "text.secondary" }}>
|
||||||
Координаты:{' '}
|
Координаты:{" "}
|
||||||
</Box>
|
</Box>
|
||||||
<Box component="span" sx={{color: (theme) => (theme.palette.mode === 'dark' ? 'grey.300' : 'grey.800')}}>
|
<Box
|
||||||
|
component="span"
|
||||||
|
sx={{
|
||||||
|
color: (theme) =>
|
||||||
|
theme.palette.mode === "dark" ? "grey.300" : "grey.800",
|
||||||
|
}}
|
||||||
|
>
|
||||||
{coordinatesPreview.latitude}, {coordinatesPreview.longitude}
|
{coordinatesPreview.latitude}, {coordinatesPreview.longitude}
|
||||||
</Box>
|
</Box>
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{/* Обложка */}
|
{/* Обложка */}
|
||||||
{thumbnailPreview && (
|
{thumbnailPreview && (
|
||||||
<Box sx={{mb: 2}}>
|
<Box sx={{ mb: 2 }}>
|
||||||
<Typography variant="body1" gutterBottom sx={{color: 'text.secondary'}}>
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
gutterBottom
|
||||||
|
sx={{ color: "text.secondary" }}
|
||||||
|
>
|
||||||
Обложка:
|
Обложка:
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box
|
<Box
|
||||||
@ -381,25 +543,33 @@ export const SightEdit = () => {
|
|||||||
src={thumbnailPreview}
|
src={thumbnailPreview}
|
||||||
alt="Обложка"
|
alt="Обложка"
|
||||||
sx={{
|
sx={{
|
||||||
maxWidth: '100%',
|
maxWidth: "100%",
|
||||||
height: '40vh',
|
height: "40vh",
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
border: '1px solid',
|
border: "1px solid",
|
||||||
borderColor: 'primary.main',
|
borderColor: "primary.main",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Водяные знаки */}
|
{/* Водяные знаки */}
|
||||||
<Box sx={{mb: 2}}>
|
<Box sx={{ mb: 2 }}>
|
||||||
<Typography variant="body1" gutterBottom sx={{color: 'text.secondary'}}>
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
gutterBottom
|
||||||
|
sx={{ color: "text.secondary" }}
|
||||||
|
>
|
||||||
Водяные знаки:
|
Водяные знаки:
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box sx={{display: 'flex', gap: 2}}>
|
<Box sx={{ display: "flex", gap: 2 }}>
|
||||||
{watermarkLUPreview && (
|
{watermarkLUPreview && (
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="body2" gutterBottom sx={{color: 'text.secondary'}}>
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
gutterBottom
|
||||||
|
sx={{ color: "text.secondary" }}
|
||||||
|
>
|
||||||
Левый верхний:
|
Левый верхний:
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box
|
<Box
|
||||||
@ -409,17 +579,21 @@ export const SightEdit = () => {
|
|||||||
sx={{
|
sx={{
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 100,
|
height: 100,
|
||||||
objectFit: 'cover',
|
objectFit: "cover",
|
||||||
borderRadius: 1,
|
borderRadius: 1,
|
||||||
border: '1px solid',
|
border: "1px solid",
|
||||||
borderColor: 'primary.main',
|
borderColor: "primary.main",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
{watermarkRDPreview && (
|
{watermarkRDPreview && (
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="body2" gutterBottom sx={{color: 'text.secondary'}}>
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
gutterBottom
|
||||||
|
sx={{ color: "text.secondary" }}
|
||||||
|
>
|
||||||
Правый нижний:
|
Правый нижний:
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box
|
<Box
|
||||||
@ -429,10 +603,10 @@ export const SightEdit = () => {
|
|||||||
sx={{
|
sx={{
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 100,
|
height: 100,
|
||||||
objectFit: 'cover',
|
objectFit: "cover",
|
||||||
borderRadius: 1,
|
borderRadius: 1,
|
||||||
border: '1px solid',
|
border: "1px solid",
|
||||||
borderColor: 'primary.main',
|
borderColor: "primary.main",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
@ -447,17 +621,18 @@ export const SightEdit = () => {
|
|||||||
</Typography> */}
|
</Typography> */}
|
||||||
{leftArticlePreview && (
|
{leftArticlePreview && (
|
||||||
<Typography variant="body1" gutterBottom>
|
<Typography variant="body1" gutterBottom>
|
||||||
<Box component="span" sx={{color: 'text.secondary'}}>
|
<Box component="span" sx={{ color: "text.secondary" }}>
|
||||||
Левая статья:{' '}
|
Левая статья:{" "}
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
component={Link}
|
component={Link}
|
||||||
to={`/article/show/${watch('left_article')}`}
|
to={`/article/show/${watch("left_article")}`}
|
||||||
sx={{
|
sx={{
|
||||||
color: (theme) => (theme.palette.mode === 'dark' ? 'grey.300' : 'grey.800'),
|
color: (theme) =>
|
||||||
textDecoration: 'none',
|
theme.palette.mode === "dark" ? "grey.300" : "grey.800",
|
||||||
'&:hover': {
|
textDecoration: "none",
|
||||||
textDecoration: 'underline',
|
"&:hover": {
|
||||||
|
textDecoration: "underline",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -467,17 +642,18 @@ export const SightEdit = () => {
|
|||||||
)}
|
)}
|
||||||
{previewArticlePreview && (
|
{previewArticlePreview && (
|
||||||
<Typography variant="body1" gutterBottom>
|
<Typography variant="body1" gutterBottom>
|
||||||
<Box component="span" sx={{color: 'text.secondary'}}>
|
<Box component="span" sx={{ color: "text.secondary" }}>
|
||||||
Статья-предпросмотр:{' '}
|
Статья-предпросмотр:{" "}
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
component={Link}
|
component={Link}
|
||||||
to={`/article/show/${watch('preview_article')}`}
|
to={`/article/show/${watch("preview_article")}`}
|
||||||
sx={{
|
sx={{
|
||||||
color: (theme) => (theme.palette.mode === 'dark' ? 'grey.300' : 'grey.800'),
|
color: (theme) =>
|
||||||
textDecoration: 'none',
|
theme.palette.mode === "dark" ? "grey.300" : "grey.800",
|
||||||
'&:hover': {
|
textDecoration: "none",
|
||||||
textDecoration: 'underline',
|
"&:hover": {
|
||||||
|
textDecoration: "underline",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -490,12 +666,24 @@ export const SightEdit = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{sightId && (
|
{sightId && (
|
||||||
<Box sx={{mt: 3}}>
|
<Box sx={{ mt: 3 }}>
|
||||||
<LinkedItems<ArticleItem> type="edit" parentId={sightId} parentResource="sight" childResource="article" fields={articleFields} title="статьи" />
|
<LinkedItems<ArticleItem>
|
||||||
|
type="edit"
|
||||||
|
parentId={sightId}
|
||||||
|
parentResource="sight"
|
||||||
|
childResource="article"
|
||||||
|
fields={articleFields}
|
||||||
|
title="статьи"
|
||||||
|
/>
|
||||||
|
|
||||||
<CreateSightArticle parentId={sightId} parentResource="sight" childResource="article" title="статью" />
|
<CreateSightArticle
|
||||||
|
parentId={sightId}
|
||||||
|
parentResource="sight"
|
||||||
|
childResource="article"
|
||||||
|
title="статью"
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Edit>
|
</Edit>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
@ -1,25 +1,27 @@
|
|||||||
import dataProvider from '@refinedev/simple-rest'
|
import dataProvider from "@refinedev/simple-rest";
|
||||||
import axios from 'axios'
|
import axios from "axios";
|
||||||
import {BACKEND_URL} from '../lib/constants'
|
|
||||||
import {TOKEN_KEY} from '../authProvider'
|
|
||||||
import Cookies from 'js-cookie'
|
|
||||||
|
|
||||||
export const axiosInstance = axios.create()
|
import { TOKEN_KEY } from "../authProvider";
|
||||||
|
import Cookies from "js-cookie";
|
||||||
|
|
||||||
|
export const axiosInstance = axios.create();
|
||||||
|
|
||||||
axiosInstance.interceptors.request.use((config) => {
|
axiosInstance.interceptors.request.use((config) => {
|
||||||
// Добавляем токен авторизации
|
// Добавляем токен авторизации
|
||||||
const token = localStorage.getItem(TOKEN_KEY)
|
const token = localStorage.getItem(TOKEN_KEY);
|
||||||
if (token) {
|
if (token) {
|
||||||
config.headers.Authorization = `Bearer ${token}`
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Добавляем язык в кастомный заголовок
|
// Добавляем язык в кастомный заголовок
|
||||||
const lang = Cookies.get('lang') || 'ru'
|
const lang = Cookies.get("lang") || "ru";
|
||||||
config.headers['X-Language'] = lang // или 'Accept-Language'
|
config.headers["X-Language"] = lang; // или 'Accept-Language'
|
||||||
|
|
||||||
// console.log('Request headers:', config.headers)
|
// console.log('Request headers:', config.headers)
|
||||||
|
|
||||||
return config
|
return config;
|
||||||
})
|
});
|
||||||
|
|
||||||
export const customDataProvider = dataProvider(BACKEND_URL, axiosInstance)
|
const apiUrl = import.meta.env.VITE_KRBL_API;
|
||||||
|
|
||||||
|
export const customDataProvider = dataProvider(apiUrl, axiosInstance);
|
||||||
|
Loading…
Reference in New Issue
Block a user