Feat: update user profile (#491)

* add update profile api

* may update email

* may update username
This commit is contained in:
an-lee
2024-04-07 14:38:43 +08:00
committed by GitHub
parent 52287357d5
commit aa334dfb09
7 changed files with 263 additions and 50 deletions

View File

@@ -0,0 +1,128 @@
import { t } from "i18next";
import {
Button,
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogTitle,
Label,
Input,
toast,
} from "@renderer/components/ui";
import { AppSettingsProviderContext } from "@renderer/context";
import { useContext, useEffect, useState } from "react";
export const EmailSettings = () => {
const { user, login, webApi } = useContext(AppSettingsProviderContext);
const [editing, setEditing] = useState(false);
const [email, setEmail] = useState(user.email);
const [code, setCode] = useState('');
const [codeSent, setCodeSent] = useState<boolean>(false);
const [countdown, setCountdown] = useState<number>(0);
const refreshProfile = () => {
webApi.me().then((profile: UserType) => {
login(Object.assign({}, user, profile));
});
};
useEffect(() => {
let timeout: NodeJS.Timeout;
if (countdown > 0) {
timeout = setTimeout(() => {
setCountdown(countdown - 1);
}, 1000);
}
return () => {
if (timeout) clearTimeout(timeout);
}
}, [countdown]);
if (!user) return null;
return (
<div className="flex items-start justify-between py-4">
<div className="">
<div className="mb-2">{t("email")}</div>
<div className="text-sm text-muted-foreground mb-2">{user.email || '-'}</div>
</div>
<Dialog open={editing} onOpenChange={(value) => setEditing(value)}>
<DialogTrigger asChild>
<Button variant="secondary" size="sm">
{t('edit')}
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>
{t('editEmail')}
</DialogTitle>
</DialogHeader>
<div className="w-full max-w-sm mx-auto py-6">
<div className="grid gap-4 mb-6">
<div className="grid gap-2">
<Label htmlFor="email">{t('email')}</Label>
<Input
id="email"
required
value={email}
onChange={e => setEmail(e.target.value)}
placeholder="m@example.com" />
</div>
<div className="grid gap-2">
<Label htmlFor="code">{t('verificationCode')}</Label>
<Input
id="code"
required
value={code}
onChange={e => setCode(e.target.value)}
placeholder={t('verificationCode')}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<Button variant="secondary" disabled={!email} onClick={() => {
webApi
.loginCode({ email })
.then(() => {
toast.success(t("codeSent"));
setCodeSent(true);
setCountdown(120);
})
.catch((err) => {
toast.error(err.message);
});
}}>
{countdown > 0 && <span className="mr-2">{countdown}</span>}
<span>{codeSent ? t("resend") : t("sendCode")}</span>
</Button>
<Button disabled={!code} onClick={() => {
webApi.updateProfile(user.id, {
email,
code
})
.then(() => {
toast.success(t("emailUpdated"));
refreshProfile();
setEditing(false);
})
.catch((err) => {
toast.error(err.message);
});
}}>{t("confirm")}</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
);
};

View File

@@ -10,6 +10,7 @@ export * from "./whisper-settings";
export * from "./google-generative-ai-settings";
export * from "./user-settings";
export * from "./email-settings";
export * from "./balance-settings";
export * from "./reset-settings";

View File

@@ -17,6 +17,7 @@ import {
} from "@renderer/components";
import { useState } from "react";
import { Tooltip } from "react-tooltip";
import { EmailSettings } from "./email-settings";
export const Preferences = () => {
const TABS = [
@@ -68,6 +69,8 @@ export const Preferences = () => {
</div>
<UserSettings />
<Separator />
<EmailSettings />
<Separator />
<BalanceSettings />
<Separator />
<LanguageSettings />
@@ -102,9 +105,8 @@ export const Preferences = () => {
key={tab.value}
variant={activeTab === tab.value ? "default" : "ghost"}
size="sm"
className={`capitilized w-full justify-start mb-2 ${
activeTab === tab.value ? "" : "hover:bg-muted"
}`}
className={`capitilized w-full justify-start mb-2 ${activeTab === tab.value ? "" : "hover:bg-muted"
}`}
onClick={() => setActiveTab(tab.value)}
>
<span className="text-sm">{tab.label}</span>

View File

@@ -13,60 +13,118 @@ import {
AvatarImage,
AvatarFallback,
Button,
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogTitle,
Label,
Input,
toast,
} from "@renderer/components/ui";
import { AppSettingsProviderContext } from "@renderer/context";
import { useContext } from "react";
import { useContext, useState } from "react";
import { redirect } from "react-router-dom";
export const UserSettings = () => {
const { user, logout } = useContext(AppSettingsProviderContext);
const { user, login, logout, webApi } = useContext(AppSettingsProviderContext);
const [name, setName] = useState(user.name);
const [editing, setEditing] = useState(false);
const refreshProfile = () => {
webApi.me().then((profile: UserType) => {
login(Object.assign({}, user, profile));
});
};
if (!user) return null;
return (
<div className="flex items-start justify-between py-4">
<div className="">
<div className="flex items-center space-x-2">
<Avatar>
<AvatarImage crossOrigin="anonymous" src={user.avatarUrl} />
<AvatarFallback className="text-xl">
{user.name[0].toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="">
<div className="text-sm font-semibold">{user.name}</div>
<div className="text-xs text-muted-foreground">{user.id}</div>
<>
<div className="flex items-start justify-between py-4">
<div className="">
<div className="flex items-center space-x-2">
<Avatar>
<AvatarImage crossOrigin="anonymous" src={user.avatarUrl} />
<AvatarFallback className="text-xl">
{user.name[0].toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="">
<div className="text-sm font-semibold">{user.name}</div>
<div className="text-xs text-muted-foreground">{user.id}</div>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-2">
<Dialog open={editing} onOpenChange={(value) => setEditing(value)}>
<DialogTrigger asChild>
<Button variant="secondary" size="sm">
{t("edit")}
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('updateUserName')}</DialogTitle>
</DialogHeader>
<div className="w-full max-w-sm mx-auto py-6">
<div className="grid gap-2 mb-6">
<Label htmlFor="name">{t('userName')}</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div className="">
<Button className="w-full" onClick={() => {
webApi
.updateProfile(user.id, { name })
.then(() => {
toast.success('profileUpdated')
setEditing(false);
refreshProfile();
}).catch(err => {
toast.error(err.message);
})
}}>{t('save')}</Button>
</div>
</div>
</DialogContent>
</Dialog>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="secondary" className="text-destructive" size="sm">
{t("logout")}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t("logout")}</AlertDialogTitle>
</AlertDialogHeader>
<AlertDialogDescription>
{t("logoutConfirmation")}
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
<AlertDialogAction
className="bg-destructive hover:bg-destructive-hover"
onClick={() => {
logout();
redirect("/");
}}
>
{t("logout")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="secondary" className="text-destructive" size="sm">
{t("logout")}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t("logout")}</AlertDialogTitle>
</AlertDialogHeader>
<AlertDialogDescription>
{t("logoutConfirmation")}
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
<AlertDialogAction
className="bg-destructive hover:bg-destructive-hover"
onClick={() => {
logout();
redirect("/");
}}
>
{t("logout")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</>
);
};