Skip to content

Commit c8837fb

Browse files
committed
Create admin new question page
1 parent e6a4efe commit c8837fb

File tree

10 files changed

+2387
-2684
lines changed

10 files changed

+2387
-2684
lines changed

frontend/package-lock.json

Lines changed: 1881 additions & 2644 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 & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,22 @@
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",
19+
"firebase": "^10.13.1",
1720
"react": "^18.3.1",
18-
"react-dom": "^18.3.1"
21+
"react-dom": "^18.3.1",
22+
"react-router-dom": "^6.26.2",
23+
"react-toastify": "^10.0.5",
24+
"uuid": "^10.0.0"
1925
},
2026
"devDependencies": {
2127
"@eslint/js": "^9.9.0",
2228
"@types/react": "^18.3.3",
2329
"@types/react-dom": "^18.3.0",
30+
"@types/uuid": "^10.0.0",
2431
"@vitejs/plugin-react": "^4.3.1",
2532
"eslint": "^9.9.0",
2633
"eslint-plugin-react-hooks": "^5.1.0-rc.0",

frontend/src/App.tsx

Lines changed: 10 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,15 @@
1-
import { useState } from 'react'
2-
import reactLogo from './assets/react.svg'
3-
import viteLogo from '/vite.svg'
4-
import './App.css'
1+
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
2+
import NewQuestion from "./pages/NewQuestion";
53

64
function App() {
7-
const [count, setCount] = useState(0)
8-
95
return (
10-
<>
11-
<div>
12-
<a href="https://vitejs.dev" target="_blank">
13-
<img src={viteLogo} className="logo" alt="Vite logo" />
14-
</a>
15-
<a href="https://react.dev" target="_blank">
16-
<img src={reactLogo} className="logo react" alt="React logo" />
17-
</a>
18-
</div>
19-
<h1>Vite + React</h1>
20-
<div className="card">
21-
<button onClick={() => setCount((count) => count + 1)}>
22-
count is {count}
23-
</button>
24-
<p>
25-
Edit <code>src/App.tsx</code> and save to test HMR
26-
</p>
27-
</div>
28-
<p className="read-the-docs">
29-
Click on the Vite and React logos to learn more
30-
</p>
31-
</>
32-
)
6+
<Router>
7+
<Routes>
8+
<Route path="/question" element={<>question page list</>} />
9+
<Route path="/question/new" element={<NewQuestion />} />
10+
</Routes>
11+
</Router>
12+
);
3313
}
3414

