Compare commits
8 Commits
0fe4683683
...
d8302e05b4
Author | SHA1 | Date | |
---|---|---|---|
d8302e05b4 | |||
e2e750877a | |||
2ca1f2cba4 | |||
64c15b2622 | |||
f4544c1888 | |||
02a1d2ea74 | |||
b09c1b3214 | |||
e37f9e14bc |
77
package-lock.json
generated
77
package-lock.json
generated
@ -11,6 +11,7 @@
|
|||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.0",
|
"@emotion/styled": "^11.14.0",
|
||||||
"@hello-pangea/dnd": "^18.0.1",
|
"@hello-pangea/dnd": "^18.0.1",
|
||||||
|
"@mui/icons-material": "^7.1.1",
|
||||||
"@mui/material": "^7.1.0",
|
"@mui/material": "^7.1.0",
|
||||||
"@mui/x-data-grid": "^8.5.1",
|
"@mui/x-data-grid": "^8.5.1",
|
||||||
"@photo-sphere-viewer/core": "^5.13.2",
|
"@photo-sphere-viewer/core": "^5.13.2",
|
||||||
@ -838,26 +839,52 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/core-downloads-tracker": {
|
"node_modules/@mui/core-downloads-tracker": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.1.1.tgz",
|
||||||
"integrity": "sha512-E0OqhZv548Qdc0PwWhLVA2zmjJZSTvaL4ZhoswmI8NJEC1tpW2js6LLP827jrW9MEiXYdz3QS6+hask83w74yQ==",
|
"integrity": "sha512-yBckQs4aQ8mqukLnPC6ivIRv6guhaXi8snVl00VtyojBbm+l6VbVhyTSZ68Abcx7Ah8B+GZhrB7BOli+e+9LkQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
"url": "https://opencollective.com/mui-org"
|
"url": "https://opencollective.com/mui-org"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@mui/icons-material": {
|
||||||
|
"version": "7.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.1.1.tgz",
|
||||||
|
"integrity": "sha512-X37+Yc8QpEnl0sYmz+WcLFy2dWgNRzbswDzLPXG7QU1XDVlP5TPp1HXjdmCupOWLL/I9m1fyhcyZl8/HPpp/Cg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.27.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/mui-org"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@mui/material": "^7.1.1",
|
||||||
|
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@mui/material": {
|
"node_modules/@mui/material": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/material/-/material-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/material/-/material-7.1.1.tgz",
|
||||||
"integrity": "sha512-ahUJdrhEv+mCp4XHW+tHIEYzZMSRLg8z4AjUOsj44QpD1ZaMxQoVOG2xiHvLFdcsIPbgSRx1bg1eQSheHBgvtg==",
|
"integrity": "sha512-mTpdmdZCaHCGOH3SrYM41+XKvNL0iQfM9KlYgpSjgadXx/fEKhhvOktxm8++Xw6FFeOHoOiV+lzOI8X1rsv71A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.27.1",
|
"@babel/runtime": "^7.27.1",
|
||||||
"@mui/core-downloads-tracker": "^7.1.0",
|
"@mui/core-downloads-tracker": "^7.1.1",
|
||||||
"@mui/system": "^7.1.0",
|
"@mui/system": "^7.1.1",
|
||||||
"@mui/types": "^7.4.2",
|
"@mui/types": "^7.4.3",
|
||||||
"@mui/utils": "^7.1.0",
|
"@mui/utils": "^7.1.1",
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
"@types/react-transition-group": "^4.4.12",
|
"@types/react-transition-group": "^4.4.12",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@ -876,7 +903,7 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@emotion/react": "^11.5.0",
|
"@emotion/react": "^11.5.0",
|
||||||
"@emotion/styled": "^11.3.0",
|
"@emotion/styled": "^11.3.0",
|
||||||
"@mui/material-pigment-css": "^7.1.0",
|
"@mui/material-pigment-css": "^7.1.1",
|
||||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
@ -903,13 +930,13 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@mui/private-theming": {
|
"node_modules/@mui/private-theming": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.1.1.tgz",
|
||||||
"integrity": "sha512-4Kck4jxhqF6YxNwJdSae1WgDfXVg0lIH6JVJ7gtuFfuKcQCgomJxPvUEOySTFRPz1IZzwz5OAcToskRdffElDA==",
|
"integrity": "sha512-M8NbLUx+armk2ZuaxBkkMk11ultnWmrPlN0Xe3jUEaBChg/mcxa5HWIWS1EE4DF36WRACaAHVAvyekWlDQf0PQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.27.1",
|
"@babel/runtime": "^7.27.1",
|
||||||
"@mui/utils": "^7.1.0",
|
"@mui/utils": "^7.1.1",
|
||||||
"prop-types": "^15.8.1"
|
"prop-types": "^15.8.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -930,9 +957,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/styled-engine": {
|
"node_modules/@mui/styled-engine": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.1.1.tgz",
|
||||||
"integrity": "sha512-m0mJ0c6iRC+f9hMeRe0W7zZX1wme3oUX0+XTVHjPG7DJz6OdQ6K/ggEOq7ZdwilcpdsDUwwMfOmvO71qDkYd2w==",
|
"integrity": "sha512-R2wpzmSN127j26HrCPYVQ53vvMcT5DaKLoWkrfwUYq3cYytL6TQrCH8JBH3z79B6g4nMZZVoaXrxO757AlShaw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.27.1",
|
"@babel/runtime": "^7.27.1",
|
||||||
@ -964,16 +991,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/system": {
|
"node_modules/@mui/system": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/system/-/system-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/system/-/system-7.1.1.tgz",
|
||||||
"integrity": "sha512-iedAWgRJMCxeMHvkEhsDlbvkK+qKf9me6ofsf7twk/jfT4P1ImVf7Rwb5VubEA0sikrVL+1SkoZM41M4+LNAVA==",
|
"integrity": "sha512-Kj1uhiqnj4Zo7PDjAOghtXJtNABunWvhcRU0O7RQJ7WOxeynoH6wXPcilphV8QTFtkKaip8EiNJRiCD+B3eROA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.27.1",
|
"@babel/runtime": "^7.27.1",
|
||||||
"@mui/private-theming": "^7.1.0",
|
"@mui/private-theming": "^7.1.1",
|
||||||
"@mui/styled-engine": "^7.1.0",
|
"@mui/styled-engine": "^7.1.1",
|
||||||
"@mui/types": "^7.4.2",
|
"@mui/types": "^7.4.3",
|
||||||
"@mui/utils": "^7.1.0",
|
"@mui/utils": "^7.1.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"csstype": "^3.1.3",
|
"csstype": "^3.1.3",
|
||||||
"prop-types": "^15.8.1"
|
"prop-types": "^15.8.1"
|
||||||
|
@ -13,9 +13,11 @@
|
|||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.0",
|
"@emotion/styled": "^11.14.0",
|
||||||
"@hello-pangea/dnd": "^18.0.1",
|
"@hello-pangea/dnd": "^18.0.1",
|
||||||
|
"@mui/icons-material": "^7.1.1",
|
||||||
"@mui/material": "^7.1.0",
|
"@mui/material": "^7.1.0",
|
||||||
"@mui/x-data-grid": "^8.5.1",
|
"@mui/x-data-grid": "^8.5.1",
|
||||||
"@photo-sphere-viewer/core": "^5.13.2",
|
"@photo-sphere-viewer/core": "^5.13.2",
|
||||||
|
"@pixi/react": "^8.0.2",
|
||||||
"@react-three/drei": "^10.1.2",
|
"@react-three/drei": "^10.1.2",
|
||||||
"@react-three/fiber": "^9.1.2",
|
"@react-three/fiber": "^9.1.2",
|
||||||
"@tailwindcss/vite": "^4.1.8",
|
"@tailwindcss/vite": "^4.1.8",
|
||||||
@ -26,6 +28,7 @@
|
|||||||
"mobx-react-lite": "^4.1.0",
|
"mobx-react-lite": "^4.1.0",
|
||||||
"ol": "^10.5.0",
|
"ol": "^10.5.0",
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
|
"pixi.js": "^8.10.1",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-colorful": "^5.6.1",
|
"react-colorful": "^5.6.1",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
|
373
public/Emblem.svg
Normal file
373
public/Emblem.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 176 KiB |
BIN
public/GET.png
Normal file
BIN
public/GET.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.4 KiB |
BIN
public/SightIcon.png
Normal file
BIN
public/SightIcon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 750 B |
BIN
public/favicon_ship.png
Normal file
BIN
public/favicon_ship.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.3 KiB |
@ -25,8 +25,20 @@ import {
|
|||||||
SnapshotCreatePage,
|
SnapshotCreatePage,
|
||||||
CountryCreatePage,
|
CountryCreatePage,
|
||||||
CityCreatePage,
|
CityCreatePage,
|
||||||
// CarrierCreatePage,
|
CarrierCreatePage,
|
||||||
VehicleCreatePage,
|
VehicleCreatePage,
|
||||||
|
CountryEditPage,
|
||||||
|
CityEditPage,
|
||||||
|
UserCreatePage,
|
||||||
|
UserEditPage,
|
||||||
|
VehicleEditPage,
|
||||||
|
CarrierEditPage,
|
||||||
|
StationCreatePage,
|
||||||
|
StationPreviewPage,
|
||||||
|
StationEditPage,
|
||||||
|
RouteCreatePage,
|
||||||
|
RoutePreview,
|
||||||
|
RouteEditPage,
|
||||||
} from "@pages";
|
} from "@pages";
|
||||||
import { authStore, createSightStore, editSightStore } from "@shared";
|
import { authStore, createSightStore, editSightStore } from "@shared";
|
||||||
import { Layout } from "@widgets";
|
import { Layout } from "@widgets";
|
||||||
@ -87,6 +99,7 @@ const router = createBrowserRouter([
|
|||||||
</PublicRoute>
|
</PublicRoute>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{ path: "route-preview/:id", element: <RoutePreview /> },
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
element: (
|
element: (
|
||||||
@ -104,7 +117,7 @@ const router = createBrowserRouter([
|
|||||||
// Sight
|
// Sight
|
||||||
{ path: "sight", element: <SightListPage /> },
|
{ path: "sight", element: <SightListPage /> },
|
||||||
{ path: "sight/create", element: <CreateSightPage /> },
|
{ path: "sight/create", element: <CreateSightPage /> },
|
||||||
{ path: "sight/:id", element: <EditSightPage /> },
|
{ path: "sight/:id/edit", element: <EditSightPage /> },
|
||||||
|
|
||||||
// Device
|
// Device
|
||||||
{ path: "devices", element: <DevicesPage /> },
|
{ path: "devices", element: <DevicesPage /> },
|
||||||
@ -121,36 +134,43 @@ const router = createBrowserRouter([
|
|||||||
{ path: "country", element: <CountryListPage /> },
|
{ path: "country", element: <CountryListPage /> },
|
||||||
{ path: "country/create", element: <CountryCreatePage /> },
|
{ path: "country/create", element: <CountryCreatePage /> },
|
||||||
{ path: "country/:id", element: <CountryPreviewPage /> },
|
{ path: "country/:id", element: <CountryPreviewPage /> },
|
||||||
|
{ path: "country/:id/edit", element: <CountryEditPage /> },
|
||||||
// City
|
// City
|
||||||
{ path: "city", element: <CityListPage /> },
|
{ path: "city", element: <CityListPage /> },
|
||||||
{ path: "city/create", element: <CityCreatePage /> },
|
{ path: "city/create", element: <CityCreatePage /> },
|
||||||
{ path: "city/:id", element: <CityPreviewPage /> },
|
{ path: "city/:id", element: <CityPreviewPage /> },
|
||||||
|
{ path: "city/:id/edit", element: <CityEditPage /> },
|
||||||
// Route
|
// Route
|
||||||
{ path: "route", element: <RouteListPage /> },
|
{ path: "route", element: <RouteListPage /> },
|
||||||
|
{ path: "route/create", element: <RouteCreatePage /> },
|
||||||
|
{ path: "route/:id/edit", element: <RouteEditPage /> },
|
||||||
|
|
||||||
// User
|
// User
|
||||||
{ path: "user", element: <UserListPage /> },
|
{ path: "user", element: <UserListPage /> },
|
||||||
|
{ path: "user/create", element: <UserCreatePage /> },
|
||||||
|
{ path: "user/:id/edit", element: <UserEditPage /> },
|
||||||
// Snapshot
|
// Snapshot
|
||||||
{ path: "snapshot", element: <SnapshotListPage /> },
|
{ path: "snapshot", element: <SnapshotListPage /> },
|
||||||
{ path: "snapshot/create", element: <SnapshotCreatePage /> },
|
{ path: "snapshot/create", element: <SnapshotCreatePage /> },
|
||||||
|
|
||||||
// Carrier
|
// Carrier
|
||||||
{ path: "carrier", element: <CarrierListPage /> },
|
{ path: "carrier", element: <CarrierListPage /> },
|
||||||
// { path: "carrier/create", element: <CarrierCreatePage /> },
|
{ path: "carrier/create", element: <CarrierCreatePage /> },
|
||||||
{ path: "carrier/:id", element: <CarrierPreviewPage /> },
|
{ path: "carrier/:id", element: <CarrierPreviewPage /> },
|
||||||
|
{ path: "carrier/:id/edit", element: <CarrierEditPage /> },
|
||||||
// Station
|
// Station
|
||||||
{ path: "station", element: <StationListPage /> },
|
{ path: "station", element: <StationListPage /> },
|
||||||
|
{ path: "station/create", element: <StationCreatePage /> },
|
||||||
|
{ path: "station/:id", element: <StationPreviewPage /> },
|
||||||
|
{ path: "station/:id/edit", element: <StationEditPage /> },
|
||||||
// Vehicle
|
// Vehicle
|
||||||
{ path: "vehicle", element: <VehicleListPage /> },
|
{ path: "vehicle", element: <VehicleListPage /> },
|
||||||
{ path: "vehicle/create", element: <VehicleCreatePage /> },
|
{ path: "vehicle/create", element: <VehicleCreatePage /> },
|
||||||
{ path: "vehicle/:id", element: <VehiclePreviewPage /> },
|
{ path: "vehicle/:id", element: <VehiclePreviewPage /> },
|
||||||
|
{ path: "vehicle/:id/edit", element: <VehicleEditPage /> },
|
||||||
// Article
|
// Article
|
||||||
{ path: "article", element: <ArticleListPage /> },
|
{ path: "article", element: <ArticleListPage /> },
|
||||||
|
// { path: "article/:id", element: <ArticlePreviewPage /> },
|
||||||
// { path: "media/create", element: <CreateMediaPage /> },
|
// { path: "media/create", element: <CreateMediaPage /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -4,7 +4,9 @@ export interface NavigationItem {
|
|||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
icon: LucideIcon;
|
icon: LucideIcon;
|
||||||
path: string;
|
path?: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
nestedItems?: NavigationItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NavigationSection = "primary" | "secondary";
|
export type NavigationSection = "primary" | "secondary";
|
||||||
|
@ -3,80 +3,125 @@ import ListItem from "@mui/material/ListItem";
|
|||||||
import ListItemButton from "@mui/material/ListItemButton";
|
import ListItemButton from "@mui/material/ListItemButton";
|
||||||
import ListItemIcon from "@mui/material/ListItemIcon";
|
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||||
import ListItemText from "@mui/material/ListItemText";
|
import ListItemText from "@mui/material/ListItemText";
|
||||||
|
import Collapse from "@mui/material/Collapse";
|
||||||
|
import List from "@mui/material/List";
|
||||||
|
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
|
||||||
|
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||||
import type { NavigationItem } from "../model";
|
import type { NavigationItem } from "../model";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate, useLocation } from "react-router-dom";
|
||||||
|
|
||||||
interface NavigationItemProps {
|
interface NavigationItemProps {
|
||||||
item: NavigationItem;
|
item: NavigationItem;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
|
isNested?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NavigationItemComponent: React.FC<NavigationItemProps> = ({
|
export const NavigationItemComponent: React.FC<NavigationItemProps> = ({
|
||||||
item,
|
item,
|
||||||
open,
|
open,
|
||||||
onClick,
|
onClick,
|
||||||
|
isNested = false,
|
||||||
}) => {
|
}) => {
|
||||||
const Icon = item.icon;
|
const Icon = item.icon;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
const [isExpanded, setIsExpanded] = React.useState(false);
|
||||||
|
|
||||||
|
const isActive = item.path ? location.pathname.startsWith(item.path) : false;
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
if (item.nestedItems) {
|
||||||
|
setIsExpanded(!isExpanded);
|
||||||
|
} else if (onClick) {
|
||||||
|
onClick();
|
||||||
|
} else if (item.path) {
|
||||||
|
navigate(item.path);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem
|
<>
|
||||||
onClick={() => {
|
<ListItem disablePadding sx={{ display: "block" }}>
|
||||||
if (onClick) {
|
<ListItemButton
|
||||||
onClick();
|
onClick={handleClick}
|
||||||
} else {
|
|
||||||
navigate(item.path);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disablePadding
|
|
||||||
sx={{ display: "block" }}
|
|
||||||
>
|
|
||||||
<ListItemButton
|
|
||||||
sx={[
|
|
||||||
{
|
|
||||||
minHeight: 48,
|
|
||||||
px: 2.5,
|
|
||||||
},
|
|
||||||
open
|
|
||||||
? {
|
|
||||||
justifyContent: "initial",
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
justifyContent: "center",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<ListItemIcon
|
|
||||||
sx={[
|
sx={[
|
||||||
{
|
{
|
||||||
minWidth: 0,
|
minHeight: 48,
|
||||||
justifyContent: "center",
|
px: 2.5,
|
||||||
},
|
},
|
||||||
open
|
open
|
||||||
? {
|
? {
|
||||||
mr: 3,
|
justifyContent: "initial",
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
mr: "auto",
|
justifyContent: "center",
|
||||||
},
|
},
|
||||||
|
isNested && {
|
||||||
|
pl: 4,
|
||||||
|
},
|
||||||
|
isActive && {
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.08)",
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.12)",
|
||||||
|
},
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Icon />
|
<ListItemIcon
|
||||||
</ListItemIcon>
|
sx={[
|
||||||
<ListItemText
|
{
|
||||||
primary={item.label}
|
minWidth: 0,
|
||||||
sx={[
|
justifyContent: "center",
|
||||||
open
|
color: isActive ? "primary.main" : "inherit",
|
||||||
? {
|
},
|
||||||
opacity: 1,
|
open
|
||||||
}
|
? {
|
||||||
: {
|
mr: 3,
|
||||||
opacity: 0,
|
}
|
||||||
},
|
: {
|
||||||
]}
|
mr: "auto",
|
||||||
/>
|
},
|
||||||
</ListItemButton>
|
]}
|
||||||
</ListItem>
|
>
|
||||||
|
<Icon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText
|
||||||
|
primary={item.label}
|
||||||
|
sx={[
|
||||||
|
open
|
||||||
|
? {
|
||||||
|
opacity: 1,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
opacity: 0,
|
||||||
|
},
|
||||||
|
isActive && {
|
||||||
|
color: "primary.main",
|
||||||
|
fontWeight: "bold",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
{item.nestedItems &&
|
||||||
|
open &&
|
||||||
|
(isExpanded ? <ExpandLessIcon /> : <ExpandMoreIcon />)}
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
{item.nestedItems && (
|
||||||
|
<Collapse in={isExpanded && open} timeout="auto" unmountOnExit>
|
||||||
|
<List component="div" disablePadding>
|
||||||
|
{item.nestedItems.map((nestedItem) => (
|
||||||
|
<NavigationItemComponent
|
||||||
|
key={nestedItem.id}
|
||||||
|
item={nestedItem}
|
||||||
|
open={open}
|
||||||
|
onClick={nestedItem.onClick}
|
||||||
|
isNested={true}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Collapse>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -2,7 +2,7 @@ import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
|||||||
import { articlesStore, languageStore } from "@shared";
|
import { articlesStore, languageStore } from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Trash2, FileText } from "lucide-react";
|
import { Trash2, Eye } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { DeleteModal, LanguageSwitcher } from "@widgets";
|
import { DeleteModal, LanguageSwitcher } from "@widgets";
|
||||||
|
|
||||||
@ -31,8 +31,8 @@ export const ArticleListPage = observer(() => {
|
|||||||
renderCell: (params: GridRenderCellParams) => {
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full gap-7 justify-center items-center">
|
<div className="flex h-full gap-7 justify-center items-center">
|
||||||
<button onClick={() => navigate(`/media/${params.row.id}`)}>
|
<button onClick={() => navigate(`/article/${params.row.id}`)}>
|
||||||
<FileText size={20} className="text-green-500" />
|
<Eye size={20} className="text-green-500" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -48,7 +48,7 @@ export const ArticleListPage = observer(() => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const rows = articleList.map((article) => ({
|
const rows = articleList[language].data.map((article) => ({
|
||||||
id: article.id,
|
id: article.id,
|
||||||
heading: article.heading,
|
heading: article.heading,
|
||||||
body: article.body,
|
body: article.body,
|
||||||
|
@ -6,7 +6,6 @@ import {
|
|||||||
MenuItem,
|
MenuItem,
|
||||||
FormControl,
|
FormControl,
|
||||||
InputLabel,
|
InputLabel,
|
||||||
Box,
|
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { ArrowLeft, Save } from "lucide-react";
|
import { ArrowLeft, Save } from "lucide-react";
|
||||||
@ -15,23 +14,22 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { carrierStore, cityStore, mediaStore } from "@shared";
|
import { carrierStore, cityStore, mediaStore } from "@shared";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { LanguageSwitcher, MediaViewer } from "@widgets";
|
import { MediaViewer } from "@widgets";
|
||||||
import { HexColorPicker } from "react-colorful";
|
|
||||||
|
|
||||||
export const CarrierCreatePage = observer(() => {
|
export const CarrierCreatePage = observer(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [fullName, setFullName] = useState("");
|
const [fullName, setFullName] = useState("");
|
||||||
const [shortName, setShortName] = useState("");
|
const [shortName, setShortName] = useState("");
|
||||||
const [cityId, setCityId] = useState<number | null>(null);
|
const [cityId, setCityId] = useState<number | null>(null);
|
||||||
const [primaryColor, setPrimaryColor] = useState("#000000");
|
const [main_color, setMainColor] = useState("#000000");
|
||||||
const [secondaryColor, setSecondaryColor] = useState("#ffffff");
|
const [left_color, setLeftColor] = useState("#ffffff");
|
||||||
const [accentColor, setAccentColor] = useState("#ff0000");
|
const [right_color, setRightColor] = useState("#ff0000");
|
||||||
const [slogan, setSlogan] = useState("");
|
const [slogan, setSlogan] = useState("");
|
||||||
const [selectedMediaId, setSelectedMediaId] = useState<string | null>(null);
|
const [selectedMediaId, setSelectedMediaId] = useState<string | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
cityStore.getCities();
|
cityStore.getCities("ru");
|
||||||
mediaStore.getMedia();
|
mediaStore.getMedia();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -41,11 +39,11 @@ export const CarrierCreatePage = observer(() => {
|
|||||||
await carrierStore.createCarrier(
|
await carrierStore.createCarrier(
|
||||||
fullName,
|
fullName,
|
||||||
shortName,
|
shortName,
|
||||||
cityStore.cities.find((c) => c.id === cityId)?.name!,
|
cityStore.cities.ru.find((c) => c.id === cityId)?.name!,
|
||||||
cityId!,
|
cityId!,
|
||||||
primaryColor,
|
main_color,
|
||||||
secondaryColor,
|
left_color,
|
||||||
accentColor,
|
right_color,
|
||||||
slogan,
|
slogan,
|
||||||
selectedMediaId!
|
selectedMediaId!
|
||||||
);
|
);
|
||||||
@ -60,11 +58,10 @@ export const CarrierCreatePage = observer(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
||||||
<LanguageSwitcher />
|
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
onClick={() => navigate("/carrier")}
|
onClick={() => navigate(-1)}
|
||||||
>
|
>
|
||||||
<ArrowLeft size={20} />
|
<ArrowLeft size={20} />
|
||||||
Назад
|
Назад
|
||||||
@ -80,7 +77,7 @@ export const CarrierCreatePage = observer(() => {
|
|||||||
required
|
required
|
||||||
onChange={(e) => setCityId(e.target.value as number)}
|
onChange={(e) => setCityId(e.target.value as number)}
|
||||||
>
|
>
|
||||||
{cityStore.cities.map((city) => (
|
{cityStore.cities.ru.map((city) => (
|
||||||
<MenuItem key={city.id} value={city.id}>
|
<MenuItem key={city.id} value={city.id}>
|
||||||
{city.name}
|
{city.name}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@ -104,51 +101,55 @@ export const CarrierCreatePage = observer(() => {
|
|||||||
onChange={(e) => setShortName(e.target.value)}
|
onChange={(e) => setShortName(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="w-full flex flex-col gap-4">
|
<div className="flex gap-4 w-full ">
|
||||||
<div className="flex items-center gap-4">
|
<TextField
|
||||||
<span className="w-32">Основной цвет:</span>
|
fullWidth
|
||||||
<Box
|
label="Основной цвет"
|
||||||
sx={{
|
value={main_color}
|
||||||
width: 40,
|
className="flex-1 w-full"
|
||||||
height: 40,
|
onChange={(e) => setMainColor(e.target.value)}
|
||||||
backgroundColor: primaryColor,
|
type="color"
|
||||||
border: "1px solid #ccc",
|
sx={{
|
||||||
|
"& input": {
|
||||||
|
height: "50px",
|
||||||
|
paddingBlock: "14px",
|
||||||
|
paddingInline: "14px",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
}}
|
},
|
||||||
/>
|
}}
|
||||||
<HexColorPicker color={primaryColor} onChange={setPrimaryColor} />
|
/>
|
||||||
</div>
|
<TextField
|
||||||
|
fullWidth
|
||||||
<div className="flex items-center gap-4">
|
label="Цвет левого виджета"
|
||||||
<span className="w-32">Вторичный цвет:</span>
|
value={left_color}
|
||||||
<Box
|
className="flex-1 w-full"
|
||||||
sx={{
|
onChange={(e) => setLeftColor(e.target.value)}
|
||||||
width: 40,
|
type="color"
|
||||||
height: 40,
|
sx={{
|
||||||
backgroundColor: secondaryColor,
|
"& input": {
|
||||||
border: "1px solid #ccc",
|
height: "50px",
|
||||||
|
paddingBlock: "14px",
|
||||||
|
paddingInline: "14px",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
}}
|
},
|
||||||
/>
|
}}
|
||||||
<HexColorPicker
|
/>
|
||||||
color={secondaryColor}
|
<TextField
|
||||||
onChange={setSecondaryColor}
|
fullWidth
|
||||||
/>
|
label="Цвет правого виджета"
|
||||||
</div>
|
value={right_color}
|
||||||
|
className="flex-1 w-full"
|
||||||
<div className="flex items-center gap-4">
|
onChange={(e) => setRightColor(e.target.value)}
|
||||||
<span className="w-32">Акцентный цвет:</span>
|
type="color"
|
||||||
<Box
|
sx={{
|
||||||
sx={{
|
"& input": {
|
||||||
width: 40,
|
height: "50px",
|
||||||
height: 40,
|
paddingBlock: "14px",
|
||||||
backgroundColor: accentColor,
|
paddingInline: "14px",
|
||||||
border: "1px solid #ccc",
|
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
}}
|
},
|
||||||
/>
|
}}
|
||||||
<HexColorPicker color={accentColor} onChange={setAccentColor} />
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
@ -167,11 +168,13 @@ export const CarrierCreatePage = observer(() => {
|
|||||||
required
|
required
|
||||||
onChange={(e) => setSelectedMediaId(e.target.value as string)}
|
onChange={(e) => setSelectedMediaId(e.target.value as string)}
|
||||||
>
|
>
|
||||||
{mediaStore.media.map((media) => (
|
{mediaStore.media
|
||||||
<MenuItem key={media.id} value={media.id}>
|
.filter((media) => media.media_type === 3)
|
||||||
{media.media_name || media.filename}
|
.map((media) => (
|
||||||
</MenuItem>
|
<MenuItem key={media.id} value={media.id}>
|
||||||
))}
|
{media.media_name || media.filename}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
{selectedMediaId && (
|
{selectedMediaId && (
|
||||||
|
307
src/pages/Carrier/CarrierEditPage/index.tsx
Normal file
307
src/pages/Carrier/CarrierEditPage/index.tsx
Normal file
@ -0,0 +1,307 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Paper,
|
||||||
|
TextField,
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { ArrowLeft, Save } from "lucide-react";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { carrierStore, cityStore, mediaStore } from "@shared";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { MediaViewer } from "@widgets";
|
||||||
|
|
||||||
|
export const CarrierEditPage = observer(() => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { id } = useParams();
|
||||||
|
const { carrier, getCarrier, setEditCarrierData, editCarrierData } =
|
||||||
|
carrierStore;
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
await getCarrier(Number(id));
|
||||||
|
setEditCarrierData(
|
||||||
|
carrier?.[Number(id)]?.full_name as string,
|
||||||
|
carrier?.[Number(id)]?.short_name as string,
|
||||||
|
carrier?.[Number(id)]?.city as string,
|
||||||
|
carrier?.[Number(id)]?.city_id as number,
|
||||||
|
carrier?.[Number(id)]?.main_color as string,
|
||||||
|
carrier?.[Number(id)]?.left_color as string,
|
||||||
|
carrier?.[Number(id)]?.right_color as string,
|
||||||
|
carrier?.[Number(id)]?.slogan as string,
|
||||||
|
carrier?.[Number(id)]?.logo as string
|
||||||
|
);
|
||||||
|
cityStore.getCities("ru");
|
||||||
|
mediaStore.getMedia();
|
||||||
|
})();
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
const handleEdit = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
await carrierStore.editCarrier(Number(id));
|
||||||
|
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">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onClick={() => navigate(-1)}
|
||||||
|
>
|
||||||
|
<ArrowLeft size={20} />
|
||||||
|
Назад
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-10 w-full items-end">
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel>Город</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={editCarrierData.city_id || ""}
|
||||||
|
label="Город"
|
||||||
|
required
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditCarrierData(
|
||||||
|
editCarrierData.full_name,
|
||||||
|
editCarrierData.short_name,
|
||||||
|
editCarrierData.city,
|
||||||
|
Number(e.target.value),
|
||||||
|
editCarrierData.main_color,
|
||||||
|
editCarrierData.left_color,
|
||||||
|
editCarrierData.right_color,
|
||||||
|
editCarrierData.slogan,
|
||||||
|
editCarrierData.logo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{cityStore.cities.ru.map((city) => (
|
||||||
|
<MenuItem key={city.id} value={city.id}>
|
||||||
|
{city.name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Полное название"
|
||||||
|
value={editCarrierData.full_name}
|
||||||
|
required
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditCarrierData(
|
||||||
|
e.target.value,
|
||||||
|
editCarrierData.short_name,
|
||||||
|
editCarrierData.city,
|
||||||
|
editCarrierData.city_id,
|
||||||
|
editCarrierData.main_color,
|
||||||
|
editCarrierData.left_color,
|
||||||
|
editCarrierData.right_color,
|
||||||
|
editCarrierData.slogan,
|
||||||
|
editCarrierData.logo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Короткое название"
|
||||||
|
value={editCarrierData.short_name}
|
||||||
|
required
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditCarrierData(
|
||||||
|
editCarrierData.full_name,
|
||||||
|
e.target.value,
|
||||||
|
editCarrierData.city,
|
||||||
|
editCarrierData.city_id,
|
||||||
|
editCarrierData.main_color,
|
||||||
|
editCarrierData.left_color,
|
||||||
|
editCarrierData.right_color,
|
||||||
|
editCarrierData.slogan,
|
||||||
|
editCarrierData.logo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex gap-4 w-full">
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Основной цвет"
|
||||||
|
value={editCarrierData.main_color}
|
||||||
|
className="flex-1 w-full"
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditCarrierData(
|
||||||
|
editCarrierData.full_name,
|
||||||
|
editCarrierData.short_name,
|
||||||
|
editCarrierData.city,
|
||||||
|
editCarrierData.city_id,
|
||||||
|
e.target.value,
|
||||||
|
editCarrierData.left_color,
|
||||||
|
editCarrierData.right_color,
|
||||||
|
editCarrierData.slogan,
|
||||||
|
editCarrierData.logo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
type="color"
|
||||||
|
sx={{
|
||||||
|
"& input": {
|
||||||
|
height: "50px",
|
||||||
|
paddingBlock: "14px",
|
||||||
|
paddingInline: "14px",
|
||||||
|
cursor: "pointer",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Цвет левого виджета"
|
||||||
|
value={editCarrierData.left_color}
|
||||||
|
className="flex-1 w-full"
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditCarrierData(
|
||||||
|
editCarrierData.full_name,
|
||||||
|
editCarrierData.short_name,
|
||||||
|
editCarrierData.city,
|
||||||
|
editCarrierData.city_id,
|
||||||
|
editCarrierData.main_color,
|
||||||
|
e.target.value,
|
||||||
|
editCarrierData.right_color,
|
||||||
|
editCarrierData.slogan,
|
||||||
|
editCarrierData.logo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
type="color"
|
||||||
|
sx={{
|
||||||
|
"& input": {
|
||||||
|
height: "50px",
|
||||||
|
paddingBlock: "14px",
|
||||||
|
paddingInline: "14px",
|
||||||
|
cursor: "pointer",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Цвет правого виджета"
|
||||||
|
value={editCarrierData.right_color}
|
||||||
|
className="flex-1 w-full"
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditCarrierData(
|
||||||
|
editCarrierData.full_name,
|
||||||
|
editCarrierData.short_name,
|
||||||
|
editCarrierData.city,
|
||||||
|
editCarrierData.city_id,
|
||||||
|
editCarrierData.main_color,
|
||||||
|
editCarrierData.left_color,
|
||||||
|
e.target.value,
|
||||||
|
editCarrierData.slogan,
|
||||||
|
editCarrierData.logo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
type="color"
|
||||||
|
sx={{
|
||||||
|
"& input": {
|
||||||
|
height: "50px",
|
||||||
|
paddingBlock: "14px",
|
||||||
|
paddingInline: "14px",
|
||||||
|
cursor: "pointer",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Слоган"
|
||||||
|
value={editCarrierData.slogan}
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditCarrierData(
|
||||||
|
editCarrierData.full_name,
|
||||||
|
editCarrierData.short_name,
|
||||||
|
editCarrierData.city,
|
||||||
|
editCarrierData.city_id,
|
||||||
|
editCarrierData.main_color,
|
||||||
|
editCarrierData.left_color,
|
||||||
|
editCarrierData.right_color,
|
||||||
|
e.target.value,
|
||||||
|
editCarrierData.logo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="w-full flex flex-col gap-4">
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel>Логотип</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={editCarrierData.logo || ""}
|
||||||
|
label="Логотип"
|
||||||
|
required
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditCarrierData(
|
||||||
|
editCarrierData.full_name,
|
||||||
|
editCarrierData.short_name,
|
||||||
|
editCarrierData.city,
|
||||||
|
editCarrierData.city_id,
|
||||||
|
editCarrierData.main_color,
|
||||||
|
editCarrierData.left_color,
|
||||||
|
editCarrierData.right_color,
|
||||||
|
editCarrierData.slogan,
|
||||||
|
e.target.value as string
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{mediaStore.media
|
||||||
|
.filter((media) => media.media_type === 3)
|
||||||
|
.map((media) => (
|
||||||
|
<MenuItem key={media.id} value={media.id}>
|
||||||
|
{media.media_name || media.filename}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
{editCarrierData.logo && (
|
||||||
|
<div className="w-32 h-32">
|
||||||
|
<MediaViewer
|
||||||
|
media={{ id: editCarrierData.logo, media_type: 1 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
className="w-min flex gap-2 items-center"
|
||||||
|
startIcon={<Save size={20} />}
|
||||||
|
onClick={handleEdit}
|
||||||
|
disabled={
|
||||||
|
isLoading ||
|
||||||
|
!editCarrierData.full_name ||
|
||||||
|
!editCarrierData.short_name ||
|
||||||
|
!editCarrierData.city_id ||
|
||||||
|
!editCarrierData.logo
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<Loader2 size={20} className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
"Обновить"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
});
|
@ -1,21 +1,20 @@
|
|||||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||||
import { carrierStore, languageStore } from "@shared";
|
import { carrierStore } from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Eye, Trash2 } from "lucide-react";
|
import { Eye, Pencil, Trash2 } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { DeleteModal, LanguageSwitcher } from "@widgets";
|
import { CreateButton, DeleteModal } from "@widgets";
|
||||||
|
|
||||||
export const CarrierListPage = observer(() => {
|
export const CarrierListPage = observer(() => {
|
||||||
const { carriers, getCarriers, deleteCarrier } = carrierStore;
|
const { carriers, getCarriers, deleteCarrier } = carrierStore;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
const [rowId, setRowId] = useState<number | null>(null); // Lifted state
|
const [rowId, setRowId] = useState<number | null>(null); // Lifted state
|
||||||
const { language } = languageStore;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getCarriers();
|
getCarriers();
|
||||||
}, [language]);
|
}, []);
|
||||||
|
|
||||||
const columns: GridColDef[] = [
|
const columns: GridColDef[] = [
|
||||||
{
|
{
|
||||||
@ -37,10 +36,15 @@ export const CarrierListPage = observer(() => {
|
|||||||
{
|
{
|
||||||
field: "actions",
|
field: "actions",
|
||||||
headerName: "Действия",
|
headerName: "Действия",
|
||||||
|
headerAlign: "center",
|
||||||
|
width: 200,
|
||||||
|
|
||||||
renderCell: (params: GridRenderCellParams) => {
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full gap-7 justify-center items-center">
|
<div className="flex h-full gap-7 justify-center items-center">
|
||||||
|
<button onClick={() => navigate(`/carrier/${params.row.id}/edit`)}>
|
||||||
|
<Pencil size={20} className="text-blue-500" />
|
||||||
|
</button>
|
||||||
<button onClick={() => navigate(`/carrier/${params.row.id}`)}>
|
<button onClick={() => navigate(`/carrier/${params.row.id}`)}>
|
||||||
<Eye size={20} className="text-green-500" />
|
<Eye size={20} className="text-green-500" />
|
||||||
</button>
|
</button>
|
||||||
@ -58,7 +62,7 @@ export const CarrierListPage = observer(() => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const rows = carriers.map((carrier) => ({
|
const rows = carriers.data?.map((carrier) => ({
|
||||||
id: carrier.id,
|
id: carrier.id,
|
||||||
full_name: carrier.full_name,
|
full_name: carrier.full_name,
|
||||||
short_name: carrier.short_name,
|
short_name: carrier.short_name,
|
||||||
@ -67,12 +71,10 @@ export const CarrierListPage = observer(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LanguageSwitcher />
|
|
||||||
|
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="flex justify-between items-center mb-10">
|
<div className="flex justify-between items-center mb-10">
|
||||||
<h1 className="text-2xl">Перевозчики</h1>
|
<h1 className="text-2xl">Перевозчики</h1>
|
||||||
{/* <CreateButton label="Создать перевозчика" path="/carrier/create" /> */}
|
<CreateButton label="Создать перевозчика" path="/carrier/create" />
|
||||||
</div>
|
</div>
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={rows}
|
rows={rows}
|
||||||
@ -86,7 +88,7 @@ export const CarrierListPage = observer(() => {
|
|||||||
open={isDeleteModalOpen}
|
open={isDeleteModalOpen}
|
||||||
onDelete={async () => {
|
onDelete={async () => {
|
||||||
if (rowId) {
|
if (rowId) {
|
||||||
deleteCarrier(rowId);
|
await deleteCarrier(rowId);
|
||||||
}
|
}
|
||||||
setIsDeleteModalOpen(false);
|
setIsDeleteModalOpen(false);
|
||||||
setRowId(null);
|
setRowId(null);
|
||||||
|
@ -8,13 +8,25 @@ import { useNavigate, useParams } from "react-router-dom";
|
|||||||
|
|
||||||
export const CarrierPreviewPage = observer(() => {
|
export const CarrierPreviewPage = observer(() => {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const { getCarrier, carrier } = carrierStore;
|
const { getCarrier, carrier, setEditCarrierData } = carrierStore;
|
||||||
const { oneMedia, getOneMedia } = mediaStore;
|
const { oneMedia, getOneMedia } = mediaStore;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const carrierResponse = await getCarrier(Number(id));
|
const carrierResponse = await getCarrier(Number(id));
|
||||||
|
setEditCarrierData(
|
||||||
|
carrierResponse?.full_name as string,
|
||||||
|
carrierResponse?.short_name as string,
|
||||||
|
carrierResponse?.city as string,
|
||||||
|
carrierResponse?.city_id as number,
|
||||||
|
carrierResponse?.main_color as string,
|
||||||
|
carrierResponse?.left_color as string,
|
||||||
|
carrierResponse?.right_color as string,
|
||||||
|
carrierResponse?.slogan as string,
|
||||||
|
carrierResponse?.logo as string
|
||||||
|
);
|
||||||
|
console.log(carrierResponse);
|
||||||
await getOneMedia(carrierResponse?.logo as string);
|
await getOneMedia(carrierResponse?.logo as string);
|
||||||
})();
|
})();
|
||||||
}, [id]);
|
}, [id]);
|
||||||
@ -31,48 +43,30 @@ export const CarrierPreviewPage = observer(() => {
|
|||||||
<ArrowLeft size={20} />
|
<ArrowLeft size={20} />
|
||||||
Назад
|
Назад
|
||||||
</button>
|
</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>
|
||||||
<div className="flex flex-col gap-10 w-full">
|
<div className="flex flex-col gap-10 w-full">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<h1 className="text-lg font-bold">Полное имя</h1>
|
<h1 className="text-lg font-bold">Полное имя</h1>
|
||||||
<p>{carrier?.full_name}</p>
|
<p>{carrier[Number(id)]?.full_name}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<h1 className="text-lg font-bold">Полное имя</h1>
|
<h1 className="text-lg font-bold">Полное имя</h1>
|
||||||
<p>{carrier?.full_name}</p>
|
<p>{carrier[Number(id)]?.full_name}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<h1 className="text-lg font-bold">Город</h1>
|
<h1 className="text-lg font-bold">Город</h1>
|
||||||
<p>{carrier?.city}</p>
|
<p>{carrier[Number(id)]?.city}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2 ">
|
<div className="flex flex-col gap-2 ">
|
||||||
<h1 className="text-lg font-bold">Основной цвет</h1>
|
<h1 className="text-lg font-bold">Основной цвет</h1>
|
||||||
<div
|
<div
|
||||||
className="w-min"
|
className="w-min"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: `${carrier?.main_color}90`,
|
backgroundColor: `${carrier[Number(id)]?.main_color}90`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{carrier?.main_color}
|
{carrier[Number(id)]?.main_color}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
@ -80,10 +74,10 @@ export const CarrierPreviewPage = observer(() => {
|
|||||||
<div
|
<div
|
||||||
className="w-min"
|
className="w-min"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: `${carrier?.left_color}90`,
|
backgroundColor: `${carrier[Number(id)]?.left_color}90`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{carrier?.left_color}
|
{carrier[Number(id)]?.left_color}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
@ -91,15 +85,15 @@ export const CarrierPreviewPage = observer(() => {
|
|||||||
<div
|
<div
|
||||||
className="w-min"
|
className="w-min"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: `${carrier?.right_color}90`,
|
backgroundColor: `${carrier[Number(id)]?.right_color}90`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{carrier?.right_color}
|
{carrier[Number(id)]?.right_color}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<h1 className="text-lg font-bold">Краткое имя</h1>
|
<h1 className="text-lg font-bold">Краткое имя</h1>
|
||||||
<p>{carrier?.short_name}</p>
|
<p>{carrier[Number(id)]?.short_name}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<h1 className="text-lg font-bold">Логотип</h1>
|
<h1 className="text-lg font-bold">Логотип</h1>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
export * from "./CarrierListPage";
|
export * from "./CarrierListPage";
|
||||||
export * from "./CarrierPreviewPage";
|
export * from "./CarrierPreviewPage";
|
||||||
export * from "./CarrierCreatePage";
|
export * from "./CarrierCreatePage";
|
||||||
|
export * from "./CarrierEditPage";
|
||||||
|
@ -13,33 +13,31 @@ import { ArrowLeft, Save, ImagePlus } from "lucide-react";
|
|||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { cityStore, countryStore, mediaStore } from "@shared";
|
import { cityStore, countryStore, languageStore, mediaStore } from "@shared";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { LanguageSwitcher, MediaViewer } from "@widgets";
|
import { LanguageSwitcher, MediaViewer } from "@widgets";
|
||||||
import { SelectMediaDialog } from "@shared";
|
import { SelectMediaDialog } from "@shared";
|
||||||
|
|
||||||
export const CityCreatePage = observer(() => {
|
export const CityCreatePage = observer(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [name, setName] = useState("");
|
const { language } = languageStore;
|
||||||
const [countryCode, setCountryCode] = useState("");
|
const { createCityData, setCreateCityData } = cityStore;
|
||||||
const [selectedMediaId, setSelectedMediaId] = useState<string | null>(null);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false);
|
const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false);
|
||||||
|
const { getCountries } = countryStore;
|
||||||
|
const { getMedia } = mediaStore;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
countryStore.getCountries();
|
(async () => {
|
||||||
mediaStore.getMedia();
|
await getCountries(language);
|
||||||
}, []);
|
await getMedia();
|
||||||
|
})();
|
||||||
|
}, [language]);
|
||||||
|
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
await cityStore.createCity(
|
await cityStore.createCity();
|
||||||
name,
|
|
||||||
countryStore.countries.find((c) => c.code === countryCode)?.name!,
|
|
||||||
countryCode,
|
|
||||||
selectedMediaId!
|
|
||||||
);
|
|
||||||
toast.success("Город успешно создан");
|
toast.success("Город успешно создан");
|
||||||
navigate("/city");
|
navigate("/city");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -55,11 +53,17 @@ export const CityCreatePage = observer(() => {
|
|||||||
media_name?: string;
|
media_name?: string;
|
||||||
media_type: number;
|
media_type: number;
|
||||||
}) => {
|
}) => {
|
||||||
setSelectedMediaId(media.id);
|
setCreateCityData(
|
||||||
|
createCityData[language].name,
|
||||||
|
createCityData.country,
|
||||||
|
createCityData.country_code,
|
||||||
|
media.id,
|
||||||
|
language
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectedMedia = selectedMediaId
|
const selectedMedia = createCityData.arms
|
||||||
? mediaStore.media.find((m) => m.id === selectedMediaId)
|
? mediaStore.media.find((m) => m.id === createCityData.arms)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -68,7 +72,7 @@ export const CityCreatePage = observer(() => {
|
|||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
onClick={() => navigate("/city")}
|
onClick={() => navigate(-1)}
|
||||||
>
|
>
|
||||||
<ArrowLeft size={20} />
|
<ArrowLeft size={20} />
|
||||||
Назад
|
Назад
|
||||||
@ -79,20 +83,39 @@ export const CityCreatePage = observer(() => {
|
|||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label="Название города"
|
label="Название города"
|
||||||
value={name}
|
value={createCityData[language]?.name || ""}
|
||||||
required
|
required
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) =>
|
||||||
|
setCreateCityData(
|
||||||
|
e.target.value,
|
||||||
|
createCityData.country,
|
||||||
|
createCityData.country_code,
|
||||||
|
createCityData.arms,
|
||||||
|
language
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormControl fullWidth>
|
<FormControl fullWidth>
|
||||||
<InputLabel>Страна</InputLabel>
|
<InputLabel>Страна</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
value={countryCode}
|
value={createCityData.country_code || ""}
|
||||||
label="Страна"
|
label="Страна"
|
||||||
required
|
required
|
||||||
onChange={(e) => setCountryCode(e.target.value)}
|
onChange={(e) => {
|
||||||
|
const selectedCountry = countryStore.countries[language]?.find(
|
||||||
|
(country) => country.code === e.target.value
|
||||||
|
);
|
||||||
|
setCreateCityData(
|
||||||
|
createCityData[language].name,
|
||||||
|
selectedCountry?.name || "",
|
||||||
|
e.target.value,
|
||||||
|
createCityData.arms,
|
||||||
|
language
|
||||||
|
);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{countryStore.countries.map((country) => (
|
{countryStore.countries[language].map((country) => (
|
||||||
<MenuItem key={country.code} value={country.code}>
|
<MenuItem key={country.code} value={country.code}>
|
||||||
{country.name}
|
{country.name}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@ -145,7 +168,7 @@ export const CityCreatePage = observer(() => {
|
|||||||
className="w-min flex gap-2 items-center"
|
className="w-min flex gap-2 items-center"
|
||||||
startIcon={<Save size={20} />}
|
startIcon={<Save size={20} />}
|
||||||
onClick={handleCreate}
|
onClick={handleCreate}
|
||||||
disabled={isLoading || !name || !countryCode}
|
disabled={isLoading || !createCityData[language]?.name}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Loader2 size={20} className="animate-spin" />
|
<Loader2 size={20} className="animate-spin" />
|
||||||
|
207
src/pages/City/CityEditPage/index.tsx
Normal file
207
src/pages/City/CityEditPage/index.tsx
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
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, useParams } from "react-router-dom";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import {
|
||||||
|
cityStore,
|
||||||
|
countryStore,
|
||||||
|
languageStore,
|
||||||
|
mediaStore,
|
||||||
|
CashedCities,
|
||||||
|
} from "@shared";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { LanguageSwitcher, MediaViewer } from "@widgets";
|
||||||
|
import { SelectMediaDialog } from "@shared";
|
||||||
|
|
||||||
|
export const CityEditPage = observer(() => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false);
|
||||||
|
const { language } = languageStore;
|
||||||
|
const { id } = useParams();
|
||||||
|
const { editCityData, editCity, getCity, setEditCityData } = cityStore;
|
||||||
|
const { getCountries } = countryStore;
|
||||||
|
const { getMedia, getOneMedia } = mediaStore;
|
||||||
|
|
||||||
|
const handleEdit = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
await editCity(id as string);
|
||||||
|
toast.success("Город успешно обновлен");
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Ошибка при обновлении города");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
if (id) {
|
||||||
|
const data = await getCity(id as string, language);
|
||||||
|
setEditCityData(
|
||||||
|
data.name,
|
||||||
|
data.country,
|
||||||
|
data.country_code,
|
||||||
|
data.arms,
|
||||||
|
language
|
||||||
|
);
|
||||||
|
await getOneMedia(data.arms as string);
|
||||||
|
await getCountries(language);
|
||||||
|
await getMedia();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [id, language]);
|
||||||
|
|
||||||
|
const handleMediaSelect = (media: {
|
||||||
|
id: string;
|
||||||
|
filename: string;
|
||||||
|
media_name?: string;
|
||||||
|
media_type: number;
|
||||||
|
}) => {
|
||||||
|
setEditCityData(
|
||||||
|
editCityData[language].name,
|
||||||
|
editCityData.country,
|
||||||
|
editCityData.country_code,
|
||||||
|
media.id,
|
||||||
|
language
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedMedia = editCityData.arms
|
||||||
|
? mediaStore.media.find((m) => m.id === editCityData.arms)
|
||||||
|
: 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(-1)}
|
||||||
|
>
|
||||||
|
<ArrowLeft size={20} />
|
||||||
|
Назад
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-10 w-full items-end">
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Название"
|
||||||
|
value={editCityData[language].name}
|
||||||
|
required
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditCityData(
|
||||||
|
e.target.value,
|
||||||
|
editCityData.country,
|
||||||
|
editCityData.country_code,
|
||||||
|
editCityData.arms,
|
||||||
|
language
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel>Страна</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={editCityData.country_code || ""}
|
||||||
|
label="Страна"
|
||||||
|
required
|
||||||
|
onChange={(e) => {
|
||||||
|
const selectedCountry = countryStore.countries[language]?.find(
|
||||||
|
(country) => country.code === e.target.value
|
||||||
|
);
|
||||||
|
setEditCityData(
|
||||||
|
editCityData[language as keyof CashedCities]?.name || "",
|
||||||
|
selectedCountry?.name || "",
|
||||||
|
e.target.value,
|
||||||
|
editCityData.arms,
|
||||||
|
language
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{countryStore.countries[language].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={handleEdit}
|
||||||
|
disabled={
|
||||||
|
isLoading || !editCityData[language as keyof CashedCities]?.name
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<Loader2 size={20} className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
"Обновить"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SelectMediaDialog
|
||||||
|
open={isSelectMediaOpen}
|
||||||
|
onClose={() => setIsSelectMediaOpen(false)}
|
||||||
|
onSelectMedia={handleMediaSelect}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
});
|
@ -1,8 +1,8 @@
|
|||||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||||
import { cityStore, languageStore } from "@shared";
|
import { languageStore, cityStore, CashedCities } from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Eye, Trash2 } from "lucide-react";
|
import { Eye, Pencil, Trash2 } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets";
|
import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets";
|
||||||
|
|
||||||
@ -14,7 +14,7 @@ export const CityListPage = observer(() => {
|
|||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getCities();
|
getCities(language);
|
||||||
}, [language]);
|
}, [language]);
|
||||||
|
|
||||||
const columns: GridColDef[] = [
|
const columns: GridColDef[] = [
|
||||||
@ -33,10 +33,14 @@ export const CityListPage = observer(() => {
|
|||||||
headerName: "Действия",
|
headerName: "Действия",
|
||||||
align: "center",
|
align: "center",
|
||||||
headerAlign: "center",
|
headerAlign: "center",
|
||||||
|
width: 200,
|
||||||
|
|
||||||
renderCell: (params: GridRenderCellParams) => {
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full gap-7 justify-center items-center">
|
<div className="flex h-full gap-7 justify-center items-center">
|
||||||
|
<button onClick={() => navigate(`/city/${params.row.id}/edit`)}>
|
||||||
|
<Pencil size={20} className="text-blue-500" />
|
||||||
|
</button>
|
||||||
<button onClick={() => navigate(`/city/${params.row.id}`)}>
|
<button onClick={() => navigate(`/city/${params.row.id}`)}>
|
||||||
<Eye size={20} className="text-green-500" />
|
<Eye size={20} className="text-green-500" />
|
||||||
</button>
|
</button>
|
||||||
@ -54,7 +58,7 @@ export const CityListPage = observer(() => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const rows = cities.map((city) => ({
|
const rows = cities[language].map((city) => ({
|
||||||
id: city.id,
|
id: city.id,
|
||||||
name: city.name,
|
name: city.name,
|
||||||
country: city.country,
|
country: city.country,
|
||||||
@ -81,7 +85,7 @@ export const CityListPage = observer(() => {
|
|||||||
open={isDeleteModalOpen}
|
open={isDeleteModalOpen}
|
||||||
onDelete={async () => {
|
onDelete={async () => {
|
||||||
if (rowId) {
|
if (rowId) {
|
||||||
deleteCity(rowId);
|
deleteCity(rowId.toString(), language as keyof CashedCities);
|
||||||
}
|
}
|
||||||
setIsDeleteModalOpen(false);
|
setIsDeleteModalOpen(false);
|
||||||
setRowId(null);
|
setRowId(null);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Paper } from "@mui/material";
|
import { Paper } from "@mui/material";
|
||||||
import { cityStore, mediaStore } from "@shared";
|
import { cityStore, languageStore, mediaStore } from "@shared";
|
||||||
import { MediaViewer } from "@widgets";
|
import { LanguageSwitcher, MediaViewer } from "@widgets";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { ArrowLeft } from "lucide-react";
|
import { ArrowLeft } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
@ -8,19 +8,30 @@ import { useNavigate, useParams } from "react-router-dom";
|
|||||||
|
|
||||||
export const CityPreviewPage = observer(() => {
|
export const CityPreviewPage = observer(() => {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const { getCity, city } = cityStore;
|
const { getCity, city, setEditCityData } = cityStore;
|
||||||
const { oneMedia, getOneMedia } = mediaStore;
|
const { oneMedia, getOneMedia } = mediaStore;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { language } = languageStore;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const cityResponse = await getCity(id as string);
|
if (id) {
|
||||||
await getOneMedia(cityResponse.arms as string);
|
const cityResponse = await getCity(id as string, language);
|
||||||
|
setEditCityData(
|
||||||
|
cityResponse.name,
|
||||||
|
cityResponse.country,
|
||||||
|
cityResponse.country_code,
|
||||||
|
cityResponse.arms,
|
||||||
|
language
|
||||||
|
);
|
||||||
|
await getOneMedia(cityResponse.arms as string);
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
}, [id]);
|
}, [id, language]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
||||||
|
<LanguageSwitcher />
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
@ -29,36 +40,18 @@ export const CityPreviewPage = observer(() => {
|
|||||||
<ArrowLeft size={20} />
|
<ArrowLeft size={20} />
|
||||||
Назад
|
Назад
|
||||||
</button>
|
</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>
|
||||||
<div className="flex flex-col gap-10 w-full">
|
<div className="flex flex-col gap-10 w-full">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<h1 className="text-lg font-bold">Название</h1>
|
<h1 className="text-lg font-bold">Название</h1>
|
||||||
<p>{city?.name}</p>
|
<p>{city[id!]?.[language]?.name}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<h1 className="text-lg font-bold">Страна</h1>
|
<h1 className="text-lg font-bold">Страна</h1>
|
||||||
<p>{city?.country}</p>
|
<p>{city[id!]?.[language]?.country}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2 pb-10">
|
||||||
<h1 className="text-lg font-bold">Герб</h1>
|
<h1 className="text-lg font-bold">Герб</h1>
|
||||||
<div className="w-[300px] h-[200px]">
|
<div className="w-[300px] h-[200px]">
|
||||||
<MediaViewer
|
<MediaViewer
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
export * from "./CityListPage";
|
export * from "./CityListPage";
|
||||||
export * from "./CityPreviewPage";
|
export * from "./CityPreviewPage";
|
||||||
export * from "./CityCreatePage";
|
export * from "./CityCreatePage";
|
||||||
|
export * from "./CityEditPage";
|
||||||
|
@ -4,20 +4,20 @@ import { ArrowLeft, Save } from "lucide-react";
|
|||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { countryStore } from "@shared";
|
import { countryStore, languageStore } from "@shared";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { LanguageSwitcher } from "@widgets";
|
import { LanguageSwitcher } from "@widgets";
|
||||||
|
|
||||||
export const CountryCreatePage = observer(() => {
|
export const CountryCreatePage = observer(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [name, setName] = useState("");
|
|
||||||
const [code, setCode] = useState("");
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const { language } = languageStore;
|
||||||
|
const { createCountryData, setCountryData, createCountry } = countryStore;
|
||||||
|
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
await countryStore.createCountry(code, name);
|
await createCountry();
|
||||||
toast.success("Страна успешно создана");
|
toast.success("Страна успешно создана");
|
||||||
navigate("/country");
|
navigate("/country");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -33,7 +33,7 @@ export const CountryCreatePage = observer(() => {
|
|||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
onClick={() => navigate("/country")}
|
onClick={() => navigate(-1)}
|
||||||
>
|
>
|
||||||
<ArrowLeft size={20} />
|
<ArrowLeft size={20} />
|
||||||
Назад
|
Назад
|
||||||
@ -44,16 +44,24 @@ export const CountryCreatePage = observer(() => {
|
|||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label="Код страны"
|
label="Код страны"
|
||||||
value={code}
|
value={createCountryData.code}
|
||||||
required
|
required
|
||||||
onChange={(e) => setCode(e.target.value)}
|
onChange={(e) =>
|
||||||
|
setCountryData(
|
||||||
|
e.target.value,
|
||||||
|
createCountryData[language].name,
|
||||||
|
language
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label="Название"
|
label="Название"
|
||||||
value={name}
|
value={createCountryData[language].name}
|
||||||
required
|
required
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) =>
|
||||||
|
setCountryData(createCountryData.code, e.target.value, language)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@ -61,7 +69,7 @@ export const CountryCreatePage = observer(() => {
|
|||||||
className="w-min flex gap-2 items-center"
|
className="w-min flex gap-2 items-center"
|
||||||
startIcon={<Save size={20} />}
|
startIcon={<Save size={20} />}
|
||||||
onClick={handleCreate}
|
onClick={handleCreate}
|
||||||
disabled={isLoading || !name || !code}
|
disabled={isLoading || !createCountryData[language].name}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Loader2 size={20} className="animate-spin" />
|
<Loader2 size={20} className="animate-spin" />
|
||||||
|
87
src/pages/Country/CountryEditPage/index.tsx
Normal file
87
src/pages/Country/CountryEditPage/index.tsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
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, useParams } from "react-router-dom";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { countryStore, languageStore } from "@shared";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { LanguageSwitcher } from "@widgets";
|
||||||
|
|
||||||
|
export const CountryEditPage = observer(() => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const { language } = languageStore;
|
||||||
|
const { id } = useParams();
|
||||||
|
const { editCountryData, editCountry, getCountry, setEditCountryData } =
|
||||||
|
countryStore;
|
||||||
|
|
||||||
|
const handleEdit = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
await editCountry(id as string);
|
||||||
|
toast.success("Страна успешно обновлена");
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Ошибка при обновлении страны");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
if (id) {
|
||||||
|
const data = await getCountry(id as string, language);
|
||||||
|
setEditCountryData(data.name, language);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [id, language]);
|
||||||
|
|
||||||
|
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(-1)}
|
||||||
|
>
|
||||||
|
<ArrowLeft size={20} />
|
||||||
|
Назад
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-10 w-full items-end">
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Код страны"
|
||||||
|
value={id as string}
|
||||||
|
required
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Название"
|
||||||
|
value={editCountryData[language].name}
|
||||||
|
required
|
||||||
|
onChange={(e) =>
|
||||||
|
countryStore.setEditCountryData(e.target.value, language)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
className="w-min flex gap-2 items-center"
|
||||||
|
startIcon={<Save size={20} />}
|
||||||
|
onClick={handleEdit}
|
||||||
|
disabled={isLoading || !editCountryData[language].name}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<Loader2 size={20} className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
"Обновить"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
});
|
@ -2,7 +2,7 @@ import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
|||||||
import { countryStore, languageStore } from "@shared";
|
import { countryStore, languageStore } from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Eye, Trash2 } from "lucide-react";
|
import { Eye, Pencil, Trash2 } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets";
|
import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets";
|
||||||
|
|
||||||
@ -14,7 +14,7 @@ export const CountryListPage = observer(() => {
|
|||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getCountries();
|
getCountries(language);
|
||||||
}, [language]);
|
}, [language]);
|
||||||
|
|
||||||
const columns: GridColDef[] = [
|
const columns: GridColDef[] = [
|
||||||
@ -28,10 +28,15 @@ export const CountryListPage = observer(() => {
|
|||||||
headerName: "Действия",
|
headerName: "Действия",
|
||||||
align: "center",
|
align: "center",
|
||||||
headerAlign: "center",
|
headerAlign: "center",
|
||||||
|
width: 200,
|
||||||
renderCell: (params: GridRenderCellParams) => {
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full gap-7 justify-center items-center">
|
<div className="flex h-full gap-7 justify-center items-center">
|
||||||
|
<button
|
||||||
|
onClick={() => navigate(`/country/${params.row.code}/edit`)}
|
||||||
|
>
|
||||||
|
<Pencil size={20} className="text-blue-500" />
|
||||||
|
</button>
|
||||||
<button onClick={() => navigate(`/country/${params.row.code}`)}>
|
<button onClick={() => navigate(`/country/${params.row.code}`)}>
|
||||||
<Eye size={20} className="text-green-500" />
|
<Eye size={20} className="text-green-500" />
|
||||||
</button>
|
</button>
|
||||||
@ -49,7 +54,7 @@ export const CountryListPage = observer(() => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const rows = countries.map((country) => ({
|
const rows = countries[language]?.map((country) => ({
|
||||||
id: country.code,
|
id: country.code,
|
||||||
code: country.code,
|
code: country.code,
|
||||||
name: country.name,
|
name: country.name,
|
||||||
@ -66,12 +71,14 @@ export const CountryListPage = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
<DataGrid rows={rows} columns={columns} hideFooter />
|
<DataGrid rows={rows} columns={columns} hideFooter />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DeleteModal
|
<DeleteModal
|
||||||
open={isDeleteModalOpen}
|
open={isDeleteModalOpen}
|
||||||
onDelete={async () => {
|
onDelete={async () => {
|
||||||
if (rowId) {
|
if (rowId) {
|
||||||
await countryStore.deleteCountry(rowId);
|
await countryStore.deleteCountry(rowId, language);
|
||||||
getCountries(); // Refresh the list after deletion
|
getCountries(language); // Refresh the list after deletion
|
||||||
|
setIsDeleteModalOpen(false);
|
||||||
}
|
}
|
||||||
setIsDeleteModalOpen(false);
|
setIsDeleteModalOpen(false);
|
||||||
setRowId(null);
|
setRowId(null);
|
||||||
|
@ -1,23 +1,29 @@
|
|||||||
import { Paper } from "@mui/material";
|
import { Paper } from "@mui/material";
|
||||||
import { countryStore } from "@shared";
|
import { countryStore, languageStore } from "@shared";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { ArrowLeft } from "lucide-react";
|
import { ArrowLeft } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import { LanguageSwitcher } from "@widgets";
|
||||||
|
|
||||||
export const CountryPreviewPage = observer(() => {
|
export const CountryPreviewPage = observer(() => {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const { getCountry, country } = countryStore;
|
const { getCountry, country, setEditCountryData } = countryStore;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { language } = languageStore;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
await getCountry(id as string);
|
if (id) {
|
||||||
|
const data = await getCountry(id as string, language);
|
||||||
|
setEditCountryData(data.name, language);
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
}, [id]);
|
}, [id, language]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
||||||
|
<LanguageSwitcher />
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
@ -45,11 +51,11 @@ export const CountryPreviewPage = observer(() => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div> */}
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
{country && (
|
{country[id!]?.[language] && (
|
||||||
<div className="flex flex-col gap-10 w-full">
|
<div className="flex flex-col gap-10 w-full">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<h1 className="text-lg font-bold">Название</h1>
|
<h1 className="text-lg font-bold">Название</h1>
|
||||||
<p>{country?.name}</p>
|
<p>{country[id!]?.[language]?.name}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
export * from "./CountryListPage";
|
export * from "./CountryListPage";
|
||||||
export * from "./CountryPreviewPage";
|
export * from "./CountryPreviewPage";
|
||||||
export * from "./CountryCreatePage";
|
export * from "./CountryCreatePage";
|
||||||
|
export * from "./CountryEditPage";
|
||||||
|
@ -40,7 +40,7 @@ export const CreateSightPage = observer(() => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
await getCities();
|
await getCities("ru");
|
||||||
await getArticles(languageStore.language);
|
await getArticles(languageStore.language);
|
||||||
};
|
};
|
||||||
fetchData();
|
fetchData();
|
||||||
|
@ -24,7 +24,7 @@ export const EditSightPage = observer(() => {
|
|||||||
const { getArticles } = articlesStore;
|
const { getArticles } = articlesStore;
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const { getCities } = cityStore;
|
const { getRuCities } = cityStore;
|
||||||
|
|
||||||
let blocker = useBlocker(
|
let blocker = useBlocker(
|
||||||
({ currentLocation, nextLocation }) =>
|
({ currentLocation, nextLocation }) =>
|
||||||
@ -40,7 +40,7 @@ export const EditSightPage = observer(() => {
|
|||||||
if (id) {
|
if (id) {
|
||||||
await getSightInfo(+id, language);
|
await getSightInfo(+id, language);
|
||||||
await getArticles(language);
|
await getArticles(language);
|
||||||
await getCities();
|
await getRuCities();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchData();
|
fetchData();
|
||||||
|
File diff suppressed because it is too large
Load Diff
143
src/pages/MapPage/mapStore.ts
Normal file
143
src/pages/MapPage/mapStore.ts
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import { languageInstance } from "@shared";
|
||||||
|
import { makeAutoObservable } from "mobx";
|
||||||
|
|
||||||
|
interface ApiRoute {
|
||||||
|
id: number;
|
||||||
|
route_number: string;
|
||||||
|
path: [number, number][];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiStation {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiSight {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MapStore {
|
||||||
|
constructor() {
|
||||||
|
makeAutoObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
routes: ApiRoute[] = [];
|
||||||
|
stations: ApiStation[] = [];
|
||||||
|
sights: ApiSight[] = [];
|
||||||
|
|
||||||
|
getRoutes = async () => {
|
||||||
|
const routes = await languageInstance("ru").get("/route");
|
||||||
|
|
||||||
|
const routedIds = routes.data.map((route: any) => route.id);
|
||||||
|
|
||||||
|
const mappedRoutes: ApiRoute[] = [];
|
||||||
|
for (const routeId of routedIds) {
|
||||||
|
const responseSoloRoute = await languageInstance("ru").get(
|
||||||
|
`/route/${routeId}`
|
||||||
|
);
|
||||||
|
const route = responseSoloRoute.data;
|
||||||
|
|
||||||
|
const mappedRoute = {
|
||||||
|
id: route.id,
|
||||||
|
route_number: route.route_number,
|
||||||
|
path: route.path,
|
||||||
|
};
|
||||||
|
|
||||||
|
mappedRoutes.push(mappedRoute);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.routes = mappedRoutes.sort((a, b) =>
|
||||||
|
a.route_number.localeCompare(b.route_number)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
getStations = async () => {
|
||||||
|
const stations = await languageInstance("ru").get("/station");
|
||||||
|
const mappedStations = stations.data.map((station: any) => ({
|
||||||
|
id: station.id,
|
||||||
|
name: station.name,
|
||||||
|
latitude: station.latitude,
|
||||||
|
longitude: station.longitude,
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.stations = mappedStations;
|
||||||
|
};
|
||||||
|
|
||||||
|
getSights = async () => {
|
||||||
|
const sights = await languageInstance("ru").get("/sight");
|
||||||
|
const mappedSights = sights.data.map((sight: any) => ({
|
||||||
|
id: sight.id,
|
||||||
|
name: sight.name,
|
||||||
|
description: sight.description,
|
||||||
|
latitude: sight.latitude,
|
||||||
|
longitude: sight.longitude,
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.sights = mappedSights;
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteRecourse = async (recourse: string, id: number) => {
|
||||||
|
await languageInstance("ru").delete(`/${recourse}/${id}`);
|
||||||
|
if (recourse === "route") {
|
||||||
|
this.routes = this.routes.filter((route) => route.id !== id);
|
||||||
|
} else if (recourse === "station") {
|
||||||
|
this.stations = this.stations.filter((station) => station.id !== id);
|
||||||
|
} else if (recourse === "sight") {
|
||||||
|
this.sights = this.sights.filter((sight) => sight.id !== id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSave = async (json: string) => {
|
||||||
|
const sights: any[] = [];
|
||||||
|
const routes: any[] = [];
|
||||||
|
const stations: any[] = [];
|
||||||
|
|
||||||
|
const parsedJSON = JSON.parse(json);
|
||||||
|
|
||||||
|
console.log(parsedJSON);
|
||||||
|
parsedJSON.features.forEach((feature: any) => {
|
||||||
|
const { geometry, properties, id } = feature;
|
||||||
|
const idCanBeSplited = id.split("-");
|
||||||
|
|
||||||
|
if (!(idCanBeSplited.length > 1)) {
|
||||||
|
if (properties.featureType === "station") {
|
||||||
|
stations.push({
|
||||||
|
name: properties.name || "",
|
||||||
|
latitude: geometry.coordinates[1],
|
||||||
|
longitude: geometry.coordinates[0],
|
||||||
|
});
|
||||||
|
} else if (properties.featureType === "route") {
|
||||||
|
routes.push({
|
||||||
|
route_number: properties.name || "",
|
||||||
|
path: geometry.coordinates,
|
||||||
|
});
|
||||||
|
} else if (properties.featureType === "sight") {
|
||||||
|
sights.push({
|
||||||
|
name: properties.name || "",
|
||||||
|
description: properties.description || "",
|
||||||
|
latitude: geometry.coordinates[1],
|
||||||
|
longitude: geometry.coordinates[0],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const station of stations) {
|
||||||
|
await languageInstance("ru").post("/station", station);
|
||||||
|
}
|
||||||
|
for (const route of routes) {
|
||||||
|
await languageInstance("ru").post("/route", route);
|
||||||
|
}
|
||||||
|
for (const sight of sights) {
|
||||||
|
await languageInstance("ru").post("/sight", sight);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new MapStore();
|
@ -164,7 +164,7 @@ export const MediaEditPage = observer(() => {
|
|||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
startIcon={<ArrowLeft size={20} />}
|
startIcon={<ArrowLeft size={20} />}
|
||||||
onClick={() => navigate("/media")}
|
onClick={() => navigate(-1)}
|
||||||
>
|
>
|
||||||
Назад
|
Назад
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -4,7 +4,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Eye, Trash2 } from "lucide-react";
|
import { Eye, Trash2 } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets";
|
import { CreateButton, DeleteModal } from "@widgets";
|
||||||
|
|
||||||
export const MediaListPage = observer(() => {
|
export const MediaListPage = observer(() => {
|
||||||
const { media, getMedia, deleteMedia } = mediaStore;
|
const { media, getMedia, deleteMedia } = mediaStore;
|
||||||
@ -43,7 +43,7 @@ export const MediaListPage = observer(() => {
|
|||||||
{
|
{
|
||||||
field: "actions",
|
field: "actions",
|
||||||
headerName: "Действия",
|
headerName: "Действия",
|
||||||
flex: 1,
|
width: 200,
|
||||||
align: "center",
|
align: "center",
|
||||||
headerAlign: "center",
|
headerAlign: "center",
|
||||||
|
|
||||||
@ -75,8 +75,6 @@ export const MediaListPage = observer(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LanguageSwitcher />
|
|
||||||
|
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="flex justify-between items-center mb-10">
|
<div className="flex justify-between items-center mb-10">
|
||||||
<h1 className="text-2xl">Медиа</h1>
|
<h1 className="text-2xl">Медиа</h1>
|
||||||
|
@ -6,20 +6,91 @@ import {
|
|||||||
MenuItem,
|
MenuItem,
|
||||||
FormControl,
|
FormControl,
|
||||||
InputLabel,
|
InputLabel,
|
||||||
|
Typography,
|
||||||
|
Box,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { ArrowLeft, Loader2, Save } from "lucide-react";
|
import { ArrowLeft, Loader2, Save } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
|
import { carrierStore } from "../../../shared/store/CarrierStore";
|
||||||
|
import { articlesStore } from "../../../shared/store/ArticlesStore";
|
||||||
|
import { Route, routeStore } from "../../../shared/store/RouteStore";
|
||||||
|
|
||||||
export const RouteCreatePage = observer(() => {
|
export const RouteCreatePage = observer(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [carrier, setCarrier] = useState<string>("");
|
||||||
const [routeNumber, setRouteNumber] = useState("");
|
const [routeNumber, setRouteNumber] = useState("");
|
||||||
const [direction, setDirection] = useState("");
|
const [routeCoords, setRouteCoords] = useState("");
|
||||||
|
const [govRouteNumber, setGovRouteNumber] = useState("");
|
||||||
|
const [governorAppeal, setGovernorAppeal] = useState<string>("");
|
||||||
|
const [direction, setDirection] = useState("backward");
|
||||||
|
const [scaleMin, setScaleMin] = useState("");
|
||||||
|
const [scaleMax, setScaleMax] = useState("");
|
||||||
|
const [turn, setTurn] = useState("");
|
||||||
|
const [centerLat, setCenterLat] = useState("");
|
||||||
|
const [centerLng, setCenterLng] = useState("");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
carrierStore.getCarriers();
|
||||||
|
articlesStore.getArticleList();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCreateRoute = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
// Преобразуем значения в нужные типы
|
||||||
|
const carrier_id = Number(carrier);
|
||||||
|
const governor_appeal = Number(governorAppeal);
|
||||||
|
const scale_min = scaleMin ? Number(scaleMin) : undefined;
|
||||||
|
const scale_max = scaleMax ? Number(scaleMax) : undefined;
|
||||||
|
const rotate = turn ? Number(turn) : undefined;
|
||||||
|
const center_latitude = centerLat ? Number(centerLat) : undefined;
|
||||||
|
const center_longitude = centerLng ? Number(centerLng) : undefined;
|
||||||
|
const route_direction = direction === "forward";
|
||||||
|
// Координаты маршрута как массив массивов чисел
|
||||||
|
const path = routeCoords
|
||||||
|
.split("\n")
|
||||||
|
.map((line) =>
|
||||||
|
line
|
||||||
|
.split(" ")
|
||||||
|
.map((coord) => Number(coord.trim()))
|
||||||
|
.filter((n) => !isNaN(n))
|
||||||
|
)
|
||||||
|
.filter((arr) => arr.length === 2);
|
||||||
|
|
||||||
|
// Собираем объект маршрута
|
||||||
|
const newRoute: Partial<Route> = {
|
||||||
|
carrier:
|
||||||
|
carrierStore.carriers.data.find((c: any) => c.id === carrier_id)
|
||||||
|
?.full_name || "",
|
||||||
|
carrier_id,
|
||||||
|
route_number: routeNumber,
|
||||||
|
route_sys_number: govRouteNumber,
|
||||||
|
governor_appeal,
|
||||||
|
route_direction,
|
||||||
|
scale_min,
|
||||||
|
scale_max,
|
||||||
|
rotate,
|
||||||
|
center_latitude,
|
||||||
|
center_longitude,
|
||||||
|
path,
|
||||||
|
};
|
||||||
|
|
||||||
|
await routeStore.createRoute(newRoute);
|
||||||
|
toast.success("Маршрут успешно создан");
|
||||||
|
navigate(-1);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
toast.error("Произошла ошибка при создании маршрута");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
@ -28,11 +99,31 @@ export const RouteCreatePage = observer(() => {
|
|||||||
onClick={() => navigate(-1)}
|
onClick={() => navigate(-1)}
|
||||||
>
|
>
|
||||||
<ArrowLeft size={20} />
|
<ArrowLeft size={20} />
|
||||||
Назад
|
Маршруты / Создать
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl font-bold">Создание маршрута</h1>
|
<Typography variant="h5" fontWeight={700}>
|
||||||
<div className="flex flex-col gap-10 w-full items-end">
|
Создать маршрут
|
||||||
|
</Typography>
|
||||||
|
<Box className="flex flex-col gap-6 w-full">
|
||||||
|
<FormControl fullWidth required>
|
||||||
|
<InputLabel>Выберите перевозчика</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={carrier}
|
||||||
|
label="Выберите перевозчика"
|
||||||
|
onChange={(e) => setCarrier(e.target.value as string)}
|
||||||
|
disabled={carrierStore.carriers.data.length === 0}
|
||||||
|
>
|
||||||
|
<MenuItem value="">Не выбрано</MenuItem>
|
||||||
|
{carrierStore.carriers.data.map(
|
||||||
|
(c: (typeof carrierStore.carriers.data)[number]) => (
|
||||||
|
<MenuItem key={c.id} value={c.id}>
|
||||||
|
{c.full_name}
|
||||||
|
</MenuItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
<TextField
|
<TextField
|
||||||
className="w-full"
|
className="w-full"
|
||||||
label="Номер маршрута"
|
label="Номер маршрута"
|
||||||
@ -40,38 +131,88 @@ export const RouteCreatePage = observer(() => {
|
|||||||
value={routeNumber}
|
value={routeNumber}
|
||||||
onChange={(e) => setRouteNumber(e.target.value)}
|
onChange={(e) => setRouteNumber(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<FormControl fullWidth>
|
<TextField
|
||||||
<InputLabel>Направление</InputLabel>
|
className="w-full"
|
||||||
|
label="Координаты маршрута"
|
||||||
|
multiline
|
||||||
|
minRows={3}
|
||||||
|
value={routeCoords}
|
||||||
|
onChange={(e) => setRouteCoords(e.target.value)}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
className="w-full"
|
||||||
|
label="Номер маршрута в Говорящем Городе"
|
||||||
|
required
|
||||||
|
value={govRouteNumber}
|
||||||
|
onChange={(e) => setGovRouteNumber(e.target.value)}
|
||||||
|
/>
|
||||||
|
<FormControl fullWidth required>
|
||||||
|
<InputLabel>Обращение губернатора</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
value={direction}
|
value={governorAppeal}
|
||||||
label="Направление"
|
label="Обращение губернатора"
|
||||||
onChange={(e) => setDirection(e.target.value)}
|
onChange={(e) => setGovernorAppeal(e.target.value as string)}
|
||||||
required
|
disabled={articlesStore.articleList.ru.data.length === 0}
|
||||||
>
|
>
|
||||||
<MenuItem value="forward">Прямое</MenuItem>
|
<MenuItem value="">Не выбрано</MenuItem>
|
||||||
<MenuItem value="backward">Обратное</MenuItem>
|
{articlesStore.articleList.ru.data.map(
|
||||||
|
(a: (typeof articlesStore.articleList.ru.data)[number]) => (
|
||||||
|
<MenuItem key={a.id} value={a.id}>
|
||||||
|
{a.heading}
|
||||||
|
</MenuItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<FormControl fullWidth required>
|
||||||
|
<InputLabel>Прямой/обратный маршрут</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={direction}
|
||||||
|
label="Прямой/обратный маршрут"
|
||||||
|
onChange={(e) => setDirection(e.target.value)}
|
||||||
|
>
|
||||||
|
<MenuItem value="forward">Прямой</MenuItem>
|
||||||
|
<MenuItem value="backward">Обратный</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<TextField
|
||||||
|
className="w-full"
|
||||||
|
label="Масштаб (мин)"
|
||||||
|
value={scaleMin}
|
||||||
|
onChange={(e) => setScaleMin(e.target.value)}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
className="w-full"
|
||||||
|
label="Масштаб (макс)"
|
||||||
|
value={scaleMax}
|
||||||
|
onChange={(e) => setScaleMax(e.target.value)}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
className="w-full"
|
||||||
|
label="Поворот"
|
||||||
|
value={turn}
|
||||||
|
onChange={(e) => setTurn(e.target.value)}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
className="w-full"
|
||||||
|
label="Центр. широта"
|
||||||
|
value={centerLat}
|
||||||
|
onChange={(e) => setCenterLat(e.target.value)}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
className="w-full"
|
||||||
|
label="Центр. долгота"
|
||||||
|
value={centerLng}
|
||||||
|
onChange={(e) => setCenterLng(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<div className="flex w-full justify-end">
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
className="w-min flex gap-2 items-center"
|
className="w-min flex gap-2 items-center"
|
||||||
startIcon={<Save size={20} />}
|
startIcon={<Save size={20} />}
|
||||||
onClick={async () => {
|
onClick={handleCreateRoute}
|
||||||
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}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
|
225
src/pages/Route/RouteEditPage/index.tsx
Normal file
225
src/pages/Route/RouteEditPage/index.tsx
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Paper,
|
||||||
|
TextField,
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
Typography,
|
||||||
|
Box,
|
||||||
|
} from "@mui/material";
|
||||||
|
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { ArrowLeft, Save } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
|
||||||
|
import { carrierStore } from "../../../shared/store/CarrierStore";
|
||||||
|
import { articlesStore } from "../../../shared/store/ArticlesStore";
|
||||||
|
import { routeStore } from "../../../shared/store/RouteStore";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
|
export const RouteEditPage = observer(() => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { id } = useParams();
|
||||||
|
const { editRouteData } = routeStore;
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchData = async () => {
|
||||||
|
const response = await routeStore.getRoute(Number(id));
|
||||||
|
routeStore.setEditRouteData(response);
|
||||||
|
carrierStore.getCarriers();
|
||||||
|
articlesStore.getArticleList();
|
||||||
|
};
|
||||||
|
fetchData();
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
await routeStore.editRoute(Number(id));
|
||||||
|
toast.success("Маршрут успешно сохранен");
|
||||||
|
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>
|
||||||
|
<Typography variant="h5" fontWeight={700}>
|
||||||
|
Редактировать маршрут
|
||||||
|
</Typography>
|
||||||
|
<Box className="flex flex-col gap-6 w-full">
|
||||||
|
<FormControl fullWidth required>
|
||||||
|
<InputLabel>Выберите перевозчика</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={editRouteData.carrier_id}
|
||||||
|
label="Выберите перевозчика"
|
||||||
|
onChange={(e) =>
|
||||||
|
routeStore.setEditRouteData({
|
||||||
|
carrier_id: Number(e.target.value),
|
||||||
|
carrier:
|
||||||
|
carrierStore.carriers.data.find(
|
||||||
|
(c) => c.id === Number(e.target.value)
|
||||||
|
)?.full_name || "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
disabled={carrierStore.carriers.data.length === 0}
|
||||||
|
>
|
||||||
|
<MenuItem value="">Не выбрано</MenuItem>
|
||||||
|
{carrierStore.carriers.data.map(
|
||||||
|
(c: (typeof carrierStore.carriers.data)[number]) => (
|
||||||
|
<MenuItem key={c.id} value={c.id}>
|
||||||
|
{c.full_name}
|
||||||
|
</MenuItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<TextField
|
||||||
|
className="w-full"
|
||||||
|
label="Номер маршрута"
|
||||||
|
required
|
||||||
|
value={editRouteData.route_number || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
routeStore.setEditRouteData({
|
||||||
|
route_number: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
className="w-full"
|
||||||
|
label="Координаты маршрута"
|
||||||
|
multiline
|
||||||
|
minRows={3}
|
||||||
|
value={editRouteData.path.map((p) => p.join(" ")).join("\n") || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
routeStore.setEditRouteData({
|
||||||
|
path: e.target.value
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => line.split(" ").map(Number)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
className="w-full"
|
||||||
|
label="Номер маршрута в Говорящем Городе"
|
||||||
|
required
|
||||||
|
value={editRouteData.route_sys_number || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
routeStore.setEditRouteData({
|
||||||
|
route_sys_number: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<FormControl fullWidth required>
|
||||||
|
<InputLabel>Обращение губернатора</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={editRouteData.governor_appeal || ""}
|
||||||
|
label="Обращение губернатора"
|
||||||
|
onChange={(e) =>
|
||||||
|
routeStore.setEditRouteData({
|
||||||
|
governor_appeal: Number(e.target.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
disabled={articlesStore.articleList.ru.data.length === 0}
|
||||||
|
>
|
||||||
|
<MenuItem value="">Не выбрано</MenuItem>
|
||||||
|
{articlesStore.articleList.ru.data.map(
|
||||||
|
(a: (typeof articlesStore.articleList.ru.data)[number]) => (
|
||||||
|
<MenuItem key={a.id} value={a.id}>
|
||||||
|
{a.heading}
|
||||||
|
</MenuItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl fullWidth required>
|
||||||
|
<InputLabel>Прямой/обратный маршрут</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={editRouteData.route_direction ? "forward" : "backward"}
|
||||||
|
label="Прямой/обратный маршрут"
|
||||||
|
onChange={(e) =>
|
||||||
|
routeStore.setEditRouteData({
|
||||||
|
route_direction: e.target.value === "forward",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MenuItem value="forward">Прямой</MenuItem>
|
||||||
|
<MenuItem value="backward">Обратный</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<TextField
|
||||||
|
className="w-full"
|
||||||
|
label="Масштаб (мин)"
|
||||||
|
value={editRouteData.scale_min || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
routeStore.setEditRouteData({
|
||||||
|
scale_min: Number(e.target.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
className="w-full"
|
||||||
|
label="Масштаб (макс)"
|
||||||
|
value={editRouteData.scale_max || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
routeStore.setEditRouteData({
|
||||||
|
scale_max: Number(e.target.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
className="w-full"
|
||||||
|
label="Поворот"
|
||||||
|
value={editRouteData.rotate || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
routeStore.setEditRouteData({
|
||||||
|
rotate: Number(e.target.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
className="w-full"
|
||||||
|
label="Центр. широта"
|
||||||
|
value={editRouteData.center_latitude || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
routeStore.setEditRouteData({
|
||||||
|
center_latitude: Number(e.target.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
className="w-full"
|
||||||
|
label="Центр. долгота"
|
||||||
|
value={editRouteData.center_longitude || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
routeStore.setEditRouteData({
|
||||||
|
center_longitude: Number(e.target.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<div className="flex w-full justify-end">
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
className="w-min flex gap-2 items-center"
|
||||||
|
startIcon={<Save size={20} />}
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
Сохранить
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
});
|
@ -2,9 +2,9 @@ import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
|||||||
import { languageStore, routeStore } from "@shared";
|
import { languageStore, routeStore } from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Eye, Trash2 } from "lucide-react";
|
import { Map, Pencil, Trash2 } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets";
|
import { CreateButton, DeleteModal } from "@widgets";
|
||||||
|
|
||||||
export const RouteListPage = observer(() => {
|
export const RouteListPage = observer(() => {
|
||||||
const { routes, getRoutes, deleteRoute } = routeStore;
|
const { routes, getRoutes, deleteRoute } = routeStore;
|
||||||
@ -49,15 +49,21 @@ export const RouteListPage = observer(() => {
|
|||||||
{
|
{
|
||||||
field: "actions",
|
field: "actions",
|
||||||
headerName: "Действия",
|
headerName: "Действия",
|
||||||
width: 140,
|
width: 250,
|
||||||
align: "center",
|
align: "center",
|
||||||
headerAlign: "center",
|
headerAlign: "center",
|
||||||
renderCell: (params: GridRenderCellParams) => {
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full gap-7 justify-center items-center">
|
<div className="flex h-full gap-7 justify-center items-center">
|
||||||
<button onClick={() => navigate(`/route/${params.row.id}`)}>
|
<button onClick={() => navigate(`/route/${params.row.id}/edit`)}>
|
||||||
<Eye size={20} className="text-green-500" />
|
<Pencil size={20} className="text-blue-500" />
|
||||||
</button>
|
</button>
|
||||||
|
<button onClick={() => navigate(`/route-preview/${params.row.id}`)}>
|
||||||
|
<Map size={20} className="text-purple-500" />
|
||||||
|
</button>
|
||||||
|
{/* <button onClick={() => navigate(`/route/${params.row.id}`)}>
|
||||||
|
<Eye size={20} className="text-green-500" />
|
||||||
|
</button> */}
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
@ -72,7 +78,7 @@ export const RouteListPage = observer(() => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const rows = routes.map((route) => ({
|
const rows = routes.data.map((route) => ({
|
||||||
id: route.id,
|
id: route.id,
|
||||||
carrier: route.carrier,
|
carrier: route.carrier,
|
||||||
route_number: route.route_number,
|
route_number: route.route_number,
|
||||||
@ -81,8 +87,6 @@ export const RouteListPage = observer(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LanguageSwitcher />
|
|
||||||
|
|
||||||
<div style={{ width: "100%" }}>
|
<div style={{ width: "100%" }}>
|
||||||
<div className="flex justify-between items-center mb-10">
|
<div className="flex justify-between items-center mb-10">
|
||||||
<h1 className="text-2xl">Маршруты</h1>
|
<h1 className="text-2xl">Маршруты</h1>
|
||||||
|
@ -1 +1,4 @@
|
|||||||
export * from "./RouteListPage";
|
export * from "./RouteListPage";
|
||||||
|
export * from "./RouteCreatePage";
|
||||||
|
export { RoutePreview } from "./route-preview";
|
||||||
|
export * from "./RouteEditPage";
|
||||||
|
9
src/pages/Route/route-preview/Constants.ts
Normal file
9
src/pages/Route/route-preview/Constants.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export const UP_SCALE = 30000;
|
||||||
|
export const PATH_WIDTH = 15;
|
||||||
|
export const STATION_RADIUS = 20;
|
||||||
|
export const STATION_OUTLINE_WIDTH = 10;
|
||||||
|
export const SIGHT_SIZE = 60;
|
||||||
|
export const SCALE_FACTOR = 50;
|
||||||
|
|
||||||
|
export const BACKGROUND_COLOR = 0x111111;
|
||||||
|
export const PATH_COLOR = 0xff4d4d;
|
230
src/pages/Route/route-preview/InfiniteCanvas.tsx
Normal file
230
src/pages/Route/route-preview/InfiniteCanvas.tsx
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
import { FederatedMouseEvent, FederatedWheelEvent } from "pixi.js";
|
||||||
|
import { Component, ReactNode, useEffect, useState, useRef } from "react";
|
||||||
|
import { useTransform } from "./TransformContext";
|
||||||
|
import { useMapData } from "./MapDataContext";
|
||||||
|
import { SCALE_FACTOR } from "./Constants";
|
||||||
|
import { useApplication } from "@pixi/react";
|
||||||
|
|
||||||
|
class ErrorBoundary extends Component<
|
||||||
|
{ children: ReactNode },
|
||||||
|
{ hasError: boolean }
|
||||||
|
> {
|
||||||
|
state = { hasError: false };
|
||||||
|
|
||||||
|
static getDerivedStateFromError() {
|
||||||
|
return { hasError: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
||||||
|
console.error("Error caught:", error, info);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return this.state.hasError ? <p>Whoopsie Daisy!</p> : this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InfiniteCanvas({
|
||||||
|
children,
|
||||||
|
}: Readonly<{ children?: ReactNode }>) {
|
||||||
|
const {
|
||||||
|
position,
|
||||||
|
setPosition,
|
||||||
|
scale,
|
||||||
|
setScale,
|
||||||
|
rotation,
|
||||||
|
setRotation,
|
||||||
|
setScreenCenter,
|
||||||
|
screenCenter,
|
||||||
|
} = useTransform();
|
||||||
|
const { routeData, originalRouteData } = useMapData();
|
||||||
|
|
||||||
|
const applicationRef = useApplication();
|
||||||
|
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const [startMousePosition, setStartMousePosition] = useState({ x: 0, y: 0 });
|
||||||
|
const [startRotation, setStartRotation] = useState(0);
|
||||||
|
const [startPosition, setStartPosition] = useState({ x: 0, y: 0 });
|
||||||
|
|
||||||
|
// Флаг для предотвращения конфликта между пользовательским вводом и данными маршрута
|
||||||
|
const [isUserInteracting, setIsUserInteracting] = useState(false);
|
||||||
|
|
||||||
|
// Реф для отслеживания последнего значения originalRouteData?.rotate
|
||||||
|
const lastOriginalRotation = useRef<number | undefined>(undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const canvas = applicationRef?.app.canvas;
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
const canvasRect = canvas.getBoundingClientRect();
|
||||||
|
const canvasLeft = canvasRect.left;
|
||||||
|
const canvasTop = canvasRect.top;
|
||||||
|
const centerX = window.innerWidth / 2 - canvasLeft;
|
||||||
|
const centerY = window.innerHeight / 2 - canvasTop;
|
||||||
|
setScreenCenter({ x: centerX, y: centerY });
|
||||||
|
}, [applicationRef?.app.canvas, setScreenCenter]);
|
||||||
|
|
||||||
|
const handlePointerDown = (e: FederatedMouseEvent) => {
|
||||||
|
setIsDragging(true);
|
||||||
|
setIsUserInteracting(true); // Устанавливаем флаг взаимодействия пользователя
|
||||||
|
setStartPosition({
|
||||||
|
x: position.x,
|
||||||
|
y: position.y,
|
||||||
|
});
|
||||||
|
setStartMousePosition({
|
||||||
|
x: e.globalX,
|
||||||
|
y: e.globalY,
|
||||||
|
});
|
||||||
|
setStartRotation(rotation);
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Устанавливаем rotation только при изменении originalRouteData и отсутствии взаимодействия пользователя
|
||||||
|
useEffect(() => {
|
||||||
|
const newRotation = originalRouteData?.rotate ?? 0;
|
||||||
|
|
||||||
|
// Обновляем rotation только если:
|
||||||
|
// 1. Пользователь не взаимодействует с канвасом
|
||||||
|
// 2. Значение действительно изменилось
|
||||||
|
if (!isUserInteracting && lastOriginalRotation.current !== newRotation) {
|
||||||
|
setRotation((newRotation * Math.PI) / 180);
|
||||||
|
lastOriginalRotation.current = newRotation;
|
||||||
|
}
|
||||||
|
}, [originalRouteData?.rotate, isUserInteracting, setRotation]);
|
||||||
|
|
||||||
|
const handlePointerMove = (e: FederatedMouseEvent) => {
|
||||||
|
if (!isDragging) return;
|
||||||
|
|
||||||
|
if (e.shiftKey) {
|
||||||
|
const center = screenCenter ?? { x: 0, y: 0 };
|
||||||
|
const startAngle = Math.atan2(
|
||||||
|
startMousePosition.y - center.y,
|
||||||
|
startMousePosition.x - center.x
|
||||||
|
);
|
||||||
|
const currentAngle = Math.atan2(
|
||||||
|
e.globalY - center.y,
|
||||||
|
e.globalX - center.x
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate rotation difference in radians
|
||||||
|
const rotationDiff = currentAngle - startAngle;
|
||||||
|
|
||||||
|
// Update rotation
|
||||||
|
setRotation(startRotation + rotationDiff);
|
||||||
|
|
||||||
|
const cosDelta = Math.cos(rotationDiff);
|
||||||
|
const sinDelta = Math.sin(rotationDiff);
|
||||||
|
|
||||||
|
setPosition({
|
||||||
|
x:
|
||||||
|
center.x * (1 - cosDelta) +
|
||||||
|
startPosition.x * cosDelta +
|
||||||
|
(center.y - startPosition.y) * sinDelta,
|
||||||
|
y:
|
||||||
|
center.y * (1 - cosDelta) +
|
||||||
|
startPosition.y * cosDelta +
|
||||||
|
(startPosition.x - center.x) * sinDelta,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setRotation(startRotation);
|
||||||
|
setPosition({
|
||||||
|
x: startPosition.x - startMousePosition.x + e.globalX,
|
||||||
|
y: startPosition.y - startMousePosition.y + e.globalY,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePointerUp = (e: FederatedMouseEvent) => {
|
||||||
|
setIsDragging(false);
|
||||||
|
// Сбрасываем флаг взаимодействия через небольшую задержку
|
||||||
|
// чтобы избежать немедленного срабатывания useEffect
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsUserInteracting(false);
|
||||||
|
}, 100);
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleWheel = (e: FederatedWheelEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsUserInteracting(true); // Устанавливаем флаг при зуме
|
||||||
|
|
||||||
|
// Get mouse position relative to canvas
|
||||||
|
const mouseX = e.globalX - position.x;
|
||||||
|
const mouseY = e.globalY - position.y;
|
||||||
|
|
||||||
|
// Calculate new scale
|
||||||
|
const scaleMin = (routeData?.scale_min ?? 10) / SCALE_FACTOR;
|
||||||
|
const scaleMax = (routeData?.scale_max ?? 20) / SCALE_FACTOR;
|
||||||
|
|
||||||
|
const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1; // Zoom out/in
|
||||||
|
const newScale = Math.max(scaleMin, Math.min(scaleMax, scale * zoomFactor));
|
||||||
|
const actualZoomFactor = newScale / scale;
|
||||||
|
|
||||||
|
if (scale === newScale) {
|
||||||
|
// Сбрасываем флаг, если зум не изменился
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsUserInteracting(false);
|
||||||
|
}, 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update position to zoom towards mouse cursor
|
||||||
|
setPosition({
|
||||||
|
x: position.x + mouseX * (1 - actualZoomFactor),
|
||||||
|
y: position.y + mouseY * (1 - actualZoomFactor),
|
||||||
|
});
|
||||||
|
|
||||||
|
setScale(newScale);
|
||||||
|
|
||||||
|
// Сбрасываем флаг взаимодействия через задержку
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsUserInteracting(false);
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
applicationRef?.app.render();
|
||||||
|
console.log(position, scale, rotation);
|
||||||
|
}, [position, scale, rotation]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorBoundary>
|
||||||
|
{applicationRef?.app && (
|
||||||
|
<pixiGraphics
|
||||||
|
draw={(g) => {
|
||||||
|
const canvas = applicationRef.app.canvas;
|
||||||
|
g.clear();
|
||||||
|
g.rect(0, 0, canvas?.width ?? 0, canvas?.height ?? 0);
|
||||||
|
g.fill("#111");
|
||||||
|
}}
|
||||||
|
eventMode={"static"}
|
||||||
|
interactive
|
||||||
|
onPointerDown={handlePointerDown}
|
||||||
|
onGlobalPointerMove={handlePointerMove}
|
||||||
|
onPointerUp={handlePointerUp}
|
||||||
|
onPointerUpOutside={handlePointerUp}
|
||||||
|
onWheel={handleWheel}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<pixiContainer
|
||||||
|
x={position.x}
|
||||||
|
y={position.y}
|
||||||
|
scale={scale}
|
||||||
|
rotation={rotation}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</pixiContainer>
|
||||||
|
{/* Show center of the screen.
|
||||||
|
<pixiGraphics
|
||||||
|
eventMode="none"
|
||||||
|
draw={(g) => {
|
||||||
|
g.clear();
|
||||||
|
const center = screenCenter ?? {x: 0, y: 0};
|
||||||
|
g.circle(center.x, center.y, 1);
|
||||||
|
g.fill("#fff");
|
||||||
|
}}
|
||||||
|
/> */}
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
89
src/pages/Route/route-preview/LeftSidebar.tsx
Normal file
89
src/pages/Route/route-preview/LeftSidebar.tsx
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import { Stack, Typography, Button } from "@mui/material";
|
||||||
|
|
||||||
|
import { useNavigate, useNavigationType } from "react-router";
|
||||||
|
|
||||||
|
export function LeftSidebar() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const navigationType = useNavigationType(); // PUSH, POP, REPLACE
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
if (navigationType === "PUSH") {
|
||||||
|
navigate(-1);
|
||||||
|
} else {
|
||||||
|
navigate("/route");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack direction="column" width="300px" p={2} bgcolor="primary.main">
|
||||||
|
<button
|
||||||
|
onClick={handleBack}
|
||||||
|
type="button"
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 10,
|
||||||
|
color: "#fff",
|
||||||
|
backgroundColor: "#222",
|
||||||
|
borderRadius: 10,
|
||||||
|
width: "100%",
|
||||||
|
border: "none",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p>Назад</p>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Stack
|
||||||
|
direction="column"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
my={10}
|
||||||
|
>
|
||||||
|
<img src={"/Emblem.svg"} alt="logo" width={100} height={100} />
|
||||||
|
<Typography sx={{ mb: 2, color: "#fff" }} textAlign="center">
|
||||||
|
При поддержке Правительства Санкт-Петербурга
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack
|
||||||
|
direction="column"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
my={10}
|
||||||
|
spacing={2}
|
||||||
|
>
|
||||||
|
<Button variant="outlined" color="warning" fullWidth>
|
||||||
|
Достопримечательности
|
||||||
|
</Button>
|
||||||
|
<Button variant="outlined" color="warning" fullWidth>
|
||||||
|
Остановки
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack
|
||||||
|
direction="column"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
my={10}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={"/GET.png"}
|
||||||
|
alt="logo"
|
||||||
|
width="80%"
|
||||||
|
style={{ margin: "0 auto" }}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
textAlign="center"
|
||||||
|
mt="auto"
|
||||||
|
sx={{ color: "#fff" }}
|
||||||
|
>
|
||||||
|
#ВсемПоПути
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
301
src/pages/Route/route-preview/MapDataContext.tsx
Normal file
301
src/pages/Route/route-preview/MapDataContext.tsx
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
import { useParams } from "react-router";
|
||||||
|
import { authInstance, languageInstance } from "@shared";
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
ReactNode,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import {
|
||||||
|
RouteData,
|
||||||
|
SightData,
|
||||||
|
SightPatchData,
|
||||||
|
StationData,
|
||||||
|
StationPatchData,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
|
const MapDataContext = createContext<{
|
||||||
|
originalRouteData?: RouteData;
|
||||||
|
originalStationData?: StationData[];
|
||||||
|
originalSightData?: SightData[];
|
||||||
|
routeData?: RouteData;
|
||||||
|
stationData?: StationDataWithLanguage;
|
||||||
|
sightData?: SightData[];
|
||||||
|
|
||||||
|
isRouteLoading: boolean;
|
||||||
|
isStationLoading: boolean;
|
||||||
|
isSightLoading: boolean;
|
||||||
|
setScaleRange: (min: number, max: number) => void;
|
||||||
|
setMapRotation: (rotation: number) => void;
|
||||||
|
setMapCenter: (x: number, y: number) => void;
|
||||||
|
setStationOffset: (stationId: number, x: number, y: number) => void;
|
||||||
|
setSightCoordinates: (
|
||||||
|
sightId: number,
|
||||||
|
latitude: number,
|
||||||
|
longitude: number
|
||||||
|
) => void;
|
||||||
|
saveChanges: () => void;
|
||||||
|
}>({
|
||||||
|
originalRouteData: undefined,
|
||||||
|
originalStationData: undefined,
|
||||||
|
originalSightData: undefined,
|
||||||
|
routeData: undefined,
|
||||||
|
stationData: undefined,
|
||||||
|
sightData: undefined,
|
||||||
|
|
||||||
|
isRouteLoading: true,
|
||||||
|
isStationLoading: true,
|
||||||
|
isSightLoading: true,
|
||||||
|
setScaleRange: () => {},
|
||||||
|
setMapRotation: () => {},
|
||||||
|
setMapCenter: () => {},
|
||||||
|
setStationOffset: () => {},
|
||||||
|
setSightCoordinates: () => {},
|
||||||
|
saveChanges: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
type StationDataWithLanguage = {
|
||||||
|
[key: string]: StationData[];
|
||||||
|
};
|
||||||
|
export const MapDataProvider = observer(
|
||||||
|
({ children }: Readonly<{ children: ReactNode }>) => {
|
||||||
|
const { id: routeId } = useParams<{ id: string }>();
|
||||||
|
|
||||||
|
const [originalRouteData, setOriginalRouteData] = useState<RouteData>();
|
||||||
|
const [originalStationData, setOriginalStationData] =
|
||||||
|
useState<StationData[]>();
|
||||||
|
const [originalSightData, setOriginalSightData] = useState<SightData[]>();
|
||||||
|
|
||||||
|
const [routeData, setRouteData] = useState<RouteData>();
|
||||||
|
const [stationData, setStationData] = useState<StationDataWithLanguage>({
|
||||||
|
RU: [],
|
||||||
|
EN: [],
|
||||||
|
ZH: [],
|
||||||
|
});
|
||||||
|
const [sightData, setSightData] = useState<SightData[]>();
|
||||||
|
|
||||||
|
const [routeChanges, setRouteChanges] = useState<Partial<RouteData>>({});
|
||||||
|
const [stationChanges, setStationChanges] = useState<StationPatchData[]>(
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const [sightChanges, setSightChanges] = useState<SightPatchData[]>([]);
|
||||||
|
|
||||||
|
const [isRouteLoading, setIsRouteLoading] = useState(true);
|
||||||
|
const [isStationLoading, setIsStationLoading] = useState(true);
|
||||||
|
const [isSightLoading, setIsSightLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
setIsRouteLoading(true);
|
||||||
|
setIsStationLoading(true);
|
||||||
|
setIsSightLoading(true);
|
||||||
|
|
||||||
|
const [
|
||||||
|
routeResponse,
|
||||||
|
ruStationResponse,
|
||||||
|
enStationResponse,
|
||||||
|
zhStationResponse,
|
||||||
|
sightResponse,
|
||||||
|
] = await Promise.all([
|
||||||
|
authInstance.get(`/route/${routeId}`),
|
||||||
|
languageInstance("ru").get(`/route/${routeId}/station`),
|
||||||
|
languageInstance("en").get(`/route/${routeId}/station`),
|
||||||
|
languageInstance("zh").get(`/route/${routeId}/station`),
|
||||||
|
authInstance.get(`/route/${routeId}/sight`),
|
||||||
|
]);
|
||||||
|
|
||||||
|
setOriginalRouteData(routeResponse.data as RouteData);
|
||||||
|
setOriginalStationData(ruStationResponse.data as StationData[]);
|
||||||
|
setStationData({
|
||||||
|
ru: ruStationResponse.data as StationData[],
|
||||||
|
en: enStationResponse.data as StationData[],
|
||||||
|
zh: zhStationResponse.data as StationData[],
|
||||||
|
});
|
||||||
|
setOriginalSightData(sightResponse as unknown as SightData[]);
|
||||||
|
|
||||||
|
setIsRouteLoading(false);
|
||||||
|
setIsStationLoading(false);
|
||||||
|
setIsSightLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching data:", error);
|
||||||
|
setIsRouteLoading(false);
|
||||||
|
setIsStationLoading(false);
|
||||||
|
setIsSightLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
}, [routeId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// combine changes with original data
|
||||||
|
if (originalRouteData)
|
||||||
|
setRouteData({ ...originalRouteData, ...routeChanges });
|
||||||
|
if (originalSightData) setSightData(originalSightData);
|
||||||
|
}, [
|
||||||
|
originalRouteData,
|
||||||
|
originalSightData,
|
||||||
|
routeChanges,
|
||||||
|
stationChanges,
|
||||||
|
sightChanges,
|
||||||
|
]);
|
||||||
|
|
||||||
|
function setScaleRange(min: number, max: number) {
|
||||||
|
setRouteChanges((prev) => {
|
||||||
|
return { ...prev, scale_min: min, scale_max: max };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMapRotation(rotation: number) {
|
||||||
|
setRouteChanges((prev) => {
|
||||||
|
return { ...prev, rotate: rotation };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMapCenter(x: number, y: number) {
|
||||||
|
setRouteChanges((prev) => {
|
||||||
|
return { ...prev, center_latitude: x, center_longitude: y };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveChanges() {
|
||||||
|
await authInstance.patch(`/route/${routeId}`, routeData);
|
||||||
|
await saveStationChanges();
|
||||||
|
await saveSightChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveStationChanges() {
|
||||||
|
for (const station of stationChanges) {
|
||||||
|
await authInstance.patch(`/route/${routeId}/station`, station);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveSightChanges() {
|
||||||
|
console.log("sightChanges", sightChanges);
|
||||||
|
for (const sight of sightChanges) {
|
||||||
|
await authInstance.patch(`/route/${routeId}/sight`, sight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStationOffset(stationId: number, x: number, y: number) {
|
||||||
|
setStationChanges((prev) => {
|
||||||
|
let found = prev.find((station) => station.station_id === stationId);
|
||||||
|
if (found) {
|
||||||
|
found.offset_x = x;
|
||||||
|
found.offset_y = y;
|
||||||
|
|
||||||
|
return prev.map((station) => {
|
||||||
|
if (station.station_id === stationId) {
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
return station;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const foundStation = stationData.ru?.find(
|
||||||
|
(station) => station.id === stationId
|
||||||
|
);
|
||||||
|
if (foundStation) {
|
||||||
|
return [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
station_id: stationId,
|
||||||
|
offset_x: x,
|
||||||
|
offset_y: y,
|
||||||
|
transfers: foundStation.transfers,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSightCoordinates(
|
||||||
|
sightId: number,
|
||||||
|
latitude: number,
|
||||||
|
longitude: number
|
||||||
|
) {
|
||||||
|
setSightChanges((prev) => {
|
||||||
|
let found = prev.find((sight) => sight.sight_id === sightId);
|
||||||
|
if (found) {
|
||||||
|
found.latitude = latitude;
|
||||||
|
found.longitude = longitude;
|
||||||
|
|
||||||
|
return prev.map((sight) => {
|
||||||
|
if (sight.sight_id === sightId) {
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
return sight;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const foundSight = sightData?.find((sight) => sight.id === sightId);
|
||||||
|
if (foundSight) {
|
||||||
|
return [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
sight_id: sightId,
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("sightChanges", sightChanges);
|
||||||
|
}, [sightChanges]);
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({
|
||||||
|
originalRouteData,
|
||||||
|
originalStationData,
|
||||||
|
originalSightData,
|
||||||
|
routeData,
|
||||||
|
stationData,
|
||||||
|
sightData,
|
||||||
|
isRouteLoading,
|
||||||
|
isStationLoading,
|
||||||
|
isSightLoading,
|
||||||
|
setScaleRange,
|
||||||
|
setMapRotation,
|
||||||
|
setMapCenter,
|
||||||
|
saveChanges,
|
||||||
|
setStationOffset,
|
||||||
|
setSightCoordinates,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
originalRouteData,
|
||||||
|
originalStationData,
|
||||||
|
originalSightData,
|
||||||
|
routeData,
|
||||||
|
stationData,
|
||||||
|
sightData,
|
||||||
|
isRouteLoading,
|
||||||
|
isStationLoading,
|
||||||
|
isSightLoading,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MapDataContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</MapDataContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const useMapData = () => {
|
||||||
|
const context = useContext(MapDataContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useMapData must be used within a MapDataProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
232
src/pages/Route/route-preview/RightSidebar.tsx
Normal file
232
src/pages/Route/route-preview/RightSidebar.tsx
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
import { Button, Stack, TextField, Typography } from "@mui/material";
|
||||||
|
import { useMapData } from "./MapDataContext";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useTransform } from "./TransformContext";
|
||||||
|
import { coordinatesToLocal, localToCoordinates } from "./utils";
|
||||||
|
|
||||||
|
export function RightSidebar() {
|
||||||
|
const {
|
||||||
|
routeData,
|
||||||
|
setScaleRange,
|
||||||
|
saveChanges,
|
||||||
|
originalRouteData,
|
||||||
|
setMapRotation,
|
||||||
|
setMapCenter,
|
||||||
|
} = useMapData();
|
||||||
|
const {
|
||||||
|
rotation,
|
||||||
|
position,
|
||||||
|
screenToLocal,
|
||||||
|
screenCenter,
|
||||||
|
rotateToAngle,
|
||||||
|
setTransform,
|
||||||
|
} = useTransform();
|
||||||
|
const [minScale, setMinScale] = useState<number>(1);
|
||||||
|
const [maxScale, setMaxScale] = useState<number>(10);
|
||||||
|
const [localCenter, setLocalCenter] = useState<{ x: number; y: number }>({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
});
|
||||||
|
const [rotationDegrees, setRotationDegrees] = useState<number>(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (originalRouteData) {
|
||||||
|
setMinScale(originalRouteData.scale_min ?? 1);
|
||||||
|
setMaxScale(originalRouteData.scale_max ?? 10);
|
||||||
|
setRotationDegrees(originalRouteData.rotate ?? 0);
|
||||||
|
setLocalCenter({
|
||||||
|
x: originalRouteData.center_latitude ?? 0,
|
||||||
|
y: originalRouteData.center_longitude ?? 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [originalRouteData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (minScale && maxScale) {
|
||||||
|
setScaleRange(minScale, maxScale);
|
||||||
|
}
|
||||||
|
}, [minScale, maxScale]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setRotationDegrees(
|
||||||
|
((Math.round((rotation * 180) / Math.PI) % 360) + 360) % 360
|
||||||
|
);
|
||||||
|
}, [rotation]);
|
||||||
|
useEffect(() => {
|
||||||
|
setMapRotation(rotationDegrees);
|
||||||
|
}, [rotationDegrees]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const center = screenCenter ?? { x: 0, y: 0 };
|
||||||
|
const localCenter = screenToLocal(center.x, center.y);
|
||||||
|
const coordinates = localToCoordinates(localCenter.x, localCenter.y);
|
||||||
|
setLocalCenter({ x: coordinates.latitude, y: coordinates.longitude });
|
||||||
|
}, [position]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMapCenter(localCenter.x, localCenter.y);
|
||||||
|
}, [localCenter]);
|
||||||
|
|
||||||
|
function setRotationFromDegrees(degrees: number) {
|
||||||
|
rotateToAngle((degrees * Math.PI) / 180);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pan({ x, y }: { x: number; y: number }) {
|
||||||
|
const coordinates = coordinatesToLocal(x, y);
|
||||||
|
setTransform(coordinates.x, coordinates.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!routeData) {
|
||||||
|
console.error("routeData is null");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
position="absolute"
|
||||||
|
right={8}
|
||||||
|
top={8}
|
||||||
|
bottom={8}
|
||||||
|
p={2}
|
||||||
|
gap={1}
|
||||||
|
minWidth="400px"
|
||||||
|
bgcolor="primary.main"
|
||||||
|
border="1px solid #e0e0e0"
|
||||||
|
borderRadius={2}
|
||||||
|
>
|
||||||
|
<Typography variant="h6" sx={{ mb: 2, color: "#fff" }} textAlign="center">
|
||||||
|
Детали о достопримечательностях
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Stack spacing={2} direction="row" alignItems="center">
|
||||||
|
<TextField
|
||||||
|
type="number"
|
||||||
|
label="Минимальный масштаб"
|
||||||
|
variant="filled"
|
||||||
|
value={minScale}
|
||||||
|
onChange={(e) => setMinScale(Number(e.target.value))}
|
||||||
|
style={{ backgroundColor: "#222", borderRadius: 4 }}
|
||||||
|
sx={{
|
||||||
|
"& .MuiInputLabel-root": {
|
||||||
|
color: "#fff",
|
||||||
|
},
|
||||||
|
"& .MuiInputBase-input": {
|
||||||
|
color: "#fff",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
min: 0.1,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
type="number"
|
||||||
|
label="Максимальный масштаб"
|
||||||
|
variant="filled"
|
||||||
|
value={maxScale}
|
||||||
|
onChange={(e) => setMaxScale(Number(e.target.value))}
|
||||||
|
style={{ backgroundColor: "#222", borderRadius: 4, color: "#fff" }}
|
||||||
|
sx={{
|
||||||
|
"& .MuiInputLabel-root": {
|
||||||
|
color: "#fff",
|
||||||
|
},
|
||||||
|
"& .MuiInputBase-input": {
|
||||||
|
color: "#fff",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
min: 0.1,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
type="number"
|
||||||
|
label="Поворот (в градусах)"
|
||||||
|
variant="filled"
|
||||||
|
value={rotationDegrees}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = Number(e.target.value);
|
||||||
|
if (!isNaN(value)) {
|
||||||
|
setRotationFromDegrees(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.currentTarget.blur();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={{ backgroundColor: "#222", borderRadius: 4 }}
|
||||||
|
sx={{
|
||||||
|
"& .MuiInputLabel-root": {
|
||||||
|
color: "#fff",
|
||||||
|
},
|
||||||
|
"& .MuiInputBase-input": {
|
||||||
|
color: "#fff",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
min: 0,
|
||||||
|
max: 360,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Stack direction="row" spacing={2}>
|
||||||
|
<TextField
|
||||||
|
type="number"
|
||||||
|
label="Центр карты, широта"
|
||||||
|
variant="filled"
|
||||||
|
value={Math.round(localCenter.x * 100000) / 100000}
|
||||||
|
onChange={(e) => {
|
||||||
|
setLocalCenter((prev) => ({ ...prev, x: Number(e.target.value) }));
|
||||||
|
pan({ x: Number(e.target.value), y: localCenter.y });
|
||||||
|
}}
|
||||||
|
style={{ backgroundColor: "#222", borderRadius: 4 }}
|
||||||
|
sx={{
|
||||||
|
"& .MuiInputLabel-root": {
|
||||||
|
color: "#fff",
|
||||||
|
},
|
||||||
|
"& .MuiInputBase-input": {
|
||||||
|
color: "#fff",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
type="number"
|
||||||
|
label="Центр карты, высота"
|
||||||
|
variant="filled"
|
||||||
|
value={Math.round(localCenter.y * 100000) / 100000}
|
||||||
|
onChange={(e) => {
|
||||||
|
setLocalCenter((prev) => ({ ...prev, y: Number(e.target.value) }));
|
||||||
|
pan({ x: localCenter.x, y: Number(e.target.value) });
|
||||||
|
}}
|
||||||
|
style={{ backgroundColor: "#222", borderRadius: 4 }}
|
||||||
|
sx={{
|
||||||
|
"& .MuiInputLabel-root": {
|
||||||
|
color: "#fff",
|
||||||
|
},
|
||||||
|
"& .MuiInputBase-input": {
|
||||||
|
color: "#fff",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
sx={{ mt: 2 }}
|
||||||
|
onClick={() => {
|
||||||
|
saveChanges();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Сохранить изменения
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
107
src/pages/Route/route-preview/Sight.tsx
Normal file
107
src/pages/Route/route-preview/Sight.tsx
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useTransform } from "./TransformContext";
|
||||||
|
import { SightData } from "./types";
|
||||||
|
import { Assets, FederatedMouseEvent, Graphics, Texture } from "pixi.js";
|
||||||
|
|
||||||
|
import { SIGHT_SIZE, UP_SCALE } from "./Constants";
|
||||||
|
import { coordinatesToLocal, localToCoordinates } from "./utils";
|
||||||
|
import { useMapData } from "./MapDataContext";
|
||||||
|
|
||||||
|
interface SightProps {
|
||||||
|
sight: SightData;
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Sight({ sight, id }: Readonly<SightProps>) {
|
||||||
|
const { rotation, scale } = useTransform();
|
||||||
|
const { setSightCoordinates } = useMapData();
|
||||||
|
|
||||||
|
const [position, setPosition] = useState(
|
||||||
|
coordinatesToLocal(sight.latitude, sight.longitude)
|
||||||
|
);
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const [startPosition, setStartPosition] = useState({ x: 0, y: 0 });
|
||||||
|
const [startMousePosition, setStartMousePosition] = useState({ x: 0, y: 0 });
|
||||||
|
|
||||||
|
const handlePointerDown = (e: FederatedMouseEvent) => {
|
||||||
|
setIsDragging(true);
|
||||||
|
setStartPosition({
|
||||||
|
x: position.x,
|
||||||
|
y: position.y,
|
||||||
|
});
|
||||||
|
setStartMousePosition({
|
||||||
|
x: e.globalX,
|
||||||
|
y: e.globalY,
|
||||||
|
});
|
||||||
|
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
const handlePointerMove = (e: FederatedMouseEvent) => {
|
||||||
|
if (!isDragging) return;
|
||||||
|
const dx = (e.globalX - startMousePosition.x) / scale / UP_SCALE;
|
||||||
|
const dy = (e.globalY - startMousePosition.y) / scale / UP_SCALE;
|
||||||
|
const cos = Math.cos(rotation);
|
||||||
|
const sin = Math.sin(rotation);
|
||||||
|
const newPosition = {
|
||||||
|
x: startPosition.x + dx * cos + dy * sin,
|
||||||
|
y: startPosition.y - dx * sin + dy * cos,
|
||||||
|
};
|
||||||
|
setPosition(newPosition);
|
||||||
|
const coordinates = localToCoordinates(newPosition.x, newPosition.y);
|
||||||
|
setSightCoordinates(sight.id, coordinates.latitude, coordinates.longitude);
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePointerUp = (e: FederatedMouseEvent) => {
|
||||||
|
setIsDragging(false);
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
const [texture, setTexture] = useState(Texture.EMPTY);
|
||||||
|
useEffect(() => {
|
||||||
|
if (texture === Texture.EMPTY) {
|
||||||
|
Assets.load("/SightIcon.png").then((result) => {
|
||||||
|
setTexture(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [texture]);
|
||||||
|
|
||||||
|
function draw(g: Graphics) {
|
||||||
|
g.clear();
|
||||||
|
g.circle(0, 0, 20);
|
||||||
|
g.fill({ color: "#000" }); // Fill circle with primary color
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sight) {
|
||||||
|
console.error("sight is null");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<pixiContainer
|
||||||
|
rotation={-rotation}
|
||||||
|
eventMode="static"
|
||||||
|
interactive
|
||||||
|
onPointerDown={handlePointerDown}
|
||||||
|
onGlobalPointerMove={handlePointerMove}
|
||||||
|
onPointerUp={handlePointerUp}
|
||||||
|
onPointerUpOutside={handlePointerUp}
|
||||||
|
x={position.x * UP_SCALE - SIGHT_SIZE / 2} // Offset by half width to center
|
||||||
|
y={position.y * UP_SCALE - SIGHT_SIZE / 2} // Offset by half height to center
|
||||||
|
>
|
||||||
|
<pixiSprite texture={texture} width={SIGHT_SIZE} height={SIGHT_SIZE} />
|
||||||
|
<pixiGraphics draw={draw} x={SIGHT_SIZE} y={0} />
|
||||||
|
<pixiText
|
||||||
|
text={`${id + 1}`}
|
||||||
|
x={SIGHT_SIZE + 1}
|
||||||
|
y={0}
|
||||||
|
anchor={0.5}
|
||||||
|
style={{
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: "bold",
|
||||||
|
fill: "#ffffff",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</pixiContainer>
|
||||||
|
);
|
||||||
|
}
|
146
src/pages/Route/route-preview/Station.tsx
Normal file
146
src/pages/Route/route-preview/Station.tsx
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import { FederatedMouseEvent, Graphics } from "pixi.js";
|
||||||
|
import {
|
||||||
|
BACKGROUND_COLOR,
|
||||||
|
PATH_COLOR,
|
||||||
|
STATION_RADIUS,
|
||||||
|
STATION_OUTLINE_WIDTH,
|
||||||
|
UP_SCALE,
|
||||||
|
} from "./Constants";
|
||||||
|
import { useTransform } from "./TransformContext";
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { StationData } from "./types";
|
||||||
|
import { useMapData } from "./MapDataContext";
|
||||||
|
import { coordinatesToLocal } from "./utils";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
|
interface StationProps {
|
||||||
|
station: StationData;
|
||||||
|
ruLabel: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Station = observer(
|
||||||
|
({ station, ruLabel }: Readonly<StationProps>) => {
|
||||||
|
const draw = useCallback((g: Graphics) => {
|
||||||
|
g.clear();
|
||||||
|
const coordinates = coordinatesToLocal(
|
||||||
|
station.latitude,
|
||||||
|
station.longitude
|
||||||
|
);
|
||||||
|
g.circle(
|
||||||
|
coordinates.x * UP_SCALE,
|
||||||
|
coordinates.y * UP_SCALE,
|
||||||
|
STATION_RADIUS
|
||||||
|
);
|
||||||
|
g.fill({ color: PATH_COLOR });
|
||||||
|
g.stroke({ color: BACKGROUND_COLOR, width: STATION_OUTLINE_WIDTH });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<pixiContainer>
|
||||||
|
<pixiGraphics draw={draw} />
|
||||||
|
<StationLabel station={station} ruLabel={ruLabel} />
|
||||||
|
</pixiContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const StationLabel = observer(
|
||||||
|
({ station, ruLabel }: Readonly<StationProps>) => {
|
||||||
|
const { rotation, scale } = useTransform();
|
||||||
|
const { setStationOffset } = useMapData();
|
||||||
|
|
||||||
|
const [position, setPosition] = useState({
|
||||||
|
x: station.offset_x,
|
||||||
|
y: station.offset_y,
|
||||||
|
});
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const [startPosition, setStartPosition] = useState({ x: 0, y: 0 });
|
||||||
|
const [startMousePosition, setStartMousePosition] = useState({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!station) {
|
||||||
|
console.error("station is null");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePointerDown = (e: FederatedMouseEvent) => {
|
||||||
|
setIsDragging(true);
|
||||||
|
setStartPosition({
|
||||||
|
x: position.x,
|
||||||
|
y: position.y,
|
||||||
|
});
|
||||||
|
setStartMousePosition({
|
||||||
|
x: e.globalX,
|
||||||
|
y: e.globalY,
|
||||||
|
});
|
||||||
|
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
const handlePointerMove = (e: FederatedMouseEvent) => {
|
||||||
|
if (!isDragging) return;
|
||||||
|
const dx = e.globalX - startMousePosition.x;
|
||||||
|
const dy = e.globalY - startMousePosition.y;
|
||||||
|
const newPosition = {
|
||||||
|
x: startPosition.x + dx,
|
||||||
|
y: startPosition.y + dy,
|
||||||
|
};
|
||||||
|
setPosition(newPosition);
|
||||||
|
setStationOffset(station.id, newPosition.x, newPosition.y);
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePointerUp = (e: FederatedMouseEvent) => {
|
||||||
|
setIsDragging(false);
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
const coordinates = coordinatesToLocal(station.latitude, station.longitude);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<pixiContainer
|
||||||
|
eventMode="static"
|
||||||
|
interactive
|
||||||
|
onPointerDown={handlePointerDown}
|
||||||
|
onGlobalPointerMove={handlePointerMove}
|
||||||
|
onPointerUp={handlePointerUp}
|
||||||
|
onPointerUpOutside={handlePointerUp}
|
||||||
|
width={48}
|
||||||
|
height={48}
|
||||||
|
x={coordinates.x * UP_SCALE}
|
||||||
|
y={coordinates.y * UP_SCALE}
|
||||||
|
rotation={-rotation}
|
||||||
|
>
|
||||||
|
<pixiText
|
||||||
|
anchor={{ x: 1, y: 0.5 }}
|
||||||
|
text={station.name}
|
||||||
|
position={{
|
||||||
|
x: position.x / scale + 24,
|
||||||
|
y: position.y / scale,
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
fontSize: 26,
|
||||||
|
fontWeight: "bold",
|
||||||
|
fill: "#ffffff",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{ruLabel && (
|
||||||
|
<pixiText
|
||||||
|
anchor={{ x: 1, y: -1 }}
|
||||||
|
text={ruLabel}
|
||||||
|
position={{
|
||||||
|
x: position.x / scale + 24,
|
||||||
|
y: position.y / scale,
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "bold",
|
||||||
|
fill: "#CCCCCC",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</pixiContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
204
src/pages/Route/route-preview/TransformContext.tsx
Normal file
204
src/pages/Route/route-preview/TransformContext.tsx
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
ReactNode,
|
||||||
|
useContext,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
useCallback,
|
||||||
|
} from "react";
|
||||||
|
import { SCALE_FACTOR, UP_SCALE } from "./Constants";
|
||||||
|
|
||||||
|
const TransformContext = createContext<{
|
||||||
|
position: { x: number; y: number };
|
||||||
|
scale: number;
|
||||||
|
rotation: number;
|
||||||
|
screenCenter?: { x: number; y: number };
|
||||||
|
|
||||||
|
setPosition: React.Dispatch<React.SetStateAction<{ x: number; y: number }>>;
|
||||||
|
setScale: React.Dispatch<React.SetStateAction<number>>;
|
||||||
|
setRotation: React.Dispatch<React.SetStateAction<number>>;
|
||||||
|
screenToLocal: (x: number, y: number) => { x: number; y: number };
|
||||||
|
localToScreen: (x: number, y: number) => { x: number; y: number };
|
||||||
|
rotateToAngle: (to: number, fromPosition?: { x: number; y: number }) => void;
|
||||||
|
setTransform: (
|
||||||
|
latitude: number,
|
||||||
|
longitude: number,
|
||||||
|
rotationDegrees?: number,
|
||||||
|
scale?: number
|
||||||
|
) => void;
|
||||||
|
setScreenCenter: React.Dispatch<
|
||||||
|
React.SetStateAction<{ x: number; y: number } | undefined>
|
||||||
|
>;
|
||||||
|
}>({
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
scale: 1,
|
||||||
|
rotation: 0,
|
||||||
|
screenCenter: undefined,
|
||||||
|
setPosition: () => {},
|
||||||
|
setScale: () => {},
|
||||||
|
setRotation: () => {},
|
||||||
|
screenToLocal: () => ({ x: 0, y: 0 }),
|
||||||
|
localToScreen: () => ({ x: 0, y: 0 }),
|
||||||
|
rotateToAngle: () => {},
|
||||||
|
setTransform: () => {},
|
||||||
|
setScreenCenter: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Provider component
|
||||||
|
export const TransformProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||||
|
const [scale, setScale] = useState(1);
|
||||||
|
const [rotation, setRotation] = useState(0);
|
||||||
|
const [screenCenter, setScreenCenter] = useState<{ x: number; y: number }>();
|
||||||
|
|
||||||
|
const screenToLocal = useCallback(
|
||||||
|
(screenX: number, screenY: number) => {
|
||||||
|
// Translate point relative to current pan position
|
||||||
|
const translatedX = (screenX - position.x) / scale;
|
||||||
|
const translatedY = (screenY - position.y) / scale;
|
||||||
|
|
||||||
|
// Rotate point around center
|
||||||
|
const cosRotation = Math.cos(-rotation); // Negative rotation to reverse transform
|
||||||
|
const sinRotation = Math.sin(-rotation);
|
||||||
|
const rotatedX = translatedX * cosRotation - translatedY * sinRotation;
|
||||||
|
const rotatedY = translatedX * sinRotation + translatedY * cosRotation;
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: rotatedX / UP_SCALE,
|
||||||
|
y: rotatedY / UP_SCALE,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[position.x, position.y, scale, rotation]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Inverse of screenToLocal
|
||||||
|
const localToScreen = useCallback(
|
||||||
|
(localX: number, localY: number) => {
|
||||||
|
const upscaledX = localX * UP_SCALE;
|
||||||
|
const upscaledY = localY * UP_SCALE;
|
||||||
|
|
||||||
|
const cosRotation = Math.cos(rotation);
|
||||||
|
const sinRotation = Math.sin(rotation);
|
||||||
|
const rotatedX = upscaledX * cosRotation - upscaledY * sinRotation;
|
||||||
|
const rotatedY = upscaledX * sinRotation + upscaledY * cosRotation;
|
||||||
|
|
||||||
|
const translatedX = rotatedX * scale + position.x;
|
||||||
|
const translatedY = rotatedY * scale + position.y;
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: translatedX,
|
||||||
|
y: translatedY,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[position.x, position.y, scale, rotation]
|
||||||
|
);
|
||||||
|
|
||||||
|
const rotateToAngle = useCallback(
|
||||||
|
(to: number, fromPosition?: { x: number; y: number }) => {
|
||||||
|
const rotationDiff = to - rotation;
|
||||||
|
|
||||||
|
const center = screenCenter ?? { x: 0, y: 0 };
|
||||||
|
const cosDelta = Math.cos(rotationDiff);
|
||||||
|
const sinDelta = Math.sin(rotationDiff);
|
||||||
|
|
||||||
|
const currentFromPosition = fromPosition ?? position;
|
||||||
|
|
||||||
|
const newPosition = {
|
||||||
|
x:
|
||||||
|
center.x * (1 - cosDelta) +
|
||||||
|
currentFromPosition.x * cosDelta +
|
||||||
|
(center.y - currentFromPosition.y) * sinDelta,
|
||||||
|
y:
|
||||||
|
center.y * (1 - cosDelta) +
|
||||||
|
currentFromPosition.y * cosDelta +
|
||||||
|
(currentFromPosition.x - center.x) * sinDelta,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update both rotation and position in a single batch to avoid stale closure
|
||||||
|
setRotation(to);
|
||||||
|
setPosition(newPosition);
|
||||||
|
},
|
||||||
|
[rotation, position, screenCenter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const setTransform = useCallback(
|
||||||
|
(
|
||||||
|
latitude: number,
|
||||||
|
longitude: number,
|
||||||
|
rotationDegrees?: number,
|
||||||
|
useScale?: number
|
||||||
|
) => {
|
||||||
|
const selectedRotation =
|
||||||
|
rotationDegrees !== undefined
|
||||||
|
? (rotationDegrees * Math.PI) / 180
|
||||||
|
: rotation;
|
||||||
|
const selectedScale =
|
||||||
|
useScale !== undefined ? useScale / SCALE_FACTOR : scale;
|
||||||
|
const center = screenCenter ?? { x: 0, y: 0 };
|
||||||
|
|
||||||
|
console.log("center", center.x, center.y);
|
||||||
|
|
||||||
|
const newPosition = {
|
||||||
|
x: -latitude * UP_SCALE * selectedScale,
|
||||||
|
y: -longitude * UP_SCALE * selectedScale,
|
||||||
|
};
|
||||||
|
|
||||||
|
const cosRot = Math.cos(selectedRotation);
|
||||||
|
const sinRot = Math.sin(selectedRotation);
|
||||||
|
|
||||||
|
// Translate point relative to center, rotate, then translate back
|
||||||
|
const dx = newPosition.x;
|
||||||
|
const dy = newPosition.y;
|
||||||
|
newPosition.x = dx * cosRot - dy * sinRot + center.x;
|
||||||
|
newPosition.y = dx * sinRot + dy * cosRot + center.y;
|
||||||
|
|
||||||
|
// Batch state updates to avoid intermediate renders
|
||||||
|
setPosition(newPosition);
|
||||||
|
setRotation(selectedRotation);
|
||||||
|
setScale(selectedScale);
|
||||||
|
},
|
||||||
|
[rotation, scale, screenCenter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({
|
||||||
|
position,
|
||||||
|
scale,
|
||||||
|
rotation,
|
||||||
|
screenCenter,
|
||||||
|
setPosition,
|
||||||
|
setScale,
|
||||||
|
setRotation,
|
||||||
|
rotateToAngle,
|
||||||
|
screenToLocal,
|
||||||
|
localToScreen,
|
||||||
|
setTransform,
|
||||||
|
setScreenCenter,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
position,
|
||||||
|
scale,
|
||||||
|
rotation,
|
||||||
|
screenCenter,
|
||||||
|
rotateToAngle,
|
||||||
|
screenToLocal,
|
||||||
|
localToScreen,
|
||||||
|
setTransform,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TransformContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</TransformContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Custom hook for easy access to transform values
|
||||||
|
export const useTransform = () => {
|
||||||
|
const context = useContext(TransformContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useTransform must be used within a TransformProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
34
src/pages/Route/route-preview/TravelPath.tsx
Normal file
34
src/pages/Route/route-preview/TravelPath.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { Graphics } from "pixi.js";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import { PATH_COLOR, PATH_WIDTH } from "./Constants";
|
||||||
|
import { coordinatesToLocal } from "./utils";
|
||||||
|
|
||||||
|
interface TravelPathProps {
|
||||||
|
points: { x: number; y: number }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TravelPath({ points }: Readonly<TravelPathProps>) {
|
||||||
|
const draw = useCallback(
|
||||||
|
(g: Graphics) => {
|
||||||
|
g.clear();
|
||||||
|
const coordStart = coordinatesToLocal(points[0].x, points[0].y);
|
||||||
|
g.moveTo(coordStart.x, coordStart.y);
|
||||||
|
for (let i = 1; i <= points.length - 1; i++) {
|
||||||
|
const coordinates = coordinatesToLocal(points[i].x, points[i].y);
|
||||||
|
g.lineTo(coordinates.x, coordinates.y);
|
||||||
|
}
|
||||||
|
g.stroke({
|
||||||
|
color: PATH_COLOR,
|
||||||
|
width: PATH_WIDTH,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[points]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (points.length === 0) {
|
||||||
|
console.error("points is empty");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <pixiGraphics draw={draw} />;
|
||||||
|
}
|
43
src/pages/Route/route-preview/Widgets.tsx
Normal file
43
src/pages/Route/route-preview/Widgets.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { Stack, Typography } from "@mui/material";
|
||||||
|
|
||||||
|
export function Widgets() {
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
direction="column"
|
||||||
|
spacing={2}
|
||||||
|
position="absolute"
|
||||||
|
top={32}
|
||||||
|
left={32}
|
||||||
|
sx={{ pointerEvents: "none" }}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
bgcolor="primary.main"
|
||||||
|
width={361}
|
||||||
|
height={96}
|
||||||
|
p={2}
|
||||||
|
m={2}
|
||||||
|
borderRadius={2}
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
>
|
||||||
|
<Typography variant="h6" sx={{ color: "#fff" }}>
|
||||||
|
Станция
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
<Stack
|
||||||
|
bgcolor="primary.main"
|
||||||
|
width={223}
|
||||||
|
height={262}
|
||||||
|
p={2}
|
||||||
|
m={2}
|
||||||
|
borderRadius={2}
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
>
|
||||||
|
<Typography variant="h6" sx={{ color: "#fff" }}>
|
||||||
|
Погода
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
167
src/pages/Route/route-preview/index.tsx
Normal file
167
src/pages/Route/route-preview/index.tsx
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
import { useRef, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { Application, extend } from "@pixi/react";
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
Graphics,
|
||||||
|
Sprite,
|
||||||
|
Texture,
|
||||||
|
TilingSprite,
|
||||||
|
Text,
|
||||||
|
} from "pixi.js";
|
||||||
|
import { Stack } from "@mui/material";
|
||||||
|
import { MapDataProvider, useMapData } from "./MapDataContext";
|
||||||
|
import { TransformProvider, useTransform } from "./TransformContext";
|
||||||
|
import { InfiniteCanvas } from "./InfiniteCanvas";
|
||||||
|
|
||||||
|
import { UP_SCALE } from "./Constants";
|
||||||
|
import { Station } from "./Station";
|
||||||
|
import { TravelPath } from "./TravelPath";
|
||||||
|
import { LeftSidebar } from "./LeftSidebar";
|
||||||
|
import { RightSidebar } from "./RightSidebar";
|
||||||
|
import { Widgets } from "./Widgets";
|
||||||
|
import { coordinatesToLocal } from "./utils";
|
||||||
|
import { LanguageSwitcher } from "@widgets";
|
||||||
|
import { languageStore } from "@shared";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
|
extend({
|
||||||
|
Container,
|
||||||
|
Graphics,
|
||||||
|
Sprite,
|
||||||
|
Texture,
|
||||||
|
TilingSprite,
|
||||||
|
Text,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const RoutePreview = () => {
|
||||||
|
return (
|
||||||
|
<MapDataProvider>
|
||||||
|
<TransformProvider>
|
||||||
|
<Stack direction="row" height="100vh" width="100vw" overflow="hidden">
|
||||||
|
<LanguageSwitcher />
|
||||||
|
|
||||||
|
<LeftSidebar />
|
||||||
|
<Stack direction="row" flex={1} position="relative" height="100%">
|
||||||
|
<Widgets />
|
||||||
|
<RouteMap />
|
||||||
|
<RightSidebar />
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</TransformProvider>
|
||||||
|
</MapDataProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RouteMap = observer(() => {
|
||||||
|
const { language } = languageStore;
|
||||||
|
const { setPosition, screenToLocal, setTransform, screenCenter } =
|
||||||
|
useTransform();
|
||||||
|
const { routeData, stationData, sightData, originalRouteData } = useMapData();
|
||||||
|
console.log(stationData);
|
||||||
|
const [points, setPoints] = useState<{ x: number; y: number }[]>([]);
|
||||||
|
const [isSetup, setIsSetup] = useState(false);
|
||||||
|
|
||||||
|
const parentRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (originalRouteData) {
|
||||||
|
const path = originalRouteData?.path;
|
||||||
|
const points =
|
||||||
|
path?.map(([x, y]: [number, number]) => ({
|
||||||
|
x: x * UP_SCALE,
|
||||||
|
y: y * UP_SCALE,
|
||||||
|
})) ?? [];
|
||||||
|
setPoints(points);
|
||||||
|
}
|
||||||
|
}, [originalRouteData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSetup || !screenCenter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
originalRouteData?.center_latitude ===
|
||||||
|
originalRouteData?.center_longitude &&
|
||||||
|
originalRouteData?.center_latitude === 0
|
||||||
|
) {
|
||||||
|
if (points.length > 0) {
|
||||||
|
let boundingBox = {
|
||||||
|
from: { x: Infinity, y: Infinity },
|
||||||
|
to: { x: -Infinity, y: -Infinity },
|
||||||
|
};
|
||||||
|
for (const point of points) {
|
||||||
|
boundingBox.from.x = Math.min(boundingBox.from.x, point.x);
|
||||||
|
boundingBox.from.y = Math.min(boundingBox.from.y, point.y);
|
||||||
|
boundingBox.to.x = Math.max(boundingBox.to.x, point.x);
|
||||||
|
boundingBox.to.y = Math.max(boundingBox.to.y, point.y);
|
||||||
|
}
|
||||||
|
const newCenter = {
|
||||||
|
x: -(boundingBox.from.x + boundingBox.to.x) / 2,
|
||||||
|
y: -(boundingBox.from.y + boundingBox.to.y) / 2,
|
||||||
|
};
|
||||||
|
setPosition(newCenter);
|
||||||
|
setIsSetup(true);
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
originalRouteData?.center_latitude &&
|
||||||
|
originalRouteData?.center_longitude
|
||||||
|
) {
|
||||||
|
const coordinates = coordinatesToLocal(
|
||||||
|
originalRouteData?.center_latitude,
|
||||||
|
originalRouteData?.center_longitude
|
||||||
|
);
|
||||||
|
|
||||||
|
setTransform(
|
||||||
|
coordinates.x,
|
||||||
|
coordinates.y,
|
||||||
|
originalRouteData?.rotate,
|
||||||
|
originalRouteData?.scale_min
|
||||||
|
);
|
||||||
|
setIsSetup(true);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
points,
|
||||||
|
originalRouteData?.center_latitude,
|
||||||
|
originalRouteData?.center_longitude,
|
||||||
|
originalRouteData?.rotate,
|
||||||
|
isSetup,
|
||||||
|
screenCenter,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!routeData || !stationData || !sightData) {
|
||||||
|
console.error("routeData, stationData or sightData is null");
|
||||||
|
return <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ width: "100%", height: "100%" }} ref={parentRef}>
|
||||||
|
<Application resizeTo={parentRef} background="#fff">
|
||||||
|
<InfiniteCanvas>
|
||||||
|
<TravelPath points={points} />
|
||||||
|
{stationData[language].map((obj, index) => (
|
||||||
|
<Station
|
||||||
|
station={obj}
|
||||||
|
key={obj.id}
|
||||||
|
ruLabel={
|
||||||
|
language === "ru"
|
||||||
|
? stationData.en[index].name
|
||||||
|
: stationData.ru[index].name
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<pixiGraphics
|
||||||
|
draw={(g) => {
|
||||||
|
g.clear();
|
||||||
|
const localCenter = screenToLocal(0, 0);
|
||||||
|
g.circle(localCenter.x, localCenter.y, 10);
|
||||||
|
g.fill("#fff");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</InfiniteCanvas>
|
||||||
|
</Application>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
69
src/pages/Route/route-preview/types.ts
Normal file
69
src/pages/Route/route-preview/types.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
export interface RouteData {
|
||||||
|
carrier: string;
|
||||||
|
carrier_id: number;
|
||||||
|
center_latitude: number;
|
||||||
|
center_longitude: number;
|
||||||
|
governor_appeal: number;
|
||||||
|
id: number;
|
||||||
|
path: [number, number][];
|
||||||
|
rotate: number;
|
||||||
|
route_direction: boolean;
|
||||||
|
route_number: string;
|
||||||
|
route_sys_number: string;
|
||||||
|
scale_max: number;
|
||||||
|
scale_min: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StationTransferData {
|
||||||
|
bus: string;
|
||||||
|
metro_blue: string;
|
||||||
|
metro_green: string;
|
||||||
|
metro_orange: string;
|
||||||
|
metro_purple: string;
|
||||||
|
metro_red: string;
|
||||||
|
train: string;
|
||||||
|
tram: string;
|
||||||
|
trolleybus: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StationData {
|
||||||
|
address: string;
|
||||||
|
city_id?: number;
|
||||||
|
description: string;
|
||||||
|
id: number;
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
name: string;
|
||||||
|
offset_x: number;
|
||||||
|
offset_y: number;
|
||||||
|
system_name: string;
|
||||||
|
transfers: StationTransferData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StationPatchData {
|
||||||
|
station_id: number;
|
||||||
|
offset_x: number;
|
||||||
|
offset_y: number;
|
||||||
|
transfers: StationTransferData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SightPatchData {
|
||||||
|
sight_id: number;
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SightData {
|
||||||
|
address: string;
|
||||||
|
city: string;
|
||||||
|
city_id: number;
|
||||||
|
id: number;
|
||||||
|
latitude: number;
|
||||||
|
left_article: number;
|
||||||
|
longitude: number;
|
||||||
|
name: string;
|
||||||
|
preview_media: number;
|
||||||
|
thumbnail: string; // uuid
|
||||||
|
watermark_lu: string; // uuid
|
||||||
|
watermark_rd: string; // uuid
|
||||||
|
}
|
14
src/pages/Route/route-preview/utils.ts
Normal file
14
src/pages/Route/route-preview/utils.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// approximation
|
||||||
|
export function coordinatesToLocal(latitude: number, longitude: number) {
|
||||||
|
return {
|
||||||
|
x: longitude,
|
||||||
|
y: -latitude*2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function localToCoordinates(x: number, y: number) {
|
||||||
|
return {
|
||||||
|
longitude: x,
|
||||||
|
latitude: -y/2,
|
||||||
|
}
|
||||||
|
}
|
@ -38,7 +38,7 @@ export const SightListPage = observer(() => {
|
|||||||
renderCell: (params: GridRenderCellParams) => {
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full gap-7 justify-center items-center">
|
<div className="flex h-full gap-7 justify-center items-center">
|
||||||
<button onClick={() => navigate(`/sight/${params.row.id}`)}>
|
<button onClick={() => navigate(`/sight/${params.row.id}/edit`)}>
|
||||||
<Pencil size={20} className="text-blue-500" />
|
<Pencil size={20} className="text-blue-500" />
|
||||||
</button>
|
</button>
|
||||||
{/* <button onClick={() => navigate(`/sight/${params.row.id}`)}>
|
{/* <button onClick={() => navigate(`/sight/${params.row.id}`)}>
|
||||||
|
@ -2,19 +2,14 @@ import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
|||||||
import { languageStore, snapshotStore } from "@shared";
|
import { languageStore, snapshotStore } from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { DatabaseBackup, Eye, Trash2 } from "lucide-react";
|
import { DatabaseBackup, Trash2 } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import {
|
import { CreateButton, DeleteModal, SnapshotRestore } from "@widgets";
|
||||||
CreateButton,
|
|
||||||
DeleteModal,
|
|
||||||
LanguageSwitcher,
|
|
||||||
SnapshotRestore,
|
|
||||||
} from "@widgets";
|
|
||||||
|
|
||||||
export const SnapshotListPage = observer(() => {
|
export const SnapshotListPage = observer(() => {
|
||||||
const { snapshots, getSnapshots, deleteSnapshot, restoreSnapshot } =
|
const { snapshots, getSnapshots, deleteSnapshot, restoreSnapshot } =
|
||||||
snapshotStore;
|
snapshotStore;
|
||||||
const navigate = useNavigate();
|
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
const [rowId, setRowId] = useState<string | null>(null); // Lifted state
|
const [rowId, setRowId] = useState<string | null>(null); // Lifted state
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
@ -76,8 +71,6 @@ export const SnapshotListPage = observer(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LanguageSwitcher />
|
|
||||||
|
|
||||||
<div style={{ width: "100%" }}>
|
<div style={{ width: "100%" }}>
|
||||||
<div className="flex justify-between items-center mb-10">
|
<div className="flex justify-between items-center mb-10">
|
||||||
<h1 className="text-2xl ">Снапшоты</h1>
|
<h1 className="text-2xl ">Снапшоты</h1>
|
||||||
|
177
src/pages/Station/StationEditPage/index.tsx
Normal file
177
src/pages/Station/StationEditPage/index.tsx
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Paper,
|
||||||
|
TextField,
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { ArrowLeft, Save } from "lucide-react";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { stationsStore, languageStore, cityStore } from "@shared";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { LanguageSwitcher } from "@widgets";
|
||||||
|
|
||||||
|
export const StationEditPage = observer(() => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const { language } = languageStore;
|
||||||
|
const { id } = useParams();
|
||||||
|
const {
|
||||||
|
editStationData,
|
||||||
|
getEditStation,
|
||||||
|
setEditCommonData,
|
||||||
|
editStation,
|
||||||
|
setLanguageEditStationData,
|
||||||
|
} = stationsStore;
|
||||||
|
const { cities, getCities } = cityStore;
|
||||||
|
|
||||||
|
const handleEdit = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
await editStation(Number(id));
|
||||||
|
toast.success("Станция успешно обновлена");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating station:", error);
|
||||||
|
toast.error("Ошибка при обновлении станции");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchAndSetStationData = async () => {
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
const stationId = Number(id);
|
||||||
|
await getEditStation(stationId);
|
||||||
|
await getCities(language);
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchAndSetStationData();
|
||||||
|
}, [id, language]);
|
||||||
|
|
||||||
|
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(-1)}
|
||||||
|
>
|
||||||
|
<ArrowLeft size={20} />
|
||||||
|
Назад
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-10 w-full items-end">
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Название"
|
||||||
|
value={editStationData[language].name || ""}
|
||||||
|
required
|
||||||
|
onChange={(e) =>
|
||||||
|
setLanguageEditStationData(language, {
|
||||||
|
name: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel id="direction-label">Прямой/обратный маршрут</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="direction-label"
|
||||||
|
value={editStationData.common.direction ? "Прямой" : "Обратный"}
|
||||||
|
label="Прямой/обратный маршрут"
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditCommonData({
|
||||||
|
direction: e.target.value === "Прямой",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MenuItem value="Прямой">Прямой</MenuItem>
|
||||||
|
<MenuItem value="Обратный">Обратный</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Описание"
|
||||||
|
value={editStationData[language].description || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setLanguageEditStationData(language, {
|
||||||
|
description: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Адрес"
|
||||||
|
value={editStationData[language].address || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setLanguageEditStationData(language, {
|
||||||
|
address: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Координаты"
|
||||||
|
value={`${editStationData.common.latitude} ${editStationData.common.longitude}`}
|
||||||
|
onChange={(e) => {
|
||||||
|
const [latitude, longitude] = e.target.value.split(" ").map(Number);
|
||||||
|
if (!isNaN(latitude) && !isNaN(longitude)) {
|
||||||
|
setEditCommonData({
|
||||||
|
latitude: latitude,
|
||||||
|
longitude: longitude,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel>Город</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={editStationData.common.city_id || ""}
|
||||||
|
label="Город"
|
||||||
|
onChange={(e) => {
|
||||||
|
const selectedCity = cities[language].find(
|
||||||
|
(city) => city.id === e.target.value
|
||||||
|
);
|
||||||
|
setEditCommonData({
|
||||||
|
city_id: e.target.value as number,
|
||||||
|
city: selectedCity?.name || "",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{cities[language].map((city) => (
|
||||||
|
<MenuItem key={city.id} value={city.id}>
|
||||||
|
{city.name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
className="w-min flex gap-2 items-center"
|
||||||
|
startIcon={<Save size={20} />}
|
||||||
|
onClick={handleEdit}
|
||||||
|
disabled={isLoading || !editStationData[language]?.name}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<Loader2 size={20} className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
"Обновить"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
});
|
@ -2,19 +2,19 @@ import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
|||||||
import { languageStore, stationsStore } from "@shared";
|
import { languageStore, stationsStore } from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Eye, Trash2 } from "lucide-react";
|
import { Eye, Pencil, Trash2 } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets";
|
import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets";
|
||||||
|
|
||||||
export const StationListPage = observer(() => {
|
export const StationListPage = observer(() => {
|
||||||
const { stations, getStations, deleteStation } = stationsStore;
|
const { stationLists, getStationList, deleteStation } = stationsStore;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
const [rowId, setRowId] = useState<number | null>(null); // Lifted state
|
const [rowId, setRowId] = useState<number | null>(null); // Lifted state
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getStations();
|
getStationList();
|
||||||
}, [language]);
|
}, [language]);
|
||||||
|
|
||||||
const columns: GridColDef[] = [
|
const columns: GridColDef[] = [
|
||||||
@ -57,6 +57,9 @@ export const StationListPage = observer(() => {
|
|||||||
renderCell: (params: GridRenderCellParams) => {
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full gap-7 justify-center items-center">
|
<div className="flex h-full gap-7 justify-center items-center">
|
||||||
|
<button onClick={() => navigate(`/station/${params.row.id}/edit`)}>
|
||||||
|
<Pencil size={20} className="text-blue-500" />
|
||||||
|
</button>
|
||||||
<button onClick={() => navigate(`/station/${params.row.id}`)}>
|
<button onClick={() => navigate(`/station/${params.row.id}`)}>
|
||||||
<Eye size={20} className="text-green-500" />
|
<Eye size={20} className="text-green-500" />
|
||||||
</button>
|
</button>
|
||||||
@ -74,7 +77,7 @@ export const StationListPage = observer(() => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const rows = stations.map((station) => ({
|
const rows = stationLists[language].data.map((station: any) => ({
|
||||||
id: station.id,
|
id: station.id,
|
||||||
name: station.name,
|
name: station.name,
|
||||||
system_name: station.system_name,
|
system_name: station.system_name,
|
||||||
|
77
src/pages/Station/StationPreviewPage/index.tsx
Normal file
77
src/pages/Station/StationPreviewPage/index.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { Paper } from "@mui/material";
|
||||||
|
import { languageStore, stationsStore } from "@shared";
|
||||||
|
import { LanguageSwitcher } 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 StationPreviewPage = observer(() => {
|
||||||
|
const { id } = useParams();
|
||||||
|
const { stationPreview, getStationPreview } = stationsStore;
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { language } = languageStore;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
if (id) {
|
||||||
|
await getStationPreview(Number(id));
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [id, language]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
||||||
|
<LanguageSwitcher />
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onClick={() => navigate(-1)}
|
||||||
|
>
|
||||||
|
<ArrowLeft size={20} />
|
||||||
|
Назад
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-10 w-full">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<h1 className="text-lg font-bold">Название</h1>
|
||||||
|
<p>{stationPreview[id!]?.[language]?.data.name}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<h1 className="text-lg font-bold">Системное название</h1>
|
||||||
|
<p>{stationPreview[id!]?.[language]?.data.system_name}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<h1 className="text-lg font-bold">Направление</h1>
|
||||||
|
<p
|
||||||
|
className={`${
|
||||||
|
stationPreview[id!]?.[language]?.data.direction
|
||||||
|
? "text-green-500"
|
||||||
|
: "text-red-500"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{stationPreview[id!]?.[language]?.data.direction
|
||||||
|
? "Прямой"
|
||||||
|
: "Обратный"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{stationPreview[id!]?.[language]?.data.address && (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<h1 className="text-lg font-bold">Адрес</h1>
|
||||||
|
<p>{stationPreview[id!]?.[language]?.data.address}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{stationPreview[id!]?.[language]?.data.description && (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<h1 className="text-lg font-bold">Описание</h1>
|
||||||
|
<p>{stationPreview[id!]?.[language]?.data.description}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
});
|
@ -1 +1,4 @@
|
|||||||
export * from "./StationListPage";
|
export * from "./StationListPage";
|
||||||
|
export * from "./StationCreatePage";
|
||||||
|
export * from "./StationPreviewPage";
|
||||||
|
export * from "./StationEditPage";
|
||||||
|
129
src/pages/User/UserCreatePage/index.tsx
Normal file
129
src/pages/User/UserCreatePage/index.tsx
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Paper,
|
||||||
|
TextField,
|
||||||
|
Checkbox,
|
||||||
|
FormControlLabel,
|
||||||
|
} 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 { userStore } from "@shared";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export const UserCreatePage = observer(() => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { createUserData, setCreateUserData, createUser } = userStore;
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleCreate = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
await createUser();
|
||||||
|
toast.success("Пользователь успешно создан");
|
||||||
|
navigate("/user");
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Ошибка при создании пользователя");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onClick={() => navigate(-1)}
|
||||||
|
>
|
||||||
|
<ArrowLeft size={20} />
|
||||||
|
Назад
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-10 w-full items-end">
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Имя"
|
||||||
|
value={createUserData.name || ""}
|
||||||
|
required
|
||||||
|
onChange={(e) =>
|
||||||
|
setCreateUserData(
|
||||||
|
e.target.value,
|
||||||
|
createUserData.email || "",
|
||||||
|
createUserData.password || "",
|
||||||
|
createUserData.is_admin || false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Email"
|
||||||
|
value={createUserData.email || ""}
|
||||||
|
required
|
||||||
|
onChange={(e) =>
|
||||||
|
setCreateUserData(
|
||||||
|
createUserData.name || "",
|
||||||
|
e.target.value,
|
||||||
|
createUserData.password || "",
|
||||||
|
createUserData.is_admin || false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Пароль"
|
||||||
|
value={createUserData.password || ""}
|
||||||
|
required
|
||||||
|
onChange={(e) =>
|
||||||
|
setCreateUserData(
|
||||||
|
createUserData.name || "",
|
||||||
|
createUserData.email || "",
|
||||||
|
e.target.value,
|
||||||
|
createUserData.is_admin || false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="w-full flex flex-col items-start">
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={createUserData.is_admin || false}
|
||||||
|
onChange={(e) => {
|
||||||
|
setCreateUserData(
|
||||||
|
createUserData.name || "",
|
||||||
|
createUserData.email || "",
|
||||||
|
createUserData.password || "",
|
||||||
|
e.target.checked
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Администратор"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
className="w-min flex gap-2 items-center"
|
||||||
|
startIcon={<Save size={20} />}
|
||||||
|
onClick={handleCreate}
|
||||||
|
disabled={
|
||||||
|
isLoading || !createUserData.name || !createUserData.password
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<Loader2 size={20} className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
"Создать"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
});
|
139
src/pages/User/UserEditPage/index.tsx
Normal file
139
src/pages/User/UserEditPage/index.tsx
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormControlLabel,
|
||||||
|
Checkbox,
|
||||||
|
Paper,
|
||||||
|
TextField,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { ArrowLeft, Save } from "lucide-react";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { userStore } from "@shared";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export const UserEditPage = observer(() => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const { id } = useParams();
|
||||||
|
const { editUserData, editUser, getUser, setEditUserData } = userStore;
|
||||||
|
|
||||||
|
const handleEdit = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
await editUser(Number(id));
|
||||||
|
toast.success("Пользователь успешно обновлен");
|
||||||
|
navigate("/user");
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Ошибка при обновлении пользователя");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
if (id) {
|
||||||
|
const data = await getUser(Number(id));
|
||||||
|
|
||||||
|
setEditUserData(
|
||||||
|
data?.name || "",
|
||||||
|
data?.email || "",
|
||||||
|
data?.password || "",
|
||||||
|
data?.is_admin || false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onClick={() => navigate(-1)}
|
||||||
|
>
|
||||||
|
<ArrowLeft size={20} />
|
||||||
|
Назад
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-10 w-full items-start">
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Имя"
|
||||||
|
value={editUserData.name || ""}
|
||||||
|
required
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditUserData(
|
||||||
|
e.target.value,
|
||||||
|
editUserData.email || "",
|
||||||
|
editUserData.password || "",
|
||||||
|
editUserData.is_admin || false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Email"
|
||||||
|
value={editUserData.email || ""}
|
||||||
|
required
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditUserData(
|
||||||
|
editUserData.name || "",
|
||||||
|
e.target.value,
|
||||||
|
editUserData.password || "",
|
||||||
|
editUserData.is_admin || false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Пароль"
|
||||||
|
value={editUserData.password || ""}
|
||||||
|
required
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditUserData(
|
||||||
|
editUserData.name || "",
|
||||||
|
editUserData.email || "",
|
||||||
|
e.target.value,
|
||||||
|
editUserData.is_admin || false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={editUserData.is_admin || false}
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditUserData(
|
||||||
|
editUserData.name || "",
|
||||||
|
editUserData.email || "",
|
||||||
|
editUserData.password || "",
|
||||||
|
e.target.checked
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Администратор"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
className="w-min flex gap-2 items-center self-end"
|
||||||
|
startIcon={<Save size={20} />}
|
||||||
|
onClick={handleEdit}
|
||||||
|
disabled={isLoading || !editUserData.name || !editUserData.email}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<Loader2 size={20} className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
"Обновить"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
});
|
@ -1,21 +1,21 @@
|
|||||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||||
import { languageStore, userStore } from "@shared";
|
import { userStore } from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Eye, Trash2 } from "lucide-react";
|
import { Pencil, Trash2 } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { DeleteModal, LanguageSwitcher } from "@widgets";
|
|
||||||
|
import { CreateButton, DeleteModal } from "@widgets";
|
||||||
|
|
||||||
export const UserListPage = observer(() => {
|
export const UserListPage = observer(() => {
|
||||||
const { users, getUsers, deleteUser } = userStore;
|
const { users, getUsers, deleteUser } = userStore;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
const [rowId, setRowId] = useState<number | null>(null); // Lifted state
|
const [rowId, setRowId] = useState<number | null>(null); // Lifted state
|
||||||
const { language } = languageStore;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getUsers();
|
getUsers();
|
||||||
}, [language]);
|
}, []);
|
||||||
|
|
||||||
const columns: GridColDef[] = [
|
const columns: GridColDef[] = [
|
||||||
{
|
{
|
||||||
@ -56,6 +56,15 @@ export const UserListPage = observer(() => {
|
|||||||
renderCell: (params: GridRenderCellParams) => {
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full gap-7 justify-center items-center">
|
<div className="flex h-full gap-7 justify-center items-center">
|
||||||
|
<button>
|
||||||
|
<Pencil
|
||||||
|
size={20}
|
||||||
|
className="text-blue-500"
|
||||||
|
onClick={() => {
|
||||||
|
navigate(`/user/${params.row.id}/edit`);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
@ -70,7 +79,7 @@ export const UserListPage = observer(() => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const rows = users.map((user) => ({
|
const rows = users.data?.map((user) => ({
|
||||||
id: user.id,
|
id: user.id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
is_admin: user.is_admin,
|
is_admin: user.is_admin,
|
||||||
@ -79,9 +88,11 @@ export const UserListPage = observer(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LanguageSwitcher />
|
<div className="w-full">
|
||||||
|
<div className="flex justify-between items-center mb-10">
|
||||||
<div style={{ width: "100%" }}>
|
<h1 className="text-2xl">Пользователи</h1>
|
||||||
|
<CreateButton label="Создать пользователя" path="/user/create" />
|
||||||
|
</div>
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={rows}
|
rows={rows}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
@ -96,6 +107,7 @@ export const UserListPage = observer(() => {
|
|||||||
if (rowId) {
|
if (rowId) {
|
||||||
await deleteUser(rowId);
|
await deleteUser(rowId);
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsDeleteModalOpen(false);
|
setIsDeleteModalOpen(false);
|
||||||
setRowId(null);
|
setRowId(null);
|
||||||
}}
|
}}
|
||||||
|
@ -1 +1,3 @@
|
|||||||
export * from "./UserListPage";
|
export * from "./UserListPage";
|
||||||
|
export * from "./UserCreatePage";
|
||||||
|
export * from "./UserEditPage";
|
||||||
|
@ -14,7 +14,6 @@ import { Loader2 } from "lucide-react";
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { LanguageSwitcher } from "@widgets";
|
|
||||||
|
|
||||||
export const VehicleCreatePage = observer(() => {
|
export const VehicleCreatePage = observer(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -32,8 +31,8 @@ export const VehicleCreatePage = observer(() => {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
await vehicleStore.createVehicle(
|
await vehicleStore.createVehicle(
|
||||||
Number(tailNumber),
|
Number(tailNumber),
|
||||||
type,
|
Number(type),
|
||||||
carrierStore.carriers.find((c) => c.id === carrierId)?.full_name!,
|
carrierStore.carriers.data.find((c) => c.id === carrierId)?.full_name!,
|
||||||
carrierId!
|
carrierId!
|
||||||
);
|
);
|
||||||
toast.success("Транспорт успешно создан");
|
toast.success("Транспорт успешно создан");
|
||||||
@ -46,11 +45,10 @@ export const VehicleCreatePage = observer(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
||||||
<LanguageSwitcher />
|
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
onClick={() => navigate("/vehicle")}
|
onClick={() => navigate(-1)}
|
||||||
>
|
>
|
||||||
<ArrowLeft size={20} />
|
<ArrowLeft size={20} />
|
||||||
Назад
|
Назад
|
||||||
@ -90,7 +88,7 @@ export const VehicleCreatePage = observer(() => {
|
|||||||
required
|
required
|
||||||
onChange={(e) => setCarrierId(e.target.value as number)}
|
onChange={(e) => setCarrierId(e.target.value as number)}
|
||||||
>
|
>
|
||||||
{carrierStore.carriers.map((carrier) => (
|
{carrierStore.carriers.data.map((carrier) => (
|
||||||
<MenuItem key={carrier.id} value={carrier.id}>
|
<MenuItem key={carrier.id} value={carrier.id}>
|
||||||
{carrier.full_name}
|
{carrier.full_name}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
140
src/pages/Vehicle/VehicleEditPage/index.tsx
Normal file
140
src/pages/Vehicle/VehicleEditPage/index.tsx
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import {
|
||||||
|
Paper,
|
||||||
|
TextField,
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
Button,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { ArrowLeft, Loader2, Save } from "lucide-react";
|
||||||
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { carrierStore, VEHICLE_TYPES, vehicleStore } from "@shared";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
|
export const VehicleEditPage = observer(() => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { id } = useParams();
|
||||||
|
const {
|
||||||
|
getVehicle,
|
||||||
|
vehicle,
|
||||||
|
editVehicleData,
|
||||||
|
setEditVehicleData,
|
||||||
|
editVehicle,
|
||||||
|
} = vehicleStore;
|
||||||
|
const { getCarriers } = carrierStore;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
await getVehicle(Number(id));
|
||||||
|
await getCarriers();
|
||||||
|
setEditVehicleData({
|
||||||
|
tail_number: vehicle[Number(id)]?.vehicle.tail_number,
|
||||||
|
type: vehicle[Number(id)]?.vehicle.type,
|
||||||
|
carrier: vehicle[Number(id)]?.vehicle.carrier,
|
||||||
|
carrier_id: vehicle[Number(id)]?.vehicle.carrier_id,
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
}, [id]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const handleEdit = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
await editVehicle(Number(id), editVehicleData);
|
||||||
|
toast.success("Транспортное средство успешно обновлено");
|
||||||
|
navigate("/vehicle");
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Ошибка при обновлении транспортного средства");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onClick={() => navigate(-1)}
|
||||||
|
>
|
||||||
|
<ArrowLeft size={20} />
|
||||||
|
Назад
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-10 w-full items-end">
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Бортовой номер"
|
||||||
|
value={editVehicleData.tail_number}
|
||||||
|
required
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditVehicleData({
|
||||||
|
...editVehicleData,
|
||||||
|
tail_number: Number(e.target.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel>Тип</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={editVehicleData.type}
|
||||||
|
label="Тип"
|
||||||
|
required
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditVehicleData({ ...editVehicleData, type: 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={editVehicleData.carrier_id}
|
||||||
|
label="Перевозчик"
|
||||||
|
required
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditVehicleData({
|
||||||
|
...editVehicleData,
|
||||||
|
carrier_id: e.target.value as number,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{carrierStore.carriers.data.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={handleEdit}
|
||||||
|
disabled={
|
||||||
|
isLoading ||
|
||||||
|
!editVehicleData.tail_number ||
|
||||||
|
!editVehicleData.type ||
|
||||||
|
!editVehicleData.carrier_id
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<Loader2 size={20} className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
"Сохранить"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
});
|
@ -2,9 +2,9 @@ import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
|||||||
import { carrierStore, languageStore, vehicleStore } from "@shared";
|
import { carrierStore, languageStore, vehicleStore } from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Eye, Trash2 } from "lucide-react";
|
import { Eye, Pencil, Trash2 } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets";
|
import { CreateButton, DeleteModal } from "@widgets";
|
||||||
import { VEHICLE_TYPES } from "@shared";
|
import { VEHICLE_TYPES } from "@shared";
|
||||||
|
|
||||||
export const VehicleListPage = observer(() => {
|
export const VehicleListPage = observer(() => {
|
||||||
@ -25,18 +25,15 @@ export const VehicleListPage = observer(() => {
|
|||||||
field: "tail_number",
|
field: "tail_number",
|
||||||
headerName: "Бортовой номер",
|
headerName: "Бортовой номер",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
align: "center",
|
|
||||||
headerAlign: "center",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "type",
|
field: "type",
|
||||||
headerName: "Тип",
|
headerName: "Тип",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
align: "center",
|
|
||||||
headerAlign: "center",
|
|
||||||
renderCell: (params: GridRenderCellParams) => {
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full gap-7 justify-center items-center">
|
<div className="flex h-full gap-7 items-center">
|
||||||
{VEHICLE_TYPES.find((type) => type.value === params.row.type)
|
{VEHICLE_TYPES.find((type) => type.value === params.row.type)
|
||||||
?.label || params.row.type}
|
?.label || params.row.type}
|
||||||
</div>
|
</div>
|
||||||
@ -47,27 +44,26 @@ export const VehicleListPage = observer(() => {
|
|||||||
field: "carrier",
|
field: "carrier",
|
||||||
headerName: "Перевозчик",
|
headerName: "Перевозчик",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
align: "center",
|
|
||||||
headerAlign: "center",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "city",
|
field: "city",
|
||||||
headerName: "Город",
|
headerName: "Город",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
align: "center",
|
|
||||||
headerAlign: "center",
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
field: "actions",
|
field: "actions",
|
||||||
headerName: "Действия",
|
headerName: "Действия",
|
||||||
flex: 1,
|
width: 200,
|
||||||
align: "center",
|
align: "center",
|
||||||
headerAlign: "center",
|
headerAlign: "center",
|
||||||
|
|
||||||
renderCell: (params: GridRenderCellParams) => {
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full gap-7 justify-center items-center">
|
<div className="flex h-full gap-7 justify-center items-center">
|
||||||
|
<button onClick={() => navigate(`/vehicle/${params.row.id}/edit`)}>
|
||||||
|
<Pencil size={20} className="text-blue-500" />
|
||||||
|
</button>
|
||||||
<button onClick={() => navigate(`/vehicle/${params.row.id}`)}>
|
<button onClick={() => navigate(`/vehicle/${params.row.id}`)}>
|
||||||
<Eye size={20} className="text-green-500" />
|
<Eye size={20} className="text-green-500" />
|
||||||
</button>
|
</button>
|
||||||
@ -85,19 +81,18 @@ export const VehicleListPage = observer(() => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const rows = vehicles.map((vehicle) => ({
|
const rows = vehicles.data?.map((vehicle) => ({
|
||||||
id: vehicle.vehicle.id,
|
id: vehicle.vehicle.id,
|
||||||
tail_number: vehicle.vehicle.tail_number,
|
tail_number: vehicle.vehicle.tail_number,
|
||||||
type: vehicle.vehicle.type,
|
type: vehicle.vehicle.type,
|
||||||
carrier: vehicle.vehicle.carrier,
|
carrier: vehicle.vehicle.carrier,
|
||||||
city: carriers.find((carrier) => carrier.id === vehicle.vehicle.carrier_id)
|
city: carriers.data?.find(
|
||||||
?.city,
|
(carrier) => carrier.id === vehicle.vehicle.carrier_id
|
||||||
|
)?.city,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LanguageSwitcher />
|
|
||||||
|
|
||||||
<div style={{ width: "100%" }}>
|
<div style={{ width: "100%" }}>
|
||||||
<div className="flex justify-between items-center mb-10">
|
<div className="flex justify-between items-center mb-10">
|
||||||
<h1 className="text-2xl">Транспортные средства</h1>
|
<h1 className="text-2xl">Транспортные средства</h1>
|
||||||
|
@ -45,23 +45,23 @@ export const VehiclePreviewPage = observer(() => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div> */}
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
{vehicle && (
|
{vehicle[id!] && (
|
||||||
<div className="flex flex-col gap-10 w-full">
|
<div className="flex flex-col gap-10 w-full">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<h1 className="text-lg font-bold">Системный номер</h1>
|
<h1 className="text-lg font-bold">Системный номер</h1>
|
||||||
<p>{vehicle?.vehicle.tail_number}</p>
|
<p>{vehicle[id!]?.vehicle.tail_number}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<h1 className="text-lg font-bold">Тип транспортного средства</h1>
|
<h1 className="text-lg font-bold">Тип транспортного средства</h1>
|
||||||
<p>
|
<p>
|
||||||
{VEHICLE_TYPES.find(
|
{VEHICLE_TYPES.find(
|
||||||
(type) => type.value === vehicle?.vehicle.type
|
(type) => type.value === vehicle[id!]?.vehicle.type
|
||||||
)?.label || vehicle?.vehicle.type}
|
)?.label || vehicle[id!]?.vehicle.type}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<h1 className="text-lg font-bold">Перевозчик</h1>
|
<h1 className="text-lg font-bold">Перевозчик</h1>
|
||||||
<p>{vehicle?.vehicle.carrier}</p>
|
<p>{vehicle[id!]?.vehicle.carrier}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
export * from "./VehicleListPage";
|
export * from "./VehicleListPage";
|
||||||
export * from "./VehiclePreviewPage";
|
export * from "./VehiclePreviewPage";
|
||||||
export * from "./VehicleCreatePage";
|
export * from "./VehicleCreatePage";
|
||||||
|
export * from "./VehicleEditPage";
|
||||||
|
@ -3,14 +3,19 @@ import {
|
|||||||
Power,
|
Power,
|
||||||
LucideIcon,
|
LucideIcon,
|
||||||
Building2,
|
Building2,
|
||||||
MonitorSmartphone,
|
|
||||||
Map,
|
Map,
|
||||||
Users,
|
Users,
|
||||||
Earth,
|
Earth,
|
||||||
Landmark,
|
Landmark,
|
||||||
BusFront,
|
BusFront,
|
||||||
Bus,
|
|
||||||
GitBranch,
|
GitBranch,
|
||||||
|
Car,
|
||||||
|
Table,
|
||||||
|
Split,
|
||||||
|
Newspaper,
|
||||||
|
PersonStanding,
|
||||||
|
Cpu,
|
||||||
|
BookImage,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
export const DRAWER_WIDTH = 300;
|
export const DRAWER_WIDTH = 300;
|
||||||
|
|
||||||
@ -20,6 +25,8 @@ interface NavigationItem {
|
|||||||
icon: LucideIcon;
|
icon: LucideIcon;
|
||||||
path?: string;
|
path?: string;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
|
nestedItems?: NavigationItem[];
|
||||||
|
isActive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NAVIGATION_ITEMS: {
|
export const NAVIGATION_ITEMS: {
|
||||||
@ -45,30 +52,7 @@ export const NAVIGATION_ITEMS: {
|
|||||||
icon: BusFront,
|
icon: BusFront,
|
||||||
path: "/carrier",
|
path: "/carrier",
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// id: "media",
|
|
||||||
// label: "Медиа",
|
|
||||||
// icon: BookImage,
|
|
||||||
// path: "/media",
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// id: "articles",
|
|
||||||
// label: "Статьи",
|
|
||||||
// icon: Newspaper,
|
|
||||||
// path: "/article",
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
id: "attractions",
|
|
||||||
label: "Достопримечательности",
|
|
||||||
icon: Landmark,
|
|
||||||
path: "/sight",
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// id: "stations",
|
|
||||||
// label: "Остановки",
|
|
||||||
// icon: PersonStanding,
|
|
||||||
// path: "/station",
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
id: "snapshots",
|
id: "snapshots",
|
||||||
label: "Снапшоты",
|
label: "Снапшоты",
|
||||||
@ -84,33 +68,59 @@ export const NAVIGATION_ITEMS: {
|
|||||||
{
|
{
|
||||||
id: "devices",
|
id: "devices",
|
||||||
label: "Устройства",
|
label: "Устройства",
|
||||||
icon: MonitorSmartphone,
|
icon: Cpu,
|
||||||
path: "/devices",
|
path: "/devices",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "all",
|
||||||
|
label: "Все сущности",
|
||||||
|
icon: Table,
|
||||||
|
nestedItems: [
|
||||||
|
{
|
||||||
|
id: "media",
|
||||||
|
label: "Медиа",
|
||||||
|
icon: BookImage,
|
||||||
|
path: "/media",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "articles",
|
||||||
|
label: "Статьи",
|
||||||
|
icon: Newspaper,
|
||||||
|
path: "/article",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "attractions",
|
||||||
|
label: "Достопримечательности",
|
||||||
|
icon: Landmark,
|
||||||
|
path: "/sight",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "stations",
|
||||||
|
label: "Остановки",
|
||||||
|
icon: PersonStanding,
|
||||||
|
path: "/station",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "routes",
|
||||||
|
label: "Маршруты",
|
||||||
|
icon: Split,
|
||||||
|
path: "/route",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
id: "vehicles",
|
id: "vehicles",
|
||||||
label: "Транспорт",
|
label: "Транспорт",
|
||||||
icon: Bus,
|
icon: Car,
|
||||||
path: "/vehicle",
|
path: "/vehicle",
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// id: "routes",
|
|
||||||
// label: "Маршруты",
|
|
||||||
// icon: Split,
|
|
||||||
// path: "/route",
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
id: "users",
|
id: "users",
|
||||||
label: "Пользователи",
|
label: "Пользователи",
|
||||||
icon: Users,
|
icon: Users,
|
||||||
path: "/user",
|
path: "/user",
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// id: "articles",
|
|
||||||
// label: "Статьи",
|
|
||||||
// icon: Newspaper,
|
|
||||||
// path: "/articles",
|
|
||||||
// },
|
|
||||||
],
|
],
|
||||||
secondary: [
|
secondary: [
|
||||||
{
|
{
|
||||||
|
@ -106,7 +106,9 @@ export const UploadMediaDialog = observer(
|
|||||||
try {
|
try {
|
||||||
const media = await uploadMedia(
|
const media = await uploadMedia(
|
||||||
mediaFilename,
|
mediaFilename,
|
||||||
mediaType,
|
hardcodeType
|
||||||
|
? (MEDIA_TYPE_VALUES[hardcodeType] as number)
|
||||||
|
: mediaType,
|
||||||
mediaFile,
|
mediaFile,
|
||||||
mediaName
|
mediaName
|
||||||
);
|
);
|
||||||
|
@ -15,6 +15,29 @@ type Media = {
|
|||||||
media_type: number;
|
media_type: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ArticleListCashed = {
|
||||||
|
ru: {
|
||||||
|
data: Article[];
|
||||||
|
loaded: boolean;
|
||||||
|
};
|
||||||
|
en: {
|
||||||
|
data: Article[];
|
||||||
|
loaded: boolean;
|
||||||
|
};
|
||||||
|
zh: {
|
||||||
|
data: Article[];
|
||||||
|
loaded: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type PreviewCashed = {
|
||||||
|
ru: Article;
|
||||||
|
en: Article;
|
||||||
|
zh: Article;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ArticlePreviewCashed = Record<string, PreviewCashed>;
|
||||||
|
|
||||||
class ArticlesStore {
|
class ArticlesStore {
|
||||||
constructor() {
|
constructor() {
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
@ -25,19 +48,47 @@ class ArticlesStore {
|
|||||||
en: [],
|
en: [],
|
||||||
zh: [],
|
zh: [],
|
||||||
};
|
};
|
||||||
articleList: Article[] = [];
|
articleList: ArticleListCashed = {
|
||||||
|
ru: {
|
||||||
|
data: [],
|
||||||
|
loaded: false,
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
data: [],
|
||||||
|
loaded: false,
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
data: [],
|
||||||
|
loaded: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
articlePreview: ArticlePreviewCashed = {};
|
||||||
articleData: Article | null = null;
|
articleData: Article | null = null;
|
||||||
articleMedia: Media | null = null;
|
articleMedia: Media | null = null;
|
||||||
articleLoading: boolean = false;
|
articleLoading: boolean = false;
|
||||||
|
|
||||||
getArticleList = async () => {
|
getArticleList = async () => {
|
||||||
|
const { language } = languageStore;
|
||||||
|
if (this.articleList[language].loaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const response = await authInstance.get("/article");
|
const response = await authInstance.get("/article");
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.articleList = response.data;
|
this.articleList[language].data = response.data;
|
||||||
|
this.articleList[language].loaded = true;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getArticlePreview = async (id: number) => {
|
||||||
|
const { language } = languageStore;
|
||||||
|
if (this.articlePreview[id][language]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const response = await authInstance.get(`/article/${id}/preview`);
|
||||||
|
this.articlePreview[id][language] = response.data;
|
||||||
|
};
|
||||||
|
|
||||||
getArticles = async (language: Language) => {
|
getArticles = async (language: Language) => {
|
||||||
this.articleLoading = true;
|
this.articleLoading = true;
|
||||||
const response = await authInstance.get("/article");
|
const response = await authInstance.get("/article");
|
||||||
|
@ -14,18 +14,32 @@ export type Carrier = {
|
|||||||
right_color: string;
|
right_color: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Carriers = {
|
||||||
|
data: Carrier[];
|
||||||
|
loaded: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CashedCarrier = Record<number, Carrier>;
|
||||||
|
|
||||||
class CarrierStore {
|
class CarrierStore {
|
||||||
carriers: Carrier[] = [];
|
carriers: Carriers = {
|
||||||
carrier: Carrier | null = null;
|
data: [],
|
||||||
|
loaded: false,
|
||||||
|
};
|
||||||
|
carrier: CashedCarrier = {};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
getCarriers = async () => {
|
getCarriers = async () => {
|
||||||
|
if (this.carriers.loaded) return;
|
||||||
|
|
||||||
const response = await authInstance.get("/carrier");
|
const response = await authInstance.get("/carrier");
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.carriers = response.data;
|
this.carriers.data = response.data;
|
||||||
|
this.carriers.loaded = true;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -33,14 +47,33 @@ class CarrierStore {
|
|||||||
await authInstance.delete(`/carrier/${id}`);
|
await authInstance.delete(`/carrier/${id}`);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.carriers = this.carriers.filter((carrier) => carrier.id !== id);
|
this.carriers.data = this.carriers.data.filter(
|
||||||
|
(carrier) => carrier.id !== id
|
||||||
|
);
|
||||||
|
delete this.carrier[id];
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
getCarrier = async (id: number) => {
|
getCarrier = async (id: number) => {
|
||||||
|
if (this.carrier[id]) return;
|
||||||
const response = await authInstance.get(`/carrier/${id}`);
|
const response = await authInstance.get(`/carrier/${id}`);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.carrier = response.data;
|
if (!this.carrier[id]) {
|
||||||
|
this.carrier[id] = {
|
||||||
|
id: 0,
|
||||||
|
short_name: "",
|
||||||
|
full_name: "",
|
||||||
|
slogan: "",
|
||||||
|
city: "",
|
||||||
|
city_id: 0,
|
||||||
|
logo: "",
|
||||||
|
main_color: "",
|
||||||
|
left_color: "",
|
||||||
|
right_color: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
this.carrier[id] = response.data;
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
@ -50,9 +83,9 @@ class CarrierStore {
|
|||||||
shortName: string,
|
shortName: string,
|
||||||
city: string,
|
city: string,
|
||||||
cityId: number,
|
cityId: number,
|
||||||
primaryColor: string,
|
main_color: string,
|
||||||
secondaryColor: string,
|
left_color: string,
|
||||||
accentColor: string,
|
right_color: string,
|
||||||
slogan: string,
|
slogan: string,
|
||||||
logoId: string
|
logoId: string
|
||||||
) => {
|
) => {
|
||||||
@ -61,14 +94,65 @@ class CarrierStore {
|
|||||||
short_name: shortName,
|
short_name: shortName,
|
||||||
city,
|
city,
|
||||||
city_id: cityId,
|
city_id: cityId,
|
||||||
primary_color: primaryColor,
|
main_color,
|
||||||
secondary_color: secondaryColor,
|
left_color,
|
||||||
accent_color: accentColor,
|
right_color,
|
||||||
slogan,
|
slogan,
|
||||||
logo: logoId,
|
logo: logoId,
|
||||||
});
|
});
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.carriers.push(response.data);
|
this.carriers.data.push(response.data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
editCarrierData = {
|
||||||
|
full_name: "",
|
||||||
|
short_name: "",
|
||||||
|
city: "",
|
||||||
|
city_id: 0,
|
||||||
|
main_color: "",
|
||||||
|
left_color: "",
|
||||||
|
right_color: "",
|
||||||
|
slogan: "",
|
||||||
|
logo: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
setEditCarrierData = (
|
||||||
|
fullName: string,
|
||||||
|
shortName: string,
|
||||||
|
city: string,
|
||||||
|
cityId: number,
|
||||||
|
main_color: string,
|
||||||
|
left_color: string,
|
||||||
|
right_color: string,
|
||||||
|
slogan: string,
|
||||||
|
logoId: string
|
||||||
|
) => {
|
||||||
|
this.editCarrierData = {
|
||||||
|
full_name: fullName,
|
||||||
|
short_name: shortName,
|
||||||
|
city,
|
||||||
|
city_id: cityId,
|
||||||
|
main_color: main_color,
|
||||||
|
left_color: left_color,
|
||||||
|
right_color: right_color,
|
||||||
|
slogan: slogan,
|
||||||
|
logo: logoId,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
editCarrier = async (id: number) => {
|
||||||
|
const response = await authInstance.patch(
|
||||||
|
`/carrier/${id}`,
|
||||||
|
this.editCarrierData
|
||||||
|
);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.carriers.data = this.carriers.data.map((carrier) =>
|
||||||
|
carrier.id === id ? { ...carrier, ...response.data } : carrier
|
||||||
|
);
|
||||||
|
|
||||||
|
this.carrier[id] = response.data;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
278
src/shared/store/CityStore/index.ts
Normal file
278
src/shared/store/CityStore/index.ts
Normal file
@ -0,0 +1,278 @@
|
|||||||
|
import {
|
||||||
|
authInstance,
|
||||||
|
languageInstance,
|
||||||
|
Language,
|
||||||
|
languageStore,
|
||||||
|
countryStore,
|
||||||
|
} from "@shared";
|
||||||
|
import { makeAutoObservable, runInAction } from "mobx";
|
||||||
|
|
||||||
|
export type City = {
|
||||||
|
id?: number;
|
||||||
|
name: string;
|
||||||
|
country: string;
|
||||||
|
country_code: string;
|
||||||
|
arms: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CashedCities = {
|
||||||
|
ru: City[];
|
||||||
|
en: City[];
|
||||||
|
zh: City[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CashedCity = {
|
||||||
|
ru: City | null;
|
||||||
|
en: City | null;
|
||||||
|
zh: City | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
class CityStore {
|
||||||
|
cities: CashedCities = {
|
||||||
|
ru: [],
|
||||||
|
en: [],
|
||||||
|
zh: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
city: Record<string, CashedCity> = {};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
makeAutoObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
ruCities: City[] = [];
|
||||||
|
|
||||||
|
getRuCities = async () => {
|
||||||
|
const response = await languageInstance("ru").get(`/city`);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.ruCities = response.data;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
getCities = async (language: keyof CashedCities) => {
|
||||||
|
if (this.cities[language] && this.cities[language].length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await authInstance.get(`/city`);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.cities[language] = response.data;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
getCity = async (code: string, language: keyof CashedCities) => {
|
||||||
|
if (this.city[code]?.[language] && this.city[code][language] !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await authInstance.get(`/city/${code}`);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
if (!this.city[code]) {
|
||||||
|
this.city[code] = {
|
||||||
|
ru: null,
|
||||||
|
en: null,
|
||||||
|
zh: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
this.city[code][language] = response.data;
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteCity = async (code: string, language: keyof CashedCities) => {
|
||||||
|
await authInstance.delete(`/city/${code}`);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.cities[language] = this.cities[language].filter(
|
||||||
|
(city) => city.country_code !== code
|
||||||
|
);
|
||||||
|
this.city[code][language] = null;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
createCityData = {
|
||||||
|
country: "",
|
||||||
|
country_code: "",
|
||||||
|
arms: "",
|
||||||
|
ru: {
|
||||||
|
name: "",
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
name: "",
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
name: "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
setCreateCityData = (
|
||||||
|
name: string,
|
||||||
|
country: string,
|
||||||
|
country_code: string,
|
||||||
|
arms: string,
|
||||||
|
language: keyof CashedCities
|
||||||
|
) => {
|
||||||
|
this.createCityData = {
|
||||||
|
...this.createCityData,
|
||||||
|
country: country,
|
||||||
|
country_code: country_code,
|
||||||
|
arms: arms,
|
||||||
|
[language]: {
|
||||||
|
name: name,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
createCity = async () => {
|
||||||
|
const { language } = languageStore;
|
||||||
|
const { country, country_code, arms } = this.createCityData;
|
||||||
|
const { name } = this.createCityData[language as keyof CashedCities];
|
||||||
|
|
||||||
|
if (name && country && country_code && arms) {
|
||||||
|
const cityResponse = await languageInstance(language as Language).post(
|
||||||
|
"/city",
|
||||||
|
{
|
||||||
|
name: name,
|
||||||
|
country: country,
|
||||||
|
country_code: country_code,
|
||||||
|
arms: arms,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.cities[language as keyof CashedCities] = [
|
||||||
|
...this.cities[language as keyof CashedCities],
|
||||||
|
cityResponse.data,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const secondaryLanguage of ["ru", "en", "zh"].filter(
|
||||||
|
(l) => l !== language
|
||||||
|
)) {
|
||||||
|
const { name } =
|
||||||
|
this.createCityData[secondaryLanguage as keyof CashedCities];
|
||||||
|
|
||||||
|
const patchResponse = await languageInstance(
|
||||||
|
secondaryLanguage as Language
|
||||||
|
).patch(`/city/${cityResponse.data.id}`, {
|
||||||
|
name: name,
|
||||||
|
country: country,
|
||||||
|
country_code: country_code,
|
||||||
|
arms: arms,
|
||||||
|
});
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.cities[secondaryLanguage as keyof CashedCities] = [
|
||||||
|
...this.cities[secondaryLanguage as keyof CashedCities],
|
||||||
|
patchResponse.data,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.createCityData = {
|
||||||
|
country: "",
|
||||||
|
country_code: "",
|
||||||
|
arms: "",
|
||||||
|
ru: {
|
||||||
|
name: "",
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
name: "",
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
name: "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
editCityData = {
|
||||||
|
country: "",
|
||||||
|
country_code: "",
|
||||||
|
arms: "",
|
||||||
|
ru: {
|
||||||
|
name: "",
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
name: "",
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
name: "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
setEditCityData = (
|
||||||
|
name: string,
|
||||||
|
country: string,
|
||||||
|
country_code: string,
|
||||||
|
arms: string,
|
||||||
|
language: keyof CashedCities
|
||||||
|
) => {
|
||||||
|
this.editCityData = {
|
||||||
|
...this.editCityData,
|
||||||
|
country: country,
|
||||||
|
country_code: country_code,
|
||||||
|
arms: arms,
|
||||||
|
|
||||||
|
[language]: {
|
||||||
|
name: name,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
editCity = async (code: string) => {
|
||||||
|
for (const language of ["ru", "en", "zh"]) {
|
||||||
|
const { country_code, arms } = this.editCityData;
|
||||||
|
const { name } = this.editCityData[language as keyof CashedCities];
|
||||||
|
const { countries } = countryStore;
|
||||||
|
|
||||||
|
const country = countries[language as keyof CashedCities].find(
|
||||||
|
(country) => country.code === country_code
|
||||||
|
);
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
await languageInstance(language as Language).patch(`/city/${code}`, {
|
||||||
|
name,
|
||||||
|
country: country?.name || "",
|
||||||
|
country_code: country_code,
|
||||||
|
arms,
|
||||||
|
});
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
if (this.city[code]) {
|
||||||
|
this.city[code][language as keyof CashedCities] = {
|
||||||
|
name,
|
||||||
|
country: country?.name || "",
|
||||||
|
country_code: country_code,
|
||||||
|
arms,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.cities[language as keyof CashedCities]) {
|
||||||
|
this.cities[language as keyof CashedCities] = this.cities[
|
||||||
|
language as keyof CashedCities
|
||||||
|
].map((city) =>
|
||||||
|
city.id === Number(code)
|
||||||
|
? {
|
||||||
|
id: city.id,
|
||||||
|
name,
|
||||||
|
country: country?.name || "",
|
||||||
|
country_code: country_code,
|
||||||
|
arms,
|
||||||
|
}
|
||||||
|
: city
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cityStore = new CityStore();
|
@ -1,63 +0,0 @@
|
|||||||
import { authInstance } from "@shared";
|
|
||||||
import { makeAutoObservable, runInAction } from "mobx";
|
|
||||||
|
|
||||||
type City = {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
country_code: string;
|
|
||||||
country: string;
|
|
||||||
arms?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
class CityStore {
|
|
||||||
cities: City[] = [];
|
|
||||||
city: City | null = null;
|
|
||||||
constructor() {
|
|
||||||
makeAutoObservable(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
getCities = async () => {
|
|
||||||
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();
|
|
@ -1,4 +1,9 @@
|
|||||||
import { authInstance } from "@shared";
|
import {
|
||||||
|
authInstance,
|
||||||
|
languageInstance,
|
||||||
|
Language,
|
||||||
|
languageStore,
|
||||||
|
} from "@shared";
|
||||||
import { makeAutoObservable, runInAction } from "mobx";
|
import { makeAutoObservable, runInAction } from "mobx";
|
||||||
|
|
||||||
export type Country = {
|
export type Country = {
|
||||||
@ -6,43 +11,208 @@ export type Country = {
|
|||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CashedCountries = {
|
||||||
|
ru: Country[];
|
||||||
|
en: Country[];
|
||||||
|
zh: Country[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CashedCountry = {
|
||||||
|
ru: Country | null;
|
||||||
|
en: Country | null;
|
||||||
|
zh: Country | null;
|
||||||
|
};
|
||||||
|
|
||||||
class CountryStore {
|
class CountryStore {
|
||||||
countries: Country[] = [];
|
countries: CashedCountries = {
|
||||||
country: Country | null = null;
|
ru: [],
|
||||||
|
en: [],
|
||||||
|
zh: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
country: Record<string, CashedCountry> = {};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
getCountries = async () => {
|
getCountries = async (language: keyof CashedCountries) => {
|
||||||
const response = await authInstance.get("/country");
|
if (this.countries[language] && this.countries[language].length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await authInstance.get(`/country`);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.countries = response.data;
|
this.countries[language] = response.data;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
getCountry = async (code: string) => {
|
getCountry = async (code: string, language: keyof CashedCountries) => {
|
||||||
|
if (
|
||||||
|
this.country[code]?.[language] &&
|
||||||
|
this.country[code][language] !== null
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const response = await authInstance.get(`/country/${code}`);
|
const response = await authInstance.get(`/country/${code}`);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.country = response.data;
|
if (!this.country[code]) {
|
||||||
|
this.country[code] = {
|
||||||
|
ru: null,
|
||||||
|
en: null,
|
||||||
|
zh: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
this.country[code][language] = response.data;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteCountry = async (code: string) => {
|
deleteCountry = async (code: string, language: keyof CashedCountries) => {
|
||||||
await authInstance.delete(`/country/${code}`);
|
await authInstance.delete(`/country/${code}`);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.countries = this.countries.filter(
|
this.countries[language] = this.countries[language].filter(
|
||||||
(country) => country.code !== code
|
(country) => country.code !== code
|
||||||
);
|
);
|
||||||
|
this.country[code][language] = null;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
createCountry = async (code: string, name: string) => {
|
createCountryData = {
|
||||||
await authInstance.post("/country", { code: code, name: name });
|
code: "",
|
||||||
await this.getCountries();
|
ru: {
|
||||||
|
name: "",
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
name: "",
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
name: "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
setCountryData = (
|
||||||
|
code: string,
|
||||||
|
name: string,
|
||||||
|
language: keyof CashedCountries
|
||||||
|
) => {
|
||||||
|
this.createCountryData = {
|
||||||
|
...this.createCountryData,
|
||||||
|
code: code,
|
||||||
|
[language]: {
|
||||||
|
name: name,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
createCountry = async () => {
|
||||||
|
const { code } = this.createCountryData;
|
||||||
|
const { language } = languageStore;
|
||||||
|
const { name } = this.createCountryData[language as keyof CashedCountries];
|
||||||
|
|
||||||
|
if (code && this.createCountryData[language].name) {
|
||||||
|
await languageInstance(language as Language).post("/country", {
|
||||||
|
code: code,
|
||||||
|
name: name,
|
||||||
|
});
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.countries[language as keyof CashedCountries] = [
|
||||||
|
...this.countries[language as keyof CashedCountries],
|
||||||
|
{ code: code, name: name },
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const secondaryLanguage of ["ru", "en", "zh"].filter(
|
||||||
|
(l) => l !== language
|
||||||
|
)) {
|
||||||
|
const { name } =
|
||||||
|
this.createCountryData[secondaryLanguage as keyof CashedCountries];
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
await languageInstance(secondaryLanguage as Language).patch(
|
||||||
|
`/country/${code}`,
|
||||||
|
{
|
||||||
|
name: name,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
runInAction(() => {
|
||||||
|
this.countries[secondaryLanguage as keyof CashedCountries] = [
|
||||||
|
...this.countries[secondaryLanguage as keyof CashedCountries],
|
||||||
|
{ code: code, name: name },
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.createCountryData = {
|
||||||
|
code: "",
|
||||||
|
ru: {
|
||||||
|
name: "",
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
name: "",
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
name: "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
editCountryData = {
|
||||||
|
ru: {
|
||||||
|
name: "",
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
name: "",
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
name: "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
setEditCountryData = (name: string, language: keyof CashedCountries) => {
|
||||||
|
this.editCountryData = {
|
||||||
|
...this.editCountryData,
|
||||||
|
[language]: {
|
||||||
|
name: name,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
editCountry = async (code: string) => {
|
||||||
|
for (const language of ["ru", "en", "zh"]) {
|
||||||
|
const { name } = this.editCountryData[language as keyof CashedCountries];
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
await languageInstance(language as Language).patch(`/country/${code}`, {
|
||||||
|
name: name,
|
||||||
|
});
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
if (this.country[code]) {
|
||||||
|
this.country[code][language as keyof CashedCountries] = {
|
||||||
|
code,
|
||||||
|
name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (this.countries[language as keyof CashedCountries]) {
|
||||||
|
this.countries[language as keyof CashedCountries] = this.countries[
|
||||||
|
language as keyof CashedCountries
|
||||||
|
].map((country) =>
|
||||||
|
country.code === code ? { code, name } : country
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,10 +106,17 @@ class CreateSightStore {
|
|||||||
try {
|
try {
|
||||||
this.needLeaveAgree = true;
|
this.needLeaveAgree = true;
|
||||||
const articleRes = await authInstance.post("/article", {
|
const articleRes = await authInstance.post("/article", {
|
||||||
translation: {
|
translations: {
|
||||||
ru: articleRuData,
|
heading: {
|
||||||
en: articleEnData,
|
ru: articleRuData.heading,
|
||||||
zh: articleZhData,
|
en: articleEnData.heading,
|
||||||
|
zh: articleZhData.heading,
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
ru: articleRuData.body,
|
||||||
|
en: articleEnData.body,
|
||||||
|
zh: articleZhData.body,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const { id } = articleRes.data; // New article's ID
|
const { id } = articleRes.data; // New article's ID
|
||||||
|
@ -265,18 +265,16 @@ class EditSightStore {
|
|||||||
this.sight.common.left_article != null
|
this.sight.common.left_article != null
|
||||||
) {
|
) {
|
||||||
await authInstance.patch(`/article/${this.sight.common.left_article}`, {
|
await authInstance.patch(`/article/${this.sight.common.left_article}`, {
|
||||||
translation: {
|
translations: {
|
||||||
ru: {
|
heading: {
|
||||||
heading: this.sight.ru.left.heading,
|
ru: this.sight.ru.left.heading,
|
||||||
body: this.sight.ru.left.body,
|
en: this.sight.en.left.heading,
|
||||||
|
zh: this.sight.zh.left.heading,
|
||||||
},
|
},
|
||||||
en: {
|
body: {
|
||||||
heading: this.sight.en.left.heading,
|
ru: this.sight.ru.left.body,
|
||||||
body: this.sight.en.left.body,
|
en: this.sight.en.left.body,
|
||||||
},
|
zh: this.sight.zh.left.body,
|
||||||
zh: {
|
|
||||||
heading: this.sight.zh.left.heading,
|
|
||||||
body: this.sight.zh.left.body,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -284,39 +282,36 @@ class EditSightStore {
|
|||||||
|
|
||||||
await authInstance.patch(`/sight/${this.sight.common.id}`, {
|
await authInstance.patch(`/sight/${this.sight.common.id}`, {
|
||||||
...this.sight.common,
|
...this.sight.common,
|
||||||
translation: {
|
translations: {
|
||||||
ru: {
|
name: {
|
||||||
name: this.sight.ru.name,
|
ru: this.sight.ru.name,
|
||||||
address: this.sight.ru.address,
|
en: this.sight.en.name,
|
||||||
|
zh: this.sight.zh.name,
|
||||||
},
|
},
|
||||||
en: {
|
address: {
|
||||||
name: this.sight.en.name,
|
ru: this.sight.ru.address,
|
||||||
address: this.sight.en.address,
|
en: this.sight.en.address,
|
||||||
},
|
zh: this.sight.zh.address,
|
||||||
zh: {
|
|
||||||
name: this.sight.zh.name,
|
|
||||||
address: this.sight.zh.address,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const article of this.sight.ru.right) {
|
for (let index = 0; index < this.sight.ru.right.length; index++) {
|
||||||
|
const article = this.sight.ru.right[index];
|
||||||
if (article.id == 0 || article.id == null) {
|
if (article.id == 0 || article.id == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
await authInstance.patch(`/article/${article.id}`, {
|
await authInstance.patch(`/article/${article.id}`, {
|
||||||
translation: {
|
translations: {
|
||||||
ru: {
|
heading: {
|
||||||
heading: article.heading,
|
ru: this.sight.ru.right[index].heading,
|
||||||
body: article.body,
|
en: this.sight.en.right[index].heading,
|
||||||
|
zh: this.sight.zh.right[index].heading,
|
||||||
},
|
},
|
||||||
en: {
|
body: {
|
||||||
heading: article.heading,
|
ru: this.sight.ru.right[index].body,
|
||||||
body: article.body,
|
en: this.sight.en.right[index].body,
|
||||||
},
|
zh: this.sight.zh.right[index].body,
|
||||||
zh: {
|
|
||||||
heading: article.heading,
|
|
||||||
body: article.body,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -604,10 +599,17 @@ class EditSightStore {
|
|||||||
body: "Новый текст (ZH)",
|
body: "Новый текст (ZH)",
|
||||||
};
|
};
|
||||||
const articleId = await authInstance.post("/article", {
|
const articleId = await authInstance.post("/article", {
|
||||||
translation: {
|
translations: {
|
||||||
ru: articleRuData,
|
heading: {
|
||||||
en: articleEnData,
|
ru: articleRuData.heading,
|
||||||
zh: articleZhData,
|
en: articleEnData.heading,
|
||||||
|
zh: articleZhData.heading,
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
ru: articleRuData.body,
|
||||||
|
en: articleEnData.body,
|
||||||
|
zh: articleZhData.body,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const { id } = articleId.data;
|
const { id } = articleId.data;
|
||||||
|
@ -19,17 +19,39 @@ export type Route = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class RouteStore {
|
class RouteStore {
|
||||||
routes: Route[] = [];
|
routes: {
|
||||||
|
data: Route[];
|
||||||
|
loaded: boolean;
|
||||||
|
} = {
|
||||||
|
data: [],
|
||||||
|
loaded: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
route: Record<string, Route> = {};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
getRoutes = async () => {
|
getRoutes = async () => {
|
||||||
|
if (this.routes.loaded) return;
|
||||||
const response = await authInstance.get("/route");
|
const response = await authInstance.get("/route");
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.routes = response.data;
|
this.routes = {
|
||||||
|
data: response.data,
|
||||||
|
loaded: true,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
createRoute = async (route: any) => {
|
||||||
|
const response = await authInstance.post("/route", route);
|
||||||
|
const id = response.data.id;
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.route[id] = { ...route, id };
|
||||||
|
this.routes.data = [...this.routes.data, { ...route, id }];
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -37,7 +59,56 @@ class RouteStore {
|
|||||||
await authInstance.delete(`/route/${id}`);
|
await authInstance.delete(`/route/${id}`);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.routes = this.routes.filter((route) => route.id !== id);
|
this.routes = {
|
||||||
|
data: this.routes.data.filter((route) => route.id !== id),
|
||||||
|
loaded: true,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
getRoute = async (id: number) => {
|
||||||
|
if (this.route[id]) return this.route[id];
|
||||||
|
const response = await authInstance.get(`/route/${id}`);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.route[id] = response.data;
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
editRouteData = {
|
||||||
|
carrier: "",
|
||||||
|
carrier_id: 0,
|
||||||
|
center_latitude: 0,
|
||||||
|
center_longitude: 0,
|
||||||
|
governor_appeal: 0,
|
||||||
|
id: 0,
|
||||||
|
path: [] as number[][],
|
||||||
|
rotate: 0,
|
||||||
|
route_direction: false,
|
||||||
|
route_number: "",
|
||||||
|
route_sys_number: "",
|
||||||
|
scale_max: 0,
|
||||||
|
scale_min: 0,
|
||||||
|
video_preview: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
setEditRouteData = (data: any) => {
|
||||||
|
this.editRouteData = { ...this.editRouteData, ...data };
|
||||||
|
};
|
||||||
|
|
||||||
|
editRoute = async (id: number) => {
|
||||||
|
const response = await authInstance.patch(
|
||||||
|
`/route/${id}`,
|
||||||
|
this.editRouteData
|
||||||
|
);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.route[id] = response.data;
|
||||||
|
this.routes.data = this.routes.data.map((route) =>
|
||||||
|
route.id === id ? response.data : route
|
||||||
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,42 @@
|
|||||||
import { authInstance } from "@shared";
|
import { authInstance, languageInstance, languageStore } from "@shared";
|
||||||
import { makeAutoObservable, runInAction } from "mobx";
|
import { makeAutoObservable, runInAction } from "mobx";
|
||||||
|
|
||||||
|
type Language = "ru" | "en" | "zh";
|
||||||
|
|
||||||
|
type StationLanguageData = {
|
||||||
|
name: string;
|
||||||
|
system_name: string;
|
||||||
|
description: string;
|
||||||
|
address: string;
|
||||||
|
loaded: boolean; // Indicates if this language's data has been loaded/modified
|
||||||
|
};
|
||||||
|
|
||||||
|
type StationCommonData = {
|
||||||
|
city_id: number;
|
||||||
|
direction: boolean;
|
||||||
|
icon: string;
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
offset_x: number;
|
||||||
|
offset_y: number;
|
||||||
|
transfers: {
|
||||||
|
bus: string;
|
||||||
|
metro_blue: string;
|
||||||
|
metro_green: string;
|
||||||
|
metro_orange: string;
|
||||||
|
metro_purple: string;
|
||||||
|
metro_red: string;
|
||||||
|
train: string;
|
||||||
|
tram: string;
|
||||||
|
trolleybus: string;
|
||||||
|
};
|
||||||
|
city: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type EditStationData = {
|
||||||
|
[key in Language]: StationLanguageData;
|
||||||
|
} & { common: StationCommonData };
|
||||||
|
|
||||||
type Station = {
|
type Station = {
|
||||||
id: number;
|
id: number;
|
||||||
address: string;
|
address: string;
|
||||||
@ -32,6 +68,77 @@ class StationsStore {
|
|||||||
stations: Station[] = [];
|
stations: Station[] = [];
|
||||||
station: Station | null = null;
|
station: Station | null = null;
|
||||||
|
|
||||||
|
stationLists: {
|
||||||
|
[key in Language]: {
|
||||||
|
data: Station[];
|
||||||
|
loaded: boolean;
|
||||||
|
};
|
||||||
|
} = {
|
||||||
|
ru: {
|
||||||
|
data: [],
|
||||||
|
loaded: false,
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
data: [],
|
||||||
|
loaded: false,
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
data: [],
|
||||||
|
loaded: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// This will store the full station data, keyed by ID and then by language
|
||||||
|
stationPreview: Record<
|
||||||
|
string,
|
||||||
|
Record<string, { loaded: boolean; data: Station }>
|
||||||
|
> = {};
|
||||||
|
|
||||||
|
editStationData: EditStationData = {
|
||||||
|
ru: {
|
||||||
|
name: "",
|
||||||
|
system_name: "",
|
||||||
|
description: "",
|
||||||
|
address: "",
|
||||||
|
loaded: false,
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
name: "",
|
||||||
|
system_name: "",
|
||||||
|
description: "",
|
||||||
|
address: "",
|
||||||
|
loaded: false,
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
name: "",
|
||||||
|
system_name: "",
|
||||||
|
description: "",
|
||||||
|
address: "",
|
||||||
|
loaded: false,
|
||||||
|
},
|
||||||
|
common: {
|
||||||
|
city: "",
|
||||||
|
city_id: 0,
|
||||||
|
direction: false,
|
||||||
|
icon: "",
|
||||||
|
latitude: 0,
|
||||||
|
longitude: 0,
|
||||||
|
offset_x: 0,
|
||||||
|
offset_y: 0,
|
||||||
|
transfers: {
|
||||||
|
bus: "",
|
||||||
|
metro_blue: "",
|
||||||
|
metro_green: "",
|
||||||
|
metro_orange: "",
|
||||||
|
metro_purple: "",
|
||||||
|
metro_red: "",
|
||||||
|
train: "",
|
||||||
|
tram: "",
|
||||||
|
trolleybus: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
}
|
}
|
||||||
@ -44,11 +151,160 @@ class StationsStore {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getStationList = async () => {
|
||||||
|
const { language } = languageStore;
|
||||||
|
if (this.stationLists[language].loaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const response = await authInstance.get("/station");
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.stationLists[language].data = response.data;
|
||||||
|
this.stationLists[language].loaded = true;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
setEditCommonData = (data: Partial<StationCommonData>) => {
|
||||||
|
this.editStationData.common = {
|
||||||
|
...this.editStationData.common,
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
getEditStation = async (id: number) => {
|
||||||
|
if (this.editStationData.ru.loaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ruResponse = await languageInstance("ru").get(`/station/${id}`);
|
||||||
|
const enResponse = await languageInstance("en").get(`/station/${id}`);
|
||||||
|
const zhResponse = await languageInstance("zh").get(`/station/${id}`);
|
||||||
|
|
||||||
|
this.editStationData = {
|
||||||
|
ru: {
|
||||||
|
name: ruResponse.data.name,
|
||||||
|
system_name: ruResponse.data.system_name,
|
||||||
|
description: ruResponse.data.description,
|
||||||
|
address: ruResponse.data.address,
|
||||||
|
loaded: true,
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
name: enResponse.data.name,
|
||||||
|
system_name: enResponse.data.system_name,
|
||||||
|
description: enResponse.data.description,
|
||||||
|
address: enResponse.data.address,
|
||||||
|
loaded: true,
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
name: zhResponse.data.name,
|
||||||
|
system_name: zhResponse.data.system_name,
|
||||||
|
description: zhResponse.data.description,
|
||||||
|
address: zhResponse.data.address,
|
||||||
|
loaded: true,
|
||||||
|
},
|
||||||
|
common: {
|
||||||
|
city: ruResponse.data.city,
|
||||||
|
city_id: ruResponse.data.city_id,
|
||||||
|
direction: ruResponse.data.direction,
|
||||||
|
icon: ruResponse.data.icon,
|
||||||
|
latitude: ruResponse.data.latitude,
|
||||||
|
longitude: ruResponse.data.longitude,
|
||||||
|
offset_x: ruResponse.data.offset_x,
|
||||||
|
offset_y: ruResponse.data.offset_y,
|
||||||
|
transfers: ruResponse.data.transfers,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sets language-specific station data
|
||||||
|
setLanguageEditStationData = (
|
||||||
|
language: Language,
|
||||||
|
data: Partial<StationLanguageData>
|
||||||
|
) => {
|
||||||
|
this.editStationData[language] = {
|
||||||
|
...this.editStationData[language],
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
editStation = async (id: number) => {
|
||||||
|
const commonDataPayload = {
|
||||||
|
city_id: this.editStationData.common.city_id,
|
||||||
|
direction: this.editStationData.common.direction,
|
||||||
|
icon: this.editStationData.common.icon,
|
||||||
|
latitude: this.editStationData.common.latitude,
|
||||||
|
longitude: this.editStationData.common.longitude,
|
||||||
|
offset_x: this.editStationData.common.offset_x,
|
||||||
|
offset_y: this.editStationData.common.offset_y,
|
||||||
|
transfers: this.editStationData.common.transfers,
|
||||||
|
city: this.editStationData.common.city,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const language of ["ru", "en", "zh"] as const) {
|
||||||
|
const { name, description, address } = this.editStationData[language];
|
||||||
|
const response = await languageInstance(language).patch(
|
||||||
|
`/station/${id}`,
|
||||||
|
{
|
||||||
|
name: name || "",
|
||||||
|
system_name: name || "", // system_name is often derived from name
|
||||||
|
description: description || "",
|
||||||
|
address: address || "",
|
||||||
|
...commonDataPayload,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
// Update the cached preview data and station lists after successful patch
|
||||||
|
if (this.stationPreview[id]) {
|
||||||
|
this.stationPreview[id][language] = {
|
||||||
|
loaded: true,
|
||||||
|
data: {
|
||||||
|
...this.stationPreview[id][language].data,
|
||||||
|
id: response.data.id,
|
||||||
|
name: response.data.name,
|
||||||
|
system_name: response.data.system_name,
|
||||||
|
description: response.data.description,
|
||||||
|
address: response.data.address,
|
||||||
|
...commonDataPayload,
|
||||||
|
} as Station,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (this.stationLists[language].data) {
|
||||||
|
this.stationLists[language].data = this.stationLists[
|
||||||
|
language
|
||||||
|
].data.map((station: Station) =>
|
||||||
|
station.id === id
|
||||||
|
? ({
|
||||||
|
...station,
|
||||||
|
name: response.data.name,
|
||||||
|
system_name: response.data.system_name,
|
||||||
|
description: response.data.description,
|
||||||
|
address: response.data.address,
|
||||||
|
...commonDataPayload,
|
||||||
|
} as Station)
|
||||||
|
: station
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
deleteStation = async (id: number) => {
|
deleteStation = async (id: number) => {
|
||||||
await authInstance.delete(`/station/${id}`);
|
await authInstance.delete(`/station/${id}`);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.stations = this.stations.filter((station) => station.id !== id);
|
this.stations = this.stations.filter((station) => station.id !== id);
|
||||||
|
// Also clear from stationPreview cache
|
||||||
|
if (this.stationPreview[id]) {
|
||||||
|
delete this.stationPreview[id];
|
||||||
|
}
|
||||||
|
// Clear from stationLists as well for all languages
|
||||||
|
for (const lang of ["ru", "en", "zh"] as const) {
|
||||||
|
if (this.stationLists[lang].data) {
|
||||||
|
this.stationLists[lang].data = this.stationLists[lang].data.filter(
|
||||||
|
(station) => station.id !== id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -57,6 +313,29 @@ class StationsStore {
|
|||||||
this.station = response.data;
|
this.station = response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getStationPreview = async (id: number) => {
|
||||||
|
const { language } = languageStore;
|
||||||
|
|
||||||
|
if (this.stationPreview[id]?.[language]?.loaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await languageInstance(language).get(`/station/${id}`);
|
||||||
|
runInAction(() => {
|
||||||
|
if (!this.stationPreview[id]) {
|
||||||
|
this.stationPreview[id] = {
|
||||||
|
ru: { loaded: false, data: {} as Station },
|
||||||
|
en: { loaded: false, data: {} as Station },
|
||||||
|
zh: { loaded: false, data: {} as Station },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
this.stationPreview[id][language] = {
|
||||||
|
loaded: true,
|
||||||
|
data: response.data as Station,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
createStation = async (
|
createStation = async (
|
||||||
name: string,
|
name: string,
|
||||||
systemName: string,
|
systemName: string,
|
||||||
@ -69,8 +348,72 @@ class StationsStore {
|
|||||||
});
|
});
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.stations.push(response.data);
|
this.stations.push(response.data);
|
||||||
|
const newStation = response.data as Station;
|
||||||
|
if (!this.stationPreview[newStation.id]) {
|
||||||
|
this.stationPreview[newStation.id] = {
|
||||||
|
ru: { loaded: false, data: newStation },
|
||||||
|
en: { loaded: false, data: newStation },
|
||||||
|
zh: { loaded: false, data: newStation },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
this.stationPreview[newStation.id]["ru"] = {
|
||||||
|
loaded: true,
|
||||||
|
data: newStation,
|
||||||
|
};
|
||||||
|
this.stationPreview[newStation.id]["en"] = {
|
||||||
|
loaded: true,
|
||||||
|
data: newStation,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Reset editStationData when navigating away or after saving
|
||||||
|
resetEditStationData = () => {
|
||||||
|
this.editStationData = {
|
||||||
|
ru: {
|
||||||
|
name: "",
|
||||||
|
system_name: "",
|
||||||
|
description: "",
|
||||||
|
address: "",
|
||||||
|
loaded: false,
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
name: "",
|
||||||
|
system_name: "",
|
||||||
|
description: "",
|
||||||
|
address: "",
|
||||||
|
loaded: false,
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
name: "",
|
||||||
|
system_name: "",
|
||||||
|
description: "",
|
||||||
|
address: "",
|
||||||
|
loaded: false,
|
||||||
|
},
|
||||||
|
common: {
|
||||||
|
city: "",
|
||||||
|
city_id: 0,
|
||||||
|
direction: false,
|
||||||
|
icon: "",
|
||||||
|
latitude: 0,
|
||||||
|
longitude: 0,
|
||||||
|
offset_x: 0,
|
||||||
|
offset_y: 0,
|
||||||
|
transfers: {
|
||||||
|
bus: "",
|
||||||
|
metro_blue: "",
|
||||||
|
metro_green: "",
|
||||||
|
metro_orange: "",
|
||||||
|
metro_purple: "",
|
||||||
|
metro_red: "",
|
||||||
|
train: "",
|
||||||
|
tram: "",
|
||||||
|
trolleybus: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const stationsStore = new StationsStore();
|
export const stationsStore = new StationsStore();
|
||||||
|
@ -6,37 +6,109 @@ export type User = {
|
|||||||
email: string;
|
email: string;
|
||||||
is_admin: boolean;
|
is_admin: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
|
password?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
class UserStore {
|
class UserStore {
|
||||||
users: User[] = [];
|
users: {
|
||||||
user: User | null = null;
|
data: User[];
|
||||||
|
loaded: boolean;
|
||||||
|
} = {
|
||||||
|
data: [],
|
||||||
|
loaded: false,
|
||||||
|
};
|
||||||
|
user: Record<string, User> = {};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
getUsers = async () => {
|
getUsers = async () => {
|
||||||
|
if (this.users.loaded) return;
|
||||||
|
|
||||||
const response = await authInstance.get("/user");
|
const response = await authInstance.get("/user");
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.users = response.data;
|
this.users.data = response.data;
|
||||||
|
this.users.loaded = true;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
getUser = async (id: number) => {
|
getUser = async (id: number) => {
|
||||||
|
if (this.user[id]) return;
|
||||||
const response = await authInstance.get(`/user/${id}`);
|
const response = await authInstance.get(`/user/${id}`);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.user = response.data as User;
|
this.user[id] = response.data as User;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteUser = async (id: number) => {
|
deleteUser = async (id: number) => {
|
||||||
await authInstance.delete(`/users/${id}`);
|
await authInstance.delete(`/user/${id}`);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.users = this.users.filter((user) => user.id !== id);
|
this.users.data = this.users.data.filter((user) => user.id !== id);
|
||||||
|
delete this.user[id];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
createUserData: Partial<User> = {
|
||||||
|
name: "",
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
is_admin: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
setCreateUserData = (
|
||||||
|
name: string,
|
||||||
|
email: string,
|
||||||
|
password: string,
|
||||||
|
is_admin: boolean
|
||||||
|
) => {
|
||||||
|
this.createUserData = { name, email, password, is_admin };
|
||||||
|
};
|
||||||
|
|
||||||
|
createUser = async () => {
|
||||||
|
let id = 1;
|
||||||
|
if (this.users.data.length > 0) {
|
||||||
|
id = this.users.data[this.users.data.length - 1].id + 1;
|
||||||
|
}
|
||||||
|
const response = await authInstance.post("/user", this.createUserData);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.users.data.push({
|
||||||
|
id: id,
|
||||||
|
...response.data,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
editUserData: Partial<User> = {
|
||||||
|
name: "",
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
is_admin: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
setEditUserData = (
|
||||||
|
name: string,
|
||||||
|
email: string,
|
||||||
|
password: string,
|
||||||
|
is_admin: boolean
|
||||||
|
) => {
|
||||||
|
this.editUserData = { name, email, password, is_admin };
|
||||||
|
};
|
||||||
|
|
||||||
|
editUser = async (id: number) => {
|
||||||
|
const response = await authInstance.patch(`/user/${id}`, this.editUserData);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.users.data = this.users.data.map((user) =>
|
||||||
|
user.id === id ? { ...user, ...response.data } : user
|
||||||
|
);
|
||||||
|
this.user[id] = { ...this.user[id], ...response.data };
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { authInstance } from "@shared";
|
import { languageInstance } from "@shared";
|
||||||
import { makeAutoObservable } from "mobx";
|
import { makeAutoObservable, runInAction } from "mobx";
|
||||||
|
|
||||||
export type Vehicle = {
|
export type Vehicle = {
|
||||||
vehicle: {
|
vehicle: {
|
||||||
@ -21,43 +21,131 @@ export type Vehicle = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class VehicleStore {
|
class VehicleStore {
|
||||||
vehicles: Vehicle[] = [];
|
vehicles: {
|
||||||
vehicle: Vehicle | null = null;
|
data: Vehicle[];
|
||||||
|
loaded: boolean;
|
||||||
|
} = {
|
||||||
|
data: [],
|
||||||
|
loaded: false,
|
||||||
|
};
|
||||||
|
vehicle: Record<string, Vehicle> = {};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
getVehicles = async () => {
|
getVehicles = async () => {
|
||||||
const response = await authInstance.get(`/vehicle`);
|
if (this.vehicles.loaded) return;
|
||||||
this.vehicles = response.data;
|
|
||||||
|
const response = await languageInstance("ru").get(`/vehicle`);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.vehicles.data = response.data;
|
||||||
|
this.vehicles.loaded = true;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteVehicle = async (id: number) => {
|
deleteVehicle = async (id: number) => {
|
||||||
await authInstance.delete(`/vehicle/${id}`);
|
await languageInstance("ru").delete(`/vehicle/${id}`);
|
||||||
this.vehicles = this.vehicles.filter(
|
|
||||||
(vehicle) => vehicle.vehicle.id !== id
|
runInAction(() => {
|
||||||
);
|
this.vehicles.data = this.vehicles.data.filter(
|
||||||
|
(vehicle) => vehicle.vehicle.id !== id
|
||||||
|
);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
getVehicle = async (id: number) => {
|
getVehicle = async (id: number) => {
|
||||||
const response = await authInstance.get(`/vehicle/${id}`);
|
const response = await languageInstance("ru").get(`/vehicle/${id}`);
|
||||||
this.vehicle = response.data;
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.vehicle[id] = response.data;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
createVehicle = async (
|
createVehicle = async (
|
||||||
tailNumber: number,
|
tailNumber: number,
|
||||||
type: string,
|
type: number,
|
||||||
carrier: string,
|
carrier: string,
|
||||||
carrierId: number
|
carrierId: number
|
||||||
) => {
|
) => {
|
||||||
await authInstance.post("/vehicle", {
|
const response = await languageInstance("ru").post("/vehicle", {
|
||||||
tail_number: tailNumber,
|
tail_number: tailNumber,
|
||||||
type,
|
type,
|
||||||
carrier,
|
carrier,
|
||||||
carrier_id: carrierId,
|
carrier_id: carrierId,
|
||||||
});
|
});
|
||||||
await this.getVehicles();
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.vehicles.data.push({
|
||||||
|
vehicle: {
|
||||||
|
id: response.data.id,
|
||||||
|
tail_number: response.data.tail_number,
|
||||||
|
type: response.data.type,
|
||||||
|
carrier_id: response.data.carrier_id,
|
||||||
|
carrier: response.data.carrier,
|
||||||
|
uuid: response.data.uuid,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
editVehicleData: {
|
||||||
|
tail_number: number;
|
||||||
|
type: number;
|
||||||
|
carrier: string;
|
||||||
|
carrier_id: number;
|
||||||
|
} = {
|
||||||
|
tail_number: 0,
|
||||||
|
type: 0,
|
||||||
|
carrier: "",
|
||||||
|
carrier_id: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
setEditVehicleData = (data: {
|
||||||
|
tail_number: number;
|
||||||
|
type: number;
|
||||||
|
carrier: string;
|
||||||
|
carrier_id: number;
|
||||||
|
}) => {
|
||||||
|
this.editVehicleData = {
|
||||||
|
...this.editVehicleData,
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
editVehicle = async (
|
||||||
|
id: number,
|
||||||
|
data: {
|
||||||
|
tail_number: number;
|
||||||
|
type: number;
|
||||||
|
carrier: string;
|
||||||
|
carrier_id: number;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const response = await languageInstance("ru").patch(`/vehicle/${id}`, {
|
||||||
|
tail_number: data.tail_number,
|
||||||
|
type: data.type,
|
||||||
|
carrier: data.carrier,
|
||||||
|
carrier_id: data.carrier_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.vehicle[id] = {
|
||||||
|
vehicle: {
|
||||||
|
...this.vehicle[id].vehicle,
|
||||||
|
...response.data,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this.vehicles.data = this.vehicles.data.map((vehicle) =>
|
||||||
|
vehicle.vehicle.id === id
|
||||||
|
? {
|
||||||
|
...vehicle,
|
||||||
|
...response.data,
|
||||||
|
}
|
||||||
|
: vehicle
|
||||||
|
);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,7 +121,7 @@ export const DevicesTable = observer(() => {
|
|||||||
// Transform the raw devices data into rows suitable for the table
|
// Transform the raw devices data into rows suitable for the table
|
||||||
// This will also filter out devices without a UUID, as those cannot be acted upon.
|
// This will also filter out devices without a UUID, as those cannot be acted upon.
|
||||||
const currentTableRows = transformDevicesToRows(
|
const currentTableRows = transformDevicesToRows(
|
||||||
vehicles as Vehicle[]
|
vehicles.data as Vehicle[]
|
||||||
// devices as ConnectedDevice[]
|
// devices as ConnectedDevice[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ export const LanguageSwitcher = observer(() => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed bottom-0 left-1/2 -translate-x-1/2 flex gap-2 p-4 z-10 ">
|
<div className="fixed bottom-0 left-1/2 -translate-x-1/2 flex gap-2 p-4 z-100000000">
|
||||||
{/* Added some styling for better visualization */}
|
{/* Added some styling for better visualization */}
|
||||||
{LANGUAGES.map((lang) => (
|
{LANGUAGES.map((lang) => (
|
||||||
<Button
|
<Button
|
||||||
|
@ -31,7 +31,7 @@ import { toast } from "react-toastify";
|
|||||||
|
|
||||||
export const CreateInformationTab = observer(
|
export const CreateInformationTab = observer(
|
||||||
({ value, index }: { value: number; index: number }) => {
|
({ value, index }: { value: number; index: number }) => {
|
||||||
const { cities } = cityStore;
|
const { ruCities } = cityStore;
|
||||||
const [mediaId, setMediaId] = useState<string>("");
|
const [mediaId, setMediaId] = useState<string>("");
|
||||||
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
|
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
|
||||||
const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false);
|
const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false);
|
||||||
@ -120,7 +120,10 @@ export const CreateInformationTab = observer(
|
|||||||
paddingBottom: "70px" /* Space for save button */,
|
paddingBottom: "70px" /* Space for save button */,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BackButton />
|
<div className="flex gap-10 items-center mb-5 max-w-[80%]">
|
||||||
|
<BackButton />
|
||||||
|
<h1 className="text-3xl break-words">{sight[language].name}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@ -172,9 +175,9 @@ export const CreateInformationTab = observer(
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
options={cities ?? []}
|
options={ruCities ?? []}
|
||||||
value={
|
value={
|
||||||
cities.find((city) => city.id === sight.city_id) ?? null
|
ruCities.find((city) => city.id === sight.city_id) ?? null
|
||||||
}
|
}
|
||||||
getOptionLabel={(option) => option.name}
|
getOptionLabel={(option) => option.name}
|
||||||
onChange={(_, value) => {
|
onChange={(_, value) => {
|
||||||
@ -271,7 +274,7 @@ export const CreateInformationTab = observer(
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<ImageUploadCard
|
<ImageUploadCard
|
||||||
title="Водяной знак (леввый верхний)"
|
title="Водяной знак (левый верхний)"
|
||||||
imageKey="watermark_lu"
|
imageKey="watermark_lu"
|
||||||
imageUrl={sight.watermark_lu}
|
imageUrl={sight.watermark_lu}
|
||||||
onImageClick={() => {
|
onImageClick={() => {
|
||||||
|
@ -109,7 +109,10 @@ export const CreateLeftTab = observer(
|
|||||||
position: "relative",
|
position: "relative",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BackButton />
|
<div className="flex gap-10 items-center mb-5 max-w-[80%]">
|
||||||
|
<BackButton />
|
||||||
|
<h1 className="text-3xl break-words">{sight[language].name}</h1>
|
||||||
|
</div>
|
||||||
<Paper
|
<Paper
|
||||||
elevation={2}
|
elevation={2}
|
||||||
sx={{
|
sx={{
|
||||||
@ -210,7 +213,7 @@ export const CreateLeftTab = observer(
|
|||||||
|
|
||||||
<ReactMarkdownEditor
|
<ReactMarkdownEditor
|
||||||
value={sight[language].left.body}
|
value={sight[language].left.body}
|
||||||
onChange={(value) =>
|
onChange={(value: any) =>
|
||||||
updateSightInfo(
|
updateSightInfo(
|
||||||
{
|
{
|
||||||
left: {
|
left: {
|
||||||
|
@ -256,7 +256,10 @@ export const CreateRightTab = observer(
|
|||||||
position: "relative",
|
position: "relative",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BackButton />
|
<div className="flex gap-10 items-center mb-5 max-w-[80%]">
|
||||||
|
<BackButton />
|
||||||
|
<h1 className="text-3xl break-words">{sight[language].name}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Box sx={{ display: "flex", flexGrow: 1, gap: 2.5 }}>
|
<Box sx={{ display: "flex", flexGrow: 1, gap: 2.5 }}>
|
||||||
{/* Left Column: Navigation & Article List */}
|
{/* Left Column: Navigation & Article List */}
|
||||||
|
@ -32,7 +32,7 @@ import { toast } from "react-toastify";
|
|||||||
|
|
||||||
export const InformationTab = observer(
|
export const InformationTab = observer(
|
||||||
({ value, index }: { value: number; index: number }) => {
|
({ value, index }: { value: number; index: number }) => {
|
||||||
const { cities } = cityStore;
|
const { ruCities } = cityStore;
|
||||||
|
|
||||||
const [mediaId, setMediaId] = useState<string>("");
|
const [mediaId, setMediaId] = useState<string>("");
|
||||||
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
|
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
|
||||||
@ -113,7 +113,11 @@ export const InformationTab = observer(
|
|||||||
paddingBottom: "70px" /* Space for save button */,
|
paddingBottom: "70px" /* Space for save button */,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BackButton />
|
<div className="flex gap-10 items-center mb-5 max-w-[80%]">
|
||||||
|
<BackButton />
|
||||||
|
<h1 className="text-3xl break-words">{sight[language].name}</h1>
|
||||||
|
</div>
|
||||||
|
<LanguageSwitcher />
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@ -159,9 +163,9 @@ export const InformationTab = observer(
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
options={cities ?? []}
|
options={ruCities ?? []}
|
||||||
value={
|
value={
|
||||||
cities.find((city) => city.id === sight.common.city_id) ??
|
ruCities.find((city) => city.id === sight.common.city_id) ??
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
getOptionLabel={(option) => option.name}
|
getOptionLabel={(option) => option.name}
|
||||||
|
@ -121,7 +121,10 @@ export const LeftWidgetTab = observer(
|
|||||||
position: "relative",
|
position: "relative",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BackButton />
|
<div className="flex gap-10 items-center mb-5 max-w-[80%]">
|
||||||
|
<BackButton />
|
||||||
|
<h1 className="text-3xl break-words">{sight[language].name}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Paper
|
<Paper
|
||||||
elevation={2}
|
elevation={2}
|
||||||
@ -223,7 +226,7 @@ export const LeftWidgetTab = observer(
|
|||||||
|
|
||||||
<ReactMarkdownEditor
|
<ReactMarkdownEditor
|
||||||
value={data?.left?.body}
|
value={data?.left?.body}
|
||||||
onChange={(value) =>
|
onChange={(value: any) =>
|
||||||
updateSightInfo(languageStore.language, {
|
updateSightInfo(languageStore.language, {
|
||||||
left: {
|
left: {
|
||||||
heading: sight[languageStore.language].left.heading,
|
heading: sight[languageStore.language].left.heading,
|
||||||
|
@ -199,7 +199,10 @@ export const RightWidgetTab = observer(
|
|||||||
position: "relative",
|
position: "relative",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BackButton />
|
<div className="flex gap-10 items-center mb-5 max-w-[80%]">
|
||||||
|
<BackButton />
|
||||||
|
<h1 className="text-3xl break-words">{sight[language].name}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Box sx={{ display: "flex", flexGrow: 1, gap: 2.5 }}>
|
<Box sx={{ display: "flex", flexGrow: 1, gap: 2.5 }}>
|
||||||
<Box className="flex flex-col w-[75%] gap-2">
|
<Box className="flex flex-col w-[75%] gap-2">
|
||||||
|
@ -35,7 +35,7 @@ export const SightsTable = observer(() => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
await getSights();
|
await getSights();
|
||||||
await getCities();
|
await getCities(language);
|
||||||
};
|
};
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [language, getSights, getCities]);
|
}, [language, getSights, getCities]);
|
||||||
@ -67,7 +67,7 @@ export const SightsTable = observer(() => {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{rows(sights, cities)?.map((row) => (
|
{rows(sights, cities[language])?.map((row) => (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={row?.id}
|
key={row?.id}
|
||||||
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
|
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
|
||||||
|
File diff suppressed because one or more lines are too long
147
yarn.lock
147
yarn.lock
@ -583,21 +583,28 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
promise-worker-transferable "^1.0.4"
|
promise-worker-transferable "^1.0.4"
|
||||||
|
|
||||||
"@mui/core-downloads-tracker@^7.1.0":
|
"@mui/core-downloads-tracker@^7.1.1":
|
||||||
version "7.1.0"
|
version "7.1.1"
|
||||||
resolved "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.1.0.tgz"
|
resolved "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.1.1.tgz"
|
||||||
integrity sha512-E0OqhZv548Qdc0PwWhLVA2zmjJZSTvaL4ZhoswmI8NJEC1tpW2js6LLP827jrW9MEiXYdz3QS6+hask83w74yQ==
|
integrity sha512-yBckQs4aQ8mqukLnPC6ivIRv6guhaXi8snVl00VtyojBbm+l6VbVhyTSZ68Abcx7Ah8B+GZhrB7BOli+e+9LkQ==
|
||||||
|
|
||||||
"@mui/material@^7.1.0":
|
"@mui/icons-material@^7.1.1":
|
||||||
version "7.1.0"
|
version "7.1.1"
|
||||||
resolved "https://registry.npmjs.org/@mui/material/-/material-7.1.0.tgz"
|
resolved "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.1.1.tgz"
|
||||||
integrity sha512-ahUJdrhEv+mCp4XHW+tHIEYzZMSRLg8z4AjUOsj44QpD1ZaMxQoVOG2xiHvLFdcsIPbgSRx1bg1eQSheHBgvtg==
|
integrity sha512-X37+Yc8QpEnl0sYmz+WcLFy2dWgNRzbswDzLPXG7QU1XDVlP5TPp1HXjdmCupOWLL/I9m1fyhcyZl8/HPpp/Cg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.27.1"
|
"@babel/runtime" "^7.27.1"
|
||||||
"@mui/core-downloads-tracker" "^7.1.0"
|
|
||||||
"@mui/system" "^7.1.0"
|
"@mui/material@^7.1.0":
|
||||||
"@mui/types" "^7.4.2"
|
version "7.1.1"
|
||||||
"@mui/utils" "^7.1.0"
|
resolved "https://registry.npmjs.org/@mui/material/-/material-7.1.1.tgz"
|
||||||
|
integrity sha512-mTpdmdZCaHCGOH3SrYM41+XKvNL0iQfM9KlYgpSjgadXx/fEKhhvOktxm8++Xw6FFeOHoOiV+lzOI8X1rsv71A==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.27.1"
|
||||||
|
"@mui/core-downloads-tracker" "^7.1.1"
|
||||||
|
"@mui/system" "^7.1.1"
|
||||||
|
"@mui/types" "^7.4.3"
|
||||||
|
"@mui/utils" "^7.1.1"
|
||||||
"@popperjs/core" "^2.11.8"
|
"@popperjs/core" "^2.11.8"
|
||||||
"@types/react-transition-group" "^4.4.12"
|
"@types/react-transition-group" "^4.4.12"
|
||||||
clsx "^2.1.1"
|
clsx "^2.1.1"
|
||||||
@ -606,19 +613,19 @@
|
|||||||
react-is "^19.1.0"
|
react-is "^19.1.0"
|
||||||
react-transition-group "^4.4.5"
|
react-transition-group "^4.4.5"
|
||||||
|
|
||||||
"@mui/private-theming@^7.1.0":
|
"@mui/private-theming@^7.1.1":
|
||||||
version "7.1.0"
|
version "7.1.1"
|
||||||
resolved "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.1.0.tgz"
|
resolved "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.1.1.tgz"
|
||||||
integrity sha512-4Kck4jxhqF6YxNwJdSae1WgDfXVg0lIH6JVJ7gtuFfuKcQCgomJxPvUEOySTFRPz1IZzwz5OAcToskRdffElDA==
|
integrity sha512-M8NbLUx+armk2ZuaxBkkMk11ultnWmrPlN0Xe3jUEaBChg/mcxa5HWIWS1EE4DF36WRACaAHVAvyekWlDQf0PQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.27.1"
|
"@babel/runtime" "^7.27.1"
|
||||||
"@mui/utils" "^7.1.0"
|
"@mui/utils" "^7.1.1"
|
||||||
prop-types "^15.8.1"
|
prop-types "^15.8.1"
|
||||||
|
|
||||||
"@mui/styled-engine@^7.1.0":
|
"@mui/styled-engine@^7.1.1":
|
||||||
version "7.1.0"
|
version "7.1.1"
|
||||||
resolved "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.1.0.tgz"
|
resolved "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.1.1.tgz"
|
||||||
integrity sha512-m0mJ0c6iRC+f9hMeRe0W7zZX1wme3oUX0+XTVHjPG7DJz6OdQ6K/ggEOq7ZdwilcpdsDUwwMfOmvO71qDkYd2w==
|
integrity sha512-R2wpzmSN127j26HrCPYVQ53vvMcT5DaKLoWkrfwUYq3cYytL6TQrCH8JBH3z79B6g4nMZZVoaXrxO757AlShaw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.27.1"
|
"@babel/runtime" "^7.27.1"
|
||||||
"@emotion/cache" "^11.13.5"
|
"@emotion/cache" "^11.13.5"
|
||||||
@ -627,28 +634,28 @@
|
|||||||
csstype "^3.1.3"
|
csstype "^3.1.3"
|
||||||
prop-types "^15.8.1"
|
prop-types "^15.8.1"
|
||||||
|
|
||||||
"@mui/system@^7.1.0":
|
"@mui/system@^7.1.1":
|
||||||
version "7.1.0"
|
version "7.1.1"
|
||||||
resolved "https://registry.npmjs.org/@mui/system/-/system-7.1.0.tgz"
|
resolved "https://registry.npmjs.org/@mui/system/-/system-7.1.1.tgz"
|
||||||
integrity sha512-iedAWgRJMCxeMHvkEhsDlbvkK+qKf9me6ofsf7twk/jfT4P1ImVf7Rwb5VubEA0sikrVL+1SkoZM41M4+LNAVA==
|
integrity sha512-Kj1uhiqnj4Zo7PDjAOghtXJtNABunWvhcRU0O7RQJ7WOxeynoH6wXPcilphV8QTFtkKaip8EiNJRiCD+B3eROA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.27.1"
|
"@babel/runtime" "^7.27.1"
|
||||||
"@mui/private-theming" "^7.1.0"
|
"@mui/private-theming" "^7.1.1"
|
||||||
"@mui/styled-engine" "^7.1.0"
|
"@mui/styled-engine" "^7.1.1"
|
||||||
"@mui/types" "^7.4.2"
|
"@mui/types" "^7.4.3"
|
||||||
"@mui/utils" "^7.1.0"
|
"@mui/utils" "^7.1.1"
|
||||||
clsx "^2.1.1"
|
clsx "^2.1.1"
|
||||||
csstype "^3.1.3"
|
csstype "^3.1.3"
|
||||||
prop-types "^15.8.1"
|
prop-types "^15.8.1"
|
||||||
|
|
||||||
"@mui/types@^7.4.2", "@mui/types@^7.4.3":
|
"@mui/types@^7.4.3":
|
||||||
version "7.4.3"
|
version "7.4.3"
|
||||||
resolved "https://registry.npmjs.org/@mui/types/-/types-7.4.3.tgz"
|
resolved "https://registry.npmjs.org/@mui/types/-/types-7.4.3.tgz"
|
||||||
integrity sha512-2UCEiK29vtiZTeLdS2d4GndBKacVyxGvReznGXGr+CzW/YhjIX+OHUdCIczZjzcRAgKBGmE9zCIgoV9FleuyRQ==
|
integrity sha512-2UCEiK29vtiZTeLdS2d4GndBKacVyxGvReznGXGr+CzW/YhjIX+OHUdCIczZjzcRAgKBGmE9zCIgoV9FleuyRQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.27.1"
|
"@babel/runtime" "^7.27.1"
|
||||||
|
|
||||||
"@mui/utils@^7.1.0", "@mui/utils@^7.1.1":
|
"@mui/utils@^7.1.1":
|
||||||
version "7.1.1"
|
version "7.1.1"
|
||||||
resolved "https://registry.npmjs.org/@mui/utils/-/utils-7.1.1.tgz"
|
resolved "https://registry.npmjs.org/@mui/utils/-/utils-7.1.1.tgz"
|
||||||
integrity sha512-BkOt2q7MBYl7pweY2JWwfrlahhp+uGLR8S+EhiyRaofeRYUWL2YKbSGQvN4hgSN1i8poN0PaUiii1kEMrchvzg==
|
integrity sha512-BkOt2q7MBYl7pweY2JWwfrlahhp+uGLR8S+EhiyRaofeRYUWL2YKbSGQvN4hgSN1i8poN0PaUiii1kEMrchvzg==
|
||||||
@ -682,9 +689,9 @@
|
|||||||
"@mui/utils" "^7.1.1"
|
"@mui/utils" "^7.1.1"
|
||||||
|
|
||||||
"@napi-rs/wasm-runtime@^0.2.10":
|
"@napi-rs/wasm-runtime@^0.2.10":
|
||||||
version "0.2.10"
|
version "0.2.11"
|
||||||
resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.10.tgz#f3b7109419c6670000b2401e0c778b98afc25f84"
|
resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.11.tgz#192c1610e1625048089ab4e35bc0649ce478500e"
|
||||||
integrity sha512-bCsCyeZEwVErsGmyPNSzwfwFn4OdxBj0mmv6hOFucB/k81Ojdu68RbZdxYsRQUPc9l6SU5F/cG+bXgWs3oUgsQ==
|
integrity sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@emnapi/core" "^1.4.3"
|
"@emnapi/core" "^1.4.3"
|
||||||
"@emnapi/runtime" "^1.4.3"
|
"@emnapi/runtime" "^1.4.3"
|
||||||
@ -723,6 +730,19 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
three "^0.175.0"
|
three "^0.175.0"
|
||||||
|
|
||||||
|
"@pixi/colord@^2.9.6":
|
||||||
|
version "2.9.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@pixi/colord/-/colord-2.9.6.tgz#7e4e7851480da6fd3cef4e331f008d60be7e1204"
|
||||||
|
integrity sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA==
|
||||||
|
|
||||||
|
"@pixi/react@^8.0.2":
|
||||||
|
version "8.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@pixi/react/-/react-8.0.2.tgz#d58288b2405cc4f40733297b9d55d016d74045bd"
|
||||||
|
integrity sha512-A42Bw/1YlxxCXhb+nDIgzPpACx5dOh7Yi+ZfBMlZ1sBB/qc7qyX9k7bdDtrnMqz8OofyF2FXg9gPw9MISFsQTA==
|
||||||
|
dependencies:
|
||||||
|
its-fine "^2.0.0"
|
||||||
|
react-reconciler "0.31.0"
|
||||||
|
|
||||||
"@popperjs/core@^2.11.8":
|
"@popperjs/core@^2.11.8":
|
||||||
version "2.11.8"
|
version "2.11.8"
|
||||||
resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz"
|
resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz"
|
||||||
@ -1050,6 +1070,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/tern" "*"
|
"@types/tern" "*"
|
||||||
|
|
||||||
|
"@types/css-font-loading-module@^0.0.12":
|
||||||
|
version "0.0.12"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/css-font-loading-module/-/css-font-loading-module-0.0.12.tgz#65494833928823f998fbe8e86312821875d80db5"
|
||||||
|
integrity sha512-x2tZZYkSxXqWvTDgveSynfjq/T2HyiZHXb00j/+gy19yp70PHCizM48XFdjBCWH7eHBD0R5i/pw9yMBP/BH5uA==
|
||||||
|
|
||||||
"@types/debug@^4.0.0":
|
"@types/debug@^4.0.0":
|
||||||
version "4.1.12"
|
version "4.1.12"
|
||||||
resolved "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz"
|
resolved "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz"
|
||||||
@ -1062,6 +1087,11 @@
|
|||||||
resolved "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz"
|
resolved "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz"
|
||||||
integrity sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==
|
integrity sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==
|
||||||
|
|
||||||
|
"@types/earcut@^3.0.0":
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/earcut/-/earcut-3.0.0.tgz#c21ab8372c47f8af1bec63cb36eecb6917b6c5b6"
|
||||||
|
integrity sha512-k/9fOUGO39yd2sCjrbAJvGDEQvRwRnQIZlBz43roGwUZo5SHAmyVvSFyaVVZkicRVCaDXPKlbxrUcBuJoSWunQ==
|
||||||
|
|
||||||
"@types/estree-jsx@^1.0.0":
|
"@types/estree-jsx@^1.0.0":
|
||||||
version "1.0.5"
|
version "1.0.5"
|
||||||
resolved "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz"
|
resolved "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz"
|
||||||
@ -1323,11 +1353,16 @@
|
|||||||
"@types/babel__core" "^7.20.5"
|
"@types/babel__core" "^7.20.5"
|
||||||
react-refresh "^0.17.0"
|
react-refresh "^0.17.0"
|
||||||
|
|
||||||
"@webgpu/types@*":
|
"@webgpu/types@*", "@webgpu/types@^0.1.40":
|
||||||
version "0.1.61"
|
version "0.1.61"
|
||||||
resolved "https://registry.npmjs.org/@webgpu/types/-/types-0.1.61.tgz"
|
resolved "https://registry.npmjs.org/@webgpu/types/-/types-0.1.61.tgz"
|
||||||
integrity sha512-w2HbBvH+qO19SB5pJOJFKs533CdZqxl3fcGonqL321VHkW7W/iBo6H8bjDy6pr/+pbMwIu5dnuaAxH7NxBqUrQ==
|
integrity sha512-w2HbBvH+qO19SB5pJOJFKs533CdZqxl3fcGonqL321VHkW7W/iBo6H8bjDy6pr/+pbMwIu5dnuaAxH7NxBqUrQ==
|
||||||
|
|
||||||
|
"@xmldom/xmldom@^0.8.10":
|
||||||
|
version "0.8.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.10.tgz#a1337ca426aa61cef9fe15b5b28e340a72f6fa99"
|
||||||
|
integrity sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==
|
||||||
|
|
||||||
acorn-jsx@^5.3.2:
|
acorn-jsx@^5.3.2:
|
||||||
version "5.3.2"
|
version "5.3.2"
|
||||||
resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
|
resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
|
||||||
@ -1686,7 +1721,7 @@ dunder-proto@^1.0.1:
|
|||||||
es-errors "^1.3.0"
|
es-errors "^1.3.0"
|
||||||
gopd "^1.2.0"
|
gopd "^1.2.0"
|
||||||
|
|
||||||
earcut@^3.0.0:
|
earcut@^3.0.0, earcut@^3.0.1:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.npmjs.org/earcut/-/earcut-3.0.1.tgz"
|
resolved "https://registry.npmjs.org/earcut/-/earcut-3.0.1.tgz"
|
||||||
integrity sha512-0l1/0gOjESMeQyYaK5IDiPNvFeu93Z/cO0TjZh9eZ1vyCtZnA7KMZ8rQggpsJHIbGSdrqYq9OhuveadOVHCshw==
|
integrity sha512-0l1/0gOjESMeQyYaK5IDiPNvFeu93Z/cO0TjZh9eZ1vyCtZnA7KMZ8rQggpsJHIbGSdrqYq9OhuveadOVHCshw==
|
||||||
@ -2075,6 +2110,13 @@ get-proto@^1.0.1:
|
|||||||
dunder-proto "^1.0.1"
|
dunder-proto "^1.0.1"
|
||||||
es-object-atoms "^1.0.0"
|
es-object-atoms "^1.0.0"
|
||||||
|
|
||||||
|
gifuct-js@^2.1.2:
|
||||||
|
version "2.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/gifuct-js/-/gifuct-js-2.1.2.tgz#06152437ba30ec914db8398bd838bd0fbc8a6ecd"
|
||||||
|
integrity sha512-rI2asw77u0mGgwhV3qA+OEgYqaDn5UNqgs+Bx0FGwSpuqfYn+Ir6RQY5ENNQ8SbIiG/m5gVa7CD5RriO4f4Lsg==
|
||||||
|
dependencies:
|
||||||
|
js-binary-schema-parser "^2.0.3"
|
||||||
|
|
||||||
glob-parent@^5.1.2:
|
glob-parent@^5.1.2:
|
||||||
version "5.1.2"
|
version "5.1.2"
|
||||||
resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz"
|
resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz"
|
||||||
@ -2372,6 +2414,11 @@ isexe@^2.0.0:
|
|||||||
resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz"
|
resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz"
|
||||||
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
|
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
|
||||||
|
|
||||||
|
ismobilejs@^1.1.1:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/ismobilejs/-/ismobilejs-1.1.1.tgz#c56ca0ae8e52b24ca0f22ba5ef3215a2ddbbaa0e"
|
||||||
|
integrity sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw==
|
||||||
|
|
||||||
its-fine@^2.0.0:
|
its-fine@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz"
|
resolved "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz"
|
||||||
@ -2384,6 +2431,11 @@ jiti@^2.4.2:
|
|||||||
resolved "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz"
|
resolved "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz"
|
||||||
integrity sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==
|
integrity sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==
|
||||||
|
|
||||||
|
js-binary-schema-parser@^2.0.3:
|
||||||
|
version "2.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/js-binary-schema-parser/-/js-binary-schema-parser-2.0.3.tgz#3d7848748e8586e63b34e8911b643f59cfb6396e"
|
||||||
|
integrity sha512-xezGJmOb4lk/M1ZZLTR/jaBHQ4gG/lqQnJqdIv4721DMggsa1bDVlHXNeHYogaIEHD9vCRv0fcL4hMA+Coarkg==
|
||||||
|
|
||||||
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
|
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz"
|
resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz"
|
||||||
@ -3073,6 +3125,11 @@ parse-json@^5.0.0:
|
|||||||
json-parse-even-better-errors "^2.3.0"
|
json-parse-even-better-errors "^2.3.0"
|
||||||
lines-and-columns "^1.1.6"
|
lines-and-columns "^1.1.6"
|
||||||
|
|
||||||
|
parse-svg-path@^0.1.2:
|
||||||
|
version "0.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/parse-svg-path/-/parse-svg-path-0.1.2.tgz#7a7ec0d1eb06fa5325c7d3e009b859a09b5d49eb"
|
||||||
|
integrity sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==
|
||||||
|
|
||||||
parse5@^7.0.0:
|
parse5@^7.0.0:
|
||||||
version "7.3.0"
|
version "7.3.0"
|
||||||
resolved "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz"
|
resolved "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz"
|
||||||
@ -3130,6 +3187,22 @@ picomatch@^4.0.2:
|
|||||||
resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz"
|
resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz"
|
||||||
integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==
|
integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==
|
||||||
|
|
||||||
|
pixi.js@^8.10.1:
|
||||||
|
version "8.10.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/pixi.js/-/pixi.js-8.10.1.tgz#da7aca668fdb13c31b7eb4467a19ce5fa89db34e"
|
||||||
|
integrity sha512-wjKJXawhTUxuyKIuwE3jK05eBh5I4GKy+YrRVniURFRkK7pYEvRvnV41dEqz6owSXav/YMXdG5783YDJeamiow==
|
||||||
|
dependencies:
|
||||||
|
"@pixi/colord" "^2.9.6"
|
||||||
|
"@types/css-font-loading-module" "^0.0.12"
|
||||||
|
"@types/earcut" "^3.0.0"
|
||||||
|
"@webgpu/types" "^0.1.40"
|
||||||
|
"@xmldom/xmldom" "^0.8.10"
|
||||||
|
earcut "^3.0.1"
|
||||||
|
eventemitter3 "^5.0.1"
|
||||||
|
gifuct-js "^2.1.2"
|
||||||
|
ismobilejs "^1.1.1"
|
||||||
|
parse-svg-path "^0.1.2"
|
||||||
|
|
||||||
postcss-selector-parser@6.0.10:
|
postcss-selector-parser@6.0.10:
|
||||||
version "6.0.10"
|
version "6.0.10"
|
||||||
resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz"
|
resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz"
|
||||||
@ -3286,7 +3359,7 @@ react-photo-sphere-viewer@^6.2.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
eventemitter3 "^5.0.1"
|
eventemitter3 "^5.0.1"
|
||||||
|
|
||||||
react-reconciler@^0.31.0:
|
react-reconciler@0.31.0, react-reconciler@^0.31.0:
|
||||||
version "0.31.0"
|
version "0.31.0"
|
||||||
resolved "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.31.0.tgz"
|
resolved "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.31.0.tgz"
|
||||||
integrity sha512-7Ob7Z+URmesIsIVRjnLoDGwBEG/tVitidU0nMsqX/eeJaLY89RISO/10ERe0MqmzuKUUB1rmY+h1itMbUHg9BQ==
|
integrity sha512-7Ob7Z+URmesIsIVRjnLoDGwBEG/tVitidU0nMsqX/eeJaLY89RISO/10ERe0MqmzuKUUB1rmY+h1itMbUHg9BQ==
|
||||||
|
Reference in New Issue
Block a user