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' ."
|
"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:^",
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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">
|
||||||
|
@ -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",
|
||||||
|
@ -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}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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:^"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user