mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-22 10:51:02 +01:00
feat(Modal): Modal, profile improvements - Adjust title based on state - Score formats based on tags
This commit is contained in:
committed by
Sam Chau
parent
174e4ec82d
commit
d9601eac0f
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
×
|
||||
</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"
|
||||
>
|
||||
×
|
||||
</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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user