This commit is contained in:
2025-12-26 18:26:09 -03:00
parent 3cdded360f
commit d0dcc0a953
11 changed files with 574 additions and 460 deletions

View File

@@ -1,63 +1,87 @@
import { useForm, Controller, useWatch } from 'react-hook-form'
import { useForm, Controller, useWatch, type Control } from 'react-hook-form'
import { PatternFormat } from 'react-number-format'
import { zodResolver } from '@hookform/resolvers/zod'
import { ErrorMessage } from '@hookform/error-message'
import z from 'zod'
import { ArrowRightIcon } from 'lucide-react'
import { ArrowRightIcon, CircleQuestionMarkIcon } from 'lucide-react'
import { Button } from '@repo/ui/components/ui/button'
import { Kbd } from '@repo/ui/components/ui/kbd'
import { Label } from '@repo/ui/components/ui/label'
import { RadioGroup, RadioGroupItem } from '@repo/ui/components/ui/radio-group'
import { Separator } from '@repo/ui/components/ui/separator'
import { Checkbox } from '@repo/ui/components/ui/checkbox'
import { Input } from '@repo/ui/components/ui/input'
import { Card, CardContent } from '@repo/ui/components/ui/card'
import {
HoverCard,
HoverCardContent,
HoverCardTrigger
} from '@repo/ui/components/ui/hover-card'
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet
} from '@repo/ui/components/ui/field'
import { Input } from '@repo/ui/components/ui/input'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '@repo/ui/components/ui/select'
import { Textarea } from '@repo/ui/components/ui/textarea'
NativeSelect,
NativeSelectOption
} from '@repo/ui/components/ui/native-select'
import { useWizard } from '@/components/wizard'
import { Card, CardContent } from '@repo/ui/components/ui/card'
const formSchema = z.object({
payment_method: z.enum(['PIX', 'BANK_SLIP', 'CREDIT_CARD'], {
error: 'Escolha uma forma de pagamento'
})
const creditCard = z.object({
holder_name: z.string().min(1),
number: z.string().min(13).max(19),
exp_month: z.string().min(2),
exp_year: z.string().min(4),
cvv: z.string().min(3).max(4)
})
const formSchema = z.discriminatedUnion(
'payment_method',
[
z.object({
payment_method: z.literal('PIX')
}),
z.object({
payment_method: z.literal('BANK_SLIP')
}),
z.object({
payment_method: z.literal('CREDIT_CARD'),
credit_card: creditCard
})
],
{ error: 'Escolha uma forma de pagamento' }
)
type Schema = z.infer<typeof formSchema>
export type CreditCard = z.infer<typeof creditCard>
type PaymentProps = {
onSubmit: (value: any) => void | Promise<void>
defaultValues?: object
payment_method?: 'PIX' | 'BANK_SLIP' | 'CREDIT_CARD'
credit_card?: CreditCard
}
export function Payment({ onSubmit, defaultValues }: PaymentProps) {
export function Payment({
onSubmit,
payment_method: paymentMethodInit,
credit_card: creditCardInit = undefined
}: PaymentProps) {
const wizard = useWizard()
const { control, handleSubmit } = useForm<Schema>({
defaultValues: {
payment_method: '' as any,
...defaultValues
payment_method: paymentMethodInit,
credit_card: creditCardInit
},
resolver: zodResolver(formSchema)
})
const paymentMethod = useWatch({ control, name: 'payment_method' })
const onSubmit_ = async (data: Schema) => {
await onSubmit(data)
await onSubmit({ credit_card: undefined, ...data })
wizard('review')
}
@@ -72,10 +96,9 @@ export function Payment({ onSubmit, defaultValues }: PaymentProps) {
value={value}
onValueChange={onChange}
className="lg:flex gap-3
*:p-5 *:border *:rounded-xl *:flex-1
*:cursor-pointer *:bg-accent/25
*:has-[button[data-state=checked]]:bg-accent
"
*:p-5 *:border *:rounded-xl *:flex-1
*:cursor-pointer *:bg-accent/25
*:has-[button[data-state=checked]]:bg-accent"
>
<Label>
<RadioGroupItem value="PIX" />
@@ -104,7 +127,9 @@ export function Payment({ onSubmit, defaultValues }: PaymentProps) {
)}
/>
{paymentMethod === 'CREDIT_CARD' ? <CreditCard /> : null}
{paymentMethod === 'CREDIT_CARD' ? (
<CreditCard control={control} />
) : null}
<Separator />
@@ -114,6 +139,7 @@ export function Payment({ onSubmit, defaultValues }: PaymentProps) {
variant="link"
className="text-black dark:text-white"
onClick={() => wizard('cart')}
tabIndex={-1}
>
Voltar
</Button>
@@ -125,72 +151,159 @@ export function Payment({ onSubmit, defaultValues }: PaymentProps) {
)
}
export function CreditCard() {
export function CreditCard({ control }: { control: Control<Schema> }) {
const currentYear = new Date().getFullYear()
const years = Array.from({ length: 10 }, (_, i) => currentYear + i)
return (
<Card className="lg:max-w-md">
<Card className="lg:w-1/2">
<CardContent>
<FieldGroup>
<FieldSet>
<FieldGroup>
<Field>
<FieldLabel htmlFor="checkout-7j9-card-number-uw1">
Número do cartão
</FieldLabel>
<Input id="checkout-7j9-card-number-uw1" required />
</Field>
<Controller
control={control}
name="credit_card.number"
render={({
field: { onChange, ref, ...field },
fieldState
}) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor={field.name}>
Número do cartão
</FieldLabel>
<PatternFormat
id={field.name}
format="#### #### #### ####"
mask="_"
placeholder="•••• •••• •••• ••••"
customInput={Input}
getInputRef={ref}
aria-invalid={fieldState.invalid}
onValueChange={({ value }) => {
onChange(value)
}}
{...field}
/>
</Field>
)}
/>
<Field>
<FieldLabel htmlFor="checkout-7j9-card-name-43j">
Nome do titular
</FieldLabel>
<Input required />
</Field>
<Controller
control={control}
name="credit_card.holder_name"
defaultValue=""
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor={field.name}>
Nome do titular
</FieldLabel>
<Input
id={field.name}
aria-invalid={fieldState.invalid}
{...field}
/>
</Field>
)}
/>
<div className="grid grid-cols-3 gap-4">
<Field>
<FieldLabel htmlFor="checkout-exp-month-ts6">Mês</FieldLabel>
<Select defaultValue="">
<SelectTrigger id="checkout-exp-month-ts6">
<SelectValue placeholder="MM" />
</SelectTrigger>
<SelectContent>
<SelectItem value="01">01</SelectItem>
<SelectItem value="02">02</SelectItem>
<SelectItem value="03">03</SelectItem>
<SelectItem value="04">04</SelectItem>
<SelectItem value="05">05</SelectItem>
<SelectItem value="06">06</SelectItem>
<SelectItem value="07">07</SelectItem>
<SelectItem value="08">08</SelectItem>
<SelectItem value="09">09</SelectItem>
<SelectItem value="10">10</SelectItem>
<SelectItem value="11">11</SelectItem>
<SelectItem value="12">12</SelectItem>
</SelectContent>
</Select>
</Field>
<Field>
<FieldLabel htmlFor="checkout-7j9-exp-year-f59">
Ano
</FieldLabel>
<Select defaultValue="">
<SelectTrigger id="checkout-7j9-exp-year-f59">
<SelectValue placeholder="AAAA" />
</SelectTrigger>
<SelectContent>
<SelectItem value="2024">2024</SelectItem>
<SelectItem value="2025">2025</SelectItem>
<SelectItem value="2026">2026</SelectItem>
<SelectItem value="2027">2027</SelectItem>
<SelectItem value="2028">2028</SelectItem>
<SelectItem value="2029">2029</SelectItem>
</SelectContent>
</Select>
</Field>
<Field>
<FieldLabel htmlFor="checkout-7j9-cvv">CVC</FieldLabel>
<Input id="checkout-7j9-cvv" placeholder="123" required />
</Field>
<Controller
control={control}
name="credit_card.exp_month"
defaultValue=""
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor={field.name}>Mês</FieldLabel>
<NativeSelect
id={field.name}
value={field.value}
onChange={field.onChange}
aria-invalid={fieldState.invalid}
>
<NativeSelectOption value="" disabled>
Selecione
</NativeSelectOption>
{Array.from({ length: 12 }, (_, i) => {
const v = String(i + 1).padStart(2, '0')
return (
<NativeSelectOption key={v} value={v}>
{v}
</NativeSelectOption>
)
})}
</NativeSelect>
</Field>
)}
/>
<Controller
control={control}
name="credit_card.exp_year"
defaultValue=""
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor={field.name}>Ano</FieldLabel>
<NativeSelect
id={field.name}
value={field.value}
onChange={field.onChange}
aria-invalid={fieldState.invalid}
>
<NativeSelectOption value="" disabled>
Selecione
</NativeSelectOption>
{years.map((year) => (
<NativeSelectOption key={year} value={String(year)}>
{year}
</NativeSelectOption>
))}
</NativeSelect>
</Field>
)}
/>
<Controller
control={control}
name="credit_card.cvv"
defaultValue=""
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor={field.name}>
CVC
<HoverCard openDelay={0}>
<HoverCardTrigger asChild>
<button type="button" tabIndex={-1}>
<CircleQuestionMarkIcon className="size-4 text-muted-foreground" />
</button>
</HoverCardTrigger>
<HoverCardContent
align="end"
className="text-sm space-y-1.5 lg:w-78"
>
<p>
O <Kbd>CVC</Kbd> é o código de segurança do cartão
de crédito.
</p>
<p>
Ele fica no verso do cartão e geralmente possui{' '}
<Kbd>3 dígitos</Kbd> (ou <Kbd>4 dígitos</Kbd> na
frente, no caso do American Express).
</p>
</HoverCardContent>
</HoverCard>
</FieldLabel>
<Input
id={field.name}
aria-invalid={fieldState.invalid}
{...field}
/>
</Field>
)}
/>
</div>
</FieldGroup>
</FieldSet>