Skip to content

Commit

Permalink
Merge pull request #4 from guanquann/admin-question-view
Browse files Browse the repository at this point in the history
Add question page
  • Loading branch information
guanquann authored Sep 20, 2024
2 parents 5788b54 + 55bb509 commit d4f7478
Show file tree
Hide file tree
Showing 13 changed files with 2,636 additions and 3,249 deletions.
5,380 changes: 2,138 additions & 3,242 deletions frontend/package-lock.json

Large diffs are not rendered by default.

12 changes: 9 additions & 3 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,24 @@
"dependencies": {
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@mdxeditor/editor": "^3.11.4",
"@fontsource/roboto": "^5.1.0",
"@mui/icons-material": "^6.1.0",
"@mui/material": "^6.1.0",
"@uiw/react-md-editor": "^4.0.4",
"axios": "^1.7.7",
"markdown-to-jsx": "^7.5.0",
"firebase": "^10.13.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.26.2"
"react-router-dom": "^6.26.2",
"react-toastify": "^10.0.5",
"markdown-to-jsx": "^7.5.0",
"uuid": "^10.0.0"
},
"devDependencies": {
"@eslint/js": "^9.9.0",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/uuid": "^10.0.0",
"@vitejs/plugin-react": "^4.3.1",
"eslint": "^9.9.0",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Layout from "./components/Layout";
import NewQuestion from "./pages/NewQuestion";
import QuestionDetail from "./pages/QuestionDetail";
import PageNotFound from "./pages/Error";

Expand All @@ -8,6 +9,8 @@ function App() {
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route path="/questions" element={<>question page list</>} />
<Route path="/questions/new" element={<NewQuestion />} />
<Route path="/questions/:questionId" element={<QuestionDetail />} />
<Route path="*" element={<PageNotFound />} />
</Route>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/Layout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const Layout: FunctionComponent = () => {
display: "flex",
flexDirection: "column",
minHeight: "100vh",
minInlineSize: "100vw",
// minInlineSize: "100vw",
}}
>
<Navbar />
Expand Down
69 changes: 69 additions & 0 deletions frontend/src/components/QuestionImage/index.tsx
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 frontend/src/components/QuestionImageContainer/index.tsx
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;
32 changes: 32 additions & 0 deletions frontend/src/components/QuestionImageDialog/index.tsx
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;
7 changes: 7 additions & 0 deletions frontend/src/components/QuestionMarkdown/index.css
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;
}
48 changes: 48 additions & 0 deletions frontend/src/components/QuestionMarkdown/index.tsx
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;
20 changes: 20 additions & 0 deletions frontend/src/firebase.ts
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 };
6 changes: 3 additions & 3 deletions frontend/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { ThemeProvider } from "@mui/material";
import { CssBaseline } from "@mui/material";
import theme from "./theme.ts";
import App from "./App.tsx";
import theme from "./theme";
import { ThemeProvider } from "@mui/material/styles";
import { CssBaseline } from "@mui/material";

createRoot(document.getElementById("root")!).render(
<StrictMode>
Expand Down
Loading

0 comments on commit d4f7478

Please sign in to comment.