Redesign homepage WIP
This commit is contained in:
parent
eb620e9220
commit
ef8c28f274
18 changed files with 697 additions and 437 deletions
|
@ -29,6 +29,7 @@
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#robertlong/group-call",
|
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#robertlong/group-call",
|
||||||
"matrix-react-sdk": "github:matrix-org/matrix-react-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",
|
"postcss-preset-env": "^6.7.0",
|
||||||
"re-resizable": "^6.9.0",
|
"re-resizable": "^6.9.0",
|
||||||
"react": "^17.0.0",
|
"react": "^17.0.0",
|
||||||
|
|
|
@ -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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(styles.nav, styles.rightNav, className)}
|
className={classNames(
|
||||||
|
styles.nav,
|
||||||
|
styles.rightNav,
|
||||||
|
{ [styles.hideMobile]: hideMobile },
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
@ -42,9 +47,9 @@ export function RightNav({ children, className, ...rest }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HeaderLogo() {
|
export function HeaderLogo({ className }) {
|
||||||
return (
|
return (
|
||||||
<Link className={styles.headerLogo} to="/">
|
<Link className={classNames(styles.headerLogo, className)} to="/">
|
||||||
<Logo />
|
<Logo />
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|
|
@ -42,6 +42,10 @@
|
||||||
margin-right: 24px;
|
margin-right: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rightNav.hideMobile {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.nav > :last-child {
|
.nav > :last-child {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
@ -103,7 +107,8 @@
|
||||||
@media (min-width: 800px) {
|
@media (min-width: 800px) {
|
||||||
.headerLogo,
|
.headerLogo,
|
||||||
.roomAvatar,
|
.roomAvatar,
|
||||||
.leftNav.hideMobile {
|
.leftNav.hideMobile,
|
||||||
|
.rightNav.hideMobile {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
392
src/Home.jsx
392
src/Home.jsx
|
@ -14,399 +14,25 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback, useState, useRef, useEffect } from "react";
|
import React from "react";
|
||||||
import { useHistory, Link } from "react-router-dom";
|
import { useClient } from "./ConferenceCallManagerHooks";
|
||||||
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 { ErrorView, LoadingView } from "./FullScreenView";
|
import { ErrorView, LoadingView } from "./FullScreenView";
|
||||||
import { useModalTriggerState } from "./Modal";
|
import { UnauthenticatedView } from "./home/UnauthenticatedView";
|
||||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
import { RegisteredView } from "./home/RegisteredView";
|
||||||
import { JoinExistingCallModal } from "./JoinExistingCallModal";
|
|
||||||
import { RecaptchaInput } from "./RecaptchaInput";
|
|
||||||
import { UserMenuContainer } from "./UserMenuContainer";
|
|
||||||
|
|
||||||
export function Home() {
|
export function Home() {
|
||||||
const {
|
const { isAuthenticated, isPasswordlessUser, loading, error, client } =
|
||||||
isAuthenticated,
|
useClient();
|
||||||
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]);
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <LoadingView />;
|
return <LoadingView />;
|
||||||
} else if (error) {
|
} else if (error) {
|
||||||
return <ErrorView error={error} />;
|
return <ErrorView error={error} />;
|
||||||
} else {
|
} else {
|
||||||
return (
|
return isAuthenticated ? (
|
||||||
<>
|
<RegisteredView isPasswordlessUser={isPasswordlessUser} client={client} />
|
||||||
{!isAuthenticated || isGuest ? (
|
|
||||||
<UnregisteredView
|
|
||||||
onCreateRoom={onCreateRoom}
|
|
||||||
createRoomError={createRoomError}
|
|
||||||
creatingRoom={creatingRoom}
|
|
||||||
onJoinRoom={onJoinRoom}
|
|
||||||
privacyPolicyUrl={privacyPolicyUrl}
|
|
||||||
recaptchaKey={recaptchaKey}
|
|
||||||
setRecaptchaResponse={setRecaptchaResponse}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<RegisteredView
|
<UnauthenticatedView />
|
||||||
client={client}
|
|
||||||
isPasswordlessUser={isPasswordlessUser}
|
|
||||||
isGuest={isGuest}
|
|
||||||
onCreateRoom={onCreateRoom}
|
|
||||||
createRoomError={createRoomError}
|
|
||||||
creatingRoom={creatingRoom}
|
|
||||||
onJoinRoom={onJoinRoom}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{modalState.isOpen && (
|
|
||||||
<JoinExistingCallModal onJoin={onJoinExistingRoom} {...modalProps} />
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
@ -8,7 +8,6 @@ export function ProfileModal({
|
||||||
client,
|
client,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
isPasswordlessUser,
|
isPasswordlessUser,
|
||||||
isGuest,
|
|
||||||
...rest
|
...rest
|
||||||
}) {
|
}) {
|
||||||
const { onClose } = rest;
|
const { onClose } = rest;
|
||||||
|
@ -66,7 +65,7 @@ export function ProfileModal({
|
||||||
onChange={onChangeDisplayName}
|
onChange={onChangeDisplayName}
|
||||||
/>
|
/>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
{isAuthenticated && !isGuest && !isPasswordlessUser && (
|
{isAuthenticated && !isPasswordlessUser && (
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<InputField
|
<InputField
|
||||||
type="file"
|
type="file"
|
||||||
|
|
46
src/Room.jsx
46
src/Room.jsx
|
@ -60,34 +60,17 @@ const canScreenshare = "getDisplayMedia" in navigator.mediaDevices;
|
||||||
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||||
|
|
||||||
export function Room() {
|
export function Room() {
|
||||||
const [registeringGuest, setRegisteringGuest] = useState(false);
|
|
||||||
const [registrationError, setRegistrationError] = useState();
|
const [registrationError, setRegistrationError] = useState();
|
||||||
const {
|
const { loading, isAuthenticated, error, client, isPasswordlessUser } =
|
||||||
loading,
|
useClient();
|
||||||
isAuthenticated,
|
|
||||||
error,
|
|
||||||
client,
|
|
||||||
registerGuest,
|
|
||||||
isGuest,
|
|
||||||
isPasswordlessUser,
|
|
||||||
} = useClient();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loading && !isAuthenticated) {
|
if (!loading && !isAuthenticated) {
|
||||||
setRegisteringGuest(true);
|
setRegistrationError(new Error("Must be registered"));
|
||||||
|
|
||||||
registerGuest()
|
|
||||||
.then(() => {
|
|
||||||
setRegisteringGuest(false);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
setRegistrationError(error);
|
|
||||||
setRegisteringGuest(false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, [loading, isAuthenticated]);
|
}, [loading, isAuthenticated]);
|
||||||
|
|
||||||
if (loading || registeringGuest) {
|
if (loading) {
|
||||||
return <LoadingView />;
|
return <LoadingView />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,16 +78,10 @@ export function Room() {
|
||||||
return <ErrorView error={registrationError || error} />;
|
return <ErrorView error={registrationError || error} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <GroupCall client={client} isPasswordlessUser={isPasswordlessUser} />;
|
||||||
<GroupCall
|
|
||||||
client={client}
|
|
||||||
isGuest={isGuest}
|
|
||||||
isPasswordlessUser={isPasswordlessUser}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GroupCall({ client, isGuest, isPasswordlessUser }) {
|
export function GroupCall({ client, isPasswordlessUser }) {
|
||||||
const { roomId: maybeRoomId } = useParams();
|
const { roomId: maybeRoomId } = useParams();
|
||||||
const { hash, search } = useLocation();
|
const { hash, search } = useLocation();
|
||||||
const [simpleGrid, viaServers] = useMemo(() => {
|
const [simpleGrid, viaServers] = useMemo(() => {
|
||||||
|
@ -132,7 +109,6 @@ export function GroupCall({ client, isGuest, isPasswordlessUser }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GroupCallView
|
<GroupCallView
|
||||||
isGuest={isGuest}
|
|
||||||
isPasswordlessUser={isPasswordlessUser}
|
isPasswordlessUser={isPasswordlessUser}
|
||||||
client={client}
|
client={client}
|
||||||
roomId={roomId}
|
roomId={roomId}
|
||||||
|
@ -144,7 +120,6 @@ export function GroupCall({ client, isGuest, isPasswordlessUser }) {
|
||||||
|
|
||||||
export function GroupCallView({
|
export function GroupCallView({
|
||||||
client,
|
client,
|
||||||
isGuest,
|
|
||||||
isPasswordlessUser,
|
isPasswordlessUser,
|
||||||
roomId,
|
roomId,
|
||||||
groupCall,
|
groupCall,
|
||||||
|
@ -201,12 +176,12 @@ export function GroupCallView({
|
||||||
const onLeave = useCallback(() => {
|
const onLeave = useCallback(() => {
|
||||||
leave();
|
leave();
|
||||||
|
|
||||||
if (!isGuest && !isPasswordlessUser) {
|
if (!isPasswordlessUser) {
|
||||||
history.push("/");
|
history.push("/");
|
||||||
} else {
|
} else {
|
||||||
setLeft(true);
|
setLeft(true);
|
||||||
}
|
}
|
||||||
}, [leave, history, isGuest]);
|
}, [leave, history]);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <ErrorView error={error} />;
|
return <ErrorView error={error} />;
|
||||||
|
@ -215,7 +190,6 @@ export function GroupCallView({
|
||||||
<InRoomView
|
<InRoomView
|
||||||
groupCall={groupCall}
|
groupCall={groupCall}
|
||||||
client={client}
|
client={client}
|
||||||
isGuest={isGuest}
|
|
||||||
roomName={groupCall.room.name}
|
roomName={groupCall.room.name}
|
||||||
microphoneMuted={microphoneMuted}
|
microphoneMuted={microphoneMuted}
|
||||||
localVideoMuted={localVideoMuted}
|
localVideoMuted={localVideoMuted}
|
||||||
|
@ -245,7 +219,6 @@ export function GroupCallView({
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<RoomSetupView
|
<RoomSetupView
|
||||||
isGuest={isGuest}
|
|
||||||
client={client}
|
client={client}
|
||||||
hasLocalParticipant={hasLocalParticipant}
|
hasLocalParticipant={hasLocalParticipant}
|
||||||
roomName={groupCall.room.name}
|
roomName={groupCall.room.name}
|
||||||
|
@ -376,7 +349,6 @@ function RoomSetupView({
|
||||||
|
|
||||||
function InRoomView({
|
function InRoomView({
|
||||||
client,
|
client,
|
||||||
isGuest,
|
|
||||||
groupCall,
|
groupCall,
|
||||||
roomName,
|
roomName,
|
||||||
microphoneMuted,
|
microphoneMuted,
|
||||||
|
@ -471,7 +443,7 @@ function InRoomView({
|
||||||
</LeftNav>
|
</LeftNav>
|
||||||
<RightNav>
|
<RightNav>
|
||||||
<GridLayoutMenu layout={layout} setLayout={setLayout} />
|
<GridLayoutMenu layout={layout} setLayout={setLayout} />
|
||||||
{!isGuest && <UserMenuContainer disableLogout />}
|
<UserMenuContainer disableLogout />
|
||||||
</RightNav>
|
</RightNav>
|
||||||
</Header>
|
</Header>
|
||||||
{items.length === 0 ? (
|
{items.length === 0 ? (
|
||||||
|
|
|
@ -8,14 +8,8 @@ import { UserMenu } from "./UserMenu";
|
||||||
export function UserMenuContainer({ disableLogout }) {
|
export function UserMenuContainer({ disableLogout }) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const {
|
const { isAuthenticated, isPasswordlessUser, logout, userName, client } =
|
||||||
isAuthenticated,
|
useClient();
|
||||||
isGuest,
|
|
||||||
isPasswordlessUser,
|
|
||||||
logout,
|
|
||||||
userName,
|
|
||||||
client,
|
|
||||||
} = useClient();
|
|
||||||
const { displayName, avatarUrl } = useProfile(client);
|
const { displayName, avatarUrl } = useProfile(client);
|
||||||
const { modalState, modalProps } = useModalTriggerState();
|
const { modalState, modalProps } = useModalTriggerState();
|
||||||
|
|
||||||
|
@ -52,7 +46,6 @@ export function UserMenuContainer({ disableLogout }) {
|
||||||
<ProfileModal
|
<ProfileModal
|
||||||
client={client}
|
client={client}
|
||||||
isAuthenticated={isAuthenticated}
|
isAuthenticated={isAuthenticated}
|
||||||
isGuest={isGuest}
|
|
||||||
isPasswordlessUser={isPasswordlessUser}
|
isPasswordlessUser={isPasswordlessUser}
|
||||||
{...modalProps}
|
{...modalProps}
|
||||||
/>
|
/>
|
||||||
|
|
11
src/form/Form.jsx
Normal file
11
src/form/Form.jsx
Normal 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
4
src/form/Form.module.css
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
.form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
131
src/home/RegisteredView.jsx
Normal file
131
src/home/RegisteredView.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
154
src/home/UnauthenticatedView.jsx
Normal file
154
src/home/UnauthenticatedView.jsx
Normal 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} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
63
src/home/UnauthenticatedView.module.css
Normal file
63
src/home/UnauthenticatedView.module.css
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,6 +20,8 @@ limitations under the License.
|
||||||
Therefore we define a unicode-range to load which excludes the glyphs
|
Therefore we define a unicode-range to load which excludes the glyphs
|
||||||
(to avoid having to maintain a fork of Inter). */
|
(to avoid having to maintain a fork of Inter). */
|
||||||
|
|
||||||
|
@import "normalize.css/normalize.css";
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--inter-unicode-range: U+0000-20e2, U+20e4-23ce, U+23d0-24c1, U+24c3-259f,
|
--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;
|
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;
|
--textColor4: #a9b2bc;
|
||||||
--inputBorderColor: #394049;
|
--inputBorderColor: #394049;
|
||||||
--inputBorderColorFocused: #0086e6;
|
--inputBorderColorFocused: #0086e6;
|
||||||
|
--linkColor: #0086e6;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
|
@ -139,6 +142,44 @@ body,
|
||||||
flex-direction: column;
|
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 {
|
a {
|
||||||
color: var(--primaryColor);
|
color: var(--primaryColor);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
@ -179,3 +220,7 @@ details[open] > summary {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.grecaptcha-badge {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
123
src/typography/Typography.jsx
Normal file
123
src/typography/Typography.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
29
src/typography/Typography.module.css
Normal file
29
src/typography/Typography.module.css
Normal 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);
|
||||||
|
}
|
25
src/typography/Typography.stories.jsx
Normal file
25
src/typography/Typography.stories.jsx
Normal 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
69
src/useRecaptcha.js
Normal 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 };
|
||||||
|
}
|
|
@ -9155,6 +9155,11 @@ normalize-range@^0.1.2:
|
||||||
resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
|
resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
|
||||||
integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=
|
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:
|
npm-run-path@^2.0.0:
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
|
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
|
||||||
|
|
Loading…
Reference in a new issue