update credit card mask

This commit is contained in:
2025-12-27 18:43:08 -03:00
parent d0dcc0a953
commit 976a7da0a9
10 changed files with 268 additions and 93 deletions

View File

@@ -339,11 +339,7 @@ export function Bulk({
type SummaryProps = {
subtotal: number
coupon?: {
code: string
type: 'FIXED' | 'PERCENT'
amount: number
}
coupon?: Coupon
setValue: UseFormSetValue<any>
}
@@ -411,7 +407,7 @@ export function Summary({ subtotal, coupon, setValue }: SummaryProps) {
tabIndex={-1}
variant="ghost"
onClick={() => {
setValue('coupon', undefined)
setValue('coupon', null)
}}
>
<XIcon />
@@ -420,11 +416,7 @@ export function Summary({ subtotal, coupon, setValue }: SummaryProps) {
<Discount
disabled={subtotal === 0}
onChange={(coupon) => {
setValue('coupon', {
// code: sk,
// amount: discount_amount,
// type: discount_type
})
setValue('coupon', coupon)
}}
/>
)}

View File

@@ -72,6 +72,8 @@ export function Discount({ onChange, ...props }: DiscountProps) {
discount_type: 'FIXED' | 'PERCENT'
}
console.log(code)
onChange?.({ code, amount, type })
reset()

View File

@@ -2,8 +2,9 @@ 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 { z } from 'zod'
import { ArrowRightIcon, CircleQuestionMarkIcon } from 'lucide-react'
import valid from 'card-validator'
import { Button } from '@repo/ui/components/ui/button'
import { Kbd } from '@repo/ui/components/ui/kbd'
@@ -19,6 +20,7 @@ import {
} from '@repo/ui/components/ui/hover-card'
import {
Field,
FieldError,
FieldGroup,
FieldLabel,
FieldSet
@@ -29,10 +31,21 @@ import {
} from '@repo/ui/components/ui/native-select'
import { useWizard } from '@/components/wizard'
import { isName } from '../_.$orgid.users.add/data'
const creditCard = z.object({
holder_name: z.string().min(1),
number: z.string().min(13).max(19),
holder_name: z
.string()
.trim()
.nonempty('Digite um nome')
.refine(isName, { message: 'Nome inválido' }),
number: z.string().refine(
(value) => {
const numberValidation = valid.number(value)
return numberValidation.isValid
},
{ error: 'Número do cartão inválido' }
),
exp_month: z.string().min(2),
exp_year: z.string().min(4),
cvv: z.string().min(3).max(4)
@@ -90,6 +103,8 @@ export function Payment({
<Controller
name="payment_method"
control={control}
// @ts-ignore
defaultValue=""
render={({ field: { name, value, onChange }, formState }) => (
<div className="space-y-1.5">
<RadioGroup
@@ -143,6 +158,7 @@ export function Payment({
>
Voltar
</Button>
<Button type="submit" variant="secondary">
Continuar <ArrowRightIcon />
</Button>
@@ -161,6 +177,7 @@ export function CreditCard({ control }: { control: Control<Schema> }) {
<FieldGroup>
<FieldSet>
<FieldGroup>
{/* Credir card number */}
<Controller
control={control}
name="credit_card.number"
@@ -185,10 +202,14 @@ export function CreditCard({ control }: { control: Control<Schema> }) {
}}
{...field}
/>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</Field>
)}
/>
{/* Holder name */}
<Controller
control={control}
name="credit_card.holder_name"
@@ -203,6 +224,9 @@ export function CreditCard({ control }: { control: Control<Schema> }) {
aria-invalid={fieldState.invalid}
{...field}
/>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</Field>
)}
/>

View File

@@ -1,6 +1,12 @@
import { useToggle } from 'ahooks'
import { PencilIcon } from 'lucide-react'
import { useForm } from 'react-hook-form'
import valid from 'card-validator'
import { Currency } from '@repo/ui/components/currency'
import { Button } from '@repo/ui/components/ui/button'
import { Separator } from '@repo/ui/components/ui/separator'
import { Spinner } from '@repo/ui/components/ui/spinner'
import {
Table,
TableBody,
@@ -10,18 +16,38 @@ import {
TableHeader,
TableRow
} from '@repo/ui/components/ui/table'
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemGroup,
ItemTitle
} from '@repo/ui/components/ui/item'
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger
} from '@repo/ui/components/ui/dialog'
import { useWizard } from '@/components/wizard'
import { type WizardState } from './route'
import { applyDiscount } from './discount'
import { paymentMethods } from '@repo/ui/routes/orders/data'
type ReviewProps = {
state: WizardState
onSubmit: (value: WizardState) => void | Promise<void>
}
export function Review({ state }: ReviewProps) {
export function Review({ state, onSubmit }: ReviewProps) {
const wizard = useWizard()
const [loading, { set }] = useToggle()
const { coupon, items } = state || { items: [], coupon: {} }
const subtotal =
items?.reduce(
@@ -36,79 +62,180 @@ export function Review({ state }: ReviewProps) {
: 0
const total = subtotal > 0 ? subtotal + discount : 0
console.log(state)
return (
<>
<Table>
<TableHeader>
<TableRow className="pointer-events-none">
<TableHead>Curso</TableHead>
<TableHead>Quantidade</TableHead>
<TableHead>Valor unit.</TableHead>
<TableHead>Total</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{items?.map(({ course, quantity }, index) => {
return (
<TableRow key={index}>
<TableCell>{course.name}</TableCell>
<TableCell>{quantity}</TableCell>
<TableCell>
<Currency>{course.unit_price}</Currency>
</TableCell>
<TableCell>
<Currency>{course.unit_price * quantity}</Currency>
</TableCell>
</TableRow>
)
})}
</TableBody>
<TableFooter>
<TableRow className="pointer-events-none">
<TableCell className="text-right" colSpan={3}>
Subtotal
</TableCell>
<TableCell>
<Currency>{subtotal}</Currency>
</TableCell>
</TableRow>
<TableRow className="pointer-events-none">
<TableCell className="text-right" colSpan={3}>
Descontos
</TableCell>
<TableCell>
<Currency>{discount}</Currency>
</TableCell>
</TableRow>
<TableRow className="pointer-events-none">
<TableCell className="text-right" colSpan={3}>
Total
</TableCell>
<TableCell>
<Currency>{total}</Currency>
</TableCell>
</TableRow>
</TableFooter>
</Table>
<Address {...state} />
<Separator />
<form
onSubmit={async (e) => {
e.preventDefault()
set(true)
await onSubmit(state)
}}
className="space-y-4"
>
<Table className="pointer-events-none">
<TableHeader>
<TableRow>
<TableHead>Curso</TableHead>
<TableHead>Quantidade</TableHead>
<TableHead>Valor unit.</TableHead>
<TableHead>Total</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{items?.map(({ course, quantity }, index) => {
return (
<TableRow key={index}>
<TableCell>{course.name}</TableCell>
<TableCell>{quantity}</TableCell>
<TableCell>
<Currency>{course.unit_price}</Currency>
</TableCell>
<TableCell>
<Currency>{course.unit_price * quantity}</Currency>
</TableCell>
</TableRow>
)
})}
</TableBody>
<TableFooter>
<TableRow>
<TableCell className="text-right" colSpan={3}>
Subtotal
</TableCell>
<TableCell>
<Currency>{subtotal}</Currency>
</TableCell>
</TableRow>
<TableRow>
<TableCell className="text-right" colSpan={3}>
Descontos
</TableCell>
<TableCell>
<Currency>{discount}</Currency>
</TableCell>
</TableRow>
<TableRow>
<TableCell className="text-right" colSpan={3}>
Total
</TableCell>
<TableCell>
<Currency>{total}</Currency>
</TableCell>
</TableRow>
</TableFooter>
</Table>
<div className="flex justify-between gap-4 *:cursor-pointer">
<Button
type="button"
variant="link"
className="text-black dark:text-white"
onClick={() => wizard('payment')}
tabIndex={-1}
>
Voltar
</Button>
<Button type="submit">
Pagar <Currency>{total}</Currency>
</Button>
</div>
<Separator />
<div className="flex justify-between gap-4 *:cursor-pointer">
<Button
type="button"
variant="link"
className="text-black dark:text-white"
onClick={() => wizard('payment')}
tabIndex={-1}
>
Voltar
</Button>
<Button type="submit" disabled={loading}>
{loading && <Spinner />}
Pagar <Currency>{total}</Currency>
</Button>
</div>
</form>
</>
)
}
export function Address({ payment_method, credit_card }: WizardState) {
const numberValidation = valid.number(credit_card?.number)
// console.log(numberValidation)
return (
<ItemGroup className="grid lg:grid-cols-2 gap-4">
<Item variant="outline" className="items-start">
<ItemContent>
<ItemTitle>Endereço de cobrança</ItemTitle>
<ul className="text-muted-foreground text-sm leading-normal font-normal text-balance">
Rua Monsenhor Ivo Zanlorenzi, 5190, ap 1802
<br />
Cidade Industrial
<br />
Curitiba, Paraná
<br />
81280-350
</ul>
</ItemContent>
<ItemActions>
<DialogDemo />
</ItemActions>
</Item>
<Item variant="outline" className="items-start">
<ItemContent>
<ItemTitle>Forma de pagamento</ItemTitle>
<ItemDescription>
{payment_method ? paymentMethods[payment_method] : payment_method}
{credit_card ? (
<>
<br />
{numberValidation.card?.niceType} ****{' '}
{credit_card.number.slice(-4)}
</>
) : null}
</ItemDescription>
</ItemContent>
</Item>
</ItemGroup>
)
}
export function DialogDemo() {
const form = useForm()
const { handleSubmit } = form
const onSubmit = async () => {}
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]">
<DialogHeader>
<DialogTitle>Editar endereço</DialogTitle>
<DialogDescription>
Este endereço será utilizado para a emissão da NFS-e.
</DialogDescription>
</DialogHeader>
<DialogFooter className="*:cursor-pointer">
<DialogClose asChild>
<Button
variant="link"
type="button"
className="text-black dark:text-white"
>
Cancel
</Button>
</DialogClose>
<Button type="submit">Atualizar endereço</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</form>
)
}

View File

@@ -25,6 +25,7 @@ import { createSearch } from '@repo/util/meili'
import { cloudflareContext } from '@repo/auth/context'
import { Label } from '@repo/ui/components/ui/label'
import { Skeleton } from '@repo/ui/components/skeleton'
import type { PaymentMethod } from '@repo/ui/routes/orders/data'
import { Wizard, WizardStep } from '@/components/wizard'
import { Step, StepItem, StepSeparator } from '@/components/step'
@@ -34,6 +35,7 @@ import { Bulk, type Item } from './bulk'
import { Payment, type CreditCard } from './payment'
import { Review } from './review'
import type { Coupon } from './discount'
import { useFetcher } from 'react-router'
export type WizardState = {
index: number
@@ -41,7 +43,7 @@ export type WizardState = {
items: Item[]
enrollments: Enrollment[]
coupon?: Coupon
payment_method?: 'PIX' | 'BANK_SLIP' | 'CREDIT_CARD'
payment_method?: PaymentMethod
credit_card?: CreditCard
}
@@ -74,12 +76,14 @@ export async function loader({ context }: Route.LoaderArgs) {
export async function action({ request }: Route.ActionArgs) {
const body = (await request.json()) as object
console.log(body)
}
export default function Route({
loaderData: { courses }
}: Route.ComponentProps) {
const fetcher = useFetcher()
const [mounted, setMounted] = useState(false)
const [state, setState] = useLocalStorageState<WizardState>('wizard_cart', {
defaultValue: emptyWizard
@@ -96,6 +100,13 @@ export default function Route({
}))
}
const onSubmit = async (data: WizardState) => {
await fetcher.submit(JSON.stringify(data), {
method: 'post',
encType: 'application/json'
})
}
useMount(() => {
setMounted(true)
})
@@ -206,7 +217,7 @@ export default function Route({
{/* Review */}
<WizardStep name="review">
<Review state={state} />
<Review state={state} onSubmit={onSubmit} />
</WizardStep>
</Wizard>
</CardContent>

View File

@@ -7,7 +7,7 @@ import {
} from 'unique-names-generator'
import { z } from 'zod'
const isName = (name: string) => name && name.includes(' ')
export const isName = (name: string) => name && name.includes(' ')
function randomEmail() {
const numberDict = NumberDictionary.generate({ min: 100, max: 999 })

View File

@@ -17,6 +17,7 @@
"@repo/ui": "*",
"@repo/util": "^0.0.0",
"@tanstack/react-table": "^8.21.3",
"card-validator": "^10.0.3",
"cookie": "^1.1.1",
"date-fns": "^4.1.0",
"flat": "^6.0.1",