init: Init React Application

This commit is contained in:
2025-05-29 13:21:33 +03:00
parent 9444939507
commit 17de7e495f
66 changed files with 10425 additions and 0 deletions

View File

@ -0,0 +1,206 @@
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import Paper from "@mui/material/Paper";
import { Check, RotateCcw, Send, X } from "lucide-react";
import { devicesStore, Modal, snapshotStore, vehicleStore } from "@shared";
import { useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
import { Button, Checkbox } from "@mui/material";
const formatDate = (dateString: string | undefined) => {
if (!dateString) return "Нет данных";
try {
const date = new Date(dateString);
return new Intl.DateTimeFormat("ru-RU", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
}).format(date);
} catch (error) {
console.error("Error formatting date:", error);
return "Некорректная дата";
}
};
function createData(
uuid: string,
online: boolean,
lastUpdate: string,
gps: boolean,
media: boolean,
connection: boolean
) {
return { uuid, online, lastUpdate, gps, media, connection };
}
const rows = (devices: any[], vehicles: any[]) => {
return devices.map((device) => {
const { device_status } = vehicles.find(
(v) => v?.device_status?.device_uuid === device
);
const findVehicle = vehicles.find((v) => v?.vehicle?.uuid === device);
console.log(findVehicle);
return createData(
findVehicle?.vehicle?.tail_number ?? "1243000",
device_status?.online,
device_status?.last_update,
device_status?.gps_ok,
device_status?.media_service_ok,
device_status?.is_connected
);
});
};
export const DevicesTable = observer(() => {
const {
devices,
getDevices,
uuid,
setSelectedDevice,
sendSnapshotModalOpen,
toggleSendSnapshotModal,
} = devicesStore;
const { snapshots, getSnapshots } = snapshotStore;
const { vehicles, getVehicles } = vehicleStore;
const [selectedDevices, setSelectedDevices] = useState<string[]>([]);
useEffect(() => {
const fetchData = async () => {
await getVehicles();
await getDevices();
await getSnapshots();
};
fetchData();
}, []);
const handleSendSnapshot = (uuid: string[]) => {
setSelectedDevice(uuid);
toggleSendSnapshotModal();
};
const handleReloadStatus = (uuid: string) => {
setSelectedDevice(uuid);
};
const handleSelectDevice = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.checked) {
setSelectedDevices([...selectedDevices, event.target.value]);
} else {
setSelectedDevices(
selectedDevices.filter((device) => device !== event.target.value)
);
}
};
return (
<>
<TableContainer component={Paper}>
<div className="flex justify-end p-3 gap-5">
<Button variant="contained" color="primary">
Выбрать все
</Button>
<Button
variant="contained"
color="primary"
disabled={selectedDevices.length === 0}
className="ml-auto"
onClick={() => handleSendSnapshot(selectedDevices)}
>
Отправить снапшот
</Button>
</div>
<Table sx={{ minWidth: 650 }} aria-label="simple table">
<TableHead>
<TableRow>
<TableCell align="center"></TableCell>
<TableCell align="center">Бортовой номер</TableCell>
<TableCell align="center">Онлайн</TableCell>
<TableCell align="center">Последнее обновление</TableCell>
<TableCell align="center">ГПС</TableCell>
<TableCell align="center">Медиа-данные</TableCell>
<TableCell align="center">Подключение</TableCell>
<TableCell align="center">Перезапросить</TableCell>
</TableRow>
</TableHead>
<TableBody>
{rows(devices, vehicles).map((row) => (
<TableRow
key={row?.uuid}
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
className="flex items-center"
>
<TableCell align="center">
<Checkbox
className="h-full"
onChange={handleSelectDevice}
value={row?.uuid}
/>
</TableCell>
<TableCell align="center" component="th" scope="row">
{row?.uuid}
</TableCell>
<TableCell align="center">
{row?.online ? (
<Check className="m-auto text-green-500" />
) : (
<X className="m-auto text-red-500" />
)}
</TableCell>
<TableCell align="center">
{formatDate(row?.lastUpdate)}
</TableCell>
<TableCell align="center">
{row?.gps ? (
<Check className="m-auto text-green-500" />
) : (
<X className="m-auto text-red-500" />
)}
</TableCell>
<TableCell align="center">
{row?.media ? (
<Check className="m-auto text-green-500" />
) : (
<X className="m-auto text-red-500" />
)}
</TableCell>
<TableCell align="center">
{row?.connection ? (
<Check className="m-auto text-green-500" />
) : (
<X className="m-auto text-red-500" />
)}
</TableCell>
<TableCell align="center" className="flex justify-center">
<button onClick={() => handleReloadStatus(row?.uuid ?? "")}>
<RotateCcw className="m-auto" />
</button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<Modal open={sendSnapshotModalOpen} onClose={toggleSendSnapshotModal}>
<p>Выбрать снапшот</p>
<div className="mt-5 flex flex-col gap-2 max-h-[300px] overflow-y-auto">
{snapshots &&
snapshots.map((snapshot) => (
<button className="p-2 rounded-xl bg-slate-100" key={snapshot.id}>
{snapshot.Name}
</button>
))}
</div>
</Modal>
</>
);
});

View File

@ -0,0 +1,29 @@
import { languageStore } from "@shared";
import { observer } from "mobx-react-lite";
export const LanguageSwitcher = observer(() => {
const { language, setLanguage } = languageStore;
return (
<div className="flex flex-col gap-2">
<button
className={`p-3 ${language === "ru" ? "bg-blue-500" : ""}`}
onClick={() => setLanguage("ru")}
>
RU
</button>
<button
className={`p-3 ${language === "en" ? "bg-blue-500" : ""}`}
onClick={() => setLanguage("en")}
>
EN
</button>
<button
className={`p-3 ${language === "zh" ? "bg-blue-500" : ""}`}
onClick={() => setLanguage("zh")}
>
zh
</button>
</div>
);
});

View File

@ -0,0 +1,67 @@
import * as React from "react";
import Box from "@mui/material/Box";
import Toolbar from "@mui/material/Toolbar";
import IconButton from "@mui/material/IconButton";
import Typography from "@mui/material/Typography";
import { Menu, ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
import { useTheme } from "@mui/material/styles";
import { AppBar } from "./ui/AppBar";
import { Drawer } from "./ui/Drawer";
import { DrawerHeader } from "./ui/DrawerHeader";
import { NavigationList } from "@features";
interface LayoutProps {
children: React.ReactNode;
}
export const Layout: React.FC<LayoutProps> = ({ children }) => {
const theme = useTheme();
const [open, setOpen] = React.useState(false);
const handleDrawerOpen = () => {
setOpen(true);
};
const handleDrawerClose = () => {
setOpen(false);
};
return (
<Box sx={{ display: "flex" }}>
<AppBar position="fixed" open={open}>
<Toolbar>
<IconButton
color="inherit"
aria-label="open drawer"
onClick={handleDrawerOpen}
edge="start"
sx={[
{
marginRight: 5,
},
open && { display: "none" },
]}
>
<Menu />
</IconButton>
</Toolbar>
</AppBar>
<Drawer variant="permanent" open={open}>
<DrawerHeader>
<IconButton onClick={handleDrawerClose}>
{theme.direction === "rtl" ? (
<ChevronRightIcon />
) : (
<ChevronLeftIcon />
)}
</IconButton>
</DrawerHeader>
<NavigationList open={open} />
</Drawer>
<Box component="main" sx={{ flexGrow: 1, p: 3 }}>
<DrawerHeader />
{children}
</Box>
</Box>
);
};

View File

@ -0,0 +1,27 @@
import { styled } from "@mui/material/styles";
import MuiAppBar, {
type AppBarProps as MuiAppBarProps,
} from "@mui/material/AppBar";
import { DRAWER_WIDTH } from "@shared";
interface AppBarProps extends MuiAppBarProps {
open?: boolean;
}
export const AppBar = styled(MuiAppBar, {
shouldForwardProp: (prop) => prop !== "open",
})<AppBarProps>(({ theme, open }) => ({
zIndex: theme.zIndex.drawer + 1,
transition: theme.transitions.create(["width", "margin"], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
...(open && {
marginLeft: DRAWER_WIDTH,
width: `calc(100% - ${DRAWER_WIDTH}px)`,
transition: theme.transitions.create(["width", "margin"], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
}),
}));

View File

@ -0,0 +1,42 @@
import { styled } from "@mui/material/styles";
import MuiDrawer from "@mui/material/Drawer";
import type { Theme, CSSObject } from "@mui/material/styles";
import { DRAWER_WIDTH } from "@shared";
const openedMixin = (theme: Theme): CSSObject => ({
width: DRAWER_WIDTH,
transition: theme.transitions.create("width", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
overflowX: "hidden",
});
const closedMixin = (theme: Theme): CSSObject => ({
transition: theme.transitions.create("width", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
overflowX: "hidden",
width: `calc(${theme.spacing(7)} + 1px)`,
[theme.breakpoints.up("sm")]: {
width: `calc(${theme.spacing(8)} + 1px)`,
},
});
export const Drawer = styled(MuiDrawer, {
shouldForwardProp: (prop) => prop !== "open",
})(({ theme, open }) => ({
width: DRAWER_WIDTH,
flexShrink: 0,
whiteSpace: "nowrap",
boxSizing: "border-box",
...(open && {
...openedMixin(theme),
"& .MuiDrawer-paper": openedMixin(theme),
}),
...(!open && {
...closedMixin(theme),
"& .MuiDrawer-paper": closedMixin(theme),
}),
}));

View File

@ -0,0 +1,10 @@
import { styled } from "@mui/material/styles";
import type { Theme } from "@mui/material/styles";
export const DrawerHeader = styled("div")(({ theme }: { theme: Theme }) => ({
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
padding: theme.spacing(0, 1),
...theme.mixins.toolbar,
}));

View File

@ -0,0 +1,50 @@
import { Box } from "@mui/material";
import ReactMarkdown from "react-markdown";
import rehypeRaw from "rehype-raw";
export const ReactMarkdownComponent = ({ value }: { value: string }) => {
return (
<Box
sx={{
"& img": {
maxWidth: "100%",
height: "auto",
borderRadius: 1,
},
"& h1, & h2, & h3, & h4, & h5, & h6": {
color: "primary.main",
mt: 2,
mb: 1,
},
"& p": {
mb: 2,
color: (theme) =>
theme.palette.mode === "dark" ? "grey.300" : "grey.800",
},
"& a": {
color: "primary.main",
textDecoration: "none",
"&:hover": {
textDecoration: "underline",
},
},
"& blockquote": {
borderLeft: "4px solid",
borderColor: "primary.main",
pl: 2,
my: 2,
color: "text.secondary",
},
"& code": {
bgcolor: (theme) =>
theme.palette.mode === "dark" ? "grey.900" : "grey.100",
p: 0.5,
borderRadius: 0.5,
color: "primary.main",
},
}}
>
<ReactMarkdown rehypePlugins={[rehypeRaw]}>{value}</ReactMarkdown>
</Box>
);
};

View File

@ -0,0 +1,109 @@
import { styled } from "@mui/material/styles";
import SimpleMDE, { SimpleMDEReactProps } from "react-simplemde-editor";
import "easymde/dist/easymde.min.css";
const StyledMarkdownEditor = styled("div")(({ theme }) => ({
"& .editor-toolbar": {
backgroundColor: theme.palette.background.paper,
borderColor: theme.palette.divider,
},
"& .editor-toolbar button": {
color: theme.palette.text.primary,
},
"& .editor-toolbar button:hover": {
backgroundColor: theme.palette.action.hover,
},
"& .editor-toolbar button:active, & .editor-toolbar button.active": {
backgroundColor: theme.palette.action.selected,
},
"& .editor-statusbar": {
display: "none",
},
// Стили для самого редактора
"& .CodeMirror": {
backgroundColor: theme.palette.background.paper,
color: theme.palette.text.primary,
borderColor: theme.palette.divider,
},
// Стили для текста в редакторе
"& .CodeMirror-selected": {
backgroundColor: `${theme.palette.action.selected} !important`,
},
"& .CodeMirror-cursor": {
borderLeftColor: theme.palette.text.primary,
},
// Стили для markdown разметки
"& .cm-header": {
color: theme.palette.primary.main,
},
"& .cm-quote": {
color: theme.palette.text.secondary,
fontStyle: "italic",
},
"& .cm-link": {
color: theme.palette.primary.main,
},
"& .cm-url": {
color: theme.palette.secondary.main,
},
"& .cm-formatting": {
color: theme.palette.text.secondary,
},
"& .CodeMirror .editor-preview-full": {
backgroundColor: theme.palette.background.paper,
color: theme.palette.text.primary,
borderColor: theme.palette.divider,
},
"& .EasyMDEContainer": {
position: "relative",
zIndex: 1000,
},
"& .guide": {
display: "none",
},
}));
export const ReactMarkdownEditor = (props: SimpleMDEReactProps) => {
if (props.options)
props.options.toolbar = [
"bold",
"italic",
"strikethrough",
{
name: "Underline",
action: (editor: any) => {
const cm = editor.codemirror;
let output = "";
const selectedText = cm.getSelection();
const text = selectedText ?? "placeholder";
output = "<u>" + text + "</u>";
cm.replaceSelection(output);
},
className: "fa fa-underline", // Look for a suitable icon
title: "Underline (Ctrl/Cmd-Alt-U)",
},
"heading",
"quote",
"unordered-list",
"ordered-list",
"link",
"image",
"code",
"table",
"horizontal-rule",
"preview",
"fullscreen",
"guide",
];
return (
<StyledMarkdownEditor
className="my-markdown-editor"
sx={{ marginTop: 1.5, marginBottom: 3 }}
>
<SimpleMDE {...props} />
</StyledMarkdownEditor>
);
};

View File

@ -0,0 +1,33 @@
import { Unlink } from "lucide-react";
import { Trash2 } from "lucide-react";
import { TextField } from "@mui/material";
import { ReactMarkdownEditor } from "@widgets";
export const SightEdit = () => {
return (
<div className="flex gap-3">
<div className="flex flex-1 flex-col gap-3">
<div className="flex items-center gap-2 justify-end">
<button className="flex items-center gap-2 border border-gray-300 bg-blue-100 rounded-md px-2 py-1">
Открепить
<Unlink />
</button>
<button className="flex items-center gap-2 border border-gray-300 bg-red-100 rounded-md px-2 py-1">
Удалить
<Trash2 />
</button>
</div>
<TextField label="Заголовок" />
<ReactMarkdownEditor />
</div>
<div className="flex flex-col gap-3 w-[350px]">
<p>Превью</p>
<div className=" w-full bg-red-500">1</div>
</div>
</div>
);
};

View File

@ -0,0 +1 @@
export * from "./ui";

View File

@ -0,0 +1,3 @@
export const SightHeader = () => {
return <div>SightHeader</div>;
};

View File

@ -0,0 +1,48 @@
import { TextField } from "@mui/material";
import { BackButton, TabPanel } from "@shared";
import { LanguageSwitcher } from "@widgets";
export const InformationTab = ({
value,
index,
}: {
value: number;
index: number;
}) => {
return (
<TabPanel value={value} index={index}>
<div className="flex-1 flex flex-col relative">
<div className="flex-1 flex flex-col gap-10">
<BackButton />
<div className="flex flex-col gap-5 w-1/2">
<TextField label="Название" />
<TextField label="Адрес" />
<TextField label="Город" />
<TextField label="Координаты" />
<div className="flex justify-around w-full mt-20">
<div className="flex flex-col gap-2 ">
<p>Логотип</p>
<button>Выбрать</button>
</div>
<div className="flex flex-col gap-2">
<p>Водяной знак (л.в)</p>
<button>Выбрать</button>
</div>
<div className="flex flex-col gap-2">
<p>Водяной знак (п.в)</p>
<button>Выбрать</button>
</div>
</div>
</div>
</div>
<button className="bg-green-400 w-min ml-auto text-white py-2 rounded-2xl px-4">
Сохранить
</button>
<div className="absolute top-1/2 -translate-y-1/2 right-0">
<LanguageSwitcher />
</div>
</div>
</TabPanel>
);
};

View File

@ -0,0 +1,60 @@
import { TextField } from "@mui/material";
import { BackButton, TabPanel } from "@shared";
import { ReactMarkdownComponent, ReactMarkdownEditor } from "@widgets";
import { Trash2 } from "lucide-react";
import { Unlink } from "lucide-react";
import { useState } from "react";
export const LeftWidgetTab = ({
value,
index,
}: {
value: number;
index: number;
}) => {
const [leftArticleData, setLeftArticleData] = useState(" ");
return (
<TabPanel value={value} index={index}>
<div className="flex flex-col gap-5">
<BackButton />
<div className="flex items-center justify-between px-5 h-14 rounded-md border">
<p className="text-2xl">Левая статья</p>
<div className="flex items-center gap-5">
<button className="flex items-center gap-2 border border-gray-300 bg-blue-100 rounded-md px-2 py-1">
Открепить
<Unlink />
</button>
<button className="flex items-center gap-2 border border-gray-300 bg-red-100 rounded-md px-2 py-1">
Удалить
<Trash2 />
</button>
</div>
</div>
<div className="flex gap-5">
<div className="flex flex-col gap-5 flex-1">
<TextField sx={{ width: "30%" }} label="Название" />
<ReactMarkdownEditor
value={leftArticleData}
onChange={setLeftArticleData}
/>
</div>
<div className="flex flex-col gap-2">
<p>Предпросмотр</p>
<div className="bg-yellow-200 w-[350px] h-full">
<div className="bg-red-100 w-full h-[200px]"></div>
<div className="bg-blue-100 w-full text-lg p-3"></div>
<div className="bg-green-100 p-3 prose max-w-none">
<ReactMarkdownComponent value={leftArticleData} />
</div>
</div>
</div>
</div>
<button className="bg-green-400 w-min ml-auto text-white py-2 rounded-2xl px-4">
Сохранить
</button>
</div>
</TabPanel>
);
};

View File

@ -0,0 +1,73 @@
import { BackButton, TabPanel } from "@shared";
import { SightEdit } from "@widgets";
import { Plus } from "lucide-react";
export const RightWidgetTab = ({
value,
index,
}: {
value: number;
index: number;
}) => {
return (
<TabPanel value={value} index={index}>
{/* Ensure the main container takes full height and uses flexbox for layout */}
<div className="flex flex-col h-full min-h-[600px]">
{/* Content area with back button and main layout */}
<div className="flex-1 flex flex-col gap-6 p-4">
{" "}
{/* Added padding for better spacing */}
<BackButton />
<div className="flex flex-1 gap-6">
{" "}
{/* flex-1 allows this div to take remaining height */}
{/* Left sidebar */}
<div className="flex flex-col justify-between w-[240px] shrink-0 bg-gray-500 rounded-lg p-3">
{" "}
{/* Added background and padding */}
<div className="flex flex-col gap-3">
<div className="border rounded-lg p-3 bg-white font-medium shadow-sm">
{" "}
{/* Adjusted background and added shadow */}
Превью медиа
</div>
<div className="border rounded-lg p-3 bg-white hover:bg-gray-100 transition-colors cursor-pointer shadow-sm">
{" "}
{/* Adjusted background and added shadow */}1 История
</div>
<div className="border rounded-lg p-3 bg-white hover:bg-gray-100 transition-colors cursor-pointer shadow-sm">
{" "}
{/* Adjusted background and added shadow */}2 Факты
</div>
</div>
<button className="w-10 h-10 rounded-full bg-blue-500 hover:bg-blue-600 text-white p-3transition-colors flex items-center justify-center">
{" "}
{/* Added margin-top */}
<Plus />
</button>
</div>
{/* Main content area */}
<div className="flex-1 border rounded-lg p-6 bg-white shadow-md">
{" "}
{/* Added shadow for depth */}
{/* Content within the main area */}
<SightEdit />
{/* Replaced '1' with more descriptive content */}
</div>
</div>
</div>
{/* Save button at the bottom, aligned to the right */}
<div className="flex justify-end p-4">
{" "}
{/* Wrapper for save button, added padding */}
<button className="bg-green-500 hover:bg-green-600 text-white py-2.5 px-6 rounded-lg transition-colors font-medium shadow-md">
{" "}
{/* Added shadow */}
Сохранить
</button>
</div>
</div>
</TabPanel>
);
};

View File

@ -0,0 +1,3 @@
export * from "./InformationTab";
export * from "./LeftWidgetTab";
export * from "./RightWidgetTab";

8
src/widgets/index.ts Normal file
View File

@ -0,0 +1,8 @@
export * from "./Layout";
export * from "./SightHeader";
export * from "./SightTabs";
export * from "./ReactMarkdown";
export * from "./ReactMarkdownEditor";
export * from "./SightEdit";
export * from "./LanguageSwitcher";
export * from "./DevicesTable";