mirror of
https://github.com/revoltchat/revite.git
synced 2024-11-29 10:20:59 -05:00
Port Login UI
This commit is contained in:
parent
aa81ebb298
commit
68a35751b3
18 changed files with 749 additions and 3 deletions
|
@ -25,6 +25,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@fontsource/open-sans": "^4.4.5",
|
"@fontsource/open-sans": "^4.4.5",
|
||||||
|
"@hcaptcha/react-hcaptcha": "^0.3.6",
|
||||||
"@preact/preset-vite": "^2.0.0",
|
"@preact/preset-vite": "^2.0.0",
|
||||||
"@styled-icons/bootstrap": "^10.34.0",
|
"@styled-icons/bootstrap": "^10.34.0",
|
||||||
"@styled-icons/feather": "^10.34.0",
|
"@styled-icons/feather": "^10.34.0",
|
||||||
|
@ -36,6 +37,7 @@
|
||||||
"@typescript-eslint/eslint-plugin": "^4.27.0",
|
"@typescript-eslint/eslint-plugin": "^4.27.0",
|
||||||
"@typescript-eslint/parser": "^4.27.0",
|
"@typescript-eslint/parser": "^4.27.0",
|
||||||
"dayjs": "^1.10.5",
|
"dayjs": "^1.10.5",
|
||||||
|
"detect-browser": "^5.2.0",
|
||||||
"eslint": "^7.28.0",
|
"eslint": "^7.28.0",
|
||||||
"eslint-config-preact": "^1.1.4",
|
"eslint-config-preact": "^1.1.4",
|
||||||
"localforage": "^1.9.0",
|
"localforage": "^1.9.0",
|
||||||
|
@ -43,6 +45,7 @@
|
||||||
"prettier": "^2.3.1",
|
"prettier": "^2.3.1",
|
||||||
"react-device-detect": "^1.17.0",
|
"react-device-detect": "^1.17.0",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
|
"react-hook-form": "6.3.0",
|
||||||
"react-overlapping-panels": "1.1.2-patch.0",
|
"react-overlapping-panels": "1.1.2-patch.0",
|
||||||
"react-redux": "^7.2.4",
|
"react-redux": "^7.2.4",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
|
|
|
@ -2,13 +2,15 @@ import { CheckAuth } from "./context/revoltjs/CheckAuth";
|
||||||
import { Route, Switch } from "react-router-dom";
|
import { Route, Switch } from "react-router-dom";
|
||||||
import Context from "./context";
|
import Context from "./context";
|
||||||
|
|
||||||
|
import { Login } from "./pages/login/Login";
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
return (
|
return (
|
||||||
<Context>
|
<Context>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/login">
|
<Route path="/login">
|
||||||
<CheckAuth>
|
<CheckAuth>
|
||||||
<h1>login</h1>
|
<Login />
|
||||||
</CheckAuth>
|
</CheckAuth>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/">
|
<Route path="/">
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { isTouchscreenDevice } from "../lib/isTouchscreenDevice";
|
import { isTouchscreenDevice } from "../lib/isTouchscreenDevice";
|
||||||
import { createGlobalStyle } from "styled-components";
|
import { createGlobalStyle } from "styled-components";
|
||||||
import { Children } from "../types/Preact";
|
import { Children } from "../types/Preact";
|
||||||
|
import { createContext } from "preact";
|
||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
|
|
||||||
export type Variables =
|
export type Variables =
|
||||||
|
@ -111,6 +112,8 @@ const GlobalTheme = createGlobalStyle<{ theme: Theme }>`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const ThemeContext = createContext<Theme>({} as any);
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: Children;
|
children: Children;
|
||||||
}
|
}
|
||||||
|
@ -119,7 +122,7 @@ export default function Theme(props: Props) {
|
||||||
const theme = PRESETS.dark;
|
const theme = PRESETS.dark;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ThemeContext.Provider value={theme}>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<meta
|
<meta
|
||||||
name="theme-color"
|
name="theme-color"
|
||||||
|
@ -132,6 +135,6 @@ export default function Theme(props: Props) {
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<GlobalTheme theme={theme} />
|
<GlobalTheme theme={theme} />
|
||||||
{props.children}
|
{props.children}
|
||||||
</>
|
</ThemeContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
18
src/context/revoltjs/error.ts
Normal file
18
src/context/revoltjs/error.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
export function takeError(
|
||||||
|
error: any
|
||||||
|
): string {
|
||||||
|
const type = error?.response?.data?.type;
|
||||||
|
let id = type;
|
||||||
|
if (!type) {
|
||||||
|
if (error?.response?.status === 403) {
|
||||||
|
return "Unauthorized";
|
||||||
|
} else if (error && (!!error.isAxiosError && !error.response)) {
|
||||||
|
return "NetworkError";
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(error);
|
||||||
|
return "UnknownError";
|
||||||
|
}
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
68
src/pages/login/FormField.tsx
Normal file
68
src/pages/login/FormField.tsx
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import Overline from '../../components/ui/Overline';
|
||||||
|
import InputBox from '../../components/ui/InputBox';
|
||||||
|
import { Text, Localizer } from 'preact-i18n';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
type: "email" | "username" | "password" | "invite" | "current_password";
|
||||||
|
showOverline?: boolean;
|
||||||
|
register: Function;
|
||||||
|
error?: string;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FormField({
|
||||||
|
type,
|
||||||
|
register,
|
||||||
|
showOverline,
|
||||||
|
error,
|
||||||
|
name
|
||||||
|
}: Props) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{showOverline && (
|
||||||
|
<Overline error={error}>
|
||||||
|
<Text id={`login.${type}`} />
|
||||||
|
</Overline>
|
||||||
|
)}
|
||||||
|
<Localizer>
|
||||||
|
<InputBox
|
||||||
|
placeholder={(<Text id={`login.enter.${type}`} />) as any}
|
||||||
|
name={
|
||||||
|
type === "current_password" ? "password" : name ?? type
|
||||||
|
}
|
||||||
|
type={
|
||||||
|
type === "invite" || type === "username"
|
||||||
|
? "text"
|
||||||
|
: type === "current_password"
|
||||||
|
? "password"
|
||||||
|
: type
|
||||||
|
}
|
||||||
|
ref={register(
|
||||||
|
type === "password" || type === "current_password"
|
||||||
|
? {
|
||||||
|
validate: (value: string) =>
|
||||||
|
value.length === 0
|
||||||
|
? "RequiredField"
|
||||||
|
: value.length < 8
|
||||||
|
? "TooShort"
|
||||||
|
: value.length > 1024
|
||||||
|
? "TooLong"
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
: type === "email"
|
||||||
|
? {
|
||||||
|
required: "RequiredField",
|
||||||
|
pattern: {
|
||||||
|
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
|
||||||
|
message: "InvalidEmail"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: type === "username"
|
||||||
|
? { required: "RequiredField" }
|
||||||
|
: { required: "RequiredField" }
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Localizer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
123
src/pages/login/Login.module.scss
Normal file
123
src/pages/login/Login.module.scss
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
.login {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.attribution {
|
||||||
|
color: var(--tertiary-background);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 12px;
|
||||||
|
margin: 8px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg {
|
||||||
|
background-size: cover !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 260px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
margin: 1em 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.create {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--tertiary-foreground);
|
||||||
|
|
||||||
|
a {
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.success {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.note {
|
||||||
|
color: var(--tertiary-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mailProvider {
|
||||||
|
padding: 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
margin-top: 12px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--tertiary6);
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--tertiary-background) !important;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0 2px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--tertiary-foreground) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 768px) {
|
||||||
|
.bg {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
70
src/pages/login/Login.tsx
Normal file
70
src/pages/login/Login.tsx
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import { Text } from "preact-i18n";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import styles from "./Login.module.scss";
|
||||||
|
import { useContext } from "preact/hooks";
|
||||||
|
import { APP_VERSION } from "../../version";
|
||||||
|
import { LIBRARY_VERSION } from "revolt.js";
|
||||||
|
import { Route, Switch } from "react-router-dom";
|
||||||
|
import { ThemeContext } from "../../context/Theme";
|
||||||
|
import { RevoltClient } from "../../context/revoltjs/RevoltClient";
|
||||||
|
|
||||||
|
import background from "./background.jpg";
|
||||||
|
|
||||||
|
import { FormLogin } from "./forms/FormLogin";
|
||||||
|
import { FormCreate } from "./forms/FormCreate";
|
||||||
|
import { FormResend } from "./forms/FormResend";
|
||||||
|
import { FormReset, FormSendReset } from "./forms/FormReset";
|
||||||
|
|
||||||
|
export const Login = () => {
|
||||||
|
const theme = useContext(ThemeContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.login}>
|
||||||
|
<Helmet>
|
||||||
|
<meta name="theme-color" content={theme.background} />
|
||||||
|
</Helmet>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className={styles.attribution}>
|
||||||
|
<span>
|
||||||
|
API:{" "}
|
||||||
|
<code>{RevoltClient.configuration?.revolt ?? "???"}</code>{" "}
|
||||||
|
· revolt.js: <code>{LIBRARY_VERSION}</code>{" "}
|
||||||
|
· App: <code>{APP_VERSION}</code>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{/*<LocaleSelector />*/}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.modal}>
|
||||||
|
<Switch>
|
||||||
|
<Route path="/login/create">
|
||||||
|
<FormCreate />
|
||||||
|
</Route>
|
||||||
|
<Route path="/login/resend">
|
||||||
|
<FormResend />
|
||||||
|
</Route>
|
||||||
|
<Route path="/login/reset/:token">
|
||||||
|
<FormReset />
|
||||||
|
</Route>
|
||||||
|
<Route path="/login/reset">
|
||||||
|
<FormSendReset />
|
||||||
|
</Route>
|
||||||
|
<Route path="/">
|
||||||
|
<FormLogin />
|
||||||
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
<div className={styles.attribution}>
|
||||||
|
<span>
|
||||||
|
<Text id="general.image_by" /> ‎@lorenzoherrera
|
||||||
|
‏· unsplash.com
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={styles.bg}
|
||||||
|
style={{ background: `url('${background}')` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
BIN
src/pages/login/background.jpg
Normal file
BIN
src/pages/login/background.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 484 KiB |
36
src/pages/login/forms/CaptchaBlock.tsx
Normal file
36
src/pages/login/forms/CaptchaBlock.tsx
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import { Text } from "preact-i18n";
|
||||||
|
import { useEffect } from "preact/hooks";
|
||||||
|
import styles from "../Login.module.scss";
|
||||||
|
import HCaptcha from "@hcaptcha/react-hcaptcha";
|
||||||
|
import Preloader from "../../../components/ui/Preloader";
|
||||||
|
import { RevoltClient } from "../../../context/revoltjs/RevoltClient";
|
||||||
|
|
||||||
|
export interface CaptchaProps {
|
||||||
|
onSuccess: (token?: string) => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CaptchaBlock(props: CaptchaProps) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!RevoltClient.configuration?.features.captcha.enabled) {
|
||||||
|
props.onSuccess();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!RevoltClient.configuration?.features.captcha.enabled)
|
||||||
|
return <Preloader />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<HCaptcha
|
||||||
|
sitekey={RevoltClient.configuration.features.captcha.key}
|
||||||
|
onVerify={token => props.onSuccess(token)}
|
||||||
|
/>
|
||||||
|
<div className={styles.footer}>
|
||||||
|
<a onClick={props.onCancel}>
|
||||||
|
<Text id="login.cancel" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
236
src/pages/login/forms/Form.tsx
Normal file
236
src/pages/login/forms/Form.tsx
Normal file
|
@ -0,0 +1,236 @@
|
||||||
|
import { Legal } from "./Legal";
|
||||||
|
import { Text } from "preact-i18n";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { useState } from "preact/hooks";
|
||||||
|
import styles from "../Login.module.scss";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { MailProvider } from "./MailProvider";
|
||||||
|
import { CheckCircle, Mail } from "@styled-icons/feather";
|
||||||
|
import { CaptchaBlock, CaptchaProps } from "./CaptchaBlock";
|
||||||
|
import { takeError } from "../../../context/revoltjs/error";
|
||||||
|
import { RevoltClient } from "../../../context/revoltjs/RevoltClient";
|
||||||
|
|
||||||
|
import FormField from "../FormField";
|
||||||
|
import Button from "../../../components/ui/Button";
|
||||||
|
import Overline from "../../../components/ui/Overline";
|
||||||
|
import Preloader from "../../../components/ui/Preloader";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
page: "create" | "login" | "send_reset" | "reset" | "resend";
|
||||||
|
callback: (fields: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
invite: string;
|
||||||
|
captcha?: string;
|
||||||
|
}) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInviteCode() {
|
||||||
|
if (typeof window === 'undefined') return '';
|
||||||
|
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const code = urlParams.get('code');
|
||||||
|
return code ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Form({ page, callback }: Props) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [success, setSuccess] = useState<string | undefined>(undefined);
|
||||||
|
const [error, setGlobalError] = useState<string | undefined>(undefined);
|
||||||
|
const [captcha, setCaptcha] = useState<CaptchaProps | undefined>(undefined);
|
||||||
|
|
||||||
|
const { handleSubmit, register, errors, setError } = useForm({
|
||||||
|
defaultValues: {
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
invite: getInviteCode()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function onSubmit(data: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
invite: string;
|
||||||
|
}) {
|
||||||
|
setGlobalError(undefined);
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
function onError(err: any) {
|
||||||
|
setLoading(false);
|
||||||
|
|
||||||
|
const error = takeError(err);
|
||||||
|
switch (error) {
|
||||||
|
case "email_in_use":
|
||||||
|
return setError("email", { type: "", message: error });
|
||||||
|
case "unknown_user":
|
||||||
|
return setError("email", { type: "", message: error });
|
||||||
|
case "invalid_invite":
|
||||||
|
return setError("invite", { type: "", message: error });
|
||||||
|
}
|
||||||
|
|
||||||
|
setGlobalError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
RevoltClient.configuration?.features.captcha.enabled &&
|
||||||
|
page !== "reset"
|
||||||
|
) {
|
||||||
|
setCaptcha({
|
||||||
|
onSuccess: async captcha => {
|
||||||
|
setCaptcha(undefined);
|
||||||
|
try {
|
||||||
|
await callback({ ...data, captcha });
|
||||||
|
setSuccess(data.email);
|
||||||
|
} catch (err) {
|
||||||
|
onError(err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
setCaptcha(undefined);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await callback(data);
|
||||||
|
setSuccess(data.email);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
onError(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof success !== "undefined") {
|
||||||
|
return (
|
||||||
|
<div className={styles.success}>
|
||||||
|
{RevoltClient.configuration?.features.email ? (
|
||||||
|
<>
|
||||||
|
<Mail size={72} />
|
||||||
|
<h2>
|
||||||
|
<Text id="login.check_mail" />
|
||||||
|
</h2>
|
||||||
|
<p className={styles.note}>
|
||||||
|
<Text id="login.email_delay" />
|
||||||
|
</p>
|
||||||
|
<MailProvider email={success} />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<CheckCircle size={72} />
|
||||||
|
<h1>
|
||||||
|
<Text id="login.successful_registration" />
|
||||||
|
</h1>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<span className={styles.footer}>
|
||||||
|
<Link to="/login">
|
||||||
|
<a>
|
||||||
|
<Text id="login.remembered" />
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (captcha) return <CaptchaBlock {...captcha} />;
|
||||||
|
if (loading) return <Preloader />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.form}>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit) as any}>
|
||||||
|
{page !== "reset" && (
|
||||||
|
<FormField
|
||||||
|
type="email"
|
||||||
|
register={register}
|
||||||
|
showOverline
|
||||||
|
error={errors.email?.message}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{(page === "login" ||
|
||||||
|
page === "create" ||
|
||||||
|
page === "reset") && (
|
||||||
|
<FormField
|
||||||
|
type="password"
|
||||||
|
register={register}
|
||||||
|
showOverline
|
||||||
|
error={errors.password?.message}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{RevoltClient.configuration?.features.invite_only &&
|
||||||
|
page === "create" && (
|
||||||
|
<FormField
|
||||||
|
type="invite"
|
||||||
|
register={register}
|
||||||
|
showOverline
|
||||||
|
error={errors.invite?.message}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{error && (
|
||||||
|
<Overline type="error" error={error}>
|
||||||
|
<Text id={`login.error.${page}`} />
|
||||||
|
</Overline>
|
||||||
|
)}
|
||||||
|
<Button>
|
||||||
|
<Text
|
||||||
|
id={
|
||||||
|
page === "create"
|
||||||
|
? "login.register"
|
||||||
|
: page === "login"
|
||||||
|
? "login.title"
|
||||||
|
: page === "reset"
|
||||||
|
? "login.set_password"
|
||||||
|
: page === "resend"
|
||||||
|
? "login.resend"
|
||||||
|
: "login.reset"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
{page === "create" && (
|
||||||
|
<>
|
||||||
|
<span className={styles.create}>
|
||||||
|
<Text id="login.existing" />
|
||||||
|
<Link to="/login">
|
||||||
|
<Text id="login.title" />
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
<span className={styles.create}>
|
||||||
|
<Text id="login.missing_verification" />
|
||||||
|
<Link to="/login/resend">
|
||||||
|
<Text id="login.resend" />
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{page === "login" && (
|
||||||
|
<>
|
||||||
|
<span className={styles.create}>
|
||||||
|
<Text id="login.new" />
|
||||||
|
<Link to="/login/create">
|
||||||
|
<Text id="login.create" />
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
<span className={styles.create}>
|
||||||
|
<Text id="login.forgot" />
|
||||||
|
<Link to="/login/reset">
|
||||||
|
<Text id="login.reset" />
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{(page === "reset" ||
|
||||||
|
page === "resend" ||
|
||||||
|
page === "send_reset") && (
|
||||||
|
<>
|
||||||
|
<span className={styles.create}>
|
||||||
|
<Link to="/login">
|
||||||
|
<Text id="login.remembered" />
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Legal />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
13
src/pages/login/forms/FormCreate.tsx
Normal file
13
src/pages/login/forms/FormCreate.tsx
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { RevoltClient } from "../../../context/revoltjs/RevoltClient";
|
||||||
|
import { Form } from "./Form";
|
||||||
|
|
||||||
|
export function FormCreate() {
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
page="create"
|
||||||
|
callback={async data => {
|
||||||
|
await RevoltClient.register(process.env.API_SERVER as string, data);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
29
src/pages/login/forms/FormLogin.tsx
Normal file
29
src/pages/login/forms/FormLogin.tsx
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { Form } from "./Form";
|
||||||
|
import { useContext } from "preact/hooks";
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
|
import { deviceDetect } from "react-device-detect";
|
||||||
|
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
||||||
|
|
||||||
|
export function FormLogin() {
|
||||||
|
const { operations } = useContext(AppContext);
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
page="login"
|
||||||
|
callback={async data => {
|
||||||
|
const browser = deviceDetect();
|
||||||
|
let device_name;
|
||||||
|
if (browser) {
|
||||||
|
const { name, os } = browser;
|
||||||
|
device_name = `${name} on ${os}`;
|
||||||
|
} else {
|
||||||
|
device_name = "Unknown Device";
|
||||||
|
}
|
||||||
|
|
||||||
|
await operations.login({ ...data, device_name });
|
||||||
|
history.push("/");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
13
src/pages/login/forms/FormResend.tsx
Normal file
13
src/pages/login/forms/FormResend.tsx
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { RevoltClient } from "../../../context/revoltjs/RevoltClient";
|
||||||
|
import { Form } from "./Form";
|
||||||
|
|
||||||
|
export function FormResend() {
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
page="resend"
|
||||||
|
callback={async data => {
|
||||||
|
await RevoltClient.req("POST", "/auth/resend", data);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
32
src/pages/login/forms/FormReset.tsx
Normal file
32
src/pages/login/forms/FormReset.tsx
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { Form } from "./Form";
|
||||||
|
import { useHistory, useParams } from "react-router-dom";
|
||||||
|
import { RevoltClient } from "../../../context/revoltjs/RevoltClient";
|
||||||
|
|
||||||
|
export function FormSendReset() {
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
page="send_reset"
|
||||||
|
callback={async data => {
|
||||||
|
await RevoltClient.req("POST", "/auth/send_reset", data);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FormReset() {
|
||||||
|
const { token } = useParams<{ token: string }>();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
page="reset"
|
||||||
|
callback={async data => {
|
||||||
|
await RevoltClient.req("POST", "/auth/reset" as any, {
|
||||||
|
token,
|
||||||
|
...(data as any)
|
||||||
|
});
|
||||||
|
history.push("/login");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
29
src/pages/login/forms/Legal.tsx
Normal file
29
src/pages/login/forms/Legal.tsx
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import styles from "../Login.module.scss";
|
||||||
|
import { Text } from "preact-i18n";
|
||||||
|
|
||||||
|
export function Legal() {
|
||||||
|
return (
|
||||||
|
<span className={styles.footer}>
|
||||||
|
<a
|
||||||
|
href="https://revolt.chat/about"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<Text id="general.about" />
|
||||||
|
</a>
|
||||||
|
·
|
||||||
|
<a
|
||||||
|
href="https://revolt.chat/terms"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<Text id="general.tos" />
|
||||||
|
</a>
|
||||||
|
·
|
||||||
|
<a
|
||||||
|
href="https://revolt.chat/privacy"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<Text id="general.privacy" />
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
55
src/pages/login/forms/MailProvider.tsx
Normal file
55
src/pages/login/forms/MailProvider.tsx
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import { Text } from "preact-i18n";
|
||||||
|
import styles from "../Login.module.scss";
|
||||||
|
import Button from "../../../components/ui/Button";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
email?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapMailProvider(email?: string): [string, string] | undefined {
|
||||||
|
if (!email) return;
|
||||||
|
|
||||||
|
const match = /@(.+)/.exec(email);
|
||||||
|
if (match === null) return;
|
||||||
|
|
||||||
|
const domain = match[1];
|
||||||
|
switch (domain) {
|
||||||
|
case "gmail.com":
|
||||||
|
return ["Gmail", "https://gmail.com"];
|
||||||
|
case "tuta.io":
|
||||||
|
return ["Tutanota", "https://mail.tutanota.com"];
|
||||||
|
case "outlook.com":
|
||||||
|
return ["Outlook", "https://outlook.live.com"];
|
||||||
|
case "yahoo.com":
|
||||||
|
return ["Yahoo", "https://mail.yahoo.com"];
|
||||||
|
case "wp.pl":
|
||||||
|
return ["WP Poczta", "https://poczta.wp.pl"];
|
||||||
|
case "protonmail.com":
|
||||||
|
case "protonmail.ch":
|
||||||
|
return ["ProtonMail", "https://mail.protonmail.com"];
|
||||||
|
case "seznam.cz":
|
||||||
|
case "email.cz":
|
||||||
|
case "post.cz":
|
||||||
|
return ["Seznam", "https://email.seznam.cz"];
|
||||||
|
default:
|
||||||
|
return [domain, `https://${domain}`];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MailProvider({ email }: Props) {
|
||||||
|
const provider = mapMailProvider(email);
|
||||||
|
if (!provider) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.mailProvider}>
|
||||||
|
<a href={provider[1]} target="_blank">
|
||||||
|
<Button>
|
||||||
|
<Text
|
||||||
|
id="login.open_mail_provider"
|
||||||
|
fields={{ provider: provider[0] }}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
1
src/version.ts
Normal file
1
src/version.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export const APP_VERSION = "0.1.9-alpha.7";
|
15
yarn.lock
15
yarn.lock
|
@ -955,6 +955,11 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@hapi/hoek" "^8.3.0"
|
"@hapi/hoek" "^8.3.0"
|
||||||
|
|
||||||
|
"@hcaptcha/react-hcaptcha@^0.3.6":
|
||||||
|
version "0.3.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@hcaptcha/react-hcaptcha/-/react-hcaptcha-0.3.6.tgz#cbbb9abdaea451a4df408bc9d476e8b17f0b63f4"
|
||||||
|
integrity sha512-DQ5nvGVbbhd2IednxRhCV9wiPcCmclEV7bH98yGynGCXzO5XftO/XC0a1M1kEf9Ee+CLO/u+1HM/uE/PSrC3vQ==
|
||||||
|
|
||||||
"@insertish/mutable@1.0.6":
|
"@insertish/mutable@1.0.6":
|
||||||
version "1.0.6"
|
version "1.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/@insertish/mutable/-/mutable-1.0.6.tgz#f42eaba8528ff68cc8065d51f9bbbd30a24f34de"
|
resolved "https://registry.yarnpkg.com/@insertish/mutable/-/mutable-1.0.6.tgz#f42eaba8528ff68cc8065d51f9bbbd30a24f34de"
|
||||||
|
@ -1752,6 +1757,11 @@ define-properties@^1.1.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
object-keys "^1.0.12"
|
object-keys "^1.0.12"
|
||||||
|
|
||||||
|
detect-browser@^5.2.0:
|
||||||
|
version "5.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/detect-browser/-/detect-browser-5.2.0.tgz#c9cd5afa96a6a19fda0bbe9e9be48a6b6e1e9c97"
|
||||||
|
integrity sha512-tr7XntDAu50BVENgQfajMLzacmSe34D+qZc4zjnniz0ZVuw/TZcLcyxHQjYpJTM36sGEkZZlYLnIM1hH7alTMA==
|
||||||
|
|
||||||
dir-glob@^3.0.1:
|
dir-glob@^3.0.1:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
|
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
|
||||||
|
@ -3061,6 +3071,11 @@ react-helmet@^6.1.0:
|
||||||
react-fast-compare "^3.1.1"
|
react-fast-compare "^3.1.1"
|
||||||
react-side-effect "^2.1.0"
|
react-side-effect "^2.1.0"
|
||||||
|
|
||||||
|
react-hook-form@6.3.0:
|
||||||
|
version "6.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-6.3.0.tgz#5c1926d51d4532f44818ef73f96d1a8c11015a76"
|
||||||
|
integrity sha512-Xz7xxnILftxttc6H+miTSi2eYPehiW3XdsPaqY5dW8HcURFZPrnpxnmaRqz6JtZcbfRM8qjjppP/pOBaUzhn4w==
|
||||||
|
|
||||||
react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
|
react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
|
||||||
version "16.13.1"
|
version "16.13.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||||
|
|
Loading…
Reference in a new issue