fix: demo首页
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
'use client';
|
||||
|
||||
import { updateUserPassword } 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, FormMessage } from '@workspace/ui/components/form';
|
||||
import { Input } from '@workspace/ui/components/input';
|
||||
import { useTranslations } from 'next-intl';
|
||||
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 form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
});
|
||||
|
||||
async function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||
await updateUserPassword({ password: data.password });
|
||||
toast.success(t('updateSuccess'));
|
||||
form.reset();
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className='min-w-80'>
|
||||
<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='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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
'use client';
|
||||
|
||||
import useGlobalStore from '@/config/use-global';
|
||||
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 } 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().default(false),
|
||||
enable_login_notify: z.boolean().default(false),
|
||||
enable_subscribe_notify: z.boolean().default(false),
|
||||
enable_trade_notify: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export default function NotifySettings() {
|
||||
const t = useTranslations('profile');
|
||||
const { user, getUserInfo } = useGlobalStore();
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: {
|
||||
enable_balance_notify: user?.enable_balance_notify ?? false,
|
||||
enable_login_notify: user?.enable_login_notify ?? false,
|
||||
enable_subscribe_notify: user?.enable_subscribe_notify ?? false,
|
||||
enable_trade_notify: user?.enable_trade_notify ?? false,
|
||||
},
|
||||
});
|
||||
|
||||
async function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||
await updateUserNotify(data);
|
||||
toast.success(t('notify.updateSuccess'));
|
||||
await getUserInfo();
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className='min-w-80'>
|
||||
<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-6 p-6'>
|
||||
<Form {...form}>
|
||||
<form id='notify-form' onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'>
|
||||
<div className='space-y-4'>
|
||||
{[
|
||||
{ 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>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import ChangePassword from './change-password';
|
||||
import NotifySettings from './notify-settings';
|
||||
import ThirdPartyAccounts from './third-party-accounts';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className='flex flex-col gap-4 lg:flex-row lg:flex-wrap lg:*:flex-auto'>
|
||||
<ThirdPartyAccounts />
|
||||
<NotifySettings />
|
||||
<ChangePassword />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
'use client';
|
||||
|
||||
import SendCode from '@/app/auth/send-code';
|
||||
import useGlobalStore from '@/config/use-global';
|
||||
import { bindOAuth, unbindOAuth, updateBindEmail, updateBindMobile } 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({
|
||||
onSuccess,
|
||||
children,
|
||||
}: {
|
||||
onSuccess: () => void;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const t = useTranslations('profile.thirdParty');
|
||||
const { common } = useGlobalStore();
|
||||
const { enable_whitelist, whitelist } = common.auth.mobile;
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const formSchema = z.object({
|
||||
area_code: z.string().min(1, 'Area code is required'),
|
||||
mobile: z.string().min(5, 'Phone number is required'),
|
||||
code: z.string().min(4, 'Verification code is required'),
|
||||
});
|
||||
|
||||
type MobileBindFormValues = z.infer<typeof formSchema>;
|
||||
|
||||
const form = useForm<MobileBindFormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
area_code: '1',
|
||||
mobile: '',
|
||||
code: '',
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (values: MobileBindFormValues) => {
|
||||
try {
|
||||
await updateBindMobile(values);
|
||||
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='mobile'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<div className='flex'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='area_code'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<AreaCodeSelect
|
||||
simple
|
||||
className='w-32 rounded-r-none border-r-0'
|
||||
placeholder='Area code...'
|
||||
value={field.value}
|
||||
whitelist={enable_whitelist ? whitelist : []}
|
||||
onChange={(value) => {
|
||||
if (value.phone) {
|
||||
form.setValue(field.name, 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='code'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<div className='flex gap-2'>
|
||||
<Input placeholder='Enter code...' type='text' {...field} />
|
||||
<SendCode
|
||||
type='phone'
|
||||
params={{
|
||||
telephone_area_code: form.getValues().area_code,
|
||||
telephone: form.getValues().mobile,
|
||||
type: 1,
|
||||
}}
|
||||
/>
|
||||
</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',
|
||||
},
|
||||
{
|
||||
id: 'device',
|
||||
icon: 'mdi:devices',
|
||||
name: 'Device',
|
||||
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') {
|
||||
await updateBindEmail({ email: value });
|
||||
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) {
|
||||
await unbindOAuth({ method: account.id });
|
||||
await getUserInfo();
|
||||
} else {
|
||||
const res = await bindOAuth({
|
||||
method: account.id,
|
||||
redirect: `${window.location.origin}/bind/${account.id}`,
|
||||
});
|
||||
if (res.data?.data?.redirect) {
|
||||
window.location.href = res.data.data.redirect;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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];
|
||||
let displayValue = '';
|
||||
|
||||
switch (account.id) {
|
||||
case 'email':
|
||||
displayValue = isEditing ? currentValue : method?.auth_identifier || '';
|
||||
break;
|
||||
default:
|
||||
displayValue = 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 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user