1
0
mirror of https://github.com/spacebarchat/client.git synced 2024-11-21 18:02:32 +01:00

animated modals

This commit is contained in:
Puyodead1 2023-08-29 12:28:14 -04:00
parent 2dee50f9ce
commit 807a7dc8e3
No known key found for this signature in database
GPG Key ID: A4FA4FEC0DD353FC
11 changed files with 505 additions and 389 deletions

View File

@ -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",

View File

@ -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'}

View File

@ -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 (
<ModalContainer>
<ModalWrapper>
<ModalCloseWrapper>
<button
onClick={closeModal}
<Modal {...props}>
<ModalCloseWrapper>
<button
onClick={closeModal}
style={{
background: "none",
border: "none",
outline: "none",
}}
>
<Icon
icon="mdiClose"
size={1}
style={{
background: "none",
border: "none",
outline: "none",
cursor: "pointer",
color: "var(--text)",
}}
>
<Icon
icon="mdiClose"
size={1}
style={{
cursor: "pointer",
color: "var(--text)",
}}
/>
</button>
</ModalCloseWrapper>
/>
</button>
</ModalCloseWrapper>
<ModalHeader>
<ModalHeaderText>Add a Guild</ModalHeaderText>
<ModalSubHeaderText>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</ModalSubHeaderText>
</ModalHeader>
<ModalHeader>
<ModalHeaderText>Add a Guild</ModalHeaderText>
<ModalSubHeaderText>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</ModalSubHeaderText>
</ModalHeader>
<ModelContentContainer>
<CreateButton
variant="filled"
size="med"
onClick={() => {
closeModal();
openModal(CreateServerModal);
}}
>
Create a Guild
</CreateButton>
<ModelContentContainer>
<CreateButton
variant="filled"
size="med"
onClick={() => {
openModal(CreateServerModal);
}}
>
Create a Guild
</CreateButton>
<JoinButton
variant="outlined"
size="med"
onClick={() => {
closeModal();
openModal(JoinServerModal);
}}
>
Join a Guild
</JoinButton>
</ModelContentContainer>
</ModalWrapper>
</ModalContainer>
<JoinButton
variant="outlined"
size="med"
onClick={() => {
openModal(JoinServerModal);
}}
>
Join a Guild
</JoinButton>
</ModelContentContainer>
</Modal>
);
}

View File

