Skip to content

Commit dbd17f8

Browse files
committed
Styling for QuestionCard.tsx
1 parent c87b0e1 commit dbd17f8

File tree

9 files changed

+460
-84
lines changed

9 files changed

+460
-84
lines changed

peerprep/api/gateway.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { cookies } from "next/headers";
2-
import { Question, StatusBody, LoginResponse, SigninResponse } from "./structs";
2+
import { LoginResponse, Question, SigninResponse, StatusBody } from "./structs";
3+
import DOMPurify from "isomorphic-dompurify";
34

45
export function generateAuthHeaders() {
56
return {
@@ -15,23 +16,28 @@ export function generateJSONHeaders() {
1516
}
1617

1718
export async function fetchQuestion(
18-
questionId: string
19+
questionId: string,
1920
): Promise<Question | StatusBody> {
2021
try {
2122
const response = await fetch(
2223
`${process.env.NEXT_PUBLIC_QUESTION_SERVICE}/questions/solve/${questionId}`,
2324
{
2425
method: "GET",
2526
headers: generateAuthHeaders(),
26-
}
27+
},
2728
);
2829
if (!response.ok) {
2930
return {
3031
error: await response.text(),
3132
status: response.status,
3233
};
3334
}
34-
return (await response.json()) as Question;
35+
36+
// NOTE: this may cause the following: "Can't resolve canvas"
37+
// https://github.com/kkomelin/isomorphic-dompurify/issues/54
38+
const question = (await response.json()) as Question;
39+
question.content = DOMPurify.sanitize(question.content);
40+
return question;
3541
} catch (err: any) {
3642
return { error: err.message, status: 400 };
3743
}
@@ -50,7 +56,7 @@ export async function getSessionLogin(validatedFields: {
5056
headers: {
5157
"Content-type": "application/json; charset=UTF-8",
5258
},
53-
}
59+
},
5460
);
5561
const json = await res.json();
5662

peerprep/api/structs.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { z } from "zod";
22

33
export enum Difficulty {
4-
All = 0,
5-
Easy = 1,
6-
Medium,
7-
Hard,
4+
All = "All",
5+
Easy = "Easy",
6+
Medium = "Medium",
7+
Hard = "Hard",
88
}
99

1010
export interface QuestionBody {

peerprep/app/questions/[question]/question.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@ function QuestionBlock({ question }: Props) {
7878
question.topicTags.map((elem, idx) => <p key={idx}>{elem}</p>)
7979
)}
8080
</div>
81-
8281
{ <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(question.content)}} /> }
8382
<br />
8483
</div>

