Compare commits

...

2 Commits

3 changed files with 409 additions and 397 deletions

View File

@ -11,6 +11,7 @@ type ContactSchema = [{
firstName: string firstName: string
lastName: string lastName: string
email: string email: string
phone: string
}] }]
const Contacts = () => { const Contacts = () => {

View File

@ -1,6 +1,6 @@
// 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;
@ -10,32 +10,38 @@ export interface ContactData {
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 = ({
onSubmit,
loading,
setShowModal,
actionStatus,
editJsonData,
}: 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 }>({});
useEffect(() => { useEffect(() => {
if (actionStatus === 'Edit' && editJsonData) { if (actionStatus === "Edit" && editJsonData) {
setFormData({ setFormData({
email: editJsonData.email, email: editJsonData.email,
firstName: editJsonData.firstName, firstName: editJsonData.firstName,
@ -43,128 +49,124 @@ const ContactFormModal = ({ onSubmit, loading, setShowModal, actionStatus, editJ
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) => { const handleChange = (field: keyof ContactData, value: string) => {
console.log(field, value);
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 updatedPhones = [...formData.phone];
updatedPhones[index] = numericValue; // updatedPhones[index] = numericValue;
setFormData((prev) => ({ ...prev, phones: updatedPhones })); // setFormData((prev) => ({ ...prev, phones: updatedPhones }));
const errorKey = `phones_${index}`; // const errorKey = `phones_${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 updatedPhones = formData.phones.filter((_, i) => i !== index);
}; // setFormData((prev) => ({ ...prev, phones: updatedPhones }));
// };
const removePhoneField = (index: number) => {
const updatedPhones = formData.phones.filter((_, i) => i !== index);
setFormData((prev) => ({ ...prev, phones: updatedPhones }));
};
const validate = () => { const validate = () => {
const newErrors: { [key: string]: string } = {}; const newErrors: { [key: string]: string } = {};
const nameRegex = /^[a-zA-Z\s\-']+$/; const emailRegex = /\S+@\S+\.\S+/;
// const nameRegex = /^[a-zA-Z\s\-']+$/;
const urlRegex = /^(https?:\/\/)?([\w-]+\.)+[\w-]{2,}(\/.*)?$/; const urlRegex = /^(https?:\/\/)?([\w-]+\.)+[\w-]{2,}(\/.*)?$/;
const phoneRegex = /^\d+$/; const phoneRegex =
/^\+?\d{1,4}?[-.\s]?\(?\d{2,5}\)?[-.\s]?\d{3,5}[-.\s]?\d{3,5}$/;
// Email // Email
if (!formData.email.trim()) { if (!formData.email.trim()) {
newErrors.email = 'Email is required'; newErrors.email = "Email is required";
} else if (!/\S+@\S+\.\S+/.test(formData.email)) { } else if (!emailRegex.test(formData.email)) {
newErrors.email = 'Invalid email address'; newErrors.email = "Invalid email address";
} }
// // First Name // // First Name
// if (!formData.firstName.trim()) { // if (!formData.firstName.trim()) {
// newErrors.firstName = 'First name is required'; // newErrors.firstName = 'First name is required';
// } else if (!nameRegex.test(formData.firstName)) { // } else
// newErrors.firstName = 'First name contains invalid characters'; // if (formData.firstName.trim() && !nameRegex.test(formData.firstName)) {
// newErrors.firstName = "First name contains invalid characters";
// } // }
// Last Name // Last Name
if (!formData.lastName.trim()) { if (!formData.lastName.trim()) {
newErrors.lastName = 'Last name is required'; newErrors.lastName = "Last name is required";
} else if (!nameRegex.test(formData.lastName)) {
newErrors.lastName = 'Last name contains invalid characters';
} }
// else if (!nameRegex.test(formData.lastName)) {
// newErrors.lastName = "Last name contains invalid characters";
// }
// // Title // // Title
// if (!formData.title.trim()) { // if (!formData.title.trim()) {
// newErrors.title = 'Title is required'; // newErrors.title = 'Title is required';
// } // }
// Website (optional but validate if present) // Website (optional but validate if present)
if (formData.website.trim() && !urlRegex.test(formData.website)) { if (formData.website.trim() && !urlRegex.test(formData.website)) {
newErrors.website = 'Invalid website URL'; newErrors.website = "Invalid website URL";
} }
// Phones // Phone
// formData.phones.forEach((phone, index) => { if (formData.phone.trim() && !phoneRegex.test(formData.phone)) {
// if (!phone.trim()) { newErrors.phone = "Phone number must contain only digits";
// newErrors[`phones_${index}`] = 'Phone number is required'; }
// } else if (!phoneRegex.test(phone)) {
// newErrors[`phones_${index}`] = 'Phone number must contain only digits';
// }
// });
return newErrors; return newErrors;
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
const validationErrors = validate(); const validationErrors = validate();
let type = 'Add' let type = "Add";
let payload = formData let payload = formData;
console.log(payload);
if (Object.keys(validationErrors).length > 0) { if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors); setErrors(validationErrors);
return; return;
} }
try { try {
if (actionStatus === 'Edit') { if (actionStatus === "Edit") {
type = 'Edit' type = "Edit";
payload = { payload = {
...formData, ...formData,
contactId: editJsonData.id contactId: editJsonData.id,
} };
} }
await onSubmit(payload, type); await onSubmit(payload, type);
setFormData({ setFormData({
email: '', email: "",
firstName: '', firstName: "",
lastName: '', lastName: "",
title: '', title: "",
company: '', company: "",
website: '', website: "",
phones: [''], phone: "",
}); });
setErrors({}); setErrors({});
} catch (err) { } catch (err) {
console.error('Submit failed:', err); console.error("Submit failed:", err);
} }
}; };
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-white/5 backdrop-blur-sm bg-opacity-50"> <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="bg-white rounded-md p-6 max-w-[700px] w-full shadow-lg relative">
@ -173,30 +175,39 @@ const ContactFormModal = ({ onSubmit, loading, setShowModal, actionStatus, editJ
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
{[ {[
{ label: 'First Name', field: 'firstName' }, { label: "First Name", field: "firstName" },
{ label: 'Last Name', field: 'lastName' }, { label: "Last Name", field: "lastName" },
{ label: 'Email', field: 'email' }, { label: "Email", field: "email", fieldType: "email" },
{ label: 'Title', field: 'title' }, { label: "Title", field: "title" },
{ label: 'Company', field: 'company' }, { label: "Company", field: "company" },
{ label: 'Website', field: 'website' }, { label: "Website", field: "website", fieldType: "url" },
].map(({ label, field }) => ( { label: "Phone", field: "phone", fieldType: "tel" },
].map(({ label, field, fieldType }) => (
<div key={field} className="flex flex-col"> <div key={field} className="flex flex-col">
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">{label}</label> <label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">
{label}
</label>
<input <input
type={field === 'email' ? 'email' : 'text'} type={fieldType ? fieldType : "text"}
value={(formData[field as keyof ContactData] as string) || ''} value={(formData[field as keyof ContactData] as string) || ""}
onChange={(e) => handleChange(field as keyof ContactData, e.target.value)} onChange={(e) =>
handleChange(field as keyof ContactData, e.target.value)
}
required={true} 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" 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}`} placeholder={`Enter ${label}`}
/> />
{errors[field] && <span className="text-sm text-red-500 mt-1">{errors[field]}</span>} {errors[field] && (
<span className="text-sm text-red-500 mt-1">
{errors[field]}
</span>
)}
</div> </div>
))} ))}
</div> </div>
<div> {/* <div>
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">Phone Numbers</label> <label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">Phone Number</label>
<div className="space-y-2"> <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">
@ -233,7 +244,7 @@ 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
@ -241,7 +252,9 @@ const ContactFormModal = ({ onSubmit, loading, setShowModal, actionStatus, editJ
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 >
{" "}
Cancel
</button> </button>
<button <button
type="button" type="button"
@ -255,13 +268,9 @@ const ContactFormModal = ({ onSubmit, loading, setShowModal, actionStatus, editJ
SAVING... SAVING...
</> </>
) : ( ) : (
`${actionStatus?.toUpperCase() || 'SAVE'} CONTACT` `${actionStatus?.toUpperCase() || "SAVE"} CONTACT`
)} )}
</button> </button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,6 +1,6 @@
// 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;
@ -10,7 +10,7 @@ export interface ContactData {
title: string; title: string;
company: string; company: string;
website: string; website: string;
phones: string[]; phone: string;
} }
interface Props { interface Props {
@ -20,13 +20,13 @@ interface Props {
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 }>({});
@ -34,44 +34,44 @@ const ContactForm = ({ onSubmit, loading }: Props) => {
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 updatedPhones = formData.phones.filter((_, i) => i !== index);
setFormData((prev) => ({ ...prev, phones: updatedPhones }));
};
const validate = () => { const validate = () => {
const newErrors: { [key: string]: string } = {}; const newErrors: { [key: string]: string } = {};
const nameRegex = /^[a-zA-Z\s\-']+$/; // const nameRegex = /^[a-zA-Z\s\-']+$/;
const urlRegex = /^(https?:\/\/)?([\w-]+\.)+[\w-]{2,}(\/.*)?$/; const urlRegex = /^(https?:\/\/)?([\w-]+\.)+[\w-]{2,}(\/.*)?$/;
const phoneRegex = /^\d+$/; const phoneRegex =
/^\+?\d{1,4}?[-.\s]?\(?\d{2,5}\)?[-.\s]?\d{3,5}[-.\s]?\d{3,5}$/;
// Email // Email
if (!formData.email.trim()) { if (!formData.email.trim()) {
newErrors.email = 'Email is required'; newErrors.email = "Email is required";
} else if (!/\S+@\S+\.\S+/.test(formData.email)) { } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Invalid email address'; newErrors.email = "Invalid email address";
} }
// // First Name // // First Name
@ -83,9 +83,7 @@ const ContactForm = ({ onSubmit, loading }: Props) => {
// Last Name // Last Name
if (!formData.lastName.trim()) { if (!formData.lastName.trim()) {
newErrors.lastName = 'Last name is required'; newErrors.lastName = "Last name is required";
} else if (!nameRegex.test(formData.lastName)) {
newErrors.lastName = 'Last name contains invalid characters';
} }
// // Title // // Title
@ -93,25 +91,19 @@ const ContactForm = ({ onSubmit, loading }: Props) => {
// newErrors.title = 'Title is required'; // newErrors.title = 'Title is required';
// } // }
// Website (optional but validate if present) // Website (optional but validate if present)
if (formData.website.trim() && !urlRegex.test(formData.website)) { if (formData.website.trim() && !urlRegex.test(formData.website)) {
newErrors.website = 'Invalid website URL'; newErrors.website = "Invalid website URL";
} }
// Phones // Phone
// formData.phones.forEach((phone, index) => { if (formData.phone.trim() && !phoneRegex.test(formData.phone)) {
// if (!phone.trim()) { newErrors.phone = "Phone number must contain only digits";
// newErrors[`phones_${index}`] = 'Phone number is required'; }
// } else if (!phoneRegex.test(phone)) {
// newErrors[`phones_${index}`] = 'Phone number must contain only digits';
// }
// });
return newErrors; return newErrors;
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
const validationErrors = validate(); const validationErrors = validate();
if (Object.keys(validationErrors).length > 0) { if (Object.keys(validationErrors).length > 0) {
@ -123,53 +115,61 @@ const ContactForm = ({ onSubmit, loading }: Props) => {
await onSubmit(formData); await onSubmit(formData);
setFormData({ setFormData({
email: '', email: "",
firstName: '', firstName: "",
lastName: '', lastName: "",
title: '', title: "",
company: '', company: "",
website: '', website: "",
phones: [''], phone: "",
}); });
setErrors({}); setErrors({});
} catch (err) { } catch (err) {
console.error('Submit failed:', err); console.error("Submit failed:", err);
} }
}; };
return ( return (
<div className="ms-2 space-y-2 max-w-[60vw]"> <div className="ms-2 space-y-2 max-w-[60vw]">
<h2 className="text-lg font-medium text-slate-500">Add Contact</h2> <h2 className="text-lg font-medium text-slate-500">Add Contact</h2>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
{[ {[
{ label: 'First Name', field: 'firstName' }, { label: "First Name", field: "firstName" },
{ label: 'Last Name', field: 'lastName' }, { label: "Last Name", field: "lastName" },
{ label: 'Email', field: 'email' }, { label: "Email", field: "email", fieldType: "email" },
{ label: 'Title', field: 'title' }, { label: "Title", field: "title" },
{ label: 'Company', field: 'company' }, { label: "Company", field: "company" },
{ label: 'Website', field: 'website' }, { label: "Website", field: "website", fieldType: "url" },
].map(({ label, field }) => ( { label: "Phone", field: "phone", fieldType: "tel" },
].map(({ label, field, fieldType }) => (
<div key={field} className="flex flex-col"> <div key={field} className="flex flex-col">
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">{label}</label> <label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">
{label}
</label>
<input <input
type={field === 'email' ? 'email' : 'text'} type={fieldType ? fieldType : "text"}
value={formData[field as keyof ContactData] as string} value={formData[field as keyof ContactData] as string}
onChange={(e) => handleChange(field as keyof ContactData, e.target.value)} onChange={(e) =>
handleChange(field as keyof ContactData, e.target.value)
}
required={true} 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" 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}`} placeholder={`Enter ${label}`}
/> />
{errors[field] && <span className="text-sm text-red-500 mt-1">{errors[field]}</span>} {errors[field] && (
<span className="text-sm text-red-500 mt-1">{errors[field]}</span>
)}
</div> </div>
))} ))}
</div> </div>
<div> {/* <div>
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">Phone Numbers</label> <label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">
Phone Numbers
</label>
<div className="space-y-2"> <div className="space-y-2">
{formData.phones.map((phone, index) => ( {formData.phone.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">
<input <input
@ -178,9 +178,9 @@ const ContactForm = ({ onSubmit, loading }: Props) => {
onChange={(e) => handlePhoneChange(index, e.target.value)} 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" 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} maxLength={15}
placeholder='Enter Phone Numbers' placeholder="Enter Phone Numbers"
/> />
{formData.phones.length > 1 && ( {formData.phone.length > 1 && (
<button <button
type="button" type="button"
onClick={() => removePhoneField(index)} onClick={() => removePhoneField(index)}
@ -190,8 +190,10 @@ const ContactForm = ({ onSubmit, loading }: Props) => {
</button> </button>
)} )}
</div> </div>
{errors[`phones_${index}`] && ( {errors[`phone_${index}`] && (
<span className="text-sm text-red-500">{errors[`phones_${index}`]}</span> <span className="text-sm text-red-500">
{errors[`phone_${index}`]}
</span>
)} )}
</div> </div>
))} ))}
@ -204,7 +206,7 @@ const ContactForm = ({ onSubmit, loading }: Props) => {
+ Add another phone + Add another phone
</button> </button>
</div> </div>
</div> </div> */}
<div className="flex justify-end"> <div className="flex justify-end">
<button <button
@ -219,7 +221,7 @@ const ContactForm = ({ onSubmit, loading }: Props) => {
SAVING... SAVING...
</> </>
) : ( ) : (
'SAVE CONTACT' "SAVE CONTACT"
)} )}
</button> </button>
</div> </div>