mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Allow username and email to be changed
This commit is contained in:
parent
08d9b9d950
commit
65a254d808
@ -14,6 +14,7 @@
|
||||
"format:fix": "prettier --write --ignore-path .gitignore '!src/utils/jotai-utils.tsx' ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@gorhom/portal": "^1.0.14",
|
||||
"@kyoo/models": "workspace:^",
|
||||
"@kyoo/primitives": "workspace:^",
|
||||
"@kyoo/ui": "workspace:^",
|
||||
|
@ -44,6 +44,7 @@ import { withTranslations } from "../i18n";
|
||||
import arrayShuffle from "array-shuffle";
|
||||
import { Tooltip } from "react-tooltip";
|
||||
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" });
|
||||
|
||||
@ -136,21 +137,23 @@ const App = ({ Component, pageProps }: AppProps) => {
|
||||
<AccountProvider ssrAccount={account}>
|
||||
<HydrationBoundary state={queryState}>
|
||||
<ThemeSelector theme={userTheme} font={{ normal: "inherit" }}>
|
||||
<GlobalCssTheme />
|
||||
<Layout
|
||||
page={
|
||||
<Component
|
||||
randomItems={
|
||||
randomItems[Component.displayName!] ??
|
||||
arrayShuffle((Component as QueryPage).randomItems ?? [])
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
randomItems={[]}
|
||||
{...layoutProps}
|
||||
/>
|
||||
<Tooltip id="tooltip" positionStrategy={"fixed"} />
|
||||
<PortalProvider>
|
||||
<GlobalCssTheme />
|
||||
<Layout
|
||||
page={
|
||||
<Component
|
||||
randomItems={
|
||||
randomItems[Component.displayName!] ??
|
||||
arrayShuffle((Component as QueryPage).randomItems ?? [])
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
randomItems={[]}
|
||||
{...layoutProps}
|
||||
/>
|
||||
<Tooltip id="tooltip" positionStrategy={"fixed"} />
|
||||
</PortalProvider>
|
||||
</ThemeSelector>
|
||||
</HydrationBoundary>
|
||||
</AccountProvider>
|
||||
|
@ -45,7 +45,7 @@ export const queryFn = async <Data,>(
|
||||
| {
|
||||
path: (string | false | undefined | null)[];
|
||||
body?: object;
|
||||
method: "GET" | "POST" | "DELETE";
|
||||
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
||||
authenticated?: boolean;
|
||||
apiUrl?: string;
|
||||
timeout?: number;
|
||||
|
@ -53,8 +53,16 @@ export const Button = forwardRef<
|
||||
props as any,
|
||||
)}
|
||||
>
|
||||
<P {...css({ textAlign: "center" }, "text")}>{text}</P>
|
||||
{icon}
|
||||
<View
|
||||
{...css({
|
||||
paddingX: ts(3),
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
})}
|
||||
>
|
||||
<P {...css({ textAlign: "center" }, "text")}>{text}</P>
|
||||
{icon}
|
||||
</View>
|
||||
</PressableFeedback>
|
||||
);
|
||||
});
|
||||
|
@ -31,6 +31,7 @@ import { useYoshiki } from "yoshiki";
|
||||
import { PressableFeedback } from "./links";
|
||||
import { P } from "./text";
|
||||
import { focusReset, ts } from "./utils";
|
||||
import { View } from "react-native";
|
||||
|
||||
export const Select = ({
|
||||
label,
|
||||
@ -68,10 +69,18 @@ export const Select = ({
|
||||
},
|
||||
})}
|
||||
>
|
||||
<P {...css({ textAlign: "center" }, "text")}>{<RSelect.Value />}</P>
|
||||
<RSelect.Icon asChild>
|
||||
<Icon icon={ExpandMore} />
|
||||
</RSelect.Icon>
|
||||
<View
|
||||
{...css({
|
||||
paddingX: ts(3),
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
})}
|
||||
>
|
||||
<P {...css({ textAlign: "center" }, "text")}>{<RSelect.Value />}</P>
|
||||
<RSelect.Icon asChild>
|
||||
<Icon icon={ExpandMore} />
|
||||
</RSelect.Icon>
|
||||
</View>
|
||||
</InternalTriger>
|
||||
</RSelect.Trigger>
|
||||
<ContrastArea mode="user">
|
||||
|
@ -8,12 +8,14 @@
|
||||
"@kyoo/primitives": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@gorhom/portal": "^1.0.14",
|
||||
"@shopify/flash-list": "^1.6.3",
|
||||
"@types/react": "18.2.39",
|
||||
"react-native-uuid": "^2.0.1",
|
||||
"typescript": "^5.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@gorhom/portal": "*",
|
||||
"@kesha-antonov/react-native-background-downloader": "*",
|
||||
"@material-symbols/svg-400": "*",
|
||||
"@shopify/flash-list": "^1.3.1",
|
||||
|
@ -19,25 +19,43 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
Account,
|
||||
MutationParam,
|
||||
QueryIdentifier,
|
||||
QueryPage,
|
||||
User,
|
||||
UserP,
|
||||
queryFn,
|
||||
setUserTheme,
|
||||
useAccount,
|
||||
useUserTheme,
|
||||
} 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 { Children, ReactElement, ReactNode } from "react";
|
||||
import { Children, ReactElement, ReactNode, useState, useTransition } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
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 Username from "@material-symbols/svg-400/outlined/badge.svg";
|
||||
import Mail from "@material-symbols/svg-400/outlined/mail.svg";
|
||||
import Password from "@material-symbols/svg-400/outlined/password.svg";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
const Preference = ({
|
||||
icon,
|
||||
@ -89,20 +107,164 @@ const SettingsContainer = ({
|
||||
return (
|
||||
<Container>
|
||||
<H1 {...css({ fontSize: rem(2) })}>{title}</H1>
|
||||
<View
|
||||
{...css({ bg: (theme) => theme.variant.background, borderRadius: px(imageBorderRadius) })}
|
||||
>
|
||||
{Children.map(children, (x, i) => (
|
||||
<>
|
||||
{i !== 0 && <HR {...css({ marginY: ts(1) })} />}
|
||||
{x}
|
||||
</>
|
||||
))}
|
||||
</View>
|
||||
<SwitchVariant>
|
||||
{({ css }) => (
|
||||
<View
|
||||
{...css({
|
||||
bg: (theme) => theme.background,
|
||||
borderRadius: px(imageBorderRadius),
|
||||
})}
|
||||
>
|
||||
{Children.map(children, (x, i) => (
|
||||
<>
|
||||
{i !== 0 && <HR {...css({ marginY: ts(1) })} />}
|
||||
{x}
|
||||
</>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</SwitchVariant>
|
||||
</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> = {
|
||||
parser: UserP,
|
||||
path: ["auth", "me"],
|
||||
@ -110,46 +272,31 @@ const query: QueryIdentifier<User> = {
|
||||
|
||||
export const SettingsPage: QueryPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const [popup, setPopup] = useState<ReactElement | undefined>(undefined);
|
||||
|
||||
const theme = useUserTheme("auto");
|
||||
const account = useAccount();
|
||||
return (
|
||||
<ScrollView contentContainerStyle={{ gap: ts(4) }}>
|
||||
<SettingsContainer title={t("settings.general.label")}>
|
||||
<Preference
|
||||
icon={Theme}
|
||||
label={t("settings.general.theme.label")}
|
||||
description={t("settings.general.theme.description")}
|
||||
>
|
||||
<Select
|
||||
<>
|
||||
<ScrollView contentContainerStyle={{ gap: ts(4) }}>
|
||||
<SettingsContainer title={t("settings.general.label")}>
|
||||
<Preference
|
||||
icon={Theme}
|
||||
label={t("settings.general.theme.label")}
|
||||
value={theme}
|
||||
onValueChange={(value) => setUserTheme(value)}
|
||||
values={["auto", "light", "dark"]}
|
||||
getLabel={(key) => t(`settings.general.theme.${key}`)}
|
||||
/>
|
||||
</Preference>
|
||||
</SettingsContainer>
|
||||
{account && (
|
||||
<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>
|
||||
description={t("settings.general.theme.description")}
|
||||
>
|
||||
<Select
|
||||
label={t("settings.general.theme.label")}
|
||||
value={theme}
|
||||
onValueChange={(value) => setUserTheme(value)}
|
||||
values={["auto", "light", "dark"]}
|
||||
getLabel={(key) => t(`settings.general.theme.${key}`)}
|
||||
/>
|
||||
</Preference>
|
||||
</SettingsContainer>
|
||||
)}
|
||||
</ScrollView>
|
||||
<AccountSettings setPopup={setPopup} />
|
||||
</ScrollView>
|
||||
{popup}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -66,7 +66,8 @@
|
||||
"cancel": "Cancel",
|
||||
"more": "More",
|
||||
"expand": "Expand",
|
||||
"collapse": "Collapse"
|
||||
"collapse": "Collapse",
|
||||
"edit": "Edit"
|
||||
},
|
||||
"navbar": {
|
||||
"home": "Home",
|
||||
|
@ -66,7 +66,8 @@
|
||||
"cancel": "Annuler",
|
||||
"more": "Plus",
|
||||
"expand": "Développer",
|
||||
"collapse": "Replier"
|
||||
"collapse": "Replier",
|
||||
"edit": "Changer"
|
||||
},
|
||||
"navbar": {
|
||||
"home": "Accueil",
|
||||
|
@ -2950,6 +2950,7 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@kyoo/ui@workspace:packages/ui"
|
||||
dependencies:
|
||||
"@gorhom/portal": ^1.0.14
|
||||
"@kesha-antonov/react-native-background-downloader": ^2.10.0
|
||||
"@kyoo/models": "workspace:^"
|
||||
"@kyoo/primitives": "workspace:^"
|
||||
@ -2960,6 +2961,7 @@ __metadata:
|
||||
react-native-uuid: ^2.0.1
|
||||
typescript: ^5.3.2
|
||||
peerDependencies:
|
||||
"@gorhom/portal": "*"
|
||||
"@kesha-antonov/react-native-background-downloader": "*"
|
||||
"@material-symbols/svg-400": "*"
|
||||
"@shopify/flash-list": ^1.3.1
|
||||
@ -15330,6 +15332,7 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "web@workspace:apps/web"
|
||||
dependencies:
|
||||
"@gorhom/portal": ^1.0.14
|
||||
"@kyoo/models": "workspace:^"
|
||||
"@kyoo/primitives": "workspace:^"
|
||||
"@kyoo/ui": "workspace:^"
|
||||
|
Loading…
x
Reference in New Issue
Block a user