From c42fafb700cedb2c99c87c0b6bdd6680863c76e7 Mon Sep 17 00:00:00 2001 From: roeeHaim Date: Wed, 25 Jun 2025 16:26:46 +0300 Subject: [PATCH 01/46] search user fucntion --- frontend/src/api/userApi.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/frontend/src/api/userApi.js b/frontend/src/api/userApi.js index 6ab9716a..8d82ff80 100644 --- a/frontend/src/api/userApi.js +++ b/frontend/src/api/userApi.js @@ -59,3 +59,25 @@ export async function loginWithJwt(mail, password) { return 500; } } +export async function searchUsers(query) { + try { + const token = localStorage.getItem("token"); + const res = await fetch(`http://localhost:3001/api/users/search?q=${encodeURIComponent(query)}`, { + headers: { + "Authorization": `Bearer ${token}` + } + }); + + if (!res.ok) { + console.error("Search failed:", await res.text()); + return []; + } + + return await res.json(); + } catch (error) { + console.error("Error searching users:", error); + return []; + } +} + + From 7122d360f95ea30e70b36424ca4ace6ff7fe81d9 Mon Sep 17 00:00:00 2001 From: roeeHaim Date: Wed, 25 Jun 2025 17:18:41 +0300 Subject: [PATCH 02/46] search user fucntion --- web_server/controllers/users.js | 152 +++++++++++++++++++++----------- 1 file changed, 101 insertions(+), 51 deletions(-) diff --git a/web_server/controllers/users.js b/web_server/controllers/users.js index fa650f03..f512a863 100644 --- a/web_server/controllers/users.js +++ b/web_server/controllers/users.js @@ -1,24 +1,26 @@ const Users = require('../models/users'); -const {signToken} = require("../utils/authentication"); - +const { signToken } = require("../utils/authentication"); /** - * Signs up a new user to the system, if a user with the same already exists it will not create a new one. - * @param req request - * @param res response + * Signs up a new user to the system, if a user with the same + * mail already exists it will not create a new one. + * + * @param {import('express').Request} req + * @param {import('express').Response} res * @returns - * - code 201 and the new user json object if created successfully - * - code 400 if failed because of an existing mail or a missing attribute + * - 201 and the new user + token if created successfully + * - 400 if mail already exists or a required field is missing */ const signupUser = async (req, res) => { - const {fullName, mail, password, dateOfBirth, image} = req.body; + const { fullName, mail, password, dateOfBirth, image } = req.body; for (const field of ['fullName', 'mail', 'password', 'dateOfBirth']) { if (!req.body[field]) { return res.status(400).json({ error: `${field} is required` }); } } - // Don't create a new user if the mail address is taken already + + // Don't create if the mail is already taken if (Users.userExist(mail)) { return res.status(400).json({ error: 'mail already exists' }); } @@ -39,86 +41,134 @@ const signupUser = async (req, res) => { }; /** - * Get a user by their id - * @param req request - * @param res response + * Get a user by their id. + * + * @param {import('express').Request} req + * @param {import('express').Response} res * @returns - * - code 200 and the user json object (without password) if found - * - code 404 if user wasn't found - * - code 400 if got an invalid id + * - 200 and the user object (without password) + * - 400 if invalid id + * - 404 if no user found */ const getUser = (req, res) => { const id = Number(req.params.id); if (!id || isNaN(id)) { return res.status(400).json({ error: 'Invalid user ID' }); } + const user = Users.getUserById(id); if (!user) { return res.status(404).json({ error: 'User not found' }); } + const { password, ...safeUser } = user; return res.status(200).json(safeUser); }; /** - * Login the user to the system by using mail and password. - * @param req request - * @param res response + * Log in the user using mail and password. + * + * @param {import('express').Request} req + * @param {import('express').Response} res * @returns - * - code 200 and the JWT token if logged in successfully - * - code 401 if input has wrong mail or password - * - code 400 if missing mail or password (or both) + * - 200 and a JWT token if credentials are correct + * - 400 if mail or password missing + * - 401 if wrong mail/password */ const loginUser = async (req, res) => { - // Get the mail and password from the body (according to instructions) - const {mail, password} = req.body; - if (!mail || !password) - return res.status(400).json({error: 'mail and password required'}); + const { mail, password } = req.body; + if (!mail || !password) { + return res.status(400).json({ error: 'mail and password required' }); + } - // Check if the mail and password match the saved ones in order to log in const user = Users.isAuthorizeUser(mail, password); - if (!user) - return res.status(401).json({error: 'wrong mail or password'}); + if (!user) { + return res.status(401).json({ error: 'wrong mail or password' }); + } const token = signToken(user); - return res.status(200).json({token: token}); + return res.status(200).json({ token }); }; /** - * Updates user's attributes (password and/or image) - * @param req request - * @param res response + * Edit user's image. + * + * @param {import('express').Request} req + * @param {import('express').Response} res * @returns - * - code 200 with the updated user object if updated successfully - * - code 400 if received invalid input - * - code 404 if the user doesn't exist + * - 200 and the updated user if successful + * - 400 if invalid user ID or missing/invalid image + * - 404 if user not found */ const editUser = (req, res) => { - // make sure the user is authenticated const id = Number(req.params.id); - if (!id || isNaN(id)) + if (!id || isNaN(id)) { return res.status(400).json({ error: 'Invalid user ID' }); - // check for image input + } + const image = req.body.image; - if (image === undefined || image === null) + if (image === undefined || image === null) { return res.status(400).json({ error: 'Invalid image input' }); - // update the attributes - const user = Users.updateUser(id, undefined, image); - if (user === 400) + } + + const updated = Users.updateUser(id, undefined, image); + if (updated === 400) { return res.status(400).json({ error: 'error invalid input' }); - if (user === 404) + } + if (updated === 404) { return res.status(404).json({ error: 'user not found' }); - return res.status(200).json(user); -} + } + + return res.status(200).json(updated); +}; /** - * Checks for JWT token validity, if we reached here successfully the JWT has been validated - * @param req request - * @param res response - * @returns code 200 + * Check if JWT token is valid. + * (Authentication middleware must run first.) + * + * @param {import('express').Request} req + * @param {import('express').Response} res + * @returns 200 and the user payload from the token */ const isTokenValid = (req, res) => { - return res.status(200).json({user: req.user}); + return res.status(200).json({ user: req.user }); +}; + +/** + * Search users by fullName or mail. + * + * @param {import('express').Request} req + * @param {import('express').Response} res + * @returns + * - 200 and an array of `{ id, name, mail }` (max 10) + * - 400 if missing query parameter `q` + */ +const searchUsers = (req, res) => { + const query = req.query.q?.toLowerCase(); + if (!query) { + return res.status(400).json({ error: 'Missing query parameter' }); + } + + const matched = Users.getAllUsers() + .filter(user => + user.fullName.toLowerCase().includes(query) || + user.mail.toLowerCase().includes(query) + ) + .slice(0, 10) + .map(user => ({ + id: user.id, + name: user.fullName, + mail: user.mail + })); + + return res.status(200).json(matched); }; -module.exports = { signupUser, getUser, loginUser, editUser, isTokenValid }; +module.exports = { + signupUser, + getUser, + loginUser, + editUser, + isTokenValid, + searchUsers +}; From a074d558f43ec660fa15a17693f2b452dc2d6446 Mon Sep 17 00:00:00 2001 From: roeeHaim Date: Wed, 25 Jun 2025 17:19:05 +0300 Subject: [PATCH 03/46] route for the fu nc --- web_server/routes/users.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/web_server/routes/users.js b/web_server/routes/users.js index b2cc6d18..a910a4f9 100644 --- a/web_server/routes/users.js +++ b/web_server/routes/users.js @@ -8,6 +8,10 @@ const {authenticateToken} = require("../utils/authentication"); // POST /api/users router.post('/users', controller.signupUser); +// GET /api/users/search?q=alice +router.get('/users/search', authenticateToken, controller.searchUsers); + + // GET /api/users/:id router.get('/users/:id', controller.getUser); @@ -20,4 +24,5 @@ router.post('/tokens', controller.loginUser); // GET /api/auth-check router.get('/auth-check', authenticateToken, controller.isTokenValid) + module.exports = router; From 3ea59f4aa0740ee3a9cbf9ed8653ad355dc8a9cc Mon Sep 17 00:00:00 2001 From: roeeHaim Date: Wed, 25 Jun 2025 17:19:42 +0300 Subject: [PATCH 04/46] . --- web_server/models/mails.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_server/models/mails.js b/web_server/models/mails.js index d039aa83..8afbda4f 100644 --- a/web_server/models/mails.js +++ b/web_server/models/mails.js @@ -268,7 +268,7 @@ const sendNewMail = (userId, subject, body, sentToIds) => { isTrashed: false, isSpam: false, }; - mails.push(atOwner); + //mails.push(atOwner); // create the mails for the recipients and save each one for (const uid of sentToIds) { const mail = { From a98fbcbffa19df397e59f2e2bb6a2dcc4a3713ce Mon Sep 17 00:00:00 2001 From: roeeHaim Date: Wed, 25 Jun 2025 17:20:02 +0300 Subject: [PATCH 05/46] send mail async func --- frontend/src/api/mailApi.js | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/frontend/src/api/mailApi.js b/frontend/src/api/mailApi.js index 05735688..d34f6e66 100644 --- a/frontend/src/api/mailApi.js +++ b/frontend/src/api/mailApi.js @@ -180,4 +180,25 @@ export const searchMails = async (userId, query) => { if (!res.ok) throw new Error(`Search failed ${res.status}`); return res.json(); -}; \ No newline at end of file +}; + +/** + * POST /api/mails + * @param {{ subject: string, body: string, sentTo: string[] }} mailData + */ +export async function sendMail({ subject, body, sentTo }) { + const url = `${API_BASE}/mails`; + const token = localStorage.getItem("token"); + + const res = await fetch(url, { + method: "POST", + headers: { + "Authorization": `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ subject, body, sentTo }) + }); + + if (!res.ok) throw new Error(`sendMail failed: ${res.status}`); + return await res.json(); +} From ee1fed97b092749c08cf9382132bb7b67139c0d4 Mon Sep 17 00:00:00 2001 From: roeeHaim Date: Wed, 25 Jun 2025 17:20:11 +0300 Subject: [PATCH 06/46] send mail --- .../main_page/ComposeEmail/ComposeEmail.css | 29 +++++++ .../main_page/ComposeEmail/ComposeEmail.jsx | 83 ++++++++++++++++--- 2 files changed, 99 insertions(+), 13 deletions(-) diff --git a/frontend/src/main_page/ComposeEmail/ComposeEmail.css b/frontend/src/main_page/ComposeEmail/ComposeEmail.css index 88e55099..bf2babd1 100644 --- a/frontend/src/main_page/ComposeEmail/ComposeEmail.css +++ b/frontend/src/main_page/ComposeEmail/ComposeEmail.css @@ -157,3 +157,32 @@ .discard-btn:hover { color: #d93025; } +/* Autocomplete suggestion dropdown for "To" field */ +.suggestions-list { + position: absolute; + top: 100%; + left: 64px; /* aligns with input field after label */ + width: calc(100% - 64px); /* align with field-input width */ + max-height: 150px; + overflow-y: auto; + margin-top: 2px; + background: white; + border: 1px solid #dadce0; + border-radius: 4px; + box-shadow: 0 2px 4px rgba(60,64,67,0.15); + z-index: 1100; + font-size: 14px; + list-style: none; + padding: 0; +} + +.suggestions-list li { + padding: 8px 12px; + cursor: pointer; + color: #202124; +} + +.suggestions-list li:hover { + background-color: #f1f3f4; +} + diff --git a/frontend/src/main_page/ComposeEmail/ComposeEmail.jsx b/frontend/src/main_page/ComposeEmail/ComposeEmail.jsx index c54d33a0..197716c6 100644 --- a/frontend/src/main_page/ComposeEmail/ComposeEmail.jsx +++ b/frontend/src/main_page/ComposeEmail/ComposeEmail.jsx @@ -1,21 +1,58 @@ "use client"; - -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; +import { sendMail } from "../../api/mailApi"; +import { searchUsers } from "../../api/userApi"; import 'bootstrap-icons/font/bootstrap-icons.css'; import './ComposeEmail.css'; export default function ComposeEmail({ onCancel, onSend }) { - const [to, setTo] = useState(''); + const [toQuery, setToQuery] = useState(''); + const [suggestions, setSuggestions] = useState([]); + const [selectedUser, setSelectedUser] = useState(null); const [subject, setSubject] = useState(''); const [body, setBody] = useState(''); - const handleSend = () => { - onSend({ to, subject, body }); + useEffect(() => { + const timeout = setTimeout(() => { + if (toQuery.length >= 2 && !selectedUser) { + searchUsers(toQuery).then(setSuggestions); + } else { + setSuggestions([]); + } + }, 300); + return () => clearTimeout(timeout); + }, [toQuery, selectedUser]); + + const handleSend = async () => { + const toEmail = toQuery.trim(); + if (!selectedUser && !toEmail) { + alert("Please select or type a valid recipient."); + return; + } + + try { + const sentTo = selectedUser + ? [selectedUser.mail] + : [toEmail]; + + + + const newMail = await sendMail({ + subject, + body, + sentTo + }); + + console.log("Mail sent:", newMail); + if (onSend) onSend(); // optional callback + } catch (err) { + console.error("Failed to send mail:", err); + alert("Failed to send mail: " + err.message); + } }; return (
- {/* Header with title and controls */}
New Message
@@ -31,18 +68,39 @@ export default function ComposeEmail({ onCancel, onSend }) {
- {/* Body with “To”, “Subject” and formatting toolbar */}
-
+
To setTo(e.target.value)} + type="text" + value={ selectedUser ? selectedUser.mail : toQuery } + + onChange={e => { + setToQuery(e.target.value); + setSelectedUser(null); + }} className="field-input" - placeholder="recipient@example.com" + placeholder="Type a name or email" /> + {suggestions.length > 0 && !selectedUser && ( +
    + {suggestions.map(user => ( +
  • { + setSelectedUser(user); + setToQuery(user.mail); + setSuggestions([]); + }} + + > + {user.mail} +
  • + ))} +
+ )}
+
Subject
- {/* Footer with attach, send and discard buttons */}
- ); -}; + ) +} -export default MainPage; \ No newline at end of file +export default MainPage From d31c3d2a5a73842d2e4be3fbb053ce951861a8f2 Mon Sep 17 00:00:00 2001 From: roeeHaim Date: Sat, 28 Jun 2025 18:50:02 +0300 Subject: [PATCH 08/46] when i click on the draft i can continue writing the mail --- frontend/src/main_page/mail_row/MailRow.jsx | 50 +++++++++++++-------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/frontend/src/main_page/mail_row/MailRow.jsx b/frontend/src/main_page/mail_row/MailRow.jsx index 567d9be3..10556965 100644 --- a/frontend/src/main_page/mail_row/MailRow.jsx +++ b/frontend/src/main_page/mail_row/MailRow.jsx @@ -7,33 +7,45 @@ import useIsMobile from "../../utils/useIsMobile"; /** - * @prop {String} theme dark or light according to user preference - * @prop {Object} email email object - * @prop {string} inboxType what type of inbox we see this mail from (e.g. spam, trash etc) - * @prop {boolean} isSelected whether this row is currently checked - * @prop {function} onSelect when marking a mail as selected for mass actions on them - * @prop {function} onUpdate when updating a mail object this function will trigger + * @prop {String} theme dark or light according to user preference + * @prop {Object} email email object + * @prop {string} inboxType what type of inbox we see this mail from (e.g. spam, trash etc) + * @prop {boolean} isSelected whether this row is currently checked + * @prop {function} onSelect when marking a mail as selected for mass actions on them + * @prop {function} onUpdate when updating a mail object this function will trigger + * @prop {function} onOpenDraft when clicking a draft row, open it in compose window */ -const MailRow = ({theme, email, inboxType, isSelected, onSelect, onUpdate}) => { +const MailRow = ({ + theme, + email, + inboxType, + isSelected, + onSelect, + onUpdate, + onOpenDraft // ADDED + }) => { + const navigate = useNavigate() - // handling clicking on a mail to read it - const navigate = useNavigate(); const handleMailOpen = async () => { - await markMailAsRead(email, onUpdate); - navigate(`/mails/${email.id}`, {state: {inboxType}}); + await markMailAsRead(email, onUpdate) + if (inboxType === 'draft' && onOpenDraft) { + // if this is a draft, open in compose instead of detail view + onOpenDraft(email) + } else { + navigate(`/mails/${email.id}`, { state: { inboxType } }) + } } return (
-
onSelect(email, e.target.checked)} - onClick={(e) => e.stopPropagation()} + onChange={e => onSelect(email, e.target.checked)} + onClick={e => e.stopPropagation()} /> - +
@@ -49,10 +61,12 @@ const MailRow = ({theme, email, inboxType, isSelected, onSelect, onUpdate}) => {
{email.files && email.files.length > 0 && ( - + )} - -
{formatDate(email.sentAt || email.createdAt)}
+ +
+ {formatDate(email.sentAt || email.createdAt)} +
From 479e5032ffc5452250eb6cfc3b0ad91e277a8c63 Mon Sep 17 00:00:00 2001 From: roeeHaim Date: Sat, 28 Jun 2025 18:54:03 +0300 Subject: [PATCH 09/46] when i click on the draft i can continue writing the mail --- frontend/src/api/mailApi.js | 98 ++++++++++++++++++++++++++++++------- 1 file changed, 80 insertions(+), 18 deletions(-) diff --git a/frontend/src/api/mailApi.js b/frontend/src/api/mailApi.js index d34f6e66..8b69bdac 100644 --- a/frontend/src/api/mailApi.js +++ b/frontend/src/api/mailApi.js @@ -9,7 +9,7 @@ const API_BASE = "http://localhost:3001/api"; * @returns {Promise} */ export async function fetchEmail(emailId) { - const url = `${API_BASE}/mails/${emailId}`; + const url = `${API_BASE}/mails/${emailId}`; const token = localStorage.getItem('token'); const res = await fetch(url, { @@ -55,7 +55,7 @@ export async function getMailsByType(inboxType = "all", page = 1) { * @returns {Promise} */ export async function deleteMail(mail) { - const url = `${API_BASE}/mails/${mail.id}`; + const url = `${API_BASE}/mails/${mail.id}`; const token = localStorage.getItem('token'); const res = await fetch(url, { @@ -63,9 +63,11 @@ export async function deleteMail(mail) { headers: { 'Authorization': `Bearer ${token}`, } - }) - if (!res.ok) - throw new Error(`getMails failed: ${res.status}`); + }); + + if (!res.ok) { + throw new Error(`deleteMail failed: ${res.status}`); + } } /** @@ -97,7 +99,7 @@ export async function toggleSpamReport(mail) { * @returns {Promise} */ export async function markAsRead(mailId) { - const url = `${API_BASE}/mails/${mailId}`; + const url = `${API_BASE}/mails/${mailId}`; const token = localStorage.getItem('token'); const res = await fetch(url, { @@ -120,7 +122,7 @@ export async function markAsRead(mailId) { * @returns {Promise} */ export async function toggleMailStar(mail) { - const url = `${API_BASE}/mails/${mail.id}`; + const url = `${API_BASE}/mails/${mail.id}`; const token = localStorage.getItem('token'); const res = await fetch(url, { method: 'PATCH', @@ -143,7 +145,7 @@ export async function toggleMailStar(mail) { * @returns {Promise} */ export async function restoreMail(mail) { - const url = `${API_BASE}/mails/${mail.id}`; + const url = `${API_BASE}/mails/${mail.id}`; const token = localStorage.getItem('token'); const res = await fetch(url, { @@ -167,7 +169,7 @@ export async function restoreMail(mail) { * @returns {Promise} */ export const searchMails = async (userId, query) => { - const url = `${API_BASE}/mails/search/${encodeURIComponent(query)}`; + const url = `${API_BASE}/mails/search/${encodeURIComponent(query)}`; const token = localStorage.getItem('token'); const res = await fetch(url, { @@ -180,25 +182,85 @@ export const searchMails = async (userId, query) => { if (!res.ok) throw new Error(`Search failed ${res.status}`); return res.json(); -}; +} /** - * POST /api/mails - * @param {{ subject: string, body: string, sentTo: string[] }} mailData + * Send a new mail or save as draft. + * @param { { + * subject: string, + * body: string, + * sentTo: string[], + * saveAsDraft?: boolean + * } } mailData + * - subject: email subject + * - body: HTML or plain text body + * - sentTo: list of recipient email addresses + * - saveAsDraft: true to save in Drafts, false to actually send */ -export async function sendMail({ subject, body, sentTo }) { +export async function sendMail({ + subject, + body, + sentTo, + saveAsDraft = false + }) { const url = `${API_BASE}/mails`; const token = localStorage.getItem("token"); + // Comments: what we send in the request body + // subject: the mail's subject line + // body: the mail's content (HTML) + // sentTo: array of recipient emails + // saveAsDraft: boolean flag—true = save to Draft folder + const payload = { + subject, + body, + sentTo, + saveAsDraft + }; + const res = await fetch(url, { method: "POST", headers: { - "Authorization": `Bearer ${token}`, - "Content-Type": "application/json", + "Authorization": `Bearer ${token}`, + "Content-Type": "application/json" }, - body: JSON.stringify({ subject, body, sentTo }) + body: JSON.stringify(payload) }); - if (!res.ok) throw new Error(`sendMail failed: ${res.status}`); - return await res.json(); + if (!res.ok) { + throw new Error(`sendMail failed: ${res.status}`); + } + + return res.json(); } +/** + * PATCH /api/mails/:id + * @param {number} mailId + * @param {{ + * subject?: string, + * body?: string, + * sentTo?: string[], + * saveAsDraft?: boolean + * }} data + */ +export async function updateMail(mailId, { + subject, + body, + sentTo, + saveAsDraft = true +}) { + const url = `${API_BASE}/mails/${mailId}`; + const token = localStorage.getItem("token"); + + const res = await fetch(url, { + method: "PATCH", + headers: { + "Authorization": `Bearer ${token}`, + "Content-Type": "application/json" + }, + body: JSON.stringify({ subject, body, sentTo, saveAsDraft }) + }); + + if (!res.ok) throw new Error(`updateMail failed: ${res.status}`); + return res.json(); +} \ No newline at end of file From d36a203024ca5ab5fba8946fb286b24986412338 Mon Sep 17 00:00:00 2001 From: roeeHaim Date: Sat, 28 Jun 2025 18:54:45 +0300 Subject: [PATCH 10/46] added function to convert to file --- frontend/src/utils/files.js | 51 +++++++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/frontend/src/utils/files.js b/frontend/src/utils/files.js index 03aea995..e2a6e923 100644 --- a/frontend/src/utils/files.js +++ b/frontend/src/utils/files.js @@ -1,20 +1,51 @@ +// frontend/src/utils/files.js + /** - * Converts a file to base64 encoded string in order to upload and store in the backend. - * @param {File} file file to convert - * @returns {Promise} Base64-encoded data URL (e.g. `"data:image/jpeg;base64,..."`) + * Convert any File into a Base64 data URL. + * @param {File} file + * @returns {Promise} e.g. "data:image/png;base64,iVBORw0KG…" */ -export function convertToBase64 (file) { +export function convertToBase64(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); - reader.onload = () => resolve(reader.result); // this is the base64 string + reader.onload = () => resolve(reader.result); reader.onerror = reject; reader.readAsDataURL(file); }); } -export function getFileNamesFromMail(mail) { - if (!mail || !Array.isArray(mail.files)) { - return []; - } - return mail.files.map(file => file.name); +/** + * Convert a File into an attachment descriptor for your mail API: + * { name, type, data } + * where `data` is the raw Base64 (no data:*;base64, prefix). +* +* @param {File} file +* @returns {Promise<{name:string,type:string,data:string}>} +*/ +export function convertToBase64Attachment(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + const [ , base64 ] = reader.result.split(','); + resolve({ + name: file.name, + type: file.type, + data: base64 + }); + }; + reader.onerror = reject; + reader.readAsDataURL(file); + }); +} + +/** + * Given a mail object with mail attachments array, + * return just the original filenames. + * + * @param {{attachments?: Array<{originalName?:string,name?:string}>}} mail + * @returns {string[]} + */ +export function getFilenamesFromMail(mail) { + if (!mail || !Array.isArray(mail.attachments)) return []; + return mail.attachments.map(att => att.originalName || att.name); } From f0a58bd114365b921b90a63b340b6d0b7653b631 Mon Sep 17 00:00:00 2001 From: roeeHaim Date: Sat, 28 Jun 2025 18:55:09 +0300 Subject: [PATCH 11/46] css fix --- .../main_page/ComposeEmail/ComposeEmail.css | 264 +++++++++++++++--- 1 file changed, 223 insertions(+), 41 deletions(-) diff --git a/frontend/src/main_page/ComposeEmail/ComposeEmail.css b/frontend/src/main_page/ComposeEmail/ComposeEmail.css index bf2babd1..85de84ee 100644 --- a/frontend/src/main_page/ComposeEmail/ComposeEmail.css +++ b/frontend/src/main_page/ComposeEmail/ComposeEmail.css @@ -2,9 +2,11 @@ .compose-email { position: fixed; - bottom: 0; - right: 24px; - width: 580px; + bottom: 2vh; + right: 2vw; + width: 35vw; + max-width: 500px; + max-height: 75vh; background: #fff; border: 1px solid #dadce0; border-radius: 8px 8px 0 0; @@ -12,23 +14,39 @@ 0 2px 6px rgba(60,64,67,0.15); display: flex; flex-direction: column; - max-height: 80vh; font-family: 'Roboto', sans-serif; z-index: 1000; + color: #202124; /* default text color */ +} + +.compose-email.minimized .compose-body, +.compose-email.minimized .compose-footer { + display: none; +} + +.compose-email.maximized { + top: 10vh !important; + bottom: 10vh !important; + left: 10vw !important; + right: 10vw !important; + width: auto !important; + height: auto !important; + max-width: none !important; + max-height: none !important; + border-radius: 8px !important; } .compose-header { background: #f1f3f4; - padding: 8px 12px; + padding: 0.5em 1em; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #dadce0; - border-radius: 8px 8px 0 0; } .compose-title { - font-size: 14px; + font-size: 1em; font-weight: 500; color: #202124; } @@ -36,8 +54,8 @@ .compose-controls .control-btn { background: transparent; border: none; - margin-left: 4px; - padding: 4px; + margin-left: 0.5em; + padding: 0.4em; cursor: pointer; color: #5f6368; border-radius: 4px; @@ -49,30 +67,74 @@ } .compose-body { - padding: 8px 12px; + padding: 0.5em 1em; overflow-y: auto; + flex: 1; } .compose-field { display: flex; align-items: center; - margin-bottom: 8px; + margin-bottom: 0.5em; + position: relative; +} + +/* Recipients chips */ +.recipient-input-container { + display: flex; + flex-wrap: wrap; + gap: 4px; + align-items: center; + flex: 1; +} + +.recipient-chip { + background: #e8f0fe; + border: 1px solid #aecbfa; + border-radius: 12px; + padding: 2px 8px; + display: flex; + align-items: center; + font-size: 0.9em; + color: #202124; +} + +.recipient-chip button { + background: transparent; + border: none; + margin-left: 4px; + cursor: pointer; + font-size: 1em; + color: #5f6368; +} + +/* Input for new recipient */ +.to-input { + flex: 1; + min-width: 120px; + padding: 0.4em; + border: none; + outline: none; + color: inherit; } .field-label { - width: 64px; - font-size: 13px; + width: 10%; + min-width: 4em; + font-size: 0.9em; color: #5f6368; } .field-input { flex: 1; - padding: 6px 8px; + padding: 0.5em; border: none; border-bottom: 1px solid #dadce0; - font-size: 14px; + font-size: 1em; outline: none; transition: border-color 0.2s; + color: inherit; + background: transparent; } .field-input:focus { @@ -81,48 +143,90 @@ .compose-toolbar { display: flex; - gap: 12px; - padding: 4px 0 8px; + gap: 0.75em; + padding: 0.5em 0; + font-size: 1em; +} + +.compose-toolbar button, +.compose-toolbar select { + background: transparent; + border: none; color: #5f6368; - font-size: 16px; + cursor: pointer; + padding: 0.4em; + font-size: 1em; + transition: color 0.2s, background 0.2s; } -.compose-toolbar i:hover { +.compose-toolbar button:hover, +.compose-toolbar select:hover { color: #202124; - cursor: pointer; + background: rgba(95,99,104,0.08); + border-radius: 4px; +} + +.font-size-select { + appearance: none; } -.compose-textarea { +.compose-editor { width: 100%; + min-height: 40vh; + padding: 0.75em; border: none; + border-bottom: 1px solid #dadce0; outline: none; - resize: none; - font-size: 14px; + font-size: 1em; line-height: 1.4; - min-height: 200px; + transition: border-bottom-color 0.2s; + direction: ltr; + text-align: left; + color: inherit; + background: transparent; +} + +.compose-editor:focus { + border-bottom-color: #1a73e8; +} + +.attachment-list { + list-style: none; + padding: 0.5em 1em; + margin: 0; +} + +.attachment-list li { + font-size: 0.9em; + color: #202124; } .compose-footer { - padding: 8px 12px; + padding: 0.5em 1em; display: flex; justify-content: space-between; align-items: center; border-top: 1px solid #dadce0; background: #f1f3f4; - border-radius: 0 0 8px 8px; } -.footer-left .attach-btn { +.footer-attach-btn { background: transparent; border: none; - font-size: 18px; + font-size: 1.2em; color: #5f6368; cursor: pointer; + padding: 0.4em; + transition: color 0.2s; +} + +.footer-attach-btn:hover { + color: #202124; } .footer-right { display: flex; - gap: 8px; + gap: 0.5em; } .send-btn { @@ -130,12 +234,12 @@ color: #fff; border: none; border-radius: 4px; - padding: 6px 20px; - font-size: 14px; + padding: 0.5em 1.5em; + font-size: 0.9em; font-weight: 500; display: flex; align-items: center; - gap: 4px; + gap: 0.4em; box-shadow: 0 1px 1px rgba(60,64,67,0.3); cursor: pointer; transition: background 0.2s; @@ -149,7 +253,7 @@ background: transparent; border: none; color: #5f6368; - font-size: 14px; + font-size: 0.9em; cursor: pointer; transition: color 0.2s; } @@ -157,27 +261,27 @@ .discard-btn:hover { color: #d93025; } -/* Autocomplete suggestion dropdown for "To" field */ + .suggestions-list { position: absolute; top: 100%; - left: 64px; /* aligns with input field after label */ - width: calc(100% - 64px); /* align with field-input width */ - max-height: 150px; + left: 10%; + width: 90%; + max-height: 30vh; overflow-y: auto; - margin-top: 2px; - background: white; + margin-top: 0.2em; + background: #fff; border: 1px solid #dadce0; border-radius: 4px; box-shadow: 0 2px 4px rgba(60,64,67,0.15); z-index: 1100; - font-size: 14px; + font-size: 0.9em; list-style: none; padding: 0; } .suggestions-list li { - padding: 8px 12px; + padding: 0.5em 0.75em; cursor: pointer; color: #202124; } @@ -186,3 +290,81 @@ background-color: #f1f3f4; } +/* Dark mode overrides */ +.compose-email.dark { + background: #202124; + border-color: #5f6368; + color: #e8eaed; +} + +.compose-email.dark .compose-header, +.compose-email.dark .compose-footer { + background: #171717; + border-color: #5f6368; +} + +.compose-email.dark .recipient-chip { + background: #3c4043; + border-color: #5f6368; + color: #e8eaed; +} + +.compose-email.dark .field-label { + color: #e8eaed; +} + +.compose-email.dark .field-input, +.compose-email.dark .to-input { + background: #303134; + color: #e8eaed; + border-bottom-color: #5f6368; +} + +.compose-email.dark .field-input::placeholder, +.compose-email.dark .to-input::placeholder { + color: #9aa0a6; +} + +.compose-email.dark .compose-editor { + background: #303134; + color: #e8eaed; + border-bottom-color: #5f6368; +} + +.compose-email.dark .compose-editor:focus { + border-bottom-color: #8ab4f8; +} + +.compose-email.dark .compose-toolbar button, +.compose-email.dark .font-size-select { + color: #e8eaed; +} + +.compose-email.dark .compose-toolbar button:hover { + background: rgba(232,234,237,0.1); +} + +.compose-email.dark .send-btn { + background-color: #8ab4f8; +} + +.compose-email.dark .send-btn:hover { + background-color: #669df6; +} + +.compose-email.dark .suggestions-list { + background: #303134; + border-color: #5f6368; +} + +.compose-email.dark .suggestions-list li { + color: #e8eaed; +} + +.compose-email.dark .suggestions-list li:hover { + background-color: #3c4043; +} + +.compose-email.dark .attachment-list li { + color: #e8eaed; +} From 266f026ccfd3a82f94669c2e0885407892501d96 Mon Sep 17 00:00:00 2001 From: roeeHaim Date: Sat, 28 Jun 2025 18:56:05 +0300 Subject: [PATCH 12/46] added a lot of fucntions to the comose mail --- .../main_page/ComposeEmail/ComposeEmail.jsx | 340 ++++++++++++------ 1 file changed, 223 insertions(+), 117 deletions(-) diff --git a/frontend/src/main_page/ComposeEmail/ComposeEmail.jsx b/frontend/src/main_page/ComposeEmail/ComposeEmail.jsx index 197716c6..23ac41c1 100644 --- a/frontend/src/main_page/ComposeEmail/ComposeEmail.jsx +++ b/frontend/src/main_page/ComposeEmail/ComposeEmail.jsx @@ -5,145 +5,251 @@ import { searchUsers } from "../../api/userApi"; import 'bootstrap-icons/font/bootstrap-icons.css'; import './ComposeEmail.css'; -export default function ComposeEmail({ onCancel, onSend }) { - const [toQuery, setToQuery] = useState(''); - const [suggestions, setSuggestions] = useState([]); - const [selectedUser, setSelectedUser] = useState(null); - const [subject, setSubject] = useState(''); - const [body, setBody] = useState(''); +import { + execCommand, + insertInlineImage, + handleFileAttachments +} from '../../utils/composeUtils' + +export default function ComposeEmail({ + onCancel, + onSend, + offset = 0, + draftMail = null + }) { + // get current theme (adds "dark" or "light" class) + const { theme } = useTheme() + + const [toQuery, setToQuery] = useState('') + const [suggestions, setSuggestions] = useState([]) + const [recipients, setRecipients] = useState([]) + const [subject, setSubject] = useState('') + const [attachments, setAttachments] = useState([]) + const [view, setView] = useState('normal') + + const editorRef = useRef(null) + const inlineImageRef = useRef(null) + const attachRef = useRef(null) + + // prefill when editing + useEffect(() => { + if (!draftMail) return + setRecipients(draftMail.sentTo.map(u => u.mail)) + setSubject(draftMail.subject || '') + if (editorRef.current) { + editorRef.current.innerHTML = draftMail.body || '' + } + setAttachments(draftMail.attachments || []) + setToQuery('') + setSuggestions([]) + }, [draftMail]) + // autocomplete useEffect(() => { - const timeout = setTimeout(() => { - if (toQuery.length >= 2 && !selectedUser) { - searchUsers(toQuery).then(setSuggestions); + const timer = setTimeout(() => { + if (toQuery.length >= 1) { + searchUsers(toQuery).then(setSuggestions) } else { - setSuggestions([]); + setSuggestions([]) } - }, 300); - return () => clearTimeout(timeout); - }, [toQuery, selectedUser]); - - const handleSend = async () => { - const toEmail = toQuery.trim(); - if (!selectedUser && !toEmail) { - alert("Please select or type a valid recipient."); - return; + }, 300) + return () => clearTimeout(timer) + }, [toQuery]) + + const addRecipient = email => { + const e = email.trim() + if (e && !recipients.includes(e)) { + setRecipients(prev => [...prev, e]) } + setToQuery('') + setSuggestions([]) + } + const removeRecipient = email => { + setRecipients(prev => prev.filter(e => e !== email)) + } + const handleKeyDown = e => { + if (e.key === 'Enter' || e.key === ',') { + e.preventDefault() + addRecipient(toQuery) + } + } - try { - const sentTo = selectedUser - ? [selectedUser.mail] - : [toEmail]; - - + // send / draft + const postMail = async saveAsDraft => { + if (!recipients.length) { + alert('Add at least one recipient.') + return + } + const body = editorRef.current.innerHTML - const newMail = await sendMail({ + if (draftMail?.id) { + await updateMail(draftMail.id, { subject, body, - sentTo - }); - - console.log("Mail sent:", newMail); - if (onSend) onSend(); // optional callback - } catch (err) { - console.error("Failed to send mail:", err); - alert("Failed to send mail: " + err.message); + sentTo: recipients, + saveAsDraft + }) + } else { + await sendMail({ + subject, + body, + sentTo: recipients, + attachments, + saveAsDraft + }) } - }; + } + + const handleSend = async () => { await postMail(false); onSend?.() } + const handleClose = async () => { await postMail(true); onCancel() } + const toggleMinimize = () => setView(v => v==='minimized' ? 'normal' : 'minimized') + const toggleMaximize = () => setView(v => v==='maximized' ? 'normal' : 'maximized') + + const rightOffset = `calc(2vw + ${offset * 36}vw)` return ( -
+
- New Message + + {draftMail ? 'Edit Draft' : 'New Message'} +
- - -
-
-
- To - { - setToQuery(e.target.value); - setSelectedUser(null); - }} - className="field-input" - placeholder="Type a name or email" - /> - {suggestions.length > 0 && !selectedUser && ( -
    - {suggestions.map(user => ( -
  • { - setSelectedUser(user); - setToQuery(user.mail); - setSuggestions([]); - }} - - > - {user.mail} -
  • - ))} -
- )} -
- -
- Subject - setSubject(e.target.value)} - className="field-input" - placeholder="Subject" - /> -
- -
- - - - - - -
- -