diff --git a/.env b/.env new file mode 100644 index 0000000..82aac97 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +VITE_KRBL_MEDIA = "https://wn.krbl.ru/media/" +VITE_KRBL_API = "https://wn.krbl.ru" diff --git a/src/authProvider.ts b/src/authProvider.ts index 1702796..32760f2 100644 --- a/src/authProvider.ts +++ b/src/authProvider.ts @@ -1,174 +1,182 @@ -import type {AuthProvider} from '@refinedev/core' -import axios, {AxiosError} from 'axios' -import {BACKEND_URL} from './lib/constants' -import {jwtDecode} from 'jwt-decode' +import type { AuthProvider } from "@refinedev/core"; +import axios, { AxiosError } from "axios"; -export const TOKEN_KEY = 'refine-auth' +import { jwtDecode } from "jwt-decode"; + +export const TOKEN_KEY = "refine-auth"; interface AuthResponse { - token: string + token: string; user: { - id: number - name: string - email: string - is_admin: boolean - } + id: number; + name: string; + email: string; + is_admin: boolean; + }; } interface ErrorResponse { - message: string + message: string; } class AuthError extends Error { constructor(message: string) { - super(message) - this.name = 'AuthError' + super(message); + this.name = "AuthError"; } } interface JWTPayload { - user_id: number - email: string - is_admin: boolean - exp: number + user_id: number; + email: string; + is_admin: boolean; + exp: number; } export const authProvider: AuthProvider = { - login: async ({email, password}) => { + login: async ({ email, password }) => { try { - const response = await axios.post(`${BACKEND_URL}/auth/login`, { - email, - password, - }) + const response = await axios.post( + `${import.meta.env.VITE_KRBL_API}/auth/login`, + { + email, + password, + } + ); - const {token, user} = response.data + const { token, user } = response.data; if (token) { - localStorage.setItem(TOKEN_KEY, token) - localStorage.setItem('user', JSON.stringify(user)) + localStorage.setItem(TOKEN_KEY, token); + localStorage.setItem("user", JSON.stringify(user)); return { success: true, - redirectTo: '/', - } + redirectTo: "/", + }; } - throw new AuthError('Неверный email или пароль') + throw new AuthError("Неверный email или пароль"); } catch (error) { - const errorMessage = (error as AxiosError)?.response?.data?.message || 'Неверный email или пароль' + const errorMessage = + (error as AxiosError)?.response?.data?.message || + "Неверный email или пароль"; return { success: false, error: new AuthError(errorMessage), - } + }; } }, logout: async () => { try { await axios.post( - `${BACKEND_URL}/auth/logout`, + `${import.meta.env.VITE_KRBL_API}/auth/logout`, {}, { headers: { Authorization: `Bearer ${localStorage.getItem(TOKEN_KEY)}`, }, - }, - ) + } + ); } catch (error) { - console.error('Ошибка при выходе:', error) + console.error("Ошибка при выходе:", error); } - localStorage.removeItem(TOKEN_KEY) - localStorage.removeItem('user') + localStorage.removeItem(TOKEN_KEY); + localStorage.removeItem("user"); return { success: true, - redirectTo: '/login', - } + redirectTo: "/login", + }; }, check: async () => { - const token = localStorage.getItem(TOKEN_KEY) + const token = localStorage.getItem(TOKEN_KEY); if (!token) { return { authenticated: false, - redirectTo: '/login', - } + redirectTo: "/login", + }; } try { - const response = await axios.get(`${BACKEND_URL}/auth/me`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }) + const response = await axios.get( + `${import.meta.env.VITE_KRBL_API}/auth/me`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); if (response.status === 200) { // Обновляем информацию о пользователе - localStorage.setItem('user', JSON.stringify(response.data)) + localStorage.setItem("user", JSON.stringify(response.data)); return { authenticated: true, - } + }; } } catch (error) { - localStorage.removeItem(TOKEN_KEY) - localStorage.removeItem('user') + localStorage.removeItem(TOKEN_KEY); + localStorage.removeItem("user"); return { authenticated: false, - redirectTo: '/login', - error: new AuthError('Сессия истекла, пожалуйста, войдите снова'), - } + redirectTo: "/login", + error: new AuthError("Сессия истекла, пожалуйста, войдите снова"), + }; } return { authenticated: false, - redirectTo: '/login', - } + redirectTo: "/login", + }; }, getPermissions: async () => { - const token = localStorage.getItem(TOKEN_KEY) - if (!token) return null + const token = localStorage.getItem(TOKEN_KEY); + if (!token) return null; try { - const decoded = jwtDecode(token) + const decoded = jwtDecode(token); if (decoded.is_admin) { - document.body.classList.add('is-admin') + document.body.classList.add("is-admin"); } 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 { - document.body.classList.remove('is-admin') - return ['user'] + document.body.classList.remove("is-admin"); + return ["user"]; } }, getIdentity: async () => { - const token = localStorage.getItem(TOKEN_KEY) - const user = localStorage.getItem('user') + const token = localStorage.getItem(TOKEN_KEY); + const user = localStorage.getItem("user"); - if (!token || !user) return null + if (!token || !user) return null; try { - const decoded = jwtDecode(token) - const userData = JSON.parse(user) + const decoded = jwtDecode(token); + const userData = JSON.parse(user); return { ...userData, is_admin: decoded.is_admin, // всегда используем значение из токена - } + }; } catch { - return null + return null; } }, onError: async (error) => { - const status = (error as AxiosError)?.response?.status + const status = (error as AxiosError)?.response?.status; if (status === 401 || status === 403) { - localStorage.removeItem(TOKEN_KEY) - localStorage.removeItem('user') + localStorage.removeItem(TOKEN_KEY); + localStorage.removeItem("user"); return { logout: true, - redirectTo: '/login', - error: new AuthError('Сессия истекла, пожалуйста, войдите снова'), - } + redirectTo: "/login", + error: new AuthError("Сессия истекла, пожалуйста, войдите снова"), + }; } - return {error} + return { error }; }, -} +}; diff --git a/src/components/CreateSightArticle.tsx b/src/components/CreateSightArticle.tsx index b5f6025..9432b7a 100644 --- a/src/components/CreateSightArticle.tsx +++ b/src/components/CreateSightArticle.tsx @@ -1,133 +1,170 @@ -import {Typography, Button, Box, Accordion, AccordionSummary, AccordionDetails, useTheme, TextField} from '@mui/material' -import ExpandMoreIcon from '@mui/icons-material/ExpandMore' -import {axiosInstance} from '../providers/data' -import {BACKEND_URL} from '../lib/constants' -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' +import { + Typography, + Button, + Box, + Accordion, + AccordionSummary, + AccordionDetails, + useTheme, + 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 = { - file: File - preview: string - uploading: boolean - mediaId?: number -} + file: File; + preview: string; + uploading: boolean; + mediaId?: number; +}; type Props = { - parentId: string | number - parentResource: string - childResource: string - title: string -} + parentId: string | number; + parentResource: string; + childResource: string; + title: string; +}; -export const CreateSightArticle = ({parentId, parentResource, childResource, title}: Props) => { - const theme = useTheme() - const [mediaFiles, setMediaFiles] = useState([]) +export const CreateSightArticle = ({ + parentId, + parentResource, + childResource, + title, +}: Props) => { + const theme = useTheme(); + const [mediaFiles, setMediaFiles] = useState([]); const { register: registerItem, control: controlItem, handleSubmit: handleSubmitItem, reset: resetItem, - formState: {errors: itemErrors}, + formState: { errors: itemErrors }, } = useForm({ defaultValues: { - heading: '', - body: '', + heading: "", + body: "", }, - }) + }); const simpleMDEOptions = React.useMemo( () => ({ - placeholder: 'Введите контент в формате Markdown...', + placeholder: "Введите контент в формате Markdown...", spellChecker: false, }), - [], - ) + [] + ); const onDrop = useCallback((acceptedFiles: File[]) => { const newFiles = acceptedFiles.map((file) => ({ file, preview: URL.createObjectURL(file), uploading: false, - })) - setMediaFiles((prev) => [...prev, ...newFiles]) - }, []) + })); + setMediaFiles((prev) => [...prev, ...newFiles]); + }, []); - const {getRootProps, getInputProps, isDragActive} = useDropzone({ + const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, accept: { - 'image/*': ALLOWED_IMAGE_TYPES, - 'video/*': ALLOWED_VIDEO_TYPES, + "image/*": ALLOWED_IMAGE_TYPES, + "video/*": ALLOWED_VIDEO_TYPES, }, multiple: true, - }) + }); const uploadMedia = async (mediaFile: MediaFile) => { - const formData = new FormData() - formData.append('media_name', mediaFile.file.name) - formData.append('filename', mediaFile.file.name) - formData.append('type', mediaFile.file.type.startsWith('image/') ? '1' : '2') - formData.append('file', mediaFile.file) + const formData = new FormData(); + formData.append("media_name", mediaFile.file.name); + formData.append("filename", mediaFile.file.name); + formData.append( + "type", + mediaFile.file.type.startsWith("image/") ? "1" : "2" + ); + formData.append("file", mediaFile.file); - const response = await axiosInstance.post(`${BACKEND_URL}/media`, formData) - return response.data.id - } + const response = await axiosInstance.post( + `${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 { // Создаем статью - const response = await axiosInstance.post(`${BACKEND_URL}/${childResource}`, data) - const itemId = response.data.id + const response = await axiosInstance.post( + `${import.meta.env.VITE_KRBL_API}/${childResource}`, + data + ); + const itemId = response.data.id; // Получаем существующие статьи для определения порядкового номера - const existingItemsResponse = await axiosInstance.get(`${BACKEND_URL}/${parentResource}/${parentId}/${childResource}`) - const existingItems = existingItemsResponse.data || [] - const nextPageNum = existingItems.length + 1 + const existingItemsResponse = await axiosInstance.get( + `${ + 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}/`, { - [`${childResource}_id`]: itemId, - page_num: nextPageNum, - }) + await axiosInstance.post( + `${ + import.meta.env.VITE_KRBL_API + }/${parentResource}/${parentId}/${childResource}/`, + { + [`${childResource}_id`]: itemId, + page_num: nextPageNum, + } + ); // Загружаем все медиа файлы и получаем их ID const mediaIds = await Promise.all( mediaFiles.map(async (mediaFile) => { - return await uploadMedia(mediaFile) - }), - ) + return await uploadMedia(mediaFile); + }) + ); // Привязываем все медиа к статье await Promise.all( mediaIds.map((mediaId, index) => - axiosInstance.post(`${BACKEND_URL}/article/${itemId}/media/`, { - media_id: mediaId, - media_order: index + 1, - }), - ), - ) + axiosInstance.post( + `${import.meta.env.VITE_KRBL_API}/article/${itemId}/media/`, + { + media_id: mediaId, + media_order: index + 1, + } + ) + ) + ); - resetItem() - setMediaFiles([]) - window.location.reload() + resetItem(); + setMediaFiles([]); + window.location.reload(); } catch (err: any) { - console.error('Error creating item:', err) + console.error("Error creating item:", err); } - } + }; const removeMedia = (index: number) => { setMediaFiles((prev) => { - const newFiles = [...prev] - URL.revokeObjectURL(newFiles[index].preview) - newFiles.splice(index, 1) - return newFiles - }) - } + const newFiles = [...prev]; + URL.revokeObjectURL(newFiles[index].preview); + newFiles.splice(index, 1); + return newFiles; + }); + }; return ( @@ -143,76 +180,95 @@ export const CreateSightArticle = ({parentId, parentResource, childResource, tit Создать {title} - + - } /> + ( + + )} + /> {/* Dropzone для медиа файлов */} - + - {isDragActive ? 'Перетащите файлы сюда...' : 'Перетащите файлы сюда или кликните для выбора'} + + {isDragActive + ? "Перетащите файлы сюда..." + : "Перетащите файлы сюда или кликните для выбора"} + {/* Превью загруженных файлов */} - + {mediaFiles.map((mediaFile, index) => ( - {mediaFile.file.type.startsWith('image/') ? ( + {mediaFile.file.type.startsWith("image/") ? ( {mediaFile.file.name} ) : ( - {mediaFile.file.name} + + {mediaFile.file.name} + )} @@ -203,31 +252,48 @@ export const LinkedItems = ({parentI )} - {type === 'edit' && ( + {type === "edit" && ( Добавить {title} item.id === selectedItemId) || null} - onChange={(_, newValue) => setSelectedItemId(newValue?.id || null)} + value={ + availableItems.find((item) => item.id === selectedItemId) || + null + } + onChange={(_, newValue) => + setSelectedItemId(newValue?.id || null) + } options={availableItems} getOptionLabel={(item) => String(item[fields[0].data])} - renderInput={(params) => } - isOptionEqualToValue={(option, value) => option.id === value?.id} - filterOptions={(options, {inputValue}) => { + renderInput={(params) => ( + + )} + isOptionEqualToValue={(option, value) => + option.id === value?.id + } + filterOptions={(options, { inputValue }) => { // return options.filter((option) => String(option[fields[0].data]).toLowerCase().includes(inputValue.toLowerCase())) const searchWords = inputValue .toLowerCase() - .split(' ') - .filter((word) => word.length > 0) + .split(" ") + .filter((word) => word.length > 0); return options.filter((option) => { - const optionWords = String(option[fields[0].data]).toLowerCase().split(' ') - return searchWords.every((searchWord) => optionWords.some((word) => word.startsWith(searchWord))) - }) + const optionWords = String(option[fields[0].data]) + .toLowerCase() + .split(" "); + return searchWords.every((searchWord) => + optionWords.some((word) => word.startsWith(searchWord)) + ); + }); }} /> - {childResource === 'article' && ( + {childResource === "article" && ( ({parentI name="page_num" value={pageNum} onChange={(e) => { - const newValue = Number(e.target.value) - const minValue = linkedItems.length + 1 // page number on articles lenght - setPageNum(newValue < minValue ? minValue : newValue) + const newValue = Number(e.target.value); + const minValue = linkedItems.length + 1; // page number on articles lenght + setPageNum(newValue < minValue ? minValue : newValue); }} fullWidth - InputLabelProps={{shrink: true}} + InputLabelProps={{ shrink: true }} /> )} - {childResource === 'media' && type === 'edit' && ( + {childResource === "media" && type === "edit" && ( { - const newValue = Number(e.target.value) - const maxValue = linkedItems.length + 1 - const value = Math.max(1, Math.min(newValue, maxValue)) - setMediaOrder(value) + const newValue = Number(e.target.value); + const maxValue = linkedItems.length + 1; + const value = Math.max(1, Math.min(newValue, maxValue)); + setMediaOrder(value); }} fullWidth - InputLabelProps={{shrink: true}} + InputLabelProps={{ shrink: true }} /> )} - @@ -271,5 +341,5 @@ export const LinkedItems = ({parentI - ) -} + ); +}; diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 493ea9c..8c11d59 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,11 +1,9 @@ -export const BACKEND_URL = 'https://wn.krbl.ru' - export const MEDIA_TYPES = [ - {label: 'Фото', value: 1}, - {label: 'Видео', value: 2}, -] + { label: "Фото", value: 1 }, + { label: "Видео", value: 2 }, +]; export const VEHICLE_TYPES = [ - {label: 'Трамвай', value: 1}, - {label: 'Троллейбус', value: 2}, -] + { label: "Трамвай", value: 1 }, + { label: "Троллейбус", value: 2 }, +]; diff --git a/src/pages/article/edit.tsx b/src/pages/article/edit.tsx index 275f3ba..c7a1cce 100644 --- a/src/pages/article/edit.tsx +++ b/src/pages/article/edit.tsx @@ -1,19 +1,19 @@ -import {Box, TextField, Typography, Paper} from '@mui/material' -import {Edit} from '@refinedev/mui' -import {useForm} from '@refinedev/react-hook-form' -import {Controller} from 'react-hook-form' -import {useParams} from 'react-router' -import React, {useState, useEffect} from 'react' -import ReactMarkdown from 'react-markdown' -import {useList} from '@refinedev/core' +import { Box, TextField, Typography, Paper } from "@mui/material"; +import { Edit } from "@refinedev/mui"; +import { useForm } from "@refinedev/react-hook-form"; +import { Controller } from "react-hook-form"; +import { useParams } from "react-router"; +import React, { useState, useEffect } from "react"; +import ReactMarkdown from "react-markdown"; +import { useList } from "@refinedev/core"; -import {MarkdownEditor} from '../../components/MarkdownEditor' -import {LinkedItems} from '../../components/LinkedItems' -import {MediaItem, mediaFields} from './types' -import {TOKEN_KEY} from '../../authProvider' -import 'easymde/dist/easymde.min.css' +import { MarkdownEditor } from "../../components/MarkdownEditor"; +import { LinkedItems } from "../../components/LinkedItems"; +import { MediaItem, mediaFields } from "./types"; +import { TOKEN_KEY } from "../../authProvider"; +import "easymde/dist/easymde.min.css"; -const MemoizedSimpleMDE = React.memo(MarkdownEditor) +const MemoizedSimpleMDE = React.memo(MarkdownEditor); export const ArticleEdit = () => { const { @@ -21,55 +21,59 @@ export const ArticleEdit = () => { register, control, watch, - formState: {errors}, - } = useForm() + formState: { errors }, + } = useForm(); - const {id: articleId} = useParams<{id: string}>() - const [preview, setPreview] = useState('') - const [headingPreview, setHeadingPreview] = useState('') + const { id: articleId } = useParams<{ id: string }>(); + const [preview, setPreview] = useState(""); + const [headingPreview, setHeadingPreview] = useState(""); // Получаем привязанные медиа - const {data: mediaData} = useList({ + const { data: mediaData } = useList({ resource: `article/${articleId}/media`, queryOptions: { enabled: !!articleId, }, - }) + }); // Следим за изменениями в полях body и heading - const bodyContent = watch('body') - const headingContent = watch('heading') + const bodyContent = watch("body"); + const headingContent = watch("heading"); useEffect(() => { - setPreview(bodyContent || '') - }, [bodyContent]) + setPreview(bodyContent || ""); + }, [bodyContent]); useEffect(() => { - setHeadingPreview(headingContent || '') - }, [headingContent]) + setHeadingPreview(headingContent || ""); + }, [headingContent]); const simpleMDEOptions = React.useMemo( () => ({ - placeholder: 'Введите контент в формате Markdown...', + placeholder: "Введите контент в формате Markdown...", spellChecker: false, }), - [], - ) + [] + ); return ( - + {/* Форма редактирования */} - + { ( + render={({ field: { onChange, value } }) => ( { )} /> - {articleId && type="edit" parentId={articleId} parentResource="article" childResource="media" fields={mediaFields} title="медиа" />} + {articleId && ( + + type="edit" + parentId={articleId} + parentResource="article" + childResource="media" + fields={mediaFields} + title="медиа" + /> + )} {/* Блок предпросмотра */} @@ -98,14 +111,15 @@ export const ArticleEdit = () => { sx={{ flex: 1, p: 2, - maxHeight: 'calc(100vh - 200px)', - overflowY: 'auto', - position: 'sticky', + maxHeight: "calc(100vh - 200px)", + overflowY: "auto", + position: "sticky", top: 16, borderRadius: 2, - border: '1px solid', - borderColor: 'primary.main', - bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'background.paper' : '#fff'), + border: "1px solid", + borderColor: "primary.main", + bgcolor: (theme) => + theme.palette.mode === "dark" ? "background.paper" : "#fff", }} > @@ -117,7 +131,8 @@ export const ArticleEdit = () => { variant="h4" gutterBottom sx={{ - color: (theme) => (theme.palette.mode === 'dark' ? 'grey.300' : 'grey.800'), + color: (theme) => + theme.palette.mode === "dark" ? "grey.300" : "grey.800", mb: 3, }} > @@ -127,39 +142,41 @@ export const ArticleEdit = () => { {/* Markdown контент */} (theme.palette.mode === 'dark' ? 'grey.300' : 'grey.800'), + color: (theme) => + theme.palette.mode === "dark" ? "grey.300" : "grey.800", }, - '& a': { - color: 'primary.main', - textDecoration: 'none', - '&:hover': { - textDecoration: 'underline', + "& a": { + color: "primary.main", + textDecoration: "none", + "&:hover": { + textDecoration: "underline", }, }, - '& blockquote': { - borderLeft: '4px solid', - borderColor: 'primary.main', + "& blockquote": { + borderLeft: "4px solid", + borderColor: "primary.main", pl: 2, my: 2, - color: 'text.secondary', + color: "text.secondary", }, - '& code': { - bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'grey.900' : 'grey.100'), + "& code": { + bgcolor: (theme) => + theme.palette.mode === "dark" ? "grey.900" : "grey.100", p: 0.5, borderRadius: 0.5, - color: 'primary.main', + color: "primary.main", }, }} > @@ -168,15 +185,15 @@ export const ArticleEdit = () => { {/* Привязанные медиа */} {mediaData?.data && mediaData.data.length > 0 && ( - + Привязанные медиа: @@ -187,18 +204,20 @@ export const ArticleEdit = () => { width: 120, height: 120, borderRadius: 1, - overflow: 'hidden', - border: '1px solid', - borderColor: 'primary.main', + overflow: "hidden", + border: "1px solid", + borderColor: "primary.main", }} > {media.media_name} @@ -209,5 +228,5 @@ export const ArticleEdit = () => { - ) -} + ); +}; diff --git a/src/pages/carrier/show.tsx b/src/pages/carrier/show.tsx index 78daa73..593458e 100644 --- a/src/pages/carrier/show.tsx +++ b/src/pages/carrier/show.tsx @@ -1,60 +1,114 @@ -import {Box, Stack, Typography} from '@mui/material' -import {useShow} from '@refinedev/core' -import {Show, TextFieldComponent as TextField} from '@refinedev/mui' -import {TOKEN_KEY} from '../../authProvider' +import { Box, Stack, Typography } from "@mui/material"; +import { useShow } from "@refinedev/core"; +import { Show, TextFieldComponent as TextField } from "@refinedev/mui"; +import { TOKEN_KEY } from "../../authProvider"; export type FieldType = { - label: string - data: any - render?: (value: any) => React.ReactNode -} + label: string; + data: any; + render?: (value: any) => React.ReactNode; +}; export const CarrierShow = () => { - const {query} = useShow({}) - const {data, isLoading} = query + const { query } = useShow({}); + const { data, isLoading } = query; - const record = data?.data + const record = data?.data; const fields: FieldType[] = [ - {label: 'Полное имя', data: 'full_name'}, - {label: 'Короткое имя', data: 'short_name'}, - {label: 'Город', data: 'city'}, + { label: "Полное имя", data: "full_name" }, + { label: "Короткое имя", data: "short_name" }, + { label: "Город", data: "city" }, { - label: 'Основной цвет', - data: 'main_color', - render: (value: string) => {value}, + label: "Основной цвет", + data: "main_color", + render: (value: string) => ( + + {value} + + ), }, { - label: 'Цвет левого виджета', - data: 'left_color', - render: (value: string) => {value}, + label: "Цвет левого виджета", + data: "left_color", + render: (value: string) => ( + + {value} + + ), }, { - label: 'Цвет правого виджета', - data: 'right_color', - render: (value: string) => {value}, + label: "Цвет правого виджета", + data: "right_color", + render: (value: string) => ( + + {value} + + ), }, - {label: 'Слоган', data: 'slogan'}, + { label: "Слоган", data: "slogan" }, { - label: 'Логотип', - data: 'logo', - render: (value: number) => {String(value)}, + label: "Логотип", + data: "logo", + render: (value: number) => ( + {String(value)} + ), }, - ] + ]; return ( - {fields.map(({label, data, render}) => ( + {fields.map(({ label, data, render }) => ( {label} - {render ? render(record?.[data]) : } + {render ? ( + render(record?.[data]) + ) : ( + + )} ))} - ) -} + ); +}; diff --git a/src/pages/city/show.tsx b/src/pages/city/show.tsx index a5c20b0..2664b7f 100644 --- a/src/pages/city/show.tsx +++ b/src/pages/city/show.tsx @@ -1,35 +1,51 @@ -import {Stack, Typography} from '@mui/material' -import {useShow} from '@refinedev/core' -import {Show, TextFieldComponent as TextField} from '@refinedev/mui' -import {TOKEN_KEY} from '../../authProvider' +import { Stack, Typography } from "@mui/material"; +import { useShow } from "@refinedev/core"; +import { Show, TextFieldComponent as TextField } from "@refinedev/mui"; +import { TOKEN_KEY } from "../../authProvider"; export const CityShow = () => { - const {query} = useShow({}) - const {data, isLoading} = query + const { query } = useShow({}); + const { data, isLoading } = query; - const record = data?.data + const record = data?.data; const fields = [ // {label: 'ID', data: 'id'}, - {label: 'Название', data: 'name'}, + { label: "Название", data: "name" }, // {label: 'Код страны', data: 'country_code'}, - {label: 'Страна', data: 'country'}, - {label: 'Герб', data: 'arms', render: (value: number) => {String(value)}}, - ] + { label: "Страна", data: "country" }, + { + label: "Герб", + data: "arms", + render: (value: number) => ( + {String(value)} + ), + }, + ]; return ( - {fields.map(({label, data, render}) => ( + {fields.map(({ label, data, render }) => ( {label} - {render ? render(record?.[data]) : } + {render ? ( + render(record?.[data]) + ) : ( + + )} ))} - ) -} + ); +}; diff --git a/src/pages/media/edit.tsx b/src/pages/media/edit.tsx index 732eb4e..dbdc544 100644 --- a/src/pages/media/edit.tsx +++ b/src/pages/media/edit.tsx @@ -1,26 +1,36 @@ -import {Box, TextField, Button, Typography, 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 { + Box, + TextField, + Button, + Typography, + 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 {ALLOWED_IMAGE_TYPES, ALLOWED_VIDEO_TYPES, useMediaFileUpload} from '../../components/media/MediaFormUtils' -import {TOKEN_KEY} from '../../authProvider' +import { MEDIA_TYPES } from "../../lib/constants"; +import { + ALLOWED_IMAGE_TYPES, + ALLOWED_VIDEO_TYPES, + useMediaFileUpload, +} from "../../components/media/MediaFormUtils"; +import { TOKEN_KEY } from "../../authProvider"; type MediaFormValues = { - media_name: string - media_type: number - file?: File -} + media_name: string; + media_type: number; + file?: File; +}; export const MediaEdit = () => { const { saveButtonProps, - refineCore: {onFinish}, + refineCore: { onFinish }, register, - formState: {errors}, + formState: { errors }, setValue, handleSubmit, watch, @@ -29,32 +39,42 @@ export const MediaEdit = () => { control, } = useForm({ defaultValues: { - media_name: '', - media_type: '', + media_name: "", + media_type: "", file: undefined, }, - }) + }); - const {query} = useShow() - const {data} = query - const record = data?.data + const { query } = useShow(); + const { data } = query; + 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, setError, clearErrors, setValue, - }) + }); useEffect(() => { if (record?.id) { - setPreviewUrl(`https://wn.krbl.ru/media/${record.id}/download?token=${localStorage.getItem(TOKEN_KEY)}`) - setValue('media_name', record?.media_name || '') - setValue('media_type', record?.media_type) + setPreviewUrl( + `${import.meta.env.VITE_KRBL_MEDIA}${ + 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 ( { media_name: data.media_name, filename: selectedFile?.name || record?.filename, type: Number(data.media_type), - } - onFinish(formData) + }; + onFinish(formData); }), }} > - + ( + render={({ field }) => ( option.value === field.value) || null} + value={ + MEDIA_TYPES.find((option) => option.value === field.value) || + null + } onChange={(_, value) => { - field.onChange(value?.value || null) - handleMediaTypeChange(value?.value || null) + field.onChange(value?.value || null); + handleMediaTypeChange(value?.value || null); }} getOptionLabel={(item) => { - return item ? item.label : '' + return item ? item.label : ""; }} isOptionEqualToValue={(option, value) => { - return option.value === value?.value + return option.value === value?.value; }} - renderInput={(params) => } + renderInput={(params) => ( + + )} /> )} /> - - - {selectedFile && ( @@ -134,11 +195,15 @@ export const MediaEdit = () => { {previewUrl && selectedMediaType === 1 && ( - Preview + Preview )} - ) -} + ); +}; diff --git a/src/pages/media/show.tsx b/src/pages/media/show.tsx index be4aa2b..7de79c1 100644 --- a/src/pages/media/show.tsx +++ b/src/pages/media/show.tsx @@ -1,39 +1,42 @@ -import {Stack, Typography, Box, Button} from '@mui/material' -import {useShow} from '@refinedev/core' -import {Show, TextFieldComponent as TextField} from '@refinedev/mui' +import { Stack, Typography, Box, Button } from "@mui/material"; +import { useShow } from "@refinedev/core"; +import { Show, TextFieldComponent as TextField } from "@refinedev/mui"; -import {MEDIA_TYPES} from '../../lib/constants' -import {TOKEN_KEY} from '../../authProvider' +import { MEDIA_TYPES } from "../../lib/constants"; +import { TOKEN_KEY } from "../../authProvider"; export const MediaShow = () => { - const {query} = useShow({}) - const {data, isLoading} = query + const { query } = useShow({}); + const { data, isLoading } = query; - const record = data?.data - const token = localStorage.getItem(TOKEN_KEY) + const record = data?.data; + const token = localStorage.getItem(TOKEN_KEY); const fields = [ // {label: 'Название файла', data: 'filename'}, - {label: 'Название', data: 'media_name'}, + { label: "Название", data: "media_name" }, { - label: 'Тип', - data: 'media_type', - render: (value: number) => MEDIA_TYPES.find((type) => type.value === value)?.label || value, + label: "Тип", + data: "media_type", + render: (value: number) => + MEDIA_TYPES.find((type) => type.value === value)?.label || value, }, // {label: 'ID', data: 'id'}, - ] + ]; return ( {record && record.media_type === 1 && ( {record?.filename} @@ -43,36 +46,45 @@ export const MediaShow = () => { Видео доступно для скачивания по ссылке: - )} - {fields.map(({label, data, render}) => ( + {fields.map(({ label, data, render }) => ( {label} - + ))} - ) -} + ); +}; diff --git a/src/pages/route/create.tsx b/src/pages/route/create.tsx index 66f1849..9e574df 100644 --- a/src/pages/route/create.tsx +++ b/src/pages/route/create.tsx @@ -1,73 +1,102 @@ -import {Autocomplete, Box, 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' +import { + Autocomplete, + Box, + 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 = () => { const { saveButtonProps, - refineCore: {formLoading}, + refineCore: { formLoading }, register, control, - formState: {errors}, + formState: { errors }, } = useForm({ refineCoreProps: { - resource: 'route/', + resource: "route/", }, - }) + }); - const {autocompleteProps: carrierAutocompleteProps} = useAutocomplete({ - resource: 'carrier', + const { autocompleteProps: carrierAutocompleteProps } = useAutocomplete({ + resource: "carrier", onSearch: (value) => [ { - field: 'short_name', - operator: 'contains', + field: "short_name", + operator: "contains", value, }, ], - }) + }); return ( - + ( + render={({ field }) => ( option.id === field.value) || null} + value={ + carrierAutocompleteProps.options.find( + (option) => option.id === field.value + ) || null + } onChange={(_, value) => { - field.onChange(value?.id || '') + field.onChange(value?.id || ""); }} getOptionLabel={(item) => { - return item ? item.short_name : '' + return item ? item.short_name : ""; }} isOptionEqualToValue={(option, value) => { - return option.id === value?.id + return option.id === value?.id; }} - filterOptions={(options, {inputValue}) => { - return options.filter((option) => option.short_name.toLowerCase().includes(inputValue.toLowerCase())) + filterOptions={(options, { inputValue }) => { + return options.filter((option) => + option.short_name + .toLowerCase() + .includes(inputValue.toLowerCase()) + ); }} - renderInput={(params) => } + renderInput={(params) => ( + + )} /> )} /> String(value), })} error={!!(errors as any)?.route_number} helperText={(errors as any)?.route_number?.message} margin="normal" fullWidth - InputLabelProps={{shrink: true}} + InputLabelProps={{ shrink: true }} type="text" - label={'Номер маршрута *'} + label={"Номер маршрута *"} name="route_number" /> @@ -75,143 +104,169 @@ export const RouteCreate = () => { name="route_direction" // boolean control={control} defaultValue={false} - render={({field}: {field: any}) => field.onChange(e.target.checked)} />} />} + render={({ field }: { field: any }) => ( + field.onChange(e.target.checked)} + /> + } + /> + )} /> - + (Прямой / Обратный) { try { // Парсим строку в массив массивов - return JSON.parse(value) + return JSON.parse(value); } catch { - return [] + return []; } }, validate: (value: unknown) => { - if (!Array.isArray(value)) return 'Неверный формат' - if (!value.every((point: unknown) => Array.isArray(point) && point.length === 2)) { - return 'Каждая точка должна быть массивом из двух координат' + if (!Array.isArray(value)) return "Неверный формат"; + if ( + !value.every( + (point: unknown) => Array.isArray(point) && point.length === 2 + ) + ) { + return "Каждая точка должна быть массивом из двух координат"; } - if (!value.every((point: unknown[]) => point.every((coord: unknown) => !isNaN(Number(coord)) && typeof coord === 'number'))) { - return 'Координаты должны быть числами' + if ( + !value.every((point: unknown[]) => + point.every( + (coord: unknown) => + !isNaN(Number(coord)) && typeof coord === "number" + ) + ) + ) { + return "Координаты должны быть числами"; } - return true + return true; }, })} error={!!(errors as any)?.path} helperText={(errors as any)?.path?.message} // 'Формат: [[lat1,lon1], [lat2,lon2], ...]' margin="normal" fullWidth - InputLabelProps={{shrink: true}} + InputLabelProps={{ shrink: true }} type="text" - label={'Координаты маршрута *'} + label={"Координаты маршрута *"} name="path" placeholder="[[1.1, 2.2], [2.1, 4.5]]" /> - ) -} + ); +}; diff --git a/src/pages/sight/create.tsx b/src/pages/sight/create.tsx index a745a0a..36a2a69 100644 --- a/src/pages/sight/create.tsx +++ b/src/pages/sight/create.tsx @@ -1,191 +1,248 @@ -import {Autocomplete, Box, TextField, Typography, Paper} from '@mui/material' -import {Create, useAutocomplete} from '@refinedev/mui' -import {useForm} from '@refinedev/react-hook-form' -import {Controller} from 'react-hook-form' -import {Link} from 'react-router' -import React, {useState, useEffect} from 'react' -import {TOKEN_KEY} from '../../authProvider' +import { Autocomplete, Box, TextField, Typography, Paper } from "@mui/material"; +import { Create, useAutocomplete } from "@refinedev/mui"; +import { useForm } from "@refinedev/react-hook-form"; +import { Controller } from "react-hook-form"; +import { Link } from "react-router"; +import React, { useState, useEffect } from "react"; +import { TOKEN_KEY } from "../../authProvider"; export const SightCreate = () => { const { saveButtonProps, - refineCore: {formLoading}, + refineCore: { formLoading }, register, control, watch, - formState: {errors}, + formState: { errors }, } = useForm({ refineCoreProps: { - resource: 'sight/', + resource: "sight/", }, - }) + }); // Состояния для предпросмотра - const [namePreview, setNamePreview] = useState('') - const [coordinatesPreview, setCoordinatesPreview] = useState({latitude: '', longitude: ''}) - const [cityPreview, setCityPreview] = useState('') - const [thumbnailPreview, setThumbnailPreview] = useState(null) - const [watermarkLUPreview, setWatermarkLUPreview] = useState(null) - const [watermarkRDPreview, setWatermarkRDPreview] = useState(null) - const [leftArticlePreview, setLeftArticlePreview] = useState('') - const [previewArticlePreview, setPreviewArticlePreview] = useState('') + const [namePreview, setNamePreview] = useState(""); + const [coordinatesPreview, setCoordinatesPreview] = useState({ + latitude: "", + longitude: "", + }); + const [cityPreview, setCityPreview] = useState(""); + const [thumbnailPreview, setThumbnailPreview] = useState(null); + const [watermarkLUPreview, setWatermarkLUPreview] = useState( + null + ); + const [watermarkRDPreview, setWatermarkRDPreview] = useState( + null + ); + const [leftArticlePreview, setLeftArticlePreview] = useState(""); + const [previewArticlePreview, setPreviewArticlePreview] = useState(""); // Автокомплиты - const {autocompleteProps: cityAutocompleteProps} = useAutocomplete({ - resource: 'city', + const { autocompleteProps: cityAutocompleteProps } = useAutocomplete({ + resource: "city", onSearch: (value) => [ { - field: 'name', - operator: 'contains', + field: "name", + operator: "contains", value, }, ], - }) + }); - const {autocompleteProps: mediaAutocompleteProps} = useAutocomplete({ - resource: 'media', + const { autocompleteProps: mediaAutocompleteProps } = useAutocomplete({ + resource: "media", onSearch: (value) => [ { - field: 'media_name', - operator: 'contains', + field: "media_name", + operator: "contains", value, }, ], - }) + }); - const {autocompleteProps: articleAutocompleteProps} = useAutocomplete({ - resource: 'article', + const { autocompleteProps: articleAutocompleteProps } = useAutocomplete({ + resource: "article", onSearch: (value) => [ { - field: 'heading', - operator: 'contains', + field: "heading", + operator: "contains", value, }, ], - }) + }); // Следим за изменениями во всех полях - const nameContent = watch('name') - const latitudeContent = watch('latitude') - const longitudeContent = watch('longitude') - const cityContent = watch('city_id') - const thumbnailContent = watch('thumbnail') - const watermarkLUContent = watch('watermark_lu') - const watermarkRDContent = watch('watermark_rd') - const leftArticleContent = watch('left_article') - const previewArticleContent = watch('preview_article') + const nameContent = watch("name"); + const latitudeContent = watch("latitude"); + const longitudeContent = watch("longitude"); + const cityContent = watch("city_id"); + const thumbnailContent = watch("thumbnail"); + const watermarkLUContent = watch("watermark_lu"); + const watermarkRDContent = watch("watermark_rd"); + const leftArticleContent = watch("left_article"); + const previewArticleContent = watch("preview_article"); // Обновляем состояния при изменении полей useEffect(() => { - setNamePreview(nameContent || '') - }, [nameContent]) + setNamePreview(nameContent || ""); + }, [nameContent]); useEffect(() => { setCoordinatesPreview({ - latitude: latitudeContent || '', - longitude: longitudeContent || '', - }) - }, [latitudeContent, longitudeContent]) + latitude: latitudeContent || "", + longitude: longitudeContent || "", + }); + }, [latitudeContent, longitudeContent]); useEffect(() => { - const selectedCity = cityAutocompleteProps.options.find((option) => option.id === cityContent) - setCityPreview(selectedCity?.name || '') - }, [cityContent, cityAutocompleteProps.options]) + const selectedCity = cityAutocompleteProps.options.find( + (option) => option.id === cityContent + ); + setCityPreview(selectedCity?.name || ""); + }, [cityContent, cityAutocompleteProps.options]); useEffect(() => { - const selectedThumbnail = mediaAutocompleteProps.options.find((option) => option.id === thumbnailContent) - setThumbnailPreview(selectedThumbnail ? `https://wn.krbl.ru/media/${selectedThumbnail.id}/download?token=${localStorage.getItem(TOKEN_KEY)}` : null) - }, [thumbnailContent, mediaAutocompleteProps.options]) + const selectedThumbnail = mediaAutocompleteProps.options.find( + (option) => option.id === thumbnailContent + ); + setThumbnailPreview( + selectedThumbnail + ? `${import.meta.env.VITE_KRBL_MEDIA}${ + selectedThumbnail.id + }/download?token=${localStorage.getItem(TOKEN_KEY)}` + : null + ); + }, [thumbnailContent, mediaAutocompleteProps.options]); useEffect(() => { - const selectedWatermarkLU = mediaAutocompleteProps.options.find((option) => option.id === watermarkLUContent) - setWatermarkLUPreview(selectedWatermarkLU ? `https://wn.krbl.ru/media/${selectedWatermarkLU.id}/download?token=${localStorage.getItem(TOKEN_KEY)}` : null) - }, [watermarkLUContent, mediaAutocompleteProps.options]) + const selectedWatermarkLU = mediaAutocompleteProps.options.find( + (option) => option.id === watermarkLUContent + ); + setWatermarkLUPreview( + selectedWatermarkLU + ? `${import.meta.env.VITE_KRBL_MEDIA}${ + selectedWatermarkLU.id + }/download?token=${localStorage.getItem(TOKEN_KEY)}` + : null + ); + }, [watermarkLUContent, mediaAutocompleteProps.options]); useEffect(() => { - const selectedWatermarkRD = mediaAutocompleteProps.options.find((option) => option.id === watermarkRDContent) - setWatermarkRDPreview(selectedWatermarkRD ? `https://wn.krbl.ru/media/${selectedWatermarkRD.id}/download?token=${localStorage.getItem(TOKEN_KEY)}` : null) - }, [watermarkRDContent, mediaAutocompleteProps.options]) + const selectedWatermarkRD = mediaAutocompleteProps.options.find( + (option) => option.id === watermarkRDContent + ); + setWatermarkRDPreview( + selectedWatermarkRD + ? `${import.meta.env.VITE_KRBL_MEDIA}${ + selectedWatermarkRD.id + }/download?token=${localStorage.getItem(TOKEN_KEY)}` + : null + ); + }, [watermarkRDContent, mediaAutocompleteProps.options]); useEffect(() => { - const selectedLeftArticle = articleAutocompleteProps.options.find((option) => option.id === leftArticleContent) - setLeftArticlePreview(selectedLeftArticle?.heading || '') - }, [leftArticleContent, articleAutocompleteProps.options]) + const selectedLeftArticle = articleAutocompleteProps.options.find( + (option) => option.id === leftArticleContent + ); + setLeftArticlePreview(selectedLeftArticle?.heading || ""); + }, [leftArticleContent, articleAutocompleteProps.options]); useEffect(() => { - const selectedPreviewArticle = articleAutocompleteProps.options.find((option) => option.id === previewArticleContent) - setPreviewArticlePreview(selectedPreviewArticle?.heading || '') - }, [previewArticleContent, articleAutocompleteProps.options]) + const selectedPreviewArticle = articleAutocompleteProps.options.find( + (option) => option.id === previewArticleContent + ); + setPreviewArticlePreview(selectedPreviewArticle?.heading || ""); + }, [previewArticleContent, articleAutocompleteProps.options]); return ( - + {/* Форма создания */} - + ( + render={({ field }) => ( option.id === field.value) || null} + value={ + cityAutocompleteProps.options.find( + (option) => option.id === field.value + ) || null + } onChange={(_, value) => { - field.onChange(value?.id || '') + field.onChange(value?.id || ""); }} getOptionLabel={(item) => { - return item ? item.name : '' + return item ? item.name : ""; }} isOptionEqualToValue={(option, value) => { - return option.id === value?.id + return option.id === value?.id; }} - filterOptions={(options, {inputValue}) => { - return options.filter((option) => option.name.toLowerCase().includes(inputValue.toLowerCase())) + filterOptions={(options, { inputValue }) => { + return options.filter((option) => + option.name.toLowerCase().includes(inputValue.toLowerCase()) + ); }} - renderInput={(params) => } + renderInput={(params) => ( + + )} /> )} /> @@ -194,23 +251,41 @@ export const SightCreate = () => { control={control} name="thumbnail" defaultValue={null} - render={({field}) => ( + render={({ field }) => ( option.id === field.value) || null} + value={ + mediaAutocompleteProps.options.find( + (option) => option.id === field.value + ) || null + } onChange={(_, value) => { - field.onChange(value?.id || '') + field.onChange(value?.id || ""); }} getOptionLabel={(item) => { - return item ? item.media_name : '' + return item ? item.media_name : ""; }} isOptionEqualToValue={(option, value) => { - return option.id === value?.id + return option.id === value?.id; }} - filterOptions={(options, {inputValue}) => { - return options.filter((option) => option.media_name.toLowerCase().includes(inputValue.toLowerCase())) + filterOptions={(options, { inputValue }) => { + return options.filter((option) => + option.media_name + .toLowerCase() + .includes(inputValue.toLowerCase()) + ); }} - renderInput={(params) => } + renderInput={(params) => ( + + )} /> )} /> @@ -219,23 +294,41 @@ export const SightCreate = () => { control={control} name="watermark_lu" defaultValue={null} - render={({field}) => ( + render={({ field }) => ( option.id === field.value) || null} + value={ + mediaAutocompleteProps.options.find( + (option) => option.id === field.value + ) || null + } onChange={(_, value) => { - field.onChange(value?.id || '') + field.onChange(value?.id || ""); }} getOptionLabel={(item) => { - return item ? item.media_name : '' + return item ? item.media_name : ""; }} isOptionEqualToValue={(option, value) => { - return option.id === value?.id + return option.id === value?.id; }} - filterOptions={(options, {inputValue}) => { - return options.filter((option) => option.media_name.toLowerCase().includes(inputValue.toLowerCase())) + filterOptions={(options, { inputValue }) => { + return options.filter((option) => + option.media_name + .toLowerCase() + .includes(inputValue.toLowerCase()) + ); }} - renderInput={(params) => } + renderInput={(params) => ( + + )} /> )} /> @@ -244,23 +337,41 @@ export const SightCreate = () => { control={control} name="watermark_rd" defaultValue={null} - render={({field}) => ( + render={({ field }) => ( option.id === field.value) || null} + value={ + mediaAutocompleteProps.options.find( + (option) => option.id === field.value + ) || null + } onChange={(_, value) => { - field.onChange(value?.id || '') + field.onChange(value?.id || ""); }} getOptionLabel={(item) => { - return item ? item.media_name : '' + return item ? item.media_name : ""; }} isOptionEqualToValue={(option, value) => { - return option.id === value?.id + return option.id === value?.id; }} - filterOptions={(options, {inputValue}) => { - return options.filter((option) => option.media_name.toLowerCase().includes(inputValue.toLowerCase())) + filterOptions={(options, { inputValue }) => { + return options.filter((option) => + option.media_name + .toLowerCase() + .includes(inputValue.toLowerCase()) + ); }} - renderInput={(params) => } + renderInput={(params) => ( + + )} /> )} /> @@ -269,23 +380,41 @@ export const SightCreate = () => { control={control} name="left_article" defaultValue={null} - render={({field}) => ( + render={({ field }) => ( option.id === field.value) || null} + value={ + articleAutocompleteProps.options.find( + (option) => option.id === field.value + ) || null + } onChange={(_, value) => { - field.onChange(value?.id || '') + field.onChange(value?.id || ""); }} getOptionLabel={(item) => { - return item ? item.heading : '' + return item ? item.heading : ""; }} isOptionEqualToValue={(option, value) => { - return option.id === value?.id + return option.id === value?.id; }} - filterOptions={(options, {inputValue}) => { - return options.filter((option) => option.heading.toLowerCase().includes(inputValue.toLowerCase())) + filterOptions={(options, { inputValue }) => { + return options.filter((option) => + option.heading + .toLowerCase() + .includes(inputValue.toLowerCase()) + ); }} - renderInput={(params) => } + renderInput={(params) => ( + + )} /> )} /> @@ -294,23 +423,41 @@ export const SightCreate = () => { control={control} name="preview_article" defaultValue={null} - render={({field}) => ( + render={({ field }) => ( option.id === field.value) || null} + value={ + articleAutocompleteProps.options.find( + (option) => option.id === field.value + ) || null + } onChange={(_, value) => { - field.onChange(value?.id || '') + field.onChange(value?.id || ""); }} getOptionLabel={(item) => { - return item ? item.heading : '' + return item ? item.heading : ""; }} isOptionEqualToValue={(option, value) => { - return option.id === value?.id + return option.id === value?.id; }} - filterOptions={(options, {inputValue}) => { - return options.filter((option) => option.heading.toLowerCase().includes(inputValue.toLowerCase())) + filterOptions={(options, { inputValue }) => { + return options.filter((option) => + option.heading + .toLowerCase() + .includes(inputValue.toLowerCase()) + ); }} - renderInput={(params) => } + renderInput={(params) => ( + + )} /> )} /> @@ -321,14 +468,15 @@ export const SightCreate = () => { sx={{ flex: 1, p: 2, - maxHeight: 'calc(100vh - 200px)', - overflowY: 'auto', - position: 'sticky', + maxHeight: "calc(100vh - 200px)", + overflowY: "auto", + position: "sticky", top: 16, borderRadius: 2, - border: '1px solid', - borderColor: 'primary.main', - bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'background.paper' : '#fff'), + border: "1px solid", + borderColor: "primary.main", + bgcolor: (theme) => + theme.palette.mode === "dark" ? "background.paper" : "#fff", }} > @@ -336,34 +484,57 @@ export const SightCreate = () => { {/* Название */} - (theme.palette.mode === 'dark' ? 'grey.300' : 'grey.800')}}> + + theme.palette.mode === "dark" ? "grey.300" : "grey.800", + }} + > {namePreview} {/* Город */} - - - Город:{' '} + + + Город:{" "} - (theme.palette.mode === 'dark' ? 'grey.300' : 'grey.800')}}> + + theme.palette.mode === "dark" ? "grey.300" : "grey.800", + }} + > {cityPreview} {/* Координаты */} - - - Координаты:{' '} + + + Координаты:{" "} - (theme.palette.mode === 'dark' ? 'grey.300' : 'grey.800')}}> + + theme.palette.mode === "dark" ? "grey.300" : "grey.800", + }} + > {coordinatesPreview.latitude}, {coordinatesPreview.longitude} {/* Обложка */} {thumbnailPreview && ( - - + + Обложка: { src={thumbnailPreview} alt="Обложка" sx={{ - maxWidth: '100%', - height: 'auto', + maxWidth: "100%", + height: "auto", borderRadius: 1, - border: '1px solid', - borderColor: 'primary.main', + border: "1px solid", + borderColor: "primary.main", }} /> )} {/* Водяные знаки */} - - + + Водяные знаки: - + {watermarkLUPreview && ( - + Левый верхний: { sx={{ width: 100, height: 100, - objectFit: 'cover', + objectFit: "cover", borderRadius: 1, - border: '1px solid', - borderColor: 'primary.main', + border: "1px solid", + borderColor: "primary.main", }} /> )} {watermarkRDPreview && ( - + Правый нижний: { sx={{ width: 100, height: 100, - objectFit: 'cover', + objectFit: "cover", borderRadius: 1, - border: '1px solid', - borderColor: 'primary.main', + border: "1px solid", + borderColor: "primary.main", }} /> @@ -432,22 +615,27 @@ export const SightCreate = () => { {/* Связанные статьи */} - + Связанные статьи: {leftArticlePreview && ( - - Левая статья:{' '} + + Левая статья:{" "} (theme.palette.mode === 'dark' ? 'grey.300' : 'grey.800'), - textDecoration: 'none', - '&:hover': { - textDecoration: 'underline', + color: (theme) => + theme.palette.mode === "dark" ? "grey.300" : "grey.800", + textDecoration: "none", + "&:hover": { + textDecoration: "underline", }, }} > @@ -457,17 +645,18 @@ export const SightCreate = () => { )} {previewArticlePreview && ( - - Статья-предпросмотр:{' '} + + Статья-предпросмотр:{" "} (theme.palette.mode === 'dark' ? 'grey.300' : 'grey.800'), - textDecoration: 'none', - '&:hover': { - textDecoration: 'underline', + color: (theme) => + theme.palette.mode === "dark" ? "grey.300" : "grey.800", + textDecoration: "none", + "&:hover": { + textDecoration: "underline", }, }} > @@ -479,5 +668,5 @@ export const SightCreate = () => { - ) -} + ); +}; diff --git a/src/pages/sight/edit.tsx b/src/pages/sight/edit.tsx index d814bc7..07e7c91 100644 --- a/src/pages/sight/edit.tsx +++ b/src/pages/sight/edit.tsx @@ -1,194 +1,248 @@ -import {Autocomplete, Box, TextField, Paper, Typography} from '@mui/material' -import {Edit, useAutocomplete} from '@refinedev/mui' -import {useForm} from '@refinedev/react-hook-form' -import {Controller} from 'react-hook-form' -import {useParams} from 'react-router' -import React, {useState, useEffect} from 'react' -import {LinkedItems} from '../../components/LinkedItems' -import {CreateSightArticle} from '../../components/CreateSightArticle' -import {ArticleItem, articleFields} from './types' -import {TOKEN_KEY} from '../../authProvider' -import {Link} from 'react-router' +import { Autocomplete, Box, TextField, Paper, Typography } from "@mui/material"; +import { Edit, useAutocomplete } from "@refinedev/mui"; +import { useForm } from "@refinedev/react-hook-form"; +import { Controller } from "react-hook-form"; +import { useParams } from "react-router"; +import React, { useState, useEffect } from "react"; +import { LinkedItems } from "../../components/LinkedItems"; +import { CreateSightArticle } from "../../components/CreateSightArticle"; +import { ArticleItem, articleFields } from "./types"; +import { TOKEN_KEY } from "../../authProvider"; +import { Link } from "react-router"; export const SightEdit = () => { - const {id: sightId} = useParams<{id: string}>() + const { id: sightId } = useParams<{ id: string }>(); const { saveButtonProps, register, control, watch, - formState: {errors}, - } = useForm({}) + formState: { errors }, + } = useForm({}); - const {autocompleteProps: cityAutocompleteProps} = useAutocomplete({ - resource: 'city', + const { autocompleteProps: cityAutocompleteProps } = useAutocomplete({ + resource: "city", onSearch: (value) => [ { - field: 'name', - operator: 'contains', + field: "name", + operator: "contains", value, }, ], - }) + }); - const {autocompleteProps: mediaAutocompleteProps} = useAutocomplete({ - resource: 'media', + const { autocompleteProps: mediaAutocompleteProps } = useAutocomplete({ + resource: "media", onSearch: (value) => [ { - field: 'media_name', - operator: 'contains', + field: "media_name", + operator: "contains", value, }, ], - }) + }); - const {autocompleteProps: articleAutocompleteProps} = useAutocomplete({ - resource: 'article', + const { autocompleteProps: articleAutocompleteProps } = useAutocomplete({ + resource: "article", onSearch: (value) => [ { - field: 'heading', - operator: 'contains', + field: "heading", + operator: "contains", value, }, ], - }) + }); // Состояния для предпросмотра - const [namePreview, setNamePreview] = useState('') + const [namePreview, setNamePreview] = useState(""); const [coordinatesPreview, setCoordinatesPreview] = useState({ - latitude: '', - longitude: '', - }) - const [cityPreview, setCityPreview] = useState('') - const [thumbnailPreview, setThumbnailPreview] = useState(null) - const [watermarkLUPreview, setWatermarkLUPreview] = useState(null) - const [watermarkRDPreview, setWatermarkRDPreview] = useState(null) - const [leftArticlePreview, setLeftArticlePreview] = useState('') - const [previewArticlePreview, setPreviewArticlePreview] = useState('') + latitude: "", + longitude: "", + }); + const [cityPreview, setCityPreview] = useState(""); + const [thumbnailPreview, setThumbnailPreview] = useState(null); + const [watermarkLUPreview, setWatermarkLUPreview] = useState( + null + ); + const [watermarkRDPreview, setWatermarkRDPreview] = useState( + null + ); + const [leftArticlePreview, setLeftArticlePreview] = useState(""); + const [previewArticlePreview, setPreviewArticlePreview] = useState(""); // Следим за изменениями во всех полях - const nameContent = watch('name') - const latitudeContent = watch('latitude') - const longitudeContent = watch('longitude') - const cityContent = watch('city_id') - const thumbnailContent = watch('thumbnail') - const watermarkLUContent = watch('watermark_lu') - const watermarkRDContent = watch('watermark_rd') - const leftArticleContent = watch('left_article') - const previewArticleContent = watch('preview_article') + const nameContent = watch("name"); + const latitudeContent = watch("latitude"); + const longitudeContent = watch("longitude"); + const cityContent = watch("city_id"); + const thumbnailContent = watch("thumbnail"); + const watermarkLUContent = watch("watermark_lu"); + const watermarkRDContent = watch("watermark_rd"); + const leftArticleContent = watch("left_article"); + const previewArticleContent = watch("preview_article"); // Обновляем состояния при изменении полей useEffect(() => { - setNamePreview(nameContent || '') - }, [nameContent]) + setNamePreview(nameContent || ""); + }, [nameContent]); useEffect(() => { setCoordinatesPreview({ - latitude: latitudeContent || '', - longitude: longitudeContent || '', - }) - }, [latitudeContent, longitudeContent]) + latitude: latitudeContent || "", + longitude: longitudeContent || "", + }); + }, [latitudeContent, longitudeContent]); useEffect(() => { - const selectedCity = cityAutocompleteProps.options.find((option) => option.id === cityContent) - setCityPreview(selectedCity?.name || '') - }, [cityContent, cityAutocompleteProps.options]) + const selectedCity = cityAutocompleteProps.options.find( + (option) => option.id === cityContent + ); + setCityPreview(selectedCity?.name || ""); + }, [cityContent, cityAutocompleteProps.options]); useEffect(() => { - const selectedThumbnail = mediaAutocompleteProps.options.find((option) => option.id === thumbnailContent) - setThumbnailPreview(selectedThumbnail ? `https://wn.krbl.ru/media/${selectedThumbnail.id}/download?token=${localStorage.getItem(TOKEN_KEY)}` : null) - }, [thumbnailContent, mediaAutocompleteProps.options]) + const selectedThumbnail = mediaAutocompleteProps.options.find( + (option) => option.id === thumbnailContent + ); + setThumbnailPreview( + selectedThumbnail + ? `${import.meta.env.VITE_KRBL_MEDIA}${ + selectedThumbnail.id + }/download?token=${localStorage.getItem(TOKEN_KEY)}` + : null + ); + }, [thumbnailContent, mediaAutocompleteProps.options]); useEffect(() => { - const selectedWatermarkLU = mediaAutocompleteProps.options.find((option) => option.id === watermarkLUContent) - setWatermarkLUPreview(selectedWatermarkLU ? `https://wn.krbl.ru/media/${selectedWatermarkLU.id}/download?token=${localStorage.getItem(TOKEN_KEY)}` : null) - }, [watermarkLUContent, mediaAutocompleteProps.options]) + const selectedWatermarkLU = mediaAutocompleteProps.options.find( + (option) => option.id === watermarkLUContent + ); + setWatermarkLUPreview( + selectedWatermarkLU + ? `${import.meta.env.VITE_KRBL_MEDIA}${ + selectedWatermarkLU.id + }/download?token=${localStorage.getItem(TOKEN_KEY)}` + : null + ); + }, [watermarkLUContent, mediaAutocompleteProps.options]); useEffect(() => { - const selectedWatermarkRD = mediaAutocompleteProps.options.find((option) => option.id === watermarkRDContent) - setWatermarkRDPreview(selectedWatermarkRD ? `https://wn.krbl.ru/media/${selectedWatermarkRD.id}/download?token=${localStorage.getItem(TOKEN_KEY)}` : null) - }, [watermarkRDContent, mediaAutocompleteProps.options]) + const selectedWatermarkRD = mediaAutocompleteProps.options.find( + (option) => option.id === watermarkRDContent + ); + setWatermarkRDPreview( + selectedWatermarkRD + ? `${import.meta.env.VITE_KRBL_MEDIA}${ + selectedWatermarkRD.id + }/download?token=${localStorage.getItem(TOKEN_KEY)}` + : null + ); + }, [watermarkRDContent, mediaAutocompleteProps.options]); useEffect(() => { - const selectedLeftArticle = articleAutocompleteProps.options.find((option) => option.id === leftArticleContent) - setLeftArticlePreview(selectedLeftArticle?.heading || '') - }, [leftArticleContent, articleAutocompleteProps.options]) + const selectedLeftArticle = articleAutocompleteProps.options.find( + (option) => option.id === leftArticleContent + ); + setLeftArticlePreview(selectedLeftArticle?.heading || ""); + }, [leftArticleContent, articleAutocompleteProps.options]); useEffect(() => { - const selectedPreviewArticle = articleAutocompleteProps.options.find((option) => option.id === previewArticleContent) - setPreviewArticlePreview(selectedPreviewArticle?.heading || '') - }, [previewArticleContent, articleAutocompleteProps.options]) + const selectedPreviewArticle = articleAutocompleteProps.options.find( + (option) => option.id === previewArticleContent + ); + setPreviewArticlePreview(selectedPreviewArticle?.heading || ""); + }, [previewArticleContent, articleAutocompleteProps.options]); return ( - + {/* Форма редактирования */} - + ( + render={({ field }) => ( option.id === field.value) || null} + value={ + cityAutocompleteProps.options.find( + (option) => option.id === field.value + ) || null + } onChange={(_, value) => { - field.onChange(value?.id || '') + field.onChange(value?.id || ""); }} getOptionLabel={(item) => { - return item ? item.name : '' + return item ? item.name : ""; }} isOptionEqualToValue={(option, value) => { - return option.id === value?.id + return option.id === value?.id; }} - filterOptions={(options, {inputValue}) => { - return options.filter((option) => option.name.toLowerCase().includes(inputValue.toLowerCase())) + filterOptions={(options, { inputValue }) => { + return options.filter((option) => + option.name.toLowerCase().includes(inputValue.toLowerCase()) + ); }} - renderInput={(params) => } + renderInput={(params) => ( + + )} /> )} /> @@ -197,23 +251,41 @@ export const SightEdit = () => { control={control} name="thumbnail" defaultValue={null} - render={({field}) => ( + render={({ field }) => ( option.id === field.value) || null} + value={ + mediaAutocompleteProps.options.find( + (option) => option.id === field.value + ) || null + } onChange={(_, value) => { - field.onChange(value?.id || '') + field.onChange(value?.id || ""); }} getOptionLabel={(item) => { - return item ? item.media_name : '' + return item ? item.media_name : ""; }} isOptionEqualToValue={(option, value) => { - return option.id === value?.id + return option.id === value?.id; }} - filterOptions={(options, {inputValue}) => { - return options.filter((option) => option.media_name.toLowerCase().includes(inputValue.toLowerCase())) + filterOptions={(options, { inputValue }) => { + return options.filter((option) => + option.media_name + .toLowerCase() + .includes(inputValue.toLowerCase()) + ); }} - renderInput={(params) => } + renderInput={(params) => ( + + )} /> )} /> @@ -222,23 +294,41 @@ export const SightEdit = () => { control={control} name="watermark_lu" defaultValue={null} - render={({field}) => ( + render={({ field }) => ( option.id === field.value) || null} + value={ + mediaAutocompleteProps.options.find( + (option) => option.id === field.value + ) || null + } onChange={(_, value) => { - field.onChange(value?.id || '') + field.onChange(value?.id || ""); }} getOptionLabel={(item) => { - return item ? item.media_name : '' + return item ? item.media_name : ""; }} isOptionEqualToValue={(option, value) => { - return option.id === value?.id + return option.id === value?.id; }} - filterOptions={(options, {inputValue}) => { - return options.filter((option) => option.media_name.toLowerCase().includes(inputValue.toLowerCase())) + filterOptions={(options, { inputValue }) => { + return options.filter((option) => + option.media_name + .toLowerCase() + .includes(inputValue.toLowerCase()) + ); }} - renderInput={(params) => } + renderInput={(params) => ( + + )} /> )} /> @@ -247,23 +337,41 @@ export const SightEdit = () => { control={control} name="watermark_rd" defaultValue={null} - render={({field}) => ( + render={({ field }) => ( option.id === field.value) || null} + value={ + mediaAutocompleteProps.options.find( + (option) => option.id === field.value + ) || null + } onChange={(_, value) => { - field.onChange(value?.id || '') + field.onChange(value?.id || ""); }} getOptionLabel={(item) => { - return item ? item.media_name : '' + return item ? item.media_name : ""; }} isOptionEqualToValue={(option, value) => { - return option.id === value?.id + return option.id === value?.id; }} - filterOptions={(options, {inputValue}) => { - return options.filter((option) => option.media_name.toLowerCase().includes(inputValue.toLowerCase())) + filterOptions={(options, { inputValue }) => { + return options.filter((option) => + option.media_name + .toLowerCase() + .includes(inputValue.toLowerCase()) + ); }} - renderInput={(params) => } + renderInput={(params) => ( + + )} /> )} /> @@ -272,23 +380,41 @@ export const SightEdit = () => { control={control} name="left_article" defaultValue={null} - render={({field}) => ( + render={({ field }) => ( option.id === field.value) || null} + value={ + articleAutocompleteProps.options.find( + (option) => option.id === field.value + ) || null + } onChange={(_, value) => { - field.onChange(value?.id || '') + field.onChange(value?.id || ""); }} getOptionLabel={(item) => { - return item ? item.heading : '' + return item ? item.heading : ""; }} isOptionEqualToValue={(option, value) => { - return option.id === value?.id + return option.id === value?.id; }} - filterOptions={(options, {inputValue}) => { - return options.filter((option) => option.heading.toLowerCase().includes(inputValue.toLowerCase())) + filterOptions={(options, { inputValue }) => { + return options.filter((option) => + option.heading + .toLowerCase() + .includes(inputValue.toLowerCase()) + ); }} - renderInput={(params) => } + renderInput={(params) => ( + + )} /> )} /> @@ -297,23 +423,41 @@ export const SightEdit = () => { control={control} name="preview_article" defaultValue={null} - render={({field}) => ( + render={({ field }) => ( option.id === field.value) || null} + value={ + articleAutocompleteProps.options.find( + (option) => option.id === field.value + ) || null + } onChange={(_, value) => { - field.onChange(value?.id || '') + field.onChange(value?.id || ""); }} getOptionLabel={(item) => { - return item ? item.heading : '' + return item ? item.heading : ""; }} isOptionEqualToValue={(option, value) => { - return option.id === value?.id + return option.id === value?.id; }} - filterOptions={(options, {inputValue}) => { - return options.filter((option) => option.heading.toLowerCase().includes(inputValue.toLowerCase())) + filterOptions={(options, { inputValue }) => { + return options.filter((option) => + option.heading + .toLowerCase() + .includes(inputValue.toLowerCase()) + ); }} - renderInput={(params) => } + renderInput={(params) => ( + + )} /> )} /> @@ -324,14 +468,15 @@ export const SightEdit = () => { sx={{ flex: 1, p: 2, - maxHeight: 'calc(100vh - 200px)', - overflowY: 'auto', - position: 'sticky', + maxHeight: "calc(100vh - 200px)", + overflowY: "auto", + position: "sticky", top: 16, borderRadius: 2, - border: '1px solid', - borderColor: 'primary.main', - bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'background.paper' : '#fff'), + border: "1px solid", + borderColor: "primary.main", + bgcolor: (theme) => + theme.palette.mode === "dark" ? "background.paper" : "#fff", }} > @@ -343,7 +488,8 @@ export const SightEdit = () => { variant="h4" gutterBottom sx={{ - color: (theme) => (theme.palette.mode === 'dark' ? 'grey.300' : 'grey.800'), + color: (theme) => + theme.palette.mode === "dark" ? "grey.300" : "grey.800", mb: 3, }} > @@ -351,29 +497,45 @@ export const SightEdit = () => { {/* Город */} - - - Город:{' '} + + + Город:{" "} - (theme.palette.mode === 'dark' ? 'grey.300' : 'grey.800')}}> + + theme.palette.mode === "dark" ? "grey.300" : "grey.800", + }} + > {cityPreview} {/* Координаты */} - - - Координаты:{' '} + + + Координаты:{" "} - (theme.palette.mode === 'dark' ? 'grey.300' : 'grey.800')}}> + + theme.palette.mode === "dark" ? "grey.300" : "grey.800", + }} + > {coordinatesPreview.latitude}, {coordinatesPreview.longitude} {/* Обложка */} {thumbnailPreview && ( - - + + Обложка: { src={thumbnailPreview} alt="Обложка" sx={{ - maxWidth: '100%', - height: '40vh', + maxWidth: "100%", + height: "40vh", borderRadius: 2, - border: '1px solid', - borderColor: 'primary.main', + border: "1px solid", + borderColor: "primary.main", }} /> )} {/* Водяные знаки */} - - + + Водяные знаки: - + {watermarkLUPreview && ( - + Левый верхний: { sx={{ width: 100, height: 100, - objectFit: 'cover', + objectFit: "cover", borderRadius: 1, - border: '1px solid', - borderColor: 'primary.main', + border: "1px solid", + borderColor: "primary.main", }} /> )} {watermarkRDPreview && ( - + Правый нижний: { sx={{ width: 100, height: 100, - objectFit: 'cover', + objectFit: "cover", borderRadius: 1, - border: '1px solid', - borderColor: 'primary.main', + border: "1px solid", + borderColor: "primary.main", }} /> @@ -447,17 +621,18 @@ export const SightEdit = () => { */} {leftArticlePreview && ( - - Левая статья:{' '} + + Левая статья:{" "} (theme.palette.mode === 'dark' ? 'grey.300' : 'grey.800'), - textDecoration: 'none', - '&:hover': { - textDecoration: 'underline', + color: (theme) => + theme.palette.mode === "dark" ? "grey.300" : "grey.800", + textDecoration: "none", + "&:hover": { + textDecoration: "underline", }, }} > @@ -467,17 +642,18 @@ export const SightEdit = () => { )} {previewArticlePreview && ( - - Статья-предпросмотр:{' '} + + Статья-предпросмотр:{" "} (theme.palette.mode === 'dark' ? 'grey.300' : 'grey.800'), - textDecoration: 'none', - '&:hover': { - textDecoration: 'underline', + color: (theme) => + theme.palette.mode === "dark" ? "grey.300" : "grey.800", + textDecoration: "none", + "&:hover": { + textDecoration: "underline", }, }} > @@ -490,12 +666,24 @@ export const SightEdit = () => { {sightId && ( - - type="edit" parentId={sightId} parentResource="sight" childResource="article" fields={articleFields} title="статьи" /> + + + type="edit" + parentId={sightId} + parentResource="sight" + childResource="article" + fields={articleFields} + title="статьи" + /> - + )} - ) -} + ); +}; diff --git a/src/providers/data.ts b/src/providers/data.ts index dbea493..2a7b309 100644 --- a/src/providers/data.ts +++ b/src/providers/data.ts @@ -1,25 +1,27 @@ -import dataProvider from '@refinedev/simple-rest' -import axios from 'axios' -import {BACKEND_URL} from '../lib/constants' -import {TOKEN_KEY} from '../authProvider' -import Cookies from 'js-cookie' +import dataProvider from "@refinedev/simple-rest"; +import axios from "axios"; -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) => { // Добавляем токен авторизации - const token = localStorage.getItem(TOKEN_KEY) + const token = localStorage.getItem(TOKEN_KEY); if (token) { - config.headers.Authorization = `Bearer ${token}` + config.headers.Authorization = `Bearer ${token}`; } // Добавляем язык в кастомный заголовок - const lang = Cookies.get('lang') || 'ru' - config.headers['X-Language'] = lang // или 'Accept-Language' + const lang = Cookies.get("lang") || "ru"; + config.headers["X-Language"] = lang; // или 'Accept-Language' // 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);