-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4 from guanquann/admin-question-view
Add question page
- Loading branch information
Showing
13 changed files
with
2,636 additions
and
3,249 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import { Box, ImageListItem, IconButton } from "@mui/material"; | ||
import ContentCopyIcon from "@mui/icons-material/ContentCopy"; | ||
import FullscreenIcon from "@mui/icons-material/Fullscreen"; | ||
|
||
import { toast } from "react-toastify"; | ||
import "react-toastify/dist/ReactToastify.css"; | ||
|
||
interface QuestionImageProps { | ||
url: string; | ||
handleClickOpen: (url: string) => void; | ||
} | ||
|
||
const QuestionImage: React.FC<QuestionImageProps> = ({ url, handleClickOpen }) => { | ||
return ( | ||
<ImageListItem | ||
sx={{ | ||
width: 128, | ||
height: 128, | ||
position: "relative", | ||
":hover .moreInfo": { | ||
opacity: 1, | ||
}, | ||
borderRadius: 1, | ||
overflow: "hidden", | ||
}} | ||
> | ||
<img | ||
src={url} | ||
loading="lazy" | ||
style={{ width: "100%", height: "100%", objectFit: "cover", borderRadius: 1 }} | ||
alt="question image" | ||
/> | ||
|
||
<Box | ||
className="moreInfo" | ||
top={0} | ||
left={0} | ||
width="100%" | ||
height="100%" | ||
position="absolute" | ||
borderRadius={1} | ||
display="flex" | ||
justifyContent="center" | ||
alignItems="center" | ||
sx={{ | ||
opacity: 0, | ||
backgroundColor: "#75757599", | ||
transition: "opacity 0.5s ease", | ||
}} | ||
> | ||
<IconButton | ||
onClick={() => { | ||
navigator.clipboard.writeText(`![image](${url})`); | ||
toast.success("Image URL copied to clipboard"); | ||
}} | ||
sx={{ color: "#fff" }} | ||
> | ||
<ContentCopyIcon /> | ||
</IconButton> | ||
|
||
<IconButton onClick={() => handleClickOpen(url)} sx={{ color: "#fff " }}> | ||
<FullscreenIcon /> | ||
</IconButton> | ||
</Box> | ||
</ImageListItem> | ||
); | ||
}; | ||
|
||
export default QuestionImage; |
109 changes: 109 additions & 0 deletions
109
frontend/src/components/QuestionImageContainer/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import { useState } from "react"; | ||
import { styled } from "@mui/material/styles"; | ||
import { Button, ImageList, ImageListItem } from "@mui/material"; | ||
import FileUploadIcon from "@mui/icons-material/FileUpload"; | ||
import "react-toastify/dist/ReactToastify.css"; | ||
|
||
import QuestionImage from "../QuestionImage"; | ||
import QuestionImageDialog from "../QuestionImageDialog"; | ||
|
||
const FileUploadInput = styled("input")({ | ||
height: 1, | ||
overflow: "hidden", | ||
position: "absolute", | ||
bottom: 0, | ||
left: 0, | ||
width: 1, | ||
}); | ||
|
||
interface QuestionImageContainerProps { | ||
handleImageUpload: (event: React.ChangeEvent<HTMLInputElement>) => void; | ||
uploadedImagesUrl: string[]; | ||
} | ||
|
||
const QuestionImageContainer: React.FC<QuestionImageContainerProps> = ({ | ||
handleImageUpload, | ||
uploadedImagesUrl, | ||
}) => { | ||
const [open, setOpen] = useState<boolean>(false); | ||
const [selectedValue, setSelectedValue] = useState<string>(""); | ||
|
||
const handleClickOpen = (url: string) => { | ||
setOpen(true); | ||
setSelectedValue(url); | ||
}; | ||
|
||
const handleClose = () => { | ||
setOpen(false); | ||
}; | ||
|
||
if (uploadedImagesUrl.length === 0) { | ||
return ( | ||
<Button | ||
component="label" | ||
variant="contained" | ||
disableElevation={true} | ||
sx={(theme) => ({ | ||
borderRadius: 1, | ||
height: 128, | ||
width: "100%", | ||
backgroundColor: "#fff", | ||
color: "#757575", | ||
border: "1px solid", | ||
borderColor: theme.palette.grey[400], | ||
marginTop: 2, | ||
})} | ||
> | ||
<FileUploadIcon /> | ||
Click to upload images. The maximum image size accepted is 5MB. | ||
<FileUploadInput | ||
type="file" | ||
accept="image/png,image/jpeg" | ||
onChange={(event) => handleImageUpload(event)} | ||
multiple | ||
/> | ||
</Button> | ||
); | ||
} | ||
|
||
return ( | ||
<> | ||
<ImageList cols={7} rowHeight={128} sx={{ paddingTop: 2 }}> | ||
{uploadedImagesUrl.map((image) => ( | ||
<QuestionImage key={image} url={image} handleClickOpen={handleClickOpen} /> | ||
))} | ||
|
||
<ImageListItem sx={{ width: 128, height: 128 }}> | ||
<Button | ||
component="label" | ||
variant="contained" | ||
disableElevation={true} | ||
sx={(theme) => ({ | ||
borderRadius: 1, | ||
height: 128, | ||
width: 128, | ||
backgroundColor: "#fff", | ||
color: "#757575", | ||
border: "1px solid", | ||
borderColor: theme.palette.grey[400], | ||
textAlign: "center", | ||
})} | ||
> | ||
<FileUploadIcon /> | ||
Upload images | ||
<FileUploadInput | ||
type="file" | ||
accept="image/png,image/jpeg" | ||
onChange={(event) => handleImageUpload(event)} | ||
multiple | ||
/> | ||
</Button> | ||
</ImageListItem> | ||
</ImageList> | ||
|
||
<QuestionImageDialog value={selectedValue} open={open} handleClose={handleClose} /> | ||
</> | ||
); | ||
}; | ||
|
||
export default QuestionImageContainer; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { Dialog, DialogContent, IconButton } from "@mui/material"; | ||
import CloseIcon from "@mui/icons-material/Close"; | ||
|
||
interface QuestionImageDialog { | ||
value: string; | ||
open: boolean; | ||
handleClose: () => void; | ||
} | ||
|
||
const QuestionImageDialog: React.FC<QuestionImageDialog> = ({ value, open, handleClose }) => { | ||
return ( | ||
<Dialog onClose={handleClose} open={open}> | ||
<DialogContent> | ||
<IconButton | ||
aria-label="close" | ||
onClick={handleClose} | ||
sx={(theme) => ({ | ||
position: "absolute", | ||
right: 8, | ||
top: 8, | ||
color: theme.palette.grey[500], | ||
})} | ||
> | ||
<CloseIcon /> | ||
</IconButton> | ||
<img src={value} loading="lazy" alt="question image enlarged" /> | ||
</DialogContent> | ||
</Dialog> | ||
); | ||
}; | ||
|
||
export default QuestionImageDialog; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
.w-md-editor-text-pre > code, | ||
.w-md-editor-text-input, | ||
.wmde-markdown { | ||
font-size: 16px !important; | ||
line-height: 24px !important; | ||
font-family: "Roboto", "Helvetica", "Arial", sans-serif; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import MDEditor, { commands } from "@uiw/react-md-editor"; | ||
import { Stack } from "@mui/material"; | ||
import "./index.css"; | ||
|
||
interface QuestionMarkdownProps { | ||
markdownText: string; | ||
setMarkdownText: (value: string) => void; | ||
} | ||
|
||
const QuestionMarkdown: React.FC<QuestionMarkdownProps> = ({ markdownText, setMarkdownText }) => { | ||
return ( | ||
<Stack data-color-mode="light" paddingTop={2}> | ||
<MDEditor | ||
textareaProps={{ | ||
placeholder: "Description", | ||
}} | ||
value={markdownText} | ||
onChange={(value) => setMarkdownText(value || "")} | ||
preview="edit" | ||
commands={[ | ||
commands.bold, | ||
commands.italic, | ||
commands.strikethrough, | ||
commands.title, | ||
commands.link, | ||
commands.quote, | ||
commands.codeBlock, | ||
commands.image, | ||
commands.table, | ||
commands.orderedListCommand, | ||
commands.unorderedListCommand, | ||
]} | ||
extraCommands={[ | ||
commands.codeEdit, | ||
commands.codeLive, | ||
commands.codePreview, | ||
commands.divider, | ||
commands.help, | ||
]} | ||
visibleDragbar={false} | ||
height={300} | ||
minHeight={270} | ||
/> | ||
</Stack> | ||
); | ||
}; | ||
|
||
export default QuestionMarkdown; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
// technically this should be in the backend... | ||
|
||
import { initializeApp } from "firebase/app"; | ||
import { getStorage } from "firebase/storage"; | ||
|
||
const firebaseConfig = { | ||
apiKey: "apiKey", | ||
authDomain: "authDomain", | ||
projectId: "projectId", | ||
storageBucket: "storageBucket", | ||
messagingSenderId: "messagingSenderId", | ||
appId: "appId", | ||
measurementId: "measurementId", | ||
}; | ||
|
||
const app = initializeApp(firebaseConfig); | ||
|
||
const storage = getStorage(app); | ||
|
||
export { storage }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.