Allow username and email to be changed

This commit is contained in:
Zoe Roux 2024-01-10 19:31:14 +01:00
parent 08d9b9d950
commit 65a254d808
10 changed files with 247 additions and 72 deletions

View File

@ -14,6 +14,7 @@
"format:fix": "prettier --write --ignore-path .gitignore '!src/utils/jotai-utils.tsx' ." "format:fix": "prettier --write --ignore-path .gitignore '!src/utils/jotai-utils.tsx' ."
}, },
"dependencies": { "dependencies": {
"@gorhom/portal": "^1.0.14",
"@kyoo/models": "workspace:^", "@kyoo/models": "workspace:^",
"@kyoo/primitives": "workspace:^", "@kyoo/primitives": "workspace:^",
"@kyoo/ui": "workspace:^", "@kyoo/ui": "workspace:^",

View File

@ -44,6 +44,7 @@ import { withTranslations } from "../i18n";
import arrayShuffle from "array-shuffle"; import arrayShuffle from "array-shuffle";
import { Tooltip } from "react-tooltip"; import { Tooltip } from "react-tooltip";
import { getCurrentAccount, readCookie, updateAccount } from "@kyoo/models/src/account-internal"; import { getCurrentAccount, readCookie, updateAccount } from "@kyoo/models/src/account-internal";
import { PortalProvider } from "@gorhom/portal";
const font = Poppins({ weight: ["300", "400", "900"], subsets: ["latin"], display: "swap" }); const font = Poppins({ weight: ["300", "400", "900"], subsets: ["latin"], display: "swap" });
@ -136,6 +137,7 @@ const App = ({ Component, pageProps }: AppProps) => {
<AccountProvider ssrAccount={account}> <AccountProvider ssrAccount={account}>
<HydrationBoundary state={queryState}> <HydrationBoundary state={queryState}>
<ThemeSelector theme={userTheme} font={{ normal: "inherit" }}> <ThemeSelector theme={userTheme} font={{ normal: "inherit" }}>
<PortalProvider>
<GlobalCssTheme /> <GlobalCssTheme />
<Layout <Layout
page={ page={
@ -151,6 +153,7 @@ const App = ({ Component, pageProps }: AppProps) => {
{...layoutProps} {...layoutProps}
/> />
<Tooltip id="tooltip" positionStrategy={"fixed"} /> <Tooltip id="tooltip" positionStrategy={"fixed"} />
</PortalProvider>
</ThemeSelector> </ThemeSelector>
</HydrationBoundary> </HydrationBoundary>
</AccountProvider> </AccountProvider>

View File

@ -45,7 +45,7 @@ export const queryFn = async <Data,>(
| { | {
path: (string | false | undefined | null)[]; path: (string | false | undefined | null)[];
body?: object; body?: object;
method: "GET" | "POST" | "DELETE"; method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
authenticated?: boolean; authenticated?: boolean;
apiUrl?: string; apiUrl?: string;
timeout?: number; timeout?: number;

View File

@ -52,9 +52,17 @@ export const Button = forwardRef<
}, },
props as any, props as any,
)} )}
>
<View
{...css({
paddingX: ts(3),
flexDirection: "row",
alignItems: "center",
})}
> >
<P {...css({ textAlign: "center" }, "text")}>{text}</P> <P {...css({ textAlign: "center" }, "text")}>{text}</P>
{icon} {icon}
</View>
</PressableFeedback> </PressableFeedback>
); );
}); });

View File

@ -31,6 +31,7 @@ import { useYoshiki } from "yoshiki";
import { PressableFeedback } from "./links"; import { PressableFeedback } from "./links";
import { P } from "./text"; import { P } from "./text";
import { focusReset, ts } from "./utils"; import { focusReset, ts } from "./utils";
import { View } from "react-native";
export const Select = ({ export const Select = ({
label, label,
@ -67,11 +68,19 @@ export const Select = ({
text: { color: (theme: Theme) => theme.colors.white }, text: { color: (theme: Theme) => theme.colors.white },
}, },
})} })}
>
<View
{...css({
paddingX: ts(3),
flexDirection: "row",
alignItems: "center",
})}
> >
<P {...css({ textAlign: "center" }, "text")}>{<RSelect.Value />}</P> <P {...css({ textAlign: "center" }, "text")}>{<RSelect.Value />}</P>
<RSelect.Icon asChild> <RSelect.Icon asChild>
<Icon icon={ExpandMore} /> <Icon icon={ExpandMore} />
</RSelect.Icon> </RSelect.Icon>
</View>
</InternalTriger> </InternalTriger>
</RSelect.Trigger> </RSelect.Trigger>
<ContrastArea mode="user"> <ContrastArea mode="user">

View File