35-
export default App
15+
export default App;
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: 3,
24+
overflow: "hidden",
25+
}}
26+
>
27+
<img
28+
src={url}
29+
loading="lazy"
30+
style={{ width: "100%", height: "100%", objectFit: "cover", borderRadius: 3 }}
31+
alt="question image"
32+
/>
33+
34+
<Box
35+
className="moreInfo"
36+
sx={{
37+
top: 0,
38+
left: 0,
39+
width: "100%",
40+
height: "100%",
41+
position: "absolute",
42+
opacity: 0,
43+
borderRadius: 3,
44+
backgroundColor: "#75757599",
45+
display: "flex",
46+
justifyContent: "center",
47+
alignItems: "center",
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: "white" }}
57+
>
58+
<ContentCopyIcon />
59+
</IconButton>
60+
61+
<IconButton onClick={() => handleClickOpen(url)} sx={{ color: "white" }}>
62+
<FullscreenIcon />
63+
</IconButton>
64+
</Box>
65+
</ImageListItem>
66+
);
67+
};
68+
69+
export default QuestionImage;
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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+
sx={{
46+
borderRadius: 3,
47+
height: 128,
48+
width: "100%",
49+
backgroundColor: "rgba(0, 0, 0, 0.01)",
50+
color: "#757575",
51+
border: "1px grey",
52+
marginTop: 2,
53+
}}
54+
>
55+
<FileUploadIcon />
56+
Click to upload images. The maximum image size accepted is 5MB.
57+
<FileUploadInput
58+
type="file"
59+
accept="image/png,image/jpeg"
60+
onChange={(event) => handleImageUpload(event)}
61+
multiple
62+
/>
63+
</Button>
64+
);
65+
}
66+
67+
return (
68+
<>
69+
<ImageList cols={7} rowHeight={128} sx={{ paddingTop: 2 }}>
70+
{uploadedImagesUrl.map((image) => (
71+
<QuestionImage key={image} url={image} handleClickOpen={handleClickOpen} />
72+
))}
73+
74+
<ImageListItem sx={{ width: 128, height: 128 }}>
75+
<Button
76+
component="label"
77+
variant="contained"
78+
sx={{
79+
borderRadius: 3,
80+
height: 128,
81+
width: 128,
82+
backgroundColor: "rgba(0, 0, 0, 0.01)",
83+
color: "#757575",
84+
border: "1px grey",
85+
textAlign: "center",
86+
}}
87+
>
88+
<FileUploadIcon />
89+
Upload images
90+
<FileUploadInput
91+
type="file"
92+
accept="image/png,image/jpeg"
93+
onChange={(event) => handleImageUpload(event)}
94+
multiple
95+
/>
96+
</Button>
97+
</ImageListItem>
98+
</ImageList>
99+
100+
<QuestionImageDialog value={selectedValue} open={open} handleClose={handleClose} />
101+
</>
102+
);
103+
};
104+
105+
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: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import MDEditor, { commands } from "@uiw/react-md-editor";
2+
import { Stack } from "@mui/material";
3+
4+
interface QuestionMarkdownProps {
5+
markdownText: string;
6+
setMarkdownText: (value: string) => void;
7+
}
8+
9+
const QuestionMarkdown: React.FC<QuestionMarkdownProps> = ({ markdownText, setMarkdownText }) => {
10+
return (
11+
<Stack data-color-mode="light" paddingTop={2}>
12+
<MDEditor
13+
value={markdownText}
14+
onChange={(value) => setMarkdownText(value || "")}
15+
preview="edit"
16+
commands={[
17+
commands.bold,
18+
commands.italic,
19+
commands.strikethrough,
20+
commands.title,
21+
commands.link,
22+
commands.quote,
23+
commands.codeBlock,
24+
commands.image,
25+
commands.table,
26+
commands.orderedListCommand,
27+
commands.unorderedListCommand,
28+
]}
29+
extraCommands={[
30+
commands.codeEdit,
31+
commands.codeLive,
32+
commands.codePreview,
33+
commands.divider,
34+
commands.help,
35+
]}
36+
visibleDragbar={false}
37+
height={300}
38+
minHeight={270}
39+
/>
40+
</Stack>
41+
);
42+
};
43+
44+
export default QuestionMarkdown;

frontend/src/firebase.ts

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

frontend/src/main.tsx

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,38 @@
1-
import { StrictMode } from 'react'
2-
import { createRoot } from 'react-dom/client'
3-
import App from './App.tsx'
4-
import './index.css'
1+
import { StrictMode } from "react";
2+
import { createRoot } from "react-dom/client";
3+
import App from "./App.tsx";
4+
import { createTheme, ThemeProvider } from "@mui/material/styles";
55

6-
createRoot(document.getElementById('root')!).render(
6+
const theme = createTheme({
7+
palette: {
8+
primary: {
9+
main: "#8FB8ED",
10+
contrastText: "#FFFFFF",
11+
},
12+
secondary: {
13+
main: "#ECECEC",
14+
contrastText: "#757575",
15+
},
16+
},
17+
components: {
18+
MuiButton: {
19+
styleOverrides: {
20+
root: {
21+
textTransform: "none",
22+
fontWeight: "bold",
23+
// ":hover": {
24+
// backgroundColor: "",
25+
// },
26+
},
27+
},
28+
},
29+
},
30+
});
31+
32+
createRoot(document.getElementById("root")!).render(
733
<StrictMode>
8-
<App />
9-
</StrictMode>,
10-
)
34+
<ThemeProvider theme={theme}>
35+
<App />
36+
</ThemeProvider>
37+
</StrictMode>
38+
);

0 commit comments

Comments
 (0)