Redesign homepage WIP

This commit is contained in:
Robert Long 2022-01-04 16:00:13 -08:00
parent eb620e9220
commit ef8c28f274
18 changed files with 697 additions and 437 deletions

View File

@ -29,6 +29,7 @@
"events": "^3.3.0",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#robertlong/group-call",
"matrix-react-sdk": "github:matrix-org/matrix-react-sdk#robertlong/group-call",
"normalize.css": "^8.0.1",
"postcss-preset-env": "^6.7.0",
"re-resizable": "^6.9.0",
"react": "^17.0.0",

View File

@ -31,10 +31,15 @@ export function LeftNav({ children, className, hideMobile, ...rest }) {
);
}
export function RightNav({ children, className, ...rest }) {
export function RightNav({ children, className, hideMobile, ...rest }) {
return (
<div
className={classNames(styles.nav, styles.rightNav, className)}
className={classNames(
styles.nav,
styles.rightNav,
{ [styles.hideMobile]: hideMobile },
className
)}
{...rest}
>
{children}
@ -42,9 +47,9 @@ export function RightNav({ children, className, ...rest }) {
);
}
export function HeaderLogo() {
export function HeaderLogo({ className }) {
return (
<Link className={styles.headerLogo} to="/">
<Link className={classNames(styles.headerLogo, className)} to="/">
<Logo />
</Link>
);

View File

@ -42,6 +42,10 @@
margin-right: 24px;
}
.rightNav.hideMobile {
display: none;
}
.nav > :last-child {
margin-right: 0;
}
@ -103,7 +107,8 @@
@media (min-width: 800px) {
.headerLogo,
.roomAvatar,
.leftNav.hideMobile {
.leftNav.hideMobile,
.rightNav.hideMobile {
display: flex;
}

View File

@ -14,399 +14,25 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useCallback, useState, useRef, useEffect } from "react";
import { useHistory, Link } from "react-router-dom";
import {
useClient,
useGroupCallRooms,
usePublicRooms,
createRoom,
roomAliasFromRoomName,
useInteractiveRegistration,
} from "./ConferenceCallManagerHooks";
import { Header, HeaderLogo, LeftNav, RightNav } from "./Header";
import styles from "./Home.module.css";
import { FieldRow, InputField, ErrorMessage } from "./Input";
import { UserMenu } from "./UserMenu";
import { Button } from "./button";
import { CallList } from "./CallList";
import classNames from "classnames";
import React from "react";
import { useClient } from "./ConferenceCallManagerHooks";
import { ErrorView, LoadingView } from "./FullScreenView";
import { useModalTriggerState } from "./Modal";
import { randomString } from "matrix-js-sdk/src/randomstring";
import { JoinExistingCallModal } from "./JoinExistingCallModal";
import { RecaptchaInput } from "./RecaptchaInput";
import { UserMenuContainer } from "./UserMenuContainer";
import { UnauthenticatedView } from "./home/UnauthenticatedView";
import { RegisteredView } from "./home/RegisteredView";
export function Home() {
const {
isAuthenticated,
isGuest,
isPasswordlessUser,
loading,
error,
client,
} = useClient();
const [{ privacyPolicyUrl, recaptchaKey }, register] =
useInteractiveRegistration();
const [recaptchaResponse, setRecaptchaResponse] = useState();
const history = useHistory();
const [creatingRoom, setCreatingRoom] = useState(false);
const [createRoomError, setCreateRoomError] = useState();
const { modalState, modalProps } = useModalTriggerState();
const [existingRoomId, setExistingRoomId] = useState();
const onCreateRoom = useCallback(
(e) => {
e.preventDefault();
const data = new FormData(e.target);
const roomName = data.get("roomName");
const userName = data.get("userName");
async function onCreateRoom() {
let _client = client;
if (!recaptchaResponse) {
return;
}
if (!_client || isGuest) {
_client = await register(
userName,
randomString(16),
recaptchaResponse,
true
);
}
const roomIdOrAlias = await createRoom(_client, roomName);
if (roomIdOrAlias) {
history.push(`/room/${roomIdOrAlias}`);
}
}
setCreateRoomError(undefined);
setCreatingRoom(true);
return onCreateRoom().catch((error) => {
if (error.errcode === "M_ROOM_IN_USE") {
setExistingRoomId(roomAliasFromRoomName(roomName));
setCreateRoomError(undefined);
modalState.open();
} else {
setCreateRoomError(error);
}
setCreatingRoom(false);
});
},
[client, history, register, isGuest, recaptchaResponse]
);
const onJoinRoom = useCallback(
(e) => {
e.preventDefault();
const data = new FormData(e.target);
const roomId = data.get("roomId");
history.push(`/${roomId}`);
},
[history]
);
const onJoinExistingRoom = useCallback(() => {
history.push(`/${existingRoomId}`);
}, [history, existingRoomId]);
const { isAuthenticated, isPasswordlessUser, loading, error, client } =
useClient();
if (loading) {
return <LoadingView />;
} else if (error) {
return <ErrorView error={error} />;
} else {
return (
<>
{!isAuthenticated || isGuest ? (
<UnregisteredView
onCreateRoom={onCreateRoom}
createRoomError={createRoomError}
creatingRoom={creatingRoom}
onJoinRoom={onJoinRoom}
privacyPolicyUrl={privacyPolicyUrl}
recaptchaKey={recaptchaKey}
setRecaptchaResponse={setRecaptchaResponse}
/>
) : (
<RegisteredView
client={client}
isPasswordlessUser={isPasswordlessUser}
isGuest={isGuest}
onCreateRoom={onCreateRoom}
createRoomError={createRoomError}
creatingRoom={creatingRoom}
onJoinRoom={onJoinRoom}
/>
)}
{modalState.isOpen && (
<JoinExistingCallModal onJoin={onJoinExistingRoom} {...modalProps} />
)}
</>
return isAuthenticated ? (
<RegisteredView isPasswordlessUser={isPasswordlessUser} client={client} />
) : (
<UnauthenticatedView />
);
}
}
function UnregisteredView({
onCreateRoom,
createRoomError,
creatingRoom,
onJoinRoom,
privacyPolicyUrl,
recaptchaKey,
setRecaptchaResponse,
}) {
const acceptTermsRef = useRef();
const [acceptTerms, setAcceptTerms] = useState(false);
useEffect(() => {
if (!acceptTermsRef.current) {
return;
}
if (!acceptTerms) {
acceptTermsRef.current.setCustomValidity(
"You must accept the terms to continue."
);
} else {
acceptTermsRef.current.setCustomValidity("");
}
}, [acceptTerms]);
return (
<div className={classNames(styles.home, styles.fullWidth)}>
<Header className={styles.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="userName"
name="userName"
label="Username"
type="text"
required
autoComplete="off"
placeholder="Username"
/>
</FieldRow>
<FieldRow className={styles.fieldRow}>
<InputField
id="roomName"
name="roomName"
label="Room Name"
type="text"
required
autoComplete="off"
placeholder="Room Name"
/>
</FieldRow>
<FieldRow>
<InputField
id="acceptTerms"
type="checkbox"
name="acceptTerms"
onChange={(e) => setAcceptTerms(e.target.checked)}
checked={acceptTerms}
label="Accept Privacy Policy"
ref={acceptTermsRef}
/>
<a target="_blank" href={privacyPolicyUrl}>
Privacy Policy
</a>
</FieldRow>
{recaptchaKey && (
<FieldRow>
<RecaptchaInput
publicKey={recaptchaKey}
onResponse={setRecaptchaResponse}
/>
</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>
<div className={styles.authLinks}>
<p>
Not registered yet?{" "}
<Link to="/register">Create an account</Link>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
function RegisteredView({
client,
isPasswordlessUser,
isGuest,
onCreateRoom,
createRoomError,
creatingRoom,
onJoinRoom,
}) {
const publicRooms = usePublicRooms(
client,
import.meta.env.VITE_PUBLIC_SPACE_ROOM_ID
);
const recentRooms = useGroupCallRooms(client);
const hideCallList = publicRooms.length === 0 && recentRooms.length === 0;
return (
<div
className={classNames(styles.home, {
[styles.fullWidth]: hideCallList,
})}
>
<Header className={styles.header}>
<LeftNav className={styles.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 || isGuest) && (
<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>
</div>
);
}

View File

@ -8,7 +8,6 @@ export function ProfileModal({
client,
isAuthenticated,
isPasswordlessUser,
isGuest,
...rest
}) {
const { onClose } = rest;
@ -66,7 +65,7 @@ export function ProfileModal({
onChange={onChangeDisplayName}
/>
</FieldRow>
{isAuthenticated && !isGuest && !isPasswordlessUser && (
{isAuthenticated && !isPasswordlessUser && (
<FieldRow>
<InputField
type="file"

View File

@ -60,34 +60,17 @@ const canScreenshare = "getDisplayMedia" in navigator.mediaDevices;
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
export function Room() {
const [registeringGuest, setRegisteringGuest] = useState(false);
const [registrationError, setRegistrationError] = useState();
const {
loading,
isAuthenticated,
error,
client,
registerGuest,
isGuest,
isPasswordlessUser,
} = useClient();
const { loading, isAuthenticated, error, client, isPasswordlessUser } =
useClient();
useEffect(() => {
if (!loading && !isAuthenticated) {
setRegisteringGuest(true);
registerGuest()
.then(() => {
setRegisteringGuest(false);
})
.catch((error) => {
setRegistrationError(error);
setRegisteringGuest(false);
});
setRegistrationError(new Error("Must be registered"));
}
}, [loading, isAuthenticated]);
if (loading || registeringGuest) {
if (loading) {
return <LoadingView />;
}
@ -95,16 +78,10 @@ export function Room() {
return <ErrorView error={registrationError || error} />;
}
return (
<GroupCall
client={client}
isGuest={isGuest}
isPasswordlessUser={isPasswordlessUser}
/>
);
return <GroupCall client={client} isPasswordlessUser={isPasswordlessUser} />;
}
export function GroupCall({ client, isGuest, isPasswordlessUser }) {
export function GroupCall({ client, isPasswordlessUser }) {
const { roomId: maybeRoomId } = useParams();
const { hash, search } = useLocation();
const [simpleGrid, viaServers] = useMemo(() => {
@ -132,7 +109,6 @@ export function GroupCall({ client, isGuest, isPasswordlessUser }) {
return (
<GroupCallView
isGuest={isGuest}
isPasswordlessUser={isPasswordlessUser}
client={client}
roomId={roomId}
@ -144,7 +120,6 @@ export function GroupCall({ client, isGuest, isPasswordlessUser }) {
export function GroupCallView({
client,
isGuest,
isPasswordlessUser,
roomId,
groupCall,
@ -201,12 +176,12 @@ export function GroupCallView({
const onLeave = useCallback(() => {
leave();
if (!isGuest && !isPasswordlessUser) {
if (!isPasswordlessUser) {
history.push("/");
} else {
setLeft(true);
}
}, [leave, history, isGuest]);
}, [leave, history]);
if (error) {
return <ErrorView error={error} />;
@ -215,7 +190,6 @@ export function GroupCallView({
<InRoomView
groupCall={groupCall}
client={client}
isGuest={isGuest}
roomName={groupCall.room.name}
microphoneMuted={microphoneMuted}
localVideoMuted={localVideoMuted}
@ -245,7 +219,6 @@ export function GroupCallView({
} else {
return (
<RoomSetupView
isGuest={isGuest}
client={client}
hasLocalParticipant={hasLocalParticipant}
roomName={groupCall.room.name}
@ -376,7 +349,6 @@ function RoomSetupView({
function InRoomView({
client,
isGuest,
groupCall,
roomName,
microphoneMuted,
@ -471,7 +443,7 @@ function InRoomView({
</LeftNav>
<RightNav>
<GridLayoutMenu layout={layout} setLayout={setLayout} />
{!isGuest && <UserMenuContainer disableLogout />}
<UserMenuContainer disableLogout />
</RightNav>
</Header>
{items.length === 0 ? (

View File

@ -8,14 +8,8 @@ import { UserMenu } from "./UserMenu";
export function UserMenuContainer({ disableLogout }) {
const location = useLocation();
const history = useHistory();
const {
isAuthenticated,
isGuest,
isPasswordlessUser,
logout,
userName,
client,
} = useClient();
const { isAuthenticated, isPasswordlessUser, logout, userName, client } =
useClient();
const { displayName, avatarUrl } = useProfile(client);
const { modalState, modalProps } = useModalTriggerState();
@ -52,7 +46,6 @@ export function UserMenuContainer({ disableLogout }) {
<ProfileModal
client={client}
isAuthenticated={isAuthenticated}
isGuest={isGuest}
isPasswordlessUser={isPasswordlessUser}
{...modalProps}
/>

11
src/form/Form.jsx Normal file
View File

@ -0,0 +1,11 @@
import classNames from "classnames";
import React, { forwardRef } from "react";
import styles from "./Form.module.css";
export const Form = forwardRef(({ children, className, ...rest }, ref) => {
return (
<form {...rest} className={classNames(styles.form, className)} ref={ref}>
{children}
</form>
);
});

4
src/form/Form.module.css Normal file
View File

@ -0,0 +1,4 @@
.form {
display: flex;
flex-direction: column;
}

131
src/home/RegisteredView.jsx Normal file
View File

@ -0,0 +1,131 @@
import React from "react";
import { Link } from "react-router-dom";
import {
useGroupCallRooms,
usePublicRooms,
} from "../ConferenceCallManagerHooks";
import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
import styles from "../Home.module.css";
import { FieldRow, InputField, ErrorMessage } from "../Input";
import { Button } from "../button";
import { CallList } from "../CallList";
import classNames from "classnames";
import { UserMenuContainer } from "../UserMenuContainer";
export function RegisteredView({
client,
isPasswordlessUser,
onCreateRoom,
createRoomError,
creatingRoom,
onJoinRoom,
}) {
const publicRooms = usePublicRooms(
client,
import.meta.env.VITE_PUBLIC_SPACE_ROOM_ID
);
const recentRooms = useGroupCallRooms(client);
const hideCallList = publicRooms.length === 0 && recentRooms.length === 0;
return (
<div
className={classNames(styles.home, {
[styles.fullWidth]: hideCallList,
})}
>
<Header className={styles.header}>
<LeftNav className={styles.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>
</div>
);
}

View File

@ -0,0 +1,154 @@
import React, { useCallback, useState } from "react";
import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
import { UserMenuContainer } from "../UserMenuContainer";
import { useHistory } from "react-router-dom";
import { FieldRow, InputField, ErrorMessage } from "../Input";
import { Button } from "../button";
import { randomString } from "matrix-js-sdk/src/randomstring";
import {
createRoom,
useInteractiveRegistration,
} from "../ConferenceCallManagerHooks";
import { useModalTriggerState } from "../Modal";
import { JoinExistingCallModal } from "../JoinExistingCallModal";
import { useRecaptcha } from "../useRecaptcha";
import { Body, Caption, Title, Link, Headline } from "../typography/Typography";
import { Form } from "../form/Form";
import styles from "./UnauthenticatedView.module.css";
export function UnauthenticatedView() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState();
const [{ privacyPolicyUrl, recaptchaKey }, register] =
useInteractiveRegistration();
const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey);
const onSubmit = useCallback(
(e) => {
e.preventDefault();
const data = new FormData(e.target);
const roomName = data.get("roomName");
const userName = data.get("userName");
async function submit() {
setError(undefined);
setLoading(true);
const recaptchaResponse = await execute();
const client = await register(
userName,
randomString(16),
recaptchaResponse,
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();
}
});
},
[register]
);
const { modalState, modalProps } = useModalTriggerState();
const [existingRoomId, setExistingRoomId] = useState();
const history = useHistory();
const onJoinExistingRoom = useCallback(() => {
history.push(`/${existingRoomId}`);
}, [history, existingRoomId]);
return (
<>
<Header>
<LeftNav>
<HeaderLogo />
</LeftNav>
<RightNav hideMobile>
<UserMenuContainer />
</RightNav>
</Header>
<div className={styles.container}>
<main className={styles.main}>
<HeaderLogo className={styles.logo} />
<Headline className={styles.headline}>Enter a call name</Headline>
<Form className={styles.form} onSubmit={onSubmit}>
<FieldRow>
<InputField
id="callName"
name="callName"
label="Call name"
placeholder="Call name"
type="text"
required
autoComplete="off"
/>
</FieldRow>
<FieldRow>
<InputField
id="userName"
name="userName"
label="Your name"
placeholder="Your name"
type="text"
required
autoComplete="off"
/>
</FieldRow>
<Caption>
This site is protected by ReCAPTCHA and the Google{" "}
<Link href="https://www.google.com/policies/privacy/">
Privacy Policy
</Link>{" "}
and{" "}
<Link href="https://policies.google.com/terms">
Terms of Service
</Link>{" "}
apply.
<br />
By clicking "Go", you agree to our{" "}
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
</Caption>
{error && (
<FieldRow>
<ErrorMessage>{error.message}</ErrorMessage>
</FieldRow>
)}
<Button type="submit" size="lg" disabled={loading}>
{loading ? "Loading..." : "Go"}
</Button>
<div id={recaptchaId} />
</Form>
</main>
<footer className={styles.footer}>
<Body className={styles.mobileLoginLink}>
<Link color="primary" to="/l;ogin">
Login to your account
</Link>
</Body>
<Body>
Not registered yet?{" "}
<Link color="primary" to="/register">
Create an account
</Link>
</Body>
</footer>
</div>
{modalState.isOpen && (
<JoinExistingCallModal onJoin={onJoinExistingRoom} {...modalProps} />
)}
</>
);
}

View File

@ -0,0 +1,63 @@
.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;
align-items: center;
padding: 28px;
}
.footer p {
margin-bottom: 0;
}
.footer .mobileLoginLink {
display: flex;
margin-bottom: 24px;
}
.logo {
display: flex;
margin-bottom: 54px;
}
.headline {
margin-bottom: 40px;
}
.form {
padding: 0 24px;
justify-content: center;
max-width: 360px;
}
.form > * + * {
margin-bottom: 24px;
}
@media (min-width: 800px) {
.logo {
display: none;
}
.mobileLoginLink {
display: none;
}
.container {
min-height: calc(100% - 76px);
}
}

View File

@ -20,6 +20,8 @@ limitations under the License.
Therefore we define a unicode-range to load which excludes the glyphs
(to avoid having to maintain a fork of Inter). */
@import "normalize.css/normalize.css";
:root {
--inter-unicode-range: U+0000-20e2, U+20e4-23ce, U+23d0-24c1, U+24c3-259f,
U+25c2-2664, U+2666-2763, U+2765-2b05, U+2b07-2b1b, U+2b1d-10FFFF;
@ -35,6 +37,7 @@ limitations under the License.
--textColor4: #a9b2bc;
--inputBorderColor: #394049;
--inputBorderColorFocused: #0086e6;
--linkColor: #0086e6;
}
@font-face {
@ -139,6 +142,44 @@ body,
flex-direction: column;
}
h1,
h2,
h3,
h4,
h5,
h6,
p,
a {
margin-top: 0;
}
/* Headline Semi Bold */
h1 {
font-weight: 600;
font-size: 32px;
line-height: 39px;
}
/* Title */
h2 {
font-weight: 600;
font-size: 24px;
line-height: 29px;
}
/* Subtitle */
h3 {
font-weight: 400;
font-size: 18px;
line-height: 22px;
}
/* Body */
p {
font-size: 15px;
line-height: 24px;
}
a {
color: var(--primaryColor);
text-decoration: none;
@ -179,3 +220,7 @@ details[open] > summary {
position: relative;
height: 100%;
}
.grecaptcha-badge {
visibility: hidden;
}

View File

@ -0,0 +1,123 @@
import React, { forwardRef } from "react";
import classNames from "classnames";
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) => {
return (
<Component
{...rest}
className={classNames(styles[fontWeight], className)}
ref={ref}
>
{children}
</Component>
);
}
);
export const Title = forwardRef(
({ as: Component = "h2", children, className, fontWeight, ...rest }, ref) => {
return (
<Component
{...rest}
className={classNames(styles[fontWeight], className)}
ref={ref}
>
{children}
</Component>
);
}
);
export const Subtitle = forwardRef(
({ as: Component = "h3", children, className, fontWeight, ...rest }, ref) => {
return (
<Component
{...rest}
className={classNames(styles[fontWeight], className)}
ref={ref}
>
{children}
</Component>
);
}
);
export const Body = forwardRef(
({ as: Component = "p", children, className, fontWeight, ...rest }, ref) => {
return (
<Component
{...rest}
className={classNames(styles[fontWeight], className)}
ref={ref}
>
{children}
</Component>
);
}
);
export const Caption = forwardRef(
({ as: Component = "p", children, className, fontWeight, ...rest }, ref) => {
return (
<Component
{...rest}
className={classNames(styles.caption, styles[fontWeight], className)}
ref={ref}
>
{children}
</Component>
);
}
);
export const Micro = forwardRef(
({ as: Component = "p", children, className, fontWeight, ...rest }, ref) => {
return (
<Component
{...rest}
className={classNames(styles.micro, styles[fontWeight], className)}
ref={ref}
>
{children}
</Component>
);
}
);
export const Link = forwardRef(
(
{
as: Component = RouterLink,
children,
className,
color = "link",
href,
fontWeight,
...rest
},
ref
) => {
let externalLinkProps;
if (href) {
externalLinkProps = {
target: "_blank",
rel: "noreferrer noopener",
};
}
return (
<Component
{...externalLinkProps}
{...rest}
className={classNames(styles[color], styles[fontWeight], className)}
ref={ref}
>
{children}
</Component>
);
}
);

View File

@ -0,0 +1,29 @@
.caption {
font-size: 12px;
line-height: 15px;
}
.micro {
font-size: 10px;
line-height: 12px;
}
.regular {
font-weight: 400;
}
.semiBold {
font-weight: 600;
}
.bold {
font-weight: 700;
}
.link {
color: var(--linkColor);
}
.primary {
color: var(--primaryColor);
}

View File

@ -0,0 +1,25 @@
import React from "react";
import { Headline, Title, Subtitle, Body, Caption, Micro } from "./Typography";
export default {
title: "Typography",
parameters: {
layout: "fullscreen",
},
};
export const Typography = () => (
<>
<Headline>Headline Semi Bold</Headline>
<Title>Title</Title>
<Subtitle>Subtitle</Subtitle>
<Subtitle fontWeight="semiBold">Subtitle Semi Bold</Subtitle>
<Body>Body</Body>
<Body fontWeight="semiBold">Body Semi Bold</Body>
<Caption>Caption</Caption>
<Caption fontWeight="semiBold">Caption Semi Bold</Caption>
<Caption fontWeight="bold">Caption Bold</Caption>
<Micro>Micro</Micro>
<Micro fontWeight="bold">Micro bold</Micro>
</>
);

69
src/useRecaptcha.js Normal file
View File

@ -0,0 +1,69 @@
import { randomString } from "matrix-js-sdk/src/randomstring";
import { useEffect, useCallback, useRef, useState } from "react";
const RECAPTCHA_SCRIPT_URL =
"https://www.recaptcha.net/recaptcha/api.js?onload=mxOnRecaptchaLoaded&render=explicit";
export function useRecaptcha(sitekey) {
const [recaptchaId] = useState(() => randomString(16));
const resolvePromiseRef = useRef();
useEffect(() => {
if (!sitekey) {
return;
}
const onRecaptchaLoaded = () => {
if (!document.getElementById(recaptchaId)) {
return;
}
window.grecaptcha.render(recaptchaId, {
sitekey,
size: "invisible",
callback: (response) => {
if (resolvePromiseRef.current) {
resolvePromiseRef.current(response);
}
},
});
};
if (
typeof window.grecaptcha !== "undefined" &&
typeof window.grecaptcha.render === "function"
) {
onRecaptchaLoaded();
} else {
window.mxOnRecaptchaLoaded = onRecaptchaLoaded;
if (!document.querySelector(`script[src="${RECAPTCHA_SCRIPT_URL}"]`)) {
const scriptTag = document.createElement("script");
scriptTag.src = RECAPTCHA_SCRIPT_URL;
scriptTag.async = true;
document.body.appendChild(scriptTag);
}
}
}, [recaptchaId, sitekey]);
const execute = useCallback(() => {
if (!window.grecaptcha) {
return Promise.reject(new Error("Recaptcha not loaded"));
}
return new Promise((resolve) => {
resolvePromiseRef.current = resolve;
window.grecaptcha.execute();
});
}, [recaptchaId]);
const reset = useCallback(() => {
if (window.grecaptcha) {
window.grecaptcha.reset();
}
}, [recaptchaId]);
console.log(recaptchaId);
return { execute, reset, recaptchaId };
}

View File

@ -9155,6 +9155,11 @@ normalize-range@^0.1.2:
resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=
normalize.css@^8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/normalize.css/-/normalize.css-8.0.1.tgz#9b98a208738b9cc2634caacbc42d131c97487bf3"
integrity sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==
npm-run-path@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"