feat(Modal): Modal, profile improvements - Adjust title based on state - Score formats based on tags

This commit is contained in:
santiagosayshey
2024-08-24 03:21:40 +09:30
committed by Sam Chau
parent 174e4ec82d
commit d9601eac0f
7 changed files with 385 additions and 215 deletions

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useRef } from "react";
import { useState, useEffect } from "react";
import PropTypes from "prop-types";
import { saveFormat, deleteFormat, getRegexes } from "../../api/api";
import ConditionModal from "../condition/ConditionModal";
@@ -6,7 +6,13 @@ import ConditionCard from "../condition/ConditionCard";
import Modal from "../ui/Modal";
import Alert from "../ui/Alert";
function FormatModal({ format: initialFormat, isOpen, onClose, onSave }) {
function FormatModal({
format: initialFormat,
isOpen,
onClose,
onSave,
isCloning = false,
}) {
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [conditions, setConditions] = useState([]);
@@ -17,10 +23,19 @@ function FormatModal({ format: initialFormat, isOpen, onClose, onSave }) {
const [tags, setTags] = useState([]);
const [newTag, setNewTag] = useState("");
const [isDeleting, setIsDeleting] = useState(false);
const [modalTitle, setModalTitle] = useState("");
useEffect(() => {
if (isOpen) {
if (initialFormat && initialFormat.id !== 0) {
if (isCloning) {
setModalTitle("Clone Custom Format");
} else if (initialFormat && initialFormat.id !== 0) {
setModalTitle("Edit Custom Format");
} else {
setModalTitle("Add Custom Format");
}
if (initialFormat && (initialFormat.id !== 0 || isCloning)) {
setName(initialFormat.name);
setDescription(initialFormat.description);
setConditions(initialFormat.conditions || []);
@@ -31,12 +46,13 @@ function FormatModal({ format: initialFormat, isOpen, onClose, onSave }) {
setConditions([]);
setTags([]);
}
setError("");
setNewTag("");
setIsDeleting(false);
fetchRegexes();
}
}, [initialFormat, isOpen]);
}, [isOpen, initialFormat, isCloning]);
const fetchRegexes = async () => {
try {
@@ -133,7 +149,7 @@ function FormatModal({ format: initialFormat, isOpen, onClose, onSave }) {
<Modal
isOpen={isOpen}
onClose={onClose}
title={initialFormat ? "Edit Custom Format" : "Add Custom Format"}
title={modalTitle}
className="max-w-3xl min-h-96"
>
{error && <div className="text-red-500 mb-4">{error}</div>}
@@ -213,17 +229,17 @@ function FormatModal({ format: initialFormat, isOpen, onClose, onSave }) {
>
Add Condition
</button>
<div className="flex justify-between">
<div className="flex justify-between mt-6">
<button
onClick={handleSave}
className="bg-blue-500 text-white px-4 py-3 rounded hover:bg-blue-600 transition-colors"
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 transition-colors"
>
Save
</button>
{initialFormat && (
{initialFormat && initialFormat.id !== 0 && (
<button
onClick={handleDelete}
className={`bg-red-500 text-white px-4 py-3 rounded hover:bg-red-600 transition-colors ${
className={`bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600 transition-colors ${
isDeleting ? "bg-red-600" : ""
}`}
>
@@ -262,6 +278,7 @@ FormatModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
onSave: PropTypes.func.isRequired,
isCloning: PropTypes.bool,
};
export default FormatModal;

View File

@@ -14,6 +14,7 @@ function FormatPage() {
const [filterType, setFilterType] = useState("none");
const [filterValue, setFilterValue] = useState("");
const [allTags, setAllTags] = useState([]);
const [isCloning, setIsCloning] = useState(false);
useEffect(() => {
fetchFormats();
@@ -35,26 +36,29 @@ function FormatPage() {
const handleOpenModal = (format = null) => {
setSelectedFormat(format);
setIsModalOpen(true);
setIsCloning(false);
};
const handleCloseModal = () => {
setSelectedFormat(null);
setIsModalOpen(false);
};
const handleSaveFormat = () => {
fetchFormats();
handleCloseModal();
setIsCloning(false);
};
const handleCloneFormat = (format) => {
const clonedFormat = {
...format,
id: 0, // Ensure the ID is 0 for a new entry
id: 0,
name: `${format.name} [COPY]`,
};
setSelectedFormat(clonedFormat); // Set cloned format
setIsModalOpen(true); // Open modal in Add mode
setSelectedFormat(clonedFormat);
setIsModalOpen(true);
setIsCloning(true);
};
const handleSaveFormat = () => {
fetchFormats();
handleCloseModal();
};
const formatDate = (dateString) => {
@@ -114,6 +118,7 @@ function FormatPage() {
onClose={handleCloseModal}
onSave={handleSaveFormat}
allTags={allTags}
isCloning={isCloning}
/>
</div>
);

View File

@@ -1,61 +1,83 @@
import { useState, useEffect } from "react";
import React, { useState, useEffect } from "react";
import PropTypes from "prop-types";
import { saveProfile, deleteProfile, getFormats } from "../../api/api";
import { saveProfile, deleteProfile } from "../../api/api";
import Modal from "../ui/Modal";
import Alert from "../ui/Alert";
import { Loader } from "lucide-react";
function unsanitize(text) {
return text.replace(/\\:/g, ":").replace(/\\n/g, "\n");
}
function ProfileModal({ profile: initialProfile, isOpen, onClose, onSave }) {
function ProfileModal({
profile: initialProfile,
isOpen,
onClose,
onSave,
formats,
isCloning = false,
}) {
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [tags, setTags] = useState([]);
const [newTag, setNewTag] = useState("");
const [customFormats, setCustomFormats] = useState([]);
const [formatTags, setFormatTags] = useState([]);
const [tagScores, setTagScores] = useState({});
const [tagFilter, setTagFilter] = useState("");
const [error, setError] = useState("");
const [isDeleting, setIsDeleting] = useState(false);
const [loading, setLoading] = useState(true);
const [modalTitle, setModalTitle] = useState("");
useEffect(() => {
if (isOpen) {
if (initialProfile && initialProfile.id !== 0) {
setName(unsanitize(initialProfile.name));
setDescription(unsanitize(initialProfile.description));
setTags(initialProfile.tags ? initialProfile.tags.map(unsanitize) : []);
setCustomFormats(initialProfile.custom_formats || []);
} else {
setName("");
setDescription("");
setTags([]);
setCustomFormats([]);
}
setError("");
setNewTag("");
fetchAllFormats();
}
}, [initialProfile, isOpen]);
setLoading(true);
const fetchAllFormats = async () => {
try {
const formats = await getFormats();
const updatedCustomFormats = formats.map((format) => {
const existingFormat =
initialProfile && initialProfile.custom_formats
? initialProfile.custom_formats.find((cf) => cf.id === format.id)
: null;
setModalTitle(
isCloning
? "Clone Profile"
: initialProfile && initialProfile.id !== 0
? "Edit Profile"
: "Add Profile"
);
setName(unsanitize(initialProfile?.name || ""));
setDescription(unsanitize(initialProfile?.description || ""));
setTags(initialProfile?.tags ? initialProfile.tags.map(unsanitize) : []);
const initialCustomFormats = initialProfile?.custom_formats || [];
const safeCustomFormats = formats.map((format) => {
const existingFormat = initialCustomFormats.find(
(cf) => cf.id === format.id
);
return {
id: format.id,
name: format.name,
score: existingFormat ? existingFormat.score : 0,
tags: format.tags || [],
};
});
setCustomFormats(updatedCustomFormats);
} catch (error) {
console.error("Error fetching formats:", error);
setError("Failed to fetch formats.");
setCustomFormats(safeCustomFormats);
// Extract all unique tags from custom formats
const allTags = [
...new Set(safeCustomFormats.flatMap((format) => format.tags)),
];
setFormatTags(allTags);
// Initialize tag scores
const initialTagScores = {};
allTags.forEach((tag) => {
initialTagScores[tag] = 0;
});
setTagScores(initialTagScores);
setError("");
setNewTag("");
setLoading(false);
}
};
}, [initialProfile, isOpen, formats, isCloning]);
const handleSave = async () => {
if (!name.trim()) {
@@ -81,9 +103,7 @@ function ProfileModal({ profile: initialProfile, isOpen, onClose, onSave }) {
const handleDelete = async () => {
if (isDeleting) {
try {
console.log("Attempting to delete profile with ID:", initialProfile.id);
const response = await deleteProfile(initialProfile.id);
console.log("Delete response:", response);
if (response.error) {
Alert.error(`Failed to delete profile: ${response.message}`);
} else {
@@ -123,110 +143,163 @@ function ProfileModal({ profile: initialProfile, isOpen, onClose, onSave }) {
);
};
const handleTagScoreChange = (tag, score) => {
setTagScores({ ...tagScores, [tag]: Math.max(parseInt(score) || 0, 0) });
// Update scores for all custom formats with this tag
setCustomFormats(
customFormats.map((format) => {
if (format.tags.includes(tag)) {
return { ...format, score: Math.max(parseInt(score) || 0, 0) };
}
return format;
})
);
};
const filteredTags = formatTags.filter((tag) =>
tag.toLowerCase().includes(tagFilter.toLowerCase())
);
return (
<Modal
isOpen={isOpen}
onClose={onClose}
title={initialProfile ? "Edit Profile" : "Add Profile"}
>
{error && <div className="text-red-500 mb-4">{error}</div>}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Profile Name
</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter profile name"
className="w-full p-2 border rounded dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600"
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Description
</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Enter description"
className="w-full p-2 border rounded dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600"
rows="3"
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Tags
</label>
<div className="flex flex-wrap mb-2">
{tags.map((tag) => (
<span
key={tag}
className="bg-blue-100 text-blue-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-blue-900 dark:text-blue-300"
>
{tag}
<button
onClick={() => handleRemoveTag(tag)}
className="ml-1 text-xs"
>
&times;
</button>
</span>
))}
<Modal isOpen={isOpen} onClose={onClose} title={modalTitle}>
{loading ? (
<div className="flex justify-center items-center">
<Loader size={24} className="animate-spin text-gray-300" />
</div>
<div className="flex">
<input
type="text"
value={newTag}
onChange={(e) => setNewTag(e.target.value)}
placeholder="Add a tag"
className="flex-grow p-2 border rounded-l dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600"
/>
<button
onClick={handleAddTag}
className="bg-blue-500 text-white px-4 py-2 rounded-r hover:bg-blue-600 transition-colors"
>
Add
</button>
</div>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Custom Formats
</label>
<div className="max-h-60 overflow-y-auto">
{customFormats.map((format) => (
<div key={format.id} className="flex items-center space-x-2 mb-2">
<span className="flex-grow">{format.name}</span>
<input
type="number"
value={format.score}
onChange={(e) => handleScoreChange(format.id, e.target.value)}
className="w-20 p-1 border rounded dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600"
min="0"
/>
) : (
<>
{error && <div className="text-red-500 mb-4">{error}</div>}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Profile Name
</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter profile name"
className="w-full p-2 border rounded dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600"
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Description
</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Enter description"
className="w-full p-2 border rounded dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600"
rows="3"
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Tags
</label>
<div className="flex flex-wrap mb-2">
{tags.map((tag) => (
<span
key={tag}
className="bg-blue-100 text-blue-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-blue-900 dark:text-blue-300"
>
{tag}
<button
onClick={() => handleRemoveTag(tag)}
className="ml-1 text-xs"
>
&times;
</button>
</span>
))}
</div>
))}
</div>
</div>
<div className="flex justify-between">
{initialProfile && (
<button
onClick={handleDelete}
className={`bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600 transition-colors ${
isDeleting ? "bg-red-600" : ""
}`}
>
{isDeleting ? "Confirm Delete" : "Delete"}
</button>
)}
<button
onClick={handleSave}
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 transition-colors"
>
Save
</button>
</div>
<div className="flex">
<input
type="text"
value={newTag}
onChange={(e) => setNewTag(e.target.value)}
placeholder="Add a tag"
className="flex-grow p-2 border rounded-l dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600"
/>
<button
onClick={handleAddTag}
className="bg-blue-500 text-white px-4 py-2 rounded-r hover:bg-blue-600 transition-colors"
>
Add
</button>
</div>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Custom Formats
</label>
<div className="max-h-60 overflow-y-auto">
{customFormats.map((format) => (
<div
key={format.id}
className="flex items-center space-x-2 mb-2"
>
<span className="flex-grow">{format.name}</span>
<input
type="number"
value={format.score}
onChange={(e) =>
handleScoreChange(format.id, e.target.value)
}
className="w-20 p-1 border rounded dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600"
min="0"
/>
</div>
))}
</div>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Tag-based Scoring
</label>
<input
type="text"
value={tagFilter}
onChange={(e) => setTagFilter(e.target.value)}
placeholder="Filter tags"
className="w-full p-2 border rounded mb-2 dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600"
/>
<div className="max-h-60 overflow-y-auto">
{filteredTags.map((tag) => (
<div key={tag} className="flex items-center space-x-2 mb-2">
<span className="flex-grow">{tag}</span>
<input
type="number"
value={tagScores[tag]}
onChange={(e) => handleTagScoreChange(tag, e.target.value)}
className="w-20 p-1 border rounded dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600"
min="0"
/>
</div>
))}
</div>
</div>
<div className="flex justify-between">
{initialProfile && (
<button
onClick={handleDelete}
className={`bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600 transition-colors ${
isDeleting ? "bg-red-600" : ""
}`}
>
{isDeleting ? "Confirm Delete" : "Delete"}
</button>
)}
<button
onClick={handleSave}
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 transition-colors"
>
Save
</button>
</div>
</>
)}
</Modal>
);
}
@@ -234,20 +307,29 @@ function ProfileModal({ profile: initialProfile, isOpen, onClose, onSave }) {
ProfileModal.propTypes = {
profile: PropTypes.shape({
id: PropTypes.number,
name: PropTypes.string.isRequired,
name: PropTypes.string,
description: PropTypes.string,
tags: PropTypes.arrayOf(PropTypes.string),
custom_formats: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
score: PropTypes.number.isRequired,
name: PropTypes.string,
score: PropTypes.number,
tags: PropTypes.arrayOf(PropTypes.string),
})
),
}),
isOpen: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
onSave: PropTypes.func.isRequired,
formats: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
tags: PropTypes.arrayOf(PropTypes.string),
})
).isRequired,
isCloning: PropTypes.bool,
};
export default ProfileModal;

View File

@@ -1,22 +1,25 @@
import { useState, useEffect } from "react";
import React, { useState, useEffect } from "react";
import ProfileCard from "./ProfileCard";
import ProfileModal from "./ProfileModal";
import AddNewCard from "../ui/AddNewCard";
import { getProfiles } from "../../api/api";
import { getProfiles, getFormats } from "../../api/api";
import FilterMenu from "../ui/FilterMenu";
import SortMenu from "../ui/SortMenu";
function ProfilePage() {
const [profiles, setProfiles] = useState([]);
const [formats, setFormats] = useState([]);
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedProfile, setSelectedProfile] = useState(null);
const [sortBy, setSortBy] = useState("name");
const [filterType, setFilterType] = useState("none");
const [filterValue, setFilterValue] = useState("");
const [allTags, setAllTags] = useState([]);
const [isCloning, setIsCloning] = useState(false);
useEffect(() => {
fetchProfiles();
fetchFormats();
}, []);
const fetchProfiles = async () => {
@@ -32,14 +35,43 @@ function ProfilePage() {
}
};
const fetchFormats = async () => {
try {
const fetchedFormats = await getFormats();
setFormats(fetchedFormats);
} catch (error) {
console.error("Error fetching formats:", error);
}
};
const handleOpenModal = (profile = null) => {
setSelectedProfile(profile);
const safeProfile = profile
? {
...profile,
custom_formats: profile.custom_formats || [],
}
: null;
setSelectedProfile(safeProfile);
setIsModalOpen(true);
setIsCloning(false);
};
const handleCloseModal = () => {
setSelectedProfile(null);
setIsModalOpen(false);
setIsCloning(false);
};
const handleCloneProfile = (profile) => {
const clonedProfile = {
...profile,
id: 0,
name: `${profile.name} [COPY]`,
custom_formats: profile.custom_formats || [],
};
setSelectedProfile(clonedProfile);
setIsModalOpen(true);
setIsCloning(true);
};
const handleSaveProfile = () => {
@@ -47,16 +79,7 @@ function ProfilePage() {
handleCloseModal();
};
const handleCloneProfile = (profile) => {
const clonedProfile = {
...profile,
id: 0, // Ensure the ID is 0 for a new entry
name: `${profile.name} [COPY]`,
};
setSelectedProfile(clonedProfile);
setIsModalOpen(true);
};
// Define the missing formatDate function
const formatDate = (dateString) => {
return new Date(dateString).toLocaleString();
};
@@ -103,7 +126,7 @@ function ProfilePage() {
onEdit={() => handleOpenModal(profile)}
onClone={handleCloneProfile}
showDate={sortBy !== "name"}
formatDate={formatDate}
formatDate={formatDate} // Pass the formatDate function to the ProfileCard
/>
))}
<AddNewCard onAdd={() => handleOpenModal()} />
@@ -113,7 +136,8 @@ function ProfilePage() {
isOpen={isModalOpen}
onClose={handleCloseModal}
onSave={handleSaveProfile}
allTags={allTags}
formats={formats}
isCloning={isCloning}
/>
</div>
);

View File

@@ -8,7 +8,13 @@ function unsanitize(text) {
return text.replace(/\\:/g, ":").replace(/\\n/g, "\n");
}
function RegexModal({ regex: initialRegex, isOpen, onClose, onSave }) {
function RegexModal({
regex: initialRegex,
isOpen,
onClose,
onSave,
isCloning = false,
}) {
const [name, setName] = useState("");
const [pattern, setPattern] = useState("");
const [description, setDescription] = useState("");
@@ -18,10 +24,20 @@ function RegexModal({ regex: initialRegex, isOpen, onClose, onSave }) {
const [error, setError] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [modalTitle, setModalTitle] = useState("");
useEffect(() => {
if (isOpen) {
if (initialRegex && initialRegex.id !== 0) {
// Set the modal title
if (isCloning) {
setModalTitle("Clone Regex Pattern");
} else if (initialRegex && initialRegex.id !== 0) {
setModalTitle("Edit Regex Pattern");
} else {
setModalTitle("Add Regex Pattern");
}
if (initialRegex && (initialRegex.id !== 0 || isCloning)) {
setName(unsanitize(initialRegex.name));
setPattern(initialRegex.pattern);
setDescription(unsanitize(initialRegex.description));
@@ -39,7 +55,7 @@ function RegexModal({ regex: initialRegex, isOpen, onClose, onSave }) {
setIsLoading(false);
setIsDeleting(false);
}
}, [initialRegex, isOpen]);
}, [initialRegex, isOpen, isCloning]);
const handleCreateRegex101Link = async () => {
if (!pattern.trim()) {
@@ -184,11 +200,7 @@ function RegexModal({ regex: initialRegex, isOpen, onClose, onSave }) {
return (
<>
<Modal
isOpen={isOpen}
onClose={onClose}
title={initialRegex ? "Edit Regex Pattern" : "Add Regex Pattern"}
>
<Modal isOpen={isOpen} onClose={onClose} title={modalTitle}>
{error && <div className="text-red-500 mb-4">{error}</div>}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
@@ -326,6 +338,7 @@ RegexModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
onSave: PropTypes.func.isRequired,
isCloning: PropTypes.bool,
};
export default RegexModal;

View File

@@ -14,6 +14,7 @@ function RegexPage() {
const [filterType, setFilterType] = useState("none");
const [filterValue, setFilterValue] = useState("");
const [allTags, setAllTags] = useState([]);
const [isCloning, setIsCloning] = useState(false);
useEffect(() => {
fetchRegexes();
@@ -35,27 +36,30 @@ function RegexPage() {
const handleOpenModal = (regex = null) => {
setSelectedRegex(regex);
setIsModalOpen(true);
setIsCloning(false);
};
const handleCloseModal = () => {
setSelectedRegex(null);
setIsModalOpen(false);
};
const handleSaveRegex = () => {
fetchRegexes();
handleCloseModal();
setIsCloning(false);
};
const handleCloneRegex = (regex) => {
const clonedRegex = {
...regex,
id: 0, // Ensure the ID is 0 for a new entry
id: 0,
name: `${regex.name} [COPY]`,
regex101Link: "", // Remove the regex101 link
regex101Link: "",
};
setSelectedRegex(clonedRegex); // Set cloned regex
setIsModalOpen(true); // Open modal in Add mode
setSelectedRegex(clonedRegex);
setIsModalOpen(true);
setIsCloning(true);
};
const handleSaveRegex = () => {
fetchRegexes();
handleCloseModal();
};
const formatDate = (dateString) => {
@@ -115,6 +119,7 @@ function RegexPage() {
onClose={handleCloseModal}
onSave={handleSaveRegex}
allTags={allTags}
isCloning={isCloning}
/>
</div>
);

View File

@@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import { useEffect, useRef, useState } from "react";
import PropTypes from "prop-types";
function Modal({
isOpen,
@@ -9,19 +9,25 @@ function Modal({
level = 0,
disableCloseOnOutsideClick = false,
disableCloseOnEscape = false,
size = 'lg', // Default size, can be overridden by the child component
size = "lg",
}) {
const modalRef = useRef();
const [isAnimating, setIsAnimating] = useState(false);
const [shouldRender, setShouldRender] = useState(false);
const [isClosing, setIsClosing] = useState(false);
useEffect(() => {
if (isOpen) {
setIsClosing(false);
setShouldRender(true);
setTimeout(() => setIsAnimating(true), 10);
} else {
setIsClosing(true);
setIsAnimating(false);
const timer = setTimeout(() => setShouldRender(false), 300);
const timer = setTimeout(() => {
setShouldRender(false);
setIsClosing(false);
}, 300);
return () => clearTimeout(timer);
}
}, [isOpen]);
@@ -29,53 +35,62 @@ function Modal({
useEffect(() => {
if (isOpen && !disableCloseOnEscape) {
const handleEscape = (event) => {
if (event.key === 'Escape') {
if (event.key === "Escape") {
onClose();
}
};
document.addEventListener('keydown', handleEscape);
document.addEventListener("keydown", handleEscape);
return () => {
document.removeEventListener('keydown', handleEscape);
document.removeEventListener("keydown", handleEscape);
};
}
}, [isOpen, onClose, disableCloseOnEscape]);
if (!shouldRender) return null;
if (!shouldRender && !isClosing) return null;
const handleClickOutside = (e) => {
if (modalRef.current && !modalRef.current.contains(e.target) && !disableCloseOnOutsideClick) {
if (
modalRef.current &&
!modalRef.current.contains(e.target) &&
!disableCloseOnOutsideClick
) {
onClose();
}
};
const sizeClasses = {
sm: 'max-w-sm',
md: 'max-w-md',
lg: 'max-w-lg',
xl: 'max-w-xl',
'2xl': 'max-w-2xl',
'3xl': 'max-w-3xl',
'4xl': 'max-w-4xl',
'5xl': 'max-w-5xl',
sm: "max-w-sm",
md: "max-w-md",
lg: "max-w-lg",
xl: "max-w-xl",
"2xl": "max-w-2xl",
"3xl": "max-w-3xl",
"4xl": "max-w-4xl",
"5xl": "max-w-5xl",
};
return (
<div
className={`fixed inset-0 overflow-y-auto h-full w-full flex items-center justify-center transition-opacity duration-300 ease-in-out ${
isAnimating ? 'opacity-100' : 'opacity-0'
isAnimating ? "opacity-100" : "opacity-0"
}`}
style={{ zIndex: 1000 + level * 10 }}
onClickCapture={handleClickOutside}
>
<div className="fixed inset-0 bg-black bg-opacity-50" style={{ zIndex: 1000 + level * 10 }}></div>
<div
className="fixed inset-0 bg-black bg-opacity-50"
style={{ zIndex: 1000 + level * 10 }}
></div>
<div
ref={modalRef}
className={`relative bg-white dark:bg-gray-800 p-8 rounded-lg shadow-xl w-full ${sizeClasses[size]} transform transition-all duration-300 ease-in-out ${
isAnimating ? 'translate-y-0 opacity-100' : 'translate-y-10 opacity-0'
className={`relative bg-white dark:bg-gray-800 p-8 rounded-lg shadow-xl w-full ${
sizeClasses[size]
} transform transition-all duration-300 ease-in-out ${
isAnimating ? "translate-y-0 opacity-100" : "translate-y-10 opacity-0"
}`}
style={{
zIndex: 1001 + level * 10,
minHeight: '300px',
minHeight: "300px",
}}
onClick={(e) => e.stopPropagation()}
>
@@ -85,14 +100,23 @@ function Modal({
onClick={onClose}
className="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition-colors"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12"></path>
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M6 18L18 6M6 6l12 12"
></path>
</svg>
</button>
</div>
<div className="pt-2">
{children}
</div>
<div className="pt-2">{children}</div>
</div>
</div>
);
@@ -106,7 +130,7 @@ Modal.propTypes = {
level: PropTypes.number,
disableCloseOnOutsideClick: PropTypes.bool,
disableCloseOnEscape: PropTypes.bool,
size: PropTypes.oneOf(['sm', 'md', 'lg', 'xl', '2xl', '3xl', '4xl', '5xl']), // Size prop
size: PropTypes.oneOf(["sm", "md", "lg", "xl", "2xl", "3xl", "4xl", "5xl"]),
};
export default Modal;