diff --git a/apps/web/app/(app)/compose/ComposeEmailForm.tsx b/apps/web/app/(app)/compose/ComposeEmailForm.tsx index 1f18e6344..a29cf0135 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 type { 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, }, }); @@ -75,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 || ""}`, @@ -126,7 +131,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 +312,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..d6b00e799 --- /dev/null +++ b/apps/web/app/(app)/compose/ComposeMailBox.tsx @@ -0,0 +1,138 @@ +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, errors } = props; + + 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 ( +
+
+ {[ + { type: CarbonCopyType.CC, width: "w-8", show: !showCC }, + { type: CarbonCopyType.BCC, width: "w-10", show: !showBCC }, + ] + .filter((button) => button.show) + .map((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..704fab1cb --- /dev/null +++ b/apps/web/app/(app)/compose/MultiEmailInput.tsx @@ -0,0 +1,131 @@ +"use client"; + +import { useState, useRef, 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 /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.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 === " " || e.key === "Tab") && 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} + )} +
+ ); +}