feat: Add carriers translation on 3 languages

This commit is contained in:
2025-06-13 11:17:18 +03:00
parent f49caf3ec8
commit 2117a6836e
34 changed files with 645 additions and 344 deletions

View File

@ -39,6 +39,7 @@ import {
RouteCreatePage, RouteCreatePage,
RoutePreview, RoutePreview,
RouteEditPage, RouteEditPage,
ArticlePreviewPage,
} from "@pages"; } from "@pages";
import { authStore, createSightStore, editSightStore } from "@shared"; import { authStore, createSightStore, editSightStore } from "@shared";
import { Layout } from "@widgets"; import { Layout } from "@widgets";
@ -170,7 +171,7 @@ const router = createBrowserRouter([
// { path: "vehicle/:id/edit", element: <VehicleEditPage /> }, // { path: "vehicle/:id/edit", element: <VehicleEditPage /> },
// Article // Article
{ path: "article", element: <ArticleListPage /> }, { path: "article", element: <ArticleListPage /> },
// { path: "article/:id", element: <ArticlePreviewPage /> }, { path: "article/:id", element: <ArticlePreviewPage /> },
// { path: "media/create", element: <CreateMediaPage /> }, // { path: "media/create", element: <CreateMediaPage /> },
], ],
}, },

View File

@ -1,4 +1,4 @@
import React, { useState } from "react"; import React from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { ArrowLeft } from "lucide-react"; import { ArrowLeft } from "lucide-react";
import { LanguageSwitcher } from "@widgets"; import { LanguageSwitcher } from "@widgets";

View File

