Files
IdP/apps/frontend/components/ui/phone-input.tsx
2026-06-24 14:37:15 +03:00

141 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
);
}