✨ feat(user): Add telephone input with area code selection and update localization
This commit is contained in:
parent
39a9ce60de
commit
585b99c2cc
@ -208,48 +208,44 @@ export default function Page() {
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('templateCode')}</Label>
|
||||
{platformConfig?.sms_template_code && (
|
||||
{platformConfig?.sms_template_code && (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('templateCode')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t('platformConfigTip', { key: platformConfig?.sms_template_code })}
|
||||
</p>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
value={data?.sms_template_code ?? ''}
|
||||
onValueBlur={(value) => updateConfig('sms_template_code', value)}
|
||||
disabled={isFetching}
|
||||
placeholder={
|
||||
platformConfig?.sms_template_code &&
|
||||
t('platformConfigTip', { key: platformConfig?.sms_template_code })
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('templateParam')}</Label>
|
||||
{platformConfig?.sms_template_param && (
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
value={data?.sms_template_code ?? ''}
|
||||
onValueBlur={(value) => updateConfig('sms_template_code', value)}
|
||||
disabled={isFetching}
|
||||
placeholder={t('platformConfigTip', { key: platformConfig?.sms_template_code })}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{platformConfig?.sms_template_param && (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('templateParam')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t('platformConfigTip', { key: platformConfig?.sms_template_param })}
|
||||
</p>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
value={data?.sms_template_param ?? 'code'}
|
||||
onValueBlur={(value) => updateConfig('sms_template_param', value)}
|
||||
disabled={isFetching}
|
||||
placeholder={
|
||||
platformConfig?.sms_template_param &&
|
||||
t('platformConfigTip', { key: platformConfig?.sms_template_param })
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
value={data?.sms_template_param ?? 'code'}
|
||||
onValueBlur={(value) => updateConfig('sms_template_param', value)}
|
||||
disabled={isFetching}
|
||||
placeholder={t('platformConfigTip', {
|
||||
key: platformConfig?.sms_template_param,
|
||||
})}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('template')}</Label>
|
||||
|
||||
@ -75,6 +75,14 @@ export default function Page() {
|
||||
accessorKey: 'email',
|
||||
header: t('userName'),
|
||||
},
|
||||
{
|
||||
accessorKey: 'telephone',
|
||||
header: t('telephone'),
|
||||
cell: ({ row }) => {
|
||||
if (!row.original.telephone) return '--';
|
||||
return `+${row.original.telephone_area_code} ${row.original.telephone}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'balance',
|
||||
header: t('balance'),
|
||||
|
||||
@ -22,6 +22,7 @@ import {
|
||||
SheetTrigger,
|
||||
} from '@workspace/ui/components/sheet';
|
||||
import { Switch } from '@workspace/ui/components/switch';
|
||||
import { AreaCodeSelect } from '@workspace/ui/custom-components/area-code-select';
|
||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||
import { unitConversion } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
@ -52,6 +53,8 @@ export default function UserForm<T extends Record<string, any>>({
|
||||
const [open, setOpen] = useState(false);
|
||||
const formSchema = z.object({
|
||||
email: z.string().email(t('form.invalidEmailFormat')),
|
||||
telephone_area_code: z.string().optional(),
|
||||
telephone: z.string().optional(),
|
||||
password: z.string().optional(),
|
||||
referer_id: z.number().optional(),
|
||||
refer_code: z.string().optional(),
|
||||
@ -115,6 +118,48 @@ export default function UserForm<T extends Record<string, any>>({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='telephone'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('form.telephone')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
prefix={
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='telephone_area_code'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<AreaCodeSelect
|
||||
className='w-32 rounded-none border-y-0 border-l-0'
|
||||
simple
|
||||
placeholder={t('form.areaCodePlaceholder')}
|
||||
value={field.value}
|
||||
onChange={(value) => {
|
||||
form.setValue(field.name, value.phone);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
placeholder={t('form.telephonePlaceholder')}
|
||||
{...field}
|
||||
onValueChange={(value) => {
|
||||
form.setValue(field.name, value);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='password'
|
||||
|
||||
@ -33,12 +33,17 @@
|
||||
"passwordPlaceholder": "Enter new password (optional)",
|
||||
"refererId": "Referer ID",
|
||||
"refererIdPlaceholder": "Enter referer ID",
|
||||
"telephone": "Phone Number",
|
||||
"telephonePlaceholder": "Enter phone number",
|
||||
"areaCode": "Area Code",
|
||||
"areaCodePlaceholder": "Area code",
|
||||
"userEmail": "User Email",
|
||||
"userEmailPlaceholder": "Enter user email"
|
||||
},
|
||||
"giftAmount": "Gift Amount",
|
||||
"inviteCode": "Invite Code",
|
||||
"referer": "Referer",
|
||||
"telephone": "Phone Number",
|
||||
"updateSuccess": "Update successful",
|
||||
"userList": "User List",
|
||||
"userName": "User Email"
|
||||
|
||||
@ -1,14 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import { Icon } from '@iconify/react';
|
||||
import { Combobox } from '@workspace/ui/custom-components/combobox';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from '@workspace/ui/components/command';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@workspace/ui/components/popover';
|
||||
import { cn } from '@workspace/ui/lib/utils';
|
||||
import { countries, type ICountry } from '@workspace/ui/utils/countries';
|
||||
import { BoxIcon, Check, ChevronsUpDown } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface AreaCodeSelectProps {
|
||||
value?: string; // phone number
|
||||
value?: string;
|
||||
onChange?: (value: ICountry) => void;
|
||||
className?: string;
|
||||
placeholder?: string;
|
||||
simple?: boolean;
|
||||
}
|
||||
|
||||
const items = countries
|
||||
@ -30,34 +43,73 @@ export const AreaCodeSelect = ({
|
||||
onChange,
|
||||
className,
|
||||
placeholder = 'Select Area Code',
|
||||
simple = false,
|
||||
}: AreaCodeSelectProps) => {
|
||||
const [selectedIndex, setSelectedIndex] = useState<number>(-1);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedItem, setSelectedItem] = useState<ICountry | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
const index = items.findIndex((item) => item.phone === value);
|
||||
setSelectedIndex(index);
|
||||
}, [value]);
|
||||
if (value !== selectedItem?.phone) {
|
||||
const found = items.find((item) => item.phone === value);
|
||||
setSelectedItem(found);
|
||||
}
|
||||
}, [selectedItem?.phone, value]);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
placeholder={placeholder}
|
||||
className={cn('min-w-fit', className)}
|
||||
options={items.map((item, index) => ({
|
||||
label: `+${item.phone} (${item.name})`,
|
||||
value: index,
|
||||
children: (
|
||||
<div className='flex items-center gap-2'>
|
||||
<Icon icon={`flagpack:${item.alpha2.toLowerCase()}`} className='!size-5' />+{item.phone}{' '}
|
||||
({item.name})
|
||||
</div>
|
||||
),
|
||||
}))}
|
||||
value={selectedIndex >= 0 ? selectedIndex : undefined}
|
||||
onChange={(index) => {
|
||||
if (typeof index !== 'number') return;
|
||||
setSelectedIndex(index);
|
||||
if (items[index]) onChange?.(items[index]);
|
||||
}}
|
||||
/>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant='outline'
|
||||
role='combobox'
|
||||
aria-expanded={open}
|
||||
className={cn('justify-between', className)}
|
||||
>
|
||||
{selectedItem ? (
|
||||
<div className='flex items-center gap-2'>
|
||||
<Icon icon={`flagpack:${selectedItem.alpha2.toLowerCase()}`} className='!size-5' />+
|
||||
{selectedItem.phone}
|
||||
{!simple && `(${selectedItem.name})`}
|
||||
</div>
|
||||
) : (
|
||||
placeholder
|
||||
)}
|
||||
<ChevronsUpDown className='ml-2 h-4 w-4 opacity-50' />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className='p-0' align='start'>
|
||||
<Command>
|
||||
<CommandInput placeholder='Search area code...' />
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
<BoxIcon className='inline-block text-slate-500' />
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{items.map((item) => (
|
||||
<CommandItem
|
||||
key={`${item.alpha2}-${item.phone}`}
|
||||
value={`${item.phone}-${item.name}`}
|
||||
onSelect={() => {
|
||||
setSelectedItem(item);
|
||||
onChange?.(item);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Icon icon={`flagpack:${item.alpha2.toLowerCase()}`} className='!size-5' />+
|
||||
{item.phone} ({item.name})
|
||||
</div>
|
||||
<Check
|
||||
className={cn(
|
||||
'ml-auto h-4 w-4',
|
||||
selectedItem?.phone === item.phone ? 'opacity-100' : 'opacity-0',
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
@ -4,8 +4,8 @@ import { ChangeEvent, ReactNode, useEffect, useState } from 'react';
|
||||
|
||||
export interface EnhancedInputProps
|
||||
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'prefix'> {
|
||||
prefix?: ReactNode;
|
||||
suffix?: ReactNode;
|
||||
prefix?: string | ReactNode;
|
||||
suffix?: string | ReactNode;
|
||||
formatInput?: (value: string | number) => string;
|
||||
formatOutput?: (value: string | number) => string | number;
|
||||
onValueChange?: (value: string | number) => void;
|
||||
@ -70,29 +70,38 @@ export function EnhancedInput({
|
||||
onValueBlur?.(outputValue);
|
||||
}
|
||||
};
|
||||
const renderPrefix = () => {
|
||||
return typeof prefix === 'string' ? (
|
||||
<div className='bg-muted relative mr-px flex h-9 items-center text-nowrap px-3'>{prefix}</div>
|
||||
) : (
|
||||
prefix
|
||||
);
|
||||
};
|
||||
const renderSuffix = () => {
|
||||
return typeof suffix === 'string' ? (
|
||||
<div className='bg-muted relative ml-px flex h-9 items-center text-nowrap px-3'>{suffix}</div>
|
||||
) : (
|
||||
suffix
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('border-input flex w-full items-center rounded-md border', className)}
|
||||
className={cn(
|
||||
'border-input flex w-full items-center overflow-hidden rounded-md border',
|
||||
className,
|
||||
)}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
{prefix && (
|
||||
<div className='bg-muted relative mr-px flex h-9 items-center text-nowrap px-3'>
|
||||
{prefix}
|
||||
</div>
|
||||
)}
|
||||
{renderPrefix()}
|
||||
<Input
|
||||
{...props}
|
||||
value={value}
|
||||
className='border-none'
|
||||
className='rounded-none border-none'
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
{suffix && (
|
||||
<div className='bg-muted relative ml-px flex h-9 items-center text-nowrap px-3'>
|
||||
{suffix}
|
||||
</div>
|
||||
)}
|
||||
{renderSuffix()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user