Back to Guide

Thursday, July 17, 2025

React Query and Reusable Data tables

cover

SOAPI: Smart Optimized API Interaction

Complete Frontend Development Guide

Table of Contents

  1. SOAPI Principles Overview
  2. Project Setup
  3. API Configuration
  4. API Service Layer
  5. React Query Integration
  6. Data Table Components
  7. CRUD Operations Implementation
  8. Update Forms with Tabs

SOAPI Principles Overview

SOAPI stands for: Small payloads, Optimized queries, Atomic updates, Protected deletes, Intelligent responses

Core Principles

🚀 Small Payloads (S)

  • Create operations use maximum 5 fields
  • Use modals for quick creation
  • Complete details via update operations

🎯 Optimized Queries (O)

  • Always use select to fetch only required fields
  • Different queries for different UI needs (list vs detail)

Atomic Updates (A)

  • Use PATCH instead of PUT
  • Update only changed fields
  • Group related fields in UI cards

🛡️ Protected Deletes (P)

  • Implement soft deletes when possible
  • Check for relationships before deletion
  • Provide deactivation alternatives

🧠 Intelligent Responses (I)

  • Return IDs instead of full objects
  • Minimize response payload
  • Use React Query for smart caching

Project Setup

1. Install Dependencies

npm install axios @tanstack/react-query sonner
npm install @hookform/resolvers/zod react-hook-form zod
npm install lucide-react date-fns clsx

2. Directory Structure

src/
├── config/
│   └── axios.ts
├── services/
│   └── products.ts
├── hooks/
│   └── useProducts.ts
├── components/
│   ├── ui/
│   │   └── data-table/
│   │       ├── index.ts
│   │       ├── data-table.tsx
│   │       ├── table-actions.tsx
│   │       ├── filter-bar.tsx
│   │       ├── entity-form.tsx
│   │       ├── confirmation-dialog.tsx
│   │       ├── table-loading.tsx
│   │       ├── date-filter.tsx
│   │       └── rows-per-page.tsx
│   └── products/
│       └── product-listing.tsx
└── app/
    └── products/
        ├── page.tsx
        └── [id]/
            └── edit/
                ├── page.tsx
                ├── layout.tsx
                ├── edit-loading.tsx
                ├── product-update-form.tsx
                └── tabs/
                    ├── basic-info-tab.tsx
                    ├── inventory-pricing-tab.tsx
                    └── additional-details-tab.tsx

API Configuration

config/axios.ts

import axios from "axios";

const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL;

// Create a base axios instance without authentication
const baseApi = axios.create({
  baseURL: `${BASE_URL}/api/v1`,
  headers: {
    "Content-Type": "application/json",
  },
});

export { baseApi as api };

API Service Layer

services/products.ts

import { api } from "@/config/axios";
import {
  ProductCreateDTO,
  ProductResponse,
  BriefProductsResponse,
  ProductData,
} from "@/types/product";

// Create product with minimal payload (SOAPI: Small Payloads)
export async function createProduct(data: ProductCreateDTO) {
  try {
    const res = await api.post("/products", data);
    return {
      success: true,
      data: res.data.data, // Return created product ID
      error: null,
    };
  } catch (error) {
    console.log(error);
    return {
      success: false,
      error: "Failed to Create Product",
      data: null,
    };
  }
}

// Get optimized product list (SOAPI: Optimized Queries)
export async function getBriefProducts(
  params = {}
): Promise<BriefProductsResponse> {
  try {
    const res = await api.get("/products/brief", { params });
    return res.data;
  } catch (error) {
    console.log(error);
    if (axios.isAxiosError(error)) {
      throw new Error(
        error.response?.data?.error || "Failed to fetch products"
      );
    }
    throw new Error("An unexpected error occurred");
  }
}

// Get full product details
export async function getProductById(id: string): Promise<ProductResponse> {
  try {
    const res = await api.get(`/products/${id}`);
    return res.data;
  } catch (error) {
    console.log(error);
    if (axios.isAxiosError(error)) {
      throw new Error(error.response?.data?.error || "Failed to fetch product");
    }
    throw new Error("An unexpected error occurred");
  }
}

// Update product with only changed fields (SOAPI: Atomic Updates)
export async function updateProductById(
  id: string,
  data: Partial<ProductData>
) {
  try {
    const res = await api.patch(`/products/${id}`, data);
    return {
      success: true,
      data: res.data.id, // Return only ID (SOAPI: Intelligent Responses)
    };
  } catch (error) {
    console.log(error);
    return { success: false };
  }
}

// Protected delete with relationship checking (SOAPI: Protected Deletes)
export async function deleteProduct(id: string) {
  try {
    const res = await api.delete(`/products/${id}`);
    return {
      success: true,
      data: res.data.id,
      error: null,
    };
  } catch (error) {
    console.log(error);
    return {
      success: false,
      data: null,
      error: "Failed to delete the Product",
    };
  }
}

Type Definitions (types/product.ts)

export interface ProductCreateDTO {
  name: string;
  sellingPrice: number;
  costPrice: number;
  sku: string;
  thumbnail?: string;
}

export interface BriefProductDTO {
  id: string;
  name: string;
  sellingPrice: number;
  salesCount: number;
  salesTotal: number;
  thumbnail: string | null;
  createdAt: Date;
}

export interface ProductData extends BriefProductDTO {
  slug: string;
  sku: string;
  barcode?: string;
  description?: string;
  dimensions?: string;
  weight?: number;
  costPrice: number;
  minStockLevel: number;
  maxStockLevel?: number;
  isActive: boolean;
  isSerialTracked: boolean;
  categoryId?: string;
  brandId?: string;
  unitId?: string;
  taxRateId?: string;
  tax?: number;
  unitOfMeasure?: string;
  imageUrls?: string[];
  // Additional fields for extended details
  upc?: string;
  ean?: string;
  mpn?: string;
  isbn?: string;
  updatedAt: Date;
}

export interface BriefProductsResponse {
  data: BriefProductDTO[];
  total: number;
}

export interface ProductResponse {
  data: ProductData;
  success: boolean;
}

React Query Integration

hooks/useProducts.ts

import {
  createProduct,
  getBriefProducts,
  getProductById,
  updateProductById,
  deleteProduct,
} from "@/services/products";
import { ProductCreateDTO } from "@/types/product";
import {
  useQuery,
  useSuspenseQuery,
  useMutation,
  useQueryClient,
} from "@tanstack/react-query";
import { toast } from "sonner";

// Query keys for caching
export const productKeys = {
  all: ["products"] as const,
  lists: () => [...productKeys.all, "list"] as const,
  list: (filters: any) => [...productKeys.lists(), { filters }] as const,
  details: () => [...productKeys.all, "detail"] as const,
  detail: (id: string) => [...productKeys.details(), id] as const,
};

export function useProducts() {
  const { data: products, refetch } = useSuspenseQuery({
    queryKey: productKeys.lists(),
    queryFn: () => getBriefProducts(),
  });

  return {
    products: products.data,
    refetch,
  };
}

export function useCreateProduct() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (data: ProductCreateDTO) => createProduct(data),
    onSuccess: () => {
      toast.success("Product added successfully");
      queryClient.invalidateQueries({ queryKey: productKeys.lists() });
    },
    onError: (error: Error) => {
      toast.error("Failed to add Product", {
        description: error.message || "Unknown error occurred",
      });
    },
  });
}

export function useDeleteProduct() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (id: string) => deleteProduct(id),
    onSuccess: () => {
      toast.success("Product deleted successfully");
      queryClient.invalidateQueries({ queryKey: productKeys.lists() });
    },
    onError: (error: Error) => {
      toast.error("Failed to delete product", {
        description: error.message || "Unknown error occurred",
      });
    },
  });
}

CRUD Operations Implementation

Product Listing Page

app/products/page.tsx

import { Suspense } from "react";
import { TableLoading } from "@/components/ui/data-table";
import ProductListing from "@/components/products/product-listing";

// Create an async component for data fetching
async function ProductListingWithData() {
  return (
    <ProductListing title="Products" subtitle="Manage your product catalog" />
  );
}

export default function ProductsPage() {
  return (
    <div className="flex h-full flex-1 flex-col gap-4 rounded-xl p-4">
      <Suspense fallback={<TableLoading />}>
        <ProductListingWithData />
      </Suspense>
    </div>
  );
}

components/products/product-listing.tsx

"use client";

import { useState, useEffect } from "react";
import { format } from "date-fns";
import * as XLSX from "xlsx";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { toast } from "sonner";
import { DollarSign } from "lucide-react";
import {
  DataTable,
  Column,
  TableActions,
  EntityForm,
  ConfirmationDialog,
} from "@/components/ui/data-table";
import {
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
  FormDescription,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
  useCreateProduct,
  useDeleteProduct,
  useProducts,
} from "@/hooks/useProducts";
import { BriefProductDTO, ProductCreateDTO } from "@/types/product";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";

interface ProductListingProps {
  title: string;
  subtitle: string;
}

// SOAPI: Small Payloads - Only 5 fields maximum for creation
const productFormSchema = z.object({
  name: z.string().min(1, "Name is required"),
  sellingPrice: z.string().min(1, "Price is required"),
  costPrice: z.string().min(1, "Cost price is required"),
  sku: z.string().min(1, "SKU is required"),
  thumbnail: z.string().optional(),
});

type ProductFormValues = z.infer<typeof productFormSchema>;