@ -8,12 +8,14 @@
"@kyoo/primitives": "workspace:^" "@kyoo/primitives": "workspace:^"
}, },
"devDependencies": { "devDependencies": {
"@gorhom/portal": "^1.0.14",
"@shopify/flash-list": "^1.6.3", "@shopify/flash-list": "^1.6.3",
"@types/react": "18.2.39", "@types/react": "18.2.39",
"react-native-uuid": "^2.0.1", "react-native-uuid": "^2.0.1",
"typescript": "^5.3.2" "typescript": "^5.3.2"
}, },
"peerDependencies": { "peerDependencies": {
"@gorhom/portal": "*",
"@kesha-antonov/react-native-background-downloader": "*", "@kesha-antonov/react-native-background-downloader": "*",
"@material-symbols/svg-400": "*", "@material-symbols/svg-400": "*",
"@shopify/flash-list": "^1.3.1", "@shopify/flash-list": "^1.3.1",

View File

@ -19,25 +19,43 @@
*/ */
import { import {
Account,
MutationParam,
QueryIdentifier, QueryIdentifier,
QueryPage, QueryPage,
User, User,
UserP, UserP,
queryFn,
setUserTheme, setUserTheme,
useAccount, useAccount,
useUserTheme, useUserTheme,
} from "@kyoo/models"; } from "@kyoo/models";
import { Container, H1, HR, Icon, P, Select, SubP, imageBorderRadius, ts } from "@kyoo/primitives"; import {
Button,
Container,
H1,
HR,
Icon,
Input,
P,
Select,
SubP,
SwitchVariant,
imageBorderRadius,
ts,
} from "@kyoo/primitives";
import { DefaultLayout } from "../layout"; import { DefaultLayout } from "../layout";
import { Children, ReactElement, ReactNode } from "react"; import { Children, ReactElement, ReactNode, useState, useTransition } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ScrollView, View } from "react-native"; import { ScrollView, View } from "react-native";
import { px, rem, useYoshiki } from "yoshiki/native"; import { Portal } from "@gorhom/portal";
import { percent, px, rem, useYoshiki } from "yoshiki/native";
import Theme from "@material-symbols/svg-400/outlined/dark_mode.svg"; import Theme from "@material-symbols/svg-400/outlined/dark_mode.svg";
import Username from "@material-symbols/svg-400/outlined/badge.svg"; import Username from "@material-symbols/svg-400/outlined/badge.svg";
import Mail from "@material-symbols/svg-400/outlined/mail.svg"; import Mail from "@material-symbols/svg-400/outlined/mail.svg";
import Password from "@material-symbols/svg-400/outlined/password.svg"; import Password from "@material-symbols/svg-400/outlined/password.svg";
import { useMutation, useQueryClient } from "@tanstack/react-query";
const Preference = ({ const Preference = ({
icon, icon,
@ -89,8 +107,13 @@ const SettingsContainer = ({
return ( return (
<Container> <Container>
<H1 {...css({ fontSize: rem(2) })}>{title}</H1> <H1 {...css({ fontSize: rem(2) })}>{title}</H1>
<SwitchVariant>
{({ css }) => (
<View <View
{...css({ bg: (theme) => theme.variant.background, borderRadius: px(imageBorderRadius) })} {...css({
bg: (theme) => theme.background,
borderRadius: px(imageBorderRadius),
})}
> >
{Children.map(children, (x, i) => ( {Children.map(children, (x, i) => (
<> <>
@ -99,10 +122,149 @@ const SettingsContainer = ({
</> </>
))} ))}
</View> </View>
)}
</SwitchVariant>
</Container> </Container>
); );
}; };
const ChangePopup = ({
label,
icon,
inital,
apply,
close,
}: {
label: string;
icon: Icon;
inital: string;
apply: (v: string) => Promise<unknown>;
close: () => void;
}) => {
const { t } = useTranslation();
const [value, setValue] = useState(inital);
return (
<Portal>
<SwitchVariant>
{({ css }) => (
<View
{...css({
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
bg: (theme) => theme.themeOverlay,
})}
>
<Container
{...css({
borderRadius: px(imageBorderRadius),
position: "absolute",
top: percent(35),
padding: ts(4),
gap: ts(2),
bg: (theme) => theme.background,
})}
>
<View {...css({ flexDirection: "row", alignItems: "center", gap: ts(2) })}>
<Icon icon={icon} />
<H1 {...css({ fontSize: rem(2) })}>{label}</H1>
</View>
<Input variant="big" value={value} onChangeText={(v) => setValue(v)} />
<View {...css({ flexDirection: "row", alignSelf: "flex-end", gap: ts(1) })}>
<Button
text={t("misc.cancel")}
onPress={() => close()}
{...css({ minWidth: rem(6) })}
/>
<Button
text={t("misc.edit")}
onPress={async () => {
await apply(value);
close();
}}
{...css({ minWidth: rem(6) })}
/>
</View>
</Container>
</View>
)}
</SwitchVariant>
</Portal>
);
};
const AccountSettings = ({ setPopup }: { setPopup: (e?: ReactElement) => void }) => {
const account = useAccount();
const { css } = useYoshiki();
const { t } = useTranslation();
const queryClient = useQueryClient();
const { mutateAsync } = useMutation({
mutationFn: async (update: Partial<Account>) =>
await queryFn({
path: ["auth", "me"],
method: "PATCH",
body: update,
}),
onSettled: async () => await queryClient.invalidateQueries({ queryKey: ["auth", "me"] }),
});
return (
account && (
<SettingsContainer title={t("settings.account.label")}>
<Preference
icon={Username}
label={t("settings.account.username.label")}
description={account.username}
>
<Button
text={t("misc.edit")}
onPress={() =>
setPopup(
<ChangePopup
icon={Username}
label={t("settings.account.username.label")}
inital={account.username}
apply={async (v) => await mutateAsync({ username: v })}
close={() => setPopup(undefined)}
/>,
)
}
/>
</Preference>
<Preference
icon={Mail}
label={t("settings.account.email.label")}
description={account.email}
>
<Button
text={t("misc.edit")}
onPress={() =>
setPopup(
<ChangePopup
icon={Mail}
label={t("settings.account.email.label")}
inital={account.email}
apply={async (v) => await mutateAsync({ email: v })}
close={() => setPopup(undefined)}
/>,
)
}
/>
</Preference>
<Preference
icon={Password}
label={t("settings.account.password.label")}
description={t("settings.account.password.description")}
></Preference>
</SettingsContainer>
)
);
};
const query: QueryIdentifier<User> = { const query: QueryIdentifier<User> = {
parser: UserP, parser: UserP,
path: ["auth", "me"], path: ["auth", "me"],
@ -110,10 +272,11 @@ const query: QueryIdentifier<User> = {
export const SettingsPage: QueryPage = () => { export const SettingsPage: QueryPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const [popup, setPopup] = useState<ReactElement | undefined>(undefined);
const theme = useUserTheme("auto"); const theme = useUserTheme("auto");
const account = useAccount();
return ( return (
<>
<ScrollView contentContainerStyle={{ gap: ts(4) }}> <ScrollView contentContainerStyle={{ gap: ts(4) }}>
<SettingsContainer title={t("settings.general.label")}> <SettingsContainer title={t("settings.general.label")}>
<Preference <Preference
@ -130,26 +293,10 @@ export const SettingsPage: QueryPage = () => {
/> />
</Preference> </Preference>
</SettingsContainer> </SettingsContainer>
{account && ( <AccountSettings setPopup={setPopup} />
<SettingsContainer title={t("settings.account.label")}>
<Preference
icon={Username}
label={t("settings.account.username.label")}
description={account.username}
></Preference>
<Preference
icon={Mail}
label={t("settings.account.email.label")}
description={account.email}
></Preference>
<Preference
icon={Password}
label={t("settings.account.password.label")}
description={t("settings.account.password.description")}
></Preference>
</SettingsContainer>
)}
</ScrollView> </ScrollView>
{popup}
</>
); );
}; };

View File

@ -66,7 +66,8 @@
"cancel": "Cancel", "cancel": "Cancel",
"more": "More", "more": "More",
"expand": "Expand", "expand": "Expand",
"collapse": "Collapse" "collapse": "Collapse",
"edit": "Edit"
}, },
"navbar": { "navbar": {
"home": "Home", "home": "Home",

View File

@ -66,7 +66,8 @@
"cancel": "Annuler", "cancel": "Annuler",
"more": "Plus", "more": "Plus",
"expand": "Développer", "expand": "Développer",
"collapse": "Replier" "collapse": "Replier",
"edit": "Changer"
}, },
"navbar": { "navbar": {
"home": "Accueil", "home": "Accueil",

View File

@ -2950,6 +2950,7 @@ __metadata:
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "@kyoo/ui@workspace:packages/ui" resolution: "@kyoo/ui@workspace:packages/ui"
dependencies: dependencies:
"@gorhom/portal": ^1.0.14
"@kesha-antonov/react-native-background-downloader": ^2.10.0 "@kesha-antonov/react-native-background-downloader": ^2.10.0
"@kyoo/models": "workspace:^" "@kyoo/models": "workspace:^"
"@kyoo/primitives": "workspace:^" "@kyoo/primitives": "workspace:^"
@ -2960,6 +2961,7 @@ __metadata:
react-native-uuid: ^2.0.1 react-native-uuid: ^2.0.1
typescript: ^5.3.2 typescript: ^5.3.2
peerDependencies: peerDependencies:
"@gorhom/portal": "*"
"@kesha-antonov/react-native-background-downloader": "*" "@kesha-antonov/react-native-background-downloader": "*"
"@material-symbols/svg-400": "*" "@material-symbols/svg-400": "*"
"@shopify/flash-list": ^1.3.1 "@shopify/flash-list": ^1.3.1
@ -15330,6 +15332,7 @@ __metadata:
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "web@workspace:apps/web" resolution: "web@workspace:apps/web"
dependencies: dependencies:
"@gorhom/portal": ^1.0.14
"@kyoo/models": "workspace:^" "@kyoo/models": "workspace:^"
"@kyoo/primitives": "workspace:^" "@kyoo/primitives": "workspace:^"
"@kyoo/ui": "workspace:^" "@kyoo/ui": "workspace:^"