edit-case at same time delete click1
This commit is contained in:
commit
334996d343
|
|
@ -3,7 +3,6 @@ import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
|
||||||
import api from '../../config/api';
|
import api from '../../config/api';
|
||||||
import { Transaction } from '../transactions/Transactions';
|
import { Transaction } from '../transactions/Transactions';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { FaCheck } from 'react-icons/fa';
|
import { FaCheck } from 'react-icons/fa';
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ type ContactSchema = [{
|
||||||
firstName: string
|
firstName: string
|
||||||
lastName: string
|
lastName: string
|
||||||
email: string
|
email: string
|
||||||
|
phone: string
|
||||||
}]
|
}]
|
||||||
|
|
||||||
const Contacts = () => {
|
const Contacts = () => {
|
||||||
|
|
@ -121,7 +122,10 @@ const Contacts = () => {
|
||||||
<div className='px-1.5'>
|
<div className='px-1.5'>
|
||||||
<div className='flex justify-end my-2 cursor-pointer'>
|
<div className='flex justify-end my-2 cursor-pointer'>
|
||||||
<div
|
<div
|
||||||
onClick={() => setShowModal((prev) => !prev)}
|
onClick={() => {
|
||||||
|
setShowModal((prev) => !prev);
|
||||||
|
setActionStatus("Add")
|
||||||
|
}}
|
||||||
className="flex items-center gap-2 bg-[var(--primary)] px-3 py-1.5 rounded-sm hover:opacity-90 ">
|
className="flex items-center gap-2 bg-[var(--primary)] px-3 py-1.5 rounded-sm hover:opacity-90 ">
|
||||||
<IoMdAdd className="text-white" />
|
<IoMdAdd className="text-white" />
|
||||||
<button
|
<button
|
||||||
|
|
@ -143,20 +147,20 @@ const Contacts = () => {
|
||||||
<tbody className="divide-y divide-gray-200">
|
<tbody className="divide-y divide-gray-200">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan="4" className="relative h-100"><LoaderUi/></td>
|
<td colSpan="4" className="relative h-100"><LoaderUi /></td>
|
||||||
</tr>
|
</tr>
|
||||||
) : contacts.length === 0 ? (
|
) : contacts.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan="4" className="text-center py-10 text-gray-500">
|
<td colSpan="4" className="text-center py-10 text-gray-500">
|
||||||
<div className="flex flex-col items-center justify-center">
|
<div className="flex flex-col items-center justify-center">
|
||||||
<img
|
<img
|
||||||
src={logoRecord}
|
src={logoRecord}
|
||||||
alt="Record Not Found"
|
alt="Record Not Found"
|
||||||
className="w-70 h-70 mb-4 opacity-70" // adjust size and appearance
|
className="w-70 h-70 mb-4 opacity-70" // adjust size and appearance
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
contacts.map((contact) => (
|
contacts.map((contact) => (
|
||||||
<tr key={contact.id} className="hover:bg-gray-50">
|
<tr key={contact.id} className="hover:bg-gray-50">
|
||||||
|
|
@ -177,7 +181,7 @@ const Contacts = () => {
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{openMenuId === contact.id && (
|
{openMenuId === contact.id && (
|
||||||
<div className="absolute top-full right-0 mt-1 bg-white border border-gray-200 rounded shadow-lg z-20 min-w-[100px]">
|
<div className="absolute top-full right-0 mt-1 bg-white border border-gray-200 rounded shadow-lg z-20 min-w-[100px]">
|
||||||
<button
|
<button
|
||||||
onMouseDown={() => handleEdit(contact.id)}
|
onMouseDown={() => handleEdit(contact.id)}
|
||||||
className="block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 text-blue-600"
|
className="block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 text-blue-600"
|
||||||
|
|
|
||||||
|
|
@ -1,203 +1,216 @@
|
||||||
// ContactForm.tsx
|
// ContactForm.tsx
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from "react";
|
||||||
import { FaSpinner } from 'react-icons/fa';
|
import { FaSpinner } from "react-icons/fa";
|
||||||
|
|
||||||
export interface ContactData {
|
export interface ContactData {
|
||||||
contactId?: number;
|
contactId?: number;
|
||||||
email: string;
|
email: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
title: string;
|
title: string;
|
||||||
company: string;
|
company: string;
|
||||||
website: string;
|
website: string;
|
||||||
phones: string[];
|
phone: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onSubmit: (data: ContactData) => void;
|
onSubmit: (data: ContactData) => void;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
setShowModal: () => React.Dispatch<React.SetStateAction<boolean>>
|
setShowModal: () => React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
actionStatus: string,
|
actionStatus: string;
|
||||||
editJsonData: any
|
editJsonData: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ContactFormModal = ({ onSubmit, loading, setShowModal, actionStatus, editJsonData }: Props) => {
|
const ContactFormModal = ({
|
||||||
const [formData, setFormData] = useState<ContactData>({
|
onSubmit,
|
||||||
email: '',
|
loading,
|
||||||
firstName: '',
|
setShowModal,
|
||||||
lastName: '',
|
actionStatus,
|
||||||
title: '',
|
editJsonData,
|
||||||
company: '',
|
}: Props) => {
|
||||||
website: '',
|
const [formData, setFormData] = useState<ContactData>({
|
||||||
phones: [''],
|
email: "",
|
||||||
});
|
firstName: "",
|
||||||
|
lastName: "",
|
||||||
|
title: "",
|
||||||
|
company: "",
|
||||||
|
website: "",
|
||||||
|
phone: "",
|
||||||
|
});
|
||||||
|
|
||||||
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (actionStatus === 'Edit' && editJsonData) {
|
if (actionStatus === "Edit" && editJsonData) {
|
||||||
setFormData({
|
setFormData({
|
||||||
email: editJsonData.email,
|
email: editJsonData.email,
|
||||||
firstName: editJsonData.firstName,
|
firstName: editJsonData.firstName,
|
||||||
lastName: editJsonData.lastName,
|
lastName: editJsonData.lastName,
|
||||||
title: editJsonData.title,
|
title: editJsonData.title,
|
||||||
company: editJsonData.company,
|
company: editJsonData.company,
|
||||||
website: editJsonData.website,
|
website: editJsonData.website,
|
||||||
phones: editJsonData.phones,
|
phone: editJsonData.phone,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}, [actionStatus, editJsonData])
|
}, [actionStatus, editJsonData]);
|
||||||
|
|
||||||
|
const handleChange = (field: keyof ContactData, value: string) => {
|
||||||
|
console.log(field, value);
|
||||||
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||||
|
if (errors[field]) {
|
||||||
|
setErrors((prev) => ({ ...prev, [field]: "" }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// const handlePhoneChange = (index: number, value: string) => {
|
||||||
|
// const numericValue = value.replace(/\D/g, ""); // Remove non-numeric
|
||||||
|
// const updatedPhones = [...formData.phone];
|
||||||
|
// updatedPhones[index] = numericValue;
|
||||||
|
// setFormData((prev) => ({ ...prev, phones: updatedPhones }));
|
||||||
|
|
||||||
|
// const errorKey = `phones_${index}`;
|
||||||
|
// if (errors[errorKey]) {
|
||||||
|
// setErrors((prev) => ({ ...prev, [errorKey]: "" }));
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const addPhoneField = () => {
|
||||||
|
// setFormData((prev) => ({ ...prev, phone: [...prev.phone, ''] }));
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const removePhoneField = (index: number) => {
|
||||||
|
// const updatedPhones = formData.phones.filter((_, i) => i !== index);
|
||||||
|
// setFormData((prev) => ({ ...prev, phones: updatedPhones }));
|
||||||
|
// };
|
||||||
|
|
||||||
|
const validate = () => {
|
||||||
|
const newErrors: { [key: string]: string } = {};
|
||||||
|
|
||||||
|
const emailRegex = /\S+@\S+\.\S+/;
|
||||||
|
// const nameRegex = /^[a-zA-Z\s\-']+$/;
|
||||||
|
const urlRegex = /^(https?:\/\/)?([\w-]+\.)+[\w-]{2,}(\/.*)?$/;
|
||||||
|
const phoneRegex =
|
||||||
|
/^\+?\d{1,4}?[-.\s]?\(?\d{2,5}\)?[-.\s]?\d{3,5}[-.\s]?\d{3,5}$/;
|
||||||
|
|
||||||
|
// Email
|
||||||
|
if (!formData.email.trim()) {
|
||||||
|
newErrors.email = "Email is required";
|
||||||
|
} else if (!emailRegex.test(formData.email)) {
|
||||||
|
newErrors.email = "Invalid email address";
|
||||||
|
}
|
||||||
|
|
||||||
|
// // First Name
|
||||||
|
// if (!formData.firstName.trim()) {
|
||||||
|
// newErrors.firstName = 'First name is required';
|
||||||
|
// } else
|
||||||
|
// if (formData.firstName.trim() && !nameRegex.test(formData.firstName)) {
|
||||||
|
// newErrors.firstName = "First name contains invalid characters";
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Last Name
|
||||||
|
if (!formData.lastName.trim()) {
|
||||||
|
newErrors.lastName = "Last name is required";
|
||||||
|
}
|
||||||
|
// else if (!nameRegex.test(formData.lastName)) {
|
||||||
|
// newErrors.lastName = "Last name contains invalid characters";
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Title
|
||||||
|
// if (!formData.title.trim()) {
|
||||||
|
// newErrors.title = 'Title is required';
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Website (optional but validate if present)
|
||||||
|
if (formData.website.trim() && !urlRegex.test(formData.website)) {
|
||||||
|
newErrors.website = "Invalid website URL";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phone
|
||||||
|
if (formData.phone.trim() && !phoneRegex.test(formData.phone)) {
|
||||||
|
newErrors.phone = "Phone number must contain only digits";
|
||||||
|
}
|
||||||
|
return newErrors;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
const validationErrors = validate();
|
||||||
|
let type = "Add";
|
||||||
|
let payload = formData;
|
||||||
|
console.log(payload);
|
||||||
|
|
||||||
|
if (Object.keys(validationErrors).length > 0) {
|
||||||
|
setErrors(validationErrors);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (actionStatus === "Edit") {
|
||||||
|
type = "Edit";
|
||||||
|
payload = {
|
||||||
|
...formData,
|
||||||
|
contactId: editJsonData.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
await onSubmit(payload, type);
|
||||||
|
|
||||||
|
setFormData({
|
||||||
|
email: "",
|
||||||
|
firstName: "",
|
||||||
|
lastName: "",
|
||||||
|
title: "",
|
||||||
|
company: "",
|
||||||
|
website: "",
|
||||||
|
phone: "",
|
||||||
|
});
|
||||||
|
setErrors({});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Submit failed:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleChange = (field: keyof ContactData, value: string) => {
|
|
||||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
|
||||||
if (errors[field]) {
|
|
||||||
setErrors((prev) => ({ ...prev, [field]: '' }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePhoneChange = (index: number, value: string) => {
|
return (
|
||||||
const numericValue = value.replace(/\D/g, ''); // Remove non-numeric
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-white/5 backdrop-blur-sm bg-opacity-50">
|
||||||
const updatedPhones = [...formData.phones];
|
<div className="bg-white rounded-md p-6 max-w-[700px] w-full shadow-lg relative">
|
||||||
updatedPhones[index] = numericValue;
|
<div className="ms-2 space-y-2 max-w-[60vw]">
|
||||||
setFormData((prev) => ({ ...prev, phones: updatedPhones }));
|
<h2 className="text-lg font-medium text-slate-500">{`${actionStatus} Contact`}</h2>
|
||||||
|
|
||||||
const errorKey = `phones_${index}`;
|
<div className="grid grid-cols-2 gap-3">
|
||||||
if (errors[errorKey]) {
|
{[
|
||||||
setErrors((prev) => ({ ...prev, [errorKey]: '' }));
|
{ label: "First Name", field: "firstName" },
|
||||||
}
|
{ label: "Last Name", field: "lastName" },
|
||||||
};
|
{ label: "Email", field: "email", fieldType: "email" },
|
||||||
|
{ label: "Title", field: "title" },
|
||||||
|
{ label: "Company", field: "company" },
|
||||||
|
{ label: "Website", field: "website", fieldType: "url" },
|
||||||
|
{ label: "Phone", field: "phone", fieldType: "tel" },
|
||||||
|
].map(({ label, field, fieldType }) => (
|
||||||
|
<div key={field} className="flex flex-col">
|
||||||
|
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type={fieldType ? fieldType : "text"}
|
||||||
|
value={(formData[field as keyof ContactData] as string) || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleChange(field as keyof ContactData, e.target.value)
|
||||||
|
}
|
||||||
|
required={true}
|
||||||
|
className="px-3 py-1.5 border border-gray-400 rounded-md focus:outline-none focus:ring-1 focus:ring-[var(--primary)] text-sm"
|
||||||
|
placeholder={`Enter ${label}`}
|
||||||
|
/>
|
||||||
|
{errors[field] && (
|
||||||
|
<span className="text-sm text-red-500 mt-1">
|
||||||
|
{errors[field]}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* <div>
|
||||||
const addPhoneField = () => {
|
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">Phone Number</label>
|
||||||
setFormData((prev) => ({ ...prev, phones: [...prev.phones, ''] }));
|
<div className="space-y-2">
|
||||||
};
|
|
||||||
|
|
||||||
const removePhoneField = (index: number) => {
|
|
||||||
const updatedPhones = formData.phones.filter((_, i) => i !== index);
|
|
||||||
setFormData((prev) => ({ ...prev, phones: updatedPhones }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const validate = () => {
|
|
||||||
const newErrors: { [key: string]: string } = {};
|
|
||||||
|
|
||||||
const nameRegex = /^[a-zA-Z\s\-']+$/;
|
|
||||||
const urlRegex = /^(https?:\/\/)?([\w-]+\.)+[\w-]{2,}(\/.*)?$/;
|
|
||||||
const phoneRegex = /^\d+$/;
|
|
||||||
|
|
||||||
// Email
|
|
||||||
if (!formData.email.trim()) {
|
|
||||||
newErrors.email = 'Email is required';
|
|
||||||
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
|
|
||||||
newErrors.email = 'Invalid email address';
|
|
||||||
}
|
|
||||||
|
|
||||||
// // First Name
|
|
||||||
// if (!formData.firstName.trim()) {
|
|
||||||
// newErrors.firstName = 'First name is required';
|
|
||||||
// } else if (!nameRegex.test(formData.firstName)) {
|
|
||||||
// newErrors.firstName = 'First name contains invalid characters';
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Last Name
|
|
||||||
if (!formData.lastName.trim()) {
|
|
||||||
newErrors.lastName = 'Last name is required';
|
|
||||||
} else if (!nameRegex.test(formData.lastName)) {
|
|
||||||
newErrors.lastName = 'Last name contains invalid characters';
|
|
||||||
}
|
|
||||||
|
|
||||||
// // Title
|
|
||||||
// if (!formData.title.trim()) {
|
|
||||||
// newErrors.title = 'Title is required';
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
// Website (optional but validate if present)
|
|
||||||
if (formData.website.trim() && !urlRegex.test(formData.website)) {
|
|
||||||
newErrors.website = 'Invalid website URL';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phones
|
|
||||||
// formData.phones.forEach((phone, index) => {
|
|
||||||
// if (!phone.trim()) {
|
|
||||||
// newErrors[`phones_${index}`] = 'Phone number is required';
|
|
||||||
// } else if (!phoneRegex.test(phone)) {
|
|
||||||
// newErrors[`phones_${index}`] = 'Phone number must contain only digits';
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
return newErrors;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
const validationErrors = validate();
|
|
||||||
let type = 'Add'
|
|
||||||
let payload = formData
|
|
||||||
if (Object.keys(validationErrors).length > 0) {
|
|
||||||
setErrors(validationErrors);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
if (actionStatus === 'Edit') {
|
|
||||||
type = 'Edit'
|
|
||||||
payload = {
|
|
||||||
...formData,
|
|
||||||
contactId: editJsonData.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await onSubmit(payload, type);
|
|
||||||
|
|
||||||
|
|
||||||
setFormData({
|
|
||||||
email: '',
|
|
||||||
firstName: '',
|
|
||||||
lastName: '',
|
|
||||||
title: '',
|
|
||||||
company: '',
|
|
||||||
website: '',
|
|
||||||
phones: [''],
|
|
||||||
});
|
|
||||||
setErrors({});
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Submit failed:', err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-white/5 backdrop-blur-sm bg-opacity-50">
|
|
||||||
<div className="bg-white rounded-md p-6 max-w-[700px] w-full shadow-lg relative">
|
|
||||||
<div className="ms-2 space-y-2 max-w-[60vw]">
|
|
||||||
<h2 className="text-lg font-medium text-slate-500">Add Contact</h2>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
|
||||||
{[
|
|
||||||
{ label: 'First Name', field: 'firstName' },
|
|
||||||
{ label: 'Last Name', field: 'lastName' },
|
|
||||||
{ label: 'Email', field: 'email' },
|
|
||||||
{ label: 'Title', field: 'title' },
|
|
||||||
{ label: 'Company', field: 'company' },
|
|
||||||
{ label: 'Website', field: 'website' },
|
|
||||||
].map(({ label, field }) => (
|
|
||||||
<div key={field} className="flex flex-col">
|
|
||||||
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">{label}</label>
|
|
||||||
<input
|
|
||||||
type={field === 'email' ? 'email' : 'text'}
|
|
||||||
value={(formData[field as keyof ContactData] as string) || ''}
|
|
||||||
onChange={(e) => handleChange(field as keyof ContactData, e.target.value)}
|
|
||||||
required={true}
|
|
||||||
className="px-3 py-1.5 border border-gray-400 rounded-md focus:outline-none focus:ring-1 focus:ring-[var(--primary)] text-sm"
|
|
||||||
placeholder={`Enter ${label}`}
|
|
||||||
/>
|
|
||||||
{errors[field] && <span className="text-sm text-red-500 mt-1">{errors[field]}</span>}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">Phone Numbers</label>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{formData?.phones?.map((phone, index) => (
|
{formData?.phones?.map((phone, index) => (
|
||||||
<div key={index} className="flex flex-col gap-1">
|
<div key={index} className="flex flex-col gap-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|
@ -233,40 +246,38 @@ const ContactFormModal = ({ onSubmit, loading, setShowModal, actionStatus, editJ
|
||||||
+ Add another phone
|
+ Add another phone
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowModal((prev) => !prev)}
|
onClick={() => setShowModal((prev) => !prev)}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="bg-gray-300 px-5 py-1.5 rounded-md text-slate-700 text-sm font-medium hover:bg-opacity-90 transition disabled:opacity-60 disabled:cursor-not-allowed cursor-pointer me-3"
|
className="bg-gray-300 px-5 py-1.5 rounded-md text-slate-700 text-sm font-medium hover:bg-opacity-90 transition disabled:opacity-60 disabled:cursor-not-allowed cursor-pointer me-3"
|
||||||
> Cancel
|
>
|
||||||
</button>
|
{" "}
|
||||||
<button
|
Cancel
|
||||||
type="button"
|
</button>
|
||||||
onClick={handleSubmit}
|
<button
|
||||||
disabled={loading}
|
type="button"
|
||||||
className="bg-gray-300 px-5 py-1.5 rounded-md text-slate-700 text-sm font-medium hover:bg-opacity-90 transition disabled:opacity-60 disabled:cursor-not-allowed cursor-pointer"
|
onClick={handleSubmit}
|
||||||
>
|
disabled={loading}
|
||||||
{loading ? (
|
className="bg-gray-300 px-5 py-1.5 rounded-md text-slate-700 text-sm font-medium hover:bg-opacity-90 transition disabled:opacity-60 disabled:cursor-not-allowed cursor-pointer"
|
||||||
<>
|
>
|
||||||
<FaSpinner className="animate-spin inline-block mr-2" />
|
{loading ? (
|
||||||
SAVING...
|
<>
|
||||||
</>
|
<FaSpinner className="animate-spin inline-block mr-2" />
|
||||||
) : (
|
SAVING...
|
||||||
`${actionStatus?.toUpperCase() || 'SAVE'} CONTACT`
|
</>
|
||||||
)}
|
) : (
|
||||||
|
`${actionStatus?.toUpperCase() || "SAVE"} CONTACT`
|
||||||
</button>
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ContactFormModal;
|
export default ContactFormModal;
|
||||||
|
|
|
||||||
|
|
@ -1,230 +1,232 @@
|
||||||
// ContactForm.tsx
|
// ContactForm.tsx
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from "react";
|
||||||
import { FaSpinner } from 'react-icons/fa';
|
import { FaSpinner } from "react-icons/fa";
|
||||||
|
|
||||||
export interface ContactData {
|
export interface ContactData {
|
||||||
contactId?: number;
|
contactId?: number;
|
||||||
email: string;
|
email: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
title: string;
|
title: string;
|
||||||
company: string;
|
company: string;
|
||||||
website: string;
|
website: string;
|
||||||
phones: string[];
|
phone: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onSubmit: (data: ContactData) => void;
|
onSubmit: (data: ContactData) => void;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ContactForm = ({ onSubmit, loading }: Props) => {
|
const ContactForm = ({ onSubmit, loading }: Props) => {
|
||||||
const [formData, setFormData] = useState<ContactData>({
|
const [formData, setFormData] = useState<ContactData>({
|
||||||
email: '',
|
email: "",
|
||||||
firstName: '',
|
firstName: "",
|
||||||
lastName: '',
|
lastName: "",
|
||||||
title: '',
|
title: "",
|
||||||
company: '',
|
company: "",
|
||||||
website: '',
|
website: "",
|
||||||
phones: [''],
|
phone: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
||||||
|
|
||||||
const handleChange = (field: keyof ContactData, value: string) => {
|
const handleChange = (field: keyof ContactData, value: string) => {
|
||||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||||
if (errors[field]) {
|
if (errors[field]) {
|
||||||
setErrors((prev) => ({ ...prev, [field]: '' }));
|
setErrors((prev) => ({ ...prev, [field]: "" }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePhoneChange = (index: number, value: string) => {
|
// const handlePhoneChange = (index: number, value: string) => {
|
||||||
const numericValue = value.replace(/\D/g, ''); // Remove non-numeric
|
// const numericValue = value.replace(/\D/g, ""); // Remove non-numeric
|
||||||
const updatedPhones = [...formData.phones];
|
// const updatedPhone = [...formData.phone];
|
||||||
updatedPhones[index] = numericValue;
|
// updatedPhone[index] = numericValue;
|
||||||
setFormData((prev) => ({ ...prev, phones: updatedPhones }));
|
// setFormData((prev) => ({ ...prev, phone: updatedPhone }));
|
||||||
|
|
||||||
const errorKey = `phones_${index}`;
|
// const errorKey = `phone_${index}`;
|
||||||
if (errors[errorKey]) {
|
// if (errors[errorKey]) {
|
||||||
setErrors((prev) => ({ ...prev, [errorKey]: '' }));
|
// setErrors((prev) => ({ ...prev, [errorKey]: "" }));
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
|
// const addPhoneField = () => {
|
||||||
|
// setFormData((prev) => ({ ...prev, phone: [...prev.phone, ""] }));
|
||||||
|
// };
|
||||||
|
|
||||||
const addPhoneField = () => {
|
// const removePhoneField = (index: number) => {
|
||||||
setFormData((prev) => ({ ...prev, phones: [...prev.phones, ''] }));
|
// const updatedPhone = formData.phone.filter((_, i) => i !== index);
|
||||||
};
|
// setFormData((prev) => ({ ...prev, phone: updatedPhone }));
|
||||||
|
// };
|
||||||
|
|
||||||
const removePhoneField = (index: number) => {
|
const validate = () => {
|
||||||
const updatedPhones = formData.phones.filter((_, i) => i !== index);
|
const newErrors: { [key: string]: string } = {};
|
||||||
setFormData((prev) => ({ ...prev, phones: updatedPhones }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const validate = () => {
|
// const nameRegex = /^[a-zA-Z\s\-']+$/;
|
||||||
const newErrors: { [key: string]: string } = {};
|
const urlRegex = /^(https?:\/\/)?([\w-]+\.)+[\w-]{2,}(\/.*)?$/;
|
||||||
|
const phoneRegex =
|
||||||
|
/^\+?\d{1,4}?[-.\s]?\(?\d{2,5}\)?[-.\s]?\d{3,5}[-.\s]?\d{3,5}$/;
|
||||||
|
|
||||||
const nameRegex = /^[a-zA-Z\s\-']+$/;
|
// Email
|
||||||
const urlRegex = /^(https?:\/\/)?([\w-]+\.)+[\w-]{2,}(\/.*)?$/;
|
if (!formData.email.trim()) {
|
||||||
const phoneRegex = /^\d+$/;
|
newErrors.email = "Email is required";
|
||||||
|
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
|
||||||
|
newErrors.email = "Invalid email address";
|
||||||
|
}
|
||||||
|
|
||||||
// Email
|
// // First Name
|
||||||
if (!formData.email.trim()) {
|
// if (!formData.firstName.trim()) {
|
||||||
newErrors.email = 'Email is required';
|
// newErrors.firstName = 'First name is required';
|
||||||
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
|
// } else if (!nameRegex.test(formData.firstName)) {
|
||||||
newErrors.email = 'Invalid email address';
|
// newErrors.firstName = 'First name contains invalid characters';
|
||||||
}
|
// }
|
||||||
|
|
||||||
// // First Name
|
// Last Name
|
||||||
// if (!formData.firstName.trim()) {
|
if (!formData.lastName.trim()) {
|
||||||
// newErrors.firstName = 'First name is required';
|
newErrors.lastName = "Last name is required";
|
||||||
// } else if (!nameRegex.test(formData.firstName)) {
|
}
|
||||||
// newErrors.firstName = 'First name contains invalid characters';
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Last Name
|
// // Title
|
||||||
if (!formData.lastName.trim()) {
|
// if (!formData.title.trim()) {
|
||||||
newErrors.lastName = 'Last name is required';
|
// newErrors.title = 'Title is required';
|
||||||
} else if (!nameRegex.test(formData.lastName)) {
|
// }
|
||||||
newErrors.lastName = 'Last name contains invalid characters';
|
|
||||||
}
|
|
||||||
|
|
||||||
// // Title
|
// Website (optional but validate if present)
|
||||||
// if (!formData.title.trim()) {
|
if (formData.website.trim() && !urlRegex.test(formData.website)) {
|
||||||
// newErrors.title = 'Title is required';
|
newErrors.website = "Invalid website URL";
|
||||||
// }
|
}
|
||||||
|
|
||||||
|
// Phone
|
||||||
|
if (formData.phone.trim() && !phoneRegex.test(formData.phone)) {
|
||||||
|
newErrors.phone = "Phone number must contain only digits";
|
||||||
|
}
|
||||||
|
|
||||||
// Website (optional but validate if present)
|
return newErrors;
|
||||||
if (formData.website.trim() && !urlRegex.test(formData.website)) {
|
};
|
||||||
newErrors.website = 'Invalid website URL';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phones
|
const handleSubmit = async () => {
|
||||||
// formData.phones.forEach((phone, index) => {
|
const validationErrors = validate();
|
||||||
// if (!phone.trim()) {
|
if (Object.keys(validationErrors).length > 0) {
|
||||||
// newErrors[`phones_${index}`] = 'Phone number is required';
|
setErrors(validationErrors);
|
||||||
// } else if (!phoneRegex.test(phone)) {
|
return;
|
||||||
// newErrors[`phones_${index}`] = 'Phone number must contain only digits';
|
}
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
return newErrors;
|
try {
|
||||||
};
|
await onSubmit(formData);
|
||||||
|
|
||||||
|
setFormData({
|
||||||
|
email: "",
|
||||||
|
firstName: "",
|
||||||
|
lastName: "",
|
||||||
|
title: "",
|
||||||
|
company: "",
|
||||||
|
website: "",
|
||||||
|
phone: "",
|
||||||
|
});
|
||||||
|
setErrors({});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Submit failed:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
return (
|
||||||
const validationErrors = validate();
|
<div className="ms-2 space-y-2 max-w-[60vw]">
|
||||||
if (Object.keys(validationErrors).length > 0) {
|
<h2 className="text-lg font-medium text-slate-500">Add Contact</h2>
|
||||||
setErrors(validationErrors);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
<div className="grid grid-cols-2 gap-3">
|
||||||
await onSubmit(formData);
|
{[
|
||||||
|
{ label: "First Name", field: "firstName" },
|
||||||
|
{ label: "Last Name", field: "lastName" },
|
||||||
|
{ label: "Email", field: "email", fieldType: "email" },
|
||||||
|
{ label: "Title", field: "title" },
|
||||||
|
{ label: "Company", field: "company" },
|
||||||
|
{ label: "Website", field: "website", fieldType: "url" },
|
||||||
|
{ label: "Phone", field: "phone", fieldType: "tel" },
|
||||||
|
].map(({ label, field, fieldType }) => (
|
||||||
|
<div key={field} className="flex flex-col">
|
||||||
|
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type={fieldType ? fieldType : "text"}
|
||||||
|
value={formData[field as keyof ContactData] as string}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleChange(field as keyof ContactData, e.target.value)
|
||||||
|
}
|
||||||
|
required={true}
|
||||||
|
className="px-3 py-1.5 border border-gray-400 rounded-md focus:outline-none focus:ring-1 focus:ring-[var(--primary)] text-sm"
|
||||||
|
placeholder={`Enter ${label}`}
|
||||||
|
/>
|
||||||
|
{errors[field] && (
|
||||||
|
<span className="text-sm text-red-500 mt-1">{errors[field]}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
setFormData({
|
{/* <div>
|
||||||
email: '',
|
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">
|
||||||
firstName: '',
|
Phone Numbers
|
||||||
lastName: '',
|
</label>
|
||||||
title: '',
|
<div className="space-y-2">
|
||||||
company: '',
|
{formData.phone.map((phone, index) => (
|
||||||
website: '',
|
<div key={index} className="flex flex-col gap-1">
|
||||||
phones: [''],
|
<div className="flex items-center gap-2">
|
||||||
});
|
<input
|
||||||
setErrors({});
|
type="text"
|
||||||
} catch (err) {
|
value={phone}
|
||||||
console.error('Submit failed:', err);
|
onChange={(e) => handlePhoneChange(index, e.target.value)}
|
||||||
}
|
className="flex-1 px-3 py-1.5 border border-gray-400 rounded-md focus:outline-none focus:ring-1 focus:ring-[var(--primary)] text-sm"
|
||||||
};
|
maxLength={15}
|
||||||
|
placeholder="Enter Phone Numbers"
|
||||||
|
/>
|
||||||
return (
|
{formData.phone.length > 1 && (
|
||||||
<div className="ms-2 space-y-2 max-w-[60vw]">
|
<button
|
||||||
<h2 className="text-lg font-medium text-slate-500">Add Contact</h2>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
|
||||||
{[
|
|
||||||
{ label: 'First Name', field: 'firstName' },
|
|
||||||
{ label: 'Last Name', field: 'lastName' },
|
|
||||||
{ label: 'Email', field: 'email' },
|
|
||||||
{ label: 'Title', field: 'title' },
|
|
||||||
{ label: 'Company', field: 'company' },
|
|
||||||
{ label: 'Website', field: 'website' },
|
|
||||||
].map(({ label, field }) => (
|
|
||||||
<div key={field} className="flex flex-col">
|
|
||||||
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">{label}</label>
|
|
||||||
<input
|
|
||||||
type={field === 'email' ? 'email' : 'text'}
|
|
||||||
value={formData[field as keyof ContactData] as string}
|
|
||||||
onChange={(e) => handleChange(field as keyof ContactData, e.target.value)}
|
|
||||||
required={true}
|
|
||||||
className="px-3 py-1.5 border border-gray-400 rounded-md focus:outline-none focus:ring-1 focus:ring-[var(--primary)] text-sm"
|
|
||||||
placeholder={`Enter ${label}`}
|
|
||||||
/>
|
|
||||||
{errors[field] && <span className="text-sm text-red-500 mt-1">{errors[field]}</span>}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">Phone Numbers</label>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{formData.phones.map((phone, index) => (
|
|
||||||
<div key={index} className="flex flex-col gap-1">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={phone}
|
|
||||||
onChange={(e) => handlePhoneChange(index, e.target.value)}
|
|
||||||
className="flex-1 px-3 py-1.5 border border-gray-400 rounded-md focus:outline-none focus:ring-1 focus:ring-[var(--primary)] text-sm"
|
|
||||||
maxLength={15}
|
|
||||||
placeholder='Enter Phone Numbers'
|
|
||||||
/>
|
|
||||||
{formData.phones.length > 1 && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => removePhoneField(index)}
|
|
||||||
className="text-xs text-red-500 hover:underline"
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{errors[`phones_${index}`] && (
|
|
||||||
<span className="text-sm text-red-500">{errors[`phones_${index}`]}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={addPhoneField}
|
|
||||||
className="text-sm text-blue-500 hover:underline mt-1 cursor-pointer"
|
|
||||||
>
|
|
||||||
+ Add another phone
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<button
|
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleSubmit}
|
onClick={() => removePhoneField(index)}
|
||||||
disabled={loading}
|
className="text-xs text-red-500 hover:underline"
|
||||||
className="bg-gray-300 px-5 py-2 rounded-md text-slate-700 text-sm font-medium hover:bg-opacity-90 transition disabled:opacity-60 disabled:cursor-not-allowed cursor-pointer"
|
>
|
||||||
>
|
Remove
|
||||||
{loading ? (
|
</button>
|
||||||
<>
|
)}
|
||||||
<FaSpinner className="animate-spin inline-block mr-2" />
|
</div>
|
||||||
SAVING...
|
{errors[`phone_${index}`] && (
|
||||||
</>
|
<span className="text-sm text-red-500">
|
||||||
) : (
|
{errors[`phone_${index}`]}
|
||||||
'SAVE CONTACT'
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={addPhoneField}
|
||||||
|
className="text-sm text-blue-500 hover:underline mt-1 cursor-pointer"
|
||||||
|
>
|
||||||
|
+ Add another phone
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
</div> */}
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={loading}
|
||||||
|
className="bg-gray-300 px-5 py-2 rounded-md text-slate-700 text-sm font-medium hover:bg-opacity-90 transition disabled:opacity-60 disabled:cursor-not-allowed cursor-pointer"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<FaSpinner className="animate-spin inline-block mr-2" />
|
||||||
|
SAVING...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"SAVE CONTACT"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ContactForm;
|
export default ContactForm;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue