Skip to content

Commit 13324f8

Browse files
committed
Merge branch 'development' into feat/questions
2 parents 9c0dd83 + ba2a021 commit 13324f8

File tree

10 files changed

+393
-305
lines changed

10 files changed

+393
-305
lines changed

backend/user-service/app.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import fs from "fs";
55
import yaml from "yaml";
66
import swaggerUi from "swagger-ui-express";
77

8-
import userRoutes from "./routes/user-routes.js";
9-
import authRoutes from "./routes/auth-routes.js";
8+
import userRoutes from "./routes/user-routes";
9+
import authRoutes from "./routes/auth-routes";
1010

1111
dotenv.config();
1212

backend/user-service/controller/auth-controller.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
import { Response } from "express";
22
import bcrypt from "bcrypt";
33
import jwt from "jsonwebtoken";
4-
import { findUserByEmail as _findUserByEmail } from "../model/repository.js";
5-
import { formatUserResponse } from "./user-controller.js";
6-
import { AuthenticatedRequest } from "../types/request.js";
4+
import { findUserByEmail as _findUserByEmail } from "../model/repository";
5+
import { formatUserResponse } from "./user-controller";
6+
import { AuthenticatedRequest } from "../types/request";
77

8-
export async function handleLogin(
9-
req: AuthenticatedRequest,
10-
res: Response
11-
): Promise<Response> {
8+
export async function handleLogin(req: AuthenticatedRequest, res: Response): Promise<Response> {
129
const { email, password } = req.body;
1310
if (email && password) {
1411
try {
@@ -49,9 +46,7 @@ export async function handleVerifyToken(
4946
): Promise<Response> {
5047
try {
5148
const verifiedUser = req.user;
52-
return res
53-
.status(200)
54-
.json({ message: "Token verified", data: verifiedUser });
49+
return res.status(200).json({ message: "Token verified", data: verifiedUser });
5550
} catch (err) {
5651
return res.status(500).json({ message: "Server error", err });
5752
}

backend/user-service/routes/user-routes.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ import {
77
getUser,
88
updateUser,
99
updateUserPrivilege,
10-
} from "../controller/user-controller.js";
10+
} from "../controller/user-controller";
1111
import {
1212
verifyAccessToken,
1313
verifyIsAdmin,
1414
verifyIsOwnerOrAdmin,
15-
} from "../middleware/basic-access-control.js";
15+
} from "../middleware/basic-access-control";
1616

1717
const router = express.Router();
1818

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { Autocomplete, Chip, TextField } from "@mui/material";
2+
import { createFilterOptions } from "@mui/material/Autocomplete";
3+
import { categoryList } from "../../utils/constants";
4+
5+
interface QuestionCategoryAutoCompleteProps {
6+
selectedCategories?: string[];
7+
setSelectedCategories: (value: string[]) => void;
8+
}
9+
10+
const QuestionCategoryAutoComplete: React.FC<QuestionCategoryAutoCompleteProps> = ({
11+
selectedCategories,
12+
setSelectedCategories,
13+
}) => {
14+
// TODO
15+
// Fetch category list from the server
16+
17+
const filter = createFilterOptions<string>();
18+
19+
return (
20+
<Autocomplete
21+
multiple
22+
freeSolo
23+
options={categoryList}
24+
size="small"
25+
sx={{ marginTop: 2 }}
26+
value={selectedCategories}
27+
onChange={(e, newCategoriesSelected) => {
28+
const newValue = newCategoriesSelected[newCategoriesSelected.length - 1];
29+
if (typeof newValue === "string" && newValue.startsWith(`Add: "`)) {
30+
categoryList.push(newValue.slice(6, -1));
31+
setSelectedCategories([...newCategoriesSelected.slice(0, -1), newValue.slice(6, -1)]);
32+
} else {
33+
setSelectedCategories(newCategoriesSelected);
34+
}
35+
}}
36+
filterOptions={(options, params) => {
37+
const filtered = filter(options, params);
38+
39+
const { inputValue } = params;
40+
41+
const isExisting = options.some((option) => inputValue === option);
42+
43+
if (inputValue !== "" && !isExisting) {
44+
filtered.push(`Add: "${inputValue}"`);
45+
}
46+
47+
return filtered;
48+
}}
49+
renderTags={(value, getTagProps) =>
50+
value.map((option, index) => {
51+
const { key, ...tagProps } = getTagProps({ index });
52+
return (
53+
<Chip
54+
size="small"
55+
label={
56+
typeof option === "string" && option.startsWith(`Add: "`)
57+
? option.slice(6, -1)
58+
: option
59+
}
60+
key={key}
61+
{...tagProps}
62+
/>
63+
);
64+
})
65+
}
66+
renderInput={(params) => <TextField {...params} label="Category" />}
67+
/>
68+
);
69+
};
70+
71+
export default QuestionCategoryAutoComplete;
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { Box, Chip, List, ListItem, Stack, Typography, useTheme } from "@mui/material";
2+
import Markdown from "markdown-to-jsx";
3+
import { grey } from "@mui/material/colors";
4+
5+
interface QuestionDetailProps {
6+
title: string;
7+
complexity: string | null;
8+
categories: string[];
9+
description: string;
10+
}
11+
12+
const QuestionDetail: React.FC<QuestionDetailProps> = ({
13+
title,
14+
complexity,
15+
categories,
16+
description,
17+
}) => {
18+
const theme = useTheme();
19+
20+
return (
21+
<Box
22+
sx={(theme) => ({
23+
marginTop: theme.spacing(4),
24+
marginBottom: theme.spacing(4),
25+
})}
26+
>
27+
<Box
28+
sx={(theme) => ({
29+
marginTop: theme.spacing(4),
30+
marginBottom: theme.spacing(4),
31+
})}
32+
>
33+
<Typography component={"h1"} variant="h3">
34+
{title}
35+
</Typography>
36+
<Stack direction={"row"} sx={(theme) => ({ marginTop: theme.spacing(2) })}>
37+
{complexity && (
38+
<Chip
39+
key={complexity}
40+
label={complexity}
41+
color="primary"
42+
sx={(theme) => ({
43+
marginLeft: theme.spacing(1),
44+
marginRight: theme.spacing(1),
45+
})}
46+
/>
47+
)}
48+
{categories.map((cat) => (
49+
<Chip
50+
key={cat}
51+
label={cat}
52+
sx={(theme) => ({
53+
marginLeft: theme.spacing(1),
54+
marginRight: theme.spacing(1),
55+
})}
56+
/>
57+
))}
58+
</Stack>
59+
</Box>
60+
<Markdown
61+
options={{
62+
overrides: {
63+
h1: {
64+
component: Typography,
65+
props: { component: "h1", variant: "h4" },
66+
},
67+
h2: {
68+
component: Typography,
69+
props: { component: "h2", variant: "h5" },
70+
},
71+
h3: {
72+
component: Typography,
73+
props: { component: "h3", variant: "h6" },
74+
},
75+
p: {
76+
component: Typography,
77+
},
78+
ol: {
79+
component: List,
80+
props: {
81+
component: "ol",
82+
sx: {
83+
paddingLeft: theme.spacing(4),
84+
listStyleType: "decimal",
85+
},
86+
},
87+
},
88+
ul: {
89+
component: List,
90+
props: {
91+
component: "ul",
92+
sx: {
93+
paddingLeft: theme.spacing(4),
94+
listStyleType: "disc",
95+
},
96+
},
97+
},
98+
li: {
99+
component: ListItem,
100+
props: { sx: { display: "list-item" } },
101+
},
102+
code: {
103+
props: {
104+
style: {
105+
backgroundColor: grey[200],
106+
padding: "0.2em",
107+
borderRadius: "0.4em",
108+
},
109+
},
110+
},
111+
},
112+
}}
113+
>
114+
{description}
115+
</Markdown>
116+
</Box>
117+
);
118+
};
119+
120+
export default QuestionDetail;

frontend/src/components/QuestionImageContainer/index.tsx

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,18 @@ import { useState } from "react";
22
import { styled } from "@mui/material/styles";
33
import { Button, ImageList, ImageListItem } from "@mui/material";
44
import FileUploadIcon from "@mui/icons-material/FileUpload";
5-
import "react-toastify/dist/ReactToastify.css";
5+
import { toast } from "react-toastify";
6+
import axios from "axios";
67

8+
import { questionClient } from "../../utils/api";
79
import QuestionImage from "../QuestionImage";
810
import QuestionImageDialog from "../QuestionImageDialog";
911

12+
interface QuestionImageContainerProps {
13+
uploadedImagesUrl: string[];
14+
setUploadedImagesUrl: React.Dispatch<React.SetStateAction<string[]>>;
15+
}
16+
1017
const FileUploadInput = styled("input")({
1118
height: 1,
1219
overflow: "hidden",
@@ -16,14 +23,9 @@ const FileUploadInput = styled("input")({
1623
width: 1,
1724
});
1825

19-
interface QuestionImageContainerProps {
20-
handleImageUpload: (event: React.ChangeEvent<HTMLInputElement>) => void;
21-
uploadedImagesUrl: string[];
22-
}
23-
2426
const QuestionImageContainer: React.FC<QuestionImageContainerProps> = ({
25-
handleImageUpload,
2627
uploadedImagesUrl,
28+
setUploadedImagesUrl,
2729
}) => {
2830
const [open, setOpen] = useState<boolean>(false);
2931
const [selectedValue, setSelectedValue] = useState<string>("");
@@ -37,6 +39,49 @@ const QuestionImageContainer: React.FC<QuestionImageContainerProps> = ({
3739
setOpen(false);
3840
};
3941

42+
const handleImageUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
43+
if (!event.target.files) {
44+
return;
45+
}
46+
47+
const formData = new FormData();
48+
for (const file of event.target.files) {
49+
if (!file.type.startsWith("image/")) {
50+
toast.error(`${file.name} is not an image`);
51+
continue;
52+
}
53+
54+
if (file.size > 5 * 1024 * 1024) {
55+
toast.error(`${file.name} is more than 5MB`);
56+
continue;
57+
}
58+
formData.append("images[]", file);
59+
}
60+
61+
try {
62+
const response = await questionClient.post("/images", formData, {
63+
headers: {
64+
"Content-Type": "multipart/form-data",
65+
},
66+
withCredentials: false,
67+
});
68+
69+
const data = response.data;
70+
for (const imageUrl of data.imageUrls) {
71+
setUploadedImagesUrl((prev) => [...prev, imageUrl]);
72+
}
73+
74+
toast.success("File uploaded successfully");
75+
} catch (error) {
76+
if (axios.isAxiosError(error)) {
77+
toast.error(error.response?.data.message || "Error uploading file");
78+
} else {
79+
console.error(error);
80+
toast.error("Error uploading file");
81+
}
82+
}
83+
};
84+
4085
if (uploadedImagesUrl.length === 0) {
4186
return (
4287
<Button

frontend/src/components/QuestionImageDialog/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const QuestionImageDialog: React.FC<QuestionImageDialog> = ({ value, open, handl
2323
>
2424
<CloseIcon />
2525
</IconButton>
26-
<img src={value} loading="lazy" alt="question image enlarged" />
26+
<img src={value} loading="lazy" alt="question image enlarged" style={{ width: "550px" }} />
2727
</DialogContent>
2828
</Dialog>
2929
);

0 commit comments

Comments
 (0)