feat: Add more pages
This commit is contained in:
1
src/App.tsx
Normal file
1
src/App.tsx
Normal file
@ -0,0 +1 @@
|
||||
|
@ -8,7 +8,6 @@ import { ToastContainer } from "react-toastify";
|
||||
export const App: React.FC = () => (
|
||||
<ThemeProvider theme={CustomTheme.Light}>
|
||||
<ToastContainer />
|
||||
|
||||
<Router />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
@ -4,11 +4,31 @@ import {
|
||||
EditSightPage,
|
||||
LoginPage,
|
||||
MainPage,
|
||||
SightPage,
|
||||
SightListPage,
|
||||
MapPage,
|
||||
MediaListPage,
|
||||
PreviewMediaPage,
|
||||
EditMediaPage,
|
||||
MediaPreviewPage,
|
||||
MediaEditPage,
|
||||
CountryListPage,
|
||||
CityListPage,
|
||||
RouteListPage,
|
||||
UserListPage,
|
||||
SnapshotListPage,
|
||||
CarrierListPage,
|
||||
StationListPage,
|
||||
VehicleListPage,
|
||||
ArticleListPage,
|
||||
CityPreviewPage,
|
||||
UserPreviewPage,
|
||||
CountryPreviewPage,
|
||||
SnapshotPreviewPage,
|
||||
VehiclePreviewPage,
|
||||
CarrierPreviewPage,
|
||||
SnapshotCreatePage,
|
||||
CountryCreatePage,
|
||||
CityCreatePage,
|
||||
// CarrierCreatePage,
|
||||
VehicleCreatePage,
|
||||
} from "@pages";
|
||||
import { authStore, createSightStore, editSightStore } from "@shared";
|
||||
import { Layout } from "@widgets";
|
||||
@ -82,14 +102,59 @@ const router = createBrowserRouter([
|
||||
),
|
||||
children: [
|
||||
{ index: true, element: <MainPage /> },
|
||||
{ path: "sight", element: <SightPage /> },
|
||||
|
||||
// Sight
|
||||
{ path: "sight", element: <SightListPage /> },
|
||||
{ path: "sight/create", element: <CreateSightPage /> },
|
||||
{ path: "sight/:id", element: <EditSightPage /> },
|
||||
|
||||
// Device
|
||||
{ path: "devices", element: <DevicesPage /> },
|
||||
|
||||
// Map
|
||||
{ path: "map", element: <MapPage /> },
|
||||
|
||||
// Media
|
||||
{ path: "media", element: <MediaListPage /> },
|
||||
{ path: "media/:id", element: <PreviewMediaPage /> },
|
||||
{ path: "media/:id/edit", element: <EditMediaPage /> },
|
||||
{ path: "media/:id", element: <MediaPreviewPage /> },
|
||||
{ path: "media/:id/edit", element: <MediaEditPage /> },
|
||||
|
||||
// Country
|
||||
{ path: "country", element: <CountryListPage /> },
|
||||
{ path: "country/create", element: <CountryCreatePage /> },
|
||||
{ path: "country/:id", element: <CountryPreviewPage /> },
|
||||
// City
|
||||
{ path: "city", element: <CityListPage /> },
|
||||
{ path: "city/create", element: <CityCreatePage /> },
|
||||
{ path: "city/:id", element: <CityPreviewPage /> },
|
||||
// Route
|
||||
{ path: "route", element: <RouteListPage /> },
|
||||
|
||||
// User
|
||||
{ path: "user", element: <UserListPage /> },
|
||||
{ path: "user/:id", element: <UserPreviewPage /> },
|
||||
|
||||
// Snapshot
|
||||
{ path: "snapshot", element: <SnapshotListPage /> },
|
||||
{ path: "snapshot/create", element: <SnapshotCreatePage /> },
|
||||
{ path: "snapshot/:id", element: <SnapshotPreviewPage /> },
|
||||
|
||||
// Carrier
|
||||
{ path: "carrier", element: <CarrierListPage /> },
|
||||
// { path: "carrier/create", element: <CarrierCreatePage /> },
|
||||
{ path: "carrier/:id", element: <CarrierPreviewPage /> },
|
||||
|
||||
// Station
|
||||
{ path: "station", element: <StationListPage /> },
|
||||
|
||||
// Vehicle
|
||||
{ path: "vehicle", element: <VehicleListPage /> },
|
||||
{ path: "vehicle/create", element: <VehicleCreatePage /> },
|
||||
{ path: "vehicle/:id", element: <VehiclePreviewPage /> },
|
||||
|
||||
// Article
|
||||
{ path: "article", element: <ArticleListPage /> },
|
||||
|
||||
// { path: "media/create", element: <CreateMediaPage /> },
|
||||
],
|
||||
},
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { App } from "./app";
|
||||
|
||||
import "./index.css";
|
||||
import { App } from "./app/index";
|
||||
|
||||
const container = document.getElementById("root");
|
||||
const root = createRoot(container!);
|
||||
|
86
src/pages/Article/ArticleListPage/index.tsx
Normal file
86
src/pages/Article/ArticleListPage/index.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||
import { articlesStore, languageStore } from "@shared";
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Trash2, FileText } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { DeleteModal, LanguageSwitcher } from "@widgets";
|
||||
|
||||
export const ArticleListPage = observer(() => {
|
||||
const { articleList, getArticleList } = articlesStore;
|
||||
const navigate = useNavigate();
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [rowId, setRowId] = useState<string | null>(null); // Lifted state
|
||||
const { language } = languageStore;
|
||||
|
||||
useEffect(() => {
|
||||
getArticleList();
|
||||
}, [language]);
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: "heading",
|
||||
headerName: "Название",
|
||||
flex: 1,
|
||||
},
|
||||
|
||||
{
|
||||
field: "actions",
|
||||
headerName: "Действия",
|
||||
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
return (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
<button onClick={() => navigate(`/media/${params.row.id}`)}>
|
||||
<FileText size={20} className="text-green-500" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={20} className="text-red-500" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const rows = articleList.map((article) => ({
|
||||
id: article.id,
|
||||
heading: article.heading,
|
||||
body: article.body,
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<LanguageSwitcher />
|
||||
|
||||
<div className="w-full">
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
hideFooterPagination
|
||||
hideFooter
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DeleteModal
|
||||
open={isDeleteModalOpen}
|
||||
onDelete={async () => {
|
||||
if (rowId) {
|
||||
getArticleList();
|
||||
}
|
||||
setIsDeleteModalOpen(false);
|
||||
setRowId(null);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setIsDeleteModalOpen(false);
|
||||
setRowId(null);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
1
src/pages/Article/index.ts
Normal file
1
src/pages/Article/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./ArticleListPage";
|
202
src/pages/Carrier/CarrierCreatePage/index.tsx
Normal file
202
src/pages/Carrier/CarrierCreatePage/index.tsx
Normal file
@ -0,0 +1,202 @@
|
||||
import {
|
||||
Button,
|
||||
Paper,
|
||||
TextField,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Box,
|
||||
} from "@mui/material";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ArrowLeft, Save } from "lucide-react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { toast } from "react-toastify";
|
||||
import { carrierStore, cityStore, mediaStore } from "@shared";
|
||||
import { useState, useEffect } from "react";
|
||||
import { LanguageSwitcher, MediaViewer } from "@widgets";
|
||||
import { HexColorPicker } from "react-colorful";
|
||||
|
||||
export const CarrierCreatePage = observer(() => {
|
||||
const navigate = useNavigate();
|
||||
const [fullName, setFullName] = useState("");
|
||||
const [shortName, setShortName] = useState("");
|
||||
const [cityId, setCityId] = useState<number | null>(null);
|
||||
const [primaryColor, setPrimaryColor] = useState("#000000");
|
||||
const [secondaryColor, setSecondaryColor] = useState("#ffffff");
|
||||
const [accentColor, setAccentColor] = useState("#ff0000");
|
||||
const [slogan, setSlogan] = useState("");
|
||||
const [selectedMediaId, setSelectedMediaId] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
cityStore.getCities();
|
||||
mediaStore.getMedia();
|
||||
}, []);
|
||||
|
||||
const handleCreate = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await carrierStore.createCarrier(
|
||||
fullName,
|
||||
shortName,
|
||||
cityStore.cities.find((c) => c.id === cityId)?.name!,
|
||||
cityId!,
|
||||
primaryColor,
|
||||
secondaryColor,
|
||||
accentColor,
|
||||
slogan,
|
||||
selectedMediaId!
|
||||
);
|
||||
toast.success("Перевозчик успешно создан");
|
||||
navigate("/carrier");
|
||||
} catch (error) {
|
||||
toast.error("Ошибка при создании перевозчика");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
||||
<LanguageSwitcher />
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => navigate("/carrier")}
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
Назад
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-10 w-full items-end">
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Город</InputLabel>
|
||||
<Select
|
||||
value={cityId || ""}
|
||||
label="Город"
|
||||
required
|
||||
onChange={(e) => setCityId(e.target.value as number)}
|
||||
>
|
||||
{cityStore.cities.map((city) => (
|
||||
<MenuItem key={city.id} value={city.id}>
|
||||
{city.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Полное название"
|
||||
value={fullName}
|
||||
required
|
||||
onChange={(e) => setFullName(e.target.value)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Короткое название"
|
||||
value={shortName}
|
||||
required
|
||||
onChange={(e) => setShortName(e.target.value)}
|
||||
/>
|
||||
|
||||
<div className="w-full flex flex-col gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="w-32">Основной цвет:</span>
|
||||
<Box
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
backgroundColor: primaryColor,
|
||||
border: "1px solid #ccc",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
/>
|
||||
<HexColorPicker color={primaryColor} onChange={setPrimaryColor} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="w-32">Вторичный цвет:</span>
|
||||
<Box
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
backgroundColor: secondaryColor,
|
||||
border: "1px solid #ccc",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
/>
|
||||
<HexColorPicker
|
||||
color={secondaryColor}
|
||||
onChange={setSecondaryColor}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="w-32">Акцентный цвет:</span>
|
||||
<Box
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
backgroundColor: accentColor,
|
||||
border: "1px solid #ccc",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
/>
|
||||
<HexColorPicker color={accentColor} onChange={setAccentColor} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Слоган"
|
||||
value={slogan}
|
||||
onChange={(e) => setSlogan(e.target.value)}
|
||||
/>
|
||||
|
||||
<div className="w-full flex flex-col gap-4">
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Логотип</InputLabel>
|
||||
<Select
|
||||
value={selectedMediaId || ""}
|
||||
label="Логотип"
|
||||
required
|
||||
onChange={(e) => setSelectedMediaId(e.target.value as string)}
|
||||
>
|
||||
{mediaStore.media.map((media) => (
|
||||
<MenuItem key={media.id} value={media.id}>
|
||||
{media.media_name || media.filename}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
{selectedMediaId && (
|
||||
<div className="w-32 h-32">
|
||||
<MediaViewer media={{ id: selectedMediaId, media_type: 1 }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
className="w-min flex gap-2 items-center"
|
||||
startIcon={<Save size={20} />}
|
||||
onClick={handleCreate}
|
||||
disabled={
|
||||
isLoading || !fullName || !shortName || !cityId || !selectedMediaId
|
||||
}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 size={20} className="animate-spin" />
|
||||
) : (
|
||||
"Создать"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</Paper>
|
||||
);
|
||||
});
|
101
src/pages/Carrier/CarrierListPage/index.tsx
Normal file
101
src/pages/Carrier/CarrierListPage/index.tsx
Normal file
@ -0,0 +1,101 @@
|
||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||
import { carrierStore, languageStore } from "@shared";
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Eye, Trash2 } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { DeleteModal, LanguageSwitcher } from "@widgets";
|
||||
|
||||
export const CarrierListPage = observer(() => {
|
||||
const { carriers, getCarriers, deleteCarrier } = carrierStore;
|
||||
const navigate = useNavigate();
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [rowId, setRowId] = useState<number | null>(null); // Lifted state
|
||||
const { language } = languageStore;
|
||||
|
||||
useEffect(() => {
|
||||
getCarriers();
|
||||
}, [language]);
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: "full_name",
|
||||
headerName: "Полное имя",
|
||||
|
||||
width: 300,
|
||||
},
|
||||
{
|
||||
field: "short_name",
|
||||
headerName: "Короткое имя",
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
field: "city",
|
||||
headerName: "Город",
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
field: "actions",
|
||||
headerName: "Действия",
|
||||
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
return (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
<button onClick={() => navigate(`/carrier/${params.row.id}`)}>
|
||||
<Eye size={20} className="text-green-500" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={20} className="text-red-500" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const rows = carriers.map((carrier) => ({
|
||||
id: carrier.id,
|
||||
full_name: carrier.full_name,
|
||||
short_name: carrier.short_name,
|
||||
city: carrier.city,
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<LanguageSwitcher />
|
||||
|
||||
<div className="w-full">
|
||||
<div className="flex justify-between items-center mb-10">
|
||||
<h1 className="text-2xl">Перевозчики</h1>
|
||||
{/* <CreateButton label="Создать перевозчика" path="/carrier/create" /> */}
|
||||
</div>
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
hideFooterPagination
|
||||
hideFooter
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DeleteModal
|
||||
open={isDeleteModalOpen}
|
||||
onDelete={async () => {
|
||||
if (rowId) {
|
||||
deleteCarrier(rowId);
|
||||
}
|
||||
setIsDeleteModalOpen(false);
|
||||
setRowId(null);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setIsDeleteModalOpen(false);
|
||||
setRowId(null);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
120
src/pages/Carrier/CarrierPreviewPage/index.tsx
Normal file
120
src/pages/Carrier/CarrierPreviewPage/index.tsx
Normal file
@ -0,0 +1,120 @@
|
||||
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 } = carrierStore;
|
||||
const { oneMedia, getOneMedia } = mediaStore;
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const carrierResponse = await getCarrier(Number(id));
|
||||
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 className="flex gap-2">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => navigate(`/carrier/${id}/edit`)}
|
||||
startIcon={<Pencil size={20} />}
|
||||
>
|
||||
Редактировать
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={() => navigate(`/carrier/${id}/delete`)}
|
||||
startIcon={<Trash2 size={20} />}
|
||||
>
|
||||
Удалить
|
||||
</Button>
|
||||
</div> */}
|
||||
</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?.full_name}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="text-lg font-bold">Полное имя</h1>
|
||||
<p>{carrier?.full_name}</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="text-lg font-bold">Город</h1>
|
||||
<p>{carrier?.city}</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 ">
|
||||
<h1 className="text-lg font-bold">Основной цвет</h1>
|
||||
<div
|
||||
className="w-min"
|
||||
style={{
|
||||
backgroundColor: `${carrier?.main_color}90`,
|
||||
}}
|
||||
>
|
||||
{carrier?.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?.left_color}90`,
|
||||
}}
|
||||
>
|
||||
{carrier?.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?.right_color}90`,
|
||||
}}
|
||||
>
|
||||
{carrier?.right_color}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="text-lg font-bold">Краткое имя</h1>
|
||||
<p>{carrier?.short_name}</p>
|
||||
</div>
|
||||
<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>
|
||||
);
|
||||
});
|
3
src/pages/Carrier/index.ts
Normal file
3
src/pages/Carrier/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./CarrierListPage";
|
||||
export * from "./CarrierPreviewPage";
|
||||
export * from "./CarrierCreatePage";
|
166
src/pages/City/CityCreatePage/index.tsx
Normal file
166
src/pages/City/CityCreatePage/index.tsx
Normal file
@ -0,0 +1,166 @@
|
||||
import {
|
||||
Button,
|
||||
Paper,
|
||||
TextField,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Box,
|
||||
} from "@mui/material";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ArrowLeft, Save, ImagePlus } from "lucide-react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { toast } from "react-toastify";
|
||||
import { cityStore, countryStore, mediaStore } from "@shared";
|
||||
import { useState, useEffect } from "react";
|
||||
import { LanguageSwitcher, MediaViewer } from "@widgets";
|
||||
import { SelectMediaDialog } from "@shared";
|
||||
|
||||
export const CityCreatePage = observer(() => {
|
||||
const navigate = useNavigate();
|
||||
const [name, setName] = useState("");
|
||||
const [countryCode, setCountryCode] = useState("");
|
||||
const [selectedMediaId, setSelectedMediaId] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
countryStore.getCountries();
|
||||
mediaStore.getMedia();
|
||||
}, []);
|
||||
|
||||
const handleCreate = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await cityStore.createCity(
|
||||
name,
|
||||
countryStore.countries.find((c) => c.code === countryCode)?.name!,
|
||||
countryCode,
|
||||
selectedMediaId!
|
||||
);
|
||||
toast.success("Город успешно создан");
|
||||
navigate("/city");
|
||||
} catch (error) {
|
||||
toast.error("Ошибка при создании города");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMediaSelect = (media: {
|
||||
id: string;
|
||||
filename: string;
|
||||
media_name?: string;
|
||||
media_type: number;
|
||||
}) => {
|
||||
setSelectedMediaId(media.id);
|
||||
};
|
||||
|
||||
const selectedMedia = selectedMediaId
|
||||
? mediaStore.media.find((m) => m.id === selectedMediaId)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
||||
<LanguageSwitcher />
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => navigate("/city")}
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
Назад
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-10 w-full items-end">
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Название города"
|
||||
value={name}
|
||||
required
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Страна</InputLabel>
|
||||
<Select
|
||||
value={countryCode}
|
||||
label="Страна"
|
||||
required
|
||||
onChange={(e) => setCountryCode(e.target.value)}
|
||||
>
|
||||
{countryStore.countries.map((country) => (
|
||||
<MenuItem key={country.code} value={country.code}>
|
||||
{country.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<div className="w-full flex flex-col gap-4">
|
||||
<label className="text-sm text-gray-600">Герб города</label>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => setIsSelectMediaOpen(true)}
|
||||
startIcon={<ImagePlus size={20} />}
|
||||
>
|
||||
Выбрать герб
|
||||
</Button>
|
||||
{selectedMedia && (
|
||||
<span className="text-sm text-gray-600">
|
||||
{selectedMedia.media_name || selectedMedia.filename}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{selectedMedia && (
|
||||
<Box
|
||||
sx={{
|
||||
width: "200px",
|
||||
height: "200px",
|
||||
border: "1px solid #e0e0e0",
|
||||
borderRadius: "8px",
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<MediaViewer
|
||||
media={{
|
||||
id: selectedMedia.id,
|
||||
media_type: selectedMedia.media_type,
|
||||
filename: selectedMedia.filename,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
className="w-min flex gap-2 items-center"
|
||||
startIcon={<Save size={20} />}
|
||||
onClick={handleCreate}
|
||||
disabled={isLoading || !name || !countryCode}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 size={20} className="animate-spin" />
|
||||
) : (
|
||||
"Создать"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<SelectMediaDialog
|
||||
open={isSelectMediaOpen}
|
||||
onClose={() => setIsSelectMediaOpen(false)}
|
||||
onSelectMedia={handleMediaSelect}
|
||||
mediaType={3} // Тип медиа для иконок
|
||||
/>
|
||||
</Paper>
|
||||
);
|
||||
});
|
96
src/pages/City/CityListPage/index.tsx
Normal file
96
src/pages/City/CityListPage/index.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||
import { cityStore, languageStore } from "@shared";
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Eye, Trash2 } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets";
|
||||
|
||||
export const CityListPage = observer(() => {
|
||||
const { cities, getCities, deleteCity } = cityStore;
|
||||
const navigate = useNavigate();
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [rowId, setRowId] = useState<number | null>(null);
|
||||
const { language } = languageStore;
|
||||
|
||||
useEffect(() => {
|
||||
getCities();
|
||||
}, [language]);
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: "country",
|
||||
headerName: "Страна",
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
field: "name",
|
||||
headerName: "Название",
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
field: "actions",
|
||||
headerName: "Действия",
|
||||
align: "center",
|
||||
headerAlign: "center",
|
||||
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
return (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
<button onClick={() => navigate(`/city/${params.row.id}`)}>
|
||||
<Eye size={20} className="text-green-500" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={20} className="text-red-500" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const rows = cities.map((city) => ({
|
||||
id: city.id,
|
||||
name: city.name,
|
||||
country: city.country,
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<LanguageSwitcher />
|
||||
|
||||
<div className="w-full">
|
||||
<div className="flex justify-between items-center mb-10">
|
||||
<h1 className="text-2xl">Города</h1>
|
||||
<CreateButton label="Создать город" path="/city/create" />
|
||||
</div>
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
hideFooterPagination
|
||||
hideFooter
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DeleteModal
|
||||
open={isDeleteModalOpen}
|
||||
onDelete={async () => {
|
||||
if (rowId) {
|
||||
deleteCity(rowId);
|
||||
}
|
||||
setIsDeleteModalOpen(false);
|
||||
setRowId(null);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setIsDeleteModalOpen(false);
|
||||
setRowId(null);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
76
src/pages/City/CityPreviewPage/index.tsx
Normal file
76
src/pages/City/CityPreviewPage/index.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import { Paper } from "@mui/material";
|
||||
import { cityStore, 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 CityPreviewPage = observer(() => {
|
||||
const { id } = useParams();
|
||||
const { getCity, city } = cityStore;
|
||||
const { oneMedia, getOneMedia } = mediaStore;
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const cityResponse = await getCity(id as string);
|
||||
await getOneMedia(cityResponse.arms as string);
|
||||
})();
|
||||
}, [id]);
|
||||
|
||||
return (
|
||||
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
||||
<div className="flex justify-between items-center">
|
||||
<button
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
Назад
|
||||
</button>
|
||||
{/* <div className="flex gap-2">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => navigate(`/city/${id}/edit`)}
|
||||
startIcon={<Pencil size={20} />}
|
||||
>
|
||||
Редактировать
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={() => navigate(`/city/${id}/edit`)}
|
||||
startIcon={<Trash2 size={20} />}
|
||||
>
|
||||
Удалить
|
||||
</Button>
|
||||
</div> */}
|
||||
</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>{city?.name}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="text-lg font-bold">Страна</h1>
|
||||
<p>{city?.country}</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="text-lg font-bold">Герб</h1>
|
||||
<div className="w-[300px] h-[200px]">
|
||||
<MediaViewer
|
||||
media={{
|
||||
id: oneMedia?.id as string,
|
||||
media_type: oneMedia?.media_type as number,
|
||||
filename: oneMedia?.filename,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Paper>
|
||||
);
|
||||
});
|
3
src/pages/City/index.ts
Normal file
3
src/pages/City/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./CityListPage";
|
||||
export * from "./CityPreviewPage";
|
||||
export * from "./CityCreatePage";
|
75
src/pages/Country/CountryCreatePage/index.tsx
Normal file
75
src/pages/Country/CountryCreatePage/index.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import { Button, Paper, TextField } from "@mui/material";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ArrowLeft, Save } from "lucide-react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { toast } from "react-toastify";
|
||||
import { countryStore } from "@shared";
|
||||
import { useState } from "react";
|
||||
import { LanguageSwitcher } from "@widgets";
|
||||
|
||||
export const CountryCreatePage = observer(() => {
|
||||
const navigate = useNavigate();
|
||||
const [name, setName] = useState("");
|
||||
const [code, setCode] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleCreate = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await countryStore.createCountry(code, name);
|
||||
toast.success("Страна успешно создана");
|
||||
navigate("/country");
|
||||
} catch (error) {
|
||||
toast.error("Ошибка при создании страны");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
||||
<LanguageSwitcher />
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => navigate("/country")}
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
Назад
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-10 w-full items-end">
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Код страны"
|
||||
value={code}
|
||||
required
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Название"
|
||||
value={name}
|
||||
required
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
className="w-min flex gap-2 items-center"
|
||||
startIcon={<Save size={20} />}
|
||||
onClick={handleCreate}
|
||||
disabled={isLoading || !name || !code}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 size={20} className="animate-spin" />
|
||||
) : (
|
||||
"Создать"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</Paper>
|
||||
);
|
||||
});
|
86
src/pages/Country/CountryListPage/index.tsx
Normal file
86
src/pages/Country/CountryListPage/index.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||
import { countryStore, languageStore } from "@shared";
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Eye, Trash2 } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets";
|
||||
|
||||
export const CountryListPage = observer(() => {
|
||||
const { countries, getCountries } = countryStore;
|
||||
const navigate = useNavigate();
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [rowId, setRowId] = useState<string | null>(null); // Lifted state
|
||||
const { language } = languageStore;
|
||||
|
||||
useEffect(() => {
|
||||
getCountries();
|
||||
}, [language]);
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: "name",
|
||||
headerName: "Название",
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
field: "actions",
|
||||
headerName: "Действия",
|
||||
align: "center",
|
||||
headerAlign: "center",
|
||||
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
return (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
<button onClick={() => navigate(`/country/${params.row.code}`)}>
|
||||
<Eye size={20} className="text-green-500" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.code);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={20} className="text-red-500" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const rows = countries.map((country) => ({
|
||||
id: country.code,
|
||||
code: country.code,
|
||||
name: country.name,
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<LanguageSwitcher />
|
||||
|
||||
<div className="w-full">
|
||||
<div className="flex justify-between items-center mb-10">
|
||||
<h1 className="text-2xl">Страны</h1>
|
||||
<CreateButton label="Создать страну" path="/country/create" />
|
||||
</div>
|
||||
<DataGrid rows={rows} columns={columns} hideFooter />
|
||||
</div>
|
||||
<DeleteModal
|
||||
open={isDeleteModalOpen}
|
||||
onDelete={async () => {
|
||||
if (rowId) {
|
||||
await countryStore.deleteCountry(rowId);
|
||||
getCountries(); // Refresh the list after deletion
|
||||
}
|
||||
setIsDeleteModalOpen(false);
|
||||
setRowId(null);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setIsDeleteModalOpen(false);
|
||||
setRowId(null);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
58
src/pages/Country/CountryPreviewPage/index.tsx
Normal file
58
src/pages/Country/CountryPreviewPage/index.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { Paper } from "@mui/material";
|
||||
import { countryStore } from "@shared";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
export const CountryPreviewPage = observer(() => {
|
||||
const { id } = useParams();
|
||||
const { getCountry, country } = countryStore;
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
await getCountry(id as string);
|
||||
})();
|
||||
}, [id]);
|
||||
|
||||
return (
|
||||
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
||||
<div className="flex justify-between items-center">
|
||||
<button
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
Назад
|
||||
</button>
|
||||
{/* <div className="flex gap-2">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => navigate(`/user/${id}/edit`)}
|
||||
startIcon={<Pencil size={20} />}
|
||||
>
|
||||
Редактировать
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={() => navigate(`/user/${id}/delete`)}
|
||||
startIcon={<Trash2 size={20} />}
|
||||
>
|
||||
Удалить
|
||||
</Button>
|
||||
</div> */}
|
||||
</div>
|
||||
{country && (
|
||||
<div className="flex flex-col gap-10 w-full">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="text-lg font-bold">Название</h1>
|
||||
<p>{country?.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
});
|
3
src/pages/Country/index.ts
Normal file
3
src/pages/Country/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./CountryListPage";
|
||||
export * from "./CountryPreviewPage";
|
||||
export * from "./CountryCreatePage";
|
@ -1,126 +0,0 @@
|
||||
// import { Button, Paper, Typography, Box, Alert, Snackbar } from "@mui/material";
|
||||
// import { useNavigate } from "react-router-dom";
|
||||
// import { ArrowLeft, Upload } from "lucide-react";
|
||||
// import { observer } from "mobx-react-lite";
|
||||
// import { useState, DragEvent, useRef, useEffect } from "react";
|
||||
// import { editSightStore, UploadMediaDialog } from "@shared";
|
||||
|
||||
// export const CreateMediaPage = observer(() => {
|
||||
// const navigate = useNavigate();
|
||||
// const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
// const [error, setError] = useState<string | null>(null);
|
||||
// const [success, setSuccess] = useState(false);
|
||||
// const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
// const [uploadDialogOpen, setUploadDialogOpen] = useState(false);
|
||||
|
||||
// const handleDrop = (e: DragEvent<HTMLDivElement>) => {
|
||||
// e.preventDefault();
|
||||
// e.stopPropagation();
|
||||
// setIsDragging(false);
|
||||
|
||||
// const files = Array.from(e.dataTransfer.files);
|
||||
// if (files.length > 0) {
|
||||
// editSightStore.fileToUpload = files[0];
|
||||
// setUploadDialogOpen(true);
|
||||
// }
|
||||
// };
|
||||
|
||||
// const handleDragOver = (e: DragEvent<HTMLDivElement>) => {
|
||||
// e.preventDefault();
|
||||
// setIsDragging(true);
|
||||
// };
|
||||
|
||||
// const handleDragLeave = () => {
|
||||
// setIsDragging(false);
|
||||
// };
|
||||
|
||||
// const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
// const files = e.target.files;
|
||||
// if (files && files.length > 0) {
|
||||
// editSightStore.fileToUpload = files[0];
|
||||
// setUploadDialogOpen(true);
|
||||
// }
|
||||
// };
|
||||
|
||||
// const handleUploadSuccess = () => {
|
||||
// setSuccess(true);
|
||||
// setUploadDialogOpen(false);
|
||||
// };
|
||||
|
||||
// return (
|
||||
// <div className="w-full h-full p-8">
|
||||
// <div className="flex items-center gap-4 mb-8">
|
||||
// <Button
|
||||
// variant="outlined"
|
||||
// startIcon={<ArrowLeft size={20} />}
|
||||
// onClick={() => navigate("/media")}
|
||||
// >
|
||||
// Назад
|
||||
// </Button>
|
||||
// <Typography variant="h5">Загрузка медиафайла</Typography>
|
||||
// </div>
|
||||
|
||||
// <Paper
|
||||
// elevation={3}
|
||||
// className={`w-full h-[60vh] flex flex-col items-center justify-center p-8 transition-colors ${
|
||||
// isDragging ? "bg-blue-50 border-2 border-blue-500" : "bg-gray-50"
|
||||
// }`}
|
||||
// onDrop={handleDrop}
|
||||
// onDragOver={handleDragOver}
|
||||
// onDragLeave={handleDragLeave}
|
||||
// >
|
||||
// <input
|
||||
// type="file"
|
||||
// ref={fileInputRef}
|
||||
// className="hidden"
|
||||
// onChange={handleFileSelect}
|
||||
// accept="image/*,video/*,.glb,.gltf"
|
||||
// />
|
||||
|
||||
// <Box className="flex flex-col items-center gap-4 text-center">
|
||||
// <Upload size={48} className="text-gray-400" />
|
||||
// <Typography variant="h6" className="text-gray-600">
|
||||
// Перетащите файл сюда или
|
||||
// </Typography>
|
||||
// <Button
|
||||
// variant="contained"
|
||||
// onClick={() => fileInputRef.current?.click()}
|
||||
// startIcon={<Upload size={20} />}
|
||||
// >
|
||||
// Выберите файл
|
||||
// </Button>
|
||||
// <Typography variant="body2" className="text-gray-500 mt-4">
|
||||
// Поддерживаемые форматы: JPG, PNG, GIF, MP4, WebM, GLB, GLTF
|
||||
// </Typography>
|
||||
// </Box>
|
||||
// </Paper>
|
||||
|
||||
// <UploadMediaDialog
|
||||
// open={uploadDialogOpen}
|
||||
// onClose={() => setUploadDialogOpen(false)}
|
||||
// afterUpload={handleUploadSuccess}
|
||||
// />
|
||||
|
||||
// <Snackbar
|
||||
// open={success}
|
||||
// autoHideDuration={2000}
|
||||
// onClose={() => setSuccess(false)}
|
||||
// >
|
||||
// <Alert severity="success" onClose={() => setSuccess(false)}>
|
||||
// Медиафайл успешно загружен
|
||||
// </Alert>
|
||||
// </Snackbar>
|
||||
|
||||
// <Snackbar
|
||||
// open={!!error}
|
||||
// autoHideDuration={6000}
|
||||
// onClose={() => setError(null)}
|
||||
// >
|
||||
// <Alert severity="error" onClose={() => setError(null)}>
|
||||
// {error}
|
||||
// </Alert>
|
||||
// </Snackbar>
|
||||
// </div>
|
||||
// );
|
||||
// });
|
89
src/pages/Media/MediaCreatePage/index.tsx
Normal file
89
src/pages/Media/MediaCreatePage/index.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
import {
|
||||
Button,
|
||||
Paper,
|
||||
TextField,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
} from "@mui/material";
|
||||
import { mediaStore, MEDIA_TYPE_LABELS } from "@shared";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ArrowLeft, Loader2, Save } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export const MediaCreatePage = observer(() => {
|
||||
const navigate = useNavigate();
|
||||
const [name, setName] = useState("");
|
||||
const [type, setType] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleCreate = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await mediaStore.createMedia(name, type);
|
||||
toast.success("Медиа успешно создано");
|
||||
navigate("/media");
|
||||
} catch (error) {
|
||||
toast.error("Ошибка при создании медиа");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
||||
<div className="flex justify-between items-center">
|
||||
<button
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
Назад
|
||||
</button>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold">Создание медиа</h1>
|
||||
<div className="flex flex-col gap-10 w-full items-end">
|
||||
<TextField
|
||||
className="w-full"
|
||||
label="Название"
|
||||
required
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Тип</InputLabel>
|
||||
<Select
|
||||
value={type}
|
||||
label="Тип"
|
||||
onChange={(e) => setType(e.target.value)}
|
||||
required
|
||||
>
|
||||
{Object.entries(MEDIA_TYPE_LABELS).map(([value, label]) => (
|
||||
<MenuItem key={value} value={value}>
|
||||
{label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
className="w-min flex gap-2 items-center"
|
||||
startIcon={<Save size={20} />}
|
||||
onClick={handleCreate}
|
||||
disabled={isLoading || !name || !type}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 size={20} className="animate-spin" />
|
||||
) : (
|
||||
"Создать"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</Paper>
|
||||
);
|
||||
});
|
@ -19,7 +19,7 @@ import { Save, ArrowLeft } from "lucide-react";
|
||||
import { authInstance, mediaStore, MEDIA_TYPE_LABELS } from "@shared";
|
||||
import { MediaViewer } from "@widgets";
|
||||
|
||||
export const EditMediaPage = observer(() => {
|
||||
export const MediaEditPage = observer(() => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
110
src/pages/Media/MediaListPage/index.tsx
Normal file
110
src/pages/Media/MediaListPage/index.tsx
Normal file
@ -0,0 +1,110 @@
|
||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||
import { languageStore, MEDIA_TYPE_LABELS, mediaStore } from "@shared";
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Eye, Trash2 } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets";
|
||||
|
||||
export const MediaListPage = observer(() => {
|
||||
const { media, getMedia, deleteMedia } = mediaStore;
|
||||
const navigate = useNavigate();
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [rowId, setRowId] = useState<string | null>(null); // Lifted state
|
||||
const { language } = languageStore;
|
||||
|
||||
useEffect(() => {
|
||||
getMedia();
|
||||
}, [language]);
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: "media_name",
|
||||
headerName: "Название",
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
field: "media_type",
|
||||
headerName: "Тип",
|
||||
width: 200,
|
||||
flex: 1,
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
return (
|
||||
<p>
|
||||
{
|
||||
MEDIA_TYPE_LABELS[
|
||||
params.row.media_type as keyof typeof MEDIA_TYPE_LABELS
|
||||
]
|
||||
}
|
||||
</p>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: "actions",
|
||||
headerName: "Действия",
|
||||
flex: 1,
|
||||
align: "center",
|
||||
headerAlign: "center",
|
||||
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
return (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
<button onClick={() => navigate(`/media/${params.row.id}`)}>
|
||||
<Eye size={20} className="text-green-500" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={20} className="text-red-500" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const rows = media.map((media) => ({
|
||||
id: media.id,
|
||||
media_name: media.media_name,
|
||||
media_type: media.media_type,
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<LanguageSwitcher />
|
||||
|
||||
<div className="w-full">
|
||||
<div className="flex justify-between items-center mb-10">
|
||||
<h1 className="text-2xl">Медиа</h1>
|
||||
<CreateButton label="Создать медиа" path="/media/create" />
|
||||
</div>
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
hideFooterPagination
|
||||
hideFooter
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DeleteModal
|
||||
open={isDeleteModalOpen}
|
||||
onDelete={async () => {
|
||||
if (rowId) {
|
||||
deleteMedia(rowId.toString());
|
||||
getMedia();
|
||||
}
|
||||
setIsDeleteModalOpen(false);
|
||||
setRowId(null);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setIsDeleteModalOpen(false);
|
||||
setRowId(null);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
@ -1,12 +1,12 @@
|
||||
import { mediaStore } from "@shared";
|
||||
import { useEffect } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { MediaViewer } from "../../widgets/MediaViewer/index";
|
||||
import { MediaViewer } from "@widgets";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Download } from "lucide-react";
|
||||
import { Button } from "@mui/material";
|
||||
|
||||
export const PreviewMediaPage = observer(() => {
|
||||
export const MediaPreviewPage = observer(() => {
|
||||
const { id } = useParams();
|
||||
const { oneMedia, getOneMedia } = mediaStore;
|
||||
|
3
src/pages/Media/index.ts
Normal file
3
src/pages/Media/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./MediaEditPage";
|
||||
export * from "./MediaListPage";
|
||||
export * from "./MediaPreviewPage";
|
@ -1,88 +0,0 @@
|
||||
import { TableBody } from "@mui/material";
|
||||
import { TableRow, TableCell } from "@mui/material";
|
||||
import { Table, TableHead } from "@mui/material";
|
||||
import { mediaStore, MEDIA_TYPE_LABELS } from "@shared";
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Eye, Pencil, Trash2 } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { DeleteModal } from "@widgets";
|
||||
|
||||
const rows = (media: any[]) => {
|
||||
return media.map((row) => ({
|
||||
id: row.id,
|
||||
media_name: row.media_name,
|
||||
media_type:
|
||||
MEDIA_TYPE_LABELS[row.media_type as keyof typeof MEDIA_TYPE_LABELS],
|
||||
}));
|
||||
};
|
||||
|
||||
export const MediaListPage = observer(() => {
|
||||
const { media, getMedia, deleteMedia } = mediaStore;
|
||||
const navigate = useNavigate();
|
||||
useEffect(() => {
|
||||
getMedia();
|
||||
}, []);
|
||||
|
||||
const currentRows = rows(media);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [rowId, setRowId] = useState<number | null>(null);
|
||||
return (
|
||||
<>
|
||||
<Table sx={{ minWidth: 650 }} aria-label="simple table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell align="center">Название</TableCell>
|
||||
<TableCell align="center">Тип</TableCell>
|
||||
<TableCell align="center">Действия</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{currentRows.map((row) => (
|
||||
<TableRow
|
||||
key={row.media_name}
|
||||
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
|
||||
>
|
||||
<TableCell align="center">{row.media_name}</TableCell>
|
||||
<TableCell align="center">{row.media_type}</TableCell>
|
||||
<TableCell align="center">
|
||||
<div className="flex gap-7 justify-center">
|
||||
<button onClick={() => navigate(`/media/${row.id}`)}>
|
||||
<Eye size={20} className="text-green-500" />
|
||||
</button>
|
||||
|
||||
<button onClick={() => navigate(`/media/${row.id}/edit`)}>
|
||||
<Pencil size={20} className="text-blue-500" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(row.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={20} className="text-red-500" />
|
||||
</button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<DeleteModal
|
||||
open={isDeleteModalOpen}
|
||||
onDelete={async () => {
|
||||
if (rowId) {
|
||||
await deleteMedia(rowId.toString());
|
||||
}
|
||||
setIsDeleteModalOpen(false);
|
||||
setRowId(null);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setIsDeleteModalOpen(false);
|
||||
setRowId(null);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
86
src/pages/Route/RouteCreatePage/index.tsx
Normal file
86
src/pages/Route/RouteCreatePage/index.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import {
|
||||
Button,
|
||||
Paper,
|
||||
TextField,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
} from "@mui/material";
|
||||
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ArrowLeft, Loader2, Save } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export const RouteCreatePage = observer(() => {
|
||||
const navigate = useNavigate();
|
||||
const [routeNumber, setRouteNumber] = useState("");
|
||||
const [direction, setDirection] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
return (
|
||||
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
||||
<div className="flex justify-between items-center">
|
||||
<button
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
Назад
|
||||
</button>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold">Создание маршрута</h1>
|
||||
<div className="flex flex-col gap-10 w-full items-end">
|
||||
<TextField
|
||||
className="w-full"
|
||||
label="Номер маршрута"
|
||||
required
|
||||
value={routeNumber}
|
||||
onChange={(e) => setRouteNumber(e.target.value)}
|
||||
/>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Направление</InputLabel>
|
||||
<Select
|
||||
value={direction}
|
||||
label="Направление"
|
||||
onChange={(e) => setDirection(e.target.value)}
|
||||
required
|
||||
>
|
||||
<MenuItem value="forward">Прямое</MenuItem>
|
||||
<MenuItem value="backward">Обратное</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
className="w-min flex gap-2 items-center"
|
||||
startIcon={<Save size={20} />}
|
||||
onClick={async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
// await createRoute(routeNumber, direction === "forward");
|
||||
setIsLoading(false);
|
||||
toast.success("Маршрут успешно создан");
|
||||
navigate(-1);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error("Произошла ошибка");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 size={20} className="animate-spin" />
|
||||
) : (
|
||||
"Сохранить"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</Paper>
|
||||
);
|
||||
});
|
115
src/pages/Route/RouteListPage/index.tsx
Normal file
115
src/pages/Route/RouteListPage/index.tsx
Normal file
@ -0,0 +1,115 @@
|
||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||
import { languageStore, routeStore } from "@shared";
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Eye, Trash2 } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets";
|
||||
|
||||
export const RouteListPage = observer(() => {
|
||||
const { routes, getRoutes, deleteRoute } = routeStore;
|
||||
const navigate = useNavigate();
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [rowId, setRowId] = useState<number | null>(null); // Lifted state
|
||||
const { language } = languageStore;
|
||||
|
||||
useEffect(() => {
|
||||
getRoutes();
|
||||
}, [language]);
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: "carrier",
|
||||
headerName: "Перевозчик",
|
||||
width: 250,
|
||||
},
|
||||
{
|
||||
field: "route_number",
|
||||
headerName: "Номер маршрута",
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
field: "route_direction",
|
||||
headerName: "Направление",
|
||||
flex: 1,
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
return (
|
||||
<p
|
||||
className={
|
||||
params.row.route_direction === "Прямой"
|
||||
? "text-green-500"
|
||||
: "text-red-500"
|
||||
}
|
||||
>
|
||||
{params.row.route_direction}
|
||||
</p>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: "actions",
|
||||
headerName: "Действия",
|
||||
width: 140,
|
||||
align: "center",
|
||||
headerAlign: "center",
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
return (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
<button onClick={() => navigate(`/route/${params.row.id}`)}>
|
||||
<Eye size={20} className="text-green-500" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={20} className="text-red-500" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const rows = routes.map((route) => ({
|
||||
id: route.id,
|
||||
carrier: route.carrier,
|
||||
route_number: route.route_number,
|
||||
route_direction: route.route_direction ? "Прямой" : "Обратный",
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<LanguageSwitcher />
|
||||
|
||||
<div style={{ width: "100%" }}>
|
||||
<div className="flex justify-between items-center mb-10">
|
||||
<h1 className="text-2xl">Маршруты</h1>
|
||||
<CreateButton label="Создать маршрут" path="/route/create" />
|
||||
</div>
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
hideFooterPagination
|
||||
hideFooter
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DeleteModal
|
||||
open={isDeleteModalOpen}
|
||||
onDelete={async () => {
|
||||
if (rowId) {
|
||||
deleteRoute(rowId);
|
||||
}
|
||||
setIsDeleteModalOpen(false);
|
||||
setRowId(null);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setIsDeleteModalOpen(false);
|
||||
setRowId(null);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
1
src/pages/Route/index.ts
Normal file
1
src/pages/Route/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./RouteListPage";
|
96
src/pages/Sight/SightListPage/index.tsx
Normal file
96
src/pages/Sight/SightListPage/index.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||
import { languageStore, sightsStore } from "@shared";
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Pencil, Trash2 } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { DeleteModal, LanguageSwitcher } from "@widgets";
|
||||
|
||||
export const SightListPage = observer(() => {
|
||||
const { sights, getSights, deleteListSight } = sightsStore;
|
||||
const navigate = useNavigate();
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [rowId, setRowId] = useState<string | null>(null); // Lifted state
|
||||
const { language } = languageStore;
|
||||
|
||||
useEffect(() => {
|
||||
getSights();
|
||||
}, [language]);
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: "name",
|
||||
headerName: "Имя",
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
field: "city",
|
||||
headerName: "Город",
|
||||
flex: 1,
|
||||
},
|
||||
|
||||
{
|
||||
field: "actions",
|
||||
headerName: "Действия",
|
||||
align: "center",
|
||||
headerAlign: "center",
|
||||
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
return (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
<button onClick={() => navigate(`/sight/${params.row.id}`)}>
|
||||
<Pencil size={20} className="text-blue-500" />
|
||||
</button>
|
||||
{/* <button onClick={() => navigate(`/sight/${params.row.id}`)}>
|
||||
<Eye size={20} className="text-green-500" />
|
||||
</button> */}
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={20} className="text-red-500" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const rows = sights.map((sight) => ({
|
||||
id: sight.id,
|
||||
name: sight.name,
|
||||
city: sight.city,
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<LanguageSwitcher />
|
||||
|
||||
<div className="w-full">
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
hideFooterPagination
|
||||
hideFooter
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DeleteModal
|
||||
open={isDeleteModalOpen}
|
||||
onDelete={async () => {
|
||||
if (rowId) {
|
||||
await deleteListSight(Number(rowId));
|
||||
}
|
||||
setIsDeleteModalOpen(false);
|
||||
setRowId(null);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setIsDeleteModalOpen(false);
|
||||
setRowId(null);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
1
src/pages/Sight/index.ts
Normal file
1
src/pages/Sight/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./SightListPage";
|
69
src/pages/Snapshot/SnapshotCreatePage/index.tsx
Normal file
69
src/pages/Snapshot/SnapshotCreatePage/index.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import { Button, Paper, TextField } from "@mui/material";
|
||||
import { snapshotStore } from "@shared";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ArrowLeft, Loader2, Save } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export const SnapshotCreatePage = observer(() => {
|
||||
const { id } = useParams();
|
||||
const { getSnapshot, createSnapshot } = snapshotStore;
|
||||
const navigate = useNavigate();
|
||||
const [name, setName] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
await getSnapshot(id as string);
|
||||
})();
|
||||
}, [id]);
|
||||
|
||||
return (
|
||||
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
||||
<div className="flex justify-between items-center">
|
||||
<button
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
Назад
|
||||
</button>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold">Создание снапшота</h1>
|
||||
<div className="flex flex-col gap-10 w-full items-end">
|
||||
<TextField
|
||||
className="w-full"
|
||||
label="Название"
|
||||
required
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
className="w-min flex gap-2 items-center"
|
||||
startIcon={<Save size={20} />}
|
||||
onClick={async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await createSnapshot(name);
|
||||
setIsLoading(false);
|
||||
toast.success("Снапшот успешно создан");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 size={20} className="animate-spin" />
|
||||
) : (
|
||||
"Сохранить"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</Paper>
|
||||
);
|
||||
});
|
127
src/pages/Snapshot/SnapshotListPage/index.tsx
Normal file
127
src/pages/Snapshot/SnapshotListPage/index.tsx
Normal file
@ -0,0 +1,127 @@
|
||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||
import { languageStore, snapshotStore } from "@shared";
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { DatabaseBackup, Eye, Trash2 } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
CreateButton,
|
||||
DeleteModal,
|
||||
LanguageSwitcher,
|
||||
SnapshotRestore,
|
||||
} from "@widgets";
|
||||
|
||||
export const SnapshotListPage = observer(() => {
|
||||
const { snapshots, getSnapshots, deleteSnapshot, restoreSnapshot } =
|
||||
snapshotStore;
|
||||
const navigate = useNavigate();
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [rowId, setRowId] = useState<string | null>(null); // Lifted state
|
||||
const { language } = languageStore;
|
||||
const [isRestoreModalOpen, setIsRestoreModalOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
getSnapshots();
|
||||
}, [language]);
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: "name",
|
||||
headerName: "Название",
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
field: "parent",
|
||||
headerName: "Родитель",
|
||||
flex: 1,
|
||||
},
|
||||
|
||||
{
|
||||
field: "actions",
|
||||
headerName: "Действия",
|
||||
width: 300,
|
||||
headerAlign: "center",
|
||||
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
return (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsRestoreModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
}}
|
||||
>
|
||||
<DatabaseBackup size={20} className="text-blue-500" />
|
||||
</button>
|
||||
<button onClick={() => navigate(`/snapshot/${params.row.id}`)}>
|
||||
<Eye size={20} className="text-green-500" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={20} className="text-red-500" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const rows = snapshots.map((snapshot) => ({
|
||||
id: snapshot.ID,
|
||||
name: snapshot.Name,
|
||||
parent: snapshots.find((s) => s.ID === snapshot.ParentID)?.Name,
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<LanguageSwitcher />
|
||||
|
||||
<div style={{ width: "100%" }}>
|
||||
<div className="flex justify-between items-center mb-10">
|
||||
<h1 className="text-2xl ">Снапшоты</h1>
|
||||
<CreateButton label="Создать снапшот" path="/snapshot/create" />
|
||||
</div>
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
hideFooterPagination
|
||||
hideFooter
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DeleteModal
|
||||
open={isDeleteModalOpen}
|
||||
onDelete={async () => {
|
||||
if (rowId) {
|
||||
await deleteSnapshot(rowId);
|
||||
}
|
||||
setIsDeleteModalOpen(false);
|
||||
setRowId(null);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setIsDeleteModalOpen(false);
|
||||
setRowId(null);
|
||||
}}
|
||||
/>
|
||||
|
||||
<SnapshotRestore
|
||||
open={isRestoreModalOpen}
|
||||
onDelete={async () => {
|
||||
if (rowId) {
|
||||
await restoreSnapshot(rowId);
|
||||
}
|
||||
setIsRestoreModalOpen(false);
|
||||
setRowId(null);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setIsRestoreModalOpen(false);
|
||||
setRowId(null);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
58
src/pages/Snapshot/SnapshotPreviewPage/index.tsx
Normal file
58
src/pages/Snapshot/SnapshotPreviewPage/index.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { Paper } from "@mui/material";
|
||||
import { snapshotStore } from "@shared";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
export const SnapshotPreviewPage = observer(() => {
|
||||
const { id } = useParams();
|
||||
const { getSnapshot, snapshot } = snapshotStore;
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
await getSnapshot(id as string);
|
||||
})();
|
||||
}, [id]);
|
||||
|
||||
return (
|
||||
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
||||
<div className="flex justify-between items-center">
|
||||
<button
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
Назад
|
||||
</button>
|
||||
{/* <div className="flex gap-2">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => navigate(`/snapshot/${id}/edit`)}
|
||||
startIcon={<Pencil size={20} />}
|
||||
>
|
||||
Редактировать
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={() => navigate(`/snapshot/${id}/delete`)}
|
||||
startIcon={<Trash2 size={20} />}
|
||||
>
|
||||
Удалить
|
||||
</Button>
|
||||
</div> */}
|
||||
</div>
|
||||
{snapshot && (
|
||||
<div className="flex flex-col gap-10 w-full">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="text-lg font-bold">Название</h1>
|
||||
<p>{snapshot?.Name}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
});
|
3
src/pages/Snapshot/index.ts
Normal file
3
src/pages/Snapshot/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./SnapshotListPage";
|
||||
export * from "./SnapshotPreviewPage";
|
||||
export * from "./SnapshotCreatePage";
|
94
src/pages/Station/StationCreatePage/index.tsx
Normal file
94
src/pages/Station/StationCreatePage/index.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
import {
|
||||
Button,
|
||||
Paper,
|
||||
TextField,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
} from "@mui/material";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ArrowLeft, Loader2, Save } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { toast } from "react-toastify";
|
||||
import { stationsStore } from "@shared";
|
||||
import { useState } from "react";
|
||||
|
||||
export const StationCreatePage = observer(() => {
|
||||
const navigate = useNavigate();
|
||||
const [name, setName] = useState("");
|
||||
const [systemName, setSystemName] = useState("");
|
||||
const [direction, setDirection] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleCreate = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await stationsStore.createStation(name, systemName, direction);
|
||||
toast.success("Станция успешно создана");
|
||||
navigate("/station");
|
||||
} catch (error) {
|
||||
toast.error("Ошибка при создании станции");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
||||
<div className="flex justify-between items-center">
|
||||
<button
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
Назад
|
||||
</button>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold">Создание станции</h1>
|
||||
<div className="flex flex-col gap-10 w-full items-end">
|
||||
<TextField
|
||||
className="w-full"
|
||||
label="Название"
|
||||
required
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
className="w-full"
|
||||
label="Системное название"
|
||||
required
|
||||
value={systemName}
|
||||
onChange={(e) => setSystemName(e.target.value)}
|
||||
/>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Направление</InputLabel>
|
||||
<Select
|
||||
value={direction}
|
||||
label="Направление"
|
||||
onChange={(e) => setDirection(e.target.value)}
|
||||
required
|
||||
>
|
||||
<MenuItem value="forward">Прямое</MenuItem>
|
||||
<MenuItem value="backward">Обратное</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
className="w-min flex gap-2 items-center"
|
||||
startIcon={<Save size={20} />}
|
||||
onClick={handleCreate}
|
||||
disabled={isLoading || !name || !systemName || !direction}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 size={20} className="animate-spin" />
|
||||
) : (
|
||||
"Создать"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</Paper>
|
||||
);
|
||||
});
|
117
src/pages/Station/StationListPage/index.tsx
Normal file
117
src/pages/Station/StationListPage/index.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||
import { languageStore, stationsStore } from "@shared";
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Eye, Trash2 } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets";
|
||||
|
||||
export const StationListPage = observer(() => {
|
||||
const { stations, getStations, deleteStation } = stationsStore;
|
||||
const navigate = useNavigate();
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [rowId, setRowId] = useState<number | null>(null); // Lifted state
|
||||
const { language } = languageStore;
|
||||
|
||||
useEffect(() => {
|
||||
getStations();
|
||||
}, [language]);
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: "name",
|
||||
headerName: "Название",
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
field: "system_name",
|
||||
headerName: "Системное название",
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
field: "direction",
|
||||
headerName: "Направление",
|
||||
width: 200,
|
||||
align: "center",
|
||||
headerAlign: "center",
|
||||
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
return (
|
||||
<p
|
||||
className={
|
||||
params.row.direction === true ? "text-green-500" : "text-red-500"
|
||||
}
|
||||
>
|
||||
{params.row.direction ? "Прямой" : "Обратный"}
|
||||
</p>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: "actions",
|
||||
headerName: "Действия",
|
||||
width: 140,
|
||||
align: "center",
|
||||
headerAlign: "center",
|
||||
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
return (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
<button onClick={() => navigate(`/station/${params.row.id}`)}>
|
||||
<Eye size={20} className="text-green-500" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={20} className="text-red-500" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const rows = stations.map((station) => ({
|
||||
id: station.id,
|
||||
name: station.name,
|
||||
system_name: station.system_name,
|
||||
direction: station.direction,
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<LanguageSwitcher />
|
||||
|
||||
<div style={{ width: "100%" }}>
|
||||
<div className="flex justify-between items-center mb-10">
|
||||
<h1 className="text-2xl">Станции</h1>
|
||||
<CreateButton label="Создать станцию" path="/station/create" />
|
||||
</div>
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
hideFooterPagination
|
||||
hideFooter
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DeleteModal
|
||||
open={isDeleteModalOpen}
|
||||
onDelete={async () => {
|
||||
if (rowId) {
|
||||
deleteStation(rowId);
|
||||
}
|
||||
setIsDeleteModalOpen(false);
|
||||
setRowId(null);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setIsDeleteModalOpen(false);
|
||||
setRowId(null);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
1
src/pages/Station/index.ts
Normal file
1
src/pages/Station/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./StationListPage";
|
112
src/pages/User/UserListPage/index.tsx
Normal file
112
src/pages/User/UserListPage/index.tsx
Normal file
@ -0,0 +1,112 @@
|
||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||
import { languageStore, userStore } from "@shared";
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Eye, Trash2 } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { DeleteModal, LanguageSwitcher } from "@widgets";
|
||||
|
||||
export const UserListPage = observer(() => {
|
||||
const { users, getUsers, deleteUser } = userStore;
|
||||
const navigate = useNavigate();
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [rowId, setRowId] = useState<number | null>(null); // Lifted state
|
||||
const { language } = languageStore;
|
||||
|
||||
useEffect(() => {
|
||||
getUsers();
|
||||
}, [language]);
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: "name",
|
||||
headerName: "Имя",
|
||||
width: 400,
|
||||
},
|
||||
{
|
||||
field: "email",
|
||||
headerName: "Email",
|
||||
width: 400,
|
||||
},
|
||||
{
|
||||
field: "is_admin",
|
||||
headerName: "Роль",
|
||||
flex: 1,
|
||||
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
return (
|
||||
<p
|
||||
className={
|
||||
params.row.is_admin === true ? "text-green-500" : "text-red-500"
|
||||
}
|
||||
>
|
||||
{params.row.is_admin ? "Администратор" : "Пользователь"}
|
||||
</p>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
field: "actions",
|
||||
headerName: "Действия",
|
||||
flex: 1,
|
||||
align: "center",
|
||||
headerAlign: "center",
|
||||
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
return (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
<button onClick={() => navigate(`/user/${params.row.id}`)}>
|
||||
<Eye size={20} className="text-green-500" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={20} className="text-red-500" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const rows = users.map((user) => ({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
is_admin: user.is_admin,
|
||||
name: user.name,
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<LanguageSwitcher />
|
||||
|
||||
<div style={{ width: "100%" }}>
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
hideFooterPagination
|
||||
hideFooter
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DeleteModal
|
||||
open={isDeleteModalOpen}
|
||||
onDelete={async () => {
|
||||
if (rowId) {
|
||||
await deleteUser(rowId);
|
||||
}
|
||||
setIsDeleteModalOpen(false);
|
||||
setRowId(null);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setIsDeleteModalOpen(false);
|
||||
setRowId(null);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
74
src/pages/User/UserPreviewPage/index.tsx
Normal file
74
src/pages/User/UserPreviewPage/index.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
import { Paper } from "@mui/material";
|
||||
import { userStore } from "@shared";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
export const UserPreviewPage = observer(() => {
|
||||
const { id } = useParams();
|
||||
const { getUser, user } = userStore;
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
await getUser(Number(id));
|
||||
})();
|
||||
}, [id]);
|
||||
|
||||
return (
|
||||
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
||||
<div className="flex justify-between items-center">
|
||||
<button
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
Назад
|
||||
</button>
|
||||
{/* <div className="flex gap-2">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => navigate(`/user/${id}/edit`)}
|
||||
startIcon={<Pencil size={20} />}
|
||||
>
|
||||
Редактировать
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={() => navigate(`/user/${id}/delete`)}
|
||||
startIcon={<Trash2 size={20} />}
|
||||
>
|
||||
Удалить
|
||||
</Button>
|
||||
</div> */}
|
||||
</div>
|
||||
{user && (
|
||||
<div className="flex flex-col gap-10 w-full">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="text-lg font-bold">Название</h1>
|
||||
<p>{user?.name}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="text-lg font-bold">Email</h1>
|
||||
<p>{user?.email}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="text-lg font-bold">Роль</h1>
|
||||
<p
|
||||
className={
|
||||
user?.is_admin === true ? "text-green-500" : "text-red-500"
|
||||
}
|
||||
>
|
||||
{user?.is_admin ? "Администратор" : "Пользователь"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
});
|
2
src/pages/User/index.ts
Normal file
2
src/pages/User/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./UserListPage";
|
||||
export * from "./UserPreviewPage";
|
117
src/pages/Vehicle/VehicleCreatePage/index.tsx
Normal file
117
src/pages/Vehicle/VehicleCreatePage/index.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
import {
|
||||
Button,
|
||||
Paper,
|
||||
TextField,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
} from "@mui/material";
|
||||
import { vehicleStore, VEHICLE_TYPES, carrierStore } from "@shared";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ArrowLeft, Save } from "lucide-react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { toast } from "react-toastify";
|
||||
import { LanguageSwitcher } from "@widgets";
|
||||
|
||||
export const VehicleCreatePage = observer(() => {
|
||||
const navigate = useNavigate();
|
||||
const [tailNumber, setTailNumber] = useState("");
|
||||
const [type, setType] = useState("");
|
||||
const [carrierId, setCarrierId] = useState<number | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
carrierStore.getCarriers();
|
||||
}, []);
|
||||
|
||||
const handleCreate = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await vehicleStore.createVehicle(
|
||||
Number(tailNumber),
|
||||
type,
|
||||
carrierStore.carriers.find((c) => c.id === carrierId)?.full_name!,
|
||||
carrierId!
|
||||
);
|
||||
toast.success("Транспорт успешно создан");
|
||||
} catch (error) {
|
||||
toast.error("Ошибка при создании транспорта");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
||||
<LanguageSwitcher />
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => navigate("/vehicle")}
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
Назад
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-10 w-full items-end">
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Бортовой номер"
|
||||
value={tailNumber}
|
||||
required
|
||||
onChange={(e) => setTailNumber(e.target.value)}
|
||||
/>
|
||||
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Тип</InputLabel>
|
||||
<Select
|
||||
value={type}
|
||||
label="Тип"
|
||||
required
|
||||
onChange={(e) => setType(e.target.value)}
|
||||
>
|
||||
{VEHICLE_TYPES.map((type) => (
|
||||
<MenuItem key={type.value} value={type.value}>
|
||||
{type.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Перевозчик</InputLabel>
|
||||
<Select
|
||||
value={carrierId || ""}
|
||||
label="Перевозчик"
|
||||
required
|
||||
onChange={(e) => setCarrierId(e.target.value as number)}
|
||||
>
|
||||
{carrierStore.carriers.map((carrier) => (
|
||||
<MenuItem key={carrier.id} value={carrier.id}>
|
||||
{carrier.full_name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
className="w-min flex gap-2 items-center"
|
||||
startIcon={<Save size={20} />}
|
||||
onClick={handleCreate}
|
||||
disabled={isLoading || !tailNumber || !type || !carrierId}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 size={20} className="animate-spin" />
|
||||
) : (
|
||||
"Создать"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</Paper>
|
||||
);
|
||||
});
|
133
src/pages/Vehicle/VehicleListPage/index.tsx
Normal file
133
src/pages/Vehicle/VehicleListPage/index.tsx
Normal file
@ -0,0 +1,133 @@
|
||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||
import { carrierStore, languageStore, vehicleStore } from "@shared";
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Eye, Trash2 } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets";
|
||||
import { VEHICLE_TYPES } from "@shared";
|
||||
|
||||
export const VehicleListPage = observer(() => {
|
||||
const { vehicles, getVehicles, deleteVehicle } = vehicleStore;
|
||||
const { carriers, getCarriers } = carrierStore;
|
||||
const navigate = useNavigate();
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [rowId, setRowId] = useState<number | null>(null);
|
||||
const { language } = languageStore;
|
||||
|
||||
useEffect(() => {
|
||||
getVehicles();
|
||||
getCarriers();
|
||||
}, [language]);
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: "tail_number",
|
||||
headerName: "Бортовой номер",
|
||||
flex: 1,
|
||||
align: "center",
|
||||
headerAlign: "center",
|
||||
},
|
||||
{
|
||||
field: "type",
|
||||
headerName: "Тип",
|
||||
flex: 1,
|
||||
align: "center",
|
||||
headerAlign: "center",
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
return (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
{VEHICLE_TYPES.find((type) => type.value === params.row.type)
|
||||
?.label || params.row.type}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: "carrier",
|
||||
headerName: "Перевозчик",
|
||||
flex: 1,
|
||||
align: "center",
|
||||
headerAlign: "center",
|
||||
},
|
||||
{
|
||||
field: "city",
|
||||
headerName: "Город",
|
||||
flex: 1,
|
||||
align: "center",
|
||||
headerAlign: "center",
|
||||
},
|
||||
|
||||
{
|
||||
field: "actions",
|
||||
headerName: "Действия",
|
||||
flex: 1,
|
||||
align: "center",
|
||||
headerAlign: "center",
|
||||
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
return (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
<button onClick={() => navigate(`/vehicle/${params.row.id}`)}>
|
||||
<Eye size={20} className="text-green-500" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={20} className="text-red-500" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const rows = vehicles.map((vehicle) => ({
|
||||
id: vehicle.vehicle.id,
|
||||
tail_number: vehicle.vehicle.tail_number,
|
||||
type: vehicle.vehicle.type,
|
||||
carrier: vehicle.vehicle.carrier,
|
||||
city: carriers.find((carrier) => carrier.id === vehicle.vehicle.carrier_id)
|
||||
?.city,
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<LanguageSwitcher />
|
||||
|
||||
<div style={{ width: "100%" }}>
|
||||
<div className="flex justify-between items-center mb-10">
|
||||
<h1 className="text-2xl">Транспортные средства</h1>
|
||||
<CreateButton
|
||||
label="Создать транспортное средство"
|
||||
path="/vehicle/create"
|
||||
/>
|
||||
</div>
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
hideFooterPagination
|
||||
hideFooter
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DeleteModal
|
||||
open={isDeleteModalOpen}
|
||||
onDelete={async () => {
|
||||
if (rowId) {
|
||||
await deleteVehicle(rowId);
|
||||
}
|
||||
setIsDeleteModalOpen(false);
|
||||
setRowId(null);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setIsDeleteModalOpen(false);
|
||||
setRowId(null);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
70
src/pages/Vehicle/VehiclePreviewPage/index.tsx
Normal file
70
src/pages/Vehicle/VehiclePreviewPage/index.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import { Paper } from "@mui/material";
|
||||
import { vehicleStore, VEHICLE_TYPES } from "@shared";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
export const VehiclePreviewPage = observer(() => {
|
||||
const { id } = useParams();
|
||||
const { getVehicle, vehicle } = vehicleStore;
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
await getVehicle(Number(id));
|
||||
})();
|
||||
}, [id]);
|
||||
|
||||
return (
|
||||
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
||||
<div className="flex justify-between items-center">
|
||||
<button
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
Назад
|
||||
</button>
|
||||
{/* <div className="flex gap-2">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => navigate(`/vehicle/${id}/edit`)}
|
||||
startIcon={<Pencil size={20} />}
|
||||
>
|
||||
Редактировать
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={() => navigate(`/vehicle/${id}/delete`)}
|
||||
startIcon={<Trash2 size={20} />}
|
||||
>
|
||||
Удалить
|
||||
</Button>
|
||||
</div> */}
|
||||
</div>
|
||||
{vehicle && (
|
||||
<div className="flex flex-col gap-10 w-full">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="text-lg font-bold">Системный номер</h1>
|
||||
<p>{vehicle?.vehicle.tail_number}</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="text-lg font-bold">Тип транспортного средства</h1>
|
||||
<p>
|
||||
{VEHICLE_TYPES.find(
|
||||
(type) => type.value === vehicle?.vehicle.type
|
||||
)?.label || vehicle?.vehicle.type}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="text-lg font-bold">Перевозчик</h1>
|
||||
<p>{vehicle?.vehicle.carrier}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
});
|
3
src/pages/Vehicle/index.ts
Normal file
3
src/pages/Vehicle/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./VehicleListPage";
|
||||
export * from "./VehiclePreviewPage";
|
||||
export * from "./VehicleCreatePage";
|
@ -5,7 +5,15 @@ export * from "./DevicesPage";
|
||||
export * from "./SightPage";
|
||||
export * from "./CreateSightPage";
|
||||
export * from "./MapPage";
|
||||
export * from "./MediaListPage";
|
||||
export * from "./PreviewMediaPage";
|
||||
export * from "./EditMediaPage";
|
||||
export * from "./Media";
|
||||
// export * from "./CreateMediaPage";
|
||||
export * from "./Country";
|
||||
export * from "./City";
|
||||
export * from "./Route";
|
||||
export * from "./User";
|
||||
export * from "./Snapshot";
|
||||
export * from "./Carrier";
|
||||
export * from "./Station";
|
||||
export * from "./Vehicle";
|
||||
export * from "./Article";
|
||||
export * from "./Sight";
|
||||
|
@ -5,7 +5,12 @@ import {
|
||||
Building2,
|
||||
MonitorSmartphone,
|
||||
Map,
|
||||
BookImage,
|
||||
Users,
|
||||
Earth,
|
||||
Landmark,
|
||||
BusFront,
|
||||
Bus,
|
||||
GitBranch,
|
||||
} from "lucide-react";
|
||||
export const DRAWER_WIDTH = 300;
|
||||
|
||||
@ -22,12 +27,54 @@ export const NAVIGATION_ITEMS: {
|
||||
secondary: NavigationItem[];
|
||||
} = {
|
||||
primary: [
|
||||
{
|
||||
id: "countries",
|
||||
label: "Страны",
|
||||
icon: Earth,
|
||||
path: "/country",
|
||||
},
|
||||
{
|
||||
id: "cities",
|
||||
label: "Города",
|
||||
icon: Building2,
|
||||
path: "/city",
|
||||
},
|
||||
{
|
||||
id: "carriers",
|
||||
label: "Перевозчики",
|
||||
icon: BusFront,
|
||||
path: "/carrier",
|
||||
},
|
||||
// {
|
||||
// id: "media",
|
||||
// label: "Медиа",
|
||||
// icon: BookImage,
|
||||
// path: "/media",
|
||||
// },
|
||||
// {
|
||||
// id: "articles",
|
||||
// label: "Статьи",
|
||||
// icon: Newspaper,
|
||||
// path: "/article",
|
||||
// },
|
||||
{
|
||||
id: "attractions",
|
||||
label: "Достопримечательности",
|
||||
icon: Building2,
|
||||
icon: Landmark,
|
||||
path: "/sight",
|
||||
},
|
||||
// {
|
||||
// id: "stations",
|
||||
// label: "Остановки",
|
||||
// icon: PersonStanding,
|
||||
// path: "/station",
|
||||
// },
|
||||
{
|
||||
id: "snapshots",
|
||||
label: "Снапшоты",
|
||||
icon: GitBranch,
|
||||
path: "/snapshot",
|
||||
},
|
||||
{
|
||||
id: "map",
|
||||
label: "Карта",
|
||||
@ -41,10 +88,22 @@ export const NAVIGATION_ITEMS: {
|
||||
path: "/devices",
|
||||
},
|
||||
{
|
||||
id: "media",
|
||||
label: "Медиа",
|
||||
icon: BookImage,
|
||||
path: "/media",
|
||||
id: "vehicles",
|
||||
label: "Транспорт",
|
||||
icon: Bus,
|
||||
path: "/vehicle",
|
||||
},
|
||||
// {
|
||||
// id: "routes",
|
||||
// label: "Маршруты",
|
||||
// icon: Split,
|
||||
// path: "/route",
|
||||
// },
|
||||
{
|
||||
id: "users",
|
||||
label: "Пользователи",
|
||||
icon: Users,
|
||||
path: "/user",
|
||||
},
|
||||
// {
|
||||
// id: "articles",
|
||||
@ -65,3 +124,8 @@ export const NAVIGATION_ITEMS: {
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const VEHICLE_TYPES = [
|
||||
{ label: "Трамвай", value: 1 },
|
||||
{ label: "Троллейбус", value: 2 },
|
||||
];
|
||||
|
@ -25,10 +25,19 @@ class ArticlesStore {
|
||||
en: [],
|
||||
zh: [],
|
||||
};
|
||||
articleList: Article[] = [];
|
||||
articleData: Article | null = null;
|
||||
articleMedia: Media | null = null;
|
||||
articleLoading: boolean = false;
|
||||
|
||||
getArticleList = async () => {
|
||||
const response = await authInstance.get("/article");
|
||||
|
||||
runInAction(() => {
|
||||
this.articleList = response.data;
|
||||
});
|
||||
};
|
||||
|
||||
getArticles = async (language: Language) => {
|
||||
this.articleLoading = true;
|
||||
const response = await authInstance.get("/article");
|
||||
|
76
src/shared/store/CarrierStore/index.tsx
Normal file
76
src/shared/store/CarrierStore/index.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import { authInstance } from "@shared";
|
||||
import { makeAutoObservable, runInAction } from "mobx";
|
||||
|
||||
export type Carrier = {
|
||||
id: number;
|
||||
short_name: string;
|
||||
full_name: string;
|
||||
slogan: string;
|
||||
city: string;
|
||||
city_id: number;
|
||||
logo: string;
|
||||
main_color: string;
|
||||
left_color: string;
|
||||
right_color: string;
|
||||
};
|
||||
|
||||
class CarrierStore {
|
||||
carriers: Carrier[] = [];
|
||||
carrier: Carrier | null = null;
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
getCarriers = async () => {
|
||||
const response = await authInstance.get("/carrier");
|
||||
|
||||
runInAction(() => {
|
||||
this.carriers = response.data;
|
||||
});
|
||||
};
|
||||
|
||||
deleteCarrier = async (id: number) => {
|
||||
await authInstance.delete(`/carrier/${id}`);
|
||||
|
||||
runInAction(() => {
|
||||
this.carriers = this.carriers.filter((carrier) => carrier.id !== id);
|
||||
});
|
||||
};
|
||||
|
||||
getCarrier = async (id: number) => {
|
||||
const response = await authInstance.get(`/carrier/${id}`);
|
||||
runInAction(() => {
|
||||
this.carrier = response.data;
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
createCarrier = async (
|
||||
fullName: string,
|
||||
shortName: string,
|
||||
city: string,
|
||||
cityId: number,
|
||||
primaryColor: string,
|
||||
secondaryColor: string,
|
||||
accentColor: string,
|
||||
slogan: string,
|
||||
logoId: string
|
||||
) => {
|
||||
const response = await authInstance.post("/carrier", {
|
||||
full_name: fullName,
|
||||
short_name: shortName,
|
||||
city,
|
||||
city_id: cityId,
|
||||
primary_color: primaryColor,
|
||||
secondary_color: secondaryColor,
|
||||
accent_color: accentColor,
|
||||
slogan,
|
||||
logo: logoId,
|
||||
});
|
||||
runInAction(() => {
|
||||
this.carriers.push(response.data);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const carrierStore = new CarrierStore();
|
@ -11,19 +11,53 @@ type City = {
|
||||
|
||||
class CityStore {
|
||||
cities: City[] = [];
|
||||
|
||||
city: City | null = null;
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
getCities = async () => {
|
||||
if (this.cities.length !== 0) return;
|
||||
|
||||
const response = await authInstance.get("/city");
|
||||
|
||||
runInAction(() => {
|
||||
this.cities = response.data;
|
||||
});
|
||||
};
|
||||
|
||||
deleteCity = async (id: number) => {
|
||||
await authInstance.delete(`/city/${id}`);
|
||||
|
||||
runInAction(() => {
|
||||
this.cities = this.cities.filter((city) => city.id !== id);
|
||||
});
|
||||
};
|
||||
|
||||
getCity = async (id: string) => {
|
||||
const response = await authInstance.get(`/city/${id}`);
|
||||
|
||||
runInAction(() => {
|
||||
this.city = response.data;
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
createCity = async (
|
||||
name: string,
|
||||
country: string,
|
||||
countryCode: string,
|
||||
mediaId: string
|
||||
) => {
|
||||
const response = await authInstance.post("/city", {
|
||||
name: name,
|
||||
country: country,
|
||||
country_code: countryCode,
|
||||
arms: mediaId,
|
||||
});
|
||||
runInAction(() => {
|
||||
this.cities.push(response.data);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const cityStore = new CityStore();
|
||||
|
49
src/shared/store/CountryStore/index.ts
Normal file
49
src/shared/store/CountryStore/index.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { authInstance } from "@shared";
|
||||
import { makeAutoObservable, runInAction } from "mobx";
|
||||
|
||||
export type Country = {
|
||||
code: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
class CountryStore {
|
||||
countries: Country[] = [];
|
||||
country: Country | null = null;
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
getCountries = async () => {
|
||||
const response = await authInstance.get("/country");
|
||||
|
||||
runInAction(() => {
|
||||
this.countries = response.data;
|
||||
});
|
||||
};
|
||||
|
||||
getCountry = async (code: string) => {
|
||||
const response = await authInstance.get(`/country/${code}`);
|
||||
|
||||
runInAction(() => {
|
||||
this.country = response.data;
|
||||
});
|
||||
};
|
||||
|
||||
deleteCountry = async (code: string) => {
|
||||
await authInstance.delete(`/country/${code}`);
|
||||
|
||||
runInAction(() => {
|
||||
this.countries = this.countries.filter(
|
||||
(country) => country.code !== code
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
createCountry = async (code: string, name: string) => {
|
||||
await authInstance.post("/country", { code: code, name: name });
|
||||
await this.getCountries();
|
||||
};
|
||||
}
|
||||
|
||||
export const countryStore = new CountryStore();
|
@ -76,6 +76,16 @@ class MediaStore {
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
createMedia = async (name: string, type: string) => {
|
||||
const response = await authInstance.post("/media", {
|
||||
media_name: name,
|
||||
media_type: type,
|
||||
});
|
||||
runInAction(() => {
|
||||
this.media.push(response.data);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const mediaStore = new MediaStore();
|
||||
|
45
src/shared/store/RouteStore/index.ts
Normal file
45
src/shared/store/RouteStore/index.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { makeAutoObservable, runInAction } from "mobx";
|
||||
import { authInstance } from "@shared";
|
||||
|
||||
export type Route = {
|
||||
carrier: string;
|
||||
carrier_id: number;
|
||||
center_latitude: number;
|
||||
center_longitude: number;
|
||||
governor_appeal: number;
|
||||
id: number;
|
||||
path: number[][];
|
||||
rotate: number;
|
||||
route_direction: boolean;
|
||||
route_number: string;
|
||||
route_sys_number: string;
|
||||
scale_max: number;
|
||||
scale_min: number;
|
||||
video_preview: string;
|
||||
};
|
||||
|
||||
class RouteStore {
|
||||
routes: Route[] = [];
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
getRoutes = async () => {
|
||||
const response = await authInstance.get("/route");
|
||||
|
||||
runInAction(() => {
|
||||
this.routes = response.data;
|
||||
});
|
||||
};
|
||||
|
||||
deleteRoute = async (id: number) => {
|
||||
await authInstance.delete(`/route/${id}`);
|
||||
|
||||
runInAction(() => {
|
||||
this.routes = this.routes.filter((route) => route.id !== id);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const routeStore = new RouteStore();
|
@ -219,6 +219,11 @@ class SightsStore {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
deleteListSight = async (id: number) => {
|
||||
await authInstance.delete(`/sight/${id}`);
|
||||
this.sights = this.sights.filter((sight) => sight.id !== id);
|
||||
};
|
||||
}
|
||||
|
||||
export const sightsStore = new SightsStore();
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { authInstance } from "@shared";
|
||||
import { API_URL } from "@shared";
|
||||
import { makeAutoObservable } from "mobx";
|
||||
|
||||
import { makeAutoObservable, runInAction } from "mobx";
|
||||
|
||||
type Snapshot = {
|
||||
ID: string;
|
||||
@ -11,14 +11,42 @@ type Snapshot = {
|
||||
|
||||
class SnapshotStore {
|
||||
snapshots: Snapshot[] = [];
|
||||
snapshot: Snapshot | null = null;
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
getSnapshots = async () => {
|
||||
const response = await authInstance.get(`${API_URL}/snapshots`);
|
||||
this.snapshots = response.data;
|
||||
const response = await authInstance.get(`/snapshots`);
|
||||
|
||||
runInAction(() => {
|
||||
this.snapshots = response.data;
|
||||
});
|
||||
};
|
||||
|
||||
deleteSnapshot = async (id: string) => {
|
||||
await authInstance.delete(`/snapshots/${id}`);
|
||||
|
||||
runInAction(() => {
|
||||
this.snapshots = this.snapshots.filter((snapshot) => snapshot.ID !== id);
|
||||
});
|
||||
};
|
||||
|
||||
getSnapshot = async (id: string) => {
|
||||
const response = await authInstance.get(`/snapshots/${id}`);
|
||||
|
||||
runInAction(() => {
|
||||
this.snapshot = response.data;
|
||||
});
|
||||
};
|
||||
|
||||
restoreSnapshot = async (id: string) => {
|
||||
await authInstance.post(`/snapshots/${id}/restore`);
|
||||
};
|
||||
|
||||
createSnapshot = async (name: string) => {
|
||||
await authInstance.post(`/snapshots`, { name });
|
||||
};
|
||||
}
|
||||
|
||||
|
76
src/shared/store/StationsStore/index.ts
Normal file
76
src/shared/store/StationsStore/index.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { authInstance } from "@shared";
|
||||
import { makeAutoObservable, runInAction } from "mobx";
|
||||
|
||||
type Station = {
|
||||
id: number;
|
||||
address: string;
|
||||
city: string;
|
||||
city_id: number;
|
||||
description: string;
|
||||
direction: boolean;
|
||||
icon: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
name: string;
|
||||
offset_x: number;
|
||||
offset_y: number;
|
||||
system_name: string;
|
||||
transfers: {
|
||||
bus: string;
|
||||
metro_blue: string;
|
||||
metro_green: string;
|
||||
metro_orange: string;
|
||||
metro_purple: string;
|
||||
metro_red: string;
|
||||
train: string;
|
||||
tram: string;
|
||||
trolleybus: string;
|
||||
};
|
||||
};
|
||||
|
||||
class StationsStore {
|
||||
stations: Station[] = [];
|
||||
station: Station | null = null;
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
getStations = async () => {
|
||||
const response = await authInstance.get("/station");
|
||||
|
||||
runInAction(() => {
|
||||
this.stations = response.data;
|
||||
});
|
||||
};
|
||||
|
||||
deleteStation = async (id: number) => {
|
||||
await authInstance.delete(`/station/${id}`);
|
||||
|
||||
runInAction(() => {
|
||||
this.stations = this.stations.filter((station) => station.id !== id);
|
||||
});
|
||||
};
|
||||
|
||||
getStation = async (id: number) => {
|
||||
const response = await authInstance.get(`/station/${id}`);
|
||||
this.station = response.data;
|
||||
};
|
||||
|
||||
createStation = async (
|
||||
name: string,
|
||||
systemName: string,
|
||||
direction: string
|
||||
) => {
|
||||
const response = await authInstance.post("/station", {
|
||||
station_name: name,
|
||||
system_name: systemName,
|
||||
direction,
|
||||
});
|
||||
runInAction(() => {
|
||||
this.stations.push(response.data);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const stationsStore = new StationsStore();
|
44
src/shared/store/UserStore/index.ts
Normal file
44
src/shared/store/UserStore/index.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { authInstance } from "@shared";
|
||||
import { makeAutoObservable, runInAction } from "mobx";
|
||||
|
||||
export type User = {
|
||||
id: number;
|
||||
email: string;
|
||||
is_admin: boolean;
|
||||
name: string;
|
||||
};
|
||||
|
||||
class UserStore {
|
||||
users: User[] = [];
|
||||
user: User | null = null;
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
getUsers = async () => {
|
||||
const response = await authInstance.get("/user");
|
||||
|
||||
runInAction(() => {
|
||||
this.users = response.data;
|
||||
});
|
||||
};
|
||||
|
||||
getUser = async (id: number) => {
|
||||
const response = await authInstance.get(`/user/${id}`);
|
||||
|
||||
runInAction(() => {
|
||||
this.user = response.data as User;
|
||||
});
|
||||
};
|
||||
|
||||
deleteUser = async (id: number) => {
|
||||
await authInstance.delete(`/users/${id}`);
|
||||
|
||||
runInAction(() => {
|
||||
this.users = this.users.filter((user) => user.id !== id);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const userStore = new UserStore();
|
@ -1,4 +1,4 @@
|
||||
import { API_URL, authInstance } from "@shared";
|
||||
import { authInstance } from "@shared";
|
||||
import { makeAutoObservable } from "mobx";
|
||||
|
||||
export type Vehicle = {
|
||||
@ -22,15 +22,43 @@ export type Vehicle = {
|
||||
|
||||
class VehicleStore {
|
||||
vehicles: Vehicle[] = [];
|
||||
vehicle: Vehicle | null = null;
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
getVehicles = async () => {
|
||||
const response = await authInstance.get(`${API_URL}/vehicle`);
|
||||
const response = await authInstance.get(`/vehicle`);
|
||||
this.vehicles = response.data;
|
||||
};
|
||||
|
||||
deleteVehicle = async (id: number) => {
|
||||
await authInstance.delete(`/vehicle/${id}`);
|
||||
this.vehicles = this.vehicles.filter(
|
||||
(vehicle) => vehicle.vehicle.id !== id
|
||||
);
|
||||
};
|
||||
|
||||
getVehicle = async (id: number) => {
|
||||
const response = await authInstance.get(`/vehicle/${id}`);
|
||||
this.vehicle = response.data;
|
||||
};
|
||||
|
||||
createVehicle = async (
|
||||
tailNumber: number,
|
||||
type: string,
|
||||
carrier: string,
|
||||
carrierId: number
|
||||
) => {
|
||||
await authInstance.post("/vehicle", {
|
||||
tail_number: tailNumber,
|
||||
type,
|
||||
carrier,
|
||||
carrier_id: carrierId,
|
||||
});
|
||||
await this.getVehicles();
|
||||
};
|
||||
}
|
||||
|
||||
export const vehicleStore = new VehicleStore();
|
||||
|
@ -9,3 +9,8 @@ export * from "./ArticlesStore";
|
||||
export * from "./EditSightStore";
|
||||
export * from "./MediaStore";
|
||||
export * from "./CreateSightStore";
|
||||
export * from "./CountryStore";
|
||||
export * from "./RouteStore";
|
||||
export * from "./UserStore";
|
||||
export * from "./CarrierStore";
|
||||
export * from "./StationsStore";
|
||||
|
25
src/widgets/CreateButton/index.tsx
Normal file
25
src/widgets/CreateButton/index.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import { Button } from "@mui/material";
|
||||
import { Plus } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
interface CreateButtonProps {
|
||||
label: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export const CreateButton = ({ label, path }: CreateButtonProps) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
navigate(path);
|
||||
}}
|
||||
startIcon={<Plus size={20} />}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
);
|
||||
};
|
@ -9,7 +9,6 @@ type Language = (typeof LANGUAGES)[number];
|
||||
export const LanguageSwitcher = observer(() => {
|
||||
const { language, setLanguage } = languageStore;
|
||||
|
||||
// Memoize getLanguageLabel for consistent rendering
|
||||
const getLanguageLabel = useCallback((lang: Language) => {
|
||||
switch (lang) {
|
||||
case "ru":
|
||||
@ -45,7 +44,7 @@ export const LanguageSwitcher = observer(() => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed top-1/2 -translate-y-1/2 right-0 flex flex-col gap-2 p-4 z-10 ">
|
||||
<div className="fixed bottom-0 left-1/2 -translate-x-1/2 flex gap-2 p-4 z-10 ">
|
||||
{/* Added some styling for better visualization */}
|
||||
{LANGUAGES.map((lang) => (
|
||||
<Button
|
||||
|
@ -21,7 +21,6 @@ export function MediaViewer({
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexGrow: 1,
|
||||
justifyContent: "center",
|
||||
}}
|
||||
className={className}
|
||||
>
|
||||
@ -33,8 +32,8 @@ export function MediaViewer({
|
||||
alt={media?.filename}
|
||||
style={{
|
||||
width: "100%",
|
||||
|
||||
objectFit: "cover",
|
||||
height: "100%",
|
||||
objectFit: "contain",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@ -47,7 +46,7 @@ export function MediaViewer({
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
objectFit: "contain",
|
||||
borderRadius: 8,
|
||||
}}
|
||||
controls
|
||||
@ -64,7 +63,7 @@ export function MediaViewer({
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
objectFit: "contain",
|
||||
borderRadius: 8,
|
||||
}}
|
||||
/>
|
||||
@ -78,7 +77,7 @@ export function MediaViewer({
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
objectFit: "contain",
|
||||
borderRadius: 8,
|
||||
}}
|
||||
/>
|
||||
|
34
src/widgets/SnapshotRestore/index.tsx
Normal file
34
src/widgets/SnapshotRestore/index.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { Button } from "@mui/material";
|
||||
|
||||
export const SnapshotRestore = ({
|
||||
onDelete,
|
||||
onCancel,
|
||||
open,
|
||||
}: {
|
||||
onDelete: () => void;
|
||||
onCancel: () => void;
|
||||
open: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={`fixed top-0 left-0 w-screen h-screen flex justify-center items-center z-10000 bg-black/30 transition-all duration-300 ${
|
||||
open ? "block" : "hidden"
|
||||
}`}
|
||||
>
|
||||
<div className="bg-white p-4 w-140 rounded-lg flex flex-col gap-4 items-center">
|
||||
<p className="text-black w-110 text-center">
|
||||
Вы уверены, что хотите восстановить этот снапшот?
|
||||
</p>
|
||||
<p className="text-black w-100 text-center">
|
||||
Это действие нельзя будет отменить.
|
||||
</p>
|
||||
<div className="flex gap-4 justify-center">
|
||||
<Button variant="contained" color="primary" onClick={onDelete}>
|
||||
Да
|
||||
</Button>
|
||||
<Button onClick={onCancel}>Нет</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -14,3 +14,5 @@ export * from "./MediaAreaForSight";
|
||||
export * from "./ImageUploadCard";
|
||||
export * from "./LeaveAgree";
|
||||
export * from "./DeleteModal";
|
||||
export * from "./SnapshotRestore";
|
||||
export * from "./CreateButton";
|
||||
|
Reference in New Issue
Block a user