peerprep/app/questions/new/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,8 @@ function NewQuestion({}: Props) {
130130
<FormTextAreaInput
131131
required
132132
disabled={loading}
133-
label="Description: "
134-
name="description"
133+
label="Content: "
134+
name="Content"
135135
value={formData.content}
136136
onChange={handleFormTextInput}
137137
/>
Lines changed: 49 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, {useState} from "react";
1+
import React, { useState } from "react";
22
import AceEditor from "react-ace";
33

44
import "ace-builds/src-noconflict/mode-python";
@@ -7,6 +7,11 @@ import "ace-builds/src-noconflict/ext-language_tools";
77
import "ace-builds/src-noconflict/ext-searchbox";
88
import "ace-builds/src-noconflict/ext-inline_autocomplete";
99
import "ace-builds/src-noconflict/keybinding-vim";
10+
import "ace-builds/src-min-noconflict/ext-searchbox";
11+
import "ace-builds/src-min-noconflict/ext-language_tools";
12+
import PeerprepDropdown from "@/components/shared/PeerprepDropdown";
13+
14+
import { Question } from "@/api/structs";
1015

1116
const languages = [
1217
"javascript",
@@ -27,92 +32,73 @@ const themes = [
2732
"textmate",
2833
"solarized_dark",
2934
"solarized_light",
30-
"terminal"
35+
"terminal",
3136
];
3237

33-
languages.forEach(lang => {
38+
languages.forEach((lang) => {
3439
require(`ace-builds/src-noconflict/mode-${lang}`);
3540
require(`ace-builds/src-noconflict/snippets/${lang}`);
3641
});
3742

38-
themes.forEach(theme => require(`ace-builds/src-noconflict/theme-${theme}`));
39-
40-
import "ace-builds/src-min-noconflict/ext-searchbox";
41-
import "ace-builds/src-min-noconflict/ext-language_tools";
42-
43-
44-
import {Question} from "@/api/structs";
43+
themes.forEach((theme) => require(`ace-builds/src-noconflict/theme-${theme}`));
4544

4645
interface Props {
4746
question: Question;
4847
}
4948

50-
51-
export default function CollabEditor({question}: Props) {
52-
const [theme, setTheme] = useState("twilight")
53-
const [fontSize, setFontSize] = useState(16)
54-
const [language, setLanguage] = useState("python")
55-
49+
export default function CollabEditor({ question }: Props) {
50+
const [theme, setTheme] = useState("twilight");
51+
const [fontSize, setFontSize] = useState(16);
52+
const [language, setLanguage] = useState("python");
5653

5754
const handleOnChange = (newValue: string) => {
5855
console.log("Content changed:", newValue);
5956
};
6057

6158
const handleOnLoad = (editor: any) => {
62-
editor.container.style.resize = "both"
63-
}
59+
editor.container.style.resize = "both";
60+
};
6461

6562
// TODO: to be taken from question props instead
6663
// const value = question[language] ?? "// Comment"
6764
const value = `def foo:
68-
pass`
69-
70-
71-
return <>
72-
<div className="flex space-x-4 items-center p-4">
73-
<div className="flex flex-col">
74-
<label className="font-semibold mb-1">Font Size</label>
75-
<input
65+
pass`;
66+
67+
return (
68+
<>
69+
<div className="flex space-x-4 items-center p-4">
70+
<div className="flex flex-col">
71+
<label className="font-semibold mb-1">Font Size</label>
72+
<input
7673
type="number"
7774
className="border border-gray-600 bg-gray-800 text-white p-2 rounded w-20"
7875
value={fontSize}
7976
onChange={(e) => setFontSize(Number(e.target.value))}
77+
/>
78+
</div>
79+
80+
<PeerprepDropdown
81+
label={"Theme"}
82+
value={theme}
83+
onChange={(e) => setTheme(e.target.value)}
84+
options={themes}
85+
className={
86+
"border border-gray-600 bg-gray-800 text-white p-2 rounded"
87+
}
8088
/>
81-
</div>
82-
83-
<div className="flex flex-col">
84-
<label className="font-semibold mb-1">Theme</label>
85-
<select
86-
className="border border-gray-600 bg-gray-800 text-white p-2 rounded"
87-
value={theme}
88-
onChange={(e) => setTheme(e.target.value)}
89-
>
90-
{themes.map((theme) => (
91-
<option key={theme} value={theme}>
92-
{theme}
93-
</option>
94-
))}
95-
</select>
96-
</div>
9789

98-
<div className="flex flex-col">
99-
<label className="font-semibold mb-1">Language</label>
100-
<select
101-
className="border border-gray-600 bg-gray-800 text-white p-2 rounded"
102-
value={language}
103-
onChange={(e) => setLanguage(e.target.value)}
104-
>
105-
{languages.map((lang) => (
106-
<option key={lang} value={lang}>
107-
{lang}
108-
</option>
109-
))}
110-
</select>
90+
<PeerprepDropdown
91+
label={"Language"}
92+
value={language}
93+
onChange={(e) => setLanguage(e.target.value)}
94+
options={languages}
95+
className={
96+
"border border-gray-600 bg-gray-800 text-white p-2 rounded"
97+
}
98+
/>
11199
</div>
112-
</div>
113-
114100

115-
<AceEditor
101+
<AceEditor
116102
mode={language}
117103
className={"editor"}
118104
width={"90%"}
@@ -133,7 +119,8 @@ export default function CollabEditor({question}: Props) {
133119
enableSnippets: false,
134120
showLineNumbers: true,
135121
tabSize: 4,
136-
}}/>
137-
</>
138-
139-
}
122+
}}
123+
/>
124+
</>
125+
);
126+
}

peerprep/components/questionpage/QuestionCard.tsx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
"use client";
22
import React from "react";
3-
import { Question, Difficulty } from "@/api/structs";
3+
import { Difficulty, Question } from "@/api/structs";
44
import PeerprepButton from "../shared/PeerprepButton";
55
import { useRouter } from "next/navigation";
66
import styles from "@/style/questionCard.module.css";
77
import { deleteQuestion } from "@/app/api/internal/questions/helper";
8+
import DOMPurify from "dompurify";
89

910
type QuestionCardProps = {
1011
question: Question;
@@ -15,7 +16,7 @@ const QuestionCard: React.FC<QuestionCardProps> = ({ question }) => {
1516
const handleDelete = async () => {
1617
if (
1718
confirm(
18-
`Are you sure you want to delete ${question.title}? (ID: ${question.id}) `
19+
`Are you sure you want to delete ${question.title}? (ID: ${question.id}) `,
1920
)
2021
) {
2122
const status = await deleteQuestion(question.id);
@@ -43,6 +44,12 @@ const QuestionCard: React.FC<QuestionCardProps> = ({ question }) => {
4344
}
4445
};
4546

47+
const questionContent = DOMPurify.sanitize(question.content);
48+
49+
const match = questionContent.match(/(.*?)(?=<\/p>)/);
50+
const questionContentSubstring =
51+
(match ? match[0] : "No description found") + "...";
52+
4653
return (
4754
<div className={styles.container}>
4855
<div className="flex-none w-full sm:w-1/3">
@@ -51,7 +58,7 @@ const QuestionCard: React.FC<QuestionCardProps> = ({ question }) => {
5158
Difficulty:{" "}
5259
<span
5360
className={`capitalize font-bold ${getDifficultyColor(
54-
question.difficulty
61+
question.difficulty,
5562
)}`}
5663
>
5764
{Difficulty[question.difficulty]}
@@ -65,8 +72,13 @@ const QuestionCard: React.FC<QuestionCardProps> = ({ question }) => {
6572
</p>
6673
</div>
6774

68-
<div className="flex-none w-full sm:w-1/2 max-h-16">
69-
<p className={styles.bodytext}>{question.content}</p>
75+
<div className="flex-none w-full sm:w-1/2 max-h-16 overflow-hidden">
76+
{
77+
<div
78+
className={styles.bodytext}
79+
dangerouslySetInnerHTML={{ __html: questionContentSubstring }}
80+
/>
81+
}
7082
</div>
7183

7284
<div className={styles.buttonContainer}>

peerprep/components/questionpage/QuestionList.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const QuestionList: React.FC = () => {
99
const [questions, setQuestions] = useState<Question[]>([]);
1010
const [loading, setLoading] = useState(true);
1111
const [difficultyFilter, setDifficultyFilter] = useState<string>(
12-
Difficulty[0]
12+
Difficulty.All
1313
);
1414
const [topicFilter, setTopicFilter] = useState<string>("all");
1515
const [searchFilter, setSearchFilter] = useState<string>("");
@@ -42,7 +42,7 @@ const QuestionList: React.FC = () => {
4242

4343
const filteredQuestions = questions.filter((question) => {
4444
const matchesDifficulty =
45-
difficultyFilter === Difficulty[0] ||
45+
difficultyFilter === Difficulty.All ||
4646
Difficulty[question.difficulty] === difficultyFilter;
4747
const matchesTopic =
4848
topicFilter === topics[0] ||

0 commit comments

Comments
 (0)