Compare commits
No commits in common. "7726647d40c650452201c47c07bc0abb7102a261" and "99f40a36895b4ed6cd152eeb255ec44fcd4e7259" have entirely different histories.
7726647d40
...
99f40a3689
|
|
@ -1,228 +0,0 @@
|
||||||
// ContactForm.tsx
|
|
||||||
import React, { useState } from 'react';
|
|
||||||
import { FaSpinner } from 'react-icons/fa';
|
|
||||||
|
|
||||||
export interface ContactData {
|
|
||||||
contactId?: number;
|
|
||||||
email: string;
|
|
||||||
firstName: string;
|
|
||||||
lastName: string;
|
|
||||||
title: string;
|
|
||||||
company: string;
|
|
||||||
website: string;
|
|
||||||
phones: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
onSubmit: (data: ContactData) => void;
|
|
||||||
loading: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ContactForm = ({ onSubmit, loading }: Props) => {
|
|
||||||
const [formData, setFormData] = useState<ContactData>({
|
|
||||||
email: '',
|
|
||||||
firstName: '',
|
|
||||||
lastName: '',
|
|
||||||
title: '',
|
|
||||||
company: '',
|
|
||||||
website: '',
|
|
||||||
phones: [''],
|
|
||||||
});
|
|
||||||
|
|
||||||
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
|
||||||
|
|
||||||
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) => {
|
|
||||||
const numericValue = value.replace(/\D/g, ''); // Remove non-numeric
|
|
||||||
const updatedPhones = [...formData.phones];
|
|
||||||
updatedPhones[index] = numericValue;
|
|
||||||
setFormData((prev) => ({ ...prev, phones: updatedPhones }));
|
|
||||||
|
|
||||||
const errorKey = `phones_${index}`;
|
|
||||||
if (errors[errorKey]) {
|
|
||||||
setErrors((prev) => ({ ...prev, [errorKey]: '' }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const addPhoneField = () => {
|
|
||||||
setFormData((prev) => ({ ...prev, phones: [...prev.phones, ''] }));
|
|
||||||
};
|
|
||||||
|
|
||||||
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();
|
|
||||||
if (Object.keys(validationErrors).length > 0) {
|
|
||||||
setErrors(validationErrors);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await onSubmit(formData);
|
|
||||||
|
|
||||||
setFormData({
|
|
||||||
email: '',
|
|
||||||
firstName: '',
|
|
||||||
lastName: '',
|
|
||||||
title: '',
|
|
||||||
company: '',
|
|
||||||
website: '',
|
|
||||||
phones: [''],
|
|
||||||
});
|
|
||||||
setErrors({});
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Submit failed:', err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<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"
|
|
||||||
/>
|
|
||||||
{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}
|
|
||||||
/>
|
|
||||||
{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"
|
|
||||||
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;
|
|
||||||
|
|
@ -9,7 +9,6 @@ import AsideSection from "./AsideSection";
|
||||||
import Signature from "./Signature/Signature";
|
import Signature from "./Signature/Signature";
|
||||||
import { Bounce, toast } from "react-toastify";
|
import { Bounce, toast } from "react-toastify";
|
||||||
import AccountNav from "../accounts/AccountNav";
|
import AccountNav from "../accounts/AccountNav";
|
||||||
import ContactForm , {ContactData} from "./ContactForm";
|
|
||||||
|
|
||||||
// Define the shape of state object
|
// Define the shape of state object
|
||||||
interface Account {
|
interface Account {
|
||||||
|
|
@ -58,7 +57,7 @@ const Setting = () => {
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState("userProfile");
|
const [activeTab, setActiveTab] = useState("userProfile");
|
||||||
const [activeSubSection, setActiveSubSection] = useState<string>("");
|
const [activeSubSection, setActiveSubSection] = useState<string>("");
|
||||||
const [user, setUser] = useState<UserData>({
|
const [user, setUser] = useState<UserData>({
|
||||||
id: '',
|
id: '',
|
||||||
name: '',
|
name: '',
|
||||||
email: '',
|
email: '',
|
||||||
|
|
@ -213,13 +212,6 @@ const Setting = () => {
|
||||||
accountOptionsLoading={accountOptionsLoading}
|
accountOptionsLoading={accountOptionsLoading}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "contacts":
|
|
||||||
return (
|
|
||||||
<ContactForm
|
|
||||||
onSubmit={handleContactSubmit}
|
|
||||||
loading={createAndInviteLoader}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
default:
|
default:
|
||||||
return <div className="bg-white p-6 rounded-lg shadow-sm">Coming Soon...</div>;
|
return <div className="bg-white p-6 rounded-lg shadow-sm">Coming Soon...</div>;
|
||||||
}
|
}
|
||||||
|
|
@ -244,7 +236,6 @@ const Setting = () => {
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ id: "userProfile", label: "User Profile", type: "settings" },
|
{ id: "userProfile", label: "User Profile", type: "settings" },
|
||||||
{ id: "create and invite", label: "Create and invite", type: "settings" },
|
{ id: "create and invite", label: "Create and invite", type: "settings" },
|
||||||
{ id: "contacts", label: "Contacts", type: "settings" },
|
|
||||||
{ id: "language", label: "Language", type: "settings" },
|
{ id: "language", label: "Language", type: "settings" },
|
||||||
{ id: "contactAdd", label: "Add Contact", type: "contact" },
|
{ id: "contactAdd", label: "Add Contact", type: "contact" },
|
||||||
{ id: "contactRemove", label: "Remove Contact", type: "contact" },
|
{ id: "contactRemove", label: "Remove Contact", type: "contact" },
|
||||||
|
|
@ -388,28 +379,6 @@ const Setting = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleContactSubmit = async (data: ContactData) => {
|
|
||||||
try {
|
|
||||||
setCreateAndInviteLoader(true);
|
|
||||||
const res = await api.post(`/api/accounts/${activeAccountId}/contacts`, [data]); // Adjust endpoint if different
|
|
||||||
if (res.status === 200) {
|
|
||||||
toast.success('Contact created successfully!', {
|
|
||||||
position: "top-center",
|
|
||||||
autoClose: 1500,
|
|
||||||
transition: Bounce,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// return res;
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error creating contact", err);
|
|
||||||
toast.error("Failed to create contact");
|
|
||||||
throw err;
|
|
||||||
} finally {
|
|
||||||
setCreateAndInviteLoader(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* <SettingNav /> */}
|
{/* <SettingNav /> */}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue