202 lines
5.6 KiB
JavaScript
202 lines
5.6 KiB
JavaScript
// File Path: web/frontend/src/components/UIX/CreateButton/CreateButton.jsx
|
|
// CreateButton Component - Performance Optimized
|
|
|
|
import React, { memo, useMemo } from "react";
|
|
import { PlusIcon } from "@heroicons/react/24/outline";
|
|
import { useUIXTheme } from "../themes/useUIXTheme.jsx";
|
|
|
|
/**
|
|
* CreateButton Component - Performance Optimized
|
|
* Green-themed button specifically for creation actions
|
|
*
|
|
* @param {React.ReactNode} children - Button text content
|
|
* @param {Function} onClick - Click handler function
|
|
* @param {boolean} disabled - Whether button is disabled
|
|
* @param {string} type - Button type (button, submit, reset)
|
|
* @param {string} className - Additional CSS classes
|
|
* @param {boolean} loading - Loading state
|
|
* @param {string} loadingText - Text to show when loading
|
|
* @param {boolean} fullWidth - Whether button should take full width
|
|
* @param {string} size - Button size (sm, md, lg, xl)
|
|
* @param {React.ComponentType} icon - Icon component (defaults to PlusIcon)
|
|
* @param {boolean} gradient - Whether to use gradient background
|
|
*/
|
|
|
|
// Static size classes - moved outside to prevent recreation
|
|
const SIZE_CLASSES = Object.freeze({
|
|
sm: "px-3 py-2 text-xs sm:text-sm",
|
|
md: "px-4 py-3 text-sm sm:text-base",
|
|
lg: "px-6 sm:px-8 py-3 sm:py-4 text-sm sm:text-base",
|
|
xl: "px-8 py-4 text-base sm:text-lg",
|
|
});
|
|
|
|
// Loading Spinner Component - Separated for better performance
|
|
const LoadingSpinner = memo(function LoadingSpinner() {
|
|
return (
|
|
<svg
|
|
className="animate-spin -ml-1 mr-2 h-4 w-4 text-current"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<circle
|
|
className="opacity-25"
|
|
cx="12"
|
|
cy="12"
|
|
r="10"
|
|
stroke="currentColor"
|
|
strokeWidth="4"
|
|
/>
|
|
<path
|
|
className="opacity-75"
|
|
fill="currentColor"
|
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
/>
|
|
</svg>
|
|
);
|
|
});
|
|
|
|
LoadingSpinner.displayName = "LoadingSpinner";
|
|
|
|
const CreateButton = memo(
|
|
function CreateButton({
|
|
children,
|
|
onClick,
|
|
disabled = false,
|
|
type = "button",
|
|
className = "",
|
|
loading = false,
|
|
loadingText,
|
|
fullWidth = false,
|
|
size = "lg",
|
|
icon: Icon = PlusIcon,
|
|
gradient = false,
|
|
}) {
|
|
const { getThemeClasses } = useUIXTheme();
|
|
|
|
// Computed disabled state
|
|
const isDisabled = disabled || loading;
|
|
|
|
// Memoize theme classes
|
|
const themeClasses = useMemo(
|
|
() => ({
|
|
buttonCreate: getThemeClasses("button-create"),
|
|
inputFocusRing: getThemeClasses("input-focus-ring"),
|
|
bgGradientSecondary: getThemeClasses("bg-gradient-secondary"),
|
|
}),
|
|
[getThemeClasses],
|
|
);
|
|
|
|
// Memoize gradient style
|
|
const gradientStyle = useMemo(() => {
|
|
if (!gradient) return {};
|
|
|
|
return {
|
|
background:
|
|
themeClasses.bgGradientSecondary ||
|
|
"linear-gradient(to right, #059669, #10b981)",
|
|
};
|
|
}, [gradient, themeClasses.bgGradientSecondary]);
|
|
|
|
// Memoize button classes
|
|
const buttonClasses = useMemo(() => {
|
|
const classes = [
|
|
SIZE_CLASSES[size] || SIZE_CLASSES.lg,
|
|
"font-medium",
|
|
"rounded-xl",
|
|
"focus:outline-none",
|
|
"transition-all",
|
|
"duration-200",
|
|
"inline-flex",
|
|
"items-center",
|
|
"justify-center",
|
|
];
|
|
|
|
if (fullWidth) {
|
|
classes.push("w-full");
|
|
}
|
|
|
|
// Variant classes
|
|
if (gradient) {
|
|
classes.push(
|
|
"border-transparent",
|
|
"text-white",
|
|
"shadow-lg",
|
|
"hover:shadow-xl",
|
|
themeClasses.inputFocusRing,
|
|
"transform",
|
|
"hover:scale-105",
|
|
"font-bold",
|
|
);
|
|
} else {
|
|
classes.push(themeClasses.buttonCreate);
|
|
}
|
|
|
|
// State classes
|
|
if (isDisabled) {
|
|
classes.push("opacity-50", "cursor-not-allowed");
|
|
} else {
|
|
classes.push("cursor-pointer");
|
|
}
|
|
|
|
// Custom className
|
|
if (className) {
|
|
classes.push(className);
|
|
}
|
|
|
|
return classes.filter(Boolean).join(" ");
|
|
}, [size, fullWidth, gradient, themeClasses, isDisabled, className]);
|
|
|
|
// Memoize button content
|
|
const ButtonContent = useMemo(() => {
|
|
if (loading) {
|
|
return (
|
|
<>
|
|
<LoadingSpinner />
|
|
{loadingText || "Creating..."}
|
|
</>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{Icon && <Icon className="w-4 h-4 mr-2" />}
|
|
{children}
|
|
</>
|
|
);
|
|
}, [loading, loadingText, Icon, children]);
|
|
|
|
return (
|
|
<button
|
|
type={type}
|
|
className={buttonClasses}
|
|
onClick={isDisabled ? undefined : onClick}
|
|
disabled={isDisabled}
|
|
style={gradientStyle}
|
|
>
|
|
{ButtonContent}
|
|
</button>
|
|
);
|
|
},
|
|
(prevProps, nextProps) => {
|
|
// Custom comparison function - only re-render when these props actually change
|
|
return (
|
|
prevProps.children === nextProps.children &&
|
|
prevProps.onClick === nextProps.onClick &&
|
|
prevProps.disabled === nextProps.disabled &&
|
|
prevProps.type === nextProps.type &&
|
|
prevProps.className === nextProps.className &&
|
|
prevProps.loading === nextProps.loading &&
|
|
prevProps.loadingText === nextProps.loadingText &&
|
|
prevProps.fullWidth === nextProps.fullWidth &&
|
|
prevProps.size === nextProps.size &&
|
|
prevProps.icon === nextProps.icon &&
|
|
prevProps.gradient === nextProps.gradient
|
|
);
|
|
},
|
|
);
|
|
|
|
// Display name for debugging
|
|
CreateButton.displayName = "CreateButton";
|
|
|
|
export default CreateButton;
|