diff --git a/src/App.jsx b/src/App.jsx index cf2770e..d462330 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -24,7 +24,7 @@ import { } from "react-router-dom"; import * as Sentry from "@sentry/react"; import { OverlayProvider } from "@react-aria/overlays"; -import { Home } from "./Home"; +import { HomePage } from "./home/HomePage"; import { LoginPage } from "./LoginPage"; import { RegisterPage } from "./RegisterPage"; import { Room } from "./Room"; @@ -45,7 +45,7 @@ export default function App({ history }) { <OverlayProvider> <Switch> <SentryRoute exact path="/"> - <Home /> + <HomePage /> </SentryRoute> <SentryRoute exact path="/login"> <LoginPage /> diff --git a/src/Home.module.css b/src/Home.module.css deleted file mode 100644 index 15e8534..0000000 --- a/src/Home.module.css +++ /dev/null @@ -1,139 +0,0 @@ -.home { - display: flex; - flex: 1; - flex-direction: column; - min-height: 100%; -} - -.splitContainer { - display: flex; - flex: 1; - flex-direction: column; -} - -.left, -.right { - display: flex; - flex-direction: column; - flex: 1; -} - -.fullWidth { - background-color: var(--bgColor1); -} - -.fullWidth .header { - background-color: var(--bgColor1); -} - -.centered { - display: flex; - flex-direction: column; - flex: 1; - width: 100%; - max-width: 512px; - min-width: 0; -} - -.content { - flex: 1; -} - -.left .content { - display: flex; - flex-direction: column; - align-items: center; -} - -.left .content form > * { - margin-top: 0; - margin-bottom: 24px; -} - -.left .content form > :last-child { - margin-bottom: 0; -} - -.left .content hr:after { - background-color: var(--bgColor1); - content: "OR"; - padding: 0 12px; - position: relative; - top: -12px; -} - -.left .content form { - display: flex; - flex-direction: column; - align-items: center; - padding: 40px 92px; -} - -.fieldRow { - width: 100%; -} - -.button { - height: 40px; - width: 100%; - font-size: 15px; - font-weight: 600; -} - -.left .content form:first-child { - padding-top: 0; -} - -.left .content form:last-child { - padding-bottom: 40px; -} - -.right .content { - padding: 0 40px 40px 40px; -} - -.right .content h3:first-child { - margin-top: 0; -} - -.authLinks { - display: flex; - flex-direction: column; - justify-content: flex-end; - align-items: center; -} - -.authLinks { - margin-bottom: 100px; - font-size: 15px; -} - -.authLinks a { - color: #0dbd8b; - font-weight: normal; - text-decoration: none; -} - -@media (min-width: 800px) { - .left { - background-color: var(--bgColor2); - } - - .home:not(.fullWidth) .left { - max-width: 50%; - } - - .home:not(.fullWidth) .leftNav { - background-color: var(--bgColor2); - } - - .splitContainer { - flex-direction: row; - } - - .fullWidth .content hr:after, - .left .content hr:after, - .fullWidth .header { - background-color: var(--bgColor2); - } -} diff --git a/src/CallList.jsx b/src/home/CallList.jsx similarity index 61% rename from src/CallList.jsx rename to src/home/CallList.jsx index 63cd8e5..8d8fc01 100644 --- a/src/CallList.jsx +++ b/src/home/CallList.jsx @@ -1,16 +1,16 @@ -import React, { useMemo } from "react"; +import React from "react"; import { Link } from "react-router-dom"; -import { CopyButton } from "./button"; -import { Facepile } from "./Facepile"; -import { Avatar } from "./Avatar"; -import { ReactComponent as VideoIcon } from "./icons/Video.svg"; +import { CopyButton } from "../button"; +import { Facepile } from "../Facepile"; +import { Avatar } from "../Avatar"; +import { ReactComponent as VideoIcon } from "../icons/Video.svg"; import styles from "./CallList.module.css"; -import { getRoomUrl } from "./ConferenceCallManagerHooks"; +import { getRoomUrl } from "../ConferenceCallManagerHooks"; +import { Body, Caption } from "../typography/Typography"; -export function CallList({ title, rooms, client }) { +export function CallList({ rooms, client }) { return ( <> - <h3>{title}</h3> <div className={styles.callList}> {rooms.map(({ roomId, roomName, avatarUrl, participants }) => ( <CallTile @@ -32,17 +32,23 @@ function CallTile({ name, avatarUrl, roomId, participants, client }) { <div className={styles.callTile}> <Link to={`/room/${roomId}`} className={styles.callTileLink}> <Avatar - size="md" + size="lg" bgKey={name} src={avatarUrl} fallback={<VideoIcon width={16} height={16} />} className={styles.avatar} /> <div className={styles.callInfo}> - <h5>{name}</h5> - <p>{getRoomUrl(roomId)}</p> + <Body overflowEllipsis fontWeight="semiBold"> + {name} + </Body> + <Caption overflowEllipsis>{getRoomUrl(roomId)}</Caption> {participants && ( - <Facepile client={client} participants={participants} /> + <Facepile + className={styles.facePile} + client={client} + participants={participants} + /> )} </div> <div className={styles.copyButtonSpacer} /> diff --git a/src/CallList.module.css b/src/home/CallList.module.css similarity index 61% rename from src/CallList.module.css rename to src/home/CallList.module.css index 1d2b62b..7c658a6 100644 --- a/src/CallList.module.css +++ b/src/home/CallList.module.css @@ -1,6 +1,6 @@ .callTile { - min-width: 240px; - height: 94px; + width: 329px; + height: 95px; padding: 12px; background-color: var(--bgColor2); border-radius: 8px; @@ -31,28 +31,11 @@ } .callInfo > * { - margin-top: 0; - margin-bottom: 8px; -} - -.callInfo > :last-child { margin-bottom: 0; } -.callInfo h5 { - font-size: 15px; - font-weight: 600; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.callInfo p { - font-weight: 400; - font-size: 12px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; +.facePile { + margin-top: 8px; } .copyButtonSpacer, @@ -69,6 +52,11 @@ .callList { display: grid; - grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + grid-template-columns: repeat(auto-fill, 329px); + max-width: calc((329px + 24px) * 3); + width: calc(100% - 48px); gap: 24px; + padding: 0 24px; + justify-content: center; + margin-bottom: 24px; } diff --git a/src/Home.jsx b/src/home/HomePage.jsx similarity index 78% rename from src/Home.jsx rename to src/home/HomePage.jsx index d26878f..0c5a811 100644 --- a/src/Home.jsx +++ b/src/home/HomePage.jsx @@ -15,12 +15,12 @@ limitations under the License. */ import React from "react"; -import { useClient } from "./ConferenceCallManagerHooks"; -import { ErrorView, LoadingView } from "./FullScreenView"; -import { UnauthenticatedView } from "./home/UnauthenticatedView"; -import { RegisteredView } from "./home/RegisteredView"; +import { useClient } from "../ConferenceCallManagerHooks"; +import { ErrorView, LoadingView } from "../FullScreenView"; +import { UnauthenticatedView } from "./UnauthenticatedView"; +import { RegisteredView } from "./RegisteredView"; -export function Home() { +export function HomePage() { const { isAuthenticated, isPasswordlessUser, loading, error, client } = useClient(); diff --git a/src/home/RegisteredView.jsx b/src/home/RegisteredView.jsx index eabec9b..e17ef12 100644 --- a/src/home/RegisteredView.jsx +++ b/src/home/RegisteredView.jsx @@ -1,131 +1,117 @@ -import React from "react"; -import { Link } from "react-router-dom"; +import React, { useState, useCallback } from "react"; import { + createRoom, useGroupCallRooms, - usePublicRooms, + roomAliasFromRoomName, } from "../ConferenceCallManagerHooks"; import { Header, HeaderLogo, LeftNav, RightNav } from "../Header"; -import styles from "../Home.module.css"; +import commonStyles from "./common.module.css"; +import styles from "./RegisteredView.module.css"; import { FieldRow, InputField, ErrorMessage } from "../Input"; import { Button } from "../button"; -import { CallList } from "../CallList"; -import classNames from "classnames"; +import { CallList } from "./CallList"; import { UserMenuContainer } from "../UserMenuContainer"; +import { useModalTriggerState } from "../Modal"; +import { JoinExistingCallModal } from "../JoinExistingCallModal"; +import { useHistory } from "react-router-dom"; +import { Headline, Title } from "../typography/Typography"; +import { Form } from "../form/Form"; -export function RegisteredView({ - client, - isPasswordlessUser, - onCreateRoom, - createRoomError, - creatingRoom, - onJoinRoom, -}) { - const publicRooms = usePublicRooms( - client, - import.meta.env.VITE_PUBLIC_SPACE_ROOM_ID +export function RegisteredView({ client }) { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(); + const onSubmit = useCallback( + (e) => { + e.preventDefault(); + const data = new FormData(e.target); + const roomName = data.get("roomName"); + + async function submit() { + setError(undefined); + setLoading(true); + + const roomIdOrAlias = await createRoom(client, roomName); + + if (roomIdOrAlias) { + history.push(`/room/${roomIdOrAlias}`); + } + } + + submit().catch((error) => { + if (error.errcode === "M_ROOM_IN_USE") { + setExistingRoomId(roomAliasFromRoomName(roomName)); + setError(undefined); + modalState.open(); + } else { + console.error(error); + setLoading(false); + setError(error); + reset(); + } + }); + }, + [client] ); + const recentRooms = useGroupCallRooms(client); - const hideCallList = publicRooms.length === 0 && recentRooms.length === 0; + const { modalState, modalProps } = useModalTriggerState(); + const [existingRoomId, setExistingRoomId] = useState(); + const history = useHistory(); + const onJoinExistingRoom = useCallback(() => { + history.push(`/${existingRoomId}`); + }, [history, existingRoomId]); return ( - <div - className={classNames(styles.home, { - [styles.fullWidth]: hideCallList, - })} - > - <Header className={styles.header}> - <LeftNav className={styles.leftNav}> + <> + <Header> + <LeftNav> <HeaderLogo /> </LeftNav> <RightNav> <UserMenuContainer /> </RightNav> </Header> - <div className={styles.splitContainer}> - <div className={styles.left}> - <div className={styles.content}> - <div className={styles.centered}> - <form onSubmit={onJoinRoom}> - <h1>Join a call</h1> - <FieldRow className={styles.fieldRow}> - <InputField - id="roomId" - name="roomId" - label="Call ID" - type="text" - required - autoComplete="off" - placeholder="Call ID" - /> - </FieldRow> - <FieldRow className={styles.fieldRow}> - <Button className={styles.button} type="submit"> - Join call - </Button> - </FieldRow> - </form> - <hr /> - <form onSubmit={onCreateRoom}> - <h1>Create a call</h1> - <FieldRow className={styles.fieldRow}> - <InputField - id="roomName" - name="roomName" - label="Room Name" - type="text" - required - autoComplete="off" - placeholder="Room Name" - /> - </FieldRow> - {createRoomError && ( - <FieldRow className={styles.fieldRow}> - <ErrorMessage>{createRoomError.message}</ErrorMessage> - </FieldRow> - )} - <FieldRow className={styles.fieldRow}> - <Button - className={styles.button} - type="submit" - disabled={creatingRoom} - > - {creatingRoom ? "Creating call..." : "Create call"} - </Button> - </FieldRow> - </form> - {isPasswordlessUser && ( - <div className={styles.authLinks}> - <p> - Not registered yet?{" "} - <Link to="/register">Create an account</Link> - </p> - </div> - )} - </div> - </div> - </div> - {!hideCallList && ( - <div className={styles.right}> - <div className={styles.content}> - {publicRooms.length > 0 && ( - <CallList - title="Public Calls" - rooms={publicRooms} - client={client} - /> - )} - {recentRooms.length > 0 && ( - <CallList - title="Recent Calls" - rooms={recentRooms} - client={client} - /> - )} - </div> - </div> - )} + <div className={commonStyles.container}> + <main className={commonStyles.main}> + <HeaderLogo className={commonStyles.logo} /> + <Headline className={commonStyles.headline}> + Enter a call name + </Headline> + <Form className={styles.form} onSubmit={onSubmit}> + <FieldRow className={styles.fieldRow}> + <InputField + id="callName" + name="callName" + label="Call name" + placeholder="Call name" + type="text" + required + autoComplete="off" + /> + <Button type="submit" size="lg" disabled={loading}> + {loading ? "Loading..." : "Go"} + </Button> + </FieldRow> + {error && ( + <FieldRow> + <ErrorMessage>{error.message}</ErrorMessage> + </FieldRow> + )} + </Form> + {recentRooms.length > 0 && ( + <> + <Title className={styles.recentCallsTitle}> + Your recent Calls + </Title> + <CallList rooms={recentRooms} client={client} /> + </> + )} + </main> </div> - </div> + {modalState.isOpen && ( + <JoinExistingCallModal onJoin={onJoinExistingRoom} {...modalProps} /> + )} + </> ); } diff --git a/src/home/RegisteredView.module.css b/src/home/RegisteredView.module.css new file mode 100644 index 0000000..03eff0a --- /dev/null +++ b/src/home/RegisteredView.module.css @@ -0,0 +1,14 @@ +.form { + padding: 0 24px; + justify-content: center; + width: 409px; + margin-bottom: 72px; +} + +.fieldRow { + margin-bottom: 0; +} + +.recentCallsTitle { + margin-bottom: 32px; +} diff --git a/src/home/UnauthenticatedView.jsx b/src/home/UnauthenticatedView.jsx index 8c0b684..c779817 100644 --- a/src/home/UnauthenticatedView.jsx +++ b/src/home/UnauthenticatedView.jsx @@ -8,13 +8,15 @@ import { randomString } from "matrix-js-sdk/src/randomstring"; import { createRoom, useInteractiveRegistration, + roomAliasFromRoomName, } from "../ConferenceCallManagerHooks"; import { useModalTriggerState } from "../Modal"; import { JoinExistingCallModal } from "../JoinExistingCallModal"; import { useRecaptcha } from "../useRecaptcha"; -import { Body, Caption, Title, Link, Headline } from "../typography/Typography"; +import { Body, Caption, Link, Headline } from "../typography/Typography"; import { Form } from "../form/Form"; import styles from "./UnauthenticatedView.module.css"; +import commonStyles from "./common.module.css"; export function UnauthenticatedView() { const [loading, setLoading] = useState(false); @@ -80,10 +82,12 @@ export function UnauthenticatedView() { <UserMenuContainer /> </RightNav> </Header> - <div className={styles.container}> - <main className={styles.main}> - <HeaderLogo className={styles.logo} /> - <Headline className={styles.headline}>Enter a call name</Headline> + <div className={commonStyles.container}> + <main className={commonStyles.main}> + <HeaderLogo className={commonStyles.logo} /> + <Headline className={commonStyles.headline}> + Enter a call name + </Headline> <Form className={styles.form} onSubmit={onSubmit}> <FieldRow> <InputField diff --git a/src/home/UnauthenticatedView.module.css b/src/home/UnauthenticatedView.module.css index 254ddb6..f336ef1 100644 --- a/src/home/UnauthenticatedView.module.css +++ b/src/home/UnauthenticatedView.module.css @@ -1,18 +1,3 @@ -.container { - display: flex; - min-height: calc(100% - 64px); - flex-direction: column; - justify-content: space-between; -} - -.main { - display: flex; - flex: 1; - flex-direction: column; - align-items: center; - justify-content: center; -} - .footer { display: flex; flex-direction: column; @@ -29,15 +14,6 @@ margin-bottom: 24px; } -.logo { - display: flex; - margin-bottom: 54px; -} - -.headline { - margin-bottom: 40px; -} - .form { padding: 0 24px; justify-content: center; @@ -49,15 +25,7 @@ } @media (min-width: 800px) { - .logo { - display: none; - } - .mobileLoginLink { display: none; } - - .container { - min-height: calc(100% - 76px); - } } diff --git a/src/home/common.module.css b/src/home/common.module.css new file mode 100644 index 0000000..13eac14 --- /dev/null +++ b/src/home/common.module.css @@ -0,0 +1,33 @@ +.container { + display: flex; + min-height: calc(100% - 64px); + flex-direction: column; + justify-content: space-between; +} + +.main { + display: flex; + flex: 1; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.logo { + display: flex; + margin-bottom: 54px; +} + +.headline { + margin-bottom: 40px; +} + +@media (min-width: 800px) { + .logo { + display: none; + } + + .container { + min-height: calc(100% - 76px); + } +} diff --git a/src/icons/Copy.svg b/src/icons/Copy.svg index 5f977f0..e539711 100644 --- a/src/icons/Copy.svg +++ b/src/icons/Copy.svg @@ -1,3 +1,3 @@ -<svg width="21" height="24" viewBox="0 0 21 24" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path fill-rule="evenodd" clip-rule="evenodd" d="M6.03125 3C4.3744 3 3.03125 4.34315 3.03125 6V13C3.03125 14.6569 4.3744 16 6.03125 16H7.07407V18C7.07407 19.6569 8.41722 21 10.0741 21H14.9683C16.6251 21 17.9683 19.6569 17.9683 18V11C17.9683 9.34315 16.6251 8 14.9683 8H13.9255V6C13.9255 4.34315 12.5823 3 10.9255 3H6.03125ZM11.9255 8V6C11.9255 5.44772 11.4777 5 10.9255 5H6.03125C5.47897 5 5.03125 5.44772 5.03125 6V13C5.03125 13.5523 5.47897 14 6.03125 14H7.07407V11C7.07407 9.34315 8.41722 8 10.0741 8H11.9255ZM9.07407 11C9.07407 10.4477 9.52179 10 10.0741 10H14.9683C15.5206 10 15.9683 10.4477 15.9683 11V18C15.9683 18.5523 15.5206 19 14.9683 19H10.0741C9.52179 19 9.07407 18.5523 9.07407 18V11Z" fill="#0DBD8B"/> +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M4 2C2.89543 2 2 2.89543 2 4V8.66667C2 9.77124 2.89543 10.6667 4 10.6667H5.33333V12C5.33333 13.1046 6.22877 14 7.33333 14H12C13.1046 14 14 13.1046 14 12V7.33333C14 6.22876 13.1046 5.33333 12 5.33333H10.6667V4C10.6667 2.89543 9.77123 2 8.66667 2H4ZM9.33333 5.33333V4C9.33333 3.63181 9.03486 3.33333 8.66667 3.33333H4C3.63181 3.33333 3.33333 3.63181 3.33333 4V8.66667C3.33333 9.03486 3.63181 9.33333 4 9.33333H5.33333V7.33333C5.33333 6.22877 6.22876 5.33333 7.33333 5.33333H9.33333ZM6.66667 7.33333C6.66667 6.96514 6.96514 6.66667 7.33333 6.66667H12C12.3682 6.66667 12.6667 6.96514 12.6667 7.33333V12C12.6667 12.3682 12.3682 12.6667 12 12.6667H7.33333C6.96514 12.6667 6.66667 12.3682 6.66667 12V7.33333Z" fill="#8E99A4"/> </svg> diff --git a/src/typography/Typography.jsx b/src/typography/Typography.jsx index 9193a48..a1f6ec2 100644 --- a/src/typography/Typography.jsx +++ b/src/typography/Typography.jsx @@ -4,11 +4,25 @@ import { Link as RouterLink } from "react-router-dom"; import styles from "./Typography.module.css"; export const Headline = forwardRef( - ({ as: Component = "h1", children, className, fontWeight, ...rest }, ref) => { + ( + { + as: Component = "h1", + children, + className, + fontWeight, + overflowEllipsis, + ...rest + }, + ref + ) => { return ( <Component {...rest} - className={classNames(styles[fontWeight], className)} + className={classNames( + styles[fontWeight], + { [styles.overflowEllipsis]: overflowEllipsis }, + className + )} ref={ref} > {children} @@ -18,11 +32,25 @@ export const Headline = forwardRef( ); export const Title = forwardRef( - ({ as: Component = "h2", children, className, fontWeight, ...rest }, ref) => { + ( + { + as: Component = "h2", + children, + className, + fontWeight, + overflowEllipsis, + ...rest + }, + ref + ) => { return ( <Component {...rest} - className={classNames(styles[fontWeight], className)} + className={classNames( + styles[fontWeight], + { [styles.overflowEllipsis]: overflowEllipsis }, + className + )} ref={ref} > {children} @@ -32,11 +60,25 @@ export const Title = forwardRef( ); export const Subtitle = forwardRef( - ({ as: Component = "h3", children, className, fontWeight, ...rest }, ref) => { + ( + { + as: Component = "h3", + children, + className, + fontWeight, + overflowEllipsis, + ...rest + }, + ref + ) => { return ( <Component {...rest} - className={classNames(styles[fontWeight], className)} + className={classNames( + styles[fontWeight], + { [styles.overflowEllipsis]: overflowEllipsis }, + className + )} ref={ref} > {children} @@ -46,11 +88,25 @@ export const Subtitle = forwardRef( ); export const Body = forwardRef( - ({ as: Component = "p", children, className, fontWeight, ...rest }, ref) => { + ( + { + as: Component = "p", + children, + className, + fontWeight, + overflowEllipsis, + ...rest + }, + ref + ) => { return ( <Component {...rest} - className={classNames(styles[fontWeight], className)} + className={classNames( + styles[fontWeight], + { [styles.overflowEllipsis]: overflowEllipsis }, + className + )} ref={ref} > {children} @@ -60,11 +116,26 @@ export const Body = forwardRef( ); export const Caption = forwardRef( - ({ as: Component = "p", children, className, fontWeight, ...rest }, ref) => { + ( + { + as: Component = "p", + children, + className, + fontWeight, + overflowEllipsis, + ...rest + }, + ref + ) => { return ( <Component {...rest} - className={classNames(styles.caption, styles[fontWeight], className)} + className={classNames( + styles.caption, + styles[fontWeight], + { [styles.overflowEllipsis]: overflowEllipsis }, + className + )} ref={ref} > {children} @@ -74,11 +145,26 @@ export const Caption = forwardRef( ); export const Micro = forwardRef( - ({ as: Component = "p", children, className, fontWeight, ...rest }, ref) => { + ( + { + as: Component = "p", + children, + className, + fontWeight, + overflowEllipsis, + ...rest + }, + ref + ) => { return ( <Component {...rest} - className={classNames(styles.micro, styles[fontWeight], className)} + className={classNames( + styles.micro, + styles[fontWeight], + { [styles.overflowEllipsis]: overflowEllipsis }, + className + )} ref={ref} > {children} @@ -96,6 +182,7 @@ export const Link = forwardRef( color = "link", href, fontWeight, + overflowEllipsis, ...rest }, ref @@ -113,7 +200,12 @@ export const Link = forwardRef( <Component {...externalLinkProps} {...rest} - className={classNames(styles[color], styles[fontWeight], className)} + className={classNames( + styles[color], + styles[fontWeight], + { [styles.overflowEllipsis]: overflowEllipsis }, + className + )} ref={ref} > {children} diff --git a/src/typography/Typography.module.css b/src/typography/Typography.module.css index ca6e354..0719fbf 100644 --- a/src/typography/Typography.module.css +++ b/src/typography/Typography.module.css @@ -27,3 +27,9 @@ .primary { color: var(--primaryColor); } + +.overflowEllipsis { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +}