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,21 +137,23 @@ 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" }}>
<GlobalCssTheme /> <PortalProvider>
<Layout <GlobalCssTheme />
page={ <Layout
<Component page={
randomItems={ <Component
randomItems[Component.displayName!] ?? randomItems={
arrayShuffle((Component as QueryPage).randomItems ?? []) randomItems[Component.displayName!] ??
} arrayShuffle((Component as QueryPage).randomItems ?? [])
{...props} }
/> {...props}
} />
randomItems={[]} }
{...layoutProps} randomItems={[]}
/> {...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

@ -53,8 +53,16 @@ export const Button = forwardRef<
props as any, props as any,
)} )}
> >
<P {...css({ textAlign: "center" }, "text")}>{text}</P> <View
{icon} {...css({
paddingX: ts(3),
flexDirection: "row",
alignItems: "center",
})}
>
<P {...css({ textAlign: "center" }, "text")}>{text}</P>
{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,
@ -68,10 +69,18 @@ export const Select = ({
}, },
})} })}
> >
<P {...css({ textAlign: "center" }, "text")}>{<RSelect.Value />}</P> <View
<RSelect.Icon asChild> {...css({
<Icon icon={ExpandMore} /> paddingX: ts(3),
</RSelect.Icon> flexDirection: "row",
alignItems: "center",
})}
>
<P {...css({ textAlign: "center" }, "text")}>{<RSelect.Value />}</P>
<RSelect.Icon asChild>
<Icon icon={ExpandMore} />
</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,20 +107,164 @@ const SettingsContainer = ({
return ( return (
<Container> <Container>
<H1 {...css({ fontSize: rem(2) })}>{title}</H1> <H1 {...css({ fontSize: rem(2) })}>{title}</H1>
<View <SwitchVariant>
{...css({ bg: (theme) => theme.variant.background, borderRadius: px(imageBorderRadius) })} {({ css }) => (
> <View
{Children.map(children, (x, i) => ( {...css({
<> bg: (theme) => theme.background,
{i !== 0 && <HR {...css({ marginY: ts(1) })} />} borderRadius: px(imageBorderRadius),
{x} })}
</> >
))} {Children.map(children, (x, i) => (
</View> <>
{i !== 0 && <HR {...css({ marginY: ts(1) })} />}
{x}
</>
))}
</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,46 +272,31 @@ 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) }}> <>
<SettingsContainer title={t("settings.general.label")}> <ScrollView contentContainerStyle={{ gap: ts(4) }}>
<Preference <SettingsContainer title={t("settings.general.label")}>
icon={Theme} <Preference
label={t("settings.general.theme.label")} icon={Theme}
description={t("settings.general.theme.description")}
>
<Select
label={t("settings.general.theme.label")} label={t("settings.general.theme.label")}
value={theme} description={t("settings.general.theme.description")}
onValueChange={(value) => setUserTheme(value)} >
values={["auto", "light", "dark"]} <Select
getLabel={(key) => t(`settings.general.theme.${key}`)} label={t("settings.general.theme.label")}
/> value={theme}
</Preference> onValueChange={(value) => setUserTheme(value)}
</SettingsContainer> values={["auto", "light", "dark"]}
{account && ( getLabel={(key) => t(`settings.general.theme.${key}`)}
<SettingsContainer title={t("settings.account.label")}> />
<Preference </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> </SettingsContainer>
)} <AccountSettings setPopup={setPopup} />
</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:^"