@ -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<File>();
const fileInputRef = React.useRef<HTMLInputElement>(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 (
<ModalContainer>
<ModalWrapper>
<ModalCloseWrapper>
<button
onClick={closeModal}
<Modal {...props}>
<ModalCloseWrapper>
<button
onClick={closeAllModals}
style={{
background: "none",
border: "none",
outline: "none",
}}
>
<Icon
icon="mdiClose"
size={1}
style={{
background: "none",
border: "none",
outline: "none",
cursor: "pointer",
color: "var(--text)",
}}
>
<Icon
icon="mdiClose"
size={1}
style={{
cursor: "pointer",
color: "var(--text)",
}}
/>
</button>
</ModalCloseWrapper>
<ModalHeader>
<ModalHeaderText>Customize your guild</ModalHeaderText>
<ModalSubHeaderText>
Give your new guild a personality with a name and an icon. You can always change it later.
</ModalSubHeaderText>
</ModalHeader>
<ModelContentContainer>
<UploadIcon>
<IconContainer>
<svg width="80" height="80" viewBox="0 0 80 80" fill="none">
<path
fillRule="evenodd"
clipRule="evenodd"
d="m 39.88 78.32 c 4.0666 0 8.0467 -0.648 11.8282 -1.9066 l -0.9101 -2.7331 c -3.4906 1.1606 -7.1626 1.7597 -10.921 1.7597 v 2.88 m 17.4528 -4.3056 c 3.5539 -1.8749 6.7824 -4.3142 9.5645 -7.2115 l -2.0765 -1.993 c -2.569 2.6755 -5.5526 4.9277 -8.833 6.6586 l 1.345 2.5459 m 13.4208 -11.9434 c 2.2752 -3.3091 4.0061 -6.9638 5.1178 -10.8346 l -2.7677 -0.7949 c -1.0253 3.5712 -2.6237 6.9437 -4.7232 9.9965 l 2.3731 1.633 m 6.2899 -16.5917 c 0.1814 -1.4918 0.2765 -2.9981 0.2794 -4.5158 c 0 -2.5805 -0.2448 -5.063 -0.7344 -7.4966 l -2.8224 0.5674 c 0.4522 2.2464 0.6797 4.5389 0.6797 6.9264 c -0.0029 1.3997 -0.0893 2.7936 -0.2592 4.1702 l 2.8598 0.3514 m -2.1254 -17.8243 c -1.4198 -3.7642 -3.4416 -7.2662 -5.976 -10.3824 l -2.2349 1.8173 c 2.3386 2.8771 4.2048 6.1114 5.5181 9.5818 l 2.6928 -1.0166 m -10.1923 -14.783 c -3.0038 -2.6669 -6.4195 -4.8384 -10.1146 -6.4195 l -1.1347 2.6467 c 3.4099 1.4602 6.5635 3.4646 9.337 5.927 l 1.9123 -2.1542 m -15.7334 -8.3117 c -2.9146 -0.7286 -5.927 -1.1059 -8.9827 -1.1174 c -1.0886 0 -2.065 0.0374 -3.0499 0.1123 l 0.2218 2.8714 c 0.9101 -0.0691 1.8115 -0.1037 2.8224 -0.1037 c 2.8195 0.0086 5.5987 0.3571 8.2915 1.031 l 0.6998 -2.7936 m -17.9654 -0.0634 c -3.9197 0.9504 -7.6406 2.5286 -11.0362 4.6627 l 1.5322 2.4394 c 3.1363 -1.9699 6.5693 -3.4243 10.1837 -4.3027 l -0.6797 -2.7994 m -15.889 8.2886 c -3.0154 2.6554 -5.5872 5.7888 -7.6118 9.2506 l 2.4883 1.4515 c 1.8691 -3.2026 4.2451 -6.0883 7.0301 -8.5421 l -1.9037 -2.16 m -10.1952 14.6189 c -1.4371 3.7238 -2.2723 7.6723 -2.4595 11.7274 l 2.8771 0.1325 c 0.1728 -3.744 0.9446 -7.3843 2.2694 -10.823 l -2.687 -1.0368 m -2.2464 17.8618 c 0.4694 4.0205 1.5811 7.9027 3.2832 11.52 l 2.6064 -1.224 c -1.5696 -3.3408 -2.5978 -6.9235 -3.0298 -10.633 l -2.8598 0.3341 m 6.2698 16.7443 c 2.2694 3.3149 5.0573 6.2467 8.2541 8.6688 l 1.7453 -2.2925 c -2.952 -2.2464 -5.5267 -4.9565 -7.6205 -8.015 l -2.376 1.6272 m 13.4352 11.9923 c 3.5424 1.872 7.3699 3.168 11.3558 3.8246 l 0.4666 -2.8426 c -3.6806 -0.6048 -7.2086 -1.8 -10.4774 -3.528 l -1.3478 2.5459 m 17.3376 4.3229 c 0.0691 0 0.0691 0 0.1382 0 v -2.88 c -0.0634 0 -0.0634 0 -0.1267 0 l -0.0115 2.88"
fill="currentColor"
></path>
<path
d="M40 29C37.794 29 36 30.794 36 33C36 35.207 37.794 37 40 37C42.206 37 44 35.207 44 33C44 30.795 42.206 29 40 29Z"
fill="currentColor"
></path>
<path
d="M48 26.001H46.07C45.402 26.001 44.777 25.667 44.406 25.111L43.594 23.891C43.223 23.335 42.598 23 41.93 23H38.07C37.402 23 36.777 23.335 36.406 23.89L35.594 25.11C35.223 25.667 34.598 26 33.93 26H32C30.895 26 30 26.896 30 28V39C30 40.104 30.895 41 32 41H48C49.104 41 50 40.104 50 39V28C50 26.897 49.104 26.001 48 26.001ZM40 39C36.691 39 34 36.309 34 33C34 29.692 36.691 27 40 27C43.309 27 46 29.692 46 33C46 36.31 43.309 39 40 39Z"
fill="currentColor"
></path>
<path
d="M24.6097 52.712V47.72H22.5457V52.736C22.5457 53.792 22.0777 54.404 21.1417 54.404C20.2177 54.404 19.7377 53.78 19.7377 52.712V47.72H17.6737V52.724C17.6737 55.04 19.0897 56.132 21.1177 56.132C23.1217 56.132 24.6097 55.016 24.6097 52.712ZM26.0314 56H28.0834V53.252H28.6114C30.6154 53.252 31.9474 52.292 31.9474 50.42C31.9474 48.62 30.7114 47.72 28.6954 47.72H26.0314V56ZM29.9554 50.456C29.9554 51.308 29.4514 51.704 28.5394 51.704H28.0594V49.268H28.5754C29.4874 49.268 29.9554 49.664 29.9554 50.456ZM37.8292 56L37.5532 54.224H35.0092V47.72H32.9572V56H37.8292ZM45.9558 51.848C45.9558 49.292 44.4078 47.564 42.0078 47.564C39.6078 47.564 38.0478 49.304 38.0478 51.872C38.0478 54.428 39.6078 56.156 41.9838 56.156C44.3958 56.156 45.9558 54.404 45.9558 51.848ZM43.8918 51.86C43.8918 53.504 43.1958 54.548 41.9958 54.548C40.8078 54.548 40.0998 53.504 40.0998 51.86C40.0998 50.216 40.8078 49.172 41.9958 49.172C43.1958 49.172 43.8918 50.216 43.8918 51.86ZM52.2916 56.084L54.3676 55.748L51.4876 47.684H49.2316L46.2556 56H48.2716L48.8236 54.284H51.6916L52.2916 56.084ZM50.2516 49.796L51.1756 52.676H49.3156L50.2516 49.796ZM62.5174 51.848C62.5174 49.388 61.0174 47.72 58.1374 47.72H55.2814V56H58.1854C60.9814 56 62.5174 54.308 62.5174 51.848ZM60.4534 51.86C60.4534 53.636 59.5414 54.404 58.0774 54.404H57.3334V49.316H58.0774C59.4814 49.316 60.4534 50.12 60.4534 51.86Z"
fill="currentColor"
></path>
</svg>
<IconInput
ref={fileInputRef}
type="file"
name="icon"
accept="image/*"
onChange={onIconChange}
/>
</button>
</ModalCloseWrapper>
<FileInput
role="button"
// disabled until I get the motiviation to not make it shit, I don't really want to use an invisible input
onClick={() => fileInputRef.current?.click()}
></FileInput>
</IconContainer>
</UploadIcon>
<ModalHeader>
<ModalHeaderText>Customize your guild</ModalHeaderText>
<ModalSubHeaderText>
Give your new guild a personality with a name and an icon. You can always change it later.
</ModalSubHeaderText>
</ModalHeader>
<ModelContentContainer>
<UploadIcon>
<IconContainer>
<svg width="80" height="80" viewBox="0 0 80 80" fill="none">
<path
fillRule="evenodd"
clipRule="evenodd"
d="m 39.88 78.32 c 4.0666 0 8.0467 -0.648 11.8282 -1.9066 l -0.9101 -2.7331 c -3.4906 1.1606 -7.1626 1.7597 -10.921 1.7597 v 2.88 m 17.4528 -4.3056 c 3.5539 -1.8749 6.7824 -4.3142 9.5645 -7.2115 l -2.0765 -1.993 c -2.569 2.6755 -5.5526 4.9277 -8.833 6.6586 l 1.345 2.5459 m 13.4208 -11.9434 c 2.2752 -3.3091 4.0061 -6.9638 5.1178 -10.8346 l -2.7677 -0.7949 c -1.0253 3.5712 -2.6237 6.9437 -4.7232 9.9965 l 2.3731 1.633 m 6.2899 -16.5917 c 0.1814 -1.4918 0.2765 -2.9981 0.2794 -4.5158 c 0 -2.5805 -0.2448 -5.063 -0.7344 -7.4966 l -2.8224 0.5674 c 0.4522 2.2464 0.6797 4.5389 0.6797 6.9264 c -0.0029 1.3997 -0.0893 2.7936 -0.2592 4.1702 l 2.8598 0.3514 m -2.1254 -17.8243 c -1.4198 -3.7642 -3.4416 -7.2662 -5.976 -10.3824 l -2.2349 1.8173 c 2.3386 2.8771 4.2048 6.1114 5.5181 9.5818 l 2.6928 -1.0166 m -10.1923 -14.783 c -3.0038 -2.6669 -6.4195 -4.8384 -10.1146 -6.4195 l -1.1347 2.6467 c 3.4099 1.4602 6.5635 3.4646 9.337 5.927 l 1.9123 -2.1542 m -15.7334 -8.3117 c -2.9146 -0.7286 -5.927 -1.1059 -8.9827 -1.1174 c -1.0886 0 -2.065 0.0374 -3.0499 0.1123 l 0.2218 2.8714 c 0.9101 -0.0691 1.8115 -0.1037 2.8224 -0.1037 c 2.8195 0.0086 5.5987 0.3571 8.2915 1.031 l 0.6998 -2.7936 m -17.9654 -0.0634 c -3.9197 0.9504 -7.6406 2.5286 -11.0362 4.6627 l 1.5322 2.4394 c 3.1363 -1.9699 6.5693 -3.4243 10.1837 -4.3027 l -0.6797 -2.7994 m -15.889 8.2886 c -3.0154 2.6554 -5.5872 5.7888 -7.6118 9.2506 l 2.4883 1.4515 c 1.8691 -3.2026 4.2451 -6.0883 7.0301 -8.5421 l -1.9037 -2.16 m -10.1952 14.6189 c -1.4371 3.7238 -2.2723 7.6723 -2.4595 11.7274 l 2.8771 0.1325 c 0.1728 -3.744 0.9446 -7.3843 2.2694 -10.823 l -2.687 -1.0368 m -2.2464 17.8618 c 0.4694 4.0205 1.5811 7.9027 3.2832 11.52 l 2.6064 -1.224 c -1.5696 -3.3408 -2.5978 -6.9235 -3.0298 -10.633 l -2.8598 0.3341 m 6.2698 16.7443 c 2.2694 3.3149 5.0573 6.2467 8.2541 8.6688 l 1.7453 -2.2925 c -2.952 -2.2464 -5.5267 -4.9565 -7.6205 -8.015 l -2.376 1.6272 m 13.4352 11.9923 c 3.5424 1.872 7.3699 3.168 11.3558 3.8246 l 0.4666 -2.8426 c -3.6806 -0.6048 -7.2086 -1.8 -10.4774 -3.528 l -1.3478 2.5459 m 17.3376 4.3229 c 0.0691 0 0.0691 0 0.1382 0 v -2.88 c -0.0634 0 -0.0634 0 -0.1267 0 l -0.0115 2.88"
fill="currentColor"
></path>
<path
d="M40 29C37.794 29 36 30.794 36 33C36 35.207 37.794 37 40 37C42.206 37 44 35.207 44 33C44 30.795 42.206 29 40 29Z"
fill="currentColor"
></path>
<path
d="M48 26.001H46.07C45.402 26.001 44.777 25.667 44.406 25.111L43.594 23.891C43.223 23.335 42.598 23 41.93 23H38.07C37.402 23 36.777 23.335 36.406 23.89L35.594 25.11C35.223 25.667 34.598 26 33.93 26H32C30.895 26 30 26.896 30 28V39C30 40.104 30.895 41 32 41H48C49.104 41 50 40.104 50 39V28C50 26.897 49.104 26.001 48 26.001ZM40 39C36.691 39 34 36.309 34 33C34 29.692 36.691 27 40 27C43.309 27 46 29.692 46 33C46 36.31 43.309 39 40 39Z"
fill="currentColor"
></path>
<path
d="M24.6097 52.712V47.72H22.5457V52.736C22.5457 53.792 22.0777 54.404 21.1417 54.404C20.2177 54.404 19.7377 53.78 19.7377 52.712V47.72H17.6737V52.724C17.6737 55.04 19.0897 56.132 21.1177 56.132C23.1217 56.132 24.6097 55.016 24.6097 52.712ZM26.0314 56H28.0834V53.252H28.6114C30.6154 53.252 31.9474 52.292 31.9474 50.42C31.9474 48.62 30.7114 47.72 28.6954 47.72H26.0314V56ZM29.9554 50.456C29.9554 51.308 29.4514 51.704 28.5394 51.704H28.0594V49.268H28.5754C29.4874 49.268 29.9554 49.664 29.9554 50.456ZM37.8292 56L37.5532 54.224H35.0092V47.72H32.9572V56H37.8292ZM45.9558 51.848C45.9558 49.292 44.4078 47.564 42.0078 47.564C39.6078 47.564 38.0478 49.304 38.0478 51.872C38.0478 54.428 39.6078 56.156 41.9838 56.156C44.3958 56.156 45.9558 54.404 45.9558 51.848ZM43.8918 51.86C43.8918 53.504 43.1958 54.548 41.9958 54.548C40.8078 54.548 40.0998 53.504 40.0998 51.86C40.0998 50.216 40.8078 49.172 41.9958 49.172C43.1958 49.172 43.8918 50.216 43.8918 51.86ZM52.2916 56.084L54.3676 55.748L51.4876 47.684H49.2316L46.2556 56H48.2716L48.8236 54.284H51.6916L52.2916 56.084ZM50.2516 49.796L51.1756 52.676H49.3156L50.2516 49.796ZM62.5174 51.848C62.5174 49.388 61.0174 47.72 58.1374 47.72H55.2814V56H58.1854C60.9814 56 62.5174 54.308 62.5174 51.848ZM60.4534 51.86C60.4534 53.636 59.5414 54.404 58.0774 54.404H57.3334V49.316H58.0774C59.4814 49.316 60.4534 50.12 60.4534 51.86Z"
fill="currentColor"
></path>
</svg>
<IconInput
ref={fileInputRef}
type="file"
name="icon"
accept="image/*"
onChange={onIconChange}
<form>
<InputContainer>
<LabelWrapper error={!!errors.name}>
<InputLabel>Guild Name</InputLabel>
{errors.name && (
<InputErrorText>
<>
<TextDivider>-</TextDivider>
{errors.name.message}
</>
</InputErrorText>
)}
</LabelWrapper>
<InputWrapper>
<Input
autoFocus
{...register("name", { required: true })}
placeholder="Guild Name"
error={!!errors.name}
disabled={isLoading}
/>
<FileInput
role="button"
// disabled until I get the motiviation to not make it shit, I don't really want to use an invisible input
onClick={() => fileInputRef.current?.click()}
></FileInput>
</IconContainer>
</UploadIcon>
</InputWrapper>
</InputContainer>
</form>
</ModelContentContainer>
<form>
<InputContainer>
<LabelWrapper error={!!errors.name}>
<InputLabel>Guild Name</InputLabel>
{errors.name && (
<InputErrorText>
<>
<TextDivider>-</TextDivider>
{errors.name.message}
</>
</InputErrorText>
)}
</LabelWrapper>
<InputWrapper>
<Input
autoFocus
{...register("name", { required: true })}
placeholder="Guild Name"
error={!!errors.name}
disabled={isLoading}
/>
</InputWrapper>
</InputContainer>
</form>
</ModelContentContainer>
<ModalFooter>
<ModalActionItem variant="filled" size="med" onClick={onSubmit} disabled={isLoading}>
Create
</ModalActionItem>
<ModalFooter>
<ModalActionItem variant="filled" size="med" onClick={onSubmit} disabled={isLoading}>
Create
</ModalActionItem>
<ModalActionItem
variant="link"
size="min"
onClick={() => {
closeModal();
openModal(AddServerModal);
}}
>
Back
</ModalActionItem>
</ModalFooter>
</ModalWrapper>
</ModalContainer>
<ModalActionItem
variant="link"
size="min"
onClick={() => {
closeModal();
}}
>
Back
</ModalActionItem>
</ModalFooter>
</Modal>
);
}

