Admin api - #14
Conversation
- Add store-specific category types, validators, and query hooks - Create server functions for fetching categories with proper filtering - Replace mock data with real API calls in category templates - Implement category tree building for hierarchical navigation - Connect product grid to actual product data with loading states - Add TanStack Query prefetching in route loaders for better UX
- Move VENDOR_STATUS_OPTIONS to shared constants file for reusability - Create admin-specific entity fetchers with ADMIN_STATUS_OPTIONS - Add admin category query schemas and types for server-side filtering - Refactor AdminCategoryTable to use server-side pagination and filtering - Implement useAdminCategories hook with React Query for state management - Add admin category server functions with proper authorization middleware - Replace mock data with real API calls in admin categories page - Add delete confirmation dialog and mutation state handling
- Add server fetcher for admin brands with filtering, sorting, and pagination - Replace client-side mock data with server-side data fetching - Integrate React Query mutations for brand toggle/delete operations - Add confirmation dialog for brand deletion - Update components to use server-side pagination pattern consistent with categories
…rations Add server functions and hooks for admin attribute management with full CRUD capabilities. Introduce server-side pagination, filtering, and sorting through createAdminAttributesFetcher. Replace client-side mock data with real database operations including toggleActive, update, and delete mutations with proper validation and error handling. Enhance AttributeTable to support both admin and shop contexts with improved loading states through AdminAttributeMutationState.
- Add server-side data fetching support to AdminCouponTable with new fetcher prop - Create use-admin-coupons hook with React Query mutations for toggle/delete - Implement createAdminCouponsFetcher for standardized data fetching - Add admin coupon server functions with filtering, sorting, and pagination - Update admin coupons page to use real mutations with loading states - Replace mock data with server-driven DataTable implementation
- Replace mock data with server-side fetching for admin orders list - Implement admin order details page with status update functionality - Add comprehensive order statistics dashboard with revenue metrics - Create reusable admin order hooks and server functions - Extend vendor order components to support admin mode - Add route for admin order details view
- Add admin product queries with enhanced filtering (stock, status, attributes) - Create admin product server functions for fetching, updating status, toggling featured, and deleting - Implement admin products hook with React Query for state management - Replace mock data with real data fetching in admin products page - Add product table component with edit/delete actions and loading states - Integrate confirmation dialog for product deletion
- Replace mock data with server functions and React Query hooks - Add delete review functionality with confirmation dialog - Refactor review table columns into separate module - Update review status terminology from "published" to "approved" - Add admin review stats and detailed view capabilities
- Replace mock data with server-side fetching using React Query hooks - Add admin-specific API endpoints and query hooks for tags, taxes, and transactions - Implement proper pagination, filtering, and sorting for admin tables - Add transaction statistics dashboard with revenue metrics - Update table components to support admin context and dynamic data - Extend type definitions to support admin-specific data structures
…cs components - Add admin dashboard template with stats cards, charts, and data tables - Create reusable dashboard components (stats cards, revenue chart, order status chart, etc.) - Implement data fetching hooks using React Query for real-time analytics - Add server functions to fetch dashboard statistics from database - Include utility functions for date calculations and dashboard helpers - Define TypeScript interfaces for dashboard data types - Replace placeholder dashboard with full-featured analytics interface
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ 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: 18
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/lib/validators/shared/product-query.ts (1)
151-159:⚠️ Potential issue | 🟡 MinorLine 156: admin filters now allow negative min/max price values.
productBaseFilterFieldsdoesn’t enforce.min(0), so admin queries can accept negative prices. If that’s unintended, reapply the constraint at the admin schema level.🛡️ Suggested fix
export const adminProductsQuerySchema = z.object({ ...paginationFields, limit: paginationFields.limit.default(ADMIN_DEFAULT_LIMIT), ...sortFields, ...searchFields, ...productBaseFilterFields, ...stockFilterFields, ...statusFilterFields, ...attributeFilterFields, + minPrice: z.coerce.number().min(0).optional(), + maxPrice: z.coerce.number().min(0).optional(), ...optionalShopIdField, ...optionalVendorIdField, });
🤖 Fix all issues with AI agents
In `@src/components/containers/shared/reviews/review-table-columns.tsx`:
- Around line 103-108: customerName may be empty which can yield undefined
initials; update the initials generation used inside AvatarFallback (the
expression using customerName.split(" ").map(...).join("").toUpperCase()) to
guard against empty strings by first checking customerName is truthy and
filtering out empty segments (e.g., .split(" ").filter(Boolean)) before mapping
to the first character, and provide a sensible fallback (empty string or
placeholder like "?") when customerName is falsy so AvatarFallback never
receives undefined characters.
In `@src/components/templates/vendor/shop-order-details-template.tsx`:
- Around line 48-55: The component currently computes a default back link using
shopSlug which can be undefined, causing invalid paths like /shop//orders;
update the default backLink logic in the ShopOrderDetailsTemplate (where
shopSlug, mode, and backLink props are used) to be mode-aware: if mode ===
"vendor" require shopSlug (or throw/assert/return a fallback) and build
/shop/{shopSlug}/orders, otherwise when mode === "admin" use the admin orders
path (e.g., /admin/orders) as the default; apply the same mode-aware fix to the
other places referencing shopSlug/backLink (the other default link computations
at the lines you noted) and if you choose to require backLink when shopSlug is
absent, add a clear runtime check that surfaces a helpful error mentioning
shopSlug/backLink and mode.
In `@src/hooks/admin/use-admin-dashboard.tsx`:
- Around line 211-220: The combined isLoading/isError aggregation in
use-admin-dashboard.tsx currently only ORs stats, revenueChart,
orderDistribution, and topShops; update the aggregation to include the remaining
query results (topProducts, lowStock, recentOrders, pendingReviews,
platformHealth) so the combined flags reflect all nine queries — locate the
aggregation where isLoading and isError are computed and add OR checks for
topProducts.isLoading/topProducts.isError, lowStock.isLoading/lowStock.isError,
recentOrders.isLoading/recentOrders.isError,
pendingReviews.isLoading/pendingReviews.isError, and
platformHealth.isLoading/platformHealth.isError (preserving existing variables
names like stats, revenueChart, orderDistribution, topShops).
In `@src/hooks/admin/use-admin-entity-fetchers.ts`:
- Around line 148-154: Remove the unnecessary double cast in the fetcher: inside
fetchFn that calls getAdminOrders, drop the "as unknown as
VendorOrderResponse[]" on the returned data and return the mapped array directly
(data: response.orders ?? []) so TypeScript can infer the correct type;
alternatively ensure getAdminOrders has an explicit return type matching
VendorOrderResponse[] so the fetcher doesn't need any assertion. Keep
references: fetchFn, getAdminOrders, VendorOrderResponse.
In `@src/hooks/admin/use-users.tsx`:
- Around line 86-88: The onSuccess handler currently calls toast.error when a
user is successfully banned; change this to a more appropriate variant (e.g.,
toast.success or toast.warning) so the UI reflects a successful operation while
conveying severity, e.g., replace toast.error("User has been banned") with
toast.success(...) or toast.warning(...), and keep the existing
invalidateUsers() call in the same onSuccess function to refresh state.
In `@src/lib/functions/admin/category.ts`:
- Around line 213-223: The code uses db.$count(categories) when counting
children; replace this with drizzle-orm's standard count() aggregation by
importing count from "drizzle-orm" and changing the select to use .select({
count: count() }) so the childCount query (the block using db.select({ count:
db.$count(categories) }).from(categories).where(eq(categories.parentId, id)))
matches the rest of the codebase and continues to check (childCount?.count ?? 0)
> 0 before throwing the error.
In `@src/lib/functions/admin/dashboard.ts`:
- Around line 66-181: ordersGrowth is computed using orderCountResult[0]?.count
which is the all-time order count, not current-month orders; update the query
that produces orderCountResult to count orders where createdAt >= startOfMonth
(use .where(gte(orders.createdAt, startOfMonth))) so currentMonthOrders reflects
only this month, then keep ordersGrowth calculation as-is using
currentMonthOrders and lastMonthStats[0]?.orderCount for comparison.
In `@src/lib/functions/admin/order.ts`:
- Around line 261-292: The code sets order status/paymentStatus to "refunded"
whenever order.paymentStatus === "paid" even if no refundable payment was found;
modify the flow in the cancel logic (around the payment lookup and createRefund
call in this file) to track whether a refund actually occurred (e.g., a local
boolean like refundSucceeded when createRefund(payment.stripePaymentIntentId)
completes), only update orders.status and orders.paymentStatus to "refunded"
when refundSucceeded is true, and otherwise set status to "cancelled" and leave
paymentStatus unchanged; update the db.update(orders)... call to use that flag
and ensure payments row is only set to "refunded" when the refundSucceeded path
executed.
In `@src/lib/functions/admin/transaction.ts`:
- Around line 98-104: The mapping that builds transactions uses
parseFloat(row.payment.amount) which can produce NaN for null/invalid inputs;
update the transactions mapping (the block that defines amount, applicationFee,
vendorAmount) to validate inputs before parsing: check row.payment.amount and
row.payment.applicationFeeAmount for null/undefined and for numeric strings
(e.g. use Number(value) and isFinite or a regex) and if invalid set amount
and/or applicationFee to null (or a safe fallback like 0 based on business
rules), then compute vendorAmount only when both amount and applicationFee are
valid numbers; ensure the variables amount, applicationFee, and vendorAmount
reflect these validated values.
- Around line 194-201: The pendingPayments query currently sums all pending
payments without the 30-day filter; update the query that builds the pending
payments aggregate (the db.select for total using
COALESCE(SUM(${payments.amount}), 0)) to add a where clause combining
eq(payments.status, "pending") and gte(payments.createdAt, thirtyDaysAgo) (use
and(...) with those two predicates) so it matches the same thirtyDaysAgo time
filter used for revenue, fees, and transaction counts.
In `@src/lib/functions/users.ts`:
- Around line 97-118: The getUsers handler currently hardcodes pagination
(limit:100) and computes total/offset incorrectly; update the createServerFn
handler to accept pagination params (e.g., limit and offset or page) from the
incoming request/context, defaulting limit to 100 and offset to 0, pass those
values into auth.api.listUsers({ query: { limit, offset }, headers:
context.headers }), and use the API's returned total (e.g., result.total)
instead of rawUsers.length; then return users: rawUsers.map(transformUser),
total: result.total, limit, and offset so ListUsersResponse reflects real
paging.
In `@src/lib/helper/tax-rate-query-helpers.ts`:
- Line 134: The field assignment 'isActive: taxRate.isActive || true' forces
true for false values; update the assignment for the taxRate object's isActive
property to only default to true when isActive is null/undefined (not when it is
false) — e.g., replace the `||` fallback with a nullish-coalescing or an
explicit undefined check so that taxRate.isActive stays false when set to false.
- Line 135: The mapping uses the expression "isCompound: taxRate.isCompound ||
false" which incorrectly converts a stored false into false via the || operator;
change it to use nullish coalescing so that only null or undefined fall back
(e.g., use taxRate.isCompound ?? false) — locate the mapping where the taxRate
object is transformed (look for the isCompound property assignment in the
tax-rate mapping function) and replace the || fallback with ?? to preserve
explicit false values.
In `@src/routes/`(admin)/admin/coupons/index.tsx:
- Line 16: The fetcher function is being recreated each render because
createAdminCouponsFetcher() is invoked directly; memoize the fetcher so its
identity is stable (e.g., wrap the call in React's useMemo or useCallback with
the appropriate dependency array) and pass that memoized value to the DataTable
fetcher prop to avoid re-initialization; locate the call to
createAdminCouponsFetcher() in the component and replace it with a memoized
version (reference: createAdminCouponsFetcher, DataTable fetcher prop).
In `@src/routes/`(admin)/admin/reviews/index.tsx:
- Around line 50-61: The handler handleConfirmDelete currently calls
deleteReview.mutate(...) and immediately calls setDeletingReview(null), which
closes ConfirmDeleteDialog before the mutation finishes and prevents the
dialog's isDeleting spinner from showing; update handleConfirmDelete to remove
the immediate setDeletingReview(null) and instead call setDeletingReview(null)
in the mutation callbacks (onSuccess and/or onSettled) provided to
deleteReview.mutate (or configure these callbacks on the deleteReview mutation
itself) so the dialog remains open while deleteReview.isLoading/isDeleting is
true and only closes after the mutation completes.
In `@src/routes/`(admin)/admin/users/index.tsx:
- Around line 31-33: handleAddUser currently awaits createUser(data) but never
closes the AddUserDialog, so the dialog stays open after successful creation;
update handleAddUser (and the similar handler around lines 67-72) to close the
dialog when createUser resolves (and optionally handle errors by keeping it open
and surfacing the error). Specifically, after createUser returns successfully
call the dialog close function (e.g., setIsAddUserOpen(false) or the Close
method provided to AddUserDialog) and ensure you only close on success, not on
caught errors.
In `@src/types/review.ts`:
- Around line 19-35: The BaseReviewFields interface currently types the status
field as a plain string—change its type to the existing ReviewStatus union to
tighten types: update the status property in BaseReviewFields to ReviewStatus,
import or reference the ReviewStatus type where BaseReviewFields is declared,
and adjust any downstream code or tests that assumed a string (e.g., comparisons
or assignments) to use the ReviewStatus values to ensure consistency with the
rest of the codebase.
In `@src/types/users.ts`:
- Around line 9-10: The User type currently has redundant fields banned and
status; remove one to avoid inconsistency. Choose which to keep (prefer keeping
status if you expect more states): delete the banned boolean from the
interface/type and replace any direct reads of user.banned with user.status ===
"banned" (or, if you instead keep banned, remove status and map reads of
user.status to boolean logic). Update any places that construct users to only
set the retained field, adjust serialization/DB mapping and tests accordingly,
and ensure helper functions (e.g., any isBanned checks) derive the value from
the remaining field.
🧹 Nitpick comments (43)
src/lib/functions/admin/tag.ts (2)
53-53: Redundant type assertion.The
inputValidator(adminTagsQuerySchema)already provides type inference fordata. Theas AdminTagsQuerycast is unnecessary and could mask type mismatches if the schema changes.♻️ Suggested fix
} = data as AdminTagsQuery; + } = data;
78-78: Prefer fallback defaults over non-null assertions.While the schema provides defaults for
limitandoffset, the non-null assertions (!) obscure this contract. Using inline defaults makes the code more defensive and self-documenting.♻️ Suggested fix
- return emptyPaginatedResponse<TagItem>(limit!, offset!); + return emptyPaginatedResponse<TagItem>(limit ?? 10, offset ?? 0);src/lib/functions/admin/product.ts (1)
172-177: Consider using.returning()to avoid a separate re-fetch query.The update followed by a separate select can be consolidated using Drizzle's
.returning()clause for better efficiency.♻️ Proposed refactor
- await db.update(products).set({ status }).where(eq(products.id, id)); - - const [updatedProduct] = await db - .select() - .from(products) - .where(eq(products.id, id)); + const [updatedProduct] = await db + .update(products) + .set({ status }) + .where(eq(products.id, id)) + .returning();src/lib/constants/index.ts (1)
1-4: Optional: narrow the option types with a const assertion.
Helps keep value types literal across the app.♻️ Suggested tweak
export const VENDOR_STATUS_OPTIONS = [ { label: "Active", value: "true" }, { label: "Inactive", value: "false" }, -]; +] as const;src/components/containers/shared/coupons/coupon-table.tsx (1)
34-43: Consider type constraints to enforce at least one data source.Both
couponsandfetcherare optional, which means neither could be provided. While the fallback to an empty array is safe, a discriminated union or runtime check could make the API clearer.♻️ Optional: Use a discriminated union for clearer API
// Option 1: Type-level enforcement type AdminCouponTableProps = CouponTableActions & { className?: string; mutationState?: CouponMutationState; isCouponMutating?: (id: string) => boolean; permissions?: CouponPermissions; } & ( | { coupons: CouponItem[]; fetcher?: never } | { fetcher: (params: DataTableFetchParams) => Promise<DataTableFetchResult<CouponItem>>; coupons?: never } );src/lib/utils/dashboard.ts (1)
83-90: Potential edge case:getStartOfWeekmay shift months unexpectedly.When the current date is early in the month (e.g., March 2nd, which is a Monday), subtracting days to reach Sunday may produce a date in the previous month. The function works correctly, but consumers should be aware this can cross month boundaries.
Additionally, the week start is Sunday (US convention). If the codebase needs Monday-start weeks (ISO 8601), this would need adjustment.
src/components/containers/admin/dashboard/top-shops-list.tsx (1)
21-28: Consider reusingformatCurrencyfrom dashboard utilities.This helper duplicates
formatCurrencyfromsrc/lib/utils/dashboard.ts. Importing the shared utility would reduce duplication.♻️ Import from shared utilities
import { Link } from "@tanstack/react-router"; import { ExternalLink, Store } from "lucide-react"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { formatCurrency } from "@/lib/utils/dashboard"; // ... other imports export function TopShopsList({ shops, isLoading = false }: TopShopsListProps) { - const formatCurrency = (value: number) => { - return new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - minimumFractionDigits: 0, - maximumFractionDigits: 0, - }).format(value); - }; - const maxRevenue = Math.max(...shops.map((s) => s.revenue), 1);src/components/containers/admin/dashboard/revenue-chart.tsx (2)
48-55: Consider reusingformatCurrencyfrom dashboard utilities.This helper is duplicated in multiple dashboard components. Import from
src/lib/utils/dashboard.tsfor consistency.♻️ Import shared utility
+import { formatCurrency } from "@/lib/utils/dashboard"; // ... other imports export function RevenueChart({ data, isLoading = false }: RevenueChartProps) { // ... loading state ... - const formatCurrency = (value: number) => { - return new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - minimumFractionDigits: 0, - maximumFractionDigits: 0, - }).format(value); - }; - const formatDate = (dateStr: string) => {
83-96: Gradient ID may conflict with multiple chart instances.The hardcoded
id="colorRevenue"could cause issues if multipleRevenueChartcomponents are rendered on the same page. Consider using a unique ID (e.g., viauseIdhook).♻️ Use React's useId for unique gradient ID
+import { useId } from "react"; export function RevenueChart({ data, isLoading = false }: RevenueChartProps) { + const gradientId = useId(); // ... <defs> - <linearGradient id="colorRevenue" x1="0" y1="0" x2="0" y2="1"> + <linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1"> // ... </linearGradient> </defs> // ... <Area // ... - fill="url(`#colorRevenue`)" + fill={`url(#${gradientId})`} />src/components/containers/admin/dashboard/top-products-list.tsx (1)
23-30: Use shared currency formatter to avoid duplication.There’s already a dashboard
formatCurrencyhelper; reusing it keeps formatting consistent and avoids recreatingIntl.NumberFormateach render.♻️ Proposed refactor
-import type { TopProduct } from "@/types/admin-dashboard"; +import type { TopProduct } from "@/types/admin-dashboard"; +import { formatCurrency } from "@/lib/utils/dashboard"; ... - const formatCurrency = (value: number) => { - return new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - minimumFractionDigits: 0, - maximumFractionDigits: 0, - }).format(value); - };src/components/containers/shared/orders/order-table-columns.tsx (1)
310-523: Consider extracting shared column builders to avoid drift.
createAdminOrderColumnslargely mirrorscreateOrderColumns(status, payment, date, actions). Extracting shared column builders or badge configs would reduce duplication and keep admin/vendor columns in sync.src/routes/(admin)/admin/products/index.tsx (1)
18-20: Avoid recreating the fetcher on every render.Creating a new fetcher function each render can cause unnecessary table resets. Consider lifting it to module scope.
♻️ Proposed refactor
+const adminProductsFetcher = createAdminProductsFetcher(); + function AdminProductsPage() { - const fetcher = createAdminProductsFetcher(); + const fetcher = adminProductsFetcher;src/types/taxes.ts (1)
82-90: Consider usingPaginatedResponse<NormalizedTaxRate>directly.
TaxRateQueryResultduplicates the exact structure ofPaginatedResponse<NormalizedTaxRate>. Since you already importPaginatedResponseand defineTaxRateListResponseas an alias, consider makingTaxRateQueryResultan alias as well to reduce duplication.♻️ Suggested simplification
-/** - * Tax Rate Query Result - */ -export interface TaxRateQueryResult { - data: NormalizedTaxRate[]; - total: number; - limit: number; - offset: number; -} +/** + * Tax Rate Query Result + */ +export type TaxRateQueryResult = PaginatedResponse<NormalizedTaxRate>;src/lib/functions/users.ts (1)
22-32: Avoidanytype for better type safety.The
transformUserhelper usesanyfor the input parameter, which bypasses TypeScript's type checking. Consider defining a proper interface for the raw user data from Better Auth.♻️ Suggested improvement
+interface RawBetterAuthUser { + id: string; + name?: string | null; + email: string; + image?: string | null; + role?: string | null; + banned?: boolean; + createdAt: Date | string; + updatedAt: Date | string; +} + -const transformUser = (u: any): AdminUser => ({ +const transformUser = (u: RawBetterAuthUser): AdminUser => ({ id: u.id, name: u.name ?? u.email, email: u.email, image: u.image ?? undefined, role: (u.role ?? "customer") as UserRole, banned: u.banned ?? false, status: u.banned ? "banned" : "active", createdAt: u.createdAt, updatedAt: u.updatedAt, });src/hooks/admin/use-admin-products.tsx (1)
150-164: Type assertions on mutation variables could be safer.The type assertions like
(deleteProductMutation.variables as string)and(updateStatusMutation.variables?.id as string)could be undefined if the mutation hasn't been called yet. Consider using optional chaining with nullish coalescing to ensure type safety.♻️ Safer type handling
const mutationState: AdminProductMutationState = { deletingId: deleteProductMutation.isPending - ? (deleteProductMutation.variables as string) + ? deleteProductMutation.variables ?? null : null, updatingId: updateStatusMutation.isPending - ? (updateStatusMutation.variables?.id as string) + ? updateStatusMutation.variables?.id ?? null : null, togglingId: toggleFeaturedMutation.isPending - ? (toggleFeaturedMutation.variables?.id as string) + ? toggleFeaturedMutation.variables?.id ?? null : null, isAnyMutating: deleteProductMutation.isPending || updateStatusMutation.isPending || toggleFeaturedMutation.isPending, };src/components/containers/shared/users/add-user-dialog.tsx (1)
45-51: Consider adding password validation.The password field has no validation rules. Consider adding minimum length and complexity requirements for security.
🛡️ Example with TanStack Form validation
const form = useForm({ defaultValues: { name: "", email: "", password: "", role: "customer" as UserRole, }, + validators: { + onChange: ({ value }) => { + const errors: Record<string, string> = {}; + if (value.password && value.password.length < 8) { + errors.password = "Password must be at least 8 characters"; + } + return Object.keys(errors).length > 0 ? errors : undefined; + }, + }, onSubmit: async ({ value }) => {src/lib/functions/admin/coupon.ts (1)
179-183: Empty conditional block is a code smell.The
if (existingCoupon.usageCount > 0)block is empty with only a comment. Either implement the soft delete logic, add a warning response, or remove the check entirely to avoid dead code.♻️ Option 1: Add warning in response
// Check usage count if (existingCoupon.usageCount > 0) { - // Soft delete might be preferred, but for now we'll allow deletion - // You could add a warning here if needed + // Warn that this coupon has been used + await db.delete(coupons).where(eq(coupons.id, id)); + return createSuccessResponse( + `Coupon deleted successfully. Note: This coupon had ${existingCoupon.usageCount} uses.` + ); } await db.delete(coupons).where(eq(coupons.id, id));♻️ Option 2: Remove the empty check
- // Check usage count - if (existingCoupon.usageCount > 0) { - // Soft delete might be preferred, but for now we'll allow deletion - // You could add a warning here if needed - } - await db.delete(coupons).where(eq(coupons.id, id));src/hooks/admin/use-admin-orders.tsx (2)
21-32: Minor: Query key structure differs from other admin hooks.This file uses a flat
["admin-orders"]base key whileuse-admin-tags.tsxuses a nested["admin", "tags"]structure. Consider aligning the pattern for consistency across admin hooks.export const adminOrderKeys = { - all: ["admin-orders"] as const, + all: ["admin", "orders"] as const, list: (params?: {
78-131: Missing error handlers in mutations.Unlike
useAdminTagMutationswhich provides toast notifications on errors, these mutation hooks lackonErrorcallbacks. Consider adding error handling for consistent user feedback.Proposed fix for useUpdateAdminOrderStatus
onSuccess: (_, variables) => { queryClient.invalidateQueries({ queryKey: adminOrderKeys.all }); queryClient.invalidateQueries({ queryKey: adminOrderKeys.detail(variables.orderId), }); }, + onError: (error: Error) => { + toast.error(error.message || "Failed to update order status"); + }, });src/components/containers/admin/dashboard/recent-orders-table.tsx (1)
54-59: Consider reusing existingformatCurrencyutility.A
formatCurrencyfunction already exists insrc/lib/utils/dashboard.ts. The implementations differ slightly (this one includes decimal places, the utility doesn't), but consider consolidating to avoid duplication. If decimal precision is needed here, the shared utility could be parameterized.+import { formatCurrency } from "@/lib/utils/dashboard"; + +// If decimals needed, create a variant or add a parameter to the shared utilitysrc/components/containers/shared/reviews/review-table-columns.tsx (1)
170-172: "View Details" action has no handler.The "View Details" menu item is rendered when
permissions?.canViewis true, but it lacks anonClickhandler. Consider either implementing the action or adding a TODO comment.src/types/transaction.ts (1)
44-54: Consider using a stricter type forstatus.
TransactionBaseResponse.statusis typed asstring, but based on the validator intransaction.tsserver file (line 29), it should be one of"pending" | "processing" | "succeeded" | "failed" | "refunded". Using a union type would provide better type safety.♻️ Proposed type refinement
+export type TransactionStatus = "pending" | "processing" | "succeeded" | "failed" | "refunded"; + export interface TransactionBaseResponse { id: string; paymentIntentId: string | null; orderId: string; orderNumber: string; currency: string; - status: string; + status: TransactionStatus; paymentMethod: string; provider: string; createdAt: string; }src/components/containers/admin/transactions/admin-transactions-table.tsx (1)
136-141: "View Details" action is a placeholder.Similar to the review table, the "View Details" menu item lacks an
onClickhandler. Consider implementing navigation to a transaction detail view or adding a TODO.src/hooks/admin/use-admin-entity-fetchers.ts (1)
36-46: Consider avoidinganyfor the query type parameter.Using
anyfor the second generic parameter increateServerFetcher<NormalizedCategory, any>loses type safety. If the query shape is consistent across fetchers, consider defining a shared query type.src/routes/(admin)/admin/brands/index.tsx (1)
16-16: Consider memoizing the fetcher to prevent unnecessary re-fetches.
createAdminBrandsFetcher()is called on every render, creating a new function reference each time. This may cause theDataTableto re-fetch data on every parent re-render since the fetcher reference changes.♻️ Proposed fix using useMemo
+import { useMemo } from "react"; ... function AdminBrandsPage() { - const fetcher = createAdminBrandsFetcher(); + const fetcher = useMemo(() => createAdminBrandsFetcher(), []);src/routes/(admin)/admin/users/index.tsx (1)
10-13: MissingpendingComponentfor route loading state.Other admin routes (brands, taxes, attributes) include
pendingComponent: PageSkeletonin their route configuration for consistent loading UX during route transitions. This route handles loading manually viaisLoadingcheck, but the route-level pending state is not covered.♻️ Proposed fix to add pendingComponent
export const Route = createFileRoute("/(admin)/admin/users/")({ component: AdminUsersPage, + pendingComponent: PageSkeleton, });src/components/templates/admin/admin-dashboard-template.tsx (2)
33-48: Consider handling error states for dashboard queries.The dashboard fetches data from 9 different hooks, but none handle error states. If a query fails, the corresponding section will remain in a loading state indefinitely. Consider either:
- Adding error boundaries around sections
- Passing
isErrorto child components for graceful degradation- Using a consolidated error indicator
85-91: Minor:changeLabeldisplays "+0 today" when no new users.When
stats?.newUsersTodayis0orundefined, the label will show "+0 today" which may look odd. Consider conditionally displaying this only when there are new users.♻️ Optional fix for cleaner display
<StatsCard title="Total Users" value={stats?.totalUsers || 0} - changeLabel={`+${stats?.newUsersToday || 0} today`} + changeLabel={stats?.newUsersToday ? `+${stats.newUsersToday} today` : undefined} icon={Users} isLoading={statsLoading} />src/routes/(admin)/admin/taxes/index.tsx (1)
16-16: Consider memoizing the fetcher to prevent unnecessary re-fetches.Same as the brands route,
createAdminTaxRatesFetcher()is called on every render. Consider memoizing for consistency and to prevent potential re-fetch issues.♻️ Proposed fix using useMemo
+import { useMemo } from "react"; ... function AdminTaxesPage() { - const fetcher = createAdminTaxRatesFetcher(); + const fetcher = useMemo(() => createAdminTaxRatesFetcher(), []);src/routes/(admin)/admin/attributes/index.tsx (2)
17-17: Consider memoizing the fetcher to prevent unnecessary re-fetches.Consistent with other admin routes, the fetcher should be memoized to maintain stable reference across renders.
♻️ Proposed fix using useMemo
+import { useMemo } from "react"; ... function AdminAttributesPage() { - const fetcher = createAdminAttributesFetcher(); + const fetcher = useMemo(() => createAdminAttributesFetcher(), []);
75-99: RedundanthandleDialogClosecall inonOpenChange.When the dialog closes (
openbecomesfalse), bothsetIsDialogOpen(open)andhandleDialogClose()are called. Looking atuseEntityCRUD,handleDialogCloselikely already callssetIsDialogOpen(false), resulting in redundant state updates. Additionally,handleDialogCloseis called inhandleAttributeSubmiton success, which may cause the cleanup to run twice.♻️ Proposed simplification
<AddAttributeDialog open={isDialogOpen} - onOpenChange={(open) => { - setIsDialogOpen(open); - if (!open) handleDialogClose(); - }} + onOpenChange={setIsDialogOpen} onSubmit={handleAttributeSubmit}If
handleDialogCloseneeds to cleareditingAttribute, ensure that's handled appropriately when the dialog closes without submitting.src/routes/(admin)/admin/categories/index.tsx (1)
16-16: Fetcher recreated on every render — memoize or hoist outside component.
createAdminCategoriesFetcher()is called directly in the component body, creating a new fetcher function instance on each render. This can cause the underlyingDataTableto treat it as a new fetcher, potentially triggering unnecessary re-fetches or disrupting caching behavior.♻️ Proposed fix: memoize the fetcher
+import { useMemo } from "react"; ... function AdminCategoriesPage() { - const fetcher = createAdminCategoriesFetcher(); + const fetcher = useMemo(() => createAdminCategoriesFetcher(), []);Alternatively, if the fetcher doesn't depend on component state, hoist it outside the component entirely.
src/hooks/admin/use-admin-attributes.tsx (1)
134-156: Comment claims optimistic updates, but none are implemented.The comment on line 134 mentions "optimistic updates," but the
onMutatehandler only cancels queries and returns context for potential rollback — no cache modification occurs. This is technically a "cancel + refetch" pattern rather than true optimistic updates. Consider either implementing actual optimistic cache updates or updating the comment to reflect the current behavior.src/hooks/admin/use-admin-taxes.tsx (1)
53-61: Consider addingstaleTimefor consistency with other admin hooks.Other admin hooks (e.g.,
use-admin-attributes.tsx) specify astaleTime(30s for lists, 60s for details). This hook omits it, defaulting to 0, which may cause more frequent refetches.♻️ Proposed fix: add staleTime
return queryOptions({ queryKey: adminTaxRatesKeys.list(mergedParams), queryFn: () => getAdminTaxRates({ data: mergedParams }), + staleTime: 30 * 1000, // 30 seconds });src/hooks/admin/use-admin-categories.tsx (2)
138-151:togglingIdconflates two different mutations — may cause incorrect loading indicators.The
togglingIdfield combinestoggleActiveMutation.variables?.idandtoggleFeaturedMutation.variables?.idusing||. If both mutations were somehow pending simultaneously (edge case), only one ID would be tracked. More critically, UI consumers cannot distinguish whether the toggle is forisActiveorfeaturedstatus.Consider splitting into separate tracking fields if the UI needs to differentiate these states.
♻️ Proposed fix: separate tracking for each toggle type
const mutationState: AdminCategoryMutationState = { deletingId: deleteCategoryMutation.isPending ? (deleteCategoryMutation.variables as string) : null, - togglingId: - toggleActiveMutation.isPending || toggleFeaturedMutation.isPending - ? ((toggleActiveMutation.variables?.id || - toggleFeaturedMutation.variables?.id) as string) - : null, + togglingActiveId: toggleActiveMutation.isPending + ? (toggleActiveMutation.variables?.id ?? null) + : null, + togglingFeaturedId: toggleFeaturedMutation.isPending + ? (toggleFeaturedMutation.variables?.id ?? null) + : null, isAnyMutating: deleteCategoryMutation.isPending || toggleActiveMutation.isPending || toggleFeaturedMutation.isPending, };This would require updating
AdminCategoryMutationStateand consumers accordingly.
54-72: Consider addingstaleTimefor consistency.Similar to the tax rates hook, this lacks explicit
staleTimeconfiguration while other admin hooks specify it.src/components/templates/admin/admin-transactions-template.tsx (1)
85-86: Consider formatting large transaction counts.
stats.totalTransactionsis rendered directly without number formatting. For large values (e.g., 1,000,000+), this could be harder to read. Consider usingIntl.NumberFormator a formatting utility for consistency with the currency values.♻️ Proposed fix
<div className="font-bold text-2xl"> - {stats.totalTransactions} + {stats.totalTransactions.toLocaleString()} </div>src/hooks/admin/use-admin-reviews.tsx (1)
140-143: Inconsistent mutation return pattern compared to other admin hooks.Other admin hooks (e.g.,
useAdminAttributeMutations,useAdminCategoryMutations,useAdminTaxRateMutations) returnmutateAsyncfunctions directly along withmutationStateandis*Mutatinghelpers:return { toggleActive: toggleActiveMutation.mutateAsync, mutationState, isAttributeMutating: (id) => ... };This hook returns the full mutation objects instead:
return { updateStatus: updateStatusMutation, deleteReview: deleteReviewMutation, };Consider aligning with the established pattern for consistency across admin hooks.
♻️ Proposed fix: align with other admin hooks
+interface AdminReviewMutationState { + updatingStatusId: string | null; + deletingId: string | null; + isAnyMutating: boolean; +} export function useAdminReviewMutations() { const queryClient = useQueryClient(); // ... existing mutation definitions ... + const mutationState: AdminReviewMutationState = { + updatingStatusId: updateStatusMutation.isPending + ? (updateStatusMutation.variables?.reviewId ?? null) + : null, + deletingId: deleteReviewMutation.isPending + ? (deleteReviewMutation.variables?.reviewId ?? null) + : null, + isAnyMutating: + updateStatusMutation.isPending || deleteReviewMutation.isPending, + }; return { - updateStatus: updateStatusMutation, - deleteReview: deleteReviewMutation, + updateStatus: updateStatusMutation.mutateAsync, + deleteReview: deleteReviewMutation.mutateAsync, + isUpdatingStatus: updateStatusMutation.isPending, + isDeleting: deleteReviewMutation.isPending, + mutationState, + isReviewMutating: (reviewId: string) => + mutationState.updatingStatusId === reviewId || + mutationState.deletingId === reviewId, }; }src/components/containers/shared/users/user-table.tsx (2)
157-165: Hardcoded role values could drift from theUserRoletype.The role options are hardcoded as string literals. If
UserRoleis updated, this menu won't reflect the changes and may cause runtime issues.♻️ Consider deriving options from UserRole type
If
UserRoleis a union type or enum, consider deriving the options programmatically:// If UserRole is defined as: type UserRole = "customer" | "vendor" | "admin" const ROLE_OPTIONS: { value: UserRole; label: string }[] = [ { value: "customer", label: "Customer" }, { value: "vendor", label: "Vendor" }, { value: "admin", label: "Admin" }, ];Then map over
ROLE_OPTIONSin the JSX. This keeps the source of truth in one place.
100-113: Status badge handles only "banned" and "active" states.The badge logic applies destructive styling for "banned" and green for "active", but any other status value would fall through to the default variant without specific styling. If additional statuses exist (e.g., "suspended", "pending"), they may need handling.
src/routes/(admin)/admin/reviews/index.tsx (1)
52-54: Consider replacingwindow.prompt()with a proper dialog input.
window.prompt()provides a poor user experience—it's blocking, cannot be styled, and looks inconsistent across browsers. A custom dialog component with an input field would provide better UX and allow proper validation feedback.src/lib/functions/admin/review.ts (1)
212-213: Replaceconsole.logwith proper audit logging.The comment acknowledges this should be stored in production. Using
console.logfor admin deletion audit trails has several issues:
- Logs are ephemeral and may be lost
- No structured format for querying/analysis
- PII (deletion reason) in logs without proper handling
Consider storing to an audit table or using a structured logging service.
♻️ Suggested approach
- // Log deletion reason (in production, you might want to store this) - console.log(`Admin deleted review ${reviewId}. Reason: ${reason}`); + // TODO: Store deletion audit to database or structured logging service + // Example: await db.insert(auditLogs).values({ + // action: 'REVIEW_DELETED', + // entityId: reviewId, + // reason, + // adminId: ctx.user.id, // if available from middleware context + // timestamp: new Date(), + // });src/hooks/admin/use-users.tsx (1)
56-61: Removeas anytype cast and align role types properly.The
roleparameter on lines 60 and 117 is cast toany, which bypasses type safety. SinceUserRoleis already properly typed as"admin" | "vendor" | "customer", consider:
- Checking if better-auth's admin client supports the
UserRoletype directly without casting- If not, create a type-safe mapping function instead of using
as any- Verify the admin client's role parameter type in better-auth's type definitions and update accordingly
| {customerName | ||
| .split(" ") | ||
| .map((n) => n[0]) | ||
| .join("") | ||
| .toUpperCase()} | ||
| </AvatarFallback> |
There was a problem hiding this comment.
Potential runtime error if customerName is empty.
If customerName is an empty string, n[0] will be undefined for empty segments, which will still work but may produce unexpected initials. Consider adding a guard.
🛡️ Proposed defensive fix
<AvatarFallback className="text-xs">
{customerName
.split(" ")
- .map((n) => n[0])
+ .filter(Boolean)
+ .map((n) => n[0] ?? "")
.join("")
.toUpperCase()}
</AvatarFallback>📝 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.
| {customerName | |
| .split(" ") | |
| .map((n) => n[0]) | |
| .join("") | |
| .toUpperCase()} | |
| </AvatarFallback> | |
| {customerName | |
| .split(" ") | |
| .filter(Boolean) | |
| .map((n) => n[0] ?? "") | |
| .join("") | |
| .toUpperCase()} | |
| </AvatarFallback> |
🤖 Prompt for AI Agents
In `@src/components/containers/shared/reviews/review-table-columns.tsx` around
lines 103 - 108, customerName may be empty which can yield undefined initials;
update the initials generation used inside AvatarFallback (the expression using
customerName.split(" ").map(...).join("").toUpperCase()) to guard against empty
strings by first checking customerName is truthy and filtering out empty
segments (e.g., .split(" ").filter(Boolean)) before mapping to the first
character, and provide a sensible fallback (empty string or placeholder like
"?") when customerName is falsy so AvatarFallback never receives undefined
characters.
| shopSlug?: string; | ||
| order: VendorOrderDetailResponse; | ||
| mode?: "vendor" | "admin"; | ||
| backLink?: { | ||
| to: string; | ||
| params?: Record<string, string>; | ||
| label?: string; | ||
| }; |
There was a problem hiding this comment.
Prevent invalid back links when shopSlug is absent.
With shopSlug optional, the default link can resolve to /shop//orders when admin mode doesn’t supply backLink. Consider making the default link mode-aware (or requiring backLink when shopSlug is missing).
🛠️ Proposed fix (mode-aware default)
- const resolvedBackLink = backLink ?? {
- to: "/shop/$slug/orders",
- params: { slug: shopSlug ?? "" },
- label: "Back to Orders",
- };
+ const resolvedBackLink =
+ backLink ??
+ (mode === "admin"
+ ? { to: "/admin/orders", label: "Back to Orders" }
+ : {
+ to: "/shop/$slug/orders",
+ params: { slug: shopSlug ?? "" },
+ label: "Back to Orders",
+ });Also applies to: 61-62, 135-139, 145-148
🤖 Prompt for AI Agents
In `@src/components/templates/vendor/shop-order-details-template.tsx` around lines
48 - 55, The component currently computes a default back link using shopSlug
which can be undefined, causing invalid paths like /shop//orders; update the
default backLink logic in the ShopOrderDetailsTemplate (where shopSlug, mode,
and backLink props are used) to be mode-aware: if mode === "vendor" require
shopSlug (or throw/assert/return a fallback) and build /shop/{shopSlug}/orders,
otherwise when mode === "admin" use the admin orders path (e.g., /admin/orders)
as the default; apply the same mode-aware fix to the other places referencing
shopSlug/backLink (the other default link computations at the lines you noted)
and if you choose to require backLink when shopSlug is absent, add a clear
runtime check that surfaces a helpful error mentioning shopSlug/backLink and
mode.
| isLoading: | ||
| stats.isLoading || | ||
| revenueChart.isLoading || | ||
| orderDistribution.isLoading || | ||
| topShops.isLoading, | ||
| isError: | ||
| stats.isError || | ||
| revenueChart.isError || | ||
| orderDistribution.isError || | ||
| topShops.isError, |
There was a problem hiding this comment.
isLoading and isError don't aggregate all queries.
The combined states only check 4 of 9 queries (stats, revenueChart, orderDistribution, topShops). If topProducts, lowStock, recentOrders, pendingReviews, or platformHealth are loading or errored, the combined isLoading/isError will incorrectly report false.
🛠️ Proposed fix to include all queries
isLoading:
stats.isLoading ||
revenueChart.isLoading ||
orderDistribution.isLoading ||
- topShops.isLoading,
+ topShops.isLoading ||
+ topProducts.isLoading ||
+ lowStock.isLoading ||
+ recentOrders.isLoading ||
+ pendingReviews.isLoading ||
+ platformHealth.isLoading,
isError:
stats.isError ||
revenueChart.isError ||
orderDistribution.isError ||
- topShops.isError,
+ topShops.isError ||
+ topProducts.isError ||
+ lowStock.isError ||
+ recentOrders.isError ||
+ pendingReviews.isError ||
+ platformHealth.isError,📝 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.
| isLoading: | |
| stats.isLoading || | |
| revenueChart.isLoading || | |
| orderDistribution.isLoading || | |
| topShops.isLoading, | |
| isError: | |
| stats.isError || | |
| revenueChart.isError || | |
| orderDistribution.isError || | |
| topShops.isError, | |
| isLoading: | |
| stats.isLoading || | |
| revenueChart.isLoading || | |
| orderDistribution.isLoading || | |
| topShops.isLoading || | |
| topProducts.isLoading || | |
| lowStock.isLoading || | |
| recentOrders.isLoading || | |
| pendingReviews.isLoading || | |
| platformHealth.isLoading, | |
| isError: | |
| stats.isError || | |
| revenueChart.isError || | |
| orderDistribution.isError || | |
| topShops.isError || | |
| topProducts.isError || | |
| lowStock.isError || | |
| recentOrders.isError || | |
| pendingReviews.isError || | |
| platformHealth.isError, |
🤖 Prompt for AI Agents
In `@src/hooks/admin/use-admin-dashboard.tsx` around lines 211 - 220, The combined
isLoading/isError aggregation in use-admin-dashboard.tsx currently only ORs
stats, revenueChart, orderDistribution, and topShops; update the aggregation to
include the remaining query results (topProducts, lowStock, recentOrders,
pendingReviews, platformHealth) so the combined flags reflect all nine queries —
locate the aggregation where isLoading and isError are computed and add OR
checks for topProducts.isLoading/topProducts.isError,
lowStock.isLoading/lowStock.isError,
recentOrders.isLoading/recentOrders.isError,
pendingReviews.isLoading/pendingReviews.isError, and
platformHealth.isLoading/platformHealth.isError (preserving existing variables
names like stats, revenueChart, orderDistribution, topShops).
| fetchFn: async (query) => { | ||
| const response = await getAdminOrders({ data: query }); | ||
| return { | ||
| data: (response.orders ?? []) as unknown as VendorOrderResponse[], | ||
| total: response.total ?? 0, | ||
| }; | ||
| }, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check the return type of getAdminOrders
ast-grep --pattern 'export const getAdminOrders = createServerFn($$$).handler(async ($$$): Promise<$_> {'Repository: FullStack-Flow/shop-stack
Length of output: 51
🏁 Script executed:
# Also search for the function definition more broadly
rg -A 5 'getAdminOrders\s*=' --type tsRepository: FullStack-Flow/shop-stack
Length of output: 484
🏁 Script executed:
# Find VendorOrderResponse type definition
rg 'type VendorOrderResponse|interface VendorOrderResponse' --type tsRepository: FullStack-Flow/shop-stack
Length of output: 127
🏁 Script executed:
# Check the response structure from getAdminOrders
rg 'getAdminOrders' src/hooks/admin/use-admin-entity-fetchers.ts -B 2 -A 8Repository: FullStack-Flow/shop-stack
Length of output: 1006
🏁 Script executed:
# Get the full implementation of getAdminOrders
cat src/lib/functions/admin/order.ts | head -80Repository: FullStack-Flow/shop-stack
Length of output: 2594
🏁 Script executed:
# Get VendorOrderResponse type definition
rg -A 15 'export interface VendorOrderResponse' src/types/orders.tsRepository: FullStack-Flow/shop-stack
Length of output: 445
🏁 Script executed:
# Also check what type the handler returns explicitly
ast-grep --pattern 'getAdminOrders = createServerFn($$$).handler(async ($$$) => {
$$$
return $_
})'Repository: FullStack-Flow/shop-stack
Length of output: 51
🏁 Script executed:
# Get more of the getAdminOrders return statement
cat src/lib/functions/admin/order.ts | tail -50Repository: FullStack-Flow/shop-stack
Length of output: 1290
🏁 Script executed:
# Get complete VendorOrderResponse definition
rg -A 25 'export interface VendorOrderResponse' src/types/orders.tsRepository: FullStack-Flow/shop-stack
Length of output: 604
🏁 Script executed:
# Get the complete getAdminOrders handler - search around line that was shown
rg -A 100 'export const getAdminOrders' src/lib/functions/admin/order.ts | head -150Repository: FullStack-Flow/shop-stack
Length of output: 3026
🏁 Script executed:
# Search specifically for the return statement in getAdminOrders
rg -A 50 'orders: adminOrders.map' src/lib/functions/admin/order.tsRepository: FullStack-Flow/shop-stack
Length of output: 1816
Remove the unnecessary double type assertion; the mapped object already matches VendorOrderResponse.
The getAdminOrders function returns a properly shaped object that exactly matches VendorOrderResponse. The double assertion as unknown as VendorOrderResponse[] is redundant and masks the actual type from TypeScript. Remove the assertion and let TypeScript infer the correct type from the mapping function, or add an explicit return type annotation to getAdminOrders to make the contract clear.
🤖 Prompt for AI Agents
In `@src/hooks/admin/use-admin-entity-fetchers.ts` around lines 148 - 154, Remove
the unnecessary double cast in the fetcher: inside fetchFn that calls
getAdminOrders, drop the "as unknown as VendorOrderResponse[]" on the returned
data and return the mapped array directly (data: response.orders ?? []) so
TypeScript can infer the correct type; alternatively ensure getAdminOrders has
an explicit return type matching VendorOrderResponse[] so the fetcher doesn't
need any assertion. Keep references: fetchFn, getAdminOrders,
VendorOrderResponse.
| onSuccess: () => { | ||
| toast.error("User has been banned"); | ||
| invalidateUsers(); |
There was a problem hiding this comment.
Use appropriate toast variant for successful ban operation.
toast.error is used for a successful ban, which may confuse users into thinking the operation failed. Consider using toast.success or toast.warning to indicate the action completed while conveying its severity.
🔧 Suggested fix
onSuccess: () => {
- toast.error("User has been banned");
+ toast.warning("User has been banned");
invalidateUsers();
},📝 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.
| onSuccess: () => { | |
| toast.error("User has been banned"); | |
| invalidateUsers(); | |
| onSuccess: () => { | |
| toast.warning("User has been banned"); | |
| invalidateUsers(); |
🤖 Prompt for AI Agents
In `@src/hooks/admin/use-users.tsx` around lines 86 - 88, The onSuccess handler
currently calls toast.error when a user is successfully banned; change this to a
more appropriate variant (e.g., toast.success or toast.warning) so the UI
reflects a successful operation while conveying severity, e.g., replace
toast.error("User has been banned") with toast.success(...) or
toast.warning(...), and keep the existing invalidateUsers() call in the same
onSuccess function to refresh state.
|
|
||
| function AdminCouponsPage() { | ||
| const [coupons, setCoupons] = useState<Coupon[]>(mockCoupons); | ||
| const fetcher = createAdminCouponsFetcher(); |
There was a problem hiding this comment.
Memoize the fetcher to prevent unnecessary re-renders.
createAdminCouponsFetcher() is called on every render, creating a new function reference. This causes the DataTable to re-initialize on each render since the fetcher prop identity changes.
🛠️ Proposed fix
+import { useMemo } from "react";
+
function AdminCouponsPage() {
- const fetcher = createAdminCouponsFetcher();
+ const fetcher = useMemo(() => createAdminCouponsFetcher(), []);📝 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 fetcher = createAdminCouponsFetcher(); | |
| import { useMemo } from "react"; | |
| function AdminCouponsPage() { | |
| const fetcher = useMemo(() => createAdminCouponsFetcher(), []); |
🤖 Prompt for AI Agents
In `@src/routes/`(admin)/admin/coupons/index.tsx at line 16, The fetcher function
is being recreated each render because createAdminCouponsFetcher() is invoked
directly; memoize the fetcher so its identity is stable (e.g., wrap the call in
React's useMemo or useCallback with the appropriate dependency array) and pass
that memoized value to the DataTable fetcher prop to avoid re-initialization;
locate the call to createAdminCouponsFetcher() in the component and replace it
with a memoized version (reference: createAdminCouponsFetcher, DataTable fetcher
prop).
| const handleConfirmDelete = () => { | ||
| if (!deletingReview) return; | ||
| const reason = window.prompt( | ||
| "Provide a reason for deletion (min 5 characters).", | ||
| ); | ||
| if (!reason || reason.trim().length < 5) { | ||
| toast.error("Deletion reason must be at least 5 characters."); | ||
| return; | ||
| } | ||
| deleteReview.mutate({ reviewId: deletingReview.id, reason: reason.trim() }); | ||
| setDeletingReview(null); | ||
| }; |
There was a problem hiding this comment.
Dialog closes before mutation completes, hiding the loading state.
setDeletingReview(null) on Line 60 closes the ConfirmDeleteDialog immediately after calling mutate(), before the deletion completes. This means users won't see the "Deleting..." spinner defined in the dialog's isDeleting prop.
🛠️ Proposed fix using mutation callbacks
const handleConfirmDelete = () => {
if (!deletingReview) return;
const reason = window.prompt(
"Provide a reason for deletion (min 5 characters).",
);
if (!reason || reason.trim().length < 5) {
toast.error("Deletion reason must be at least 5 characters.");
return;
}
- deleteReview.mutate({ reviewId: deletingReview.id, reason: reason.trim() });
- setDeletingReview(null);
+ deleteReview.mutate(
+ { reviewId: deletingReview.id, reason: reason.trim() },
+ {
+ onSettled: () => setDeletingReview(null),
+ }
+ );
};📝 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 handleConfirmDelete = () => { | |
| if (!deletingReview) return; | |
| const reason = window.prompt( | |
| "Provide a reason for deletion (min 5 characters).", | |
| ); | |
| if (!reason || reason.trim().length < 5) { | |
| toast.error("Deletion reason must be at least 5 characters."); | |
| return; | |
| } | |
| deleteReview.mutate({ reviewId: deletingReview.id, reason: reason.trim() }); | |
| setDeletingReview(null); | |
| }; | |
| const handleConfirmDelete = () => { | |
| if (!deletingReview) return; | |
| const reason = window.prompt( | |
| "Provide a reason for deletion (min 5 characters).", | |
| ); | |
| if (!reason || reason.trim().length < 5) { | |
| toast.error("Deletion reason must be at least 5 characters."); | |
| return; | |
| } | |
| deleteReview.mutate( | |
| { reviewId: deletingReview.id, reason: reason.trim() }, | |
| { | |
| onSettled: () => setDeletingReview(null), | |
| } | |
| ); | |
| }; |
🤖 Prompt for AI Agents
In `@src/routes/`(admin)/admin/reviews/index.tsx around lines 50 - 61, The handler
handleConfirmDelete currently calls deleteReview.mutate(...) and immediately
calls setDeletingReview(null), which closes ConfirmDeleteDialog before the
mutation finishes and prevents the dialog's isDeleting spinner from showing;
update handleConfirmDelete to remove the immediate setDeletingReview(null) and
instead call setDeletingReview(null) in the mutation callbacks (onSuccess and/or
onSettled) provided to deleteReview.mutate (or configure these callbacks on the
deleteReview mutation itself) so the dialog remains open while
deleteReview.isLoading/isDeleting is true and only closes after the mutation
completes.
| const handleAddUser = async (data: AdminUserFormValues) => { | ||
| await createUser(data); | ||
| }; |
There was a problem hiding this comment.
Add dialog close on successful user creation.
handleAddUser calls createUser but doesn't close the AddUserDialog on success. The user will need to manually close the dialog after adding a user, which degrades the UX.
🐛 Proposed fix to close dialog on success
const handleAddUser = async (data: AdminUserFormValues) => {
await createUser(data);
+ setIsAddDialogOpen(false);
};Also applies to: 67-72
🤖 Prompt for AI Agents
In `@src/routes/`(admin)/admin/users/index.tsx around lines 31 - 33, handleAddUser
currently awaits createUser(data) but never closes the AddUserDialog, so the
dialog stays open after successful creation; update handleAddUser (and the
similar handler around lines 67-72) to close the dialog when createUser resolves
(and optionally handle errors by keeping it open and surfacing the error).
Specifically, after createUser returns successfully call the dialog close
function (e.g., setIsAddUserOpen(false) or the Close method provided to
AddUserDialog) and ensure you only close on success, not on caught errors.
| export interface BaseReviewFields { | ||
| id: string; | ||
| userId: string; | ||
| userName: string; | ||
| userAvatar: string | null; | ||
| productId: string; | ||
| productName: string; | ||
| rating: number; | ||
| title: string; | ||
| comment: string; | ||
| status: string; | ||
| helpfulCount: number; | ||
| isVerifiedPurchase: boolean; | ||
| vendorResponse: string | null; | ||
| vendorRespondedAt: string | null; | ||
| createdAt: string; | ||
| updatedAt: string; |
There was a problem hiding this comment.
Narrow BaseReviewFields.status to ReviewStatus for consistency.
Right now it’s a plain string, which weakens type safety relative to the new status union used elsewhere.
🔧 Proposed fix
export interface BaseReviewFields {
id: string;
userId: string;
userName: string;
userAvatar: string | null;
productId: string;
productName: string;
rating: number;
title: string;
comment: string;
- status: string;
+ status: ReviewStatus;
helpfulCount: number;
isVerifiedPurchase: boolean;
vendorResponse: string | null;
vendorRespondedAt: string | null;
createdAt: string;
updatedAt: string;
}📝 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.
| export interface BaseReviewFields { | |
| id: string; | |
| userId: string; | |
| userName: string; | |
| userAvatar: string | null; | |
| productId: string; | |
| productName: string; | |
| rating: number; | |
| title: string; | |
| comment: string; | |
| status: string; | |
| helpfulCount: number; | |
| isVerifiedPurchase: boolean; | |
| vendorResponse: string | null; | |
| vendorRespondedAt: string | null; | |
| createdAt: string; | |
| updatedAt: string; | |
| export interface BaseReviewFields { | |
| id: string; | |
| userId: string; | |
| userName: string; | |
| userAvatar: string | null; | |
| productId: string; | |
| productName: string; | |
| rating: number; | |
| title: string; | |
| comment: string; | |
| status: ReviewStatus; | |
| helpfulCount: number; | |
| isVerifiedPurchase: boolean; | |
| vendorResponse: string | null; | |
| vendorRespondedAt: string | null; | |
| createdAt: string; | |
| updatedAt: string; | |
| } |
🤖 Prompt for AI Agents
In `@src/types/review.ts` around lines 19 - 35, The BaseReviewFields interface
currently types the status field as a plain string—change its type to the
existing ReviewStatus union to tighten types: update the status property in
BaseReviewFields to ReviewStatus, import or reference the ReviewStatus type
where BaseReviewFields is declared, and adjust any downstream code or tests that
assumed a string (e.g., comparisons or assignments) to use the ReviewStatus
values to ensure consistency with the rest of the codebase.
| banned: boolean; | ||
| status: "active" | "banned"; |
There was a problem hiding this comment.
Redundant fields: banned and status convey the same information.
The banned: boolean field and status: "active" | "banned" field are semantically equivalent — one can be derived from the other. This creates a risk of inconsistency if they fall out of sync.
Consider removing one field. If status might expand to include more values in the future (e.g., "suspended"), keep status and derive banned where needed. Otherwise, banned alone may suffice.
♻️ Option A: Keep only status
export interface AdminUser {
id: string;
name: string;
email: string;
image?: string;
role: UserRole;
- banned: boolean;
status: "active" | "banned";
createdAt: Date | string;
updatedAt: Date | string;
}♻️ Option B: Keep only banned
export interface AdminUser {
id: string;
name: string;
email: string;
image?: string;
role: UserRole;
banned: boolean;
- status: "active" | "banned";
createdAt: Date | string;
updatedAt: Date | string;
}📝 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.
| banned: boolean; | |
| status: "active" | "banned"; | |
| export interface AdminUser { | |
| id: string; | |
| name: string; | |
| email: string; | |
| image?: string; | |
| role: UserRole; | |
| status: "active" | "banned"; | |
| createdAt: Date | string; | |
| updatedAt: Date | string; | |
| } |
| banned: boolean; | |
| status: "active" | "banned"; | |
| export interface AdminUser { | |
| id: string; | |
| name: string; | |
| email: string; | |
| image?: string; | |
| role: UserRole; | |
| banned: boolean; | |
| createdAt: Date | string; | |
| updatedAt: Date | string; | |
| } |
🤖 Prompt for AI Agents
In `@src/types/users.ts` around lines 9 - 10, The User type currently has
redundant fields banned and status; remove one to avoid inconsistency. Choose
which to keep (prefer keeping status if you expect more states): delete the
banned boolean from the interface/type and replace any direct reads of
user.banned with user.status === "banned" (or, if you instead keep banned,
remove status and map reads of user.status to boolean logic). Update any places
that construct users to only set the retained field, adjust serialization/DB
mapping and tests accordingly, and ensure helper functions (e.g., any isBanned
checks) derive the value from the remaining field.
Summary by CodeRabbit
New Features
Improvements