contests
This commit is contained in:
12
src/App.tsx
12
src/App.tsx
@@ -8,8 +8,17 @@ 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 ">
|
||||||
@@ -29,3 +38,6 @@ function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
function useAppdispatch() {
|
||||||
|
throw new Error('Function not implemented.');
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const instance = axios.create({
|
|||||||
instance.interceptors.request.use(
|
instance.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
const token = localStorage.getItem('jwt'); // или можно брать из Redux через store.getState()
|
const token = localStorage.getItem('jwt'); // или можно брать из Redux через store.getState()
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
|
|||||||
7
src/hooks/useQuery.ts
Normal file
7
src/hooks/useQuery.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
export function useQuery() {
|
||||||
|
const { search } = useLocation();
|
||||||
|
return useMemo(() => new URLSearchParams(search), [search]);
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import { fetchMySubmitsByMission, submitMission } from '../redux/slices/submit';
|
|||||||
import { fetchMissionById } from '../redux/slices/missions';
|
import { fetchMissionById } from '../redux/slices/missions';
|
||||||
import Header from '../views/mission/statement/Header';
|
import Header from '../views/mission/statement/Header';
|
||||||
import MissionSubmissions from '../views/mission/statement/MissionSubmissions';
|
import MissionSubmissions from '../views/mission/statement/MissionSubmissions';
|
||||||
|
import { useQuery } from '../hooks/useQuery';
|
||||||
|
|
||||||
const Mission = () => {
|
const Mission = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@@ -16,6 +17,10 @@ const Mission = () => {
|
|||||||
const { missionId } = useParams<{ missionId: string }>();
|
const { missionId } = useParams<{ missionId: string }>();
|
||||||
const mission = useAppSelector((state) => state.missions.currentMission);
|
const mission = useAppSelector((state) => state.missions.currentMission);
|
||||||
const missionIdNumber = Number(missionId);
|
const missionIdNumber = Number(missionId);
|
||||||
|
|
||||||
|
const query = useQuery();
|
||||||
|
const back = query.get('back') ?? undefined;
|
||||||
|
|
||||||
if (!missionId || isNaN(missionIdNumber)) {
|
if (!missionId || isNaN(missionIdNumber)) {
|
||||||
return <Navigate to="/home" replace />;
|
return <Navigate to="/home" replace />;
|
||||||
}
|
}
|
||||||
@@ -38,7 +43,9 @@ const Mission = () => {
|
|||||||
const hasWaiting = submissionsRef.current.some(
|
const hasWaiting = submissionsRef.current.some(
|
||||||
(s: any) =>
|
(s: any) =>
|
||||||
s.solution.status == 'Waiting' ||
|
s.solution.status == 'Waiting' ||
|
||||||
s.solution.testerState === 'Waiting',
|
s.solution.testerState === 'Waiting' ||
|
||||||
|
s.solution.status === 'Compiling' ||
|
||||||
|
s.solution.testerState === 'Compiling',
|
||||||
);
|
);
|
||||||
if (!hasWaiting) {
|
if (!hasWaiting) {
|
||||||
// Всё проверено — стоп
|
// Всё проверено — стоп
|
||||||
@@ -73,7 +80,9 @@ const Mission = () => {
|
|||||||
const hasWaiting = submissions.some(
|
const hasWaiting = submissions.some(
|
||||||
(s) =>
|
(s) =>
|
||||||
s.solution.status === 'Waiting' ||
|
s.solution.status === 'Waiting' ||
|
||||||
s.solution.testerState === 'Waiting',
|
s.solution.testerState === 'Waiting' ||
|
||||||
|
s.solution.status === 'Compiling' ||
|
||||||
|
s.solution.testerState === 'Compiling',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (hasWaiting) {
|
if (hasWaiting) {
|
||||||
@@ -145,7 +154,7 @@ const Mission = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="h-screen grid grid-rows-[60px,1fr]">
|
<div className="h-screen grid grid-rows-[60px,1fr]">
|
||||||
<div className="">
|
<div className="">
|
||||||
<Header missionId={missionIdNumber} />
|
<Header missionId={missionIdNumber} back={back} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 h-full min-h-0 gap-[20px]">
|
<div className="grid grid-cols-2 h-full min-h-0 gap-[20px]">
|
||||||
|
|||||||
@@ -1,25 +1,36 @@
|
|||||||
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
||||||
import axios from '../../axios';
|
import axios from '../../axios';
|
||||||
|
|
||||||
// Типы данных
|
// 🔹 Функция для декодирования 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 {
|
interface AuthState {
|
||||||
jwt: string | null;
|
jwt: string | null;
|
||||||
refreshToken: string | null;
|
refreshToken: string | null;
|
||||||
username: string | null;
|
username: string | null;
|
||||||
|
email: string | null; // <-- добавили email
|
||||||
|
id: string | null;
|
||||||
status: 'idle' | 'loading' | 'successful' | 'failed';
|
status: 'idle' | 'loading' | 'successful' | 'failed';
|
||||||
error: string | null;
|
error: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Инициализация состояния
|
// 🔹 Инициализация состояния
|
||||||
const initialState: AuthState = {
|
const initialState: AuthState = {
|
||||||
jwt: null,
|
jwt: null,
|
||||||
refreshToken: null,
|
refreshToken: null,
|
||||||
username: null,
|
username: null,
|
||||||
|
email: null, // <-- добавили email
|
||||||
|
id: null,
|
||||||
status: 'idle',
|
status: 'idle',
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// AsyncThunk: Регистрация
|
// 🔹 AsyncThunk: Регистрация
|
||||||
export const registerUser = createAsyncThunk(
|
export const registerUser = createAsyncThunk(
|
||||||
'auth/register',
|
'auth/register',
|
||||||
async (
|
async (
|
||||||
@@ -45,7 +56,7 @@ export const registerUser = createAsyncThunk(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// AsyncThunk: Логин
|
// 🔹 AsyncThunk: Логин
|
||||||
export const loginUser = createAsyncThunk(
|
export const loginUser = createAsyncThunk(
|
||||||
'auth/login',
|
'auth/login',
|
||||||
async (
|
async (
|
||||||
@@ -66,7 +77,7 @@ export const loginUser = createAsyncThunk(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// AsyncThunk: Обновление токена
|
// 🔹 AsyncThunk: Обновление токена
|
||||||
export const refreshToken = createAsyncThunk(
|
export const refreshToken = createAsyncThunk(
|
||||||
'auth/refresh',
|
'auth/refresh',
|
||||||
async ({ refreshToken }: { refreshToken: string }, { rejectWithValue }) => {
|
async ({ refreshToken }: { refreshToken: string }, { rejectWithValue }) => {
|
||||||
@@ -74,7 +85,7 @@ export const refreshToken = createAsyncThunk(
|
|||||||
const response = await axios.post('/authentication/refresh', {
|
const response = await axios.post('/authentication/refresh', {
|
||||||
refreshToken,
|
refreshToken,
|
||||||
});
|
});
|
||||||
return response.data; // { username }
|
return response.data; // { jwt, refreshToken }
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return rejectWithValue(
|
return rejectWithValue(
|
||||||
err.response?.data?.message || 'Refresh token failed',
|
err.response?.data?.message || 'Refresh token failed',
|
||||||
@@ -83,7 +94,7 @@ export const refreshToken = createAsyncThunk(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// AsyncThunk: Получение информации о пользователе
|
// 🔹 AsyncThunk: Получение информации о пользователе
|
||||||
export const fetchWhoAmI = createAsyncThunk(
|
export const fetchWhoAmI = createAsyncThunk(
|
||||||
'auth/whoami',
|
'auth/whoami',
|
||||||
async (_, { rejectWithValue }) => {
|
async (_, { rejectWithValue }) => {
|
||||||
@@ -98,10 +109,10 @@ export const fetchWhoAmI = createAsyncThunk(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// AsyncThunk: Загрузка токенов из localStorage
|
// 🔹 AsyncThunk: Загрузка токенов из localStorage
|
||||||
export const loadTokensFromLocalStorage = createAsyncThunk(
|
export const loadTokensFromLocalStorage = createAsyncThunk(
|
||||||
'auth/loadTokens',
|
'auth/loadTokens',
|
||||||
async (_, {}) => {
|
async () => {
|
||||||
const jwt = localStorage.getItem('jwt');
|
const jwt = localStorage.getItem('jwt');
|
||||||
const refreshToken = localStorage.getItem('refreshToken');
|
const refreshToken = localStorage.getItem('refreshToken');
|
||||||
|
|
||||||
@@ -114,7 +125,7 @@ export const loadTokensFromLocalStorage = createAsyncThunk(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Slice
|
// 🔹 Slice
|
||||||
const authSlice = createSlice({
|
const authSlice = createSlice({
|
||||||
name: 'auth',
|
name: 'auth',
|
||||||
initialState,
|
initialState,
|
||||||
@@ -123,6 +134,8 @@ 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.id = null;
|
||||||
state.status = 'idle';
|
state.status = 'idle';
|
||||||
state.error = null;
|
state.error = null;
|
||||||
localStorage.removeItem('jwt');
|
localStorage.removeItem('jwt');
|
||||||
@@ -136,118 +149,145 @@ const authSlice = createSlice({
|
|||||||
state.status = 'loading';
|
state.status = 'loading';
|
||||||
state.error = null;
|
state.error = null;
|
||||||
});
|
});
|
||||||
builder.addCase(
|
builder.addCase(registerUser.fulfilled, (state, action) => {
|
||||||
registerUser.fulfilled,
|
state.status = 'successful';
|
||||||
(
|
state.jwt = action.payload.jwt;
|
||||||
state,
|
state.refreshToken = action.payload.refreshToken;
|
||||||
action: PayloadAction<{ jwt: string; refreshToken: string }>,
|
|
||||||
) => {
|
// 🔸 Декодируем JWT
|
||||||
state.status = 'successful';
|
const decoded = decodeJwt(action.payload.jwt);
|
||||||
state.jwt = action.payload.jwt;
|
state.username =
|
||||||
state.refreshToken = action.payload.refreshToken;
|
decoded[
|
||||||
axios.defaults.headers.common[
|
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'
|
||||||
'Authorization'
|
] || null;
|
||||||
] = `Bearer ${action.payload.jwt}`;
|
state.email =
|
||||||
localStorage.setItem('jwt', action.payload.jwt);
|
decoded[
|
||||||
localStorage.setItem(
|
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'
|
||||||
'refreshToken',
|
] || null;
|
||||||
action.payload.refreshToken,
|
state.id =
|
||||||
);
|
decoded[
|
||||||
},
|
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier'
|
||||||
);
|
] || null;
|
||||||
builder.addCase(
|
|
||||||
registerUser.rejected,
|
axios.defaults.headers.common[
|
||||||
(state, action: PayloadAction<any>) => {
|
'Authorization'
|
||||||
state.status = 'failed';
|
] = `Bearer ${action.payload.jwt}`;
|
||||||
state.error = action.payload;
|
localStorage.setItem('jwt', action.payload.jwt);
|
||||||
},
|
localStorage.setItem('refreshToken', action.payload.refreshToken);
|
||||||
);
|
});
|
||||||
|
builder.addCase(registerUser.rejected, (state, action) => {
|
||||||
|
state.status = 'failed';
|
||||||
|
state.error = action.payload as string;
|
||||||
|
});
|
||||||
|
|
||||||
// Логин
|
// Логин
|
||||||
builder.addCase(loginUser.pending, (state) => {
|
builder.addCase(loginUser.pending, (state) => {
|
||||||
state.status = 'loading';
|
state.status = 'loading';
|
||||||
state.error = null;
|
state.error = null;
|
||||||
});
|
});
|
||||||
builder.addCase(
|
builder.addCase(loginUser.fulfilled, (state, action) => {
|
||||||
loginUser.fulfilled,
|
state.status = 'successful';
|
||||||
(
|
state.jwt = action.payload.jwt;
|
||||||
state,
|
state.refreshToken = action.payload.refreshToken;
|
||||||
action: PayloadAction<{ jwt: string; refreshToken: string }>,
|
|
||||||
) => {
|
// 🔸 Декодируем JWT
|
||||||
state.status = 'successful';
|
const decoded = decodeJwt(action.payload.jwt);
|
||||||
state.jwt = action.payload.jwt;
|
state.username =
|
||||||
state.refreshToken = action.payload.refreshToken;
|
decoded[
|
||||||
axios.defaults.headers.common[
|
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'
|
||||||
'Authorization'
|
] || null;
|
||||||
] = `Bearer ${action.payload.jwt}`;
|
state.email =
|
||||||
localStorage.setItem('jwt', action.payload.jwt);
|
decoded[
|
||||||
localStorage.setItem(
|
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'
|
||||||
'refreshToken',
|
] || null;
|
||||||
action.payload.refreshToken,
|
state.id =
|
||||||
);
|
decoded[
|
||||||
},
|
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier'
|
||||||
);
|
] || null;
|
||||||
builder.addCase(
|
|
||||||
loginUser.rejected,
|
axios.defaults.headers.common[
|
||||||
(state, action: PayloadAction<any>) => {
|
'Authorization'
|
||||||
state.status = 'failed';
|
] = `Bearer ${action.payload.jwt}`;
|
||||||
state.error = action.payload;
|
localStorage.setItem('jwt', action.payload.jwt);
|
||||||
},
|
localStorage.setItem('refreshToken', action.payload.refreshToken);
|
||||||
);
|
});
|
||||||
|
builder.addCase(loginUser.rejected, (state, action) => {
|
||||||
|
state.status = 'failed';
|
||||||
|
state.error = action.payload as string;
|
||||||
|
});
|
||||||
|
|
||||||
// Обновление токена
|
// Обновление токена
|
||||||
builder.addCase(refreshToken.pending, (state) => {
|
builder.addCase(refreshToken.pending, (state) => {
|
||||||
state.status = 'loading';
|
state.status = 'loading';
|
||||||
state.error = null;
|
state.error = null;
|
||||||
});
|
});
|
||||||
builder.addCase(
|
builder.addCase(refreshToken.fulfilled, (state, action) => {
|
||||||
refreshToken.fulfilled,
|
state.status = 'successful';
|
||||||
(state, action: PayloadAction<{ username: string }>) => {
|
state.jwt = action.payload.jwt;
|
||||||
state.status = 'successful';
|
state.refreshToken = action.payload.refreshToken;
|
||||||
state.username = action.payload.username;
|
|
||||||
},
|
// 🔸 Декодируем JWT
|
||||||
);
|
const decoded = decodeJwt(action.payload.jwt);
|
||||||
builder.addCase(
|
state.username =
|
||||||
refreshToken.rejected,
|
decoded[
|
||||||
(state, action: PayloadAction<any>) => {
|
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'
|
||||||
state.status = 'failed';
|
] || null;
|
||||||
state.error = action.payload;
|
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}`;
|
||||||
|
localStorage.setItem('jwt', action.payload.jwt);
|
||||||
|
localStorage.setItem('refreshToken', action.payload.refreshToken);
|
||||||
|
});
|
||||||
|
builder.addCase(refreshToken.rejected, (state, action) => {
|
||||||
|
state.status = 'failed';
|
||||||
|
state.error = action.payload as string;
|
||||||
|
});
|
||||||
|
|
||||||
// Получение информации о пользователе
|
// Получение информации о пользователе
|
||||||
builder.addCase(fetchWhoAmI.pending, (state) => {
|
builder.addCase(fetchWhoAmI.pending, (state) => {
|
||||||
state.status = 'loading';
|
state.status = 'loading';
|
||||||
state.error = null;
|
state.error = null;
|
||||||
});
|
});
|
||||||
builder.addCase(
|
builder.addCase(fetchWhoAmI.fulfilled, (state, action) => {
|
||||||
fetchWhoAmI.fulfilled,
|
state.status = 'successful';
|
||||||
(state, action: PayloadAction<{ username: string }>) => {
|
state.username = action.payload.username;
|
||||||
state.status = 'successful';
|
});
|
||||||
state.username = action.payload.username;
|
builder.addCase(fetchWhoAmI.rejected, (state, action) => {
|
||||||
},
|
state.status = 'failed';
|
||||||
);
|
state.error = action.payload as string;
|
||||||
builder.addCase(
|
});
|
||||||
fetchWhoAmI.rejected,
|
|
||||||
(state, action: PayloadAction<any>) => {
|
|
||||||
state.status = 'failed';
|
|
||||||
state.error = action.payload;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Загрузка токенов из localStorage
|
// Загрузка токенов из localStorage
|
||||||
builder.addCase(
|
builder.addCase(
|
||||||
loadTokensFromLocalStorage.fulfilled,
|
loadTokensFromLocalStorage.fulfilled,
|
||||||
(
|
(state, action) => {
|
||||||
state,
|
|
||||||
action: PayloadAction<{
|
|
||||||
jwt: string | null;
|
|
||||||
refreshToken: string | null;
|
|
||||||
}>,
|
|
||||||
) => {
|
|
||||||
state.jwt = action.payload.jwt;
|
state.jwt = action.payload.jwt;
|
||||||
state.refreshToken = action.payload.refreshToken;
|
state.refreshToken = action.payload.refreshToken;
|
||||||
|
|
||||||
if (action.payload.jwt) {
|
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[
|
axios.defaults.headers.common[
|
||||||
'Authorization'
|
'Authorization'
|
||||||
] = `Bearer ${action.payload.jwt}`;
|
] = `Bearer ${action.payload.jwt}`;
|
||||||
|
|||||||
@@ -6,9 +6,16 @@ import axios from '../../axios';
|
|||||||
// =====================
|
// =====================
|
||||||
|
|
||||||
export interface Mission {
|
export interface Mission {
|
||||||
missionId: number;
|
id: number;
|
||||||
|
authorId: number;
|
||||||
name: string;
|
name: string;
|
||||||
sortOrder: number;
|
difficulty: number;
|
||||||
|
tags: string[];
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
timeLimitMilliseconds: number;
|
||||||
|
memoryLimitBytes: number;
|
||||||
|
statements: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Member {
|
export interface Member {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
background-color: var(--color-liquid-background);
|
background-color: var(--color-liquid-background);
|
||||||
color: rgba(255, 255, 255, 0.87);
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
#root {
|
#root {
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ const Contest = () => {
|
|||||||
if (contestIdNumber === null) {
|
if (contestIdNumber === null) {
|
||||||
return <Navigate to="/home/contests" replace />;
|
return <Navigate to="/home/contests" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const contest = useAppSelector((state) => state.contests.selectedContest);
|
const contest = useAppSelector((state) => state.contests.selectedContest);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { cn } from '../../../lib/cn';
|
import { cn } from '../../../lib/cn';
|
||||||
import { IconError, IconSuccess } from '../../../assets/icons/missions';
|
import { IconError, IconSuccess } from '../../../assets/icons/missions';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
export interface MissionItemProps {
|
export interface MissionItemProps {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -31,6 +32,8 @@ const MissionItem: React.FC<MissionItemProps> = ({
|
|||||||
status,
|
status,
|
||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
const path = location.pathname + location.search;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -45,7 +48,7 @@ const MissionItem: React.FC<MissionItemProps> = ({
|
|||||||
'cursor-pointer brightness-100 hover:brightness-125 transition-all duration-300',
|
'cursor-pointer brightness-100 hover:brightness-125 transition-all duration-300',
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate(`/mission/${id}`);
|
navigate(`/mission/${id}?back=${path}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="text-[18px] font-bold">#{id}</div>
|
<div className="text-[18px] font-bold">#{id}</div>
|
||||||
|
|||||||
@@ -28,8 +28,10 @@ const ContestMissions: FC<ContestMissionsProps> = ({ contest }) => {
|
|||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
{contest.missions.map((v, i) => (
|
{contest.missions.map((v, i) => (
|
||||||
<MissionItem
|
<MissionItem
|
||||||
id={v.missionId}
|
id={v.id}
|
||||||
name={v.name}
|
name={v.name}
|
||||||
|
timeLimit={v.timeLimitMilliseconds}
|
||||||
|
memoryLimit={v.memoryLimitBytes}
|
||||||
type={i % 2 ? 'second' : 'first'}
|
type={i % 2 ? 'second' : 'first'}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
missionId: number;
|
missionId: number;
|
||||||
|
back?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Header: React.FC<HeaderProps> = ({ missionId }) => {
|
const Header: React.FC<HeaderProps> = ({ missionId, back }) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
return (
|
return (
|
||||||
<header className="w-full h-[60px] flex items-center px-4 gap-[20px]">
|
<header className="w-full h-[60px] flex items-center px-4 gap-[20px]">
|
||||||
@@ -29,7 +30,8 @@ const Header: React.FC<HeaderProps> = ({ missionId }) => {
|
|||||||
alt="back"
|
alt="back"
|
||||||
className="h-[24px] w-[24px] cursor-pointer"
|
className="h-[24px] w-[24px] cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate('/home/missions');
|
if (back) navigate(back);
|
||||||
|
else navigate('/home/missions');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -39,7 +41,10 @@ const Header: React.FC<HeaderProps> = ({ missionId }) => {
|
|||||||
alt="back"
|
alt="back"
|
||||||
className="h-[24px] w-[24px] cursor-pointer"
|
className="h-[24px] w-[24px] cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate(`/mission/${missionId - 1}`);
|
if (missionId <= 1) return;
|
||||||
|
if (back)
|
||||||
|
navigate(`/mission/${missionId - 1}?back=${back}`);
|
||||||
|
else navigate(`/mission/${missionId - 1}`);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span>{missionId}</span>
|
<span>{missionId}</span>
|
||||||
@@ -48,7 +53,9 @@ const Header: React.FC<HeaderProps> = ({ missionId }) => {
|
|||||||
alt="back"
|
alt="back"
|
||||||
className="h-[24px] w-[24px] cursor-pointer"
|
className="h-[24px] w-[24px] cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate(`/mission/${missionId + 1}`);
|
if (back)
|
||||||
|
navigate(`/mission/${missionId + 1}?back=${back}`);
|
||||||
|
else navigate(`/mission/${missionId + 1}`);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user