View File

@ -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 (
<ModalContainer>
<ModalWrapper>
<ModalCloseWrapper>
<button
onClick={closeModal}
<Modal {...props}>
<ModalCloseWrapper>
<button
onClick={closeModal}
style={{
background: "none",
border: "none",
outline: "none",
}}
>
<Icon
icon="mdiClose"
size={1}
style={{
background: "none",
border: "none",
outline: "none",
cursor: "pointer",
color: "var(--text)",
}}
>
<Icon
icon="mdiClose"
size={1}
style={{
cursor: "pointer",
color: "var(--text)",
}}
/>
</button>
</ModalCloseWrapper>
/>
</button>
</ModalCloseWrapper>
<ModalHeader>
<ModalHeaderText>Instructions Sent</ModalHeaderText>
</ModalHeader>
<ModalHeader>
<ModalHeaderText>Instructions Sent</ModalHeaderText>
</ModalHeader>
<ModelContentContainer>
We sent instructions to change your password to user@example.com, please check both your inbox and
spam folder.
</ModelContentContainer>
<ModelContentContainer>
We sent instructions to change your password to user@example.com, please check both your inbox and spam
folder.
</ModelContentContainer>
<ModalFooter>
<SubmitButton variant="filled" size="med" onClick={closeModal}>
Okay
</SubmitButton>
</ModalFooter>
</ModalWrapper>
</ModalContainer>
<ModalFooter>
<SubmitButton variant="filled" size="med" onClick={closeModal}>
Okay
</SubmitButton>
</ModalFooter>
</Modal>
);
}

