🎉 chore(init): project initialization

This commit is contained in:
web@ppanel
2024-11-14 01:22:43 +07:00
commit 829edfa824
479 changed files with 61413 additions and 0 deletions
+146
View File
@@ -0,0 +1,146 @@
'use client';
import { ProTable, ProTableActions } from '@/components/pro-table';
import { createUser, deleteUser, getUserList, updateUser } from '@/services/admin/user';
import { ConfirmButton } from '@repo/ui/confirm-button';
import { formatDate, unitConversion } from '@repo/ui/utils';
import { Button } from '@shadcn/ui/button';
import { toast } from '@shadcn/ui/lib/sonner';
import { Switch } from '@shadcn/ui/switch';
import { useTranslations } from 'next-intl';
import { useRef, useState } from 'react';
import { UserDetail } from './user-detail';
import UserForm from './user-form';
export default function Page() {
const t = useTranslations('user');
const [loading, setLoading] = useState(false);
const ref = useRef<ProTableActions>();
return (
<ProTable<API.User, any>
action={ref}
header={{
title: t('userList'),
toolbar: (
<UserForm<API.CreateUserRequest>
key='create'
trigger={t('create')}
title={t('createUser')}
loading={loading}
onSubmit={async (values) => {
setLoading(true);
try {
await createUser(values);
toast.success(t('createSuccess'));
ref.current?.refresh();
setLoading(false);
return true;
} catch (error) {
setLoading(false);
return false;
}
}}
/>
),
}}
columns={[
{
accessorKey: 'enable',
header: t('enable'),
cell: ({ row }) => {
return (
<Switch
defaultChecked={row.getValue('enable')}
onCheckedChange={async (checked) => {
await updateUser({
...row.original,
enable: checked,
} as unknown as API.UpdateUserRequest);
toast.success(t('updateSuccess'));
ref.current?.refresh();
}}
/>
);
},
},
{
accessorKey: 'id',
header: 'ID',
},
{
accessorKey: 'email',
header: t('userName'),
},
{
accessorKey: 'balance',
header: t('balance'),
cell: ({ row }) => unitConversion('centsToDollars', row.getValue('balance')),
},
{
accessorKey: 'referer_id',
header: t('referer'),
cell: ({ row }) => <UserDetail id={row.original.referer_id} />,
},
{
accessorKey: 'created_at',
header: t('createdAt'),
cell: ({ row }) => formatDate(row.getValue('created_at')),
},
]}
request={async (pagination) => {
const { data } = await getUserList(pagination);
return {
list: data.data?.list || [],
total: data.data?.total || 0,
};
}}
actions={{
render: (row) => {
return [
<UserForm<API.UpdateUserRequest>
key='edit'
trigger={t('edit')}
title={t('editUser')}
loading={loading}
initialValues={row as unknown as API.UpdateUserRequest}
onSubmit={async (values) => {
setLoading(true);
try {
await updateUser({
...row,
...values,
});
toast.success(t('updateSuccess'));
ref.current?.refresh();
setLoading(false);
return true;
} catch (error) {
setLoading(false);
return false;
}
}}
/>,
<ConfirmButton
key='edit'
trigger={<Button variant='destructive'>{t('delete')}</Button>}
title={t('confirmDelete')}
description={t('deleteDescription')}
onConfirm={async () => {
await deleteUser({ id: row.id });
toast.success(t('deleteSuccess'));
ref.current?.refresh();
}}
cancelText={t('cancel')}
confirmText={t('confirm')}
/>,
];
},
}}
/>
);
}
@@ -0,0 +1,57 @@
'use client';
import { getUserDetail } from '@/services/admin/user';
import { formatDate, unitConversion } from '@repo/ui/utils';
import { Button } from '@shadcn/ui/button';
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@shadcn/ui/hover-card';
import { useQuery } from '@tanstack/react-query';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
export function UserDetail({ id }: { id: number }) {
const t = useTranslations('user');
const [shouldFetch, setShouldFetch] = useState(false);
const { data } = useQuery({
enabled: id !== 0 && shouldFetch,
queryKey: ['getUserDetail', id],
queryFn: async () => {
const { data } = await getUserDetail({ id });
return data.data;
},
});
if (!id) return '--';
return (
<HoverCard>
<HoverCardTrigger asChild onMouseEnter={() => setShouldFetch(true)}>
<Button variant='link' className='p-0'>
@{data?.email?.split('@')[0] || 'Loading...'}
</Button>
</HoverCardTrigger>
<HoverCardContent>
<div className='grid gap-3'>
<ul className='grid gap-3'>
<li className='flex items-center justify-between font-semibold'>
<span className='text-muted-foreground'>ID</span>
<span>{data?.id}</span>
</li>
<li className='flex items-center justify-between font-semibold'>
<span className='text-muted-foreground'>{t('email')}</span>
<span>{data?.email}</span>
</li>
<li className='flex items-center justify-between font-semibold'>
<span className='text-muted-foreground'>{t('balance')}</span>
<span>{unitConversion('centsToDollars', data?.balance)}</span>
</li>
<li className='flex items-center justify-between'>
<span className='text-muted-foreground'>{t('createdAt')}</span>
<span>{data?.created_at && formatDate(data?.created_at)}</span>
</li>
</ul>
</div>
</HoverCardContent>
</HoverCard>
);
}
+224
View File
@@ -0,0 +1,224 @@
'use client';
import { Icon } from '@iconify/react';
import { EnhancedInput } from '@repo/ui/enhanced-input';
import { unitConversion } from '@repo/ui/utils';
import { Button } from '@shadcn/ui/button';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@shadcn/ui/form';
import { useForm } from '@shadcn/ui/lib/react-hook-form';
import { z, zodResolver } from '@shadcn/ui/lib/zod';
import { ScrollArea } from '@shadcn/ui/scroll-area';
import {
Sheet,
SheetContent,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@shadcn/ui/sheet';
import { Switch } from '@shadcn/ui/switch';
import { useTranslations } from 'next-intl';
import { useEffect, useState } from 'react';
interface UserFormProps<T> {
onSubmit: (data: T) => Promise<boolean> | boolean;
initialValues?: T;
loading?: boolean;
trigger: string;
title: string;
update?: boolean;
}
export default function UserForm<T extends Record<string, any>>({
onSubmit,
initialValues,
loading,
trigger,
title,
}: UserFormProps<T>) {
const t = useTranslations('user');
const [open, setOpen] = useState(false);
const formSchema = z.object({
email: z.string().email(t('form.invalidEmailFormat')),
password: z.string().optional(),
referer_id: z.number().optional(),
refer_code: z.string().optional(),
is_admin: z.boolean().optional(),
balance: z.number().optional(),
});
const form = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
...initialValues,
},
});
useEffect(() => {
form?.reset(initialValues);
}, [form, initialValues]);
async function handleSubmit(data: { [x: string]: any }) {
const bool = await onSubmit(data as T);
if (bool) setOpen(false);
}
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button
size='sm'
onClick={() => {
form.reset();
setOpen(true);
}}
>
{trigger}
</Button>
</SheetTrigger>
<SheetContent className='w-[500px] max-w-full md:max-w-screen-md'>
<SheetHeader>
<SheetTitle>{title}</SheetTitle>
</SheetHeader>
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))]'>
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className='space-y-4 px-6 pt-4'>
<FormField
control={form.control}
name='email'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.userEmail')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder={t('form.userEmailPlaceholder')}
{...field}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='password'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.password')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder={t('form.passwordPlaceholder')}
{...field}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='referer_id'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.refererId')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder={t('form.refererIdPlaceholder')}
{...field}
type='number'
onValueChange={(value) => {
form.setValue(field.name, value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='refer_code'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.inviteCode')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder={t('form.inviteCodePlaceholder')}
{...field}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='balance'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.balance')}</FormLabel>
<FormControl>
<EnhancedInput
prefix='¥'
placeholder={t('form.balancePlaceholder')}
type='number'
{...field}
formatInput={(value) => unitConversion('centsToDollars', value)}
formatOutput={(value) => unitConversion('dollarsToCents', value)}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
min={0}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='is_admin'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.manager')}</FormLabel>
<FormControl>
<div className='pt-2'>
<Switch checked={!!field.value} onCheckedChange={field.onChange} />
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</ScrollArea>
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
<Button
variant='outline'
disabled={loading}
onClick={() => {
setOpen(false);
}}
>
{t('form.cancel')}
</Button>
<Button disabled={loading} onClick={form.handleSubmit(handleSubmit)}>
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}{' '}
{t('form.confirm')}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}