diff --git a/package.json b/package.json index 7da922f..334f552 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,8 @@ "i18next": "^24.2.2", "js-cookie": "^3.0.5", "jwt-decode": "^4.0.0", + "mobx": "^6.13.7", + "mobx-react-lite": "^4.1.0", "react": "19.0.0", "react-beautiful-dnd": "^13.1.1", "react-dom": "19.0.0", diff --git a/src/components/CustomDataGrid.tsx b/src/components/CustomDataGrid.tsx index 4121e11..88594cf 100644 --- a/src/components/CustomDataGrid.tsx +++ b/src/components/CustomDataGrid.tsx @@ -1,80 +1,132 @@ -import {DataGrid, type DataGridProps, type GridColumnVisibilityModel} from '@mui/x-data-grid' -import {Stack, Button, Typography} from '@mui/material' -import {ExportButton} from '@refinedev/mui' -import {useExport} from '@refinedev/core' -import React, {useState, useEffect, useMemo} from 'react' -import Cookies from 'js-cookie' +import { + DataGrid, + type DataGridProps, + type GridColumnVisibilityModel, +} from "@mui/x-data-grid"; +import { Stack, Button, Typography } from "@mui/material"; +import { ExportButton } from "@refinedev/mui"; +import { useExport } from "@refinedev/core"; +import React, { useState, useEffect, useMemo } from "react"; +import Cookies from "js-cookie"; -import {localeText} from '../locales/ru/localeText' +import { localeText } from "../locales/ru/localeText"; interface CustomDataGridProps extends DataGridProps { - hasCoordinates?: boolean - resource?: string // Add this prop + hasCoordinates?: boolean; + resource?: string; // Add this prop } -const DEV_FIELDS = ['id', 'code', 'country_code', 'city_id', 'carrier_id', 'main_color', 'left_color', 'right_color', 'logo', 'slogan', 'filename', 'arms', 'thumbnail', 'route_sys_number', 'governor_appeal', 'scale_min', 'scale_max', 'rotate', 'center_latitude', 'center_longitude', 'watermark_lu', 'watermark_rd', 'left_article', 'preview_article', 'offset_x', 'offset_y'] as const +const DEV_FIELDS = [ + "id", + "code", + "country_code", + "city_id", + "carrier_id", + "main_color", + "left_color", + "right_color", + "logo", + "slogan", + "filename", + "arms", + "thumbnail", + "route_sys_number", + "governor_appeal", + "scale_min", + "scale_max", + "rotate", + "center_latitude", + "center_longitude", + "watermark_lu", + "watermark_rd", + "left_article", + "preview_article", + "offset_x", + "offset_y", +] as const; -export const CustomDataGrid = ({hasCoordinates = false, columns = [], resource, ...props}: CustomDataGridProps) => { +export const CustomDataGrid = ({ + hasCoordinates = false, + columns = [], + resource, + ...props +}: CustomDataGridProps) => { // const isDev = import.meta.env.DEV - const {triggerExport, isLoading: exportLoading} = useExport({ - resource: resource ?? '', + const { triggerExport, isLoading: exportLoading } = useExport({ + resource: resource ?? "", // pageSize: 100, #* // maxItemCount: 100, #* - }) + }); - const initialShowCoordinates = Cookies.get('showCoordinates') === 'true' - const initialShowDevData = false // Default to false in both prod and dev - const [showCoordinates, setShowCoordinates] = useState(initialShowCoordinates) - const [showDevData, setShowDevData] = useState(Cookies.get('showDevData') === 'true') + const initialShowCoordinates = Cookies.get("showCoordinates") === "true"; + const initialShowDevData = false; // Default to false in both prod and dev + const [showCoordinates, setShowCoordinates] = useState( + initialShowCoordinates + ); + const [showDevData, setShowDevData] = useState( + Cookies.get("showDevData") === "true" + ); - const availableDevFields = useMemo(() => DEV_FIELDS.filter((field) => columns.some((column) => column.field === field)), [columns]) + const availableDevFields = useMemo( + () => + DEV_FIELDS.filter((field) => + columns.some((column) => column.field === field) + ), + [columns] + ); const initialVisibilityModel = useMemo(() => { - const model: GridColumnVisibilityModel = {} + const model: GridColumnVisibilityModel = {}; availableDevFields.forEach((field) => { - model[field] = initialShowDevData - }) + model[field] = initialShowDevData; + }); if (hasCoordinates) { - model.latitude = initialShowCoordinates - model.longitude = initialShowCoordinates + model.latitude = initialShowCoordinates; + model.longitude = initialShowCoordinates; } - return model - }, [availableDevFields, hasCoordinates, initialShowCoordinates, initialShowDevData]) + return model; + }, [ + availableDevFields, + hasCoordinates, + initialShowCoordinates, + initialShowDevData, + ]); - const [columnVisibilityModel, setColumnVisibilityModel] = useState(initialVisibilityModel) + const [columnVisibilityModel, setColumnVisibilityModel] = + useState(initialVisibilityModel); useEffect(() => { setColumnVisibilityModel((prevModel) => { - const newModel = {...prevModel} + const newModel = { ...prevModel }; availableDevFields.forEach((field) => { - newModel[field] = showDevData - }) + newModel[field] = showDevData; + }); if (hasCoordinates) { - newModel.latitude = showCoordinates - newModel.longitude = showCoordinates + newModel.latitude = showCoordinates; + newModel.longitude = showCoordinates; } - return newModel - }) + return newModel; + }); if (hasCoordinates) { - Cookies.set('showCoordinates', String(showCoordinates)) + Cookies.set("showCoordinates", String(showCoordinates)); } - Cookies.set('showDevData', String(showDevData)) - }, [showCoordinates, showDevData, hasCoordinates, availableDevFields]) + Cookies.set("showDevData", String(showDevData)); + }, [showCoordinates, showDevData, hasCoordinates, availableDevFields]); const toggleCoordinates = () => { - setShowCoordinates((prev) => !prev) - } + setShowCoordinates((prev) => !prev); + }; const toggleDevData = () => { - setShowDevData((prev) => !prev) - } + setShowDevData((prev) => !prev); + }; return ( @@ -92,31 +144,38 @@ export const CustomDataGrid = ({hasCoordinates = false, columns = [], resource, // paginationModel: {pageSize: 25, page: 0}, // }, sorting: { - sortModel: [{field: 'id', sort: 'asc'}], + sortModel: [{ field: "id", sort: "asc" }], }, }} pageSizeOptions={[10, 25, 50, 100]} /> - + {hasCoordinates && ( )} - {(import.meta.env.DEV || showDevData) && availableDevFields.length > 0 && ( - - )} + {(import.meta.env.DEV || showDevData) && + availableDevFields.length > 0 && ( + + )} - - Экспорт + + Экспорт - ) -} + ); +}; diff --git a/src/components/LinkedItems.tsx b/src/components/LinkedItems.tsx index 976a4f8..bdf7cf8 100644 --- a/src/components/LinkedItems.tsx +++ b/src/components/LinkedItems.tsx @@ -24,8 +24,9 @@ import { import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import DragIndicatorIcon from "@mui/icons-material/DragIndicator"; import { axiosInstance } from "../providers/data"; -import { TOKEN_KEY } from "../authProvider"; + import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd"; +import axios from "axios"; function insertAtPosition(arr: T[], pos: number, value: T): T[] { const index = pos - 1; @@ -79,6 +80,41 @@ export const LinkedItems = ({ type, onSave, }: LinkedItemsProps) => { + const [articleLanguages, setArticleLanguages] = useState< + Record + >({}); + + const handleArticleLanguageChange = ( + articleId: number, + languageCode: string + ) => { + setArticleLanguages((prev) => ({ ...prev, [articleId]: languageCode })); + console.log(articleId, languageCode); + // Отправка запроса на сервер для сохранения языка + axios + .get( + `${import.meta.env.VITE_KRBL_API}/article/${articleId}/`, // Пример эндпоинта + { + headers: { + Authorization: `Bearer ${localStorage.getItem("refine-auth")}`, + "X-language": languageCode.toLowerCase(), + }, + } + ) + .then((response) => { + setLinkedItems( + linkedItems.map((item) => { + if (item.id == articleId) { + console.log(response.data); + return { ...response.data, language: languageCode }; + } else { + return item; + } + }) + ); + }); + }; + const [position, setPosition] = useState(1); const [items, setItems] = useState([]); const [linkedItems, setLinkedItems] = useState([]); @@ -88,6 +124,33 @@ export const LinkedItems = ({ const [mediaOrder, setMediaOrder] = useState(1); const theme = useTheme(); + let availableItems = items.filter( + (item) => !linkedItems.some((linked) => linked.id === item.id) + ); + useEffect(() => { + if (childResource == "station") { + availableItems = availableItems.sort((a, b) => + a.name.localeCompare(b.name) + ); + } + }, [childResource, availableItems]); + + useEffect(() => { + // При загрузке linkedItems можно запросить текущие языки для статей + if (childResource === "article" && linkedItems.length > 0) { + const initialLanguages: Record = {}; + linkedItems.forEach((article) => { + // Предполагается, что у объекта article есть свойство language + if (article.language) { + initialLanguages[article.id] = article.language; + } else { + initialLanguages[article.id] = "RU"; // Или другой язык по умолчанию + } + }); + setArticleLanguages(initialLanguages); + } + }, [linkedItems, childResource]); + const onDragEnd = (result: any) => { if (!result.destination) return; @@ -149,10 +212,6 @@ export const LinkedItems = ({ } }, [linkedItems, childResource, parentResource]); - const availableItems = items.filter( - (item) => !linkedItems.some((linked) => linked.id === item.id) - ); - const linkItem = () => { if (selectedItemId !== null) { const requestData = @@ -256,12 +315,19 @@ export const LinkedItems = ({ {field.label} ))} + {childResource === "article" && ( + Язык + )} {type === "edit" && ( Действие )} - + + {(provided) => ( ({ key={item.id} draggableId={"q" + String(item.id)} index={index} - isDragDisabled={type !== "edit" && dragAllowed} + isDragDisabled={type !== "edit" || !dragAllowed} > {(provided) => ( ({ {index + 1} - {fields.map((field) => ( - + {fields.map((field, index) => ( + {field.render ? field.render(item[field.data]) : item[field.data]} ))} + {childResource === "article" && ( + + + + handleArticleLanguageChange( + item.id, + "RU" + ) + } + > + RU + + + handleArticleLanguageChange( + item.id, + "EN" + ) + } + > + EN + + + handleArticleLanguageChange( + item.id, + "ZH" + ) + } + > + ZN + + + + )} {type === "edit" && ( - ))} - - - { - setMode() - }} - sx={{ - marginRight: '2px', - }} - > - {mode === 'dark' ? : } - - - {(user?.avatar || user?.name) && ( - - {user?.name && ( - - - {user?.name} - - - - {isAdmin ? 'Администратор' : 'Пользователь'} - - + + {city_id && cities && ( + )} - + + + {["ru", "en", "zh"].map((lang) => ( + + ))} - )} + + { + setMode(); + }} + sx={{ + marginRight: "2px", + }} + > + {mode === "dark" ? : } + + + {(user?.avatar || user?.name) && ( + + {user?.name && ( + + + {user?.name} + + + + {isAdmin ? "Администратор" : "Пользователь"} + + + )} + + + )} + - - - - ) -} + + + ); + } +); diff --git a/src/pages/article/create.tsx b/src/pages/article/create.tsx index 462c0ed..a650864 100644 --- a/src/pages/article/create.tsx +++ b/src/pages/article/create.tsx @@ -1,72 +1,196 @@ -import {Box, TextField, Typography, Paper} from '@mui/material' -import {Create} from '@refinedev/mui' -import {useForm} from '@refinedev/react-hook-form' -import {Controller} from 'react-hook-form' -import React, {useState, useEffect} from 'react' -import ReactMarkdown from 'react-markdown' +import { Box, TextField, Typography, Paper } from "@mui/material"; +import { Create } from "@refinedev/mui"; +import { useForm } from "@refinedev/react-hook-form"; +import { Controller } from "react-hook-form"; +import React, { useState, useEffect } from "react"; +import ReactMarkdown from "react-markdown"; +import Cookies from "js-cookie"; +import { MarkdownEditor } from "../../components/MarkdownEditor"; +import "easymde/dist/easymde.min.css"; -import {MarkdownEditor} from '../../components/MarkdownEditor' -import 'easymde/dist/easymde.min.css' - -const MemoizedSimpleMDE = React.memo(MarkdownEditor) +const MemoizedSimpleMDE = React.memo(MarkdownEditor); export const ArticleCreate = () => { + const [language, setLanguage] = useState(Cookies.get("lang")!); + const [articleData, setArticleData] = useState<{ + ru: { heading: string; body: string }; + en: { heading: string; body: string }; + zh: { heading: string; body: string }; + }>({ + ru: { heading: "", body: "" }, + en: { heading: "", body: "" }, + zh: { heading: "", body: "" }, + }); + const { saveButtonProps, - refineCore: {formLoading}, + refineCore: { formLoading }, register, control, watch, - formState: {errors}, + formState: { errors }, + setValue, } = useForm({ refineCoreProps: { - resource: 'article/', + resource: "article/", + meta: { + headers: { + "Accept-Language": language, + }, + }, }, - }) + }); - const [preview, setPreview] = useState('') - const [headingPreview, setHeadingPreview] = useState('') + useEffect(() => { + const lang = Cookies.get("lang")!; + Cookies.set("lang", language); + return () => { + Cookies.set("lang", lang); + }; + }, [language]); + + useEffect(() => { + setValue( + "heading", + articleData[language as keyof typeof articleData]?.heading || "" + ); + setValue( + "body", + articleData[language as keyof typeof articleData]?.body || "" + ); + setPreview(articleData[language as keyof typeof articleData]?.body || ""); + setHeadingPreview( + articleData[language as keyof typeof articleData]?.heading || "" + ); + }, [language, articleData, setValue]); + + const handleLanguageChange = (lang: string) => { + setArticleData((prevData) => ({ + ...prevData, + [language]: { + heading: watch("heading") || "", + body: watch("body") || "", + }, + })); + setLanguage(lang); + Cookies.set("lang", lang); + }; + + const [preview, setPreview] = useState(""); + const [headingPreview, setHeadingPreview] = useState(""); // Следим за изменениями в полях body и heading - const bodyContent = watch('body') - const headingContent = watch('heading') + 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 ( - + {/* Форма создания */} - - + + + handleLanguageChange("ru")} + > + RU + + handleLanguageChange("en")} + > + EN + + handleLanguageChange("zh")} + > + ZH + + + + - } /> + ( + + )} + /> + {/* Блок предпросмотра */} @@ -74,14 +198,15 @@ export const ArticleCreate = () => { 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", }} > @@ -93,7 +218,8 @@ export const ArticleCreate = () => { 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, }} > @@ -103,39 +229,41 @@ export const ArticleCreate = () => { {/* 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", }, }} > @@ -144,5 +272,5 @@ export const ArticleCreate = () => { - ) -} + ); +}; diff --git a/src/pages/article/edit.tsx b/src/pages/article/edit.tsx index c7a1cce..a999d87 100644 --- a/src/pages/article/edit.tsx +++ b/src/pages/article/edit.tsx @@ -2,7 +2,7 @@ import { Box, TextField, Typography, Paper } from "@mui/material"; import { Edit } from "@refinedev/mui"; import { useForm } from "@refinedev/react-hook-form"; import { Controller } from "react-hook-form"; -import { useParams } from "react-router"; +import { useLocation, useParams } from "react-router"; import React, { useState, useEffect } from "react"; import ReactMarkdown from "react-markdown"; import { useList } from "@refinedev/core"; @@ -12,31 +12,90 @@ import { LinkedItems } from "../../components/LinkedItems"; import { MediaItem, mediaFields } from "./types"; import { TOKEN_KEY } from "../../authProvider"; import "easymde/dist/easymde.min.css"; - +import Cookies from "js-cookie"; const MemoizedSimpleMDE = React.memo(MarkdownEditor); export const ArticleEdit = () => { + // const [initialLanguage] = useState(Cookies.get("lang")!); + // const { pathname } = useLocation(); + + // useEffect(() => { + // Cookies.set("lang", initialLanguage); + // }, [pathname]); + const [language, setLanguage] = useState(Cookies.get("lang")!); + const [articleData, setArticleData] = useState<{ + ru: { heading: string; body: string }; + en: { heading: string; body: string }; + zh: { heading: string; body: string }; + }>({ + ru: { heading: "", body: "" }, + en: { heading: "", body: "" }, + zh: { heading: "", body: "" }, + }); + const { id: articleId } = useParams<{ id: string }>(); + const [preview, setPreview] = useState(""); + const [headingPreview, setHeadingPreview] = useState(""); + const simpleMDEOptions = React.useMemo( + () => ({ + placeholder: "Введите контент в формате Markdown...", + spellChecker: false, + }), + [] + ); + const { saveButtonProps, register, control, + handleSubmit, watch, formState: { errors }, - } = useForm(); - - const { id: articleId } = useParams<{ id: string }>(); - const [preview, setPreview] = useState(""); - const [headingPreview, setHeadingPreview] = useState(""); - - // Получаем привязанные медиа - const { data: mediaData } = useList({ - resource: `article/${articleId}/media`, - queryOptions: { - enabled: !!articleId, + setValue, + } = useForm({ + refineCoreProps: { + meta: { + headers: { + "Accept-Language": language, + }, + }, }, }); - // Следим за изменениями в полях body и heading + useEffect(() => { + const lang = Cookies.get("lang")!; + Cookies.set("lang", language); + return () => { + Cookies.set("lang", lang); + }; + }, [language]); + + useEffect(() => { + setValue( + "heading", + articleData[language as keyof typeof articleData]?.heading || "" + ); + setValue( + "body", + articleData[language as keyof typeof articleData]?.body || "" + ); + setPreview(articleData[language as keyof typeof articleData]?.body || ""); + setHeadingPreview( + articleData[language as keyof typeof articleData]?.heading || "" + ); + }, [language, articleData, setValue]); + + const handleLanguageChange = (lang: string) => { + setArticleData((prevData) => ({ + ...prevData, + [language]: { + heading: watch("heading") || "", + body: watch("body") || "", + }, + })); + setLanguage(lang); + Cookies.set("lang", lang); + }; + const bodyContent = watch("body"); const headingContent = watch("heading"); @@ -48,62 +107,124 @@ export const ArticleEdit = () => { setHeadingPreview(headingContent || ""); }, [headingContent]); - const simpleMDEOptions = React.useMemo( - () => ({ - placeholder: "Введите контент в формате Markdown...", - spellChecker: false, - }), - [] - ); + const onSubmit = (data: { heading: string; body: string }) => { + // Здесь вы будете отправлять данные на сервер, + // учитывая текущий язык (language) + console.log("Данные для сохранения:", data, language); + // ... ваша логика сохранения ... + }; + + const { data: mediaData } = useList({ + resource: `article/${articleId}/media`, + queryOptions: { + enabled: !!articleId, + }, + }); return ( {/* Форма редактирования */} - - + {/* Форма создания */} + + + handleLanguageChange("ru")} + > + RU + + handleLanguageChange("en")} + > + EN + + handleLanguageChange("zh")} + > + ZH + + + + - ( - ( + + )} + /> + + {articleId && ( + + type="edit" + parentId={articleId} + parentResource="article" + childResource="media" + fields={mediaFields} + title="медиа" /> )} - /> - - {articleId && ( - - type="edit" - parentId={articleId} - parentResource="article" - childResource="media" - fields={mediaFields} - title="медиа" - /> - )} + {/* Блок предпросмотра */} diff --git a/src/pages/carrier/edit.tsx b/src/pages/carrier/edit.tsx index 22b4631..7c37d69 100644 --- a/src/pages/carrier/edit.tsx +++ b/src/pages/carrier/edit.tsx @@ -1,171 +1,191 @@ -import {Autocomplete, Box, TextField} from '@mui/material' -import {Edit, useAutocomplete} from '@refinedev/mui' -import {useForm} from '@refinedev/react-hook-form' -import {Controller} from 'react-hook-form' +import { Autocomplete, Box, TextField } from "@mui/material"; +import { Edit, useAutocomplete } from "@refinedev/mui"; +import { useForm } from "@refinedev/react-hook-form"; +import { Controller } from "react-hook-form"; export const CarrierEdit = () => { const { saveButtonProps, register, control, - 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, }, ], - }) + }); 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) => ( + + )} /> )} /> @@ -174,27 +194,44 @@ export const CarrierEdit = () => { name="logo" // rules={{required: 'Это поле является обязательным'}} 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) => ( + + )} /> )} /> - ) -} + ); +}; diff --git a/src/pages/carrier/list.tsx b/src/pages/carrier/list.tsx index 143a95e..136eb27 100644 --- a/src/pages/carrier/list.tsx +++ b/src/pages/carrier/list.tsx @@ -1,110 +1,173 @@ -import {type GridColDef} from '@mui/x-data-grid' -import {CustomDataGrid} from '../../components/CustomDataGrid' -import {DeleteButton, EditButton, List, ShowButton, useDataGrid} from '@refinedev/mui' -import React from 'react' +import { type GridColDef } from "@mui/x-data-grid"; +import { CustomDataGrid } from "../../components/CustomDataGrid"; +import { + DeleteButton, + EditButton, + List, + ShowButton, + useDataGrid, +} from "@refinedev/mui"; +import React from "react"; +import { observer } from "mobx-react-lite"; +import { cityStore } from "../../store/CityStore"; -export const CarrierList = () => { - const {dataGridProps} = useDataGrid({}) +export const CarrierList = observer(() => { + const { city_id } = cityStore; + const { dataGridProps } = useDataGrid({ + resource: "carrier", + filters: { + permanent: [ + { + field: "cityID", + operator: "eq", + value: city_id, + }, + ], + }, + }); const columns = React.useMemo( () => [ { - field: 'id', - headerName: 'ID', - type: 'number', + field: "id", + headerName: "ID", + type: "number", minWidth: 50, - align: 'left', - headerAlign: 'left', + align: "left", + headerAlign: "left", }, { - field: 'city_id', - headerName: 'ID Города', - type: 'number', + field: "city_id", + headerName: "ID Города", + type: "number", minWidth: 100, - align: 'left', - headerAlign: 'left', + align: "left", + headerAlign: "left", }, { - field: 'full_name', - headerName: 'Полное имя', - type: 'string', + field: "full_name", + headerName: "Полное имя", + type: "string", minWidth: 200, }, { - field: 'short_name', - headerName: 'Короткое имя', - type: 'string', + field: "short_name", + headerName: "Короткое имя", + type: "string", minWidth: 125, }, { - field: 'city', - headerName: 'Город', - type: 'string', + field: "city", + headerName: "Город", + type: "string", minWidth: 125, - align: 'left', - headerAlign: 'left', + align: "left", + headerAlign: "left", flex: 1, }, { - field: 'main_color', - headerName: 'Основной цвет', - type: 'string', + field: "main_color", + headerName: "Основной цвет", + type: "string", minWidth: 150, - renderCell: ({value}) =>
{value}
, + renderCell: ({ value }) => ( +
+ {value} +
+ ), }, { - field: 'left_color', - headerName: 'Цвет левого виджета', - type: 'string', + field: "left_color", + headerName: "Цвет левого виджета", + type: "string", minWidth: 150, - renderCell: ({value}) =>
{value}
, + renderCell: ({ value }) => ( +
+ {value} +
+ ), }, { - field: 'right_color', - headerName: 'Цвет правого виджета', - type: 'string', + field: "right_color", + headerName: "Цвет правого виджета", + type: "string", minWidth: 150, - renderCell: ({value}) =>
{value}
, + renderCell: ({ value }) => ( +
+ {value} +
+ ), }, { - field: 'logo', - headerName: 'Лого', - type: 'string', + field: "logo", + headerName: "Лого", + type: "string", minWidth: 150, }, { - field: 'slogan', - headerName: 'Слоган', - type: 'string', + field: "slogan", + headerName: "Слоган", + type: "string", minWidth: 150, }, { - field: 'actions', - headerName: 'Действия', + field: "actions", + headerName: "Действия", minWidth: 120, - display: 'flex', - align: 'right', - headerAlign: 'center', + display: "flex", + align: "right", + headerAlign: "center", sortable: false, filterable: false, disableColumnMenu: true, - renderCell: function render({row}) { + renderCell: function render({ row }) { return ( <> - + - ) + ); }, }, ], - [], - ) + [] + ); return ( - ) -} + ); +}); diff --git a/src/pages/route/list.tsx b/src/pages/route/list.tsx index 364d046..35ada92 100644 --- a/src/pages/route/list.tsx +++ b/src/pages/route/list.tsx @@ -1,156 +1,175 @@ -import {type GridColDef} from '@mui/x-data-grid' -import {CustomDataGrid} from '../../components/CustomDataGrid' -import {DeleteButton, EditButton, List, ShowButton, useDataGrid} from '@refinedev/mui' -import {Typography} from '@mui/material' -import React from 'react' +import { type GridColDef } from "@mui/x-data-grid"; +import { CustomDataGrid } from "../../components/CustomDataGrid"; +import { + DeleteButton, + EditButton, + List, + ShowButton, + useDataGrid, +} from "@refinedev/mui"; +import { Typography } from "@mui/material"; +import React from "react"; -import {localeText} from '../../locales/ru/localeText' +import { localeText } from "../../locales/ru/localeText"; export const RouteList = () => { - const {dataGridProps} = useDataGrid({ - resource: 'route/', - }) + const { dataGridProps } = useDataGrid({ + resource: "route/", + }); const columns = React.useMemo( () => [ { - field: 'id', - headerName: 'ID', - type: 'number', + field: "id", + headerName: "ID", + type: "number", minWidth: 70, - display: 'flex', - align: 'left', - headerAlign: 'left', + display: "flex", + align: "left", + headerAlign: "left", }, { - field: 'carrier_id', - headerName: 'ID перевозчика', - type: 'number', + field: "carrier_id", + headerName: "ID перевозчика", + type: "number", minWidth: 150, - display: 'flex', - align: 'left', - headerAlign: 'left', + display: "flex", + align: "left", + headerAlign: "left", }, { - field: 'carrier', - headerName: 'Перевозчик', - type: 'string', + field: "carrier", + headerName: "Перевозчик", + type: "string", minWidth: 150, - display: 'flex', - align: 'left', - headerAlign: 'left', + display: "flex", + align: "left", + headerAlign: "left", }, { - field: 'route_number', - headerName: 'Номер маршрута', - type: 'string', + field: "route_number", + headerName: "Номер маршрута", + type: "string", minWidth: 150, - display: 'flex', - align: 'left', - headerAlign: 'left', + display: "flex", + align: "left", + headerAlign: "left", }, { - field: 'route_sys_number', - headerName: 'Системный номер маршрута', - type: 'string', + field: "route_sys_number", + headerName: "Системный номер маршрута", + type: "string", minWidth: 120, - display: 'flex', - align: 'left', - headerAlign: 'left', + display: "flex", + align: "left", + headerAlign: "left", }, { - field: 'governor_appeal', - headerName: 'Обращение губернатора', - type: 'number', + field: "governor_appeal", + headerName: "Обращение губернатора", + type: "number", minWidth: 120, - display: 'flex', - align: 'left', - headerAlign: 'left', + display: "flex", + align: "left", + headerAlign: "left", }, { - field: 'scale_min', - headerName: 'Масштаб (мин)', - type: 'number', + field: "scale_min", + headerName: "Масштаб (мин)", + type: "number", minWidth: 120, - display: 'flex', - align: 'left', - headerAlign: 'left', + display: "flex", + align: "left", + headerAlign: "left", }, { - field: 'scale_max', - headerName: 'Масштаб (макс)', - type: 'number', + field: "scale_max", + headerName: "Масштаб (макс)", + type: "number", minWidth: 120, - display: 'flex', - align: 'left', - headerAlign: 'left', + display: "flex", + align: "left", + headerAlign: "left", }, { - field: 'rotate', - headerName: 'Поворот', - type: 'number', + field: "rotate", + headerName: "Поворот", + type: "number", minWidth: 120, - display: 'flex', - align: 'left', - headerAlign: 'left', + display: "flex", + align: "left", + headerAlign: "left", }, { - field: 'center_latitude', - headerName: 'Центр. широта', - type: 'number', + field: "center_latitude", + headerName: "Центр. широта", + type: "number", minWidth: 120, - display: 'flex', - align: 'left', - headerAlign: 'left', + display: "flex", + align: "left", + headerAlign: "left", }, { - field: 'center_longitude', - headerName: 'Центр. долгота', - type: 'number', + field: "center_longitude", + headerName: "Центр. долгота", + type: "number", minWidth: 120, - display: 'flex', - align: 'left', - headerAlign: 'left', + display: "flex", + align: "left", + headerAlign: "left", }, { - field: 'route_direction', - headerName: 'Направление маршрута', - type: 'boolean', - display: 'flex', - align: 'left', - headerAlign: 'left', + field: "route_direction", + headerName: "Направление маршрута", + type: "boolean", + display: "flex", + align: "left", + headerAlign: "left", minWidth: 120, flex: 1, - renderCell: ({value}) => {value ? 'прямое' : 'обратное'}, + renderCell: ({ value }) => ( + + {value ? "прямое" : "обратное"} + + ), }, { - field: 'actions', - headerName: 'Действия', - cellClassName: 'route-actions', - align: 'right', - headerAlign: 'center', + field: "actions", + headerName: "Действия", + cellClassName: "route-actions", + align: "right", + headerAlign: "center", minWidth: 120, - display: 'flex', + display: "flex", sortable: false, filterable: false, disableColumnMenu: true, - renderCell: function render({row}) { + renderCell: function render({ row }) { return ( <> - + - ) + ); }, }, ], - [], - ) + [] + ); return ( - row.id} /> + row.id} + /> - ) -} + ); +}; diff --git a/src/pages/route/types.ts b/src/pages/route/types.ts index a5bf814..9e46072 100644 --- a/src/pages/route/types.ts +++ b/src/pages/route/types.ts @@ -1,35 +1,36 @@ -import {VEHICLE_TYPES} from '../../lib/constants' +import { VEHICLE_TYPES } from "../../lib/constants"; export type StationItem = { - id: number - name: string - description: string - [key: string]: string | number -} + id: number; + name: string; + description: string; + [key: string]: string | number; +}; export type VehicleItem = { - id: number - tail_number: number - type: number - [key: string]: string | number -} + id: number; + tail_number: number; + type: number; + [key: string]: string | number; +}; export type FieldType = { - label: string - data: keyof T - render?: (value: any) => React.ReactNode -} + label: string; + data: keyof T; + render?: (value: any) => React.ReactNode; +}; export const stationFields: Array> = [ - {label: 'Название', data: 'system_name'}, - {label: 'Описание', data: 'description'}, -] + { label: "Название", data: "name" }, + { label: "Описание", data: "description" }, +]; export const vehicleFields: Array> = [ - {label: 'Бортовой номер', data: 'tail_number'}, + { label: "Бортовой номер", data: "tail_number" }, { - label: 'Тип', - data: 'type', - render: (value: number) => VEHICLE_TYPES.find((type) => type.value === value)?.label || value, + label: "Тип", + data: "type", + render: (value: number) => + VEHICLE_TYPES.find((type) => type.value === value)?.label || value, }, -] +]; diff --git a/src/pages/sight/create.tsx b/src/pages/sight/create.tsx index 36a2a69..24d819d 100644 --- a/src/pages/sight/create.tsx +++ b/src/pages/sight/create.tsx @@ -2,25 +2,45 @@ 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 { cityStore } from "../../store/CityStore"; import React, { useState, useEffect } from "react"; import { TOKEN_KEY } from "../../authProvider"; +import { observer } from "mobx-react-lite"; +import Cookies from "js-cookie"; +import { useLocation } from "react-router"; + +export const SightCreate = observer(() => { + const [language, setLanguage] = useState(Cookies.get("lang") || "ru"); + // Состояния для предпросмотра + const handleLanguageChange = (lang: string) => { + setLanguage(lang); + Cookies.set("lang", lang); + }; + + useEffect(() => { + const lang = Cookies.get("lang")!; + Cookies.set("lang", language); + + return () => { + Cookies.set("lang", lang); + }; + }, [language]); -export const SightCreate = () => { const { saveButtonProps, refineCore: { formLoading }, register, control, watch, + setValue, formState: { errors }, } = useForm({ refineCoreProps: { resource: "sight/", }, }); + const { city_id } = cityStore; - // Состояния для предпросмотра const [namePreview, setNamePreview] = useState(""); const [coordinatesPreview, setCoordinatesPreview] = useState({ latitude: "", @@ -37,6 +57,16 @@ export const SightCreate = () => { const [leftArticlePreview, setLeftArticlePreview] = useState(""); const [previewArticlePreview, setPreviewArticlePreview] = useState(""); + const handleCoordinatesChange = (e: React.ChangeEvent) => { + const [lat, lon] = e.target.value.split(",").map((s) => s.trim()); + setCoordinatesPreview({ + latitude: lat, + longitude: lon, + }); + setValue("latitude", lat); + setValue("longitude", lon); + }; + // Автокомплиты const { autocompleteProps: cityAutocompleteProps } = useAutocomplete({ resource: "city", @@ -47,6 +77,11 @@ export const SightCreate = () => { value, }, ], + meta: { + headers: { + "Accept-Language": language, + }, + }, }); const { autocompleteProps: mediaAutocompleteProps } = useAutocomplete({ @@ -73,6 +108,7 @@ export const SightCreate = () => { // Следим за изменениями во всех полях const nameContent = watch("name"); + const addressContent = watch("address"); const latitudeContent = watch("latitude"); const longitudeContent = watch("longitude"); const cityContent = watch("city_id"); @@ -114,6 +150,12 @@ export const SightCreate = () => { ); }, [thumbnailContent, mediaAutocompleteProps.options]); + useEffect(() => { + if (city_id) { + setValue("city_id", +city_id); + } + }, [city_id, setValue]); + useEffect(() => { const selectedWatermarkLU = mediaAutocompleteProps.options.find( (option) => option.id === watermarkLUContent @@ -157,313 +199,380 @@ export const SightCreate = () => { return ( - {/* Форма создания */} - - - - + + {/* Форма создания */} + + handleLanguageChange("ru")} + > + RU + + handleLanguageChange("en")} + > + EN + + handleLanguageChange("zh")} + > + ZH + + - ( - option.id === field.value - ) || null - } - onChange={(_, value) => { - field.onChange(value?.id || ""); - }} - getOptionLabel={(item) => { - return item ? item.name : ""; - }} - isOptionEqualToValue={(option, value) => { - return option.id === value?.id; - }} - filterOptions={(options, { inputValue }) => { - return options.filter((option) => - option.name.toLowerCase().includes(inputValue.toLowerCase()) - ); - }} - renderInput={(params) => ( - - )} - /> - )} - /> + + - ( - option.id === field.value - ) || null - } - onChange={(_, value) => { - field.onChange(value?.id || ""); - }} - getOptionLabel={(item) => { - return item ? item.media_name : ""; - }} - isOptionEqualToValue={(option, value) => { - return option.id === value?.id; - }} - filterOptions={(options, { inputValue }) => { - return options.filter((option) => - option.media_name - .toLowerCase() - .includes(inputValue.toLowerCase()) - ); - }} - renderInput={(params) => ( - - )} - /> - )} - /> + + + - ( - option.id === field.value - ) || null - } - onChange={(_, value) => { - field.onChange(value?.id || ""); - }} - getOptionLabel={(item) => { - return item ? item.media_name : ""; - }} - isOptionEqualToValue={(option, value) => { - return option.id === value?.id; - }} - filterOptions={(options, { inputValue }) => { - return options.filter((option) => - option.media_name - .toLowerCase() - .includes(inputValue.toLowerCase()) - ); - }} - renderInput={(params) => ( - - )} - /> - )} - /> + - ( - option.id === field.value - ) || null - } - onChange={(_, value) => { - field.onChange(value?.id || ""); - }} - getOptionLabel={(item) => { - return item ? item.media_name : ""; - }} - isOptionEqualToValue={(option, value) => { - return option.id === value?.id; - }} - filterOptions={(options, { inputValue }) => { - return options.filter((option) => - option.media_name - .toLowerCase() - .includes(inputValue.toLowerCase()) - ); - }} - renderInput={(params) => ( - - )} - /> - )} - /> + ( + option.id === field.value + ) || null + } + onChange={(_, value) => { + field.onChange(value?.id || ""); + }} + getOptionLabel={(item) => { + return item ? item.name : ""; + }} + isOptionEqualToValue={(option, value) => { + return option.id === value?.id; + }} + filterOptions={(options, { inputValue }) => { + return options.filter((option) => + option.name + .toLowerCase() + .includes(inputValue.toLowerCase()) + ); + }} + renderInput={(params) => ( + + )} + /> + )} + /> - ( - option.id === field.value - ) || null - } - onChange={(_, value) => { - field.onChange(value?.id || ""); - }} - getOptionLabel={(item) => { - return item ? item.heading : ""; - }} - isOptionEqualToValue={(option, value) => { - return option.id === value?.id; - }} - filterOptions={(options, { inputValue }) => { - return options.filter((option) => - option.heading - .toLowerCase() - .includes(inputValue.toLowerCase()) - ); - }} - renderInput={(params) => ( - - )} - /> - )} - /> + ( + option.id === field.value + ) || null + } + onChange={(_, value) => { + field.onChange(value?.id || ""); + }} + getOptionLabel={(item) => { + return item ? item.media_name : ""; + }} + isOptionEqualToValue={(option, value) => { + return option.id === value?.id; + }} + filterOptions={(options, { inputValue }) => { + return options.filter((option) => + option.media_name + .toLowerCase() + .includes(inputValue.toLowerCase()) + ); + }} + renderInput={(params) => ( + + )} + /> + )} + /> - ( - option.id === field.value - ) || null - } - onChange={(_, value) => { - field.onChange(value?.id || ""); - }} - getOptionLabel={(item) => { - return item ? item.heading : ""; - }} - isOptionEqualToValue={(option, value) => { - return option.id === value?.id; - }} - filterOptions={(options, { inputValue }) => { - return options.filter((option) => - option.heading - .toLowerCase() - .includes(inputValue.toLowerCase()) - ); - }} - renderInput={(params) => ( - - )} - /> - )} - /> + ( + option.id === field.value + ) || null + } + onChange={(_, value) => { + field.onChange(value?.id || ""); + }} + getOptionLabel={(item) => { + return item ? item.media_name : ""; + }} + isOptionEqualToValue={(option, value) => { + return option.id === value?.id; + }} + filterOptions={(options, { inputValue }) => { + return options.filter((option) => + option.media_name + .toLowerCase() + .includes(inputValue.toLowerCase()) + ); + }} + renderInput={(params) => ( + + )} + /> + )} + /> + + ( + option.id === field.value + ) || null + } + onChange={(_, value) => { + field.onChange(value?.id || ""); + }} + getOptionLabel={(item) => { + return item ? item.media_name : ""; + }} + isOptionEqualToValue={(option, value) => { + return option.id === value?.id; + }} + filterOptions={(options, { inputValue }) => { + return options.filter((option) => + option.media_name + .toLowerCase() + .includes(inputValue.toLowerCase()) + ); + }} + renderInput={(params) => ( + + )} + /> + )} + /> + + ( + option.id === field.value + ) || null + } + onChange={(_, value) => { + field.onChange(value?.id || ""); + }} + getOptionLabel={(item) => { + return item ? item.heading : ""; + }} + isOptionEqualToValue={(option, value) => { + return option.id === value?.id; + }} + filterOptions={(options, { inputValue }) => { + return options.filter((option) => + option.heading + .toLowerCase() + .includes(inputValue.toLowerCase()) + ); + }} + renderInput={(params) => ( + + )} + /> + )} + /> + + ( + option.id === field.value + ) || null + } + onChange={(_, value) => { + field.onChange(value?.id || ""); + }} + getOptionLabel={(item) => { + return item ? item.heading : ""; + }} + isOptionEqualToValue={(option, value) => { + return option.id === value?.id; + }} + filterOptions={(options, { inputValue }) => { + return options.filter((option) => + option.heading + .toLowerCase() + .includes(inputValue.toLowerCase()) + ); + }} + renderInput={(params) => ( + + )} + /> + )} + /> + - {/* Блок предпросмотра */} + {/* Preview Panel */} { Предпросмотр - {/* Название */} + {/* Название достопримечательности */} theme.palette.mode === "dark" ? "grey.300" : "grey.800", + mb: 3, }} > {namePreview} @@ -511,6 +621,22 @@ export const SightCreate = () => { + {/* Адрес */} + + + Адрес:{" "} + + + theme.palette.mode === "dark" ? "grey.300" : "grey.800", + }} + > + {addressContent} + + + {/* Координаты */} @@ -523,7 +649,7 @@ export const SightCreate = () => { theme.palette.mode === "dark" ? "grey.300" : "grey.800", }} > - {coordinatesPreview.latitude}, {coordinatesPreview.longitude} + {`${coordinatesPreview.latitude}, ${coordinatesPreview.longitude}`} @@ -543,8 +669,8 @@ export const SightCreate = () => { alt="Обложка" sx={{ maxWidth: "100%", - height: "auto", - borderRadius: 1, + height: "40vh", + borderRadius: 2, border: "1px solid", borderColor: "primary.main", }} @@ -593,7 +719,7 @@ export const SightCreate = () => { gutterBottom sx={{ color: "text.secondary" }} > - Правый нижний: + Правый верхний: { {/* Связанные статьи */} - - Связанные статьи: - {leftArticlePreview && ( Левая статья:{" "} theme.palette.mode === "dark" ? "grey.300" : "grey.800", - textDecoration: "none", - "&:hover": { - textDecoration: "underline", - }, }} > {leftArticlePreview} @@ -649,15 +763,10 @@ export const SightCreate = () => { Статья-предпросмотр:{" "} theme.palette.mode === "dark" ? "grey.300" : "grey.800", - textDecoration: "none", - "&:hover": { - textDecoration: "underline", - }, }} > {previewArticlePreview} @@ -669,4 +778,4 @@ export const SightCreate = () => { ); -}; +}); diff --git a/src/pages/sight/edit.tsx b/src/pages/sight/edit.tsx index 07e7c91..dde8043 100644 --- a/src/pages/sight/edit.tsx +++ b/src/pages/sight/edit.tsx @@ -1,4 +1,12 @@ -import { Autocomplete, Box, TextField, Paper, Typography } from "@mui/material"; +import { + Autocomplete, + Box, + TextField, + Paper, + Typography, + Tab, + Tabs, +} from "@mui/material"; import { Edit, useAutocomplete } from "@refinedev/mui"; import { useForm } from "@refinedev/react-hook-form"; import { Controller } from "react-hook-form"; @@ -9,17 +17,70 @@ import { CreateSightArticle } from "../../components/CreateSightArticle"; import { ArticleItem, articleFields } from "./types"; import { TOKEN_KEY } from "../../authProvider"; import { Link } from "react-router"; +import Cookies from "js-cookie"; + +function a11yProps(index: number) { + return { + id: `simple-tab-${index}`, + "aria-controls": `simple-tabpanel-${index}`, + }; +} + +interface TabPanelProps { + children?: React.ReactNode; + index: number; + value: number; +} + +function CustomTabPanel(props: TabPanelProps) { + const { children, value, index, ...other } = props; + + return ( + + ); +} export const SightEdit = () => { const { id: sightId } = useParams<{ id: string }>(); + const [language, setLanguage] = useState(Cookies.get("lang") || "ru"); + + const handleLanguageChange = (lang: string) => { + setLanguage(lang); + }; + + useEffect(() => { + const lang = Cookies.get("lang")!; + Cookies.set("lang", language); + return () => { + Cookies.set("lang", lang); + }; + }, [language]); const { saveButtonProps, register, control, watch, + getValues, + setValue, formState: { errors }, - } = useForm({}); + } = useForm({ + refineCoreProps: { + meta: { + headers: { + "Accept-Language": language, + }, + }, + }, + }); const { autocompleteProps: cityAutocompleteProps } = useAutocomplete({ resource: "city", @@ -30,8 +91,11 @@ export const SightEdit = () => { value, }, ], + queryOptions: { + queryKey: ["sight", language], + }, }); - + const [tabValue, setTabValue] = useState(0); const { autocompleteProps: mediaAutocompleteProps } = useAutocomplete({ resource: "media", onSearch: (value) => [ @@ -45,6 +109,9 @@ export const SightEdit = () => { const { autocompleteProps: articleAutocompleteProps } = useAutocomplete({ resource: "article", + queryOptions: { + queryKey: ["article", language], + }, onSearch: (value) => [ { field: "heading", @@ -54,6 +121,27 @@ export const SightEdit = () => { ], }); + useEffect(() => { + const latitude = getValues("latitude"); + const longitude = getValues("longitude"); + if (latitude && longitude) { + setCoordinatesPreview({ + latitude: latitude, + longitude: longitude, + }); + } + }, [getValues]); + + const handleCoordinatesChange = (e: React.ChangeEvent) => { + const [lat, lon] = e.target.value.split(",").map((s) => s.trim()); + setCoordinatesPreview({ + latitude: lat, + longitude: lon, + }); + setValue("latitude", lat); + setValue("longitude", lon); + }; + // Состояния для предпросмотра const [namePreview, setNamePreview] = useState(""); const [coordinatesPreview, setCoordinatesPreview] = useState({ @@ -72,6 +160,8 @@ export const SightEdit = () => { const [previewArticlePreview, setPreviewArticlePreview] = useState(""); // Следим за изменениями во всех полях + const coordinatesContent = watch("coordinates"); + const addressContent = watch("address"); const nameContent = watch("name"); const latitudeContent = watch("latitude"); const longitudeContent = watch("longitude"); @@ -155,41 +245,132 @@ export const SightEdit = () => { }, [previewArticleContent, articleAutocompleteProps.options]); return ( - - - {/* Форма редактирования */} - + + setTabValue(newValue)} + aria-label="basic tabs example" > - - + + + + + + + + + {/* Language Selection */} + + + handleLanguageChange("ru")} + > + RU + + handleLanguageChange("en")} + > + EN + + handleLanguageChange("zh")} + > + ZH + + + + {/* Форма редактирования */} + + + + + + + {/* { type="number" label={"Долгота *"} name="longitude" - /> + /> */} - ( - option.id === field.value - ) || null - } - onChange={(_, value) => { - field.onChange(value?.id || ""); - }} - getOptionLabel={(item) => { - return item ? item.name : ""; - }} - isOptionEqualToValue={(option, value) => { - return option.id === value?.id; - }} - filterOptions={(options, { inputValue }) => { - return options.filter((option) => - option.name.toLowerCase().includes(inputValue.toLowerCase()) - ); - }} - renderInput={(params) => ( - - )} - /> - )} - /> + {/* */} - ( - option.id === field.value - ) || null - } - onChange={(_, value) => { - field.onChange(value?.id || ""); - }} - getOptionLabel={(item) => { - return item ? item.media_name : ""; - }} - isOptionEqualToValue={(option, value) => { - return option.id === value?.id; - }} - filterOptions={(options, { inputValue }) => { - return options.filter((option) => - option.media_name - .toLowerCase() - .includes(inputValue.toLowerCase()) - ); - }} - renderInput={(params) => ( - - )} - /> - )} - /> + - ( - option.id === field.value - ) || null - } - onChange={(_, value) => { - field.onChange(value?.id || ""); - }} - getOptionLabel={(item) => { - return item ? item.media_name : ""; - }} - isOptionEqualToValue={(option, value) => { - return option.id === value?.id; - }} - filterOptions={(options, { inputValue }) => { - return options.filter((option) => - option.media_name - .toLowerCase() - .includes(inputValue.toLowerCase()) - ); - }} - renderInput={(params) => ( - - )} - /> - )} - /> + ( + option.id === field.value + ) || null + } + onChange={(_, value) => { + field.onChange(value?.id || ""); + }} + getOptionLabel={(item) => { + return item ? item.name : ""; + }} + isOptionEqualToValue={(option, value) => { + return option.id === value?.id; + }} + filterOptions={(options, { inputValue }) => { + return options.filter((option) => + option.name + .toLowerCase() + .includes(inputValue.toLowerCase()) + ); + }} + renderInput={(params) => ( + + )} + /> + )} + /> - ( - option.id === field.value - ) || null - } - onChange={(_, value) => { - field.onChange(value?.id || ""); - }} - getOptionLabel={(item) => { - return item ? item.media_name : ""; - }} - isOptionEqualToValue={(option, value) => { - return option.id === value?.id; - }} - filterOptions={(options, { inputValue }) => { - return options.filter((option) => - option.media_name - .toLowerCase() - .includes(inputValue.toLowerCase()) - ); - }} - renderInput={(params) => ( - - )} - /> - )} - /> + ( + option.id === field.value + ) || null + } + onChange={(_, value) => { + field.onChange(value?.id || ""); + }} + getOptionLabel={(item) => { + return item ? item.media_name : ""; + }} + isOptionEqualToValue={(option, value) => { + return option.id === value?.id; + }} + filterOptions={(options, { inputValue }) => { + return options.filter((option) => + option.media_name + .toLowerCase() + .includes(inputValue.toLowerCase()) + ); + }} + renderInput={(params) => ( + + )} + /> + )} + /> - ( - option.id === field.value - ) || null - } - onChange={(_, value) => { - field.onChange(value?.id || ""); - }} - getOptionLabel={(item) => { - return item ? item.heading : ""; - }} - isOptionEqualToValue={(option, value) => { - return option.id === value?.id; - }} - filterOptions={(options, { inputValue }) => { - return options.filter((option) => - option.heading - .toLowerCase() - .includes(inputValue.toLowerCase()) - ); - }} - renderInput={(params) => ( - - )} - /> - )} - /> + ( + option.id === field.value + ) || null + } + onChange={(_, value) => { + field.onChange(value?.id || ""); + }} + getOptionLabel={(item) => { + return item ? item.media_name : ""; + }} + isOptionEqualToValue={(option, value) => { + return option.id === value?.id; + }} + filterOptions={(options, { inputValue }) => { + return options.filter((option) => + option.media_name + .toLowerCase() + .includes(inputValue.toLowerCase()) + ); + }} + renderInput={(params) => ( + + )} + /> + )} + /> - ( - option.id === field.value - ) || null - } - onChange={(_, value) => { - field.onChange(value?.id || ""); - }} - getOptionLabel={(item) => { - return item ? item.heading : ""; - }} - isOptionEqualToValue={(option, value) => { - return option.id === value?.id; - }} - filterOptions={(options, { inputValue }) => { - return options.filter((option) => - option.heading - .toLowerCase() - .includes(inputValue.toLowerCase()) - ); - }} - renderInput={(params) => ( - - )} - /> - )} - /> - + ( + option.id === field.value + ) || null + } + onChange={(_, value) => { + field.onChange(value?.id || ""); + }} + getOptionLabel={(item) => { + return item ? item.media_name : ""; + }} + isOptionEqualToValue={(option, value) => { + return option.id === value?.id; + }} + filterOptions={(options, { inputValue }) => { + return options.filter((option) => + option.media_name + .toLowerCase() + .includes(inputValue.toLowerCase()) + ); + }} + renderInput={(params) => ( + + )} + /> + )} + /> - {/* Блок предпросмотра */} - - theme.palette.mode === "dark" ? "background.paper" : "#fff", - }} - > - - Предпросмотр - + ( + option.id === field.value + ) || null + } + onChange={(_, value) => { + field.onChange(value?.id || ""); + }} + getOptionLabel={(item) => { + return item ? item.heading : ""; + }} + isOptionEqualToValue={(option, value) => { + return option.id === value?.id; + }} + filterOptions={(options, { inputValue }) => { + return options.filter((option) => + option.heading + .toLowerCase() + .includes(inputValue.toLowerCase()) + ); + }} + renderInput={(params) => ( + + )} + /> + )} + /> - {/* Название достопримечательности */} - - theme.palette.mode === "dark" ? "grey.300" : "grey.800", - mb: 3, - }} - > - {namePreview} - - - {/* Город */} - - - Город:{" "} + ( + option.id === field.value + ) || null + } + onChange={(_, value) => { + field.onChange(value?.id || ""); + }} + getOptionLabel={(item) => { + return item ? item.heading : ""; + }} + isOptionEqualToValue={(option, value) => { + return option.id === value?.id; + }} + filterOptions={(options, { inputValue }) => { + return options.filter((option) => + option.heading + .toLowerCase() + .includes(inputValue.toLowerCase()) + ); + }} + renderInput={(params) => ( + + )} + /> + )} + /> + - - theme.palette.mode === "dark" ? "grey.300" : "grey.800", + flex: 1, + p: 2, + + position: "sticky", + top: 16, + borderRadius: 2, + border: "1px solid", + borderColor: "primary.main", + bgcolor: (theme) => + theme.palette.mode === "dark" ? "background.paper" : "#fff", }} > - {cityPreview} - - + + Предпросмотр + - {/* Координаты */} - - - Координаты:{" "} - - - theme.palette.mode === "dark" ? "grey.300" : "grey.800", - }} - > - {coordinatesPreview.latitude}, {coordinatesPreview.longitude} - - - - {/* Обложка */} - {thumbnailPreview && ( - + {/* Название достопримечательности */} - Обложка: - - + theme.palette.mode === "dark" ? "grey.300" : "grey.800", + mb: 3, }} - /> - - )} + > + {namePreview} + - {/* Водяные знаки */} - - - Водяные знаки: - - - {watermarkLUPreview && ( - + {/* Город */} + + + Город:{" "} + + + theme.palette.mode === "dark" ? "grey.300" : "grey.800", + }} + > + {cityPreview} + + + + {/* Адрес */} + + + Адрес:{" "} + + + theme.palette.mode === "dark" ? "grey.300" : "grey.800", + }} + > + {addressContent} + + + + {/* Координаты */} + + + Координаты:{" "} + + + theme.palette.mode === "dark" ? "grey.300" : "grey.800", + }} + > + {`${coordinatesPreview.latitude}, ${coordinatesPreview.longitude}`} + + + + {/* Обложка */} + {thumbnailPreview && ( + - Левый верхний: + Обложка: )} - {watermarkRDPreview && ( - - - Правый нижний: + + {/* Водяные знаки */} + + + Водяные знаки: + + + {watermarkLUPreview && ( + + + Левый верхний: + + + + )} + {watermarkRDPreview && ( + + + Правый верхний: + + + + )} + + + + {/* Связанные статьи */} + + {/* + Связанные статьи: + */} + {leftArticlePreview && ( + + + Левая статья:{" "} + + + theme.palette.mode === "dark" + ? "grey.300" + : "grey.800", + textDecoration: "none", + "&:hover": { + textDecoration: "underline", + }, + }} + > + {leftArticlePreview} + - - - )} - + )} + {previewArticlePreview && ( + + + Статья-предпросмотр:{" "} + + + theme.palette.mode === "dark" + ? "grey.300" + : "grey.800", + textDecoration: "none", + "&:hover": { + textDecoration: "underline", + }, + }} + > + {previewArticlePreview} + + + )} + + + + + + {sightId && ( + + + type="edit" + parentId={sightId} + parentResource="sight" + childResource="article" + fields={articleFields} + title="статьи" + /> - {/* Связанные статьи */} - - {/* - Связанные статьи: - */} - {leftArticlePreview && ( - - - Левая статья:{" "} - - - theme.palette.mode === "dark" ? "grey.300" : "grey.800", - textDecoration: "none", - "&:hover": { - textDecoration: "underline", - }, - }} - > - {leftArticlePreview} - - - )} - {previewArticlePreview && ( - - - Статья-предпросмотр:{" "} - - - theme.palette.mode === "dark" ? "grey.300" : "grey.800", - textDecoration: "none", - "&:hover": { - textDecoration: "underline", - }, - }} - > - {previewArticlePreview} - - - )} + - - - - {sightId && ( - - - type="edit" - parentId={sightId} - parentResource="sight" - childResource="article" - fields={articleFields} - title="статьи" - /> - - - - )} - + )} + +
); }; diff --git a/src/pages/sight/list.tsx b/src/pages/sight/list.tsx index 42d3177..a59b03a 100644 --- a/src/pages/sight/list.tsx +++ b/src/pages/sight/list.tsx @@ -1,128 +1,158 @@ -import React from 'react' -import {type GridColDef} from '@mui/x-data-grid' -import {DeleteButton, EditButton, List, ShowButton, useDataGrid} from '@refinedev/mui' -import {Stack} from '@mui/material' -import {CustomDataGrid} from '../../components/CustomDataGrid' -import {localeText} from '../../locales/ru/localeText' +import React from "react"; +import { type GridColDef } from "@mui/x-data-grid"; +import { + DeleteButton, + EditButton, + List, + ShowButton, + useDataGrid, +} from "@refinedev/mui"; +import { Stack } from "@mui/material"; +import { CustomDataGrid } from "../../components/CustomDataGrid"; +import { localeText } from "../../locales/ru/localeText"; +import { cityStore } from "../../store/CityStore"; +import { observer } from "mobx-react-lite"; -export const SightList = () => { - const {dataGridProps} = useDataGrid({resource: 'sight/'}) +export const SightList = observer(() => { + const { city_id } = cityStore; + const { dataGridProps } = useDataGrid({ + resource: "sight/", + filters: { + permanent: [ + { + field: "cityID", + operator: "eq", + value: city_id, + }, + ], + }, + }); const columns = React.useMemo( () => [ { - field: 'id', - headerName: 'ID', - type: 'number', + field: "id", + headerName: "ID", + type: "number", minWidth: 70, - display: 'flex', - align: 'left', - headerAlign: 'left', + display: "flex", + align: "left", + headerAlign: "left", }, { - field: 'name', - headerName: 'Название', - type: 'string', + field: "name", + headerName: "Название", + type: "string", minWidth: 150, - display: 'flex', - align: 'left', - headerAlign: 'left', + display: "flex", + align: "left", + headerAlign: "left", }, { - field: 'latitude', - headerName: 'Широта', - type: 'number', + field: "latitude", + headerName: "Широта", + type: "number", minWidth: 150, - display: 'flex', - align: 'left', - headerAlign: 'left', + display: "flex", + align: "left", + headerAlign: "left", }, { - field: 'longitude', - headerName: 'Долгота', - type: 'number', + field: "longitude", + headerName: "Долгота", + type: "number", minWidth: 150, - display: 'flex', - align: 'left', - headerAlign: 'left', + display: "flex", + align: "left", + headerAlign: "left", }, { - field: 'city_id', - headerName: 'ID города', - type: 'number', + field: "city_id", + headerName: "ID города", + type: "number", minWidth: 70, - display: 'flex', - align: 'left', - headerAlign: 'left', + display: "flex", + align: "left", + headerAlign: "left", }, { - field: 'city', - headerName: 'Город', - type: 'string', + field: "city", + headerName: "Город", + type: "string", minWidth: 100, - align: 'left', - headerAlign: 'left', + align: "left", + headerAlign: "left", flex: 1, }, { - field: 'thumbnail', - headerName: 'Карточка', - type: 'string', + field: "thumbnail", + headerName: "Карточка", + type: "string", minWidth: 150, }, { - field: 'watermark_lu', - headerName: 'Вод. знак (lu)', - type: 'string', + field: "watermark_lu", + headerName: "Вод. знак (lu)", + type: "string", minWidth: 150, }, { - field: 'watermark_rd', - headerName: 'Вод. знак (rd)', - type: 'string', + field: "watermark_rd", + headerName: "Вод. знак (rd)", + type: "string", minWidth: 150, }, { - field: 'left_article', - headerName: 'Левая статья', - type: 'number', + field: "left_article", + headerName: "Левая статья", + type: "number", minWidth: 150, }, { - field: 'preview_article', - headerName: 'Пред. просмотр статьи', - type: 'number', + field: "preview_article", + headerName: "Пред. просмотр статьи", + type: "number", minWidth: 150, }, { - field: 'actions', - headerName: 'Действия', + field: "actions", + headerName: "Действия", minWidth: 120, - display: 'flex', - align: 'right', - headerAlign: 'center', + display: "flex", + align: "right", + headerAlign: "center", sortable: false, filterable: false, disableColumnMenu: true, - renderCell: function render({row}) { + renderCell: function render({ row }) { return ( <> - + - ) + ); }, }, ], - [], - ) + [] + ); return ( - row.id} hasCoordinates /> + row.id} + hasCoordinates + /> - ) -} + ); +}); diff --git a/src/pages/sight/show.tsx b/src/pages/sight/show.tsx index 6ff6953..621fd7e 100644 --- a/src/pages/sight/show.tsx +++ b/src/pages/sight/show.tsx @@ -1,27 +1,28 @@ -import {Stack, Typography} from '@mui/material' -import {useShow} from '@refinedev/core' -import {Show, TextFieldComponent} from '@refinedev/mui' -import {LinkedItems} from '../../components/LinkedItems' -import {ArticleItem, articleFields} from './types' +import { Stack, Typography } from "@mui/material"; +import { useShow } from "@refinedev/core"; +import { Show, TextFieldComponent } from "@refinedev/mui"; +import { LinkedItems } from "../../components/LinkedItems"; +import { ArticleItem, articleFields } from "./types"; export const SightShow = () => { - const {query} = useShow({}) - const {data, isLoading} = query - const record = data?.data + const { query } = useShow({}); + const { data, isLoading } = query; + const record = data?.data; const fields = [ // {label: 'ID', data: 'id'}, - {label: 'Название', data: 'name'}, + { label: "Название", data: "name" }, // {label: 'Широта', data: 'latitude'}, #* // {label: 'Долгота', data: 'longitude'}, #* // {label: 'ID города', data: 'city_id'}, - {label: 'Город', data: 'city'}, - ] + { label: "Адрес", data: "address" }, + { label: "Город", data: "city" }, + ]; return ( - {fields.map(({label, data}) => ( + {fields.map(({ label, data }) => ( {label} @@ -30,8 +31,17 @@ export const SightShow = () => { ))} - {record?.id && type="show" parentId={record.id} parentResource="sight" childResource="article" fields={articleFields} title="статьи" />} + {record?.id && ( + + type="show" + parentId={record.id} + parentResource="sight" + childResource="article" + fields={articleFields} + title="статьи" + /> + )} - ) -} + ); +}; diff --git a/src/pages/station/create.tsx b/src/pages/station/create.tsx index 00b3940..4ff08c2 100644 --- a/src/pages/station/create.tsx +++ b/src/pages/station/create.tsx @@ -1,183 +1,253 @@ -import {Autocomplete, Box, TextField, Typography, Paper, Grid} 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, + Typography, + FormControlLabel, + Checkbox, + Grid, + Paper, +} from "@mui/material"; +import { Create, useAutocomplete } from "@refinedev/mui"; +import { useForm } from "@refinedev/react-hook-form"; +import { Controller } from "react-hook-form"; const TRANSFER_FIELDS = [ - {name: 'bus', label: 'Автобус'}, - {name: 'metro_blue', label: 'Метро (синяя)'}, - {name: 'metro_green', label: 'Метро (зеленая)'}, - {name: 'metro_orange', label: 'Метро (оранжевая)'}, - {name: 'metro_purple', label: 'Метро (фиолетовая)'}, - {name: 'metro_red', label: 'Метро (красная)'}, - {name: 'train', label: 'Электричка'}, - {name: 'tram', label: 'Трамвай'}, - {name: 'trolleybus', label: 'Троллейбус'}, -] + { name: "bus", label: "Автобус" }, + { name: "metro_blue", label: "Метро (синяя)" }, + { name: "metro_green", label: "Метро (зеленая)" }, + { name: "metro_orange", label: "Метро (оранжевая)" }, + { name: "metro_purple", label: "Метро (фиолетовая)" }, + { name: "metro_red", label: "Метро (красная)" }, + { name: "train", label: "Электричка" }, + { name: "tram", label: "Трамвай" }, + { name: "trolleybus", label: "Троллейбус" }, +]; export const StationCreate = () => { const { saveButtonProps, - refineCore: {formLoading}, + refineCore: { formLoading }, register, control, - formState: {errors}, + formState: { errors }, } = useForm({ refineCoreProps: { - resource: 'station/', + resource: "station/", }, - }) + }); - const {autocompleteProps: cityAutocompleteProps} = useAutocomplete({ - resource: 'city', + const { autocompleteProps: cityAutocompleteProps } = useAutocomplete({ + resource: "city", onSearch: (value) => [ { - field: 'name', - operator: 'contains', + field: "name", + operator: "contains", value, }, ], - }) + }); return ( - + + + + ( + field.onChange(e.target.checked)} + /> + } + /> + )} + /> ( + 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) => ( + + )} /> )} /> {/* Группа полей пересадок */} - + Пересадки {TRANSFER_FIELDS.map((field) => ( - + ))} - ) -} + ); +}; diff --git a/src/pages/station/edit.tsx b/src/pages/station/edit.tsx index 983ec74..6523772 100644 --- a/src/pages/station/edit.tsx +++ b/src/pages/station/edit.tsx @@ -1,179 +1,248 @@ -import {Autocomplete, Box, TextField, Typography, Paper, Grid} from '@mui/material' -import {Edit, useAutocomplete} from '@refinedev/mui' -import {useForm} from '@refinedev/react-hook-form' -import {Controller} from 'react-hook-form' +import { + Autocomplete, + Box, + TextField, + Typography, + FormControlLabel, + Paper, + Grid, + Checkbox, +} 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 {LinkedItems} from '../../components/LinkedItems' -import {type SightItem, sightFields} from './types' +import { useParams } from "react-router"; +import { LinkedItems } from "../../components/LinkedItems"; +import { type SightItem, sightFields } from "./types"; const TRANSFER_FIELDS = [ - {name: 'bus', label: 'Автобус'}, - {name: 'metro_blue', label: 'Метро (синяя)'}, - {name: 'metro_green', label: 'Метро (зеленая)'}, - {name: 'metro_orange', label: 'Метро (оранжевая)'}, - {name: 'metro_purple', label: 'Метро (фиолетовая)'}, - {name: 'metro_red', label: 'Метро (красная)'}, - {name: 'train', label: 'Электричка'}, - {name: 'tram', label: 'Трамвай'}, - {name: 'trolleybus', label: 'Троллейбус'}, -] + { name: "bus", label: "Автобус" }, + { name: "metro_blue", label: "Метро (синяя)" }, + { name: "metro_green", label: "Метро (зеленая)" }, + { name: "metro_orange", label: "Метро (оранжевая)" }, + { name: "metro_purple", label: "Метро (фиолетовая)" }, + { name: "metro_red", label: "Метро (красная)" }, + { name: "train", label: "Электричка" }, + { name: "tram", label: "Трамвай" }, + { name: "trolleybus", label: "Троллейбус" }, +]; export const StationEdit = () => { const { saveButtonProps, register, control, - formState: {errors}, - } = useForm({}) + formState: { errors }, + } = useForm({}); - const {id: stationId} = useParams<{id: string}>() + const { id: stationId } = useParams<{ id: string }>(); - const {autocompleteProps: cityAutocompleteProps} = useAutocomplete({ - resource: 'city', + const { autocompleteProps: cityAutocompleteProps } = useAutocomplete({ + resource: "city", onSearch: (value) => [ { - field: 'name', - operator: 'contains', + field: "name", + operator: "contains", value, }, ], - }) + }); return ( - + + ( + field.onChange(e.target.checked)} + /> + } + /> + )} + /> + ( + 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) => ( + + )} /> )} /> {/* Группа полей пересадок */} - + Пересадки {TRANSFER_FIELDS.map((field) => ( - + ))} @@ -188,8 +257,9 @@ export const StationEdit = () => { childResource="sight" fields={sightFields} title="достопримечательности" + dragAllowed={false} /> )} - ) -} + ); +}; diff --git a/src/pages/station/list.tsx b/src/pages/station/list.tsx index 6b858f7..fc8683f 100644 --- a/src/pages/station/list.tsx +++ b/src/pages/station/list.tsx @@ -1,126 +1,157 @@ -import React from 'react' -import {type GridColDef} from '@mui/x-data-grid' -import {DeleteButton, EditButton, List, ShowButton, useDataGrid} from '@refinedev/mui' -import {Stack} from '@mui/material' -import {CustomDataGrid} from '../../components/CustomDataGrid' -import {localeText} from '../../locales/ru/localeText' +import React, { useMemo } from "react"; +import { type GridColDef } from "@mui/x-data-grid"; +import { + DeleteButton, + EditButton, + List, + ShowButton, + useDataGrid, +} from "@refinedev/mui"; +import { Stack } from "@mui/material"; +import { CustomDataGrid } from "../../components/CustomDataGrid"; +import { localeText } from "../../locales/ru/localeText"; +import { cityStore } from "../../store/CityStore"; +import { observer } from "mobx-react-lite"; -export const StationList = () => { - const {dataGridProps} = useDataGrid({resource: 'station/'}) +export const StationList = observer(() => { + const { city_id } = cityStore; + + const { dataGridProps } = useDataGrid({ + resource: "station", + filters: { + permanent: [ + { + field: "cityID", + operator: "eq", + value: city_id, + }, + ], + }, + }); const columns = React.useMemo( () => [ { - field: 'id', - headerName: 'ID', - type: 'number', + field: "id", + headerName: "ID", + type: "number", minWidth: 70, - display: 'flex', - align: 'left', - headerAlign: 'left', + display: "flex", + align: "left", + headerAlign: "left", }, { - field: 'name', - headerName: 'Название', - type: 'string', + field: "name", + headerName: "Название", + type: "string", minWidth: 300, - display: 'flex', - align: 'left', - headerAlign: 'left', + display: "flex", + align: "left", + headerAlign: "left", }, { - field: 'system_name', - headerName: 'Системное название', - type: 'string', + field: "system_name", + headerName: "Системное название", + type: "string", minWidth: 200, - display: 'flex', - align: 'left', - headerAlign: 'left', + display: "flex", + align: "left", + headerAlign: "left", }, { - field: 'latitude', - headerName: 'Широта', - type: 'number', + field: "latitude", + headerName: "Широта", + type: "number", minWidth: 150, - display: 'flex', - align: 'left', - headerAlign: 'left', + display: "flex", + align: "left", + headerAlign: "left", }, { - field: 'longitude', - headerName: 'Долгота', - type: 'number', + field: "longitude", + headerName: "Долгота", + type: "number", minWidth: 150, - display: 'flex', - align: 'left', - headerAlign: 'left', + display: "flex", + align: "left", + headerAlign: "left", }, { - field: 'city_id', - headerName: 'ID города', - type: 'number', + field: "city_id", + headerName: "ID города", + type: "number", minWidth: 120, - display: 'flex', - align: 'left', - headerAlign: 'left', + display: "flex", + align: "left", + headerAlign: "left", }, { - field: 'offset_x', - headerName: 'Смещение (X)', - type: 'number', + field: "offset_x", + headerName: "Смещение (X)", + type: "number", minWidth: 120, - display: 'flex', - align: 'left', - headerAlign: 'left', + display: "flex", + align: "left", + headerAlign: "left", }, { - field: 'offset_y', - headerName: 'Смещение (Y)', - type: 'number', + field: "offset_y", + headerName: "Смещение (Y)", + type: "number", minWidth: 120, - display: 'flex', - align: 'left', - headerAlign: 'left', + display: "flex", + align: "left", + headerAlign: "left", }, { - field: 'description', - headerName: 'Описание', - type: 'string', - display: 'flex', - align: 'left', - headerAlign: 'left', + field: "description", + headerName: "Описание", + type: "string", + display: "flex", + align: "left", + headerAlign: "left", flex: 1, }, { - field: 'actions', - headerName: 'Действия', - cellClassName: 'station-actions', - align: 'right', - headerAlign: 'center', + field: "actions", + headerName: "Действия", + cellClassName: "station-actions", + align: "right", + headerAlign: "center", minWidth: 120, - display: 'flex', + display: "flex", sortable: false, filterable: false, disableColumnMenu: true, - renderCell: function render({row}) { + renderCell: function render({ row }) { return ( <> - + - ) + ); }, }, ], - [], - ) + [] + ); return ( - + - row.id} hasCoordinates /> + row.id} + hasCoordinates + /> - ) -} + ); +}); diff --git a/src/pages/station/show.tsx b/src/pages/station/show.tsx index 2d1286c..63609d9 100644 --- a/src/pages/station/show.tsx +++ b/src/pages/station/show.tsx @@ -1,23 +1,31 @@ -import {useShow} from '@refinedev/core' -import {Show, TextFieldComponent as TextField} from '@refinedev/mui' -import {Stack, Typography} from '@mui/material' -import {LinkedItems} from '../../components/LinkedItems' -import {type SightItem, sightFields, stationFields} from './types' +import { useShow } from "@refinedev/core"; +import { Show, TextFieldComponent as TextField } from "@refinedev/mui"; +import { Box, Stack, Typography } from "@mui/material"; +import { LinkedItems } from "../../components/LinkedItems"; +import { type SightItem, sightFields, stationFields } from "./types"; export const StationShow = () => { - const {query} = useShow({}) - const {data, isLoading} = query - const record = data?.data + const { query } = useShow({}); + const { data, isLoading } = query; + const record = data?.data; return ( - {stationFields.map(({label, data}) => ( + {stationFields.map(({ label, data }) => ( {label} + {label === "Системное название" && ( + + + + )} - + + ))} @@ -33,5 +41,5 @@ export const StationShow = () => { )} - ) -} + ); +}; diff --git a/src/pages/station/types.ts b/src/pages/station/types.ts index 6ac865b..5cd0d02 100644 --- a/src/pages/station/types.ts +++ b/src/pages/station/types.ts @@ -1,44 +1,45 @@ -import React from 'react' +import React from "react"; export type StationItem = { - id: number - name: string - description: string - latitude: number - longitude: number - [key: string]: string | number -} + id: number; + name: string; + description: string; + latitude: number; + longitude: number; + [key: string]: string | number; +}; export type SightItem = { - id: number - name: string - latitude: number - longitude: number - city_id: number - city: string - [key: string]: string | number -} + id: number; + name: string; + latitude: number; + longitude: number; + city_id: number; + city: string; + [key: string]: string | number; +}; export type FieldType = { - label: string - data: keyof T - render?: (value: any) => React.ReactNode -} + label: string; + data: keyof T; + render?: (value: any) => React.ReactNode; +}; export const stationFields: Array> = [ // {label: 'ID', data: 'id'}, - {label: 'Название', data: 'name'}, - {label: 'Системное название', data: 'system_name'}, + { label: "Название", data: "name" }, + { label: "Системное название", data: "system_name" }, + { label: "Адрес", data: "address" }, // {label: 'Широта', data: 'latitude'}, // {label: 'Долгота', data: 'longitude'}, - {label: 'Описание', data: 'description'}, -] + { label: "Описание", data: "description" }, +]; export const sightFields: Array> = [ // {label: 'ID', data: 'id'}, - {label: 'Название', data: 'name'}, + { label: "Название", data: "name" }, // {label: 'Широта', data: 'latitude'}, // {label: 'Долгота', data: 'longitude'}, // {label: 'ID города', data: 'city_id'}, - {label: 'Город', data: 'city'}, -] + { label: "Город", data: "city" }, +]; diff --git a/src/providers/data.ts b/src/providers/data.ts index c3a7980..86777ff 100644 --- a/src/providers/data.ts +++ b/src/providers/data.ts @@ -1,14 +1,22 @@ import dataProvider from "@refinedev/simple-rest"; -import axios from "axios"; +import axios, { InternalAxiosRequestConfig } from "axios"; import { TOKEN_KEY } from "../authProvider"; import Cookies from "js-cookie"; +interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig { + meta?: { + headers?: { + "X-Language"?: string; + }; + }; +} + export const axiosInstance = axios.create({ baseURL: import.meta.env.VITE_KRBL_API, }); -axiosInstance.interceptors.request.use((config) => { +axiosInstance.interceptors.request.use((config: CustomAxiosRequestConfig) => { // Добавляем токен авторизации const token = localStorage.getItem(TOKEN_KEY); if (token) { @@ -16,9 +24,15 @@ axiosInstance.interceptors.request.use((config) => { } // Добавляем язык в кастомный заголовок - const lang = Cookies.get("lang") || "ru"; - config.headers["X-Language"] = lang; // или 'Accept-Language' - + const metaLang = config.meta?.headers?.["X-Language"]; + if (metaLang) { + console.log("metaLang", metaLang); + config.headers["X-Language"] = metaLang; + } else { + const lang = Cookies.get("lang") || "ru"; + console.log("lang", lang); + config.headers["X-Language"] = lang; + } // console.log('Request headers:', config.headers) return config; diff --git a/src/store/CityStore.ts b/src/store/CityStore.ts new file mode 100644 index 0000000..71b9fae --- /dev/null +++ b/src/store/CityStore.ts @@ -0,0 +1,21 @@ +import { makeAutoObservable } from "mobx"; + +class CityStore { + city_id: string = ""; + + constructor() { + makeAutoObservable(this); + this.initialize(); + } + + initialize() { + this.city_id = localStorage.getItem("city_id") || "1"; + } + + setCityIdAction = (city_id: string) => { + this.city_id = city_id; + localStorage.setItem("city_id", city_id); + }; +} + +export const cityStore = new CityStore(); diff --git a/yarn.lock b/yarn.lock index eb75ed7..81f0be6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4789,6 +4789,18 @@ mkdirp@^0.5.1: dependencies: minimist "^1.2.6" +mobx-react-lite@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-4.1.0.tgz#6a03ed2d94150848213cfebd7d172e123528a972" + integrity sha512-QEP10dpHHBeQNv1pks3WnHRCem2Zp636lq54M2nKO2Sarr13pL4u6diQXf65yzXUn0mkk18SyIDCm9UOJYTi1w== + dependencies: + use-sync-external-store "^1.4.0" + +mobx@^6.13.7: + version "6.13.7" + resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.13.7.tgz#70e5dda7a45da947f773b3cd3b065dfe7c8a75de" + integrity sha512-aChaVU/DO5aRPmk1GX8L+whocagUUpBQqoPtJk+cm7UOXUk87J4PeWCh6nNmTTIfEhiR9DI/+FnA8dln/hTK7g== + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"