Skip to content

Commit 76bf68b

Browse files
authored
Merge pull request #2 from CS3219-AY2425S1/qn-page
Add Individual Question Page and Rudimentary Add Question Page
2 parents 8febf76 + ecc89a5 commit 76bf68b

File tree

15 files changed

+519
-2
lines changed

15 files changed

+519
-2
lines changed

peerprep/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
22

3+
# environment file
4+
.env
5+
36
# dependencies
47
/node_modules
58
/.pnp

peerprep/api/gateway.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { Question, StatusBody, QuestionFullBody } from "./structs";
2+
3+
const questions: { [key: string]: Question } = {
4+
"0" : {
5+
"id": 0,
6+
"difficulty": 2,
7+
"title": "Two Sum",
8+
"description": "Given an array of integers, return indices of the two numbers such that they add up to a specific target.",
9+
"test_cases": {
10+
"[2, 7, 11, 15], 9" : "[0, 1]",
11+
"[3, 2, 4], 6" : "[1, 2]",
12+
"[3, 3], 6" : "[0, 1]"
13+
}
14+
},
15+
"1" : {
16+
"id": 1,
17+
"difficulty": 1,
18+
"title": "Reverse Integer",
19+
"description": "Given a 32-bit signed integer, reverse digits of an integer.",
20+
"test_cases": {
21+
"123" : "321",
22+
"1" : "1",
23+
"22" : "22"
24+
}
25+
}
26+
};
27+
28+
export async function fetchQuestion(questionId: string): Promise<Question|StatusBody> {
29+
// remove this when services are up
30+
if (process.env.DEV_ENV === "dev") {
31+
return questions[questionId] === undefined
32+
? {error: "Question not found", status: 404}
33+
: questions[questionId];
34+
}
35+
try {
36+
const response = await fetch(`${process.env.NEXT_PUBLIC_QUESTION_SERVICE}/questions/solve/${questionId}`);
37+
if (!response.ok) {
38+
return {
39+
...(await response.json()),
40+
status: response.status
41+
};
42+
}
43+
return await response.json() as Question;
44+
} catch (err: any) {
45+
return { error: err.message, status: 0};
46+
}
47+
}
48+
49+
export async function addQuestion(body: QuestionFullBody): Promise<StatusBody> {
50+
try {
51+
const response = await fetch(
52+
`${process.env.NEXT_PUBLIC_QUESTION_SERVICE}/questions`,
53+
{
54+
method: "POST",
55+
body: JSON.stringify(body).replace(/(\"difficulty\":)\"([1-3])\"/, `$1$2`),
56+
headers: {
57+
"Content-type": "application/json; charset=UTF-8"
58+
}
59+
}
60+
);
61+
if (response.ok) {
62+
return {
63+
status: response.status
64+
};
65+
}
66+
return {
67+
error: (await response.json())["Error adding question: "],
68+
status: response.status
69+
};
70+
} catch (err: any) {
71+
return { error: err.message, status: 0};
72+
}
73+
}

peerprep/api/structs.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
export enum Difficulty {
2+
Easy = 1,
3+
Medium,
4+
Hard
5+
}
6+
7+
export interface QuestionBody {
8+
difficulty: Difficulty;
9+
title: string;
10+
description: string;
11+
}
12+
13+
export interface TestCase {
14+
test_cases: {
15+
[key: string]: string;
16+
};
17+
}
18+
19+
export interface QuestionFullBody extends QuestionBody, TestCase {}
20+
21+
export interface Question extends QuestionFullBody {
22+
id: number;
23+
}
24+
25+
export interface StatusBody {
26+
status: number;
27+
error?: string;
28+
}
29+
30+
export function isError(obj: Question | StatusBody): obj is StatusBody {
31+
return (obj as StatusBody).error !== undefined;
32+
}

peerprep/app/globals.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
@tailwind components;
33
@tailwind utilities;
44

5+
html, body {
6+
margin: 0;
7+
height: 100%;
8+
overflow: hidden;
9+
}
10+
511
:root {
612
--foreground-rgb: 0, 0, 0;
713
--background-start-rgb: 214, 219, 220;

peerprep/app/layout.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import TitleBar from '@/components/shared/TitleBar'
12
import './globals.css'
23
import type { Metadata } from 'next'
34
import { Inter } from 'next/font/google'
5+
import Container from '@/components/shared/Container'
6+
import styles from '@/style/layout.module.css';
47

58
const inter = Inter({ subsets: ['latin'] })
69

@@ -17,8 +20,8 @@ export default function RootLayout({
1720
return (
1821
<html lang="en">
1922
<body className={inter.className}>
20-
<h1>NavBar</h1>
21-
{children}
23+
<TitleBar />
24+
<Container className={styles.main_container}>{children}</Container>
2225
</body>
2326
</html>
2427
)

peerprep/app/q/[question]/page.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { fetchQuestion } from '@/api/gateway';
2+
import { Question as QnType, StatusBody, isError } from "@/api/structs";
3+
import styles from '@/style/question.module.css';
4+
import ErrorBlock from '@/components/shared/ErrorBlock';
5+
import React from 'react'
6+
import QuestionBlock from './question';
7+
8+
type Props = {
9+
params: {
10+
question: string
11+
}
12+
}
13+
14+
async function Question({ params }: Props) {
15+
const question = await fetchQuestion(params.question);
16+
17+
return (
18+
<div className={styles.wrapper}>
19+
{
20+
isError(question)
21+
? <ErrorBlock err={question as StatusBody}/>
22+
: <QuestionBlock question={question as QnType}/>
23+
}
24+
</div>
25+
)
26+
}
27+
28+
export default Question;
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { Question, StatusBody, Difficulty } from "@/api/structs";
2+
import styles from '@/style/question.module.css';
3+
4+
interface Props {
5+
question : Question;
6+
}
7+
8+
const difficultyColor = (diff: Difficulty) => {
9+
return (
10+
diff === Difficulty.Easy ? <p className={`${styles.title} ${styles.easy}`}>Easy</p>
11+
: diff === Difficulty.Medium ? <p className={`${styles.title} ${styles.med}`}>Med</p>
12+
: <p className={`${styles.title} ${styles.hard}`}>Hard</p>
13+
);
14+
}
15+
16+
function QuestionBlock({ question }: Props) {
17+
const keys = question.test_cases ? Object.keys(question.test_cases) : [];
18+
19+
const createRow = (key: string) => (
20+
<tr key={key}>
21+
<td className={`${styles.table} ${styles.cell}`}>{key}</td>
22+
<td className={`${styles.table} ${styles.cell}`}>{question.test_cases[key]}</td>
23+
</tr>
24+
);
25+
26+
return (
27+
<>
28+
<div className={styles.qn_container}>
29+
<div className={styles.title_wrapper}>
30+
<h1 className={styles.title}>Q{question.id}: {question.title}</h1>
31+
{difficultyColor(question.difficulty)}
32+
</div>
33+
<br/>
34+
<p>{question.description}</p>
35+
<br/>
36+
{question.test_cases && (
37+
<table className={styles.table}>
38+
<tbody>
39+
<tr>
40+
<th className={`${styles.table} ${styles.header} ${styles.input}`}>Input</th>
41+
<th className={`${styles.table} ${styles.header} ${styles.output}`}>Expected Output</th>
42+
</tr>
43+
{keys.map(createRow)}
44+
</tbody>
45+
</table>
46+
)}
47+
</div>
48+
<form className={styles.editor_container}>
49+
<textarea className={styles.code_editor}/>
50+
</form>
51+
</>
52+
);
53+
}
54+
55+
export default QuestionBlock;

peerprep/app/q/new/page.tsx

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
'use client';
2+
import { useState, ChangeEvent, MouseEvent, FormEvent } from 'react';
3+
import { QuestionBody, Difficulty, QuestionFullBody } from '@/api/structs';
4+
import { addQuestion } from '@/api/gateway';
5+
6+
type Props = {}
7+
8+
interface Mapping {
9+
key: string,
10+
value: string
11+
}
12+
13+
function NewQuestion({}: Props) {
14+
const [testCases, setTestCases] = useState<Mapping[]>([{
15+
key: "", value: ""
16+
}]);
17+
const [formData, setFormData] = useState<QuestionBody>({
18+
title: "",
19+
difficulty: Difficulty.Easy,
20+
description: "",
21+
});
22+
23+
const handleTextInput = (e: ChangeEvent<HTMLInputElement|HTMLTextAreaElement>) => setFormData({
24+
...formData,
25+
[e.target.name]: e.target.value
26+
});
27+
28+
const handleTestCaseInput = (e: ChangeEvent<HTMLInputElement>, idx: number) => {
29+
const values = [...testCases];
30+
values[idx] = {
31+
...values[idx],
32+
[e.target.name]: e.target.value
33+
};
34+
setTestCases(values);
35+
}
36+
37+
const handleAddField = (e: MouseEvent<HTMLElement>) =>
38+
setTestCases([...testCases, { key: "", value: ""}]);
39+
40+
const handleDeleteField = (e: MouseEvent<HTMLElement>, idx: number) => {
41+
const values = [...testCases];
42+
values.splice(idx, 1);
43+
setTestCases(values);
44+
}
45+
46+
const handleSubmission = async (e: FormEvent<HTMLFormElement>) => {
47+
e.preventDefault();
48+
const question: QuestionFullBody = {
49+
...formData,
50+
test_cases: testCases.map((elem: Mapping) => ({
51+
[elem.key]: elem.value
52+
})).reduce((res, item) => ({...res, ...item}), {})
53+
}
54+
const status = await addQuestion(question);
55+
if (status.error) {
56+
console.log("Failed to add question.");
57+
console.log(`Code ${status.status}: ${status.error}`);
58+
return;
59+
}
60+
console.log(`Successfully added the question.`);
61+
}
62+
63+
return (
64+
<div>
65+
<form style={{color: "black", padding: "5px"}} onSubmit={handleSubmission}>
66+
<input type="text" name="title" value={formData.title} onChange={handleTextInput}/><br/>
67+
<input type="radio" id="easy" name="difficulty" value={1} onChange={handleTextInput} />
68+
<label htmlFor="easy">Easy</label><br/>
69+
<input type="radio" id="med" name="difficulty" value={2} onChange={handleTextInput} />
70+
<label htmlFor="med">Medium</label><br/>
71+
<input type="radio" id="hard" name="difficulty" value={3} onChange={handleTextInput} />
72+
<label htmlFor="hard">Hard</label><br/>
73+
<textarea name="description" value={formData.description} onChange={handleTextInput}/><br/>
74+
{testCases.map((elem, idx) => (
75+
<>
76+
<input
77+
name="key"
78+
type="text"
79+
id={`key_${idx.toLocaleString()}`}
80+
value={elem.key}
81+
onChange={e => handleTestCaseInput(e, idx)} />
82+
<input
83+
name="value"
84+
type="text"
85+
id={`val_${idx.toLocaleString()}`}
86+
value={elem.value}
87+
onChange={e => handleTestCaseInput(e, idx)} />
88+
<input
89+
type="button"
90+
name="del_entry"
91+
value="Delete..."
92+
onClick={e => handleDeleteField(e, idx)}
93+
style={{ backgroundColor: "white"}} />
94+
<br/>
95+
</>
96+
))}
97+
<input
98+
type="button"
99+
name="add_entry"
100+
value="Add..."
101+
onClick={handleAddField}
102+
style={{ backgroundColor: "white"}} />
103+
<button
104+
type="submit"
105+
name="submit"
106+
style={{ backgroundColor: "white"}}>Submit</button>
107+
</form>
108+
</div>
109+
)
110+
}
111+
112+
export default NewQuestion;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import React, { ReactNode } from 'react';
2+
3+
interface Props {
4+
children : ReactNode,
5+
className : string
6+
}
7+
8+
function Container({ children, className }: Props) {
9+
return (
10+
<div className={className}>
11+
{children}
12+
</div>
13+
)
14+
}
15+
16+
export default Container;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import React from 'react'
2+
import styles from '@/style/error.module.css';
3+
import { StatusBody } from '@/api/structs';
4+
5+
interface Props {
6+
err : StatusBody
7+
}
8+
9+
function ErrorBlock({ err }: Props) {
10+
return (
11+
<div className={styles.container}>
12+
<h1 className={styles.title}>
13+
{err.status}: We've seen better days.
14+
</h1>
15+
<p className={styles.details}>
16+
Reason: {err.error}
17+
</p>
18+
</div>
19+
)
20+
}
21+
22+
export default ErrorBlock

0 commit comments

Comments
 (0)