feat(user): Add telephone input with area code selection and update localization

This commit is contained in:
web@ppanel 2025-01-14 19:24:46 +07:00
parent 39a9ce60de
commit 585b99c2cc
6 changed files with 191 additions and 76 deletions

View File

@ -208,48 +208,44 @@ export default function Page() {
/> />
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow> {platformConfig?.sms_template_code && (
<TableCell> <TableRow>
<Label>{t('templateCode')}</Label> <TableCell>
{platformConfig?.sms_template_code && ( <Label>{t('templateCode')}</Label>
<p className='text-muted-foreground text-xs'> <p className='text-muted-foreground text-xs'>
{t('platformConfigTip', { key: platformConfig?.sms_template_code })} {t('platformConfigTip', { key: platformConfig?.sms_template_code })}
</p> </p>
)} </TableCell>
</TableCell> <TableCell className='text-right'>
<TableCell className='text-right'> <EnhancedInput
<EnhancedInput value={data?.sms_template_code ?? ''}
value={data?.sms_template_code ?? ''} onValueBlur={(value) => updateConfig('sms_template_code', value)}
onValueBlur={(value) => updateConfig('sms_template_code', value)} disabled={isFetching}
disabled={isFetching} placeholder={t('platformConfigTip', { key: platformConfig?.sms_template_code })}
placeholder={ />
platformConfig?.sms_template_code && </TableCell>
t('platformConfigTip', { key: platformConfig?.sms_template_code }) </TableRow>
} )}
/> {platformConfig?.sms_template_param && (
</TableCell> <TableRow>
</TableRow> <TableCell>
<TableRow> <Label>{t('templateParam')}</Label>
<TableCell>
<Label>{t('templateParam')}</Label>
{platformConfig?.sms_template_param && (
<p className='text-muted-foreground text-xs'> <p className='text-muted-foreground text-xs'>
{t('platformConfigTip', { key: platformConfig?.sms_template_param })} {t('platformConfigTip', { key: platformConfig?.sms_template_param })}
</p> </p>
)} </TableCell>
</TableCell> <TableCell className='text-right'>
<TableCell className='text-right'> <EnhancedInput
<EnhancedInput value={data?.sms_template_param ?? 'code'}
value={data?.sms_template_param ?? 'code'} onValueBlur={(value) => updateConfig('sms_template_param', value)}
onValueBlur={(value) => updateConfig('sms_template_param', value)} disabled={isFetching}
disabled={isFetching} placeholder={t('platformConfigTip', {
placeholder={ key: platformConfig?.sms_template_param,
platformConfig?.sms_template_param && })}
t('platformConfigTip', { key: platformConfig?.sms_template_param }) />
} </TableCell>
/> </TableRow>
</TableCell> )}
</TableRow>
<TableRow> <TableRow>
<TableCell> <TableCell>
<Label>{t('template')}</Label> <Label>{t('template')}</Label>

View File

@ -75,6 +75,14 @@ export default function Page() {
accessorKey: 'email', accessorKey: 'email',
header: t('userName'), 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', accessorKey: 'balance',
header: t('balance'), header: t('balance'),

View File

@ -22,6 +22,7 @@ import {
SheetTrigger, SheetTrigger,
} from '@workspace/ui/components/sheet'; } from '@workspace/ui/components/sheet';
import { Switch } from '@workspace/ui/components/switch'; 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 { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { unitConversion } from '@workspace/ui/utils'; import { unitConversion } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
@ -52,6 +53,8 @@ export default function UserForm<T extends Record<string, any>>({
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const formSchema = z.object({ const formSchema = z.object({
email: z.string().email(t('form.invalidEmailFormat')), email: z.string().email(t('form.invalidEmailFormat')),
telephone_area_code: z.string().optional(),
telephone: z.string().optional(),
password: z.string().optional(), password: z.string().optional(),
referer_id: z.number().optional(), referer_id: z.number().optional(),
refer_code: z.string().optional(), refer_code: z.string().optional(),
@ -115,6 +118,48 @@ export default function UserForm<T extends Record<string, any>>({
</FormItem> </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 <FormField
control={form.control} control={form.control}
name='password' name='password'

View File

@ -33,12 +33,17 @@
"passwordPlaceholder": "Enter new password (optional)", "passwordPlaceholder": "Enter new password (optional)",
"refererId": "Referer ID", "refererId": "Referer ID",
"refererIdPlaceholder": "Enter referer ID", "refererIdPlaceholder": "Enter referer ID",
"telephone": "Phone Number",
"telephonePlaceholder": "Enter phone number",
"areaCode": "Area Code",
"areaCodePlaceholder": "Area code",
"userEmail": "User Email", "userEmail": "User Email",
"userEmailPlaceholder": "Enter user email" "userEmailPlaceholder": "Enter user email"
}, },
"giftAmount": "Gift Amount", "giftAmount": "Gift Amount",
"inviteCode": "Invite Code", "inviteCode": "Invite Code",
"referer": "Referer", "referer": "Referer",
"telephone": "Phone Number",
"updateSuccess": "Update successful", "updateSuccess": "Update successful",
"userList": "User List", "userList": "User List",
"userName": "User Email" "userName": "User Email"

View File

@ -1,14 +1,27 @@
'use client';
import { Icon } from '@iconify/react'; 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 { cn } from '@workspace/ui/lib/utils';
import { countries, type ICountry } from '@workspace/ui/utils/countries'; import { countries, type ICountry } from '@workspace/ui/utils/countries';
import { BoxIcon, Check, ChevronsUpDown } from 'lucide-react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
interface AreaCodeSelectProps { interface AreaCodeSelectProps {
value?: string; // phone number value?: string;
onChange?: (value: ICountry) => void; onChange?: (value: ICountry) => void;
className?: string; className?: string;
placeholder?: string; placeholder?: string;
simple?: boolean;
} }
const items = countries const items = countries
@ -30,34 +43,73 @@ export const AreaCodeSelect = ({
onChange, onChange,
className, className,
placeholder = 'Select Area Code', placeholder = 'Select Area Code',
simple = false,
}: AreaCodeSelectProps) => { }: AreaCodeSelectProps) => {
const [selectedIndex, setSelectedIndex] = useState<number>(-1); const [open, setOpen] = useState(false);
const [selectedItem, setSelectedItem] = useState<ICountry | undefined>();
useEffect(() => { useEffect(() => {
const index = items.findIndex((item) => item.phone === value); if (value !== selectedItem?.phone) {
setSelectedIndex(index); const found = items.find((item) => item.phone === value);
}, [value]); setSelectedItem(found);
}
}, [selectedItem?.phone, value]);
return ( return (
<Combobox <Popover open={open} onOpenChange={setOpen}>
placeholder={placeholder} <PopoverTrigger asChild>
className={cn('min-w-fit', className)} <Button
options={items.map((item, index) => ({ variant='outline'
label: `+${item.phone} (${item.name})`, role='combobox'
value: index, aria-expanded={open}
children: ( className={cn('justify-between', className)}
<div className='flex items-center gap-2'> >
<Icon icon={`flagpack:${item.alpha2.toLowerCase()}`} className='!size-5' />+{item.phone}{' '} {selectedItem ? (
({item.name}) <div className='flex items-center gap-2'>
</div> <Icon icon={`flagpack:${selectedItem.alpha2.toLowerCase()}`} className='!size-5' />+
), {selectedItem.phone}
}))} {!simple && `(${selectedItem.name})`}
value={selectedIndex >= 0 ? selectedIndex : undefined} </div>
onChange={(index) => { ) : (
if (typeof index !== 'number') return; placeholder
setSelectedIndex(index); )}
if (items[index]) onChange?.(items[index]); <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>
); );
}; };

View File

@ -4,8 +4,8 @@ import { ChangeEvent, ReactNode, useEffect, useState } from 'react';
export interface EnhancedInputProps export interface EnhancedInputProps
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'prefix'> { extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'prefix'> {
prefix?: ReactNode; prefix?: string | ReactNode;
suffix?: ReactNode; suffix?: string | ReactNode;
formatInput?: (value: string | number) => string; formatInput?: (value: string | number) => string;
formatOutput?: (value: string | number) => string | number; formatOutput?: (value: string | number) => string | number;
onValueChange?: (value: string | number) => void; onValueChange?: (value: string | number) => void;
@ -70,29 +70,38 @@ export function EnhancedInput({
onValueBlur?.(outputValue); 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 ( return (
<div <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 suppressHydrationWarning
> >
{prefix && ( {renderPrefix()}
<div className='bg-muted relative mr-px flex h-9 items-center text-nowrap px-3'>
{prefix}
</div>
)}
<Input <Input
{...props} {...props}
value={value} value={value}
className='border-none' className='rounded-none border-none'
onChange={handleChange} onChange={handleChange}
onBlur={handleBlur} onBlur={handleBlur}
/> />
{suffix && ( {renderSuffix()}
<div className='bg-muted relative ml-px flex h-9 items-center text-nowrap px-3'>
{suffix}
</div>
)}
</div> </div>
); );
} }