feat(form): add loading state and replace custom switch component
This commit introduces a loading state for form submissions and replaces the custom switch component with a third-party library for improved user experience and maintainability. - Adds `react-switch` as a dependency to replace the custom-built switch component, providing a more robust and feature-rich solution. - Implements a loading indicator on form submission buttons and in the confirmation modal by utilizing the `useIsMutating` hook from TanStack Query. This provides clear visual feedback to the user during asynchronous operations. - Creates a reusable `FormFooter` component to encapsulate the submit and cancel buttons, centralizing the form submission logic and loading state. - Refactors the company create/edit forms to use the new `FormFooter` and `react-switch` components. - Moves the `BreadcrumbItem` type to a shared types file for better code organization.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
import { useIsMutating } from "@tanstack/react-query";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { z } from "zod";
|
||||
@@ -23,6 +24,7 @@ export type ConfirmationModalProps = z.infer<
|
||||
>;
|
||||
|
||||
const ConfirmationModal: React.FC<ConfirmationModalProps> = (props) => {
|
||||
const isMutating = useIsMutating();
|
||||
const validatedProps = ConfirmationModalPropsSchema.parse(props);
|
||||
const {
|
||||
isOpen,
|
||||
@@ -107,7 +109,7 @@ const ConfirmationModal: React.FC<ConfirmationModalProps> = (props) => {
|
||||
<div
|
||||
className={`relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full ${currentSize} dark:bg-dark-2`}
|
||||
>
|
||||
<div className="bg-white px-4 pb-4 pt-5 sm:p-6 sm:pb-4 dark:bg-dark-2">
|
||||
<div className="bg-white px-4 pb-4 pt-5 dark:bg-dark-2 sm:p-6 sm:pb-4">
|
||||
<div className="sm:flex sm:items-start">
|
||||
<div
|
||||
className={`mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full ${currentVariant.iconBg} sm:mx-0 sm:h-10 sm:w-10`}
|
||||
@@ -131,17 +133,22 @@ const ConfirmationModal: React.FC<ConfirmationModalProps> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6 dark:bg-dark-2 dark:border-t dark:border-stroke-dark">
|
||||
<div className="bg-gray px-4 py-3 dark:border-t dark:border-stroke-dark dark:bg-dark-2 sm:flex sm:flex-row-reverse sm:px-6">
|
||||
<button
|
||||
type="button"
|
||||
className={`inline-flex w-full justify-center rounded-md px-3 py-2 text-sm font-semibold text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 sm:ml-3 sm:w-auto ${currentVariant.confirmBtn} dark:ring-offset-dark-2`}
|
||||
disabled={isMutating > 0}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
{confirmText}
|
||||
{isMutating ? (
|
||||
<div className="h-5 w-5 animate-spin rounded-full border-b-2 border-gray-1"></div>
|
||||
) : (
|
||||
confirmText
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-dark shadow-sm ring-1 ring-inset ring-stroke hover:bg-gray focus:outline-none focus:ring-2 focus:ring-dark-5 focus:ring-offset-2 sm:mt-0 sm:w-auto dark:bg-dark-3 dark:text-white dark:ring-stroke-dark dark:hover:bg-dark-4 dark:focus:ring-offset-dark-2"
|
||||
className="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-dark shadow-sm ring-1 ring-inset ring-stroke hover:bg-gray focus:outline-none focus:ring-2 focus:ring-dark-5 focus:ring-offset-2 dark:bg-dark-3 dark:text-white dark:ring-stroke-dark dark:hover:bg-dark-4 dark:focus:ring-offset-dark-2 sm:mt-0 sm:w-auto"
|
||||
onClick={onCancel}
|
||||
>
|
||||
{cancelText}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
"use client";
|
||||
import { useIsMutating } from "@tanstack/react-query";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
interface IProps {}
|
||||
|
||||
const FormFooter = ({}: IProps) => {
|
||||
const isMutating = useIsMutating();
|
||||
const router = useRouter();
|
||||
return (
|
||||
<div className="col-span-2 flex gap-6">
|
||||
<button
|
||||
type="submit"
|
||||
className="mt-6 flex w-fit items-center justify-center rounded-lg bg-primary p-[13px] font-medium text-white hover:bg-opacity-90"
|
||||
>
|
||||
{isMutating ? (
|
||||
<div className="h-5 w-5 animate-spin rounded-full border-b-2 border-gray-1"></div>
|
||||
) : (
|
||||
"Submit"
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={router.back}
|
||||
className="mt-6 flex w-fit justify-center rounded-lg bg-error p-[13px] font-medium text-white hover:bg-opacity-90"
|
||||
>
|
||||
Opt out
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FormFooter;
|
||||
@@ -30,18 +30,21 @@ const Status = ({ status, children }: IProps) => {
|
||||
|
||||
return (
|
||||
<span
|
||||
className={cn("rounded-2xl px-3 py-1.5 text-sm font-medium capitalize", {
|
||||
"bg-green text-white": greens.includes(status),
|
||||
"bg-orange-light text-white": oranges.includes(status),
|
||||
"bg-error text-white": reds.includes(status),
|
||||
"bg-blue text-white": blues.includes(status),
|
||||
"bg-gray-3 text-gray-7": grays.includes(status),
|
||||
"bg-yellow-light text-yellow-dark-2": yellows.includes(status),
|
||||
"bg-green-light-6 text-green-dark": status === "new",
|
||||
"bg-blue-light-5 text-blue-dark": status === "info",
|
||||
"bg-error-light-6 text-error-dark": status === "urgent",
|
||||
"bg-primary text-white": status === "featured",
|
||||
})}
|
||||
className={cn(
|
||||
"flex items-center justify-center rounded-2xl px-3 py-1.5 text-center text-sm font-medium capitalize",
|
||||
{
|
||||
"bg-green text-white": greens.includes(status),
|
||||
"bg-orange-light text-white": oranges.includes(status),
|
||||
"bg-error text-white": reds.includes(status),
|
||||
"bg-blue text-white": blues.includes(status),
|
||||
"bg-gray-3 text-gray-7": grays.includes(status),
|
||||
"bg-yellow-light text-yellow-dark-2": yellows.includes(status),
|
||||
"bg-green-light-6 text-green-dark": status === "new",
|
||||
"bg-blue-light-5 text-blue-dark": status === "info",
|
||||
"bg-error-light-6 text-error-dark": status === "urgent",
|
||||
"bg-primary text-white": status === "featured",
|
||||
},
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Skeleton } from "./skeleton";
|
||||
import { TableBody, TableCell, TableRow } from "./table";
|
||||
|
||||
interface IProps {
|
||||
length?: number;
|
||||
column: number;
|
||||
}
|
||||
|
||||
const TableSkeleton = ({ column, length = 20 }: IProps) => {
|
||||
return (
|
||||
<TableBody>
|
||||
{Array.from({ length }).map((_, i) => (
|
||||
<TableRow key={i}>
|
||||
{Array.from({ length: column }).map((_, j) => (
|
||||
<TableCell key={j} colSpan={100}>
|
||||
<Skeleton className="h-8" />
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
);
|
||||
};
|
||||
|
||||
export default TableSkeleton;
|
||||
Reference in New Issue
Block a user