This commit is contained in:
Robert Long 2020-10-29 13:39:36 -07:00
Родитель 6ab1747080
Коммит cb10f5428a
34 изменённых файлов: 923 добавлений и 644 удалений

Просмотреть файл

@ -1,10 +1,11 @@
import React from "react";
import { useAccessibleOutlineStyle } from "../src/react-components/input/useAccessibleOutlineStyle";
import "../src/react-components/styles/global.scss";
import { WrappedIntlProvider } from "../src/react-components/wrapped-intl-provider";
const Layout = ({ children }) => {
useAccessibleOutlineStyle();
return <>{children}</>;
return <WrappedIntlProvider>{children}</WrappedIntlProvider>;
};
export const decorators = [

Просмотреть файл

@ -4,7 +4,7 @@ import "./utils/configs";
import styles from "./assets/stylesheets/cloud.scss";
import classNames from "classnames";
import { WrappedIntlProvider } from "./react-components/wrapped-intl-provider";
import { Page } from "./react-components/layout/Page";
import { PageContainer } from "./react-components/layout/PageContainer";
import { AuthContextProvider } from "./react-components/auth/AuthContext";
import Store from "./storage/store";
@ -14,7 +14,7 @@ registerTelemetry("/cloud", "Hubs Cloud Landing Page");
function HubsCloudPage() {
return (
<Page>
<PageContainer>
<div className={styles.hero}>
<section className={styles.colLg}>
<div className={classNames(styles.hideLgUp, styles.centerLg)}>
@ -76,7 +76,7 @@ function HubsCloudPage() {
</p>
</div>
</section>
</Page>
</PageContainer>
);
}

56
src/react-components/auth-dialog.js поставляемый
Просмотреть файл

@ -1,56 +0,0 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import { injectIntl, FormattedMessage } from "react-intl";
import DialogContainer from "./dialog-container.js";
import IfFeature from "./if-feature";
class AuthDialog extends Component {
static propTypes = {
intl: PropTypes.object,
verifying: PropTypes.bool,
verified: PropTypes.bool,
authOrigin: PropTypes.string
};
render() {
const { authOrigin, verifying, verified } = this.props;
const { formatMessage } = this.props.intl;
const title = verifying || !verified ? "" : formatMessage({ id: "auth.verified-title" });
if (!verifying && !verified) {
return (
<DialogContainer title={title} closable={true} {...this.props}>
<FormattedMessage className="preformatted" id="auth.verify-failed" />
</DialogContainer>
);
} else {
return (
<DialogContainer title={title} closable={!verifying} {...this.props}>
{verifying ? (
<div className="loader-wrap loader-mid">
<div className="loader">
<div className="loader-center" />
</div>
</div>
) : authOrigin === "spoke" ? (
<FormattedMessage className="preformatted" id="auth.spoke-verified" />
) : (
<div>
<FormattedMessage className="preformatted" id="auth.verified" />
<IfFeature name="show_newsletter_signup">
<p>
Want Hubs news sent to your inbox?{"\n"}
<a href="https://eepurl.com/gX_fH9" target="_blank" rel="noopener noreferrer">
Subscribe for updates
</a>.
</p>
</IfFeature>
</div>
)}
</DialogContainer>
);
}
}
}
export default injectIntl(AuthDialog);

47
src/react-components/auth/RoomSignInModalContainer.js поставляемый Normal file
Просмотреть файл

@ -0,0 +1,47 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
import configs from "../../utils/configs";
import { SignInModal, SignInStep, SubmitEmail, WaitForVerification, SignInComplete } from "./SignInModal";
// TODO: Migrate to use AuthContext
export function RoomSignInModalContainer({ onClose, step, onSubmitEmail, message, continueText, onContinue }) {
const [cachedEmail, setCachedEmail] = useState();
return (
<SignInModal onClose={onClose} closeable>
{step === SignInStep.submit && (
<SubmitEmail
onSubmitEmail={email => {
setCachedEmail(email);
onSubmitEmail(email);
}}
initialEmail={cachedEmail}
termsUrl={configs.link("terms_of_use", "https://github.com/mozilla/hubs/blob/master/TERMS.md")}
showTerms={configs.feature("show_terms")}
privacyUrl={configs.link("privacy_notice", "https://github.com/mozilla/hubs/blob/master/PRIVACY.md")}
showPrivacy={configs.feature("show_privacy")}
message={message}
/>
)}
{step === SignInStep.waitForVerification && (
<WaitForVerification
onCancel={onClose}
email={cachedEmail}
showNewsletterSignup={configs.feature("show_newsletter_signup")}
/>
)}
{step === SignInStep.complete && (
<SignInComplete message={message} continueText={continueText} onContinue={onContinue} />
)}
</SignInModal>
);
}
RoomSignInModalContainer.propTypes = {
onClose: PropTypes.func,
onSubmitEmail: PropTypes.func,
step: PropTypes.string,
message: PropTypes.string,
continueText: PropTypes.string,
onContinue: PropTypes.func
};

134
src/react-components/auth/SignInModal.js поставляемый Normal file
Просмотреть файл

@ -0,0 +1,134 @@
import React, { useCallback, useState } from "react";
import PropTypes from "prop-types";
import { CloseButton } from "../input/CloseButton";
import { Modal } from "../modal/Modal";
import { FormattedMessage } from "react-intl";
import styles from "./SignInModal.scss";
import { Button } from "../input/Button";
import { TextInputField } from "../input/TextInputField";
export const SignInStep = {
submit: "submit",
waitForVerification: "waitForVerification",
complete: "complete"
};
export function SubmitEmail({ onSubmitEmail, initialEmail, showPrivacy, privacyUrl, showTerms, termsUrl, message }) {
const [email, setEmail] = useState(initialEmail);
const onSubmitForm = useCallback(
e => {
e.preventDefault();
onSubmitEmail(email);
},
[onSubmitEmail, email]
);
return (
<form onSubmit={onSubmitForm} className={styles.modalContent}>
<p>{message || <FormattedMessage id="sign-in.prompt" />}</p>
<TextInputField
name="email"
type="email"
required
value={email}
onChange={e => setEmail(e.target.value)}
placeholder="example@example.com"
/>
{(showTerms || showPrivacy) && (
<b className={styles.terms}>
By proceeding, you agree to the{" "}
{showTerms && (
<>
<a rel="noopener noreferrer" target="_blank" href={termsUrl}>
terms of use
</a>{" "}
</>
)}
{showTerms && showPrivacy && "and "}
{showPrivacy && (
<a rel="noopener noreferrer" target="_blank" href={privacyUrl}>
privacy notice
</a>
)}.
</b>
)}
<Button preset="accept" type="submit">
Next
</Button>
</form>
);
}
SubmitEmail.defaultProps = {
initialEmail: ""
};
SubmitEmail.propTypes = {
message: PropTypes.string,
showTerms: PropTypes.bool,
termsUrl: PropTypes.string,
showPrivacy: PropTypes.bool,
privacyUrl: PropTypes.string,
initialEmail: PropTypes.string,
onSubmitEmail: PropTypes.func.isRequired
};
export function WaitForVerification({ email, onCancel, showNewsletterSignup }) {
return (
<div className={styles.modalContent}>
<p>
<FormattedMessage id="sign-in.auth-started" values={{ email }} />
</p>
{showNewsletterSignup && (
<p className={styles.newsletter}>
Want Hubs news sent to your inbox?<br />
<a href="https://eepurl.com/gX_fH9" target="_blank" rel="noopener noreferrer">
Subscribe for updates
</a>
</p>
)}
<Button preset="cancel" onClick={onCancel}>
Cancel
</Button>
</div>
);
}
WaitForVerification.propTypes = {
showNewsletterSignup: PropTypes.bool,
email: PropTypes.string.isRequired,
onCancel: PropTypes.func.isRequired
};
export function SignInComplete({ message, continueText, onContinue }) {
return (
<div className={styles.modalContent}>
<b>{message}</b>
<p>{continueText}</p>
<Button preset="green" onClick={onContinue}>
Continue
</Button>
</div>
);
}
SignInComplete.propTypes = {
message: PropTypes.string.isRequired,
continueText: PropTypes.string.isRequired,
onContinue: PropTypes.func.isRequired
};
export function SignInModal({ closeable, onClose, children, ...rest }) {
return (
<Modal title="Sign In" beforeTitle={closeable && <CloseButton onClick={onClose} />} {...rest}>
{children}
</Modal>
);
}
SignInModal.propTypes = {
closeable: PropTypes.bool,
onClose: PropTypes.func,
children: PropTypes.node
};

Просмотреть файл

@ -0,0 +1,29 @@
@use "../styles/theme.scss";
:local(.modal-content) {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 20px;
line-height: 1.25;
& > * {
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
}
}
:local(.terms) {
color: theme.$darkgrey;
font-size: theme.$font-size-xs;
}
:local(.newsletter) {
color: theme.$darkgrey;
font-size: theme.$font-size-sm;
line-height: 1.5;
}

Просмотреть файл

@ -0,0 +1,76 @@
import React from "react";
import { Center } from "../layout/Center";
import { Page } from "../layout/Page";
import { RoomLayout } from "../layout/RoomLayout";
import { SignInModal, SubmitEmail, WaitForVerification } from "./SignInModal";
import backgroundUrl from "../../assets/images/home-hero-background-unbranded.png";
export default {
title: "SignInModal"
};
export const PageSubmit = () => (
<Page style={{ backgroundImage: `url(${backgroundUrl})`, backgroundSize: "cover" }}>
<Center>
<SignInModal disableFullscreen>
<SubmitEmail
termsUrl="https://github.com/mozilla/hubs/blob/master/TERMS.md"
showTerms
privacyUrl="https://github.com/mozilla/hubs/blob/master/PRIVACY.md"
showPrivacy
/>
</SignInModal>
</Center>
</Page>
);
PageSubmit.parameters = {
layout: "fullscreen"
};
export const PageWaitForVerification = () => (
<Page style={{ backgroundImage: `url(${backgroundUrl})`, backgroundSize: "cover" }}>
<Center>
<SignInModal disableFullscreen>
<WaitForVerification email="example@example.com" showNewsletterSignup />
</SignInModal>
</Center>
</Page>
);
PageWaitForVerification.parameters = {
layout: "fullscreen"
};
export const RoomSubmit = () => (
<RoomLayout
modal={
<SignInModal closeable>
<SubmitEmail
termsUrl="https://github.com/mozilla/hubs/blob/master/TERMS.md"
showTerms
privacyUrl="https://github.com/mozilla/hubs/blob/master/PRIVACY.md"
showPrivacy
/>
</SignInModal>
}
/>
);
RoomSubmit.parameters = {
layout: "fullscreen"
};
export const RoomWaitForVerification = () => (
<RoomLayout
modal={
<SignInModal closeable>
<WaitForVerification email="example@example.com" showNewsletterSignup />
</SignInModal>
}
/>
);
RoomWaitForVerification.parameters = {
layout: "fullscreen"
};

89
src/react-components/auth/SignInModalContainer.js поставляемый Normal file
Просмотреть файл

@ -0,0 +1,89 @@
import React, { useCallback, useReducer, useContext, useEffect } from "react";
import configs from "../../utils/configs";
import { AuthContext } from "./AuthContext";
import { SignInModal, SignInStep, WaitForVerification, SubmitEmail } from "./SignInModal";
const SignInAction = {
submitEmail: "submitEmail",
verificationReceived: "verificationReceived",
cancel: "cancel"
};
const initialSignInState = {
step: SignInStep.submit,
email: ""
};
function loginReducer(state, action) {
switch (action.type) {
case SignInAction.submitEmail:
return { step: SignInStep.waitForVerification, email: action.email };
case SignInAction.verificationReceived:
return { ...state, step: SignInStep.complete };
case SignInAction.cancel:
return { ...state, step: SignInStep.submit };
}
}
function useSignIn() {
const auth = useContext(AuthContext);
const [state, dispatch] = useReducer(loginReducer, initialSignInState);
const submitEmail = useCallback(
email => {
auth.signIn(email).then(() => {
dispatch({ type: SignInAction.verificationReceived });
});
dispatch({ type: SignInAction.submitEmail, email });
},
[auth]
);
const cancel = useCallback(() => {
dispatch({ type: SignInAction.cancel });
}, []);
return {
step: state.step,
email: state.email,
submitEmail,
cancel
};
}
export function SignInModalContainer() {
const qs = new URLSearchParams(location.search);
const { step, submitEmail, cancel, email } = useSignIn();
const redirectUrl = qs.get("sign_in_destination_url") || "/";
useEffect(
() => {
if (step === SignInStep.complete) {
window.location = redirectUrl;
}
},
[step, redirectUrl]
);
return (
<SignInModal disableFullscreen>
{step === SignInStep.submit ? (
<SubmitEmail
onSubmitEmail={submitEmail}
initialEmail={email}
signInReason={qs.get("sign_in_reason")}
termsUrl={configs.link("terms_of_use", "https://github.com/mozilla/hubs/blob/master/TERMS.md")}
showTerms={configs.feature("show_terms")}
privacyUrl={configs.link("privacy_notice", "https://github.com/mozilla/hubs/blob/master/PRIVACY.md")}
showPrivacy={configs.feature("show_privacy")}
/>
) : (
<WaitForVerification
onCancel={cancel}
email={email}
showNewsletterSignup={configs.feature("show_newsletter_signup")}
/>
)}
</SignInModal>
);
}

176
src/react-components/auth/SignInPage.js поставляемый
Просмотреть файл

@ -1,176 +0,0 @@
import React, { useCallback, useState, useReducer, useContext, useEffect } from "react";
import PropTypes from "prop-types";
import { Page } from "../layout/Page";
import styles from "./SignInPage.scss";
import configs from "../../utils/configs";
import IfFeature from "../if-feature";
import { FormattedMessage } from "react-intl";
import { AuthContext } from "../auth/AuthContext";
const SignInStep = {
submit: "submit",
waitForVerification: "waitForVerification",
complete: "complete"
};
const SignInAction = {
submitEmail: "submitEmail",
verificationReceived: "verificationReceived",
cancel: "cancel"
};
const initialSignInState = {
step: SignInStep.submit,
email: ""
};
function loginReducer(state, action) {
switch (action.type) {
case SignInAction.submitEmail:
return { step: SignInStep.waitForVerification, email: action.email };
case SignInAction.verificationReceived:
return { ...state, step: SignInStep.complete };
case SignInAction.cancel:
return { ...state, step: SignInStep.submit };
}
}
function useSignIn() {
const auth = useContext(AuthContext);
const [state, dispatch] = useReducer(loginReducer, initialSignInState);
const submitEmail = useCallback(
email => {
auth.signIn(email).then(() => {
dispatch({ type: SignInAction.verificationReceived });
});
dispatch({ type: SignInAction.submitEmail, email });
},
[auth]
);
const cancel = useCallback(() => {
dispatch({ type: SignInAction.cancel });
}, []);
return {
step: state.step,
email: state.email,
submitEmail,
cancel
};
}
function SubmitEmail({ onSubmitEmail, initialEmail }) {
const [email, setEmail] = useState(initialEmail);
const onSubmitForm = useCallback(
e => {
e.preventDefault();
onSubmitEmail(email);
},
[onSubmitEmail, email]
);
return (
<form onSubmit={onSubmitForm} className={styles.signInContainer}>
<h1>
<FormattedMessage id="sign-in.in" />
</h1>
<b>
<FormattedMessage id="sign-in.prompt" />
</b>
<input
name="email"
type="email"
required
value={email}
onChange={e => setEmail(e.target.value)}
placeholder="example@example.com"
/>
{(configs.feature("show_terms") || configs.feature("show_privacy")) && (
<b className={styles.terms}>
By proceeding, you agree to the{" "}
<IfFeature name="show_terms">
<a
rel="noopener noreferrer"
target="_blank"
href={configs.link("terms_of_use", "https://github.com/mozilla/hubs/blob/master/TERMS.md")}
>
terms of use
</a>{" "}
</IfFeature>
{configs.feature("show_terms") && configs.feature("show_privacy") && "and "}
<IfFeature name="show_privacy">
<a
rel="noopener noreferrer"
target="_blank"
href={configs.link("privacy_notice", "https://github.com/mozilla/hubs/blob/master/PRIVACY.md")}
>
privacy notice
</a>
</IfFeature>.
</b>
)}
<button type="submit">next</button>
</form>
);
}
SubmitEmail.defaultProps = {
initialEmail: ""
};
SubmitEmail.propTypes = {
initialEmail: PropTypes.string,
onSubmitEmail: PropTypes.func.isRequired
};
function WaitForVerification({ email, onCancel }) {
return (
<div className={styles.signInContainer}>
<p>
<FormattedMessage id="sign-in.auth-started" values={{ email }} />
</p>
<IfFeature name="show_newsletter_signup">
<p>
Want Hubs news sent to your inbox?{"\n"}
<a href="https://eepurl.com/gX_fH9" target="_blank" rel="noopener noreferrer">
Subscribe for updates
</a>.
</p>
</IfFeature>
<button onClick={onCancel}>cancel</button>
</div>
);
}
WaitForVerification.propTypes = {
email: PropTypes.string.isRequired,
onCancel: PropTypes.func.isRequired
};
export function SignInPage() {
const qs = new URLSearchParams(location.search);
const { step, submitEmail, cancel, email } = useSignIn();
const redirectUrl = qs.get("sign_in_destination_url") || "/";
useEffect(
() => {
if (step === SignInStep.complete) {
window.location = redirectUrl;
}
},
[step, redirectUrl]
);
return (
<Page style={{ backgroundImage: configs.image("home_background", true), backgroundSize: "cover" }}>
{step === SignInStep.submit ? (
<SubmitEmail onSubmitEmail={submitEmail} initialEmail={email} signInReason={qs.get("sign_in_reason")} />
) : (
<WaitForVerification onCancel={cancel} email={email} />
)}
</Page>
);
}

Просмотреть файл

@ -1,35 +0,0 @@
@import '../../assets/stylesheets/shared';
main {
display: flex;
background-color: #F3F3F3;
height: 100%;
justify-content: center;
align-items: center;
}
:local(.sign-in-container) {
@extend %centered-flex-column;
background-color: white;
border-radius: 4px;
width: 480px;
padding: 2em;
h1 {
margin-top: 0;
}
input {
@extend %input-field;
margin: 1.5em 0 0.5em 0;
}
button {
@extend %action-button;
}
:local(.terms) {
font-size: 7pt;
margin-bottom: 32px;
}
}

58
src/react-components/auth/VerifyModal.js поставляемый Normal file
Просмотреть файл

@ -0,0 +1,58 @@
import React from "react";
import PropTypes from "prop-types";
import styles from "./VerifyModal.scss";
import { Spinner } from "../misc/Spinner";
import { Modal } from "../modal/Modal";
export const VerificationStep = {
verifying: "verifying",
complete: "complete",
error: "error"
};
export function EmailVerifying() {
return (
<div className={styles.modalContent}>
<b>Email Verifying</b>
<Spinner />
</div>
);
}
export function EmailVerified({ origin }) {
return (
<div className={styles.modalContent}>
<b>Verification Complete</b>
<p>Please close this browser window and return to {origin}.</p>
</div>
);
}
EmailVerified.propTypes = {
origin: PropTypes.string.isRequired
};
export function VerificationError({ error }) {
return (
<div className={styles.modalContent}>
<b>Error Verifying Email</b>
<p>{(error && error.message) || "Unknown Error"}</p>
</div>
);
}
VerificationError.propTypes = {
error: PropTypes.object
};
export function VerifyModal({ children }) {
return (
<Modal title="Verify" disableFullscreen>
{children}
</Modal>
);
}
VerifyModal.propTypes = {
children: PropTypes.node
};

Просмотреть файл

@ -0,0 +1,20 @@
:local(.modal-content) {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 20px;
line-height: 1.25;
svg, p {
margin-top: 16px;
}
& > * {
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
}
}

Просмотреть файл

@ -0,0 +1,51 @@
import React from "react";
import { Center } from "../layout/Center";
import { Page } from "../layout/Page";
import { VerifyModal, EmailVerifying, EmailVerified, VerificationError } from "./VerifyModal";
import backgroundUrl from "../../assets/images/home-hero-background-unbranded.png";
export default {
title: "VerifyModal"
};
export const Verifying = () => (
<Page style={{ backgroundImage: `url(${backgroundUrl})`, backgroundSize: "cover" }}>
<Center>
<VerifyModal>
<EmailVerifying />
</VerifyModal>
</Center>
</Page>
);
Verifying.parameters = {
layout: "fullscreen"
};
export const Verified = () => (
<Page style={{ backgroundImage: `url(${backgroundUrl})`, backgroundSize: "cover" }}>
<Center>
<VerifyModal>
<EmailVerified origin="hubs.mozilla.com" />
</VerifyModal>
</Center>
</Page>
);
Verified.parameters = {
layout: "fullscreen"
};
export const Error = () => (
<Page style={{ backgroundImage: `url(${backgroundUrl})`, backgroundSize: "cover" }}>
<Center>
<VerifyModal>
<VerificationError />
</VerifyModal>
</Center>
</Page>
);
Error.parameters = {
layout: "fullscreen"
};

61
src/react-components/auth/VerifyModalContainer.js поставляемый Normal file
Просмотреть файл

@ -0,0 +1,61 @@
import React, { useState, useContext, useEffect } from "react";
import { AuthContext } from "./AuthContext";
import { VerifyModal, VerificationError, EmailVerified, EmailVerifying } from "./VerifyModal";
const VerificationStep = {
verifying: "verifying",
complete: "complete",
error: "error"
};
function useVerify() {
const [step, setStep] = useState(VerificationStep.verifying);
const [error, setError] = useState();
const { verify } = useContext(AuthContext);
useEffect(
() => {
const verifyAsync = async () => {
try {
const qs = new URLSearchParams(location.search);
const authParams = {
topic: qs.get("auth_topic"),
token: qs.get("auth_token"),
origin: qs.get("auth_origin"),
payload: qs.get("auth_payload")
};
await verify(authParams);
setStep(VerificationStep.complete);
} catch (error) {
setStep(VerificationStep.error);
setError(error);
}
};
verifyAsync();
},
[verify]
);
return { step, error };
}
export function VerifyModalContainer() {
const { step, error } = useVerify();
let content;
if (step === VerificationStep.error) {
content = <VerificationError error={error} />;
} else if (step === VerificationStep.complete) {
const qs = new URLSearchParams(location.search);
const origin = qs.get("auth_origin");
content = <EmailVerified origin={origin} />;
} else {
content = <EmailVerifying />;
}
return <VerifyModal>{content}</VerifyModal>;
}

96
src/react-components/auth/VerifyPage.js поставляемый
Просмотреть файл

@ -1,96 +0,0 @@
import React, { useState, useContext, useEffect } from "react";
import PropTypes from "prop-types";
import { Page } from "../layout/Page";
import styles from "./SignInPage.scss";
import { Loader } from "../misc/Loader";
import { AuthContext } from "../auth/AuthContext";
import configs from "../../utils/configs";
const VerificationStep = {
verifying: "verifying",
complete: "complete",
error: "error"
};
function useVerify() {
const [step, setStep] = useState(VerificationStep.verifying);
const [error, setError] = useState();
const auth = useContext(AuthContext);
useEffect(() => {
const verifyAsync = async () => {
try {
const qs = new URLSearchParams(location.search);
const authParams = {
topic: qs.get("auth_topic"),
token: qs.get("auth_token"),
origin: qs.get("auth_origin"),
payload: qs.get("auth_payload")
};
await auth.verify(authParams);
setStep(VerificationStep.complete);
} catch (error) {
setStep(VerificationStep.error);
setError(error);
}
};
verifyAsync();
}, []);
return { step, error };
}
function EmailVerifying() {
return (
<div className={styles.signInContainer}>
<h1>Email Verifying</h1>
<Loader />
</div>
);
}
function EmailVerified() {
const qs = new URLSearchParams(location.search);
const origin = qs.get("auth_origin");
return (
<div className={styles.signInContainer}>
<h1>Verification Complete</h1>
<b>Please close this browser window and return to {origin}.</b>
</div>
);
}
function VerificationError({ error }) {
return (
<div className={styles.signInContainer}>
<h1>Error Verifying Email</h1>
<b>{(error && error.message) || "Unknown Error"}</b>
</div>
);
}
VerificationError.propTypes = {
error: PropTypes.object
};
export function VerifyPage() {
const { step, error } = useVerify();
let content;
if (step === VerificationStep.error) {
content = <VerificationError error={error} />;
} else if (step === VerificationStep.complete) {
content = <EmailVerified />;
} else {
content = <EmailVerifying />;
}
return (
<Page style={{ backgroundImage: configs.image("home_background", true), backgroundSize: "cover" }}>{content}</Page>
);
}

8
src/react-components/avatar-editor.js поставляемый
Просмотреть файл

@ -45,10 +45,8 @@ const fetchAvatar = async avatarId => {
export default class AvatarEditor extends Component {
static propTypes = {
avatarId: PropTypes.string,
onSignIn: PropTypes.func,
onSave: PropTypes.func,
onClose: PropTypes.func,
signedIn: PropTypes.bool,
hideDelete: PropTypes.bool,
debug: PropTypes.bool,
className: PropTypes.string
@ -453,7 +451,7 @@ export default class AvatarEditor extends Component {
<div className="loader">
<div className="loader-center" />
</div>
) : this.props.signedIn ? (
) : (
<form onSubmit={this.uploadAvatar} className="center">
{this.textField("name", "Name", false, true)}
<div className="split">
@ -580,10 +578,6 @@ export default class AvatarEditor extends Component {
</div>
)}
</form>
) : (
<a onClick={this.props.onSignIn}>
<FormattedMessage id="sign-in.in" />
</a>
)}
</div>
);

6
src/react-components/home/HomePage.js поставляемый
Просмотреть файл

@ -3,7 +3,6 @@ import { FormattedMessage } from "react-intl";
import classNames from "classnames";
import configs from "../../utils/configs";
import IfFeature from "../if-feature";
import { Page } from "../layout/Page";
import { CreateRoomButton } from "./CreateRoomButton";
import { PWAButton } from "./PWAButton";
import { useFavoriteRooms } from "./useFavoriteRooms";
@ -14,6 +13,7 @@ import { AuthContext } from "../auth/AuthContext";
import { createAndRedirectToNewHub } from "../../utils/phoenix-utils";
import { MediaGrid } from "./MediaGrid";
import { RoomTile } from "./RoomTile";
import { PageContainer } from "../layout/PageContainer";
export function HomePage() {
const auth = useContext(AuthContext);
@ -57,7 +57,7 @@ export function HomePage() {
});
return (
<Page className={styles.homePage} style={pageStyle}>
<PageContainer className={styles.homePage} style={pageStyle}>
<section>
<div className={styles.appInfo}>
<div className={logoStyles}>
@ -129,6 +129,6 @@ export function HomePage() {
</div>
</div>
</section>
</Page>
</PageContainer>
);
}

17
src/react-components/layout/Center.js поставляемый Normal file
Просмотреть файл

@ -0,0 +1,17 @@
import React from "react";
import PropTypes from "prop-types";
import classNames from "classnames";
import styles from "./Center.scss";
export function Center({ children, className, ...rest }) {
return (
<div className={classNames(styles.center, className)} {...rest}>
{children}
</div>
);
}
Center.propTypes = {
children: PropTypes.node,
className: PropTypes.string
};

Просмотреть файл

@ -0,0 +1,6 @@
:local(.center) {
display: flex;
justify-content: center;
align-items: center;
flex: 1;
}

112
src/react-components/layout/Footer.js поставляемый
Просмотреть файл

@ -1,65 +1,73 @@
import React from "react";
import PropTypes from "prop-types";
import { FormattedMessage } from "react-intl";
import { WrappedIntlProvider } from "../wrapped-intl-provider";
import IfFeature from "../if-feature";
import UnlessFeature from "../unless-feature";
import configs from "../../utils/configs";
import styles from "./Footer.scss";
export function Footer() {
export function Footer({
hidePoweredBy,
showWhatsNewLink,
showTerms,
termsUrl,
showPrivacy,
privacyUrl,
showCompanyLogo,
companyLogoUrl
}) {
return (
<WrappedIntlProvider>
<footer>
<div className={styles.poweredBy}>
<UnlessFeature name="hide_powered_by">
<footer>
<div className={styles.poweredBy}>
{!hidePoweredBy && (
<>
<span className={styles.prefix}>
<FormattedMessage id="home.powered_by_prefix" />
</span>
<a className={styles.link} href="https://hubs.mozilla.com/cloud">
<FormattedMessage id="home.powered_by_link" />
</a>
</UnlessFeature>
</div>
<nav>
<ul>
<IfFeature name="show_whats_new_link">
<li>
<a href="/whats-new">
<FormattedMessage id="home.whats_new_link" />
</a>
</li>
</IfFeature>
<IfFeature name="show_terms">
<li>
<a
target="_blank"
rel="noopener noreferrer"
href={configs.link("terms_of_use", "https://github.com/mozilla/hubs/blob/master/TERMS.md")}
>
<FormattedMessage id="home.terms_of_use" />
</a>
</li>
</IfFeature>
<IfFeature name="show_privacy">
<li>
<a
className={styles.link}
target="_blank"
rel="noopener noreferrer"
href={configs.link("privacy_notice", "https://github.com/mozilla/hubs/blob/master/PRIVACY.md")}
>
<FormattedMessage id="home.privacy_notice" />
</a>
</li>
</IfFeature>
<IfFeature name="show_company_logo">
<li>
<img className={styles.companyLogo} src={configs.image("company_logo")} />
</li>
</IfFeature>
</ul>
</nav>
</footer>
</WrappedIntlProvider>
</>
)}
</div>
<nav>
<ul>
{showWhatsNewLink && (
<li>
<a href="/whats-new">
<FormattedMessage id="home.whats_new_link" />
</a>
</li>
)}
{showTerms && (
<li>
<a target="_blank" rel="noopener noreferrer" href={termsUrl}>
<FormattedMessage id="home.terms_of_use" />
</a>
</li>
)}
{showPrivacy && (
<li>
<a className={styles.link} target="_blank" rel="noopener noreferrer" href={privacyUrl}>
<FormattedMessage id="home.privacy_notice" />
</a>
</li>
)}
{showCompanyLogo && (
<li>
<img className={styles.companyLogo} src={companyLogoUrl} />
</li>
)}
</ul>
</nav>
</footer>
);
}
Footer.propTypes = {
hidePoweredBy: PropTypes.bool,
showWhatsNewLink: PropTypes.bool,
showTerms: PropTypes.bool,
termsUrl: PropTypes.string,
showPrivacy: PropTypes.bool,
privacyUrl: PropTypes.string,
showCompanyLogo: PropTypes.bool,
companyLogoUrl: PropTypes.string
};

175
src/react-components/layout/Header.js поставляемый
Просмотреть файл

@ -1,90 +1,109 @@
import React, { useContext } from "react";
import React from "react";
import PropTypes from "prop-types";
import { FormattedMessage } from "react-intl";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCog } from "@fortawesome/free-solid-svg-icons/faCog";
import IfFeature from "../if-feature";
import configs from "../../utils/configs";
import maskEmail from "../../utils/mask-email";
import styles from "./Header.scss";
import { AuthContext } from "../auth/AuthContext";
import { WrappedIntlProvider } from "../wrapped-intl-provider";
export function Header() {
const auth = useContext(AuthContext);
export function Header({
showCloud,
enableSpoke,
showDocsLink,
docsUrl,
showSourceLink,
showCommunityLink,
communityUrl,
isAdmin,
isSignedIn,
email,
onSignOut
}) {
return (
<WrappedIntlProvider>
<header>
<nav>
<ul>
<header>
<nav>
<ul>
<li>
<a href="/">Home</a>
</li>
{showCloud && (
<li>
<a href="/">Home</a>
</li>
<IfFeature name="show_cloud">
<li>
<a href="/cloud">
<FormattedMessage id="home.cloud_link" />
</a>
</li>
</IfFeature>
<IfFeature name="enable_spoke">
<li>
<a href="/spoke">
<FormattedMessage id="editor-name" />
</a>
</li>
</IfFeature>
<IfFeature name="show_docs_link">
<li>
<a href={configs.link("docs", "https://hubs.mozilla.com/docs")}>
<FormattedMessage id="home.docs_link" />
</a>
</li>
</IfFeature>
<IfFeature name="show_source_link">
<li>
<a href="https://github.com/mozilla/hubs">
<FormattedMessage id="home.source_link" />
</a>
</li>
</IfFeature>
<IfFeature name="show_community_link">
<li>
<a href={configs.link("community", "https://discord.gg/wHmY4nd")}>
<FormattedMessage id="home.community_link" />
</a>
</li>
</IfFeature>
{auth.isAdmin && (
<li>
<a href="/admin" rel="noreferrer noopener">
<i>
<FontAwesomeIcon icon={faCog} />
</i>
&nbsp;
<FormattedMessage id="home.admin" />
</a>
</li>
)}
</ul>
</nav>
<div className={styles.signIn}>
{auth.isSignedIn ? (
<div>
<span>
<FormattedMessage id="sign-in.as" /> {maskEmail(auth.email)}
</span>{" "}
<a href="#" onClick={auth.signOut}>
<FormattedMessage id="sign-in.out" />
<a href="/cloud">
<FormattedMessage id="home.cloud_link" />
</a>
</div>
) : (
<a href="/signin" rel="noreferrer noopener">
<FormattedMessage id="sign-in.in" />
</a>
</li>
)}
</div>
</header>
</WrappedIntlProvider>
{enableSpoke && (
<li>
<a href="/spoke">
<FormattedMessage id="editor-name" />
</a>
</li>
)}
{showDocsLink && (
<li>
<a href={docsUrl}>
<FormattedMessage id="home.docs_link" />
</a>
</li>
)}
{showSourceLink && (
<li>
<a href="https://github.com/mozilla/hubs">
<FormattedMessage id="home.source_link" />
</a>
</li>
)}
{showCommunityLink && (
<li>
<a href={communityUrl}>
<FormattedMessage id="home.community_link" />
</a>
</li>
)}
{isAdmin && (
<li>
<a href="/admin" rel="noreferrer noopener">
<i>
<FontAwesomeIcon icon={faCog} />
</i>
&nbsp;
<FormattedMessage id="home.admin" />
</a>
</li>
)}
</ul>
</nav>
<div className={styles.signIn}>
{isSignedIn ? (
<div>
<span>
<FormattedMessage id="sign-in.as" /> {maskEmail(email)}
</span>{" "}
<a href="#" onClick={onSignOut}>
<FormattedMessage id="sign-in.out" />
</a>
</div>
) : (
<a href="/signin" rel="noreferrer noopener">
<FormattedMessage id="sign-in.in" />
</a>
)}
</div>
</header>
);
}
Header.propTypes = {
showCloud: PropTypes.bool,
enableSpoke: PropTypes.bool,
showDocsLink: PropTypes.bool,
docsUrl: PropTypes.string,
showSourceLink: PropTypes.bool,
showCommunityLink: PropTypes.bool,
communityUrl: PropTypes.string,
isAdmin: PropTypes.bool,
isSignedIn: PropTypes.bool,
email: PropTypes.string,
onSignOut: PropTypes.func
};

68
src/react-components/layout/Page.js поставляемый
Просмотреть файл

@ -4,16 +4,78 @@ import "./Page.scss";
import { Header } from "./Header";
import { Footer } from "./Footer";
export function Page({ children, ...rest }) {
export function Page({
showCloud,
enableSpoke,
showDocsLink,
docsUrl,
showSourceLink,
showCommunityLink,
communityUrl,
isAdmin,
isSignedIn,
email,
onSignOut,
hidePoweredBy,
showWhatsNewLink,
showTerms,
termsUrl,
showPrivacy,
privacyUrl,
showCompanyLogo,
companyLogoUrl,
children,
...rest
}) {
return (
<>
<Header />
<Header
showCloud={showCloud}
enableSpoke={enableSpoke}
showDocsLink={showDocsLink}
docsUrl={docsUrl}
showSourceLink={showSourceLink}
showCommunityLink={showCommunityLink}
communityUrl={communityUrl}
isAdmin={isAdmin}
isSignedIn={isSignedIn}
email={email}
onSignOut={onSignOut}
/>
<main {...rest}>{children}</main>
<Footer />
<Footer
hidePoweredBy={hidePoweredBy}
showWhatsNewLink={showWhatsNewLink}
showTerms={showTerms}
termsUrl={termsUrl}
showPrivacy={showPrivacy}
privacyUrl={privacyUrl}
showCompanyLogo={showCompanyLogo}
companyLogoUrl={companyLogoUrl}
/>
</>
);
}
Page.propTypes = {
showCloud: PropTypes.bool,
enableSpoke: PropTypes.bool,
showDocsLink: PropTypes.bool,
docsUrl: PropTypes.string,
showSourceLink: PropTypes.bool,
showCommunityLink: PropTypes.bool,
communityUrl: PropTypes.string,
isAdmin: PropTypes.bool,
isSignedIn: PropTypes.bool,
email: PropTypes.string,
onSignOut: PropTypes.func,
hidePoweredBy: PropTypes.bool,
showWhatsNewLink: PropTypes.bool,
showTerms: PropTypes.bool,
termsUrl: PropTypes.string,
showPrivacy: PropTypes.bool,
privacyUrl: PropTypes.string,
showCompanyLogo: PropTypes.bool,
companyLogoUrl: PropTypes.string,
children: PropTypes.node
};

Просмотреть файл

@ -6,7 +6,7 @@
box-sizing: border-box;
}
html, body, #ui-root, .home-root {
html, body, :global(#root), :global(#ui-root), :global(.home-root) {
margin: 0;
height:100%;
}
@ -17,7 +17,7 @@ body {
color: var(--home-text-color);
}
#ui-root, .home-root {
:global(#root), :global(#ui-root), :global(.home-root) {
display:flex;
flex-direction:column;
}
@ -33,6 +33,7 @@ h2 {
main {
display: block;
order: -1;
flex: 1;
@media(min-width: $breakpoint-lg) {
order: 0;

40
src/react-components/layout/PageContainer.js поставляемый Normal file
Просмотреть файл

@ -0,0 +1,40 @@
import React, { useContext } from "react";
import PropTypes from "prop-types";
import { Page } from "./Page";
import { AuthContext } from "../auth/AuthContext";
import configs from "../../utils/configs";
export function PageContainer({ children, ...rest }) {
const auth = useContext(AuthContext);
return (
<Page
showCloud={configs.feature("show_cloud")}
enableSpoke={configs.feature("enable_spoke")}
showDocsLink={configs.feature("show_docs_link")}
docsUrl={configs.link("docs", "https://hubs.mozilla.com/docs")}
showSourceLink={configs.feature("show_source_link")}
showCommunityLink={configs.feature("show_community_link")}
communityUrl={configs.link("community", "https://discord.gg/wHmY4nd")}
isAdmin={auth.isAdmin}
isSignedIn={auth.isSignedIn}
email={auth.email}
onSignOut={auth.signOut}
hidePoweredBy={configs.feature("hide_powered_by")}
showWhatsNewLink={configs.feature("show_whats_new_link")}
showTerms={configs.feature("show_terms")}
termsUrl={configs.link("terms_of_use", "https://github.com/mozilla/hubs/blob/master/TERMS.md")}
showPrivacy={configs.feature("show_privacy")}
privacyUrl={configs.link("privacy_notice", "https://github.com/mozilla/hubs/blob/master/PRIVACY.md")}
showCompanyLogo={configs.feature("show_company_logo")}
companyLogoUrl={configs.image("company_logo")}
{...rest}
>
{children}
</Page>
);
}
PageContainer.propTypes = {
children: PropTypes.node
};

20
src/react-components/misc/Loader.js поставляемый
Просмотреть файл

@ -1,20 +0,0 @@
import React from "react";
import PropTypes from "prop-types";
import styles from "./Loader.scss";
export function Loader({ message }) {
return (
<>
<div className={styles.loader}>
<div />
<div />
<div />
</div>
{message && <div className={styles.loaderText}>{message}</div>}
</>
);
}
Loader.propTypes = {
message: PropTypes.string
};

Просмотреть файл

@ -1,52 +0,0 @@
:local(.loader) {
display: inline-block;
position: relative;
width: 40px;
height: 80px;
div {
display: inline-block;
position: absolute;
left: 8px;
width: 0.6em;
background: var(--loading-spinner-color);
animation: loading 1s ease-in-out infinite;
border-radius: 5px;
}
div:nth-child(1) {
left: 0.2em;
animation-delay: -0.32s;
}
div:nth-child(2) {
left: 1em;
animation-delay: -0.16s;
}
div:nth-child(3) {
left: 1.8em;
animation-delay: 0;
}
}
@keyframes loading {
0%,
80%,
100% {
top: 24px;
height: 2em;
}
40% {
top: 8px;
height: 3.5em;
}
}
:local(.loading-text) {
font-weight: normal;
font-size: 0.8em;
color: var(--loading-text-color);
margin-top: 0;
}

7
src/react-components/misc/Spinner.js поставляемый Normal file
Просмотреть файл

@ -0,0 +1,7 @@
import React from "react";
import { ReactComponent as SpinnerSvg } from "./Spinner.svg";
import styles from "./Spinner.scss";
export function Spinner() {
return <SpinnerSvg className={styles.spinner} />;
}

Просмотреть файл

@ -0,0 +1,22 @@
@use "../styles/theme.scss";
:local(.spinner) {
animation: rotate 3s infinite linear;
*[stroke=\#000] {
stroke: theme.$blue;
}
*[fill=\#000] {
fill: theme.$blue;
}
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
}

10
src/react-components/modal/Modal.js поставляемый
Просмотреть файл

@ -2,16 +2,6 @@ import React from "react";
import PropTypes from "prop-types";
import classNames from "classnames";
import styles from "./Modal.scss";
import { IconButton } from "../input/IconButton";
import { ReactComponent as CloseIcon } from "../icons/Close.svg";
export function CloseButton(props) {
return (
<IconButton {...props} className={styles.sidebarButton}>
<CloseIcon width={16} height={16} />
</IconButton>
);
}
export function Modal({ title, beforeTitle, afterTitle, children, contentClassName, className, disableFullscreen }) {
return (

4
src/react-components/room/LoadingScreen.js поставляемый
Просмотреть файл

@ -1,7 +1,7 @@
import React from "react";
import PropTypes from "prop-types";
import styles from "./LoadingScreen.scss";
import { ReactComponent as Spinner } from "../misc/Spinner.svg";
import { Spinner } from "../misc/Spinner";
import { useRandomMessageTransition } from "./useRandomMessageTransition";
export function LoadingScreen({ logoSrc, message, infoMessages }) {
@ -11,7 +11,7 @@ export function LoadingScreen({ logoSrc, message, infoMessages }) {
<div className={styles.loadingScreen}>
<div className={styles.center}>
<img className={styles.logo} src={logoSrc} />
<Spinner className={styles.spinner} />
<Spinner />
<p>{message}</p>
</div>
<div className={styles.bottom}>

Просмотреть файл

@ -41,27 +41,6 @@
max-height: 140px;
}
:local(.spinner) {
animation: rotate 3s infinite linear;
*[stroke=\#000] {
stroke: theme.$blue;
}
*[fill=\#000] {
fill: theme.$blue;
}
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
}
:local(.bottom) {
position: absolute;
bottom: 10%;

35
src/react-components/ui-root.js поставляемый
Просмотреть файл

@ -32,7 +32,6 @@ import ChangeSceneDialog from "./change-scene-dialog.js";
import AvatarUrlDialog from "./avatar-url-dialog.js";
import InviteDialog from "./invite-dialog.js";
import InviteTeamDialog from "./invite-team-dialog.js";
import SignInDialog from "./sign-in-dialog.js";
import RoomSettingsDialog from "./room-settings-dialog.js";
import CloseRoomDialog from "./close-room-dialog.js";
import Tip from "./tip.js";
@ -100,6 +99,8 @@ import { PlacePopoverContainer } from "./room/PlacePopoverContainer";
import { SharePopoverContainer } from "./room/SharePopoverContainer";
import { VoiceButtonContainer } from "./room/VoiceButtonContainer";
import { ReactionButtonContainer } from "./room/ReactionButtonContainer";
import { RoomSignInModalContainer } from "./auth/RoomSignInModalContainer";
import { SignInStep } from "./auth/SignInModal";
const avatarEditorDebug = qsTruthy("avatarEditorDebug");
@ -417,18 +418,22 @@ class UIRoot extends Component {
onContinueAfterSignIn
} = this.props;
this.showNonHistoriedDialog(SignInDialog, {
this.showNonHistoriedDialog(RoomSignInModalContainer, {
step: SignInStep.submit,
message: getMessages()[signInMessageId],
onSignIn: async email => {
onSubmitEmail: async email => {
const { authComplete } = await authChannel.startAuthentication(email, this.props.hubChannel);
this.showNonHistoriedDialog(SignInDialog, { authStarted: true, onClose: onContinueAfterSignIn });
this.showNonHistoriedDialog(RoomSignInModalContainer, {
step: SignInStep.waitForVerification,
onClose: onContinueAfterSignIn
});
await authComplete;
this.setState({ signedIn: true });
this.showNonHistoriedDialog(SignInDialog, {
authComplete: true,
this.showNonHistoriedDialog(RoomSignInModalContainer, {
step: SignInStep.complete,
message: getMessages()[signInCompleteMessageId],
continueText: getMessages()[signInContinueTextId],
onClose: onContinueAfterSignIn,
@ -861,22 +866,6 @@ class UIRoot extends Component {
renderDialog = (DialogClass, props = {}) => <DialogClass {...{ onClose: this.closeDialog, ...props }} />;
showSignInDialog = () => {
this.showNonHistoriedDialog(SignInDialog, {
message: getMessages()["sign-in.prompt"],
onSignIn: async email => {
const { authComplete } = await this.props.authChannel.startAuthentication(email, this.props.hubChannel);
this.showNonHistoriedDialog(SignInDialog, { authStarted: true });
await authComplete;
this.setState({ signedIn: true });
this.closeDialog();
}
});
};
signOut = async () => {
await this.props.authChannel.signOut(this.props.hubChannel);
this.setState({ signedIn: false });
@ -1499,7 +1488,7 @@ class UIRoot extends Component {
<AvatarEditor
className={styles.avatarEditor}
signedIn={this.state.signedIn}
onSignIn={this.showSignInDialog}
onSignIn={this.showContextualSignInDialog}
onSave={() => {
if (props.location.state.detail && props.location.state.detail.returnToProfile) {
this.props.history.goBack();

Просмотреть файл

@ -6,8 +6,11 @@ import Store from "./storage/store";
import "./utils/theme";
import { getLocale, getMessages } from "./utils/i18n";
import { AuthContextProvider } from "./react-components/auth/AuthContext";
import { SignInPage } from "./react-components/auth/SignInPage";
import { SignInModalContainer } from "./react-components/auth/SignInModalContainer";
import { PageContainer } from "./react-components/layout/PageContainer";
import configs from "./utils/configs";
import "./assets/stylesheets/globals.scss";
import { Center } from "./react-components/layout/Center";
registerTelemetry("/signin", "Hubs Sign In Page");
@ -18,7 +21,11 @@ function Root() {
return (
<WrappedIntlProvider locale={getLocale()} messages={getMessages()}>
<AuthContextProvider store={store}>
<SignInPage />
<PageContainer style={{ backgroundImage: configs.image("home_background", true), backgroundSize: "cover" }}>
<Center>
<SignInModalContainer />
</Center>
</PageContainer>
</AuthContextProvider>
</WrappedIntlProvider>
);

Просмотреть файл

@ -5,8 +5,11 @@ import registerTelemetry from "./telemetry";
import Store from "./storage/store";
import "./utils/theme";
import { AuthContextProvider } from "./react-components/auth/AuthContext";
import { VerifyPage } from "./react-components/auth/VerifyPage";
import { VerifyModalContainer } from "./react-components/auth/VerifyModalContainer";
import configs from "./utils/configs";
import "./assets/stylesheets/globals.scss";
import { PageContainer } from "./react-components/layout/PageContainer";
import { Center } from "./react-components/layout/Center";
registerTelemetry("/verify", "Hubs Verify Email Page");
@ -17,7 +20,11 @@ function Root() {
return (
<WrappedIntlProvider>
<AuthContextProvider store={store}>
<VerifyPage />
<PageContainer style={{ backgroundImage: configs.image("home_background", true), backgroundSize: "cover" }}>
<Center>
<VerifyModalContainer />
</Center>
</PageContainer>
</AuthContextProvider>
</WrappedIntlProvider>
);