diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f1153a75..fca8a7fe 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,8 +14,9 @@ "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", "@fontsource/roboto": "^5.0.8", - "@mui/icons-material": "^5.15.4", + "@mui/icons-material": "^5.15.5", "@mui/material": "^5.15.3", + "@mui/x-date-pickers": "^6.19.0", "@react-hook/window-size": "^3.1.1", "@reduxjs/toolkit": "^2.0.1", "@swc/helpers": "^0.5.3", @@ -34,6 +35,8 @@ "randomcolor": "^0.6.2", "react": "^17.0.0 || ^18.0.0", "react-dom": "^17.0.0 || ^18.0.0", + "react-hook-form": "^7.49.3", + "react-hook-form-mui": "^7.0.0-beta.0", "react-infinite-scroller": "^1.2.6", "react-redux": "^9.0.4", "react-resizable-layout": "^0.7.2", @@ -1410,11 +1413,11 @@ } }, "node_modules/@mui/icons-material": { - "version": "5.15.4", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.4.tgz", - "integrity": "sha512-q/Yk7aokN8qGMpR7bwoDpBSeaNe6Bv7vaY9yHYodP37c64TM6ime05ueb/wgksOVszrKkNXC67E/XYbRWOoUFA==", + "version": "5.15.5", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.5.tgz", + "integrity": "sha512-qiql0fd1JY7TZ1wm1RldvU7sL8QUatE9OC12i/qm5rnm/caTFyAfOyTIR7qqxorsJvoZGyrzwoMkal6Ij9kM0A==", "dependencies": { - "@babel/runtime": "^7.23.7" + "@babel/runtime": "^7.23.8" }, "engines": { "node": ">=12.0.0" @@ -1614,6 +1617,71 @@ } } }, + "node_modules/@mui/x-date-pickers": { + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-6.19.0.tgz", + "integrity": "sha512-/GccT+iFJTKjI6b9b0MWojyRKnizL/VYYAfPnR1q0wSVVXjYv7a1NK0uQlan4JbnovqoQCNVeTOCy/0bUJyD2Q==", + "dependencies": { + "@babel/runtime": "^7.23.2", + "@mui/base": "^5.0.0-beta.22", + "@mui/utils": "^5.14.16", + "@types/react-transition-group": "^4.4.8", + "clsx": "^2.0.0", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.8.6", + "@mui/system": "^5.8.0", + "date-fns": "^2.25.0 || ^3.2.0", + "date-fns-jalali": "^2.13.0-0", + "dayjs": "^1.10.7", + "luxon": "^3.0.2", + "moment": "^2.29.4", + "moment-hijri": "^2.1.2", + "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "date-fns": { + "optional": true + }, + "date-fns-jalali": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + }, + "moment-hijri": { + "optional": true + }, + "moment-jalaali": { + "optional": true + } + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -5582,6 +5650,45 @@ "react": "^18.2.0" } }, + "node_modules/react-hook-form": { + "version": "7.49.3", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.49.3.tgz", + "integrity": "sha512-foD6r3juidAT1cOZzpmD/gOKt7fRsDhXXZ0y28+Al1CHgX+AY1qIN9VSIIItXRq1dN68QrRwl1ORFlwjBaAqeQ==", + "engines": { + "node": ">=18", + "pnpm": "8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, + "node_modules/react-hook-form-mui": { + "version": "7.0.0-beta.0", + "resolved": "https://registry.npmjs.org/react-hook-form-mui/-/react-hook-form-mui-7.0.0-beta.0.tgz", + "integrity": "sha512-pj+GNsrWtmhS1BhXJdEqSxPcsuZqI2xJ/N/yPMVKT0iVwKPrIzl9MkC564Xtgti3Ucd4fByEmVkUacS4SWHxSA==", + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@mui/icons-material": ">= 5.x <6", + "@mui/material": ">= 5.x <6", + "@mui/x-date-pickers": ">=6.1.0 <7", + "react": ">=17 <19", + "react-hook-form": ">=7.33.1" + }, + "peerDependenciesMeta": { + "@mui/icons-material": { + "optional": true + }, + "@mui/x-date-pickers": { + "optional": true + } + } + }, "node_modules/react-infinite-scroller": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/react-infinite-scroller/-/react-infinite-scroller-1.2.6.tgz", diff --git a/frontend/package.json b/frontend/package.json index 91e03aac..2607685b 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,8 +18,9 @@ "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", "@fontsource/roboto": "^5.0.8", - "@mui/icons-material": "^5.15.4", + "@mui/icons-material": "^5.15.5", "@mui/material": "^5.15.3", + "@mui/x-date-pickers": "^6.19.0", "@react-hook/window-size": "^3.1.1", "@reduxjs/toolkit": "^2.0.1", "@swc/helpers": "^0.5.3", @@ -38,6 +39,8 @@ "randomcolor": "^0.6.2", "react": "^17.0.0 || ^18.0.0", "react-dom": "^17.0.0 || ^18.0.0", + "react-hook-form": "^7.49.3", + "react-hook-form-mui": "^7.0.0-beta.0", "react-infinite-scroller": "^1.2.6", "react-redux": "^9.0.4", "react-resizable-layout": "^0.7.2", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1ea930ba..feb453f5 100755 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -22,6 +22,11 @@ function App() { const defaultMode = prefersDarkMode ? "dark" : "light"; return createTheme({ + typography: { + button: { + textTransform: "none", + }, + }, palette: { mode: config.theme == "default" ? defaultMode : config.theme, }, diff --git a/frontend/src/components/drawers/WorkspaceDrawer.tsx b/frontend/src/components/drawers/WorkspaceDrawer.tsx index f99002e9..d97576ef 100644 --- a/frontend/src/components/drawers/WorkspaceDrawer.tsx +++ b/frontend/src/components/drawers/WorkspaceDrawer.tsx @@ -1,6 +1,7 @@ import { Avatar, Box, + Button, Divider, Drawer, IconButton, @@ -20,6 +21,9 @@ import { useGetWorkspaceQuery } from "../../hooks/api/workspace"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; import WorkspaceListPopover from "../popovers/WorkspaceListPopover"; +import AddIcon from "@mui/icons-material/Add"; +import CreateModal from "../modals/CreateModal"; +import { useCreateDocumentMutation } from "../../hooks/api/workspaceDocument"; const DRAWER_WIDTH = 240; @@ -27,10 +31,12 @@ function WorkspaceDrawer() { const params = useParams(); const userStore = useSelector(selectUser); const { data: workspace } = useGetWorkspaceQuery(params.workspaceSlug); + const { mutateAsync: createDocument } = useCreateDocumentMutation(workspace?.id || ""); const [profileAnchorEl, setProfileAnchorEl] = useState<(EventTarget & Element) | null>(null); const [workspaceListAnchorEl, setWorkspaceListAnchorEl] = useState< (EventTarget & Element) | null >(null); + const [createWorkspaceModalOpen, setCreateWorkspaceModalOpen] = useState(false); const handleOpenProfilePopover: MouseEventHandler = (event) => { setProfileAnchorEl(event.currentTarget); @@ -48,6 +54,14 @@ function WorkspaceDrawer() { setWorkspaceListAnchorEl(null); }; + const handleCreateWorkspace = async (data: { title: string }) => { + await createDocument(data); + }; + + const handleCreateWorkspaceModalOpen = () => { + setCreateWorkspaceModalOpen((prev) => !prev); + }; + return ( + + + + @@ -108,6 +135,12 @@ function WorkspaceDrawer() { /> + ); } diff --git a/frontend/src/components/modals/CreateModal.tsx b/frontend/src/components/modals/CreateModal.tsx new file mode 100644 index 00000000..42b12a74 --- /dev/null +++ b/frontend/src/components/modals/CreateModal.tsx @@ -0,0 +1,60 @@ +import { Button, FormControl, Modal, ModalProps, Paper, Stack, Typography } from "@mui/material"; +import { FormContainer, TextFieldElement } from "react-hook-form-mui"; + +interface CreateRequest { + title: string; +} + +interface CreateModalProps extends Omit { + title: string; + onSuccess: (data: CreateRequest) => Promise; +} + +function CreateModal(props: CreateModalProps) { + const { title, onSuccess, ...modalProps } = props; + + const handleCreate = async (data: CreateRequest) => { + await onSuccess(data); + modalProps?.onClose?.(new Event("Close Modal"), "escapeKeyDown"); + }; + + return ( + + + + Create New {title} + + + + + + + + + + + + ); +} + +export default CreateModal; diff --git a/frontend/src/hooks/api/types/document.d.ts b/frontend/src/hooks/api/types/document.d.ts index 5ae36e45..7bc536e1 100644 --- a/frontend/src/hooks/api/types/document.d.ts +++ b/frontend/src/hooks/api/types/document.d.ts @@ -7,8 +7,3 @@ export class Document { createdAt: Date; updatedAt: Date; } - -export class GetWorkspaceDocumentListResponse { - cursor: string | null; - documents: Array; -} diff --git a/frontend/src/hooks/api/types/workspace.d.ts b/frontend/src/hooks/api/types/workspace.d.ts index bc7af04b..19497919 100644 --- a/frontend/src/hooks/api/types/workspace.d.ts +++ b/frontend/src/hooks/api/types/workspace.d.ts @@ -12,3 +12,9 @@ export class GetWorkspaceListResponse { cursor: string | null; workspaces: Array; } + +export class CreateWorkspaceRequest { + title: string; +} + +export class CreateWorkspaceResponse extends Workspace {} diff --git a/frontend/src/hooks/api/types/workspaceDocument.d.ts b/frontend/src/hooks/api/types/workspaceDocument.d.ts new file mode 100644 index 00000000..ee35a854 --- /dev/null +++ b/frontend/src/hooks/api/types/workspaceDocument.d.ts @@ -0,0 +1,12 @@ +import { Document } from "./document"; + +export class GetWorkspaceDocumentListResponse { + cursor: string | null; + documents: Array; +} + +export class CreateDocumentRequest { + title: string; +} + +export class CreateDocumentResponse extends Document {} diff --git a/frontend/src/hooks/api/workspace.ts b/frontend/src/hooks/api/workspace.ts index f5264303..b34ebe78 100644 --- a/frontend/src/hooks/api/workspace.ts +++ b/frontend/src/hooks/api/workspace.ts @@ -1,6 +1,11 @@ -import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; +import { useInfiniteQuery, useMutation, useQuery } from "@tanstack/react-query"; import axios from "axios"; -import { GetWorkspaceListResponse, GetWorkspaceResponse } from "./types/workspace"; +import { + CreateWorkspaceRequest, + CreateWorkspaceResponse, + GetWorkspaceListResponse, + GetWorkspaceResponse, +} from "./types/workspace"; export const generateGetWorkspaceQueryKey = (workspaceId: string) => { return ["workspaces", workspaceId]; @@ -44,3 +49,13 @@ export const useGetWorkspaceListQuery = () => { return query; }; + +export const useCreateWorkspaceMutation = () => { + return useMutation({ + mutationFn: async (data: CreateWorkspaceRequest) => { + const res = await axios.post("/workspaces", data); + + return res.data; + }, + }); +}; diff --git a/frontend/src/hooks/api/workspaceDocument.ts b/frontend/src/hooks/api/workspaceDocument.ts index f97d533d..1e637b88 100644 --- a/frontend/src/hooks/api/workspaceDocument.ts +++ b/frontend/src/hooks/api/workspaceDocument.ts @@ -1,6 +1,10 @@ -import { useInfiniteQuery } from "@tanstack/react-query"; +import { useInfiniteQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import axios from "axios"; -import { GetWorkspaceDocumentListResponse } from "./types/document"; +import { + CreateDocumentRequest, + CreateDocumentResponse, + GetWorkspaceDocumentListResponse, +} from "./types/workspaceDocument"; export const generateGetWorkspaceDocumentListQueryKey = (workspaceId: string) => { return ["workspaces", workspaceId, "documents"]; @@ -28,3 +32,23 @@ export const useGetWorkspaceDocumentListQuery = (workspaceId?: string) => { return query; }; + +export const useCreateDocumentMutation = (workspaceId: string) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (data: CreateDocumentRequest) => { + const res = await axios.post( + `/workspaces/${workspaceId}/documents`, + data + ); + + return res.data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: generateGetWorkspaceDocumentListQueryKey(workspaceId), + }); + }, + }); +}; diff --git a/frontend/src/pages/workspace/Index.tsx b/frontend/src/pages/workspace/Index.tsx index 6cbac467..e42a056a 100644 --- a/frontend/src/pages/workspace/Index.tsx +++ b/frontend/src/pages/workspace/Index.tsx @@ -22,7 +22,7 @@ function WorkspaceIndex() { return ( - +