Skip to content

Commit d4f7478

Browse files
authored
Merge pull request #4 from guanquann/admin-question-view
Add question page
2 parents 5788b54 + 55bb509 commit d4f7478

File tree

13 files changed

+2636
-3249
lines changed

13 files changed

+2636
-3249
lines changed

frontend/package-lock.json

Lines changed: 2138 additions & 3242 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,24 @@
1212
"dependencies": {
1313
"@emotion/react": "^11.13.3",
1414
"@emotion/styled": "^11.13.0",
15-
"@mdxeditor/editor": "^3.11.4",
15+
"@fontsource/roboto": "^5.1.0",
16+
"@mui/icons-material": "^6.1.0",
1617
"@mui/material": "^6.1.0",
18+
"@uiw/react-md-editor": "^4.0.4",
1719
"axios": "^1.7.7",
18-
"markdown-to-jsx": "^7.5.0",
20+
"firebase": "^10.13.1",
1921
"react": "^18.3.1",
2022
"react-dom": "^18.3.1",
21-
"react-router-dom": "^6.26.2"
23+
"react-router-dom": "^6.26.2",
24+
"react-toastify": "^10.0.5",
25+
"markdown-to-jsx": "^7.5.0",
26+
"uuid": "^10.0.0"
2227
},
2328
"devDependencies": {
2429
"@eslint/js": "^9.9.0",
2530
"@types/react": "^18.3.3",
2631
"@types/react-dom": "^18.3.0",
32+
"@types/uuid": "^10.0.0",
2733
"@vitejs/plugin-react": "^4.3.1",
2834
"eslint": "^9.9.0",
2935
"eslint-plugin-react-hooks": "^5.1.0-rc.0",

frontend/src/App.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { BrowserRouter, Routes, Route } from "react-router-dom";
22
import Layout from "./components/Layout";
3+
import NewQuestion from "./pages/NewQuestion";
34
import QuestionDetail from "./pages/QuestionDetail";
45
import PageNotFound from "./pages/Error";
56

@@ -8,6 +9,8 @@ function App() {
89
<BrowserRouter>
910
<Routes>
1011
<Route path="/" element={<Layout />}>
12+
<Route path="/questions" element={<>question page list</>} />
13+
<Route path="/questions/new" element={<NewQuestion />} />
1114
<Route path="/questions/:questionId" element={<QuestionDetail />} />
1215
<Route path="*" element={<PageNotFound />} />
1316
</Route>

frontend/src/components/Layout/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const Layout: FunctionComponent = () => {
1010
display: "flex",
1111
flexDirection: "column",
1212
minHeight: "100vh",
13-
minInlineSize: "100vw",
13+
// minInlineSize: "100vw",
1414
}}
1515
>
1616
<Navbar />
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { Box, ImageListItem, IconButton } from "@mui/material";
2+
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
3+
import FullscreenIcon from "@mui/icons-material/Fullscreen";
4+
5+
import { toast } from "react-toastify";
6+
import "react-toastify/dist/ReactToastify.css";
7+
8+
interface QuestionImageProps {
9+
url: string;
10+
handleClickOpen: (url: string) => void;
11+
}
12+
13+
const QuestionImage: React.FC<QuestionImageProps> = ({ url, handleClickOpen }) => {
14+
return (
15+
<ImageListItem
16+
sx={{
17+
width: 128,
18+
height: 128,
19+
position: "relative",
20+
":hover .moreInfo": {
21+
opacity: 1,
22+
},
23+
borderRadius: 1,
24+
overflow: "hidden",
25+
}}
26+
>
27+
<img
28+
src={url}
29+
loading="lazy"
30+
style={{ width: "100%", height: "100%", objectFit: "cover", borderRadius: 1 }}
31+
alt="question image"
32+
/>
33+
34+
<Box
35+
className="moreInfo"
36+
top={0}
37+
left={0}
38+
width="100%"
39+
height="100%"
40+
position="absolute"
41+
borderRadius={1}
42+
display="flex"
43+
justifyContent="center"
44+
alignItems="center"
45+
sx={{
46+
opacity: 0,
47+
backgroundColor: "#75757599",
48+
transition: "opacity 0.5s ease",
49+
}}
50+
>
51+
<IconButton
52+
onClick={() => {
53+
navigator.clipboard.writeText(`![image](${url})`);
54+
toast.success("Image URL copied to clipboard");
55+
}}
56+
sx={{ color: "#fff" }}
57+
>
58+
<ContentCopyIcon />
59+
</IconButton>
60+
61+
<IconButton onClick={() => handleClickOpen(url)} sx={{ color: "#fff " }}>
62+
<FullscreenIcon />
63+
</IconButton>
64+
</Box>
65+
</ImageListItem>
66+
);
67+
};
68+
69+
export default QuestionImage;
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { useState } from "react";
2+
import { styled } from "@mui/material/styles";
3+
import { Button, ImageList, ImageListItem } from "@mui/material";
4+
import FileUploadIcon from "@mui/icons-material/FileUpload";
5+
import "react-toastify/dist/ReactToastify.css";
6+
7+
import QuestionImage from "../QuestionImage";
8+
import QuestionImageDialog from "../QuestionImageDialog";
9+
10+
const FileUploadInput = styled("input")({
11+
height: 1,
12+
overflow: "hidden",
13+
position: "absolute",
14+
bottom: 0,
15+
left: 0,
16+
width: 1,
17+
});
18+
19+
interface QuestionImageContainerProps {
20+
handleImageUpload: (event: React.ChangeEvent<HTMLInputElement>) => void;
21+
uploadedImagesUrl: string[];
22+
}
23+
24+
const QuestionImageContainer: React.FC<QuestionImageContainerProps> = ({
25+
handleImageUpload,
26+
uploadedImagesUrl,
27+
}) => {
28+
const [open, setOpen] = useState<boolean>(false);
29+
const [selectedValue, setSelectedValue] = useState<string>("");
30+
31+
const handleClickOpen = (url: string) => {
32+
setOpen(true);
33+
setSelectedValue(url);
34+
};
35+
36+
const handleClose = () => {
37+
setOpen(false);
38+
};
39+
40+
if (uploadedImagesUrl.length === 0) {
41+
return (
42+
<Button
43+
component="label"
44+
variant="contained"
45+
disableElevation={true}
46+
sx={(theme) => ({
47+
borderRadius: 1,
48+
height: 128,
49+
width: "100%",
50+
backgroundColor: "#fff",
51+
color: "#757575",
52+
border: "1px solid",
53+
borderColor: theme.palette.grey[400],
54+
marginTop: 2,
55+
})}
56+
>
57+
<FileUploadIcon />
58+
Click to upload images. The maximum image size accepted is 5MB.
59+
<FileUploadInput
60+
type="file"
61+
accept="image/png,image/jpeg"
62+
onChange={(event) => handleImageUpload(event)}
63+
multiple
64+
/>
65+
</Button>
66+
);
67+
}
68+
69+
return (
70+
<>
71+
<ImageList cols={7} rowHeight={128} sx={{ paddingTop: 2 }}>
72+
{uploadedImagesUrl.map((image) => (
73+
<QuestionImage key={image} url={image} handleClickOpen={handleClickOpen} />
74+
))}
75+
76+
<ImageListItem sx={{ width: 128, height: 128 }}>
77+
<Button
78+
component="label"
79+
variant="contained"
80+
disableElevation={true}
81+
sx={(theme) => ({
82+
borderRadius: 1,
83+
height: 128,
84+
width: 128,
85+
backgroundColor: "#fff",
86+
color: "#757575",
87+
border: "1px solid",
88+
borderColor: theme.palette.grey[400],
89+
textAlign: "center",
90+
})}
91+
>
92+
<FileUploadIcon />
93+
Upload images
94+
<FileUploadInput
95+
type="file"
96+
accept="image/png,image/jpeg"
97+
onChange={(event) => handleImageUpload(event)}
98+
multiple
99+
/>
100+
</Button>
101+
</ImageListItem>
102+
</ImageList>
103+
104+
<QuestionImageDialog value={selectedValue} open={open} handleClose={handleClose} />
105+
</>
106+
);
107+
};
108+
109+
export default QuestionImageContainer;
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Dialog, DialogContent, IconButton } from "@mui/material";
2+
import CloseIcon from "@mui/icons-material/Close";
3+
4+
interface QuestionImageDialog {
5+
value: string;
6+
open: boolean;
7+
handleClose: () => void;
8+
}
9+
10+
const QuestionImageDialog: React.FC<QuestionImageDialog> = ({ value, open, handleClose }) => {
11+
return (
12+
<Dialog onClose={handleClose} open={open}>
13+
<DialogContent>
14+
<IconButton
15+
aria-label="close"
16+
onClick={handleClose}
17+
sx={(theme) => ({
18+
position: "absolute",
19+
right: 8,
20+
top: 8,
21+
color: theme.palette.grey[500],
22+
})}
23+
>
24+
<CloseIcon />
25+
</IconButton>
26+
<img src={value} loading="lazy" alt="question image enlarged" />
27+
</DialogContent>
28+
</Dialog>
29+
);
30+
};
31+
32+
export default QuestionImageDialog;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.w-md-editor-text-pre > code,
2+
.w-md-editor-text-input,
3+
.wmde-markdown {
4+
font-size: 16px !important;
5+
line-height: 24px !important;
6+
font-family: "Roboto", "Helvetica", "Arial", sans-serif;
7+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import MDEditor, { commands } from "@uiw/react-md-editor";
2+
import { Stack } from "@mui/material";
3+
import "./index.css";
4+
5+
interface QuestionMarkdownProps {
6+
markdownText: string;
7+
setMarkdownText: (value: string) => void;
8+
}
9+
10+
const QuestionMarkdown: React.FC<QuestionMarkdownProps> = ({ markdownText, setMarkdownText }) => {
11+
return (
12+
<Stack data-color-mode="light" paddingTop={2}>
13+
<MDEditor
14+
textareaProps={{
15+
placeholder: "Description",
16+
}}
17+
value={markdownText}
18+
onChange={(value) => setMarkdownText(value || "")}
19+
preview="edit"
20+
commands={[
21+
commands.bold,
22+
commands.italic,
23+
commands.strikethrough,
24+
commands.title,
25+
commands.link,
26+
commands.quote,
27+
commands.codeBlock,
28+
commands.image,
29+
commands.table,
30+
commands.orderedListCommand,
31+
commands.unorderedListCommand,
32+
]}
33+
extraCommands={[
34+
commands.codeEdit,
35+
commands.codeLive,
36+
commands.codePreview,
37+
commands.divider,
38+
commands.help,
39+
]}
40+
visibleDragbar={false}
41+
height={300}
42+
minHeight={270}
43+
/>
44+
</Stack>
45+
);
46+
};
47+
48+
export default QuestionMarkdown;

frontend/src/firebase.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// technically this should be in the backend...
2+
3+
import { initializeApp } from "firebase/app";
4+
import { getStorage } from "firebase/storage";
5+
6+
const firebaseConfig = {
7+
apiKey: "apiKey",
8+
authDomain: "authDomain",
9+
projectId: "projectId",
10+
storageBucket: "storageBucket",
11+
messagingSenderId: "messagingSenderId",
12+
appId: "appId",
13+
measurementId: "measurementId",
14+
};
15+
16+
const app = initializeApp(firebaseConfig);
17+
18+
const storage = getStorage(app);
19+
20+
export { storage };

0 commit comments

Comments
 (0)