diff --git a/package.json b/package.json index 1070ef8..6915890 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@types/node": "^16.18.28", "@types/react": "^18.2.6", "@types/react-dom": "^18.2.4", + "framer-motion": "^10.16.1", "mobx": "^6.9.0", "mobx-react-lite": "^3.4.3", "react": "^18.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2eee85d..084bc6f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,9 @@ dependencies: '@types/react-dom': specifier: ^18.2.4 version: 18.2.4 + framer-motion: + specifier: ^10.16.1 + version: 10.16.1(react-dom@18.2.0)(react@18.2.0) mobx: specifier: ^6.9.0 version: 6.9.0 @@ -1626,12 +1629,26 @@ packages: resolution: {integrity: sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==} dev: false + /@emotion/is-prop-valid@0.8.8: + resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==} + requiresBuild: true + dependencies: + '@emotion/memoize': 0.7.4 + dev: false + optional: true + /@emotion/is-prop-valid@1.2.1: resolution: {integrity: sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==} dependencies: '@emotion/memoize': 0.8.1 dev: false + /@emotion/memoize@0.7.4: + resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==} + requiresBuild: true + dev: false + optional: true + /@emotion/memoize@0.8.1: resolution: {integrity: sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==} dev: false @@ -6032,6 +6049,24 @@ packages: map-cache: 0.2.2 dev: true + /framer-motion@10.16.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-K6TXr5mZtitC/dxQCBdg7xzdN0d5IAIrlaqCPKtIQVdzVPGC0qBuJKXggHX1vjnP5gPOFwB1KbCCTWcnFc3kWg==} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + tslib: 2.5.0 + optionalDependencies: + '@emotion/is-prop-valid': 0.8.8 + dev: false + /fresh@0.5.2: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} diff --git a/src/components/modals/AddServerModal.tsx b/src/components/modals/AddServerModal.tsx index f3cd960..c5106a5 100644 --- a/src/components/modals/AddServerModal.tsx +++ b/src/components/modals/AddServerModal.tsx @@ -4,14 +4,14 @@ import Icon from "../Icon"; import CreateServerModal from "./CreateServerModal"; import JoinServerModal from "./JoinServerModal"; import { + Modal, ModalActionItem, ModalCloseWrapper, - ModalContainer, ModalHeaderText, ModalSubHeaderText, - ModalWrapper, ModelContentContainer, } from "./ModalComponents"; +import { AnimatedModalProps } from "./ModalRenderer"; export const ModalHeader = styled.div` padding: 16px; @@ -39,66 +39,58 @@ const JoinButton = styled(ModalActionItem)` } `; -function AddServerModal() { +function AddServerModal(props: AnimatedModalProps) { const { openModal, closeModal } = useModals(); - if (!open) { - return null; - } - return ( - - - - - + /> + + - - Add a Guild - Lorem ipsum dolor sit amet, consectetur adipiscing elit. - + + Add a Guild + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + - - { - closeModal(); - openModal(CreateServerModal); - }} - > - Create a Guild - + + { + openModal(CreateServerModal); + }} + > + Create a Guild + - { - closeModal(); - openModal(JoinServerModal); - }} - > - Join a Guild - - - - + { + openModal(JoinServerModal); + }} + > + Join a Guild + + + ); } diff --git a/src/components/modals/CreateServerModal.tsx b/src/components/modals/CreateServerModal.tsx index 541409f..984e517 100644 --- a/src/components/modals/CreateServerModal.tsx +++ b/src/components/modals/CreateServerModal.tsx @@ -10,17 +10,16 @@ import { messageFromFieldError } from "../../utils/messageFromFieldError"; import { Input, InputErrorText, InputLabel, InputWrapper, LabelWrapper } from "../AuthComponents"; import { TextDivider } from "../Divider"; import Icon from "../Icon"; -import AddServerModal from "./AddServerModal"; import { + Modal, ModalActionItem, ModalCloseWrapper, - ModalContainer, ModalFooter, ModalHeaderText, ModalSubHeaderText, - ModalWrapper, ModelContentContainer, } from "./ModalComponents"; +import { AnimatedModalProps } from "./ModalRenderer"; export const ModalHeader = styled.div` margin-bottom: 30px; @@ -67,18 +66,14 @@ type FormValues = { name: string; }; -function CreateServerModal() { +function CreateServerModal(props: AnimatedModalProps) { const app = useAppStore(); const logger = useLogger("CreateServerModal"); - const { openModal, closeModal } = useModals(); + const { openModal, closeModal, closeAllModals } = useModals(); const [selectedFile, setSelectedFile] = React.useState(); const fileInputRef = React.useRef(null); const navigate = useNavigate(); - if (!open) { - return null; - } - const { register, handleSubmit, @@ -103,7 +98,7 @@ function CreateServerModal() { }) .then((r) => { navigate(`/channels/${r.id}`); - closeModal(); + closeAllModals(); }) .catch((r) => { if ("message" in r) { @@ -138,117 +133,114 @@ function CreateServerModal() { }); return ( - - - - + + + + Customize your guild + + Give your new guild a personality with a name and an icon. You can always change it later. + + + + + + + + + + + + + - - + fileInputRef.current?.click()} + > + + - - Customize your guild - - Give your new guild a personality with a name and an icon. You can always change it later. - - - - - - - - - - - - - + + + Guild Name + {errors.name && ( + + <> + - + {errors.name.message} + + + )} + + + - fileInputRef.current?.click()} - > - - + + + + -
- - - Guild Name - {errors.name && ( - - <> - - - {errors.name.message} - - - )} - - - - - -
-
+ + + Create + - - - Create - - - { - closeModal(); - openModal(AddServerModal); - }} - > - Back - - -
-
+ { + closeModal(); + }} + > + Back + + + ); } diff --git a/src/components/modals/ForgotPasswordModal.tsx b/src/components/modals/ForgotPasswordModal.tsx index f86fe98..db88136 100644 --- a/src/components/modals/ForgotPasswordModal.tsx +++ b/src/components/modals/ForgotPasswordModal.tsx @@ -2,14 +2,14 @@ import { useModals } from "@mattjennings/react-modal-stack"; import styled from "styled-components"; import Icon from "../Icon"; import { + Modal, ModalActionItem, ModalCloseWrapper, - ModalContainer, ModalFooter, ModalHeaderText, - ModalWrapper, ModelContentContainer, } from "./ModalComponents"; +import { AnimatedModalProps } from "./ModalRenderer"; export const ModalHeader = styled.div` padding: 16px; @@ -25,52 +25,46 @@ const SubmitButton = styled(ModalActionItem)` } `; -function ForgotPasswordModal() { - const { openModal, closeModal } = useModals(); - - if (!open) { - return null; - } +function ForgotPasswordModal(props: AnimatedModalProps) { + const { closeModal } = useModals(); return ( - - - - - + /> + + - - Instructions Sent - + + Instructions Sent + - - We sent instructions to change your password to user@example.com, please check both your inbox and - spam folder. - + + We sent instructions to change your password to user@example.com, please check both your inbox and spam + folder. + - - - Okay - - - - + + + Okay + + + ); } diff --git a/src/components/modals/JoinServerModal.tsx b/src/components/modals/JoinServerModal.tsx index e4e22de..4008780 100644 --- a/src/components/modals/JoinServerModal.tsx +++ b/src/components/modals/JoinServerModal.tsx @@ -11,15 +11,15 @@ import { TextDivider } from "../Divider"; import Icon from "../Icon"; import AddServerModal from "./AddServerModal"; import { + Modal, ModalActionItem, ModalCloseWrapper, - ModalContainer, ModalFooter, ModalHeaderText, ModalSubHeaderText, - ModalWrapper, ModelContentContainer, } from "./ModalComponents"; +import { AnimatedModalProps } from "./ModalRenderer"; export const ModalHeader = styled.div` padding: 16px; @@ -34,16 +34,12 @@ type FormValues = { code: string; }; -function JoinServerModal() { +function JoinServerModal(props: AnimatedModalProps) { const logger = useLogger("JoinServerModal"); - const { openModal, closeModal } = useModals(); + const { openModal, closeAllModals } = useModals(); const app = useAppStore(); const navigate = useNavigate(); - if (!open) { - return null; - } - const { register, handleSubmit, @@ -59,7 +55,7 @@ function JoinServerModal() { .post(Routes.invite(code)) .then((r) => { navigate(`/channels/${r.guild_id}/${r.channel_id}`); - closeModal(); + closeAllModals(); }) .catch((r) => { if ("message" in r) { @@ -94,81 +90,78 @@ function JoinServerModal() { }); return ( - - - - + + + + Join a Guild + Enter an invite below to join an existing guild. + + + +
+ + + Invite Link + + {errors.code && ( + + <> + - + {errors.code.message} + + + )} + + - - + +
+
- - Join a Guild - Enter an invite below to join an existing guild. - + + + Join Guild + - -
- - - Invite Link - - {errors.code && ( - - <> - - - {errors.code.message} - - - )} - - - -
-
- - - - Join Guild - - - { - closeModal(); - openModal(AddServerModal); - }} - > - Back - - -
-
+ { + openModal(AddServerModal); + }} + > + Back + + + ); } diff --git a/src/components/modals/LeaveServerModal.tsx b/src/components/modals/LeaveServerModal.tsx index 56b5d33..c7b0c02 100644 --- a/src/components/modals/LeaveServerModal.tsx +++ b/src/components/modals/LeaveServerModal.tsx @@ -6,14 +6,14 @@ import { useAppStore } from "../../stores/AppStore"; import Guild from "../../stores/objects/Guild"; import Icon from "../Icon"; import { + Modal, ModalActionItem, ModalCloseWrapper, - ModalContainer, ModalFooter, ModalHeaderText, - ModalWrapper, ModelContentContainer, } from "./ModalComponents"; +import { AnimatedModalProps } from "./ModalRenderer"; export const ModalHeader = styled.div` padding: 16px; @@ -47,7 +47,7 @@ interface Props { guild: Guild; } -function LeaveServerModal({ guild }: Props) { +function LeaveServerModal(props: Props & AnimatedModalProps) { const app = useAppStore(); const { closeModal } = useModals(); const navigate = useNavigate(); @@ -57,7 +57,7 @@ function LeaveServerModal({ guild }: Props) { } const handleLeaveServer = () => { - app.rest.delete(Routes.userGuild(guild.id)).finally(() => { + app.rest.delete(Routes.userGuild(props.guild.id)).finally(() => { closeModal(); // navigate to @me navigate("channels/@me"); @@ -65,68 +65,66 @@ function LeaveServerModal({ guild }: Props) { }; return ( - - - - - - - - Leave {guild.name} - - - - - Are you sure you want to leave {guild.name}? You won't be able to rejoin this server - unless you are re-invited. - - - - + + + + + + Leave {props.guild.name} + + + + + Are you sure you want to leave {props.guild.name}? You won't be able to rejoin this server + unless you are re-invited. + + + + + { + closeModal(); + }} + > + Cancel + + + + Leave + + + ); } diff --git a/src/components/modals/ModalComponents.tsx b/src/components/modals/ModalComponents.tsx index 45e00ac..1fb915d 100644 --- a/src/components/modals/ModalComponents.tsx +++ b/src/components/modals/ModalComponents.tsx @@ -1,9 +1,12 @@ +import { type StackedModalProps } from "@mattjennings/react-modal-stack"; +import { AnimatePresence, motion } from "framer-motion"; +import React from "react"; import styled from "styled-components"; /** * Main container for all modals, handles the background overlay and positioning */ -export const ModalContainer = styled.div` +export const ModalBase = styled(motion.div)` z-index: 100; position: fixed; top: 0; @@ -13,22 +16,22 @@ export const ModalContainer = styled.div` display: flex; justify-content: center; align-items: center; - &::before { - content: ""; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: black; - opacity: 0.85; - } + // &::before { + // content: ""; + // position: absolute; + // top: 0; + // left: 0; + // right: 0; + // bottom: 0; + // background-color: black; + // opacity: 0.85; + // } `; /** * Wrapper for modal content, handles the sizing and positioning */ -export const ModalWrapper = styled.div<{ full?: boolean }>` +export const ModalWrapper = styled(motion.div)<{ full?: boolean }>` width: ${(props) => (props.full ? "100%" : "440px")}; height: ${(props) => (props.full ? "100%" : "auto")}; border-radius: 4px; @@ -175,3 +178,35 @@ export const ModalFullContent = styled.div` align-items: flex-start; background-color: var(--background-primary); `; + +interface ModalProps extends StackedModalProps { + children: React.ReactNode; + full?: boolean; +} + +export function Modal(props: ModalProps) { + return ( + + {props.open && ( + + {props.children} + + )} + + ); +} diff --git a/src/components/modals/ModalRenderer.tsx b/src/components/modals/ModalRenderer.tsx new file mode 100644 index 0000000..62a94c4 --- /dev/null +++ b/src/components/modals/ModalRenderer.tsx @@ -0,0 +1,76 @@ +import { ModalStackValue } from "@mattjennings/react-modal-stack"; +import { AnimatePresence, motion } from "framer-motion"; +import React from "react"; + +export type AnimatedModalProps = { + open: boolean; +}; + +function ModalRenderer({ stack }: ModalStackValue) { + const [displayedStack, setDisplayedStack] = React.useState(stack); + const [isOpen, setOpen] = React.useState(false); + + React.useEffect(() => { + console.log(stack.length, displayedStack.length); + // we're opening the first modal, so update the stack right away + if (stack.length === 1 && displayedStack.length === 0) { + setOpen(true); + setDisplayedStack(stack); + } + // stack updated, trigger a dismissal of the current modal + else { + setOpen(false); + } + }, [stack]); + + return ( + <> + + {stack.length > 0 && ( + + )} + + {displayedStack.map((modal, index) => ( + { + // set open state for next modal + if (stack.length > 0) { + setOpen(true); + } else { + setOpen(false); + } + + // update displayed stack + // setTimeout is a hack to prevent a warning about updating state + // in an unmounted component (I can't figure out why it happens, or why this fixes it) + setTimeout(() => setDisplayedStack(stack)); + + modal.props?.onAnimationComplete?.(); + }} + {...modal.props} + /> + ))} + + ); +} + +export default ModalRenderer; diff --git a/src/components/modals/SettingsModal.tsx b/src/components/modals/SettingsModal.tsx index c92ac1e..2d0e136 100644 --- a/src/components/modals/SettingsModal.tsx +++ b/src/components/modals/SettingsModal.tsx @@ -1,39 +1,38 @@ import { useModals } from "@mattjennings/react-modal-stack"; import Icon from "../Icon"; -import { ModalCloseWrapper, ModalContainer, ModalFullContent, ModalFullSidebar, ModalWrapper } from "./ModalComponents"; +import { Modal, ModalCloseWrapper, ModalFullContent, ModalFullSidebar } from "./ModalComponents"; +import { AnimatedModalProps } from "./ModalRenderer"; -function SettingsModal() { +function SettingsModal(props: AnimatedModalProps) { const { closeModal } = useModals(); return ( - - - Sidebar + + Sidebar - - - - - Content - - - + /> + + + Content + + ); } diff --git a/src/index.tsx b/src/index.tsx index 5ebef52..16894bd 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -15,6 +15,7 @@ import { ModalStack } from "@mattjennings/react-modal-stack"; import ReactDOM from "react-dom/client"; import { BrowserRouter } from "react-router-dom"; import App from "./App"; +import ModalRenderer from "./components/modals/ModalRenderer"; import { ContextMenuContextProvider } from "./contexts/ContextMenuContext"; import Theme from "./contexts/Theme"; import "./index.css"; @@ -22,7 +23,7 @@ import "./index.css"; const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement); root.render( - +