/**
* An array of colors that can be used to color user profiles.
* @type {Array<string>}
*/
const profileColors = [
"#FF7A00",
"#FF5EB3",
"#6E52FF",
"#9327FF",
"#00BEE8",
"#1FC7C1",
"#8B9467",
"#FF745E",
"#FFA35E",
"#FC71FF",
"#FFC701",
"#0038FF",
"#B22222",
"#C3FF2B",
"#FFE62B",
"#FF4646",
"#FFBB2B",
"#FF7A00",
"#FF5EB3",
"#6E52FF",
];
/**
* Timeout in milliseconds that is used for how long the warning in the form
* validation is shown.
* @constant {number}
*/
const TIMEOUT = 2000;
/**
* Opens the contact modal and inserts it into the DOM.
*
* Depending on the type, the modal will either be in "add" or "edit" mode.
* If in "edit" mode, the initials are derived from the provided name.
* The contact modal is created using the provided name, email, phone, and color,
* and then inserted into the body of the document.
* The function also applies a "slide-in" animation to the modal.
*
* @param {string} type - The type of the modal, either "add" or "edit".
* @param {string} [name=""] - The name of the contact.
* @param {string} [email=""] - The email of the contact.
* @param {string} [phone=""] - The phone number of the contact.
* @param {string} [color=""] - The color associated with the contact.
*/
function openContactModal(type, name = "", email = "", phone = "", color = "") {
const initials = type === "edit" ? getInitialsFromContact({ name: name }) : "";
const modalHtml = getContactModalTemplate(type, name, email, phone, initials, color);
let modalElement = document.getElementById("contact-modal");
if (modalElement) modalElement.remove();
document.body.insertAdjacentHTML("beforeend", modalHtml);
applyAnimation("slide-in");
}
/**
* Closes the contact modal, removing it from the DOM.
*
* @param {Event} [event] - Optional event.
*/
function closeContactModal(event) {
if (event) event.preventDefault();
const modal = document.getElementById("contact-modal");
if (modal) {
applyAnimation("slide-out");
modal.addEventListener("animationend", () => modal.remove());
}
}
/**
* Applies the given animation to the modal content element. The animation is
* applied by setting the animation CSS property on the element.
* @param {string} animationType - The type of animation to apply.
*/
function applyAnimation(animationType) {
const modalContent = document.getElementById("modal-content");
modalContent.style.animation = `${animationType} 0.3s ease-out forwards`;
}
/**
* Handles the save button click in the contact modal.
*
* If the button says "Save", it will update a contact. It will find the contact
* to update based on the data-created-at attribute of the #createdAt element.
*
* If the button says "Add", it will create a contact.
*
* @param {Event} event - The save button click event.
* @returns {Promise<void>}
*/
async function handleSaveClick(event) {
event.preventDefault();
const saveBtn = document.querySelector(".save-btn");
if (!saveBtn) return;
const isSave = saveBtn.innerText.includes("Save");
if (isSave) {
const contactNameElement = document.getElementById("createdAt");
if (!contactNameElement) return;
const createdAt = Number(contactNameElement.dataset.createdat);
const contact = globalContacts.find((c) => c.createdAt === createdAt);
const contactForm = document.getElementById("contact-form");
const formData = new FormData(contactForm);
await updateContact(contact, formData);
await updateAssignedMembers(contact, formData);
} else {
await createContact();
}
}
/**
* Updates the given contact in the Firebase Realtime Database.
*
* @param {Object} contact - The contact to be updated.
* @returns {Promise<void>}
*/
async function updateContact(contact, formData) {
const contactId = await getContactIdByCreatedAt("guest", contact.createdAt);
if (formData && contactId && validateFormdata()) {
const phoneNumber = formData.get("phone");
const updatedPhoneNumber = phoneNumber.startsWith("0") ? "+49" + phoneNumber.slice(1) : phoneNumber;
const updatedContact = {
...Object.fromEntries(formData),
phone: updatedPhoneNumber,
createdAt: Date.now(),
};
const status = await updateContactInDatabase("guest", contactId, updatedContact);
showToastMessage("update", status);
closeContactModal();
renderContactsPage();
await selectLatestCreatedContact();
}
}
/**
* Updates the assigned members of all todos in the globalTodos array that match the email of the given contact
* with the new name from the given formData. Finally, it patches the updated todos object in the Firebase Realtime Database.
*
* @param {Object} contact - The contact to be updated.
* @param {FormData} formData - The form data containing the new name of the contact.
* @returns {Promise<void>}
*/
async function updateAssignedMembers(contact, formData) {
const newContactName = formData.get("name");
const contactEmail = contact.email;
const updatedTodos = globalTodos.map((todo) => {
const updatedAssignedMembers = Object.fromEntries(
Object.entries(todo.assignedMembers).map(([key, member]) => {
const updatedMember = member.email === contactEmail ? { ...member, name: newContactName } : member;
return [key, updatedMember];
})
);
return { ...todo, assignedMembers: updatedAssignedMembers };
});
await updateTodosInFirebase("guest", arrayToObject(updatedTodos));
}
/**
* Deletes the given contact from the assigned members of all todos in the globalTodos array
* and patches the updated todos object in the Firebase Realtime Database.
*
* @param {Object} contact - The contact to be deleted.
* @returns {Promise<void>}
*/
async function deleteContactFromAssignedMembers(createdAt) {
const contact = globalContacts.find((c) => c.createdAt === createdAt);
const contactEmail = contact.email;
const updatedTodos = globalTodos.map((todo) => {
const updatedAssignedMembers = Object.fromEntries(
Object.entries(todo.assignedMembers).filter(([key, member]) => member.email !== contactEmail)
);
return { ...todo, assignedMembers: updatedAssignedMembers };
});
await updateTodosInFirebase("guest", arrayToObject(updatedTodos));
}
/**
* Creates a new contact with the form data and adds it to the Firebase Realtime
* Database. The function first retrieves the form data, validates it, and
* creates a new contact object by spreading the form data and adding the current
* timestamp for createdAt. The function then calls putDataInFirebase to add the
* contact to the database and shows a toast message with the status of the
* operation. Finally, the function closes the contact modal, renders the
* contacts page and selects the latest created contact.
*
* @returns {Promise<void>}
*/
async function createContact() {
const formData = getFormData();
if (!validateFormdata()) return;
const profileColor = profileColors[Math.floor(Math.random() * profileColors.length)];
const createdAt = Date.now();
const newContact = { ...formData, color: profileColor, contactSelect: false, createdAt };
const status = await createContactInDatabase("guest", newContact);
if (status.status === 200) {
showToastMessage("create", status);
closeContactModal();
renderContactsPage();
await selectLatestCreatedContact();
} else {
showToastMessage("exists", status);
}
}
/**
* Validates the form data in the contact form.
*
* @returns {boolean} True if the form is valid, false otherwise.
*/
function validateFormdata() {
const { name, email, phone } = getFormData();
const nameRegex = /^[A-Z][a-z]+(-[A-Z][a-z]+)* [A-Z][a-z]+$/;
const emailRegex = /^\S+@\S+\.\S+$/;
const phoneRegex = /^\+?\d{1,3}?[-.\s]?(\(?\d{1,5}?\)?[-.\s]?)?\d{5,12}$/;
if (!nameRegex.test(name)) {
showNameWarning();
return false;
}
if (!emailRegex.test(email)) {
showEmailWarning();
return false;
}
if (!phoneRegex.test(phone)) {
showPhoneWarning();
return false;
}
return true;
}
/**
* Gets the form data from the contact form.
*
* @returns {Object} An object with the form data: {name: string, email: string, phone: string}.
*/
function getFormData() {
const contactForm = document.getElementById("contact-form");
const formData = new FormData(contactForm);
const name = formData.get("name");
const email = formData.get("email");
const phone = formData.get("phone");
return { name, email, phone };
}
/**
* Shows a warning message for the contact name input field when the name is not in the
* correct format. The warning message is shown for 2 seconds and then removed.
* @returns {void}
*/
function showNameWarning() {
const inputNameField = document.getElementById("contact-name");
inputNameField.style.borderColor = "red";
inputNameField.insertAdjacentHTML(
"afterend",
`<p style="color: red; font-size: 12px;">Name must be in the format: Firstname Lastname</p>`
);
setTimeout(() => {
inputNameField.style.borderColor = "";
const feedback = inputNameField.nextElementSibling;
if (feedback && feedback.tagName === "P") {
feedback.remove();
}
}, TIMEOUT);
}
/**
* Shows a warning message for the contact email input field when the email is not in the
* correct format. The warning message is shown for 2 seconds and then removed.
* @returns {void}
*/
function showEmailWarning() {
const inputEmailField = document.getElementById("contact-email");
inputEmailField.style.borderColor = "red";
inputEmailField.insertAdjacentHTML(
"afterend",
`<p style="color: red; font-size: 12px;">Email must be in the format: example@domain.com</p>`
);
setTimeout(() => {
inputEmailField.style.borderColor = "";
const feedback = inputEmailField.nextElementSibling;
if (feedback && feedback.tagName === "P") {
feedback.remove();
}
}, TIMEOUT);
}
/**
* Shows a warning message for the contact phone input field when the phone is not in the
* correct format. The warning message is shown for 2 seconds and then removed.
* @returns {void}
*/
function showPhoneWarning() {
const inputPhoneField = document.getElementById("contact-phone");
inputPhoneField.style.borderColor = "red";
inputPhoneField.insertAdjacentHTML(
"afterend",
`<p style="color: red; font-size: 12px;">Phone number cannot be empty</p>`
);
setTimeout(() => {
inputPhoneField.style.borderColor = "";
const feedback = inputPhoneField.nextElementSibling;
if (feedback && feedback.tagName === "P") {
feedback.remove();
}
}, TIMEOUT);
}
/**
* Selects the latest created contact from the list of contacts and shows its details
* in the contact view by calling `toggleContactView` with the index of the contact.
*
* @returns {Promise<void>}
*/
async function selectLatestCreatedContact() {
const latestContact = await getLatestCreatedContact("guest");
const contactElements = [...document.querySelectorAll(".contact-item")];
const selectedContactElement = contactElements.find(
(contactElement) => contactElement.querySelector(".contact-email").textContent === latestContact.email
);
const index = selectedContactElement ? parseInt(selectedContactElement.dataset.sortedIndex, 10) : null;
toggleContactView(index);
}
/**
* Deletes the contact with the id specified in the data-created-at attribute of the
* #createdAt element from the Firebase Realtime Database. If the deletion is successful,
* the contact view is removed and the contacts page is re-rendered.
*
* @returns {Promise<void>}
*/
async function deleteContact() {
const contactCreatedAtElement = document.getElementById("createdAt");
if (!contactCreatedAtElement) return;
const contactId = await getContactIdByCreatedAt("guest", Number(contactCreatedAtElement.dataset.createdat));
await deleteContactFromAssignedMembers(Number(contactCreatedAtElement.dataset.createdat));
if (!contactId) return;
const status = await deleteContactFromDatabase("guest", contactId);
showToastMessage("delete", status);
closeContactModal();
removeContactView();
renderContactsPage();
}