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

View File

@@ -1,6 +1,9 @@
import { useToggle } from 'ahooks' import { useToggle } from 'ahooks'
import { PencilIcon } from 'lucide-react' import { PatternFormat } from 'react-number-format'
import { useForm } from 'react-hook-form' 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 valid from 'card-validator'
import { Currency } from '@repo/ui/components/currency' import { Currency } from '@repo/ui/components/currency'
@@ -39,6 +42,21 @@ import { useWizard } from '@/components/wizard'
import { type WizardState } from './route' import { type WizardState } from './route'
import { applyDiscount } from './discount' import { applyDiscount } from './discount'
import { paymentMethods } from '@repo/ui/routes/orders/data' 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 = { type ReviewProps = {
state: WizardState state: WizardState
@@ -64,7 +82,7 @@ export function Review({ state, onSubmit }: ReviewProps) {
return ( return (
<> <>
<Address {...state} /> <Address total={total} {...state} />
<form <form
onSubmit={async (e) => { 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) const numberValidation = valid.number(credit_card?.number)
// console.log(numberValidation)
return ( return (
<ItemGroup className="grid lg:grid-cols-2 gap-4"> <ItemGroup className="grid lg:grid-cols-2 gap-4">
@@ -170,7 +191,7 @@ export function Address({ payment_method, credit_card }: WizardState) {
</ul> </ul>
</ItemContent> </ItemContent>
<ItemActions> <ItemActions>
<DialogDemo /> <AddressDialog />
</ItemActions> </ItemActions>
</Item> </Item>
@@ -178,14 +199,20 @@ export function Address({ payment_method, credit_card }: WizardState) {
<ItemContent> <ItemContent>
<ItemTitle>Forma de pagamento</ItemTitle> <ItemTitle>Forma de pagamento</ItemTitle>
<ItemDescription> <ItemDescription>
{payment_method ? paymentMethods[payment_method] : payment_method}
{credit_card ? ( {credit_card ? (
<> <>
<br /> {numberValidation.card?.niceType} (Crédito) ****{' '}
{numberValidation.card?.niceType} ****{' '}
{credit_card.number.slice(-4)} {credit_card.number.slice(-4)}
<br />
1x <Currency>{total}</Currency>
</> </>
) : null} ) : (
<>
{payment_method
? paymentMethods[payment_method]
: payment_method}
</>
)}
</ItemDescription> </ItemDescription>
</ItemContent> </ItemContent>
</Item> </Item>
@@ -193,27 +220,41 @@ export function Address({ payment_method, credit_card }: WizardState) {
) )
} }
export function DialogDemo() { const formSchema = z.object({
const form = useForm() postcode: z.string().min(1, 'Digite um CEP'),
const { handleSubmit } = form 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 ( return (
<form onSubmit={handleSubmit(onSubmit)}> <Dialog>
<Dialog> <DialogTrigger asChild>
<DialogTrigger asChild> <Button
<Button variant="ghost"
variant="ghost" type="button"
type="button" className="text-muted-foreground cursor-pointer"
className="text-muted-foreground cursor-pointer" size="icon-sm"
size="icon-sm" >
> <PencilIcon />
<PencilIcon /> <span className="sr-only">Editar endereço</span>
<span className="sr-only">Editar endereço</span> </Button>
</Button> </DialogTrigger>
</DialogTrigger> <DialogContent className="sm:max-w-[425px]">
<DialogContent className="sm:max-w-[425px]"> <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<DialogHeader> <DialogHeader>
<DialogTitle>Editar endereço</DialogTitle> <DialogTitle>Editar endereço</DialogTitle>
<DialogDescription> <DialogDescription>
@@ -221,6 +262,170 @@ export function DialogDemo() {
</DialogDescription> </DialogDescription>
</DialogHeader> </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"> <DialogFooter className="*:cursor-pointer">
<DialogClose asChild> <DialogClose asChild>
<Button <Button
@@ -234,8 +439,8 @@ export function DialogDemo() {
<Button type="submit">Atualizar endereço</Button> <Button type="submit">Atualizar endereço</Button>
</DialogFooter> </DialogFooter>
</DialogContent> </form>
</Dialog> </DialogContent>
</form> </Dialog>
) )
} }