@ -1,14 +1,14 @@
import React, { useEffect, useState } from "react"; import React, { useEffect } from "react";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { ArrowLeft } from "lucide-react"; import { ArrowLeft } from "lucide-react";
import { LanguageSwitcher } from "@widgets"; import { LanguageSwitcher } from "@widgets";
import { articlesStore, languageStore } from "@shared"; import { articlesStore } from "@shared";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
const ArticleEditPage: React.FC = observer(() => { const ArticleEditPage: React.FC = observer(() => {
const navigate = useNavigate(); const navigate = useNavigate();
const { id } = useParams(); const { id } = useParams();
const { language } = languageStore;
const { articleData, getArticle } = articlesStore; const { articleData, getArticle } = articlesStore;
useEffect(() => { useEffect(() => {

View File

@ -0,0 +1,85 @@
import { Paper, Box, Typography } from "@mui/material";
import { MediaViewer, ReactMarkdownComponent } from "@widgets";
import { articlesStore, languageStore } from "@shared";
import { observer } from "mobx-react-lite";
export const PreviewLeftWidget = observer(() => {
const { articleMedia, articleData } = articlesStore;
const { language } = languageStore;
return (
<Paper
elevation={3}
sx={{
width: "100%",
minWidth: 320,
background:
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
overflowY: "auto",
display: "flex",
flexDirection: "column",
borderRadius: "10px",
}}
>
<Box
sx={{
overflow: "hidden",
width: "100%",
minHeight: 100,
padding: "3px",
display: "flex",
alignItems: "center",
justifyContent: "center",
"& img": {
borderTopLeftRadius: "10px",
borderTopRightRadius: "10px",
width: "100%",
height: "auto",
objectFit: "contain",
},
}}
>
{articleMedia && <MediaViewer media={articleMedia} fullWidth />}
</Box>
<Box
sx={{
background:
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
color: "white",
margin: "5px 0px 5px 0px",
display: "flex",
flexDirection: "column",
gap: 1,
padding: 1,
}}
>
<Typography
variant="h5"
component="h2"
sx={{
wordBreak: "break-word",
fontSize: "24px",
fontWeight: 700,
lineHeight: "120%",
}}
>
{articleData?.[language]?.heading || "Название информации"}
</Typography>
</Box>
{articleData?.[language]?.body && (
<Box
sx={{
padding: 1,
maxHeight: "300px",
overflowY: "scroll",
background:
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
flexGrow: 1,
}}
>
<ReactMarkdownComponent value={articleData?.[language]?.body} />
</Box>
)}
</Paper>
);
});

View File

@ -0,0 +1,139 @@
import { Paper, Box, Typography } from "@mui/material";
import { MediaViewer, ReactMarkdownComponent } from "@widgets";
import { articlesStore, languageStore } from "@shared";
import { observer } from "mobx-react-lite";
import { ImagePlus } from "lucide-react";
export const PreviewRightWidget = observer(() => {
const { articleData, articleMedia } = articlesStore;
const { language } = languageStore;
const article = articleData?.[language];
if (!article) return null;
return (
<Paper
className="flex-1 flex flex-col max-w-[500px]"
sx={{
borderRadius: "10px",
overflow: "hidden",
}}
elevation={2}
>
<Box
className="overflow-hidden"
sx={{
width: "100%",
background: "#877361",
borderColor: "grey.300",
display: "flex",
flexDirection: "column",
}}
>
{articleMedia ? (
<Box
sx={{
overflow: "hidden",
width: "100%",
padding: "2px 2px 0px 2px",
"& img": {
borderTopLeftRadius: "10px",
borderTopRightRadius: "10px",
width: "100%",
height: "auto",
objectFit: "contain",
},
}}
>
<MediaViewer media={articleMedia} fullWidth />
</Box>
) : (
<Box
sx={{
width: "100%",
height: 200,
flexShrink: 0,
backgroundColor: "rgba(0,0,0,0.1)",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<ImagePlus size={48} color="white" />
</Box>
)}
<Box
sx={{
p: 1,
wordBreak: "break-word",
fontSize: "24px",
fontWeight: 700,
lineHeight: "120%",
backdropFilter: "blur(12px)",
borderBottom: "1px solid #A89F90",
boxShadow: "inset 4px 4px 12px 0 rgba(255,255,255,0.12)",
background:
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
}}
>
<Typography variant="h6" color="white">
{article.heading || "Выберите статью"}
</Typography>
</Box>
<Box
sx={{
padding: 1,
minHeight: "200px",
maxHeight: "300px",
overflowY: "scroll",
background:
"rgba(179, 165, 152, 0.4), linear-gradient(180deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.2) 100%)",
flexGrow: 1,
}}
>
{article.body ? (
<ReactMarkdownComponent value={article.body} />
) : (
<Typography
color="rgba(255,255,255,0.7)"
sx={{ textAlign: "center", mt: 4 }}
>
Предпросмотр статьи появится здесь
</Typography>
)}
</Box>
{/* @ts-ignore */}
{articleData?.right && articleData?.right.length > 1 && (
<Box
sx={{
p: 2,
display: "flex",
justifyContent: "space-between",
fontSize: "24px",
fontWeight: 700,
lineHeight: "120%",
flexWrap: "wrap",
gap: 1,
backdropFilter: "blur(12px)",
boxShadow: "inset 4px 4px 12px 0 rgba(255,255,255,0.12)",
background:
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
}}
>
{/* @ts-ignore */}
{articleData.right.map((a, idx) => (
<button
key={idx}
className="inline-block text-left text-xs text-white"
>
{a.heading}
</button>
))}
</Box>
)}
</Box>
</Paper>
);
});

View File

@ -0,0 +1,57 @@
import { useNavigate, useParams } from "react-router-dom";
import { useEffect } from "react";
import { Box } from "@mui/material";
import { PreviewLeftWidget } from "./PreviewLeftWidget";
import { PreviewRightWidget } from "./PreviewRightWidget";
import { articlesStore, languageStore } from "@shared";
import { ArrowLeft } from "lucide-react";
export const ArticlePreviewPage = () => {
const navigate = useNavigate();
const { id } = useParams();
const { getArticle, getArticleMedia, getArticlePreview } = articlesStore;
const { language } = languageStore;
useEffect(() => {
const fetchData = async () => {
if (id) {
await getArticle(Number(id), language);
await getArticleMedia(Number(id));
await getArticlePreview(Number(id));
}
};
fetchData();
}, [id, language]);
return (
<>
<div className="flex items-center gap-4 mb-10">
<button
className="flex items-center gap-2"
onClick={() => navigate(-1)}
>
<ArrowLeft size={20} />
Назад
</button>
</div>
<Box
sx={{
display: "flex",
gap: 2,
p: 2,
justifyContent: "center",
margin: "0 auto",
}}
>
<Box sx={{ width: "320px" }}>
<PreviewLeftWidget />
</Box>
<Box sx={{ width: "500px" }}>
<PreviewRightWidget />
</Box>
</Box>
</>
);
};

View File

@ -1 +1,2 @@
export * from "./ArticleListPage"; export * from "./ArticleListPage";
export * from "./ArticlePreviewPage";

View File

@ -12,9 +12,9 @@ import { ArrowLeft, Save } from "lucide-react";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { carrierStore, cityStore, mediaStore, languageStore } from "@shared"; import { carrierStore, cityStore, mediaStore } from "@shared";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { MediaViewer, ImageUploadCard, LanguageSwitcher } from "@widgets"; import { ImageUploadCard, LanguageSwitcher } from "@widgets";
import { import {
SelectMediaDialog, SelectMediaDialog,
UploadMediaDialog, UploadMediaDialog,
@ -23,7 +23,7 @@ import {
export const CarrierCreatePage = observer(() => { export const CarrierCreatePage = observer(() => {
const navigate = useNavigate(); const navigate = useNavigate();
const { language } = languageStore;
const [fullName, setFullName] = useState(""); const [fullName, setFullName] = useState("");
const [shortName, setShortName] = useState(""); const [shortName, setShortName] = useState("");
const [cityId, setCityId] = useState<number | null>(null); const [cityId, setCityId] = useState<number | null>(null);

View File

@ -12,9 +12,9 @@ import { ArrowLeft, Save } from "lucide-react";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { carrierStore, cityStore, mediaStore } from "@shared"; import { carrierStore, cityStore, mediaStore, languageStore } from "@shared";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { MediaViewer, ImageUploadCard } from "@widgets"; import { ImageUploadCard, LanguageSwitcher } from "@widgets";
import { import {
SelectMediaDialog, SelectMediaDialog,
UploadMediaDialog, UploadMediaDialog,
@ -24,8 +24,8 @@ import {
export const CarrierEditPage = observer(() => { export const CarrierEditPage = observer(() => {
const navigate = useNavigate(); const navigate = useNavigate();
const { id } = useParams(); const { id } = useParams();
const { carrier, getCarrier, setEditCarrierData, editCarrierData } = const { getCarrier, setEditCarrierData, editCarrierData } = carrierStore;
carrierStore; const { language } = languageStore;
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false); const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false);
@ -41,16 +41,34 @@ export const CarrierEditPage = observer(() => {
await cityStore.getCities("ru"); await cityStore.getCities("ru");
await cityStore.getCities("en"); await cityStore.getCities("en");
await cityStore.getCities("zh"); await cityStore.getCities("zh");
await getCarrier(Number(id)); const carrierData = await getCarrier(Number(id));
setEditCarrierData( if (carrierData) {
carrier?.[Number(id)]?.full_name as string, setEditCarrierData(
carrier?.[Number(id)]?.short_name as string, carrierData.ru?.full_name || "",
carrierData.ru?.short_name || "",
carrier?.[Number(id)]?.city_id as number, carrierData.ru?.city_id || 0,
carrier?.[Number(id)]?.slogan as string, carrierData.ru?.slogan || "",
carrier?.[Number(id)]?.logo as string carrierData.ru?.logo || "",
); "ru"
);
setEditCarrierData(
carrierData.en?.full_name || "",
carrierData.en?.short_name || "",
carrierData.en?.city_id || 0,
carrierData.en?.slogan || "",
carrierData.en?.logo || "",
"en"
);
setEditCarrierData(
carrierData.zh?.full_name || "",
carrierData.zh?.short_name || "",
carrierData.zh?.city_id || 0,
carrierData.zh?.slogan || "",
carrierData.zh?.logo || "",
"zh"
);
}
mediaStore.getMedia(); mediaStore.getMedia();
})(); })();
@ -76,12 +94,12 @@ export const CarrierEditPage = observer(() => {
media_type: number; media_type: number;
}) => { }) => {
setEditCarrierData( setEditCarrierData(
editCarrierData.full_name, editCarrierData[language].full_name,
editCarrierData.short_name, editCarrierData[language].short_name,
editCarrierData.city_id, editCarrierData.city_id,
editCarrierData.slogan, editCarrierData[language].slogan,
media.id media.id,
language
); );
}; };
@ -91,6 +109,7 @@ export const CarrierEditPage = observer(() => {
return ( return (
<Paper className="w-full h-full p-3 flex flex-col gap-10"> <Paper className="w-full h-full p-3 flex flex-col gap-10">
<LanguageSwitcher />
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<button <button
className="flex items-center gap-2" className="flex items-center gap-2"
@ -101,6 +120,9 @@ export const CarrierEditPage = observer(() => {
</button> </button>
</div> </div>
<div className="flex gap-10 items-center mb-5 max-w-[80%] self-start">
<h1 className="text-3xl break-words">{editCarrierData.ru.full_name}</h1>
</div>
<div className="flex flex-col gap-10 w-full items-end"> <div className="flex flex-col gap-10 w-full items-end">
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel>Город</InputLabel> <InputLabel>Город</InputLabel>
@ -110,15 +132,16 @@ export const CarrierEditPage = observer(() => {
required required
onChange={(e) => onChange={(e) =>
setEditCarrierData( setEditCarrierData(
editCarrierData.full_name, editCarrierData[language].full_name,
editCarrierData.short_name, editCarrierData[language].short_name,
Number(e.target.value), Number(e.target.value),
editCarrierData.slogan, editCarrierData[language].slogan,
editCarrierData.logo editCarrierData.logo,
language
) )
} }
> >
{cityStore.cities.ru.data?.map((city) => ( {cityStore.cities[language].data?.map((city) => (
<MenuItem key={city.id} value={city.id}> <MenuItem key={city.id} value={city.id}>
{city.name} {city.name}
</MenuItem> </MenuItem>
@ -129,16 +152,16 @@ export const CarrierEditPage = observer(() => {
<TextField <TextField
fullWidth fullWidth
label="Полное название" label="Полное название"
value={editCarrierData.full_name} value={editCarrierData[language].full_name}
required required
onChange={(e) => onChange={(e) =>
setEditCarrierData( setEditCarrierData(
e.target.value, e.target.value,
editCarrierData.short_name, editCarrierData[language].short_name,
editCarrierData.city_id, editCarrierData.city_id,
editCarrierData.slogan, editCarrierData[language].slogan,
editCarrierData.logo editCarrierData.logo,
language
) )
} }
/> />
@ -146,16 +169,16 @@ export const CarrierEditPage = observer(() => {
<TextField <TextField
fullWidth fullWidth
label="Короткое название" label="Короткое название"
value={editCarrierData.short_name} value={editCarrierData[language].short_name}
required required
onChange={(e) => onChange={(e) =>
setEditCarrierData( setEditCarrierData(
editCarrierData.full_name, editCarrierData[language].full_name,
e.target.value, e.target.value,
editCarrierData.city_id, editCarrierData.city_id,
editCarrierData.slogan, editCarrierData[language].slogan,
editCarrierData.logo editCarrierData.logo,
language
) )
} }
/> />
@ -163,15 +186,15 @@ export const CarrierEditPage = observer(() => {
<TextField <TextField
fullWidth fullWidth
label="Слоган" label="Слоган"
value={editCarrierData.slogan} value={editCarrierData[language].slogan}
onChange={(e) => onChange={(e) =>
setEditCarrierData( setEditCarrierData(
editCarrierData.full_name, editCarrierData[language].full_name,
editCarrierData.short_name, editCarrierData[language].short_name,
editCarrierData.city_id, editCarrierData.city_id,
e.target.value, e.target.value,
editCarrierData.logo editCarrierData.logo,
language
) )
} }
/> />
@ -187,12 +210,12 @@ export const CarrierEditPage = observer(() => {
}} }}
onDeleteImageClick={() => { onDeleteImageClick={() => {
setEditCarrierData( setEditCarrierData(
editCarrierData.full_name, editCarrierData[language].full_name,
editCarrierData.short_name, editCarrierData[language].short_name,
editCarrierData.city_id, editCarrierData.city_id,
editCarrierData.slogan, editCarrierData[language].slogan,
"" "",
language
); );
setActiveMenuType(null); setActiveMenuType(null);
}} }}
@ -214,8 +237,8 @@ export const CarrierEditPage = observer(() => {
onClick={handleEdit} onClick={handleEdit}
disabled={ disabled={
isLoading || isLoading ||
!editCarrierData.full_name || !editCarrierData[language].full_name ||
!editCarrierData.short_name || !editCarrierData[language].short_name ||
!editCarrierData.city_id || !editCarrierData.city_id ||
!editCarrierData.logo !editCarrierData.logo
} }

View File

@ -1,10 +1,10 @@
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
import { carrierStore } from "@shared"; import { carrierStore, languageStore } from "@shared";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Eye, Pencil, Trash2, Minus } from "lucide-react"; import { Pencil, Trash2, Minus } from "lucide-react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { CreateButton, DeleteModal } from "@widgets"; import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets";
export const CarrierListPage = observer(() => { export const CarrierListPage = observer(() => {
const { carriers, getCarriers, deleteCarrier } = carrierStore; const { carriers, getCarriers, deleteCarrier } = carrierStore;
@ -13,10 +13,13 @@ export const CarrierListPage = observer(() => {
const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false); const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false);
const [rowId, setRowId] = useState<number | null>(null); const [rowId, setRowId] = useState<number | null>(null);
const [ids, setIds] = useState<number[]>([]); const [ids, setIds] = useState<number[]>([]);
const { language } = languageStore;
useEffect(() => { useEffect(() => {
getCarriers(); (async () => {
}, []); await getCarriers(language);
})();
}, [language]);
const columns: GridColDef[] = [ const columns: GridColDef[] = [
{ {
@ -96,7 +99,7 @@ export const CarrierListPage = observer(() => {
}, },
]; ];
const rows = carriers.data?.map((carrier) => ({ const rows = carriers[language].data?.map((carrier) => ({
id: carrier.id, id: carrier.id,
full_name: carrier.full_name, full_name: carrier.full_name,
short_name: carrier.short_name, short_name: carrier.short_name,
@ -105,6 +108,7 @@ export const CarrierListPage = observer(() => {
return ( return (
<> <>
<LanguageSwitcher />
<div className="w-full"> <div className="w-full">
<div className="flex justify-between items-center mb-10"> <div className="flex justify-between items-center mb-10">
<h1 className="text-2xl">Перевозчики</h1> <h1 className="text-2xl">Перевозчики</h1>
@ -155,7 +159,7 @@ export const CarrierListPage = observer(() => {
open={isBulkDeleteModalOpen} open={isBulkDeleteModalOpen}
onDelete={async () => { onDelete={async () => {
await Promise.all(ids.map((id) => deleteCarrier(id))); await Promise.all(ids.map((id) => deleteCarrier(id)));
getCarriers(); await getCarriers(language);
setIsBulkDeleteModalOpen(false); setIsBulkDeleteModalOpen(false);
setIds([]); setIds([]);
}} }}

View File

@ -1,116 +0,0 @@
import { Paper } from "@mui/material";
import { carrierStore, mediaStore } from "@shared";
import { MediaViewer } from "@widgets";
import { observer } from "mobx-react-lite";
import { ArrowLeft } from "lucide-react";
import { useEffect } from "react";
import { useNavigate, useParams } from "react-router-dom";
export const CarrierPreviewPage = observer(() => {
const { id } = useParams();
const { getCarrier, carrier, setEditCarrierData } = carrierStore;
const { oneMedia, getOneMedia } = mediaStore;
const navigate = useNavigate();
useEffect(() => {
(async () => {
const carrierResponse = await getCarrier(Number(id));
setEditCarrierData(
carrierResponse?.full_name as string,
carrierResponse?.short_name as string,
carrierResponse?.city as string,
carrierResponse?.city_id as number,
// carrierResponse?.main_color as string,
// carrierResponse?.left_color as string,
// carrierResponse?.right_color as string,
carrierResponse?.slogan as string,
carrierResponse?.logo as string
);
console.log(carrierResponse);
await getOneMedia(carrierResponse?.logo as string);
})();
}, [id]);
return (
<Paper className="w-full h-full p-3 flex flex-col gap-10">
{carrier && (
<>
<div className="flex justify-between items-center">
<button
className="flex items-center gap-2"
onClick={() => navigate(-1)}
>
<ArrowLeft size={20} />
Назад
</button>
</div>
<div className="flex flex-col gap-10 w-full">
<div className="flex flex-col gap-2">
<h1 className="text-lg font-bold">Полное имя</h1>
<p>{carrier[Number(id)]?.full_name}</p>
</div>
<div className="flex flex-col gap-2">
<h1 className="text-lg font-bold">Полное имя</h1>
<p>{carrier[Number(id)]?.full_name}</p>
</div>
<div className="flex flex-col gap-2">
<h1 className="text-lg font-bold">Город</h1>
<p>{carrier[Number(id)]?.city}</p>
</div>
{/* <div className="flex flex-col gap-2 ">
<h1 className="text-lg font-bold">Основной цвет</h1>
<div
className="w-min"
style={{
backgroundColor: `${carrier[Number(id)]?.main_color}90`,
}}
>
{carrier[Number(id)]?.main_color}
</div>
</div>
<div className="flex flex-col gap-2">
<h1 className="text-lg font-bold">Цвет левого виджета</h1>
<div
className="w-min"
style={{
backgroundColor: `${carrier[Number(id)]?.left_color}90`,
}}
>
{carrier[Number(id)]?.left_color}
</div>
</div>
<div className="flex flex-col gap-2">
<h1 className="text-lg font-bold">Цвет правого виджета</h1>
<div
className="w-min"
style={{
backgroundColor: `${carrier[Number(id)]?.right_color}90`,
}}
>
{carrier[Number(id)]?.right_color}
</div>
</div> */}
<div className="flex flex-col gap-2">
<h1 className="text-lg font-bold">Краткое имя</h1>
<p>{carrier[Number(id)]?.short_name}</p>
</div>
{oneMedia && (
<div className="flex flex-col gap-2">
<h1 className="text-lg font-bold">Логотип</h1>
<MediaViewer
media={{
id: oneMedia?.id as string,
media_type: oneMedia?.media_type as number,
filename: oneMedia?.filename,
}}
/>
</div>
)}
</div>
</>
)}
</Paper>
);
});

View File

@ -1,4 +1,4 @@
export * from "./CarrierListPage"; export * from "./CarrierListPage";
export * from "./CarrierPreviewPage";
export * from "./CarrierCreatePage"; export * from "./CarrierCreatePage";
export * from "./CarrierEditPage"; export * from "./CarrierEditPage";

View File

@ -6,16 +6,15 @@ import {
MenuItem, MenuItem,
FormControl, FormControl,
InputLabel, InputLabel,
Box,
} from "@mui/material"; } from "@mui/material";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { ArrowLeft, Save, ImagePlus, Minus } from "lucide-react"; import { ArrowLeft, Save, Minus } from "lucide-react";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { cityStore, countryStore, languageStore, mediaStore } from "@shared"; import { cityStore, countryStore, languageStore, mediaStore } from "@shared";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { LanguageSwitcher, MediaViewer, ImageUploadCard } from "@widgets"; import { LanguageSwitcher, ImageUploadCard } from "@widgets";
import { import {
SelectMediaDialog, SelectMediaDialog,
UploadMediaDialog, UploadMediaDialog,
@ -91,8 +90,8 @@ export const CityCreatePage = observer(() => {
</div> </div>
<div className="flex flex-col gap-10 w-full items-end"> <div className="flex flex-col gap-10 w-full items-end">
<div className="flex gap-10 items-center mb-5 max-w-[80%]"> <div className="flex gap-10 items-center mb-5 max-w-[80%] self-start">
<h1 className="text-3xl break-words">Создание города</h1> <h1 className="text-3xl break-words">{createCityData.ru.name}</h1>
</div> </div>
<TextField <TextField
fullWidth fullWidth

View File

@ -6,10 +6,9 @@ import {
MenuItem, MenuItem,
FormControl, FormControl,
InputLabel, InputLabel,
Box,
} from "@mui/material"; } from "@mui/material";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { ArrowLeft, Save, ImagePlus } from "lucide-react"; import { ArrowLeft, Save } from "lucide-react";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
@ -21,7 +20,7 @@ import {
CashedCities, CashedCities,
} from "@shared"; } from "@shared";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { LanguageSwitcher, MediaViewer, ImageUploadCard } from "@widgets"; import { LanguageSwitcher, ImageUploadCard } from "@widgets";
import { import {
SelectMediaDialog, SelectMediaDialog,
UploadMediaDialog, UploadMediaDialog,
@ -70,7 +69,7 @@ export const CityEditPage = observer(() => {
setEditCityData(zhData.name, zhData.country_code, zhData.arms, "zh"); setEditCityData(zhData.name, zhData.country_code, zhData.arms, "zh");
await getOneMedia(ruData.arms as string); await getOneMedia(ruData.arms as string);
await getCountries(language); await getCountries("ru");
await getMedia(); await getMedia();
} }
})(); })();
@ -108,7 +107,7 @@ export const CityEditPage = observer(() => {
</div> </div>
<div className="flex flex-col gap-10 w-full items-end"> <div className="flex flex-col gap-10 w-full items-end">
<div className="flex gap-10 items-center mb-5 max-w-[80%]"> <div className="flex gap-10 items-center mb-5 max-w-[80%] self-start ">
<h1 className="text-3xl break-words">{editCityData.ru.name}</h1> <h1 className="text-3xl break-words">{editCityData.ru.name}</h1>
</div> </div>
<TextField <TextField
@ -141,7 +140,7 @@ export const CityEditPage = observer(() => {
); );
}} }}
> >
{countryStore.countries[language].data.map((country) => ( {countryStore.countries.ru.data.map((country) => (
<MenuItem key={country.code} value={country.code}> <MenuItem key={country.code} value={country.code}>
{country.name} {country.name}
</MenuItem> </MenuItem>

View File

@ -1,5 +1,5 @@
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
import { languageStore, cityStore, CashedCities } from "@shared"; import { languageStore, cityStore } from "@shared";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Eye, Pencil, Trash2, Minus } from "lucide-react"; import { Eye, Pencil, Trash2, Minus } from "lucide-react";

View File

@ -41,8 +41,8 @@ export const CountryCreatePage = observer(() => {
</div> </div>
<div className="flex flex-col gap-10 w-full items-end"> <div className="flex flex-col gap-10 w-full items-end">
<div className="flex gap-10 items-center mb-5 max-w-[80%]"> <div className="flex gap-10 items-center mb-5 max-w-[80%] self-start">
<h1 className="text-3xl break-words">Создание страны</h1> <h1 className="text-3xl break-words">{createCountryData.ru.name}</h1>
</div> </div>
<TextField <TextField
fullWidth fullWidth

View File

@ -58,8 +58,8 @@ export const CountryEditPage = observer(() => {
</div> </div>
<div className="flex flex-col gap-10 w-full items-end"> <div className="flex flex-col gap-10 w-full items-end">
<div className="flex gap-10 items-center mb-5 max-w-[80%]"> <div className="flex gap-10 items-center mb-5 max-w-[80%] self-start">
<h1 className="text-3xl break-words">{editCountryData.ru.name}</h1> <h1 className="text-3xl break-words t">{editCountryData.ru.name}</h1>
</div> </div>
<TextField <TextField
fullWidth fullWidth

View File

@ -3,12 +3,7 @@ import { InformationTab, LeaveAgree, RightWidgetTab } from "@widgets";
import { LeftWidgetTab } from "@widgets"; import { LeftWidgetTab } from "@widgets";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { import { articlesStore, cityStore, editSightStore } from "@shared";
articlesStore,
cityStore,
editSightStore,
languageStore,
} from "@shared";
import { useBlocker, useParams } from "react-router-dom"; import { useBlocker, useParams } from "react-router-dom";
function a11yProps(index: number) { function a11yProps(index: number) {
@ -22,7 +17,7 @@ export const EditSightPage = observer(() => {
const [value, setValue] = useState(0); const [value, setValue] = useState(0);
const { sight, getSightInfo, needLeaveAgree } = editSightStore; const { sight, getSightInfo, needLeaveAgree } = editSightStore;
const { getArticles } = articlesStore; const { getArticles } = articlesStore;
const { language } = languageStore;
const { id } = useParams(); const { id } = useParams();
const { getRuCities } = cityStore; const { getRuCities } = cityStore;

View File

@ -3,7 +3,6 @@ import React, {
useRef, useRef,
useState, useState,
useCallback, useCallback,
ReactNode,
useMemo, useMemo,
} from "react"; } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
@ -33,7 +32,6 @@ import {
ArrowRightLeft, ArrowRightLeft,
Landmark, Landmark,
Pencil, Pencil,
Lasso,
InfoIcon, InfoIcon,
X, X,
Loader2, Loader2,
@ -86,9 +84,7 @@ class MapStore {
for (const id of routesIds) { for (const id of routesIds) {
const route = await languageInstance("ru").get(`/route/${id}`); const route = await languageInstance("ru").get(`/route/${id}`);
this.routes.push({ this.routes.push({
id: route.data.id, ...route.data,
route_number: route.data.route_number,
path: route.data.path,
}); });
} }
@ -100,21 +96,14 @@ class MapStore {
getStations = async () => { getStations = async () => {
const stations = await languageInstance("ru").get("/station"); const stations = await languageInstance("ru").get("/station");
this.stations = stations.data.map((station: any) => ({ this.stations = stations.data.map((station: any) => ({
id: station.id, ...station,
name: station.name,
latitude: station.latitude,
longitude: station.longitude,
})); }));
}; };
getSights = async () => { getSights = async () => {
const sights = await languageInstance("ru").get("/sight"); const sights = await languageInstance("ru").get("/sight");
this.sights = sights.data.map((sight: any) => ({ this.sights = sights.data.map((sight: any) => ({
id: sight.id, ...sight,
name: sight.name,
description: sight.description,
latitude: sight.latitude,
longitude: sight.longitude,
})); }));
}; };
@ -194,9 +183,25 @@ class MapStore {
throw new Error(`Unknown feature type for update: ${featureType}`); throw new Error(`Unknown feature type for update: ${featureType}`);
} }
let oldData;
if (featureType === "route") {
oldData = this.routes.find((f) => f.id === numericId);
} else if (featureType === "station") {
oldData = this.stations.find((f) => f.id === numericId);
} else if (featureType === "sight") {
oldData = this.sights.find((f) => f.id === numericId);
}
console.log(oldData);
console.log(data);
const response = await languageInstance("ru").patch( const response = await languageInstance("ru").patch(
`/${featureType}/${numericId}`, `/${featureType}/${numericId}`,
data {
...oldData,
latitude: data.latitude,
longitude: data.longitude,
}
); );
if (featureType === "route") { if (featureType === "route") {
@ -277,6 +282,7 @@ class MapService {
private tooltipElement: HTMLElement; private tooltipElement: HTMLElement;
private tooltipOverlay: Overlay | null; private tooltipOverlay: Overlay | null;
private mode: string | null; private mode: string | null;
// @ts-ignore
private currentDrawingType: "Point" | "LineString" | null; private currentDrawingType: "Point" | "LineString" | null;
private currentDrawingFeatureType: FeatureType | null; private currentDrawingFeatureType: FeatureType | null;
private currentInteraction: Draw | null; private currentInteraction: Draw | null;
@ -620,7 +626,7 @@ class MapService {
filter: (_: FeatureLike, l: Layer<Source, any> | null) => filter: (_: FeatureLike, l: Layer<Source, any> | null) =>
l === this.vectorLayer, l === this.vectorLayer,
}); });
// @ts-ignore
this.modifyInteraction.on("modifystart", (event) => { this.modifyInteraction.on("modifystart", (event) => {
const geoJSONFormat = new GeoJSON(); const geoJSONFormat = new GeoJSON();
if (!this.map) return; if (!this.map) return;
@ -959,7 +965,8 @@ class MapService {
feature.set("name", `${baseName} ${maxNumber + 1}`); feature.set("name", `${baseName} ${maxNumber + 1}`);
await this.saveNewFeature(feature); await this.saveNewFeature(feature);
this.stopDrawing(); // Убираем вызов stopDrawing, чтобы режим рисования оставался активным
// this.stopDrawing();
}); });
this.map.addInteraction(this.currentInteraction); this.map.addInteraction(this.currentInteraction);
@ -988,7 +995,8 @@ class MapService {
this.currentInteraction = null; this.currentInteraction = null;
this.currentDrawingType = null; this.currentDrawingType = null;
this.currentDrawingFeatureType = null; this.currentDrawingFeatureType = null;
this.activateEditMode(); // Убираем автоматическое переключение в режим редактирования
// this.activateEditMode();
} }
public finishDrawing(): void { public finishDrawing(): void {
@ -1007,6 +1015,10 @@ class MapService {
this.currentInteraction instanceof Draw this.currentInteraction instanceof Draw
) { ) {
this.finishDrawing(); this.finishDrawing();
// После завершения рисования маршрута, останавливаем режим рисования
if (this.currentDrawingType === "LineString") {
this.stopDrawing();
}
} }
} }
@ -1098,6 +1110,7 @@ class MapService {
if (this.mode === "edit") { if (this.mode === "edit") {
this.selectInteraction.getFeatures().clear(); this.selectInteraction.getFeatures().clear();
this.selectInteraction.getFeatures().push(feature); this.selectInteraction.getFeatures().push(feature);
// @ts-ignore
const selectEvent = new SelectEvent("select", [feature], []); const selectEvent = new SelectEvent("select", [feature], []);
this.selectInteraction.dispatchEvent(selectEvent); this.selectInteraction.dispatchEvent(selectEvent);
} }
@ -1332,7 +1345,8 @@ class MapService {
feature.set("name", newName); feature.set("name", newName);
this.updateFeaturesInReact(); this.updateFeaturesInReact();
this.selectFeature(newFeatureId); // Убираем автоматический выбор созданного объекта
// this.selectFeature(newFeatureId);
} catch (error) { } catch (error) {
console.error("Failed to save new feature:", error); console.error("Failed to save new feature:", error);
toast.error("Не удалось сохранить объект."); toast.error("Не удалось сохранить объект.");
@ -1366,6 +1380,7 @@ interface ControlItem {
const MapControls: React.FC<MapControlsProps> = ({ const MapControls: React.FC<MapControlsProps> = ({
mapService, mapService,
activeMode, activeMode,
// @ts-ignore
isLassoActive, isLassoActive,
isUnselectDisabled, isUnselectDisabled,
}) => { }) => {
@ -1473,11 +1488,13 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
}, [mapFeatures, searchQuery]); }, [mapFeatures, searchQuery]);
const handleFeatureClick = useCallback( const handleFeatureClick = useCallback(
// @ts-ignore
(id) => mapService?.selectFeature(id), (id) => mapService?.selectFeature(id),
[mapService] [mapService]
); );
const handleDeleteFeature = useCallback( const handleDeleteFeature = useCallback(
// @ts-ignore
(id, recourse) => { (id, recourse) => {
if ( if (
mapService && mapService &&
@ -1490,6 +1507,7 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
); );
const handleCheckboxChange = useCallback( const handleCheckboxChange = useCallback(
// @ts-ignore
(id) => { (id) => {
if (id === undefined) return; if (id === undefined) return;
const newSet = new Set(selectedIds); const newSet = new Set(selectedIds);
@ -1512,7 +1530,10 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
} }
}, [mapService, selectedIds, setSelectedIds]); }, [mapService, selectedIds, setSelectedIds]);
// @ts-ignore
const handleEditFeature = useCallback( const handleEditFeature = useCallback(
// @ts-ignore
(featureType, fullId) => { (featureType, fullId) => {
if (!featureType || !fullId) return; if (!featureType || !fullId) return;
const numericId = String(fullId).split("-")[1]; const numericId = String(fullId).split("-")[1];
@ -1522,8 +1543,11 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
); );
const sortFeatures = ( const sortFeatures = (
// @ts-ignore
features, features,
// @ts-ignore
currentSelectedIds, currentSelectedIds,
// @ts-ignore
currentSelectedFeature currentSelectedFeature
) => { ) => {
const selectedId = currentSelectedFeature?.getId(); const selectedId = currentSelectedFeature?.getId();

View File

@ -6,7 +6,7 @@ import {
MenuItem, MenuItem,
FormControl, FormControl,
InputLabel, InputLabel,
Typography, // Typography,
Box, Box,
} from "@mui/material"; } from "@mui/material";
import { LanguageSwitcher } from "@widgets"; import { LanguageSwitcher } from "@widgets";
@ -18,6 +18,7 @@ import { toast } from "react-toastify";
import { carrierStore } from "../../../shared/store/CarrierStore"; import { carrierStore } from "../../../shared/store/CarrierStore";
import { articlesStore } from "../../../shared/store/ArticlesStore"; import { articlesStore } from "../../../shared/store/ArticlesStore";
import { Route, routeStore } from "../../../shared/store/RouteStore"; import { Route, routeStore } from "../../../shared/store/RouteStore";
import { languageStore } from "@shared";
export const RouteCreatePage = observer(() => { export const RouteCreatePage = observer(() => {
const navigate = useNavigate(); const navigate = useNavigate();
@ -33,11 +34,11 @@ export const RouteCreatePage = observer(() => {
const [centerLat, setCenterLat] = useState(""); const [centerLat, setCenterLat] = useState("");
const [centerLng, setCenterLng] = useState(""); const [centerLng, setCenterLng] = useState("");
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { language } = languageStore;
useEffect(() => { useEffect(() => {
carrierStore.getCarriers(); carrierStore.getCarriers(language);
articlesStore.getArticleList(); articlesStore.getArticleList();
}, []); }, [language]);
const handleCreateRoute = async () => { const handleCreateRoute = async () => {
try { try {
@ -65,8 +66,9 @@ export const RouteCreatePage = observer(() => {
// Собираем объект маршрута // Собираем объект маршрута
const newRoute: Partial<Route> = { const newRoute: Partial<Route> = {
carrier: carrier:
carrierStore.carriers.data.find((c: any) => c.id === carrier_id) carrierStore.carriers[
?.full_name || "", language as keyof typeof carrierStore.carriers
].data?.find((c: any) => c.id === carrier_id)?.full_name || "",
carrier_id, carrier_id,
route_number: routeNumber, route_number: routeNumber,
route_sys_number: govRouteNumber, route_sys_number: govRouteNumber,
@ -112,16 +114,20 @@ export const RouteCreatePage = observer(() => {
value={carrier} value={carrier}
label="Выберите перевозчика" label="Выберите перевозчика"
onChange={(e) => setCarrier(e.target.value as string)} onChange={(e) => setCarrier(e.target.value as string)}
disabled={carrierStore.carriers.data.length === 0} disabled={
carrierStore.carriers[
language as keyof typeof carrierStore.carriers
].data?.length === 0
}
> >
<MenuItem value="">Не выбрано</MenuItem> <MenuItem value="">Не выбрано</MenuItem>
{carrierStore.carriers.data.map( {carrierStore.carriers[
(c: (typeof carrierStore.carriers.data)[number]) => ( language as keyof typeof carrierStore.carriers
<MenuItem key={c.id} value={c.id}> ].data?.map((carrier) => (
{c.full_name} <MenuItem key={carrier.id} value={carrier.id}>
</MenuItem> {carrier.full_name}
) </MenuItem>
)} ))}
</Select> </Select>
</FormControl> </FormControl>
<TextField <TextField

View File

@ -6,7 +6,7 @@ import {
MenuItem, MenuItem,
FormControl, FormControl,
InputLabel, InputLabel,
Typography, // Typography,
Box, Box,
} from "@mui/material"; } from "@mui/material";
import { LanguageSwitcher } from "@widgets"; import { LanguageSwitcher } from "@widgets";
@ -19,22 +19,23 @@ import { carrierStore } from "../../../shared/store/CarrierStore";
import { articlesStore } from "../../../shared/store/ArticlesStore"; import { articlesStore } from "../../../shared/store/ArticlesStore";
import { routeStore } from "../../../shared/store/RouteStore"; import { routeStore } from "../../../shared/store/RouteStore";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { languageStore } from "@shared";
export const RouteEditPage = observer(() => { export const RouteEditPage = observer(() => {
const navigate = useNavigate(); const navigate = useNavigate();
const { id } = useParams(); const { id } = useParams();
const { editRouteData } = routeStore; const { editRouteData } = routeStore;
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { language } = languageStore;
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
const response = await routeStore.getRoute(Number(id)); const response = await routeStore.getRoute(Number(id));
routeStore.setEditRouteData(response); routeStore.setEditRouteData(response);
carrierStore.getCarriers(); carrierStore.getCarriers(language);
articlesStore.getArticleList(); articlesStore.getArticleList();
}; };
fetchData(); fetchData();
}, [id]); }, [id, language]);
const handleSave = async () => { const handleSave = async () => {
setIsLoading(true); setIsLoading(true);
@ -67,21 +68,26 @@ export const RouteEditPage = observer(() => {
routeStore.setEditRouteData({ routeStore.setEditRouteData({
carrier_id: Number(e.target.value), carrier_id: Number(e.target.value),
carrier: carrier:
carrierStore.carriers.data.find( carrierStore.carriers[
(c) => c.id === Number(e.target.value) language as keyof typeof carrierStore.carriers
)?.full_name || "", ].data?.find((c) => c.id === Number(e.target.value))
?.full_name || "",
}) })
} }
disabled={carrierStore.carriers.data.length === 0} disabled={
carrierStore.carriers[
language as keyof typeof carrierStore.carriers
].data?.length === 0
}
> >
<MenuItem value="">Не выбрано</MenuItem> <MenuItem value="">Не выбрано</MenuItem>
{carrierStore.carriers.data.map( {carrierStore.carriers[
(c: (typeof carrierStore.carriers.data)[number]) => ( language as keyof typeof carrierStore.carriers
<MenuItem key={c.id} value={c.id}> ].data?.map((carrier) => (
{c.full_name} <MenuItem key={carrier.id} value={carrier.id}>
</MenuItem> {carrier.full_name}
) </MenuItem>
)} ))}
</Select> </Select>
</FormControl> </FormControl>
<TextField <TextField

View File

@ -48,8 +48,8 @@ export const StationCreatePage = observer(() => {
</button> </button>
</div> </div>
<div className="flex flex-col gap-10 w-full items-end"> <div className="flex flex-col gap-10 w-full items-end">
<div className="flex gap-10 items-center mb-5 max-w-[80%]"> <div className="flex gap-10 items-center mb-5 max-w-[80%] self-start">
<h1 className="text-3xl break-words">Создание станции</h1> <h1 className="text-3xl break-words">{name}</h1>
</div> </div>
<TextField <TextField
className="w-full" className="w-full"

View File

@ -7,7 +7,12 @@ import {
FormControl, FormControl,
InputLabel, InputLabel,
} from "@mui/material"; } from "@mui/material";
import { vehicleStore, VEHICLE_TYPES, carrierStore } from "@shared"; import {
vehicleStore,
VEHICLE_TYPES,
carrierStore,
languageStore,
} from "@shared";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { ArrowLeft, Save } from "lucide-react"; import { ArrowLeft, Save } from "lucide-react";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
@ -21,10 +26,11 @@ export const VehicleCreatePage = observer(() => {
const [type, setType] = useState(""); const [type, setType] = useState("");
const [carrierId, setCarrierId] = useState<number | null>(null); const [carrierId, setCarrierId] = useState<number | null>(null);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { language } = languageStore;
useEffect(() => { useEffect(() => {
carrierStore.getCarriers(); carrierStore.getCarriers(language);
}, []); }, [language]);
const handleCreate = async () => { const handleCreate = async () => {
try { try {
@ -32,7 +38,8 @@ export const VehicleCreatePage = observer(() => {
await vehicleStore.createVehicle( await vehicleStore.createVehicle(
Number(tailNumber), Number(tailNumber),
Number(type), Number(type),
carrierStore.carriers.data.find((c) => c.id === carrierId)?.full_name!, carrierStore.carriers[language].data?.find((c) => c.id === carrierId)
?.full_name as string,
carrierId! carrierId!
); );
toast.success("Транспорт успешно создан"); toast.success("Транспорт успешно создан");
@ -88,7 +95,7 @@ export const VehicleCreatePage = observer(() => {
required required
onChange={(e) => setCarrierId(e.target.value as number)} onChange={(e) => setCarrierId(e.target.value as number)}
> >
{carrierStore.carriers.data.map((carrier) => ( {carrierStore.carriers[language].data?.map((carrier) => (
<MenuItem key={carrier.id} value={carrier.id}> <MenuItem key={carrier.id} value={carrier.id}>
{carrier.full_name} {carrier.full_name}
</MenuItem> </MenuItem>

View File

@ -11,7 +11,12 @@ import { observer } from "mobx-react-lite";
import { ArrowLeft, Loader2, Save } from "lucide-react"; import { ArrowLeft, Loader2, Save } from "lucide-react";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { carrierStore, VEHICLE_TYPES, vehicleStore } from "@shared"; import {
carrierStore,
languageStore,
VEHICLE_TYPES,
vehicleStore,
} from "@shared";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
export const VehicleEditPage = observer(() => { export const VehicleEditPage = observer(() => {
@ -25,11 +30,12 @@ export const VehicleEditPage = observer(() => {
editVehicle, editVehicle,
} = vehicleStore; } = vehicleStore;
const { getCarriers } = carrierStore; const { getCarriers } = carrierStore;
const { language } = languageStore;
useEffect(() => { useEffect(() => {
(async () => { (async () => {
await getVehicle(Number(id)); await getVehicle(Number(id));
await getCarriers(); await getCarriers(language);
setEditVehicleData({ setEditVehicleData({
tail_number: vehicle[Number(id)]?.vehicle.tail_number, tail_number: vehicle[Number(id)]?.vehicle.tail_number,
type: vehicle[Number(id)]?.vehicle.type, type: vehicle[Number(id)]?.vehicle.type,
@ -37,7 +43,7 @@ export const VehicleEditPage = observer(() => {
carrier_id: vehicle[Number(id)]?.vehicle.carrier_id, carrier_id: vehicle[Number(id)]?.vehicle.carrier_id,
}); });
})(); })();
}, [id]); }, [id, language]);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const handleEdit = async () => { const handleEdit = async () => {
try { try {
@ -108,7 +114,7 @@ export const VehicleEditPage = observer(() => {
}) })
} }
> >
{carrierStore.carriers.data.map((carrier) => ( {carrierStore.carriers[language].data?.map((carrier) => (
<MenuItem key={carrier.id} value={carrier.id}> <MenuItem key={carrier.id} value={carrier.id}>
{carrier.full_name} {carrier.full_name}
</MenuItem> </MenuItem>

View File

@ -19,7 +19,7 @@ export const VehicleListPage = observer(() => {
useEffect(() => { useEffect(() => {
getVehicles(); getVehicles();
getCarriers(); getCarriers(language);
}, [language]); }, [language]);
const columns: GridColDef[] = [ const columns: GridColDef[] = [
@ -123,7 +123,7 @@ export const VehicleListPage = observer(() => {
tail_number: vehicle.vehicle.tail_number, tail_number: vehicle.vehicle.tail_number,
type: vehicle.vehicle.type, type: vehicle.vehicle.type,
carrier: vehicle.vehicle.carrier, carrier: vehicle.vehicle.carrier,
city: carriers.data?.find( city: carriers[language].data?.find(
(carrier) => carrier.id === vehicle.vehicle.carrier_id (carrier) => carrier.id === vehicle.vehicle.carrier_id
)?.city, )?.city,
})); }));

View File

@ -8,7 +8,7 @@ import {
Earth, Earth,
Landmark, Landmark,
GitBranch, GitBranch,
Car, // Car,
Table, Table,
Notebook, Notebook,
Split, Split,

View File

@ -1,4 +1,10 @@
import { authInstance, cityStore, languageStore } from "@shared"; import {
authInstance,
cityStore,
languageStore,
languageInstance,
Language,
} from "@shared";
import { makeAutoObservable, runInAction } from "mobx"; import { makeAutoObservable, runInAction } from "mobx";
export type Carrier = { export type Carrier = {
@ -14,17 +20,40 @@ export type Carrier = {
// right_color: string; // right_color: string;
}; };
type Carriers = { type CarrierData = {
data: Carrier[]; data: Carrier[];
loaded: boolean; loaded: boolean;
}; };
type CashedCarrier = Record<number, Carrier>; type Carriers = {
ru: CarrierData;
en: CarrierData;
zh: CarrierData;
};
type CashedCarrier = Record<
number,
{
ru: Carrier | null;
en: Carrier | null;
zh: Carrier | null;
}
>;
class CarrierStore { class CarrierStore {
carriers: Carriers = { carriers: Carriers = {
data: [], ru: {
loaded: false, data: [],
loaded: false,
},
en: {
data: [],
loaded: false,
},
zh: {
data: [],
loaded: false,
},
}; };
carrier: CashedCarrier = {}; carrier: CashedCarrier = {};
@ -32,14 +61,14 @@ class CarrierStore {
makeAutoObservable(this); makeAutoObservable(this);
} }
getCarriers = async () => { getCarriers = async (language: Language) => {
if (this.carriers.loaded) return; if (this.carriers[language as keyof Carriers].loaded) return;
const response = await authInstance.get("/carrier"); const response = await authInstance.get("/carrier");
runInAction(() => { runInAction(() => {
this.carriers.data = response.data; this.carriers[language as keyof Carriers].data = response.data;
this.carriers.loaded = true; this.carriers[language as keyof Carriers].loaded = true;
}); });
}; };
@ -47,116 +76,163 @@ class CarrierStore {
await authInstance.delete(`/carrier/${id}`); await authInstance.delete(`/carrier/${id}`);
runInAction(() => { runInAction(() => {
this.carriers.data = this.carriers.data.filter( for (const language of ["ru", "en", "zh"] as const) {
(carrier) => carrier.id !== id this.carriers[language].data = this.carriers[language].data.filter(
); (carrier: Carrier) => carrier.id !== id
);
}
delete this.carrier[id]; delete this.carrier[id];
}); });
}; };
getCarrier = async (id: number) => { getCarrier = async (id: number) => {
if (this.carrier[id]) return; if (this.carrier[id]?.ru && this.carrier[id]?.en && this.carrier[id]?.zh)
const response = await authInstance.get(`/carrier/${id}`); return;
const ruResponse = await languageInstance("ru").get(`/carrier/${id}`);
const enResponse = await languageInstance("en").get(`/carrier/${id}`);
const zhResponse = await languageInstance("zh").get(`/carrier/${id}`);
runInAction(() => { runInAction(() => {
if (!this.carrier[id]) { if (!this.carrier[id]) {
this.carrier[id] = { this.carrier[id] = {
id: 0, ru: null,
short_name: "", en: null,
full_name: "", zh: null,
slogan: "",
city: "",
city_id: 0,
logo: "",
// main_color: "",
// left_color: "",
// right_color: "",
}; };
} }
this.carrier[id] = response.data; this.carrier[id].ru = ruResponse.data;
this.carrier[id].en = enResponse.data;
this.carrier[id].zh = zhResponse.data;
}); });
return response.data; return this.carrier[id];
}; };
createCarrier = async ( createCarrier = async (
fullName: string, fullName: string,
shortName: string, shortName: string,
cityId: number, cityId: number,
// main_color: string,
// left_color: string,
// right_color: string,
slogan: string, slogan: string,
logoId: string logoId: string
) => { ) => {
const response = await authInstance.post("/carrier", { const { language } = languageStore;
const cityName =
cityStore.cities[language].data.find((city) => city.id === cityId)
?.name || "";
const response = await languageInstance(language).post("/carrier", {
full_name: fullName, full_name: fullName,
short_name: shortName, short_name: shortName,
city: "", city: cityName,
city_id: cityId, city_id: cityId,
// main_color,
// left_color,
// right_color,
slogan, slogan,
logo: logoId, logo: logoId,
}); });
const carrierId = response.data.id;
// Create translations for other languages
for (const lang of ["ru", "en", "zh"].filter((l) => l !== language)) {
await languageInstance(lang as Language).patch(`/carrier/${carrierId}`, {
full_name: fullName,
short_name: shortName,
city: cityName,
city_id: cityId,
slogan,
logo: logoId,
});
}
runInAction(() => { runInAction(() => {
this.carriers.data.push(response.data); for (const language of ["ru", "en", "zh"] as const) {
this.carriers[language].data.push(response.data);
}
}); });
}; };
editCarrierData = { editCarrierData = {
full_name: "", ru: {
short_name: "", full_name: "",
short_name: "",
// main_color: "",
// left_color: "",
// right_color: "",
slogan: "",
},
en: {
full_name: "",
short_name: "",
// main_color: "",
// left_color: "",
// right_color: "",
slogan: "",
},
city_id: 0, city_id: 0,
// main_color: "",
// left_color: "",
// right_color: "",
slogan: "",
logo: "", logo: "",
zh: {
full_name: "",
short_name: "",
// main_color: "",
// left_color: "",
// right_color: "",
slogan: "",
},
}; };
setEditCarrierData = ( setEditCarrierData = (
fullName: string, fullName: string,
shortName: string, shortName: string,
cityId: number, cityId: number,
// main_color: string, // main_color: string,
// left_color: string, // left_color: string,
// right_color: string, // right_color: string,
slogan: string, slogan: string,
logoId: string logoId: string,
language: Language
) => { ) => {
this.editCarrierData = { this.editCarrierData.city_id = cityId;
this.editCarrierData.logo = logoId;
this.editCarrierData[language] = {
full_name: fullName, full_name: fullName,
short_name: shortName, short_name: shortName,
city_id: cityId,
// main_color: main_color, // main_color: main_color,
// left_color: left_color, // left_color: left_color,
// right_color: right_color, // right_color: right_color,
slogan: slogan, slogan: slogan,
logo: logoId,
}; };
}; };
editCarrier = async (id: number) => { editCarrier = async (id: number) => {
const { language } = languageStore; const cityName =
const response = await authInstance.patch(`/carrier/${id}`, { cityStore.cities[languageStore.language].data.find(
...this.editCarrierData,
city: cityStore.cities[language].data.find(
(city) => city.id === this.editCarrierData.city_id (city) => city.id === this.editCarrierData.city_id
)?.name, )?.name || "";
});
runInAction(() => { for (const language of ["ru", "en", "zh"] as const) {
this.carriers.data = this.carriers.data.map((carrier) => const response = await languageInstance(language).patch(
carrier.id === id ? { ...carrier, ...response.data } : carrier `/carrier/${id}`,
{
...this.editCarrierData[language],
city: cityName,
logo: this.editCarrierData.logo,
}
); );
this.carrier[id] = response.data; runInAction(() => {
}); if (this.carrier[id]) {
this.carrier[id][language] = response.data;
}
for (const language of ["ru", "en", "zh"] as const) {
this.carriers[language].data = this.carriers[language].data.map(
(carrier: Carrier) =>
carrier.id === id ? { ...carrier, ...response.data } : carrier
);
}
});
}
}; };
} }

View File

@ -175,9 +175,10 @@ export const CreateInformationTab = observer(
/> />
<Autocomplete <Autocomplete
options={ruCities ?? []} options={ruCities.data ?? []}
value={ value={
ruCities.find((city) => city.id === sight.city_id) ?? null ruCities.data.find((city) => city.id === sight.city_id) ??
null
} }
getOptionLabel={(option) => option.name} getOptionLabel={(option) => option.name}
onChange={(_, value) => { onChange={(_, value) => {

View File

@ -19,14 +19,7 @@ import {
MediaViewer, MediaViewer,
DeleteModal, DeleteModal,
} from "@widgets"; } from "@widgets";
import { import { Trash2, ImagePlus, Unlink, Plus, Save, Search } from "lucide-react";
Trash2,
ImagePlus,
Unlink,
MousePointer,
Plus,
Save,
} from "lucide-react";
import { useState, useCallback } from "react"; import { useState, useCallback } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
@ -160,7 +153,7 @@ export const CreateLeftTab = observer(
variant="contained" variant="contained"
color="primary" color="primary"
size="small" size="small"
startIcon={<MousePointer color="white" size={18} />} startIcon={<Search color="white" size={18} />}
onClick={() => setIsSelectArticleDialogOpen(true)} onClick={() => setIsSelectArticleDialogOpen(true)}
> >
Выбрать статью Выбрать статью

View File

@ -551,6 +551,7 @@ export const CreateRightTab = observer(
media={ media={
sight[language].right[activeArticleIndex].media[0] sight[language].right[activeArticleIndex].media[0]
} }
fullWidth
/> />
</Box> </Box>
) : ( ) : (

View File

@ -18,14 +18,7 @@ import {
MediaViewer, MediaViewer,
DeleteModal, DeleteModal,
} from "@widgets"; } from "@widgets";
import { import { Trash2, ImagePlus, Unlink, Plus, Save, Search } from "lucide-react";
Trash2,
ImagePlus,
Unlink,
Plus,
MousePointer,
Save,
} from "lucide-react";
import { useState, useCallback } from "react"; import { useState, useCallback } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
@ -175,7 +168,7 @@ export const LeftWidgetTab = observer(
variant="contained" variant="contained"
color="primary" color="primary"
size="small" size="small"
startIcon={<MousePointer color="white" size={18} />} startIcon={<Search color="white" size={18} />}
onClick={() => setIsSelectArticleDialogOpen(true)} onClick={() => setIsSelectArticleDialogOpen(true)}
> >
Выбрать статью Выбрать статью

View File

@ -493,6 +493,7 @@ export const RightWidgetTab = observer(
media={ media={
sight[language].right[activeArticleIndex].media[0] sight[language].right[activeArticleIndex].media[0]
} }
fullWidth
/> />
</Box> </Box>
) : ( ) : (

View File

@ -67,7 +67,7 @@ export const SightsTable = observer(() => {
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{rows(sights, cities[language])?.map((row) => ( {rows(sights, cities[language]?.data ?? [])?.map((row) => (
<TableRow <TableRow
key={row?.id} key={row?.id}
sx={{ "&:last-child td, &:last-child th": { border: 0 } }} sx={{ "&:last-child td, &:last-child th": { border: 0 } }}

File diff suppressed because one or more lines are too long