✨ feat(profile): Update localization strings and enhance third-party account binding
This commit is contained in:
@@ -11,77 +11,67 @@ import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
const FormSchema = z
|
||||
.object({
|
||||
password: z.string().min(6),
|
||||
repeat_password: z.string(),
|
||||
})
|
||||
.refine((data) => data.password === data.repeat_password, {
|
||||
message: 'passwordMismatch',
|
||||
path: ['repeat_password'],
|
||||
});
|
||||
|
||||
export default function ChangePassword() {
|
||||
const t = useTranslations('profile.accountSettings');
|
||||
|
||||
const FormSchema = z
|
||||
.object({
|
||||
password: z.string(),
|
||||
repeat_password: z.string(),
|
||||
})
|
||||
.superRefine(({ password, repeat_password }, ctx) => {
|
||||
if (password !== repeat_password) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: t('passwordMismatch'),
|
||||
path: ['repeat_password'],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
});
|
||||
|
||||
async function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||
await updateUserPassword({
|
||||
password: data.password,
|
||||
} as API.UpdateUserPasswordRequest);
|
||||
await updateUserPassword({ password: data.password });
|
||||
toast.success(t('updateSuccess'));
|
||||
form.setValue('password', '');
|
||||
form.setValue('repeat_password', '');
|
||||
form.reset();
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className='bg-muted/50 flex flex-row items-start'>
|
||||
<CardTitle>{t('accountSettings')}</CardTitle>
|
||||
<CardHeader className='bg-muted/50'>
|
||||
<CardTitle className='flex items-center justify-between'>
|
||||
{t('accountSettings')}
|
||||
<Button type='submit' size='sm' form='password-form'>
|
||||
{t('updatePassword')}
|
||||
</Button>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className='grid gap-4 p-6 text-sm'>
|
||||
<div className='grid gap-3'>
|
||||
<div className='font-semibold'>{t('loginPassword')}</div>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className='grid gap-6'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='password'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input type='password' placeholder={t('newPassword')} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='repeat_password'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input type='password' placeholder={t('repeatNewPassword')} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button className='size-full' type='submit'>
|
||||
{t('updatePassword')}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
<CardContent className='p-6'>
|
||||
<Form {...form}>
|
||||
<form id='password-form' onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='password'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input type='password' placeholder={t('newPassword')} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='repeat_password'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input type='password' placeholder={t('repeatNewPassword')} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import useGlobalStore from '@/config/use-global';
|
||||
import { updateUserNotify } from '@/services/user/user';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@workspace/ui/components/form';
|
||||
import { Switch } from '@workspace/ui/components/switch';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
const FormSchema = z.object({
|
||||
enable_balance_notify: z.boolean(),
|
||||
enable_login_notify: z.boolean(),
|
||||
enable_subscribe_notify: z.boolean(),
|
||||
enable_trade_notify: z.boolean(),
|
||||
});
|
||||
|
||||
export default function NotifyEvent() {
|
||||
const t = useTranslations('profile.notifyEvent');
|
||||
const { user, getUserInfo } = useGlobalStore();
|
||||
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: {
|
||||
enable_balance_notify: user?.enable_balance_notify,
|
||||
enable_login_notify: user?.enable_login_notify,
|
||||
enable_subscribe_notify: user?.enable_subscribe_notify,
|
||||
enable_trade_notify: user?.enable_trade_notify,
|
||||
},
|
||||
});
|
||||
|
||||
async function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||
await updateUserNotify(data);
|
||||
toast.success(t('updateSuccess'));
|
||||
getUserInfo();
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className='bg-muted/50 flex flex-row items-start'>
|
||||
<CardTitle>{t('notificationEvents')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className='grid gap-4 p-6 text-sm'>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className='grid gap-6'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='enable_balance_notify'
|
||||
render={({ field }) => (
|
||||
<FormItem className='text-muted-foreground flex items-center justify-between'>
|
||||
<FormLabel>{t('balanceChange')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={(value) => {
|
||||
field.onChange(value);
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='enable_login_notify'
|
||||
render={({ field }) => (
|
||||
<FormItem className='text-muted-foreground flex items-center justify-between'>
|
||||
<FormLabel>{t('login')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={(value) => {
|
||||
field.onChange(value);
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='enable_subscribe_notify'
|
||||
render={({ field }) => (
|
||||
<FormItem className='text-muted-foreground flex items-center justify-between'>
|
||||
<FormLabel>{t('subscribe')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={(value) => {
|
||||
field.onChange(value);
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='enable_trade_notify'
|
||||
render={({ field }) => (
|
||||
<FormItem className='text-muted-foreground flex items-center justify-between'>
|
||||
<FormLabel>{t('finance')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={(value) => {
|
||||
field.onChange(value);
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import useGlobalStore from '@/config/use-global';
|
||||
import { bindTelegram, unbindTelegram, updateUserNotifySetting } from '@/services/user/user';
|
||||
import { updateUserNotify } from '@/services/user/user';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@workspace/ui/components/form';
|
||||
import { Input } from '@workspace/ui/components/input';
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel } from '@workspace/ui/components/form';
|
||||
import { Switch } from '@workspace/ui/components/switch';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useForm } from 'react-hook-form';
|
||||
@@ -24,10 +16,14 @@ const FormSchema = z.object({
|
||||
telegram: z.number().nullish(),
|
||||
enable_email_notify: z.boolean(),
|
||||
enable_telegram_notify: z.boolean(),
|
||||
enable_balance_notify: z.boolean(),
|
||||
enable_login_notify: z.boolean(),
|
||||
enable_subscribe_notify: z.boolean(),
|
||||
enable_trade_notify: z.boolean(),
|
||||
});
|
||||
|
||||
export default function NotifySettings() {
|
||||
const t = useTranslations('profile.notify');
|
||||
const t = useTranslations('profile');
|
||||
const { user, getUserInfo } = useGlobalStore();
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
@@ -35,101 +31,58 @@ export default function NotifySettings() {
|
||||
telegram: user?.telegram,
|
||||
enable_email_notify: user?.enable_email_notify,
|
||||
enable_telegram_notify: user?.enable_telegram_notify,
|
||||
enable_balance_notify: user?.enable_balance_notify,
|
||||
enable_login_notify: user?.enable_login_notify,
|
||||
enable_subscribe_notify: user?.enable_subscribe_notify,
|
||||
enable_trade_notify: user?.enable_trade_notify,
|
||||
},
|
||||
});
|
||||
|
||||
async function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||
await updateUserNotifySetting(data as API.UpdateUserNotifySettingRequet);
|
||||
toast.success(t('updateSuccess'));
|
||||
await updateUserNotify(data);
|
||||
toast.success(t('notify.updateSuccess'));
|
||||
getUserInfo();
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className='bg-muted/50 flex flex-row items-start'>
|
||||
<CardTitle>{t('notificationSettings')}</CardTitle>
|
||||
<CardHeader className='bg-muted/50'>
|
||||
<CardTitle className='flex items-center justify-between'>
|
||||
{t('notify.notificationSettings')}
|
||||
<Button type='submit' size='sm' form='notify-form'>
|
||||
{t('notify.save')}
|
||||
</Button>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className='grid gap-4 p-6 text-sm'>
|
||||
<CardContent className='grid gap-6 p-6'>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className='grid gap-6'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='telegram'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('telegramId')}</FormLabel>
|
||||
<FormControl>
|
||||
<div className='flex w-full items-center space-x-2'>
|
||||
<Input
|
||||
type='number'
|
||||
placeholder={t('telegramIdPlaceholder')}
|
||||
{...field}
|
||||
value={field.value ? field.value : ''}
|
||||
onChange={(e) => {
|
||||
field.onChange(e.target.value ? Number(e.target.value) : '');
|
||||
}}
|
||||
disabled
|
||||
/>
|
||||
<Button
|
||||
size='sm'
|
||||
type='button'
|
||||
onClick={async () => {
|
||||
if (user?.telegram) {
|
||||
await unbindTelegram();
|
||||
await getUserInfo();
|
||||
} else {
|
||||
const { data } = await bindTelegram();
|
||||
if (data.data?.url) {
|
||||
window.open(data.data.url, '_blank');
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t(user?.telegram ? 'unbind' : 'bind')}
|
||||
</Button>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='enable_email_notify'
|
||||
render={({ field }) => (
|
||||
<FormItem className='text-muted-foreground flex items-center justify-between'>
|
||||
<FormLabel>{t('emailNotification')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={(value) => {
|
||||
field.onChange(value);
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='enable_telegram_notify'
|
||||
render={({ field }) => (
|
||||
<FormItem className='text-muted-foreground flex items-center justify-between'>
|
||||
<FormLabel>{t('telegramNotification')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={(value) => {
|
||||
field.onChange(value);
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<form id='notify-form' onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'>
|
||||
<div className='space-y-4'>
|
||||
{[
|
||||
{ name: 'enable_email_notify', label: 'emailNotification' },
|
||||
{ name: 'enable_telegram_notify', label: 'telegramNotification' },
|
||||
{ name: 'enable_balance_notify', label: 'balanceChange' },
|
||||
{ name: 'enable_login_notify', label: 'login' },
|
||||
{ name: 'enable_subscribe_notify', label: 'subscribe' },
|
||||
{ name: 'enable_trade_notify', label: 'finance' },
|
||||
].map(({ name, label }) => (
|
||||
<FormField
|
||||
key={name}
|
||||
control={form.control}
|
||||
name={name as any}
|
||||
render={({ field }) => (
|
||||
<FormItem className='flex items-center justify-between space-x-4'>
|
||||
<FormLabel className='text-muted-foreground'>
|
||||
{t(`notify.${label}`)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import ChangePassword from './change-password';
|
||||
import NotifyEvent from './notify-event';
|
||||
import NotifySettings from './notify-settings';
|
||||
import ThirdPartyAccounts from './third-party-accounts';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className='grid gap-4 md:grid-cols-2 lg:grid-cols-3'>
|
||||
<div className='flex flex-col gap-3 lg:flex-row lg:flex-wrap lg:*:flex-auto'>
|
||||
<ThirdPartyAccounts />
|
||||
<NotifySettings />
|
||||
<NotifyEvent />
|
||||
<ChangePassword />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
'use client';
|
||||
|
||||
import SendCode from '@/app/auth/send-code';
|
||||
import useGlobalStore from '@/config/use-global';
|
||||
import { bindOAuth } from '@/services/user/user';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@workspace/ui/components/dialog';
|
||||
import { Form, FormControl, FormField, FormItem, FormMessage } from '@workspace/ui/components/form';
|
||||
import { Input } from '@workspace/ui/components/input';
|
||||
import { AreaCodeSelect } from '@workspace/ui/custom-components/area-code-select';
|
||||
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
function MobileBindDialog({
|
||||
method,
|
||||
|
||||
onSuccess,
|
||||
children,
|
||||
}: {
|
||||
method?: API.UserAuthMethod;
|
||||
onSuccess: () => void;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const t = useTranslations('profile.thirdParty');
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const formSchema = z.object({
|
||||
telephone_area_code: z.string().min(1, 'Area code is required'),
|
||||
telephone: z.string().min(5, 'Phone number is required'),
|
||||
telephone_code: z.string().min(4, 'Verification code is required'),
|
||||
});
|
||||
|
||||
type MobileBindFormValues = z.infer<typeof formSchema>;
|
||||
|
||||
const form = useForm<MobileBindFormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
// @ts-ignore
|
||||
telephone_area_code: method?.area_code || '1',
|
||||
telephone: method?.auth_identifier || '',
|
||||
telephone_code: '',
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (values: MobileBindFormValues) => {
|
||||
try {
|
||||
toast.success(t('bindSuccess'));
|
||||
onSuccess();
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
toast.error(t('bindFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger>{children}</DialogTrigger>
|
||||
<DialogContent className='sm:max-w-[425px]'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('bindMobile')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='telephone'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<div className='flex'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='telephone_area_code'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<AreaCodeSelect
|
||||
simple
|
||||
className='w-32 rounded-r-none border-r-0'
|
||||
placeholder='Area code...'
|
||||
value={field.value}
|
||||
onChange={(value) => {
|
||||
if (value.phone) {
|
||||
form.setValue('telephone_area_code', value.phone);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Input
|
||||
className='rounded-l-none'
|
||||
placeholder='Enter your telephone...'
|
||||
type='tel'
|
||||
{...field}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='telephone_code'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<div className='flex gap-2'>
|
||||
<Input placeholder='Enter code...' type='text' {...field} />
|
||||
<SendCode type='phone' params={form.getValues()} />
|
||||
</div>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type='submit' className='w-full'>
|
||||
{t('confirm')}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ThirdPartyAccounts() {
|
||||
const t = useTranslations('profile.thirdParty');
|
||||
const { user, getUserInfo, common } = useGlobalStore();
|
||||
const { oauth_methods } = common;
|
||||
|
||||
const accounts = [
|
||||
{
|
||||
id: 'email',
|
||||
icon: 'logos:mailgun-icon',
|
||||
name: 'Email',
|
||||
type: 'Basic',
|
||||
},
|
||||
{
|
||||
id: 'mobile',
|
||||
icon: 'mdi:telephone',
|
||||
name: 'Mobile',
|
||||
type: 'Basic',
|
||||
},
|
||||
{
|
||||
id: 'telegram',
|
||||
icon: 'logos:telegram',
|
||||
name: 'Telegram',
|
||||
type: 'OAuth',
|
||||
},
|
||||
{
|
||||
id: 'apple',
|
||||
icon: 'uil:apple',
|
||||
name: 'Apple',
|
||||
type: 'OAuth',
|
||||
},
|
||||
{
|
||||
id: 'google',
|
||||
icon: 'logos:google',
|
||||
name: 'Google',
|
||||
type: 'OAuth',
|
||||
},
|
||||
{
|
||||
id: 'facebook',
|
||||
icon: 'logos:facebook',
|
||||
name: 'Facebook',
|
||||
type: 'OAuth',
|
||||
},
|
||||
{
|
||||
id: 'github',
|
||||
icon: 'uil:github',
|
||||
name: 'GitHub',
|
||||
type: 'OAuth',
|
||||
},
|
||||
];
|
||||
// .filter((account) => oauth_methods?.includes(account.id));
|
||||
|
||||
const [editValues, setEditValues] = useState<Record<string, any>>({});
|
||||
|
||||
const handleBasicAccountUpdate = async (account: (typeof accounts)[0], value: string) => {
|
||||
if (account.id === 'email') {
|
||||
// TODO: Create a new email auth or update the existing one
|
||||
await getUserInfo();
|
||||
toast.success(t('updateSuccess'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleAccountAction = async (account: (typeof accounts)[number]) => {
|
||||
const isBound = user?.auth_methods?.find(
|
||||
(auth) => auth.auth_type === account.id,
|
||||
)?.auth_identifier;
|
||||
if (isBound) {
|
||||
// unbindOAuth
|
||||
// await unbindOAuth(account.id);
|
||||
await getUserInfo();
|
||||
} else {
|
||||
const res = await bindOAuth({
|
||||
method: account.id,
|
||||
redirect: `${window.location.origin}/bind/${account.id}`,
|
||||
});
|
||||
if (res.data?.data?.url) {
|
||||
window.location.href = res.data.data.url;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
<CardHeader className='bg-muted/50'>
|
||||
<CardTitle>{t('title')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className='p-6'>
|
||||
<div className='space-y-4'>
|
||||
{accounts.map((account) => {
|
||||
const method = user?.auth_methods?.find((auth) => auth.auth_type === account.id);
|
||||
const isEditing = account.id === 'email';
|
||||
const currentValue = method?.auth_identifier || editValues[account.id];
|
||||
const displayValue = isEditing
|
||||
? currentValue
|
||||
: method?.auth_identifier || t(`${account.id}.description`);
|
||||
|
||||
return (
|
||||
<div key={account.id} className='flex w-full flex-col gap-2'>
|
||||
<span className='flex gap-3 font-medium'>
|
||||
<Icon icon={account.icon} className='size-6' />
|
||||
{account.name}
|
||||
</span>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Input
|
||||
value={displayValue}
|
||||
disabled={!isEditing}
|
||||
className='bg-muted flex-1 truncate'
|
||||
onChange={(e) =>
|
||||
isEditing &&
|
||||
setEditValues((prev) => ({ ...prev, [account.id]: e.target.value }))
|
||||
}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && isEditing) {
|
||||
handleBasicAccountUpdate(account, currentValue);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{account.id === 'mobile' ? (
|
||||
<MobileBindDialog method={method} onSuccess={getUserInfo}>
|
||||
<Button
|
||||
variant={method?.auth_identifier ? 'outline' : 'default'}
|
||||
className='whitespace-nowrap'
|
||||
>
|
||||
{t(method?.auth_identifier ? 'update' : 'bind')}
|
||||
</Button>
|
||||
</MobileBindDialog>
|
||||
) : (
|
||||
<Button
|
||||
variant={method?.auth_identifier ? 'outline' : 'default'}
|
||||
onClick={() =>
|
||||
isEditing
|
||||
? handleBasicAccountUpdate(account, currentValue)
|
||||
: handleAccountAction(account)
|
||||
}
|
||||
className='whitespace-nowrap'
|
||||
>
|
||||
{t(isEditing ? 'save' : method?.auth_identifier ? 'unbind' : 'bind')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+25
-31
@@ -28,27 +28,21 @@ export default function Page() {
|
||||
const { common } = useGlobalStore();
|
||||
const { site, auth, oauth_methods } = common;
|
||||
|
||||
const AUTH_COMPONENT_MAP = {
|
||||
email: <EmailAuthForm />,
|
||||
sms: <PhoneAuthForm />,
|
||||
} as const;
|
||||
const AUTH_METHODS = [
|
||||
{
|
||||
key: 'email',
|
||||
enabled: auth.email.enable,
|
||||
children: <EmailAuthForm />,
|
||||
},
|
||||
{
|
||||
key: 'mobile',
|
||||
enabled: auth.mobile.enable,
|
||||
children: <PhoneAuthForm />,
|
||||
},
|
||||
].filter((method) => method.enabled);
|
||||
|
||||
type AuthMethod = keyof typeof AUTH_COMPONENT_MAP;
|
||||
const OAUTH_METHODS = oauth_methods?.filter((method) => !['mobile', 'email'].includes(method));
|
||||
|
||||
const enabledAuthMethods = (Object.keys(AUTH_COMPONENT_MAP) as AuthMethod[]).filter((key) => {
|
||||
const value = auth[key];
|
||||
const enabledKey = `${key}_enabled` as const;
|
||||
|
||||
if (typeof value !== 'object' || value === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(enabledKey in value)) {
|
||||
return false;
|
||||
}
|
||||
const isEnabled = (value as unknown as Record<typeof enabledKey, boolean>)[enabledKey];
|
||||
return isEnabled;
|
||||
});
|
||||
return (
|
||||
<main className='bg-muted/50 flex h-full min-h-screen items-center'>
|
||||
<div className='flex size-full flex-auto flex-col lg:flex-row'>
|
||||
@@ -79,27 +73,27 @@ export default function Page() {
|
||||
<div className='text-muted-foreground mb-6 text-center font-medium'>
|
||||
{t('verifyAccountDesc')}
|
||||
</div>
|
||||
{enabledAuthMethods.length === 1
|
||||
? AUTH_COMPONENT_MAP[enabledAuthMethods[0] as AuthMethod]
|
||||
: enabledAuthMethods[0] && (
|
||||
<Tabs defaultValue={enabledAuthMethods[0]}>
|
||||
{AUTH_METHODS.length === 1
|
||||
? AUTH_METHODS[0]?.children
|
||||
: AUTH_METHODS[0] && (
|
||||
<Tabs defaultValue={AUTH_METHODS[0].key}>
|
||||
<TabsList className='mb-6 flex w-full *:flex-1'>
|
||||
{enabledAuthMethods.map((method) => (
|
||||
<TabsTrigger key={method} value={method}>
|
||||
{t(`methods.${method}`)}
|
||||
{AUTH_METHODS.map((item) => (
|
||||
<TabsTrigger key={item.key} value={item.key}>
|
||||
{t(`methods.${item.key}`)}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
{enabledAuthMethods.map((method) => (
|
||||
<TabsContent key={method} value={method}>
|
||||
{AUTH_COMPONENT_MAP[method]}
|
||||
{AUTH_METHODS.map((item) => (
|
||||
<TabsContent key={item.key} value={item.key}>
|
||||
{item.children}
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
)}
|
||||
</div>
|
||||
<div className='py-8'>
|
||||
{oauth_methods?.length > 0 && (
|
||||
{OAUTH_METHODS?.length > 0 && (
|
||||
<>
|
||||
<div className='after:border-border relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t'>
|
||||
<span className='bg-background text-muted-foreground relative z-10 px-2'>
|
||||
@@ -107,7 +101,7 @@ export default function Page() {
|
||||
</span>
|
||||
</div>
|
||||
<div className='mt-6 flex justify-center gap-4 *:size-12 *:p-2'>
|
||||
{oauth_methods?.map((method: any) => {
|
||||
{OAUTH_METHODS?.map((method: any) => {
|
||||
return (
|
||||
<Button
|
||||
key={method}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
'use client';
|
||||
|
||||
import { bindOAuthCallback } from '@/services/user/user';
|
||||
import { getAllUrlParams } from '@/utils/common';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
interface CertificationProps {
|
||||
platform: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function Certification({ platform, children }: CertificationProps) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
const searchParams = getAllUrlParams();
|
||||
bindOAuthCallback({
|
||||
method: platform,
|
||||
callback: searchParams,
|
||||
})
|
||||
.then((res) => {
|
||||
router.replace('/profile');
|
||||
router.refresh();
|
||||
})
|
||||
.catch((error) => {
|
||||
router.replace('/auth');
|
||||
});
|
||||
}, [pathname]);
|
||||
|
||||
return children;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import HyperText from '@workspace/ui/components/hyper-text';
|
||||
import { OrbitingCircles } from '@workspace/ui/components/orbiting-circles';
|
||||
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import Certification from './certification';
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return [
|
||||
{
|
||||
platform: 'telegram',
|
||||
},
|
||||
{
|
||||
platform: 'apple',
|
||||
},
|
||||
{
|
||||
platform: 'facebook',
|
||||
},
|
||||
{
|
||||
platform: 'google',
|
||||
},
|
||||
{
|
||||
platform: 'github',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{
|
||||
platform: string;
|
||||
}>;
|
||||
}) {
|
||||
const { platform } = await params;
|
||||
const t = await getTranslations('auth');
|
||||
return (
|
||||
<Certification platform={platform}>
|
||||
<div className='bg-background relative flex h-screen w-full flex-col items-center justify-center overflow-hidden'>
|
||||
<div className='pointer-events-none flex animate-pulse flex-col items-center whitespace-pre-wrap bg-gradient-to-r from-blue-500 via-indigo-500 to-violet-500 bg-clip-text text-center font-black tracking-tight text-transparent dark:from-blue-400 dark:via-indigo-300 dark:to-violet-400'>
|
||||
<HyperText className='text-xl uppercase md:text-2xl'>{platform}</HyperText>
|
||||
<HyperText className='text-lg md:text-xl'>{t('authenticating')}</HyperText>
|
||||
</div>
|
||||
|
||||
<OrbitingCircles iconSize={40} speed={0.8}>
|
||||
<Icon icon='logos:telegram' className='size-12' />
|
||||
<Icon icon='uil:apple' className='size-12' />
|
||||
<Icon icon='logos:google-icon' className='size-12' />
|
||||
<Icon icon='logos:facebook' className='size-12' />
|
||||
<Icon icon='uil:github' className='size-12' />
|
||||
</OrbitingCircles>
|
||||
<OrbitingCircles iconSize={30} radius={100} reverse speed={0.4}>
|
||||
<Icon icon='logos:telegram' className='size-10' />
|
||||
<Icon icon='uil:apple' className='size-10' />
|
||||
<Icon icon='logos:google-icon' className='size-10' />
|
||||
<Icon icon='logos:facebook' className='size-10' />
|
||||
<Icon icon='uil:github' className='size-10' />
|
||||
</OrbitingCircles>
|
||||
</div>
|
||||
</Certification>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user