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

12
src/shared/api/index.tsx Normal file
View File

@ -0,0 +1,12 @@
import axios from "axios";
const authInstance = axios.create({
baseURL: "https://wn.krbl.ru",
});
authInstance.interceptors.request.use((config) => {
config.headers.Authorization = `Bearer ${localStorage.getItem("token")}`;
return config;
});
export { authInstance };

View File

@ -0,0 +1,37 @@
import { Power, LucideIcon, Building2, MonitorSmartphone } from "lucide-react";
export const DRAWER_WIDTH = 300;
interface NavigationItem {
id: string;
label: string;
icon: LucideIcon;
path: string;
}
export const NAVIGATION_ITEMS: {
primary: NavigationItem[];
secondary: NavigationItem[];
} = {
primary: [
{
id: "attractions",
label: "Достопримечательности",
icon: Building2,
path: "/sights",
},
{
id: "devices",
label: "Устройства",
icon: MonitorSmartphone,
path: "/devices",
},
],
secondary: [
{
id: "logout",
label: "Выйти",
icon: Power,
path: "/logout",
},
],
};

View File

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

View File

@ -0,0 +1 @@
export const API_URL = "https://wn.krbl.ru";

6
src/shared/index.tsx Normal file
View File

@ -0,0 +1,6 @@
export * from "./config";
export * from "./lib";
export * from "./ui";
export * from "./store";
export * from "./const";
export * from "./api";

View File

@ -0,0 +1,27 @@
export const decodeJWT = (token?: string): any | null => {
if (!token) {
console.error("No token provided");
return null;
}
try {
const base64Url = token.split(".")[1];
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
const jsonPayload = decodeURIComponent(
atob(base64)
.split("")
.map((c) => `%${("00" + c.charCodeAt(0).toString(16)).slice(-2)}`)
.join("")
);
const decodedPayload: any = JSON.parse(jsonPayload);
if (decodedPayload.exp && Date.now() >= decodedPayload.exp * 1000) {
return null;
}
return decodedPayload;
} catch {
return null;
}
};

2
src/shared/lib/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from "./mui/theme";
export * from "./DecodeJWT";

View File

@ -0,0 +1,17 @@
import { createTheme } from "@mui/material/styles";
export const theme = createTheme({
// You can customize your theme here
palette: {
mode: "light",
},
components: {
MuiDrawer: {
styleOverrides: {
paper: {
backgroundColor: "#fff",
},
},
},
},
});

View File

@ -0,0 +1,96 @@
import { API_URL, decodeJWT } from "@shared";
import { makeAutoObservable } from "mobx";
import axios, { AxiosError } from "axios";
type LoginResponse = {
token: string;
user: {
id: number;
name: string;
email: string;
is_admin: boolean;
};
};
class AuthStore {
payload: LoginResponse | null = null;
isLoading = false;
error: string | null = null;
constructor() {
makeAutoObservable(this);
this.initialize();
}
private initialize() {
const storedToken = localStorage.getItem("token") || undefined;
const decoded = decodeJWT(storedToken);
if (decoded) {
this.payload = decoded;
// Set the token in axios defaults for future requests
if (storedToken) {
axios.defaults.headers.common[
"Authorization"
] = `Bearer ${storedToken}`;
}
} else {
// If token is invalid or missing, clear it
this.logout();
}
}
private setAuthToken(token: string) {
localStorage.setItem("token", token);
axios.defaults.headers.common["Authorization"] = `Bearer ${token}`;
}
login = async (email: string, password: string) => {
this.isLoading = true;
this.error = null;
try {
const response = await axios.post<LoginResponse>(
`${API_URL}/auth/login`,
{
email,
password,
}
);
const { token } = response.data;
// Update auth token and store state
this.setAuthToken(token);
this.payload = response.data;
this.error = null;
} catch (error) {
if (error instanceof AxiosError) {
this.error =
error.response?.data?.message || "Ошибка при входе в систему";
} else {
this.error = "Неизвестная ошибка при входе в систему";
}
throw new Error(this.error ?? "Неизвестная ошибка при входе в систему");
} finally {
this.isLoading = false;
}
};
logout = () => {
localStorage.removeItem("token");
this.payload = null;
this.error = null;
};
get isAuthenticated() {
return !!this.payload?.token;
}
get user() {
return this.payload?.user;
}
}
export const authStore = new AuthStore();

