monorepo/web/maplefile-frontend/src/components/UIX/DeleteConfirmationCard/DeleteConfirmationCard.jsx

477 lines
18 KiB
JavaScript

import React, { useState, useEffect, useMemo, useCallback, memo } from "react";
import { Link } from "react-router";
import {
XMarkIcon,
TrashIcon,
ExclamationTriangleIcon,
PencilSquareIcon,
ArrowLeftIcon,
LockClosedIcon,
ShieldExclamationIcon,
ClockIcon,
UserIcon,
GlobeAltIcon,
InformationCircleIcon,
} from "@heroicons/react/24/outline";
// Constants outside component
const GRADIENT_STYLE = {
background: "linear-gradient(to right, #8a1622, #dc2626)",
};
// Helper functions outside component
const formatDate = (dateString) => {
return dateString ? new Date(dateString).toLocaleDateString() : "N/A";
};
const formatDateTime = (dateString) => {
return dateString ? new Date(dateString).toLocaleString() : "Not available";
};
const getItemDisplayName = (item) => {
return item?.name || item?.text || "item name";
};
const getItemStatus = (status) => {
return status === 1 ? "Active" : "Inactive";
};
const DeleteConfirmationCard = memo(
({
item,
itemType,
isDeleting,
error,
confirmText,
onConfirmTextChange,
onDelete,
onCancel,
onErrorClear,
detailRoute,
editRoute,
impactWarnings = [],
alternativeText,
customFields = [],
systemInfo = true,
className = "",
}) => {
if (!item) return null;
// Memoized values
const itemDisplayName = useMemo(() => getItemDisplayName(item), [item]);
const itemStatus = useMemo(() => getItemStatus(item.status), [item.status]);
const createdDate = useMemo(
() => formatDate(item.createdAt),
[item.createdAt],
);
const createdDateTime = useMemo(
() => formatDateTime(item.createdAt),
[item.createdAt],
);
const modifiedDateTime = useMemo(
() => formatDateTime(item.modifiedAt),
[item.modifiedAt],
);
const isConfirmValid = useMemo(
() => confirmText === itemDisplayName,
[confirmText, itemDisplayName],
);
const itemTypeLower = useMemo(() => itemType.toLowerCase(), [itemType]);
// Memoized callbacks
const handleConfirmTextChange = useCallback(
(e) => {
onConfirmTextChange(e.target.value);
},
[onConfirmTextChange],
);
const handleDelete = useCallback(() => {
if (isConfirmValid && !isDeleting) {
onDelete();
}
}, [isConfirmValid, isDeleting, onDelete]);
// Memoized class strings
const inputClasses = useMemo(() => {
const baseClasses =
"w-full px-4 py-3 border-2 rounded-xl focus:ring-4 focus:outline-none transition-all duration-200 text-sm font-medium";
if (isConfirmValid) {
return `${baseClasses} border-green-400 bg-green-50 focus:ring-green-200 text-green-800`;
}
return `${baseClasses} border-red-300 bg-red-50 focus:ring-red-200 text-red-800`;
}, [isConfirmValid]);
const deleteButtonClasses = useMemo(
() =>
"inline-flex items-center justify-center px-6 py-3 text-sm font-bold text-white bg-gradient-to-r from-red-700 to-red-800 rounded-xl hover:from-red-800 hover:to-red-900 hover:shadow-xl focus:outline-none focus:ring-4 focus:ring-red-300 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 shadow-lg",
[],
);
const cancelButtonClasses = useMemo(
() =>
"inline-flex items-center justify-center px-6 py-3 text-sm font-semibold text-gray-700 bg-white border-2 border-gray-300 rounded-xl hover:bg-gray-50 hover:border-gray-400 hover:shadow-md focus:outline-none focus:ring-4 focus:ring-gray-200 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200",
[],
);
const editLinkClasses = useMemo(
() =>
`inline-flex items-center justify-center px-6 py-3 text-sm font-semibold text-white bg-gradient-to-r from-red-500 to-red-600 rounded-xl hover:from-red-600 hover:to-red-700 hover:shadow-lg focus:outline-none focus:ring-4 focus:ring-red-200 transition-all duration-200 ${
isDeleting ? "opacity-50 pointer-events-none" : ""
}`,
[isDeleting],
);
// Memoized components
const errorSection = useMemo(() => {
if (!error) return null;
return (
<div className="mb-6 bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded-lg flex items-center justify-between">
<span className="flex items-center">
<ExclamationTriangleIcon className="w-5 h-5 mr-2" />
{error}
</span>
<button
onClick={onErrorClear}
className="text-red-600 hover:text-red-800"
>
<XMarkIcon className="w-5 h-5" />
</button>
</div>
);
}, [error, onErrorClear]);
const impactWarningSection = useMemo(() => {
if (impactWarnings.length === 0) return null;
return (
<div className="p-4 bg-amber-50 border border-amber-200 rounded-lg mb-6">
<h4 className="text-base font-medium text-amber-800 mb-3 flex items-center">
<ExclamationTriangleIcon className="w-5 h-5 mr-2" />
This deletion will affect:
</h4>
<ul className="text-sm text-amber-700 space-y-1 ml-7">
{impactWarnings.map((warning, index) => (
<li key={index} className="flex items-start">
{warning.icon && (
<warning.icon className="w-4 h-4 mr-2 flex-shrink-0 mt-0.5" />
)}
<span>{warning.text}</span>
</li>
))}
</ul>
{alternativeText && (
<div className="mt-3 pt-3 border-t border-amber-200">
<p className="text-sm font-medium text-amber-900">
<strong>Alternative:</strong> {alternativeText}
</p>
</div>
)}
</div>
);
}, [impactWarnings, alternativeText]);
const customFieldsSection = useMemo(() => {
return customFields.map((field, index) => (
<div key={index}>
<div className="block text-base font-medium text-gray-700 mb-2">
{field.label}
</div>
<div className="p-4 bg-white rounded-xl border border-gray-200 flex items-center">
{field.icon && <field.icon className="w-5 h-5 mr-2 text-red-500" />}
<span className={field.className || "font-medium text-red-700"}>
{field.value}
</span>
</div>
</div>
));
}, [customFields]);
const systemInfoSection = useMemo(() => {
if (!systemInfo) return null;
return (
<div className="mt-8 bg-white shadow-xl rounded-2xl overflow-hidden border border-gray-100 hover:shadow-2xl transition-shadow duration-300">
<div className="px-6 py-4 bg-blue-50 border-b border-blue-200">
<h2 className="text-lg font-bold text-blue-900 flex items-center">
<InformationCircleIcon className="w-6 h-6 mr-3 text-blue-600" />
System Information
</h2>
</div>
<div className="p-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 text-sm">
<div>
<p className="font-medium text-gray-500 mb-1 flex items-center">
<ClockIcon className="w-4 h-4 mr-1 text-blue-600" />
Created At:
</p>
<p className="text-gray-900 ml-5">{createdDateTime}</p>
</div>
<div>
<p className="font-medium text-gray-500 mb-1 flex items-center">
<UserIcon className="w-4 h-4 mr-1 text-blue-600" />
Created By:
</p>
<p className="text-gray-900 ml-5">
{item.createdByUserName || "Not available"}
</p>
</div>
<div>
<p className="font-medium text-gray-500 mb-1 flex items-center">
<ClockIcon className="w-4 h-4 mr-1 text-blue-600" />
Last Modified:
</p>
<p className="text-gray-900 ml-5">{modifiedDateTime}</p>
</div>
{item.modifiedByUserName && (
<div>
<p className="font-medium text-gray-500 mb-1 flex items-center">
<UserIcon className="w-4 h-4 mr-1 text-blue-600" />
Modified By:
</p>
<p className="text-gray-900 ml-5">
{item.modifiedByUserName}
</p>
</div>
)}
{item.createdFromIpAddress && (
<div>
<p className="font-medium text-gray-500 mb-1 flex items-center">
<GlobeAltIcon className="w-4 h-4 mr-1 text-blue-600" />
Created From IP:
</p>
<p className="text-gray-900 ml-5">
{item.createdFromIpAddress}
</p>
</div>
)}
{item.modifiedFromIpAddress && (
<div>
<p className="font-medium text-gray-500 mb-1 flex items-center">
<GlobeAltIcon className="w-4 h-4 mr-1 text-blue-600" />
Modified From IP:
</p>
<p className="text-gray-900 ml-5">
{item.modifiedFromIpAddress}
</p>
</div>
)}
</div>
</div>
</div>
);
}, [systemInfo, item, createdDateTime, modifiedDateTime]);
return (
<div className={`max-w-4xl mx-auto ${className}`}>
{/* Permanent Deletion Warning Alert */}
<div className="mb-6 bg-red-50 border-2 border-red-300 text-red-800 px-4 py-4 rounded-lg flex items-start">
<ShieldExclamationIcon className="w-6 h-6 mr-3 flex-shrink-0 mt-0.5" />
<div className="flex-1">
<p className="font-bold text-lg">Permanent Deletion Warning</p>
<p className="text-sm mt-1">
You are about to permanently delete this {itemTypeLower}. This
action cannot be undone.
</p>
</div>
</div>
<div className="bg-white shadow-xl rounded-2xl overflow-hidden border border-gray-100 hover:shadow-2xl transition-shadow duration-300">
<div className="px-6 py-4" style={GRADIENT_STYLE}>
<h2 className="text-base font-bold text-white uppercase tracking-wider flex items-center">
<ShieldExclamationIcon className="w-5 h-5 mr-2" />
Deletion Confirmation
</h2>
</div>
<div className="p-6">
{/* Error Messages */}
{errorSection}
{/* Item Details Section */}
<div className="mb-6">
<div className="block text-base sm:text-lg font-semibold text-gray-700 mb-3 flex items-center">
{itemType} to be deleted
</div>
<div className="p-5 bg-red-50 rounded-xl border border-red-200">
<div className="space-y-4">
<div>
<div className="block text-base sm:text-lg font-semibold text-gray-700 mb-3">
Name
</div>
<div className="p-5 bg-white rounded-xl border border-gray-200 font-semibold text-lg">
{itemDisplayName}
</div>
</div>
{item.description && (
<div>
<div className="block text-base font-medium text-gray-700 mb-2">
Description
</div>
<div className="p-4 bg-white rounded-xl border border-gray-200">
{item.description}
</div>
</div>
)}
{/* Custom Fields */}
{customFieldsSection}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm pt-2 border-t border-red-200">
<div>
<span className="font-medium text-gray-600">Status:</span>{" "}
<span className="text-gray-900">{itemStatus}</span>
</div>
<div>
<span className="font-medium text-gray-600">
Created:
</span>{" "}
<span className="text-gray-900">{createdDate}</span>
</div>
</div>
</div>
</div>
</div>
{/* Impact Warning */}
{impactWarningSection}
{/* Confirmation Section */}
<div className="p-6 bg-gradient-to-br from-red-50 via-red-25 to-white border-2 border-red-200 rounded-xl mb-6 shadow-lg">
<h4 className="text-lg font-bold text-red-800 mb-4 flex items-center">
<LockClosedIcon className="w-6 h-6 mr-3 text-red-600" />
Confirmation Required
</h4>
<div className="bg-white bg-opacity-60 backdrop-blur-sm rounded-lg p-4 mb-4 border border-red-100">
<p className="text-sm text-gray-800 mb-3 leading-relaxed">
This action will permanently remove the {itemTypeLower} from
the system. All data will be lost and cannot be recovered.
</p>
<label
htmlFor="delete-confirmation-input"
className="text-sm font-semibold text-gray-900 mb-4 block"
>
To confirm deletion, please type{" "}
<code className="px-3 py-1 bg-red-100 border border-red-300 rounded-md text-red-700 font-mono text-sm">
{itemDisplayName}
</code>{" "}
in the box below:
</label>
<div className="relative">
<input
type="text"
id="delete-confirmation-input"
name="delete-confirmation"
value={confirmText}
onChange={handleConfirmTextChange}
placeholder={`Type "${itemDisplayName}" to confirm`}
className={inputClasses}
disabled={isDeleting}
autoComplete="off"
autoFocus
/>
{isConfirmValid && (
<div className="absolute right-3 top-1/2 transform -translate-y-1/2">
<svg
className="w-5 h-5 text-green-500"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</div>
)}
</div>
</div>
</div>
{/* Action Buttons */}
<div className="pt-8 border-t-2 border-red-100 flex flex-col space-y-4 lg:flex-row lg:items-center lg:justify-between lg:space-y-0">
<Link
to={detailRoute}
className="inline-flex items-center text-sm font-medium text-red-600 hover:text-red-800 transition-colors duration-200"
>
<ArrowLeftIcon className="w-4 h-4 mr-2" />
Back to Detail
</Link>
<div className="flex flex-col space-y-3 lg:flex-row lg:items-center lg:space-y-0 lg:space-x-4">
<button
type="button"
onClick={onCancel}
disabled={isDeleting}
className={cancelButtonClasses}
>
<XMarkIcon className="w-4 h-4 mr-2" />
Cancel
</button>
{editRoute && (
<Link to={editRoute} className={editLinkClasses}>
<PencilSquareIcon className="w-4 h-4 mr-2" />
Edit Instead
</Link>
)}
<button
type="button"
onClick={handleDelete}
disabled={isDeleting || !isConfirmValid}
className={deleteButtonClasses}
>
{isDeleting ? (
<>
<svg
className="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
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"
></circle>
<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"
></path>
</svg>
Deleting...
</>
) : (
<>
<TrashIcon className="w-5 h-5 mr-2" />
Delete Permanently
</>
)}
</button>
</div>
</div>
</div>
</div>
{/* System Information Card */}
{systemInfoSection}
</div>
);
},
);
// Add display name for better debugging
DeleteConfirmationCard.displayName = "DeleteConfirmationCard";
export default DeleteConfirmationCard;