✨ 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>
|
</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>
|
||||||
|
|||||||
@ -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'),
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user