View File

@ -0,0 +1,28 @@
import { API_URL, authInstance } from "@shared";
import { makeAutoObservable } from "mobx";
class DevicesStore {
devices: any[] = [];
uuid: string | null = null;
sendSnapshotModalOpen = false;
constructor() {
makeAutoObservable(this);
}
getDevices = async () => {
const response = await authInstance.get(`${API_URL}/devices/connected`);
console.log(response.data);
this.devices = response.data;
};
setSelectedDevice = (uuid: string) => {
this.uuid = uuid;
};
toggleSendSnapshotModal = () => {
this.sendSnapshotModalOpen = !this.sendSnapshotModalOpen;
};
}
export const devicesStore = new DevicesStore();

View File

@ -0,0 +1,15 @@
import { makeAutoObservable } from "mobx";
class LanguageStore {
language: string = "ru";
constructor() {
makeAutoObservable(this);
}
setLanguage = (language: string) => {
this.language = language;
};
}
export const languageStore = new LanguageStore();

View File

@ -0,0 +1,18 @@
import { authInstance } from "@shared";
import { API_URL } from "@shared";
import { makeAutoObservable } from "mobx";
class SnapshotStore {
snapshots: any[] = [];
constructor() {
makeAutoObservable(this);
}
getSnapshots = async () => {
const response = await authInstance.get(`${API_URL}/snapshots`);
this.snapshots = response.data;
};
}
export const snapshotStore = new SnapshotStore();

View File

@ -0,0 +1,17 @@
import { API_URL, authInstance } from "@shared";
import { makeAutoObservable } from "mobx";
class VehicleStore {
vehicles: any[] = [];
constructor() {
makeAutoObservable(this);
}
getVehicles = async () => {
const response = await authInstance.get(`${API_URL}/vehicle`);
this.vehicles = response.data;
};
}
export const vehicleStore = new VehicleStore();

View File

@ -0,0 +1,5 @@
export * from "./AuthStore";
export * from "./LanguageStore";
export * from "./DevicesStore";
export * from "./VehicleStore";
export * from "./SnapshotStore";

View File

@ -0,0 +1,13 @@
import { MoveLeft } from "lucide-react";
import { useNavigate } from "react-router-dom";
export const BackButton = () => {
const navigate = useNavigate();
return (
<button className="flex items-center gap-2" onClick={() => navigate(-1)}>
<MoveLeft />
Назад
</button>
);
};

View File

@ -0,0 +1,3 @@
export const Input = () => {
return <input type="text" />;
};

View File

@ -0,0 +1,45 @@
import { Modal as MuiModal, Typography, Box } from "@mui/material";
interface ModalProps {
open: boolean;
onClose: () => void;
children: React.ReactNode;
title?: string;
}
const style = {
position: "absolute" as const,
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 400,
bgcolor: "background.paper",
boxShadow: 24,
p: 4,
borderRadius: 2,
};
export const Modal = ({ open, onClose, children, title }: ModalProps) => {
return (
<MuiModal
open={open}
onClose={onClose}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Box sx={style}>
{title && (
<Typography
id="modal-modal-title"
variant="h6"
component="h2"
gutterBottom
>
{title}
</Typography>
)}
<Box id="modal-modal-description">{children}</Box>
</Box>
</MuiModal>
);
};

View File

@ -0,0 +1,23 @@
import { Box } from "@mui/material";
interface TabPanelProps {
children?: React.ReactNode;
index: number;
value: number;
}
export const TabPanel = (props: TabPanelProps) => {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`sight-tabpanel-${index}`}
aria-labelledby={`sight-tab-${index}`}
{...other}
>
{value === index && <Box sx={{ p: 3 }}>{children}</Box>}
</div>
);
};

3
src/shared/ui/index.ts Normal file
View File

@ -0,0 +1,3 @@
export * from "./TabPanel";
export * from "./BackButton";
export * from "./Modal";