add dialog

This commit is contained in:
2025-12-27 20:48:04 -03:00
parent 976a7da0a9
commit 8b81d5c245
2 changed files with 357 additions and 161 deletions

View File

@@ -176,160 +176,151 @@ export function CreditCard({ control }: { control: Control<Schema> }) {
<CardContent>
<FieldGroup>
<FieldSet>
<FieldGroup>
{/* Credir card number */}
{/* Credir card number */}
<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}
/>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</Field>
)}
/>
{/* Holder name */}
<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}
/>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</Field>
)}
/>
<FieldSet className="grid grid-cols-3 gap-4">
<Controller
control={control}
name="credit_card.number"
render={({
field: { onChange, ref, ...field },
fieldState
}) => (
name="credit_card.exp_month"
defaultValue=""
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor={field.name}>
Número do cartão
</FieldLabel>
<PatternFormat
<FieldLabel htmlFor={field.name}>Mês</FieldLabel>
<NativeSelect
id={field.name}
format="#### #### #### ####"
mask="_"
placeholder="•••• •••• •••• ••••"
customInput={Input}
getInputRef={ref}
value={field.value}
onChange={field.onChange}
aria-invalid={fieldState.invalid}
onValueChange={({ value }) => {
onChange(value)
}}
{...field}
/>
>
<NativeSelectOption value="" disabled>
Selecione
</NativeSelectOption>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
{Array.from({ length: 12 }, (_, i) => {
const v = String(i + 1).padStart(2, '0')
return (
<NativeSelectOption key={v} value={v}>
{v}
</NativeSelectOption>
)
})}
</NativeSelect>
</Field>
)}
/>
{/* Holder name */}
<Controller
control={control}
name="credit_card.holder_name"
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}>
Nome do titular
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}
/>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</Field>
)}
/>
<div className="grid grid-cols-3 gap-4">
<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>
</FieldSet>
</FieldGroup>
</CardContent>

View File

