Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
780 changes: 778 additions & 2 deletions backend/package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "mailmern-backend",
"version": "0.1.0",
"main": "src/server.js",
"main": "src/server.js",
"scripts": {
"start": "node src/server.js",
"dev": "nodemon src/server.js"
Expand All @@ -12,7 +12,9 @@
"csv-parser": "^3.2.0",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"googleapis": "^164.1.0",
"jsonwebtoken": "^9.0.2",
"luxon": "^3.7.2",
"mongoose": "^6.9.1",
"multer": "^2.0.2",
"nodemailer": "^6.10.1",
Expand Down
101 changes: 101 additions & 0 deletions backend/src/controllers/googleController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// controllers/googleController.js
const { google } = require("googleapis");
const { DateTime } = require("luxon");

const oauth2Client = new google.auth.OAuth2(
process.env.GOOGLE_CLIENT_ID,
process.env.GOOGLE_CLIENT_SECRET,
"http://localhost:5001/api/google-calendar/oauth2callback"
);

exports.getAuthUrl = (req, res) => {
const url = oauth2Client.generateAuthUrl({
access_type: "offline",
scope: ["https://www.googleapis.com/auth/calendar.events"],
});
res.json({ url });
};

exports.oauthCallback = async (req, res) => {
try {
const { code } = req.query;
const { tokens } = await oauth2Client.getToken(code);
oauth2Client.setCredentials(tokens);
res.send("✅ Google Calendar connected. You can close this tab now.");
} catch (error) {
console.error("OAuth callback error:", error);
res.status(500).send("Failed to authenticate with Google Calendar.");
}
};

exports.scheduleMeeting = async (req, res) => {
try {
const { title, date, time, duration = 30 } = req.body;

if (!oauth2Client.credentials.access_token) {
return res
.status(401)
.json({ success: false, message: "Google Calendar not connected" });
}

const calendar = google.calendar({ version: "v3", auth: oauth2Client });

// Helper: parse AM/PM time
const normalizeTime = (t) => {
const match = t.match(/(\d{1,2})(:(\d{2}))?\s*(am|pm)?/i);
let hour = parseInt(match[1]);
const minute = match[3] ? parseInt(match[3]) : 0;
const meridian = match[4]?.toLowerCase();

if (meridian === "pm" && hour < 12) hour += 12;
if (meridian === "am" && hour === 12) hour = 0;

return { hour, minute };
};

const { hour, minute } = normalizeTime(time);

const startDateTime = DateTime.fromObject(
{
year: Number(date.split("-")[0]),
month: Number(date.split("-")[1]),
day: Number(date.split("-")[2]),
hour,
minute,
},
{ zone: "Asia/Kolkata" }
);

const endDateTime = startDateTime.plus({ minutes: duration });

const event = {
summary: title || "Untitled Meeting",
start: {
dateTime: startDateTime.toISO(),
timeZone: "Asia/Kolkata",
},
end: {
dateTime: endDateTime.toISO(),
timeZone: "Asia/Kolkata",
},
};

const result = await calendar.events.insert({
calendarId: "primary",
resource: event,
});

res.json({
success: true,
message: "Meeting scheduled successfully!",
eventLink: result.data.htmlLink,
start: startDateTime.toFormat("yyyy-MM-dd HH:mm"),
});
} catch (error) {
console.error("❌ Google Calendar error:", error);
res.status(500).json({ success: false, error: error.message });
}
};

// Export OAuth client for reuse if needed
exports.oauth2Client = oauth2Client;
10 changes: 7 additions & 3 deletions backend/src/middlewares/errorMiddleware.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@

export function errorMiddleware(err, req, res, next) {

const statusCode = err.statusCode || 500;
const message = err.message || 'Internal Server Error';
res.status(statusCode).json({
success: false,
message,
stack: process.env.NODE_ENV === 'production' ? null : err.stack
});
next();
}

});
next();
}

15 changes: 15 additions & 0 deletions backend/src/routes/googleRoute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// routes/googleRoute.js
const express = require("express");
const {
getAuthUrl,
oauthCallback,
scheduleMeeting,
} = require("../controllers/googleController");

const router = express.Router();

router.get("/auth", getAuthUrl);
router.get("/oauth2callback", oauthCallback);
router.post("/schedule", scheduleMeeting);

module.exports = router;
9 changes: 6 additions & 3 deletions backend/src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ const contactRoutes = require('./routes/contactRoutes');
const app = express();
app.use(
cors({
origin: "http://localhost:5173",
origin: ["http://localhost:5173"],
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization"],
credentials: true,
})
);
Expand All @@ -23,8 +25,9 @@ app.use('/api/users', userRoutes);
app.use('/api/chatbot', chatbotRoutes);
app.use('/api/auth', userRoutes);
app.use('/api/emails', emailRoutes);
app.use('/api/track', trackRoutes);
app.use('/api/contacts',contactRoutes);
app.use('/api/contacts',contactRoutes);
app.use("/api/google-calendar", googleRoutes);
app.use('/api/track', trackRoutes);

const PORT = process.env.PORT || 5000;

Expand Down
22 changes: 11 additions & 11 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import { Toaster } from "react-hot-toast";
import { Toaster } from "react-hot-toast";

