Feat: update user profile (#491)
* add update profile api * may update email * may update username
This commit is contained in:
128
enjoy/src/renderer/components/preferences/email-settings.tsx
Normal file
128
enjoy/src/renderer/components/preferences/email-settings.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user