diff --git a/src/App.tsx b/src/App.tsx
index 522fbb5..6d0ec91 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -8,17 +8,8 @@ import Home from './pages/Home';
import Mission from './pages/Mission';
import ArticleEditor from './pages/ArticleEditor';
import Article from './pages/Article';
-import { useEffect } from 'react';
-import { loadTokensFromLocalStorage } from './redux/slices/auth';
-import { useAppDispatch } from './redux/hooks';
function App() {
- const dispatch = useAppDispatch();
-
- useEffect(() => {
- dispatch(loadTokensFromLocalStorage());
- }, []);
-
return (
@@ -38,6 +29,3 @@ function App() {
}
export default App;
-function useAppdispatch() {
- throw new Error('Function not implemented.');
-}
diff --git a/src/components/button/ReverseButton.tsx b/src/components/button/ReverseButton.tsx
index ee38a6e..67ddceb 100644
--- a/src/components/button/ReverseButton.tsx
+++ b/src/components/button/ReverseButton.tsx
@@ -7,14 +7,32 @@ interface ButtonProps {
className?: string;
onClick: (e: React.MouseEvent) => void;
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 = ({
disabled = false,
text = '',
className,
onClick,
children,
+ color = 'secondary',
}) => {
return (
diff --git a/src/components/router/ProtectedRoute.tsx b/src/components/router/ProtectedRoute.tsx
new file mode 100644
index 0000000..775a461
--- /dev/null
+++ b/src/components/router/ProtectedRoute.tsx
@@ -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
;
+ }
+
+ return
;
+}
diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx
index 49e9651..9cebabb 100644
--- a/src/pages/Home.tsx
+++ b/src/pages/Home.tsx
@@ -13,6 +13,8 @@ import Contests from '../views/home/contests/Contests';
import { PrimaryButton } from '../components/button/PrimaryButton';
import Group from '../views/home/groups/Group';
import Contest from '../views/home/contest/Contest';
+import Account from '../views/home/account/Account';
+import ProtectedRoute from '../components/router/ProtectedRoute';
const Home = () => {
const name = useAppSelector((state) => state.auth.username);
@@ -28,10 +30,13 @@ const Home = () => {
-
+
+ }>
+ } />
+
+
} />
- } />
} />
} />
} />
diff --git a/src/redux/slices/account.ts b/src/redux/slices/account.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/redux/slices/auth.ts b/src/redux/slices/auth.ts
index 2fba03e..50f6523 100644
--- a/src/redux/slices/auth.ts
+++ b/src/redux/slices/auth.ts
@@ -1,36 +1,63 @@
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import axios from '../../axios';
-// πΉ Π€ΡΠ½ΠΊΡΠΈΡ Π΄Π»Ρ Π΄Π΅ΠΊΠΎΠ΄ΠΈΡΠΎΠ²Π°Π½ΠΈΡ JWT
+// πΉ ΠΠ΅ΠΊΠΎΠ΄ΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅ JWT
function decodeJwt(token: string) {
const [, payload] = token.split('.');
const json = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
return JSON.parse(decodeURIComponent(escape(json)));
}
-// πΉ Π’ΠΈΠΏΡ Π΄Π°Π½Π½ΡΡ
+// πΉ Π’ΠΈΠΏΡ
interface AuthState {
jwt: string | null;
refreshToken: string | null;
username: string | null;
- email: string | null; // <-- Π΄ΠΎΠ±Π°Π²ΠΈΠ»ΠΈ email
+ email: string | null;
id: string | null;
status: 'idle' | 'loading' | 'successful' | 'failed';
error: string | null;
}
-// πΉ ΠΠ½ΠΈΡΠΈΠ°Π»ΠΈΠ·Π°ΡΠΈΡ ΡΠΎΡΡΠΎΡΠ½ΠΈΡ
+// πΉ ΠΠ½ΠΈΡΠΈΠ°Π»ΠΈΠ·Π°ΡΠΈΡ ΡΠΎΡΡΠΎΡΠ½ΠΈΡ Ρ ΡΠΈΠ½Ρ
ΡΠΎΠ½Π½ΠΎΠΉ Π·Π°Π³ΡΡΠ·ΠΊΠΎΠΉ ΠΈΠ· localStorage
+const jwtFromStorage = localStorage.getItem('jwt');
+const refreshTokenFromStorage = localStorage.getItem('refreshToken');
+
const initialState: AuthState = {
- jwt: null,
- refreshToken: null,
+ jwt: jwtFromStorage || null,
+ refreshToken: refreshTokenFromStorage || null,
username: null,
- email: null, // <-- Π΄ΠΎΠ±Π°Π²ΠΈΠ»ΠΈ email
+ email: null,
id: null,
status: 'idle',
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(
'auth/register',
async (
@@ -47,7 +74,7 @@ export const registerUser = createAsyncThunk(
email,
password,
});
- return response.data; // { jwt, refreshToken }
+ return response.data;
} catch (err: any) {
return rejectWithValue(
err.response?.data?.message || 'Registration failed',
@@ -56,7 +83,6 @@ export const registerUser = createAsyncThunk(
},
);
-// πΉ AsyncThunk: ΠΠΎΠ³ΠΈΠ½
export const loginUser = createAsyncThunk(
'auth/login',
async (
@@ -68,7 +94,7 @@ export const loginUser = createAsyncThunk(
username,
password,
});
- return response.data; // { jwt, refreshToken }
+ return response.data;
} catch (err: any) {
return rejectWithValue(
err.response?.data?.message || 'Login failed',
@@ -77,7 +103,6 @@ export const loginUser = createAsyncThunk(
},
);
-// πΉ AsyncThunk: ΠΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅ ΡΠΎΠΊΠ΅Π½Π°
export const refreshToken = createAsyncThunk(
'auth/refresh',
async ({ refreshToken }: { refreshToken: string }, { rejectWithValue }) => {
@@ -85,7 +110,7 @@ export const refreshToken = createAsyncThunk(
const response = await axios.post('/authentication/refresh', {
refreshToken,
});
- return response.data; // { jwt, refreshToken }
+ return response.data;
} catch (err: any) {
return rejectWithValue(
err.response?.data?.message || 'Refresh token failed',
@@ -94,13 +119,12 @@ export const refreshToken = createAsyncThunk(
},
);
-// πΉ AsyncThunk: ΠΠΎΠ»ΡΡΠ΅Π½ΠΈΠ΅ ΠΈΠ½ΡΠΎΡΠΌΠ°ΡΠΈΠΈ ΠΎ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Π΅
export const fetchWhoAmI = createAsyncThunk(
'auth/whoami',
async (_, { rejectWithValue }) => {
try {
const response = await axios.get('/authentication/whoami');
- return response.data; // { username }
+ return response.data;
} catch (err: any) {
return rejectWithValue(
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
const authSlice = createSlice({
name: 'auth',
@@ -134,7 +142,7 @@ const authSlice = createSlice({
state.jwt = null;
state.refreshToken = null;
state.username = null;
- state.email = null; // <-- ΠΎΡΠΈΡΡΠΊΠ° email
+ state.email = null;
state.id = null;
state.status = 'idle';
state.error = null;
@@ -144,7 +152,7 @@ const authSlice = createSlice({
},
},
extraReducers: (builder) => {
- // Π Π΅Π³ΠΈΡΡΡΠ°ΡΠΈΡ
+ // ----------------- Register -----------------
builder.addCase(registerUser.pending, (state) => {
state.status = 'loading';
state.error = null;
@@ -154,7 +162,6 @@ const authSlice = createSlice({
state.jwt = action.payload.jwt;
state.refreshToken = action.payload.refreshToken;
- // πΈ ΠΠ΅ΠΊΠΎΠ΄ΠΈΡΡΠ΅ΠΌ JWT
const decoded = decodeJwt(action.payload.jwt);
state.username =
decoded[
@@ -180,7 +187,7 @@ const authSlice = createSlice({
state.error = action.payload as string;
});
- // ΠΠΎΠ³ΠΈΠ½
+ // ----------------- Login -----------------
builder.addCase(loginUser.pending, (state) => {
state.status = 'loading';
state.error = null;
@@ -190,7 +197,6 @@ const authSlice = createSlice({
state.jwt = action.payload.jwt;
state.refreshToken = action.payload.refreshToken;
- // πΈ ΠΠ΅ΠΊΠΎΠ΄ΠΈΡΡΠ΅ΠΌ JWT
const decoded = decodeJwt(action.payload.jwt);
state.username =
decoded[
@@ -216,7 +222,7 @@ const authSlice = createSlice({
state.error = action.payload as string;
});
- // ΠΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅ ΡΠΎΠΊΠ΅Π½Π°
+ // ----------------- Refresh -----------------
builder.addCase(refreshToken.pending, (state) => {
state.status = 'loading';
state.error = null;
@@ -226,7 +232,6 @@ const authSlice = createSlice({
state.jwt = action.payload.jwt;
state.refreshToken = action.payload.refreshToken;
- // πΈ ΠΠ΅ΠΊΠΎΠ΄ΠΈΡΡΠ΅ΠΌ JWT
const decoded = decodeJwt(action.payload.jwt);
state.username =
decoded[
@@ -252,7 +257,7 @@ const authSlice = createSlice({
state.error = action.payload as string;
});
- // ΠΠΎΠ»ΡΡΠ΅Π½ΠΈΠ΅ ΠΈΠ½ΡΠΎΡΠΌΠ°ΡΠΈΠΈ ΠΎ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Π΅
+ // ----------------- WhoAmI -----------------
builder.addCase(fetchWhoAmI.pending, (state) => {
state.status = 'loading';
state.error = null;
@@ -265,35 +270,6 @@ const authSlice = createSlice({
state.status = 'failed';
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}`;
- }
- },
- );
},
});
diff --git a/src/views/home/account/Account.tsx b/src/views/home/account/Account.tsx
new file mode 100644
index 0000000..b508835
--- /dev/null
+++ b/src/views/home/account/Account.tsx
@@ -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 (
+
+
+
+
+
+
+ }
+ />
+ }
+ />
+ }
+ />
+ } />
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Account;
diff --git a/src/views/home/account/AccoutMenu.tsx b/src/views/home/account/AccoutMenu.tsx
new file mode 100644
index 0000000..204bf1b
--- /dev/null
+++ b/src/views/home/account/AccoutMenu.tsx
@@ -0,0 +1,5 @@
+const AccountMenu = () => {
+ return ;
+};
+
+export default AccountMenu;
diff --git a/src/views/home/account/ArticlesBlock.tsx b/src/views/home/account/ArticlesBlock.tsx
new file mode 100644
index 0000000..a0ec79c
--- /dev/null
+++ b/src/views/home/account/ArticlesBlock.tsx
@@ -0,0 +1,5 @@
+const ArticlesBlock = () => {
+ return ;
+};
+
+export default ArticlesBlock;
diff --git a/src/views/home/account/ContestsBlock.tsx b/src/views/home/account/ContestsBlock.tsx
new file mode 100644
index 0000000..ef6d1f9
--- /dev/null
+++ b/src/views/home/account/ContestsBlock.tsx
@@ -0,0 +1,5 @@
+const ContestsBlock = () => {
+ return ;
+};
+
+export default ContestsBlock;
diff --git a/src/views/home/account/MissionsBlock.tsx b/src/views/home/account/MissionsBlock.tsx
new file mode 100644
index 0000000..b439d81
--- /dev/null
+++ b/src/views/home/account/MissionsBlock.tsx
@@ -0,0 +1,5 @@
+const MissionsBlock = () => {
+ return ;
+};
+
+export default MissionsBlock;
diff --git a/src/views/home/account/RightPanel.tsx b/src/views/home/account/RightPanel.tsx
new file mode 100644
index 0000000..effd006
--- /dev/null
+++ b/src/views/home/account/RightPanel.tsx
@@ -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 (
+
+
{name}
+
{email}
+
{
+ dispatch(logout());
+ }}
+ text="ΠΡΡ
ΠΎΠ΄"
+ color="error"
+ />
+
+ );
+};
+
+export default RightPanel;
diff --git a/src/views/home/auth/Login.tsx b/src/views/home/auth/Login.tsx
index d44668f..548be70 100644
--- a/src/views/home/auth/Login.tsx
+++ b/src/views/home/auth/Login.tsx
@@ -30,7 +30,7 @@ const Login = () => {
useEffect(() => {
if (jwt) {
- navigate('/home/offices'); // ΠΈΠ»ΠΈ Π΄ΡΡΠ³Π°Ρ ΡΡΡΠ°Π½ΠΈΡΠ° ΠΏΠΎΡΠ»Π΅ Π²Ρ
ΠΎΠ΄Π°
+ navigate('/home/account'); // ΠΈΠ»ΠΈ Π΄ΡΡΠ³Π°Ρ ΡΡΡΠ°Π½ΠΈΡΠ° ΠΏΠΎΡΠ»Π΅ Π²Ρ
ΠΎΠ΄Π°
}
}, [jwt]);
diff --git a/src/views/home/auth/Register.tsx b/src/views/home/auth/Register.tsx
index afdbc30..d341b39 100644
--- a/src/views/home/auth/Register.tsx
+++ b/src/views/home/auth/Register.tsx
@@ -32,7 +32,7 @@ const Register = () => {
useEffect(() => {
if (jwt) {
- navigate('/home');
+ navigate('/home/account');
}
console.log(submitClicked);
}, [jwt]);