View File

@ -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<never, { guild_id: string; channel_id: string }>(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 (
<ModalContainer>
<ModalWrapper>
<ModalCloseWrapper>
<button
onClick={closeModal}
<Modal {...props}>
<ModalCloseWrapper>
<button
onClick={closeAllModals}
style={{
background: "none",
border: "none",
outline: "none",
}}
>
<Icon
icon="mdiClose"
size={1}
style={{
background: "none",
border: "none",
outline: "none",
cursor: "pointer",
color: "var(--text)",
}}
>
<Icon
icon="mdiClose"
size={1}
style={{
cursor: "pointer",
color: "var(--text)",
}}
/>
</button>
</ModalCloseWrapper>
<ModalHeader>
<ModalHeaderText>Join a Guild</ModalHeaderText>
<ModalSubHeaderText>Enter an invite below to join an existing guild.</ModalSubHeaderText>
</ModalHeader>
<ModelContentContainer>
<form>
<InviteInputContainer>
<LabelWrapper error={!!errors.code}>
<InputLabel>Invite Link</InputLabel>
{errors.code && (
<InputErrorText>
<>
<TextDivider>-</TextDivider>
{errors.code.message}
</>
</InputErrorText>
)}
</LabelWrapper>
<Input
{...register("code", { required: true })}
placeholder="https://app.spacebar.chat/invite/cool-guild"
type="text"
maxLength={9999}
required
error={!!errors.code}
disabled={isLoading}
autoFocus
minLength={6}
/>
</button>
</ModalCloseWrapper>
</InviteInputContainer>
</form>
</ModelContentContainer>
<ModalHeader>
<ModalHeaderText>Join a Guild</ModalHeaderText>
<ModalSubHeaderText>Enter an invite below to join an existing guild.</ModalSubHeaderText>
</ModalHeader>
<ModalFooter>
<ModalActionItem variant="filled" size="med" onClick={onSubmit}>
Join Guild
</ModalActionItem>
<ModelContentContainer>
<form>
<InviteInputContainer>
<LabelWrapper error={!!errors.code}>
<InputLabel>Invite Link</InputLabel>
{errors.code && (
<InputErrorText>
<>
<TextDivider>-</TextDivider>
{errors.code.message}
</>
</InputErrorText>
)}
</LabelWrapper>
<Input
{...register("code", { required: true })}
placeholder="https://app.spacebar.chat/invite/cool-guild"
type="text"
maxLength={9999}
required
error={!!errors.code}
disabled={isLoading}
autoFocus
minLength={6}
/>
</InviteInputContainer>
</form>
</ModelContentContainer>
<ModalFooter>
<ModalActionItem variant="filled" size="med" onClick={onSubmit}>
Join Guild
</ModalActionItem>
<ModalActionItem
variant="link"
size="min"
onClick={() => {
closeModal();
openModal(AddServerModal);
}}
>
Back
</ModalActionItem>
</ModalFooter>
</ModalWrapper>
</ModalContainer>
<ModalActionItem
variant="link"
size="min"
onClick={() => {
openModal(AddServerModal);
}}
>
Back
</ModalActionItem>
</ModalFooter>
</Modal>
);
}

