Source: scripts/contactModal.js

  1. /**
  2. * An array of colors that can be used to color user profiles.
  3. * @type {Array<string>}
  4. */
  5. const profileColors = [
  6. "#FF7A00",
  7. "#FF5EB3",
  8. "#6E52FF",
  9. "#9327FF",
  10. "#00BEE8",
  11. "#1FC7C1",
  12. "#8B9467",
  13. "#FF745E",
  14. "#FFA35E",
  15. "#FC71FF",
  16. "#FFC701",
  17. "#0038FF",
  18. "#B22222",
  19. "#C3FF2B",
  20. "#FFE62B",
  21. "#FF4646",
  22. "#FFBB2B",
  23. "#FF7A00",
  24. "#FF5EB3",
  25. "#6E52FF",
  26. ];
  27. /**
  28. * Timeout in milliseconds that is used for how long the warning in the form
  29. * validation is shown.
  30. * @constant {number}
  31. */
  32. const TIMEOUT = 2000;
  33. /**
  34. * Opens the contact modal and inserts it into the DOM.
  35. *
  36. * Depending on the type, the modal will either be in "add" or "edit" mode.
  37. * If in "edit" mode, the initials are derived from the provided name.
  38. * The contact modal is created using the provided name, email, phone, and color,
  39. * and then inserted into the body of the document.
  40. * The function also applies a "slide-in" animation to the modal.
  41. *
  42. * @param {string} type - The type of the modal, either "add" or "edit".
  43. * @param {string} [name=""] - The name of the contact.
  44. * @param {string} [email=""] - The email of the contact.
  45. * @param {string} [phone=""] - The phone number of the contact.
  46. * @param {string} [color=""] - The color associated with the contact.
  47. */
  48. function openContactModal(type, name = "", email = "", phone = "", color = "") {
  49. const initials = type === "edit" ? getInitialsFromContact({ name: name }) : "";
  50. const modalHtml = getContactModalTemplate(type, name, email, phone, initials, color);
  51. let modalElement = document.getElementById("contact-modal");
  52. if (modalElement) modalElement.remove();
  53. document.body.insertAdjacentHTML("beforeend", modalHtml);
  54. applyAnimation("slide-in");
  55. }
  56. /**
  57. * Closes the contact modal, removing it from the DOM.
  58. *
  59. * @param {Event} [event] - Optional event.
  60. */
  61. function closeContactModal(event) {
  62. if (event) event.preventDefault();
  63. const modal = document.getElementById("contact-modal");
  64. if (modal) {
  65. applyAnimation("slide-out");
  66. modal.addEventListener("animationend", () => modal.remove());
  67. }
  68. }
  69. /**
  70. * Applies the given animation to the modal content element. The animation is
  71. * applied by setting the animation CSS property on the element.
  72. * @param {string} animationType - The type of animation to apply.
  73. */
  74. function applyAnimation(animationType) {
  75. const modalContent = document.getElementById("modal-content");
  76. modalContent.style.animation = `${animationType} 0.3s ease-out forwards`;
  77. }
  78. /**
  79. * Handles the save button click in the contact modal.
  80. *
  81. * If the button says "Save", it will update a contact. It will find the contact
  82. * to update based on the data-created-at attribute of the #createdAt element.
  83. *
  84. * If the button says "Add", it will create a contact.
  85. *
  86. * @param {Event} event - The save button click event.
  87. * @returns {Promise<void>}
  88. */
  89. async function handleSaveClick(event) {
  90. event.preventDefault();
  91. const saveBtn = document.querySelector(".save-btn");
  92. if (!saveBtn) return;
  93. const isSave = saveBtn.innerText.includes("Save");
  94. if (isSave) {
  95. const contactNameElement = document.getElementById("createdAt");
  96. if (!contactNameElement) return;
  97. const createdAt = Number(contactNameElement.dataset.createdat);
  98. const contact = globalContacts.find((c) => c.createdAt === createdAt);
  99. const contactForm = document.getElementById("contact-form");
  100. const formData = new FormData(contactForm);
  101. await updateContact(contact, formData);
  102. await updateAssignedMembers(contact, formData);
  103. } else {
  104. await createContact();
  105. }
  106. }
  107. /**
  108. * Updates the given contact in the Firebase Realtime Database.
  109. *
  110. * @param {Object} contact - The contact to be updated.
  111. * @returns {Promise<void>}
  112. */
  113. async function updateContact(contact, formData) {
  114. const contactId = await getContactIdByCreatedAt("guest", contact.createdAt);
  115. if (formData && contactId && validateFormdata()) {
  116. const phoneNumber = formData.get("phone");
  117. const updatedPhoneNumber = phoneNumber.startsWith("0") ? "+49" + phoneNumber.slice(1) : phoneNumber;
  118. const updatedContact = {
  119. ...Object.fromEntries(formData),
  120. phone: updatedPhoneNumber,
  121. createdAt: Date.now(),
  122. };
  123. const status = await updateContactInDatabase("guest", contactId, updatedContact);
  124. showToastMessage("update", status);
  125. closeContactModal();
  126. renderContactsPage();
  127. await selectLatestCreatedContact();
  128. }
  129. }
  130. /**
  131. * Updates the assigned members of all todos in the globalTodos array that match the email of the given contact
  132. * with the new name from the given formData. Finally, it patches the updated todos object in the Firebase Realtime Database.
  133. *
  134. * @param {Object} contact - The contact to be updated.
  135. * @param {FormData} formData - The form data containing the new name of the contact.
  136. * @returns {Promise<void>}
  137. */
  138. async function updateAssignedMembers(contact, formData) {
  139. const newContactName = formData.get("name");
  140. const contactEmail = contact.email;
  141. const updatedTodos = globalTodos.map((todo) => {
  142. const updatedAssignedMembers = Object.fromEntries(
  143. Object.entries(todo.assignedMembers).map(([key, member]) => {
  144. const updatedMember = member.email === contactEmail ? { ...member, name: newContactName } : member;
  145. return [key, updatedMember];
  146. })
  147. );
  148. return { ...todo, assignedMembers: updatedAssignedMembers };
  149. });
  150. await updateTodosInFirebase("guest", arrayToObject(updatedTodos));
  151. }
  152. /**
  153. * Deletes the given contact from the assigned members of all todos in the globalTodos array
  154. * and patches the updated todos object in the Firebase Realtime Database.
  155. *
  156. * @param {Object} contact - The contact to be deleted.
  157. * @returns {Promise<void>}
  158. */
  159. async function deleteContactFromAssignedMembers(createdAt) {
  160. const contact = globalContacts.find((c) => c.createdAt === createdAt);
  161. const contactEmail = contact.email;
  162. const updatedTodos = globalTodos.map((todo) => {
  163. const updatedAssignedMembers = Object.fromEntries(
  164. Object.entries(todo.assignedMembers).filter(([key, member]) => member.email !== contactEmail)
  165. );
  166. return { ...todo, assignedMembers: updatedAssignedMembers };
  167. });
  168. await updateTodosInFirebase("guest", arrayToObject(updatedTodos));
  169. }
  170. /**
  171. * Creates a new contact with the form data and adds it to the Firebase Realtime
  172. * Database. The function first retrieves the form data, validates it, and
  173. * creates a new contact object by spreading the form data and adding the current
  174. * timestamp for createdAt. The function then calls putDataInFirebase to add the
  175. * contact to the database and shows a toast message with the status of the
  176. * operation. Finally, the function closes the contact modal, renders the
  177. * contacts page and selects the latest created contact.
  178. *
  179. * @returns {Promise<void>}
  180. */
  181. async function createContact() {
  182. const formData = getFormData();
  183. if (!validateFormdata()) return;
  184. const profileColor = profileColors[Math.floor(Math.random() * profileColors.length)];
  185. const createdAt = Date.now();
  186. const newContact = { ...formData, color: profileColor, contactSelect: false, createdAt };
  187. const status = await createContactInDatabase("guest", newContact);
  188. if (status.status === 200) {
  189. showToastMessage("create", status);
  190. closeContactModal();
  191. renderContactsPage();
  192. await selectLatestCreatedContact();
  193. } else {
  194. showToastMessage("exists", status);
  195. }
  196. }
  197. /**
  198. * Validates the form data in the contact form.
  199. *
  200. * @returns {boolean} True if the form is valid, false otherwise.
  201. */
  202. function validateFormdata() {
  203. const { name, email, phone } = getFormData();
  204. const nameRegex = /^[A-Z][a-z]+(-[A-Z][a-z]+)* [A-Z][a-z]+$/;
  205. const emailRegex = /^\S+@\S+\.\S+$/;
  206. const phoneRegex = /^\+?\d{1,3}?[-.\s]?(\(?\d{1,5}?\)?[-.\s]?)?\d{5,12}$/;
  207. if (!nameRegex.test(name)) {
  208. showNameWarning();
  209. return false;
  210. }
  211. if (!emailRegex.test(email)) {
  212. showEmailWarning();
  213. return false;
  214. }
  215. if (!phoneRegex.test(phone)) {
  216. showPhoneWarning();
  217. return false;
  218. }
  219. return true;
  220. }
  221. /**
  222. * Gets the form data from the contact form.
  223. *
  224. * @returns {Object} An object with the form data: {name: string, email: string, phone: string}.
  225. */
  226. function getFormData() {
  227. const contactForm = document.getElementById("contact-form");
  228. const formData = new FormData(contactForm);
  229. const name = formData.get("name");
  230. const email = formData.get("email");
  231. const phone = formData.get("phone");
  232. return { name, email, phone };
  233. }
  234. /**
  235. * Shows a warning message for the contact name input field when the name is not in the
  236. * correct format. The warning message is shown for 2 seconds and then removed.
  237. * @returns {void}
  238. */
  239. function showNameWarning() {
  240. const inputNameField = document.getElementById("contact-name");
  241. inputNameField.style.borderColor = "red";
  242. inputNameField.insertAdjacentHTML(
  243. "afterend",
  244. `<p style="color: red; font-size: 12px;">Name must be in the format: Firstname Lastname</p>`
  245. );
  246. setTimeout(() => {
  247. inputNameField.style.borderColor = "";
  248. const feedback = inputNameField.nextElementSibling;
  249. if (feedback && feedback.tagName === "P") {
  250. feedback.remove();
  251. }
  252. }, TIMEOUT);
  253. }
  254. /**
  255. * Shows a warning message for the contact email input field when the email is not in the
  256. * correct format. The warning message is shown for 2 seconds and then removed.
  257. * @returns {void}
  258. */
  259. function showEmailWarning() {
  260. const inputEmailField = document.getElementById("contact-email");
  261. inputEmailField.style.borderColor = "red";
  262. inputEmailField.insertAdjacentHTML(
  263. "afterend",
  264. `<p style="color: red; font-size: 12px;">Email must be in the format: example@domain.com</p>`
  265. );
  266. setTimeout(() => {
  267. inputEmailField.style.borderColor = "";
  268. const feedback = inputEmailField.nextElementSibling;
  269. if (feedback && feedback.tagName === "P") {
  270. feedback.remove();
  271. }
  272. }, TIMEOUT);
  273. }
  274. /**
  275. * Shows a warning message for the contact phone input field when the phone is not in the
  276. * correct format. The warning message is shown for 2 seconds and then removed.
  277. * @returns {void}
  278. */
  279. function showPhoneWarning() {
  280. const inputPhoneField = document.getElementById("contact-phone");
  281. inputPhoneField.style.borderColor = "red";
  282. inputPhoneField.insertAdjacentHTML(
  283. "afterend",
  284. `<p style="color: red; font-size: 12px;">Phone number cannot be empty</p>`
  285. );
  286. setTimeout(() => {
  287. inputPhoneField.style.borderColor = "";
  288. const feedback = inputPhoneField.nextElementSibling;
  289. if (feedback && feedback.tagName === "P") {
  290. feedback.remove();
  291. }
  292. }, TIMEOUT);
  293. }
  294. /**
  295. * Selects the latest created contact from the list of contacts and shows its details
  296. * in the contact view by calling `toggleContactView` with the index of the contact.
  297. *
  298. * @returns {Promise<void>}
  299. */
  300. async function selectLatestCreatedContact() {
  301. const latestContact = await getLatestCreatedContact("guest");
  302. const contactElements = [...document.querySelectorAll(".contact-item")];
  303. const selectedContactElement = contactElements.find(
  304. (contactElement) => contactElement.querySelector(".contact-email").textContent === latestContact.email
  305. );
  306. const index = selectedContactElement ? parseInt(selectedContactElement.dataset.sortedIndex, 10) : null;
  307. toggleContactView(index);
  308. }
  309. /**
  310. * Deletes the contact with the id specified in the data-created-at attribute of the
  311. * #createdAt element from the Firebase Realtime Database. If the deletion is successful,
  312. * the contact view is removed and the contacts page is re-rendered.
  313. *
  314. * @returns {Promise<void>}
  315. */
  316. async function deleteContact() {
  317. const contactCreatedAtElement = document.getElementById("createdAt");
  318. if (!contactCreatedAtElement) return;
  319. const contactId = await getContactIdByCreatedAt("guest", Number(contactCreatedAtElement.dataset.createdat));
  320. await deleteContactFromAssignedMembers(Number(contactCreatedAtElement.dataset.createdat));
  321. if (!contactId) return;
  322. const status = await deleteContactFromDatabase("guest", contactId);
  323. showToastMessage("delete", status);
  324. closeContactModal();
  325. removeContactView();
  326. renderContactsPage();
  327. }