first commit

This commit is contained in:
lendry
2026-06-24 14:37:15 +03:00
commit 995adeedd4
188 changed files with 28810 additions and 0 deletions

View 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>
);
}