View File

@ -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 (
<ModalContainer>
<ModalWrapper>
<ModalCloseWrapper>
<button
onClick={closeModal}
style={{
background: "none",
border: "none",
outline: "none",
}}
>
<Icon
icon="mdiClose"
size={1}
style={{
cursor: "pointer",
color: "var(--text)",
}}
/>
</button>
</ModalCloseWrapper>
<ModalHeader>
<ModalHeaderText>Leave {guild.name}</ModalHeaderText>
</ModalHeader>
<ModelContentContainer>
<span>
Are you sure you want to leave <b>{guild.name}</b>? You won't be able to rejoin this server
unless you are re-invited.
</span>
</ModelContentContainer>
<ModalFooter
<Modal {...props}>
<ModalCloseWrapper>
<button
onClick={closeModal}
style={{
flexDirection: "row",
justifyContent: "flex-end",
background: "none",
border: "none",
outline: "none",
}}
>
<CancelButton
variant="link"
size="med"
onClick={() => {
closeModal();
}}
>
Cancel
</CancelButton>
<LeaveButton
variant="outlined"
size="med"
onClick={handleLeaveServer}
<Icon
icon="mdiClose"
size={1}
style={{
backgroundColor: "var(--danger)",
cursor: "pointer",
color: "var(--text)",
}}
>
Leave
</LeaveButton>
</ModalFooter>
</ModalWrapper>
</ModalContainer>
/>
</button>
</ModalCloseWrapper>
<ModalHeader>
<ModalHeaderText>Leave {props.guild.name}</ModalHeaderText>
</ModalHeader>
<ModelContentContainer>
<span>
Are you sure you want to leave <b>{props.guild.name}</b>? You won't be able to rejoin this server
unless you are re-invited.
</span>
</ModelContentContainer>
<ModalFooter
style={{
flexDirection: "row",
justifyContent: "flex-end",
}}
>
<CancelButton
variant="link"
size="med"
onClick={() => {
closeModal();
}}
>
Cancel
</CancelButton>
<LeaveButton
variant="outlined"
size="med"
onClick={handleLeaveServer}
style={{
backgroundColor: "var(--danger)",
}}
>
Leave
</LeaveButton>
</ModalFooter>
</Modal>
);
}