export default function ProductListing({
  title,
  subtitle,
}: ProductListingProps) {
  // SOAPI: React Query with Suspense
  const { products, refetch } = useProducts();
  const createProductMutation = useCreateProduct();
  const deleteProductMutation = useDeleteProduct();

  // Local state
  const [formDialogOpen, setFormDialogOpen] = useState(false);
  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
  const [productToDelete, setProductToDelete] =
    useState<BriefProductDTO | null>(null);

  // SOAPI: Small payload form
  const form = useForm<ProductCreateDTO>({
    resolver: zodResolver(productFormSchema),
    defaultValues: {
      name: "",
      sellingPrice: 0,
      costPrice: 0,
      sku: "",
      thumbnail:
        "https://14j7oh8kso.ufs.sh/f/HLxTbDBCDLwfAXaapcezIN7vwylkF1PXSCqAuseUG0gx8mhd",
    },
  });

  const router = useRouter();

  // Format currency
  const formatCurrency = (amount: number) => {
    return new Intl.NumberFormat("en-UG", {
      style: "currency",
      currency: "UGX",
      minimumFractionDigits: 0,
    }).format(amount);
  };

  // Export to Excel
  const handleExport = async (filteredProducts: BriefProductDTO[]) => {
    try {
      const exportData = filteredProducts.map((product) => ({
        Name: product.name,
        Price: product.sellingPrice,
        "Sales Count": product.salesCount,
        "Total Sales": formatCurrency(product.salesTotal),
        "Date Added": format(new Date(product.createdAt), "MMM dd, yyyy"),
      }));

      const worksheet = XLSX.utils.json_to_sheet(exportData);
      const workbook = XLSX.utils.book_new();
      XLSX.utils.book_append_sheet(workbook, worksheet, "Products");

      const fileName = `Products_${format(new Date(), "yyyy-MM-dd")}.xlsx`;
      XLSX.writeFile(workbook, fileName);

      toast.success("Export successful", {
        description: `Products exported to ${fileName}`,
      });
    } catch (error) {
      toast.error("Export failed");
    }
  };

  // Handle add new click
  const handleAddClick = () => {
    setFormDialogOpen(true);
  };

  // Handle edit click - Navigate to edit page
  const handleEditClick = (product: BriefProductDTO) => {
    router.push(`/products/${product.id}/edit`);
  };

  // Handle delete click
  const handleDeleteClick = (product: BriefProductDTO) => {
    setProductToDelete(product);
    setDeleteDialogOpen(true);
  };

  // SOAPI: Small payload submission
  const onSubmit = async (data: ProductCreateDTO) => {
    // Convert string prices to numbers
    data.costPrice = Number(data.costPrice);
    data.sellingPrice = Number(data.sellingPrice);

    createProductMutation.mutate(data, {
      onSuccess: () => {
        setFormDialogOpen(false);
        form.reset();
      },
    });
  };

  // Handle confirming delete
  const handleConfirmDelete = () => {
    if (productToDelete) {
      deleteProductMutation.mutate(productToDelete.id, {
        onSuccess: () => {
          setDeleteDialogOpen(false);
          setProductToDelete(null);
        },
      });
    }
  };

  // SOAPI: Optimized columns - only display what's needed for list view
  const columns: Column<BriefProductDTO>[] = [
    {
      header: "Image",
      accessorKey: "thumbnail",
      cell: (row) => (
        <img
          className="w-10 h-10 rounded-md object-cover"
          src={row.thumbnail ?? "/placeholder.png"}
          alt={row.name}
        />
      ),
    },
    {
      header: "Name",
      accessorKey: "name",
      cell: (row) => (
        <span className="font-medium line-clamp-1">
          {row.name.length > 20 ? `${row.name.substring(0, 20)}...` : row.name}
        </span>
      ),
    },
    {
      header: "Price",
      accessorKey: (row) => formatCurrency(row.sellingPrice),
    },
    {
      header: "Sales Count",
      accessorKey: "salesCount",
    },
    {
      header: "Total Sales",
      accessorKey: (row) => formatCurrency(row.salesTotal),
    },
  ];

  return (
    <>
      <DataTable<BriefProductDTO>
        title={title}
        subtitle={subtitle}
        data={products}
        columns={columns}
        keyField="id"
        isLoading={false}
        onRefresh={refetch}
        actions={{
          onAdd: handleAddClick,
          onExport: handleExport,
        }}
        filters={{
          searchFields: ["name"],
          enableDateFilter: true,
          getItemDate: (item) => item.createdAt,
        }}
        renderRowActions={(item) => (
          <TableActions.RowActions
            onEdit={() => handleEditClick(item)}
            onDelete={() => handleDeleteClick(item)}
            isDeleting={
              deleteProductMutation.isPending && productToDelete?.id === item.id
            }
          />
        )}
      />

      {/* SOAPI: Small Payload Form - Only 5 fields */}
      <EntityForm
        size="md"
        open={formDialogOpen}
        onOpenChange={setFormDialogOpen}
        title="Add New Product"
        form={form}
        onSubmit={onSubmit}
        isSubmitting={createProductMutation.isPending}
        submitLabel="Add Product"
      >
        <FormField
          control={form.control}
          name="name"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Product Name</FormLabel>
              <FormControl>
                <Input placeholder="Enter product name" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />

        <div className="grid gap-3 grid-cols-2">
          <FormField
            control={form.control}
            name="sellingPrice"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Selling Price</FormLabel>
                <FormControl>
                  <div className="relative">
                    <DollarSign className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
                    <Input placeholder="25,000" className="pl-8" {...field} />
                  </div>
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />
          <FormField
            control={form.control}
            name="costPrice"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Cost Price</FormLabel>
                <FormControl>
                  <div className="relative">
                    <DollarSign className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
                    <Input placeholder="20,000" className="pl-8" {...field} />
                  </div>
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />
        </div>

        <FormField
          control={form.control}
          name="sku"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Product SKU</FormLabel>
              <FormControl>
                <Input placeholder="SKU-001" {...field} />
              </FormControl>
              <FormDescription>Unique product identifier</FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />
      </EntityForm>

      {/* Delete Confirmation Dialog */}
      <ConfirmationDialog
        open={deleteDialogOpen}
        onOpenChange={setDeleteDialogOpen}
        title="Delete Product"
        description={
          productToDelete ? (
            <>
              Are you sure you want to delete{" "}
              <strong>{productToDelete.name}</strong>? This action cannot be
              undone.
            </>
          ) : (
            "Are you sure you want to delete this product?"
          )
        }
        onConfirm={handleConfirmDelete}
        isConfirming={deleteProductMutation.isPending}
        confirmLabel="Delete"
        variant="destructive"
      />
    </>
  );
}

AN EXAMPLE PAGE FOR COURSES

Courses page

import { Suspense } from "react";
import { TableLoading } from "@/components/ui/data-table";
import DashboardCourseListing from "@/components/Lists/DashboardCourseListing";
import { getCourses } from "@/actions/courses";
import { getAuthUser } from "@/config/useAuth";

// Create an async component for data fetching
async function CourseListingWithData() {
  const user = await getAuthUser();
  const courses = (await getCourses()) || [];

  return (
    <DashboardCourseListing
      title={`Courses (${courses.length})`}
      subtitle="Manage Courses"
      courses={courses}
      instructorId={user?.id ?? ""}
    />
  );
}

export default function Courses() {
  return (
    <div className="flex h-full flex-1 flex-col gap-4 rounded-xl p-4">
      <Suspense fallback={<TableLoading />}>
        <CourseListingWithData />
      </Suspense>
    </div>
  );
}

DashboardCourseListing

"use client";
export type CourseBrief = {
  id: string;
  title: string;
  thumbnail: string | null;
  students: number;
  createdAt: Date;
};

import { useState } from "react";
import * as XLSX from "xlsx";
// import DataTable from '../ReUsableDataTable/DataTable';
import { formatDate, formatDate2, formatISODate } from "@/lib/utils";

import { format } from "date-fns";
import { toast } from "sonner";

import {
  Column,
  ConfirmationDialog,
  DataTable,
  TableActions,
} from "../ui/data-table";
import { useRouter } from "next/navigation";
import CourseCreateForm from "@/app/(dashboard)/dashboard/courses/components/CourseCreateForm";
import { Button } from "../ui/button";

import { deleteCourse } from "@/actions/courses";
import { Pencil } from "lucide-react";

export default function DashboardCourseListing({
  courses,
  title,
  subtitle,
  instructorId,
}: {
  courses: CourseBrief[];
  title: string;
  subtitle: string;
  instructorId: string;
}) {
  const router = useRouter();
  const [isDeleting, setIsDeleting] = useState(false);
  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
  const [deleteItem, setDeleteItem] = useState<CourseBrief | null>(null);
  const [modalOpen, setModalOpen] = useState(false);

  console.log(deleteItem);
  const columns: Column<CourseBrief>[] = [
    {
      accessorKey: "title",
      header: "Course title",
      cell: (row) => {
        const course = row;
        const image = course.thumbnail || "/default.png";
        return (
          <div className="flex items-center gap-3">
            <img
              src={`${image}`}
              alt={course.title}
              className="h-10 w-10 rounded-md object-cover"
            />
            <span className="font-medium">{course.title}</span>
          </div>
        );
      },
    },
    {
      accessorKey: "students",
      header: "Students",
      cell: (row) => {
        const course = row;
        return (
          <div className="">
            <span className="font-medium">{course.students}</span>
          </div>
        );
      },
    },
    {
      accessorKey: "id",
      header: "Add Module",
      cell: (row) => {
        const course = row;
        return (
          <div className="">
            <Button variant={"outline"} href={`/course-module/${course.id}`}>
              <Pencil className="w-4 h-4" />
              Add Course Modules
            </Button>
          </div>
        );
      },
    },
    {
      header: "Date Added",
      accessorKey: (row) => formatISODate(row.createdAt),
    },
  ];

  const handleAddNew = () => {
    setModalOpen(true);
  };

  // Export to Excel
  const handleExport = async (filteredCourses: CourseBrief[]) => {
    try {
      // Prepare data for export
      const exportData = filteredCourses.map((course) => ({
        ID: course.id,
        Title: course.title,
        Thumbnail: course.thumbnail,
        "Date Added": formatISODate(course.createdAt),
      }));

      // Create workbook and worksheet
      const worksheet = XLSX.utils.json_to_sheet(exportData);
      const workbook = XLSX.utils.book_new();
      XLSX.utils.book_append_sheet(workbook, worksheet, "Products");

      // Generate filename with current date
      const fileName = `Products_${format(new Date(), "yyyy-MM-dd")}.xlsx`;

      // Export to file
      XLSX.writeFile(workbook, fileName);

      toast.success("Export successful", {
        description: `Products exported to ${fileName}`,
      });
    } catch (error) {
      toast.error("Export failed", {
        description:
          error instanceof Error ? error.message : "Unknown error occurred",
      });
    } finally {
      // setIsExporting(false);
    }
  };

  // Handle edit click
  const handleEditClick = (course: CourseBrief) => {
    router.push(`/dashboard/courses/${course.id}/edit`);
    // setEditingCategory(category);
    // setModalOpen(true);
  };

  // Handle delete click
  const handleDeleteClick = (course: CourseBrief) => {
    setDeleteItem(course);
    setDeleteDialogOpen(true);
  };
  const handleConfirmDelete = async () => {
    if (deleteItem) {
      setIsDeleting(true);
      const count = deleteItem.students ?? 0;
      if (count > 0) {
        toast.error("Delete Failed", {
          description: "This Course has associated students",
        });
        setDeleteDialogOpen(false);
        setIsDeleting(false);
        return;
      }
      await deleteCourse(deleteItem.id);
      console.log("Deleting Course with ID:", deleteItem.id);
    }
  };

  return (
    <div className="container mx-auto py-6">
      <CourseCreateForm
        isOpen={modalOpen}
        onClose={() => setModalOpen(false)}
        userId={instructorId}
      />
      <DataTable<CourseBrief>
        title={title}
        subtitle={subtitle}
        data={courses}
        columns={columns}
        keyField="id"
        isLoading={false} // With Suspense, we're guaranteed to have data
        onRefresh={() => console.log("refreshing")}
        actions={{
          onAdd: handleAddNew,
          onExport: handleExport,
        }}
        filters={{
          searchFields: ["title"],
          enableDateFilter: true,
          getItemDate: (item) => item.createdAt,
        }}
        renderRowActions={(item) => (
          <TableActions.RowActions
            onEdit={() => handleEditClick(item)}
            onDelete={() => handleDeleteClick(item)}
            // isDeleting={isDeleting}
          />
        )}
      />

      <ConfirmationDialog
        open={deleteDialogOpen}
        onOpenChange={setDeleteDialogOpen}
        title="Delete Course"
        description={
          deleteItem ? (
            <>
              Are you sure you want to delete{" "}
              <strong>{deleteItem.title}</strong>? This action cannot be undone.
            </>
          ) : (
            "Are you sure you want to delete this Department?"
          )
        }
        onConfirm={handleConfirmDelete}
        isConfirming={isDeleting}
        confirmLabel="Delete"
        variant="destructive"
      />
    </div>
  );
}

The Course create form

"use client";
import React, { useState } from "react";
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog";

import { Check, DollarSign, FolderPlus, Pen, Pencil, Plus } from "lucide-react";
import { useForm } from "react-hook-form";

import { Button } from "@/components/ui/button";
import TextInput from "@/components/FormInputs/TextInput";
import SubmitButton from "@/components/FormInputs/SubmitButton";
import { CourseCreateDTO } from "@/types/course";
import { toast } from "sonner";
import TextArea from "@/components/FormInputs/TextAreaInput";
import { generateSlug } from "@/lib/generateSlug";
import { createCourse } from "@/actions/courses";
import { useRouter } from "next/navigation";

export default function CourseCreateForm({
  userId,
  initialContent,
  editingId,
  isOpen,
  onClose,
}: {
  userId: string;
  initialContent?: string;
  editingId?: string;
  isOpen: boolean;
  onClose: () => void;
}) {
  const {
    register,
    handleSubmit,
    reset,
    formState: { errors },
  } = useForm<CourseCreateDTO>({
    defaultValues: {
      title: "",
      summary: "",
    },
  });

  const [loading, setLoading] = useState(false);
  const router = useRouter();
  async function saveCourse(data: CourseCreateDTO) {
    data.instructorId = userId;
    data.slug = generateSlug(data.title);
    data.price = Number(data.price);
    try {
      setLoading(true);
      const res = await createCourse(data);
      if (res.error) {
        toast.error("Failed to Create Course", {
          description: res.error,
        });
        return;
      }
      setLoading(false);
      toast.success("Successfully Created!");
      reset();
      setTimeout(() => {
        router.push(`/dashboard/courses/${res.data?.id}/edit`);
      }, 3000);
    } catch (error) {
      toast.error("Something went wrong");
      setLoading(false);
      console.log(error);
    }
  }

  return (
    <div>
      <div className="py-1">
        <Dialog
          open={isOpen}
          onOpenChange={(open) => {
            if (!open) {
              reset();
              onClose();
            }
          }}
        >
          <DialogContent>
            <DialogHeader>
              <DialogTitle>
                {editingId ? "Edit Course" : "Add New Course"}
              </DialogTitle>
            </DialogHeader>
            <form className="" onSubmit={handleSubmit(saveCourse)}>
              <div className="">
                <div className="space-y-3">
                  <div className="grid gap-3">
                    <TextInput
                      register={register}
                      errors={errors}
                      label="Course title"
                      name="title"
                      icon={Check}
                    />
                  </div>
                  <div className="grid gap-3">
                    <TextInput
                      register={register}
                      errors={errors}
                      label="Course Price"
                      name="price"
                      type="number"
                      icon={DollarSign}
                    />
                  </div>
                  <div className="grid gap-3">
                    <TextArea
                      register={register}
                      errors={errors}
                      label="Course Summary Description"
                      name="summary"
                      helperText="The summary should short to 125 characters"
                    />
                  </div>
                </div>
                <div className="py-3 flex items-center justify-between">
                  <Button
                    type="button"
                    variant="outline"
                    onClick={() => {
                      reset();
                      onClose();
                    }}
                  >
                    Cancel
                  </Button>
                  <SubmitButton
                    title={editingId ? "Update" : "Add Course"}
                    loading={loading}
                  />
                </div>
              </div>
            </form>
          </DialogContent>
        </Dialog>
      </div>
    </div>
  );
}

