DEV Community

buildr
buildr

Posted on

This article will implete pagination on the web Project

import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";

export interface Property {
  id: number;
  propertyTitle: string;
  agencyId: number;
  propertyTypeId: number;
  propertyTypeName: string;
  description: string;
  amenities: string;
  monthlyRent: number;
  location: string;
  unitsAvailable: number;
  applicationDeadline: string;
  status: string;
  createdBy: string;
  createdDate: string;
  isDeleted: boolean;
  deletedDate: string;
  deletedBy: string;
  updatedDate: string;
  updatedBy: string;
}

export interface PropertyResponse {
  data: Property[];
  message: string;
  totalCount: number;
  pageNumber: number;
  pageSize: number;
}

export interface GetAllPropertyParams {
  pageSize: number;
  pageNumber: number;
  searchTerm?: string;
  statusFilter?: string;
}

export interface DeleteResponse {
  response: string;
}

export const propertyApi = createApi({
  reducerPath: "propertyApi",

  baseQuery: fetchBaseQuery({
    baseUrl: "https://localhost:7083/api/",

    prepareHeaders: (headers) => {
      const token = localStorage.getItem("token");

      if (token) {
        headers.set("Authorization", `Bearer ${token}`);
      }

      headers.set("Content-Type", "application/json");

      return headers;
    },
  }),

  tagTypes: ["Property"],

  endpoints: (builder) => ({
    // ==========================================
    // GET ALL PROPERTY
    // ==========================================
    getAllProperty: builder.query<
      PropertyResponse,
      GetAllPropertyParams
    >({
      query: ({
        pageSize,
        pageNumber,
        searchTerm = "",
        statusFilter = "",
      }) => ({
        url: "Property/GetAllProperty",
        method: "GET",
        params: {
          PageSize: pageSize,
          PageNumber: pageNumber,
          SearchTerm: searchTerm,
          StatusFilter: statusFilter,
        },
      }),

      providesTags: ["Property"],
    }),

    // ==========================================
    // DELETE PROPERTY
    // ==========================================
    deleteProperty: builder.mutation<
      DeleteResponse,
      number
    >({
      query: (propertyId) => ({
        url: "Property/DeleteProperty",
        method: "DELETE",
        body: {
          propertyId,
        },
      }),

      invalidatesTags: ["Property"],
    }),
  }),
});

export const {
  useGetAllPropertyQuery,
  useDeletePropertyMutation,
} = propertyApi;  
Enter fullscreen mode Exit fullscreen mode

Table

import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import {
  FaEdit,
  FaTrash,
  FaSearch,
  FaExclamationTriangle,
  FaTimes,
} from "react-icons/fa";

import {
  useGetAllPropertyQuery,
  useDeletePropertyMutation,
} from "../services/propertyApi";

const PAGE_SIZE = 10;

