From 976a7da0a988336123c169e4a93383a80f9b6a7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Rafael=20Siqueira?= Date: Sat, 27 Dec 2025 18:43:08 -0300 Subject: [PATCH] update credit card mask --- .../routes/_.$orgid.enrollments.buy/bulk.tsx | 14 +- .../_.$orgid.enrollments.buy/discount.tsx | 2 + .../_.$orgid.enrollments.buy/payment.tsx | 32 ++- .../_.$orgid.enrollments.buy/review.tsx | 269 +++++++++++++----- .../routes/_.$orgid.enrollments.buy/route.tsx | 15 +- .../app/routes/_.$orgid.users.add/data.ts | 2 +- apps/admin.saladeaula.digital/package.json | 1 + package-lock.json | 16 ++ packages/ui/src/routes/orders/columns.tsx | 4 +- packages/ui/src/routes/orders/data.tsx | 6 +- 10 files changed, 268 insertions(+), 93 deletions(-) diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/bulk.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/bulk.tsx index f7e792c..6ff09b8 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/bulk.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/bulk.tsx @@ -339,11 +339,7 @@ export function Bulk({ type SummaryProps = { subtotal: number - coupon?: { - code: string - type: 'FIXED' | 'PERCENT' - amount: number - } + coupon?: Coupon setValue: UseFormSetValue } @@ -411,7 +407,7 @@ export function Summary({ subtotal, coupon, setValue }: SummaryProps) { tabIndex={-1} variant="ghost" onClick={() => { - setValue('coupon', undefined) + setValue('coupon', null) }} > @@ -420,11 +416,7 @@ export function Summary({ subtotal, coupon, setValue }: SummaryProps) { { - setValue('coupon', { - // code: sk, - // amount: discount_amount, - // type: discount_type - }) + setValue('coupon', coupon) }} /> )} diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/discount.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/discount.tsx index 7d99a74..2ea8935 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/discount.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/discount.tsx @@ -72,6 +72,8 @@ export function Discount({ onChange, ...props }: DiscountProps) { discount_type: 'FIXED' | 'PERCENT' } + console.log(code) + onChange?.({ code, amount, type }) reset() diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/payment.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/payment.tsx index 320fed2..f125273 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/payment.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/payment.tsx @@ -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({ (
Voltar + @@ -161,6 +177,7 @@ export function CreditCard({ control }: { control: Control }) {
+ {/* Credir card number */} }) { }} {...field} /> + + {fieldState.invalid && ( + + )} )} /> - + {/* Holder name */} }) { aria-invalid={fieldState.invalid} {...field} /> + {fieldState.invalid && ( + + )} )} /> diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/review.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/review.tsx index 9ec3ad9..7b1c1a6 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/review.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/review.tsx @@ -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 } -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 ( <> - - - - Curso - Quantidade - Valor unit. - Total - - - - {items?.map(({ course, quantity }, index) => { - return ( - - {course.name} - {quantity} - - {course.unit_price} - - - {course.unit_price * quantity} - - - ) - })} - - - - - Subtotal - - - {subtotal} - - - - - Descontos - - - {discount} - - - - - Total - - - {total} - - - -
+
- +
{ + e.preventDefault() + set(true) + await onSubmit(state) + }} + className="space-y-4" + > + + + + Curso + Quantidade + Valor unit. + Total + + + + {items?.map(({ course, quantity }, index) => { + return ( + + {course.name} + {quantity} + + {course.unit_price} + + + {course.unit_price * quantity} + + + ) + })} + + + + + Subtotal + + + {subtotal} + + + + + Descontos + + + {discount} + + + + + Total + + + {total} + + + +
-
- - -
+ + +
+ + + +
+ ) } + +export function Address({ payment_method, credit_card }: WizardState) { + const numberValidation = valid.number(credit_card?.number) + // console.log(numberValidation) + + return ( + + + + Endereço de cobrança +
    + Rua Monsenhor Ivo Zanlorenzi, nº 5190, ap 1802 +
    + Cidade Industrial +
    + Curitiba, Paraná +
    + 81280-350 +
+
+ + + +
+ + + + Forma de pagamento + + {payment_method ? paymentMethods[payment_method] : payment_method} + {credit_card ? ( + <> +
+ {numberValidation.card?.niceType} ****{' '} + {credit_card.number.slice(-4)} + + ) : null} +
+
+
+
+ ) +} + +export function DialogDemo() { + const form = useForm() + const { handleSubmit } = form + + const onSubmit = async () => {} + + return ( +
+ + + + + + + Editar endereço + + Este endereço será utilizado para a emissão da NFS-e. + + + + + + + + + + + + +
+ ) +} diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/route.tsx index 22ac6af..025d6f9 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/route.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/route.tsx @@ -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('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 */} - + diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.add/data.ts b/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.add/data.ts index ebc17ee..65b58ac 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.add/data.ts +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.add/data.ts @@ -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 }) diff --git a/apps/admin.saladeaula.digital/package.json b/apps/admin.saladeaula.digital/package.json index 7aa1d02..3a35073 100644 --- a/apps/admin.saladeaula.digital/package.json +++ b/apps/admin.saladeaula.digital/package.json @@ -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", diff --git a/package-lock.json b/package-lock.json index ee1cdd5..fd69985 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,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", @@ -4783,6 +4784,15 @@ ], "license": "CC-BY-4.0" }, + "node_modules/card-validator": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/card-validator/-/card-validator-10.0.3.tgz", + "integrity": "sha512-xOEDsK3hojV0OIpmrR64eZGpngnOqRDEP20O+WSRtvjLSW6nyekW4i2N9SzYg679uFO3RyHcFHxb+mml5tXc4A==", + "license": "MIT", + "dependencies": { + "credit-card-type": "^10.0.2" + } + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -4905,6 +4915,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/credit-card-type": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/credit-card-type/-/credit-card-type-10.1.0.tgz", + "integrity": "sha512-yd9ebGqWQ9zDvxrvCOkX80GiTvVJ28940uIMqRA58Cu3ReiPkOS4p45d9Y2cmJZtZgp90WdwLSiUtI8lHDgE2g==", + "license": "MIT" + }, "node_modules/crypto-js": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", diff --git a/packages/ui/src/routes/orders/columns.tsx b/packages/ui/src/routes/orders/columns.tsx index bdf3dd4..86524c4 100644 --- a/packages/ui/src/routes/orders/columns.tsx +++ b/packages/ui/src/routes/orders/columns.tsx @@ -11,7 +11,7 @@ import { HelpCircleIcon } from 'lucide-react' import { cn } from '@repo/ui/lib/utils' import { Badge } from '@repo/ui/components/ui/badge' -import { labels, methods, statuses, type Order } from './data' +import { labels, paymentMethods, statuses, type Order } from './data' export type { Order } @@ -21,7 +21,7 @@ export const columns: ColumnDef[] = [ header: 'Forma de pag.', cell: ({ row }) => { const paymentMethod = row.getValue('payment_method') as string - return <>{methods[paymentMethod] ?? paymentMethod} + return <>{paymentMethods[paymentMethod] ?? paymentMethod} } }, { diff --git a/packages/ui/src/routes/orders/data.tsx b/packages/ui/src/routes/orders/data.tsx index 5839fe0..ce8ba89 100644 --- a/packages/ui/src/routes/orders/data.tsx +++ b/packages/ui/src/routes/orders/data.tsx @@ -8,13 +8,15 @@ import { type LucideIcon } from 'lucide-react' +export type PaymentMethod = 'PIX' | 'BANK_SLIP' | 'CREDIT_CARD' | 'MANUAL' + // This type is used to define the shape of our data. // You can use a Zod schema here if you want. export type Order = { id: string total: number status: 'PENDING' | 'PAID' | 'DECLINED' | 'EXPIRED' | 'REFUNDED' | 'CANCELED' - payment_method: 'PIX' | 'CREDIT_CARD' | 'BANK_SLIP' | 'MANUAL' + payment_method: PaymentMethod name: string email: string } @@ -64,7 +66,7 @@ export const labels: Record = { CANCELED: 'Cancelado' } -export const methods: Record = { +export const paymentMethods: Record = { PIX: 'Pix', CREDIT_CARD: 'Cartão de crédito', BANK_SLIP: 'Boleto bancário',