diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 59be6cb..9b67162 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -1,94 +1,66 @@ -import styled from "styled-components"; +// Adapted from https://github.com/revoltchat/components/blob/master/src/components/design/atoms/inputs/Button.tsx -interface Props { - variant?: "primary" | "secondary" | "danger" | "success" | "warning"; - outlined?: boolean; +import styled, { css } from "styled-components"; + +export interface Props { + readonly compact?: boolean | "icon"; + palette?: "primary" | "secondary" | "success" | "warning" | "danger" | "accent" | "link"; + readonly disabled?: boolean; } export default styled.button` - background: ${(props) => { - if (props.outlined) return "transparent"; - switch (props.variant) { - case "primary": - return "var(--primary)"; - case "secondary": - return "var(--secondary)"; - case "danger": - return "var(--danger)"; - case "success": - return "var(--success)"; - case "warning": - return "var(--warning)"; - default: - return "var(--primary)"; - } - }}; - - border: ${(props) => { - if (!props.outlined) return "none"; - switch (props.variant) { - case "primary": - return "1px solid var(--primary)"; - case "secondary": - return "1px solid var(--secondary)"; - case "danger": - return "1px solid var(--danger)"; - case "success": - return "1px solid var(--success)"; - case "warning": - return "1px solid var(--warning)"; - default: - return "1px solid var(--primary)"; - } - }}; - color: var(--text); padding: 8px 16px; border-radius: 8px; - font-size: 13px; + font-size: 12px; font-weight: var(--font-weight-medium); - cursor: pointer; outline: none; + border: none; transition: background 0.2s ease-in-out; - pointer-events: ${(props) => (props.disabled ? "none" : null)}; + cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")}; opacity: ${(props) => (props.disabled ? 0.5 : 1)}; + font-weight: var(--font-weight-bold); - &:hover { - background: ${(props) => { - switch (props.variant) { - case "primary": - return "var(--primary-light)"; - case "secondary": - return "var(--secondary-light)"; - case "danger": - return "var(--danger-light)"; - case "success": - return "var(--success-light)"; - case "warning": - return "var(--warning-light)"; - default: - return "var(--primary-light)"; - } - }}; - cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")}; - } + ${(props) => { + if (!props.palette) props.palette = "primary"; + switch (props.palette) { + case "primary": + case "secondary": + case "success": + case "warning": + case "danger": + case "accent": + return css` + background: var(--${props.palette}); - &:active { - background: ${(props) => { - switch (props.variant) { - case "primary": - return "var(--primary-dark)"; - case "secondary": - return "var(--secondary-dark)"; - case "danger": - return "var(--danger-dark)"; - case "success": - return "var(--success-dark)"; - case "warning": - return "var(--warning-dark)"; - default: - return "var(--primary-dark)"; - } - }}; - } + &:hover { + filter: brightness(1.2); + } + + &:active { + filter: brightness(0.8); + } + + &:disabled { + filter: brightness(0.7); + } + `; + case "link": + return css` + background: transparent; + + &:hover { + text-decoration: underline; + } + + &:active { + filter: brightness(0.8); + } + + &:disabled { + filter: brightness(0.7); + } + `; + } + }} `; diff --git a/src/components/ChannelList/ChannelListItem.tsx b/src/components/ChannelList/ChannelListItem.tsx index d5359cc..80e7f36 100644 --- a/src/components/ChannelList/ChannelListItem.tsx +++ b/src/components/ChannelList/ChannelListItem.tsx @@ -3,11 +3,11 @@ import React from "react"; import { useNavigate } from "react-router-dom"; import styled from "styled-components"; import { ContextMenuContext } from "../../contexts/ContextMenuContext"; +import { modalController } from "../../controllers/modals/ModalController"; import Channel from "../../stores/objects/Channel"; import { IContextMenuItem } from "../ContextMenuItem"; import Icon from "../Icon"; import Tooltip from "../Tooltip"; -import CreateInviteModal from "../modals/CreateInviteModal"; const ListItem = styled.div<{ isCategory?: boolean }>` padding: ${(props) => (props.isCategory ? "16px 8px 0 0" : "1px 8px 0 0")}; @@ -64,7 +64,10 @@ function ChannelListItem({ channel, isCategory, active }: Props) { index: 0, label: "Create Channel Invite", onClick: () => { - openModal(CreateInviteModal, { guild_id: channel.guildId!, channel_id: channel.id }); + modalController.push({ + type: "create_invite", + target: channel, + }); }, iconProps: { icon: "mdiAccountPlus", diff --git a/src/components/GuildItem.tsx b/src/components/GuildItem.tsx index 103721d..8849b21 100644 --- a/src/components/GuildItem.tsx +++ b/src/components/GuildItem.tsx @@ -1,10 +1,11 @@ -import { useModals } from "@mattjennings/react-modal-stack"; import { CDNRoutes, ChannelType, ImageFormat } from "@spacebarchat/spacebar-api-types/v9"; import { observer } from "mobx-react-lite"; import React from "react"; import { useNavigate } from "react-router-dom"; import styled from "styled-components"; import { ContextMenuContext } from "../contexts/ContextMenuContext"; +import { modalController } from "../controllers/modals/ModalController"; +import useLogger from "../hooks/useLogger"; import { useAppStore } from "../stores/AppStore"; import Guild from "../stores/objects/Guild"; import { Permissions } from "../utils/Permissions"; @@ -13,7 +14,6 @@ import Container from "./Container"; import { IContextMenuItem } from "./ContextMenuItem"; import SidebarPill, { PillType } from "./SidebarPill"; import Tooltip from "./Tooltip"; -import CreateInviteModal from "./modals/CreateInviteModal"; export const GuildSidebarListItem = styled.div` position: relative; @@ -48,9 +48,9 @@ interface Props { * List item for use in the guild sidebar */ function GuildItem({ guild, active }: Props) { + const logger = useLogger("GuildItem"); const app = useAppStore(); const navigate = useNavigate(); - const { openModal } = useModals(); const [pillType, setPillType] = React.useState("none"); const [isHovered, setHovered] = React.useState(false); @@ -71,7 +71,17 @@ function GuildItem({ guild, active }: Props) { index: 0, label: "Create Invite", onClick: () => { - openModal(CreateInviteModal, { guild_id: guild.id }); + // get first channel with view permissions in guild + const channel = guild.channels.find((x) => { + const permission = Permissions.getPermission(app.account!.id, guild, x); + return permission.has("VIEW_CHANNEL") && x.type !== ChannelType.GuildCategory; + }); + if (!channel) return logger.error("No suitable channel found for invite creation"); + + modalController.push({ + type: "create_invite", + target: channel, + }); }, iconProps: { icon: "mdiAccountPlus", diff --git a/src/components/GuildSidebar.tsx b/src/components/GuildSidebar.tsx index 6f7dcad..1734abf 100644 --- a/src/components/GuildSidebar.tsx +++ b/src/components/GuildSidebar.tsx @@ -4,10 +4,10 @@ import React from "react"; import { useNavigate } from "react-router-dom"; import { AutoSizer, List, ListRowProps } from "react-virtualized"; import styled from "styled-components"; +import { modalController } from "../controllers/modals/ModalController"; import { useAppStore } from "../stores/AppStore"; import GuildItem, { GuildSidebarListItem } from "./GuildItem"; import SidebarAction from "./SidebarAction"; -import AddServerModal from "./modals/AddServerModal"; const Container = styled.div` display: flex; @@ -80,7 +80,9 @@ function GuildSidebar() { color: "var(--success)", }} action={() => { - openModal(AddServerModal); + modalController.push({ + type: "add_server", + }); }} margin={false} disablePill diff --git a/src/components/UserPanel.tsx b/src/components/UserPanel.tsx index e38d122..b10ba2d 100644 --- a/src/components/UserPanel.tsx +++ b/src/components/UserPanel.tsx @@ -1,14 +1,13 @@ -import { useModals } from "@mattjennings/react-modal-stack"; import React from "react"; import styled from "styled-components"; import { PopoutContext } from "../contexts/PopoutContext"; +import { modalController } from "../controllers/modals/ModalController"; import { useAppStore } from "../stores/AppStore"; import Avatar from "./Avatar"; import Icon from "./Icon"; import IconButton from "./IconButton"; import Tooltip from "./Tooltip"; import UserProfilePopout from "./UserProfilePopout"; -import SettingsModal from "./modals/SettingsModal"; const Section = styled.section` flex: 0 0 auto; @@ -73,11 +72,14 @@ const ActionsWrapper = styled.div` function UserPanel() { const app = useAppStore(); const popoutContext = React.useContext(PopoutContext); - const { openModal } = useModals(); const ref = React.useRef(null); const openSettingsModal = () => { - openModal(SettingsModal); + modalController.push({ + type: "error", + title: "File Too Large", + error: "Max file size is 25MB.", + }); }; const openPopout = (e: React.MouseEvent) => { diff --git a/src/components/common/animations.ts b/src/components/common/animations.ts new file mode 100644 index 0000000..c9e9dee --- /dev/null +++ b/src/components/common/animations.ts @@ -0,0 +1,26 @@ +// https://github.com/revoltchat/components/blob/master/src/components/common/animations.ts + +import { keyframes } from "styled-components"; + +export const animationFadeIn = keyframes` + 0% {opacity: 0;} + 70% {opacity: 0;} + 100% {opacity: 1;} +`; + +export const animationFadeOut = keyframes` + 0% {opacity: 1;} + 70% {opacity: 0;} + 100% {opacity: 0;} +`; + +export const animationZoomIn = keyframes` + 0% {transform: scale(0.5);} + 98% {transform: scale(1.01);} + 100% {transform: scale(1);} +`; + +export const animationZoomOut = keyframes` + 0% {transform: scale(1);} + 100% {transform: scale(0.5);} +`; diff --git a/src/components/messaging/MessageInput.tsx b/src/components/messaging/MessageInput.tsx index 484f2da..f63eadd 100644 --- a/src/components/messaging/MessageInput.tsx +++ b/src/components/messaging/MessageInput.tsx @@ -5,6 +5,7 @@ import { ChannelType, MessageType, RESTPostAPIChannelMessageJSONBody } from "@sp import { observer } from "mobx-react-lite"; import React from "react"; import styled from "styled-components"; +import { modalController } from "../../controllers/modals/ModalController"; import useLogger from "../../hooks/useLogger"; import { useAppStore } from "../../stores/AppStore"; import Guild from "../../stores/objects/Guild"; @@ -12,7 +13,6 @@ import Snowflake from "../../utils/Snowflake"; import { MAX_ATTACHMENTS } from "../../utils/constants"; import { debounce } from "../../utils/debounce"; import { isTouchscreenDevice } from "../../utils/isTouchscreenDevice"; -import ErrorModal from "../modals/ErrorModal"; import MessageTextArea from "./MessageTextArea"; import AttachmentUpload from "./attachments/AttachmentUpload"; import AttachmentUploadList from "./attachments/AttachmentUploadPreview"; @@ -161,13 +161,10 @@ function MessageInput({ channel }: Props) { const appendAttachment = (files: File[]) => { if (files.length === 0) return; if (files.length > MAX_ATTACHMENTS || attachments.length + files.length > MAX_ATTACHMENTS) { - openModal(ErrorModal, { + modalController.push({ + type: "error", title: "Too many attachments", - message: ( -
- You can only attach {MAX_ATTACHMENTS} files at once. -
- ), + error: `You can only attach ${MAX_ATTACHMENTS} files at once.`, }); return; } diff --git a/src/components/messaging/attachments/AttachmentUpload.tsx b/src/components/messaging/attachments/AttachmentUpload.tsx index 483eafa..19648e1 100644 --- a/src/components/messaging/attachments/AttachmentUpload.tsx +++ b/src/components/messaging/attachments/AttachmentUpload.tsx @@ -1,12 +1,11 @@ -import { useModals } from "@mattjennings/react-modal-stack"; import { observer } from "mobx-react-lite"; import React from "react"; import styled from "styled-components"; +import { modalController } from "../../../controllers/modals/ModalController"; import useLogger from "../../../hooks/useLogger"; import { bytesToSize } from "../../../utils/Utils"; import { MAX_UPLOAD_SIZE } from "../../../utils/constants"; import Icon from "../../Icon"; -import ErrorModal from "../../modals/ErrorModal"; const Container = styled.button` height: 45px; @@ -64,16 +63,12 @@ interface Props { function AttachmentUpload({ append }: Props) { const logger = useLogger("AttachmentUpload"); - const { openModal } = useModals(); const fileTooLarge = () => { - openModal(ErrorModal, { + modalController.push({ + type: "error", title: "File Too Large", - message: ( -
- Max file size is {bytesToSize(MAX_UPLOAD_SIZE)}. -
- ), + error: `Max file size is ${bytesToSize(MAX_UPLOAD_SIZE)}.`, }); return; }; diff --git a/src/components/modals/AddServerModal.tsx b/src/components/modals/AddServerModal.tsx index c5106a5..8135ce5 100644 --- a/src/components/modals/AddServerModal.tsx +++ b/src/components/modals/AddServerModal.tsx @@ -1,95 +1,40 @@ -import { useModals } from "@mattjennings/react-modal-stack"; import styled from "styled-components"; -import Icon from "../Icon"; -import CreateServerModal from "./CreateServerModal"; -import JoinServerModal from "./JoinServerModal"; -import { - Modal, - ModalActionItem, - ModalCloseWrapper, - ModalHeaderText, - ModalSubHeaderText, - ModelContentContainer, -} from "./ModalComponents"; -import { AnimatedModalProps } from "./ModalRenderer"; +import { modalController } from "../../controllers/modals/ModalController"; +import { ModalProps } from "../../controllers/modals/types"; +import Button from "../Button"; +import { Modal } from "./ModalComponents"; -export const ModalHeader = styled.div` - padding: 16px; +export const ActionWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 8px; `; - -const CreateButton = styled(ModalActionItem)` - transition: background-color 0.2s ease-in-out; - margin-bottom: 8px; - font-size: 16px; - font-weight: var(--font-weight-medium); - - &:hover { - background-color: var(--primary-light); - } -`; - -const JoinButton = styled(ModalActionItem)` - transition: background-color 0.2s ease-in-out; - margin-bottom: 8px; - font-size: 16px; - font-weight: var(--font-weight-medium); - - &:hover { - background-color: var(--background-secondary-highlight); - } -`; - -function AddServerModal(props: AnimatedModalProps) { - const { openModal, closeModal } = useModals(); - +function AddServerModal({ ...props }: ModalProps<"add_server">) { return ( - - - - - - - Add a Guild - Lorem ipsum dolor sit amet, consectetur adipiscing elit. - - - - + + - { - openModal(JoinServerModal); + modalController.push({ + type: "join_server", + }); }} > Join a Guild - - + + ); } diff --git a/src/components/modals/AttachmentPreviewModal.tsx b/src/components/modals/AttachmentPreviewModal.tsx index 962a817..854f304 100644 --- a/src/components/modals/AttachmentPreviewModal.tsx +++ b/src/components/modals/AttachmentPreviewModal.tsx @@ -1,30 +1,5 @@ -import { APIAttachment } from "@spacebarchat/spacebar-api-types/v9"; -import { Modal } from "./ModalComponents"; -import { AnimatedModalProps } from "./ModalRenderer"; - -interface Props extends AnimatedModalProps { - attachment: APIAttachment; - width?: number; - height?: number; -} - -function AttachmentPreviewModal(props: Props) { - const width = props.width ?? props.attachment.width ?? 0; - const height = props.height ?? props.attachment.height ?? 0; - - return ( - - - - ); +function AttachmentPreviewModal() { + return null; } export default AttachmentPreviewModal; diff --git a/src/components/modals/CreateInviteModal.tsx b/src/components/modals/CreateInviteModal.tsx index bcfda54..46c9a5c 100644 --- a/src/components/modals/CreateInviteModal.tsx +++ b/src/components/modals/CreateInviteModal.tsx @@ -4,6 +4,7 @@ import dayjs from "dayjs"; import React from "react"; import { useForm } from "react-hook-form"; import styled from "styled-components"; +import { ModalProps } from "../../controllers/modals/types"; import useLogger from "../../hooks/useLogger"; import { useAppStore } from "../../stores/AppStore"; import { messageFromFieldError } from "../../utils/messageFromFieldError"; @@ -13,15 +14,7 @@ import { TextDivider } from "../Divider"; import { InputSelect, InputSelectOption } from "../FormComponents"; import Icon from "../Icon"; import IconButton from "../IconButton"; -import { InputContainer } from "./CreateServerModal"; -import { - Modal, - ModalCloseWrapper, - ModalHeaderText, - ModalSubHeaderText, - ModelContentContainer, -} from "./ModalComponents"; -import { AnimatedModalProps } from "./ModalRenderer"; +import { InputContainer, Modal } from "./ModalComponents"; const EXPIRE_OPTIONS = [ { @@ -89,25 +82,12 @@ const MAX_USES_OPTIONS = [ }, ]; -const Mention = styled.span` - padding: 0 2px; -`; - -const ModalHeader = styled.div` - padding: 24px 24px 0; -`; - const InputWrapper = styled.div` width: 100%; display: flex; align-items: center; `; -interface InviteModalProps extends AnimatedModalProps { - channel_id?: string; - guild_id: string; -} - interface APICreateInvite { flags: 0; target_type: null; @@ -121,7 +101,7 @@ interface FormValues extends APICreateInvite { code: string; } -function CreateInviteModal(props: InviteModalProps) { +function CreateInviteModal({ target, ...props }: ModalProps<"create_invite">) { const logger = useLogger("CreateInviteModal"); const app = useAppStore(); const { openModal, closeModal } = useModals(); @@ -130,14 +110,6 @@ function CreateInviteModal(props: InviteModalProps) { const [isEdited, setIsEdited] = React.useState(false); const [inviteExpiresAt, setInviteExpiresAt] = React.useState(null); - const guild = app.guilds.get(props.guild_id); - const channel = props.channel_id ? guild?.channels.find((x) => x.id === props.channel_id) : guild?.channels[0]; - - if (!guild || !channel) { - closeModal(); - return null; - } - const { register, handleSubmit, @@ -154,7 +126,7 @@ function CreateInviteModal(props: InviteModalProps) { clearErrors(); app.rest .post( - Routes.channelInvites(channel.id), + Routes.channelInvites(target.id), Object.assign( { flags: 0, @@ -223,144 +195,115 @@ function CreateInviteModal(props: InviteModalProps) { React.useEffect(() => createCode(), []); return ( - - - + + + - + Invite Code + {errors.code && ( + + <> + - + {errors.code.message} + + + )} + + + - - - - - Invite People - - to #{channel.name} in {guild.name} - - - - - { - if (e.key === "Enter") { - e.preventDefault(); - onSubmit(); - } - }} - > - - - Expire after - - - - {EXPIRE_OPTIONS.map((option) => ( - {option.label} - ))} - - - - - - - Maximum Uses - - - - {MAX_USES_OPTIONS.map((option) => ( - {option.label} - ))} - - - - -
- -
- - - - Invite Code - {errors.code && ( - - <> - - - {errors.code.message} - - - )} - + - { + e.preventDefault(); + navigator.clipboard.writeText(getValues("code")); }} > - + + + - { - e.preventDefault(); - navigator.clipboard.writeText(getValues("code")); - }} - > - - - - - - {inviteExpiresAt ? ( - <>This invite will expire {dayjs(inviteExpiresAt).calendar()} - ) : ( - "Invite will never expire." - )} - - - -
+ + {inviteExpiresAt ? ( + <>This invite will expire {dayjs(inviteExpiresAt).fromNow()} + ) : ( + "Invite will never expire." + )} + + +
); } diff --git a/src/components/modals/CreateServerModal.tsx b/src/components/modals/CreateServerModal.tsx index e7098a7..90e5698 100644 --- a/src/components/modals/CreateServerModal.tsx +++ b/src/components/modals/CreateServerModal.tsx @@ -1,25 +1,16 @@ -import { useModals } from "@mattjennings/react-modal-stack"; import { APIGuild, Routes } from "@spacebarchat/spacebar-api-types/v9"; import React from "react"; import { useForm } from "react-hook-form"; import { useNavigate } from "react-router-dom"; import styled from "styled-components"; +import { modalController } from "../../controllers/modals/ModalController"; +import { ModalProps } from "../../controllers/modals/types"; import useLogger from "../../hooks/useLogger"; import { useAppStore } from "../../stores/AppStore"; import { messageFromFieldError } from "../../utils/messageFromFieldError"; import { Input, InputErrorText, InputLabel, InputWrapper, LabelWrapper } from "../AuthComponents"; import { TextDivider } from "../Divider"; -import Icon from "../Icon"; -import { - Modal, - ModalActionItem, - ModalCloseWrapper, - ModalFooter, - ModalHeaderText, - ModalSubHeaderText, - ModelContentContainer, -} from "./ModalComponents"; -import { AnimatedModalProps } from "./ModalRenderer"; +import { InputContainer, Modal } from "./ModalComponents"; export const ModalHeader = styled.div` margin-bottom: 30px; @@ -56,20 +47,13 @@ const FileInput = styled.div` font-size: 0px; `; -export const InputContainer = styled.div` - margin-top: 24px; - display: flex; - flex-direction: column; -`; - type FormValues = { name: string; }; -function CreateServerModal(props: AnimatedModalProps) { +function CreateServerModal({ ...props }: ModalProps<"create_server">) { const app = useAppStore(); const logger = useLogger("CreateServerModal"); - const { openModal, closeModal, closeAllModals } = useModals(); const [selectedFile, setSelectedFile] = React.useState(); const fileInputRef = React.useRef(null); const navigate = useNavigate(); @@ -98,7 +82,7 @@ function CreateServerModal(props: AnimatedModalProps) { }) .then((r) => { navigate(`/channels/${r.id}`); - closeAllModals(); + modalController.closeAll(); }) .catch((r) => { if ("message" in r) { @@ -135,122 +119,86 @@ function CreateServerModal(props: AnimatedModalProps) { return ( Create, + palette: "primary", + confirmation: true, + disabled: isLoading, + }, + { + onClick: () => modalController.pop("close"), + children: Back, + palette: "link", + disabled: isLoading, + }, + ]} > - - - + + + + + + + + + + fileInputRef.current?.click()} + > + + - - Customize your guild - - Give your new guild a personality with a name and an icon. You can always change it later. - - - - - - - - - - - - - { + if (e.key === "Enter") { + e.preventDefault(); + onSubmit(); + } + }} + > + + + Guild Name + {errors.name && ( + + <> + - + {errors.name.message} + + + )} + + + - fileInputRef.current?.click()} - > - - - -
{ - if (e.key === "Enter") { - e.preventDefault(); - onSubmit(); - } - }} - > - - - Guild Name - {errors.name && ( - - <> - - - {errors.name.message} - - - )} - - - - - -
-
- - - - Create - - - { - closeModal(); - }} - > - Back - - + + +
); } diff --git a/src/components/modals/ErrorModal.tsx b/src/components/modals/ErrorModal.tsx index 409b3ea..8dd6b96 100644 --- a/src/components/modals/ErrorModal.tsx +++ b/src/components/modals/ErrorModal.tsx @@ -1,71 +1,22 @@ -import { useModals } from "@mattjennings/react-modal-stack"; -import styled from "styled-components"; -import Icon from "../Icon"; -import { - Modal, - ModalActionItem, - ModalCloseWrapper, - ModalFooter, - ModalHeaderText, - ModalSubHeaderText, - ModelContentContainer, -} from "./ModalComponents"; -import { AnimatedModalProps } from "./ModalRenderer"; - -export const ModalHeader = styled.div` - padding: 16px; -`; - -const CloseButton = styled(ModalActionItem)` - transition: background-color 0.2s ease-in-out; - margin-bottom: 8px; - font-size: 16px; - font-weight: var(--font-weight-medium); -`; - -interface Props extends AnimatedModalProps { - title: string; - subtitle?: string; - message: React.ReactNode; -} - -function ErrorModal(props: Props) { - const { closeModal } = useModals(); +import { ModalProps } from "../../controllers/modals/types"; +import { Modal } from "./ModalComponents"; +function ErrorModal({ error, ...props }: ModalProps<"error">) { return ( - - - - - - - {props.title} - {props.subtitle && {props.subtitle}} - - - {props.message} - - - closeModal()}> - Dismiss - - + true, + confirmation: true, + children: Dismiss, + palette: "primary", + disabled: !(props.recoverable ?? true), + }, + ]} + nonDismissable={!(props.recoverable ?? true)} + > + {error} ); } diff --git a/src/components/modals/ForgotPasswordModal.tsx b/src/components/modals/ForgotPasswordModal.tsx index db88136..b0b3d16 100644 --- a/src/components/modals/ForgotPasswordModal.tsx +++ b/src/components/modals/ForgotPasswordModal.tsx @@ -1,71 +1,5 @@ -import { useModals } from "@mattjennings/react-modal-stack"; -import styled from "styled-components"; -import Icon from "../Icon"; -import { - Modal, - ModalActionItem, - ModalCloseWrapper, - ModalFooter, - ModalHeaderText, - ModelContentContainer, -} from "./ModalComponents"; -import { AnimatedModalProps } from "./ModalRenderer"; - -export const ModalHeader = styled.div` - padding: 16px; -`; - -const SubmitButton = styled(ModalActionItem)` - transition: background-color 0.2s ease-in-out; - font-size: 16px; - font-weight: var(--font-weight-medium); - - &:hover { - background-color: var(--background-secondary-highlight); - } -`; - -function ForgotPasswordModal(props: AnimatedModalProps) { - const { closeModal } = useModals(); - - return ( - - - - - - - Instructions Sent - - - - We sent instructions to change your password to user@example.com, please check both your inbox and spam - folder. - - - - - Okay - - - - ); +function ForgotPasswordModal() { + return null; } export default ForgotPasswordModal; diff --git a/src/components/modals/JoinServerModal.tsx b/src/components/modals/JoinServerModal.tsx index e926c43..e619aa9 100644 --- a/src/components/modals/JoinServerModal.tsx +++ b/src/components/modals/JoinServerModal.tsx @@ -3,23 +3,14 @@ import { Routes } from "@spacebarchat/spacebar-api-types/v9"; import { useForm } from "react-hook-form"; import { useNavigate } from "react-router-dom"; import styled from "styled-components"; +import { modalController } from "../../controllers/modals/ModalController"; +import { ModalProps } from "../../controllers/modals/types"; import useLogger from "../../hooks/useLogger"; import { useAppStore } from "../../stores/AppStore"; import { messageFromFieldError } from "../../utils/messageFromFieldError"; import { Input, InputErrorText, InputLabel, LabelWrapper } from "../AuthComponents"; import { TextDivider } from "../Divider"; -import Icon from "../Icon"; -import AddServerModal from "./AddServerModal"; -import { - Modal, - ModalActionItem, - ModalCloseWrapper, - ModalFooter, - ModalHeaderText, - ModalSubHeaderText, - ModelContentContainer, -} from "./ModalComponents"; -import { AnimatedModalProps } from "./ModalRenderer"; +import { Modal } from "./ModalComponents"; export const ModalHeader = styled.div` padding: 16px; @@ -34,7 +25,7 @@ type FormValues = { code: string; }; -function JoinServerModal(props: AnimatedModalProps) { +function JoinServerModal({ ...props }: ModalProps<"join_server">) { const logger = useLogger("JoinServerModal"); const { openModal, closeAllModals } = useModals(); const app = useAppStore(); @@ -92,86 +83,58 @@ function JoinServerModal(props: AnimatedModalProps) { return ( Join, + palette: "primary", + confirmation: true, + disabled: isLoading, + }, + { + onClick: () => modalController.pop("close"), + children: Back, + palette: "link", + disabled: isLoading, + }, + ]} > - - - - - - Join a Guild - Enter an invite below to join an existing guild. - - - -
{ - if (e.key === "Enter") { - e.preventDefault(); - onSubmit(); - } - }} - > - - - Invite Link - - {errors.code && ( - - <> - - - {errors.code.message} - - - )} - - - -
-
- - - - Join Guild - - - { - openModal(AddServerModal); - }} - > - Back - - + +
); } diff --git a/src/components/modals/KickModal.tsx b/src/components/modals/KickModal.tsx index 069781a..60c44ac 100644 --- a/src/components/modals/KickModal.tsx +++ b/src/components/modals/KickModal.tsx @@ -1,74 +1,5 @@ -import { useModals } from "@mattjennings/react-modal-stack"; -import styled from "styled-components"; -import GuildMember from "../../stores/objects/GuildMember"; -import Icon from "../Icon"; -import { - Modal, - ModalActionItem, - ModalCloseWrapper, - ModalFooter, - ModalHeaderText, - ModalSubHeaderText, -} from "./ModalComponents"; -import { AnimatedModalProps } from "./ModalRenderer"; - -export const ModalHeader = styled.div` - padding: 16px; -`; - -const CloseButton = styled(ModalActionItem)` - transition: background-color 0.2s ease-in-out; - margin-bottom: 8px; - font-size: 16px; - font-weight: var(--font-weight-medium); -`; - -interface Props extends AnimatedModalProps { - member: GuildMember; -} - -function KickModal(props: Props) { - const { closeModal } = useModals(); - - return ( - - - - - - - Kick {props.member.user!.username} - - Are you sure you want to kick @{props.member.user!.username} from the server? They will be able to - rejoin again with a new invite. - - - - {/* {props.message} */} - - - closeModal()}> - Dismiss - - - - ); +function KickModal() { + return null; } export default KickModal; diff --git a/src/components/modals/LeaveServerModal.tsx b/src/components/modals/LeaveServerModal.tsx index 5fdc2f2..ece6937 100644 --- a/src/components/modals/LeaveServerModal.tsx +++ b/src/components/modals/LeaveServerModal.tsx @@ -1,131 +1,5 @@ -import { useModals } from "@mattjennings/react-modal-stack"; -import { Routes } from "@spacebarchat/spacebar-api-types/v9"; -import { useNavigate } from "react-router-dom"; -import styled from "styled-components"; -import { useAppStore } from "../../stores/AppStore"; -import Guild from "../../stores/objects/Guild"; -import Icon from "../Icon"; -import { - Modal, - ModalActionItem, - ModalCloseWrapper, - ModalFooter, - ModalHeaderText, - ModelContentContainer, -} from "./ModalComponents"; -import { AnimatedModalProps } from "./ModalRenderer"; - -export const ModalHeader = styled.div` - padding: 16px; -`; - -const CancelButton = styled(ModalActionItem)` - transition: background-color 0.2s ease-in-out; - margin-bottom: 8px; - font-size: 14px; - font-weight: var(--font-weight-medium); - - &:hover { - text-decoration: underline; - } -`; - -const LeaveButton = styled(ModalActionItem)` - transition: background-color 0.2s ease-in-out; - margin-bottom: 8px; - font-size: 14px; - font-weight: var(--font-weight-medium); - border-radius: 4px; - background-color: var(--danger); - - &:hover { - background-color: var(--background-secondary-highlight); - } -`; - -interface Props { - guild: Guild; -} - -function LeaveServerModal(props: Props & AnimatedModalProps) { - const app = useAppStore(); - const { closeModal } = useModals(); - const navigate = useNavigate(); - - if (!open) { - return null; - } - - const handleLeaveServer = () => { - app.rest.delete(Routes.userGuild(props.guild.id)).finally(() => { - closeModal(); - // navigate to @me - navigate("/channels/@me"); - }); - }; - - return ( - - - - - - - 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 - - - - ); +function LeaveServerModal() { + return null; } export default LeaveServerModal; diff --git a/src/components/modals/ModalComponents.tsx b/src/components/modals/ModalComponents.tsx index 44091cc..895b1a6 100644 --- a/src/components/modals/ModalComponents.tsx +++ b/src/components/modals/ModalComponents.tsx @@ -1,253 +1,217 @@ -import { useModals, type StackedModalProps } from "@mattjennings/react-modal-stack"; -import { AnimatePresence, motion } from "framer-motion"; -import React from "react"; -import styled from "styled-components"; +import { type StackedModalProps } from "@mattjennings/react-modal-stack"; +import React, { useCallback, useEffect, useState } from "react"; +import { createPortal } from "react-dom"; +import styled, { css } from "styled-components"; +import Button, { Props as ButtonProps } from "../Button"; +import { animationFadeIn, animationFadeOut, animationZoomIn, animationZoomOut } from "../common/animations"; + +export type ModalAction = Omit, "as"> & + Omit & { + confirmation?: boolean; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onClick: () => any | Promise; + }; + +interface ModalProps extends StackedModalProps { + children: React.ReactNode; + onClose?: (force: boolean) => void; + signal?: "close" | "confirm" | "cancel"; + title?: string; + description?: string; + transparent?: boolean; + nonDismissable?: boolean; + maxWidth?: string; + maxHeight?: string; + padding?: string; + actions?: ModalAction[]; + disabled?: boolean; + withEmptyActionBar?: boolean; +} /** * Main container for all modals, handles the background overlay and positioning */ -export const ModalBase = styled(motion.div)` - z-index: 100; - position: fixed; +export const ModalBase = styled.div<{ closing?: boolean }>` top: 0; left: 0; - right: 0; - bottom: 0; - display: flex; - justify-content: center; - align-items: center; + width: 100%; + height: 100%; + + z-index: 9999; + position: fixed; + + max-height: 100%; + user-select: none; + + animation-duration: 0.2s; + animation-fill-mode: forwards; + + display: grid; + overflow-y: auto; + place-items: center; + color: var(--text); + background: rgba(0, 0, 0, 0.8); + + ${(props) => + props.closing + ? css` + animation-name: ${animationFadeOut}; + + > div { + animation-name: ${animationZoomOut}; + } + ` + : css` + animation-name: ${animationFadeIn}; + `} `; /** * Wrapper for modal content, handles the sizing and positioning */ -export const ModalWrapper = styled(motion.div)<{ full?: boolean }>` - width: ${(props) => (props.full ? "100%" : "440px")}; - height: ${(props) => (props.full ? "100%" : "auto")}; - border-radius: 4px; - background-color: var(--background-secondary); - box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05), 0 1px 3px 1px rgba(0, 0, 0, 0.05); - position: relative; +export const ModalWrapper = styled.div< + Pick & { actions: boolean } +>` + min-height: 200px; + max-width: min(calc(100vw - 20px), ${(props) => props.maxWidth ?? "450px"}); + max-height: min(calc(100vh - 20px), ${(props) => props.maxHeight ?? "650px"}); + + margin: 20px; display: flex; - justify-content: ${(props) => (props.full ? undefined : "center")}; - flex-direction: ${(props) => (props.full ? "row" : "column")}; + flex-direction: column; + + animation-name: ${animationZoomIn}; + animation-duration: 0.25s; + animation-timing-function: cubic-bezier(0.3, 0.3, 0.18, 1.1); + + ${(props) => + !props.maxWidth && + css` + width: 100%; + `} + + ${(props) => + !props.transparent && + css` + overflow: hidden; + background: var(--background-secondary); + border-radius: 8px; + `} `; -/** - * Wrapper for modal close button - */ -export const ModalCloseWrapper = styled.div` - position: absolute; - top: 10px; - right: 10px; +export const ModalHeader = styled.div` + padding: 16px; + flex-shrink: 0; + word-break: break-word; + gap: 8px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; `; -export const ModalHeaderText = styled.h1` - font-size: 24px; - font-weight: var(--font-weight-bold); - color: var(--text-header); - text-align: center; - margin: 0; - padding: 0; +export const ModalContentContainer = styled.div>` + display: flex; + flex-direction: column; + flex-grow: 1; + padding-top: 0; + padding: ${(props) => props.padding ?? "0 16px 16px"}; + + overflow-y: auto; + font-size: 0.9375rem; + + ${(props) => + !props.transparent && + css` + background: var(--background-secondary); + `} +`; + +const Actions = styled.div` + gap: 8px; + display: flex; + padding: 16px; + flex-direction: row-reverse; + background: var(--background-primary); + border-radius: 0 0 4px 4px; `; export const ModalSubHeaderText = styled.div` font-size: 16px; font-weight: var(--font-weight-regular); color: var(--text-header-secondary); - text-align: center; margin-top: 8px; `; -export const ModelContentContainer = styled.div` - display: flex; - flex-direction: column; - padding: 0 16px; - margin: 16px 0; - border-radius: 5px 5px 0 0; -`; - -export const ModalActionItem = styled.button<{ - variant?: "filled" | "blank" | "outlined" | "link"; - size?: "med" | "min"; -}>` +export const ModalHeaderText = styled.div` + font-size: 24px; + font-weight: var(--font-weight-bold); color: var(--text); - display: flex; - position: relative; - justify-content: center; - align-items: center; - background: none; - border: none; - outline: none; - border-radius: 3px; - font-size: 14px; - font-weight: var(--font-weight-medium); - padding: 2px 16px; - cursor: pointer; - transition: background-color 0.2s ease-in-out; - - ${(props) => { - if (props.variant === "filled") { - return ` - background-color: var(--primary); - - &:hover { - background-color: var(--primary-light); - } - `; - } else if (props.variant === "blank") { - return ` - background: transparent; - `; - } else if (props.variant === "link") { - return ` - background: transparent; - - &:hover { - text-decoration: underline; - } - `; - } else if (props.variant === "outlined") { - return ` - background: transparent; - border: 1px solid var(--background-secondary-highlight); - `; - } - }} - - ${(props) => { - if (props.size === "med") { - return ` - width: auto; - height: 38px; - min-width: 96px; - min-height: 38px; - `; - } else if (props.size === "min") { - return ` - width: auto; - display: inline; - height: auto; - padding: 2px 4px; - `; - } - }} - - // disabled styling - ${(props) => { - if (props.disabled) { - return ` - opacity: 0.5; - cursor: not-allowed; - `; - } - }} + margin: 0; + padding: 0; `; -export const ModalFooter = styled.div` - border-radius: 0 0 5px 5px; - background-color: var(--background-primary-alt); - position: relative; - padding: 16px; - display: flex; - flex-direction: row-reverse; - justify-content: space-between; -`; - -export const ModalFullSidebar = styled.div` - display: flex; - justify-content: flex-end; - flex: 1 0 15%; - z-index: 1; - padding: 10px; -`; - -export const ModalFullSidebarContainer = styled.div` - overflow: hidden scroll; - flex: 1 0 auto; - flex-direction: row; - display: flex; - justify-content: flex-end; - background-color: var(--background-secondary); -`; - -export const ModalFullSidebarContent = styled.div` - width: 200px; - padding: 40px 0; +export const InputContainer = styled.div` + margin-top: 24px; display: flex; flex-direction: column; `; -export const ModalFullContentContainer = styled.div` - display: flex; - flex: 1 1 700px; - align-items: flex-start; -`; - -export const ModalFullContentWrapper = styled.div` - flex: 1; - height: 100%; -`; - -export const ModalFullContentContainerContentWrapper = styled.div` - height: 100%; - display: flex; - flex-direction: row; - background-color: var(--background-primary); -`; - -export const ModalFullContent = styled.div` - padding: 50px; - flex: 1 1 auto; - max-width: 80%; - min-width: 50%; - min-height: 100%; -`; - -interface ModalProps extends StackedModalProps { - children: React.ReactNode; - full?: boolean; - onClick?: () => void; - /** - * Custom callback for when a modal is closed by clicking the background overlay - */ - onClose?: () => void; - style?: React.CSSProperties; -} - export function Modal(props: ModalProps) { - const { closeModal } = useModals(); + const [closing, setClosing] = useState(false); - return ( - - {props.open && ( - { - if (e.target !== e.currentTarget) return; - if (props.onClose) props.onClose(); - else closeModal(); - }} - {...props} - > - - {props.children} - - - )} - + const closeModal = useCallback(() => { + setClosing(true); + if (!closing) setTimeout(() => props.onClose?.(true), 2e2); + }, [closing, props]); + + const confirm = useCallback(async () => { + if (await props.actions?.find((x) => x.confirmation)?.onClick?.()) { + closeModal(); + } + }, [props.actions]); + + useEffect(() => { + if (props.signal === "confirm") { + confirm(); + } else if (props.signal) { + if (props.signal === "close" && props.nonDismissable) { + return; + } + + closeModal(); + } + }, [props.signal]); + + return createPortal( + !props.nonDismissable && closeModal()}> + e.stopPropagation()} actions={false}> + {(props.title || props.description) && ( + + {props.title && {props.title}} + {props.description && {props.description}} + + )} + {props.children} + {props.actions && props.actions.length > 0 && ( + + {props.actions.map((x, index) => ( + - - - - - - - - - - - - -
- Client Version: - - {GIT_REVISION.substring(0, 7)} - - {` `} - - ({GIT_BRANCH}) - -
- - - app.setFpsShown(e.target.checked)} - /> - } - label="Show FPS Graph" - /> - -
-
-
-
-
- ); +function SettingsModal() { + return null; } export default observer(SettingsModal); diff --git a/src/contexts/Theme.tsx b/src/contexts/Theme.tsx index 7a1edf7..693b98d 100644 --- a/src/contexts/Theme.tsx +++ b/src/contexts/Theme.tsx @@ -58,7 +58,8 @@ export type ThemeVariables = | "scrollbarTrack" | "scrollbarThumb" | "statusIdle" - | "statusOffline"; + | "statusOffline" + | "accent"; export type Overrides = { [variable in ThemeVariables]: string; @@ -108,9 +109,10 @@ export const ThemePresets: Record = { primaryLight: "#339dff", primaryDark: "#005db2", primaryContrastText: "#ffffff", - secondary: "#000115", - secondaryLight: "#000677", - secondaryDark: "#000111", + accent: "#000115", + secondary: "#4e4e4e", + secondaryLight: "#ff9633", + secondaryDark: "#b25e00", secondaryContrastText: "", danger: "", dangerLight: "", @@ -153,11 +155,10 @@ export const ThemePresets: Record = { primaryLight: "#339dff", primaryDark: "#005db2", primaryContrastText: "#ffffff", - secondary: "#000115", - secondaryLight: "#000677", - secondaryDark: "#000111", - // secondary: "#ff7c01", - // secondaryLight: "#ff9633", + accent: "#000115", + secondary: "#4e4e4e", + secondaryLight: "#ff9633", + secondaryDark: "#b25e00", secondaryContrastText: "#040404", danger: "#ff3a3b", dangerLight: "#ff6162", diff --git a/src/controllers/modals/ModalController.tsx b/src/controllers/modals/ModalController.tsx new file mode 100644 index 0000000..5572c32 --- /dev/null +++ b/src/controllers/modals/ModalController.tsx @@ -0,0 +1,178 @@ +// adapted from https://github.com/revoltchat/revite/blob/master/src/controllers/modals/ModalController.tsx + +import { action, computed, makeObservable, observable } from "mobx"; +import AddServerModal from "../../components/modals/AddServerModal"; +import CreateInviteModal from "../../components/modals/CreateInviteModal"; +import CreateServerModal from "../../components/modals/CreateServerModal"; +import ErrorModal from "../../components/modals/ErrorModal"; +import JoinServerModal from "../../components/modals/JoinServerModal"; +import { Modal } from "./types"; + +function randomUUID() { + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { + // eslint-disable-next-line no-bitwise + const r = (Math.random() * 16) | 0; + // eslint-disable-next-line no-bitwise, no-mixed-operators + const v = c === "x" ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Components = Record>; + +/** + * Handles layering and displaying modals to the user. + */ +class ModalController { + stack: T[] = []; + components: Components; + + constructor(components: Components) { + this.components = components; + + makeObservable(this, { + stack: observable, + push: action, + pop: action, + remove: action, + rendered: computed, + isVisible: computed, + }); + + this.close = this.close.bind(this); + } + + /** + * Display a new modal on the stack + * @param modal Modal data + */ + push(modal: T) { + this.stack = [ + ...this.stack, + { + ...modal, + key: randomUUID(), // TODO: + }, + ]; + } + + /** + * Remove the top modal from the screen + * @param signal What action to trigger + */ + pop(signal: "close" | "confirm" | "force") { + this.stack = this.stack.map((entry, index) => (index === this.stack.length - 1 ? { ...entry, signal } : entry)); + } + + /** + * Close the top modal + */ + close() { + this.pop("close"); + } + + /** + * Close all modals on the stack + */ + closeAll() { + this.stack = []; + } + + /** + * Remove the keyed modal from the stack + */ + remove(key: string) { + this.stack = this.stack.filter((x) => x.key !== key); + } + + /** + * Render modals + */ + get rendered() { + return ( + <> + {this.stack.map((modal) => { + const Component = this.components[modal.type]; + return this.remove(modal.key!)} />; + })} + + ); + } + + /** + * Whether a modal is currently visible + */ + get isVisible() { + return this.stack.length > 0; + } +} + +/** + * Modal controller with additional helpers. + */ +class ModalControllerExtended extends ModalController { + /** + * Write text to the clipboard + * @param text Text to write + */ + writeText(text: string) { + if (navigator.clipboard) { + navigator.clipboard.writeText(text); + } else { + this.push({ + type: "clipboard", + text, + }); + } + } +} + +export const modalController = new ModalControllerExtended({ + add_server: AddServerModal, + // add_friend: AddFriend, + // ban_member: BanMember, + // changelog: Changelog, + // channel_info: ChannelInfo, + // clipboard: Clipboard, + // leave_group: ConfirmLeave, + // close_dm: Confirmation, + // leave_server: ConfirmLeave, + // delete_server: Confirmation, + // delete_channel: Confirmation, + // delete_bot: Confirmation, + // block_user: Confirmation, + // unfriend_user: Confirmation, + // create_category: CreateCategory, + // create_channel: CreateChannel, + // create_group: CreateGroup, + create_invite: CreateInviteModal, + // create_role: CreateRole, + create_server: CreateServerModal, + join_server: JoinServerModal, + // create_bot: CreateBot, + // custom_status: CustomStatus, + // delete_message: DeleteMessage, + error: ErrorModal, + // image_viewer: ImageViewer, + // kick_member: KickMember, + // link_warning: LinkWarning, + // mfa_flow: MFAFlow, + // mfa_recovery: MFARecovery, + // mfa_enable_totp: MFAEnableTOTP, + // modify_account: ModifyAccount, + // onboarding: OnboardingModal, + // out_of_date: OutOfDate, + // pending_friend_requests: PendingFriendRequests, + // server_identity: ServerIdentity, + // server_info: ServerInfo, + // show_token: ShowToken, + // signed_out: SignedOut, + // sign_out_sessions: SignOutSessions, + // user_picker: UserPicker, + // user_profile: UserProfile, + // report: ReportContent, + // report_success: ReportSuccess, + // modify_displayname: ModifyDisplayname, + // changelog_usernames: ChangelogUsernames, +}); diff --git a/src/controllers/modals/ModalRenderer.tsx b/src/controllers/modals/ModalRenderer.tsx new file mode 100644 index 0000000..3ad44e7 --- /dev/null +++ b/src/controllers/modals/ModalRenderer.tsx @@ -0,0 +1,24 @@ +// adapted from https://github.com/revoltchat/revite/blob/master/src/controllers/modals/ModalRenderer.tsx +// Removed usage of `Prompt` + +import { observer } from "mobx-react-lite"; +import { useEffect } from "react"; +import { modalController } from "./ModalController"; + +export default observer(() => { + useEffect(() => { + function keyDown(event: KeyboardEvent) { + if (event.key === "Escape") { + modalController.pop("close"); + } else if (event.key === "Enter") { + if (event.target instanceof HTMLSelectElement) return; + modalController.pop("confirm"); + } + } + + document.addEventListener("keydown", keyDown); + return () => document.removeEventListener("keydown", keyDown); + }, []); + + return <>{modalController.rendered}; +}); diff --git a/src/controllers/modals/types.ts b/src/controllers/modals/types.ts new file mode 100644 index 0000000..05c703a --- /dev/null +++ b/src/controllers/modals/types.ts @@ -0,0 +1,31 @@ +// adapted from https://github.com/revoltchat/revite/blob/master/src/controllers/modals/types.ts + +import Channel from "../../stores/objects/Channel"; + +export type Modal = { + key?: string; +} & ( + | { + type: "add_server" | "create_server" | "join_server"; + } + | { + type: "error"; + title: string; + description?: string; + error: string; + recoverable?: boolean; + } + | { + type: "clipboard"; + text: string; + } + | { + type: "create_invite"; + target: Channel; + } +); + +export type ModalProps = Modal & { type: T } & { + onClose: () => void; + signal?: "close" | "confirm"; +}; diff --git a/src/index.tsx b/src/index.tsx index d910d54..4cb6af5 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -11,7 +11,6 @@ import "@fontsource/roboto/400.css"; import "@fontsource/roboto/500.css"; import "@fontsource/roboto/700.css"; import "@fontsource/roboto/900.css"; -import { ModalStack } from "@mattjennings/react-modal-stack"; import dayjs from "dayjs"; import calendar from "dayjs/plugin/calendar"; import relativeTime from "dayjs/plugin/relativeTime"; @@ -19,11 +18,11 @@ import ReactDOM from "react-dom/client"; import { BrowserRouter } from "react-router-dom"; import { ErrorBoundaryContext } from "react-use-error-boundary"; import App from "./App"; -import ModalRenderer from "./components/modals/ModalRenderer"; import { BannerContextProvider } from "./contexts/BannerContext"; import { ContextMenuContextProvider } from "./contexts/ContextMenuContext"; import { PopoutContextProvider } from "./contexts/PopoutContext"; import Theme from "./contexts/Theme"; +import ModalRenderer from "./controllers/modals/ModalRenderer"; import "./index.css"; import { calendarStrings } from "./utils/i18n"; @@ -33,16 +32,15 @@ dayjs.extend(calendar, calendarStrings); ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - - - - - - - - - - + + + + + + + + + , ); diff --git a/src/pages/LoadingPage.tsx b/src/pages/LoadingPage.tsx index 0f0bd07..ad2fa73 100644 --- a/src/pages/LoadingPage.tsx +++ b/src/pages/LoadingPage.tsx @@ -40,7 +40,7 @@ function LoadingPage() { bottom: "30vh", }} > - diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx index 76e84ff..1a74efa 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -331,7 +331,7 @@ function LoginPage() { Forgot your password? */} - + Login diff --git a/src/pages/RegistrationPage.tsx b/src/pages/RegistrationPage.tsx index e162288..9acca56 100644 --- a/src/pages/RegistrationPage.tsx +++ b/src/pages/RegistrationPage.tsx @@ -275,7 +275,7 @@ function RegistrationPage() { - + Create Account diff --git a/src/pages/subpages/MFA.tsx b/src/pages/subpages/MFA.tsx index 86b273e..a9f3031 100644 --- a/src/pages/subpages/MFA.tsx +++ b/src/pages/subpages/MFA.tsx @@ -130,7 +130,7 @@ function MFA(props: Props) { - + Log In