account and protected router
This commit is contained in:
12
src/App.tsx
12
src/App.tsx
@@ -8,17 +8,8 @@ import Home from './pages/Home';
|
|||||||
import Mission from './pages/Mission';
|
import Mission from './pages/Mission';
|
||||||
import ArticleEditor from './pages/ArticleEditor';
|
import ArticleEditor from './pages/ArticleEditor';
|
||||||
import Article from './pages/Article';
|
import Article from './pages/Article';
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { loadTokensFromLocalStorage } from './redux/slices/auth';
|
|
||||||
import { useAppDispatch } from './redux/hooks';
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
dispatch(loadTokensFromLocalStorage());
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full bg-liquid-background flex justify-center">
|
<div className="w-full h-full bg-liquid-background flex justify-center">
|
||||||
<div className="relative w-full max-w-[1600px] h-full ">
|
<div className="relative w-full max-w-[1600px] h-full ">
|
||||||
@@ -38,6 +29,3 @@ function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
function useAppdispatch() {
|
|
||||||
throw new Error('Function not implemented.');
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,14 +7,32 @@ interface ButtonProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
onClick: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
onClick: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
color?: 'primary' | 'secondary' | 'error' | 'warning' | 'success';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ColorBgVariants = {
|
||||||
|
primary: 'group-hover:bg-liquid-brightmain ring-liquid-brightmain',
|
||||||
|
secondary: 'group-hover:bg-liquid-darkmain ring-liquid-darkmain',
|
||||||
|
error: 'group-hover:bg-liquid-red ring-liquid-red',
|
||||||
|
warning: 'group-hover:bg-liquid-orange ring-liquid-orange',
|
||||||
|
success: 'group-hover:bg-liquid-green ring-liquid-green',
|
||||||
|
};
|
||||||
|
|
||||||
|
const ColorTextVariants = {
|
||||||
|
primary: 'text-liquid-brightmain ',
|
||||||
|
secondary: 'text-liquid-brightmain ',
|
||||||
|
error: 'text-liquid-red ',
|
||||||
|
warning: 'text-liquid-orange ',
|
||||||
|
success: 'text-liquid-green ',
|
||||||
|
};
|
||||||
|
|
||||||
export const ReverseButton: React.FC<ButtonProps> = ({
|
export const ReverseButton: React.FC<ButtonProps> = ({
|
||||||
disabled = false,
|
disabled = false,
|
||||||
text = '',
|
text = '',
|
||||||
className,
|
className,
|
||||||
onClick,
|
onClick,
|
||||||
children,
|
children,
|
||||||
|
color = 'secondary',
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
@@ -32,6 +50,7 @@ export const ReverseButton: React.FC<ButtonProps> = ({
|
|||||||
'group-hover:bg-liquid-darkmain ',
|
'group-hover:bg-liquid-darkmain ',
|
||||||
'px-[16px] py-[8px]',
|
'px-[16px] py-[8px]',
|
||||||
'bg-liquid-lighter ring-[1px] ring-liquid-darkmain ring-inset',
|
'bg-liquid-lighter ring-[1px] ring-liquid-darkmain ring-inset',
|
||||||
|
ColorBgVariants[color],
|
||||||
disabled && 'bg-liquid-lighter',
|
disabled && 'bg-liquid-lighter',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -61,8 +80,9 @@ export const ReverseButton: React.FC<ButtonProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'transition-all duration-300 text-liquid-brightmain text-[18px] font-bold p-0 m-0 leading-[23px]',
|
'transition-all duration-300 text-[18px] font-bold p-0 m-0 leading-[23px]',
|
||||||
'group-hover:text-liquid-white ',
|
'group-hover:text-liquid-white ',
|
||||||
|
ColorTextVariants[color],
|
||||||
disabled && 'text-liquid-light',
|
disabled && 'text-liquid-light',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
12
src/components/router/ProtectedRoute.tsx
Normal file
12
src/components/router/ProtectedRoute.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// src/routes/ProtectedRoute.tsx
|
||||||
|
import { Navigate, Outlet } from 'react-router-dom';
|
||||||
|
import { useAppSelector } from '../../redux/hooks';
|
||||||
|
|
||||||
|
export default function ProtectedRoute() {
|
||||||
|
const isAuthenticated = useAppSelector((state) => !!state.auth.jwt);
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return <Navigate to="/home/login" replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Outlet />;
|
||||||
|
}
|
||||||
@@ -13,6 +13,8 @@ import Contests from '../views/home/contests/Contests';
|
|||||||
import { PrimaryButton } from '../components/button/PrimaryButton';
|
import { PrimaryButton } from '../components/button/PrimaryButton';
|
||||||
import Group from '../views/home/groups/Group';
|
import Group from '../views/home/groups/Group';
|
||||||
import Contest from '../views/home/contest/Contest';
|
import Contest from '../views/home/contest/Contest';
|
||||||
|
import Account from '../views/home/account/Account';
|
||||||
|
import ProtectedRoute from '../components/router/ProtectedRoute';
|
||||||
|
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
const name = useAppSelector((state) => state.auth.username);
|
const name = useAppSelector((state) => state.auth.username);
|
||||||
@@ -28,10 +30,13 @@ const Home = () => {
|
|||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<Menu />
|
<Menu />
|
||||||
</div>
|
</div>
|
||||||
<div className="">
|
<div className="h-screen">
|
||||||
<Routes>
|
<Routes>
|
||||||
|
<Route element={<ProtectedRoute />}>
|
||||||
|
<Route path="account/*" element={<Account />} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
<Route path="login" element={<Login />} />
|
<Route path="login" element={<Login />} />
|
||||||
<Route path="account" element={<Login />} />
|
|
||||||
<Route path="register" element={<Register />} />
|
<Route path="register" element={<Register />} />
|
||||||
<Route path="missions/*" element={<Missions />} />
|
<Route path="missions/*" element={<Missions />} />
|
||||||
<Route path="articles/*" element={<Articles />} />
|
<Route path="articles/*" element={<Articles />} />
|
||||||
|
|||||||
0
src/redux/slices/account.ts
Normal file
0
src/redux/slices/account.ts
Normal file
@@ -1,36 +1,63 @@
|
|||||||
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
||||||
import axios from '../../axios';
|
import axios from '../../axios';
|
||||||
|
|
||||||
// 🔹 Функция для декодирования JWT
|
// 🔹 Декодирование JWT
|
||||||
function decodeJwt(token: string) {
|
function decodeJwt(token: string) {
|
||||||
const [, payload] = token.split('.');
|
const [, payload] = token.split('.');
|
||||||
const json = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
|
const json = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
|
||||||
return JSON.parse(decodeURIComponent(escape(json)));
|
return JSON.parse(decodeURIComponent(escape(json)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔹 Типы данных
|
// 🔹 Типы
|
||||||
interface AuthState {
|
interface AuthState {
|
||||||
jwt: string | null;
|
jwt: string | null;
|
||||||
refreshToken: string | null;
|
refreshToken: string | null;
|
||||||
username: string | null;
|
username: string | null;
|
||||||
email: string | null; // <-- добавили email
|
email: string | null;
|
||||||
id: string | null;
|
id: string | null;
|
||||||
status: 'idle' | 'loading' | 'successful' | 'failed';
|
status: 'idle' | 'loading' | 'successful' | 'failed';
|
||||||
error: string | null;
|
error: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔹 Инициализация состояния
|
// 🔹 Инициализация состояния с синхронной загрузкой из localStorage
|
||||||
|
const jwtFromStorage = localStorage.getItem('jwt');
|
||||||
|
const refreshTokenFromStorage = localStorage.getItem('refreshToken');
|
||||||
|
|
||||||
const initialState: AuthState = {
|
const initialState: AuthState = {
|
||||||
jwt: null,
|
jwt: jwtFromStorage || null,
|
||||||
refreshToken: null,
|
refreshToken: refreshTokenFromStorage || null,
|
||||||
username: null,
|
username: null,
|
||||||
email: null, // <-- добавили email
|
email: null,
|
||||||
id: null,
|
id: null,
|
||||||
status: 'idle',
|
status: 'idle',
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 🔹 AsyncThunk: Регистрация
|
// Если токен есть, подставляем в axios и декодируем
|
||||||
|
if (jwtFromStorage) {
|
||||||
|
axios.defaults.headers.common['Authorization'] = `Bearer ${jwtFromStorage}`;
|
||||||
|
try {
|
||||||
|
const decoded = decodeJwt(jwtFromStorage);
|
||||||
|
initialState.username =
|
||||||
|
decoded[
|
||||||
|
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'
|
||||||
|
] || null;
|
||||||
|
initialState.email =
|
||||||
|
decoded[
|
||||||
|
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'
|
||||||
|
] || null;
|
||||||
|
initialState.id =
|
||||||
|
decoded[
|
||||||
|
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier'
|
||||||
|
] || null;
|
||||||
|
} catch {
|
||||||
|
localStorage.removeItem('jwt');
|
||||||
|
localStorage.removeItem('refreshToken');
|
||||||
|
delete axios.defaults.headers.common['Authorization'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔹 AsyncThunk-ы (login/register/refresh/whoami) остаются как были
|
||||||
export const registerUser = createAsyncThunk(
|
export const registerUser = createAsyncThunk(
|
||||||
'auth/register',
|
'auth/register',
|
||||||
async (
|
async (
|
||||||
@@ -47,7 +74,7 @@ export const registerUser = createAsyncThunk(
|
|||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
return response.data; // { jwt, refreshToken }
|
return response.data;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return rejectWithValue(
|
return rejectWithValue(
|
||||||
err.response?.data?.message || 'Registration failed',
|
err.response?.data?.message || 'Registration failed',
|
||||||
@@ -56,7 +83,6 @@ export const registerUser = createAsyncThunk(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// 🔹 AsyncThunk: Логин
|
|
||||||
export const loginUser = createAsyncThunk(
|
export const loginUser = createAsyncThunk(
|
||||||
'auth/login',
|
'auth/login',
|
||||||
async (
|
async (
|
||||||
@@ -68,7 +94,7 @@ export const loginUser = createAsyncThunk(
|
|||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
return response.data; // { jwt, refreshToken }
|
return response.data;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return rejectWithValue(
|
return rejectWithValue(
|
||||||
err.response?.data?.message || 'Login failed',
|
err.response?.data?.message || 'Login failed',
|
||||||
@@ -77,7 +103,6 @@ export const loginUser = createAsyncThunk(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// 🔹 AsyncThunk: Обновление токена
|
|
||||||
export const refreshToken = createAsyncThunk(
|
export const refreshToken = createAsyncThunk(
|
||||||
'auth/refresh',
|
'auth/refresh',
|
||||||
async ({ refreshToken }: { refreshToken: string }, { rejectWithValue }) => {
|
async ({ refreshToken }: { refreshToken: string }, { rejectWithValue }) => {
|
||||||
@@ -85,7 +110,7 @@ export const refreshToken = createAsyncThunk(
|
|||||||
const response = await axios.post('/authentication/refresh', {
|
const response = await axios.post('/authentication/refresh', {
|
||||||
refreshToken,
|
refreshToken,
|
||||||
});
|
});
|
||||||
return response.data; // { jwt, refreshToken }
|
return response.data;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return rejectWithValue(
|
return rejectWithValue(
|
||||||
err.response?.data?.message || 'Refresh token failed',
|
err.response?.data?.message || 'Refresh token failed',
|
||||||
@@ -94,13 +119,12 @@ export const refreshToken = createAsyncThunk(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// 🔹 AsyncThunk: Получение информации о пользователе
|
|
||||||
export const fetchWhoAmI = createAsyncThunk(
|
export const fetchWhoAmI = createAsyncThunk(
|
||||||
'auth/whoami',
|
'auth/whoami',
|
||||||
async (_, { rejectWithValue }) => {
|
async (_, { rejectWithValue }) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('/authentication/whoami');
|
const response = await axios.get('/authentication/whoami');
|
||||||
return response.data; // { username }
|
return response.data;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return rejectWithValue(
|
return rejectWithValue(
|
||||||
err.response?.data?.message || 'Failed to fetch user info',
|
err.response?.data?.message || 'Failed to fetch user info',
|
||||||
@@ -109,22 +133,6 @@ export const fetchWhoAmI = createAsyncThunk(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// 🔹 AsyncThunk: Загрузка токенов из localStorage
|
|
||||||
export const loadTokensFromLocalStorage = createAsyncThunk(
|
|
||||||
'auth/loadTokens',
|
|
||||||
async () => {
|
|
||||||
const jwt = localStorage.getItem('jwt');
|
|
||||||
const refreshToken = localStorage.getItem('refreshToken');
|
|
||||||
|
|
||||||
if (jwt && refreshToken) {
|
|
||||||
axios.defaults.headers.common['Authorization'] = `Bearer ${jwt}`;
|
|
||||||
return { jwt, refreshToken };
|
|
||||||
} else {
|
|
||||||
return { jwt: null, refreshToken: null };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// 🔹 Slice
|
// 🔹 Slice
|
||||||
const authSlice = createSlice({
|
const authSlice = createSlice({
|
||||||
name: 'auth',
|
name: 'auth',
|
||||||
@@ -134,7 +142,7 @@ const authSlice = createSlice({
|
|||||||
state.jwt = null;
|
state.jwt = null;
|
||||||
state.refreshToken = null;
|
state.refreshToken = null;
|
||||||
state.username = null;
|
state.username = null;
|
||||||
state.email = null; // <-- очистка email
|
state.email = null;
|
||||||
state.id = null;
|
state.id = null;
|
||||||
state.status = 'idle';
|
state.status = 'idle';
|
||||||
state.error = null;
|
state.error = null;
|
||||||
@@ -144,7 +152,7 @@ const authSlice = createSlice({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
// Регистрация
|
// ----------------- Register -----------------
|
||||||
builder.addCase(registerUser.pending, (state) => {
|
builder.addCase(registerUser.pending, (state) => {
|
||||||
state.status = 'loading';
|
state.status = 'loading';
|
||||||
state.error = null;
|
state.error = null;
|
||||||
@@ -154,7 +162,6 @@ const authSlice = createSlice({
|
|||||||
state.jwt = action.payload.jwt;
|
state.jwt = action.payload.jwt;
|
||||||
state.refreshToken = action.payload.refreshToken;
|
state.refreshToken = action.payload.refreshToken;
|
||||||
|
|
||||||
// 🔸 Декодируем JWT
|
|
||||||
const decoded = decodeJwt(action.payload.jwt);
|
const decoded = decodeJwt(action.payload.jwt);
|
||||||
state.username =
|
state.username =
|
||||||
decoded[
|
decoded[
|
||||||
@@ -180,7 +187,7 @@ const authSlice = createSlice({
|
|||||||
state.error = action.payload as string;
|
state.error = action.payload as string;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Логин
|
// ----------------- Login -----------------
|
||||||
builder.addCase(loginUser.pending, (state) => {
|
builder.addCase(loginUser.pending, (state) => {
|
||||||
state.status = 'loading';
|
state.status = 'loading';
|
||||||
state.error = null;
|
state.error = null;
|
||||||
@@ -190,7 +197,6 @@ const authSlice = createSlice({
|
|||||||
state.jwt = action.payload.jwt;
|
state.jwt = action.payload.jwt;
|
||||||
state.refreshToken = action.payload.refreshToken;
|
state.refreshToken = action.payload.refreshToken;
|
||||||
|
|
||||||
// 🔸 Декодируем JWT
|
|
||||||
const decoded = decodeJwt(action.payload.jwt);
|
const decoded = decodeJwt(action.payload.jwt);
|
||||||
state.username =
|
state.username =
|
||||||
decoded[
|
decoded[
|
||||||
@@ -216,7 +222,7 @@ const authSlice = createSlice({
|
|||||||
state.error = action.payload as string;
|
state.error = action.payload as string;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Обновление токена
|
// ----------------- Refresh -----------------
|
||||||
builder.addCase(refreshToken.pending, (state) => {
|
builder.addCase(refreshToken.pending, (state) => {
|
||||||
state.status = 'loading';
|
state.status = 'loading';
|
||||||
state.error = null;
|
state.error = null;
|
||||||
@@ -226,7 +232,6 @@ const authSlice = createSlice({
|
|||||||
state.jwt = action.payload.jwt;
|
state.jwt = action.payload.jwt;
|
||||||
state.refreshToken = action.payload.refreshToken;
|
state.refreshToken = action.payload.refreshToken;
|
||||||
|
|
||||||
// 🔸 Декодируем JWT
|
|
||||||
const decoded = decodeJwt(action.payload.jwt);
|
const decoded = decodeJwt(action.payload.jwt);
|
||||||
state.username =
|
state.username =
|
||||||
decoded[
|
decoded[
|
||||||
@@ -252,7 +257,7 @@ const authSlice = createSlice({
|
|||||||
state.error = action.payload as string;
|
state.error = action.payload as string;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Получение информации о пользователе
|
// ----------------- WhoAmI -----------------
|
||||||
builder.addCase(fetchWhoAmI.pending, (state) => {
|
builder.addCase(fetchWhoAmI.pending, (state) => {
|
||||||
state.status = 'loading';
|
state.status = 'loading';
|
||||||
state.error = null;
|
state.error = null;
|
||||||
@@ -265,35 +270,6 @@ const authSlice = createSlice({
|
|||||||
state.status = 'failed';
|
state.status = 'failed';
|
||||||
state.error = action.payload as string;
|
state.error = action.payload as string;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Загрузка токенов из localStorage
|
|
||||||
builder.addCase(
|
|
||||||
loadTokensFromLocalStorage.fulfilled,
|
|
||||||
(state, action) => {
|
|
||||||
state.jwt = action.payload.jwt;
|
|
||||||
state.refreshToken = action.payload.refreshToken;
|
|
||||||
|
|
||||||
if (action.payload.jwt) {
|
|
||||||
const decoded = decodeJwt(action.payload.jwt);
|
|
||||||
state.username =
|
|
||||||
decoded[
|
|
||||||
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'
|
|
||||||
] || null;
|
|
||||||
state.email =
|
|
||||||
decoded[
|
|
||||||
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'
|
|
||||||
] || null;
|
|
||||||
state.id =
|
|
||||||
decoded[
|
|
||||||
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier'
|
|
||||||
] || null;
|
|
||||||
|
|
||||||
axios.defaults.headers.common[
|
|
||||||
'Authorization'
|
|
||||||
] = `Bearer ${action.payload.jwt}`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
42
src/views/home/account/Account.tsx
Normal file
42
src/views/home/account/Account.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { Route, Routes } from 'react-router-dom';
|
||||||
|
import AccountMenu from './AccoutMenu';
|
||||||
|
import RightPanel from './RightPanel';
|
||||||
|
import MissionsBlock from './MissionsBlock';
|
||||||
|
import ContestsBlock from './ContestsBlock';
|
||||||
|
import ArticlesBlock from './ArticlesBlock';
|
||||||
|
|
||||||
|
const Account = () => {
|
||||||
|
return (
|
||||||
|
<div className="h-full w-[calc(100%+250px)] box-border grid grid-cols-[1fr,520px] relative">
|
||||||
|
<div className=" h-full min-h-0 flex flex-col">
|
||||||
|
<div className=" h-full grid grid-rows-[80px,1fr] ">
|
||||||
|
<div className="">
|
||||||
|
<AccountMenu />
|
||||||
|
</div>
|
||||||
|
<div className="h-full min-h-0 overflow-y-scroll medium-scrollbar flex flex-col gap-[20px] ">
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path="/home/account/missions"
|
||||||
|
element={<MissionsBlock />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/home/account/articles"
|
||||||
|
element={<ArticlesBlock />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/home/account/contests"
|
||||||
|
element={<ContestsBlock />}
|
||||||
|
/>
|
||||||
|
<Route path="*" element={<MissionsBlock />} />
|
||||||
|
</Routes>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className=" h-full min-h-0">
|
||||||
|
<RightPanel />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Account;
|
||||||
5
src/views/home/account/AccoutMenu.tsx
Normal file
5
src/views/home/account/AccoutMenu.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
const AccountMenu = () => {
|
||||||
|
return <div className="h-full w-full relative "></div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AccountMenu;
|
||||||
5
src/views/home/account/ArticlesBlock.tsx
Normal file
5
src/views/home/account/ArticlesBlock.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
const ArticlesBlock = () => {
|
||||||
|
return <div className="h-full w-full relative "></div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ArticlesBlock;
|
||||||
5
src/views/home/account/ContestsBlock.tsx
Normal file
5
src/views/home/account/ContestsBlock.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
const ContestsBlock = () => {
|
||||||
|
return <div className="h-full w-full relative bg-fuchsia-600"></div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContestsBlock;
|
||||||
5
src/views/home/account/MissionsBlock.tsx
Normal file
5
src/views/home/account/MissionsBlock.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
const MissionsBlock = () => {
|
||||||
|
return <div className="h-full w-full relative "></div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MissionsBlock;
|
||||||
25
src/views/home/account/RightPanel.tsx
Normal file
25
src/views/home/account/RightPanel.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { ReverseButton } from '../../../components/button/ReverseButton';
|
||||||
|
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
||||||
|
import { logout } from '../../../redux/slices/auth';
|
||||||
|
|
||||||
|
const RightPanel = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const name = useAppSelector((state) => state.auth.username);
|
||||||
|
const email = useAppSelector((state) => state.auth.email);
|
||||||
|
return (
|
||||||
|
<div className="h-full w-full relative p-[20px]">
|
||||||
|
<div>{name}</div>
|
||||||
|
<div>{email}</div>
|
||||||
|
<ReverseButton
|
||||||
|
className="absolute bottom-[20px] right-[20px]"
|
||||||
|
onClick={() => {
|
||||||
|
dispatch(logout());
|
||||||
|
}}
|
||||||
|
text="Выход"
|
||||||
|
color="error"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RightPanel;
|
||||||
@@ -30,7 +30,7 @@ const Login = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (jwt) {
|
if (jwt) {
|
||||||
navigate('/home/offices'); // или другая страница после входа
|
navigate('/home/account'); // или другая страница после входа
|
||||||
}
|
}
|
||||||
}, [jwt]);
|
}, [jwt]);
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const Register = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (jwt) {
|
if (jwt) {
|
||||||
navigate('/home');
|
navigate('/home/account');
|
||||||
}
|
}
|
||||||
console.log(submitClicked);
|
console.log(submitClicked);
|
||||||
}, [jwt]);
|
}, [jwt]);
|
||||||
|
|||||||
Reference in New Issue
Block a user