OTHER FORM COMPONENTS


Update Forms with Tabs

Product Edit Page Structure

app/products/[id]/edit/layout.tsx

import React, { ReactNode } from "react";

export default async function Layout({ children }: { children: ReactNode }) {
  // Add any permission checks here if needed
  return <div>{children}</div>;
}

app/products/[id]/edit/page.tsx

import { notFound } from "next/navigation";
import { ProductUpdateForm } from "./product-update-form";
import { getProductById } from "@/services/products";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { ArrowLeft } from "lucide-react";
import { Suspense } from "react";
import EditProductLoading from "./edit-loading";

export default async function EditProductPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const id = (await params).id;
  const { data: product, success } = await getProductById(id);

  if (!product) {
    notFound();
  }

  // Mock options - replace with actual API calls
  const brandOptions = [
    { label: "Nike", value: "1" },
    { label: "Adidas", value: "2" },
  ];

  const categoryOptions = [
    { label: "Shoes", value: "1" },
    { label: "Clothing", value: "2" },
  ];

  const taxOptions = [
    { label: "VAT-18%", value: "1" },
    { label: "GST-12%", value: "2" },
  ];

  const unitOptions = [
    { label: "Piece-pcs", value: "1" },
    { label: "Kilogram-kg", value: "2" },
  ];

  return (
    <Suspense fallback={<EditProductLoading />}>
      <div className="container">
        <div className="mb-8 space-y-4">
          <div className="flex items-center gap-2">
            <Link href="/products">
              <Button variant="ghost" size="icon" className="rounded-full">
                <ArrowLeft className="h-5 w-5" />
                <span className="sr-only">Back to products</span>
              </Button>
            </Link>
            <div className="text-sm text-muted-foreground">
              <Link href="/products" className="hover:underline">
                Products
              </Link>{" "}
              / <span>Edit</span>
            </div>
          </div>

          <div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
            <div>
              <h1 className="text-3xl font-bold tracking-tight">
                {product.name}
              </h1>
              <p className="text-muted-foreground mt-1">
                SKU: {product.sku} • Last updated:{" "}
                {new Date(product.updatedAt).toLocaleDateString()}
              </p>
            </div>
            <div className="flex items-center gap-2">
              <Button variant="outline">Preview</Button>
            </div>
          </div>
        </div>

        <ProductUpdateForm
          brandOptions={brandOptions}
          categoryOptions={categoryOptions}
          unitOptions={unitOptions}
          taxOptions={taxOptions}
          product={product}
        />
      </div>
    </Suspense>
  );
}

app/products/[id]/edit/product-update-form.tsx

"use client";

import { useState } from "react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { BasicInfoTab } from "./tabs/basic-info-tab";
import { InventoryPricingTab } from "./tabs/inventory-pricing-tab";
import { AdditionalDetailsTab } from "./tabs/additional-details-tab";
import { ProductData } from "@/types/product";
import { cn } from "@/lib/utils";

export interface Option {
  label: string;
  value: string;
}

export function ProductUpdateForm({
  product,
  brandOptions,
  categoryOptions,
  taxOptions,
  unitOptions,
}: {
  product: ProductData;
  categoryOptions: Option[];
  taxOptions: Option[];
  unitOptions: Option[];
  brandOptions: Option[];
}) {
  const [activeTab, setActiveTab] = useState("basic-info");

  return (
    <Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
      <TabsList className="w-full p-0 bg-transparent border-b rounded-none mb-6 relative">
        <div className="absolute bottom-0 left-0 right-0 h-0.5 bg-muted"></div>
        <TabsTrigger
          value="basic-info"
          className={cn(
            "py-3 px-6 rounded-none data-[state=active]:shadow-none relative",
            "data-[state=active]:text-primary data-[state=active]:font-medium",
            "after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5 after:bg-primary after:scale-x-0 data-[state=active]:after:scale-x-100 after:transition-transform"
          )}
        >
          Basic Information
        </TabsTrigger>
        <TabsTrigger
          value="inventory-pricing"
          className={cn(
            "py-3 px-6 rounded-none data-[state=active]:shadow-none relative",
            "data-[state=active]:text-primary data-[state=active]:font-medium",
            "after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5 after:bg-primary after:scale-x-0 data-[state=active]:after:scale-x-100 after:transition-transform"
          )}
        >
          Inventory & Pricing
        </TabsTrigger>
        <TabsTrigger
          value="additional-details"
          className={cn(
            "py-3 px-6 rounded-none data-[state=active]:shadow-none relative",
            "data-[state=active]:text-primary data-[state=active]:font-medium",
            "after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5 after:bg-primary after:scale-x-0 data-[state=active]:after:scale-x-100 after:transition-transform"
          )}
        >
          Additional Details
        </TabsTrigger>
      </TabsList>

      <TabsContent value="basic-info">
        <BasicInfoTab
          brandOptions={brandOptions}
          categoryOptions={categoryOptions}
          product={product}
        />
      </TabsContent>

      <TabsContent value="inventory-pricing">
        <InventoryPricingTab
          unitOptions={unitOptions}
          taxOptions={taxOptions}
          product={product}
        />
      </TabsContent>

      <TabsContent value="additional-details">
        <AdditionalDetailsTab product={product} />
      </TabsContent>
    </Tabs>
  );
}

Tab Components

app/products/[id]/edit/tabs/basic-info-tab.tsx

"use client";

