first commit
This commit is contained in:
140
apps/frontend/components/ui/phone-input.tsx
Normal file
140
apps/frontend/components/ui/phone-input.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
'use client';
|
||||
|
||||
import { ChevronDown, X } from 'lucide-react';
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface CountryOption {
|
||||
id: string;
|
||||
code: string;
|
||||
label: string;
|
||||
maxDigits: number;
|
||||
/** Path to the country flag asset inside the public directory. */
|
||||
flagSrc: string;
|
||||
}
|
||||
|
||||
export const FLAG_WIDTH = 24;
|
||||
export const FLAG_HEIGHT = 18;
|
||||
|
||||
export const phoneCountries: CountryOption[] = [
|
||||
{ id: 'RU', code: '+7', label: 'Россия', maxDigits: 10, flagSrc: '/flags/ru.svg' },
|
||||
{ id: 'BY', code: '+375', label: 'Беларусь', maxDigits: 9, flagSrc: '/flags/by.svg' },
|
||||
{ id: 'KZ', code: '+7', label: 'Казахстан', maxDigits: 10, flagSrc: '/flags/kz.svg' }
|
||||
];
|
||||
|
||||
function FlagIcon({ src, className }: { src: string; className?: string }) {
|
||||
return (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={cn('inline-block shrink-0 overflow-hidden rounded-[3px] bg-center bg-no-repeat shadow-[inset_0_0_0_1px_rgba(0,0,0,0.08)]', className)}
|
||||
style={{
|
||||
width: `${FLAG_WIDTH}px`,
|
||||
height: `${FLAG_HEIGHT}px`,
|
||||
backgroundImage: `url('${src}')`,
|
||||
backgroundSize: 'cover'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function formatPhoneNumber(digits: string, country: CountryOption) {
|
||||
const clean = digits.replace(/\D/g, '').slice(0, country.maxDigits);
|
||||
if (country.code === '+375') {
|
||||
const part1 = clean.slice(0, 2);
|
||||
const part2 = clean.slice(2, 5);
|
||||
const part3 = clean.slice(5, 7);
|
||||
const part4 = clean.slice(7, 9);
|
||||
if (clean.length <= 2) return part1 ? `(${part1}` : '';
|
||||
if (clean.length <= 5) return `(${part1}) ${part2}`;
|
||||
if (clean.length <= 7) return `(${part1}) ${part2}-${part3}`;
|
||||
return `(${part1}) ${part2}-${part3}-${part4}`;
|
||||
}
|
||||
|
||||
const part1 = clean.slice(0, 3);
|
||||
const part2 = clean.slice(3, 6);
|
||||
const part3 = clean.slice(6, 8);
|
||||
const part4 = clean.slice(8, 10);
|
||||
if (clean.length <= 3) return part1 ? `(${part1}` : '';
|
||||
if (clean.length <= 6) return `(${part1}) ${part2}`;
|
||||
if (clean.length <= 8) return `(${part1}) ${part2}-${part3}`;
|
||||
return `(${part1}) ${part2}-${part3}-${part4}`;
|
||||
}
|
||||
|
||||
export function toE164(country: CountryOption, digits: string) {
|
||||
return `${country.code}${digits.replace(/\D/g, '').slice(0, country.maxDigits)}`;
|
||||
}
|
||||
|
||||
export function PhoneInput({
|
||||
country,
|
||||
value,
|
||||
onCountryChange,
|
||||
onValueChange,
|
||||
className
|
||||
}: {
|
||||
country: CountryOption;
|
||||
value: string;
|
||||
onCountryChange: (country: CountryOption) => void;
|
||||
onValueChange: (digits: string) => void;
|
||||
className?: string;
|
||||
}) {
|
||||
const maskedValue = formatPhoneNumber(value, country);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-[58px] w-full items-center rounded-2xl border border-[#555762] bg-transparent text-white transition focus-within:border-[#7b7f8f] focus-within:ring-4 focus-within:ring-white/10',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<button type="button" className="flex h-full shrink-0 items-center gap-2 rounded-l-2xl px-4 text-white outline-none hover:bg-[#2a2c36]" aria-label="Выбрать страну">
|
||||
<FlagIcon src={country.flagSrc} />
|
||||
<span className="text-base font-semibold">{country.code}</span>
|
||||
<ChevronDown className="h-4 w-4 text-[#8f92a0]" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80 p-1">
|
||||
<Command>
|
||||
<CommandInput placeholder="Страна или код" />
|
||||
<CommandList>
|
||||
<CommandEmpty>Страна не найдена</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{phoneCountries.map((item) => (
|
||||
<CommandItem
|
||||
key={item.id}
|
||||
value={`${item.label} ${item.code}`}
|
||||
onSelect={() => {
|
||||
onCountryChange(item);
|
||||
onValueChange(value.replace(/\D/g, '').slice(0, item.maxDigits));
|
||||
}}
|
||||
>
|
||||
<FlagIcon src={item.flagSrc} />
|
||||
<span className="flex-1">{item.label}</span>
|
||||
<span className="font-semibold text-[#667085]">{item.code}</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<input
|
||||
className="h-full min-w-0 flex-1 bg-transparent px-1 text-lg text-white outline-none placeholder:text-[#8f92a0]"
|
||||
inputMode="tel"
|
||||
placeholder={country.code === '+375' ? '(29) 123-45-67' : '(999) 123-45-67'}
|
||||
value={maskedValue}
|
||||
onChange={(event) => onValueChange(event.target.value.replace(/\D/g, '').slice(0, country.maxDigits))}
|
||||
required
|
||||
/>
|
||||
|
||||
{value ? (
|
||||
<button type="button" className="mr-3 flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-[#8f92a0] hover:bg-[#2a2c36] hover:text-white" onClick={() => onValueChange('')} aria-label="Очистить телефон">
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user