// Your Components
import Navbar from "./components/Navbar";
Expand All @@ -14,7 +14,7 @@ import NotFound from "./pages/NotFound";
import TemplateBuilder from "./pages/Campaign";
import { AuthProvider } from "./context/AuthContext";
import ForgotPassword from "./pages/Forgotpassword";
import Contacts from "./pages/Contact";
import Contacts from "./pages/Contact";

export default function App() {
return (
Expand All @@ -23,24 +23,24 @@ export default function App() {
<div className="app-container">
<Navbar />


<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/chatbot" element={<Chatbot />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/builder" element={<TemplateBuilder />} />
<Route path='/forgot-password' element={<ForgotPassword/>}/>
<Route path="/builder" element={<TemplateBuilder />} />
<Route path='/forgot-password' element={<ForgotPassword/>}/>
<Route path='/contacts' element={<Contacts/>}/>
<Route path="*" element={<NotFound />} />
</Routes>

<Footer />
</Routes>


<Toaster
position="top-right"
<Footer />


<Toaster
position="top-right"
toastOptions={{
success: {
style: {
Expand Down
75 changes: 63 additions & 12 deletions frontend/src/components/ChatbotWidget.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ export default function ChatbotWidget() {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};

const detectMeetingIntent = (text) => {
const t = text.toLowerCase();
// Match phrases like "schedule a meeting", "book meeting", "set meeting", etc.
return /(schedule|book|set|create).*(meeting|call|appointment)/i.test(t);
};


useEffect(() => {
scrollToBottom();
}, [messages]);
Expand All @@ -28,6 +35,36 @@ export default function ChatbotWidget() {
}
}, [isOpen]);


const scheduleMeeting = async (meetingData) => {
try {
const res = await fetch("http://localhost:5001/api/google-calendar/schedule", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(meetingData),
});

const data = await res.json();
if (data.success) {
return `✅ Meeting scheduled successfully! [View on Google Calendar](${data.eventLink})`;
} else {
throw new Error(data.message || "Failed to schedule meeting");
}
} catch (err) {
console.error("Google Calendar not connected, using mock scheduler.");
// fallback mock scheduler
const res = await fetch("http://localhost:5001/api/meetings", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(meetingData),
});
const data = await res.json();
return data.success
? `📅 Meeting added to internal scheduler for ${meetingData.date} at ${meetingData.time}.`
: "❌ Failed to schedule meeting.";
}
};

const sendMessage = async () => {
if (!inputMessage.trim()) return;

Expand All @@ -43,7 +80,24 @@ export default function ChatbotWidget() {
setIsLoading(true);

try {
const response = await fetch('http://localhost:5000/api/chatbot/message', {
if (detectMeetingIntent(inputMessage)) {
const meetingData = {
title: "Chatbot Meeting",
date: new Date().toISOString().split("T")[0],
time: inputMessage.match(/(\d{1,2})(:\d{2})?\s?(am|pm)?/)?.[0] || "10:00 AM",
duration: 30,
userId: "widget-user",
};

const confirmation = await scheduleMeeting(meetingData);
const botMessage = { id: Date.now() + 1, message: confirmation, timestamp: new Date(), isBot: true };
setMessages((prev) => [...prev, botMessage]);
setIsLoading(false);
return;
}

// fallback: normal chatbot message
const response = await fetch('http://localhost:5001/api/chatbot/message', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand All @@ -59,9 +113,7 @@ export default function ChatbotWidget() {
if (data.success) {
const botMessage = {
id: Date.now() + 1,
message: data.data.response,
timestamp: new Date(data.data.timestamp),
isBot: true
message: data.data.response, timestamp: new Date(data.data.timestamp), isBot: true
};

setMessages(prev => [...prev, botMessage]);
Expand Down Expand Up @@ -126,37 +178,36 @@ export default function ChatbotWidget() {
</div>

{/* Messages */}
<div className="flex-1 overflow-y-auto p-3 space-y-2 bg-gray-50">
<div className="flex-1 overflow-y-auto p-3 space-y-2 bg-gray-50 text-black">
{messages.map((msg) => (
<div
key={msg.id}
className={`flex ${msg.isBot ? 'justify-start' : 'justify-end'}`}
>
<div
className={`max-w-xs px-3 py-2 rounded-lg text-sm ${
msg.isBot
className={`max-w-xs px-3 py-2 rounded-lg text-sm ${msg.isBot
? 'bg-white border border-gray-200'
: 'bg-blue-600 text-white'
}`}
: 'bg-blue-600 text-black'
}`}
>
<p>{msg.message}</p>
</div>
</div>
))}

{isLoading && (
<div className="flex justify-start">
<div className="bg-white border border-gray-200 px-3 py-2 rounded-lg">
<p className="text-sm">🤖 Typing...</p>
</div>
</div>
)}

<div ref={messagesEndRef} />
</div>

{/* Input */}
<div className="p-3 bg-white border-t border-gray-200 rounded-b-lg">
<div className="p-3 bg-white border-t border-gray-200 rounded-b-lg text-black">
<div className="flex space-x-2">
<input
type="text"
Expand Down
Loading
Loading