import { useState } from "react";
import {
  Card,
  CardContent,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { toast } from "sonner";
import { ProductData } from "@/types/product";
import { updateProductById } from "@/services/products";
import { Option } from "../product-update-form";

export function BasicInfoTab({
  product,
  brandOptions,
  categoryOptions,
}: {
  product: ProductData;
  categoryOptions: Option[];
  brandOptions: Option[];
}) {
  return (
    <div className="grid gap-6 mt-6">
      <NameSlugCard product={product} />
      <SkuBarcodeCard product={product} />
      <DescriptionDimensionsCard product={product} />
      <WeightThumbnailCard product={product} />
      <CategoryBrandCard
        categoryOptions={categoryOptions}
        brandOptions={brandOptions}
        product={product}
      />
    </div>
  );
}

// SOAPI: Atomic Updates - Each card updates only related fields
function NameSlugCard({ product }: { product: ProductData }) {
  const [name, setName] = useState(product.name);
  const [slug, setSlug] = useState(product.slug);
  const [isUpdating, setIsUpdating] = useState(false);

  const handleUpdate = async () => {
    if (!name.trim()) {
      toast.error("Name is required");
      return;
    }

    setIsUpdating(true);

    try {
      // SOAPI: Atomic Updates - Only send changed fields
      const data = { name, slug };
      await updateProductById(product.id, data);
      toast.success("Name and slug updated successfully");
    } catch (error) {
      toast.error("Failed to update name and slug");
      console.error(error);
    } finally {
      setIsUpdating(false);
    }
  };

  return (
    <Card>
      <CardHeader>
        <CardTitle>Basic Details</CardTitle>
      </CardHeader>
      <CardContent className="grid gap-6">
        <div className="grid gap-3">
          <Label htmlFor="name">Name</Label>
          <Input
            id="name"
            value={name}
            onChange={(e) => setName(e.target.value)}
            placeholder="Product name"
          />
        </div>
        <div className="grid gap-3">
          <Label htmlFor="slug">Slug</Label>
          <Input
            id="slug"
            value={slug}
            onChange={(e) => setSlug(e.target.value)}
            placeholder="product-slug"
          />
        </div>
      </CardContent>
      <CardFooter>
        <Button onClick={handleUpdate} disabled={isUpdating}>
          {isUpdating ? "Updating..." : "Update Basic Details"}
        </Button>
      </CardFooter>
    </Card>
  );
}

function SkuBarcodeCard({ product }: { product: ProductData }) {
  const [sku, setSku] = useState(product.sku);
  const [barcode, setBarcode] = useState(product.barcode || "");
  const [isUpdating, setIsUpdating] = useState(false);

  const handleUpdate = async () => {
    if (!sku.trim()) {
      toast.error("SKU is required");
      return;
    }

    setIsUpdating(true);

    try {
      // SOAPI: Atomic Updates - Only changed fields
      const data = { sku, barcode: barcode || undefined };
      await updateProductById(product.id, data);
      toast.success("SKU and barcode updated successfully");
    } catch (error) {
      toast.error("Failed to update SKU and barcode");
      console.error(error);
    } finally {
      setIsUpdating(false);
    }
  };

  return (
    <Card>
      <CardHeader>
        <CardTitle>Product Identifiers</CardTitle>
      </CardHeader>
      <CardContent className="grid gap-6">
        <div className="grid gap-3">
          <Label htmlFor="sku">SKU</Label>
          <Input
            id="sku"
            value={sku}
            onChange={(e) => setSku(e.target.value)}
            placeholder="SKU123456"
          />
        </div>
        <div className="grid gap-3">
          <Label htmlFor="barcode">Barcode</Label>
          <Input
            id="barcode"
            value={barcode}
            onChange={(e) => setBarcode(e.target.value)}
            placeholder="123456789012"
          />
        </div>
      </CardContent>
      <CardFooter>
        <Button onClick={handleUpdate} disabled={isUpdating}>
          {isUpdating ? "Updating..." : "Update Identifiers"}
        </Button>
      </CardFooter>
    </Card>
  );
}

function DescriptionDimensionsCard({ product }: { product: ProductData }) {
  const [description, setDescription] = useState(product.description || "");
  const [dimensions, setDimensions] = useState(product.dimensions || "");
  const [isUpdating, setIsUpdating] = useState(false);

  const handleUpdate = async () => {
    setIsUpdating(true);

    try {
      const data = {
        description: description || undefined,
        dimensions: dimensions || undefined,
      };
      await updateProductById(product.id, data);
      toast.success("Description and dimensions updated successfully");
    } catch (error) {
      toast.error("Failed to update description and dimensions");
      console.error(error);
    } finally {
      setIsUpdating(false);
    }
  };

  return (
    <Card>
      <CardHeader>
        <CardTitle>Product Description</CardTitle>
      </CardHeader>
      <CardContent className="grid gap-6">
        <div className="grid gap-3">
          <Label htmlFor="description">Description</Label>
          <Textarea
            id="description"
            value={description}
            onChange={(e) => setDescription(e.target.value)}
            placeholder="Product description"
            rows={3}
          />
        </div>
        <div className="grid gap-3">
          <Label htmlFor="dimensions">Dimensions</Label>
          <Input
            id="dimensions"
            value={dimensions}
            onChange={(e) => setDimensions(e.target.value)}
            placeholder="10x20x30 cm"
          />
        </div>
      </CardContent>
      <CardFooter>
        <Button onClick={handleUpdate} disabled={isUpdating}>
          {isUpdating ? "Updating..." : "Update Description"}
        </Button>
      </CardFooter>
    </Card>
  );
}

function WeightThumbnailCard({ product }: { product: ProductData }) {
  const [weight, setWeight] = useState(product.weight?.toString() || "");
  const [thumbnail, setThumbnail] = useState(product.thumbnail || "");
  const [isUpdating, setIsUpdating] = useState(false);

  const handleUpdate = async () => {
    setIsUpdating(true);

    try {
      const data = {
        weight: weight ? Number.parseFloat(weight) : undefined,
        thumbnail: thumbnail || undefined,
      };
      await updateProductById(product.id, data);
      toast.success("Weight and thumbnail updated successfully");
    } catch (error) {
      toast.error("Failed to update weight and thumbnail");
      console.error(error);
    } finally {
      setIsUpdating(false);
    }
  };

  return (
    <Card>
      <CardHeader>
        <CardTitle>Weight & Thumbnail</CardTitle>
      </CardHeader>
      <CardContent className="grid gap-6">
        <div className="grid gap-3">
          <Label htmlFor="weight">Weight (kg)</Label>
          <Input
            id="weight"
            type="number"
            step="0.01"
            value={weight}
            onChange={(e) => setWeight(e.target.value)}
            placeholder="1.5"
          />
        </div>
        <div className="grid gap-3">
          <Label htmlFor="thumbnail">Thumbnail URL</Label>
          <Input
            id="thumbnail"
            value={thumbnail}
            onChange={(e) => setThumbnail(e.target.value)}
            placeholder="https://example.com/image.jpg"
          />
        </div>
      </CardContent>
      <CardFooter>
        <Button onClick={handleUpdate} disabled={isUpdating}>
          {isUpdating ? "Updating..." : "Update Weight & Thumbnail"}
        </Button>
      </CardFooter>
    </Card>
  );
}

function CategoryBrandCard({
  product,
  brandOptions,
  categoryOptions,
}: {
  product: ProductData;
  categoryOptions: Option[];
  brandOptions: Option[];
}) {
  const [categoryId, setCategoryId] = useState(product.categoryId || "");
  const [brandId, setBrandId] = useState(product.brandId || "");
  const [isUpdating, setIsUpdating] = useState(false);

  const handleUpdate = async () => {
    setIsUpdating(true);

    try {
      const data = {
        categoryId: categoryId || undefined,
        brandId: brandId || undefined,
      };
      await updateProductById(product.id, data);
      toast.success("Category and brand updated successfully");
    } catch (error) {
      toast.error("Failed to update category and brand");
      console.error(error);
    } finally {
      setIsUpdating(false);
    }
  };

  return (
    <Card>
      <CardHeader>
        <CardTitle>Category & Brand</CardTitle>
      </CardHeader>
      <CardContent className="grid gap-6">
        <div className="grid gap-3">
          <Label htmlFor="category">Category</Label>
          <select
            id="category"
            value={categoryId}
            onChange={(e) => setCategoryId(e.target.value)}
            className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
          >
            <option value="">Select Category</option>
            {categoryOptions.map((option) => (
              <option key={option.value} value={option.value}>
                {option.label}
              </option>
            ))}
          </select>
        </div>
        <div className="grid gap-3">
          <Label htmlFor="brand">Brand</Label>
          <select
            id="brand"
            value={brandId}
            onChange={(e) => setBrandId(e.target.value)}
            className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
          >
            <option value="">Select Brand</option>
            {brandOptions.map((option) => (
              <option key={option.value} value={option.value}>
                {option.label}
              </option>
            ))}
          </select>
        </div>
      </CardContent>
      <CardFooter>
        <Button onClick={handleUpdate} disabled={isUpdating}>
          {isUpdating ? "Updating..." : "Update Category & Brand"}
        </Button>
      </CardFooter>
    </Card>
  );
}

app/products/[id]/edit/tabs/inventory-pricing-tab.tsx

"use client";

import { useState } from "react";
import {
  Card,
  CardContent,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { toast } from "sonner";
import { ProductData } from "@/types/product";
import { updateProductById } from "@/services/products";
import { Option } from "../product-update-form";

export function InventoryPricingTab({
  product,
  taxOptions,
  unitOptions,
}: {
  product: ProductData;
  taxOptions: Option[];
  unitOptions: Option[];
}) {
  return (
    <div className="grid gap-6 mt-6">
      <PricingCard product={product} />
      <StockLevelsCard product={product} />
      <StatusCard product={product} />
      <UnitTaxCard
        taxOptions={taxOptions}
        unitOptions={unitOptions}
        product={product}
      />
    </div>
  );
}

// SOAPI: Atomic Updates - Pricing card only handles pricing fields
function PricingCard({ product }: { product: ProductData }) {
  const [costPrice, setCostPrice] = useState(product.costPrice.toString());
  const [sellingPrice, setSellingPrice] = useState(
    product.sellingPrice.toString()
  );
  const [isUpdating, setIsUpdating] = useState(false);

  const handleUpdate = async () => {
    if (!costPrice.trim() || !sellingPrice.trim()) {
      toast.error("Both prices are required");
      return;
    }

    setIsUpdating(true);

    try {
      // SOAPI: Atomic Updates - Only pricing data
      const data = {
        costPrice: Number.parseFloat(costPrice),
        sellingPrice: Number.parseFloat(sellingPrice),
      };
      await updateProductById(product.id, data);
      toast.success("Prices updated successfully");
    } catch (error) {
      toast.error("Failed to update prices");
      console.error(error);
    } finally {
      setIsUpdating(false);
    }
  };

  return (
    <Card>
      <CardHeader>
        <CardTitle>Pricing</CardTitle>
      </CardHeader>
      <CardContent className="grid gap-6">
        <div className="grid gap-3">
          <Label htmlFor="costPrice">Cost Price</Label>
          <Input
            id="costPrice"
            type="number"
            step="0.01"
            value={costPrice}
            onChange={(e) => setCostPrice(e.target.value)}
            placeholder="0.00"
          />
        </div>
        <div className="grid gap-3">
          <Label htmlFor="sellingPrice">Selling Price</Label>
          <Input
            id="sellingPrice"
            type="number"
            step="0.01"
            value={sellingPrice}
            onChange={(e) => setSellingPrice(e.target.value)}
            placeholder="0.00"
          />
        </div>
      </CardContent>
      <CardFooter>
        <Button onClick={handleUpdate} disabled={isUpdating}>
          {isUpdating ? "Updating..." : "Update Pricing"}
        </Button>
      </CardFooter>
    </Card>
  );
}

function StockLevelsCard({ product }: { product: ProductData }) {
  const [minStockLevel, setMinStockLevel] = useState(
    product.minStockLevel.toString()
  );
  const [maxStockLevel, setMaxStockLevel] = useState(
    product.maxStockLevel?.toString() || ""
  );
  const [isUpdating, setIsUpdating] = useState(false);

  const handleUpdate = async () => {
    if (!minStockLevel.trim()) {
      toast.error("Minimum stock level is required");
      return;
    }

    setIsUpdating(true);

    try {
      const data = {
        minStockLevel: Number.parseInt(minStockLevel),
        maxStockLevel: maxStockLevel
          ? Number.parseInt(maxStockLevel)
          : undefined,
      };
      await updateProductById(product.id, data);
      toast.success("Stock levels updated successfully");
    } catch (error) {
      toast.error("Failed to update stock levels");
      console.error(error);
    } finally {
      setIsUpdating(false);
    }
  };

  return (
    <Card>
      <CardHeader>
        <CardTitle>Stock Levels</CardTitle>
      </CardHeader>
      <CardContent className="grid gap-6">
        <div className="grid gap-3">
          <Label htmlFor="minStockLevel">Minimum Stock Level</Label>
          <Input
            id="minStockLevel"
            type="number"
            value={minStockLevel}
            onChange={(e) => setMinStockLevel(e.target.value)}
            placeholder="0"
          />
        </div>
        <div className="grid gap-3">
          <Label htmlFor="maxStockLevel">Maximum Stock Level</Label>
          <Input
            id="maxStockLevel"
            type="number"
            value={maxStockLevel}
            onChange={(e) => setMaxStockLevel(e.target.value)}
            placeholder="Optional"
          />
        </div>
      </CardContent>
      <CardFooter>
        <Button onClick={handleUpdate} disabled={isUpdating}>
          {isUpdating ? "Updating..." : "Update Stock Levels"}
        </Button>
      </CardFooter>
    </Card>
  );
}

function StatusCard({ product }: { product: ProductData }) {
  const [isActive, setIsActive] = useState(product.isActive);
  const [isSerialTracked, setIsSerialTracked] = useState(
    product.isSerialTracked
  );
  const [isUpdating, setIsUpdating] = useState(false);

  const handleUpdate = async () => {
    setIsUpdating(true);

    try {
      const data = { isActive, isSerialTracked };
      await updateProductById(product.id, data);
      toast.success("Status updated successfully");
    } catch (error) {
      toast.error("Failed to update status");
      console.error(error);
    } finally {
      setIsUpdating(false);
    }
  };

  return (
    <Card>
      <CardHeader>
        <CardTitle>Product Status</CardTitle>
      </CardHeader>
      <CardContent className="grid gap-6">
        <div className="flex items-center justify-between">
          <Label htmlFor="isActive" className="cursor-pointer">
            Active Product
          </Label>
          <Switch
            id="isActive"
            checked={isActive}
            onCheckedChange={setIsActive}
          />
        </div>
        <div className="flex items-center justify-between">
          <Label htmlFor="isSerialTracked" className="cursor-pointer">
            Serial Number Tracking
          </Label>
          <Switch
            id="isSerialTracked"
            checked={isSerialTracked}
            onCheckedChange={setIsSerialTracked}
          />
        </div>
      </CardContent>
      <CardFooter>
        <Button onClick={handleUpdate} disabled={isUpdating}>
          {isUpdating ? "Updating..." : "Update Status"}
        </Button>
      </CardFooter>
    </Card>
  );
}

function UnitTaxCard({
  product,
  unitOptions,
  taxOptions,
}: {
  product: ProductData;
  unitOptions: Option[];
  taxOptions: Option[];
}) {
  const [taxId, setTaxId] = useState(product.taxRateId || "");
  const [unitId, setUnitId] = useState(product.unitId || "");
  const [isUpdating, setIsUpdating] = useState(false);

  const handleUpdate = async () => {
    setIsUpdating(true);

    try {
      const selectedTax = taxOptions.find((option) => option.value === taxId);
      const selectedUnit = unitOptions.find(
        (option) => option.value === unitId
      );

      const taxRate = selectedTax?.label.split("-")[1]?.replace("%", "") || "0";
      const unitName = selectedUnit?.label.split("-")[0] || "";

      const data = {
        taxRateId: taxId || undefined,
        unitId: unitId || undefined,
        unitOfMeasure: unitName,
        tax: Number.parseFloat(taxRate),
      };
      await updateProductById(product.id, data);
      toast.success("Unit and tax updated successfully");
    } catch (error) {
      toast.error("Failed to update unit and tax");
      console.error(error);
    } finally {
      setIsUpdating(false);
    }
  };

  return (
    <Card>
      <CardHeader>
        <CardTitle>Unit & Tax</CardTitle>
      </CardHeader>
      <CardContent className="grid gap-6">
        <div className="grid gap-3">
          <Label htmlFor="unit">Unit of Measure</Label>
          <select
            id="unit"
            value={unitId}
            onChange={(e) => setUnitId(e.target.value)}
            className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
          >
            <option value="">Select Unit</option>
            {unitOptions.map((option) => (
              <option key={option.value} value={option.value}>
                {option.label}
              </option>
            ))}
          </select>
        </div>
        <div className="grid gap-3">
          <Label htmlFor="tax">Tax Rate</Label>
          <select
            id="tax"
            value={taxId}
            onChange={(e) => setTaxId(e.target.value)}
            className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
          >
            <option value="">Select Tax Rate</option>
            {taxOptions.map((option) => (
              <option key={option.value} value={option.value}>
                {option.label}
              </option>
            ))}
          </select>
        </div>
      </CardContent>
      <CardFooter>
        <Button onClick={handleUpdate} disabled={isUpdating}>
          {isUpdating ? "Updating..." : "Update Unit & Tax"}
        </Button>
      </CardFooter>
    </Card>
  );
}

app/products/[id]/edit/tabs/additional-details-tab.tsx

"use client";

import { useState } from "react";
import {
  Card,
  CardContent,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { toast } from "sonner";
import { ProductData } from "@/types/product";
import { updateProductById } from "@/services/products";

export function AdditionalDetailsTab({ product }: { product: ProductData }) {
  return (
    <div className="grid gap-6 mt-6">
      <UpcEanCard product={product} />
      <MpnIsbnCard product={product} />
      <SalesInfoCard product={product} />
    </div>
  );
}

function UpcEanCard({ product }: { product: ProductData }) {
  const [upc, setUpc] = useState(product.upc || "");
  const [ean, setEan] = useState(product.ean || "");
  const [isUpdating, setIsUpdating] = useState(false);

  const handleUpdate = async () => {
    setIsUpdating(true);

    try {
      const data = {
        upc: upc || undefined,
        ean: ean || undefined,
      };
      await updateProductById(product.id, data);
      toast.success("UPC and EAN updated successfully");
    } catch (error) {
      toast.error("Failed to update UPC and EAN");
      console.error(error);
    } finally {
      setIsUpdating(false);
    }
  };

  return (
    <Card>
      <CardHeader>
        <CardTitle>UPC & EAN</CardTitle>
      </CardHeader>
      <CardContent className="grid gap-6">
        <div className="grid gap-3">
          <Label htmlFor="upc">UPC (Universal Product Code)</Label>
          <Input
            id="upc"
            value={upc}
            onChange={(e) => setUpc(e.target.value)}
            placeholder="123456789012"
            maxLength={12}
          />
          <p className="text-xs text-muted-foreground">
            12-digit unique number associated with the barcode
          </p>
        </div>
        <div className="grid gap-3">
          <Label htmlFor="ean">EAN (International Article Number)</Label>
          <Input
            id="ean"
            value={ean}
            onChange={(e) => setEan(e.target.value)}
            placeholder="1234567890123"
            maxLength={13}
          />
          <p className="text-xs text-muted-foreground">
            13-digit unique number
          </p>
        </div>
      </CardContent>
      <CardFooter>
        <Button onClick={handleUpdate} disabled={isUpdating}>
          {isUpdating ? "Updating..." : "Update UPC & EAN"}
        </Button>
      </CardFooter>
    </Card>
  );
}

function MpnIsbnCard({ product }: { product: ProductData }) {
  const [mpn, setMpn] = useState(product.mpn || "");
  const [isbn, setIsbn] = useState(product.isbn || "");
  const [isUpdating, setIsUpdating] = useState(false);

  const handleUpdate = async () => {
    setIsUpdating(true);

    try {
      const data = {
        mpn: mpn || undefined,
        isbn: isbn || undefined,
      };
      await updateProductById(product.id, data);
      toast.success("MPN and ISBN updated successfully");
    } catch (error) {
      toast.error("Failed to update MPN and ISBN");
      console.error(error);
    } finally {
      setIsUpdating(false);
    }
  };

  return (
    <Card>
      <CardHeader>
        <CardTitle>MPN & ISBN</CardTitle>
      </CardHeader>
      <CardContent className="grid gap-6">
        <div className="grid gap-3">
          <Label htmlFor="mpn">MPN (Manufacturing Part Number)</Label>
          <Input
            id="mpn"
            value={mpn}
            onChange={(e) => setMpn(e.target.value)}
            placeholder="MPN123456"
          />
          <p className="text-xs text-muted-foreground">
            Unambiguously identifies a part design
          </p>
        </div>
        <div className="grid gap-3">
          <Label htmlFor="isbn">
            ISBN (International Standard Book Number)
          </Label>
          <Input
            id="isbn"
            value={isbn}
            onChange={(e) => setIsbn(e.target.value)}
            placeholder="9781234567897"
            maxLength={13}
          />
          <p className="text-xs text-muted-foreground">
            13-digit unique commercial book identifier
          </p>
        </div>
      </CardContent>
      <CardFooter>
        <Button onClick={handleUpdate} disabled={isUpdating}>
          {isUpdating ? "Updating..." : "Update MPN & ISBN"}
        </Button>
      </CardFooter>
    </Card>
  );
}

// SOAPI: Protected Deletes - Sales info is read-only to prevent data corruption
function SalesInfoCard({ product }: { product: ProductData }) {
  return (
    <Card>
      <CardHeader>
        <CardTitle>Sales Information</CardTitle>
      </CardHeader>
      <CardContent className="grid gap-6">
        <div className="grid gap-3">
          <Label htmlFor="salesCount">Sales Count</Label>
          <Input
            id="salesCount"
            type="number"
            value={product.salesCount.toString()}
            disabled
            className="bg-muted"
          />
          <p className="text-xs text-muted-foreground">
            Total number of units sold (read-only)
          </p>
        </div>
        <div className="grid gap-3">
          <Label htmlFor="salesTotal">Sales Total</Label>
          <Input
            id="salesTotal"
            type="number"
            step="0.01"
            value={product.salesTotal.toString()}
            disabled
            className="bg-muted"
          />
          <p className="text-xs text-muted-foreground">
            Total revenue from this product (read-only)
          </p>
        </div>
      </CardContent>
      <CardFooter>
        <p className="text-sm text-muted-foreground">
          Sales information is automatically calculated and cannot be manually
          edited
        </p>
      </CardFooter>
    </Card>
  );
}

app/products/[id]/edit/edit-loading.tsx

import { Skeleton } from "@/components/ui/skeleton";
import { Button } from "@/components/ui/button";

export default function EditProductLoading() {
  return (
    <div className="container py-10 animate-pulse">
      {/* Header Section */}
      <div className="mb-8 space-y-4">
        <div className="flex items-center gap-2">
          <div className="h-9 w-9 rounded-full bg-muted"></div>
          <Skeleton className="h-4 w-32" />
        </div>

        <div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
          <div>
            <Skeleton className="h-9 w-[450px] mb-2" />
            <Skeleton className="h-5 w-[300px]" />
          </div>
          <div className="flex items-center gap-2">
            <Button variant="outline" disabled>
              Preview
            </Button>
          </div>
        </div>
      </div>

      {/* Tabs */}
      <div className="w-full p-0 border-b rounded-none mb-6 relative">
        <div className="absolute bottom-0 left-0 right-0 h-0.5 bg-muted"></div>
        <div className="flex">
          <div className="py-3 px-6 relative">
            <Skeleton className="h-5 w-32" />
            <div className="absolute bottom-0 left-0 right-0 h-0.5 bg-primary"></div>
          </div>
          <div className="py-3 px-6">
            <Skeleton className="h-5 w-36" />
          </div>
          <div className="py-3 px-6">
            <Skeleton className="h-5 w-36" />
          </div>
        </div>
      </div>

      {/* Content Cards */}
      <div className="grid gap-6 mt-6">
        {[1, 2, 3].map((i) => (
          <div key={i} className="border rounded-lg p-6 space-y-6">
            <div className="flex justify-between items-center">
              <Skeleton className="h-6 w-32" />
            </div>
            <div className="space-y-6">
              <div className="space-y-3">
                <Skeleton className="h-4 w-16" />
                <Skeleton className="h-10 w-full" />
              </div>
              <div className="space-y-3">
                <Skeleton className="h-4 w-16" />
                <Skeleton className="h-10 w-full" />
              </div>
            </div>
            <div className="pt-2">
              <Skeleton className="h-10 w-40" />
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

Form and Common Input Components

components/ui/button.tsx

import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { LoaderCircle, LucideIcon } from "lucide-react";

import { cn } from "@/lib/utils";
import Link from "next/link";

const buttonVariants = cva(
  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 outline-none cursor-pointer",
  {
    variants: {
      variant: {
        default:
          "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
        destructive:
          "bg-destructive text-white shadow-xs hover:bg-destructive/90",
        outline:
          "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
        secondary:
          "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
        ghost: "hover:bg-accent hover:text-accent-foreground",
        link: "text-primary underline-offset-4 hover:underline",
      },
      size: {
        default: "h-11 px-4 py-2 has-[>svg]:px-3",
        sm: "h-8 rounded-md px-3 has-[>svg]:px-2.5",
        lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
        icon: "size-9",
      },
      focus: {
        default: "focus:border-primary focus:border-2",
        ring: "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
        none: "",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
      focus: "default",
    },
  }
);

interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean;
  icon?: LucideIcon;
  iconPosition?: "left" | "right";
  isLoading?: boolean;
  loadingText?: string;
  href?: string;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  (
    {
      className,
      variant,
      size,
      focus,
      asChild = false,
      icon: Icon,
      iconPosition = "left",
      isLoading = false,
      loadingText,
      href,
      children,
      ...props
    },
    ref
  ) => {
    // If href is provided, render as Link
    if (href) {
      // Extract only the properties that are valid for Link component
      const { rel, title, id, role, tabIndex, "aria-label": ariaLabel } = props;

      return (
        <Link
          href={href}
          className={cn(buttonVariants({ variant, size, focus, className }))}
          rel={rel}
          title={title}
          id={id}
          role={role}
          tabIndex={tabIndex}
          aria-label={ariaLabel}
        >
          {isLoading ? (
            <>
              <LoaderCircle className="size-4 animate-spin" />
              {loadingText || children}
            </>
          ) : (
            <>
              {Icon && iconPosition === "left" && <Icon className="size-4" />}
              {children}
              {Icon && iconPosition === "right" && <Icon className="size-4" />}
            </>
          )}
        </Link>
      );
    }

    // Otherwise render as button or slot
    const Comp = asChild ? Slot : "button";

    return (
      <Comp
        data-slot="button"
        className={cn(buttonVariants({ variant, size, focus, className }))}
        disabled={isLoading || props.disabled}
        ref={ref}
        {...props}
      >
        {isLoading ? (
          <>
            <LoaderCircle className="size-4 animate-spin" />
            {loadingText || children}
          </>
        ) : (
          <>
            {Icon && iconPosition === "left" && <Icon className="size-4" />}
            {children}
            {Icon && iconPosition === "right" && <Icon className="size-4" />}
          </>
        )}
      </Comp>
    );
  }
);

Button.displayName = "Button";

export { Button, buttonVariants, type ButtonProps };

components/ui/input.tsx

import * as React from "react";
import { cn } from "@/lib/utils";
import { LucideIcon } from "lucide-react";

interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  icon?: LucideIcon;
  iconPosition?: "left" | "right";
  iconClassName?: string;
}

const Input = React.forwardRef<HTMLInputElement, InputProps>(
  (
    {
      className,
      type,
      icon: Icon,
      iconPosition = "left",
      iconClassName,
      ...props
    },
    ref
  ) => {
    return (
      <div className="relative flex items-center w-full">
        {Icon && iconPosition === "left" && (
          <div className="absolute left-3 flex items-center pointer-events-none">
            <Icon
              className={cn("h-5 w-5 text-muted-foreground", iconClassName)}
            />
          </div>
        )}
        <input
          type={type}
          data-slot="input"
          className={cn(
            "border-input placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground flex h-10 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-colors outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
            "focus:border-primary focus:border-2",
            "aria-invalid:border-destructive",
            Icon && iconPosition === "left" && "pl-10",
            Icon && iconPosition === "right" && "pr-10",
            className
          )}
          ref={ref}
          {...props}
        />
        {Icon && iconPosition === "right" && (
          <div className="absolute right-3 flex items-center pointer-events-none">
            <Icon
              className={cn("h-5 w-5 text-muted-foreground", iconClassName)}
            />
          </div>
        )}
      </div>
    );
  }
);

Input.displayName = "Input";

export { Input };

components/ui/editor

// components/Editor/index.tsx
"use client";
import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Link from "@tiptap/extension-link";
import { Button } from "../ui/button";
import {
  Bold,
  Italic,
  List,
  ListOrdered,
  Quote,
  Redo,
  Strikethrough,
  Undo,
} from "lucide-react";

interface EditorProps {
  value: string;
  onChange: (value: string) => void;
}

const Editor = ({ value, onChange }: EditorProps) => {
  const editor = useEditor({
    extensions: [
      StarterKit,
      Link.configure({
        openOnClick: false,
      }),
    ],
    content: value,
    editorProps: {
      attributes: {
        class:
          "prose prose-sm sm:prose-base dark:prose-invert max-w-none focus:outline-none min-h-[150px] px-3 py-2 border rounded-md",
      },
    },
    onUpdate: ({ editor }) => {
      onChange(editor.getHTML());
    },
  });

  if (!editor) {
    return null;
  }

  return (
    <div className="border rounded-lg">
      <div className="flex flex-wrap gap-2 p-2 border-b bg-muted">
        <Button
          variant="ghost"
          size="sm"
          type="button"
          onClick={() => editor.chain().focus().toggleBold().run()}
          className={editor.isActive("bold") ? "bg-muted-foreground/20" : ""}
        >
          <Bold className="w-4 h-4" />
        </Button>
        <Button
          variant="ghost"
          type="button"
          size="sm"
          onClick={() => editor.chain().focus().toggleItalic().run()}
          className={editor.isActive("italic") ? "bg-muted-foreground/20" : ""}
        >
          <Italic className="w-4 h-4" />
        </Button>
        <Button
          variant="ghost"
          type="button"
          size="sm"
          onClick={() => editor.chain().focus().toggleStrike().run()}
          className={editor.isActive("strike") ? "bg-muted-foreground/20" : ""}
        >
          <Strikethrough className="w-4 h-4" />
        </Button>
        <div className="w-px h-4 bg-border my-auto" />
        <Button
          variant="ghost"
          type="button"
          size="sm"
          onClick={() => editor.chain().focus().toggleBulletList().run()}
          className={
            editor.isActive("bulletList") ? "bg-muted-foreground/20" : ""
          }
        >
          <List className="w-4 h-4" />
        </Button>
        <Button
          variant="ghost"
          type="button"
          size="sm"
          onClick={() => editor.chain().focus().toggleOrderedList().run()}
          className={
            editor.isActive("orderedList") ? "bg-muted-foreground/20" : ""
          }
        >
          <ListOrdered className="w-4 h-4" />
        </Button>
        <Button
          variant="ghost"
          type="button"
          size="sm"
          onClick={() => editor.chain().focus().toggleBlockquote().run()}
          className={
            editor.isActive("blockquote") ? "bg-muted-foreground/20" : ""
          }
        >
          <Quote className="w-4 h-4" />
        </Button>
        <div className="w-px h-4 bg-border my-auto" />
        <Button
          type="button"
          variant="ghost"
          size="sm"
          onClick={() => editor.chain().focus().undo().run()}
        >
          <Undo className="w-4 h-4" />
        </Button>
        <Button
          variant="ghost"
          size="sm"
          type="button"
          onClick={() => editor.chain().focus().redo().run()}
        >
          <Redo className="w-4 h-4" />
        </Button>
      </div>
      <EditorContent editor={editor} />
    </div>
  );
};

export default Editor;

Editor Usage

function NotesCard({ item, courseId }: { item: Lesson; courseId: string }) {
  const [notes, setNotes] = useState(item.notes || "");

  const [isUpdating, setIsUpdating] = useState(false);

  const handleUpdate = async () => {
    setIsUpdating(true);

    try {
      const data = {
        notes: notes,
      };
      // console.log(data);
      await updateLessonById(item.id, data, courseId);
      toast.success("Lesson Notes updated successfully");
    } catch (error) {
      toast.error("Failed to update Notes");
      console.error(error);
    } finally {
      setIsUpdating(false);
    }
  };

  return (
    <Card>
      <CardHeader>
        <CardTitle>Lesson Notes</CardTitle>
      </CardHeader>
      <CardContent className="grid gap-6">
        <div className="grid gap-3">
          <Editor value={notes} onChange={(value) => setNotes(value)} />
          <p className="text-xs text-muted-foreground">Enter Lesson Notes</p>
        </div>
      </CardContent>
      <CardFooter>
        <Button onClick={handleUpdate} disabled={isUpdating}>
          {isUpdating ? "Updating..." : "Update Lesson Notes "}
        </Button>
      </CardFooter>
    </Card>
  );
}

Data Table Components

Core Data Table Structure

The data table system consists of 9 reusable components that work together to provide a complete table solution.

components/ui/data-table/index.ts

import DataTable, { Column } from "./data-table";
import FilterBar from "./filter-bar";
import TableActions from "./table-actions";
import EntityForm from "./entity-form";
import ConfirmationDialog from "./confirmation-dialog";
import TableLoading from "./table-loading";

export {
  DataTable,
  FilterBar,
  TableActions,
  EntityForm,
  ConfirmationDialog,
  TableLoading,
};

export type { Column };

components/ui/data-table/data-table.tsx

// components/ui/data-table/data-table.tsx
"use client";

import { useState, useEffect, ReactNode } from "react";
import clsx from "clsx";
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table";
import {
  Pagination,
  PaginationContent,
  PaginationEllipsis,
  PaginationItem,
  PaginationLink,
  PaginationNext,
  PaginationPrevious,
} from "@/components/ui/pagination";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { RefreshCw } from "lucide-react";
import RowsPerPage from "@/components/ui/data-table/rows-per-page";
import FilterBar from "./filter-bar";
import TableActions from "./table-actions";
import {
  DateRange,
  DateFilterOption,
} from "@/components/ui/data-table/date-filter";

export interface Column<T> {
  header: string;
  accessorKey: keyof T | ((row: T) => any);
  cell?: (row: T) => ReactNode;
}

interface DataTableProps<T> {
  title: string;
  subtitle?: string;
  data: T[];
  columns: Column<T>[];
  keyField: keyof T;
  isLoading?: boolean;
  onRefresh?: () => void;
  actions?: {
    onAdd?: () => void;
    onEdit?: (item: T) => void;
    onDelete?: (item: T) => void;
    onExport?: (filteredData: T[]) => void;
  };
  filters?: {
    searchFields?: (keyof T)[];
    enableDateFilter?: boolean;
    getItemDate?: (item: T) => Date | string;
    additionalFilters?: ReactNode;
  };
  renderRowActions?: (item: T) => ReactNode;
  emptyState?: ReactNode;
}

export default function DataTable<T>({
  title,
  subtitle,
  data,
  columns,
  keyField,
  isLoading = false,
  onRefresh,
  actions,
  filters,
  renderRowActions,
  emptyState,
}: DataTableProps<T>) {
  // Pagination state
  const [currentPage, setCurrentPage] = useState(1);
  const [itemsPerPage, setItemsPerPage] = useState(10);

  // Filter state
  const [searchQuery, setSearchQuery] = useState("");
  const [dateFilter, setDateFilter] = useState<{
    range: DateRange | null;
    option: DateFilterOption;
  }>({
    range: null,
    option: "lifetime",
  });

  // Reset page when filters change
  useEffect(() => {
    setCurrentPage(1);
  }, [searchQuery, dateFilter, itemsPerPage]);

  // Apply search filter
  const applySearchFilter = (items: T[]): T[] => {
    if (!searchQuery.trim() || !filters?.searchFields?.length) return items;

    const query = searchQuery.toLowerCase();
    return items.filter((item) => {
      return filters.searchFields!.some((field) => {
        const value = item[field];
        if (value === null || value === undefined) return false;
        return String(value).toLowerCase().includes(query);
      });
    });
  };

  // Apply date filter
  const applyDateFilter = (items: T[]): T[] => {
    if (
      !dateFilter.range?.from ||
      !dateFilter.range?.to ||
      !filters?.getItemDate
    ) {
      return items;
    }

    const from = new Date(dateFilter.range.from);
    const to = new Date(dateFilter.range.to);

    return items.filter((item) => {
      const itemDate =
        filters && filters.getItemDate
          ? new Date(filters.getItemDate(item))
          : new Date();
      return itemDate >= from && itemDate <= to;
    });
  };

  // Apply all filters
  const filteredData = applyDateFilter(applySearchFilter(data));

  // Calculate pagination
  const totalPages = Math.ceil(filteredData.length / itemsPerPage);
  const indexOfLastItem = currentPage * itemsPerPage;
  const indexOfFirstItem = indexOfLastItem - itemsPerPage;
  const currentItems = filteredData.slice(indexOfFirstItem, indexOfLastItem);

  // Handle page change
  const handlePageChange = (pageNumber: number) => {
    setCurrentPage(pageNumber);
  };

  // Generate page numbers for pagination
  const getPageNumbers = () => {
    const pageNumbers = [];

    if (totalPages <= 10) {
      // Show all pages if 10 or fewer
      for (let i = 1; i <= totalPages; i++) {
        pageNumbers.push(i);
      }
    } else {
      // Show first page, current page and neighbors, and last page
      if (currentPage <= 3) {
        // Near the beginning
        for (let i = 1; i <= 4; i++) {
          pageNumbers.push(i);
        }
        pageNumbers.push("ellipsis");
        pageNumbers.push(totalPages);
      } else if (currentPage >= totalPages - 2) {
        // Near the end
        pageNumbers.push(1);
        pageNumbers.push("ellipsis");
        for (let i = totalPages - 3; i <= totalPages; i++) {
          pageNumbers.push(i);
        }
      } else {
        // Middle
        pageNumbers.push(1);
        pageNumbers.push("ellipsis");
        pageNumbers.push(currentPage - 1);
        pageNumbers.push(currentPage);
        pageNumbers.push(currentPage + 1);
        pageNumbers.push("ellipsis");
        pageNumbers.push(totalPages);
      }
    }

    return pageNumbers;
  };

  // Get value from accessorKey (which could be a string or function)
  const getCellValue = (item: T, accessor: keyof T | ((row: T) => any)) => {
    if (typeof accessor === "function") {
      return accessor(item);
    }
    return item[accessor];
  };

  return (
    <Card className="w-full">
      <CardHeader className="flex flex-row items-center justify-between">
        <div>
          <CardTitle className="text-2xl">{title}</CardTitle>
          {subtitle && <p className="text-muted-foreground mt-1">{subtitle}</p>}
        </div>

        <div className="flex items-center gap-2">
          {onRefresh && (
            <Button
              variant="outline"
              size="icon"
              onClick={onRefresh}
              disabled={isLoading}
              title="Refresh data"
            >
              <RefreshCw
                className={clsx("h-4 w-4", isLoading && "animate-spin")}
              />
            </Button>
          )}
          {actions?.onAdd && <TableActions.AddButton onClick={actions.onAdd} />}
        </div>
      </CardHeader>

      <CardContent>
        {/* Filter bar */}
        {filters && (
          <FilterBar
            searchQuery={searchQuery}
            onSearchChange={setSearchQuery}
            showDateFilter={filters.enableDateFilter}
            dateFilter={dateFilter}
            onDateFilterChange={(range, option) =>
              setDateFilter({ range, option })
            }
            additionalFilters={filters.additionalFilters}
            onExport={
              actions?.onExport
                ? () =>
                    actions &&
                    actions.onExport &&
                    actions.onExport(filteredData)
                : undefined
            }
          />
        )}

        {/* Table */}
        <Table>
          <TableHeader>
            <TableRow>
              {columns.map((column, index) => (
                <TableHead key={index}>{column.header}</TableHead>
              ))}
              {renderRowActions && (
                <TableHead className="text-right">Actions</TableHead>
              )}
            </TableRow>
          </TableHeader>
          <TableBody>
            {currentItems.length > 0 ? (
              currentItems.map((item) => (
                <TableRow key={String(item[keyField])}>
                  {columns.map((column, index) => (
                    <TableCell key={index}>
                      {column.cell
                        ? column.cell(item)
                        : getCellValue(item, column.accessorKey)}
                    </TableCell>
                  ))}
                  {renderRowActions && (
                    <TableCell className="text-right">
                      {renderRowActions(item)}
                    </TableCell>
                  )}
                </TableRow>
              ))
            ) : (
              <TableRow>
                <TableCell
                  colSpan={columns.length + (renderRowActions ? 1 : 0)}
                  className="text-center py-6"
                >
                  {emptyState ||
                    (searchQuery || dateFilter.option !== "lifetime"
                      ? "No matching items found for the selected filters"
                      : "No items found")}
                </TableCell>
              </TableRow>
            )}
          </TableBody>
        </Table>

        {/* Pagination */}
        {filteredData.length > 0 && (
          <div className="mt-4 flex flex-col sm:flex-row justify-between items-center">
            <div className="mb-2 sm:mb-0">
              <RowsPerPage
                value={itemsPerPage}
                onChange={setItemsPerPage}
                options={[10, 25, 50, 100]}
              />
            </div>
            <div className="text-sm text-muted-foreground">
              Showing {indexOfFirstItem + 1}-
              {Math.min(indexOfLastItem, filteredData.length)} of{" "}
              {filteredData.length}
            </div>
            {totalPages > 1 && (
              <Pagination>
                <PaginationContent>
                  <PaginationItem>
                    <PaginationPrevious
                      onClick={() =>
                        handlePageChange(Math.max(1, currentPage - 1))
                      }
                      className={clsx(
                        currentPage === 1
                          ? "pointer-events-none opacity-50"
                          : "cursor-pointer"
                      )}
                    />
                  </PaginationItem>

                  {getPageNumbers().map((page, index) =>
                    page === "ellipsis" ? (
                      <PaginationItem key={`ellipsis-${index}`}>
                        <PaginationEllipsis />
                      </PaginationItem>
                    ) : (
                      <PaginationItem key={`page-${page}`}>
                        <PaginationLink
                          onClick={() => handlePageChange(page as number)}
                          className={clsx(
                            currentPage === page
                              ? "bg-primary text-primary-foreground"
                              : "cursor-pointer"
                          )}
                        >
                          {page}
                        </PaginationLink>
                      </PaginationItem>
                    )
                  )}

                  <PaginationItem>
                    <PaginationNext
                      onClick={() =>
                        handlePageChange(Math.min(totalPages, currentPage + 1))
                      }
                      className={clsx(
                        currentPage === totalPages
                          ? "pointer-events-none opacity-50"
                          : "cursor-pointer"
                      )}
                    />
                  </PaginationItem>
                </PaginationContent>
              </Pagination>
            )}
          </div>
        )}
      </CardContent>
    </Card>
  );
}

components/ui/data-table/table-actions.tsx

import {
  Plus,
  Edit,
  Trash2,
  FileSpreadsheet,
  Loader2,
  Eye,
} from "lucide-react";
import { Button } from "@/components/ui/button";

interface ActionButtonProps {
  onClick: () => void;
  disabled?: boolean;
  loading?: boolean;
  className?: string;
}

const AddButton = ({
  onClick,
  disabled = false,
  loading = false,
}: ActionButtonProps) => (
  <Button onClick={onClick} disabled={disabled || loading}>
    {loading ? (
      <Loader2 className="mr-2 h-4 w-4 animate-spin" />
    ) : (
      <Plus className="mr-2 h-4 w-4" />
    )}
    Add New
  </Button>
);

const EditButton = ({
  onClick,
  disabled = false,
  loading = false,
}: ActionButtonProps) => (
  <Button
    variant="outline"
    size="icon"
    onClick={onClick}
    disabled={disabled || loading}
    title="Edit"
  >
    {loading ? (
      <Loader2 className="h-4 w-4 animate-spin" />
    ) : (
      <Edit className="h-4 w-4" />
    )}
  </Button>
);

const DeleteButton = ({
  onClick,
  disabled = false,
  loading = false,
}: ActionButtonProps) => (
  <Button
    variant="outline"
    size="icon"
    onClick={onClick}
    disabled={disabled || loading}
    title="Delete"
    className="text-destructive"
  >
    <Trash2 className="h-4 w-4" />
  </Button>
);

const ExportButton = ({
  onClick,
  disabled = false,
  loading = false,
}: ActionButtonProps) => (
  <Button variant="outline" onClick={onClick} disabled={disabled || loading}>
    {loading ? (
      <>
        <Loader2 className="mr-2 h-4 w-4 animate-spin" />
        Exporting...
      </>
    ) : (
      <>
        <FileSpreadsheet className="mr-2 h-4 w-4" />
        Export
      </>
    )}
  </Button>
);

const RowActions = ({
  onEdit,
  onDelete,
  onView,
  isDeleting = false,
}: {
  onEdit?: () => void;
  onDelete?: () => void;
  onView?: () => void;
  isDeleting?: boolean;
}) => (
  <div className="flex justify-end gap-2">
    {onEdit && <EditButton onClick={onEdit} />}
    {onView && (
      <Button variant="outline" size="icon" onClick={onView}>
        <Eye className="h-4 w-4" />
      </Button>
    )}
    {onDelete && <DeleteButton onClick={onDelete} loading={isDeleting} />}
  </div>
);

const TableActions = {
  AddButton,
  EditButton,
  DeleteButton,
  ExportButton,
  RowActions,
};

export default TableActions;

components/ui/data-table/entity-form.tsx

"use client";

import { ReactNode } from "react";
import { UseFormReturn, FieldValues } from "react-hook-form";
import { Loader2 } from "lucide-react";
import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogFooter,
  DialogClose,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Form } from "@/components/ui/form";

interface EntityFormProps<TFormValues extends FieldValues> {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  title: string;
  form: UseFormReturn<TFormValues>;
  onSubmit: (values: TFormValues) => void | Promise<void>;
  children: ReactNode;
  isSubmitting?: boolean;
  submitLabel?: string;
  cancelLabel?: string;
  size?: "sm" | "md" | "lg" | "xl";
}

export default function EntityForm<TFormValues extends FieldValues>({
  open,
  onOpenChange,
  title,
  form,
  onSubmit,
  children,
  isSubmitting = false,
  submitLabel = "Save",
  cancelLabel = "Cancel",
  size = "sm",
}: EntityFormProps<TFormValues>) {
  const sizeClasses = {
    sm: "sm:max-w-[425px]",
    md: "sm:max-w-[550px]",
    lg: "sm:max-w-[650px]",
    xl: "sm:max-w-[800px]",
  };

  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent className={sizeClasses[size]}>
        <DialogHeader>
          <DialogTitle>{title}</DialogTitle>
        </DialogHeader>
        <Form {...form}>
          <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
            {children}
            <DialogFooter className="mt-6">
              <DialogClose asChild>
                <Button type="button" variant="outline" disabled={isSubmitting}>
                  {cancelLabel}
                </Button>
              </DialogClose>
              <Button type="submit" disabled={isSubmitting}>
                {isSubmitting ? (
                  <>
                    <Loader2 className="mr-2 h-4 w-4 animate-spin" />
                    Saving...
                  </>
                ) : (
                  submitLabel
                )}
              </Button>
            </DialogFooter>
          </form>
        </Form>
      </DialogContent>
    </Dialog>
  );
}

Additional Data Table Components

components/ui/data-table/filter-bar.tsx

import { ReactNode } from "react";
import { Search, X } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import DateFilter, { DateRange, DateFilterOption } from "./date-filter";
import TableActions from "./table-actions";

interface FilterBarProps {
  searchQuery: string;
  onSearchChange: (query: string) => void;
  showDateFilter?: boolean;
  dateFilter?: {
    range: DateRange | null;
    option: DateFilterOption;
  };
  onDateFilterChange?: (
    range: DateRange | null,
    option: DateFilterOption
  ) => void;
  additionalFilters?: ReactNode;
  onExport?: () => void;
}

export default function FilterBar({
  searchQuery,
  onSearchChange,
  showDateFilter = false,
  dateFilter,
  onDateFilterChange,
  additionalFilters,
  onExport,
}: FilterBarProps) {
  return (
    <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-4">
      <div className="flex flex-col sm:flex-row items-start sm:items-center gap-2 w-full">
        {/* Search Input */}
        <div className="relative w-full sm:max-w-sm">
          <Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
          <Input
            placeholder="Search..."
            value={searchQuery}
            onChange={(e) => onSearchChange(e.target.value)}
            className="pl-8 w-full"
          />
          {searchQuery && (
            <Button
              variant="ghost"
              size="icon"
              className="absolute right-1 top-1.5 h-6 w-6"
              onClick={() => onSearchChange("")}
            >
              <X className="h-4 w-4" />
            </Button>
          )}
        </div>

        {/* Date Filter */}
        {showDateFilter && dateFilter && onDateFilterChange && (
          <DateFilter onFilterChange={onDateFilterChange} />
        )}

        {/* Additional Custom Filters */}
        {additionalFilters}
      </div>

      {/* Export Button */}
      {onExport && (
        <div className="w-full sm:w-auto">
          <TableActions.ExportButton onClick={onExport} />
        </div>
      )}
    </div>
  );
}

components/ui/data-table/date-filter.tsx

"use client";

import { useState } from "react";
import {
  format,
  startOfDay,
  endOfDay,
  subDays,
  startOfMonth,
  endOfMonth,
  startOfYear,
  endOfYear,
} from "date-fns";
import { Calendar as CalendarIcon, ChevronDown } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Calendar } from "@/components/ui/calendar";
import { cn } from "@/lib/utils";
import { DateRange as CalendarDateRange } from "react-day-picker";

export type DateRange = {
  from: Date | undefined;
  to: Date | undefined;
};

export type DateFilterOption =
  | "lifetime"
  | "today"
  | "last7days"
  | "thisMonth"
  | "thisYear"
  | "custom";

interface DateFilterProps {
  onFilterChange: (range: DateRange | null, option: DateFilterOption) => void;
}

export default function DateFilter({ onFilterChange }: DateFilterProps) {
  const [isCalendarOpen, setIsCalendarOpen] = useState(false);
  const [dateRange, setDateRange] = useState<CalendarDateRange | undefined>(
    undefined
  );
  const [selectedOption, setSelectedOption] =
    useState<DateFilterOption>("lifetime");

  const handleOptionSelect = (option: DateFilterOption) => {
    setSelectedOption(option);
    let newRange: DateRange | null = null;

    const today = new Date();

    switch (option) {
      case "lifetime":
        newRange = null;
        break;
      case "today":
        newRange = { from: startOfDay(today), to: endOfDay(today) };
        break;
      case "last7days":
        newRange = { from: startOfDay(subDays(today, 6)), to: endOfDay(today) };
        break;
      case "thisMonth":
        newRange = { from: startOfMonth(today), to: endOfMonth(today) };
        break;
      case "thisYear":
        newRange = { from: startOfYear(today), to: endOfYear(today) };
        break;
      case "custom":
        setIsCalendarOpen(true);
        return;
    }

    onFilterChange(newRange, option);
  };

  const getFilterLabel = () => {
    switch (selectedOption) {
      case "lifetime":
        return "All Time";
      case "today":
        return "Today";
      case "last7days":
        return "Last 7 Days";
      case "thisMonth":
        return "This Month";
      case "thisYear":
        return "This Year";
      case "custom":
        if (dateRange?.from && dateRange?.to) {
          return `${format(dateRange.from, "MMM dd, yyyy")} - ${format(
            dateRange.to,
            "MMM dd, yyyy"
          )}`;
        }
        return "Custom Range";
      default:
        return "All Time";
    }
  };

  return (
    <div className="flex items-center space-x-2">
      <DropdownMenu>
        <DropdownMenuTrigger asChild>
          <Button variant="outline" className="min-w-[180px] justify-between">
            <span>{getFilterLabel()}</span>
            <ChevronDown className="ml-2 h-4 w-4 opacity-50" />
          </Button>
        </DropdownMenuTrigger>
        <DropdownMenuContent align="end" className="w-[200px]">
          <DropdownMenuItem onSelect={() => handleOptionSelect("lifetime")}>
            All Time
          </DropdownMenuItem>
          <DropdownMenuItem onSelect={() => handleOptionSelect("today")}>
            Today
          </DropdownMenuItem>
          <DropdownMenuItem onSelect={() => handleOptionSelect("last7days")}>
            Last 7 Days
          </DropdownMenuItem>
          <DropdownMenuItem onSelect={() => handleOptionSelect("thisMonth")}>
            This Month
          </DropdownMenuItem>
          <DropdownMenuItem onSelect={() => handleOptionSelect("thisYear")}>
            This Year
          </DropdownMenuItem>
          <DropdownMenuSeparator />
          <DropdownMenuItem onSelect={() => handleOptionSelect("custom")}>
            Custom Range
          </DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>

      {selectedOption === "custom" && (
        <Popover open={isCalendarOpen} onOpenChange={setIsCalendarOpen}>
          <PopoverTrigger asChild>
            <Button
              variant="outline"
              className={cn(
                "justify-start text-left font-normal",
                !dateRange?.from && !dateRange?.to && "text-muted-foreground"
              )}
            >
              <CalendarIcon className="mr-2 h-4 w-4" />
              {dateRange?.from && dateRange?.to
                ? `${format(dateRange.from, "MMM dd, yyyy")} - ${format(
                    dateRange.to,
                    "MMM dd, yyyy"
                  )}`
                : "Select date range"}
            </Button>
          </PopoverTrigger>
          <PopoverContent className="w-auto p-0" align="end">
            <Calendar
              initialFocus
              mode="range"
              defaultMonth={dateRange?.from}
              selected={dateRange}
              onSelect={(range) => {
                setDateRange(range);
                if (range?.from && range?.to) {
                  setTimeout(() => {
                    onFilterChange(
                      {
                        from: startOfDay(range.from!),
                        to: endOfDay(range.to!),
                      },
                      "custom"
                    );
                  }, 0);
                }
              }}
              numberOfMonths={2}
            />
          </PopoverContent>
        </Popover>
      )}
    </div>
  );
}

components/ui/data-table/confirmation-dialog.tsx

import { ReactNode } from "react";
import { Loader2, AlertTriangle } from "lucide-react";
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { cn } from "@/lib/utils";

interface ConfirmationDialogProps {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  title: string;
  description?: string | ReactNode;
  onConfirm: () => void;
  isConfirming?: boolean;
  confirmLabel?: string;
  cancelLabel?: string;
  variant?: "default" | "destructive";
}

export default function ConfirmationDialog({
  open,
  onOpenChange,
  title,
  description,
  onConfirm,
  isConfirming = false,
  confirmLabel = "Confirm",
  cancelLabel = "Cancel",
  variant = "default",
}: ConfirmationDialogProps) {
  return (
    <AlertDialog open={open} onOpenChange={onOpenChange}>
      <AlertDialogContent>
        <AlertDialogHeader>
          <div className="flex items-center gap-2">
            {variant === "destructive" && (
              <AlertTriangle className="h-5 w-5 text-destructive" />
            )}
            <AlertDialogTitle>{title}</AlertDialogTitle>
          </div>
          {description && (
            <AlertDialogDescription>{description}</AlertDialogDescription>
          )}
        </AlertDialogHeader>
        <AlertDialogFooter>
          <AlertDialogCancel disabled={isConfirming}>
            {cancelLabel}
          </AlertDialogCancel>
          <AlertDialogAction
            disabled={isConfirming}
            onClick={(e) => {
              e.preventDefault();
              onConfirm();
            }}
            className={cn(
              variant === "destructive" &&
                "bg-destructive hover:bg-destructive/90"
            )}
          >
            {isConfirming ? (
              <>
                <Loader2 className="mr-2 h-4 w-4 animate-spin" />
                Processing...
              </>
            ) : (
              confirmLabel
            )}
          </AlertDialogAction>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  );
}

components/ui/data-table/table-loading.tsx

import { Skeleton } from "@/components/ui/skeleton";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import {
  Table,
  TableHeader,
  TableRow,
  TableHead,
  TableBody,
  TableCell,
} from "@/components/ui/table";

interface TableLoadingProps {
  title?: string;
  columnCount?: number;
  rowCount?: number;
}

export default function TableLoading({
  title = "Loading data",
  columnCount = 6,
  rowCount = 5,
}: TableLoadingProps) {
  return (
    <Card className="w-full">
      <CardHeader className="flex flex-row items-center justify-between pb-2">
        <div>
          <CardTitle className="text-2xl font-bold">
            <Skeleton className="h-8 w-64" />
          </CardTitle>
          <Skeleton className="h-5 w-80 mt-2" />
        </div>
        <Skeleton className="h-10 w-32" />
      </CardHeader>

      <CardContent className="px-0">
        <div className="flex items-center justify-between px-6 mb-4">
          <Skeleton className="h-10 w-64" />
          <Skeleton className="h-10 w-40" />
        </div>

        <Table>
          <TableHeader>
            <TableRow>
              {Array(columnCount)
                .fill(0)
                .map((_, i) => (
                  <TableHead key={i}>
                    <Skeleton className="h-4 w-full max-w-24" />
                  </TableHead>
                ))}
            </TableRow>
          </TableHeader>
          <TableBody>
            {Array(rowCount)
              .fill(0)
              .map((_, i) => (
                <TableRow key={i}>
                  {Array(columnCount)
                    .fill(0)
                    .map((_, j) => (
                      <TableCell key={j}>
                        <Skeleton className="h-5 w-full" />
                      </TableCell>
                    ))}
                </TableRow>
              ))}
          </TableBody>
        </Table>
      </CardContent>
    </Card>
  );
}

components/ui/data-table/rows-per-page.tsx

"use client";

import React from "react";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";

interface RowsPerPageProps {
  value: number;
  onChange: (value: number) => void;
  options?: number[];
  className?: string;
}

export default function RowsPerPage({
  value,
  onChange,
  options = [5, 10, 25, 50, 100],
  className = "",
}: RowsPerPageProps) {
  return (
    <div className={`flex items-center space-x-2 ${className}`}>
      <span className="text-sm text-muted-foreground whitespace-nowrap">
        Rows per page:
      </span>
      <Select
        value={value.toString()}
        onValueChange={(val) => onChange(parseInt(val, 10))}
      >
        <SelectTrigger className="h-8 w-[70px]">
          <SelectValue placeholder={value.toString()} />
        </SelectTrigger>
        <SelectContent>
          {options.map((option) => (
            <SelectItem key={option} value={option.toString()}>
              {option}
            </SelectItem>
          ))}
        </SelectContent>
      </Select>
    </div>
  );
}

React Query Provider Setup

app/providers.tsx

"use client";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { useState } from "react";
import { Toaster } from "sonner";

export default function Providers({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(
    () =>
      new QueryClient({
        defaultOptions: {
          queries: {
            // SOAPI: Intelligent caching strategy
            staleTime: 60 * 1000, // 1 minute
            gcTime: 10 * 60 * 1000, // 10 minutes (previously cacheTime)
            retry: (failureCount, error: any) => {
              // Don't retry on 4xx errors
              if (
                error?.response?.status >= 400 &&
                error?.response?.status < 500
              ) {
                return false;
              }
              return failureCount < 3;
            },
          },
          mutations: {
            retry: 1,
          },
        },
      })
  );

  return (
    <QueryClientProvider client={queryClient}>
      {children}
      <Toaster position="top-right" />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

app/layout.tsx

import Providers from "./providers";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

Advanced Features

Error Handling Service

services/error-handler.ts

import { toast } from "sonner";
import axios from "axios";

export interface ApiError {
  message: string;
  status?: number;
  code?: string;
}

export class ErrorHandler {
  static handle(error: unknown): ApiError {
    if (axios.isAxiosError(error)) {
      const status = error.response?.status;
      const message = error.response?.data?.message || error.message;

      // SOAPI: Intelligent error responses
      switch (status) {
        case 400:
          toast.error("Invalid request", { description: message });
          break;
        case 401:
          toast.error("Authentication required");
          // Redirect to login or refresh token
          break;
        case 403:
          toast.error("Permission denied", { description: message });
          break;
        case 404:
          toast.error("Resource not found");
          break;
        case 409:
          toast.error("Conflict", { description: message });
          break;
        case 422:
          toast.error("Validation failed", { description: message });
          break;
        case 500:
          toast.error("Server error", {
            description: "Please try again later",
          });
          break;
        default:
          toast.error("Request failed", { description: message });
      }

      return {
        message,
        status,
        code: error.response?.data?.code,
      };
    }

    if (error instanceof Error) {
      toast.error("Error", { description: error.message });
      return { message: error.message };
    }

    toast.error("Unknown error occurred");
    return { message: "An unexpected error occurred" };
  }
}

Optimistic Updates Hook

hooks/useOptimisticUpdate.ts

import { useQueryClient } from "@tanstack/react-query";
import { useCallback } from "react";

export function useOptimisticUpdate() {
  const queryClient = useQueryClient();

  const updateOptimistically = useCallback(
    <T>(queryKey: any[], updater: (old: T) => T) => {
      // SOAPI: Intelligent responses with optimistic updates
      queryClient.setQueryData(queryKey, updater);
    },
    [queryClient]
  );

  const rollback = useCallback(
    (queryKey: any[], previousData: any) => {
      queryClient.setQueryData(queryKey, previousData);
    },
    [queryClient]
  );

  return { updateOptimistically, rollback };
}

Bulk Operations Hook

hooks/useBulkOperations.ts

import { useMutation, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";

interface BulkOperationResult {
  successful: string[];
  failed: { id: string; error: string }[];
}

export function useBulkDelete<T extends { id: string }>(
  deleteFunction: (id: string) => Promise<any>,
  queryKeys: any[]
) {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (items: T[]): Promise<BulkOperationResult> => {
      const results: BulkOperationResult = {
        successful: [],
        failed: [],
      };

      // SOAPI: Protected Deletes - Process each item safely
      for (const item of items) {
        try {
          await deleteFunction(item.id);
          results.successful.push(item.id);
        } catch (error) {
          results.failed.push({
            id: item.id,
            error: error instanceof Error ? error.message : "Unknown error",
          });
        }
      }

      return results;
    },
    onSuccess: (results) => {
      const { successful, failed } = results;

      if (successful.length > 0) {
        toast.success(`Deleted ${successful.length} items successfully`);
        queryClient.invalidateQueries({ queryKey: queryKeys });
      }

      if (failed.length > 0) {
        toast.error(`Failed to delete ${failed.length} items`, {
          description: failed.map((f) => f.error).join(", "),
        });
      }
    },
    onError: () => {
      toast.error("Bulk delete operation failed");
    },
  });
}

Performance Optimization

Virtual Scrolling for Large Datasets

components/ui/virtual-table.tsx

"use client";

import { useVirtualizer } from "@tanstack/react-virtual";
import { useRef } from "react";
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table";

interface VirtualTableProps<T> {
  data: T[];
  columns: Array<{
    header: string;
    accessorKey: keyof T | ((row: T) => any);
    width?: number;
  }>;
  rowHeight?: number;
  overscan?: number;
}

export default function VirtualTable<T>({
  data,
  columns,
  rowHeight = 50,
  overscan = 5,
}: VirtualTableProps<T>) {
  const parentRef = useRef<HTMLDivElement>(null);

  const virtualizer = useVirtualizer({
    count: data.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => rowHeight,
    overscan,
  });

  const getCellValue = (item: T, accessor: keyof T | ((row: T) => any)) => {
    if (typeof accessor === "function") {
      return accessor(item);
    }
    return item[accessor];
  };

  return (
    <div ref={parentRef} className="h-[400px] overflow-auto">
      <div
        style={{
          height: `${virtualizer.getTotalSize()}px`,
          position: "relative",
        }}
      >
        <Table>
          <TableHeader className="sticky top-0 bg-background z-10">
            <TableRow>
              {columns.map((column, index) => (
                <TableHead key={index} style={{ width: column.width }}>
                  {column.header}
                </TableHead>
              ))}
            </TableRow>
          </TableHeader>
          <TableBody>
            {virtualizer.getVirtualItems().map((virtualRow) => {
              const item = data[virtualRow.index];
              return (
                <TableRow
                  key={virtualRow.index}
                  style={{
                    position: "absolute",
                    top: 0,
                    left: 0,
                    width: "100%",
                    height: `${virtualRow.size}px`,
                    transform: `translateY(${virtualRow.start}px)`,
                  }}
                >
                  {columns.map((column, colIndex) => (
                    <TableCell key={colIndex}>
                      {getCellValue(item, column.accessorKey)}
                    </TableCell>
                  ))}
                </TableRow>
              );
            })}
          </TableBody>
        </Table>
      </div>
    </div>
  );
}

Infinite Scroll Hook

hooks/useInfiniteProducts.ts

import { useInfiniteQuery } from "@tanstack/react-query";
import { getBriefProducts } from "@/services/products";

interface InfiniteProductsParams {
  pageSize?: number;
  search?: string;
  filters?: Record<string, any>;
}

export function useInfiniteProducts({
  pageSize = 20,
  search,
  filters,
}: InfiniteProductsParams = {}) {
  return useInfiniteQuery({
    queryKey: ["products", "infinite", { pageSize, search, filters }],
    queryFn: ({ pageParam = 1 }) =>
      getBriefProducts({
        page: pageParam,
        limit: pageSize,
        search,
        ...filters,
      }),
    getNextPageParam: (lastPage, pages) => {
      // SOAPI: Optimized queries with pagination
      const hasMore = lastPage.data.length === pageSize;
      return hasMore ? pages.length + 1 : undefined;
    },
    initialPageParam: 1,
  });
}

Implementation Checklist

Phase 1: Setup

  • Install required dependencies
  • Create directory structure
  • Set up axios configuration
  • Configure React Query provider

Phase 2: API Layer

  • Create type definitions
  • Implement service functions following SOAPI principles
  • Create React Query hooks
  • Test API integration

Phase 3: Components

  • Implement data table components
  • Create product listing page
  • Add CRUD functionality
  • Implement export features

Phase 4: Update Forms

  • Create edit page structure
  • Implement tabbed update forms
  • Add atomic update cards
  • Test update functionality

Phase 5: Optimization

  • Implement proper error handling
  • Add loading states
  • Optimize queries with proper selects
  • Add data validation

Best Practices Summary

  1. Small Payloads: Never exceed 5 fields in creation forms
  2. Optimized Queries: Always use select to fetch only required fields
  3. Atomic Updates: Update only changed fields using PATCH
  4. Protected Deletes: Check relationships before deletion
  5. Intelligent Responses: Return IDs instead of full objects
  6. Smart UI: Group related fields in cards with dedicated update buttons
  7. React Query: Use for caching, error handling, and data synchronization
  8. TypeScript: Strong typing for better development experience

Conclusion

The SOAPI methodology provides a comprehensive approach to building efficient, scalable frontend applications with optimal API interactions. By following these principles:

  • Small Payloads: Faster requests, better UX
  • Optimized Queries: Reduced bandwidth, improved performance
  • Atomic Updates: Precise changes, better reliability
  • Protected Deletes: Data integrity, safer operations
  • Intelligent Responses: Minimal overhead, smart caching

You'll create applications that are not only performant but also maintainable and user-friendly.

Key Benefits Achieved:

50-70% reduction in API payload sizes
Improved page load times through optimized queries
Better user experience with atomic updates
Reduced data corruption with protected operations
Enhanced developer experience with TypeScript and React Query

This documentation serves as your complete reference for implementing SOAPI principles in any React-based project. Each code example is production-ready and follows industry best practices while maintaining the core SOAPI philosophy.---

Subscribe to My Latest Guides

All the latest Guides and tutorials, straight from the team.

Subscribe to be the first to receive the new Guide when it comes out

Get All Guides at Once