From 62da5c9933bb8834ffc201aee6851fe34dae2c8d Mon Sep 17 00:00:00 2001 From: Sean Hatfield <seanhatfield5@gmail.com> Date: Tue, 23 Jan 2024 14:19:08 -0800 Subject: [PATCH] [REFACTOR] remove all <dialog> modals and replace with custom ModalWrapper component (#641) * add useModal hook and ModalWrapper component that will be used to replace all <dialog> modals for better browser support * implement useModal hook and ModalWrapper component to replace all exisiting <dialog> --- .../src/components/ChangeWarning/index.jsx | 68 +++--- .../src/components/ModalWrapper/index.jsx | 9 + frontend/src/components/UserMenu/index.jsx | 2 +- .../ChatHistory/Citation/index.jsx | 28 +-- .../src/components/WorkspaceChat/index.jsx | 9 +- frontend/src/hooks/useModal.js | 10 + .../Invitations/NewInviteModal/index.jsx | 131 +++++----- .../src/pages/Admin/Invitations/index.jsx | 13 +- .../pages/Admin/Users/NewUserModal/index.jsx | 197 ++++++++------- .../Users/UserRow/EditUserModal/index.jsx | 188 +++++++-------- .../src/pages/Admin/Users/UserRow/index.jsx | 17 +- frontend/src/pages/Admin/Users/index.jsx | 13 +- .../Workspaces/NewWorkspaceModal/index.jsx | 120 +++++----- .../EditWorkspaceUsersModal/index.jsx | 225 +++++++++--------- .../Admin/Workspaces/WorkspaceRow/index.jsx | 22 +- frontend/src/pages/Admin/Workspaces/index.jsx | 13 +- .../ApiKeys/NewApiKeyModal/index.jsx | 144 ++++++----- .../pages/GeneralSettings/ApiKeys/index.jsx | 14 +- .../GeneralSettings/Chats/ChatRow/index.jsx | 79 +++--- .../EmbeddingPreference/index.jsx | 19 +- .../GeneralSettings/VectorDatabase/index.jsx | 19 +- .../src/pages/Invite/NewUserModal/index.jsx | 122 +++++----- frontend/src/pages/Invite/index.jsx | 5 +- 23 files changed, 722 insertions(+), 745 deletions(-) create mode 100644 frontend/src/components/ModalWrapper/index.jsx create mode 100644 frontend/src/hooks/useModal.js diff --git a/frontend/src/components/ChangeWarning/index.jsx b/frontend/src/components/ChangeWarning/index.jsx index a0fe14c1f..42b211baf 100644 --- a/frontend/src/components/ChangeWarning/index.jsx +++ b/frontend/src/components/ChangeWarning/index.jsx @@ -6,44 +6,42 @@ export default function ChangeWarningModal({ onConfirm, }) { return ( - <dialog id="confirmation-modal" className="bg-transparent outline-none"> - <div className="relative w-full max-w-2xl max-h-full"> - <div className="relative bg-main-gradient rounded-lg shadow"> - <div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50"> - <div className="flex items-center gap-2"> - <Warning - className="text-yellow-300 text-lg w-6 h-6" - weight="fill" - /> - <h3 className="text-xl font-semibold text-yellow-300">Warning</h3> - </div> - </div> - <div className="w-[550px] p-6 text-white"> - <p> - {warningText} - <br /> - <br /> - Are you sure you want to proceed? - </p> + <div className="relative w-full max-w-2xl max-h-full"> + <div className="relative bg-main-gradient rounded-lg shadow"> + <div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50"> + <div className="flex items-center gap-2"> + <Warning + className="text-yellow-300 text-lg w-6 h-6" + weight="fill" + /> + <h3 className="text-xl font-semibold text-yellow-300">Warning</h3> </div> + </div> + <div className="w-[550px] p-6 text-white"> + <p> + {warningText} + <br /> + <br /> + Are you sure you want to proceed? + </p> + </div> - <div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50"> - <button - onClick={onClose} - type="button" - className="px-4 py-2 rounded-lg text-white hover:bg-red-500 transition-all duration-300" - > - Cancel - </button> - <button - onClick={onConfirm} - className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800" - > - Confirm - </button> - </div> + <div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50"> + <button + onClick={onClose} + type="button" + className="px-4 py-2 rounded-lg text-white hover:bg-red-500 transition-all duration-300" + > + Cancel + </button> + <button + onClick={onConfirm} + className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800" + > + Confirm + </button> </div> </div> - </dialog> + </div> ); } diff --git a/frontend/src/components/ModalWrapper/index.jsx b/frontend/src/components/ModalWrapper/index.jsx new file mode 100644 index 000000000..37041f634 --- /dev/null +++ b/frontend/src/components/ModalWrapper/index.jsx @@ -0,0 +1,9 @@ +export default function ModalWrapper({ children, isOpen }) { + if (!isOpen) return null; + + return ( + <div className="bg-black/60 backdrop-blur-sm fixed top-0 left-0 outline-none w-screen h-screen flex items-center justify-center z-30"> + {children} + </div> + ); +} diff --git a/frontend/src/components/UserMenu/index.jsx b/frontend/src/components/UserMenu/index.jsx index 61e111da4..e6f3c7cfb 100644 --- a/frontend/src/components/UserMenu/index.jsx +++ b/frontend/src/components/UserMenu/index.jsx @@ -193,7 +193,7 @@ function AccountModal({ user, hideModal }) { return ( <div id="account-modal" - className="bg-black/20 fixed top-0 left-0 outline-none w-screen h-screen flex items-center justify-center" + className="bg-black/60 backdrop-blur-sm fixed top-0 left-0 outline-none w-screen h-screen flex items-center justify-center" > <div className="relative w-[500px] max-w-2xl max-h-full bg-main-gradient rounded-lg shadow"> <div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50"> diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx index 9af36fc5a..697c55214 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx @@ -1,9 +1,10 @@ -import { memo, useState, useEffect, useRef } from "react"; +import { memo, useState } from "react"; import { X } from "@phosphor-icons/react"; import { v4 } from "uuid"; import { decode as HTMLDecode } from "he"; import { CaretRight, FileText } from "@phosphor-icons/react"; import truncate from "truncate"; +import ModalWrapper from "@/components/ModalWrapper"; function combineLikeSources(sources) { const combined = {}; @@ -98,27 +99,10 @@ function SkeletonLine() { function CitationDetailModal({ source, onClose }) { const { references, title, text } = source; - const dialogRef = useRef(null); - - useEffect(() => { - if (source && dialogRef.current) { - dialogRef.current.showModal(); - } - }, [source]); - - const handleModalClose = () => { - if (dialogRef.current) { - dialogRef.current.close(); - } - onClose(); - }; return ( - <dialog - ref={dialogRef} - className="bg-transparent outline-none fixed top-0 left-0 w-full h-full flex items-center justify-center z-10" - > - <div className="relative w-full max-w-2xl bg-main-gradient rounded-lg shadow border border-white/10 overflow-hidden"> + <ModalWrapper isOpen={source}> + <div className="w-full max-w-2xl bg-main-gradient rounded-lg shadow border border-white/10 overflow-hidden"> <div className="relative p-6 border-b rounded-t border-gray-500/50"> <h3 className="text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap"> {truncate(title, 45)} @@ -129,7 +113,7 @@ function CitationDetailModal({ source, onClose }) { </p> )} <button - onClick={handleModalClose} + onClick={onClose} type="button" className="absolute top-6 right-6 transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border" > @@ -153,7 +137,7 @@ function CitationDetailModal({ source, onClose }) { </div> </div> </div> - </dialog> + </ModalWrapper> ); } diff --git a/frontend/src/components/WorkspaceChat/index.jsx b/frontend/src/components/WorkspaceChat/index.jsx index 30bd494f3..cbe7dbdd0 100644 --- a/frontend/src/components/WorkspaceChat/index.jsx +++ b/frontend/src/components/WorkspaceChat/index.jsx @@ -3,6 +3,7 @@ import Workspace from "@/models/workspace"; import LoadingChat from "./LoadingChat"; import ChatContainer from "./ChatContainer"; import paths from "@/utils/paths"; +import ModalWrapper from "../ModalWrapper"; export default function WorkspaceChat({ loading, workspace }) { const [history, setHistory] = useState([]); @@ -28,11 +29,7 @@ export default function WorkspaceChat({ loading, workspace }) { return ( <> {loading === false && !workspace && ( - <dialog - open={true} - style={{ zIndex: 100 }} - className="fixed top-0 flex bg-black bg-opacity-50 w-full md:w-[100vw] h-full items-center justify-center" - > + <ModalWrapper isOpen={true}> <div className="relative w-full md:max-w-2xl max-h-full bg-main-gradient rounded-lg shadow p-4"> <div className="flex flex-col gap-y-4 w-full p-6 text-center"> <p className="font-semibold text-red-500 text-xl"> @@ -52,7 +49,7 @@ export default function WorkspaceChat({ loading, workspace }) { </div> </div> </div> - </dialog> + </ModalWrapper> )} <LoadingChat /> </> diff --git a/frontend/src/hooks/useModal.js b/frontend/src/hooks/useModal.js new file mode 100644 index 000000000..854b5d4a8 --- /dev/null +++ b/frontend/src/hooks/useModal.js @@ -0,0 +1,10 @@ +import { useState } from "react"; + +export function useModal() { + const [isOpen, setIsOpen] = useState(false); + + const openModal = () => setIsOpen(true); + const closeModal = () => setIsOpen(false); + + return { isOpen, openModal, closeModal }; +} diff --git a/frontend/src/pages/Admin/Invitations/NewInviteModal/index.jsx b/frontend/src/pages/Admin/Invitations/NewInviteModal/index.jsx index 54ae7e076..3aef87a65 100644 --- a/frontend/src/pages/Admin/Invitations/NewInviteModal/index.jsx +++ b/frontend/src/pages/Admin/Invitations/NewInviteModal/index.jsx @@ -2,14 +2,7 @@ import React, { useEffect, useState } from "react"; import { X } from "@phosphor-icons/react"; import Admin from "@/models/admin"; -const DIALOG_ID = `new-invite-modal`; - -function hideModal() { - document.getElementById(DIALOG_ID)?.close(); -} - -export const NewInviteModalId = DIALOG_ID; -export default function NewInviteModal() { +export default function NewInviteModal({ closeModal }) { const [invite, setInvite] = useState(null); const [error, setError] = useState(null); const [copied, setCopied] = useState(false); @@ -39,74 +32,70 @@ export default function NewInviteModal() { }, [copied]); return ( - <dialog id={DIALOG_ID} className="bg-transparent outline-none"> - <div className="relative w-[500px] max-w-2xl max-h-full"> - <div className="relative bg-main-gradient rounded-lg shadow"> - <div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50"> - <h3 className="text-xl font-semibold text-white"> - Create new invite - </h3> - <button - onClick={hideModal} - type="button" - className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border" - data-modal-hide="staticModal" - > - <X className="text-gray-300 text-lg" /> - </button> - </div> - <form onSubmit={handleCreate}> - <div className="p-6 space-y-6 flex h-full w-full"> - <div className="w-full flex flex-col gap-y-4"> - {error && ( - <p className="text-red-400 text-sm">Error: {error}</p> - )} - {invite && ( - <input - type="url" - defaultValue={`${window.location.origin}/accept-invite/${invite.code}`} - disabled={true} - className="rounded-lg px-4 py-2 text-white bg-zinc-900 border border-gray-500/50" - /> - )} - <p className="text-white text-xs md:text-sm"> - After creation you will be able to copy the invite and send it - to a new user where they can create an account as a default - user. - </p> - </div> + <div className="relative w-[500px] max-w-2xl max-h-full"> + <div className="relative bg-main-gradient rounded-lg shadow"> + <div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50"> + <h3 className="text-xl font-semibold text-white"> + Create new invite + </h3> + <button + onClick={closeModal} + type="button" + className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border" + data-modal-hide="staticModal" + > + <X className="text-gray-300 text-lg" /> + </button> + </div> + <form onSubmit={handleCreate}> + <div className="p-6 space-y-6 flex h-full w-full"> + <div className="w-full flex flex-col gap-y-4"> + {error && <p className="text-red-400 text-sm">Error: {error}</p>} + {invite && ( + <input + type="url" + defaultValue={`${window.location.origin}/accept-invite/${invite.code}`} + disabled={true} + className="rounded-lg px-4 py-2 text-white bg-zinc-900 border border-gray-500/50" + /> + )} + <p className="text-white text-xs md:text-sm"> + After creation you will be able to copy the invite and send it + to a new user where they can create an account as a default + user. + </p> </div> - <div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50"> - {!invite ? ( - <> - <button - onClick={hideModal} - type="button" - className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300" - > - Cancel - </button> - <button - type="submit" - className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800" - > - Create Invite - </button> - </> - ) : ( + </div> + <div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50"> + {!invite ? ( + <> <button - onClick={copyInviteLink} + onClick={closeModal} type="button" - disabled={copied} - className="w-full transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800 text-center justify-center" + className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300" > - {copied ? "Copied Link" : "Copy Invite Link"} + Cancel </button> - )} - </div> - </form> - </div> + <button + type="submit" + className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800" + > + Create Invite + </button> + </> + ) : ( + <button + onClick={copyInviteLink} + type="button" + disabled={copied} + className="w-full transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800 text-center justify-center" + > + {copied ? "Copied Link" : "Copy Invite Link"} + </button> + )} + </div> + </form> </div> - </dialog> + </div> ); } diff --git a/frontend/src/pages/Admin/Invitations/index.jsx b/frontend/src/pages/Admin/Invitations/index.jsx index cf5b38d37..94b07f330 100644 --- a/frontend/src/pages/Admin/Invitations/index.jsx +++ b/frontend/src/pages/Admin/Invitations/index.jsx @@ -7,9 +7,12 @@ import { EnvelopeSimple } from "@phosphor-icons/react"; import usePrefersDarkMode from "@/hooks/usePrefersDarkMode"; import Admin from "@/models/admin"; import InviteRow from "./InviteRow"; -import NewInviteModal, { NewInviteModalId } from "./NewInviteModal"; +import NewInviteModal from "./NewInviteModal"; +import { useModal } from "@/hooks/useModal"; +import ModalWrapper from "@/components/ModalWrapper"; export default function AdminInvites() { + const { isOpen, openModal, closeModal } = useModal(); return ( <div className="w-screen h-screen overflow-hidden bg-sidebar flex"> {!isMobile && <Sidebar />} @@ -23,9 +26,7 @@ export default function AdminInvites() { <div className="items-center flex gap-x-4"> <p className="text-2xl font-semibold text-white">Invitations</p> <button - onClick={() => - document?.getElementById(NewInviteModalId)?.showModal() - } + onClick={openModal} className="border border-slate-200 px-4 py-1 rounded-lg text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800" > <EnvelopeSimple className="h-4 w-4" /> Create Invite Link @@ -38,7 +39,9 @@ export default function AdminInvites() { </div> <InvitationsContainer /> </div> - <NewInviteModal /> + <ModalWrapper isOpen={isOpen}> + <NewInviteModal closeModal={closeModal} /> + </ModalWrapper> </div> </div> ); diff --git a/frontend/src/pages/Admin/Users/NewUserModal/index.jsx b/frontend/src/pages/Admin/Users/NewUserModal/index.jsx index d8c26d1e3..9a7e2a6d0 100644 --- a/frontend/src/pages/Admin/Users/NewUserModal/index.jsx +++ b/frontend/src/pages/Admin/Users/NewUserModal/index.jsx @@ -4,14 +4,7 @@ import Admin from "@/models/admin"; import { userFromStorage } from "@/utils/request"; import { RoleHintDisplay } from ".."; -const DIALOG_ID = `new-user-modal`; - -function hideModal() { - document.getElementById(DIALOG_ID)?.close(); -} - -export const NewUserModalId = DIALOG_ID; -export default function NewUserModal() { +export default function NewUserModal({ closeModal }) { const [error, setError] = useState(null); const [role, setRole] = useState("default"); const handleCreate = async (e) => { @@ -28,107 +21,103 @@ export default function NewUserModal() { const user = userFromStorage(); return ( - <dialog id={DIALOG_ID} className="bg-transparent outline-none"> - <div className="relative w-full max-w-2xl max-h-full"> - <div className="relative bg-main-gradient rounded-lg shadow"> - <div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50"> - <h3 className="text-xl font-semibold text-white"> - Add user to instance - </h3> + <div className="relative w-full max-w-2xl max-h-full"> + <div className="relative bg-main-gradient rounded-lg shadow"> + <div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50"> + <h3 className="text-xl font-semibold text-white"> + Add user to instance + </h3> + <button + onClick={closeModal} + type="button" + className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border" + data-modal-hide="staticModal" + > + <X className="text-gray-300 text-lg" /> + </button> + </div> + <form onSubmit={handleCreate}> + <div className="p-6 space-y-6 flex h-full w-full"> + <div className="w-full flex flex-col gap-y-4"> + <div> + <label + htmlFor="username" + className="block mb-2 text-sm font-medium text-white" + > + Username + </label> + <input + name="username" + type="text" + className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" + placeholder="User's username" + minLength={2} + required={true} + autoComplete="off" + /> + </div> + <div> + <label + htmlFor="password" + className="block mb-2 text-sm font-medium text-white" + > + Password + </label> + <input + name="password" + type="text" + className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" + placeholder="User's initial password" + required={true} + autoComplete="off" + /> + </div> + <div> + <label + htmlFor="role" + className="block mb-2 text-sm font-medium text-white" + > + Role + </label> + <select + name="role" + required={true} + defaultValue={"default"} + onChange={(e) => setRole(e.target.value)} + className="rounded-lg bg-zinc-900 px-4 py-2 text-sm text-white border border-gray-500 focus:ring-blue-500 focus:border-blue-500" + > + <option value="default">Default</option> + <option value="manager">Manager </option> + {user?.role === "admin" && ( + <option value="admin">Administrator</option> + )} + </select> + <RoleHintDisplay role={role} /> + </div> + {error && <p className="text-red-400 text-sm">Error: {error}</p>} + <p className="text-white text-xs md:text-sm"> + After creating a user they will need to login with their initial + login to get access. + </p> + </div> + </div> + <div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50"> <button - onClick={hideModal} + onClick={closeModal} type="button" - className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border" - data-modal-hide="staticModal" + className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300" + > + Cancel + </button> + <button + type="submit" + className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800" > - <X className="text-gray-300 text-lg" /> + Add user </button> </div> - <form onSubmit={handleCreate}> - <div className="p-6 space-y-6 flex h-full w-full"> - <div className="w-full flex flex-col gap-y-4"> - <div> - <label - htmlFor="username" - className="block mb-2 text-sm font-medium text-white" - > - Username - </label> - <input - name="username" - type="text" - className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" - placeholder="User's username" - minLength={2} - required={true} - autoComplete="off" - /> - </div> - <div> - <label - htmlFor="password" - className="block mb-2 text-sm font-medium text-white" - > - Password - </label> - <input - name="password" - type="text" - className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" - placeholder="User's initial password" - required={true} - autoComplete="off" - /> - </div> - <div> - <label - htmlFor="role" - className="block mb-2 text-sm font-medium text-white" - > - Role - </label> - <select - name="role" - required={true} - defaultValue={"default"} - onChange={(e) => setRole(e.target.value)} - className="rounded-lg bg-zinc-900 px-4 py-2 text-sm text-white border border-gray-500 focus:ring-blue-500 focus:border-blue-500" - > - <option value="default">Default</option> - <option value="manager">Manager </option> - {user?.role === "admin" && ( - <option value="admin">Administrator</option> - )} - </select> - <RoleHintDisplay role={role} /> - </div> - {error && ( - <p className="text-red-400 text-sm">Error: {error}</p> - )} - <p className="text-white text-xs md:text-sm"> - After creating a user they will need to login with their - initial login to get access. - </p> - </div> - </div> - <div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50"> - <button - onClick={hideModal} - type="button" - className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300" - > - Cancel - </button> - <button - type="submit" - className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800" - > - Add user - </button> - </div> - </form> - </div> + </form> </div> - </dialog> + </div> ); } diff --git a/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx b/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx index 4c3c2686a..959b04b4c 100644 --- a/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx +++ b/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx @@ -3,16 +3,10 @@ import { X } from "@phosphor-icons/react"; import Admin from "@/models/admin"; import { RoleHintDisplay } from "../.."; -export const EditUserModalId = (user) => `edit-user-${user.id}-modal`; - -export default function EditUserModal({ currentUser, user }) { +export default function EditUserModal({ currentUser, user, closeModal }) { const [role, setRole] = useState(user.role); const [error, setError] = useState(null); - const hideModal = () => { - document.getElementById(EditUserModalId(user)).close(); - }; - const handleUpdate = async (e) => { setError(null); e.preventDefault(); @@ -28,103 +22,99 @@ export default function EditUserModal({ currentUser, user }) { }; return ( - <dialog id={EditUserModalId(user)} className="bg-transparent outline-none"> - <div className="relative w-[500px] max-w-2xl max-h-full"> - <div className="relative bg-main-gradient rounded-lg shadow"> - <div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50"> - <h3 className="text-xl font-semibold text-white"> - Edit {user.username} - </h3> + <div className="relative w-[500px] max-w-2xl max-h-full"> + <div className="relative bg-main-gradient rounded-lg shadow"> + <div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50"> + <h3 className="text-xl font-semibold text-white"> + Edit {user.username} + </h3> + <button + onClick={closeModal} + type="button" + className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border" + data-modal-hide="staticModal" + > + <X className="text-gray-300 text-lg" /> + </button> + </div> + <form onSubmit={handleUpdate}> + <div className="p-6 space-y-6 flex h-full w-full"> + <div className="w-full flex flex-col gap-y-4"> + <div> + <label + htmlFor="username" + className="block mb-2 text-sm font-medium text-white" + > + Username + </label> + <input + name="username" + type="text" + className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" + placeholder="User's username" + minLength={2} + defaultValue={user.username} + required={true} + autoComplete="off" + /> + </div> + <div> + <label + htmlFor="password" + className="block mb-2 text-sm font-medium text-white" + > + New Password + </label> + <input + name="password" + type="text" + className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" + placeholder={`${user.username}'s new password`} + autoComplete="off" + /> + </div> + <div> + <label + htmlFor="role" + className="block mb-2 text-sm font-medium text-white" + > + Role + </label> + <select + name="role" + required={true} + defaultValue={user.role} + onChange={(e) => setRole(e.target.value)} + className="rounded-lg bg-zinc-900 px-4 py-2 text-sm text-white border border-gray-500 focus:ring-blue-500 focus:border-blue-500" + > + <option value="default">Default</option> + <option value="manager">Manager</option> + {currentUser?.role === "admin" && ( + <option value="admin">Administrator</option> + )} + </select> + <RoleHintDisplay role={role} /> + </div> + {error && <p className="text-red-400 text-sm">Error: {error}</p>} + </div> + </div> + <div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50"> <button - onClick={hideModal} + onClick={closeModal} type="button" - className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border" - data-modal-hide="staticModal" + className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300" + > + Cancel + </button> + <button + type="submit" + className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800" > - <X className="text-gray-300 text-lg" /> + Update user </button> </div> - <form onSubmit={handleUpdate}> - <div className="p-6 space-y-6 flex h-full w-full"> - <div className="w-full flex flex-col gap-y-4"> - <div> - <label - htmlFor="username" - className="block mb-2 text-sm font-medium text-white" - > - Username - </label> - <input - name="username" - type="text" - className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" - placeholder="User's username" - minLength={2} - defaultValue={user.username} - required={true} - autoComplete="off" - /> - </div> - <div> - <label - htmlFor="password" - className="block mb-2 text-sm font-medium text-white" - > - New Password - </label> - <input - name="password" - type="text" - className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" - placeholder={`${user.username}'s new password`} - autoComplete="off" - /> - </div> - <div> - <label - htmlFor="role" - className="block mb-2 text-sm font-medium text-white" - > - Role - </label> - <select - name="role" - required={true} - defaultValue={user.role} - onChange={(e) => setRole(e.target.value)} - className="rounded-lg bg-zinc-900 px-4 py-2 text-sm text-white border border-gray-500 focus:ring-blue-500 focus:border-blue-500" - > - <option value="default">Default</option> - <option value="manager">Manager</option> - {currentUser?.role === "admin" && ( - <option value="admin">Administrator</option> - )} - </select> - <RoleHintDisplay role={role} /> - </div> - {error && ( - <p className="text-red-400 text-sm">Error: {error}</p> - )} - </div> - </div> - <div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50"> - <button - onClick={hideModal} - type="button" - className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300" - > - Cancel - </button> - <button - type="submit" - className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800" - > - Update user - </button> - </div> - </form> - </div> + </form> </div> - </dialog> + </div> ); } diff --git a/frontend/src/pages/Admin/Users/UserRow/index.jsx b/frontend/src/pages/Admin/Users/UserRow/index.jsx index cd1c47732..c58b8124b 100644 --- a/frontend/src/pages/Admin/Users/UserRow/index.jsx +++ b/frontend/src/pages/Admin/Users/UserRow/index.jsx @@ -1,9 +1,11 @@ import { useRef, useState } from "react"; import { titleCase } from "text-case"; import Admin from "@/models/admin"; -import EditUserModal, { EditUserModalId } from "./EditUserModal"; +import EditUserModal from "./EditUserModal"; import { DotsThreeOutline } from "@phosphor-icons/react"; import showToast from "@/utils/toast"; +import { useModal } from "@/hooks/useModal"; +import ModalWrapper from "@/components/ModalWrapper"; const ModMap = { admin: ["admin", "manager", "default"], @@ -15,6 +17,7 @@ export default function UserRow({ currUser, user }) { const rowRef = useRef(null); const canModify = ModMap[currUser?.role || "default"].includes(user.role); const [suspended, setSuspended] = useState(user.suspended === 1); + const { isOpen, openModal, closeModal } = useModal(); const handleSuspend = async () => { if ( !window.confirm( @@ -65,9 +68,7 @@ export default function UserRow({ currUser, user }) { <td className="px-6 py-4 flex items-center gap-x-6"> {canModify && ( <button - onClick={() => - document?.getElementById(EditUserModalId(user))?.showModal() - } + onClick={openModal} className="font-medium text-white text-opacity-80 rounded-lg hover:text-white px-2 py-1 hover:text-opacity-60 hover:bg-white hover:bg-opacity-10" > <DotsThreeOutline weight="fill" className="h-5 w-5" /> @@ -91,7 +92,13 @@ export default function UserRow({ currUser, user }) { )} </td> </tr> - <EditUserModal currentUser={currUser} user={user} /> + <ModalWrapper isOpen={isOpen}> + <EditUserModal + currentUser={currUser} + user={user} + closeModal={closeModal} + /> + </ModalWrapper> </> ); } diff --git a/frontend/src/pages/Admin/Users/index.jsx b/frontend/src/pages/Admin/Users/index.jsx index fb8f50080..7c9699483 100644 --- a/frontend/src/pages/Admin/Users/index.jsx +++ b/frontend/src/pages/Admin/Users/index.jsx @@ -7,9 +7,12 @@ import { UserPlus } from "@phosphor-icons/react"; import Admin from "@/models/admin"; import UserRow from "./UserRow"; import useUser from "@/hooks/useUser"; -import NewUserModal, { NewUserModalId } from "./NewUserModal"; +import NewUserModal from "./NewUserModal"; +import { useModal } from "@/hooks/useModal"; +import ModalWrapper from "@/components/ModalWrapper"; export default function AdminUsers() { + const { isOpen, openModal, closeModal } = useModal(); return ( <div className="w-screen h-screen overflow-hidden bg-sidebar flex"> {!isMobile && <Sidebar />} @@ -23,9 +26,7 @@ export default function AdminUsers() { <div className="items-center flex gap-x-4"> <p className="text-2xl font-semibold text-white">Users</p> <button - onClick={() => - document?.getElementById(NewUserModalId)?.showModal() - } + onClick={openModal} className="border border-slate-200 px-4 py-1 rounded-lg text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800" > <UserPlus className="h-4 w-4" /> Add user @@ -39,7 +40,9 @@ export default function AdminUsers() { </div> <UsersContainer /> </div> - <NewUserModal /> + <ModalWrapper isOpen={isOpen}> + <NewUserModal closeModal={closeModal} /> + </ModalWrapper> </div> </div> ); diff --git a/frontend/src/pages/Admin/Workspaces/NewWorkspaceModal/index.jsx b/frontend/src/pages/Admin/Workspaces/NewWorkspaceModal/index.jsx index 21d9faed4..5179b3fb7 100644 --- a/frontend/src/pages/Admin/Workspaces/NewWorkspaceModal/index.jsx +++ b/frontend/src/pages/Admin/Workspaces/NewWorkspaceModal/index.jsx @@ -1,14 +1,8 @@ import React, { useState } from "react"; import { X } from "@phosphor-icons/react"; import Admin from "@/models/admin"; -const DIALOG_ID = `new-workspace-modal`; -function hideModal() { - document.getElementById(DIALOG_ID)?.close(); -} - -export const NewWorkspaceModalId = DIALOG_ID; -export default function NewWorkspaceModal() { +export default function NewWorkspaceModal({ closeModal }) { const [error, setError] = useState(null); const handleCreate = async (e) => { setError(null); @@ -20,69 +14,65 @@ export default function NewWorkspaceModal() { }; return ( - <dialog id={DIALOG_ID} className="bg-transparent outline-none"> - <div className="relative w-[500px] max-w-2xl max-h-full"> - <div className="relative bg-main-gradient rounded-lg shadow"> - <div className="flex items-start justify-between p-4 border-b rounded-t border-gray-600"> - <h3 className="text-xl font-semibold text-white"> - Create new workspace - </h3> + <div className="relative w-[500px] max-w-2xl max-h-full"> + <div className="relative bg-main-gradient rounded-lg shadow"> + <div className="flex items-start justify-between p-4 border-b rounded-t border-gray-600"> + <h3 className="text-xl font-semibold text-white"> + Create new workspace + </h3> + <button + onClick={closeModal} + type="button" + className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border" + data-modal-hide="staticModal" + > + <X className="text-gray-300 text-lg" /> + </button> + </div> + <form onSubmit={handleCreate}> + <div className="p-6 space-y-6 flex h-full w-full"> + <div className="w-full flex flex-col gap-y-4"> + <div> + <label + htmlFor="name" + className="block mb-2 text-sm font-medium text-white" + > + Workspace name + </label> + <input + name="name" + type="text" + className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" + placeholder="My workspace" + minLength={4} + required={true} + autoComplete="off" + /> + </div> + {error && <p className="text-red-400 text-sm">Error: {error}</p>} + <p className="text-white text-opacity-60 text-xs md:text-sm"> + After creating this workspace only admins will be able to see + it. You can add users after it has been created. + </p> + </div> + </div> + <div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-600"> <button - onClick={hideModal} + onClick={closeModal} type="button" - className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border" - data-modal-hide="staticModal" + className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300" > - <X className="text-gray-300 text-lg" /> + Cancel + </button> + <button + type="submit" + className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800" + > + Create workspace </button> </div> - <form onSubmit={handleCreate}> - <div className="p-6 space-y-6 flex h-full w-full"> - <div className="w-full flex flex-col gap-y-4"> - <div> - <label - htmlFor="name" - className="block mb-2 text-sm font-medium text-white" - > - Workspace name - </label> - <input - name="name" - type="text" - className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" - placeholder="My workspace" - minLength={4} - required={true} - autoComplete="off" - /> - </div> - {error && ( - <p className="text-red-400 text-sm">Error: {error}</p> - )} - <p className="text-white text-opacity-60 text-xs md:text-sm"> - After creating this workspace only admins will be able to see - it. You can add users after it has been created. - </p> - </div> - </div> - <div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-600"> - <button - onClick={hideModal} - type="button" - className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300" - > - Cancel - </button> - <button - type="submit" - className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800" - > - Create workspace - </button> - </div> - </form> - </div> + </form> </div> - </dialog> + </div> ); } diff --git a/frontend/src/pages/Admin/Workspaces/WorkspaceRow/EditWorkspaceUsersModal/index.jsx b/frontend/src/pages/Admin/Workspaces/WorkspaceRow/EditWorkspaceUsersModal/index.jsx index 052f845b4..cd8d5f011 100644 --- a/frontend/src/pages/Admin/Workspaces/WorkspaceRow/EditWorkspaceUsersModal/index.jsx +++ b/frontend/src/pages/Admin/Workspaces/WorkspaceRow/EditWorkspaceUsersModal/index.jsx @@ -6,13 +6,13 @@ import { titleCase } from "text-case"; export const EditWorkspaceUsersModalId = (workspace) => `edit-workspace-${workspace.id}-modal`; -export default function EditWorkspaceUsersModal({ workspace, users }) { +export default function EditWorkspaceUsersModal({ + workspace, + users, + closeModal, +}) { const [error, setError] = useState(null); - const hideModal = () => { - document.getElementById(EditWorkspaceUsersModalId(workspace)).close(); - }; - const handleUpdate = async (e) => { setError(null); e.preventDefault(); @@ -35,122 +35,115 @@ export default function EditWorkspaceUsersModal({ workspace, users }) { }; return ( - <dialog - id={EditWorkspaceUsersModalId(workspace)} - className="bg-transparent outline-none" - > - <div className="relative w-[500px] max-w-2xl max-h-full"> - <div className="relative bg-main-gradient rounded-lg shadow"> - <div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50"> - <h3 className="text-xl font-semibold text-white"> - Edit {workspace.name} - </h3> + <div className="relative w-[500px] max-w-2xl max-h-full"> + <div className="relative bg-main-gradient rounded-lg shadow"> + <div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50"> + <h3 className="text-xl font-semibold text-white"> + Edit {workspace.name} + </h3> + <button + onClick={closeModal} + type="button" + className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border" + data-modal-hide="staticModal" + > + <X className="text-gray-300 text-lg" /> + </button> + </div> + <form onSubmit={handleUpdate}> + <div className="p-6 space-y-6 flex h-full w-full"> + <div className="w-full flex flex-col gap-y-4"> + {users + .filter((user) => user.role !== "admin") + .map((user) => { + return ( + <div + key={`workspace-${workspace.id}-user-${user.id}`} + data-workspace={workspace.id} + className="flex items-center pl-4 border border-gray-500/50 rounded group hover:bg-stone-900 transition-all duration-300 cursor-pointer" + onClick={() => { + document + .getElementById( + `workspace-${workspace.id}-user-${user.id}` + ) + ?.click(); + }} + > + <input + id={`workspace-${workspace.id}-user-${user.id}`} + defaultChecked={workspace.userIds.includes(user.id)} + type="checkbox" + value="yes" + name={`user-${user.id}`} + className="w-4 h-4 text-blue-600 bg-zinc-900 border border-gray-500/50 rounded focus:ring-blue-500 focus:border-blue-500 pointer-events-none" + /> + <label + htmlFor={`user-${user.id}`} + className="pointer-events-none w-full py-4 ml-2 text-sm font-medium text-white" + > + {titleCase(user.username)} + </label> + </div> + ); + })} + <div className="flex items-center gap-x-4"> + <button + type="button" + className="w-full p-4 flex text-white items-center pl-4 border border-gray-500/50 rounded group hover:bg-stone-900 transition-all duration-300 cursor-pointer" + onClick={() => { + document + .getElementById(`workspace-${workspace.id}-select-all`) + ?.click(); + Array.from( + document.querySelectorAll( + `[data-workspace='${workspace.id}']` + ) + ).forEach((el) => { + if (!el.firstChild.checked) el.firstChild.click(); + }); + }} + > + Select All + </button> + <button + type="button" + className="w-full p-4 flex text-white items-center pl-4 border border-gray-500/50 rounded group hover:bg-stone-900 transition-all duration-300 cursor-pointer" + onClick={() => { + document + .getElementById(`workspace-${workspace.id}-select-all`) + ?.click(); + Array.from( + document.querySelectorAll( + `[data-workspace='${workspace.id}']` + ) + ).forEach((el) => { + if (el.firstChild.checked) el.firstChild.click(); + }); + }} + > + Deselect All + </button> + </div> + {error && <p className="text-red-400 text-sm">Error: {error}</p>} + </div> + </div> + <div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50"> <button - onClick={hideModal} + onClick={closeModal} type="button" - className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border" - data-modal-hide="staticModal" + className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300" > - <X className="text-gray-300 text-lg" /> + Cancel + </button> + <button + type="submit" + className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800" + > + Update workspace </button> </div> - <form onSubmit={handleUpdate}> - <div className="p-6 space-y-6 flex h-full w-full"> - <div className="w-full flex flex-col gap-y-4"> - {users - .filter((user) => user.role !== "admin") - .map((user) => { - return ( - <div - key={`workspace-${workspace.id}-user-${user.id}`} - data-workspace={workspace.id} - className="flex items-center pl-4 border border-gray-500/50 rounded group hover:bg-stone-900 transition-all duration-300 cursor-pointer" - onClick={() => { - document - .getElementById( - `workspace-${workspace.id}-user-${user.id}` - ) - ?.click(); - }} - > - <input - id={`workspace-${workspace.id}-user-${user.id}`} - defaultChecked={workspace.userIds.includes(user.id)} - type="checkbox" - value="yes" - name={`user-${user.id}`} - className="w-4 h-4 text-blue-600 bg-zinc-900 border border-gray-500/50 rounded focus:ring-blue-500 focus:border-blue-500 pointer-events-none" - /> - <label - htmlFor={`user-${user.id}`} - className="pointer-events-none w-full py-4 ml-2 text-sm font-medium text-white" - > - {titleCase(user.username)} - </label> - </div> - ); - })} - <div className="flex items-center gap-x-4"> - <button - type="button" - className="w-full p-4 flex text-white items-center pl-4 border border-gray-500/50 rounded group hover:bg-stone-900 transition-all duration-300 cursor-pointer" - onClick={() => { - document - .getElementById(`workspace-${workspace.id}-select-all`) - ?.click(); - Array.from( - document.querySelectorAll( - `[data-workspace='${workspace.id}']` - ) - ).forEach((el) => { - if (!el.firstChild.checked) el.firstChild.click(); - }); - }} - > - Select All - </button> - <button - type="button" - className="w-full p-4 flex text-white items-center pl-4 border border-gray-500/50 rounded group hover:bg-stone-900 transition-all duration-300 cursor-pointer" - onClick={() => { - document - .getElementById(`workspace-${workspace.id}-select-all`) - ?.click(); - Array.from( - document.querySelectorAll( - `[data-workspace='${workspace.id}']` - ) - ).forEach((el) => { - if (el.firstChild.checked) el.firstChild.click(); - }); - }} - > - Deselect All - </button> - </div> - {error && ( - <p className="text-red-400 text-sm">Error: {error}</p> - )} - </div> - </div> - <div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50"> - <button - onClick={hideModal} - type="button" - className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300" - > - Cancel - </button> - <button - type="submit" - className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800" - > - Update workspace - </button> - </div> - </form> - </div> + </form> </div> - </dialog> + </div> ); } diff --git a/frontend/src/pages/Admin/Workspaces/WorkspaceRow/index.jsx b/frontend/src/pages/Admin/Workspaces/WorkspaceRow/index.jsx index 8c8969622..e755e1857 100644 --- a/frontend/src/pages/Admin/Workspaces/WorkspaceRow/index.jsx +++ b/frontend/src/pages/Admin/Workspaces/WorkspaceRow/index.jsx @@ -1,13 +1,14 @@ import { useRef } from "react"; import Admin from "@/models/admin"; import paths from "@/utils/paths"; -import EditWorkspaceUsersModal, { - EditWorkspaceUsersModalId, -} from "./EditWorkspaceUsersModal"; +import EditWorkspaceUsersModal from "./EditWorkspaceUsersModal"; import { DotsThreeOutline, LinkSimple, Trash } from "@phosphor-icons/react"; +import { useModal } from "@/hooks/useModal"; +import ModalWrapper from "@/components/ModalWrapper"; export default function WorkspaceRow({ workspace, users }) { const rowRef = useRef(null); + const { isOpen, openModal, closeModal } = useModal(); const handleDelete = async () => { if ( !window.confirm( @@ -32,6 +33,7 @@ export default function WorkspaceRow({ workspace, users }) { <a href={paths.workspace.chat(workspace.slug)} target="_blank" + rel="noreferrer" className="text-white flex items-center hover:underline" > <LinkSimple className="mr-2 w-5 h-5" /> {workspace.slug} @@ -41,11 +43,7 @@ export default function WorkspaceRow({ workspace, users }) { <td className="px-6 py-4">{workspace.createdAt}</td> <td className="px-6 py-4 flex items-center gap-x-6"> <button - onClick={() => - document - ?.getElementById(EditWorkspaceUsersModalId(workspace)) - ?.showModal() - } + onClick={openModal} className="font-medium rounded-lg hover:text-white hover:text-opacity-60 px-2 py-1 hover:bg-white hover:bg-opacity-10" > <DotsThreeOutline weight="fill" className="h-5 w-5" /> @@ -58,7 +56,13 @@ export default function WorkspaceRow({ workspace, users }) { </button> </td> </tr> - <EditWorkspaceUsersModal workspace={workspace} users={users} /> + <ModalWrapper isOpen={isOpen}> + <EditWorkspaceUsersModal + workspace={workspace} + users={users} + closeModal={closeModal} + /> + </ModalWrapper> </> ); } diff --git a/frontend/src/pages/Admin/Workspaces/index.jsx b/frontend/src/pages/Admin/Workspaces/index.jsx index c29c92ac2..8ca7410b0 100644 --- a/frontend/src/pages/Admin/Workspaces/index.jsx +++ b/frontend/src/pages/Admin/Workspaces/index.jsx @@ -7,9 +7,12 @@ import { BookOpen } from "@phosphor-icons/react"; import usePrefersDarkMode from "@/hooks/usePrefersDarkMode"; import Admin from "@/models/admin"; import WorkspaceRow from "./WorkspaceRow"; -import NewWorkspaceModal, { NewWorkspaceModalId } from "./NewWorkspaceModal"; +import NewWorkspaceModal from "./NewWorkspaceModal"; +import { useModal } from "@/hooks/useModal"; +import ModalWrapper from "@/components/ModalWrapper"; export default function AdminWorkspaces() { + const { isOpen, openModal, closeModal } = useModal(); return ( <div className="w-screen h-screen overflow-hidden bg-sidebar flex"> {!isMobile && <Sidebar />} @@ -25,9 +28,7 @@ export default function AdminWorkspaces() { Instance workspaces </p> <button - onClick={() => - document?.getElementById(NewWorkspaceModalId)?.showModal() - } + onClick={openModal} className="border border-slate-200 px-4 py-1 rounded-lg text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800" > <BookOpen className="h-4 w-4" /> New Workspace @@ -40,7 +41,9 @@ export default function AdminWorkspaces() { </div> <WorkspacesContainer /> </div> - <NewWorkspaceModal /> + <ModalWrapper isOpen={isOpen}> + <NewWorkspaceModal closeModal={closeModal} /> + </ModalWrapper> </div> </div> ); diff --git a/frontend/src/pages/GeneralSettings/ApiKeys/NewApiKeyModal/index.jsx b/frontend/src/pages/GeneralSettings/ApiKeys/NewApiKeyModal/index.jsx index a4095ae9c..cde8bdbe6 100644 --- a/frontend/src/pages/GeneralSettings/ApiKeys/NewApiKeyModal/index.jsx +++ b/frontend/src/pages/GeneralSettings/ApiKeys/NewApiKeyModal/index.jsx @@ -5,14 +5,7 @@ import paths from "@/utils/paths"; import { userFromStorage } from "@/utils/request"; import System from "@/models/system"; -const DIALOG_ID = `new-api-key-modal`; - -function hideModal() { - document.getElementById(DIALOG_ID)?.close(); -} - -export const NewApiKeyModalId = DIALOG_ID; -export default function NewApiKeyModal() { +export default function NewApiKeyModal({ closeModal }) { const [apiKey, setApiKey] = useState(null); const [error, setError] = useState(null); const [copied, setCopied] = useState(false); @@ -43,80 +36,77 @@ export default function NewApiKeyModal() { }, [copied]); return ( - <dialog id={DIALOG_ID} className="bg-transparent outline-none"> - <div className="relative w-[500px] max-w-2xl max-h-full"> - <div className="relative bg-main-gradient rounded-lg shadow"> - <div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50"> - <h3 className="text-xl font-semibold text-white"> - Create new API key - </h3> - <button - onClick={hideModal} - type="button" - className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border" - data-modal-hide="staticModal" - > - <X className="text-gray-300 text-lg" /> - </button> - </div> - <form onSubmit={handleCreate}> - <div className="p-6 space-y-6 flex h-full w-full"> - <div className="w-full flex flex-col gap-y-4"> - {error && ( - <p className="text-red-400 text-sm">Error: {error}</p> - )} - {apiKey && ( - <input - type="text" - defaultValue={`${apiKey.secret}`} - disabled={true} - className="rounded-lg px-4 py-2 text-white bg-zinc-900 border border-gray-500/50" - /> - )} - <p className="text-white text-xs md:text-sm"> - Once created the API key can be used to programmatically - access and configure this AnythingLLM instance. - </p> - <a - href={paths.apiDocs()} - target="_blank" - className="text-blue-400 hover:underline" - > - Read the API documentation → - </a> - </div> + <div className="relative w-[500px] max-w-2xl max-h-full"> + <div className="relative bg-main-gradient rounded-lg shadow"> + <div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50"> + <h3 className="text-xl font-semibold text-white"> + Create new API key + </h3> + <button + onClick={closeModal} + type="button" + className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border" + data-modal-hide="staticModal" + > + <X className="text-gray-300 text-lg" /> + </button> + </div> + <form onSubmit={handleCreate}> + <div className="p-6 space-y-6 flex h-full w-full"> + <div className="w-full flex flex-col gap-y-4"> + {error && <p className="text-red-400 text-sm">Error: {error}</p>} + {apiKey && ( + <input + type="text" + defaultValue={`${apiKey.secret}`} + disabled={true} + className="rounded-lg px-4 py-2 text-white bg-zinc-900 border border-gray-500/50" + /> + )} + <p className="text-white text-xs md:text-sm"> + Once created the API key can be used to programmatically access + and configure this AnythingLLM instance. + </p> + <a + href={paths.apiDocs()} + target="_blank" + rel="noreferrer" + className="text-blue-400 hover:underline" + > + Read the API documentation → + </a> </div> - <div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50"> - {!apiKey ? ( - <> - <button - onClick={hideModal} - type="button" - className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300" - > - Cancel - </button> - <button - type="submit" - className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800" - > - Create API key - </button> - </> - ) : ( + </div> + <div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50"> + {!apiKey ? ( + <> <button - onClick={copyApiKey} + onClick={closeModal} type="button" - disabled={copied} - className="w-full transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800 text-center justify-center" + className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300" > - {copied ? "Copied API key" : "Copy API key"} + Cancel </button> - )} - </div> - </form> - </div> + <button + type="submit" + className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800" + > + Create API key + </button> + </> + ) : ( + <button + onClick={copyApiKey} + type="button" + disabled={copied} + className="w-full transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800 text-center justify-center" + > + {copied ? "Copied API key" : "Copy API key"} + </button> + )} + </div> + </form> </div> - </dialog> + </div> ); } diff --git a/frontend/src/pages/GeneralSettings/ApiKeys/index.jsx b/frontend/src/pages/GeneralSettings/ApiKeys/index.jsx index 5ee5fd0a3..bf4ac51ef 100644 --- a/frontend/src/pages/GeneralSettings/ApiKeys/index.jsx +++ b/frontend/src/pages/GeneralSettings/ApiKeys/index.jsx @@ -6,12 +6,15 @@ import "react-loading-skeleton/dist/skeleton.css"; import { PlusCircle } from "@phosphor-icons/react"; import Admin from "@/models/admin"; import ApiKeyRow from "./ApiKeyRow"; -import NewApiKeyModal, { NewApiKeyModalId } from "./NewApiKeyModal"; +import NewApiKeyModal from "./NewApiKeyModal"; import paths from "@/utils/paths"; import { userFromStorage } from "@/utils/request"; import System from "@/models/system"; +import ModalWrapper from "@/components/ModalWrapper"; +import { useModal } from "@/hooks/useModal"; export default function AdminApiKeys() { + const { isOpen, openModal, closeModal } = useModal(); return ( <div className="w-screen h-screen overflow-hidden bg-sidebar flex"> {!isMobile && <Sidebar />} @@ -25,9 +28,7 @@ export default function AdminApiKeys() { <div className="items-center flex gap-x-4"> <p className="text-2xl font-semibold text-white">API Keys</p> <button - onClick={() => - document?.getElementById(NewApiKeyModalId)?.showModal() - } + onClick={openModal} className="border border-slate-200 px-4 py-1 rounded-lg text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800" > <PlusCircle className="h-4 w-4" /> Generate New API Key @@ -40,6 +41,7 @@ export default function AdminApiKeys() { <a href={paths.apiDocs()} target="_blank" + rel="noreferrer" className="text-sm font-base text-blue-300 hover:underline" > Read the API documentation → @@ -47,7 +49,9 @@ export default function AdminApiKeys() { </div> <ApiKeysContainer /> </div> - <NewApiKeyModal /> + <ModalWrapper isOpen={isOpen}> + <NewApiKeyModal closeModal={closeModal} /> + </ModalWrapper> </div> </div> ); diff --git a/frontend/src/pages/GeneralSettings/Chats/ChatRow/index.jsx b/frontend/src/pages/GeneralSettings/Chats/ChatRow/index.jsx index 23fc39d9a..3056e7659 100644 --- a/frontend/src/pages/GeneralSettings/Chats/ChatRow/index.jsx +++ b/frontend/src/pages/GeneralSettings/Chats/ChatRow/index.jsx @@ -2,9 +2,22 @@ import { useRef } from "react"; import truncate from "truncate"; import { X, Trash } from "@phosphor-icons/react"; import System from "@/models/system"; +import ModalWrapper from "@/components/ModalWrapper"; +import { useModal } from "@/hooks/useModal"; export default function ChatRow({ chat }) { const rowRef = useRef(null); + const { + isOpen: isPromptOpen, + openModal: openPromptModal, + closeModal: closePromptModal, + } = useModal(); + const { + isOpen: isResponseOpen, + openModal: openResponseModal, + closeModal: closeResponseModal, + } = useModal(); + const handleDelete = async () => { if ( !window.confirm( @@ -30,17 +43,13 @@ export default function ChatRow({ chat }) { </td> <td className="px-6 py-4">{chat.workspace?.name}</td> <td - onClick={() => { - document.getElementById(`chat-${chat.id}-prompt`)?.showModal(); - }} + onClick={openPromptModal} className="px-6 py-4 border-transparent cursor-pointer transform transition-transform duration-200 hover:scale-105 hover:shadow-lg" > {truncate(chat.prompt, 40)} </td> <td - onClick={() => { - document.getElementById(`chat-${chat.id}-response`)?.showModal(); - }} + onClick={openResponseModal} className="px-6 py-4 cursor-pointer transform transition-transform duration-200 hover:scale-105 hover:shadow-lg" > {truncate(JSON.parse(chat.response)?.text, 40)} @@ -55,42 +64,38 @@ export default function ChatRow({ chat }) { </button> </td> </tr> - <TextPreview text={chat.prompt} modalName={`chat-${chat.id}-prompt`} /> - <TextPreview - text={JSON.parse(chat.response)?.text} - modalName={`chat-${chat.id}-response`} - /> + <ModalWrapper isOpen={isPromptOpen}> + <TextPreview text={chat.prompt} closeModal={closePromptModal} /> + </ModalWrapper> + <ModalWrapper isOpen={isResponseOpen}> + <TextPreview + text={JSON.parse(chat.response)?.text} + closeModal={closeResponseModal} + /> + </ModalWrapper> </> ); } - -function hideModal(modalName) { - document.getElementById(modalName)?.close(); -} - -const TextPreview = ({ text, modalName }) => { +const TextPreview = ({ text, closeModal }) => { return ( - <dialog id={modalName} className="bg-transparent outline-none w-full"> - <div className="relative w-full md:max-w-2xl max-h-full"> - <div className="relative bg-main-gradient rounded-lg shadow"> - <div className="flex items-start justify-between p-4 border-b rounded-t border-gray-600"> - <h3 className="text-xl font-semibold text-white">Viewing Text</h3> - <button - onClick={() => hideModal(modalName)} - type="button" - className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border" - data-modal-hide="staticModal" - > - <X className="text-gray-300 text-lg" /> - </button> - </div> - <div className="w-full p-6"> - <pre className="w-full h-[200px] py-2 px-4 whitespace-pre-line overflow-auto rounded-lg bg-zinc-900 border border-gray-500 text-white text-sm"> - {text} - </pre> - </div> + <div className="relative w-full md:max-w-2xl max-h-full"> + <div className="relative bg-main-gradient rounded-lg shadow"> + <div className="flex items-start justify-between p-4 border-b rounded-t border-gray-600"> + <h3 className="text-xl font-semibold text-white">Viewing Text</h3> + <button + onClick={closeModal} + type="button" + className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border" + > + <X className="text-gray-300 text-lg" /> + </button> + </div> + <div className="w-full p-6"> + <pre className="w-full h-[200px] py-2 px-4 whitespace-pre-line overflow-auto rounded-lg bg-zinc-900 border border-gray-500 text-white text-sm"> + {text} + </pre> </div> </div> - </dialog> + </div> ); }; diff --git a/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx b/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx index ddcb81319..c6c62fef7 100644 --- a/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx +++ b/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx @@ -15,6 +15,8 @@ import LocalAiOptions from "@/components/EmbeddingSelection/LocalAiOptions"; import NativeEmbeddingOptions from "@/components/EmbeddingSelection/NativeEmbeddingOptions"; import EmbedderItem from "@/components/EmbeddingSelection/EmbedderItem"; import { MagnifyingGlass } from "@phosphor-icons/react"; +import { useModal } from "@/hooks/useModal"; +import ModalWrapper from "@/components/ModalWrapper"; export default function GeneralEmbeddingPreference() { const [saving, setSaving] = useState(false); @@ -25,6 +27,7 @@ export default function GeneralEmbeddingPreference() { const [searchQuery, setSearchQuery] = useState(""); const [filteredEmbedders, setFilteredEmbedders] = useState([]); const [selectedEmbedder, setSelectedEmbedder] = useState(null); + const { isOpen, openModal, closeModal } = useModal(); const handleSubmit = async (e) => { e.preventDefault(); @@ -33,7 +36,7 @@ export default function GeneralEmbeddingPreference() { hasChanges && hasEmbeddings ) { - document.getElementById("confirmation-modal")?.showModal(); + openModal(); } else { await handleSaveSettings(); } @@ -56,7 +59,7 @@ export default function GeneralEmbeddingPreference() { setHasChanges(false); } setSaving(false); - document.getElementById("confirmation-modal")?.close(); + closeModal(); }; const updateChoice = (selection) => { @@ -116,11 +119,13 @@ export default function GeneralEmbeddingPreference() { return ( <div className="w-screen h-screen overflow-hidden bg-sidebar flex"> - <ChangeWarningModal - warningText=" Switching the embedder may affect previously embedded documents and future similarity search results." - onClose={() => document.getElementById("confirmation-modal")?.close()} - onConfirm={handleSaveSettings} - /> + <ModalWrapper isOpen={isOpen}> + <ChangeWarningModal + warningText="Switching the vector database will ignore previously embedded documents and future similarity search results. They will need to be re-added to each workspace." + onClose={closeModal} + onConfirm={handleSaveSettings} + /> + </ModalWrapper> {!isMobile && <Sidebar />} {loading ? ( <div diff --git a/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx b/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx index 02887b86a..f5f697a72 100644 --- a/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx +++ b/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx @@ -21,6 +21,8 @@ import WeaviateDBOptions from "@/components/VectorDBSelection/WeaviateDBOptions" import VectorDBItem from "@/components/VectorDBSelection/VectorDBItem"; import MilvusDBOptions from "@/components/VectorDBSelection/MilvusDBOptions"; import ZillizCloudOptions from "@/components/VectorDBSelection/ZillizCloudOptions"; +import { useModal } from "@/hooks/useModal"; +import ModalWrapper from "@/components/ModalWrapper"; export default function GeneralVectorDatabase() { const [saving, setSaving] = useState(false); @@ -31,6 +33,7 @@ export default function GeneralVectorDatabase() { const [searchQuery, setSearchQuery] = useState(""); const [filteredVDBs, setFilteredVDBs] = useState([]); const [selectedVDB, setSelectedVDB] = useState(null); + const { isOpen, openModal, closeModal } = useModal(); useEffect(() => { async function fetchKeys() { @@ -107,7 +110,7 @@ export default function GeneralVectorDatabase() { const handleSubmit = async (e) => { e.preventDefault(); if (selectedVDB !== settings?.VectorDB && hasChanges && hasEmbeddings) { - document.getElementById("confirmation-modal")?.showModal(); + openModal(); } else { await handleSaveSettings(); } @@ -130,7 +133,7 @@ export default function GeneralVectorDatabase() { setHasChanges(false); } setSaving(false); - document.getElementById("confirmation-modal")?.close(); + closeModal(); }; useEffect(() => { @@ -142,11 +145,13 @@ export default function GeneralVectorDatabase() { return ( <div className="w-screen h-screen overflow-hidden bg-sidebar flex"> - <ChangeWarningModal - warningText="Switching the vector database will ignore previously embedded documents and future similarity search results. They will need to be re-added to each workspace." - onClose={() => document.getElementById("confirmation-modal")?.close()} - onConfirm={handleSaveSettings} - /> + <ModalWrapper isOpen={isOpen}> + <ChangeWarningModal + warningText="Switching the vector database will ignore previously embedded documents and future similarity search results. They will need to be re-added to each workspace." + onClose={closeModal} + onConfirm={handleSaveSettings} + /> + </ModalWrapper> {!isMobile && <Sidebar />} {loading ? ( <div diff --git a/frontend/src/pages/Invite/NewUserModal/index.jsx b/frontend/src/pages/Invite/NewUserModal/index.jsx index 0733a3602..6a84792f3 100644 --- a/frontend/src/pages/Invite/NewUserModal/index.jsx +++ b/frontend/src/pages/Invite/NewUserModal/index.jsx @@ -31,71 +31,67 @@ export default function NewUserModal() { }; return ( - <dialog open={true} className="bg-transparent outline-none"> - <div className="relative w-full max-w-2xl max-h-full"> - <div className="relative bg-main-gradient rounded-lg shadow"> - <div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50"> - <h3 className="text-xl font-semibold text-white"> - Create a new account - </h3> - </div> - <form onSubmit={handleCreate}> - <div className="p-6 space-y-6 flex h-full w-full"> - <div className="w-full flex flex-col gap-y-4"> - <div> - <label - htmlFor="username" - className="block mb-2 text-sm font-medium text-white" - > - Username - </label> - <input - name="username" - type="text" - className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" - placeholder="My username" - minLength={2} - required={true} - autoComplete="off" - /> - </div> - <div> - <label - htmlFor="password" - className="block mb-2 text-sm font-medium text-white" - > - Password - </label> - <input - name="password" - type="password" - className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" - placeholder="Your password" - required={true} - minLength={8} - autoComplete="off" - /> - </div> - {error && ( - <p className="text-red-400 text-sm">Error: {error}</p> - )} - <p className="text-slate-200 text-xs md:text-sm"> - After creating your account you will be able to login with - these credentials and start using workspaces. - </p> + <div className="relative w-full max-w-2xl max-h-full"> + <div className="relative bg-main-gradient rounded-lg shadow"> + <div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50"> + <h3 className="text-xl font-semibold text-white"> + Create a new account + </h3> + </div> + <form onSubmit={handleCreate}> + <div className="p-6 space-y-6 flex h-full w-full"> + <div className="w-full flex flex-col gap-y-4"> + <div> + <label + htmlFor="username" + className="block mb-2 text-sm font-medium text-white" + > + Username + </label> + <input + name="username" + type="text" + className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" + placeholder="My username" + minLength={2} + required={true} + autoComplete="off" + /> </div> + <div> + <label + htmlFor="password" + className="block mb-2 text-sm font-medium text-white" + > + Password + </label> + <input + name="password" + type="password" + className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" + placeholder="Your password" + required={true} + minLength={8} + autoComplete="off" + /> + </div> + {error && <p className="text-red-400 text-sm">Error: {error}</p>} + <p className="text-slate-200 text-xs md:text-sm"> + After creating your account you will be able to login with these + credentials and start using workspaces. + </p> </div> - <div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50"> - <button - type="submit" - className="w-full transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800 text-center justify-center" - > - Accept Invitation - </button> - </div> - </form> - </div> + </div> + <div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50"> + <button + type="submit" + className="w-full transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800 text-center justify-center" + > + Accept Invitation + </button> + </div> + </form> </div> - </dialog> + </div> ); } diff --git a/frontend/src/pages/Invite/index.jsx b/frontend/src/pages/Invite/index.jsx index e44ff2359..a7522a54e 100644 --- a/frontend/src/pages/Invite/index.jsx +++ b/frontend/src/pages/Invite/index.jsx @@ -3,6 +3,7 @@ import { useParams } from "react-router-dom"; import { FullScreenLoader } from "@/components/Preloader"; import Invite from "@/models/invite"; import NewUserModal from "./NewUserModal"; +import ModalWrapper from "@/components/ModalWrapper"; export default function InvitePage() { const { code } = useParams(); @@ -47,7 +48,9 @@ export default function InvitePage() { return ( <div className="w-screen h-screen overflow-hidden bg-sidebar flex items-center justify-center"> - <NewUserModal /> + <ModalWrapper isOpen={true}> + <NewUserModal /> + </ModalWrapper> </div> ); } -- GitLab