Add a register page

This commit is contained in:
Zoe Roux 2023-02-01 00:34:58 +09:00 committed by Zoe Roux
parent b62b272492
commit 2b87aacc58
12 changed files with 425 additions and 188 deletions

View File

@ -0,0 +1,23 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { RegisterPage } from "@kyoo/ui";
export default RegisterPage;

View File

@ -0,0 +1,24 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { RegisterPage } from "@kyoo/ui";
import { withRoute } from "~/router";
export default withRoute(RegisterPage);

View File

@ -18,7 +18,8 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { px, Stylable, Theme, useYoshiki } from "yoshiki/native";
import { ComponentProps } from "react";
import { Theme, useYoshiki } from "yoshiki/native";
import { PressableFeedback } from "./links";
import { P } from "./text";
import { ts } from "./utils";
@ -27,16 +28,15 @@ export const Button = ({
text,
onPress,
...props
}: { text: string; onPress?: () => void } & Stylable) => {
}: { text: string } & ComponentProps<typeof PressableFeedback>) => {
const { css } = useYoshiki();
return (
<PressableFeedback
onPress={onPress}
{...css(
{
flexGrow: 0,
p: ts(.5),
p: ts(0.5),
borderRadius: ts(5),
borderColor: (theme) => theme.accent,
borderWidth: ts(0.5),
@ -45,6 +45,7 @@ export const Button = ({
text: { color: (theme: Theme) => theme.colors.white },
},
},
// @ts-ignore ??
props,
)}
>

View File

@ -23,4 +23,4 @@ export { BrowsePage } from "./browse";
export { MovieDetails, ShowDetails } from "./details";
export { Player } from "./player";
export { SearchPage } from "./search";
export { LoginPage } from "./login";
export { LoginPage, RegisterPage } from "./login";

View File

@ -0,0 +1,86 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { ts } from "@kyoo/primitives";
import { ReactNode } from "react";
import { ImageBackground, ImageProps, Platform, View } from "react-native";
import Svg, { SvgProps, Path } from "react-native-svg";
import { min, percent, px, Stylable, useYoshiki, vh, vw } from "yoshiki/native";
const SvgBlob = (props: SvgProps) => {
const { css, theme } = useYoshiki();
return (
<View {...css({ width: percent(100), aspectRatio: 5 / 6 }, props)}>
<Svg width="100%" height="100%" viewBox="0 0 500 600">
<Path
d="M459.7 0c-20.2 43.3-40.3 86.6-51.7 132.6-11.3 45.9-13.9 94.6-36.1 137.6-22.2 43-64.1 80.3-111.5 88.2s-100.2-13.7-144.5-1.8C71.6 368.6 35.8 414.2 0 459.7V0h459.7z"
fill={theme.background}
/>
</Svg>
</View>
);
};
export const FormPage = ({ children, ...props }: { children: ReactNode } & Stylable) => {
const { css } = useYoshiki();
// TODO: Replace the hardcoded 1 to a random show/movie thumbnail.
const src = `${Platform.OS === "web" ? "/api/" : process.env.PUBLIC_BACK_URL}/shows/1/thumbnail`;
const nativeProps = Platform.select<ImageProps>({
web: {
defaultSource: typeof src === "string" ? { uri: src! } : Array.isArray(src) ? src[0] : src!,
},
default: {},
});
return (
<ImageBackground
source={{ uri: src }}
{...nativeProps}
{...css({
flexDirection: "row",
flexGrow: 1,
backgroundColor: (theme) => theme.dark.background,
})}
>
<View
{...css({
width: min(vh(90), px(1200)),
height: min(vh(90), px(1200)),
})}
>
<SvgBlob {...css({ position: "absolute", top: 0, left: 0 })} />
<View
{...css(
{
width: percent(75),
paddingHorizontal: ts(3),
maxWidth: vw(100),
},
props,
)}
>
{children}
</View>
</View>
</ImageBackground>
);
};

View File

@ -0,0 +1,22 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
export { LoginPage } from "./login";
export { RegisterPage } from "./register";

View File

@ -1,181 +0,0 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { KyooErrors, kyooUrl, QueryPage } from "@kyoo/models";
import { Button, P, Input, ts, H1, A, IconButton } from "@kyoo/primitives";
import { ComponentProps, useState } from "react";
import { useTranslation } from "react-i18next";
import { ImageBackground, ImageProps, Platform, View } from "react-native";
import { Trans } from "react-i18next";
import { min, percent, px, useYoshiki, vh, vw } from "yoshiki/native";
import Visibility from "@material-symbols/svg-400/rounded/visibility-fill.svg";
import VisibilityOff from "@material-symbols/svg-400/rounded/visibility_off-fill.svg";
import { DefaultLayout } from "../layout";
import Svg, { SvgProps, Path } from "react-native-svg";
const SvgBlob = (props: SvgProps) => {
const { css, theme } = useYoshiki();
return (
<View {...css({ width: percent(100), aspectRatio: 5 / 6 }, props)}>
<Svg width="100%" height="100%" viewBox="0 0 500 600">
<Path
d="M459.7 0c-20.2 43.3-40.3 86.6-51.7 132.6-11.3 45.9-13.9 94.6-36.1 137.6-22.2 43-64.1 80.3-111.5 88.2s-100.2-13.7-144.5-1.8C71.6 368.6 35.8 414.2 0 459.7V0h459.7z"
fill={theme.background}
/>
</Svg>
</View>
);
};
const PasswordInput = (props: ComponentProps<typeof Input>) => {
const { css } = useYoshiki();
const [show, setVisibility] = useState(false);
return (
<Input
secureTextEntry={!show}
right={
<IconButton
icon={show ? VisibilityOff : Visibility}
size={19}
onPress={() => setVisibility(!show)}
{...css({ width: px(19), height: px(19), m: 0, p: 0 })}
/>
}
{...props}
/>
);
};
const login = async (username: string, password: string) => {
let resp;
try {
resp = await fetch(`${kyooUrl}/auth/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
username,
password,
}),
});
} catch (e) {
console.error("Login error", e);
throw { errors: ["Could not reach Kyoo's server."] } as KyooErrors;
}
if (!resp.ok) {
const err = await resp.json() as KyooErrors;
return { type: "error", value: null, error: err.errors[0] };
}
const token = await resp.json();
// TODO: Save the token in the secure storage.
return { type: "value", value: token, error: null };
};
export const LoginPage: QueryPage = () => {
const { t } = useTranslation();
const { css } = useYoshiki();
// TODO: Replace the hardcoded 1 to a random show/movie thumbnail.
const src = `${Platform.OS === "web" ? "/api/" : process.env.PUBLIC_BACK_URL}/shows/1/thumbnail`;
const nativeProps = Platform.select<ImageProps>({
web: {
defaultSource: typeof src === "string" ? { uri: src! } : Array.isArray(src) ? src[0] : src!,
},
default: {},
});
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState<string | null>(null);
return (
<ImageBackground
source={{ uri: src }}
{...nativeProps}
{...css({
flexDirection: "row",
flexGrow: 1,
backgroundColor: (theme) => theme.dark.background,
})}
>
<View
{...css({
width: min(vh(90), px(1200)),
height: min(vh(90), px(1200)),
})}
>
<SvgBlob {...css({ position: "absolute", top: 0, left: 0 })} />
<View
{...css({
width: percent(75),
maxWidth: vw(100),
paddingHorizontal: ts(3),
marginTop: Platform.OS === "web" ? ts(6) : 0,
})}
>
<H1>{t("login.login")}</H1>
{Platform.OS !== "web" && (
<>
<P {...css({ paddingLeft: ts(1) })}>{t("login.server")}</P>
<Input variant="big" />
</>
)}
<P {...css({ paddingLeft: ts(1) })}>{t("login.username")}</P>
<Input
autoComplete="username"
variant="big"
onChangeText={(value) => setUsername(value)}
/>
<P {...css({ paddingLeft: ts(1) })}>{t("login.password")}</P>
<PasswordInput
autoComplete="password"
variant="big"
onChangeText={(value) => setPassword(value)}
/>
{error && <P {...css({ color: (theme) => theme.colors.red })}>{error}</P>}
<Button
text={t("login.login")}
onPress={async () => {
const { error } = await login(username, password);
setError(error);
}}
{...css({
m: ts(1),
width: px(250),
maxWidth: percent(100),
alignSelf: "center",
mY: ts(3),
})}
/>
<P>
<Trans i18nKey="login.or-register">
Dont have an account? <A href="/register">Register</A>.
</Trans>
</P>
</View>
</View>
</ImageBackground>
);
};
LoginPage.getLayout = DefaultLayout;

View File

@ -0,0 +1,111 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { KyooErrors, kyooUrl, QueryPage } from "@kyoo/models";
import { Button, P, Input, ts, H1, A } from "@kyoo/primitives";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Platform } from "react-native";
import { Trans } from "react-i18next";
import { percent, px, useYoshiki } from "yoshiki/native";
import { DefaultLayout } from "../layout";
import { FormPage } from "./form";
import { PasswordInput } from "./password-input";
const login = async (username: string, password: string) => {
let resp;
try {
resp = await fetch(`${kyooUrl}/auth/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
username,
password,
}),
});
} catch (e) {
console.error("Login error", e);
throw { errors: ["Could not reach Kyoo's server."] } as KyooErrors;
}
if (!resp.ok) {
const err = (await resp.json()) as KyooErrors;
return { type: "error", value: null, error: err.errors[0] };
}
const token = await resp.json();
// TODO: Save the token in the secure storage.
return { type: "value", value: token, error: null };
};
export const LoginPage: QueryPage = () => {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState<string | null>(null);
const { t } = useTranslation();
const { css } = useYoshiki();
return (
<FormPage
{...css({
marginTop: Platform.OS === "web" ? ts(6) : 0,
})}
>
<H1>{t("login.login")}</H1>
{Platform.OS !== "web" && (
<>
<P {...css({ paddingLeft: ts(1) })}>{t("login.server")}</P>
<Input variant="big" />
</>
)}
<P {...css({ paddingLeft: ts(1) })}>{t("login.username")}</P>
<Input autoComplete="username" variant="big" onChangeText={(value) => setUsername(value)} />
<P {...css({ paddingLeft: ts(1) })}>{t("login.password")}</P>
<PasswordInput
autoComplete="password"
variant="big"
onChangeText={(value) => setPassword(value)}
/>
{error && <P {...css({ color: (theme) => theme.colors.red })}>{error}</P>}
<Button
text={t("login.login")}
onPress={async () => {
const { error } = await login(username, password);
setError(error);
}}
{...css({
m: ts(1),
width: px(250),
maxWidth: percent(100),
alignSelf: "center",
mY: ts(3),
})}
/>
<P>
<Trans i18nKey="login.or-register">
Dont have an account? <A href="/register">Register</A>.
</Trans>
</P>
</FormPage>
);
};
LoginPage.getLayout = DefaultLayout;