View File

@ -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 (
<AnimatePresence>
{props.open && (
<ModalBase
variants={{
show: {
opacity: 1,
scale: 1,
},
hide: {
opacity: 0,
scale: 0,
},
}}
initial="hide"
animate="show"
exit="hide"
{...props}
>
<ModalWrapper full={props.full}>{props.children}</ModalWrapper>
</ModalBase>
)}
</AnimatePresence>
);
}

View File

@ -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 (
<>
<AnimatePresence>
{stack.length > 0 && (
<motion.div
style={{
zIndex: 90,
position: `fixed`,
top: 0,
left: 0,
right: 0,
bottom: 0,
background: "rgba(0,0,0,0.8)",
}}
variants={{
show: { opacity: 1 },
hide: { opacity: 0 },
}}
initial="hide"
animate="show"
exit="hide"
/>
)}
</AnimatePresence>
{displayedStack.map((modal, index) => (
<modal.component
key={index}
open={index === displayedStack.length - 1 && isOpen}
onAnimationComplete={() => {
// 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;

View File

@ -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 (
<ModalContainer>
<ModalWrapper full>
<ModalFullSidebar>Sidebar</ModalFullSidebar>
<Modal full {...props}>
<ModalFullSidebar>Sidebar</ModalFullSidebar>
<ModalFullContent>
<ModalCloseWrapper>
<button
onClick={closeModal}
<ModalFullContent>
<ModalCloseWrapper>
<button
onClick={closeModal}
style={{
background: "none",
border: "none",
outline: "none",
}}
>
<Icon
icon="mdiClose"
size={1}
style={{
background: "none",
border: "none",
outline: "none",
cursor: "pointer",
color: "var(--text)",
}}
>
<Icon
icon="mdiClose"
size={1}
style={{
cursor: "pointer",
color: "var(--text)",
}}
/>
</button>
</ModalCloseWrapper>
Content
</ModalFullContent>
</ModalWrapper>
</ModalContainer>
/>
</button>
</ModalCloseWrapper>
Content
</ModalFullContent>
</Modal>
);
}

View File

@ -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(
<BrowserRouter>
<ModalStack>
<ModalStack renderModals={ModalRenderer}>
<ContextMenuContextProvider>
<App />
</ContextMenuContextProvider>