more fix and update
This commit is contained in:
@@ -28,6 +28,8 @@ const buttonVariants = cva(
|
||||
}
|
||||
);
|
||||
|
||||
export { buttonVariants };
|
||||
|
||||
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
79
apps/frontend/components/ui/calendar.tsx
Normal file
79
apps/frontend/components/ui/calendar.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { DayPicker, getDefaultClassNames, type DayButtonProps } from 'react-day-picker';
|
||||
import { ru } from 'react-day-picker/locale';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
|
||||
function CalendarDayButton({ className, day, modifiers, ...props }: DayButtonProps) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
buttonVariants({ variant: 'ghost' }),
|
||||
'h-9 w-9 p-0 font-normal aria-selected:opacity-100',
|
||||
modifiers.selected && 'bg-[#111827] text-white hover:bg-[#111827] hover:text-white',
|
||||
modifiers.today && !modifiers.selected && 'bg-[#f4f5f8] text-[#111827]',
|
||||
modifiers.outside && 'text-[#a8adbc] opacity-50',
|
||||
modifiers.disabled && 'text-[#a8adbc] opacity-50',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Calendar({ className, classNames, showOutsideDays = true, ...props }: React.ComponentProps<typeof DayPicker>) {
|
||||
const defaults = getDefaultClassNames();
|
||||
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
locale={ru}
|
||||
className={cn('p-3', className)}
|
||||
classNames={{
|
||||
root: cn('rdp-root', defaults.root),
|
||||
months: cn('flex flex-col gap-4 sm:flex-row', defaults.months),
|
||||
month: cn('flex flex-col gap-4', defaults.month),
|
||||
month_caption: cn('flex justify-center pt-1 relative items-center', defaults.month_caption),
|
||||
caption_label: cn('text-sm font-medium capitalize', defaults.caption_label),
|
||||
dropdowns: cn('flex items-center justify-center gap-2', defaults.dropdowns),
|
||||
dropdown_root: cn('relative inline-flex items-center gap-1', defaults.dropdown_root),
|
||||
dropdown: defaults.dropdown,
|
||||
months_dropdown: defaults.months_dropdown,
|
||||
years_dropdown: defaults.years_dropdown,
|
||||
nav: cn('flex items-center gap-1', defaults.nav),
|
||||
button_previous: cn(
|
||||
buttonVariants({ variant: 'ghost' }),
|
||||
'absolute left-1 h-7 w-7 bg-transparent p-0 opacity-70 hover:opacity-100',
|
||||
defaults.button_previous
|
||||
),
|
||||
button_next: cn(
|
||||
buttonVariants({ variant: 'ghost' }),
|
||||
'absolute right-1 h-7 w-7 bg-transparent p-0 opacity-70 hover:opacity-100',
|
||||
defaults.button_next
|
||||
),
|
||||
month_grid: cn('w-full border-collapse space-y-1', defaults.month_grid),
|
||||
weekdays: cn('flex', defaults.weekdays),
|
||||
weekday: cn('text-[#667085] rounded-md w-9 font-normal text-[0.8rem]', defaults.weekday),
|
||||
week: cn('flex w-full mt-2', defaults.week),
|
||||
day: cn('relative p-0 text-center text-sm focus-within:relative focus-within:z-20', defaults.day),
|
||||
day_button: cn('h-9 w-9 p-0 font-normal aria-selected:opacity-100', defaults.day_button),
|
||||
selected: cn('bg-[#111827] text-white hover:bg-[#111827] hover:text-white focus:bg-[#111827] focus:text-white', defaults.selected),
|
||||
today: cn('bg-[#f4f5f8] text-[#111827]', defaults.today),
|
||||
outside: cn('text-[#a8adbc] opacity-50', defaults.outside),
|
||||
disabled: cn('text-[#a8adbc] opacity-50', defaults.disabled),
|
||||
hidden: cn('invisible', defaults.hidden),
|
||||
...classNames
|
||||
}}
|
||||
components={{
|
||||
Chevron: ({ orientation }) => (orientation === 'left' ? <ChevronLeft className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />),
|
||||
DayButton: CalendarDayButton
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
59
apps/frontend/components/ui/date-picker.tsx
Normal file
59
apps/frontend/components/ui/date-picker.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
'use client';
|
||||
|
||||
import { format, parseISO } from 'date-fns';
|
||||
import { ru } from 'date-fns/locale';
|
||||
import { CalendarIcon } from 'lucide-react';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Calendar } from '@/components/ui/calendar';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export function DatePicker({
|
||||
value,
|
||||
onChange,
|
||||
placeholder = 'Дата рождения',
|
||||
className,
|
||||
disabled
|
||||
}: {
|
||||
value?: string;
|
||||
onChange: (value: string | undefined) => void;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const selected = value ? parseISO(value) : undefined;
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
'h-11 w-full justify-start rounded-2xl border border-[#eceef4] bg-white px-4 text-left font-normal text-[#111827] hover:bg-white',
|
||||
!value && 'text-[#667085]',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<CalendarIcon className="mr-2 h-4 w-4 text-[#667085]" />
|
||||
{value ? format(parseISO(value), 'd MMMM yyyy', { locale: ru }) : placeholder}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={selected}
|
||||
onSelect={(date) => onChange(date ? format(date, 'yyyy-MM-dd') : undefined)}
|
||||
disabled={(date) => date > new Date()}
|
||||
defaultMonth={selected ?? new Date(1990, 0, 1)}
|
||||
captionLayout="dropdown"
|
||||
hideNavigation
|
||||
startMonth={new Date(1920, 0)}
|
||||
endMonth={new Date()}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -3,16 +3,35 @@
|
||||
import { useRef } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const otpThemes = {
|
||||
dark: {
|
||||
box: 'border-[#555762] bg-transparent text-white focus:border-[#7b7f8f] focus:ring-white/10'
|
||||
},
|
||||
light: {
|
||||
box: 'border-[#eceef4] bg-white text-[#111827] focus:border-[#c7cad6] focus:ring-[#eceef4]'
|
||||
}
|
||||
} as const;
|
||||
|
||||
interface OtpInputProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
onComplete?: (value: string) => void;
|
||||
length?: number;
|
||||
disabled?: boolean;
|
||||
variant?: keyof typeof otpThemes;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function OtpInput({ value, onChange, onComplete, length = 6, disabled = false, className }: OtpInputProps) {
|
||||
export function OtpInput({
|
||||
value,
|
||||
onChange,
|
||||
onComplete,
|
||||
length = 6,
|
||||
disabled = false,
|
||||
variant = 'dark',
|
||||
className
|
||||
}: OtpInputProps) {
|
||||
const theme = otpThemes[variant];
|
||||
const inputsRef = useRef<Array<HTMLInputElement | null>>([]);
|
||||
const digits = Array.from({ length }, (_, index) => value[index] ?? '');
|
||||
|
||||
@@ -83,7 +102,10 @@ export function OtpInput({ value, onChange, onComplete, length = 6, disabled = f
|
||||
onChange={(event) => handleChange(index, event.target.value)}
|
||||
onKeyDown={(event) => handleKeyDown(index, event)}
|
||||
onPaste={handlePaste}
|
||||
className="h-14 w-full rounded-2xl border border-[#555762] bg-transparent text-center text-2xl font-semibold text-white outline-none transition focus:border-[#7b7f8f] focus:ring-4 focus:ring-white/10 disabled:opacity-50"
|
||||
className={cn(
|
||||
'h-14 w-full rounded-2xl border text-center text-2xl font-semibold outline-none transition focus:ring-4 disabled:opacity-50',
|
||||
theme.box
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -65,34 +65,71 @@ export function toE164(country: CountryOption, digits: string) {
|
||||
return `${country.code}${digits.replace(/\D/g, '').slice(0, country.maxDigits)}`;
|
||||
}
|
||||
|
||||
export function parseE164Phone(e164: string): { country: CountryOption; digits: string } {
|
||||
if (!e164) {
|
||||
return { country: phoneCountries[0], digits: '' };
|
||||
}
|
||||
const sorted = [...phoneCountries].sort((a, b) => b.code.length - a.code.length);
|
||||
for (const country of sorted) {
|
||||
if (e164.startsWith(country.code)) {
|
||||
return { country, digits: e164.slice(country.code.length).replace(/\D/g, '') };
|
||||
}
|
||||
}
|
||||
return { country: phoneCountries[0], digits: e164.replace(/\D/g, '') };
|
||||
}
|
||||
|
||||
const phoneThemes = {
|
||||
dark: {
|
||||
shell: 'border-[#555762] bg-transparent text-white focus-within:border-[#7b7f8f] focus-within:ring-white/10',
|
||||
trigger: 'text-white hover:bg-[#2a2c36]',
|
||||
chevron: 'text-[#8f92a0]',
|
||||
input: 'text-white placeholder:text-[#8f92a0]',
|
||||
clear: 'text-[#8f92a0] hover:bg-[#2a2c36] hover:text-white'
|
||||
},
|
||||
light: {
|
||||
shell: 'border-[#eceef4] bg-white text-[#111827] focus-within:border-[#c7cad6] focus-within:ring-[#eceef4]',
|
||||
trigger: 'text-[#111827] hover:bg-[#f4f5f8]',
|
||||
chevron: 'text-[#667085]',
|
||||
input: 'text-[#111827] placeholder:text-[#667085]',
|
||||
clear: 'text-[#667085] hover:bg-[#f4f5f8] hover:text-[#111827]'
|
||||
}
|
||||
} as const;
|
||||
|
||||
export function PhoneInput({
|
||||
country,
|
||||
value,
|
||||
onCountryChange,
|
||||
onValueChange,
|
||||
className
|
||||
className,
|
||||
variant = 'dark',
|
||||
required = true
|
||||
}: {
|
||||
country: CountryOption;
|
||||
value: string;
|
||||
onCountryChange: (country: CountryOption) => void;
|
||||
onValueChange: (digits: string) => void;
|
||||
className?: string;
|
||||
variant?: keyof typeof phoneThemes;
|
||||
required?: boolean;
|
||||
}) {
|
||||
const maskedValue = formatPhoneNumber(value, country);
|
||||
const theme = phoneThemes[variant];
|
||||
|
||||
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',
|
||||
'flex h-[58px] w-full items-center rounded-2xl border transition focus-within:ring-4',
|
||||
variant === 'light' ? 'h-11' : '',
|
||||
theme.shell,
|
||||
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="Выбрать страну">
|
||||
<button type="button" className={cn('flex h-full shrink-0 items-center gap-2 rounded-l-2xl px-4 outline-none', theme.trigger)} aria-label="Выбрать страну">
|
||||
<FlagIcon src={country.flagSrc} />
|
||||
<span className="text-base font-semibold">{country.code}</span>
|
||||
<ChevronDown className="h-4 w-4 text-[#8f92a0]" />
|
||||
<ChevronDown className={cn('h-4 w-4', theme.chevron)} />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80 p-1">
|
||||
@@ -122,16 +159,16 @@ export function PhoneInput({
|
||||
</Popover>
|
||||
|
||||
<input
|
||||
className="h-full min-w-0 flex-1 bg-transparent px-1 text-lg text-white outline-none placeholder:text-[#8f92a0]"
|
||||
className={cn('h-full min-w-0 flex-1 bg-transparent px-1 text-lg outline-none', variant === 'light' ? 'text-base' : '', theme.input)}
|
||||
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
|
||||
required={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="Очистить телефон">
|
||||
<button type="button" className={cn('mr-3 flex h-8 w-8 shrink-0 items-center justify-center rounded-full', theme.clear)} onClick={() => onValueChange('')} aria-label="Очистить телефон">
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
Reference in New Issue
Block a user