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()}
- >
-
-
+
+
+
+
-
-
+
+
+ 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.
+
+
+
+
-
+
+
+
-
- Join a Guild
- Enter an invite below to join an existing guild.
-
+
+
+ Join Guild
+
-
-
-
-
-
-
- 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.
-
-
-
-
+
+
- {
- closeModal();
- }}
- >
- Cancel
-
-
-
- Leave
-
-
-
-
+ />
+
+
+
+
+ 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(
-
+