@@ -1,6 +1,9 @@
import { useToggle } from 'ahooks'
import { PencilIcon } from 'lucide-react'
import { useForm } from 'react-hook-form'
import { PatternFormat } from 'react-number-format'
import { ExternalLinkIcon, PencilIcon, SearchIcon } from 'lucide-react'
import { Controller, useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import valid from 'card-validator'
import { Currency } from '@repo/ui/components/currency'
@@ -39,6 +42,21 @@ import { useWizard } from '@/components/wizard'
import { type WizardState } from './route'
import { applyDiscount } from './discount'
import { paymentMethods } from '@repo/ui/routes/orders/data'
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldSet
} from '@repo/ui/components/ui/field'
import { Input } from '@repo/ui/components/ui/input'
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput
} from '@repo/ui/components/ui/input-group'
type ReviewProps = {
state: WizardState
@@ -64,7 +82,7 @@ export function Review({ state, onSubmit }: ReviewProps) {
return (
<>
<Address {...state} />
<Address total={total} {...state} />
<form
onSubmit={async (e) => {
@@ -150,9 +168,12 @@ export function Review({ state, onSubmit }: ReviewProps) {
)
}
export function Address({ payment_method, credit_card }: WizardState) {
export function Address({
total,
payment_method,
credit_card
}: WizardState & { total: number }) {
const numberValidation = valid.number(credit_card?.number)
// console.log(numberValidation)
return (
<ItemGroup className="grid lg:grid-cols-2 gap-4">
@@ -170,7 +191,7 @@ export function Address({ payment_method, credit_card }: WizardState) {
</ul>
</ItemContent>
<ItemActions>
<DialogDemo />
<AddressDialog />
</ItemActions>
</Item>
@@ -178,14 +199,20 @@ export function Address({ payment_method, credit_card }: WizardState) {
<ItemContent>
<ItemTitle>Forma de pagamento</ItemTitle>
<ItemDescription>
{payment_method ? paymentMethods[payment_method] : payment_method}
{credit_card ? (
<>
<br />
{numberValidation.card?.niceType} ****{' '}
{numberValidation.card?.niceType} (Crédito) ****{' '}
{credit_card.number.slice(-4)}
<br />
1x <Currency>{total}</Currency>
</>
) : null}
) : (
<>
{payment_method
? paymentMethods[payment_method]
: payment_method}
</>
)}
</ItemDescription>
</ItemContent>
</Item>
@@ -193,27 +220,41 @@ export function Address({ payment_method, credit_card }: WizardState) {
)
}
export function DialogDemo() {
const form = useForm()
const { handleSubmit } = form
const formSchema = z.object({
postcode: z.string().min(1, 'Digite um CEP'),
address1: z.string().min(1, 'Digite um endereço'),
address2: z.string().optional(),
neighborhood: z.string().min(1, 'Digite o bairro'),
city: z.string().min(1, 'Digite a cidade'),
state: z.string().min(1, 'Digite o estado')
})
const onSubmit = async () => {}
type Schema = z.infer<typeof formSchema>
export function AddressDialog() {
const { handleSubmit, control } = useForm({
resolver: zodResolver(formSchema)
})
const onSubmit = async (data: Schema) => {
console.log(data)
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Dialog>
<DialogTrigger asChild>
<Button
variant="ghost"
type="button"
className="text-muted-foreground cursor-pointer"
size="icon-sm"
>
<PencilIcon />
<span className="sr-only">Editar endereço</span>
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<Dialog>
<DialogTrigger asChild>
<Button
variant="ghost"
type="button"
className="text-muted-foreground cursor-pointer"
size="icon-sm"
>
<PencilIcon />
<span className="sr-only">Editar endereço</span>
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<DialogHeader>
<DialogTitle>Editar endereço</DialogTitle>
<DialogDescription>
@@ -221,6 +262,170 @@ export function DialogDemo() {
</DialogDescription>
</DialogHeader>
<FieldGroup>
<FieldSet>
<Controller
control={control}
name="postcode"
defaultValue=""
render={({
field: { onChange, ref, ...field },
fieldState
}) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor={field.name}>CEP</FieldLabel>
<InputGroup>
{/*<InputGroupInput
id={field.name}
aria-invalid={fieldState.invalid}
{...field}
/>*/}
<PatternFormat
id={field.name}
format="#####-###"
mask="_"
placeholder="_____-___"
customInput={InputGroupInput}
getInputRef={ref}
aria-invalid={fieldState.invalid}
onValueChange={({ value }) => {
onChange(value)
}}
{...field}
/>
<InputGroupAddon align="inline-end">
<InputGroupButton type="button" variant="secondary">
<SearchIcon />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
<FieldDescription>
<a
href="https://buscacepinter.correios.com.br/app/endereco/index.php"
target="_blank"
rel="noopener noreferrer"
className="inline-flex gap-1 items-center "
>
Não sei o CEP <ExternalLinkIcon className="size-4" />
</a>
</FieldDescription>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</Field>
)}
/>
<Controller
control={control}
name="address1"
defaultValue=""
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor={field.name}>Endereço</FieldLabel>
<Input
id={field.name}
aria-invalid={fieldState.invalid}
{...field}
/>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</Field>
)}
/>
<Controller
control={control}
name="address2"
defaultValue=""
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor={field.name}>
Complemento{' '}
<span className="text-muted-foreground">(opcional)</span>
</FieldLabel>
<Input
id={field.name}
aria-invalid={fieldState.invalid}
{...field}
/>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</Field>
)}
/>
</FieldSet>
<FieldSet className="grid grid-cols-3">
{/* Neighborhood */}
<Controller
control={control}
name="neighborhood"
defaultValue=""
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor={field.name}>Bairro</FieldLabel>
<Input
id={field.name}
aria-invalid={fieldState.invalid}
{...field}
/>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</Field>
)}
/>
{/* City */}
<Controller
control={control}
name="city"
defaultValue=""
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor={field.name}>Cidade</FieldLabel>
<Input
id={field.name}
aria-invalid={fieldState.invalid}
{...field}
/>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</Field>
)}
/>
{/* State */}
<Controller
control={control}
name="state"
defaultValue=""
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor={field.name}>Estado</FieldLabel>
<Input
id={field.name}
aria-invalid={fieldState.invalid}
{...field}
/>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</Field>
)}
/>
</FieldSet>
</FieldGroup>
<DialogFooter className="*:cursor-pointer">
<DialogClose asChild>
<Button
@@ -234,8 +439,8 @@ export function DialogDemo() {
<Button type="submit">Atualizar endereço</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</form>
</form>
</DialogContent>
</Dialog>
)
}