Compare commits
2 Commits
ac8e3ab972
...
9274b3314d
Author | SHA1 | Date | |
---|---|---|---|
9274b3314d | |||
0be3286bb3 |
@ -2,3 +2,5 @@ node_modules/
|
|||||||
/dist/
|
/dist/
|
||||||
/pnpm-lock.yaml
|
/pnpm-lock.yaml
|
||||||
/postcss.config.cjs
|
/postcss.config.cjs
|
||||||
|
|
||||||
|
src/components/Languages.tsx
|
||||||
|
38
src/app/services/submission.ts
Normal file
38
src/app/services/submission.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import type { Meta, Wrap } from "./base";
|
||||||
|
import type { UserProfile } from "./user";
|
||||||
|
import type { ProblemInfo } from "./problem";
|
||||||
|
import { api } from "./api";
|
||||||
|
|
||||||
|
export interface SubmissionInfo {
|
||||||
|
meta: Meta;
|
||||||
|
problem: ProblemInfo;
|
||||||
|
user: UserProfile;
|
||||||
|
language: string;
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubmitRequest {
|
||||||
|
pid: number;
|
||||||
|
language: string;
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReJudgeResponse {
|
||||||
|
sid: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const submissionApi = api.injectEndpoints({
|
||||||
|
endpoints: (builder) => ({
|
||||||
|
submit: builder.mutation<Wrap<string>, SubmitRequest>({
|
||||||
|
query: (data: SubmitRequest) => ({
|
||||||
|
url: "/submission/create",
|
||||||
|
method: "POST",
|
||||||
|
body: data,
|
||||||
|
}),
|
||||||
|
invalidatesTags: [{ type: "Status", id: "Search" }],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
// TODO: admin endpoints: rejudge
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { useSubmitMutation } = submissionApi;
|
10
src/components/Languages.tsx
Normal file
10
src/components/Languages.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/* eslint-disable prettier/prettier */
|
||||||
|
export const LanguageMap: { [lang: string]: { label: string; mode: string; prism: string } } = {
|
||||||
|
c: { label: "C", mode: "c_cpp", prism: "cpp" },
|
||||||
|
cpp: { label: "C++", mode: "c_cpp", prism: "cpp" },
|
||||||
|
python3: { label: "Python3", mode: "python", prism: "python" },
|
||||||
|
pypy3: { label: "PyPy3", mode: "python", prism: "python" },
|
||||||
|
rust: { label: "Rust", mode: "rust", prism: "rust" },
|
||||||
|
go: { label: "Go", mode: "golang", prism: "go" },
|
||||||
|
};
|
||||||
|
/* eslint-enable prettier/prettier */
|
@ -1,79 +0,0 @@
|
|||||||
.row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row > button {
|
|
||||||
margin-left: 4px;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row:not(:last-child) {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.value {
|
|
||||||
font-size: 78px;
|
|
||||||
padding-left: 16px;
|
|
||||||
padding-right: 16px;
|
|
||||||
margin-top: 2px;
|
|
||||||
font-family: "Courier New", Courier, monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
appearance: none;
|
|
||||||
font-size: 32px;
|
|
||||||
padding-left: 12px;
|
|
||||||
padding-right: 12px;
|
|
||||||
outline: none;
|
|
||||||
border: 2px solid transparent;
|
|
||||||
color: rgb(112, 76, 182);
|
|
||||||
padding-bottom: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
background: rgba(112, 76, 182, 0.1) none;
|
|
||||||
border-radius: 2px;
|
|
||||||
transition: all 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.textbox {
|
|
||||||
font-size: 32px;
|
|
||||||
padding: 2px;
|
|
||||||
width: 64px;
|
|
||||||
text-align: center;
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button:hover,
|
|
||||||
.button:focus {
|
|
||||||
border: 2px solid rgba(112, 76, 182, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.button:active {
|
|
||||||
background-color: rgba(112, 76, 182, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.asyncButton {
|
|
||||||
composes: button;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asyncButton:after {
|
|
||||||
content: "";
|
|
||||||
background-color: rgba(112, 76, 182, 0.15);
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
opacity: 0;
|
|
||||||
transition: width 1s linear,
|
|
||||||
opacity 0.5s ease 1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asyncButton:active:after {
|
|
||||||
width: 0;
|
|
||||||
opacity: 1;
|
|
||||||
transition: 0s;
|
|
||||||
}
|
|
@ -1,67 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import { useAppDispatch, useAppSelector } from "../../hooks/store";
|
|
||||||
import styles from "./Counter.module.css";
|
|
||||||
import {
|
|
||||||
decrement,
|
|
||||||
increment,
|
|
||||||
incrementAsync,
|
|
||||||
incrementByAmount,
|
|
||||||
incrementIfOdd,
|
|
||||||
selectCount,
|
|
||||||
selectStatus,
|
|
||||||
} from "./counterSlice";
|
|
||||||
|
|
||||||
export const Counter = () => {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const count = useAppSelector(selectCount);
|
|
||||||
const status = useAppSelector(selectStatus);
|
|
||||||
const [incrementAmount, setIncrementAmount] = useState("2");
|
|
||||||
|
|
||||||
const incrementValue = Number(incrementAmount) || 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className={styles.row}>
|
|
||||||
<button className={styles.button} aria-label="Decrement value" onClick={() => dispatch(decrement())}>
|
|
||||||
-
|
|
||||||
</button>
|
|
||||||
<span aria-label="Count" className={styles.value}>
|
|
||||||
{count}
|
|
||||||
</span>
|
|
||||||
<button className={styles.button} aria-label="Increment value" onClick={() => dispatch(increment())}>
|
|
||||||
+
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className={styles.row}>
|
|
||||||
<input
|
|
||||||
className={styles.textbox}
|
|
||||||
aria-label="Set increment amount"
|
|
||||||
value={incrementAmount}
|
|
||||||
type="number"
|
|
||||||
onChange={(e) => {
|
|
||||||
setIncrementAmount(e.target.value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<button className={styles.button} onClick={() => dispatch(incrementByAmount(incrementValue))}>
|
|
||||||
Add Amount
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={styles.asyncButton}
|
|
||||||
disabled={status !== "idle"}
|
|
||||||
onClick={() => void dispatch(incrementAsync(incrementValue))}
|
|
||||||
>
|
|
||||||
Add Async
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={styles.button}
|
|
||||||
onClick={() => {
|
|
||||||
dispatch(incrementIfOdd(incrementValue));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Add If Odd
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,4 +0,0 @@
|
|||||||
// A mock function to mimic making an async request for data
|
|
||||||
export const fetchCount = (amount = 1) => {
|
|
||||||
return new Promise<{ data: number }>((resolve) => setTimeout(() => resolve({ data: amount }), 500));
|
|
||||||
};
|
|
@ -1,86 +0,0 @@
|
|||||||
import type { PayloadAction } from "@reduxjs/toolkit";
|
|
||||||
import { createAppSlice } from "../../app/createAppSlice";
|
|
||||||
import type { AppThunk } from "../../app/store";
|
|
||||||
import { fetchCount } from "./counterAPI";
|
|
||||||
|
|
||||||
export interface CounterSliceState {
|
|
||||||
value: number;
|
|
||||||
status: "idle" | "loading" | "failed";
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialState: CounterSliceState = {
|
|
||||||
value: 0,
|
|
||||||
status: "idle",
|
|
||||||
};
|
|
||||||
|
|
||||||
// If you are not using async thunks you can use the standalone `createSlice`.
|
|
||||||
export const counterSlice = createAppSlice({
|
|
||||||
name: "counter",
|
|
||||||
// `createSlice` will infer the state type from the `initialState` argument
|
|
||||||
initialState,
|
|
||||||
// The `reducers` field lets us define reducers and generate associated actions
|
|
||||||
reducers: (create) => ({
|
|
||||||
increment: create.reducer((state) => {
|
|
||||||
// Redux Toolkit allows us to write "mutating" logic in reducers. It
|
|
||||||
// doesn't actually mutate the state because it uses the Immer library,
|
|
||||||
// which detects changes to a "draft state" and produces a brand new
|
|
||||||
// immutable state based off those changes
|
|
||||||
state.value += 1;
|
|
||||||
}),
|
|
||||||
decrement: create.reducer((state) => {
|
|
||||||
state.value -= 1;
|
|
||||||
}),
|
|
||||||
// Use the `PayloadAction` type to declare the contents of `action.payload`
|
|
||||||
incrementByAmount: create.reducer((state, action: PayloadAction<number>) => {
|
|
||||||
state.value += action.payload;
|
|
||||||
}),
|
|
||||||
// The function below is called a thunk and allows us to perform async logic. It
|
|
||||||
// can be dispatched like a regular action: `dispatch(incrementAsync(10))`. This
|
|
||||||
// will call the thunk with the `dispatch` function as the first argument. Async
|
|
||||||
// code can then be executed and other actions can be dispatched. Thunks are
|
|
||||||
// typically used to make async requests.
|
|
||||||
incrementAsync: create.asyncThunk(
|
|
||||||
async (amount: number) => {
|
|
||||||
const response = await fetchCount(amount);
|
|
||||||
// The value we return becomes the `fulfilled` action payload
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pending: (state) => {
|
|
||||||
state.status = "loading";
|
|
||||||
},
|
|
||||||
fulfilled: (state, action) => {
|
|
||||||
state.status = "idle";
|
|
||||||
state.value += action.payload;
|
|
||||||
},
|
|
||||||
rejected: (state) => {
|
|
||||||
state.status = "failed";
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
// You can define your selectors here. These selectors receive the slice
|
|
||||||
// state as their first argument.
|
|
||||||
selectors: {
|
|
||||||
selectCount: (counter) => counter.value,
|
|
||||||
selectStatus: (counter) => counter.status,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Action creators are generated for each case reducer function.
|
|
||||||
export const { decrement, increment, incrementByAmount, incrementAsync } = counterSlice.actions;
|
|
||||||
|
|
||||||
// Selectors returned by `slice.selectors` take the root state as their first argument.
|
|
||||||
export const { selectCount, selectStatus } = counterSlice.selectors;
|
|
||||||
|
|
||||||
// We can also write thunks by hand, which may contain both sync and async logic.
|
|
||||||
// Here's an example of conditionally dispatching actions based on current state.
|
|
||||||
export const incrementIfOdd =
|
|
||||||
(amount: number): AppThunk =>
|
|
||||||
(dispatch, getState) => {
|
|
||||||
const currentValue = selectCount(getState());
|
|
||||||
|
|
||||||
if (currentValue % 2 === 1 || currentValue % 2 === -1) {
|
|
||||||
dispatch(incrementByAmount(amount));
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,18 +0,0 @@
|
|||||||
.select {
|
|
||||||
font-size: 25px;
|
|
||||||
padding: 2px 5px;
|
|
||||||
size: 50px;
|
|
||||||
outline: none;
|
|
||||||
border: 2px solid transparent;
|
|
||||||
color: rgb(112, 76, 182);
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: rgba(112, 76, 182, 0.1);
|
|
||||||
border-radius: 5px;
|
|
||||||
transition: all 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import styles from "./Quotes.module.css";
|
|
||||||
import { useGetQuotesQuery } from "./quotesApiSlice";
|
|
||||||
|
|
||||||
const options = [5, 10, 20, 30];
|
|
||||||
|
|
||||||
export const Quotes = () => {
|
|
||||||
const [numberOfQuotes, setNumberOfQuotes] = useState(10);
|
|
||||||
// Using a query hook automatically fetches data and returns query values
|
|
||||||
const { data, isError, isLoading, isSuccess } = useGetQuotesQuery(numberOfQuotes);
|
|
||||||
|
|
||||||
if (isError) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>There was an error!!!</h1>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>Loading...</h1>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSuccess) {
|
|
||||||
return (
|
|
||||||
<div className={styles.container}>
|
|
||||||
<h3>Select the Quantity of Quotes to Fetch:</h3>
|
|
||||||
<select
|
|
||||||
className={styles.select}
|
|
||||||
value={numberOfQuotes}
|
|
||||||
onChange={(e) => {
|
|
||||||
setNumberOfQuotes(Number(e.target.value));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{options.map((option) => (
|
|
||||||
<option key={option} value={option}>
|
|
||||||
{option}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
{data.quotes.map(({ author, quote, id }) => (
|
|
||||||
<blockquote key={id}>
|
|
||||||
“{quote}”
|
|
||||||
<footer>
|
|
||||||
<cite>{author}</cite>
|
|
||||||
</footer>
|
|
||||||
</blockquote>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
@ -1,38 +0,0 @@
|
|||||||
// Need to use the React-specific entry point to import `createApi`
|
|
||||||
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
|
|
||||||
|
|
||||||
interface Quote {
|
|
||||||
id: number;
|
|
||||||
quote: string;
|
|
||||||
author: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface QuotesApiResponse {
|
|
||||||
quotes: Quote[];
|
|
||||||
total: number;
|
|
||||||
skip: number;
|
|
||||||
limit: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define a service using a base URL and expected endpoints
|
|
||||||
export const quotesApiSlice = createApi({
|
|
||||||
baseQuery: fetchBaseQuery({ baseUrl: "https://dummyjson.com/quotes" }),
|
|
||||||
reducerPath: "quotesApi",
|
|
||||||
// Tag types are used for caching and invalidation.
|
|
||||||
tagTypes: ["Quotes"],
|
|
||||||
endpoints: (build) => ({
|
|
||||||
// Supply generics for the return type (in this case `QuotesApiResponse`)
|
|
||||||
// and the expected query argument. If there is no argument, use `void`
|
|
||||||
// for the argument type instead.
|
|
||||||
getQuotes: build.query<QuotesApiResponse, number>({
|
|
||||||
query: (limit = 10) => `?limit=${limit}`,
|
|
||||||
// `providesTags` determines which 'tag' is attached to the
|
|
||||||
// cached data returned by the query.
|
|
||||||
providesTags: (result, error, id) => [{ type: "Quotes", id }],
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Hooks are auto-generated by RTK-Query
|
|
||||||
// Same as `quotesApiSlice.endpoints.getQuotes.useQuery`
|
|
||||||
export const { useGetQuotesQuery } = quotesApiSlice;
|
|
@ -85,7 +85,7 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// authSlice will automatically handle the response
|
// authSlice will automatically handle the response
|
||||||
await login(formState).unwrap();
|
await login(formState);
|
||||||
navigate("/");
|
navigate("/");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errTyped = err as { status: number | string; data: Wrap<void> };
|
const errTyped = err as { status: number | string; data: Wrap<void> };
|
||||||
|
65
src/pages/SubmitPage.tsx
Normal file
65
src/pages/SubmitPage.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { lazy, useEffect, useState } from "react";
|
||||||
|
import { Box, Button, Stack, useToast } from "@chakra-ui/react";
|
||||||
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import { MdPlayCircleOutline } from "react-icons/md";
|
||||||
|
|
||||||
|
import { LanguageMap } from "../components/Languages";
|
||||||
|
import { useDetailQuery } from "../app/services/problem";
|
||||||
|
import { useSubmitMutation } from "../app/services/submission";
|
||||||
|
|
||||||
|
const Editor = lazy(() => import("../components/Editor"));
|
||||||
|
const ProblemInfoMenu = lazy(() => import("../components/ProblemInfoMenu"));
|
||||||
|
|
||||||
|
export default function SubmitPage() {
|
||||||
|
const { id } = useParams();
|
||||||
|
const toast = useToast();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { data: details } = useDetailQuery({ pid: Number(id) || 0 });
|
||||||
|
const [submit, result] = useSubmitMutation();
|
||||||
|
|
||||||
|
const [lang, setLang] = useState("c");
|
||||||
|
const [code, setCode] = useState("");
|
||||||
|
|
||||||
|
const submitCode = () => {
|
||||||
|
void submit({ pid: Number(id) || 0, language: lang, code: code });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (result.isSuccess) {
|
||||||
|
navigate(`/problem/${id}/status`);
|
||||||
|
} else if (result.isError) {
|
||||||
|
toast({
|
||||||
|
status: "error",
|
||||||
|
title: "Oops...",
|
||||||
|
description: "Submit Failed",
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [id, navigate, result, toast]);
|
||||||
|
|
||||||
|
const actionBtn = (
|
||||||
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
size="sm"
|
||||||
|
colorScheme="green"
|
||||||
|
leftIcon={<MdPlayCircleOutline />}
|
||||||
|
isLoading={result.isLoading}
|
||||||
|
onClick={submitCode}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack direction={{ base: "column", md: "row" }}>
|
||||||
|
<Box w="100%">
|
||||||
|
<Editor mode={LanguageMap[lang].mode} onChange={setCode} />
|
||||||
|
</Box>
|
||||||
|
<Box w="480px">
|
||||||
|
<ProblemInfoMenu data={details?.body} onLanguageSelect={setLang} action={actionBtn} />
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -3,13 +3,14 @@ import type { RouteObject } from "react-router-dom";
|
|||||||
|
|
||||||
import { Root } from "./pages/Root";
|
import { Root } from "./pages/Root";
|
||||||
import { ErrorPage } from "./pages/ErrorPage";
|
import { ErrorPage } from "./pages/ErrorPage";
|
||||||
// import { RequireAuth } from "./components/RequireAuth";
|
import { RequireAuth } from "./components/RequireAuth";
|
||||||
|
|
||||||
const HomePage = lazy(() => import("./pages/HomePage"));
|
const HomePage = lazy(() => import("./pages/HomePage"));
|
||||||
const LoginPage = lazy(() => import("./pages/LoginPage"));
|
const LoginPage = lazy(() => import("./pages/LoginPage"));
|
||||||
const LogoutPage = lazy(() => import("./pages/LogoutPage"));
|
const LogoutPage = lazy(() => import("./pages/LogoutPage"));
|
||||||
const ProblemListPage = lazy(() => import("./pages/ProblemListPage"));
|
const ProblemListPage = lazy(() => import("./pages/ProblemListPage"));
|
||||||
const ProblemDetailPage = lazy(() => import("./pages/ProblemDetailPage"));
|
const ProblemDetailPage = lazy(() => import("./pages/ProblemDetailPage"));
|
||||||
|
const SubmitPage = lazy(() => import("./pages/SubmitPage"));
|
||||||
|
|
||||||
export const router: RouteObject[] = [
|
export const router: RouteObject[] = [
|
||||||
{
|
{
|
||||||
@ -41,6 +42,14 @@ export const router: RouteObject[] = [
|
|||||||
path: "problem/:id",
|
path: "problem/:id",
|
||||||
element: <ProblemDetailPage />,
|
element: <ProblemDetailPage />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "problem/:id/submit",
|
||||||
|
element: (
|
||||||
|
<RequireAuth>
|
||||||
|
<SubmitPage />
|
||||||
|
</RequireAuth>
|
||||||
|
),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
Loading…
Reference in New Issue
Block a user