Auth & Vendor API - #11
Conversation
…d vendor registration This commit introduces a comprehensive authentication system including: - Sign-in and sign-up pages with form validation - Vendor registration flow - Database schema and migrations for auth - API routes for auth handlers - Client-side auth utilities and hooks - Password validation and security features - Social login integration (GitHub, Google) - Session management
- Add 2FA support with email verification codes - Create security settings UI for enabling/disabling 2FA - Implement OTP input component and email templates - Add user menu dropdown with profile links - Update auth schema to support 2FA configuration - Move toast notifications to bottom-right
…reation - Implement vendor registration flow with personal and store info steps - Add vendor and shop schemas to database - Create slug utilities for shop URLs - Add admin plugin for role-based access control - Implement vendor registration server function with validation
- Replace mock user data with actual session user from auth client - Update VendorUserMenu to handle nullable user fields and implement sign out - Add conditional rendering to show sign-in button when user is not authenticated - Use auth client's signOut function for proper session termination
Protect vendor dashboard routes by requiring a valid session. Unauthenticated users are redirected to the sign-in page.
…tions - Add @uploadcare/react-uploader dependency for image uploads - Update shop types and schema to support real data operations - Create vendor helper functions for user role and vendor lookup - Implement comprehensive shop CRUD functions with server-side validation - Add React Query hooks for shops data fetching and mutations - Replace mock data with real API calls in my-shops page - Enhance shop card with edit dialog and image upload capabilities - Add empty state and shop count badge to vendor dashboard sidebar - Improve shop form with better validation and file upload integration
- Add admin middleware to protect admin routes and server functions - Create admin error component for access denied and error handling - Add admin shop query validator and server function for shop management - Move hooks to common directory and extract shared CRUD hook - Fix header console log removal and update import paths - Add forbidden route for admin access denied scenarios
…a fetching - Add uuid dependency for category ID generation - Create category database schema with hierarchical support - Implement server-side pagination, sorting, and filtering for categories - Add vendor category management UI with reusable components - Create shared category validators and query helpers for DRY compliance - Implement category CRUD operations with TanStack Query mutations - Add server fetcher factory for consistent data fetching patterns - Create reusable confirm delete dialog and page skeleton components - Update sidebar to use real session data for user display - Fix type definitions and filter column generics for better type safety
- Add brand schema to database and include in drizzle config - Create server functions for brand operations with auth middleware - Implement vendor brand management UI with data table and dialogs - Add form validation using Zod schemas for create/update operations - Set up TanStack Query for data fetching and mutation state management - Move Uploadcare CSS import to root layout for global availability - Remove unused CSS import from add-category-dialog component
…alog - Replace NormalizedBrand with BrandItem as the single brand type - Extract shared form logic into EntityFormDialog component - Add product count validation before brand deletion - Remove unused product count queries and FIXME comments
This change enforces consistent quote style by updating all single quotes in imports to double quotes, aligning with the project's biome.json configuration. The update improves code consistency and readability without affecting functionality.
- Refactor category table into separate admin and vendor components for better maintainability - Fix empty string validation in optional field validator to properly handle empty values - Update category deletion to check actual product count before allowing deletion - Simplify category query helpers by removing unused product count queries - Update build scripts to exclude generated route tree file from formatting/linting - Fix slug auto-generation in entity form dialog to use onChange instead of onBlur
Improve code maintainability by moving inline fields definition to a separate constant. This reduces duplication and makes the component structure clearer.
…operations - Add attribute database schema with tables for attributes and attribute values - Implement vendor attribute CRUD operations with server functions and validation - Create attribute management UI components with data table integration - Add attribute query helpers for optimized batch fetching and normalization - Integrate attribute management into vendor shop dashboard
…CRUD operations - Add tags database schema with shop-scoped table and relations - Create shared tag query validators and helper functions for data fetching - Implement vendor tag CRUD server functions with proper authorization - Add vendor tags page with data table, filtering, and mutation handling - Create reusable tag components (header, table, dialog) for vendor interface - Refactor brand and category validators to use shared query schemas - Integrate tags into vendor entity fetchers for consistent data fetching patterns
- Add database schema for tax rates with shop scoping and regional support - Create comprehensive tax rate CRUD operations with validation - Implement vendor tax rate management UI with data table integration - Add server functions for tax rate queries, creation, updates, and deletion - Create reusable tax rate form dialog and table column components - Integrate tax rate management into vendor shop routes - Update types and hooks to support new tax rate functionality - Set Toaster theme to dark for consistent UI appearance
- Replace AddAttributeDialog with inline form in EntityFormDialog for better UX - Add proper validation and dynamic form fields for attribute values - Fix type safety in data table components with generic FilterableColumn - Ensure default values for attribute values array to prevent runtime errors - Update attribute header to use simple button instead of dialog trigger
- Update BrandFormValues, TagFormValues, and TaxRateFormValues interfaces to make non-essential fields optional for flexible form handling - Add missing TaxPermissions and Taxes interfaces to complete tax type definitions - Extend mock data to include all required fields from updated BrandItem and TagItem interfaces - Refactor admin components to use proper form value types and pass full objects to delete handlers - Consolidate table components by removing separate permission props in favor of mode-based rendering
…h button - Remove internal dialog state and AddTagDialog component - Replace with a simple button that triggers parent-provided onAddTag callback - Update description text to be more concise - Simplify props interface by removing TagFormValues dependency
- Replace individual header components (TagHeader, TaxHeader, etc.) with a factory function
- Unify prop naming from onAdd{Entity} to consistent onAdd callback
- Reduce code duplication and improve maintainability
- Update all template files to use the new prop naming convention
…ports Consolidate the shared entity header factory function into the base/common directory to improve code organization and maintainability. Update all component imports to use the new location instead of relative paths.
…t types Move type definitions from hook to shared validator file to centralize type definitions and improve maintainability.
…pagination - Add product database schema with relations to categories, brands, tags, and attributes - Create server-side pagination fetcher for vendor products with filtering and sorting - Replace mock product table with TanStack Query-based data fetching and mutations - Implement product CRUD operations with proper error handling and loading states - Add product form dialog with image upload support and attribute selection - Migrate products route to use real data instead of mock data - Update entity header component for reusable product headers - Add product table columns with action dropdown for edit/delete/toggle - Integrate with existing vendor entity fetchers pattern
…gination - Add coupon database schema with product/category restrictions and usage tracking - Create vendor coupon management pages with server-side data fetching - Implement shared coupon table components for admin and vendor dashboards - Add coupon validation and mutation hooks with TanStack Query integration - Replace mock data with real API calls for coupon CRUD operations - Introduce date picker component for coupon validity period selection - Refactor coupon types and validators into centralized modules
Prevents form validation errors by ensuring the applicableTo select field has a default value of "all" when the dialog opens.
…tion Extract coupon data creation into a shared object to eliminate repetitive code in create and update operations. This improves maintainability by having a single source of truth for the coupon data structure.
|
Important Review skippedToo many files! This PR contains 299 files, which is 149 over the limit of 150. You can disable this status message by setting the
✨ Finishing touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (16)
src/components/base/store/checkout/shipping-address-form.tsx (1)
37-41:⚠️ Potential issue | 🟠 MajorCall
form.handleSubmit(); current handler is a no-op.
form.handleSubmit;only references the function, so submission/validation never runs afterpreventDefault. This breaks the form.✅ Suggested fix
onSubmit={(e) => { e.preventDefault(); e.stopPropagation(); - form.handleSubmit; + form.handleSubmit(); }}src/components/containers/store/accounts/wishlist/wishlist-list.tsx (1)
9-21:⚠️ Potential issue | 🔴 CriticalReturn the fallback UI to avoid null
.map()crash.
Currently the NotFound block is evaluated but never returned, so the code continues and will throw ifmockWishlistsis null/undefined.✅ Suggested fix
- if (!mockWishlists) { - <div className="@container container mx-auto px-4 py-8"> + if (!mockWishlists) { + return ( + <div className="@container container mx-auto px-4 py-8"> <NotFound title="Category not found" description="The category you're looking for doesn't exist or has been removed." icon={<ShoppingBag className="h-10 w-10 text-muted-foreground" />} > <Link to="/product"> <Button variant="outline">Start Shopping</Button> </Link> </NotFound> - </div>; + </div> + ); }src/components/templates/store/category/category-detail-template.tsx (1)
45-56:⚠️ Potential issue | 🟠 MajorReturn the NotFound UI for missing categories.
Right now the JSX in the
!categorybranch is not returned, so the NotFound view never renders and the component continues withcategorynull. That yields a broken/blank experience.✅ Suggested fix
if (!category) { - <div className="@container container mx-auto px-4 py-8"> + return ( + <div className="@container container mx-auto px-4 py-8"> <NotFound title="Category not found" description="The category you're looking for doesn't exist or has been removed." icon={<ShoppingBag className="h-10 w-10 text-muted-foreground" />} > <Link to="/category"> <Button variant="outline">Browse All Categories</Button> </Link> </NotFound> - </div>; + </div> + ); }src/components/containers/shared/orders/order-table.tsx (2)
161-168:⚠️ Potential issue | 🟡 Minor
onUpdateStatusreceives current status, not a new status.The callback is invoked with
row.original.status(the current status), but the prop signature names this parameternewStatus. This is either a naming issue or a logic bug—typically "Update Status" would open a dialog to select a new status, then call the handler with that selection.If the intent is to pass the current status for context, consider renaming the parameter to
currentStatusin the interface to avoid confusion.Option 1: Rename parameter if intent is to pass current status
- onUpdateStatus?: (orderId: string, newStatus: string) => void; + onUpdateStatus?: (orderId: string, currentStatus: string) => void;
170-170:⚠️ Potential issue | 🟡 Minor"Download Invoice" has no click handler.
This menu item is rendered but does nothing when clicked—likely incomplete functionality.
Would you like me to open an issue to track implementing the invoice download functionality?
src/components/containers/store/checkout/checkout-order-summary.tsx (1)
19-36:⚠️ Potential issue | 🔴 CriticalCritical: Missing return statement prevents empty state from rendering.
The empty cart condition creates a JSX element but doesn't return it. Execution continues to line 37, causing the full cart UI to render even when
items.length === 0. Users will never see the empty state.Note: This is a pre-existing bug not introduced by this PR, but it critically affects the user experience and should be fixed.
🐛 Proposed fix: Add return statement
if (items.length === 0) { - <div className="rounded-lg border bg-background p-6 shadow-sm"> + return ( + <div className="rounded-lg border bg-background p-6 shadow-sm"> - <h2 className="mb-6 font-semibold text-xl">Your Cart</h2> - <EmptyState - icon={<ShoppingBag className="h-10 w-10 text-muted-foreground" />} - title="No items yet" - description="Add items to your cart to continue with checkout." - action={ - <Link to="/product"> - <Button variant="outline" className="rounded-full"> - Browse Products - </Button> - </Link> - } - className="py-8" - /> - </div>; + <h2 className="mb-6 font-semibold text-xl">Your Cart</h2> + <EmptyState + icon={<ShoppingBag className="h-10 w-10 text-muted-foreground" />} + title="No items yet" + description="Add items to your cart to continue with checkout." + action={ + <Link to="/product"> + <Button variant="outline" className="rounded-full"> + Browse Products + </Button> + </Link> + } + className="py-8" + /> + </div> + ); }src/components/containers/shared/shipping/shipping-table.tsx (1)
70-106:⚠️ Potential issue | 🟡 MinorEmpty dropdown menu when no actions are available.
When both
canEditandcanDeletearefalse, the actions button still renders but opens an empty dropdown menu—this is confusing UX. Consider hiding the entire actions column or the menu trigger when no actions are permitted.🛠️ Proposed fix: conditionally render actions cell
{ id: "actions", enableHiding: false, cell: ({ row }) => { const shippingMethod = row.original; + const hasActions = permissions.canEdit || permissions.canDelete; + + if (!hasActions) return null; return ( <DropdownMenu>src/components/containers/shared/users/user-table.tsx (1)
153-155:⚠️ Potential issue | 🟡 Minor"View Details" menu item has no handler.
The item is rendered when
permissions.canViewis true but clicking it does nothing. Consider adding anonViewUsercallback prop similar toonEditUserandonDeleteUser.src/components/base/error/default-error-component.tsx (1)
84-88:⚠️ Potential issue | 🟡 MinorVerify Tailwind supports
h-37.5in production builds.The
h-37.5class is not a standard Tailwind utility and won't be recognized without a custom theme extension. In production, this class will be dropped, causing the scroll area to expand unexpectedly. Use an arbitrary value instead:✅ Recommended fix
- <ScrollArea className="mt-2 h-37.5 w-full rounded border bg-background/50 p-2 text-muted-foreground"> + <ScrollArea className="mt-2 h-[2.34375rem] w-full rounded border bg-background/50 p-2 text-muted-foreground">biome.json (1)
39-56:⚠️ Potential issue | 🟠 MajorKeep
noDocumentCookieenabled to avoid normalizing unsafe patterns.
Turning this rule off makes it easier fordocument.cookieusage to creep in, which is a common XSS/CSRF risk vector. Prefer keeping the rule on and using a targeted suppression only where absolutely required.Suggested adjustment
- "noDocumentCookie": "off" + "noDocumentCookie": "warn"src/components/containers/shared/users/add-user-dialog.tsx (1)
43-55:⚠️ Potential issue | 🟠 MajorMissing validation for required fields.
The
nameand@tanstack/react-formrequires explicit validators.🛡️ Proposed fix to add validation
const form = useForm({ defaultValues: { name: "", email: "", status: "active" as "active" | "inactive" | "suspended", avatar: null as FileList | null, }, + validators: { + onSubmit: ({ value }) => { + const errors: Record<string, string> = {}; + if (!value.name.trim()) { + errors.name = "Name is required"; + } + if (!value.email.trim()) { + errors.email = "Email is required"; + } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.email)) { + errors.email = "Invalid email format"; + } + return Object.keys(errors).length ? errors : undefined; + }, + }, onSubmit: async ({ value }) => {Alternatively, add field-level validators for immediate feedback:
<form.Field name="name" validators={{ onChange: ({ value }) => !value.trim() ? "Name is required" : undefined, }} >src/components/templates/admin/admin-taxes-template.tsx (1)
36-40:⚠️ Potential issue | 🔴 CriticalMissing import and incorrect component name for
TaxTable.The component used on line 36 is named
TaxesTable(which doesn't exist), but the actual exported component fromtax-table.tsxis namedTaxTable. Additionally, the props passed to the component don't match its expected interface—TaxTableexpectsonDelete,onEdit, andonToggleActivecallbacks, notpermissionsandonDeleteTax.Correct the import and component name:
🐛 Import fix
import { Plus } from "lucide-react"; import { useState } from "react"; import { AddTaxDialog } from "@/components/containers/shared/taxes/add-tax-dialog"; import TaxHeader from "@/components/containers/shared/taxes/tax-header"; +import { TaxTable } from "@/components/containers/shared/taxes/tax-table"; import { Button } from "@/components/ui/button";Then update the component usage to match
TaxTable's props signature.src/components/containers/shared/transactions/transaction-table.tsx (1)
111-135:⚠️ Potential issue | 🟡 MinorHide action items when handlers are missing.
With default permissions, users can see menu items that no-op if the handler isn’t provided.🔧 Suggested guard
- {permissions.canView && ( + {permissions.canView && onViewTransaction && ( <DropdownMenuItem onClick={() => onViewTransaction?.(transaction.id)} > <Eye className="mr-2 h-4 w-4" /> View Details </DropdownMenuItem> )} - {permissions.canRefund && + {permissions.canRefund && + onRefundTransaction && transaction.paymentStatus === "paid" && ( <DropdownMenuItem onClick={() => onRefundTransaction?.(transaction.id)} > <RefreshCcw className="mr-2 h-4 w-4" /> Refund </DropdownMenuItem> )} - {permissions.canDelete && ( + {permissions.canDelete && onDeleteTransaction && ( <DropdownMenuItem onClick={() => onDeleteTransaction?.(transaction.id)} className="text-destructive" > <Trash2 className="mr-2 h-4 w-4" /> Delete </DropdownMenuItem> )}src/components/containers/shared/shops/add-shop-dialog.tsx (2)
49-64:⚠️ Potential issue | 🟠 MajorSubmission result not awaited — dialog closes prematurely on async errors.
The
onSubmitcallback supportsPromise<void>, but its result is not awaited. If the parent's submission fails (e.g., network error), the dialog will have already closed and reset the form, preventing error feedback to the user.🐛 Proposed fix
onSubmit: async ({ value, formApi }) => { await formApi.validateAllFields("blur"); await formApi.validateAllFields("change"); const hasErrors = Object.values(formApi.state.fieldMeta).some( (meta) => meta?.errors && meta.errors.length > 0 ); if (hasErrors) { return; } - onSubmit(value); - onOpenChange(false); - form.reset(); + await onSubmit(value); + onOpenChange(false); + form.reset(); },
28-36:⚠️ Potential issue | 🟡 MinorUnused
isSubmittingprop.The
isSubmittingprop is declared in the interface but never destructured or used. Either remove it from the interface or wire it into the form's submission state.🧹 Option A: Remove unused prop
interface AddShopDialogProps { open: boolean; onOpenChange: (open: boolean) => void; onSubmit: (data: ShopFormValues) => void | Promise<void>; - isSubmitting?: boolean; }src/components/base/data-table/data-table-container.tsx (1)
93-104:⚠️ Potential issue | 🟡 MinorHandle empty array filter values (Line 100).
Empty multi-selects (e.g.,[]) will still be pushed as active filters. Consider treating empty arrays as “no filter” to prevent unintended filtering.💡 Suggested fix
- if (value !== "" && value !== undefined && value !== null) { + const isEmpty = + value === "" || + value === undefined || + value === null || + (Array.isArray(value) && value.length === 0); + if (!isEmpty) { filtered.push({ id: columnId, value }); }
🤖 Fix all issues with AI agents
In `@src/components/base/error/admin-error-component.tsx`:
- Around line 53-63: AdminErrorComponent currently reads error.message directly
which can throw if ErrorComponentProps is parameterized with a non-Error shape;
add a type guard like the pattern used in DefaultErrorComponent to ensure error
is an Error (e.g., error instanceof Error or typeof error?.message === "string")
before checking message, and only render AdminAccessDenied when the guard passes
and message.includes("Admin access required"); otherwise fall back to rendering
DefaultErrorComponent with the original error, reset, and info.
In `@src/components/base/forms/entity-form-dialog.tsx`:
- Around line 123-153: The effect currently lists form in its dependency array
which can cause re-renders loops because useForm may return a new object
reference; remove form from the dependency array and instead reference a stable
ref for the form methods (e.g., create a formRef = useRef(form) and update
formRef.current = form on each render, then call formRef.current.setFieldValue
and formRef.current.reset inside the useEffect), and change the dependency array
to only [open, initialValues, fields]; keep the existing logic around
prevOpenRef, initialValues, and defaults but use the stable formRef methods to
avoid infinite loops.
- Around line 96-121: The dialog currently closes and the form resets
unconditionally after awaiting onSubmit in the useForm onSubmit handler; wrap
the await onSubmit(value as T) call in a try/catch so that onOpenChange(false)
and form.reset() only run on successful completion, and in the catch log the
error and show a user-facing error (e.g., toast.error) so the dialog remains
open when onSubmit throws; update the onSubmit handler in the useForm invocation
(the onSubmit function, form.reset and onOpenChange calls) to implement this
error handling.
In `@src/components/base/vendors/my-shop/edit-shop-dialog.tsx`:
- Around line 55-57: handleSubmit currently calls onSubmit(data) but doesn't
await the Promise; make handleSubmit async and await onSubmit(data) so
submission completion and errors are propagated; locate the handleSubmit
function and change it to async, await onSubmit(data) and optionally wrap the
await in try/catch to handle/log/rethrow submission errors as appropriate for
the component's error handling flow (referencing handleSubmit, onSubmit, and
UpdateShopInput).
In `@src/components/containers/auth/auth-form.tsx`:
- Around line 227-234: The Checkbox handler is incorrect: Radix Checkbox
(Checkbox / CheckboxPrimitive.Root) uses onCheckedChange(boolean) not
onChange(event). Replace the onChange usage on the Checkbox with onCheckedChange
and pass the boolean directly to field.handleChange (keep id/name as field.name
and checked as Boolean(field.state.value)); remove the event casting to
HTMLInputElement to avoid the runtime failure.
In `@src/components/containers/shared/coupons/add-coupon-dialog.tsx`:
- Line 1: The component AddCouponDialog currently uses
toISOString().split("T")[0] to derive default/initial date strings (three places
around the existing toISOString usage), which forces UTC and shifts local dates;
replace those uses with date-fns format(...) producing a local-date string like
format(someDate, "yyyy-MM-dd") (you already import format and parseISO), e.g.
when building defaultStart/defaultEnd and initial form values call format(new
Date(valueOrToday), "yyyy-MM-dd") instead of toISOString().split("T")[0], and
ensure any parseISO usages remain for converting back to Date objects where
needed (update references in AddCouponDialog and related form initialization
code).
In `@src/components/containers/shared/taxes/add-tax-dialog.tsx`:
- Around line 8-82: The dialog doesn’t provide shopId required by
createTaxRateSchema, so update AddTaxDialog to always supply shopId to
EntityFormDialog: ensure initialValues passed into EntityFormDialog includes a
shopId (fallback to a required prop or context value) or intercept onSubmit to
merge { shopId } into the submitted TaxRateFormValues before validation;
reference AddTaxDialog, initialValues, EntityFormDialog, createTaxRateSchema and
onSubmit when making the change so every create/update operation includes
shopId.
In `@src/components/containers/store/accounts/enable-2fa-dialog.tsx`:
- Around line 22-105: The dialog leaves the password in state when closed via
overlay/escape; update Enable2FADialog to clear password whenever the dialog
closes by either (a) wrapping the passed onOpenChange with a local handler that
calls onOpenChange(value) and when value is false calls setPassword("") before
returning, and/or (b) adding a useEffect that watches the open prop and calls
setPassword("") when open becomes false; apply the same fix to the other dialog
component referenced (the similar dialog at lines 113-197) so any close path
clears the password state for privacy.
In `@src/components/containers/vendors/products/product-table-columns.tsx`:
- Around line 22-34: Rename the local type ProductMutationState to
ProductTableMutationState in the product-table-columns.tsx file and update all
usages and imports that reference it (e.g., in ProductColumnConfig, and where
product-table.tsx and shop-products-template.tsx import or refer to
ProductMutationState) so they now import/accept ProductTableMutationState;
ensure you update the type name in product-table.tsx (where the
columns/mutationState props are typed) and in shop-products-template.tsx (where
the component prop types or imports reference the old name) to avoid colliding
with the other ProductMutationState type.
In `@src/components/templates/admin/admin-coupons-template.tsx`:
- Around line 1-3: The AdminCouponTable import is pointing at the wrong module
that only exports VendorCouponTable; update the import of AdminCouponTable in
this file to import from the correct module
"@/components/containers/admin/coupons/admin-coupon-table" (and update any other
identical imports in this file around the other occurrences) so that the named
export AdminCouponTable is resolved from admin-coupon-table instead of
coupon-table.
🟡 Minor comments (18)
src/components/containers/store/marquee.tsx-31-35 (1)
31-35:⚠️ Potential issue | 🟡 MinorDuplicate keys when
el.keyis missing.
key: \${el.key ?? "dup"}`yields"dup"` for all duplicates when keys are missing, which causes React key collisions. Make the duplicate keys unique (e.g., include the index or a suffix).✅ Suggested fix
- ...items.map((el) => ({ el, key: `${el.key ?? "dup"}` })), + ...items.map((el, i) => ({ el, key: `${el.key ?? i}-dup` })),src/components/containers/shared/shipping/shipping-table.tsx-45-47 (1)
45-47:⚠️ Potential issue | 🟡 MinorPotential runtime error if
priceis nullish.The type assertion
as numberdoesn't provide runtime safety. Ifpriceisnullorundefined, calling.toFixed(2)will throw.🛡️ Proposed fix with defensive fallback
cell: ({ row }) => { const price = row.getValue("price") as number; - return <div className="font-medium">${price.toFixed(2)}</div>; + return <div className="font-medium">${(price ?? 0).toFixed(2)}</div>; },src/components/containers/shared/users/user-table.tsx-57-62 (1)
57-62:⚠️ Potential issue | 🟡 MinorPotential runtime error if
user.nameis undefined or empty.If
user.nameisundefined,null, or an empty string, calling.split(" ")will throw or produce unexpected fallback text (e.g., empty initials).🛡️ Suggested defensive handling
<AvatarFallback> - {user.name - .split(" ") - .map((n) => n[0]) - .join("") - .toUpperCase()} + {(user.name || "U") + .split(" ") + .map((n) => n[0]) + .filter(Boolean) + .join("") + .toUpperCase() || "U"} </AvatarFallback>src/components/containers/shared/users/user-table.tsx-125-129 (1)
125-129:⚠️ Potential issue | 🟡 MinorDate parsing may fail if
createdAtis a string from API response.If the data originates from a JSON API,
createdAtwill be a string, not aDateobject. Theformat()function from date-fns can handle ISO strings, but the type cast is misleading and could cause issues with other date operations.🔧 Suggested explicit parsing
cell: ({ row }) => { - const createdAt = row.getValue("createdAt") as Date; + const createdAt = new Date(row.getValue("createdAt") as string | Date); return (src/components/base/forms/subscribe-form.tsx-46-49 (1)
46-49:⚠️ Potential issue | 🟡 MinorEnsure
max-w-119.5is a valid Tailwind class.
If not configured in Tailwind, it won’t apply. Consider bracket syntax.🎯 Possible fix (adjust value as intended)
- <Field - data-invalid={isInvalid} - className="relative w-full max-w-119.5" - > + <Field + data-invalid={isInvalid} + className="relative w-full max-w-[119.5px]" + >src/components/base/common/collection-item.tsx-24-24 (1)
24-24:⚠️ Potential issue | 🟡 MinorFix typo in Tailwind class (
relativea).
This class won’t apply; likely intendedrelative.🐛 Proposed fix
- <div className={cn("relativea border-dashed `@6xl`:p-[30px] p-5", className)}> + <div className={cn("relative border-dashed `@6xl`:p-[30px] p-5", className)}>drizzle.config.ts-4-4 (1)
4-4:⚠️ Potential issue | 🟡 MinorAdd early validation for
DATABASE_URLenvironment variable.The non-null assertion (
!) onprocess.env.DATABASE_URLwill result inundefinedbeing passed to drizzle-kit if the variable is missing, potentially causing confusing downstream errors. Fail fast with a clear message.🛡️ Proposed fix to add validation
-const connectionString = process.env.DATABASE_URL!; +const connectionString = process.env.DATABASE_URL; + +if (!connectionString) { + throw new Error("DATABASE_URL environment variable is required"); +}src/components/base/vendors/my-shop/edit-shop-dialog.tsx-133-133 (1)
133-133:⚠️ Potential issue | 🟡 MinorNon-null assertion on environment variable may cause runtime error.
Using
import.meta.env.VITE_UPLOADCARE_PUB_KEY!assumes the environment variable is always defined. If it's missing, the uploader will receiveundefined, potentially causing a cryptic error.Consider adding a fallback or validating the key exists, or centralizing this config.
🛡️ Suggested approach
// In a config file or at module level const UPLOADCARE_PUB_KEY = import.meta.env.VITE_UPLOADCARE_PUB_KEY; if (!UPLOADCARE_PUB_KEY) { console.warn("VITE_UPLOADCARE_PUB_KEY is not configured"); } // Then use UPLOADCARE_PUB_KEY in the componentAlso applies to: 180-180
src/components/base/forms/form.tsx-51-58 (1)
51-58:⚠️ Potential issue | 🟡 MinorAsync function called without await may cause unexpected behavior.
handleStepSubmitis an async function, but it's called withoutawaitin theonSubmithandler. This means errors won't be caught properly and the submission flow may complete before validation finishes.Additionally,
event.preventDefault()is called both inhandleStepSubmit(line 24) and in the else branch (line 56), but ifmultiStepis truthy, the else branch'spreventDefaultwon't run - this is fine, but the async call without await is the main concern.🔧 Proposed fix
onSubmit={(event) => { + event.preventDefault(); onSubmit?.(event); if (multiStep) { - handleStepSubmit(event); + void handleStepSubmit(event); } else { - event.preventDefault(); form.handleSubmit(); } }}src/components/containers/auth/two-factor-form.tsx-93-107 (1)
93-107:⚠️ Potential issue | 🟡 MinorPotential double submission when OTP auto-completes.
The
onCompletecallback triggershandleVerifyautomatically when 6 digits are entered. If the user then clicks the "Verify Code" button while the first request is still in flight,handleVerifycould be called twice. The loading check at line 25 won't prevent theonCompletetrigger.Consider disabling OTP input during loading or adding a guard in
onComplete.🛡️ Proposed fix
<InputOTP maxLength={6} value={otp} onChange={setOtp} - onComplete={handleVerify} + onComplete={() => { + if (!loading) { + handleVerify(); + } + }} + disabled={loading} >src/components/containers/admin/admin-dashboard-sidebar.tsx-48-61 (1)
48-61:⚠️ Potential issue | 🟡 MinorAvoid nesting a Button inside a Link (invalid interactive element nesting).
A<button>inside an<a>is invalid HTML and breaks accessibility/keyboard behavior. TheButtoncomponent supportsasChild— use it to render the Link as the button element instead.Suggested fix
- ) : ( - <Link to="/auth/sign-in"> - <Button - variant="default" - className="w-full" - type="button" - size="lg" - > - Sign In - </Button> - </Link> - )} + ) : ( + <Button variant="default" className="w-full" size="lg" asChild> + <Link to="/auth/sign-in">Sign In</Link> + </Button> + )}src/components/base/vendors/my-shop/shop-card.tsx-32-39 (1)
32-39:⚠️ Potential issue | 🟡 MinorAdd user feedback when shop update fails.
The error is only logged to console. Users won't know why the update failed.
💡 Proposed fix
+import { toast } from "sonner"; + const handleUpdateShop = async (data: UpdateShopInput) => { try { await updateShop(data); setIsDialogOpen(false); + toast.success("Shop updated successfully"); } catch (error) { console.error("Failed to update shop:", error); + toast.error(error instanceof Error ? error.message : "Failed to update shop"); } };README.md-51-51 (1)
51-51:⚠️ Potential issue | 🟡 MinorFix typo: "bearbones" should be "barebones".
✏️ Proposed fix
-- **[Zustand](https://zustand-demo.pmnd.rs/)**: A small, fast, and scalable bearbones state-management solution. +- **[Zustand](https://zustand-demo.pmnd.rs/)**: A small, fast, and scalable barebones state-management solution.src/components/containers/shared/attributes/attribute-header.tsx-3-8 (1)
3-8:⚠️ Potential issue | 🟡 MinorFix description grammar (use plural “Attributes”).
The UI descriptions read awkwardly against the plural header title.💬 Proposed copy update
- adminDescription: "Manage product Attribute across the platform", - vendorDescription: "Manage your product Attribute and organization", + adminDescription: "Manage product Attributes across the platform", + vendorDescription: "Manage your product Attributes and organization",src/components/base/forms/entity-form-dialog.tsx-203-204 (1)
203-204:⚠️ Potential issue | 🟡 MinorEnvironment variable accessed without fallback handling.
Line 204 uses a non-null assertion (
!) onimport.meta.env.VITE_UPLOADCARE_PUB_KEY. If this variable is missing, the uploader will fail silently or produce cryptic errors.🛡️ Proposed defensive handling
- pubkey={import.meta.env.VITE_UPLOADCARE_PUB_KEY!} + pubkey={import.meta.env.VITE_UPLOADCARE_PUB_KEY ?? ""}Or add a runtime check:
const uploadcareKey = import.meta.env.VITE_UPLOADCARE_PUB_KEY; if (!uploadcareKey) { console.warn("VITE_UPLOADCARE_PUB_KEY is not configured"); }src/components/containers/shared/categories/add-category-dialog.tsx-29-32 (1)
29-32:⚠️ Potential issue | 🟡 Minor"None (Root Category)" option uses string "none" which may cause validation issues.
The
parentIdfield has a "none" string value for the root category option, butcreateCategorySchemadefinesparentIdasstring, optional and nullable. If the form submits "none" as theparentId, it won't be treated as null/undefined, potentially causing backend issues.🐛 Proposed fix: use empty string or handle "none" in submission
Option 1 - Use empty string:
const categoryOptions: EntityFormField["selectOptions"] = [ - { label: "None (Root Category)", value: "none" }, + { label: "None (Root Category)", value: "" }, ...categories.map((cat) => ({ label: cat.name, value: cat.id })), ];Option 2 - Transform on submit (in parent component):
const handleSubmit = (data: CategoryFormValues) => { const payload = { ...data, parentId: data.parentId === "none" ? null : data.parentId, }; // submit payload };src/components/containers/shared/coupons/coupon-table-columns.tsx-46-60 (1)
46-60:⚠️ Potential issue | 🟡 MinorUse the caller-provided status options.
getSharedCouponFiltersignoresoptions.statusOptionsand hardcodes values, so custom labels/values won’t take effect.♻️ Suggested fix
{ id: "isActive", label: "Status", type: "select", - options: [ - { label: "Active", value: "true" }, - { label: "Inactive", value: "false" }, - ], + options: options.statusOptions, placeholder: "Filter by status", },src/components/containers/shared/attributes/attribute-table.tsx-58-79 (1)
58-79:⚠️ Potential issue | 🟡 MinorClient mode drops filter/search configuration.
filterableColumnsis computed but only used in the server branch, so local data loses filters and search UI. Consider passing the same props in the client path for parity.Suggested fix
return ( <DataTable columns={columns} data={attributes || []} + filterableColumns={filterableColumns} + globalFilterPlaceholder="Search attributes..." className={className} /> );
| export default function AdminErrorComponent({ | ||
| error, | ||
| reset, | ||
| info, | ||
| }: ErrorComponentProps) { | ||
| if (error.message.includes("Admin access required")) { | ||
| return <AdminAccessDenied />; | ||
| } | ||
|
|
||
| return <DefaultErrorComponent error={error} reset={reset} info={info} />; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's look at the file mentioned in the review
cat -n src/components/base/error/admin-error-component.tsx | head -70Repository: FullStack-Flow/shop-stack
Length of output: 2652
🏁 Script executed:
# Find where ErrorComponentProps is defined
fd . src --type f -name "*.ts" -o -name "*.tsx" | xargs rg "ErrorComponentProps" -B 2 -A 5Repository: FullStack-Flow/shop-stack
Length of output: 2715
🏁 Script executed:
# Check for imports and type definitions in the error component directory
ls -la src/components/base/error/
cat -n src/components/base/error/*.tsx src/components/base/error/*.ts 2>/dev/null | head -200Repository: FullStack-Flow/shop-stack
Length of output: 8054
🌐 Web query:
TanStack React Router ErrorComponentProps type definition error parameter
💡 Result:
In TanStack React Router, the errorComponent receives ErrorComponentProps<TError = Error>, defined as:
export type ErrorComponentProps<TError = Error> = {
error: TError
info?: { componentStack: string }
reset: () => void
}So the error parameter is generic (defaults to Error). If you throw something else (e.g. a custom error shape), type it like:
function MyErrorComponent({ error }: ErrorComponentProps<MyCustomError>) {
// ...
}[1], [2]
Guard against non-Error shapes before reading error.message.
TanStack React Router's ErrorComponentProps is a generic type that defaults to Error but can be parameterized with any custom type. Without a type guard, accessing error.message may fail if a non-Error type is used. The DefaultErrorComponent in this codebase already implements this pattern—follow the same approach for consistency.
🛠️ Suggested guard
export default function AdminErrorComponent({
error,
reset,
info,
}: ErrorComponentProps) {
- if (error.message.includes("Admin access required")) {
+ const message = error instanceof Error ? error.message : String(error);
+ if (message.includes("Admin access required")) {
return <AdminAccessDenied />;
}
return <DefaultErrorComponent error={error} reset={reset} info={info} />;
}🤖 Prompt for AI Agents
In `@src/components/base/error/admin-error-component.tsx` around lines 53 - 63,
AdminErrorComponent currently reads error.message directly which can throw if
ErrorComponentProps is parameterized with a non-Error shape; add a type guard
like the pattern used in DefaultErrorComponent to ensure error is an Error
(e.g., error instanceof Error or typeof error?.message === "string") before
checking message, and only render AdminAccessDenied when the guard passes and
message.includes("Admin access required"); otherwise fall back to rendering
DefaultErrorComponent with the original error, reset, and info.
| const form = useForm({ | ||
| defaultValues, | ||
| onSubmit: async ({ value, formApi }) => { | ||
| if (validationSchema) { | ||
| await formApi.validateAllFields("blur"); | ||
| await formApi.validateAllFields("change"); | ||
|
|
||
| const errors = Object.entries(formApi.state.fieldMeta) | ||
| .filter(([_key, meta]) => meta?.errors && meta.errors.length > 0) | ||
| .map(([key, meta]) => ({ | ||
| field: key, | ||
| errors: meta?.errors, | ||
| })); | ||
|
|
||
| if (errors.length > 0) { | ||
| console.error("Form validation failed:", errors); | ||
| toast.error("Please fix the errors in the form before submitting."); | ||
| return; | ||
| } | ||
| } | ||
|
|
||
| await onSubmit(value as T); | ||
| onOpenChange(false); | ||
| form.reset(); | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Potential issue: Dialog closes even when onSubmit throws an error.
The submission handler calls onSubmit(value), then unconditionally closes the dialog and resets the form (lines 117-119). If onSubmit throws an exception, the catch block is missing, so the dialog will still close and form will reset despite the failure.
🐛 Proposed fix to handle submission errors properly
await onSubmit(value as T);
- onOpenChange(false);
- form.reset();
+ } catch (error) {
+ // Let the caller handle the error - don't close the dialog
+ throw error;
+ }
+ onOpenChange(false);
+ form.reset();Alternatively, wrap the entire success path:
- await onSubmit(value as T);
- onOpenChange(false);
- form.reset();
+ try {
+ await onSubmit(value as T);
+ onOpenChange(false);
+ form.reset();
+ } catch (error) {
+ // Re-throw or handle - dialog stays open on failure
+ throw error;
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const form = useForm({ | |
| defaultValues, | |
| onSubmit: async ({ value, formApi }) => { | |
| if (validationSchema) { | |
| await formApi.validateAllFields("blur"); | |
| await formApi.validateAllFields("change"); | |
| const errors = Object.entries(formApi.state.fieldMeta) | |
| .filter(([_key, meta]) => meta?.errors && meta.errors.length > 0) | |
| .map(([key, meta]) => ({ | |
| field: key, | |
| errors: meta?.errors, | |
| })); | |
| if (errors.length > 0) { | |
| console.error("Form validation failed:", errors); | |
| toast.error("Please fix the errors in the form before submitting."); | |
| return; | |
| } | |
| } | |
| await onSubmit(value as T); | |
| onOpenChange(false); | |
| form.reset(); | |
| }, | |
| }); | |
| const form = useForm({ | |
| defaultValues, | |
| onSubmit: async ({ value, formApi }) => { | |
| if (validationSchema) { | |
| await formApi.validateAllFields("blur"); | |
| await formApi.validateAllFields("change"); | |
| const errors = Object.entries(formApi.state.fieldMeta) | |
| .filter(([_key, meta]) => meta?.errors && meta.errors.length > 0) | |
| .map(([key, meta]) => ({ | |
| field: key, | |
| errors: meta?.errors, | |
| })); | |
| if (errors.length > 0) { | |
| console.error("Form validation failed:", errors); | |
| toast.error("Please fix the errors in the form before submitting."); | |
| return; | |
| } | |
| } | |
| try { | |
| await onSubmit(value as T); | |
| onOpenChange(false); | |
| form.reset(); | |
| } catch (error) { | |
| // Re-throw or handle - dialog stays open on failure | |
| throw error; | |
| } | |
| }, | |
| }); |
🤖 Prompt for AI Agents
In `@src/components/base/forms/entity-form-dialog.tsx` around lines 96 - 121, The
dialog currently closes and the form resets unconditionally after awaiting
onSubmit in the useForm onSubmit handler; wrap the await onSubmit(value as T)
call in a try/catch so that onOpenChange(false) and form.reset() only run on
successful completion, and in the catch log the error and show a user-facing
error (e.g., toast.error) so the dialog remains open when onSubmit throws;
update the onSubmit handler in the useForm invocation (the onSubmit function,
form.reset and onOpenChange calls) to implement this error handling.
| const prevOpenRef = useRef(open); | ||
|
|
||
| useEffect(() => { | ||
| if (open && !prevOpenRef.current) { | ||
| if (initialValues) { | ||
| Object.entries(initialValues).forEach(([key, value]) => { | ||
| form.setFieldValue(key as any, value); | ||
| }); | ||
| // Also set defaults for fields not in initialValues | ||
| fields.forEach((field) => { | ||
| if (!(field.name in initialValues)) { | ||
| form.setFieldValue(field.name as any, field.defaultValue ?? ""); | ||
| } | ||
| }); | ||
| } else { | ||
| form.reset(); | ||
| fields.forEach((field) => { | ||
| const value = field.defaultValue ?? ""; | ||
| if (value !== "") { | ||
| form.setFieldValue(field.name, value); | ||
| } | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| if (!open && prevOpenRef.current) { | ||
| form.reset(); | ||
| } | ||
|
|
||
| prevOpenRef.current = open; | ||
| }, [open, initialValues, fields, form]); |
There was a problem hiding this comment.
The form object in useEffect dependencies may cause infinite loops.
Including form in the dependency array (line 153) can cause repeated re-renders because useForm may return a new object reference on each render. The effect sets field values which triggers state changes, potentially causing a loop.
🔧 Proposed fix: remove `form` from dependencies or memoize form methods
- }, [open, initialValues, fields, form]);
+ }, [open, initialValues, fields]);If form methods are needed, consider using refs or extracting only the stable methods needed:
+ const setFieldValue = form.setFieldValue;
+ const reset = form.reset;
+
useEffect(() => {
if (open && !prevOpenRef.current) {
if (initialValues) {
Object.entries(initialValues).forEach(([key, value]) => {
- form.setFieldValue(key as any, value);
+ setFieldValue(key as any, value);
});
// ...
}
}
// ...
- }, [open, initialValues, fields, form]);
+ }, [open, initialValues, fields, setFieldValue, reset]);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const prevOpenRef = useRef(open); | |
| useEffect(() => { | |
| if (open && !prevOpenRef.current) { | |
| if (initialValues) { | |
| Object.entries(initialValues).forEach(([key, value]) => { | |
| form.setFieldValue(key as any, value); | |
| }); | |
| // Also set defaults for fields not in initialValues | |
| fields.forEach((field) => { | |
| if (!(field.name in initialValues)) { | |
| form.setFieldValue(field.name as any, field.defaultValue ?? ""); | |
| } | |
| }); | |
| } else { | |
| form.reset(); | |
| fields.forEach((field) => { | |
| const value = field.defaultValue ?? ""; | |
| if (value !== "") { | |
| form.setFieldValue(field.name, value); | |
| } | |
| }); | |
| } | |
| } | |
| if (!open && prevOpenRef.current) { | |
| form.reset(); | |
| } | |
| prevOpenRef.current = open; | |
| }, [open, initialValues, fields, form]); | |
| const prevOpenRef = useRef(open); | |
| useEffect(() => { | |
| if (open && !prevOpenRef.current) { | |
| if (initialValues) { | |
| Object.entries(initialValues).forEach(([key, value]) => { | |
| form.setFieldValue(key as any, value); | |
| }); | |
| // Also set defaults for fields not in initialValues | |
| fields.forEach((field) => { | |
| if (!(field.name in initialValues)) { | |
| form.setFieldValue(field.name as any, field.defaultValue ?? ""); | |
| } | |
| }); | |
| } else { | |
| form.reset(); | |
| fields.forEach((field) => { | |
| const value = field.defaultValue ?? ""; | |
| if (value !== "") { | |
| form.setFieldValue(field.name, value); | |
| } | |
| }); | |
| } | |
| } | |
| if (!open && prevOpenRef.current) { | |
| form.reset(); | |
| } | |
| prevOpenRef.current = open; | |
| }, [open, initialValues, fields]); |
🤖 Prompt for AI Agents
In `@src/components/base/forms/entity-form-dialog.tsx` around lines 123 - 153, The
effect currently lists form in its dependency array which can cause re-renders
loops because useForm may return a new object reference; remove form from the
dependency array and instead reference a stable ref for the form methods (e.g.,
create a formRef = useRef(form) and update formRef.current = form on each
render, then call formRef.current.setFieldValue and formRef.current.reset inside
the useEffect), and change the dependency array to only [open, initialValues,
fields]; keep the existing logic around prevOpenRef, initialValues, and defaults
but use the stable formRef methods to avoid infinite loops.
| const handleSubmit = (data: UpdateShopInput) => { | ||
| onSubmit(data); | ||
| }; |
There was a problem hiding this comment.
onSubmit returns a Promise but is not awaited.
The handleSubmit function calls onSubmit(data) without awaiting it. Since onSubmit is typed as Promise<void>, any errors thrown during submission won't be caught, and the form won't know when submission completes.
🐛 Proposed fix
- const handleSubmit = (data: UpdateShopInput) => {
- onSubmit(data);
+ const handleSubmit = async (data: UpdateShopInput) => {
+ await onSubmit(data);
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const handleSubmit = (data: UpdateShopInput) => { | |
| onSubmit(data); | |
| }; | |
| const handleSubmit = async (data: UpdateShopInput) => { | |
| await onSubmit(data); | |
| }; |
🤖 Prompt for AI Agents
In `@src/components/base/vendors/my-shop/edit-shop-dialog.tsx` around lines 55 -
57, handleSubmit currently calls onSubmit(data) but doesn't await the Promise;
make handleSubmit async and await onSubmit(data) so submission completion and
errors are propagated; locate the handleSubmit function and change it to async,
await onSubmit(data) and optionally wrap the await in try/catch to
handle/log/rethrow submission errors as appropriate for the component's error
handling flow (referencing handleSubmit, onSubmit, and UpdateShopInput).
| <Checkbox | ||
| id={field.name} | ||
| name={field.name} | ||
| checked={Boolean(field.state.value)} | ||
| onChange={(e) => | ||
| field.handleChange((e.target as HTMLInputElement).checked) | ||
| } | ||
| /> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check the Checkbox component's onChange signature
rg -A 15 'export.*Checkbox' src/components/ui/checkbox.tsxRepository: FullStack-Flow/shop-stack
Length of output: 89
🏁 Script executed:
cat src/components/ui/checkbox.tsxRepository: FullStack-Flow/shop-stack
Length of output: 1284
🏁 Script executed:
#!/bin/bash
# Search for other usages of the Checkbox component with onChange handler
rg -B 3 -A 3 'Checkbox.*onChange' src/ --type tsxRepository: FullStack-Flow/shop-stack
Length of output: 96
🏁 Script executed:
#!/bin/bash
# Search for Checkbox component usages with onChange handler
rg -B 3 -A 3 'Checkbox.*onChange' src/Repository: FullStack-Flow/shop-stack
Length of output: 51
🏁 Script executed:
#!/bin/bash
# Read the auth-form.tsx file to see the Checkbox onChange handler
sed -n '220,240p' src/components/containers/auth/auth-form.tsxRepository: FullStack-Flow/shop-stack
Length of output: 782
🏁 Script executed:
#!/bin/bash
# Check package.json for Radix version
cat package.json | grep -A 2 -B 2 "@radix-ui/react-checkbox"Repository: FullStack-Flow/shop-stack
Length of output: 290
🏁 Script executed:
#!/bin/bash
# Search for all Checkbox component usages in the codebase
rg 'Checkbox' src/ -A 5 | head -100Repository: FullStack-Flow/shop-stack
Length of output: 6809
🌐 Web query:
Radix UI checkbox onChange event signature React v1.3
💡 Result:
In @radix-ui/react-checkbox v1.3.x, Checkbox.Root (and Checkbox) does not use React’s onChange prop. It exposes:
onCheckedChange?(checked: boolean | "indeterminate"): voidSo your handler receives the next checked state as boolean | "indeterminate", not a DOM event. [1][2]
Sources: Radix Checkbox docs (v1.3.3) [1], published type definitions for v1.3.3 [2].
Fix Checkbox handler: use onCheckedChange instead of onChange.
The Checkbox component is based on Radix UI's CheckboxPrimitive.Root (v1.3.3), which does not have an onChange prop. It uses onCheckedChange that receives the checked state as a boolean directly, not a DOM event. The current code attempting to cast e.target to HTMLInputElement will fail at runtime.
Change to:
<Checkbox
id={field.name}
name={field.name}
checked={Boolean(field.state.value)}
onCheckedChange={(checked) =>
field.handleChange(checked)
}
/>🤖 Prompt for AI Agents
In `@src/components/containers/auth/auth-form.tsx` around lines 227 - 234, The
Checkbox handler is incorrect: Radix Checkbox (Checkbox /
CheckboxPrimitive.Root) uses onCheckedChange(boolean) not onChange(event).
Replace the onChange usage on the Checkbox with onCheckedChange and pass the
boolean directly to field.handleChange (keep id/name as field.name and checked
as Boolean(field.state.value)); remove the event casting to HTMLInputElement to
avoid the runtime failure.
| @@ -1,243 +1,369 @@ | |||
| import { useForm } from '@tanstack/react-form'; | |||
| import { Button } from '@/components/ui/button'; | |||
| import { format, parseISO } from "date-fns"; | |||
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n src/components/containers/shared/coupons/add-coupon-dialog.tsx | head -50Repository: FullStack-Flow/shop-stack
Length of output: 1853
🏁 Script executed:
# Check the file size first
wc -l src/components/containers/shared/coupons/add-coupon-dialog.tsxRepository: FullStack-Flow/shop-stack
Length of output: 135
🏁 Script executed:
# Look at the specific lines mentioned (1, 138-188, 331-343)
sed -n '1,15p' src/components/containers/shared/coupons/add-coupon-dialog.tsxRepository: FullStack-Flow/shop-stack
Length of output: 665
🏁 Script executed:
# Check lines 138-188 for date handling
sed -n '138,188p' src/components/containers/shared/coupons/add-coupon-dialog.tsxRepository: FullStack-Flow/shop-stack
Length of output: 1740
🏁 Script executed:
# Check lines 331-343
sed -n '331,343p' src/components/containers/shared/coupons/add-coupon-dialog.tsxRepository: FullStack-Flow/shop-stack
Length of output: 670
🏁 Script executed:
# Find CouponItem type definition
rg "interface CouponItem|type CouponItem" --type typescript -A 10Repository: FullStack-Flow/shop-stack
Length of output: 103
🏁 Script executed:
# Check DatePicker component implementation
find . -type f -name "date-picker*" | head -5Repository: FullStack-Flow/shop-stack
Length of output: 104
🏁 Script executed:
# Look at DatePicker component
cat src/components/ui/date-picker.tsx | head -80Repository: FullStack-Flow/shop-stack
Length of output: 1383
🏁 Script executed:
# Find CouponItem type definition
rg "CouponItem" --type tsx --type ts -A 15 | head -60Repository: FullStack-Flow/shop-stack
Length of output: 96
🏁 Script executed:
# Search for CouponFormValues type
rg "CouponFormValues" --type tsx --type ts -A 10 | head -50Repository: FullStack-Flow/shop-stack
Length of output: 96
🏁 Script executed:
# Look at coupons types file directly
find . -type f -path "*/types/coupons*"Repository: FullStack-Flow/shop-stack
Length of output: 91
🏁 Script executed:
# Read the coupons types file
cat src/types/coupons.tsRepository: FullStack-Flow/shop-stack
Length of output: 6528
🏁 Script executed:
# Find the Coupon validator definition
find . -type f -path "*/validators/*coupon*"Repository: FullStack-Flow/shop-stack
Length of output: 143
🏁 Script executed:
# Check the coupon-query validator
cat src/lib/validators/shared/coupon-query.ts | head -150Repository: FullStack-Flow/shop-stack
Length of output: 4013
🏁 Script executed:
# Check the coupon validator for CouponFormValues definition
cat src/lib/validators/coupon.ts | grep -A 50 "CouponFormValues\|couponFormSchema"Repository: FullStack-Flow/shop-stack
Length of output: 192
🏁 Script executed:
# Get more of the coupon-query file to find the form schema
cat src/lib/validators/shared/coupon-query.ts | tail -200Repository: FullStack-Flow/shop-stack
Length of output: 6237
🏁 Script executed:
# Find the couponFormSchema definition in the file
rg "couponFormSchema\s*=" src/lib/validators/shared/coupon-query.ts -A 30Repository: FullStack-Flow/shop-stack
Length of output: 1250
Replace UTC toISOString() with local-date formatting for defaults and initial values.
Using toISOString().split("T")[0] forces UTC conversion, which shifts the displayed day for users in non-UTC timezones. For example, a user in UTC+5 selecting 2024-01-15 will see the date stored as 2024-01-14 due to UTC offset. Use format() with local dates instead, matching the DatePicker's expected format.
✅ Suggested fix
-import { format, parseISO } from "date-fns";
+import { addDays, format, parseISO } from "date-fns";
...
- defaultValue: new Date().toISOString().split("T")[0],
+ defaultValue: format(new Date(), "yyyy-MM-dd"),
...
- defaultValue: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
- .toISOString()
- .split("T")[0],
+ defaultValue: format(addDays(new Date(), 30), "yyyy-MM-dd"),
...
- activeFrom: initialValues.activeFrom
- ? new Date(initialValues.activeFrom).toISOString().split("T")[0]
- : "",
- activeTo: initialValues.activeTo
- ? new Date(initialValues.activeTo).toISOString().split("T")[0]
- : "",
+ activeFrom: initialValues.activeFrom
+ ? format(new Date(initialValues.activeFrom), "yyyy-MM-dd")
+ : "",
+ activeTo: initialValues.activeTo
+ ? format(new Date(initialValues.activeTo), "yyyy-MM-dd")
+ : "",Lines 138-140, 180-182, 331-343
🤖 Prompt for AI Agents
In `@src/components/containers/shared/coupons/add-coupon-dialog.tsx` at line 1,
The component AddCouponDialog currently uses toISOString().split("T")[0] to
derive default/initial date strings (three places around the existing
toISOString usage), which forces UTC and shifts local dates; replace those uses
with date-fns format(...) producing a local-date string like format(someDate,
"yyyy-MM-dd") (you already import format and parseISO), e.g. when building
defaultStart/defaultEnd and initial form values call format(new
Date(valueOrToday), "yyyy-MM-dd") instead of toISOString().split("T")[0], and
ensure any parseISO usages remain for converting back to Date objects where
needed (update references in AddCouponDialog and related form initialization
code).
| interface AddTaxDialogProps { | ||
| open: boolean; | ||
| onOpenChange: (open: boolean) => void; | ||
| onSubmit: (data: TaxFormValues) => void; | ||
| onSubmit: (data: TaxRateFormValues) => void; | ||
| isSubmitting?: boolean; | ||
| initialValues?: Partial<TaxRateFormValues> | null; | ||
| } | ||
|
|
||
| export function AddTaxDialog({ | ||
| open, | ||
| onOpenChange, | ||
| onSubmit, | ||
| isSubmitting = false, | ||
| initialValues, | ||
| }: AddTaxDialogProps) { | ||
| const form = useForm({ | ||
| defaultValues: { | ||
| name: '', | ||
| rate: 0, | ||
| country: '', | ||
| state: '', | ||
| zip: '', | ||
| priority: 1, | ||
| const fields: EntityFormField[] = [ | ||
| { | ||
| name: "name", | ||
| label: "Name", | ||
| required: true, | ||
| placeholder: "Enter tax rate name", | ||
| description: "Name for your tax rate (e.g., VAT, Sales Tax)", | ||
| }, | ||
| { | ||
| name: "rate", | ||
| label: "Rate (%)", | ||
| required: true, | ||
| placeholder: "0.00", | ||
| description: "Tax rate percentage (between 0.01 and 100)", | ||
| }, | ||
| { | ||
| name: "country", | ||
| label: "Country", | ||
| required: true, | ||
| placeholder: "US, UK, CA", | ||
| description: "ISO country code (2 characters)", | ||
| }, | ||
| { | ||
| name: "state", | ||
| label: "State", | ||
| placeholder: "NY, CA, TX", | ||
| description: "State/Province code (optional)", | ||
| }, | ||
| { | ||
| name: "zip", | ||
| label: "ZIP Code", | ||
| placeholder: "12345", | ||
| description: "ZIP/Postal code pattern (optional)", | ||
| }, | ||
| onSubmit: async ({ value }) => { | ||
| onSubmit(value); | ||
| onOpenChange(false); | ||
| form.reset(); | ||
| { | ||
| name: "priority", | ||
| label: "Priority", | ||
| required: true, | ||
| placeholder: "1", | ||
| description: | ||
| "Priority for tax calculation (lower number = higher priority)", | ||
| }, | ||
| }); | ||
| ]; | ||
|
|
||
| return ( | ||
| <Dialog open={open} onOpenChange={onOpenChange}> | ||
| <DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-150"> | ||
| <DialogHeader> | ||
| <DialogTitle>Add New Tax Rate</DialogTitle> | ||
| <DialogDescription> | ||
| Add a new tax rate for your shop. | ||
| </DialogDescription> | ||
| </DialogHeader> | ||
|
|
||
| <form | ||
| onSubmit={(e) => { | ||
| e.preventDefault(); | ||
| e.stopPropagation(); | ||
| form.handleSubmit(); | ||
| }} | ||
| className="space-y-6" | ||
| > | ||
| <FieldGroup> | ||
| <div className="grid gap-4"> | ||
| {/* Name Field */} | ||
| <form.Field name="name"> | ||
| {(field) => { | ||
| const isInvalid = | ||
| field.state.meta.isTouched && !field.state.meta.isValid; | ||
| return ( | ||
| <Field data-invalid={isInvalid}> | ||
| <FieldLabel htmlFor={field.name}>Name*</FieldLabel> | ||
| <Input | ||
| id={field.name} | ||
| name={field.name} | ||
| value={field.state.value} | ||
| onBlur={field.handleBlur} | ||
| onChange={(e) => field.handleChange(e.target.value)} | ||
| placeholder="e.g. VAT Standard" | ||
| aria-invalid={isInvalid} | ||
| /> | ||
| {isInvalid && ( | ||
| <FieldError | ||
| errors={getFieldErrors(field.state.meta.errors)} | ||
| /> | ||
| )} | ||
| </Field> | ||
| ); | ||
| }} | ||
| </form.Field> | ||
|
|
||
| {/* Rate Field */} | ||
| <form.Field name="rate"> | ||
| {(field) => { | ||
| const isInvalid = | ||
| field.state.meta.isTouched && !field.state.meta.isValid; | ||
| return ( | ||
| <Field data-invalid={isInvalid}> | ||
| <FieldLabel htmlFor={field.name}>Rate (%)*</FieldLabel> | ||
| <Input | ||
| id={field.name} | ||
| name={field.name} | ||
| type="number" | ||
| step="0.01" | ||
| min="0" | ||
| max="100" | ||
| value={field.state.value} | ||
| onBlur={field.handleBlur} | ||
| onChange={(e) => | ||
| field.handleChange(parseFloat(e.target.value)) | ||
| } | ||
| placeholder="0.00" | ||
| aria-invalid={isInvalid} | ||
| /> | ||
| {isInvalid && ( | ||
| <FieldError | ||
| errors={getFieldErrors(field.state.meta.errors)} | ||
| /> | ||
| )} | ||
| </Field> | ||
| ); | ||
| }} | ||
| </form.Field> | ||
|
|
||
| {/* Country Field */} | ||
| <form.Field name="country"> | ||
| {(field) => { | ||
| const isInvalid = | ||
| field.state.meta.isTouched && !field.state.meta.isValid; | ||
| return ( | ||
| <Field data-invalid={isInvalid}> | ||
| <FieldLabel htmlFor={field.name}>Country*</FieldLabel> | ||
| <Input | ||
| id={field.name} | ||
| name={field.name} | ||
| value={field.state.value} | ||
| onBlur={field.handleBlur} | ||
| onChange={(e) => field.handleChange(e.target.value)} | ||
| placeholder="e.g. US, UK, CA" | ||
| aria-invalid={isInvalid} | ||
| /> | ||
| {isInvalid && ( | ||
| <FieldError | ||
| errors={getFieldErrors(field.state.meta.errors)} | ||
| /> | ||
| )} | ||
| </Field> | ||
| ); | ||
| }} | ||
| </form.Field> | ||
|
|
||
| {/* State Field */} | ||
| <form.Field name="state"> | ||
| {(field) => { | ||
| return ( | ||
| <Field> | ||
| <FieldLabel htmlFor={field.name}> | ||
| State (Optional) | ||
| </FieldLabel> | ||
| <Input | ||
| id={field.name} | ||
| name={field.name} | ||
| value={field.state.value} | ||
| onBlur={field.handleBlur} | ||
| onChange={(e) => field.handleChange(e.target.value)} | ||
| placeholder="e.g. NY, CA (optional)" | ||
| /> | ||
| </Field> | ||
| ); | ||
| }} | ||
| </form.Field> | ||
|
|
||
| {/* ZIP Field */} | ||
| <form.Field name="zip"> | ||
| {(field) => { | ||
| return ( | ||
| <Field> | ||
| <FieldLabel htmlFor={field.name}> | ||
| ZIP Code (Optional) | ||
| </FieldLabel> | ||
| <Input | ||
| id={field.name} | ||
| name={field.name} | ||
| value={field.state.value} | ||
| onBlur={field.handleBlur} | ||
| onChange={(e) => field.handleChange(e.target.value)} | ||
| placeholder="e.g. 10001 (optional)" | ||
| /> | ||
| </Field> | ||
| ); | ||
| }} | ||
| </form.Field> | ||
|
|
||
| {/* Priority Field */} | ||
| <form.Field name="priority"> | ||
| {(field) => { | ||
| const isInvalid = | ||
| field.state.meta.isTouched && !field.state.meta.isValid; | ||
| return ( | ||
| <Field data-invalid={isInvalid}> | ||
| <FieldLabel htmlFor={field.name}>Priority*</FieldLabel> | ||
| <Input | ||
| id={field.name} | ||
| name={field.name} | ||
| type="number" | ||
| min="1" | ||
| value={field.state.value} | ||
| onBlur={field.handleBlur} | ||
| onChange={(e) => | ||
| field.handleChange(parseInt(e.target.value, 10)) | ||
| } | ||
| placeholder="1" | ||
| aria-invalid={isInvalid} | ||
| /> | ||
| {isInvalid && ( | ||
| <FieldError | ||
| errors={getFieldErrors(field.state.meta.errors)} | ||
| /> | ||
| )} | ||
| </Field> | ||
| ); | ||
| }} | ||
| </form.Field> | ||
| </div> | ||
| </FieldGroup> | ||
|
|
||
| <DialogFooter> | ||
| <Button | ||
| type="button" | ||
| variant="outline" | ||
| onClick={() => onOpenChange(false)} | ||
| > | ||
| Cancel | ||
| </Button> | ||
| <form.Subscribe | ||
| selector={(state) => [state.canSubmit, state.isSubmitting]} | ||
| > | ||
| {([canSubmit, isSubmitting]) => ( | ||
| <Button type="submit" disabled={!canSubmit || isSubmitting}> | ||
| {isSubmitting ? 'Adding...' : 'Add Tax Rate'} | ||
| </Button> | ||
| )} | ||
| </form.Subscribe> | ||
| </DialogFooter> | ||
| </form> | ||
| </DialogContent> | ||
| </Dialog> | ||
| <EntityFormDialog<TaxRateFormValues> | ||
| open={open} | ||
| onOpenChange={onOpenChange} | ||
| onSubmit={onSubmit} | ||
| isSubmitting={isSubmitting} | ||
| initialValues={initialValues} | ||
| title="Tax Rate" | ||
| description="Create or update a tax rate for your shop." | ||
| validationSchema={createTaxRateSchema} | ||
| submitButtonText={{ | ||
| create: "Create Tax Rate", | ||
| update: "Update Tax Rate", | ||
| }} | ||
| fields={fields} | ||
| /> |
There was a problem hiding this comment.
Ensure shopId is always provided to satisfy the schema.
createTaxRateSchema requires shopId, but this dialog does not collect or inject it. New tax rates will fail validation unless initialValues.shopId is always set.
✅ One way to guarantee `shopId`
interface AddTaxDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSubmit: (data: TaxRateFormValues) => void;
isSubmitting?: boolean;
initialValues?: Partial<TaxRateFormValues> | null;
+ shopId: string;
}
export function AddTaxDialog({
open,
onOpenChange,
onSubmit,
isSubmitting = false,
initialValues,
+ shopId,
}: AddTaxDialogProps) {
...
return (
<EntityFormDialog<TaxRateFormValues>
open={open}
onOpenChange={onOpenChange}
onSubmit={onSubmit}
isSubmitting={isSubmitting}
- initialValues={initialValues}
+ initialValues={{ ...(initialValues ?? {}), shopId }}
title="Tax Rate"
...
/>
);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| interface AddTaxDialogProps { | |
| open: boolean; | |
| onOpenChange: (open: boolean) => void; | |
| onSubmit: (data: TaxFormValues) => void; | |
| onSubmit: (data: TaxRateFormValues) => void; | |
| isSubmitting?: boolean; | |
| initialValues?: Partial<TaxRateFormValues> | null; | |
| } | |
| export function AddTaxDialog({ | |
| open, | |
| onOpenChange, | |
| onSubmit, | |
| isSubmitting = false, | |
| initialValues, | |
| }: AddTaxDialogProps) { | |
| const form = useForm({ | |
| defaultValues: { | |
| name: '', | |
| rate: 0, | |
| country: '', | |
| state: '', | |
| zip: '', | |
| priority: 1, | |
| const fields: EntityFormField[] = [ | |
| { | |
| name: "name", | |
| label: "Name", | |
| required: true, | |
| placeholder: "Enter tax rate name", | |
| description: "Name for your tax rate (e.g., VAT, Sales Tax)", | |
| }, | |
| { | |
| name: "rate", | |
| label: "Rate (%)", | |
| required: true, | |
| placeholder: "0.00", | |
| description: "Tax rate percentage (between 0.01 and 100)", | |
| }, | |
| { | |
| name: "country", | |
| label: "Country", | |
| required: true, | |
| placeholder: "US, UK, CA", | |
| description: "ISO country code (2 characters)", | |
| }, | |
| { | |
| name: "state", | |
| label: "State", | |
| placeholder: "NY, CA, TX", | |
| description: "State/Province code (optional)", | |
| }, | |
| { | |
| name: "zip", | |
| label: "ZIP Code", | |
| placeholder: "12345", | |
| description: "ZIP/Postal code pattern (optional)", | |
| }, | |
| onSubmit: async ({ value }) => { | |
| onSubmit(value); | |
| onOpenChange(false); | |
| form.reset(); | |
| { | |
| name: "priority", | |
| label: "Priority", | |
| required: true, | |
| placeholder: "1", | |
| description: | |
| "Priority for tax calculation (lower number = higher priority)", | |
| }, | |
| }); | |
| ]; | |
| return ( | |
| <Dialog open={open} onOpenChange={onOpenChange}> | |
| <DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-150"> | |
| <DialogHeader> | |
| <DialogTitle>Add New Tax Rate</DialogTitle> | |
| <DialogDescription> | |
| Add a new tax rate for your shop. | |
| </DialogDescription> | |
| </DialogHeader> | |
| <form | |
| onSubmit={(e) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| form.handleSubmit(); | |
| }} | |
| className="space-y-6" | |
| > | |
| <FieldGroup> | |
| <div className="grid gap-4"> | |
| {/* Name Field */} | |
| <form.Field name="name"> | |
| {(field) => { | |
| const isInvalid = | |
| field.state.meta.isTouched && !field.state.meta.isValid; | |
| return ( | |
| <Field data-invalid={isInvalid}> | |
| <FieldLabel htmlFor={field.name}>Name*</FieldLabel> | |
| <Input | |
| id={field.name} | |
| name={field.name} | |
| value={field.state.value} | |
| onBlur={field.handleBlur} | |
| onChange={(e) => field.handleChange(e.target.value)} | |
| placeholder="e.g. VAT Standard" | |
| aria-invalid={isInvalid} | |
| /> | |
| {isInvalid && ( | |
| <FieldError | |
| errors={getFieldErrors(field.state.meta.errors)} | |
| /> | |
| )} | |
| </Field> | |
| ); | |
| }} | |
| </form.Field> | |
| {/* Rate Field */} | |
| <form.Field name="rate"> | |
| {(field) => { | |
| const isInvalid = | |
| field.state.meta.isTouched && !field.state.meta.isValid; | |
| return ( | |
| <Field data-invalid={isInvalid}> | |
| <FieldLabel htmlFor={field.name}>Rate (%)*</FieldLabel> | |
| <Input | |
| id={field.name} | |
| name={field.name} | |
| type="number" | |
| step="0.01" | |
| min="0" | |
| max="100" | |
| value={field.state.value} | |
| onBlur={field.handleBlur} | |
| onChange={(e) => | |
| field.handleChange(parseFloat(e.target.value)) | |
| } | |
| placeholder="0.00" | |
| aria-invalid={isInvalid} | |
| /> | |
| {isInvalid && ( | |
| <FieldError | |
| errors={getFieldErrors(field.state.meta.errors)} | |
| /> | |
| )} | |
| </Field> | |
| ); | |
| }} | |
| </form.Field> | |
| {/* Country Field */} | |
| <form.Field name="country"> | |
| {(field) => { | |
| const isInvalid = | |
| field.state.meta.isTouched && !field.state.meta.isValid; | |
| return ( | |
| <Field data-invalid={isInvalid}> | |
| <FieldLabel htmlFor={field.name}>Country*</FieldLabel> | |
| <Input | |
| id={field.name} | |
| name={field.name} | |
| value={field.state.value} | |
| onBlur={field.handleBlur} | |
| onChange={(e) => field.handleChange(e.target.value)} | |
| placeholder="e.g. US, UK, CA" | |
| aria-invalid={isInvalid} | |
| /> | |
| {isInvalid && ( | |
| <FieldError | |
| errors={getFieldErrors(field.state.meta.errors)} | |
| /> | |
| )} | |
| </Field> | |
| ); | |
| }} | |
| </form.Field> | |
| {/* State Field */} | |
| <form.Field name="state"> | |
| {(field) => { | |
| return ( | |
| <Field> | |
| <FieldLabel htmlFor={field.name}> | |
| State (Optional) | |
| </FieldLabel> | |
| <Input | |
| id={field.name} | |
| name={field.name} | |
| value={field.state.value} | |
| onBlur={field.handleBlur} | |
| onChange={(e) => field.handleChange(e.target.value)} | |
| placeholder="e.g. NY, CA (optional)" | |
| /> | |
| </Field> | |
| ); | |
| }} | |
| </form.Field> | |
| {/* ZIP Field */} | |
| <form.Field name="zip"> | |
| {(field) => { | |
| return ( | |
| <Field> | |
| <FieldLabel htmlFor={field.name}> | |
| ZIP Code (Optional) | |
| </FieldLabel> | |
| <Input | |
| id={field.name} | |
| name={field.name} | |
| value={field.state.value} | |
| onBlur={field.handleBlur} | |
| onChange={(e) => field.handleChange(e.target.value)} | |
| placeholder="e.g. 10001 (optional)" | |
| /> | |
| </Field> | |
| ); | |
| }} | |
| </form.Field> | |
| {/* Priority Field */} | |
| <form.Field name="priority"> | |
| {(field) => { | |
| const isInvalid = | |
| field.state.meta.isTouched && !field.state.meta.isValid; | |
| return ( | |
| <Field data-invalid={isInvalid}> | |
| <FieldLabel htmlFor={field.name}>Priority*</FieldLabel> | |
| <Input | |
| id={field.name} | |
| name={field.name} | |
| type="number" | |
| min="1" | |
| value={field.state.value} | |
| onBlur={field.handleBlur} | |
| onChange={(e) => | |
| field.handleChange(parseInt(e.target.value, 10)) | |
| } | |
| placeholder="1" | |
| aria-invalid={isInvalid} | |
| /> | |
| {isInvalid && ( | |
| <FieldError | |
| errors={getFieldErrors(field.state.meta.errors)} | |
| /> | |
| )} | |
| </Field> | |
| ); | |
| }} | |
| </form.Field> | |
| </div> | |
| </FieldGroup> | |
| <DialogFooter> | |
| <Button | |
| type="button" | |
| variant="outline" | |
| onClick={() => onOpenChange(false)} | |
| > | |
| Cancel | |
| </Button> | |
| <form.Subscribe | |
| selector={(state) => [state.canSubmit, state.isSubmitting]} | |
| > | |
| {([canSubmit, isSubmitting]) => ( | |
| <Button type="submit" disabled={!canSubmit || isSubmitting}> | |
| {isSubmitting ? 'Adding...' : 'Add Tax Rate'} | |
| </Button> | |
| )} | |
| </form.Subscribe> | |
| </DialogFooter> | |
| </form> | |
| </DialogContent> | |
| </Dialog> | |
| <EntityFormDialog<TaxRateFormValues> | |
| open={open} | |
| onOpenChange={onOpenChange} | |
| onSubmit={onSubmit} | |
| isSubmitting={isSubmitting} | |
| initialValues={initialValues} | |
| title="Tax Rate" | |
| description="Create or update a tax rate for your shop." | |
| validationSchema={createTaxRateSchema} | |
| submitButtonText={{ | |
| create: "Create Tax Rate", | |
| update: "Update Tax Rate", | |
| }} | |
| fields={fields} | |
| /> | |
| interface AddTaxDialogProps { | |
| open: boolean; | |
| onOpenChange: (open: boolean) => void; | |
| onSubmit: (data: TaxRateFormValues) => void; | |
| isSubmitting?: boolean; | |
| initialValues?: Partial<TaxRateFormValues> | null; | |
| shopId: string; | |
| } | |
| export function AddTaxDialog({ | |
| open, | |
| onOpenChange, | |
| onSubmit, | |
| isSubmitting = false, | |
| initialValues, | |
| shopId, | |
| }: AddTaxDialogProps) { | |
| const fields: EntityFormField[] = [ | |
| { | |
| name: "name", | |
| label: "Name", | |
| required: true, | |
| placeholder: "Enter tax rate name", | |
| description: "Name for your tax rate (e.g., VAT, Sales Tax)", | |
| }, | |
| { | |
| name: "rate", | |
| label: "Rate (%)", | |
| required: true, | |
| placeholder: "0.00", | |
| description: "Tax rate percentage (between 0.01 and 100)", | |
| }, | |
| { | |
| name: "country", | |
| label: "Country", | |
| required: true, | |
| placeholder: "US, UK, CA", | |
| description: "ISO country code (2 characters)", | |
| }, | |
| { | |
| name: "state", | |
| label: "State", | |
| placeholder: "NY, CA, TX", | |
| description: "State/Province code (optional)", | |
| }, | |
| { | |
| name: "zip", | |
| label: "ZIP Code", | |
| placeholder: "12345", | |
| description: "ZIP/Postal code pattern (optional)", | |
| }, | |
| { | |
| name: "priority", | |
| label: "Priority", | |
| required: true, | |
| placeholder: "1", | |
| description: | |
| "Priority for tax calculation (lower number = higher priority)", | |
| }, | |
| ]; | |
| return ( | |
| <EntityFormDialog<TaxRateFormValues> | |
| open={open} | |
| onOpenChange={onOpenChange} | |
| onSubmit={onSubmit} | |
| isSubmitting={isSubmitting} | |
| initialValues={{ ...(initialValues ?? {}), shopId }} | |
| title="Tax Rate" | |
| description="Create or update a tax rate for your shop." | |
| validationSchema={createTaxRateSchema} | |
| submitButtonText={{ | |
| create: "Create Tax Rate", | |
| update: "Update Tax Rate", | |
| }} | |
| fields={fields} | |
| /> | |
| ); | |
| } |
🤖 Prompt for AI Agents
In `@src/components/containers/shared/taxes/add-tax-dialog.tsx` around lines 8 -
82, The dialog doesn’t provide shopId required by createTaxRateSchema, so update
AddTaxDialog to always supply shopId to EntityFormDialog: ensure initialValues
passed into EntityFormDialog includes a shopId (fallback to a required prop or
context value) or intercept onSubmit to merge { shopId } into the submitted
TaxRateFormValues before validation; reference AddTaxDialog, initialValues,
EntityFormDialog, createTaxRateSchema and onSubmit when making the change so
every create/update operation includes shopId.
| export function Enable2FADialog({ | ||
| open, | ||
| onOpenChange, | ||
| onSuccess, | ||
| }: Enable2FADialogProps) { | ||
| const [password, setPassword] = useState(""); | ||
| const [loading, setLoading] = useState(false); | ||
|
|
||
| const handleEnable = async () => { | ||
| if (!password) { | ||
| toast.error("Please enter your password"); | ||
| return; | ||
| } | ||
|
|
||
| setLoading(true); | ||
| try { | ||
| const { data, error } = await twoFactor.enable({ | ||
| password, | ||
| }); | ||
|
|
||
| if (error) { | ||
| toast.error(error.message || "Failed to enable 2FA"); | ||
| return; | ||
| } | ||
|
|
||
| if (data?.backupCodes) { | ||
| toast.success("Two-factor authentication enabled!"); | ||
| onSuccess?.(data.backupCodes); | ||
| onOpenChange(false); | ||
| setPassword(""); | ||
| } | ||
| } catch (err) { | ||
| toast.error(err instanceof Error ? err.message : "Failed to enable 2FA"); | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <Dialog open={open} onOpenChange={onOpenChange}> | ||
| <DialogContent className="sm:max-w-md"> | ||
| <DialogHeader> | ||
| <DialogTitle>Enable Two-Factor Authentication</DialogTitle> | ||
| <DialogDescription> | ||
| Enter your password to confirm and enable 2FA on your account. | ||
| You'll receive backup codes after enabling. | ||
| </DialogDescription> | ||
| </DialogHeader> | ||
|
|
||
| <div className="space-y-4 py-4"> | ||
| <div className="space-y-2"> | ||
| <Label htmlFor="password">Current Password</Label> | ||
| <Input | ||
| id="password" | ||
| type="password" | ||
| placeholder="Enter your password" | ||
| value={password} | ||
| onChange={(e) => setPassword(e.target.value)} | ||
| onKeyDown={(e) => { | ||
| if (e.key === "Enter") { | ||
| handleEnable(); | ||
| } | ||
| }} | ||
| /> | ||
| </div> | ||
| </div> | ||
|
|
||
| <DialogFooter> | ||
| <Button | ||
| variant="outline" | ||
| onClick={() => { | ||
| onOpenChange(false); | ||
| setPassword(""); | ||
| }} | ||
| > | ||
| Cancel | ||
| </Button> | ||
| <Button onClick={handleEnable} disabled={loading || !password}> | ||
| {loading ? "Enabling…" : "Enable 2FA"} | ||
| </Button> | ||
| </DialogFooter> | ||
| </DialogContent> | ||
| </Dialog> | ||
| ); |
There was a problem hiding this comment.
Clear password state when the dialog is closed (privacy).
If users dismiss the dialog via overlay/escape, the password remains in state and can reappear on reopen.
🛡️ Proposed fix (reset on any close)
export function Enable2FADialog({
open,
onOpenChange,
onSuccess,
}: Enable2FADialogProps) {
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
+ const handleOpenChange = (nextOpen: boolean) => {
+ if (!nextOpen) setPassword("");
+ onOpenChange(nextOpen);
+ };
return (
- <Dialog open={open} onOpenChange={onOpenChange}>
+ <Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="sm:max-w-md">
...
</DialogContent>
</Dialog>
);
}
export function Disable2FADialog({
open,
onOpenChange,
onSuccess,
}: Disable2FADialogProps) {
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
+ const handleOpenChange = (nextOpen: boolean) => {
+ if (!nextOpen) setPassword("");
+ onOpenChange(nextOpen);
+ };
return (
- <Dialog open={open} onOpenChange={onOpenChange}>
+ <Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="sm:max-w-md">
...
</DialogContent>
</Dialog>
);
}Also applies to: 113-197
🤖 Prompt for AI Agents
In `@src/components/containers/store/accounts/enable-2fa-dialog.tsx` around lines
22 - 105, The dialog leaves the password in state when closed via
overlay/escape; update Enable2FADialog to clear password whenever the dialog
closes by either (a) wrapping the passed onOpenChange with a local handler that
calls onOpenChange(value) and when value is false calls setPassword("") before
returning, and/or (b) adding a useEffect that watches the open prop and calls
setPassword("") when open becomes false; apply the same fix to the other dialog
component referenced (the similar dialog at lines 113-197) so any close path
clears the password state for privacy.
| export interface ProductMutationState { | ||
| deletingId?: string | null; | ||
| togglingId?: string | null; | ||
| updatingId?: string | null; | ||
| creatingId?: string | null; | ||
| } | ||
|
|
||
| export interface ProductColumnConfig { | ||
| mode?: "vendor" | "customer"; | ||
| actions: ProductTableActions; | ||
| mutationState?: ProductMutationState; | ||
| isMutating?: (id: string) => boolean; | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find all occurrences of ProductMutationState with context
rg -n --type=ts --type=tsx -C3 '\bProductMutationState\b'Repository: FullStack-Flow/shop-stack
Length of output: 96
🏁 Script executed:
#!/bin/bash
# Find all occurrences of ProductMutationState
rg -n 'ProductMutationState' --type tsRepository: FullStack-Flow/shop-stack
Length of output: 1031
🏁 Script executed:
#!/bin/bash
# Read both type definitions to compare shapes
echo "=== Type from src/types/products.ts (lines around 286) ==="
sed -n '280,310p' src/types/products.ts
echo ""
echo "=== Type from product-table-columns.tsx (lines 22-26) ==="
sed -n '22,26p' src/components/containers/vendors/products/product-table-columns.tsxRepository: FullStack-Flow/shop-stack
Length of output: 1177
🏁 Script executed:
#!/bin/bash
# Check the full type definitions and their usages more clearly
echo "=== Full view of product-table-columns.tsx lines 22-34 ==="
sed -n '22,34p' src/components/containers/vendors/products/product-table-columns.tsx
echo ""
echo "=== Check shop-products-template.tsx import and usage ==="
sed -n '1,30p' src/components/templates/vendor/products/shop-products-template.tsx
echo ""
echo "=== Check product-table.tsx import and usage ==="
sed -n '1,30p' src/components/containers/vendors/products/product-table.tsxRepository: FullStack-Flow/shop-stack
Length of output: 2439
🏁 Script executed:
#!/bin/bash
# Verify the complete picture - check what use-products.tsx actually uses
echo "=== use-products.tsx mutation state usage ==="
sed -n '20,140p' src/hooks/vendors/use-products.tsx | grep -A 20 "const mutationState"
echo ""
echo "=== Check if types are actually incompatible by structure ==="
echo "Shared type fields: creatingId (required), deletingId (required), updatingId (required), isCreating (boolean), isAnyMutating (boolean)"
echo "Local type fields: all optional, includes togglingId instead"Repository: FullStack-Flow/shop-stack
Length of output: 1031
🏁 Script executed:
#!/bin/bash
# Final verification - check all the sites that use the local ProductMutationState
echo "=== All usages of local ProductMutationState from product-table-columns ==="
rg -n "ProductMutationState" src/components/templates/vendor/products/shop-products-template.tsx src/components/containers/vendors/products/product-table.tsx
echo ""
echo "=== Confirm the import chain ==="
echo "shop-products-template.tsx imports from product-table-columns:"
grep "ProductMutationState" src/components/templates/vendor/products/shop-products-template.tsx | head -1
echo ""
echo "product-table.tsx imports from product-table-columns:"
grep "ProductMutationState" src/components/containers/vendors/products/product-table.tsx | head -1Repository: FullStack-Flow/shop-stack
Length of output: 919
Avoid type name collision with existing ProductMutationState.
There's another ProductMutationState defined in src/types/products.ts with a fundamentally different shape (required fields including isCreating and isAnyMutating, no togglingId), creating confusion and risk of accidental mis-imports. Rename this local type to ProductTableMutationState and update its usages in product-table.tsx and shop-products-template.tsx.
♻️ Suggested changes
product-table-columns.tsx:
-export interface ProductMutationState {
+export interface ProductTableMutationState {
deletingId?: string | null;
togglingId?: string | null;
updatingId?: string | null;
creatingId?: string | null;
}
export interface ProductColumnConfig {
mode?: "vendor" | "customer";
actions: ProductTableActions;
- mutationState?: ProductMutationState;
+ mutationState?: ProductTableMutationState;
isMutating?: (id: string) => boolean;
}product-table.tsx (line 10):
- type ProductMutationState,
+ type ProductTableMutationState,product-table.tsx (line 22):
- mutationState?: ProductMutationState;
+ mutationState?: ProductTableMutationState;shop-products-template.tsx (line 7):
-import type { ProductMutationState } from "@/components/containers/vendors/products/product-table-columns";
+import type { ProductTableMutationState } from "@/components/containers/vendors/products/product-table-columns";shop-products-template.tsx (line 18):
- mutationState?: ProductMutationState;
+ mutationState?: ProductTableMutationState;🤖 Prompt for AI Agents
In `@src/components/containers/vendors/products/product-table-columns.tsx` around
lines 22 - 34, Rename the local type ProductMutationState to
ProductTableMutationState in the product-table-columns.tsx file and update all
usages and imports that reference it (e.g., in ProductColumnConfig, and where
product-table.tsx and shop-products-template.tsx import or refer to
ProductMutationState) so they now import/accept ProductTableMutationState;
ensure you update the type name in product-table.tsx (where the
columns/mutationState props are typed) and in shop-products-template.tsx (where
the component prop types or imports reference the old name) to avoid colliding
with the other ProductMutationState type.
| import CouponHeader from "@/components/containers/shared/coupons/coupon-header"; | ||
| import { AdminCouponTable } from "@/components/containers/shared/coupons/coupon-table"; | ||
| import { ADMIN_COUPON_PERMISSIONS } from "@/lib/config/coupon-permissions"; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find all relevant coupon-table files and their exports
echo "=== Looking for coupon-table.tsx files ==="
fd 'coupon-table\.tsx' src/components/containers
echo -e "\n=== Looking for admin-coupon-table.tsx ==="
fd 'admin-coupon-table\.tsx' src/components/containers
echo -e "\n=== Checking exports in shared/coupons/coupon-table.tsx ==="
if [ -f "src/components/containers/shared/coupons/coupon-table.tsx" ]; then
rg -n "export.*AdminCouponTable" src/components/containers/shared/coupons/coupon-table.tsx
fi
echo -e "\n=== Checking exports in admin/coupons/admin-coupon-table.tsx ==="
if [ -f "src/components/containers/admin/coupons/admin-coupon-table.tsx" ]; then
rg -n "export.*AdminCouponTable" src/components/containers/admin/coupons/admin-coupon-table.tsx
fi
echo -e "\n=== Checking all files in shared/coupons directory ==="
fd . src/components/containers/shared/coupons --type f
echo -e "\n=== Checking all files in admin/coupons directory ==="
fd . src/components/containers/admin/coupons --type fRepository: FullStack-Flow/shop-stack
Length of output: 924
🏁 Script executed:
#!/bin/bash
echo "=== Content of shared/coupons/coupon-table.tsx ==="
cat -n src/components/containers/shared/coupons/coupon-table.tsx | head -50
echo -e "\n=== Checking for any AdminCouponTable references in shared/coupons/coupon-table.tsx ==="
rg -i "admincoupontable" src/components/containers/shared/coupons/coupon-table.tsxRepository: FullStack-Flow/shop-stack
Length of output: 2049
Fix incorrect AdminCouponTable import path.
The import attempts to get AdminCouponTable from @/components/containers/shared/coupons/coupon-table, which exports only VendorCouponTable. The correct source is @/components/containers/admin/coupons/admin-coupon-table.
Fix
-import { AdminCouponTable } from "@/components/containers/shared/coupons/coupon-table";
+import { AdminCouponTable } from "@/components/containers/admin/coupons/admin-coupon-table";Also applies to: 33-37
🤖 Prompt for AI Agents
In `@src/components/templates/admin/admin-coupons-template.tsx` around lines 1 -
3, The AdminCouponTable import is pointing at the wrong module that only exports
VendorCouponTable; update the import of AdminCouponTable in this file to import
from the correct module
"@/components/containers/admin/coupons/admin-coupon-table" (and update any other
identical imports in this file around the other occurrences) so that the named
export AdminCouponTable is resolved from admin-coupon-table instead of
coupon-table.
Summary by CodeRabbit
Release Notes
New Features
Documentation
Chores
✏️ Tip: You can customize this high-level summary in your review settings.