View File

@ -0,0 +1,45 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { IconButton, Input } from "@kyoo/primitives";
import { ComponentProps, useState } from "react";
import { px, useYoshiki } from "yoshiki/native";
import Visibility from "@material-symbols/svg-400/rounded/visibility-fill.svg";
import VisibilityOff from "@material-symbols/svg-400/rounded/visibility_off-fill.svg";
export const PasswordInput = (props: ComponentProps<typeof Input>) => {
const { css } = useYoshiki();
const [show, setVisibility] = useState(false);
return (
<Input
secureTextEntry={!show}
right={
<IconButton
icon={show ? VisibilityOff : Visibility}
size={19}
onPress={() => setVisibility(!show)}
{...css({ width: px(19), height: px(19), m: 0, p: 0 })}
/>
}
{...props}
/>
);
};

View File

@ -0,0 +1,100 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { KyooErrors, kyooUrl, QueryPage } from "@kyoo/models";
import { Button, P, Input, ts, H1, A } from "@kyoo/primitives";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Platform } from "react-native";
import { Trans } from "react-i18next";
import { percent, px, useYoshiki } from "yoshiki/native";
import { DefaultLayout } from "../layout";
import { FormPage } from "./form";
import { PasswordInput } from "./password-input";
export const RegisterPage: QueryPage = () => {
const [email, setEmail] = useState("");
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [confirm, setConfirm] = useState("");
const [error, setError] = useState<string | null>(null);
const { t } = useTranslation();
const { css } = useYoshiki();
return (
<FormPage>
<H1>{t("login.register")}</H1>
{Platform.OS !== "web" && (
<>
<P {...css({ paddingLeft: ts(1) })}>{t("login.server")}</P>
<Input variant="big" />
</>
)}
<P {...css({ paddingLeft: ts(1) })}>{t("login.username")}</P>
<Input autoComplete="username" variant="big" onChangeText={(value) => setUsername(value)} />
<P {...css({ paddingLeft: ts(1) })}>{t("login.email")}</P>
<Input autoComplete="email" variant="big" onChangeText={(value) => setEmail(value)} />
<P {...css({ paddingLeft: ts(1) })}>{t("login.password")}</P>
<PasswordInput
autoComplete="password-new"
variant="big"
onChangeText={(value) => setPassword(value)}
/>
<P {...css({ paddingLeft: ts(1) })}>{t("login.confirm")}</P>
<PasswordInput
autoComplete="password-new"
variant="big"
onChangeText={(value) => setConfirm(value)}
/>
{password !== confirm && (
<P {...css({ color: (theme) => theme.colors.red })}>{t("login.password-no-match")}</P>
)}
{error && <P {...css({ color: (theme) => theme.colors.red })}>{error}</P>}
<Button
text={t("login.login")}
disabled={error != null || password !== confirm}
onPress={async () => {
const { error } = await register(email, username, password);
setError(error);
}}
{...css({
m: ts(1),
width: px(250),
maxWidth: percent(100),
alignSelf: "center",
mY: ts(3),
})}
/>
<P>
<Trans i18nKey="login.or-login">
Have an account already? <A href="/login">Log in</A>.
</Trans>
</P>
</FormPage>
);
};
RegisterPage.getLayout = DefaultLayout;

View File

@ -57,6 +57,9 @@
"email": "Email",
"username": "Username",
"password": "Password",
"or-register": "Dont have an account? <1>Register</1>."
"confirm": "Confirm Password",
"or-register": "Dont have an account? <1>Register</1>.",
"or-login": "Have an account already? <1>Log in</1>.",
"password-no-match": "Passwords do not match."
}
}

View File

@ -57,6 +57,9 @@
"email": "Email",
"username": "Username",
"password": "Mot de passe",
"or-register": "Vous n'avez pas de compte ? <1>Inscrivez-vous</1>."
"confirm": "Confirm Password",
"or-register": "Vous n'avez pas de compte ? <1>Inscrivez-vous</1>.",
"or-login": "Vous avez déjà un compte ? <1>Connectez-vous.<1/>",
"password-no-match": "Mots de passe differents"
}
}