const PropertyTable = () => {
  const navigate = useNavigate();

  // ==========================================
  // PAGINATION
  // ==========================================
  const [pageNumber, setPageNumber] = useState(1);

  // ==========================================
  // SEARCH
  // ==========================================
  const [searchInput, setSearchInput] = useState("");
  const [searchTerm, setSearchTerm] = useState("");

  // ==========================================
  // STATUS FILTER
  // ==========================================
  const [statusFilter, setStatusFilter] = useState("");

  // ==========================================
  // DELETE MODAL
  // ==========================================
  const [deletePropertyId, setDeletePropertyId] =
    useState<number | null>(null);

  // ==========================================
  // ROLE
  // ==========================================
  const role = localStorage.getItem("role");

  const isTenant =
    role?.toLowerCase() === "tenant";

  // ==========================================
  // GET PROPERTIES
  // ==========================================
  const {
    data,
    isLoading,
    isFetching,
    isError,
  } = useGetAllPropertyQuery({
    pageSize: PAGE_SIZE,
    pageNumber,
    searchTerm,
    statusFilter,
  });

  // ==========================================
  // DELETE MUTATION
  // ==========================================
  const [
    deleteProperty,
    {
      isLoading: isDeleting,
    },
  ] = useDeletePropertyMutation();

  // ==========================================
  // DEBOUNCE SEARCH
  // ==========================================
  useEffect(() => {
    const timer = setTimeout(() => {
      setPageNumber(1);
      setSearchTerm(searchInput);
    }, 500);

    return () => clearTimeout(timer);
  }, [searchInput]);

  // ==========================================
  // DATA
  // ==========================================
  const properties = data?.data ?? [];

  const totalCount =
    data?.totalCount ?? 0;

  const totalPages = Math.ceil(
    totalCount / PAGE_SIZE
  );

  // ==========================================
  // APPLY
  // ==========================================
  const handleApply = (
    propertyId: number
  ) => {
    navigate(
      `/application-form/${propertyId}`
    );
  };

  // ==========================================
  // EDIT
  // ==========================================
  const handleEdit = (
    propertyId: number
  ) => {
    console.log(
      "Edit Property ID:",
      propertyId
    );

    // Future:
    // navigate(`/property/edit/${propertyId}`);
  };

  // ==========================================
  // OPEN DELETE MODAL
  // ==========================================
  const handleDeleteClick = (
    propertyId: number
  ) => {
    setDeletePropertyId(propertyId);
  };

  // ==========================================
  // CLOSE DELETE MODAL
  // ==========================================
  const handleCancelDelete = () => {
    if (isDeleting) return;

    setDeletePropertyId(null);
  };

  // ==========================================
  // CONFIRM DELETE
  // ==========================================
  const handleConfirmDelete = async () => {
    if (deletePropertyId === null) {
      return;
    }

    try {
      const result =
        await deleteProperty(
          deletePropertyId
        ).unwrap();

      console.log(
        result.response
      );

      // Close modal
      setDeletePropertyId(null);

      /*
       * Because deleteProperty has:
       *
       * invalidatesTags: ["Property"]
       *
       * RTK Query automatically refetches
       * the property list.
       */
    } catch (error) {
      console.error(
        "Failed to delete property:",
        error
      );
    }
  };

  // ==========================================
  // STATUS STYLE
  // ==========================================
  const getStatusStyle = (
    status: string
  ) => {
    switch (
      status.toLowerCase()
    ) {
      case "available":
        return "bg-green-100 text-green-700";

      case "rented":
        return "bg-red-100 text-red-700";

      case "onhold":
        return "bg-yellow-100 text-yellow-700";

      default:
        return "bg-gray-200 text-gray-700";
    }
  };

  // ==========================================
  // LOADING
  // ==========================================
  if (isLoading) {
    return (
      <div className="flex justify-center py-10">
        <p className="text-gray-700">
          Loading properties...
        </p>
      </div>
    );
  }

  // ==========================================
  // ERROR
  // ==========================================
  if (isError) {
    return (
      <div className="py-10 text-center text-red-600">
        Failed to load properties.
      </div>
    );
  }

  return (
    <>
      {/* ========================================
          PROPERTY TABLE
      ======================================== */}
      <div className="w-full rounded-xl border border-gray-300 bg-white shadow-sm">

        {/* HEADER */}
        <div
          className="flex flex-col gap-4
                     border-b border-gray-300
                     px-6 py-4
                     md:flex-row
                     md:items-center
                     md:justify-between"
        >
          <h2 className="text-xl font-semibold text-gray-900">
            Properties
          </h2>

          {/* SEARCH */}
          <div className="relative w-full md:w-80">
            <FaSearch
              className="absolute left-3 top-1/2
                         -translate-y-1/2
                         text-gray-500"
              size={14}
            />

            <input
              type="text"
              value={searchInput}
              onChange={(e) =>
                setSearchInput(
                  e.target.value
                )
              }
              placeholder="Search properties..."
              className="w-full rounded-lg
                         border border-gray-400
                         bg-white
                         py-2 pl-9 pr-3
                         text-gray-900
                         outline-none
                         placeholder:text-gray-500
                         focus:border-blue-500
                         focus:ring-2
                         focus:ring-blue-200"
            />
          </div>

          {/* STATUS FILTER */}
          <select
            value={statusFilter}
            onChange={(e) => {
              setStatusFilter(
                e.target.value
              );

              setPageNumber(1);
            }}
            className="rounded-lg
                       border border-gray-400
                       bg-white
                       px-3 py-2
                       text-gray-800
                       outline-none
                       focus:border-blue-500
                       focus:ring-2
                       focus:ring-blue-200"
          >
            <option value="">
              All Status
            </option>

            <option value="Available">
              Available
            </option>

            <option value="Rented">
              Rented
            </option>

            <option value="OnHold">
              OnHold
            </option>
          </select>
        </div>

        {/* TABLE */}
        <div className="overflow-x-auto">
          <table className="w-full text-left text-sm">

            {/* TABLE HEADER */}
            <thead className="bg-gray-300 text-gray-900">
              <tr>
                <th className="whitespace-nowrap px-6 py-4 font-semibold">
                  Property Name
                </th>

                <th className="whitespace-nowrap px-6 py-4 font-semibold">
                  Monthly Rent
                </th>

                <th className="whitespace-nowrap px-6 py-4 font-semibold">
                  Location
                </th>

                <th className="whitespace-nowrap px-6 py-4 font-semibold">
                  Units Available
                </th>

                <th className="whitespace-nowrap px-6 py-4 font-semibold">
                  Application Deadline
                </th>

                <th className="whitespace-nowrap px-6 py-4 font-semibold">
                  Status
                </th>

                {isTenant && (
                  <th className="px-6 py-4 text-center font-semibold">
                    Apply
                  </th>
                )}

                {!isTenant && (
                  <th className="px-6 py-4 text-center font-semibold">
                    Action
                  </th>
                )}
              </tr>
            </thead>

            {/* TABLE BODY */}
            <tbody className="divide-y divide-gray-300">

              {properties.length === 0 ? (
                <tr>
                  <td
                    colSpan={7}
                    className="px-6 py-12 text-center"
                  >
                    <p className="text-gray-700">
                      No properties found.
                    </p>
                  </td>
                </tr>
              ) : (
                properties.map(
                  (property) => (
                    <tr
                      key={property.id}
                      className="transition hover:bg-gray-100"
                    >

                      {/* PROPERTY NAME */}
                      <td className="px-6 py-4 font-medium text-gray-900">
                        {property.propertyTitle}
                      </td>

                      {/* RENT */}
                      <td className="px-6 py-4 font-medium text-gray-800">{property.monthlyRent.toLocaleString(
                          "en-IN"
                        )}
                      </td>

                      {/* LOCATION */}
                      <td className="px-6 py-4 text-gray-700">
                        {property.location}
                      </td>

                      {/* UNITS */}
                      <td className="px-6 py-4 text-gray-700">
                        {property.unitsAvailable}
                      </td>

                      {/* DEADLINE */}
                      <td className="px-6 py-4 text-gray-700">
                        {new Date(
                          property.applicationDeadline
                        ).toLocaleDateString(
                          "en-IN"
                        )}
                      </td>

                      {/* STATUS */}
                      <td className="px-6 py-4">
                        <span
                          className={`inline-flex
                            rounded-full
                            px-3 py-1
                            text-xs
                            font-semibold
                            ${getStatusStyle(
                              property.status
                            )}`}
                        >
                          {property.status}
                        </span>
                      </td>

                      {/* TENANT */}
                      {isTenant && (
                        <td className="px-6 py-4 text-center">
                          <button
                            type="button"
                            onClick={() =>
                              handleApply(
                                property.id
                              )
                            }
                            className="rounded-lg
                                       bg-blue-600
                                       px-4 py-2
                                       font-medium
                                       text-white
                                       transition
                                       hover:bg-blue-700"
                          >
                            Apply
                          </button>
                        </td>
                      )}

                      {/* ADMIN / MANAGER */}
                      {!isTenant && (
                        <td className="px-6 py-4">
                          <div
                            className="flex
                                       items-center
                                       justify-center
                                       gap-4"
                          >

                            {/* EDIT */}
                            <button
                              type="button"
                              title="Edit Property"
                              onClick={() =>
                                handleEdit(
                                  property.id
                                )
                              }
                              className="text-blue-600
                                         transition
                                         hover:text-blue-800"
                            >
                              <FaEdit
                                size={18}
                              />
                            </button>

                            {/* DELETE */}
                            <button
                              type="button"
                              title="Delete Property"
                              onClick={() =>
                                handleDeleteClick(
                                  property.id
                                )
                              }
                              className="text-red-600
                                         transition
                                         hover:text-red-800"
                            >
                              <FaTrash
                                size={18}
                              />
                            </button>

                          </div>
                        </td>
                      )}

                    </tr>
                  )
                )
              )}

            </tbody>
          </table>
        </div>

        {/* PAGINATION */}
        <div
          className="flex flex-col gap-3
                     border-t border-gray-300
                     px-6 py-4
                     sm:flex-row
                     sm:items-center
                     sm:justify-between"
        >
          <p className="text-sm text-gray-600">
            Page{" "}
            <span className="font-semibold text-gray-900">
              {pageNumber}
            </span>{" "}
            of{" "}
            <span className="font-semibold text-gray-900">
              {totalPages || 1}
            </span>
          </p>

          <div className="flex gap-2">

            <button
              type="button"
              disabled={
                pageNumber === 1 ||
                isFetching
              }
              onClick={() =>
                setPageNumber(
                  (prev) => prev - 1
                )
              }
              className="rounded-lg
                         border border-gray-400
                         bg-white
                         px-4 py-2
                         text-sm
                         font-medium
                         text-gray-700
                         transition
                         hover:bg-gray-100
                         disabled:cursor-not-allowed
                         disabled:opacity-50"
            >
              Previous
            </button>

            <button
              type="button"
              disabled={
                pageNumber >=
                  totalPages ||
                isFetching
              }
              onClick={() =>
                setPageNumber(
                  (prev) => prev + 1
                )
              }
              className="rounded-lg
                         border border-gray-400
                         bg-white
                         px-4 py-2
                         text-sm
                         font-medium
                         text-gray-700
                         transition
                         hover:bg-gray-100
                         disabled:cursor-not-allowed
                         disabled:opacity-50"
            >
              Next
            </button>

          </div>
        </div>

      </div>

      {/* ========================================
          DELETE CONFIRMATION MODAL
      ======================================== */}
      {deletePropertyId !== null && (
        <div
          className="fixed inset-0 z-50
                     flex items-center
                     justify-center
                     bg-black/50
                     px-4"
        >

          <div
            className="w-full max-w-md
                       rounded-xl
                       bg-white
                       p-6
                       shadow-2xl"
          >

            {/* MODAL HEADER */}
            <div className="flex items-start justify-between">

              <div className="flex items-center gap-3">

                <div
                  className="flex h-10 w-10
                             items-center
                             justify-center
                             rounded-full
                             bg-red-100"
                >
                  <FaExclamationTriangle
                    className="text-red-600"
                    size={18}
                  />
                </div>

                <h3 className="text-lg font-semibold text-gray-900">
                  Delete Property
                </h3>

              </div>

              {/* CLOSE */}
              <button
                type="button"
                disabled={isDeleting}
                onClick={
                  handleCancelDelete
                }
                className="text-gray-500
                           transition
                           hover:text-gray-800
                           disabled:opacity-50"
              >
                <FaTimes size={18} />
              </button>

            </div>

            {/* MESSAGE */}
            <div className="mt-5">
              <p className="text-sm leading-6 text-gray-600">
                Are you sure you want to delete
                this property?
              </p>

              <p className="mt-2 text-sm font-medium text-red-600">
                This action cannot be undone.
              </p>
            </div>

            {/* BUTTONS */}
            <div
              className="mt-6 flex
                         justify-end
                         gap-3"
            >

              {/* CANCEL */}
              <button
                type="button"
                disabled={isDeleting}
                onClick={
                  handleCancelDelete
                }
                className="rounded-lg
                           border border-gray-400
                           bg-white
                           px-4 py-2
                           text-sm
                           font-medium
                           text-gray-700
                           transition
                           hover:bg-gray-100
                           disabled:cursor-not-allowed
                           disabled:opacity-50"
              >
                Cancel
              </button>

              {/* CONFIRM DELETE */}
              <button
                type="button"
                disabled={isDeleting}
                onClick={
                  handleConfirmDelete
                }
                className="rounded-lg
                           bg-red-600
                           px-4 py-2
                           text-sm
                           font-medium
                           text-white
                           transition
                           hover:bg-red-700
                           disabled:cursor-not-allowed
                           disabled:opacity-50"
              >
                {isDeleting
                  ? "Deleting..."
                  : "Delete"}
              </button>

            </div>

          </div>
        </div>
      )}
    </>
  );
};

export default PropertyTable;```

Enter fullscreen mode Exit fullscreen mode

Top comments (0)