From 0468823711dcb054b5732c8fbcb6c47a14a202cd Mon Sep 17 00:00:00 2001 From: Yash Date: Wed, 2 Apr 2025 04:42:51 +0530 Subject: [PATCH 1/3] add multi-email input component and refactor compose email form --- .../app/(app)/compose/ComposeEmailForm.tsx | 23 +-- apps/web/app/(app)/compose/ComposeMailBox.tsx | 150 ++++++++++++++++++ .../web/app/(app)/compose/MultiEmailInput.tsx | 130 +++++++++++++++ 3 files changed, 294 insertions(+), 9 deletions(-) create mode 100644 apps/web/app/(app)/compose/ComposeMailBox.tsx create mode 100644 apps/web/app/(app)/compose/MultiEmailInput.tsx diff --git a/apps/web/app/(app)/compose/ComposeEmailForm.tsx b/apps/web/app/(app)/compose/ComposeEmailForm.tsx index 1f18e6344..06bace8c4 100644 --- a/apps/web/app/(app)/compose/ComposeEmailForm.tsx +++ b/apps/web/app/(app)/compose/ComposeEmailForm.tsx @@ -24,9 +24,10 @@ import { isActionError } from "@/utils/error"; import { Tiptap, type TiptapHandle } from "@/components/editor/Tiptap"; import { sendEmailAction } from "@/utils/actions/mail"; import type { ContactsResponse } from "@/app/api/google/contacts/route"; -import type { SendEmailBody } from "@/utils/gmail/mail"; import { CommandShortcut } from "@/components/ui/command"; import { useModifierKey } from "@/hooks/useModifierKey"; +import ComposeMailBox from "@/app/(app)/compose/ComposeMailBox"; +import { SendEmailBody } from "@/utils/gmail/mail"; export type ReplyingToEmail = { threadId: string; @@ -66,7 +67,8 @@ export const ComposeEmailForm = ({ replyToEmail: replyingToEmail, subject: replyingToEmail?.subject, to: replyingToEmail?.to, - cc: replyingToEmail?.cc, + cc: "", + bcc: "", messageHtml: replyingToEmail?.draftHtml, }, }); @@ -126,7 +128,10 @@ export const ComposeEmailForm = ({ ); // TODO not in love with how this was implemented - const selectedEmailAddressses = watch("to", "").split(",").filter(Boolean); + const toField = watch("to", ""); + const selectedEmailAddressses = ( + Array.isArray(toField) ? toField : toField.split(",") + ).filter(Boolean); const onRemoveSelectedEmail = (emailAddress: string) => { const filteredEmailAddresses = selectedEmailAddressses.filter( @@ -304,12 +309,12 @@ export const ComposeEmailForm = ({ ) : ( - )} diff --git a/apps/web/app/(app)/compose/ComposeMailBox.tsx b/apps/web/app/(app)/compose/ComposeMailBox.tsx new file mode 100644 index 000000000..b2435aec8 --- /dev/null +++ b/apps/web/app/(app)/compose/ComposeMailBox.tsx @@ -0,0 +1,150 @@ +import MultiEmailInput from "@/app/(app)/compose/MultiEmailInput"; +import React, { useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/utils"; + +enum CarbonCopyType { + CC = "cc", + BCC = "bcc", +} + +type ComposeMailBoxProps = { + to: string; + cc?: string; + bcc?: string; + register: any; + errors?: any; +}; + +export default function ComposeMailBox(props: ComposeMailBoxProps) { + const { register, to, cc, bcc, errors } = props; + useEffect(() => { + console.log("ComposeMailBox to", to, typeof to, to?.length); + console.log("ComposeMailBox cc", cc, typeof cc); + console.log("ComposeMailBox bcc", bcc, typeof bcc); + }, [to, cc, bcc]); + + useEffect(() => { + console.log("ComposeMailBox errors", errors); + }, [errors]); + + const [carbonCopy, setCarbonCopy] = useState({ + cc: false, + bcc: false, + }); + + const showCC = carbonCopy.cc; + const showBCC = carbonCopy.bcc; + + const toggleCarbonCopy = (type: CarbonCopyType) => { + setCarbonCopy((prev) => ({ + ...prev, + [type]: !prev[type], + })); + }; + + const moveToggleButtonsToNewLine = + carbonCopy.cc || carbonCopy.bcc || (to && to.length > 0); + + return ( +
+
+ + { + // when no email is present, show the toggle buttons in-line. + !moveToggleButtonsToNewLine && ( + + ) + } +
+ {showCC && ( + + )} + {showBCC && ( + + )} + {/* Moved ToggleButtonsWrapper to a new line below if email is present */} + {moveToggleButtonsToNewLine && ( + + )} +
+ ); +} + +const ToggleButtonsWrapper = ({ + toggleCarbonCopy, + showCC, + showBCC, +}: { + toggleCarbonCopy: (type: CarbonCopyType) => void; + showCC: boolean; + showBCC: boolean; +}) => { + return ( +
+
+ {[ + !showCC && { type: CarbonCopyType.CC, width: "w-8" }, + !showBCC && { type: CarbonCopyType.BCC, width: "w-10" }, + ] + .filter(Boolean) + .map( + (button) => + button && ( + toggleCarbonCopy(button.type)} + /> + ), + )} +
+
+ ); +}; + +const ToggleButton = ({ + label, + className, + onClick, +}: { + label: string; + className?: string; + onClick: () => void; +}) => { + return ( + + ); +}; diff --git a/apps/web/app/(app)/compose/MultiEmailInput.tsx b/apps/web/app/(app)/compose/MultiEmailInput.tsx new file mode 100644 index 000000000..8b2a700fd --- /dev/null +++ b/apps/web/app/(app)/compose/MultiEmailInput.tsx @@ -0,0 +1,130 @@ +"use client"; + +import { + useState, + useRef, + useEffect, + type KeyboardEvent, + type ChangeEvent, +} from "react"; +import { X } from "lucide-react"; +import { Label } from "@/components/ui/label"; +import { cn } from "@/utils"; + +interface MultiEmailInputProps { + name: string; + label: string; + placeholder?: string; + className?: string; + register?: any; + error?: any; +} + +export default function MultiEmailInput({ + name, + label = "To", + placeholder = "", + className = "", + register, + error, +}: MultiEmailInputProps) { + const [emails, setEmails] = useState([]); + const [inputValue, setInputValue] = useState(""); + const inputRef = useRef(null); + + // Email validation regex + const isValidEmail = (email: string) => { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); + }; + + const addEmail = (email: string) => { + const trimmedEmail = email.trim(); + if ( + trimmedEmail && + isValidEmail(trimmedEmail) && + !emails.includes(trimmedEmail) + ) { + const newEmails = [...emails, trimmedEmail]; + setEmails(newEmails); + if (register) { + register(name).onChange({ + target: { name, value: newEmails }, + }); + } + } + setInputValue(""); + }; + + const removeEmail = (index: number) => { + const newEmails = emails.filter((_, i) => i !== index); + setEmails(newEmails); + if (register) { + register(name).onChange({ + target: { name, value: newEmails }, + }); + } + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === " " && inputValue) { + e.preventDefault(); + addEmail(inputValue); + } else if (e.key === "Backspace" && !inputValue && emails.length > 0) { + removeEmail(emails.length - 1); + } + }; + + const handleChange = (e: ChangeEvent) => { + setInputValue(e.target.value); + }; + + const handleBlur = () => { + if (inputValue) { + addEmail(inputValue); + } + }; + + return ( +
+ + {emails.map((email, index) => ( +
+ {email} + +
+ ))} + + + {error && ( + {error.message} + )} +
+ ); +} From e80f327a33fbf55361dd4538068bd4f07fff6a82 Mon Sep 17 00:00:00 2001 From: Yash Date: Sat, 5 Apr 2025 16:44:34 +0530 Subject: [PATCH 2/3] refactor email input handling and improve UI components --- apps/web/app/(app)/compose/ComposeEmailForm.tsx | 3 +++ apps/web/app/(app)/compose/ComposeMailBox.tsx | 9 --------- apps/web/app/(app)/compose/MultiEmailInput.tsx | 10 ++++++++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/web/app/(app)/compose/ComposeEmailForm.tsx b/apps/web/app/(app)/compose/ComposeEmailForm.tsx index 06bace8c4..73353f5f8 100644 --- a/apps/web/app/(app)/compose/ComposeEmailForm.tsx +++ b/apps/web/app/(app)/compose/ComposeEmailForm.tsx @@ -77,6 +77,9 @@ export const ComposeEmailForm = ({ async (data) => { const enrichedData = { ...data, + to: Array.isArray(data.to) ? data.to.join(",") : data.to, + cc: Array.isArray(data.cc) ? data.cc.join(",") : data.cc, + bcc: Array.isArray(data.bcc) ? data.bcc.join(",") : data.bcc, messageHtml: showFullContent ? data.messageHtml || "" : `${data.messageHtml || ""}
${replyingToEmail?.quotedContentHtml || ""}`, diff --git a/apps/web/app/(app)/compose/ComposeMailBox.tsx b/apps/web/app/(app)/compose/ComposeMailBox.tsx index b2435aec8..86b7469ef 100644 --- a/apps/web/app/(app)/compose/ComposeMailBox.tsx +++ b/apps/web/app/(app)/compose/ComposeMailBox.tsx @@ -18,15 +18,6 @@ type ComposeMailBoxProps = { export default function ComposeMailBox(props: ComposeMailBoxProps) { const { register, to, cc, bcc, errors } = props; - useEffect(() => { - console.log("ComposeMailBox to", to, typeof to, to?.length); - console.log("ComposeMailBox cc", cc, typeof cc); - console.log("ComposeMailBox bcc", bcc, typeof bcc); - }, [to, cc, bcc]); - - useEffect(() => { - console.log("ComposeMailBox errors", errors); - }, [errors]); const [carbonCopy, setCarbonCopy] = useState({ cc: false, diff --git a/apps/web/app/(app)/compose/MultiEmailInput.tsx b/apps/web/app/(app)/compose/MultiEmailInput.tsx index 8b2a700fd..61bcb52ff 100644 --- a/apps/web/app/(app)/compose/MultiEmailInput.tsx +++ b/apps/web/app/(app)/compose/MultiEmailInput.tsx @@ -66,7 +66,8 @@ export default function MultiEmailInput({ }; const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === " " && inputValue) { + if ((e.key === " " || e.key === "Tab") && inputValue) { + console.log("came here,", e.key); e.preventDefault(); addEmail(inputValue); } else if (e.key === "Backspace" && !inputValue && emails.length > 0) { @@ -92,7 +93,12 @@ export default function MultiEmailInput({ className, )} > - {emails.map((email, index) => (
{email}