♻️ refactor(view): System and Auth Control
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
'use client';
|
||||
|
||||
import { getCurrencyConfig, updateCurrencyConfig } from '@/services/admin/system';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@workspace/ui/components/form';
|
||||
import { ScrollArea } from '@workspace/ui/components/scroll-area';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from '@workspace/ui/components/sheet';
|
||||
|
||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
// Constants
|
||||
const EXCHANGE_RATE_HOST_URL = 'https://exchangerate.host';
|
||||
|
||||
const currencySchema = z.object({
|
||||
access_key: z.string().optional(),
|
||||
currency_unit: z.string().min(1),
|
||||
currency_symbol: z.string().min(1),
|
||||
});
|
||||
|
||||
type CurrencyFormData = z.infer<typeof currencySchema>;
|
||||
|
||||
export default function CurrencyConfig() {
|
||||
const t = useTranslations('system');
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: ['getCurrencyConfig'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getCurrencyConfig();
|
||||
return data.data;
|
||||
},
|
||||
enabled: open, // Only request data when the modal is open
|
||||
});
|
||||
|
||||
const form = useForm<CurrencyFormData>({
|
||||
resolver: zodResolver(currencySchema),
|
||||
defaultValues: {
|
||||
access_key: '',
|
||||
currency_unit: 'USD',
|
||||
currency_symbol: '$',
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset(data);
|
||||
}
|
||||
}, [data, form]);
|
||||
|
||||
async function onSubmit(values: CurrencyFormData) {
|
||||
setLoading(true);
|
||||
try {
|
||||
await updateCurrencyConfig(values as API.CurrencyConfig);
|
||||
toast.success(t('common.saveSuccess'));
|
||||
refetch();
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
toast.error(t('common.saveFailed'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<div className='flex cursor-pointer items-center justify-between transition-colors'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<div className='bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg'>
|
||||
<Icon icon='mdi:currency-usd' className='text-primary h-5 w-5' />
|
||||
</div>
|
||||
<div className='flex-1'>
|
||||
<p className='font-medium'>{t('currency.title')}</p>
|
||||
<p className='text-muted-foreground text-sm'>{t('currency.description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Icon icon='mdi:chevron-right' className='size-6' />
|
||||
</div>
|
||||
</SheetTrigger>
|
||||
<SheetContent className='w-[600px] max-w-full md:max-w-screen-md'>
|
||||
<SheetHeader>
|
||||
<SheetTitle>{t('currency.title')}</SheetTitle>
|
||||
</SheetHeader>
|
||||
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))] px-6'>
|
||||
<Form {...form}>
|
||||
<form
|
||||
id='currency-form'
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className='space-y-2 pt-4'
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='access_key'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('currency.accessKey')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
placeholder={t('currency.accessKeyPlaceholder')}
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('currency.accessKeyDescription', { url: EXCHANGE_RATE_HOST_URL })}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='currency_unit'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('currency.currencyUnit')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
placeholder={t('currency.currencyUnitPlaceholder')}
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>{t('currency.currencyUnitDescription')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='currency_symbol'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('currency.currencySymbol')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
placeholder={t('currency.currencySymbolPlaceholder')}
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>{t('currency.currencySymbolDescription')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</ScrollArea>
|
||||
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
|
||||
<Button variant='outline' disabled={loading} onClick={() => setOpen(false)}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button disabled={loading} type='submit' form='currency-form'>
|
||||
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
'use client';
|
||||
|
||||
import { getPrivacyPolicyConfig, updatePrivacyPolicyConfig } from '@/services/admin/system';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@workspace/ui/components/form';
|
||||
import { ScrollArea } from '@workspace/ui/components/scroll-area';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from '@workspace/ui/components/sheet';
|
||||
|
||||
import { MarkdownEditor } from '@workspace/ui/custom-components/editor';
|
||||
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
const privacyPolicySchema = z.object({
|
||||
content: z.string().optional(),
|
||||
});
|
||||
|
||||
type PrivacyPolicyFormData = z.infer<typeof privacyPolicySchema>;
|
||||
|
||||
export default function PrivacyPolicyConfig() {
|
||||
const t = useTranslations('system');
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: ['getPrivacyPolicyConfig'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getPrivacyPolicyConfig();
|
||||
return data.data;
|
||||
},
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
const form = useForm<PrivacyPolicyFormData>({
|
||||
resolver: zodResolver(privacyPolicySchema),
|
||||
defaultValues: {
|
||||
content: '',
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
content: data.content || '',
|
||||
});
|
||||
}
|
||||
}, [data, form]);
|
||||
|
||||
async function onSubmit(values: PrivacyPolicyFormData) {
|
||||
setLoading(true);
|
||||
try {
|
||||
await updatePrivacyPolicyConfig(values as API.PrivacyPolicyConfig);
|
||||
toast.success(t('common.saveSuccess'));
|
||||
refetch();
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
toast.error(t('common.saveFailed'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<div className='flex cursor-pointer items-center justify-between transition-colors'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<div className='bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg'>
|
||||
<Icon icon='mdi:shield-account-outline' className='text-primary h-5 w-5' />
|
||||
</div>
|
||||
<div className='flex-1'>
|
||||
<p className='font-medium'>{t('privacyPolicy.title')}</p>
|
||||
<p className='text-muted-foreground text-sm'>{t('privacyPolicy.description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Icon icon='mdi:chevron-right' className='size-6' />
|
||||
</div>
|
||||
</SheetTrigger>
|
||||
<SheetContent className='w-[600px] max-w-full md:max-w-screen-md'>
|
||||
<SheetHeader>
|
||||
<SheetTitle>{t('privacyPolicy.title')}</SheetTitle>
|
||||
</SheetHeader>
|
||||
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))] px-6'>
|
||||
<Form {...form}>
|
||||
<form
|
||||
id='privacy-policy-form'
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className='space-y-2 pt-4'
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='content'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('privacyPolicy.title')}</FormLabel>
|
||||
<FormControl>
|
||||
<MarkdownEditor value={field.value || ''} onChange={field.onChange} />
|
||||
</FormControl>
|
||||
<FormDescription>{t('privacyPolicy.description')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</ScrollArea>
|
||||
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
|
||||
<Button variant='outline' disabled={loading} onClick={() => setOpen(false)}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button disabled={loading} type='submit' form='privacy-policy-form'>
|
||||
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
'use client';
|
||||
|
||||
import { getSiteConfig, updateSiteConfig } from '@/services/admin/system';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@workspace/ui/components/form';
|
||||
import { ScrollArea } from '@workspace/ui/components/scroll-area';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from '@workspace/ui/components/sheet';
|
||||
|
||||
import { Textarea } from '@workspace/ui/components/textarea';
|
||||
import { JSONEditor } from '@workspace/ui/custom-components/editor';
|
||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||
import { UploadImage } from '@workspace/ui/custom-components/upload-image';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
const siteSchema = z.object({
|
||||
site_logo: z.string().optional(),
|
||||
site_name: z.string().min(1),
|
||||
site_desc: z.string().optional(),
|
||||
keywords: z.string().optional(),
|
||||
custom_html: z.string().optional(),
|
||||
host: z.string().optional(),
|
||||
custom_data: z.any().optional(),
|
||||
});
|
||||
|
||||
type SiteFormData = z.infer<typeof siteSchema>;
|
||||
|
||||
export default function SiteConfig() {
|
||||
const t = useTranslations('system');
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: ['getSiteConfig'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getSiteConfig();
|
||||
return data.data;
|
||||
},
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
const form = useForm<SiteFormData>({
|
||||
resolver: zodResolver(siteSchema),
|
||||
defaultValues: {
|
||||
site_logo: '',
|
||||
site_name: '',
|
||||
site_desc: '',
|
||||
keywords: '',
|
||||
custom_html: '',
|
||||
host: '',
|
||||
custom_data: {},
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset(data);
|
||||
}
|
||||
}, [data, form]);
|
||||
|
||||
async function onSubmit(values: SiteFormData) {
|
||||
setLoading(true);
|
||||
try {
|
||||
await updateSiteConfig(values as API.SiteConfig);
|
||||
toast.success(t('common.saveSuccess'));
|
||||
refetch();
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
toast.error(t('common.saveFailed'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<div className='flex cursor-pointer items-center justify-between transition-colors'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<div className='bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg'>
|
||||
<Icon icon='mdi:web' className='text-primary h-5 w-5' />
|
||||
</div>
|
||||
<div className='flex-1'>
|
||||
<p className='font-medium'>{t('site.title')}</p>
|
||||
<p className='text-muted-foreground text-sm'>{t('site.description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Icon icon='mdi:chevron-right' className='size-6' />
|
||||
</div>
|
||||
</SheetTrigger>
|
||||
<SheetContent className='w-[600px] max-w-full md:max-w-screen-md'>
|
||||
<SheetHeader>
|
||||
<SheetTitle>{t('site.title')}</SheetTitle>
|
||||
</SheetHeader>
|
||||
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))] px-6'>
|
||||
<Form {...form}>
|
||||
<form id='site-form' onSubmit={form.handleSubmit(onSubmit)} className='space-y-2 pt-4'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='site_logo'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('site.logo')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
placeholder={t('site.logoPlaceholder')}
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
suffix={
|
||||
<UploadImage
|
||||
className='bg-muted h-9 rounded-none border-none px-2'
|
||||
onChange={(value) => {
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>{t('site.logoDescription')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='site_name'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('site.siteName')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
placeholder={t('site.siteNamePlaceholder')}
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>{t('site.siteNameDescription')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='site_desc'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('site.siteDesc')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
placeholder={t('site.siteDescPlaceholder')}
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>{t('site.siteDescDescription')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='keywords'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('site.keywords')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
placeholder={t('site.keywordsPlaceholder')}
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>{t('site.keywordsDescription')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='custom_html'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('site.customHtml')}</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className='h-32'
|
||||
placeholder={t('site.customHtmlDescription')}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>{t('site.customHtmlDescription')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='host'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('site.siteDomain')}</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className='h-32'
|
||||
placeholder={`${t('site.siteDomainPlaceholder')}\nexample.com\nwww.example.com`}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>{t('site.siteDomainDescription')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='custom_data'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('site.customData')}</FormLabel>
|
||||
<FormControl>
|
||||
<JSONEditor
|
||||
schema={{
|
||||
type: 'object',
|
||||
additionalProperties: true,
|
||||
properties: {
|
||||
website: { type: 'string', title: 'Website' },
|
||||
contacts: {
|
||||
type: 'object',
|
||||
title: 'Contacts',
|
||||
additionalProperties: true,
|
||||
properties: {
|
||||
email: { type: 'string', title: 'Email' },
|
||||
telephone: { type: 'string', title: 'Telephone' },
|
||||
address: { type: 'string', title: 'Address' },
|
||||
},
|
||||
},
|
||||
community: {
|
||||
type: 'object',
|
||||
title: 'Community',
|
||||
additionalProperties: true,
|
||||
properties: {
|
||||
telegram: { type: 'string', title: 'Telegram' },
|
||||
twitter: { type: 'string', title: 'Twitter' },
|
||||
discord: { type: 'string', title: 'Discord' },
|
||||
instagram: { type: 'string', title: 'Instagram' },
|
||||
linkedin: { type: 'string', title: 'Linkedin' },
|
||||
facebook: { type: 'string', title: 'Facebook' },
|
||||
github: { type: 'string', title: 'Github' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
value={field.value}
|
||||
onBlur={(value) => field.onChange(value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>{t('site.customDataDescription')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</ScrollArea>
|
||||
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
|
||||
<Button variant='outline' disabled={loading} onClick={() => setOpen(false)}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button disabled={loading} type='submit' form='site-form'>
|
||||
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
'use client';
|
||||
|
||||
import { getTosConfig, updateTosConfig } from '@/services/admin/system';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@workspace/ui/components/form';
|
||||
import { ScrollArea } from '@workspace/ui/components/scroll-area';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from '@workspace/ui/components/sheet';
|
||||
import { MarkdownEditor } from '@workspace/ui/custom-components/editor';
|
||||
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
const tosSchema = z.object({
|
||||
content: z.string().optional(),
|
||||
});
|
||||
|
||||
type TosFormData = z.infer<typeof tosSchema>;
|
||||
|
||||
export default function TosConfig() {
|
||||
const t = useTranslations('system');
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: ['getTosConfig'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getTosConfig();
|
||||
return data.data;
|
||||
},
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
const form = useForm<TosFormData>({
|
||||
resolver: zodResolver(tosSchema),
|
||||
defaultValues: {
|
||||
content: '',
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
content: data.content || '',
|
||||
});
|
||||
}
|
||||
}, [data, form]);
|
||||
|
||||
async function onSubmit(values: TosFormData) {
|
||||
setLoading(true);
|
||||
try {
|
||||
await updateTosConfig(values as API.TosConfig);
|
||||
toast.success(t('common.saveSuccess'));
|
||||
refetch();
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
toast.error(t('common.saveFailed'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<div className='flex cursor-pointer items-center justify-between transition-colors'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<div className='bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg'>
|
||||
<Icon icon='mdi:file-document-outline' className='text-primary h-5 w-5' />
|
||||
</div>
|
||||
<div className='flex-1'>
|
||||
<p className='font-medium'>{t('tos.title')}</p>
|
||||
<p className='text-muted-foreground text-sm'>{t('tos.description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Icon icon='mdi:chevron-right' className='size-6' />
|
||||
</div>
|
||||
</SheetTrigger>
|
||||
<SheetContent className='w-[600px] max-w-full md:max-w-screen-md'>
|
||||
<SheetHeader>
|
||||
<SheetTitle>{t('tos.title')}</SheetTitle>
|
||||
</SheetHeader>
|
||||
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))] px-6'>
|
||||
<Form {...form}>
|
||||
<form id='tos-form' onSubmit={form.handleSubmit(onSubmit)} className='space-y-2 pt-4'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='content'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('tos.title')}</FormLabel>
|
||||
<FormControl>
|
||||
<MarkdownEditor value={field.value || ''} onChange={field.onChange} />
|
||||
</FormControl>
|
||||
<FormDescription>{t('tos.description')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</ScrollArea>
|
||||
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
|
||||
<Button variant='outline' disabled={loading} onClick={() => setOpen(false)}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button disabled={loading} type='submit' form='tos-form'>
|
||||
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { getCurrencyConfig, updateCurrencyConfig } from '@/services/admin/system';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Label } from '@workspace/ui/components/label';
|
||||
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
|
||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export default function Site() {
|
||||
const t = useTranslations('system.currency');
|
||||
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: ['getCurrencyConfig'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getCurrencyConfig();
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
async function updateConfig(key: string, value: unknown) {
|
||||
if (data?.[key] === value) return;
|
||||
try {
|
||||
await updateCurrencyConfig({
|
||||
...data,
|
||||
[key]: value,
|
||||
} as API.CurrencyConfig);
|
||||
toast.success(t('saveSuccess'));
|
||||
refetch();
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('accessKey')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('accessKeyDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
value={data?.access_key}
|
||||
onValueBlur={(value) => updateConfig('access_key', value)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('currencyUnit')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('currencyUnitDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder='USD'
|
||||
value={data?.currency_unit}
|
||||
onValueBlur={(value) => updateConfig('currency_unit', value)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('currencySymbol')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('currencySymbolDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder='$'
|
||||
value={data?.currency_symbol}
|
||||
onValueBlur={(value) => updateConfig('currency_symbol', value)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
@@ -1,33 +1,62 @@
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import Currency from './currency';
|
||||
import PrivacyPolicy from './privacy-policy';
|
||||
import Site from './site';
|
||||
import Tos from './tos';
|
||||
'use client';
|
||||
|
||||
export default async function Page() {
|
||||
const t = await getTranslations('system');
|
||||
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import CurrencyForm from './basic-settings/currency-form';
|
||||
import PrivacyPolicyForm from './basic-settings/privacy-policy-form';
|
||||
import SiteForm from './basic-settings/site-form';
|
||||
import TosForm from './basic-settings/tos-form';
|
||||
import InviteForm from './user-security/invite-form';
|
||||
import RegisterForm from './user-security/register-form';
|
||||
import VerifyCodeForm from './user-security/verify-code-form';
|
||||
import VerifyForm from './user-security/verify-form';
|
||||
|
||||
export default function Page() {
|
||||
const t = useTranslations('system');
|
||||
|
||||
// 定义表单配置
|
||||
const formSections = [
|
||||
{
|
||||
title: t('basicSettings'),
|
||||
forms: [
|
||||
{ component: SiteForm },
|
||||
{ component: CurrencyForm },
|
||||
{ component: TosForm },
|
||||
{ component: PrivacyPolicyForm },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t('userSecuritySettings'),
|
||||
forms: [
|
||||
{ component: RegisterForm },
|
||||
{ component: InviteForm },
|
||||
{ component: VerifyForm },
|
||||
{ component: VerifyCodeForm },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Tabs defaultValue='site'>
|
||||
<TabsList className='h-full flex-wrap'>
|
||||
<TabsTrigger value='site'>{t('tabs.site')}</TabsTrigger>
|
||||
<TabsTrigger value='currency'>{t('tabs.currency')}</TabsTrigger>
|
||||
<TabsTrigger value='tos'>{t('tabs.tos')}</TabsTrigger>
|
||||
<TabsTrigger value='privacy-policy'>{t('privacy-policy.title')}</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value='site'>
|
||||
<Site />
|
||||
</TabsContent>
|
||||
<TabsContent value='currency'>
|
||||
<Currency />
|
||||
</TabsContent>
|
||||
<TabsContent value='tos'>
|
||||
<Tos />
|
||||
</TabsContent>
|
||||
<TabsContent value='privacy-policy'>
|
||||
<PrivacyPolicy />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<div className='space-y-8'>
|
||||
{formSections.map((section, sectionIndex) => (
|
||||
<div key={sectionIndex}>
|
||||
<h2 className='mb-4 text-lg font-semibold'>{section.title}</h2>
|
||||
<Table>
|
||||
<TableBody>
|
||||
{section.forms.map((form, formIndex) => {
|
||||
const FormComponent = form.component;
|
||||
return (
|
||||
<TableRow key={formIndex}>
|
||||
<TableCell>
|
||||
<FormComponent />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { getPrivacyPolicyConfig, updatePrivacyPolicyConfig } from '@/services/admin/system';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { MarkdownEditor } from '@workspace/ui/custom-components/editor';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export default function PrivacyPolicy() {
|
||||
const t = useTranslations('system.privacy-policy');
|
||||
const { data, refetch, isFetched } = useQuery({
|
||||
queryKey: ['getPrivacyPolicyConfig'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getPrivacyPolicyConfig();
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
async function updateConfig(key: string, value: unknown) {
|
||||
if (data?.[key] === value) return;
|
||||
try {
|
||||
await updatePrivacyPolicyConfig({
|
||||
...data,
|
||||
[key]: value,
|
||||
} as API.PrivacyPolicyConfig);
|
||||
toast.success(t('saveSuccess'));
|
||||
refetch();
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
isFetched && (
|
||||
<div className='h-[calc(100dvh-132px-env(safe-area-inset-top))] overflow-hidden'>
|
||||
<MarkdownEditor
|
||||
title={t('title')}
|
||||
value={data?.privacy_policy}
|
||||
onBlur={(value) => {
|
||||
if (data?.privacy_policy !== value) {
|
||||
updateConfig('privacy_policy', value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { getSiteConfig, updateSiteConfig } from '@/services/admin/system';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Label } from '@workspace/ui/components/label';
|
||||
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
|
||||
import { Textarea } from '@workspace/ui/components/textarea';
|
||||
import { JSONEditor } from '@workspace/ui/custom-components/editor';
|
||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||
import { UploadImage } from '@workspace/ui/custom-components/upload-image';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useRef } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export default function Site() {
|
||||
const t = useTranslations('system.site');
|
||||
const ref = useRef<API.SiteConfig | undefined>(undefined);
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: ['getSiteConfig'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getSiteConfig();
|
||||
ref.current = data.data;
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
async function updateConfig(key: string, value: unknown) {
|
||||
if (data?.[key] === value) return;
|
||||
try {
|
||||
await updateSiteConfig({
|
||||
...ref.current,
|
||||
[key]: value,
|
||||
} as API.SiteConfig);
|
||||
toast.success(t('saveSuccess'));
|
||||
refetch();
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('logo')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('logoDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('logoPlaceholder')}
|
||||
value={data?.site_logo}
|
||||
onValueBlur={(value) => updateConfig('site_logo', value)}
|
||||
suffix={
|
||||
<UploadImage
|
||||
className='bg-muted h-9 rounded-none border-none px-2'
|
||||
onChange={(value) => {
|
||||
updateConfig('site_logo', value);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('siteName')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('siteNameDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('siteNamePlaceholder')}
|
||||
value={data?.site_name}
|
||||
onValueBlur={(value) => updateConfig('site_name', value)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('siteDesc')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('siteDescDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('siteDescPlaceholder')}
|
||||
value={data?.site_desc}
|
||||
onValueBlur={(value) => updateConfig('site_desc', value)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('keywords')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('keywordsDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('keywordsDescription')}
|
||||
value={data?.keywords}
|
||||
onValueBlur={(value) => updateConfig('keywords', value)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className='align-top'>
|
||||
<Label>{t('customHtml')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('customHtmlDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Textarea
|
||||
className='h-52'
|
||||
placeholder={t('customHtmlDescription')}
|
||||
defaultValue={data?.custom_html}
|
||||
onBlur={(e) => {
|
||||
updateConfig('custom_html', e.target.value);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className='align-top'>
|
||||
<Label>{t('siteDomain')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('siteDomainDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Textarea
|
||||
className='h-52'
|
||||
placeholder={`${t('siteDomainPlaceholder')}\nexample.com\nwww.example.com`}
|
||||
defaultValue={data?.host}
|
||||
onBlur={(e) => {
|
||||
updateConfig('host', e.target.value);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('customData')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('customDataDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='w-1/2 text-right'>
|
||||
<JSONEditor
|
||||
schema={{
|
||||
type: 'object',
|
||||
additionalProperties: true,
|
||||
properties: {
|
||||
website: { type: 'string', title: 'Website' },
|
||||
contacts: {
|
||||
type: 'object',
|
||||
title: 'Contacts',
|
||||
additionalProperties: true,
|
||||
properties: {
|
||||
email: { type: 'string', title: 'Email' },
|
||||
telephone: { type: 'string', title: 'Telephone' },
|
||||
address: { type: 'string', title: 'Address' },
|
||||
},
|
||||
},
|
||||
community: {
|
||||
type: 'object',
|
||||
title: 'Community',
|
||||
additionalProperties: true,
|
||||
properties: {
|
||||
telegram: { type: 'string', title: 'Telegram' },
|
||||
twitter: { type: 'string', title: 'Twitter' },
|
||||
discord: { type: 'string', title: 'Discord' },
|
||||
instagram: { type: 'string', title: 'Instagram' },
|
||||
linkedin: { type: 'string', title: 'Linkedin' },
|
||||
facebook: { type: 'string', title: 'Facebook' },
|
||||
github: { type: 'string', title: 'Github' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
value={data?.custom_data}
|
||||
onBlur={(value) => updateConfig('custom_data', value)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { getTosConfig, updateTosConfig } from '@/services/admin/system';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { MarkdownEditor } from '@workspace/ui/custom-components/editor';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export default function Tos() {
|
||||
const t = useTranslations('system.tos');
|
||||
const { data, refetch, isFetched } = useQuery({
|
||||
queryKey: ['getTosConfig'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getTosConfig();
|
||||
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
async function updateConfig(key: string, value: unknown) {
|
||||
if (data?.[key] === value) return;
|
||||
try {
|
||||
await updateTosConfig({
|
||||
...data,
|
||||
[key]: value,
|
||||
} as API.TosConfig);
|
||||
toast.success(t('saveSuccess'));
|
||||
refetch();
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
isFetched && (
|
||||
<div className='h-[calc(100dvh-132px-env(safe-area-inset-top))] overflow-hidden'>
|
||||
<MarkdownEditor
|
||||
title={t('title')}
|
||||
value={data?.tos_content}
|
||||
onBlur={(value) => {
|
||||
if (data?.tos_content !== value) {
|
||||
updateConfig('tos_content', value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
'use client';
|
||||
|
||||
import { getInviteConfig, updateInviteConfig } from '@/services/admin/system';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@workspace/ui/components/form';
|
||||
import { ScrollArea } from '@workspace/ui/components/scroll-area';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from '@workspace/ui/components/sheet';
|
||||
import { Switch } from '@workspace/ui/components/switch';
|
||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
const inviteSchema = z.object({
|
||||
forced_invite: z.boolean().optional(),
|
||||
referral_percentage: z.number().optional(),
|
||||
only_first_purchase: z.boolean().optional(),
|
||||
});
|
||||
|
||||
type InviteFormData = z.infer<typeof inviteSchema>;
|
||||
|
||||
export default function InviteConfig() {
|
||||
const t = useTranslations('system.invite');
|
||||
const systemT = useTranslations('system');
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: ['getInviteConfig'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getInviteConfig();
|
||||
return data.data;
|
||||
},
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
const form = useForm<InviteFormData>({
|
||||
resolver: zodResolver(inviteSchema),
|
||||
defaultValues: {
|
||||
forced_invite: false,
|
||||
referral_percentage: 0,
|
||||
only_first_purchase: false,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset(data);
|
||||
}
|
||||
}, [data, form]);
|
||||
|
||||
async function onSubmit(values: InviteFormData) {
|
||||
setLoading(true);
|
||||
try {
|
||||
await updateInviteConfig(values as API.InviteConfig);
|
||||
toast.success(t('saveSuccess'));
|
||||
refetch();
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
toast.error(t('saveFailed'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<div className='flex cursor-pointer items-center justify-between transition-colors'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<div className='bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg'>
|
||||
<Icon icon='mdi:account-multiple-plus-outline' className='text-primary h-5 w-5' />
|
||||
</div>
|
||||
<div className='flex-1'>
|
||||
<p className='font-medium'>{t('title')}</p>
|
||||
<p className='text-muted-foreground text-sm'>{t('description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Icon icon='mdi:chevron-right' className='size-6' />
|
||||
</div>
|
||||
</SheetTrigger>
|
||||
<SheetContent className='w-[600px] max-w-full md:max-w-screen-md'>
|
||||
<SheetHeader>
|
||||
<SheetTitle>{t('title')}</SheetTitle>
|
||||
</SheetHeader>
|
||||
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))] px-6'>
|
||||
<Form {...form}>
|
||||
<form
|
||||
id='invite-form'
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className='space-y-2 pt-4'
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='forced_invite'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('forcedInvite')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
className='float-end !mt-0'
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>{t('forcedInviteDescription')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='referral_percentage'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('referralPercentage')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={field.value}
|
||||
type='number'
|
||||
min={0}
|
||||
max={100}
|
||||
suffix='%'
|
||||
onValueBlur={(value) => field.onChange(Number(value))}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>{t('referralPercentageDescription')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='only_first_purchase'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('onlyFirstPurchase')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
className='float-end !mt-0'
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>{t('onlyFirstPurchaseDescription')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</ScrollArea>
|
||||
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
|
||||
<Button variant='outline' disabled={loading} onClick={() => setOpen(false)}>
|
||||
{systemT('common.cancel')}
|
||||
</Button>
|
||||
<Button disabled={loading} type='submit' form='invite-form'>
|
||||
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
|
||||
{systemT('common.save')}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
'use client';
|
||||
|
||||
import { getSubscribeList } from '@/services/admin/subscribe';
|
||||
import { getRegisterConfig, updateRegisterConfig } from '@/services/admin/system';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@workspace/ui/components/form';
|
||||
import { ScrollArea } from '@workspace/ui/components/scroll-area';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from '@workspace/ui/components/sheet';
|
||||
import { Switch } from '@workspace/ui/components/switch';
|
||||
import { Combobox } from '@workspace/ui/custom-components/combobox';
|
||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
const registerSchema = z.object({
|
||||
stop_register: z.boolean().optional(),
|
||||
enable_ip_register_limit: z.boolean().optional(),
|
||||
ip_register_limit_count: z.number().optional(),
|
||||
ip_register_limit_expire_day: z.number().optional(),
|
||||
trial_flow: z.number().optional(),
|
||||
trial_day: z.number().optional(),
|
||||
default_subscribe_id: z.number().optional(),
|
||||
});
|
||||
|
||||
type RegisterFormData = z.infer<typeof registerSchema>;
|
||||
|
||||
export default function RegisterConfig() {
|
||||
const t = useTranslations('system.register');
|
||||
const systemT = useTranslations('system');
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: ['getRegisterConfig'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getRegisterConfig();
|
||||
return data.data;
|
||||
},
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
const { data: subscribe } = useQuery({
|
||||
queryKey: ['getSubscribeList', 'all'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getSubscribeList({
|
||||
page: 1,
|
||||
size: 9999,
|
||||
});
|
||||
return data.data?.list as API.Subscribe[];
|
||||
},
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
const form = useForm<RegisterFormData>({
|
||||
resolver: zodResolver(registerSchema),
|
||||
defaultValues: {
|
||||
stop_register: false,
|
||||
enable_ip_register_limit: false,
|
||||
ip_register_limit_count: 1,
|
||||
ip_register_limit_expire_day: 1,
|
||||
trial_flow: 0,
|
||||
trial_day: 0,
|
||||
default_subscribe_id: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset(data);
|
||||
}
|
||||
}, [data, form]);
|
||||
|
||||
async function onSubmit(values: RegisterFormData) {
|
||||
setLoading(true);
|
||||
try {
|
||||
await updateRegisterConfig(values as API.RegisterConfig);
|
||||
toast.success(t('saveSuccess'));
|
||||
refetch();
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
toast.error(t('saveFailed'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<div className='flex cursor-pointer items-center justify-between transition-colors'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<div className='bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg'>
|
||||
<Icon icon='mdi:account-plus-outline' className='text-primary h-5 w-5' />
|
||||
</div>
|
||||
<div className='flex-1'>
|
||||
<p className='font-medium'>{t('title')}</p>
|
||||
<p className='text-muted-foreground text-sm'>{t('description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Icon icon='mdi:chevron-right' className='size-6' />
|
||||
</div>
|
||||
</SheetTrigger>
|
||||
<SheetContent className='w-[600px] max-w-full md:max-w-screen-md'>
|
||||
<SheetHeader>
|
||||
<SheetTitle>{t('title')}</SheetTitle>
|
||||
</SheetHeader>
|
||||
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))] px-6'>
|
||||
<Form {...form}>
|
||||
<form
|
||||
id='register-form'
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className='space-y-2 pt-4'
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='stop_register'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('stopNewUserRegistration')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
className='float-end !mt-0'
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>{t('stopNewUserRegistrationDescription')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='enable_ip_register_limit'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('ipRegistrationLimit')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
className='float-end !mt-0'
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>{t('ipRegistrationLimitDescription')}</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='ip_register_limit_count'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('registrationLimitCount')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={field.value}
|
||||
type='number'
|
||||
min={1}
|
||||
onValueBlur={(value) => field.onChange(Number(value))}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>{t('registrationLimitCountDescription')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='ip_register_limit_expire_day'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('registrationLimitExpire')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={field.value}
|
||||
type='number'
|
||||
min={1}
|
||||
suffix={t('day')}
|
||||
onValueBlur={(value) => field.onChange(Number(value))}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>{t('registrationLimitExpireDescription')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='trial_flow'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('trialFlow')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={field.value}
|
||||
type='number'
|
||||
min={0}
|
||||
suffix='GB'
|
||||
onValueBlur={(value) => field.onChange(Number(value))}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>{t('trialFlowDescription')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='trial_day'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('trialDay')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={field.value}
|
||||
type='number'
|
||||
min={0}
|
||||
suffix={t('day')}
|
||||
onValueBlur={(value) => field.onChange(Number(value))}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>{t('trialDayDescription')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='default_subscribe_id'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('defaultSubscribe')}</FormLabel>
|
||||
<FormControl>
|
||||
<Combobox
|
||||
placeholder={t('selectPlaceholder')}
|
||||
value={field.value}
|
||||
onChange={(value: number) => {
|
||||
if (value) {
|
||||
field.onChange(value);
|
||||
}
|
||||
}}
|
||||
options={
|
||||
subscribe?.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
})) || []
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>{t('defaultSubscribeDescription')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</ScrollArea>
|
||||
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
|
||||
<Button variant='outline' disabled={loading} onClick={() => setOpen(false)}>
|
||||
{systemT('common.cancel')}
|
||||
</Button>
|
||||
<Button disabled={loading} type='submit' form='register-form'>
|
||||
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
|
||||
{systemT('common.save')}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
'use client';
|
||||
|
||||
import { getVerifyCodeConfig, updateVerifyCodeConfig } from '@/services/admin/system';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@workspace/ui/components/form';
|
||||
import { ScrollArea } from '@workspace/ui/components/scroll-area';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from '@workspace/ui/components/sheet';
|
||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
const verifyCodeSchema = z.object({
|
||||
verify_code_expire_time: z.number().optional(),
|
||||
verify_code_interval: z.number().optional(),
|
||||
verify_code_limit: z.number().optional(),
|
||||
});
|
||||
|
||||
type VerifyCodeFormData = z.infer<typeof verifyCodeSchema>;
|
||||
|
||||
export default function VerifyCodeConfig() {
|
||||
const t = useTranslations('system.verifyCode');
|
||||
const systemT = useTranslations('system');
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: ['getVerifyCodeConfig'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getVerifyCodeConfig();
|
||||
return data.data;
|
||||
},
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
const form = useForm<VerifyCodeFormData>({
|
||||
resolver: zodResolver(verifyCodeSchema),
|
||||
defaultValues: {
|
||||
verify_code_expire_time: 300,
|
||||
verify_code_interval: 60,
|
||||
verify_code_limit: 10,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset(data);
|
||||
}
|
||||
}, [data, form]);
|
||||
|
||||
async function onSubmit(values: VerifyCodeFormData) {
|
||||
setLoading(true);
|
||||
try {
|
||||
await updateVerifyCodeConfig(values as API.VerifyCodeConfig);
|
||||
toast.success(t('saveSuccess'));
|
||||
refetch();
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
toast.error(t('saveFailed'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<div className='flex cursor-pointer items-center justify-between transition-colors'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<div className='bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg'>
|
||||
<Icon icon='mdi:message-text-clock-outline' className='text-primary h-5 w-5' />
|
||||
</div>
|
||||
<div className='flex-1'>
|
||||
<p className='font-medium'>{t('title')}</p>
|
||||
<p className='text-muted-foreground text-sm'>{t('description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Icon icon='mdi:chevron-right' className='size-6' />
|
||||
</div>
|
||||
</SheetTrigger>
|
||||
<SheetContent className='w-[600px] max-w-full md:max-w-screen-md'>
|
||||
<SheetHeader>
|
||||
<SheetTitle>{t('title')}</SheetTitle>
|
||||
</SheetHeader>
|
||||
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))] px-6'>
|
||||
<Form {...form}>
|
||||
<form
|
||||
id='verify-code-form'
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className='space-y-2 pt-4'
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='verify_code_expire_time'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('expireTime')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={field.value}
|
||||
type='number'
|
||||
min={60}
|
||||
suffix={t('seconds')}
|
||||
onValueBlur={(value) => field.onChange(Number(value))}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>{t('expireTimeDescription')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='verify_code_interval'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('interval')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={field.value}
|
||||
type='number'
|
||||
min={30}
|
||||
suffix={t('seconds')}
|
||||
onValueBlur={(value) => field.onChange(Number(value))}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>{t('intervalDescription')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='verify_code_limit'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('dailyLimit')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={field.value}
|
||||
type='number'
|
||||
min={1}
|
||||
suffix={t('times')}
|
||||
onValueBlur={(value) => field.onChange(Number(value))}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>{t('dailyLimitDescription')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</ScrollArea>
|
||||
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
|
||||
<Button variant='outline' disabled={loading} onClick={() => setOpen(false)}>
|
||||
{systemT('common.cancel')}
|
||||
</Button>
|
||||
<Button disabled={loading} type='submit' form='verify-code-form'>
|
||||
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
|
||||
{systemT('common.save')}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
'use client';
|
||||
|
||||
import { getVerifyConfig, updateVerifyConfig } from '@/services/admin/system';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@workspace/ui/components/form';
|
||||
import { ScrollArea } from '@workspace/ui/components/scroll-area';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from '@workspace/ui/components/sheet';
|
||||
import { Switch } from '@workspace/ui/components/switch';
|
||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
const verifySchema = z.object({
|
||||
turnstile_site_key: z.string().optional(),
|
||||
turnstile_secret: z.string().optional(),
|
||||
enable_register_verify: z.boolean().optional(),
|
||||
enable_login_verify: z.boolean().optional(),
|
||||
enable_password_verify: z.boolean().optional(),
|
||||
});
|
||||
|
||||
type VerifyFormData = z.infer<typeof verifySchema>;
|
||||
|
||||
export default function VerifyConfig() {
|
||||
const t = useTranslations('system.verify');
|
||||
const systemT = useTranslations('system');
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: ['getVerifyConfig'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getVerifyConfig();
|
||||
return data.data;
|
||||
},
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
const form = useForm<VerifyFormData>({
|
||||
resolver: zodResolver(verifySchema),
|
||||
defaultValues: {
|
||||
turnstile_site_key: '',
|
||||
turnstile_secret: '',
|
||||
enable_register_verify: false,
|
||||
enable_login_verify: false,
|
||||
enable_password_verify: false,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset(data);
|
||||
}
|
||||
}, [data, form]);
|
||||
|
||||
async function onSubmit(values: VerifyFormData) {
|
||||
setLoading(true);
|
||||
try {
|
||||
await updateVerifyConfig(values as API.VerifyConfig);
|
||||
toast.success(t('saveSuccess'));
|
||||
refetch();
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
toast.error(t('saveFailed'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<div className='flex cursor-pointer items-center justify-between transition-colors'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<div className='bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg'>
|
||||
<Icon icon='mdi:shield-check-outline' className='text-primary h-5 w-5' />
|
||||
</div>
|
||||
<div className='flex-1'>
|
||||
<p className='font-medium'>{t('title')}</p>
|
||||
<p className='text-muted-foreground text-sm'>{t('description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Icon icon='mdi:chevron-right' className='size-6' />
|
||||
</div>
|
||||
</SheetTrigger>
|
||||
<SheetContent className='w-[600px] max-w-full md:max-w-screen-md'>
|
||||
<SheetHeader>
|
||||
<SheetTitle>{t('title')}</SheetTitle>
|
||||
</SheetHeader>
|
||||
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))] px-6'>
|
||||
<Form {...form}>
|
||||
<form
|
||||
id='verify-form'
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className='space-y-2 pt-4'
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='turnstile_site_key'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('turnstileSiteKey')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
placeholder={t('turnstileSiteKeyPlaceholder')}
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>{t('turnstileSiteKeyDescription')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='turnstile_secret'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('turnstileSecret')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
placeholder={t('turnstileSecretPlaceholder')}
|
||||
value={field.value}
|
||||
type='password'
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>{t('turnstileSecretDescription')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='enable_register_verify'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('enableRegisterVerify')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
className='float-end !mt-0'
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>{t('enableRegisterVerifyDescription')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='enable_login_verify'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('enableLoginVerify')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
className='float-end !mt-0'
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>{t('enableLoginVerifyDescription')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='enable_password_verify'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('enablePasswordVerify')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
className='float-end !mt-0'
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>{t('enablePasswordVerifyDescription')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</ScrollArea>
|
||||
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
|
||||
<Button variant='outline' disabled={loading} onClick={() => setOpen(false)}>
|
||||
{systemT('common.cancel')}
|
||||
</Button>
|
||||
<Button disabled={loading} type='submit' form='verify-form'>
|
||||
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
|
||||
{systemT('common.save')}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user