init: Init React Application
This commit is contained in:
12
src/shared/api/index.tsx
Normal file
12
src/shared/api/index.tsx
Normal 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 };
|
37
src/shared/config/constants.tsx
Normal file
37
src/shared/config/constants.tsx
Normal 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",
|
||||
},
|
||||
],
|
||||
};
|
1
src/shared/config/index.ts
Normal file
1
src/shared/config/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./constants";
|
1
src/shared/const/index.ts
Normal file
1
src/shared/const/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export const API_URL = "https://wn.krbl.ru";
|
6
src/shared/index.tsx
Normal file
6
src/shared/index.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
export * from "./config";
|
||||
export * from "./lib";
|
||||
export * from "./ui";
|
||||
export * from "./store";
|
||||
export * from "./const";
|
||||
export * from "./api";
|
27
src/shared/lib/DecodeJWT/index.ts
Normal file
27
src/shared/lib/DecodeJWT/index.ts
Normal 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
2
src/shared/lib/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./mui/theme";
|
||||
export * from "./DecodeJWT";
|
17
src/shared/lib/mui/theme.ts
Normal file
17
src/shared/lib/mui/theme.ts
Normal 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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
96
src/shared/store/AuthStore/index.tsx
Normal file
96
src/shared/store/AuthStore/index.tsx
Normal 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();
|
28
src/shared/store/DevicesStore/index.tsx
Normal file
28
src/shared/store/DevicesStore/index.tsx
Normal 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();
|
15
src/shared/store/LanguageStore/index.tsx
Normal file
15
src/shared/store/LanguageStore/index.tsx
Normal 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();
|
18
src/shared/store/SnapshotStore/index.ts
Normal file
18
src/shared/store/SnapshotStore/index.ts
Normal 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();
|
17
src/shared/store/VehicleStore/index.ts
Normal file
17
src/shared/store/VehicleStore/index.ts
Normal 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();
|
5
src/shared/store/index.ts
Normal file
5
src/shared/store/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from "./AuthStore";
|
||||
export * from "./LanguageStore";
|
||||
export * from "./DevicesStore";
|
||||
export * from "./VehicleStore";
|
||||
export * from "./SnapshotStore";
|
13
src/shared/ui/BackButton/index.tsx
Normal file
13
src/shared/ui/BackButton/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
3
src/shared/ui/Input/index.tsx
Normal file
3
src/shared/ui/Input/index.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export const Input = () => {
|
||||
return <input type="text" />;
|
||||
};
|
45
src/shared/ui/Modal/index.tsx
Normal file
45
src/shared/ui/Modal/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
23
src/shared/ui/TabPanel/index.tsx
Normal file
23
src/shared/ui/TabPanel/index.tsx
Normal 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
3
src/shared/ui/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./TabPanel";
|
||||
export * from "./BackButton";
|
||||
export * from "./Modal";
|
Reference in New Issue
Block a user