From 44bbed9be20305f085bf3d6c8f79c43916046a34 Mon Sep 17 00:00:00 2001 From: m-zeeshan-saeed Date: Mon, 26 Jan 2026 16:19:31 +0500 Subject: [PATCH 01/15] feat: Implement core backend models and routes for deals, meetings, notifications, and collaboration, alongside integrating corresponding frontend services and pages. --- .env | 2 + backend/database.ts | 64 + backend/middlewares/auth.middleware.ts | 41 + backend/models/CollaborationRequest.model.ts | 26 + backend/models/Deal.model.ts | 41 + backend/models/Document.model.ts | 33 + backend/models/Meeting.model.ts | 42 + backend/models/Message.model.ts | 18 + backend/models/Notification.model.ts | 49 + backend/models/SupportTicket.model.ts | 46 + backend/models/User.model.ts | 57 + backend/routes/auth.routes.ts | 132 ++ backend/routes/collaboration.routes.ts | 108 ++ backend/routes/dashboard.routes.ts | 91 ++ backend/routes/deal.routes.ts | 100 ++ backend/routes/directory.routes.ts | 31 + backend/routes/document.routes.ts | 91 ++ backend/routes/meeting.routes.ts | 177 +++ backend/routes/message.routes.ts | 148 ++ backend/routes/notification.routes.ts | 87 ++ backend/routes/support.routes.ts | 83 ++ backend/routes/user.routes.ts | 99 ++ backend/server.ts | 57 + package-lock.json | 1282 ++++++++++++++++- package.json | 23 +- src/App.tsx | 80 +- src/components/chat/ChatMessage.tsx | 56 +- src/components/chat/ChatUserList.tsx | 86 +- src/components/layout/DashboardLayout.tsx | 24 +- src/components/layout/Sidebar.tsx | 173 ++- src/components/meetings/MeetingCalendar.tsx | 244 ++++ .../meetings/ScheduleMeetingModal.tsx | 163 +++ src/components/ui/Badge.tsx | 55 +- src/components/ui/Card.tsx | 54 +- src/context/AuthContext.tsx | 293 ++-- src/pages/auth/ForgotPasswordPage.tsx | 49 +- src/pages/auth/ResetPasswordPage.tsx | 60 +- src/pages/chat/ChatPage.tsx | 182 ++- src/pages/collaboration/CollaborationPage.tsx | 277 ++++ src/pages/dashboard/EntrepreneurDashboard.tsx | 326 ++++- src/pages/dashboard/InvestorDashboard.tsx | 393 +++-- src/pages/deals/DealsPage.tsx | 612 +++++--- src/pages/documents/DocumentsPage.tsx | 441 ++++-- src/pages/entrepreneurs/EntrepreneursPage.tsx | 182 ++- src/pages/help/HelpPage.tsx | 293 +++- src/pages/investors/InvestorsPage.tsx | 172 ++- src/pages/meetings/MeetingsPage.tsx | 17 + src/pages/messages/MessagesPage.tsx | 54 +- src/pages/notifications/NotificationsPage.tsx | 288 ++-- src/pages/profile/EntrepreneurProfile.tsx | 409 +++--- src/pages/profile/InvestorProfile.tsx | 282 ++-- src/pages/settings/SettingsPage.tsx | 587 ++++++-- src/services/api.ts | 18 + src/services/dashboardService.ts | 21 + src/services/dealService.ts | 33 + src/services/documentService.ts | 32 + src/services/meetingService.ts | 33 + src/services/messageService.ts | 31 + src/services/notificationService.ts | 30 + src/services/supportService.ts | 33 + src/types/index.ts | 111 +- 61 files changed, 7374 insertions(+), 1748 deletions(-) create mode 100644 .env create mode 100644 backend/database.ts create mode 100644 backend/middlewares/auth.middleware.ts create mode 100644 backend/models/CollaborationRequest.model.ts create mode 100644 backend/models/Deal.model.ts create mode 100644 backend/models/Document.model.ts create mode 100644 backend/models/Meeting.model.ts create mode 100644 backend/models/Message.model.ts create mode 100644 backend/models/Notification.model.ts create mode 100644 backend/models/SupportTicket.model.ts create mode 100644 backend/models/User.model.ts create mode 100644 backend/routes/auth.routes.ts create mode 100644 backend/routes/collaboration.routes.ts create mode 100644 backend/routes/dashboard.routes.ts create mode 100644 backend/routes/deal.routes.ts create mode 100644 backend/routes/directory.routes.ts create mode 100644 backend/routes/document.routes.ts create mode 100644 backend/routes/meeting.routes.ts create mode 100644 backend/routes/message.routes.ts create mode 100644 backend/routes/notification.routes.ts create mode 100644 backend/routes/support.routes.ts create mode 100644 backend/routes/user.routes.ts create mode 100644 backend/server.ts create mode 100644 src/components/meetings/MeetingCalendar.tsx create mode 100644 src/components/meetings/ScheduleMeetingModal.tsx create mode 100644 src/pages/collaboration/CollaborationPage.tsx create mode 100644 src/pages/meetings/MeetingsPage.tsx create mode 100644 src/services/api.ts create mode 100644 src/services/dashboardService.ts create mode 100644 src/services/dealService.ts create mode 100644 src/services/documentService.ts create mode 100644 src/services/meetingService.ts create mode 100644 src/services/messageService.ts create mode 100644 src/services/notificationService.ts create mode 100644 src/services/supportService.ts diff --git a/.env b/.env new file mode 100644 index 000000000..312871455 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +MONGODB_URL=mongodb+srv://zeeshansheikh0313_db_user:Zeeshan123@cluster0.czhqovt.mongodb.net/?appName=Cluster0 +JWT_SECRET=088bd22dc60db737b89b0b09608c40c4b631775ddf571ac3b0ceabf5794463a8 diff --git a/backend/database.ts b/backend/database.ts new file mode 100644 index 000000000..6b99b63cd --- /dev/null +++ b/backend/database.ts @@ -0,0 +1,64 @@ +import mongoose from "mongoose"; +import dotenv from "dotenv"; +import bcrypt from "bcryptjs"; +import { entrepreneurs, investors } from "../src/data/users"; +import { messages } from "../src/data/messages"; +import { collaborationRequests } from "../src/data/collaborationRequests"; + +import User from "./models/User.model"; +import Message from "./models/Message.model"; +import CollaborationRequest from "./models/CollaborationRequest.model"; + +dotenv.config(); + +const MONGO_URL = process.env.MONGODB_URL; + +if (!MONGO_URL) { + console.error("MONGODB_URL not found in .env"); + process.exit(1); +} + +async function database() { + try { + await mongoose.connect(MONGO_URL as string); + console.log("Connected to MongoDB for database..."); + + // Clear existing data + await User.deleteMany({}); + await Message.deleteMany({}); + await CollaborationRequest.deleteMany({}); + console.log("Cleared existing collections."); + + // Hash a default password for all mock users + const defaultPassword = await bcrypt.hash("password123", 10); + + // Seed Users + const allUsers = [...entrepreneurs, ...investors].map((user) => ({ + ...user, + password: defaultPassword, + })); + + await User.insertMany(allUsers); + console.log( + `Successfully added ${allUsers.length} users with hashed passwords.`, + ); + + // Seed Messages + await Message.insertMany(messages); + console.log(`Successfully added ${messages.length} messages.`); + + // Seed Collaboration Requests + await CollaborationRequest.insertMany(collaborationRequests); + console.log( + `Successfully added ${collaborationRequests.length} collaboration requests.`, + ); + + console.log("Database added successfully!"); + process.exit(0); + } catch (error) { + console.error("Error adding to database:", error); + process.exit(1); + } +} + +database(); diff --git a/backend/middlewares/auth.middleware.ts b/backend/middlewares/auth.middleware.ts new file mode 100644 index 000000000..f662943ce --- /dev/null +++ b/backend/middlewares/auth.middleware.ts @@ -0,0 +1,41 @@ +import { Request, Response, NextFunction } from "express"; +import jwt from "jsonwebtoken"; + +const JWT_SECRET = process.env.JWT_SECRET || "your_fallback_secret"; + +export interface AuthRequest extends Request { + user?: { + userId: string; + role: string; + }; +} + +export const authenticateToken = ( + req: AuthRequest, + res: Response, + next: NextFunction, +) => { + const authHeader = req.headers["authorization"]; + const token = authHeader && authHeader.split(" ")[1]; + + if (!token) { + return res.status(401).json({ message: "No token provided" }); + } + + jwt.verify(token, JWT_SECRET, (err, decoded) => { + if (err) { + return res.status(403).json({ message: "Invalid or expired token" }); + } + req.user = decoded as { userId: string; role: string }; + next(); + }); +}; + +export const checkRole = (roles: string[]) => { + return (req: AuthRequest, res: Response, next: NextFunction) => { + if (!req.user || !roles.includes(req.user.role)) { + return res.status(403).json({ message: "Access denied" }); + } + next(); + }; +}; diff --git a/backend/models/CollaborationRequest.model.ts b/backend/models/CollaborationRequest.model.ts new file mode 100644 index 000000000..18a5c619c --- /dev/null +++ b/backend/models/CollaborationRequest.model.ts @@ -0,0 +1,26 @@ +import mongoose, { Schema, Document } from "mongoose"; +import { CollaborationRequest } from "../../src/types"; + +export interface ICollaborationRequestDocument + extends Omit, Document { + id: string; +} + +const CollaborationRequestSchema: Schema = new Schema({ + id: { type: String, required: true, unique: true }, + investorId: { type: String, required: true }, + entrepreneurId: { type: String, required: true }, + message: { type: String, required: true }, + status: { + type: String, + enum: ["pending", "accepted", "rejected"], + default: "pending", + }, + createdAt: { type: String, required: true }, +}); + +export default mongoose.models.CollaborationRequest || + mongoose.model( + "CollaborationRequest", + CollaborationRequestSchema, + ); diff --git a/backend/models/Deal.model.ts b/backend/models/Deal.model.ts new file mode 100644 index 000000000..ab98edf86 --- /dev/null +++ b/backend/models/Deal.model.ts @@ -0,0 +1,41 @@ +import mongoose, { Schema, Document } from "mongoose"; + +export interface IDeal { + id: string; + investorId: string; + entrepreneurId: string; + amount: string; + equity: string; + status: "Due Diligence" | "Term Sheet" | "Negotiation" | "Closed" | "Passed"; + stage: string; + notes?: string; + createdAt: string; + lastActivity: string; +} + +export interface IDealDocument extends Omit, Document { + id: string; +} + +const DealSchema: Schema = new Schema({ + id: { type: String, required: true, unique: true }, + investorId: { type: String, required: true }, + entrepreneurId: { type: String, required: true }, + amount: { type: String, required: true }, + equity: { type: String, required: true }, + status: { + type: String, + enum: ["Due Diligence", "Term Sheet", "Negotiation", "Closed", "Passed"], + default: "Due Diligence", + }, + stage: { type: String, required: true }, + notes: { type: String }, + createdAt: { type: String, default: () => new Date().toISOString() }, + lastActivity: { type: String, default: () => new Date().toISOString() }, +}); + +// Index for fetching deals for a specific investor +DealSchema.index({ investorId: 1, lastActivity: -1 }); + +export default mongoose.models.Deal || + mongoose.model("Deal", DealSchema); diff --git a/backend/models/Document.model.ts b/backend/models/Document.model.ts new file mode 100644 index 000000000..6f7b69317 --- /dev/null +++ b/backend/models/Document.model.ts @@ -0,0 +1,33 @@ +import mongoose, { Schema, Document } from "mongoose"; + +export interface IDocument { + id: string; + userId: string; + name: string; + type: string; + size: string; + content: string; // Base64 content + shared: boolean; + createdAt: string; +} + +export interface IDocumentDocument extends Omit, Document { + id: string; +} + +const DocumentSchema: Schema = new Schema({ + id: { type: String, required: true, unique: true }, + userId: { type: String, required: true }, + name: { type: String, required: true }, + type: { type: String, required: true }, + size: { type: String, required: true }, + content: { type: String, required: true }, + shared: { type: Boolean, default: false }, + createdAt: { type: String, default: () => new Date().toISOString() }, +}); + +// Index for fetching documents for a specific user +DocumentSchema.index({ userId: 1, createdAt: -1 }); + +export default mongoose.models.Document || + mongoose.model("Document", DocumentSchema); diff --git a/backend/models/Meeting.model.ts b/backend/models/Meeting.model.ts new file mode 100644 index 000000000..70e1776b7 --- /dev/null +++ b/backend/models/Meeting.model.ts @@ -0,0 +1,42 @@ +import mongoose, { Schema, Document } from "mongoose"; + +export interface IMeeting { + id: string; + title: string; + description?: string; + investorId: string; + entrepreneurId: string; + startTime: string; + endTime: string; + status: "pending" | "accepted" | "rejected" | "cancelled"; + location?: string; + createdAt: string; +} + +export interface IMeetingDocument extends Omit, Document { + id: string; +} + +const MeetingSchema: Schema = new Schema({ + id: { type: String, required: true, unique: true }, + title: { type: String, required: true }, + description: { type: String }, + investorId: { type: String, required: true }, + entrepreneurId: { type: String, required: true }, + startTime: { type: String, required: true }, + endTime: { type: String, required: true }, + status: { + type: String, + enum: ["pending", "accepted", "rejected", "cancelled"], + default: "pending", + }, + location: { type: String }, + createdAt: { type: String, default: () => new Date().toISOString() }, +}); + +// Index for conflict detection queries +MeetingSchema.index({ investorId: 1, startTime: 1, endTime: 1 }); +MeetingSchema.index({ entrepreneurId: 1, startTime: 1, endTime: 1 }); + +export default mongoose.models.Meeting || + mongoose.model("Meeting", MeetingSchema); diff --git a/backend/models/Message.model.ts b/backend/models/Message.model.ts new file mode 100644 index 000000000..efa16fa54 --- /dev/null +++ b/backend/models/Message.model.ts @@ -0,0 +1,18 @@ +import mongoose, { Schema, Document } from "mongoose"; +import { Message } from "../../src/types"; + +export interface IMessageDocument extends Omit, Document { + id: string; +} + +const MessageSchema: Schema = new Schema({ + id: { type: String, required: true, unique: true }, + senderId: { type: String, required: true }, + receiverId: { type: String, required: true }, + content: { type: String, required: true }, + timestamp: { type: String, required: true }, + isRead: { type: Boolean, default: false }, +}); + +export default mongoose.models.Message || + mongoose.model("Message", MessageSchema); diff --git a/backend/models/Notification.model.ts b/backend/models/Notification.model.ts new file mode 100644 index 000000000..2c8422f0f --- /dev/null +++ b/backend/models/Notification.model.ts @@ -0,0 +1,49 @@ +import mongoose, { Schema, Document } from "mongoose"; + +export interface INotification { + id: string; + userId: string; + type: + | "message" + | "collaboration_request" + | "collaboration_accepted" + | "meeting_scheduled" + | "meeting_status"; + title: string; + message: string; + link: string; + isRead: boolean; + createdAt: string; +} + +export interface INotificationDocument + extends Omit, Document { + id: string; +} + +const NotificationSchema: Schema = new Schema({ + id: { type: String, required: true, unique: true }, + userId: { type: String, required: true }, + type: { + type: String, + enum: [ + "message", + "collaboration_request", + "collaboration_accepted", + "meeting_scheduled", + "meeting_status", + ], + required: true, + }, + title: { type: String, required: true }, + message: { type: String, required: true }, + link: { type: String, required: true }, + isRead: { type: Boolean, default: false }, + createdAt: { type: String, default: () => new Date().toISOString() }, +}); + +// Index for fetching notifications for a specific user +NotificationSchema.index({ userId: 1, createdAt: -1 }); + +export default mongoose.models.Notification || + mongoose.model("Notification", NotificationSchema); diff --git a/backend/models/SupportTicket.model.ts b/backend/models/SupportTicket.model.ts new file mode 100644 index 000000000..aaf952d37 --- /dev/null +++ b/backend/models/SupportTicket.model.ts @@ -0,0 +1,46 @@ +import mongoose, { Schema, Document } from "mongoose"; + +export interface ISupportTicket { + id: string; + userId: string; + name: string; + email: string; + subject: string; + message: string; + status: "open" | "in-progress" | "resolved" | "closed"; + priority: "low" | "medium" | "high"; + createdAt: string; + updatedAt: string; +} + +export interface ISupportTicketDocument + extends Omit, Document { + id: string; +} + +const SupportTicketSchema: Schema = new Schema({ + id: { type: String, required: true, unique: true }, + userId: { type: String, required: true }, + name: { type: String, required: true }, + email: { type: String, required: true }, + subject: { type: String, required: true }, + message: { type: String, required: true }, + status: { + type: String, + enum: ["open", "in-progress", "resolved", "closed"], + default: "open", + }, + priority: { + type: String, + enum: ["low", "medium", "high"], + default: "medium", + }, + createdAt: { type: String, default: () => new Date().toISOString() }, + updatedAt: { type: String, default: () => new Date().toISOString() }, +}); + +// Index for fetching tickets for a specific user +SupportTicketSchema.index({ userId: 1, createdAt: -1 }); + +export default mongoose.models.SupportTicket || + mongoose.model("SupportTicket", SupportTicketSchema); diff --git a/backend/models/User.model.ts b/backend/models/User.model.ts new file mode 100644 index 000000000..e0a227a6d --- /dev/null +++ b/backend/models/User.model.ts @@ -0,0 +1,57 @@ +import mongoose, { Schema, Document } from "mongoose"; +import { User } from "../../src/types"; + +export interface IUserDocument extends Omit, Document { + id: string; // Keep the 'id' field for compatibility with frontend mock data + password: string; + startupName?: string; + pitchSummary?: string; + fundingNeeded?: string; + industry?: string; + location?: string; + foundedYear?: number; + teamSize?: number; + investmentInterests?: string[]; + investmentStage?: string[]; + portfolioCompanies?: string[]; + totalInvestments?: number; + minimumInvestment?: string; + maximumInvestment?: string; +} + +const UserSchema: Schema = new Schema( + { + id: { type: String, required: true, unique: true }, + name: { type: String, required: true }, + email: { type: String, required: true, unique: true }, + password: { type: String, required: true }, + role: { type: String, enum: ["entrepreneur", "investor"], required: true }, + avatarUrl: { type: String, default: "https://via.placeholder.com/150" }, + bio: { type: String, default: "" }, + isOnline: { type: Boolean, default: false }, + createdAt: { type: String, default: () => new Date().toISOString() }, + + // Entrepreneur fields + startupName: { type: String }, + pitchSummary: { type: String }, + fundingNeeded: { type: String }, + industry: { type: String }, + location: { type: String }, + foundedYear: { type: Number }, + teamSize: { type: Number }, + + // Investor fields + investmentInterests: [{ type: String }], + investmentStage: [{ type: String }], + portfolioCompanies: [{ type: String }], + totalInvestments: { type: Number }, + minimumInvestment: { type: String }, + maximumInvestment: { type: String }, + }, + { + timestamps: false, // Using custom createdAt from mock data + }, +); + +export default mongoose.models.User || + mongoose.model("User", UserSchema); diff --git a/backend/routes/auth.routes.ts b/backend/routes/auth.routes.ts new file mode 100644 index 000000000..14b10232f --- /dev/null +++ b/backend/routes/auth.routes.ts @@ -0,0 +1,132 @@ +import express from "express"; +import bcrypt from "bcryptjs"; +import jwt from "jsonwebtoken"; +import User from "../models/User.model"; + +const router = express.Router(); + +const JWT_SECRET = process.env.JWT_SECRET || "your_fallback_secret"; + +// User Registration +router.post("/register", async (req, res) => { + try { + const { name, email, password, role, ...extraInfo } = req.body; + + // Check if user already exists + const existingUser = await User.findOne({ email }); + if (existingUser) { + return res.status(400).json({ message: "User already exists" }); + } + + // Hash password + const hashedPassword = await bcrypt.hash(password, 10); + + // Create new user + const newUser = new User({ + id: `u${Date.now()}`, + name, + email, + password: hashedPassword, + role, + avatarUrl: extraInfo.avatarUrl || "https://via.placeholder.com/150", + bio: extraInfo.bio || "", + createdAt: new Date().toISOString(), + ...extraInfo, + }); + + await newUser.save(); + + res.status(201).json({ message: "User registered successfully" }); + } catch (error) { + console.error("Registration error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +// User Login +router.post("/login", async (req, res) => { + try { + const { email, password } = req.body; + console.log("Login attempt for:", email); + + // Find user + const user = await User.findOne({ email }); + if (!user) { + console.log("Login failed: User not found"); + return res.status(400).json({ message: "Invalid email or password" }); + } + + // Validate password + const isMatch = await bcrypt.compare(password, user.password); + if (!isMatch) { + console.log("Login failed: Wrong password"); + return res.status(400).json({ message: "Invalid email or password" }); + } + + // Create JWT + const token = jwt.sign({ userId: user.id, role: user.role }, JWT_SECRET, { + expiresIn: "1d", + }); + + console.log("Login successful for:", email); + res.json({ + token, + user: { + id: user.id, + name: user.name, + email: user.email, + role: user.role, + }, + }); + } catch (error) { + console.error("Login error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +// Forgot Password +router.post("/forgot-password", async (req, res) => { + try { + const { email } = req.body; + const user = await User.findOne({ email }); + + if (!user) { + return res.status(404).json({ message: "User not found" }); + } + + const resetToken = jwt.sign({ userId: user.id }, JWT_SECRET, { + expiresIn: "10m", + }); + res.json({ message: "Password reset instructions sent", resetToken }); + } catch (error) { + console.error("Forgot password error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +// Reset Password +router.post("/reset-password", async (req, res) => { + try { + const { token, newPassword } = req.body; + + const decoded = jwt.verify(token, JWT_SECRET) as { userId: string }; + const hashedPassword = await bcrypt.hash(newPassword, 10); + + const user = await User.findOneAndUpdate( + { id: decoded.userId }, + { $set: { password: hashedPassword } }, + { new: true }, + ); + + if (!user) { + return res.status(404).json({ message: "User not found" }); + } + + res.json({ message: "Password reset successful" }); + } catch (error) { + console.error("Reset password error:", error); + res.status(400).json({ message: "Invalid or expired token" }); + } +}); + +export default router; diff --git a/backend/routes/collaboration.routes.ts b/backend/routes/collaboration.routes.ts new file mode 100644 index 000000000..b04fb2f27 --- /dev/null +++ b/backend/routes/collaboration.routes.ts @@ -0,0 +1,108 @@ +import express from "express"; +import CollaborationRequest from "../models/CollaborationRequest.model"; +import Notification from "../models/Notification.model"; +import { authenticateToken, AuthRequest } from "../middlewares/auth.middleware"; + +const router = express.Router(); + +// Get requests for current user (as entrepreneur or investor) +router.get("/", authenticateToken, async (req: AuthRequest, res) => { + try { + const { userId, role } = req.user!; + let query = {}; + + if (role === "entrepreneur") { + query = { entrepreneurId: userId }; + } else { + query = { investorId: userId }; + } + + const requests = await CollaborationRequest.find(query).sort({ + createdAt: -1, + }); + res.json(requests); + } catch (error) { + console.error("Fetch requests error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +// Create a new request +router.post("/", authenticateToken, async (req: AuthRequest, res) => { + try { + const { entrepreneurId, message } = req.body; + const { userId: investorId } = req.user!; + + const newRequest = new CollaborationRequest({ + id: `req${Date.now()}`, + investorId, + entrepreneurId, + message, + status: "pending", + createdAt: new Date().toISOString(), + }); + + await newRequest.save(); + + // Create notification for entrepreneur + const notification = new Notification({ + id: `notif${Date.now()}`, + userId: entrepreneurId, + type: "collaboration_request", + title: "New Collaboration Request", + message: `An investor has sent you a collaboration request.`, + link: "/dashboard/entrepreneur", + isRead: false, + }); + await notification.save(); + + res.status(201).json(newRequest); + } catch (error) { + console.error("Create request error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +// Update request status +router.put("/:id/status", authenticateToken, async (req: AuthRequest, res) => { + try { + const { status } = req.body; + const { id } = req.params; + + const request = await CollaborationRequest.findOneAndUpdate( + { id }, + { $set: { status } }, + { new: true }, + ); + + if (!request) { + return res.status(404).json({ message: "Request not found" }); + } + + // Create notification for the other user + const targetUserId = + req.user?.role === "entrepreneur" + ? request.investorId + : request.entrepreneurId; + const notification = new Notification({ + id: `notif${Date.now()}`, + userId: targetUserId, + type: status === "accepted" ? "collaboration_accepted" : "meeting_status", // Reusing meeting_status for simplicity or we can add collaboration_rejected + title: `Collaboration Request ${status.charAt(0).toUpperCase() + status.slice(1)}`, + message: `Your collaboration request has been ${status}.`, + link: + req.user?.role === "entrepreneur" + ? "/dashboard/investor" + : "/dashboard/entrepreneur", + isRead: false, + }); + await notification.save(); + + res.json(request); + } catch (error) { + console.error("Update request error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +export default router; diff --git a/backend/routes/dashboard.routes.ts b/backend/routes/dashboard.routes.ts new file mode 100644 index 000000000..e7177d3bd --- /dev/null +++ b/backend/routes/dashboard.routes.ts @@ -0,0 +1,91 @@ +import express from "express"; +import { authenticateToken, AuthRequest } from "../middlewares/auth.middleware"; +import User from "../models/User.model"; +import Collaboration from "../models/CollaborationRequest.model"; +import Meeting, { IMeetingDocument } from "../models/Meeting.model"; +import Message from "../models/Message.model"; +const router = express.Router(); + +interface DashboardStats { + pendingRequests: number; + totalConnections: number; + upcomingMeetings: number; + unreadMessages: number; + recentActivity: unknown[]; + meetings?: IMeetingDocument[]; + profileViews?: number; + totalStartups?: number; +} + +// Get dashboard summary data +router.get("/summary", authenticateToken, async (req: AuthRequest, res) => { + try { + const userId = req.user?.userId; + const userRole = req.user?.role; + + if (!userId) { + return res.status(401).json({ message: "Unauthorized" }); + } + + // Basic stats + const stats: DashboardStats = { + pendingRequests: 0, + totalConnections: 0, + upcomingMeetings: 0, + unreadMessages: 0, + recentActivity: [], + }; + + // 1. Get Collaboration Stats + const collaborations = await Collaboration.find({ + $or: [{ investorId: userId }, { entrepreneurId: userId }], + }); + + stats.pendingRequests = collaborations.filter( + (c) => + c.status === "pending" && + (userRole === "entrepreneur" ? c.entrepreneurId === userId : true), + ).length; + stats.totalConnections = collaborations.filter( + (c) => c.status === "accepted", + ).length; + + // 2. Get Unread Messages + stats.unreadMessages = await Message.countDocuments({ + receiverId: userId, + isRead: false, + }); + + // 2. Get Upcoming Meetings + const now = new Date().toISOString(); + const meetings = await Meeting.find({ + $or: [{ investorId: userId }, { entrepreneurId: userId }], + startTime: { $gte: now }, + status: "accepted", + }) + .sort({ startTime: 1 }) + .limit(5); + + stats.upcomingMeetings = await Meeting.countDocuments({ + $or: [{ investorId: userId }, { entrepreneurId: userId }], + startTime: { $gte: now }, + status: "accepted", + }); + + stats.meetings = meetings; + + // 3. Role specific data + if (userRole === "entrepreneur") { + stats.profileViews = 24 + Math.floor(Math.random() * 10); + } else { + stats.totalStartups = await User.countDocuments({ role: "entrepreneur" }); + } + + res.json(stats); + } catch (error) { + console.error("Dashboard summary error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +export default router; diff --git a/backend/routes/deal.routes.ts b/backend/routes/deal.routes.ts new file mode 100644 index 000000000..08544c4fe --- /dev/null +++ b/backend/routes/deal.routes.ts @@ -0,0 +1,100 @@ +import express from "express"; +import { authenticateToken, AuthRequest } from "../middlewares/auth.middleware"; +import Deal from "../models/Deal.model"; + +const router = express.Router(); + +// Get all deals for current investor +router.get("/", authenticateToken, async (req: AuthRequest, res) => { + try { + const userId = req.user?.userId; + if (!userId) return res.status(401).json({ message: "Unauthorized" }); + + const deals = await Deal.find({ investorId: userId }).sort({ + lastActivity: -1, + }); + res.json(deals); + } catch (error) { + console.error("Fetch deals error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +// Create a new deal +router.post("/", authenticateToken, async (req: AuthRequest, res) => { + try { + const userId = req.user?.userId; + const { entrepreneurId, amount, equity, status, stage, notes } = req.body; + + if (!userId) return res.status(401).json({ message: "Unauthorized" }); + + const newDeal = new Deal({ + id: `deal${Date.now()}`, + investorId: userId, + entrepreneurId, + amount, + equity, + status: status || "Due Diligence", + stage, + notes, + }); + + await newDeal.save(); + res.status(201).json(newDeal); + } catch (error) { + console.error("Create deal error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +// Update a deal +router.put("/:id", authenticateToken, async (req: AuthRequest, res) => { + try { + const userId = req.user?.userId; + const { id } = req.params; + const updates = req.body; + + if (!userId) return res.status(401).json({ message: "Unauthorized" }); + + // Update lastActivity timestamp + updates.lastActivity = new Date().toISOString(); + + const deal = await Deal.findOneAndUpdate( + { id, investorId: userId }, + { $set: updates }, + { new: true }, + ); + + if (!deal) { + return res.status(404).json({ message: "Deal not found" }); + } + + res.json(deal); + } catch (error) { + console.error("Update deal error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +// Delete a deal +router.delete("/:id", authenticateToken, async (req: AuthRequest, res) => { + try { + const userId = req.user?.userId; + const { id } = req.params; + + if (!userId) return res.status(401).json({ message: "Unauthorized" }); + + const deal = await Deal.findOneAndDelete({ id, investorId: userId }); + + if (!deal) { + return res.status(404).json({ message: "Deal not found" }); + } + + res.json({ message: "Deal deleted" }); + } catch (error) { + console.error("Delete deal error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +export default router; diff --git a/backend/routes/directory.routes.ts b/backend/routes/directory.routes.ts new file mode 100644 index 000000000..62f31fb71 --- /dev/null +++ b/backend/routes/directory.routes.ts @@ -0,0 +1,31 @@ +import express from "express"; +import User from "../models/User.model"; +import { authenticateToken } from "../middlewares/auth.middleware"; + +const router = express.Router(); + +// Get investors +router.get("/investors", authenticateToken, async (req, res) => { + try { + const investors = await User.find({ role: "investor" }).select("-password"); + res.json(investors); + } catch (error) { + console.error("Fetch investors error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +// Get entrepreneurs +router.get("/entrepreneurs", authenticateToken, async (req, res) => { + try { + const entrepreneurs = await User.find({ role: "entrepreneur" }).select( + "-password", + ); + res.json(entrepreneurs); + } catch (error) { + console.error("Fetch entrepreneurs error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +export default router; diff --git a/backend/routes/document.routes.ts b/backend/routes/document.routes.ts new file mode 100644 index 000000000..f64ddfdcd --- /dev/null +++ b/backend/routes/document.routes.ts @@ -0,0 +1,91 @@ +import express from "express"; +import { authenticateToken, AuthRequest } from "../middlewares/auth.middleware"; +import Document from "../models/Document.model"; + +const router = express.Router(); + +// Get all documents for current user +router.get("/", authenticateToken, async (req: AuthRequest, res) => { + try { + const userId = req.user?.userId; + if (!userId) return res.status(401).json({ message: "Unauthorized" }); + + const documents = await Document.find({ userId }).sort({ createdAt: -1 }); + res.json(documents); + } catch (error) { + console.error("Fetch documents error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +// Upload a document +router.post("/", authenticateToken, async (req: AuthRequest, res) => { + try { + const userId = req.user?.userId; + const { name, type, size, content, shared } = req.body; + + if (!userId) return res.status(401).json({ message: "Unauthorized" }); + + const newDocument = new Document({ + id: `doc${Date.now()}`, + userId, + name, + type, + size, + content, + shared: shared || false, + }); + + await newDocument.save(); + res.status(201).json(newDocument); + } catch (error) { + console.error("Upload document error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +// Delete a document +router.delete("/:id", authenticateToken, async (req: AuthRequest, res) => { + try { + const userId = req.user?.userId; + const { id } = req.params; + + if (!userId) return res.status(401).json({ message: "Unauthorized" }); + + const document = await Document.findOneAndDelete({ id, userId }); + + if (!document) { + return res.status(404).json({ message: "Document not found" }); + } + + res.json({ message: "Document deleted" }); + } catch (error) { + console.error("Delete document error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +// Toggle document sharing +router.put("/:id/share", authenticateToken, async (req: AuthRequest, res) => { + try { + const userId = req.user?.userId; + const { id } = req.params; + + if (!userId) return res.status(401).json({ message: "Unauthorized" }); + + const document = await Document.findOne({ id, userId }); + if (!document) { + return res.status(404).json({ message: "Document not found" }); + } + + document.shared = !document.shared; + await document.save(); + + res.json(document); + } catch (error) { + console.error("Share document error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +export default router; diff --git a/backend/routes/meeting.routes.ts b/backend/routes/meeting.routes.ts new file mode 100644 index 000000000..85fdb0288 --- /dev/null +++ b/backend/routes/meeting.routes.ts @@ -0,0 +1,177 @@ +import express from "express"; +import Meeting from "../models/Meeting.model"; +import Notification from "../models/Notification.model"; +import { authenticateToken, AuthRequest } from "../middlewares/auth.middleware"; + +const router = express.Router(); + +// Get all meetings for current user +router.get("/", authenticateToken, async (req: AuthRequest, res) => { + try { + const userId = req.user?.userId; + const meetings = await Meeting.find({ + $or: [{ investorId: userId }, { entrepreneurId: userId }], + }).sort({ startTime: 1 }); + + res.json(meetings); + } catch (error) { + console.error("Fetch meetings error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +// Schedule a meeting +router.post("/", authenticateToken, async (req: AuthRequest, res) => { + try { + const { + title, + description, + entrepreneurId, + investorId, + startTime, + endTime, + location, + } = req.body; + + // Validate that the request involves the current user + const currentUserId = req.user?.userId; + if (currentUserId !== entrepreneurId && currentUserId !== investorId) { + return res + .status(403) + .json({ message: "You can only schedule meetings for yourself" }); + } + + // Conflict detection for both parties + const conflict = await Meeting.findOne({ + $or: [ + { investorId }, + { entrepreneurId }, + { investorId: entrepreneurId }, // In case entrepreneur is also an investor in another context + { entrepreneurId: investorId }, // In case investor is also an entrepreneur in another context + ], + status: { $in: ["accepted", "pending"] }, + $and: [{ startTime: { $lt: endTime } }, { endTime: { $gt: startTime } }], + }); + + if (conflict) { + return res.status(409).json({ + message: + "Conflict detected. One of the participants already has a meeting or request during this time.", + conflict: { + title: conflict.title, + startTime: conflict.startTime, + endTime: conflict.endTime, + }, + }); + } + + const newMeeting = new Meeting({ + id: `m${Date.now()}`, + title, + description, + investorId, + entrepreneurId, + startTime, + endTime, + location, + status: "pending", + }); + + await newMeeting.save(); + + // Create notification for the other participant + const targetUserId = + currentUserId === entrepreneurId ? investorId : entrepreneurId; + const notification = new Notification({ + id: `notif${Date.now()}`, + userId: targetUserId, + type: "meeting_scheduled", + title: "New Meeting Scheduled", + message: `A new meeting "${title}" has been scheduled with you.`, + link: "/meetings", + isRead: false, + }); + await notification.save(); + + res.status(201).json(newMeeting); + } catch (error) { + console.error("Schedule meeting error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +// Update meeting status (accept/reject/cancel) +router.put("/:id/status", authenticateToken, async (req: AuthRequest, res) => { + try { + const { status } = req.body; + const userId = req.user?.userId; + + if (!["accepted", "rejected", "cancelled"].includes(status)) { + return res.status(400).json({ message: "Invalid status" }); + } + + const meeting = await Meeting.findOne({ id: req.params.id }); + if (!meeting) { + return res.status(404).json({ message: "Meeting not found" }); + } + + // Authorization checks + if (status === "cancelled") { + if (meeting.investorId !== userId && meeting.entrepreneurId !== userId) { + return res.status(403).json({ message: "Permission denied" }); + } + } else { + // Only the recipient of the invitation (or the other party) should ideally accept/reject + // But for simplicity, we allow either party to manage their status if they are involved + if (meeting.investorId !== userId && meeting.entrepreneurId !== userId) { + return res.status(403).json({ message: "Permission denied" }); + } + } + + meeting.status = status; + await meeting.save(); + + // Create notification for the other user + const targetUserId = + userId === meeting.investorId + ? meeting.entrepreneurId + : meeting.investorId; + const notification = new Notification({ + id: `notif${Date.now()}`, + userId: targetUserId, + type: "meeting_status", + title: `Meeting ${status.charAt(0).toUpperCase() + status.slice(1)}`, + message: `The status of your meeting "${meeting.title}" has been updated to ${status}.`, + link: "/meetings", + isRead: false, + }); + await notification.save(); + + res.json(meeting); + } catch (error) { + console.error("Update meeting status error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +// Get single meeting +router.get("/:id", authenticateToken, async (req: AuthRequest, res) => { + try { + const userId = req.user?.userId; + const meeting = await Meeting.findOne({ + id: req.params.id, + $or: [{ investorId: userId }, { entrepreneurId: userId }], + }); + + if (!meeting) { + return res.status(404).json({ message: "Meeting not found" }); + } + + res.json(meeting); + } catch (error) { + console.error("Fetch meeting error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +export default router; diff --git a/backend/routes/message.routes.ts b/backend/routes/message.routes.ts new file mode 100644 index 000000000..e85a936f9 --- /dev/null +++ b/backend/routes/message.routes.ts @@ -0,0 +1,148 @@ +import express from "express"; +import { authenticateToken, AuthRequest } from "../middlewares/auth.middleware"; +import Message from "../models/Message.model"; +import User from "../models/User.model"; +import Notification from "../models/Notification.model"; + +const router = express.Router(); + +// Get conversations for current user +router.get( + "/conversations", + authenticateToken, + async (req: AuthRequest, res) => { + try { + const userId = req.user?.userId; + if (!userId) return res.status(401).json({ message: "Unauthorized" }); + + // Find all messages involving the user + const messages = await Message.find({ + $or: [{ senderId: userId }, { receiverId: userId }], + }).sort({ timestamp: -1 }); + + // Group by conversation partner + const conversationsMap = new Map(); + + for (const msg of messages) { + const partnerId = + msg.senderId === userId ? msg.receiverId : msg.senderId; + if (!conversationsMap.has(partnerId)) { + // Fetch partner info + const partner = await User.findOne({ id: partnerId }).select( + "name avatarUrl isOnline", + ); + if (partner) { + conversationsMap.set(partnerId, { + id: `conv-${userId}-${partnerId}`, + participants: [userId, partnerId], + partner: { + id: partnerId, + name: partner.name, + avatarUrl: partner.avatarUrl, + isOnline: partner.isOnline, + }, + lastMessage: msg, + updatedAt: msg.timestamp, + }); + } + } + } + + res.json(Array.from(conversationsMap.values())); + } catch (error) { + console.error("Fetch conversations error:", error); + res.status(500).json({ message: "Server error" }); + } + }, +); + +// Get total unread count +router.get( + "/unread-count", + authenticateToken, + async (req: AuthRequest, res) => { + try { + const userId = req.user?.userId; + if (!userId) return res.status(401).json({ message: "Unauthorized" }); + + const count = await Message.countDocuments({ + receiverId: userId, + isRead: false, + }); + + res.json({ count }); + } catch (error) { + console.error("Fetch unread count error:", error); + res.status(500).json({ message: "Server error" }); + } + }, +); + +// Get messages between two users +router.get("/:partnerId", authenticateToken, async (req: AuthRequest, res) => { + try { + const userId = req.user?.userId; + const partnerId = req.params.partnerId; + + if (!userId) return res.status(401).json({ message: "Unauthorized" }); + + const messages = await Message.find({ + $or: [ + { senderId: userId, receiverId: partnerId }, + { senderId: partnerId, receiverId: userId }, + ], + }).sort({ timestamp: 1 }); + + // Mark as read + await Message.updateMany( + { senderId: partnerId, receiverId: userId, isRead: false }, + { $set: { isRead: true } }, + ); + + res.json(messages); + } catch (error) { + console.error("Fetch messages error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +// Send a message +router.post("/", authenticateToken, async (req: AuthRequest, res) => { + try { + const userId = req.user?.userId; + const { receiverId, content } = req.body; + + if (!userId) return res.status(401).json({ message: "Unauthorized" }); + + const newMessage = new Message({ + id: `m${Date.now()}`, + senderId: userId, + receiverId, + content, + timestamp: new Date().toISOString(), + isRead: false, + }); + + await newMessage.save(); + + // Create notification for receiver + const sender = await User.findOne({ id: userId }).select("name"); + const notification = new Notification({ + id: `notif${Date.now()}`, + userId: receiverId, + type: "message", + title: "New Message", + message: `You have received a new message from ${sender?.name || "someone"}.`, + link: `/chat/${userId}`, + isRead: false, + }); + await notification.save(); + + res.status(201).json(newMessage); + } catch (error) { + console.error("Send message error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +export default router; diff --git a/backend/routes/notification.routes.ts b/backend/routes/notification.routes.ts new file mode 100644 index 000000000..60d019434 --- /dev/null +++ b/backend/routes/notification.routes.ts @@ -0,0 +1,87 @@ +import express from "express"; +import { authenticateToken, AuthRequest } from "../middlewares/auth.middleware"; +import Notification from "../models/Notification.model"; + +const router = express.Router(); + +// Get all notifications for current user +router.get("/", authenticateToken, async (req: AuthRequest, res) => { + try { + const userId = req.user?.userId; + if (!userId) return res.status(401).json({ message: "Unauthorized" }); + + const notifications = await Notification.find({ userId }).sort({ + createdAt: -1, + }); + res.json(notifications); + } catch (error) { + console.error("Fetch notifications error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +// Mark notification as read +router.put("/:id/read", authenticateToken, async (req: AuthRequest, res) => { + try { + const userId = req.user?.userId; + const { id } = req.params; + + if (!userId) return res.status(401).json({ message: "Unauthorized" }); + + const notification = await Notification.findOneAndUpdate( + { id, userId }, + { $set: { isRead: true } }, + { new: true }, + ); + + if (!notification) { + return res.status(404).json({ message: "Notification not found" }); + } + + res.json(notification); + } catch (error) { + console.error("Update notification error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +// Mark all notifications as read +router.put("/read-all", authenticateToken, async (req: AuthRequest, res) => { + try { + const userId = req.user?.userId; + if (!userId) return res.status(401).json({ message: "Unauthorized" }); + + await Notification.updateMany( + { userId, isRead: false }, + { $set: { isRead: true } }, + ); + + res.json({ message: "All notifications marked as read" }); + } catch (error) { + console.error("Read all notifications error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +// Delete a notification +router.delete("/:id", authenticateToken, async (req: AuthRequest, res) => { + try { + const userId = req.user?.userId; + const { id } = req.params; + + if (!userId) return res.status(401).json({ message: "Unauthorized" }); + + const notification = await Notification.findOneAndDelete({ id, userId }); + + if (!notification) { + return res.status(404).json({ message: "Notification not found" }); + } + + res.json({ message: "Notification deleted" }); + } catch (error) { + console.error("Delete notification error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +export default router; diff --git a/backend/routes/support.routes.ts b/backend/routes/support.routes.ts new file mode 100644 index 000000000..2d46d02a7 --- /dev/null +++ b/backend/routes/support.routes.ts @@ -0,0 +1,83 @@ +import express from "express"; +import { authenticateToken, AuthRequest } from "../middlewares/auth.middleware"; +import SupportTicket from "../models/SupportTicket.model"; + +const router = express.Router(); + +// Get all tickets for current user +router.get("/tickets", authenticateToken, async (req: AuthRequest, res) => { + try { + const userId = req.user?.userId; + if (!userId) return res.status(401).json({ message: "Unauthorized" }); + + const tickets = await SupportTicket.find({ userId }).sort({ + createdAt: -1, + }); + res.json(tickets); + } catch (error) { + console.error("Fetch tickets error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +// Create a new support ticket +router.post("/tickets", authenticateToken, async (req: AuthRequest, res) => { + try { + const userId = req.user?.userId; + const { name, email, subject, message, priority } = req.body; + + if (!userId) return res.status(401).json({ message: "Unauthorized" }); + + const newTicket = new SupportTicket({ + id: `ticket${Date.now()}`, + userId, + name, + email, + subject: subject || "Support Request", + message, + priority: priority || "medium", + status: "open", + }); + + await newTicket.save(); + res.status(201).json(newTicket); + } catch (error) { + console.error("Create ticket error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +// Update ticket status (could be used by admin in future) +router.put("/tickets/:id", authenticateToken, async (req: AuthRequest, res) => { + try { + const userId = req.user?.userId; + const { id } = req.params; + const { status, priority } = req.body; + + if (!userId) return res.status(401).json({ message: "Unauthorized" }); + + const updates: any = { + updatedAt: new Date().toISOString(), + }; + + if (status) updates.status = status; + if (priority) updates.priority = priority; + + const ticket = await SupportTicket.findOneAndUpdate( + { id, userId }, + { $set: updates }, + { new: true }, + ); + + if (!ticket) { + return res.status(404).json({ message: "Ticket not found" }); + } + + res.json(ticket); + } catch (error) { + console.error("Update ticket error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +export default router; diff --git a/backend/routes/user.routes.ts b/backend/routes/user.routes.ts new file mode 100644 index 000000000..bf47667e7 --- /dev/null +++ b/backend/routes/user.routes.ts @@ -0,0 +1,99 @@ +import express from "express"; +import User from "../models/User.model"; +import { authenticateToken, AuthRequest } from "../middlewares/auth.middleware"; +import bcrypt from "bcryptjs"; + +const router = express.Router(); + +// Get current user profile +router.get("/profile", authenticateToken, async (req: AuthRequest, res) => { + try { + const user = await User.findOne({ id: req.user?.userId }).select( + "-password", + ); + if (!user) { + return res.status(404).json({ message: "User not found" }); + } + res.json(user); + } catch (error) { + console.error("Fetch profile error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +// Get user profile by ID +router.get("/:id", authenticateToken, async (req, res) => { + try { + const user = await User.findOne({ id: req.params.id }).select("-password"); + if (!user) { + return res.status(404).json({ message: "User not found" }); + } + res.json(user); + } catch (error) { + console.error("Fetch profile by ID error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +// Update user profile +router.put("/profile", authenticateToken, async (req: AuthRequest, res) => { + try { + const updates = req.body; + + // Prevent sensitive fields from being updated directly here + delete updates.password; + delete updates.email; + delete updates.id; + delete updates.role; + + const user = await User.findOneAndUpdate( + { id: req.user?.userId }, + { $set: updates }, + { new: true }, + ).select("-password"); + + if (!user) { + return res.status(404).json({ message: "User not found" }); + } + + res.json(user); + } catch (error) { + console.error("Update profile error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +// Change password +router.put( + "/change-password", + authenticateToken, + async (req: AuthRequest, res) => { + try { + const { currentPassword, newPassword } = req.body; + const userId = req.user?.userId; + + const user = await User.findOne({ id: userId }); + if (!user) { + return res.status(404).json({ message: "User not found" }); + } + + // Verify current password + const isMatch = await bcrypt.compare(currentPassword, user.password); + if (!isMatch) { + return res.status(400).json({ message: "Incorrect current password" }); + } + + // Hash new password + const salt = await bcrypt.genSalt(10); + user.password = await bcrypt.hash(newPassword, salt); + await user.save(); + + res.json({ message: "Password updated successfully" }); + } catch (error) { + console.error("Change password error:", error); + res.status(500).json({ message: "Server error" }); + } + }, +); + +export default router; diff --git a/backend/server.ts b/backend/server.ts new file mode 100644 index 000000000..0020745e9 --- /dev/null +++ b/backend/server.ts @@ -0,0 +1,57 @@ +import "dotenv/config"; +import express from "express"; +import cors from "cors"; +import mongoose from "mongoose"; + +import authRoutes from "./routes/auth.routes"; +import userRoutes from "./routes/user.routes"; +import collaborationRoutes from "./routes/collaboration.routes"; +import directoryRoutes from "./routes/directory.routes"; +import meetingRoutes from "./routes/meeting.routes"; +import dashboardRoutes from "./routes/dashboard.routes"; +import messageRoutes from "./routes/message.routes"; +import notificationRoutes from "./routes/notification.routes"; +import documentRoutes from "./routes/document.routes"; +import dealRoutes from "./routes/deal.routes"; +import supportRoutes from "./routes/support.routes"; + +const app = express(); +const port = process.env.PORT || 3001; +const MONGO_URL = process.env.MONGODB_URL; + +// Middleware +app.use(cors()); +app.use(express.json({ limit: "10mb" })); +app.use(express.urlencoded({ limit: "10mb", extended: true })); + +// MongoDB connection +if (!MONGO_URL) { + throw new Error("Please provide MONGODB_URL in the environment variables"); +} + +mongoose + .connect(MONGO_URL) + .then(() => console.log("Connected to MongoDB")) + .catch((err) => console.error("MongoDB connection error:", err)); + +// Routes +app.use("/api/auth", authRoutes); +app.use("/api/users", userRoutes); +app.use("/api/collaboration", collaborationRoutes); +app.use("/api/directory", directoryRoutes); +app.use("/api/meetings", meetingRoutes); +app.use("/api/dashboard", dashboardRoutes); +app.use("/api/messages", messageRoutes); +app.use("/api/notifications", notificationRoutes); +app.use("/api/documents", documentRoutes); +app.use("/api/deals", dealRoutes); +app.use("/api/support", supportRoutes); + +app.get("/", (req, res) => { + res.send("API is running..."); +}); + +// Start the server +app.listen(port, () => { + console.log(`Server is running on port ${port}`); +}); diff --git a/package-lock.json b/package-lock.json index bf00121ba..79a516652 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,15 @@ "version": "0.1.0", "dependencies": { "axios": "^1.6.7", + "bcryptjs": "^3.0.3", + "cors": "^2.8.6", "date-fns": "^3.3.1", + "dotenv": "^17.2.3", + "express": "^5.2.1", + "jsonwebtoken": "^9.0.3", "lucide-react": "^0.344.0", + "mongodb": "^7.0.0", + "mongoose": "^9.1.5", "react": "^18.3.1", "react-dom": "^18.3.1", "react-dropzone": "^14.2.3", @@ -19,6 +26,10 @@ }, "devDependencies": { "@eslint/js": "^9.9.1", + "@types/bcryptjs": "^2.4.6", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.6", + "@types/jsonwebtoken": "^9.0.10", "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.1", @@ -31,7 +42,7 @@ "tailwindcss": "^3.4.1", "typescript": "^5.5.3", "typescript-eslint": "^8.3.0", - "vite": "^5.4.2" + "vite": "^5.4.21" } }, "node_modules/@alloc/quick-lru": { @@ -86,6 +97,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.7.tgz", "integrity": "sha512-yJ474Zv3cwiSOO9nXJuqzvwEeM+chDuQ8GJirw+pZ91sCGCyOZ3dJkVE09fTV0VEVzXyLWhh3G/AolYTPX7Mow==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.25.7", @@ -955,6 +967,15 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.5.tgz", + "integrity": "sha512-k64Lbyb7ycCSXHSLzxVdb2xsKGPMvYZfCICXvDsI8Z65CeWQzTEKS4YmGbnqw+U9RBvLPTsB6UCmwkgsDTGWIw==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1258,24 +1279,136 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.0.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.10.tgz", + "integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, "node_modules/@types/prop-types": { "version": "15.7.13", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", "dev": true }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/react": { "version": "18.3.11", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.11.tgz", @@ -1295,6 +1428,42 @@ "@types/react": "*" } }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.8.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.1.tgz", @@ -1333,6 +1502,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.8.1.tgz", "integrity": "sha512-hQUVn2Lij2NAxVFEdvIGxT9gP1tq2yM83m+by3whWFsWC+1y8pxxxHUFE1UqDu2VsGi2i6RLcv4QvouM84U+ow==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.8.1", "@typescript-eslint/types": "8.8.1", @@ -1544,11 +1714,50 @@ "vite": "^4.2.0 || ^5.0.0" } }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/acorn": { "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1705,6 +1914,15 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -1717,6 +1935,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1758,6 +2000,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001663", "electron-to-chromium": "^1.5.28", @@ -1771,6 +2014,30 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bson": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/bson/-/bson-7.1.1.tgz", + "integrity": "sha512-TtJgBB+QyOlWjrbM+8bRgH84VM/xrDjyBFgSgGrfZF4xvt6gbEDtcswm27Tn9F9TWsjQybxT8b8VpCP/oJK4Dw==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -1784,6 +2051,22 @@ "node": ">= 0.4" } }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1803,9 +2086,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001667", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001667.tgz", - "integrity": "sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw==", + "version": "1.0.30001766", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz", + "integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==", "dev": true, "funding": [ { @@ -1820,7 +2103,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/chalk": { "version": "2.4.2", @@ -1914,12 +2198,69 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1949,7 +2290,8 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "peer": true }, "node_modules/date-fns": { "version": "3.6.0", @@ -1962,10 +2304,10 @@ } }, "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -1993,6 +2335,15 @@ "node": ">=0.4.0" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -2005,6 +2356,18 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "dev": true }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2025,6 +2388,21 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.33", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.33.tgz", @@ -2037,6 +2415,15 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -2129,6 +2516,12 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -2143,6 +2536,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.12.0.tgz", "integrity": "sha512-UVIOlTEWxwIopRL1wgSQYdnVDcEvs2wyaO6DGo5mXqe3r16IoCNWkR29iHhyaP4cICWjbgbmFUGAhh0GJRuGZw==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.11.0", @@ -2388,6 +2782,83 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2479,6 +2950,27 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2565,6 +3057,15 @@ "node": ">= 6" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -2578,6 +3079,15 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2788,10 +3298,46 @@ "node": ">= 0.4" } }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "engines": { "node": ">= 4" @@ -2822,6 +3368,21 @@ "node": ">=0.8.19" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -2888,6 +3449,12 @@ "node": ">=0.12.0" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2977,6 +3544,70 @@ "node": ">=6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kareem": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-3.0.0.tgz", + "integrity": "sha512-RKhaOBSPN8L7y4yAgNhDT2602G5FD6QbOIISbjN9D6mjHPeqeg7K+EB5IGSU5o81/X2Gzm3ICnAvQW3x3OP8HA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3029,12 +3660,54 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -3072,6 +3745,33 @@ "node": ">= 0.4" } }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -3136,11 +3836,108 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mongodb": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.0.0.tgz", + "integrity": "sha512-vG/A5cQrvGGvZm2mTnCSz1LUcbOPl83hfB6bxULKQ8oFZauyox/2xbZOoGNl+64m8VBrETkdGCDBdOsCr3F3jg==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^7.0.0", + "mongodb-connection-string-url": "^7.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.806.0", + "@mongodb-js/zstd": "^7.0.0", + "gcp-metadata": "^7.0.1", + "kerberos": "^7.0.0", + "mongodb-client-encryption": ">=7.0.0 <7.1.0", + "snappy": "^7.3.2", + "socks": "^2.8.6" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.0.tgz", + "integrity": "sha512-irhhjRVLE20hbkRl4zpAYLnDMM+zIZnp0IDB9akAFFUZp/3XdOfwwddc7y6cNvF2WCEtfTYRwYbIfYa2kVY0og==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^13.0.0", + "whatwg-url": "^14.1.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/mongoose": { + "version": "9.1.5", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.1.5.tgz", + "integrity": "sha512-N6gypEO+wLmZp8kCYNQmrEWxVMT0KhyHvVttBZoKA/1ngY7aUsBjqHzCPtDgz+i8JAnqMOiEKmuJIDEQu1b9Dw==", + "license": "MIT", + "dependencies": { + "kareem": "3.0.0", + "mongodb": "~7.0", + "mpath": "0.9.0", + "mquery": "6.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-6.0.0.tgz", + "integrity": "sha512-b2KQNsmgtkscfeDgkYMcWGn9vZI9YoXh802VDEwE6qc50zxBFQ0Oo8ROkawbPAsXCY1/Z1yp0MagqsZStPWJjw==", + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/mz": { "version": "2.7.0", @@ -3177,6 +3974,15 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/node-releases": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", @@ -3218,6 +4024,39 @@ "node": ">= 6" } }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -3283,6 +4122,15 @@ "node": ">=6" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3329,6 +4177,16 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/picocolors": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", @@ -3384,6 +4242,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.0", @@ -3540,6 +4399,19 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -3550,11 +4422,25 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "engines": { "node": ">=6" } }, + "node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -3575,10 +4461,35 @@ } ] }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -3590,6 +4501,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -3771,6 +4683,22 @@ "fsevents": "~2.3.2" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -3794,6 +4722,32 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -3811,6 +4765,82 @@ "semver": "bin/semver.js" } }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/send/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -3832,6 +4862,84 @@ "node": ">=8" } }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "license": "MIT" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -3853,6 +4961,24 @@ "node": ">=0.10.0" } }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -4092,6 +5218,27 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/ts-api-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", @@ -4128,11 +5275,51 @@ "node": ">= 0.8.0" } }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/typescript": { "version": "5.6.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4164,6 +5351,22 @@ } } }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", @@ -4209,11 +5412,22 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vite": { - "version": "5.4.8", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz", - "integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==", + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, + "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -4268,6 +5482,28 @@ } } }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4413,6 +5649,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index cdfd37b29..2e08b5d6d 100644 --- a/package.json +++ b/package.json @@ -10,17 +10,28 @@ "preview": "vite preview" }, "dependencies": { + "axios": "^1.6.7", + "bcryptjs": "^3.0.3", + "cors": "^2.8.6", + "date-fns": "^3.3.1", + "dotenv": "^17.2.3", + "express": "^5.2.1", + "jsonwebtoken": "^9.0.3", "lucide-react": "^0.344.0", + "mongodb": "^7.0.0", + "mongoose": "^9.1.5", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router-dom": "^6.22.1", - "axios": "^1.6.7", - "date-fns": "^3.3.1", "react-dropzone": "^14.2.3", - "react-hot-toast": "^2.4.1" + "react-hot-toast": "^2.4.1", + "react-router-dom": "^6.22.1" }, "devDependencies": { "@eslint/js": "^9.9.1", + "@types/bcryptjs": "^2.4.6", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.6", + "@types/jsonwebtoken": "^9.0.10", "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.1", @@ -33,6 +44,6 @@ "tailwindcss": "^3.4.1", "typescript": "^5.5.3", "typescript-eslint": "^8.3.0", - "vite": "^5.4.2" + "vite": "^5.4.21" } -} \ No newline at end of file +} diff --git a/src/App.tsx b/src/App.tsx index 51b12d8bf..517bc9f52 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,34 +1,40 @@ -import React from 'react'; -import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; -import { AuthProvider } from './context/AuthContext'; +import { + BrowserRouter as Router, + Routes, + Route, + Navigate, +} from "react-router-dom"; +import { AuthProvider } from "./context/AuthContext"; // Layouts -import { DashboardLayout } from './components/layout/DashboardLayout'; +import { DashboardLayout } from "./components/layout/DashboardLayout"; // Auth Pages -import { LoginPage } from './pages/auth/LoginPage'; -import { RegisterPage } from './pages/auth/RegisterPage'; +import { LoginPage } from "./pages/auth/LoginPage"; +import { RegisterPage } from "./pages/auth/RegisterPage"; // Dashboard Pages -import { EntrepreneurDashboard } from './pages/dashboard/EntrepreneurDashboard'; -import { InvestorDashboard } from './pages/dashboard/InvestorDashboard'; +import { EntrepreneurDashboard } from "./pages/dashboard/EntrepreneurDashboard"; +import { InvestorDashboard } from "./pages/dashboard/InvestorDashboard"; // Profile Pages -import { EntrepreneurProfile } from './pages/profile/EntrepreneurProfile'; -import { InvestorProfile } from './pages/profile/InvestorProfile'; +import { EntrepreneurProfile } from "./pages/profile/EntrepreneurProfile"; +import { InvestorProfile } from "./pages/profile/InvestorProfile"; // Feature Pages -import { InvestorsPage } from './pages/investors/InvestorsPage'; -import { EntrepreneursPage } from './pages/entrepreneurs/EntrepreneursPage'; -import { MessagesPage } from './pages/messages/MessagesPage'; -import { NotificationsPage } from './pages/notifications/NotificationsPage'; -import { DocumentsPage } from './pages/documents/DocumentsPage'; -import { SettingsPage } from './pages/settings/SettingsPage'; -import { HelpPage } from './pages/help/HelpPage'; -import { DealsPage } from './pages/deals/DealsPage'; +import { InvestorsPage } from "./pages/investors/InvestorsPage"; +import { EntrepreneursPage } from "./pages/entrepreneurs/EntrepreneursPage"; +import { MessagesPage } from "./pages/messages/MessagesPage"; +import { NotificationsPage } from "./pages/notifications/NotificationsPage"; +import { DocumentsPage } from "./pages/documents/DocumentsPage"; +import { SettingsPage } from "./pages/settings/SettingsPage"; +import { HelpPage } from "./pages/help/HelpPage"; +import { DealsPage } from "./pages/deals/DealsPage"; +import { CollaborationPage } from "./pages/collaboration/CollaborationPage"; // Chat Pages -import { ChatPage } from './pages/chat/ChatPage'; +import { ChatPage } from "./pages/chat/ChatPage"; +import { MeetingsPage } from "./pages/meetings/MeetingsPage"; function App() { return ( @@ -38,61 +44,69 @@ function App() { {/* Authentication Routes */} } /> } /> - + {/* Dashboard Routes */} }> } /> } /> - + {/* Profile Routes */} }> } /> } /> - + {/* Feature Routes */} }> } /> - + }> } /> - + }> } /> - + }> } /> - + }> } /> - + + }> + } /> + + }> } /> - + }> } /> - + }> } /> - + + }> + } /> + + {/* Chat Routes */} }> } /> } /> - + {/* Redirect root to login */} } /> - + {/* Catch all other routes and redirect to login */} } /> @@ -101,4 +115,4 @@ function App() { ); } -export default App; \ No newline at end of file +export default App; diff --git a/src/components/chat/ChatMessage.tsx b/src/components/chat/ChatMessage.tsx index c3cf93ef4..264435a80 100644 --- a/src/components/chat/ChatMessage.tsx +++ b/src/components/chat/ChatMessage.tsx @@ -1,56 +1,64 @@ -import React from 'react'; -import { formatDistanceToNow } from 'date-fns'; -import { Message } from '../../types'; -import { Avatar } from '../ui/Avatar'; -import { findUserById } from '../../data/users'; - +import React from "react"; +import { formatDistanceToNow } from "date-fns"; +import { Message } from "../../types"; +import { Avatar } from "../ui/Avatar"; interface ChatMessageProps { message: Message; isCurrentUser: boolean; + sender: { + name: string; + avatarUrl: string; + }; } -export const ChatMessage: React.FC = ({ message, isCurrentUser }) => { - const user = findUserById(message.senderId); - - if (!user) return null; - +export const ChatMessage: React.FC = ({ + message, + isCurrentUser, + sender, +}) => { + if (!sender) return null; + return (
{!isCurrentUser && ( )} - -
+ +

{message.content}

- + - {formatDistanceToNow(new Date(message.timestamp), { addSuffix: true })} + {formatDistanceToNow(new Date(message.timestamp), { + addSuffix: true, + })}
- + {isCurrentUser && ( )}
); -}; \ No newline at end of file +}; diff --git a/src/components/chat/ChatUserList.tsx b/src/components/chat/ChatUserList.tsx index 62a295101..2c8b75405 100644 --- a/src/components/chat/ChatUserList.tsx +++ b/src/components/chat/ChatUserList.tsx @@ -1,23 +1,24 @@ -import React from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; -import { formatDistanceToNow } from 'date-fns'; -import { ChatConversation } from '../../types'; -import { Avatar } from '../ui/Avatar'; -import { Badge } from '../ui/Badge'; -import { findUserById } from '../../data/users'; -import { useAuth } from '../../context/AuthContext'; +import React from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { formatDistanceToNow } from "date-fns"; +import { ChatConversation } from "../../types"; +import { Avatar } from "../ui/Avatar"; +import { Badge } from "../ui/Badge"; +import { useAuth } from "../../context/AuthContext"; interface ChatUserListProps { conversations: ChatConversation[]; } -export const ChatUserList: React.FC = ({ conversations }) => { +export const ChatUserList: React.FC = ({ + conversations, +}) => { const navigate = useNavigate(); const { userId: activeUserId } = useParams<{ userId: string }>(); const { user: currentUser } = useAuth(); - + if (!currentUser) return null; - + const handleSelectUser = (userId: string) => { navigate(`/chat/${userId}`); }; @@ -25,28 +26,26 @@ export const ChatUserList: React.FC = ({ conversations }) => return (
-

Messages

- +

+ Messages +

+
{conversations.length > 0 ? ( - conversations.map(conversation => { - // Get the other participant (not the current user) - const otherParticipantId = conversation.participants.find(id => id !== currentUser.id); - if (!otherParticipantId) return null; - - const otherUser = findUserById(otherParticipantId); + conversations.map((conversation) => { + const otherUser = conversation.partner; if (!otherUser) return null; - + const lastMessage = conversation.lastMessage; - const isActive = activeUserId === otherParticipantId; - + const isActive = activeUserId === otherUser.id; + return (
handleSelectUser(otherUser.id)} > @@ -54,34 +53,43 @@ export const ChatUserList: React.FC = ({ conversations }) => src={otherUser.avatarUrl} alt={otherUser.name} size="md" - status={otherUser.isOnline ? 'online' : 'offline'} + status={otherUser.isOnline ? "online" : "offline"} className="mr-3 flex-shrink-0" /> - +

{otherUser.name}

- - {lastMessage && ( - - {formatDistanceToNow(new Date(lastMessage.timestamp), { addSuffix: false })} - - )} + + {/* Assuming ChatConversation now has an 'updatedAt' field */} + {/* The original snippet was syntactically incorrect, + this change assumes the intent was to use conversation.updatedAt */} + + {formatDistanceToNow(new Date(conversation.updatedAt), { + addSuffix: false, + })} +
- +
{lastMessage && (

- {lastMessage.senderId === currentUser.id ? 'You: ' : ''} + {lastMessage.senderId === currentUser.id + ? "You: " + : ""} {lastMessage.content}

)} - - {lastMessage && !lastMessage.isRead && lastMessage.senderId !== currentUser.id && ( - New - )} + + {lastMessage && + !lastMessage.isRead && + lastMessage.senderId !== currentUser.id && ( + + New + + )}
@@ -96,4 +104,4 @@ export const ChatUserList: React.FC = ({ conversations }) =>
); -}; \ No newline at end of file +}; diff --git a/src/components/layout/DashboardLayout.tsx b/src/components/layout/DashboardLayout.tsx index f99043426..17238bcec 100644 --- a/src/components/layout/DashboardLayout.tsx +++ b/src/components/layout/DashboardLayout.tsx @@ -1,12 +1,12 @@ -import React from 'react'; -import { Outlet, Navigate } from 'react-router-dom'; -import { useAuth } from '../../context/AuthContext'; -import { Navbar } from './Navbar'; -import { Sidebar } from './Sidebar'; +import React from "react"; +import { Outlet, Navigate } from "react-router-dom"; +import { useAuth } from "../../context/AuthContext"; +import { Navbar } from "./Navbar"; +import { Sidebar } from "./Sidebar"; export const DashboardLayout: React.FC = () => { - const { user, isAuthenticated, isLoading } = useAuth(); - + const { isAuthenticated, isLoading } = useAuth(); + if (isLoading) { return (
@@ -14,18 +14,18 @@ export const DashboardLayout: React.FC = () => {
); } - + if (!isAuthenticated) { return ; } - + return (
- +
- +
@@ -34,4 +34,4 @@ export const DashboardLayout: React.FC = () => {
); -}; \ No newline at end of file +}; diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 842c11da0..905895777 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -1,67 +1,155 @@ -import React from 'react'; -import { NavLink } from 'react-router-dom'; -import { useAuth } from '../../context/AuthContext'; -import { - Home, Building2, CircleDollarSign, Users, MessageCircle, - Bell, FileText, Settings, HelpCircle -} from 'lucide-react'; +import React from "react"; +import { NavLink } from "react-router-dom"; +import { useAuth } from "../../context/AuthContext"; +import { + Home, + Building2, + CircleDollarSign, + Users, + MessageCircle, + Bell, + FileText, + Settings, + HelpCircle, + CalendarDays, +} from "lucide-react"; +import { notificationService } from "../../services/notificationService"; +import { messageService } from "../../services/messageService"; +import { Badge } from "../ui/Badge"; +import { useState, useEffect } from "react"; interface SidebarItemProps { to: string; icon: React.ReactNode; text: string; + badge?: number; } -const SidebarItem: React.FC = ({ to, icon, text }) => { +const SidebarItem: React.FC = ({ to, icon, text, badge }) => { return ( + className={({ isActive }) => `flex items-center py-2.5 px-4 rounded-md transition-colors duration-200 ${ - isActive - ? 'bg-primary-50 text-primary-700' - : 'text-gray-600 hover:bg-gray-100 hover:text-gray-900' + isActive + ? "bg-primary-50 text-primary-700" + : "text-gray-600 hover:bg-gray-100 hover:text-gray-900" }` } > - {icon} - {text} +
+ {icon} + {text} +
+ {badge !== undefined && badge > 0 && ( + + {badge} + + )}
); }; export const Sidebar: React.FC = () => { const { user } = useAuth(); - + const [unreadMessages, setUnreadMessages] = useState(0); + const [unreadNotifications, setUnreadNotifications] = useState(0); + + useEffect(() => { + const fetchData = async () => { + if (user) { + try { + const [msgCount, notifData] = await Promise.all([ + messageService.getUnreadCount(), + notificationService.getNotifications(), + ]); + setUnreadMessages(msgCount); + setUnreadNotifications(notifData.filter((n) => !n.isRead).length); + } catch (error) { + console.error("Failed to fetch counts:", error); + } + } + }; + + fetchData(); + const interval = setInterval(fetchData, 10000); + return () => clearInterval(interval); + }, [user]); + if (!user) return null; - + // Define sidebar items based on user role const entrepreneurItems = [ - { to: '/dashboard/entrepreneur', icon: , text: 'Dashboard' }, - { to: '/profile/entrepreneur/' + user.id, icon: , text: 'My Startup' }, - { to: '/investors', icon: , text: 'Find Investors' }, - { to: '/messages', icon: , text: 'Messages' }, - { to: '/notifications', icon: , text: 'Notifications' }, - { to: '/documents', icon: , text: 'Documents' }, + { + to: "/dashboard/entrepreneur", + icon: , + text: "Dashboard", + }, + { + to: "/profile/entrepreneur/" + user.id, + icon: , + text: "My Startup", + }, + { + to: "/investors", + icon: , + text: "Find Investors", + }, + { + to: "/messages", + icon: , + text: "Messages", + badge: unreadMessages, + }, + { to: "/meetings", icon: , text: "Schedule" }, + { + to: "/notifications", + icon: , + text: "Notifications", + badge: unreadNotifications, + }, + { to: "/requests", icon: , text: "Requests" }, + { to: "/documents", icon: , text: "Documents" }, ]; - + const investorItems = [ - { to: '/dashboard/investor', icon: , text: 'Dashboard' }, - { to: '/profile/investor/' + user.id, icon: , text: 'My Portfolio' }, - { to: '/entrepreneurs', icon: , text: 'Find Startups' }, - { to: '/messages', icon: , text: 'Messages' }, - { to: '/notifications', icon: , text: 'Notifications' }, - { to: '/deals', icon: , text: 'Deals' }, + { to: "/dashboard/investor", icon: , text: "Dashboard" }, + { + to: "/profile/investor/" + user.id, + icon: , + text: "My Portfolio", + }, + { + to: "/entrepreneurs", + icon: , + text: "Find Startups", + }, + { + to: "/messages", + icon: , + text: "Messages", + badge: unreadMessages, + }, + { to: "/meetings", icon: , text: "Schedule" }, + { + to: "/notifications", + icon: , + text: "Notifications", + badge: unreadNotifications, + }, + { to: "/requests", icon: , text: "Requests" }, + { to: "/deals", icon: , text: "Deals" }, ]; - - const sidebarItems = user.role === 'entrepreneur' ? entrepreneurItems : investorItems; - + + const sidebarItems = + user.role === "entrepreneur" ? entrepreneurItems : investorItems; + // Common items at the bottom const commonItems = [ - { to: '/settings', icon: , text: 'Settings' }, - { to: '/help', icon: , text: 'Help & Support' }, + { to: "/settings", icon: , text: "Settings" }, + { to: "/help", icon: , text: "Help & Support" }, ]; - + return (
@@ -73,10 +161,11 @@ export const Sidebar: React.FC = () => { to={item.to} icon={item.icon} text={item.text} + badge={(item as { badge?: number }).badge} /> ))}
- +

Settings @@ -93,13 +182,15 @@ export const Sidebar: React.FC = () => {

- + ); -}; \ No newline at end of file +}; diff --git a/src/components/meetings/MeetingCalendar.tsx b/src/components/meetings/MeetingCalendar.tsx new file mode 100644 index 000000000..29e239e91 --- /dev/null +++ b/src/components/meetings/MeetingCalendar.tsx @@ -0,0 +1,244 @@ +import React, { useState, useEffect } from "react"; +import { + format, + startOfMonth, + endOfMonth, + startOfWeek, + endOfWeek, + eachDayOfInterval, + isSameMonth, + isSameDay, + addMonths, + subMonths, + parseISO, + isToday, +} from "date-fns"; +import { + ChevronLeft, + ChevronRight, + Clock, + MapPin, + Calendar as CalendarIcon, +} from "lucide-react"; +import { Meeting } from "../../types"; +import meetingService from "../../services/meetingService"; +import { Badge } from "../ui/Badge"; +import { Card, CardBody } from "../ui/Card"; + +export const MeetingCalendar: React.FC = () => { + const [currentMonth, setCurrentMonth] = useState(new Date()); + const [meetings, setMeetings] = useState([]); + const [selectedDate, setSelectedDate] = useState(new Date()); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const fetchMeetings = async () => { + setIsLoading(true); + try { + const data = await meetingService.getMeetings(); + setMeetings(data); + } catch (error) { + console.error("Failed to fetch meetings:", error); + } finally { + setIsLoading(false); + } + }; + fetchMeetings(); + }, []); + + const monthStart = startOfMonth(currentMonth); + const monthEnd = endOfMonth(monthStart); + const startDate = startOfWeek(monthStart); + const endDate = endOfWeek(monthEnd); + + const days = eachDayOfInterval({ + start: startDate, + end: endDate, + }); + + const nextMonth = () => setCurrentMonth(addMonths(currentMonth, 1)); + const prevMonth = () => setCurrentMonth(subMonths(currentMonth, 1)); + + const meetingsOnSelectedDate = meetings.filter( + (meeting) => + selectedDate && isSameDay(parseISO(meeting.startTime), selectedDate), + ); + + const getStatusVariant = (status: string) => { + switch (status) { + case "accepted": + return "success"; + case "rejected": + return "error"; + case "cancelled": + return "gray"; + case "pending": + return "warning"; + default: + return "primary"; + } + }; + + return ( +
+ {/* Calendar Grid */} + + +
+

+ {format(currentMonth, "MMMM yyyy")} +

+
+ + +
+
+ +
+ {["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].map((day) => ( +
+ {day} +
+ ))} +
+ +
+ {days.map((day, idx) => { + const dayMeetings = meetings.filter((m) => + isSameDay(parseISO(m.startTime), day), + ); + const isCurrentMonth = isSameMonth(day, monthStart); + const isSelected = selectedDate && isSameDay(day, selectedDate); + const today = isToday(day); + + return ( +
setSelectedDate(day)} + className={`min-h-[100px] p-2 border-r border-b cursor-pointer transition-colors ${ + !isCurrentMonth ? "bg-gray-50" : "bg-white" + } ${isSelected ? " ring-2 ring-primary-500 ring-inset" : "hover:bg-blue-50"}`} + > +
+ + {format(day, "d")} + + {dayMeetings.length > 0 && ( + + )} +
+ +
+ {dayMeetings.slice(0, 2).map((m, i) => ( +
+ {m.title} +
+ ))} + {dayMeetings.length > 2 && ( +
+ + {dayMeetings.length - 2} more +
+ )} +
+
+ ); + })} +
+
+
+ + {/* Selected Date Summary */} +
+ + +

+ + {selectedDate + ? format(selectedDate, "MMMM d, yyyy") + : "Select a date"} +

+ + {isLoading ? ( +
+
+
+ ) : meetingsOnSelectedDate.length > 0 ? ( +
+ {meetingsOnSelectedDate.map((meeting) => ( +
+
+

+ {meeting.title} +

+ + {meeting.status} + +
+ +
+
+ + {format(parseISO(meeting.startTime), "h:mm a")} -{" "} + {format(parseISO(meeting.endTime), "h:mm a")} +
+
+ + {meeting.location} +
+
+
+ ))} +
+ ) : ( +
+

+ No meetings scheduled for this day +

+
+ )} +
+
+ +
+

+ Quick Tip +

+

+ Click on any date in the calendar to see the detailed schedule and + manage your appointments. +

+
+
+
+ ); +}; diff --git a/src/components/meetings/ScheduleMeetingModal.tsx b/src/components/meetings/ScheduleMeetingModal.tsx new file mode 100644 index 000000000..b2ea0b731 --- /dev/null +++ b/src/components/meetings/ScheduleMeetingModal.tsx @@ -0,0 +1,163 @@ +import React, { useState } from "react"; +import { Calendar, Clock, MapPin, X } from "lucide-react"; +import { Button } from "../ui/Button"; +import { Input } from "../ui/Input"; +import { Meeting } from "../../types"; +import meetingService from "../../services/meetingService"; +import toast from "react-hot-toast"; +import { format, addHours, parseISO } from "date-fns"; + +interface ScheduleMeetingModalProps { + isOpen: boolean; + onClose: () => void; + investorId: string; + entrepreneurId: string; + onSuccess?: (meeting: Meeting) => void; +} + +export const ScheduleMeetingModal: React.FC = ({ + isOpen, + onClose, + investorId, + entrepreneurId, + onSuccess, +}) => { + const [title, setTitle] = useState(""); + const [description, setDescription] = useState(""); + const [date, setDate] = useState(format(new Date(), "yyyy-MM-dd")); + const [time, setTime] = useState(format(new Date(), "HH:mm")); + const [duration, setDuration] = useState("1"); // hours + const [location, setLocation] = useState("Google Meet"); + const [isLoading, setIsLoading] = useState(false); + + if (!isOpen) return null; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + + try { + const startTime = parseISO(`${date}T${time}`); + const endTime = addHours(startTime, parseFloat(duration)); + + const meeting = await meetingService.scheduleMeeting({ + title, + description, + investorId, + entrepreneurId, + startTime: startTime.toISOString(), + endTime: endTime.toISOString(), + location, + }); + + toast.success("Meeting scheduled successfully!"); + if (onSuccess) onSuccess(meeting); + onClose(); + } catch (error: unknown) { + const err = error as { response?: { data?: { message?: string } } }; + const message = + err.response?.data?.message || "Failed to schedule meeting"; + toast.error(message); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+
+

+ Schedule Meeting +

+ +
+ +
+ setTitle(e.target.value)} + placeholder="e.g., Pitch Deck Review" + required + fullWidth + /> + +
+ + +
+ +
+ setDate(e.target.value)} + required + fullWidth + startAdornment={} + /> + setTime(e.target.value)} + required + fullWidth + startAdornment={} + /> +
+ +
+
+ + +
+ setLocation(e.target.value)} + placeholder="e.g., Zoom, Office" + fullWidth + startAdornment={} + /> +
+ +
+ + +
+
+
+
+ ); +}; diff --git a/src/components/ui/Badge.tsx b/src/components/ui/Badge.tsx index 8c84dc3ed..e95c0867d 100644 --- a/src/components/ui/Badge.tsx +++ b/src/components/ui/Badge.tsx @@ -1,46 +1,55 @@ -import React from 'react'; +import React from "react"; -export type BadgeVariant = 'primary' | 'secondary' | 'accent' | 'success' | 'warning' | 'error' | 'gray'; -export type BadgeSize = 'sm' | 'md' | 'lg'; +export type BadgeVariant = + | "primary" + | "secondary" + | "accent" + | "success" + | "warning" + | "error" + | "gray"; +export type BadgeSize = "sm" | "md" | "lg"; -interface BadgeProps { +interface BadgeProps extends React.HTMLAttributes { children: React.ReactNode; variant?: BadgeVariant; - size?: BadgeSize; + size?: BadgeSize; // Reverted to original as the provided snippet was syntactically incorrect and semantically out of place for BadgeProps rounded?: boolean; className?: string; } export const Badge: React.FC = ({ children, - variant = 'primary', - size = 'md', + variant = "primary", + size = "md", rounded = false, - className = '', + className = "", + ...props }) => { const variantClasses = { - primary: 'bg-primary-100 text-primary-800', - secondary: 'bg-secondary-100 text-secondary-800', - accent: 'bg-accent-100 text-accent-800', - success: 'bg-success-50 text-success-700', - warning: 'bg-warning-50 text-warning-700', - error: 'bg-error-50 text-error-700', - gray: 'bg-gray-100 text-gray-800', + primary: "bg-primary-100 text-primary-800", + secondary: "bg-secondary-100 text-secondary-800", + accent: "bg-accent-100 text-accent-800", + success: "bg-success-50 text-success-700", + warning: "bg-warning-50 text-warning-700", + error: "bg-error-50 text-error-700", + gray: "bg-gray-100 text-gray-800", }; - + const sizeClasses = { - sm: 'text-xs px-2 py-0.5', - md: 'text-sm px-2.5 py-0.5', - lg: 'text-base px-3 py-1', + sm: "text-xs px-2 py-0.5", + md: "text-sm px-2.5 py-0.5", + lg: "text-base px-3 py-1", }; - - const roundedClass = rounded ? 'rounded-full' : 'rounded'; - + + const roundedClass = rounded ? "rounded-full" : "rounded"; + return ( {children} ); -}; \ No newline at end of file +}; diff --git a/src/components/ui/Card.tsx b/src/components/ui/Card.tsx index 418ba0756..073ef7947 100644 --- a/src/components/ui/Card.tsx +++ b/src/components/ui/Card.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React from "react"; interface CardProps { children: React.ReactNode; @@ -7,24 +7,26 @@ interface CardProps { hoverable?: boolean; } -export const Card: React.FC = ({ - children, - className = '', - onClick, - hoverable = false, -}) => { - const hoverableClass = hoverable ? 'transform hover:-translate-y-1 transition-transform duration-300 cursor-pointer' : ''; - const clickableClass = onClick ? 'cursor-pointer' : ''; - - return ( -
- {children} -
- ); -}; +export const Card = React.forwardRef( + ({ children, className = "", onClick, hoverable = false }, ref) => { + const hoverableClass = hoverable + ? "transform hover:-translate-y-1 transition-transform duration-300 cursor-pointer" + : ""; + const clickableClass = onClick ? "cursor-pointer" : ""; + + return ( +
+ {children} +
+ ); + }, +); + +Card.displayName = "Card"; interface CardHeaderProps { children: React.ReactNode; @@ -33,7 +35,7 @@ interface CardHeaderProps { export const CardHeader: React.FC = ({ children, - className = '', + className = "", }) => { return (
@@ -49,13 +51,9 @@ interface CardBodyProps { export const CardBody: React.FC = ({ children, - className = '', + className = "", }) => { - return ( -
- {children} -
- ); + return
{children}
; }; interface CardFooterProps { @@ -65,11 +63,11 @@ interface CardFooterProps { export const CardFooter: React.FC = ({ children, - className = '', + className = "", }) => { return (
{children}
); -}; \ No newline at end of file +}; diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index 5d07bcf0a..9a1c25b2f 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -1,171 +1,214 @@ -import React, { createContext, useState, useContext, useEffect } from 'react'; -import { User, UserRole, AuthContextType } from '../types'; -import { users } from '../data/users'; -import toast from 'react-hot-toast'; +import React, { createContext, useState, useContext, useEffect } from "react"; +import { User, UserRole, AuthContextType } from "../types"; +import api from "../services/api"; +import toast from "react-hot-toast"; // Create Auth Context const AuthContext = createContext(undefined); // Local storage keys -const USER_STORAGE_KEY = 'business_nexus_user'; -const RESET_TOKEN_KEY = 'business_nexus_reset_token'; +const USER_STORAGE_KEY = "business_nexus_user"; +const TOKEN_STORAGE_KEY = "business_nexus_token"; // Auth Provider Component -export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { +export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { const [user, setUser] = useState(null); const [isLoading, setIsLoading] = useState(true); - // Check for stored user on initial load + // Check for stored user and token on initial load useEffect(() => { - const storedUser = localStorage.getItem(USER_STORAGE_KEY); - if (storedUser) { - setUser(JSON.parse(storedUser)); - } - setIsLoading(false); + const initAuth = async () => { + const storedUser = localStorage.getItem(USER_STORAGE_KEY); + const token = localStorage.getItem(TOKEN_STORAGE_KEY); + + console.log("Initializing auth from storage:", { + hasUser: !!storedUser, + hasToken: !!token, + }); + + if (storedUser && token) { + try { + const parsedUser = JSON.parse(storedUser); + setUser(parsedUser); + + // Verify session with backend and get most up-to-date profile + console.log("Verifying token with backend..."); + const res = await api.get("/users/profile"); + setUser(res.data); + localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(res.data)); + console.log("Session restored successfully"); + } catch (error) { + console.error("Failed to restore auth session:", error); + logout(); + } + } + setIsLoading(false); + }; + + initAuth(); }, []); - // Mock login function - in a real app, this would make an API call - const login = async (email: string, password: string, role: UserRole): Promise => { + // Login function + const login = async ( + email: string, + password: string, + role: UserRole, + ): Promise => { setIsLoading(true); - + console.log("Attempting login for:", email, "Role:", role); try { - // Simulate API call delay - await new Promise(resolve => setTimeout(resolve, 1000)); - - // Find user with matching email and role - const foundUser = users.find(u => u.email === email && u.role === role); - - if (foundUser) { - setUser(foundUser); - localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(foundUser)); - toast.success('Successfully logged in!'); - } else { - throw new Error('Invalid credentials or user not found'); + const response = await api.post("/auth/login", { email, password }); + console.log("Login success:", response.data); + const { token, user: userData } = response.data; + + if (userData.role !== role) { + console.warn( + "Role mismatch. User is:", + userData.role, + "expected:", + role, + ); + throw new Error( + `Account found but role is ${userData.role}, not ${role}`, + ); } - } catch (error) { - toast.error((error as Error).message); - throw error; + + setUser(userData); + localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(userData)); + localStorage.setItem(TOKEN_STORAGE_KEY, token); + + toast.success("Successfully logged in!"); + } catch (error: unknown) { + const err = error as { + response?: { data?: { message?: string } }; + message?: string; + }; + const message = + err.response?.data?.message || err.message || "Login failed"; + console.error("Login failed:", message); + toast.error(message); + throw new Error(message); } finally { setIsLoading(false); } }; - // Mock register function - in a real app, this would make an API call - const register = async (name: string, email: string, password: string, role: UserRole): Promise => { + // Register function + const register = async ( + name: string, + email: string, + password: string, + role: UserRole, + ): Promise => { setIsLoading(true); - + console.log("Registering user:", email); try { - // Simulate API call delay - await new Promise(resolve => setTimeout(resolve, 1000)); - - // Check if email already exists - if (users.some(u => u.email === email)) { - throw new Error('Email already in use'); - } - - // Create new user - const newUser: User = { - id: `${role[0]}${users.length + 1}`, - name, - email, - role, - avatarUrl: `https://ui-avatars.com/api/?name=${encodeURIComponent(name)}&background=random`, - bio: '', - isOnline: true, - createdAt: new Date().toISOString() + await api.post("/auth/register", { name, email, password, role }); + console.log("Registration success, logging in..."); + await login(email, password, role); + } catch (error: unknown) { + const err = error as { + response?: { data?: { message?: string } }; + message?: string; }; - - // Add user to mock data - users.push(newUser); - - setUser(newUser); - localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(newUser)); - toast.success('Account created successfully!'); - } catch (error) { - toast.error((error as Error).message); - throw error; + const message = + err.response?.data?.message || err.message || "Registration failed"; + console.error("Registration failed:", message); + toast.error(message); + throw new Error(message); } finally { setIsLoading(false); } }; - // Mock forgot password function + // Forgot password function const forgotPassword = async (email: string): Promise => { try { - // Simulate API call delay - await new Promise(resolve => setTimeout(resolve, 1000)); - - // Check if user exists - const user = users.find(u => u.email === email); - if (!user) { - throw new Error('No account found with this email'); + const response = await api.post("/auth/forgot-password", { email }); + if (response.data.resetToken) { + localStorage.setItem( + "business_nexus_reset_token", + response.data.resetToken, + ); + console.log("Demo Reset Token:", response.data.resetToken); } - - // Generate reset token (in a real app, this would be a secure token) - const resetToken = Math.random().toString(36).substring(2, 15); - localStorage.setItem(RESET_TOKEN_KEY, resetToken); - - // In a real app, this would send an email - toast.success('Password reset instructions sent to your email'); - } catch (error) { - toast.error((error as Error).message); - throw error; + toast.success("Password reset instructions sent to your email"); + } catch (error: unknown) { + const err = error as { response?: { data?: { message?: string } } }; + const message = + err.response?.data?.message || "Failed to send reset email"; + toast.error(message); + throw new Error(message); } }; - // Mock reset password function - const resetPassword = async (token: string, newPassword: string): Promise => { + // Reset password function + const resetPassword = async ( + token: string, + newPassword: string, + ): Promise => { try { - // Simulate API call delay - await new Promise(resolve => setTimeout(resolve, 1000)); - - // Verify token - const storedToken = localStorage.getItem(RESET_TOKEN_KEY); - if (token !== storedToken) { - throw new Error('Invalid or expired reset token'); - } - - // In a real app, this would update the user's password in the database - localStorage.removeItem(RESET_TOKEN_KEY); - toast.success('Password reset successfully'); - } catch (error) { - toast.error((error as Error).message); - throw error; + await api.post("/auth/reset-password", { token, newPassword }); + toast.success("Password reset successfully"); + } catch (error: unknown) { + const err = error as { response?: { data?: { message?: string } } }; + const message = err.response?.data?.message || "Reset failed"; + toast.error(message); + throw new Error(message); } }; // Logout function const logout = (): void => { + console.log("Logging out..."); setUser(null); localStorage.removeItem(USER_STORAGE_KEY); - toast.success('Logged out successfully'); + localStorage.removeItem(TOKEN_STORAGE_KEY); + toast.success("Logged out successfully"); }; // Update user profile - const updateProfile = async (userId: string, updates: Partial): Promise => { + const updateProfile = async ( + _userId: string, + updates: Partial, + ): Promise => { try { - // Simulate API call delay - await new Promise(resolve => setTimeout(resolve, 1000)); - - // Update user in mock data - const userIndex = users.findIndex(u => u.id === userId); - if (userIndex === -1) { - throw new Error('User not found'); - } - - const updatedUser = { ...users[userIndex], ...updates }; - users[userIndex] = updatedUser; - - // Update current user if it's the same user - if (user?.id === userId) { - setUser(updatedUser); - localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(updatedUser)); - } - - toast.success('Profile updated successfully'); - } catch (error) { - toast.error((error as Error).message); - throw error; + const response = await api.put("/users/profile", updates); + const updatedUser = response.data; + setUser(updatedUser); + localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(updatedUser)); + toast.success("Profile updated successfully"); + } catch (error: unknown) { + const err = error as { + response?: { data?: { message?: string } }; + message?: string; + }; + const message = + err.response?.data?.message || err.message || "Update failed"; + toast.error(message); + throw new Error(message); + } + }; + + // Change password function + const changePassword = async ( + currentPassword: string, + newPassword: string, + ): Promise => { + try { + await api.put("/users/change-password", { + currentPassword, + newPassword, + }); + toast.success("Password updated successfully"); + } catch (error: unknown) { + const err = error as { response?: { data?: { message?: string } } }; + const message = + err.response?.data?.message || "Failed to update password"; + toast.error(message); + throw new Error(message); } }; @@ -177,18 +220,18 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children forgotPassword, resetPassword, updateProfile, + changePassword, isAuthenticated: !!user, - isLoading + isLoading, }; return {children}; }; -// Custom hook for using auth context export const useAuth = (): AuthContextType => { const context = useContext(AuthContext); if (context === undefined) { - throw new Error('useAuth must be used within an AuthProvider'); + throw new Error("useAuth must be used within an AuthProvider"); } return context; -}; \ No newline at end of file +}; diff --git a/src/pages/auth/ForgotPasswordPage.tsx b/src/pages/auth/ForgotPasswordPage.tsx index 14a53a209..128152978 100644 --- a/src/pages/auth/ForgotPasswordPage.tsx +++ b/src/pages/auth/ForgotPasswordPage.tsx @@ -1,31 +1,31 @@ -import React, { useState } from 'react'; -import { Link } from 'react-router-dom'; -import { Mail, ArrowLeft } from 'lucide-react'; -import { useAuth } from '../../context/AuthContext'; -import { Button } from '../../components/ui/Button'; -import { Input } from '../../components/ui/Input'; +import React, { useState } from "react"; +import { Link } from "react-router-dom"; +import { Mail, ArrowLeft } from "lucide-react"; +import { useAuth } from "../../context/AuthContext"; +import { Button } from "../../components/ui/Button"; +import { Input } from "../../components/ui/Input"; export const ForgotPasswordPage: React.FC = () => { - const [email, setEmail] = useState(''); + const [email, setEmail] = useState(""); const [isLoading, setIsLoading] = useState(false); const [isSubmitted, setIsSubmitted] = useState(false); - + const { forgotPassword } = useAuth(); - + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setIsLoading(true); - + try { await forgotPassword(email); setIsSubmitted(true); - } catch (error) { + } catch { // Error is handled by the AuthContext } finally { setIsLoading(false); } }; - + if (isSubmitted) { return (
@@ -39,13 +39,13 @@ export const ForgotPasswordPage: React.FC = () => { We've sent password reset instructions to {email}

- +

Didn't receive the email? Check your spam folder or try again.

- + - +
); } - + return (
@@ -79,10 +79,11 @@ export const ForgotPasswordPage: React.FC = () => { Forgot your password?

- Enter your email address and we'll send you instructions to reset your password. + Enter your email address and we'll send you instructions to reset + your password.

- +
{ fullWidth startAdornment={} /> - - - +
); -}; \ No newline at end of file +}; diff --git a/src/pages/auth/ResetPasswordPage.tsx b/src/pages/auth/ResetPasswordPage.tsx index ec64920fb..33c95cab6 100644 --- a/src/pages/auth/ResetPasswordPage.tsx +++ b/src/pages/auth/ResetPasswordPage.tsx @@ -1,43 +1,43 @@ -import React, { useState } from 'react'; -import { useNavigate, useSearchParams } from 'react-router-dom'; -import { Lock } from 'lucide-react'; -import { useAuth } from '../../context/AuthContext'; -import { Button } from '../../components/ui/Button'; -import { Input } from '../../components/ui/Input'; +import React, { useState } from "react"; +import { useNavigate, useSearchParams } from "react-router-dom"; +import { Lock } from "lucide-react"; +import { useAuth } from "../../context/AuthContext"; +import { Button } from "../../components/ui/Button"; +import { Input } from "../../components/ui/Input"; export const ResetPasswordPage: React.FC = () => { - const [password, setPassword] = useState(''); - const [confirmPassword, setConfirmPassword] = useState(''); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); const [isLoading, setIsLoading] = useState(false); const [searchParams] = useSearchParams(); const navigate = useNavigate(); - + const { resetPassword } = useAuth(); - const token = searchParams.get('token'); - + const token = searchParams.get("token"); + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - + if (!token) { return; } - + if (password !== confirmPassword) { return; } - + setIsLoading(true); - + try { await resetPassword(token, password); - navigate('/login'); - } catch (error) { + navigate("/login"); + } catch { // Error is handled by the AuthContext } finally { setIsLoading(false); } }; - + if (!token) { return (
@@ -51,7 +51,7 @@ export const ResetPasswordPage: React.FC = () => {

@@ -60,7 +60,7 @@ export const ResetPasswordPage: React.FC = () => {
); } - + return (
@@ -73,7 +73,7 @@ export const ResetPasswordPage: React.FC = () => { Enter your new password below

- +
{ fullWidth startAdornment={} /> - + { required fullWidth startAdornment={} - error={password !== confirmPassword ? 'Passwords do not match' : undefined} + error={ + password !== confirmPassword + ? "Passwords do not match" + : undefined + } /> - - @@ -109,4 +109,4 @@ export const ResetPasswordPage: React.FC = () => {
); -}; \ No newline at end of file +}; diff --git a/src/pages/chat/ChatPage.tsx b/src/pages/chat/ChatPage.tsx index 10c55b077..f4a8d5dbf 100644 --- a/src/pages/chat/ChatPage.tsx +++ b/src/pages/chat/ChatPage.tsx @@ -1,73 +1,104 @@ -import React, { useState, useEffect, useRef } from 'react'; -import { useParams } from 'react-router-dom'; -import { Send, Phone, Video, Info, Smile } from 'lucide-react'; -import { Avatar } from '../../components/ui/Avatar'; -import { Button } from '../../components/ui/Button'; -import { Input } from '../../components/ui/Input'; -import { ChatMessage } from '../../components/chat/ChatMessage'; -import { ChatUserList } from '../../components/chat/ChatUserList'; -import { useAuth } from '../../context/AuthContext'; -import { Message } from '../../types'; -import { findUserById } from '../../data/users'; -import { getMessagesBetweenUsers, sendMessage, getConversationsForUser } from '../../data/messages'; -import { MessageCircle } from 'lucide-react'; +import React, { useState, useEffect, useRef } from "react"; +import { useParams } from "react-router-dom"; +import { Send, Phone, Video, Info, Smile } from "lucide-react"; +import { Avatar } from "../../components/ui/Avatar"; +import { Button } from "../../components/ui/Button"; +import { Input } from "../../components/ui/Input"; +import { ChatMessage } from "../../components/chat/ChatMessage"; +import { ChatUserList } from "../../components/chat/ChatUserList"; +import { useAuth } from "../../context/AuthContext"; +import { Message, ChatConversation, User } from "../../types"; +import { messageService } from "../../services/messageService"; +import api from "../../services/api"; +import { MessageCircle } from "lucide-react"; export const ChatPage: React.FC = () => { const { userId } = useParams<{ userId: string }>(); const { user: currentUser } = useAuth(); const [messages, setMessages] = useState([]); - const [newMessage, setNewMessage] = useState(''); - const [conversations, setConversations] = useState([]); + const [newMessage, setNewMessage] = useState(""); + const [conversations, setConversations] = useState([]); + const [chatPartner, setChatPartner] = useState(null); const messagesEndRef = useRef(null); - - const chatPartner = userId ? findUserById(userId) : null; - + useEffect(() => { - // Load conversations - if (currentUser) { - setConversations(getConversationsForUser(currentUser.id)); - } + const fetchConversations = async () => { + if (currentUser) { + try { + const data = await messageService.getConversations(); + setConversations(data); + } catch (error) { + console.error("Failed to fetch conversations:", error); + } + } + }; + fetchConversations(); + const interval = setInterval(fetchConversations, 5000); // Poll for new conversations + return () => clearInterval(interval); }, [currentUser]); - + useEffect(() => { - // Load messages between users - if (currentUser && userId) { - setMessages(getMessagesBetweenUsers(currentUser.id, userId)); - } + const fetchMessages = async () => { + if (currentUser && userId) { + try { + const data = await messageService.getMessages(userId); + setMessages(data); + } catch (error) { + console.error("Failed to fetch messages:", error); + } + } + }; + + const fetchPartner = async () => { + if (userId) { + try { + const res = await api.get(`/users/${userId}`); + setChatPartner(res.data); + } catch (error) { + console.error("Failed to fetch partner:", error); + } + } + }; + + fetchMessages(); + fetchPartner(); + + const interval = setInterval(fetchMessages, 3000); // Poll for new messages + return () => clearInterval(interval); }, [currentUser, userId]); - + useEffect(() => { // Scroll to bottom of messages - messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); - - const handleSendMessage = (e: React.FormEvent) => { + + const handleSendMessage = async (e: React.FormEvent) => { e.preventDefault(); - + if (!newMessage.trim() || !currentUser || !userId) return; - - const message = sendMessage({ - senderId: currentUser.id, - receiverId: userId, - content: newMessage - }); - - setMessages([...messages, message]); - setNewMessage(''); - - // Update conversations - setConversations(getConversationsForUser(currentUser.id)); + + try { + const message = await messageService.sendMessage(userId, newMessage); + setMessages([...messages, message]); + setNewMessage(""); + + // Refresh conversations + const data = await messageService.getConversations(); + setConversations(data); + } catch (error) { + console.error("Failed to send message:", error); + } }; - + if (!currentUser) return null; - + return (
{/* Conversations sidebar */}
- + {/* Main chat area */}
{/* Chat header */} @@ -79,18 +110,20 @@ export const ChatPage: React.FC = () => { src={chatPartner.avatarUrl} alt={chatPartner.name} size="md" - status={chatPartner.isOnline ? 'online' : 'offline'} + status={chatPartner.isOnline ? "online" : "offline"} className="mr-3" /> - +
-

{chatPartner.name}

+

+ {chatPartner.name} +

- {chatPartner.isOnline ? 'Online' : 'Last seen recently'} + {chatPartner.isOnline ? "Online" : "Last seen recently"}

- +
- + - +
- + {/* Messages container */}
{messages.length > 0 ? (
- {messages.map(message => ( - - ))} + {messages.map((message) => { + const isCurrentUser = message.senderId === currentUser.id; + const sender = isCurrentUser ? currentUser : chatPartner; + return ( + + ); + })}
) : ( @@ -139,12 +177,16 @@ export const ChatPage: React.FC = () => {
-

No messages yet

-

Send a message to start the conversation

+

+ No messages yet +

+

+ Send a message to start the conversation +

)}
- + {/* Message input */}
@@ -157,7 +199,7 @@ export const ChatPage: React.FC = () => { > - + { fullWidth className="flex-1" /> - +
); -}; \ No newline at end of file +}; diff --git a/src/pages/collaboration/CollaborationPage.tsx b/src/pages/collaboration/CollaborationPage.tsx new file mode 100644 index 000000000..1ecb29836 --- /dev/null +++ b/src/pages/collaboration/CollaborationPage.tsx @@ -0,0 +1,277 @@ +import React, { useState, useEffect } from "react"; +import { Link } from "react-router-dom"; +import { Send, CheckCircle, XCircle, Clock, User } from "lucide-react"; +import { Card, CardBody } from "../../components/ui/Card"; +import { Button } from "../../components/ui/Button"; +import { Badge } from "../../components/ui/Badge"; +import { Avatar } from "../../components/ui/Avatar"; +import { useAuth } from "../../context/AuthContext"; +import { CollaborationRequest, User as UserType } from "../../types"; +import api from "../../services/api"; +import { format, parseISO } from "date-fns"; +import toast from "react-hot-toast"; + +type FilterTab = "all" | "pending" | "accepted" | "rejected"; + +export const CollaborationPage: React.FC = () => { + const { user } = useAuth(); + const [requests, setRequests] = useState([]); + const [users, setUsers] = useState>({}); + const [activeFilter, setActiveFilter] = useState("all"); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const fetchRequests = async () => { + if (user) { + try { + const res = await api.get("/collaboration"); + setRequests(res.data); + + // Fetch user details for all participants + const userIds = new Set(); + res.data.forEach((req: CollaborationRequest) => { + userIds.add(req.investorId); + userIds.add(req.entrepreneurId); + }); + + const userPromises = Array.from(userIds).map((id) => + api.get(`/users/${id}`), + ); + const userResponses = await Promise.all(userPromises); + + const usersMap: Record = {}; + userResponses.forEach((response) => { + usersMap[response.data.id] = response.data; + }); + setUsers(usersMap); + } catch (error) { + console.error("Failed to fetch requests:", error); + } finally { + setIsLoading(false); + } + } + }; + fetchRequests(); + }, [user]); + + const handleStatusUpdate = async ( + requestId: string, + status: "accepted" | "rejected", + ) => { + try { + await api.put(`/collaboration/${requestId}/status`, { status }); + setRequests((prev) => + prev.map((req) => (req.id === requestId ? { ...req, status } : req)), + ); + toast.success(`Request ${status}`); + } catch (error) { + console.error("Failed to update status:", error); + toast.error("Failed to update request"); + } + }; + + const filteredRequests = requests.filter((req) => { + if (activeFilter === "all") return true; + return req.status === activeFilter; + }); + + const isInvestor = user?.role === "investor"; + const getOtherUser = (req: CollaborationRequest) => { + const userId = isInvestor ? req.entrepreneurId : req.investorId; + return users[userId]; + }; + + if (isLoading) { + return ( +
+
+
+ ); + } + + return ( +
+
+

+ {isInvestor + ? "Collaboration Requests Sent" + : "Collaboration Requests Received"} +

+

+ {isInvestor + ? "Track your collaboration requests to startups" + : "Manage incoming collaboration requests from investors"} +

+
+ + {/* Filter tabs */} + + +
+ + + + +
+
+
+ + {/* Requests list */} +
+ {filteredRequests.length > 0 ? ( + filteredRequests.map((request) => { + const otherUser = getOtherUser(request); + return ( + + +
+
+ +
+
+ + {otherUser?.name || "Unknown"} + + {request.status === "pending" && ( + + Pending + + )} + {request.status === "accepted" && ( + + Accepted + + )} + {request.status === "rejected" && ( + + Rejected + + )} +
+ + {!isInvestor && otherUser?.startupName && ( +

+ {otherUser.startupName} +

+ )} + {isInvestor && otherUser?.startupName && ( +

+ {otherUser.startupName} +

+ )} + +

+ {request.message} +

+ +

+ Sent{" "} + {format( + parseISO(request.createdAt), + "MMM d, yyyy • h:mm a", + )} +

+
+
+ + {!isInvestor && request.status === "pending" && ( +
+ + +
+ )} + + {isInvestor && request.status === "accepted" && ( + + + + )} +
+
+
+ ); + }) + ) : ( +
+
+ {isInvestor ? : } +
+

+ {activeFilter === "all" + ? "No requests yet" + : `No ${activeFilter} requests`} +

+

+ {isInvestor + ? "Start by browsing startups and sending collaboration requests." + : "Investors will appear here when they request to collaborate with you."} +

+ {isInvestor && ( + + + + )} +
+ )} +
+
+ ); +}; diff --git a/src/pages/dashboard/EntrepreneurDashboard.tsx b/src/pages/dashboard/EntrepreneurDashboard.tsx index 6c1dd3b17..83b41e092 100644 --- a/src/pages/dashboard/EntrepreneurDashboard.tsx +++ b/src/pages/dashboard/EntrepreneurDashboard.tsx @@ -1,58 +1,123 @@ -import React, { useState, useEffect } from 'react'; -import { Link } from 'react-router-dom'; -import { Users, Bell, Calendar, TrendingUp, AlertCircle, PlusCircle } from 'lucide-react'; -import { Button } from '../../components/ui/Button'; -import { Card, CardBody, CardHeader } from '../../components/ui/Card'; -import { Badge } from '../../components/ui/Badge'; -import { CollaborationRequestCard } from '../../components/collaboration/CollaborationRequestCard'; -import { InvestorCard } from '../../components/investor/InvestorCard'; -import { useAuth } from '../../context/AuthContext'; -import { CollaborationRequest } from '../../types'; -import { getRequestsForEntrepreneur } from '../../data/collaborationRequests'; -import { investors } from '../../data/users'; +import React, { useState, useEffect } from "react"; +import { Link } from "react-router-dom"; +import { + Users, + Bell, + Calendar, + MessageCircle, + AlertCircle, + PlusCircle, + Clock, + MapPin, +} from "lucide-react"; +import { Button } from "../../components/ui/Button"; +import { Card, CardBody, CardHeader } from "../../components/ui/Card"; +import { Badge } from "../../components/ui/Badge"; +import { CollaborationRequestCard } from "../../components/collaboration/CollaborationRequestCard"; +import { InvestorCard } from "../../components/investor/InvestorCard"; +import { useAuth } from "../../context/AuthContext"; +import api from "../../services/api"; +import dashboardService, { + DashboardSummary, +} from "../../services/dashboardService"; +import documentService from "../../services/documentService"; +import { format, parseISO } from "date-fns"; +import { CollaborationRequest, Investor, Meeting, Document } from "../../types"; +import { FileText } from "lucide-react"; export const EntrepreneurDashboard: React.FC = () => { const { user } = useAuth(); - const [collaborationRequests, setCollaborationRequests] = useState([]); - const [recommendedInvestors, setRecommendedInvestors] = useState(investors.slice(0, 3)); - + const [summary, setSummary] = useState(null); + const [collaborationRequests, setCollaborationRequests] = useState< + CollaborationRequest[] + >([]); + const [recommendedInvestors, setRecommendedInvestors] = useState( + [], + ); + const [recentDocuments, setRecentDocuments] = useState([]); + const [isLoading, setIsLoading] = useState(true); + useEffect(() => { - if (user) { - // Load collaboration requests - const requests = getRequestsForEntrepreneur(user.id); - setCollaborationRequests(requests); - } + const fetchDashboardData = async () => { + if (user) { + setIsLoading(true); + try { + // Fetch summary stats + const summaryData = await dashboardService.getSummary(); + setSummary(summaryData); + + // Fetch collaboration requests from API + const requestsRes = await api.get("/collaboration"); + setCollaborationRequests(requestsRes.data); + + // Fetch investors for recommendation from API + const investorsRes = await api.get("/directory/investors"); + setRecommendedInvestors(investorsRes.data.slice(0, 3)); + + // Fetch recent documents + const docsRes = await documentService.getDocuments(); + setRecentDocuments(docsRes.slice(0, 4)); + } catch (error) { + console.error("Failed to fetch dashboard data:", error); + } finally { + setIsLoading(false); + } + } + }; + + fetchDashboardData(); }, [user]); - - const handleRequestStatusUpdate = (requestId: string, status: 'accepted' | 'rejected') => { - setCollaborationRequests(prevRequests => - prevRequests.map(req => - req.id === requestId ? { ...req, status } : req - ) - ); + + const handleRequestStatusUpdate = async ( + requestId: string, + status: "accepted" | "rejected", + ) => { + try { + await api.put(`/collaboration/${requestId}/status`, { status }); + setCollaborationRequests((prevRequests) => + prevRequests.map((req) => + req.id === requestId ? { ...req, status } : req, + ), + ); + // Refresh summary to update counts + const summaryData = await dashboardService.getSummary(); + setSummary(summaryData); + } catch (error) { + console.error("Failed to update request status:", error); + } }; - + if (!user) return null; - - const pendingRequests = collaborationRequests.filter(req => req.status === 'pending'); - + + const pendingRequests = collaborationRequests.filter( + (req) => req.status === "pending", + ); + + if (isLoading) { + return ( +
+
+
+ ); + } + return (
-

Welcome, {user.name}

-

Here's what's happening with your startup today

+

+ Welcome, {user.name} +

+

+ Here's what's happening with your startup today +

- + - +
- + {/* Summary cards */}
@@ -62,13 +127,17 @@ export const EntrepreneurDashboard: React.FC = () => {
-

Pending Requests

-

{pendingRequests.length}

+

+ Pending Requests +

+

+ {summary?.pendingRequests || 0} +

- +
@@ -76,15 +145,17 @@ export const EntrepreneurDashboard: React.FC = () => {
-

Total Connections

+

+ Total Connections +

- {collaborationRequests.filter(req => req.status === 'accepted').length} + {summary?.totalConnections || 0}

- +
@@ -92,41 +163,51 @@ export const EntrepreneurDashboard: React.FC = () => {
-

Upcoming Meetings

-

2

+

+ Upcoming Meetings +

+

+ {summary?.upcomingMeetings || 0} +

- +
- +
-

Profile Views

-

24

+

+ New Messages +

+

+ {summary?.unreadMessages || 0} +

- +
{/* Collaboration requests */}
-

Collaboration Requests

+

+ Collaboration Requests +

{pendingRequests.length} pending
- + {collaborationRequests.length > 0 ? (
- {collaborationRequests.map(request => ( + {collaborationRequests.map((request) => ( {

No collaboration requests yet

-

When investors are interested in your startup, their requests will appear here

+

+ When investors are interested in your startup, their + requests will appear here +

)} + + {/* Upcoming Meetings List */} + {summary?.meetings && summary.meetings.length > 0 && ( + + +

+ Upcoming Meetings +

+
+ +
+ {summary.meetings.map((meeting: Meeting) => ( +
+
+
+ + {format(parseISO(meeting.startTime), "MMM")} + + + {format(parseISO(meeting.startTime), "d")} + +
+
+

+ {meeting.title} +

+
+ + + {format(parseISO(meeting.startTime), "h:mm a")} + + + + {meeting.location} + +
+
+
+ + + +
+ ))} +
+
+
+ )}
- - {/* Recommended investors */} + + {/* Sidebar - right side */}
+ {/* Recent Documents */} -

Recommended Investors

- +

+ Recent Documents +

+ View all
- + +
+ {recentDocuments.length > 0 ? ( + recentDocuments.map((doc) => ( +
+
+ +
+
+

+ {doc.name} +

+
+ + {doc.type} + + {doc.shared && ( + + Public + + )} +
+
+
+ )) + ) : ( +
+

+ No documents uploaded yet +

+ + + +
+ )} +
+
+
+ + + +

+ Recommended Investors +

+ + View all + +
+ - {recommendedInvestors.map(investor => ( + {recommendedInvestors.map((investor) => ( {
); -}; \ No newline at end of file +}; diff --git a/src/pages/dashboard/InvestorDashboard.tsx b/src/pages/dashboard/InvestorDashboard.tsx index b72480abc..95dcf20d6 100644 --- a/src/pages/dashboard/InvestorDashboard.tsx +++ b/src/pages/dashboard/InvestorDashboard.tsx @@ -1,107 +1,116 @@ -import React, { useState } from 'react'; -import { Link } from 'react-router-dom'; -import { Users, PieChart, Filter, Search, PlusCircle } from 'lucide-react'; -import { Button } from '../../components/ui/Button'; -import { Card, CardBody, CardHeader } from '../../components/ui/Card'; -import { Input } from '../../components/ui/Input'; -import { Badge } from '../../components/ui/Badge'; -import { EntrepreneurCard } from '../../components/entrepreneur/EntrepreneurCard'; -import { useAuth } from '../../context/AuthContext'; -import { Entrepreneur } from '../../types'; -import { entrepreneurs } from '../../data/users'; -import { getRequestsFromInvestor } from '../../data/collaborationRequests'; +import React, { useState, useEffect } from "react"; +import { Link } from "react-router-dom"; +import { + Users, + MessageCircle, + Filter, + Search, + PlusCircle, + Calendar, + Clock, +} from "lucide-react"; +import { Button } from "../../components/ui/Button"; +import { Card, CardBody, CardHeader } from "../../components/ui/Card"; +import { Input } from "../../components/ui/Input"; +import { Badge } from "../../components/ui/Badge"; +import { EntrepreneurCard } from "../../components/entrepreneur/EntrepreneurCard"; +import { useAuth } from "../../context/AuthContext"; +import { Entrepreneur, Meeting } from "../../types"; +import api from "../../services/api"; +import dashboardService, { + DashboardSummary, +} from "../../services/dashboardService"; +import { format, parseISO } from "date-fns"; export const InvestorDashboard: React.FC = () => { const { user } = useAuth(); - const [searchQuery, setSearchQuery] = useState(''); + const [summary, setSummary] = useState(null); + const [entrepreneurs, setEntrepreneurs] = useState([]); + const [searchQuery, setSearchQuery] = useState(""); const [selectedIndustries, setSelectedIndustries] = useState([]); - + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const fetchData = async () => { + if (user) { + setIsLoading(true); + try { + // Fetch summary stats + const summaryData = await dashboardService.getSummary(); + setSummary(summaryData); + + // Fetch entrepreneurs for directory from API + const entrepreneursRes = await api.get("/directory/entrepreneurs"); + setEntrepreneurs(entrepreneursRes.data); + } catch (error) { + console.error("Failed to fetch investor dashboard data:", error); + } finally { + setIsLoading(false); + } + } + }; + fetchData(); + }, [user]); + if (!user) return null; - - // Get collaboration requests sent by this investor - const sentRequests = getRequestsFromInvestor(user.id); - const requestedEntrepreneurIds = sentRequests.map(req => req.entrepreneurId); - + // Filter entrepreneurs based on search and industry filters - const filteredEntrepreneurs = entrepreneurs.filter(entrepreneur => { - // Search filter - const matchesSearch = searchQuery === '' || + const filteredEntrepreneurs = entrepreneurs.filter((entrepreneur) => { + const matchesSearch = + searchQuery === "" || entrepreneur.name.toLowerCase().includes(searchQuery.toLowerCase()) || - entrepreneur.startupName.toLowerCase().includes(searchQuery.toLowerCase()) || + entrepreneur.startupName + .toLowerCase() + .includes(searchQuery.toLowerCase()) || entrepreneur.industry.toLowerCase().includes(searchQuery.toLowerCase()) || - entrepreneur.pitchSummary.toLowerCase().includes(searchQuery.toLowerCase()); - - // Industry filter - const matchesIndustry = selectedIndustries.length === 0 || + entrepreneur.pitchSummary + .toLowerCase() + .includes(searchQuery.toLowerCase()); + + const matchesIndustry = + selectedIndustries.length === 0 || selectedIndustries.includes(entrepreneur.industry); - + return matchesSearch && matchesIndustry; }); - - // Get unique industries for filter - const industries = Array.from(new Set(entrepreneurs.map(e => e.industry))); - - // Toggle industry selection + + const industries = Array.from(new Set(entrepreneurs.map((e) => e.industry))); + const toggleIndustry = (industry: string) => { - setSelectedIndustries(prevSelected => + setSelectedIndustries((prevSelected) => prevSelected.includes(industry) - ? prevSelected.filter(i => i !== industry) - : [...prevSelected, industry] + ? prevSelected.filter((i) => i !== industry) + : [...prevSelected, industry], ); }; - + + if (isLoading) { + return ( +
+
+
+ ); + } + return (
-

Discover Startups

-

Find and connect with promising entrepreneurs

+

+ Welcome back, {user.name} +

+

+ Discover and connect with innovative startups +

- + - +
- - {/* Filters and search */} -
-
- setSearchQuery(e.target.value)} - fullWidth - startAdornment={} - /> -
- -
-
- - Filter by: - -
- {industries.map(industry => ( - toggleIndustry(industry)} - > - {industry} - - ))} -
-
-
-
- + {/* Stats summary */} -
+
@@ -109,27 +118,35 @@ export const InvestorDashboard: React.FC = () => {
-

Total Startups

-

{entrepreneurs.length}

+

+ Total Startups +

+

+ {summary?.totalStartups || 0} +

- +
- +
-

Industries

-

{industries.length}

+

+ New Messages +

+

+ {summary?.unreadMessages || 0} +

- +
@@ -137,51 +154,185 @@ export const InvestorDashboard: React.FC = () => {
-

Your Connections

+

+ Connections +

- {sentRequests.filter(req => req.status === 'accepted').length} + {summary?.totalConnections || 0}

-
- - {/* Entrepreneurs grid */} -
- - -

Featured Startups

-
- + + - {filteredEntrepreneurs.length > 0 ? ( -
- {filteredEntrepreneurs.map(entrepreneur => ( - - ))} +
+
+
- ) : ( -
-

No startups match your filters

- +
+

Upcoming

+

+ {summary?.upcomingMeetings || 0} +

- )} +
+ +
+
+ {/* Filters and search */} + + +
+
+ setSearchQuery(e.target.value)} + fullWidth + startAdornment={} + /> +
+ +
+ + + Filter by: + + +
+ {industries.slice(0, 3).map((industry) => ( + toggleIndustry(industry)} + > + {industry} + + ))} + {industries.length > 3 && ( + + +{industries.length - 3} more + + )} +
+
+
+
+
+ + {/* Entrepreneurs grid */} +
+ + +

+ Recommended Startups +

+
+ + + {filteredEntrepreneurs.length > 0 ? ( +
+ {filteredEntrepreneurs.slice(0, 4).map((entrepreneur) => ( + + ))} +
+ ) : ( +
+

+ No startups match your filters +

+
+ )} +
+
+
+
+ +
+ {/* Upcoming Meetings List */} + {summary?.meetings && summary.meetings.length > 0 && ( + + +

Schedule

+
+ +
+ {summary.meetings.map((meeting: Meeting) => ( +
+
+
+ + {format(parseISO(meeting.startTime), "MMM")} + + + {format(parseISO(meeting.startTime), "d")} + +
+
+

+ {meeting.title} +

+

+ {format(parseISO(meeting.startTime), "h:mm a")} +

+
+
+ + + +
+ ))} +
+
+
+ )} + + {/* Quick Actions */} + + +

+ Quick Actions +

+
+ + + + + + + + +
+
+
); -}; \ No newline at end of file +}; diff --git a/src/pages/deals/DealsPage.tsx b/src/pages/deals/DealsPage.tsx index ea26870c5..b872a15cc 100644 --- a/src/pages/deals/DealsPage.tsx +++ b/src/pages/deals/DealsPage.tsx @@ -1,97 +1,344 @@ -import React, { useState } from 'react'; -import { Search, Filter, DollarSign, TrendingUp, Users, Calendar } from 'lucide-react'; -import { Card, CardHeader, CardBody } from '../../components/ui/Card'; -import { Input } from '../../components/ui/Input'; -import { Button } from '../../components/ui/Button'; -import { Badge } from '../../components/ui/Badge'; -import { Avatar } from '../../components/ui/Avatar'; - -const deals = [ - { - id: 1, - startup: { - name: 'TechWave AI', - logo: 'https://images.pexels.com/photos/774909/pexels-photo-774909.jpeg', - industry: 'FinTech' - }, - amount: '$1.5M', - equity: '15%', - status: 'Due Diligence', - stage: 'Series A', - lastActivity: '2024-02-15' - }, - { - id: 2, - startup: { - name: 'GreenLife Solutions', - logo: 'https://images.pexels.com/photos/614810/pexels-photo-614810.jpeg', - industry: 'CleanTech' - }, - amount: '$2M', - equity: '20%', - status: 'Term Sheet', - stage: 'Seed', - lastActivity: '2024-02-10' - }, - { - id: 3, - startup: { - name: 'HealthPulse', - logo: 'https://images.pexels.com/photos/415829/pexels-photo-415829.jpeg', - industry: 'HealthTech' - }, - amount: '$800K', - equity: '12%', - status: 'Negotiation', - stage: 'Pre-seed', - lastActivity: '2024-02-05' - } -]; +import React, { useState, useEffect } from "react"; +import { Link } from "react-router-dom"; +import { + Search, + Filter, + DollarSign, + TrendingUp, + Users, + Calendar, + X, +} from "lucide-react"; +import { Card, CardHeader, CardBody } from "../../components/ui/Card"; +import { Input } from "../../components/ui/Input"; +import { Button } from "../../components/ui/Button"; +import { Badge } from "../../components/ui/Badge"; +import { Avatar } from "../../components/ui/Avatar"; +import { useAuth } from "../../context/AuthContext"; +import { Deal, User as UserType } from "../../types"; +import dealService from "../../services/dealService"; +import api from "../../services/api"; +import toast from "react-hot-toast"; export const DealsPage: React.FC = () => { - const [searchQuery, setSearchQuery] = useState(''); + const { user } = useAuth(); + const [deals, setDeals] = useState([]); + const [entrepreneurs, setEntrepreneurs] = useState>( + {}, + ); + const [allEntrepreneurs, setAllEntrepreneurs] = useState([]); + const [searchQuery, setSearchQuery] = useState(""); const [selectedStatus, setSelectedStatus] = useState([]); - - const statuses = ['Due Diligence', 'Term Sheet', 'Negotiation', 'Closed', 'Passed']; - + const [isLoading, setIsLoading] = useState(true); + const [showAddModal, setShowAddModal] = useState(false); + + // New deal form state + const [newDeal, setNewDeal] = useState({ + entrepreneurId: "", + amount: "", + equity: "", + stage: "Seed", + status: "Due Diligence", + notes: "", + }); + + const statuses = [ + "Due Diligence", + "Term Sheet", + "Negotiation", + "Closed", + "Passed", + ]; + + useEffect(() => { + const fetchDeals = async () => { + if (user) { + try { + const dealsData = await dealService.getDeals(); + setDeals(dealsData); + + // Fetch entrepreneur details + const entrepreneurIds = new Set( + dealsData.map((d) => d.entrepreneurId), + ); + const entrepreneurPromises = Array.from(entrepreneurIds).map((id) => + api.get(`/users/${id}`), + ); + const entrepreneurResponses = await Promise.all(entrepreneurPromises); + + const entrepreneursMap: Record = {}; + entrepreneurResponses.forEach((response) => { + entrepreneursMap[response.data.id] = response.data; + }); + setEntrepreneurs(entrepreneursMap); + + // Fetch all entrepreneurs for dropdown + const allEntrepreneursRes = await api.get("/directory/entrepreneurs"); + setAllEntrepreneurs(allEntrepreneursRes.data); + } catch (error) { + console.error("Failed to fetch deals:", error); + } finally { + setIsLoading(false); + } + } + }; + fetchDeals(); + }, [user]); + + const handleCreateDeal = async (e: React.FormEvent) => { + e.preventDefault(); + try { + const createdDeal = await dealService.createDeal(newDeal); + setDeals((prev) => [createdDeal, ...prev]); + + // Fetch entrepreneur details + const entrepreneurRes = await api.get(`/users/${newDeal.entrepreneurId}`); + setEntrepreneurs((prev) => ({ + ...prev, + [newDeal.entrepreneurId]: entrepreneurRes.data, + })); + + toast.success("Deal created successfully"); + setShowAddModal(false); + setNewDeal({ + entrepreneurId: "", + amount: "", + equity: "", + stage: "Seed", + status: "Due Diligence", + notes: "", + }); + } catch (error) { + console.error("Failed to create deal:", error); + toast.error("Failed to create deal"); + } + }; + const toggleStatus = (status: string) => { - setSelectedStatus(prev => + setSelectedStatus((prev) => prev.includes(status) - ? prev.filter(s => s !== status) - : [...prev, status] + ? prev.filter((s) => s !== status) + : [...prev, status], ); }; - + const getStatusColor = (status: string) => { switch (status) { - case 'Due Diligence': - return 'primary'; - case 'Term Sheet': - return 'secondary'; - case 'Negotiation': - return 'accent'; - case 'Closed': - return 'success'; - case 'Passed': - return 'error'; + case "Due Diligence": + return "primary"; + case "Term Sheet": + return "secondary"; + case "Negotiation": + return "accent"; + case "Closed": + return "success"; + case "Passed": + return "error"; default: - return 'gray'; + return "gray"; } }; - + + const filteredDeals = deals.filter((deal) => { + const entrepreneur = entrepreneurs[deal.entrepreneurId]; + const matchesSearch = + searchQuery === "" || + entrepreneur?.name.toLowerCase().includes(searchQuery.toLowerCase()) || + entrepreneur?.startupName + ?.toLowerCase() + .includes(searchQuery.toLowerCase()) || + entrepreneur?.industry?.toLowerCase().includes(searchQuery.toLowerCase()); + + const matchesStatus = + selectedStatus.length === 0 || selectedStatus.includes(deal.status); + + return matchesSearch && matchesStatus; + }); + + // Calculate stats + const totalInvestment = deals + .filter((d) => d.status === "Closed") + .reduce((sum, d) => { + const amount = parseFloat(d.amount.replace(/[$,M]/g, "")); + return sum + (isNaN(amount) ? 0 : amount); + }, 0); + + const activeDeals = deals.filter( + (d) => d.status !== "Closed" && d.status !== "Passed", + ).length; + + const portfolioCompanies = deals.filter((d) => d.status === "Closed").length; + + const closedThisMonth = deals.filter((d) => { + if (d.status !== "Closed") return false; + const dealDate = new Date(d.lastActivity); + const now = new Date(); + return ( + dealDate.getMonth() === now.getMonth() && + dealDate.getFullYear() === now.getFullYear() + ); + }).length; + + if (isLoading) { + return ( +
+
+
+ ); + } + return (

Investment Deals

-

Track and manage your investment pipeline

+

+ Track and manage your investment pipeline +

- - + +
- + + {/* Add Deal Modal */} + {showAddModal && ( +
+
+
+

+ Add New Deal +

+ +
+ + +
+ + +
+ +
+
+ + + setNewDeal({ ...newDeal, amount: e.target.value }) + } + /> +
+ +
+ + + setNewDeal({ ...newDeal, equity: e.target.value }) + } + /> +
+
+ +
+
+ + +
+ +
+ + +
+
+ +
+ +
- +
- +
); -}; \ No newline at end of file +}; diff --git a/src/pages/investors/InvestorsPage.tsx b/src/pages/investors/InvestorsPage.tsx index 0f234cf4d..155a54f15 100644 --- a/src/pages/investors/InvestorsPage.tsx +++ b/src/pages/investors/InvestorsPage.tsx @@ -1,61 +1,102 @@ -import React, { useState } from 'react'; -import { Search, Filter, MapPin } from 'lucide-react'; -import { Input } from '../../components/ui/Input'; -import { Card, CardHeader, CardBody } from '../../components/ui/Card'; -import { Badge } from '../../components/ui/Badge'; -import { InvestorCard } from '../../components/investor/InvestorCard'; -import { investors } from '../../data/users'; +import React, { useState, useEffect } from "react"; +import { Search, Filter } from "lucide-react"; +import { Input } from "../../components/ui/Input"; +import { Card, CardHeader, CardBody } from "../../components/ui/Card"; +import { Badge } from "../../components/ui/Badge"; +import { InvestorCard } from "../../components/investor/InvestorCard"; +import { Investor } from "../../types"; +import api from "../../services/api"; export const InvestorsPage: React.FC = () => { - const [searchQuery, setSearchQuery] = useState(''); + const [investors, setInvestors] = useState([]); + const [searchQuery, setSearchQuery] = useState(""); const [selectedStages, setSelectedStages] = useState([]); const [selectedInterests, setSelectedInterests] = useState([]); - + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const fetchInvestors = async () => { + setIsLoading(true); + try { + const res = await api.get("/directory/investors"); + setInvestors(res.data); + } catch (error) { + console.error("Failed to fetch investors:", error); + } finally { + setIsLoading(false); + } + }; + fetchInvestors(); + }, []); + // Get unique investment stages and interests - const allStages = Array.from(new Set(investors.flatMap(i => i.investmentStage))); - const allInterests = Array.from(new Set(investors.flatMap(i => i.investmentInterests))); - + const allStages = Array.from( + new Set(investors.flatMap((i) => i.investmentStage || [])), + ); + const allInterests = Array.from( + new Set(investors.flatMap((i) => i.investmentInterests || [])), + ); + // Filter investors based on search and filters - const filteredInvestors = investors.filter(investor => { - const matchesSearch = searchQuery === '' || + const filteredInvestors = investors.filter((investor) => { + const matchesSearch = + searchQuery === "" || investor.name.toLowerCase().includes(searchQuery.toLowerCase()) || - investor.bio.toLowerCase().includes(searchQuery.toLowerCase()) || - investor.investmentInterests.some(interest => - interest.toLowerCase().includes(searchQuery.toLowerCase()) - ); - - const matchesStages = selectedStages.length === 0 || - investor.investmentStage.some(stage => selectedStages.includes(stage)); - - const matchesInterests = selectedInterests.length === 0 || - investor.investmentInterests.some(interest => selectedInterests.includes(interest)); - + (investor.bio && + investor.bio.toLowerCase().includes(searchQuery.toLowerCase())) || + (investor.investmentInterests && + investor.investmentInterests.some((interest) => + interest.toLowerCase().includes(searchQuery.toLowerCase()), + )); + + const matchesStages = + selectedStages.length === 0 || + (investor.investmentStage && + investor.investmentStage.some((stage) => + selectedStages.includes(stage), + )); + + const matchesInterests = + selectedInterests.length === 0 || + (investor.investmentInterests && + investor.investmentInterests.some((interest) => + selectedInterests.includes(interest), + )); + return matchesSearch && matchesStages && matchesInterests; }); - + const toggleStage = (stage: string) => { - setSelectedStages(prev => - prev.includes(stage) - ? prev.filter(s => s !== stage) - : [...prev, stage] + setSelectedStages((prev) => + prev.includes(stage) ? prev.filter((s) => s !== stage) : [...prev, stage], ); }; - + const toggleInterest = (interest: string) => { - setSelectedInterests(prev => + setSelectedInterests((prev) => prev.includes(interest) - ? prev.filter(i => i !== interest) - : [...prev, interest] + ? prev.filter((i) => i !== interest) + : [...prev, interest], ); }; - + + if (isLoading) { + return ( +
+
+
+ ); + } + return (

Find Investors

-

Connect with investors who match your startup's needs

+

+ Connect with investors who match your startup's needs +

- +
{/* Filters sidebar */}
@@ -65,16 +106,18 @@ export const InvestorsPage: React.FC = () => {
-

Investment Stage

+

+ Investment Stage +

- {allStages.map(stage => ( + {allStages.map((stage) => (
- +
-

Investment Interests

+

+ Investment Interests +

- {allInterests.map(interest => ( + {allInterests.map((interest) => ( toggleInterest(interest)} > @@ -98,28 +147,10 @@ export const InvestorsPage: React.FC = () => { ))}
- -
-

Location

-
- - - -
-
- + {/* Main content */}
@@ -130,7 +161,7 @@ export const InvestorsPage: React.FC = () => { startAdornment={} fullWidth /> - +
@@ -138,17 +169,14 @@ export const InvestorsPage: React.FC = () => {
- +
- {filteredInvestors.map(investor => ( - + {filteredInvestors.map((investor) => ( + ))}
); -}; \ No newline at end of file +}; diff --git a/src/pages/meetings/MeetingsPage.tsx b/src/pages/meetings/MeetingsPage.tsx new file mode 100644 index 000000000..069e5b11c --- /dev/null +++ b/src/pages/meetings/MeetingsPage.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import { MeetingCalendar } from "../../components/meetings/MeetingCalendar"; + +export const MeetingsPage: React.FC = () => { + return ( +
+
+

Your Schedule

+

Manage your meetings and appointments

+
+ + +
+ ); +}; + +export default MeetingsPage; diff --git a/src/pages/messages/MessagesPage.tsx b/src/pages/messages/MessagesPage.tsx index c7589fb80..103d5369b 100644 --- a/src/pages/messages/MessagesPage.tsx +++ b/src/pages/messages/MessagesPage.tsx @@ -1,18 +1,43 @@ -import React from 'react'; -import { useNavigate } from 'react-router-dom'; -import { useAuth } from '../../context/AuthContext'; -import { getConversationsForUser } from '../../data/messages'; -import { ChatUserList } from '../../components/chat/ChatUserList'; -// import { MessageCircle } from 'lucide-react'; +import React, { useState, useEffect } from "react"; +import { useAuth } from "../../context/AuthContext"; +import { messageService } from "../../services/messageService"; +import { ChatUserList } from "../../components/chat/ChatUserList"; +import { ChatConversation } from "../../types"; +import { MessageCircle } from "lucide-react"; export const MessagesPage: React.FC = () => { const { user } = useAuth(); - const navigate = useNavigate(); - + const [conversations, setConversations] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const fetchConversations = async () => { + if (user) { + try { + const data = await messageService.getConversations(); + setConversations(data); + } catch (error) { + console.error("Failed to fetch conversations:", error); + } finally { + setIsLoading(false); + } + } + }; + fetchConversations(); + const interval = setInterval(fetchConversations, 5000); // Poll for new messages/conversations + return () => clearInterval(interval); + }, [user]); + if (!user) return null; - - const conversations = getConversationsForUser(user.id); - + + if (isLoading) { + return ( +
+
+
+ ); + } + return (
{conversations.length > 0 ? ( @@ -20,14 +45,15 @@ export const MessagesPage: React.FC = () => { ) : (
- {/* */} +

No messages yet

- Start connecting with entrepreneurs and investors to begin conversations + Start connecting with entrepreneurs and investors to begin + conversations

)}
); -}; \ No newline at end of file +}; diff --git a/src/pages/notifications/NotificationsPage.tsx b/src/pages/notifications/NotificationsPage.tsx index 3bb806c25..091ba0b23 100644 --- a/src/pages/notifications/NotificationsPage.tsx +++ b/src/pages/notifications/NotificationsPage.tsx @@ -1,112 +1,208 @@ -import React from 'react'; -import { Bell, MessageCircle, UserPlus, DollarSign } from 'lucide-react'; -import { Card, CardBody } from '../../components/ui/Card'; -import { Avatar } from '../../components/ui/Avatar'; -import { Badge } from '../../components/ui/Badge'; -import { Button } from '../../components/ui/Button'; - -const notifications = [ - { - id: 1, - type: 'message', - user: { - name: 'Sarah Johnson', - avatar: 'https://images.pexels.com/photos/774909/pexels-photo-774909.jpeg' - }, - content: 'sent you a message about your startup', - time: '5 minutes ago', - unread: true - }, - { - id: 2, - type: 'connection', - user: { - name: 'Michael Rodriguez', - avatar: 'https://images.pexels.com/photos/2379004/pexels-photo-2379004.jpeg' - }, - content: 'accepted your connection request', - time: '2 hours ago', - unread: true - }, - { - id: 3, - type: 'investment', - user: { - name: 'Jennifer Lee', - avatar: 'https://images.pexels.com/photos/1181686/pexels-photo-1181686.jpeg' - }, - content: 'showed interest in investing in your startup', - time: '1 day ago', - unread: false - } -]; +import React, { useState, useEffect } from "react"; +import { Link } from "react-router-dom"; +import { + Bell, + MessageCircle, + UserPlus, + DollarSign, + Calendar, + Trash2, +} from "lucide-react"; +import { Card, CardBody } from "../../components/ui/Card"; +import { Badge } from "../../components/ui/Badge"; +import { Button } from "../../components/ui/Button"; +import { useAuth } from "../../context/AuthContext"; +import { notificationService } from "../../services/notificationService"; +import { Notification } from "../../types"; +import { formatDistanceToNow, parseISO } from "date-fns"; +import toast from "react-hot-toast"; export const NotificationsPage: React.FC = () => { + const { user } = useAuth(); + const [notifications, setNotifications] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + const fetchNotifications = async () => { + if (user) { + try { + const data = await notificationService.getNotifications(); + setNotifications(data); + } catch (error) { + console.error("Failed to fetch notifications:", error); + } finally { + setIsLoading(false); + } + } + }; + + useEffect(() => { + fetchNotifications(); + const interval = setInterval(fetchNotifications, 10000); + return () => clearInterval(interval); + }, [user]); + + const handleMarkAsRead = async (id: string) => { + try { + await notificationService.markAsRead(id); + setNotifications((prev) => + prev.map((n) => (n.id === id ? { ...n, isRead: true } : n)), + ); + } catch (error) { + console.error("Failed to mark as read:", error); + } + }; + + const handleMarkAllAsRead = async () => { + try { + await notificationService.markAllAsRead(); + setNotifications((prev) => prev.map((n) => ({ ...n, isRead: true }))); + toast.success("All notifications marked as read"); + } catch (error) { + console.error("Failed to mark all as read:", error); + } + }; + + const handleDeleteNotification = async (id: string, e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + try { + await notificationService.deleteNotification(id); + setNotifications((prev) => prev.filter((n) => n.id !== id)); + toast.success("Notification deleted"); + } catch (error) { + console.error("Failed to delete notification:", error); + } + }; + const getNotificationIcon = (type: string) => { switch (type) { - case 'message': - return ; - case 'connection': - return ; - case 'investment': - return ; + case "message": + return ; + case "collaboration_request": + return ; + case "collaboration_accepted": + return ; + case "meeting_scheduled": + case "meeting_status": + return ; default: - return ; + return ; } }; - + + if (isLoading) { + return ( +
+
+
+ ); + } + return ( -
+

Notifications

-

Stay updated with your network activity

+

+ Stay updated with your latest network activity +

- - + + {notifications.length > 0 && ( + + )}
- -
- {notifications.map(notification => ( - - - - -
-
- - {notification.user.name} - - {notification.unread && ( - New - )} -
- -

- {notification.content} -

- -
- {getNotificationIcon(notification.type)} - {notification.time} -
-
-
-
- ))} + +
+ {notifications.length > 0 ? ( + notifications.map((notification) => ( + + !notification.isRead && handleMarkAsRead(notification.id) + } + className="block" + > + + +
+ {getNotificationIcon(notification.type)} +
+ +
+
+
+

+ {notification.title} +

+ {!notification.isRead && ( + + New + + )} +
+ + {formatDistanceToNow(parseISO(notification.createdAt), { + addSuffix: true, + })} + +
+ +

+ {notification.message} +

+
+ + +
+
+ + )) + ) : ( +
+
+ +
+

+ No notifications +

+

+ We'll let you know when something important happens. +

+
+ )}
); -}; \ No newline at end of file +}; diff --git a/src/pages/profile/EntrepreneurProfile.tsx b/src/pages/profile/EntrepreneurProfile.tsx index 3c05a18e6..aaed985cf 100644 --- a/src/pages/profile/EntrepreneurProfile.tsx +++ b/src/pages/profile/EntrepreneurProfile.tsx @@ -1,56 +1,118 @@ -import React from 'react'; -import { useParams, Link } from 'react-router-dom'; -import { MessageCircle, Users, Calendar, Building2, MapPin, UserCircle, FileText, DollarSign, Send } from 'lucide-react'; -import { Avatar } from '../../components/ui/Avatar'; -import { Button } from '../../components/ui/Button'; -import { Card, CardBody, CardHeader } from '../../components/ui/Card'; -import { Badge } from '../../components/ui/Badge'; -import { useAuth } from '../../context/AuthContext'; -import { findUserById } from '../../data/users'; -import { createCollaborationRequest, getRequestsFromInvestor } from '../../data/collaborationRequests'; -import { Entrepreneur } from '../../types'; +import React, { useState, useEffect } from "react"; +import { useParams, Link } from "react-router-dom"; +import { + MessageCircle, + Users, + Calendar, + Building2, + MapPin, + UserCircle, + FileText, + DollarSign, + Send, + CalendarDays, +} from "lucide-react"; +import { ScheduleMeetingModal } from "../../components/meetings/ScheduleMeetingModal"; +import { Avatar } from "../../components/ui/Avatar"; +import { Button } from "../../components/ui/Button"; +import { Card, CardBody, CardHeader } from "../../components/ui/Card"; +import { Badge } from "../../components/ui/Badge"; +import { useAuth } from "../../context/AuthContext"; +import { Entrepreneur, CollaborationRequest } from "../../types"; +import api from "../../services/api"; export const EntrepreneurProfile: React.FC = () => { const { id } = useParams<{ id: string }>(); const { user: currentUser } = useAuth(); - - // Fetch entrepreneur data - const entrepreneur = findUserById(id || '') as Entrepreneur | null; - - if (!entrepreneur || entrepreneur.role !== 'entrepreneur') { + const [entrepreneur, setEntrepreneur] = useState(null); + const [collaborationRequests, setCollaborationRequests] = useState< + CollaborationRequest[] + >([]); + const [isMeetingModalOpen, setIsMeetingModalOpen] = useState(false); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const fetchProfileData = async () => { + if (!id) return; + + setIsLoading(true); + try { + const res = await api.get(`/users/${id}`); + setEntrepreneur(res.data); + + // If current user is an investor, check for collaboration requests + if (currentUser?.role === "investor") { + const requestsRes = await api.get("/collaboration"); + setCollaborationRequests(requestsRes.data); + } + } catch (error) { + console.error("Failed to fetch entrepreneur profile:", error); + } finally { + setIsLoading(false); + } + }; + + fetchProfileData(); + }, [id, currentUser]); + + if (isLoading) { + return ( +
+
+
+ ); + } + + if (!entrepreneur || entrepreneur.role !== "entrepreneur") { return (
-

Entrepreneur not found

-

The entrepreneur profile you're looking for doesn't exist or has been removed.

+

+ Entrepreneur not found +

+

+ The entrepreneur profile you're looking for doesn't exist or has been + removed. +

- +
); } - + const isCurrentUser = currentUser?.id === entrepreneur.id; - const isInvestor = currentUser?.role === 'investor'; - - // Check if the current investor has already sent a request to this entrepreneur - const hasRequestedCollaboration = isInvestor && id - ? getRequestsFromInvestor(currentUser.id).some(req => req.entrepreneurId === id) - : false; - - const handleSendRequest = () => { + const isInvestor = currentUser?.role === "investor"; + + const collaborationRequest = + isInvestor && id + ? collaborationRequests.find( + (req) => + req.entrepreneurId === id && req.investorId === currentUser.id, + ) + : null; + + const hasRequestedCollaboration = !!collaborationRequest; + const isCollaborationAccepted = collaborationRequest?.status === "accepted"; + + const handleSendRequest = async () => { if (isInvestor && currentUser && id) { - createCollaborationRequest( - currentUser.id, - id, - `I'm interested in learning more about ${entrepreneur.startupName} and would like to explore potential investment opportunities.` - ); - - // In a real app, we would refresh the data or update state - // For this demo, we'll force a page reload - window.location.reload(); + try { + await api.post("/collaboration", { + entrepreneurId: id, + message: `I'm interested in learning more about ${entrepreneur.startupName} and would like to explore potential investment opportunities.`, + }); + + // Refresh collaboration requests + const requestsRes = await api.get("/collaboration"); + setCollaborationRequests(requestsRes.data); + } catch (error) { + console.error("Failed to send collaboration request:", error); + } } }; - + return (
{/* Profile header */} @@ -61,35 +123,41 @@ export const EntrepreneurProfile: React.FC = () => { src={entrepreneur.avatarUrl} alt={entrepreneur.name} size="xl" - status={entrepreneur.isOnline ? 'online' : 'offline'} + status={entrepreneur.isOnline ? "online" : "offline"} className="mx-auto sm:mx-0" /> - +
-

{entrepreneur.name}

+

+ {entrepreneur.name} +

Founder at {entrepreneur.startupName}

- +
{entrepreneur.industry} {entrepreneur.location} - - - Founded {entrepreneur.foundedYear} - - - - {entrepreneur.teamSize} team members - + {entrepreneur.foundedYear && ( + + + Founded {entrepreneur.foundedYear} + + )} + {entrepreneur.teamSize && ( + + + {entrepreneur.teamSize} team members + + )}
- +
{!isCurrentUser && ( <> @@ -101,31 +169,47 @@ export const EntrepreneurProfile: React.FC = () => { Message - + {isInvestor && ( - + <> + + + )} )} - + {isCurrentUser && ( - + + + )}
- +
{/* Main content - left side */}
@@ -135,53 +219,51 @@ export const EntrepreneurProfile: React.FC = () => {

About

-

{entrepreneur.bio}

+

+ {entrepreneur.bio || "No bio provided."} +

- + {/* Startup Description */} -

Startup Overview

+

+ Startup Overview +

-

Problem Statement

+

+ Pitch Summary +

- {entrepreneur?.pitchSummary?.split('.')[0]}. + {entrepreneur.pitchSummary || "No pitch summary provided."}

- -
-

Solution

-

- {entrepreneur.pitchSummary} -

-
- -
-

Market Opportunity

-

- The {entrepreneur.industry} market is experiencing significant growth, with a projected CAGR of 14.5% through 2027. Our solution addresses key pain points in this expanding market. -

-
- +
-

Competitive Advantage

+

+ Market Opportunity +

- Unlike our competitors, we offer a unique approach that combines innovative technology with deep industry expertise, resulting in superior outcomes for our customers. + The {entrepreneur.industry} market is experiencing + significant growth. Our solution addresses key pain points + in this expanding market.

- + {/* Team */}

Team

- {entrepreneur.teamSize} members + + {entrepreneur.teamSize || 1} members +
@@ -193,47 +275,17 @@ export const EntrepreneurProfile: React.FC = () => { className="mr-3" />
-

{entrepreneur.name}

+

+ {entrepreneur.name} +

Founder & CEO

- -
- -
-

Alex Johnson

-

CTO

-
-
- -
- -
-

Jessica Chen

-

Head of Product

-
-
- - {entrepreneur.teamSize > 3 && ( -
-

+ {entrepreneur.teamSize - 3} more team members

-
- )}
- + {/* Sidebar - right side */}
{/* Funding Details */} @@ -247,41 +299,15 @@ export const EntrepreneurProfile: React.FC = () => { Current Round
-

{entrepreneur.fundingNeeded}

-
-
- -
- Valuation -

$8M - $12M

-
- -
- Previous Funding -

$750K Seed (2022)

-
- -
- Funding Timeline -
-
- Pre-seed - Completed -
-
- Seed - Completed -
-
- Series A - In Progress -
+

+ {entrepreneur.fundingNeeded || "Undisclosed"} +

- + {/* Documents */} @@ -294,62 +320,47 @@ export const EntrepreneurProfile: React.FC = () => {
-

Pitch Deck

-

Updated 2 months ago

-
- -
- -
-
- -
-
-

Business Plan

-

Updated 1 month ago

+

+ Pitch Deck +

+

+ Request access to view +

- -
- -
-
- -
-
-

Financial Projections

-

Updated 2 weeks ago

-
-
- + {!isCurrentUser && isInvestor && (

- Request access to detailed documents and financials by sending a collaboration request. + Request access to detailed documents and financials by + sending a collaboration request.

- - {!hasRequestedCollaboration ? ( - - ) : ( - - )} + +
)}
+ + {currentUser?.id && id && ( + setIsMeetingModalOpen(false)} + investorId={currentUser.id} + entrepreneurId={id} + /> + )} ); -}; \ No newline at end of file +}; diff --git a/src/pages/profile/InvestorProfile.tsx b/src/pages/profile/InvestorProfile.tsx index 22b722d62..c723e1e23 100644 --- a/src/pages/profile/InvestorProfile.tsx +++ b/src/pages/profile/InvestorProfile.tsx @@ -1,35 +1,71 @@ -import React from 'react'; -import { useParams, Link } from 'react-router-dom'; -import { MessageCircle, Building2, MapPin, UserCircle, BarChart3, Briefcase } from 'lucide-react'; -import { Avatar } from '../../components/ui/Avatar'; -import { Button } from '../../components/ui/Button'; -import { Card, CardBody, CardHeader } from '../../components/ui/Card'; -import { Badge } from '../../components/ui/Badge'; -import { useAuth } from '../../context/AuthContext'; -import { findUserById } from '../../data/users'; -import { Investor } from '../../types'; +import React, { useState, useEffect } from "react"; +import { useParams, Link } from "react-router-dom"; +import { + MessageCircle, + Building2, + MapPin, + UserCircle, + BarChart3, +} from "lucide-react"; +import { Avatar } from "../../components/ui/Avatar"; +import { Button } from "../../components/ui/Button"; +import { Card, CardBody, CardHeader } from "../../components/ui/Card"; +import { Badge } from "../../components/ui/Badge"; +import { useAuth } from "../../context/AuthContext"; +import { Investor } from "../../types"; +import api from "../../services/api"; export const InvestorProfile: React.FC = () => { const { id } = useParams<{ id: string }>(); const { user: currentUser } = useAuth(); - - // Fetch investor data - const investor = findUserById(id || '') as Investor | null; - - if (!investor || investor.role !== 'investor') { + const [investor, setInvestor] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const fetchProfileData = async () => { + if (!id) return; + + setIsLoading(true); + try { + const res = await api.get(`/users/${id}`); + setInvestor(res.data); + } catch (error) { + console.error("Failed to fetch investor profile:", error); + } finally { + setIsLoading(false); + } + }; + + fetchProfileData(); + }, [id]); + + if (isLoading) { + return ( +
+
+
+ ); + } + + if (!investor || investor.role !== "investor") { return (

Investor not found

-

The investor profile you're looking for doesn't exist or has been removed.

+

+ The investor profile you're looking for doesn't exist or has been + removed. +

- +
); } - + const isCurrentUser = currentUser?.id === investor.id; - + return (
{/* Profile header */} @@ -40,52 +76,51 @@ export const InvestorProfile: React.FC = () => { src={investor.avatarUrl} alt={investor.name} size="xl" - status={investor.isOnline ? 'online' : 'offline'} + status={investor.isOnline ? "online" : "offline"} className="mx-auto sm:mx-0" /> - +
-

{investor.name}

+

+ {investor.name} +

- Investor • {investor.totalInvestments} investments + Investor • {investor.totalInvestments || 0} investments

- +
- San Francisco, CA + {investor.location || "Global"} - {investor.investmentStage.map((stage, index) => ( - {stage} + {investor.investmentStage?.map((stage, index) => ( + + {stage} + ))}
- +
{!isCurrentUser && ( - + )} - + {isCurrentUser && ( - + + + )}
- +
{/* Main content - left side */}
@@ -95,169 +130,102 @@ export const InvestorProfile: React.FC = () => {

About

-

{investor.bio}

+

+ {investor.bio || "No bio provided."} +

- + {/* Investment Interests */} -

Investment Interests

+

+ Investment Interests +

-

Industries

+

+ Industries +

- {investor.investmentInterests.map((interest, index) => ( - {interest} + {investor.investmentInterests?.map((interest, index) => ( + + {interest} + ))}
- +
-

Investment Stages

+

+ Investment Stages +

- {investor.investmentStage.map((stage, index) => ( - {stage} + {investor.investmentStage?.map((stage, index) => ( + + {stage} + ))}
- -
-

Investment Criteria

-
    -
  • - - Strong founding team with domain expertise -
  • -
  • - - Clear market opportunity and product-market fit -
  • -
  • - - Scalable business model with strong unit economics -
  • -
  • - - Potential for significant growth and market impact -
  • -
-
-
-
-
- - {/* Portfolio Companies */} - - -

Portfolio Companies

- {investor.portfolioCompanies.length} companies -
- -
- {investor.portfolioCompanies.map((company, index) => ( -
-
- -
-
-

{company}

-

Invested in 2022

-
-
- ))}
- + {/* Sidebar - right side */}
{/* Investment Details */} -

Investment Details

+

+ Investment Details +

- Investment Range + + Investment Range +

- {investor.minimumInvestment} - {investor.maximumInvestment} + {investor.minimumInvestment || "$0"} -{" "} + {investor.maximumInvestment || "Unlimited"}

- -
- Total Investments -

{investor.totalInvestments} companies

-
- +
- Typical Investment Timeline -

3-5 years

-
- -
- Investment Focus -
-
- SaaS & B2B -
-
-
-
-
- FinTech -
-
-
-
-
- HealthTech -
-
-
-
-
+ + Total Investments + +

+ {investor.totalInvestments || 0} companies +

- + {/* Stats */} -

Investment Stats

+

+ Investment Stats +

-

Successful Exits

-

4

-
- -
-
- -
-
-
-

Avg. ROI

-

3.2x

-
- -
-
- -
-
-
-

Active Investments

-

{investor.portfolioCompanies.length}

+

+ Successful Exits +

+

+ -- +

@@ -269,4 +237,4 @@ export const InvestorProfile: React.FC = () => {
); -}; \ No newline at end of file +}; diff --git a/src/pages/settings/SettingsPage.tsx b/src/pages/settings/SettingsPage.tsx index bb6d5f17f..d7219acfa 100644 --- a/src/pages/settings/SettingsPage.tsx +++ b/src/pages/settings/SettingsPage.tsx @@ -1,175 +1,494 @@ -import React from 'react'; -import { User, Lock, Bell, Globe, Palette, CreditCard } from 'lucide-react'; -import { Card, CardHeader, CardBody } from '../../components/ui/Card'; -import { Input } from '../../components/ui/Input'; -import { Button } from '../../components/ui/Button'; -import { Badge } from '../../components/ui/Badge'; -import { Avatar } from '../../components/ui/Avatar'; -import { useAuth } from '../../context/AuthContext'; +import React, { useState } from "react"; +import { + User as UserIcon, + Lock, + Bell, + Building2, + CircleDollarSign, +} from "lucide-react"; +import { Card, CardHeader, CardBody } from "../../components/ui/Card"; +import { Input } from "../../components/ui/Input"; +import { Button } from "../../components/ui/Button"; +import { Badge } from "../../components/ui/Badge"; +import { Avatar } from "../../components/ui/Avatar"; +import { useAuth } from "../../context/AuthContext"; +import toast from "react-hot-toast"; + +type SettingsTab = "profile" | "security" | "role" | "notifications"; export const SettingsPage: React.FC = () => { - const { user } = useAuth(); - + const { user, updateProfile, changePassword } = useAuth(); + const [activeTab, setActiveTab] = useState("profile"); + + // Profile state + const [name, setName] = useState(user?.name || ""); + const [bio, setBio] = useState(user?.bio || ""); + const [location, setLocation] = useState(user?.location || ""); + const [avatar, setAvatar] = useState(user?.avatarUrl || ""); + const [isUpdatingProfile, setIsUpdatingProfile] = useState(false); + const fileInputRef = React.useRef(null); + + // Password state + const [currentPassword, setCurrentPassword] = useState(""); + const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [isChangingPassword, setIsChangingPassword] = useState(false); + + // Entrepreneur-specific state + const [startupName, setStartupName] = useState(user?.startupName || ""); + const [pitchSummary, setPitchSummary] = useState(user?.pitchSummary || ""); + const [industry, setIndustry] = useState(user?.industry || ""); + const [fundingNeeded, setFundingNeeded] = useState(user?.fundingNeeded || ""); + + // Investor-specific state + const [investmentInterests, setInvestmentInterests] = useState( + user?.investmentInterests?.join(", ") || "", + ); + const [minInvestment, setMinInvestment] = useState( + user?.minimumInvestment || "", + ); + const [maxInvestment, setMaxInvestment] = useState( + user?.maximumInvestment || "", + ); + if (!user) return null; - + + const handleUpdateProfile = async () => { + setIsUpdatingProfile(true); + try { + await updateProfile(user.id, { name, bio, location, avatarUrl: avatar }); + } catch { + // Error handled by context + } finally { + setIsUpdatingProfile(false); + } + }; + + const handlePhotoChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + if (file.size > 800 * 1024) { + toast.error("File size exceeds 800KB limit"); + return; + } + + const reader = new FileReader(); + reader.onloadend = () => { + setAvatar(reader.result as string); + }; + reader.readAsDataURL(file); + }; + + const handleUpdateRoleSettings = async () => { + setIsUpdatingProfile(true); + try { + if (user.role === "entrepreneur") { + await updateProfile(user.id, { + startupName, + pitchSummary, + industry, + fundingNeeded, + }); + } else { + await updateProfile(user.id, { + investmentInterests: investmentInterests + .split(",") + .map((i) => i.trim()), + minimumInvestment: minInvestment, + maximumInvestment: maxInvestment, + }); + } + } catch { + // Error handled by context + } finally { + setIsUpdatingProfile(false); + } + }; + + const handleChangePassword = async (e: React.FormEvent) => { + e.preventDefault(); + if (newPassword !== confirmPassword) { + toast.error("New passwords do not match"); + return; + } + + setIsChangingPassword(true); + try { + await changePassword(currentPassword, newPassword); + setCurrentPassword(""); + setNewPassword(""); + setConfirmPassword(""); + } catch { + // Error handled by context + } finally { + setIsChangingPassword(false); + } + }; + return (

Settings

-

Manage your account preferences and settings

+

+ Manage your account preferences and professional details +

- +
{/* Settings navigation */} - + - + {/* Main settings content */} -
- {/* Profile Settings */} - - -

Profile Settings

-
- -
- - -
- -

- JPG, GIF or PNG. Max size of 800K -

-
-
- -
- - - - - - - -
- -
- - -
- -
- - -
-
-
- - {/* Security Settings */} - - -

Security Settings

-
- -
-

Two-Factor Authentication

-
+
+ {activeTab === "profile" && ( + + +

+ Basic Profile +

+
+ +
+
-

- Add an extra layer of security to your account + + +

+ JPG, GIF or PNG. Max size of 800K

- Not Enabled
-
-
- -
-

Change Password

-
+ +
setName(e.target.value)} /> - - + setLocation(e.target.value)} + /> +
+ +
+ + +
+ +
+ + +
+ + + )} + + {activeTab === "role" && ( + + +

+ {user.role === "entrepreneur" + ? "Startup Details" + : "Investment Preferences"} +

+
+ + {user.role === "entrepreneur" ? ( + <> +
+ setStartupName(e.target.value)} + /> + setIndustry(e.target.value)} + /> + setFundingNeeded(e.target.value)} + /> +
+
+ + +
+ + ) : ( + <> +
+ setMinInvestment(e.target.value)} + /> + setMaxInvestment(e.target.value)} + /> +
+
+ setInvestmentInterests(e.target.value)} + placeholder="e.g. Fintech, AI, Sustanability" + /> +
+ + )} + +
+ +
+
+
+ )} + + {activeTab === "security" && ( + + +

+ Security Settings +

+
+ +
+

+ Change Password +

+ setCurrentPassword(e.target.value)} + required /> - +
+ setNewPassword(e.target.value)} + required + /> + setConfirmPassword(e.target.value)} + required + /> +
- + +
+
+ +
+

+ Two-Factor Authentication +

+
+
+

+ Add an extra layer of security to your account +

+ + Not Enabled + +
+ +
+
+
+
+ )} + + {activeTab === "notifications" && ( + + +

+ Notifications +

+
+ +
+ {[ + { + label: "Email Notifications", + desc: "Receive emails for new messages and requests", + }, + { + label: "Push Notifications", + desc: "Receive browser notifications for activity", + }, + { + label: "Marketing Emails", + desc: "Receive news and product updates", + }, + ].map((item, i) => ( +
+
+

+ {item.label} +

+

{item.desc}

+
+ +
+ ))} +
+
-
- - + + + )}
); -}; \ No newline at end of file +}; diff --git a/src/services/api.ts b/src/services/api.ts new file mode 100644 index 000000000..d9e1a7b24 --- /dev/null +++ b/src/services/api.ts @@ -0,0 +1,18 @@ +import axios from "axios"; + +const API_URL = "http://localhost:3001/api"; + +const api = axios.create({ + baseURL: API_URL, +}); + +// Add a request interceptor to attach the token +api.interceptors.request.use((config) => { + const token = localStorage.getItem("business_nexus_token"); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}); + +export default api; diff --git a/src/services/dashboardService.ts b/src/services/dashboardService.ts new file mode 100644 index 000000000..abe1a3f3e --- /dev/null +++ b/src/services/dashboardService.ts @@ -0,0 +1,21 @@ +import api from "./api"; +import { Meeting } from "../types"; + +export interface DashboardSummary { + pendingRequests: number; + totalConnections: number; + upcomingMeetings: number; + unreadMessages: number; + meetings: Meeting[]; + profileViews?: number; + totalStartups?: number; +} + +export const dashboardService = { + getSummary: async () => { + const response = await api.get("/dashboard/summary"); + return response.data; + }, +}; + +export default dashboardService; diff --git a/src/services/dealService.ts b/src/services/dealService.ts new file mode 100644 index 000000000..11ef2c942 --- /dev/null +++ b/src/services/dealService.ts @@ -0,0 +1,33 @@ +import api from "./api"; +import { Deal } from "../types"; + +export const dealService = { + getDeals: async () => { + const response = await api.get("/deals"); + return response.data; + }, + + createDeal: async (deal: { + entrepreneurId: string; + amount: string; + equity: string; + status?: string; + stage: string; + notes?: string; + }) => { + const response = await api.post("/deals", deal); + return response.data; + }, + + updateDeal: async (id: string, updates: Partial) => { + const response = await api.put(`/deals/${id}`, updates); + return response.data; + }, + + deleteDeal: async (id: string) => { + const response = await api.delete<{ message: string }>(`/deals/${id}`); + return response.data; + }, +}; + +export default dealService; diff --git a/src/services/documentService.ts b/src/services/documentService.ts new file mode 100644 index 000000000..54bba4536 --- /dev/null +++ b/src/services/documentService.ts @@ -0,0 +1,32 @@ +import api from "./api"; +import { Document } from "../types"; + +export const documentService = { + getDocuments: async () => { + const response = await api.get("/documents"); + return response.data; + }, + + uploadDocument: async (doc: { + name: string; + type: string; + size: string; + content: string; + shared?: boolean; + }) => { + const response = await api.post("/documents", doc); + return response.data; + }, + + deleteDocument: async (id: string) => { + const response = await api.delete<{ message: string }>(`/documents/${id}`); + return response.data; + }, + + toggleShare: async (id: string) => { + const response = await api.put(`/documents/${id}/share`); + return response.data; + }, +}; + +export default documentService; diff --git a/src/services/meetingService.ts b/src/services/meetingService.ts new file mode 100644 index 000000000..674db1dbf --- /dev/null +++ b/src/services/meetingService.ts @@ -0,0 +1,33 @@ +import api from "./api"; +import { Meeting } from "../types"; + +export const meetingService = { + getMeetings: async () => { + const response = await api.get("/meetings"); + return response.data; + }, + + scheduleMeeting: async ( + meetingData: Omit, + ) => { + const response = await api.post("/meetings", meetingData); + return response.data; + }, + + updateMeetingStatus: async ( + meetingId: string, + status: "accepted" | "rejected" | "cancelled", + ) => { + const response = await api.put(`/meetings/${meetingId}/status`, { + status, + }); + return response.data; + }, + + getMeetingById: async (meetingId: string) => { + const response = await api.get(`/meetings/${meetingId}`); + return response.data; + }, +}; + +export default meetingService; diff --git a/src/services/messageService.ts b/src/services/messageService.ts new file mode 100644 index 000000000..392ad643c --- /dev/null +++ b/src/services/messageService.ts @@ -0,0 +1,31 @@ +import api from "./api"; +import { Message, ChatConversation } from "../types"; + +export const messageService = { + getConversations: async () => { + const response = await api.get( + "/messages/conversations", + ); + return response.data; + }, + + getMessages: async (partnerId: string) => { + const response = await api.get(`/messages/${partnerId}`); + return response.data; + }, + + sendMessage: async (receiverId: string, content: string) => { + const response = await api.post("/messages", { + receiverId, + content, + }); + return response.data; + }, + + getUnreadCount: async () => { + const response = await api.get<{ count: number }>("/messages/unread-count"); + return response.data.count; + }, +}; + +export default messageService; diff --git a/src/services/notificationService.ts b/src/services/notificationService.ts new file mode 100644 index 000000000..15d2908bb --- /dev/null +++ b/src/services/notificationService.ts @@ -0,0 +1,30 @@ +import api from "./api"; +import { Notification } from "../types"; + +export const notificationService = { + getNotifications: async () => { + const response = await api.get("/notifications"); + return response.data; + }, + + markAsRead: async (id: string) => { + const response = await api.put(`/notifications/${id}/read`); + return response.data; + }, + + markAllAsRead: async () => { + const response = await api.put<{ message: string }>( + "/notifications/read-all", + ); + return response.data; + }, + + deleteNotification: async (id: string) => { + const response = await api.delete<{ message: string }>( + `/notifications/${id}`, + ); + return response.data; + }, +}; + +export default notificationService; diff --git a/src/services/supportService.ts b/src/services/supportService.ts new file mode 100644 index 000000000..c80766072 --- /dev/null +++ b/src/services/supportService.ts @@ -0,0 +1,33 @@ +import api from "./api"; +import { SupportTicket } from "../types"; + +export const supportService = { + getTickets: async () => { + const response = await api.get("/support/tickets"); + return response.data; + }, + + createTicket: async (ticket: { + name: string; + email: string; + subject?: string; + message: string; + priority?: string; + }) => { + const response = await api.post("/support/tickets", ticket); + return response.data; + }, + + updateTicket: async ( + id: string, + updates: { status?: string; priority?: string }, + ) => { + const response = await api.put( + `/support/tickets/${id}`, + updates, + ); + return response.data; + }, +}; + +export default supportService; diff --git a/src/types/index.ts b/src/types/index.ts index 02212bbf9..cb14f7171 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,4 +1,5 @@ -export type UserRole = 'entrepreneur' | 'investor'; +// No imports needed for plain interfaces +export type UserRole = "entrepreneur" | "investor"; export interface User { id: string; @@ -7,12 +8,29 @@ export interface User { role: UserRole; avatarUrl: string; bio: string; + location?: string; isOnline?: boolean; createdAt: string; + + // Entrepreneur fields + startupName?: string; + pitchSummary?: string; + fundingNeeded?: string; + industry?: string; + foundedYear?: number; + teamSize?: number; + + // Investor fields + investmentInterests?: string[]; + investmentStage?: string[]; + portfolioCompanies?: string[]; + totalInvestments?: number; + minimumInvestment?: string; + maximumInvestment?: string; } export interface Entrepreneur extends User { - role: 'entrepreneur'; + role: "entrepreneur"; startupName: string; pitchSummary: string; fundingNeeded: string; @@ -23,7 +41,7 @@ export interface Entrepreneur extends User { } export interface Investor extends User { - role: 'investor'; + role: "investor"; investmentInterests: string[]; investmentStage: string[]; portfolioCompanies: string[]; @@ -45,6 +63,12 @@ export interface ChatConversation { id: string; participants: string[]; lastMessage?: Message; + partner?: { + id: string; + name: string; + avatarUrl: string; + isOnline: boolean; + }; updatedAt: string; } @@ -53,7 +77,7 @@ export interface CollaborationRequest { investorId: string; entrepreneurId: string; message: string; - status: 'pending' | 'accepted' | 'rejected'; + status: "pending" | "accepted" | "rejected"; createdAt: string; } @@ -68,14 +92,89 @@ export interface Document { ownerId: string; } +export interface Meeting { + id: string; + title: string; + description?: string; + investorId: string; + entrepreneurId: string; + startTime: string; + endTime: string; + status: "pending" | "accepted" | "rejected" | "cancelled"; + location?: string; + createdAt: string; +} + +export interface Notification { + id: string; + userId: string; + type: + | "message" + | "collaboration_request" + | "collaboration_accepted" + | "meeting_scheduled" + | "meeting_status"; + title: string; + message: string; + link: string; + isRead: boolean; + createdAt: string; +} + +export interface Document { + id: string; + userId: string; + name: string; + type: string; + size: string; + content: string; + shared: boolean; + createdAt: string; +} + +export interface Deal { + id: string; + investorId: string; + entrepreneurId: string; + amount: string; + equity: string; + status: "Due Diligence" | "Term Sheet" | "Negotiation" | "Closed" | "Passed"; + stage: string; + notes?: string; + createdAt: string; + lastActivity: string; +} + +export interface SupportTicket { + id: string; + userId: string; + name: string; + email: string; + subject: string; + message: string; + status: "open" | "in-progress" | "resolved" | "closed"; + priority: "low" | "medium" | "high"; + createdAt: string; + updatedAt: string; +} + export interface AuthContextType { user: User | null; login: (email: string, password: string, role: UserRole) => Promise; - register: (name: string, email: string, password: string, role: UserRole) => Promise; + register: ( + name: string, + email: string, + password: string, + role: UserRole, + ) => Promise; logout: () => void; forgotPassword: (email: string) => Promise; resetPassword: (token: string, newPassword: string) => Promise; updateProfile: (userId: string, updates: Partial) => Promise; + changePassword: ( + currentPassword: string, + newPassword: string, + ) => Promise; isAuthenticated: boolean; isLoading: boolean; -} \ No newline at end of file +} From 52ca2e8a23d87f62eb96c3c538902952b249f689 Mon Sep 17 00:00:00 2001 From: m-zeeshan-saeed Date: Mon, 26 Jan 2026 16:21:48 +0500 Subject: [PATCH 02/15] chore: Add *.env to .gitignore to prevent sensitive environment variables from being committed. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index a547bf36d..d7307976c 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ dist-ssr *.njsproj *.sln *.sw? +*.env From b254bb04a631e5c1f0c95e771462f535ed8765fe Mon Sep 17 00:00:00 2001 From: m-zeeshan-saeed Date: Mon, 26 Jan 2026 16:23:53 +0500 Subject: [PATCH 03/15] chore: add .env and .env.local to .gitignore. --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index d7307976c..365d109b6 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,5 @@ dist-ssr *.sln *.sw? *.env +.env +.env.local From f9b0d2b04d568910377cf3f4cd99605d3e0179ef Mon Sep 17 00:00:00 2001 From: m-zeeshan-saeed Date: Mon, 26 Jan 2026 16:34:28 +0500 Subject: [PATCH 04/15] feat: add README.md and .env.example for project setup and configuration. --- .env | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 .env diff --git a/.env b/.env deleted file mode 100644 index 312871455..000000000 --- a/.env +++ /dev/null @@ -1,2 +0,0 @@ -MONGODB_URL=mongodb+srv://zeeshansheikh0313_db_user:Zeeshan123@cluster0.czhqovt.mongodb.net/?appName=Cluster0 -JWT_SECRET=088bd22dc60db737b89b0b09608c40c4b631775ddf571ac3b0ceabf5794463a8 From 37c9dd68e3ff0379939f7979065b10f37b63564d Mon Sep 17 00:00:00 2001 From: m-zeeshan-saeed Date: Mon, 26 Jan 2026 16:34:50 +0500 Subject: [PATCH 05/15] feat: Add initial README.md with project setup instructions and refine .gitignore to ignore environment files. --- .gitignore | 3 +- README.md | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 README.md diff --git a/.gitignore b/.gitignore index 365d109b6..d5aa5887d 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ dist-ssr *.njsproj *.sln *.sw? -*.env +.env* .env .env.local +.env.example diff --git a/README.md b/README.md new file mode 100644 index 000000000..a454f805b --- /dev/null +++ b/README.md @@ -0,0 +1,97 @@ +# Business Nexus + +A comprehensive web platform for business connections, connecting startups, investors, and professionals. + +## Tech Stack + +**Frontend:** + +- React +- TypeScript +- Vite +- Tailwind CSS +- Lucide React (Icons) + +**Backend:** + +- Node.js +- Express +- MongoDB (Mongoose) +- TypeScript + +## Prerequisites + +Before running this project, ensure you have the following installed: + +- [Node.js](https://nodejs.org/) (v16+ recommended) +- [MongoDB](https://www.mongodb.com/) (running locally or a cloud instance like MongoDB Atlas) + +## Installation + +1. **Clone the repository:** + + ```bash + git clone + cd Nexus + ``` + +2. **Install dependencies:** + ```bash + npm install + ``` + +## Configuration + +1. **Backend Environment Variables:** + Create a `.env` file in the root directory based on `.env.example`: + + ```bash + cp .env.example .env + ``` + + Update the `.env` file with your specific configuration, particularly the `MONGODB_URL` if you are not using a local default instance. + + ```env + PORT=3001 + MONGODB_URL=mongodb://localhost:27017/business-nexus + ``` + +## Running the Project + +You will need to run the backend and frontend in separate terminal windows. + +### 1. Start the Backend Server + +The backend requires a running MongoDB instance. Make sure MongoDB is started. + +In the first terminal window, run: + +```bash +npx tsx watch backend/server.ts +``` + +The server should start on port `3001` and connect to MongoDB. + +### 2. Start the Frontend Application + +In a second terminal window, run: + +```bash +npm run dev +``` + +The frontend will start (usually on `http://localhost:5173`) and will automatically connect to the backend at `http://localhost:3001`. + +## Building for Production + +To build the frontend for production: + +```bash +npm run build +``` + +## Project Structure + +- `src/` - Frontend source code (React components, pages, services) +- `backend/` - Backend source code (Express server, Mongoose models, routes) +- `public/` - Static assets From a97c1115b04d976051b3eae5c20327eae8194486 Mon Sep 17 00:00:00 2001 From: m-zeeshan-saeed Date: Mon, 26 Jan 2026 18:11:20 +0500 Subject: [PATCH 06/15] refactor: include detailed error message in the registration failure response. --- backend/routes/auth.routes.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/backend/routes/auth.routes.ts b/backend/routes/auth.routes.ts index 14b10232f..29e8505aa 100644 --- a/backend/routes/auth.routes.ts +++ b/backend/routes/auth.routes.ts @@ -39,7 +39,12 @@ router.post("/register", async (req, res) => { res.status(201).json({ message: "User registered successfully" }); } catch (error) { console.error("Registration error:", error); - res.status(500).json({ message: "Server error" }); + res + .status(500) + .json({ + message: "Server error", + error: error instanceof Error ? error.message : String(error), + }); } }); From f5b43772ce1139622375df5d7355cd7dc88c65c6 Mon Sep 17 00:00:00 2001 From: m-zeeshan-saeed Date: Sat, 14 Feb 2026 23:49:17 +0500 Subject: [PATCH 07/15] feat: Implement payment and wallet management with real-time communication and video call features. --- backend/models/Transaction.model.ts | 39 + backend/models/User.model.ts | 2 + backend/routes/auth.routes.ts | 10 +- backend/routes/collaboration.routes.ts | 49 +- backend/routes/payment.routes.ts | 234 ++++++ backend/routes/support.routes.ts | 6 +- backend/routes/user.routes.ts | 70 ++ backend/server.ts | 23 +- backend/socket.handler.ts | 170 ++++ debug_db.ts | 14 + gen_token.ts | 10 + package-lock.json | 771 +++++++++++++++++- package.json | 11 +- src/App.tsx | 156 ++-- src/components/chat/ChatUserList.tsx | 6 +- src/components/chat/VideoCallModal.tsx | 255 ++++++ .../CollaborationRequestCard.tsx | 100 +-- src/components/dashboard/PaymentModals.tsx | 308 +++++++ .../dashboard/TransactionHistory.tsx | 157 ++++ src/components/dashboard/WalletCard.tsx | 78 ++ .../entrepreneur/EntrepreneurCard.tsx | 84 +- src/components/investor/InvestorCard.tsx | 72 +- src/components/layout/Navbar.tsx | 149 ++-- src/components/layout/Sidebar.tsx | 11 + src/components/ui/Avatar.tsx | 51 +- src/context/AuthContext.tsx | 40 +- src/context/SocketContext.tsx | 93 +++ src/pages/chat/ChatPage.tsx | 93 ++- src/pages/dashboard/EntrepreneurDashboard.tsx | 138 +++- src/pages/dashboard/InvestorDashboard.tsx | 126 ++- src/pages/dashboard/TransactionsPage.tsx | 204 +++++ src/pages/help/HelpPage.tsx | 10 +- src/pages/notifications/NotificationsPage.tsx | 8 +- src/pages/profile/EntrepreneurProfile.tsx | 67 +- src/pages/profile/InvestorProfile.tsx | 4 +- src/services/paymentService.ts | 67 ++ src/types/index.ts | 20 + vite.config.ts | 15 +- 38 files changed, 3406 insertions(+), 315 deletions(-) create mode 100644 backend/models/Transaction.model.ts create mode 100644 backend/routes/payment.routes.ts create mode 100644 backend/socket.handler.ts create mode 100644 debug_db.ts create mode 100644 gen_token.ts create mode 100644 src/components/chat/VideoCallModal.tsx create mode 100644 src/components/dashboard/PaymentModals.tsx create mode 100644 src/components/dashboard/TransactionHistory.tsx create mode 100644 src/components/dashboard/WalletCard.tsx create mode 100644 src/context/SocketContext.tsx create mode 100644 src/pages/dashboard/TransactionsPage.tsx create mode 100644 src/services/paymentService.ts diff --git a/backend/models/Transaction.model.ts b/backend/models/Transaction.model.ts new file mode 100644 index 000000000..9563c1186 --- /dev/null +++ b/backend/models/Transaction.model.ts @@ -0,0 +1,39 @@ +import mongoose, { Schema, Document } from "mongoose"; + +export interface ITransactionDocument extends Document { + id: string; // Custom ID for consistency + userId: string; + type: "deposit" | "withdraw" | "transfer"; + amount: number; + status: "pending" | "completed" | "failed"; + description: string; + recipientId?: string; + createdAt: string; +} + +const TransactionSchema: Schema = new Schema( + { + id: { type: String, unique: true, sparse: true }, + userId: { type: String, required: true }, + type: { + type: String, + enum: ["deposit", "withdraw", "transfer"], + required: true, + }, + amount: { type: Number, required: true }, + status: { + type: String, + enum: ["pending", "completed", "failed"], + default: "completed", + }, + description: { type: String, required: true }, + recipientId: { type: String }, + createdAt: { type: String, default: () => new Date().toISOString() }, + }, + { + timestamps: false, + }, +); + +export default mongoose.models.Transaction || + mongoose.model("Transaction", TransactionSchema); diff --git a/backend/models/User.model.ts b/backend/models/User.model.ts index e0a227a6d..5bb5b6454 100644 --- a/backend/models/User.model.ts +++ b/backend/models/User.model.ts @@ -17,6 +17,7 @@ export interface IUserDocument extends Omit, Document { totalInvestments?: number; minimumInvestment?: string; maximumInvestment?: string; + walletBalance: number; } const UserSchema: Schema = new Schema( @@ -47,6 +48,7 @@ const UserSchema: Schema = new Schema( totalInvestments: { type: Number }, minimumInvestment: { type: String }, maximumInvestment: { type: String }, + walletBalance: { type: Number, default: 0 }, }, { timestamps: false, // Using custom createdAt from mock data diff --git a/backend/routes/auth.routes.ts b/backend/routes/auth.routes.ts index 29e8505aa..22b010e12 100644 --- a/backend/routes/auth.routes.ts +++ b/backend/routes/auth.routes.ts @@ -39,12 +39,10 @@ router.post("/register", async (req, res) => { res.status(201).json({ message: "User registered successfully" }); } catch (error) { console.error("Registration error:", error); - res - .status(500) - .json({ - message: "Server error", - error: error instanceof Error ? error.message : String(error), - }); + res.status(500).json({ + message: "Server error", + error: error instanceof Error ? error.message : String(error), + }); } }); diff --git a/backend/routes/collaboration.routes.ts b/backend/routes/collaboration.routes.ts index b04fb2f27..3472bf73e 100644 --- a/backend/routes/collaboration.routes.ts +++ b/backend/routes/collaboration.routes.ts @@ -1,5 +1,6 @@ import express from "express"; import CollaborationRequest from "../models/CollaborationRequest.model"; +import User from "../models/User.model"; import Notification from "../models/Notification.model"; import { authenticateToken, AuthRequest } from "../middlewares/auth.middleware"; @@ -17,10 +18,28 @@ router.get("/", authenticateToken, async (req: AuthRequest, res) => { query = { investorId: userId }; } - const requests = await CollaborationRequest.find(query).sort({ - createdAt: -1, - }); - res.json(requests); + const requests = await CollaborationRequest.find(query) + .sort({ + createdAt: -1, + }) + .lean(); + + // Fetch user details for each request + const populatedRequests = await Promise.all( + requests.map(async (request: any) => { + const partnerId = + role === "entrepreneur" ? request.investorId : request.entrepreneurId; + const partner = await User.findOne({ id: partnerId }) + .select("id name email avatarUrl role isOnline") + .lean(); + return { + ...request, + partner, + }; + }), + ); + + res.json(populatedRequests); } catch (error) { console.error("Fetch requests error:", error); res.status(500).json({ message: "Server error" }); @@ -33,6 +52,18 @@ router.post("/", authenticateToken, async (req: AuthRequest, res) => { const { entrepreneurId, message } = req.body; const { userId: investorId } = req.user!; + console.log( + "[DEBUG] POST /collaboration - from (investor):", + investorId, + "to (entrepreneur):", + entrepreneurId, + ); + + if (!entrepreneurId) { + console.log("[DEBUG] POST /collaboration - entrepreneurId is missing!"); + return res.status(400).json({ message: "Entrepreneur ID is required" }); + } + const newRequest = new CollaborationRequest({ id: `req${Date.now()}`, investorId, @@ -42,6 +73,10 @@ router.post("/", authenticateToken, async (req: AuthRequest, res) => { createdAt: new Date().toISOString(), }); + console.log( + "[DEBUG] POST /collaboration - saving new request:", + newRequest.id, + ); await newRequest.save(); // Create notification for entrepreneur @@ -54,11 +89,15 @@ router.post("/", authenticateToken, async (req: AuthRequest, res) => { link: "/dashboard/entrepreneur", isRead: false, }); + console.log( + "[DEBUG] POST /collaboration - sending notification to:", + entrepreneurId, + ); await notification.save(); res.status(201).json(newRequest); } catch (error) { - console.error("Create request error:", error); + console.error("[DEBUG] POST /collaboration - ERROR:", error); res.status(500).json({ message: "Server error" }); } }); diff --git a/backend/routes/payment.routes.ts b/backend/routes/payment.routes.ts new file mode 100644 index 000000000..43ca3b5ce --- /dev/null +++ b/backend/routes/payment.routes.ts @@ -0,0 +1,234 @@ +import express from "express"; +import User from "../models/User.model"; +import Transaction from "../models/Transaction.model"; +import { authenticateToken, AuthRequest } from "../middlewares/auth.middleware"; +import { getSocketId } from "../socket.handler"; + +const router = express.Router(); + +// Get wallet balance +router.get("/balance", authenticateToken, async (req: AuthRequest, res) => { + try { + const user = await User.findOne({ id: req.user?.userId }); + if (!user) { + return res.status(404).json({ message: "User not found" }); + } + res.json({ balance: user.walletBalance || 0 }); + } catch (error) { + console.error("Fetch balance error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +// Get transaction history +router.get( + "/transactions", + authenticateToken, + async (req: AuthRequest, res) => { + try { + const transactions = await Transaction.find({ + userId: req.user?.userId, + }).sort({ + createdAt: -1, + }); + res.json(transactions); + } catch (error) { + console.error("Fetch transactions error:", error); + res.status(500).json({ message: "Server error" }); + } + }, +); + +// Deposit funds (Simulated) +router.post("/deposit", authenticateToken, async (req: AuthRequest, res) => { + try { + const { amount, method } = req.body; + + if (!amount || amount <= 0) { + return res.status(400).json({ message: "Invalid amount" }); + } + + const user = await User.findOne({ id: req.user?.userId }); + if (!user) { + return res.status(404).json({ message: "User not found" }); + } + + // Update balance + user.walletBalance = (user.walletBalance || 0) + amount; + await user.save(); + + // Create transaction record + const transaction = new Transaction({ + id: `tx${Date.now()}`, + userId: user.id, + type: "deposit", + amount, + status: "completed", + description: `Deposit via ${method || "Card"}`, + }); + await transaction.save(); + + res.json({ + message: "Deposit successful", + balance: user.walletBalance, + transaction, + }); + } catch (error) { + console.error("Deposit error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +// Withdraw funds (Simulated) +router.post("/withdraw", authenticateToken, async (req: AuthRequest, res) => { + try { + const { amount, method } = req.body; + + if (!amount || amount <= 0) { + return res.status(400).json({ message: "Invalid amount" }); + } + + const user = await User.findOne({ id: req.user?.userId }); + if (!user) { + return res.status(404).json({ message: "User not found" }); + } + + if (user.walletBalance < amount) { + return res.status(400).json({ message: "Insufficient funds" }); + } + + // Update balance + user.walletBalance -= amount; + await user.save(); + + // Create transaction record + const transaction = new Transaction({ + id: `tx${Date.now()}`, + userId: user.id, + type: "withdraw", + amount, + status: "completed", + description: `Withdrawal to ${method || "Bank Account"}`, + }); + await transaction.save(); + + res.json({ + message: "Withdrawal successful", + balance: user.walletBalance, + transaction, + }); + } catch (error) { + console.error("Withdrawal error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +// Transfer funds between users +router.post("/transfer", authenticateToken, async (req: AuthRequest, res) => { + try { + const { recipientId, amount, description } = req.body; + const { userId: senderId } = req.user!; + + console.log("Transfer Start:", { senderId, recipientId, amount }); + + if (!amount || amount <= 0 || !recipientId) { + console.log("Transfer Validation Failed:", { recipientId, amount }); + return res.status(400).json({ message: "Invalid transfer details" }); + } + + if (recipientId === senderId) { + console.log("Self-transfer Attempt:", senderId); + return res.status(400).json({ message: "Cannot transfer to yourself" }); + } + + const sender = await User.findOne({ id: senderId }); + const recipient = await User.findOne({ id: recipientId }); + + if (!sender || !recipient) { + console.log("Users not found:", { + sender: !!sender, + recipient: !!recipient, + }); + return res.status(404).json({ message: "User not found" }); + } + + console.log("Balances Before:", { + sender: sender.walletBalance, + recipient: recipient.walletBalance, + }); + + if (sender.walletBalance < amount) { + console.log("Insufficient Funds:", { + balance: sender.walletBalance, + amount, + }); + return res.status(400).json({ message: "Insufficient funds" }); + } + + // Update balances + sender.walletBalance -= amount; + recipient.walletBalance = (recipient.walletBalance || 0) + amount; + + await sender.save(); + await recipient.save(); + + console.log("Balances After:", { + sender: sender.walletBalance, + recipient: recipient.walletBalance, + }); + + // Create transaction records for both + const senderTransaction = new Transaction({ + id: `tx${Date.now()}-S`, + userId: sender.id, + type: "transfer", + amount: -amount, + status: "completed", + description: description || `Transfer to ${recipient.name}`, + recipientId: recipient.id, + }); + + const recipientTransaction = new Transaction({ + id: `tx${Date.now()}-R`, + userId: recipient.id, + type: "transfer", + amount: amount, + status: "completed", + description: description || `Transfer from ${sender.name}`, + recipientId: sender.id, + }); + + await senderTransaction.save(); + await recipientTransaction.save(); + + console.log("Transactions Saved:", { + senderTx: senderTransaction.id, + recipientTx: recipientTransaction.id, + }); + + // Notify recipient via socket if online + const io = req.app.get("io"); + if (io) { + const recipientSocketId = getSocketId(recipient.id); + if (recipientSocketId) { + console.log("Emitting payment_received to:", recipientSocketId); + io.to(recipientSocketId).emit("payment_received", { + amount, + senderName: sender.name, + transaction: recipientTransaction.toObject(), + }); + } + } + + res.json({ + message: "Transfer successful", + balance: sender.walletBalance, + transaction: senderTransaction, + }); + } catch (error) { + console.error("Transfer error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +export default router; diff --git a/backend/routes/support.routes.ts b/backend/routes/support.routes.ts index 2d46d02a7..d2faeaafe 100644 --- a/backend/routes/support.routes.ts +++ b/backend/routes/support.routes.ts @@ -56,7 +56,11 @@ router.put("/tickets/:id", authenticateToken, async (req: AuthRequest, res) => { if (!userId) return res.status(401).json({ message: "Unauthorized" }); - const updates: any = { + const updates: Partial<{ + status: string; + priority: string; + updatedAt: string; + }> = { updatedAt: new Date().toISOString(), }; diff --git a/backend/routes/user.routes.ts b/backend/routes/user.routes.ts index bf47667e7..eb26e8749 100644 --- a/backend/routes/user.routes.ts +++ b/backend/routes/user.routes.ts @@ -1,5 +1,6 @@ import express from "express"; import User from "../models/User.model"; +import CollaborationRequest from "../models/CollaborationRequest.model"; import { authenticateToken, AuthRequest } from "../middlewares/auth.middleware"; import bcrypt from "bcryptjs"; @@ -21,6 +22,75 @@ router.get("/profile", authenticateToken, async (req: AuthRequest, res) => { } }); +// Search users by name, email, or ID +router.get("/search", authenticateToken, async (req: AuthRequest, res) => { + try { + const { query } = req.query; + if (!query || typeof query !== "string") { + return res.status(400).json({ message: "Search query is required" }); + } + + // Search for users by direct ID match or case-insensitive name/email match + // Filter out the current user from results + const users = await User.find({ + $and: [ + { id: { $ne: req.user?.userId } }, + { + $or: [ + { id: query }, + { name: { $regex: query, $options: "i" } }, + { email: { $regex: query, $options: "i" } }, + ], + }, + ], + }) + .select("id name email role") + .limit(10); + + res.json(users); + } catch (error) { + console.error("User search error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +// Get connected users (accepted collaborations) +router.get("/connections", authenticateToken, async (req: AuthRequest, res) => { + try { + const { userId } = req.user!; + console.log("[DEBUG] /connections - fetching for user:", userId); + + const acceptedRequests = await CollaborationRequest.find({ + status: "accepted", + $or: [{ investorId: userId }, { entrepreneurId: userId }], + }); + + console.log( + "[DEBUG] /connections - accepted requests count:", + acceptedRequests.length, + ); + + const partnerIds = acceptedRequests.map((req: any) => + req.investorId === userId ? req.entrepreneurId : req.investorId, + ); + + console.log("[DEBUG] /connections - unique partner IDs:", [ + ...new Set(partnerIds), + ]); + + const connections = await User.find({ id: { $in: partnerIds } }) + .select("id name email role avatarUrl startupName industry") + .lean(); + + console.log("[DEBUG] /connections - users found:", connections.length); + + res.json(connections); + } catch (error) { + console.error("Fetch connections error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + // Get user profile by ID router.get("/:id", authenticateToken, async (req, res) => { try { diff --git a/backend/server.ts b/backend/server.ts index 0020745e9..cb4164a97 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -2,6 +2,9 @@ import "dotenv/config"; import express from "express"; import cors from "cors"; import mongoose from "mongoose"; +import { createServer } from "http"; +import { Server } from "socket.io"; +import { setupSocketHandlers } from "./socket.handler.js"; import authRoutes from "./routes/auth.routes"; import userRoutes from "./routes/user.routes"; @@ -14,6 +17,7 @@ import notificationRoutes from "./routes/notification.routes"; import documentRoutes from "./routes/document.routes"; import dealRoutes from "./routes/deal.routes"; import supportRoutes from "./routes/support.routes"; +import paymentRoutes from "./routes/payment.routes"; const app = express(); const port = process.env.PORT || 3001; @@ -46,12 +50,29 @@ app.use("/api/notifications", notificationRoutes); app.use("/api/documents", documentRoutes); app.use("/api/deals", dealRoutes); app.use("/api/support", supportRoutes); +app.use("/api/payments", paymentRoutes); app.get("/", (req, res) => { res.send("API is running..."); }); +// Create HTTP server +const httpServer = createServer(app); + +// Initialize Socket.IO +const io = new Server(httpServer, { + cors: { + origin: "*", // Adjust this for production + methods: ["GET", "POST"], + }, +}); + +app.set("io", io); + +// Setup socket handlers +setupSocketHandlers(io); + // Start the server -app.listen(port, () => { +httpServer.listen(port, () => { console.log(`Server is running on port ${port}`); }); diff --git a/backend/socket.handler.ts b/backend/socket.handler.ts new file mode 100644 index 000000000..f99ac8d3a --- /dev/null +++ b/backend/socket.handler.ts @@ -0,0 +1,170 @@ +import { Server, Socket } from "socket.io"; + +interface SignalData { + roomId: string; + targetUserId?: string; + offer?: unknown; + answer?: unknown; + candidate?: unknown; +} + +const users = new Map(); // userId -> socketId +const recentlyActive = new Map(); // userId -> timeoutId +const lastSeen = new Map(); // userId -> ISO date string + +export const getSocketId = (userId: string) => users.get(userId); + +type UserStatus = "online" | "offline" | "recently_active"; + +interface StatusInfo { + status: UserStatus; + lastSeen: string; +} + +const broadcastStatuses = (io: Server) => { + const statuses: Record = {}; + + // Combine all known users + const allUserIds = new Set([ + ...users.keys(), + ...recentlyActive.keys(), + ...lastSeen.keys(), + ]); + + for (const userId of allUserIds) { + let status: UserStatus = "offline"; + if (users.has(userId)) { + status = "online"; + } else if (recentlyActive.has(userId)) { + status = "recently_active"; + } + + statuses[userId] = { + status, + lastSeen: lastSeen.get(userId) || new Date().toISOString(), + }; + } + + io.emit("user-statuses", statuses); +}; + +export const setupSocketHandlers = (io: Server) => { + console.log("Socket.IO handlers initialized"); + + io.on("connection", (socket: Socket) => { + console.log("User connected:", socket.id); + + socket.on("setup", (userId: string) => { + users.set(userId, socket.id); + lastSeen.set(userId, new Date().toISOString()); + + // If they were recently active, clear that timeout + if (recentlyActive.has(userId)) { + clearTimeout(recentlyActive.get(userId)); + recentlyActive.delete(userId); + } + + console.log(`User ${userId} setup with socket ${socket.id}`); + broadcastStatuses(io); + }); + + // Join a specific chat room + socket.on("join-room", (roomId: string) => { + socket.join(roomId); + console.log(`User ${socket.id} joined room: ${roomId}`); + socket.to(roomId).emit("user-joined", socket.id); + }); + + // WebRTC Signaling: Offer + socket.on("offer", ({ roomId, targetUserId, offer }: SignalData) => { + if (targetUserId) { + const targetSocketId = users.get(targetUserId); + if (targetSocketId) { + console.log( + `Sending offer from ${socket.id} directly to ${targetUserId}`, + ); + io.to(targetSocketId).emit("offer", { + from: socket.id, + offer, + roomId, + }); + } + } else { + console.log(`Sending offer from ${socket.id} to room ${roomId}`); + socket.to(roomId).emit("offer", { from: socket.id, offer }); + } + }); + + // WebRTC Signaling: Answer + socket.on("answer", ({ roomId, targetUserId, answer }: SignalData) => { + if (targetUserId) { + const targetSocketId = users.get(targetUserId); + if (targetSocketId) { + io.to(targetSocketId).emit("answer", { + from: socket.id, + answer, + roomId, + }); + } + } else { + socket.to(roomId).emit("answer", { from: socket.id, answer }); + } + }); + + // WebRTC Signaling: ICE Candidate + socket.on( + "ice-candidate", + ({ roomId, targetUserId, candidate }: SignalData) => { + if (targetUserId) { + const targetSocketId = users.get(targetUserId); + if (targetSocketId) { + io.to(targetSocketId).emit("ice-candidate", { + from: socket.id, + candidate, + roomId, + }); + } + } else { + socket + .to(roomId) + .emit("ice-candidate", { from: socket.id, candidate }); + } + }, + ); + + // Ending the call + socket.on("end-call", (roomId: string) => { + console.log(`Call ended in room ${roomId} by ${socket.id}`); + socket.to(roomId).emit("call-ended", { from: socket.id }); + }); + + socket.on("disconnect", () => { + console.log("User disconnected:", socket.id); + let disconnectedUserId: string | null = null; + + for (const [userId, socketId] of users.entries()) { + if (socketId === socket.id) { + disconnectedUserId = userId; + users.delete(userId); + break; + } + } + + if (disconnectedUserId) { + lastSeen.set(disconnectedUserId, new Date().toISOString()); + // Set to recently active for 5 minutes + const timeoutId = setTimeout( + () => { + recentlyActive.delete(disconnectedUserId!); + broadcastStatuses(io); + }, + 5 * 60 * 1000, + ); // 5 minutes + + recentlyActive.set(disconnectedUserId, timeoutId); + } + + broadcastStatuses(io); + }); + }); +}; diff --git a/debug_db.ts b/debug_db.ts new file mode 100644 index 000000000..3d7c59bd0 --- /dev/null +++ b/debug_db.ts @@ -0,0 +1,14 @@ +import mongoose from "mongoose"; +import "dotenv/config"; + +const mongoUri = process.env.MONGODB_URL; + +async function debug() { + await mongoose.connect(mongoUri!); + const User = mongoose.connection.collection("users"); + const user = await User.findOne({ id: "u1771080958847" }); + console.log("Saeed Email:", user?.email); + process.exit(0); +} + +debug(); diff --git a/gen_token.ts b/gen_token.ts new file mode 100644 index 000000000..b3701a4f1 --- /dev/null +++ b/gen_token.ts @@ -0,0 +1,10 @@ +import jwt from "jsonwebtoken"; +import "dotenv/config"; + +const JWT_SECRET = process.env.JWT_SECRET || "your_fallback_secret"; +const token = jwt.sign( + { userId: "u1771080958847", role: "investor" }, + JWT_SECRET, +); + +console.log(token); diff --git a/package-lock.json b/package-lock.json index 79a516652..1293d407a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,15 +14,19 @@ "date-fns": "^3.3.1", "dotenv": "^17.2.3", "express": "^5.2.1", + "express-validator": "^7.3.1", "jsonwebtoken": "^9.0.3", "lucide-react": "^0.344.0", "mongodb": "^7.0.0", "mongoose": "^9.1.5", + "nodemailer": "^8.0.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-dropzone": "^14.2.3", "react-hot-toast": "^2.4.1", - "react-router-dom": "^6.22.1" + "react-router-dom": "^6.22.1", + "socket.io": "^4.8.3", + "socket.io-client": "^4.8.3" }, "devDependencies": { "@eslint/js": "^9.9.1", @@ -30,6 +34,7 @@ "@types/cors": "^2.8.19", "@types/express": "^5.0.6", "@types/jsonwebtoken": "^9.0.10", + "@types/nodemailer": "^7.0.9", "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.1", @@ -40,6 +45,7 @@ "globals": "^15.9.0", "postcss": "^8.4.35", "tailwindcss": "^3.4.1", + "tsx": "^4.21.0", "typescript": "^5.5.3", "typescript-eslint": "^8.3.0", "vite": "^5.4.21" @@ -634,6 +640,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/netbsd-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", @@ -650,6 +673,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/openbsd-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", @@ -666,6 +706,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", @@ -1238,6 +1295,12 @@ "win32" ] }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1311,7 +1374,6 @@ "version": "2.8.19", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -1383,12 +1445,21 @@ "version": "25.0.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.10.tgz", "integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~7.16.0" } }, + "node_modules/@types/nodemailer": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.9.tgz", + "integrity": "sha512-vI8oF1M+8JvQhsId0Pc38BdUP2evenIIys7c7p+9OZXSPOH5c1dyINP1jT8xQ2xPuBUXmIC87s+91IZMDjH8Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/prop-types": { "version": "15.7.13", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", @@ -1914,6 +1985,15 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/bcryptjs": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", @@ -2424,6 +2504,70 @@ "node": ">= 0.8" } }, + "node_modules/engine.io": { + "version": "6.6.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.5.tgz", + "integrity": "sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz", + "integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -2834,6 +2978,19 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-validator": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.1.tgz", + "integrity": "sha512-IGenaSf+DnWc69lKuqlRE9/i/2t5/16VpH5bXoqdxWz1aCpRvEdrBuu1y95i/iL5QP8ZYVATiwLFhwk3EDl5vg==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21", + "validator": "~13.15.23" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/express/node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -3156,6 +3313,19 @@ "node": ">= 0.4" } }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -3660,6 +3830,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -3989,6 +4165,15 @@ "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", "dev": true }, + "node_modules/nodemailer": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.1.tgz", + "integrity": "sha512-5kcldIXmaEjZcHR6F28IKGSgpmZHaF1IXLWFTG+Xh3S+Cce4MiakLtWY+PlBU69fLbRa8HlaGIrC/QolUpHkhg==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -4638,6 +4823,16 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -4952,6 +5147,84 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/socket.io": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz", + "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==", + "license": "MIT", + "dependencies": { + "debug": "~4.4.1", + "ws": "~8.18.3" + } + }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz", + "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5263,6 +5536,459 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -5355,7 +6081,6 @@ "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, "license": "MIT" }, "node_modules/unpipe": { @@ -5412,6 +6137,15 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "node_modules/validator": { + "version": "13.15.26", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz", + "integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -5655,6 +6389,35 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index 2e08b5d6d..264593fd8 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "dev": "vite", "build": "vite build", "lint": "eslint .", - "preview": "vite preview" + "preview": "vite preview", + "start": "tsx watch backend/server.ts" }, "dependencies": { "axios": "^1.6.7", @@ -16,15 +17,19 @@ "date-fns": "^3.3.1", "dotenv": "^17.2.3", "express": "^5.2.1", + "express-validator": "^7.3.1", "jsonwebtoken": "^9.0.3", "lucide-react": "^0.344.0", "mongodb": "^7.0.0", "mongoose": "^9.1.5", + "nodemailer": "^8.0.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-dropzone": "^14.2.3", "react-hot-toast": "^2.4.1", - "react-router-dom": "^6.22.1" + "react-router-dom": "^6.22.1", + "socket.io": "^4.8.3", + "socket.io-client": "^4.8.3" }, "devDependencies": { "@eslint/js": "^9.9.1", @@ -32,6 +37,7 @@ "@types/cors": "^2.8.19", "@types/express": "^5.0.6", "@types/jsonwebtoken": "^9.0.10", + "@types/nodemailer": "^7.0.9", "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.1", @@ -42,6 +48,7 @@ "globals": "^15.9.0", "postcss": "^8.4.35", "tailwindcss": "^3.4.1", + "tsx": "^4.21.0", "typescript": "^5.5.3", "typescript-eslint": "^8.3.0", "vite": "^5.4.21" diff --git a/src/App.tsx b/src/App.tsx index 517bc9f52..04d3b0bd1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,6 +5,8 @@ import { Navigate, } from "react-router-dom"; import { AuthProvider } from "./context/AuthContext"; +import { SocketProvider } from "./context/SocketContext"; +import { Toaster } from "react-hot-toast"; // Layouts import { DashboardLayout } from "./components/layout/DashboardLayout"; @@ -35,82 +37,92 @@ import { CollaborationPage } from "./pages/collaboration/CollaborationPage"; // Chat Pages import { ChatPage } from "./pages/chat/ChatPage"; import { MeetingsPage } from "./pages/meetings/MeetingsPage"; +import { TransactionsPage } from "./pages/dashboard/TransactionsPage"; function App() { return ( - - - {/* Authentication Routes */} - } /> - } /> - - {/* Dashboard Routes */} - }> - } /> - } /> - - - {/* Profile Routes */} - }> - } /> - } /> - - - {/* Feature Routes */} - }> - } /> - - - }> - } /> - - - }> - } /> - - - }> - } /> - - - }> - } /> - - - }> - } /> - - - }> - } /> - - - }> - } /> - - - }> - } /> - - - }> - } /> - - - {/* Chat Routes */} - }> - } /> - } /> - - - {/* Redirect root to login */} - } /> - - {/* Catch all other routes and redirect to login */} - } /> - - + + + + + {/* Authentication Routes */} + } /> + } /> + + {/* Dashboard Routes */} + }> + } /> + } /> + + + {/* Profile Routes */} + }> + } + /> + } /> + + + {/* Feature Routes */} + }> + } /> + + + }> + } /> + + + }> + } /> + + + }> + } /> + + + }> + } /> + + + }> + } /> + + + }> + } /> + + + }> + } /> + + + }> + } /> + + + }> + } /> + + + }> + } /> + } /> + + + }> + } /> + + + {/* Redirect root to login */} + } /> + + {/* Catch all other routes and redirect to login */} + } /> + + + ); } diff --git a/src/components/chat/ChatUserList.tsx b/src/components/chat/ChatUserList.tsx index 2c8b75405..24a76dfff 100644 --- a/src/components/chat/ChatUserList.tsx +++ b/src/components/chat/ChatUserList.tsx @@ -5,6 +5,7 @@ import { ChatConversation } from "../../types"; import { Avatar } from "../ui/Avatar"; import { Badge } from "../ui/Badge"; import { useAuth } from "../../context/AuthContext"; +import { useSocket } from "../../context/SocketContext"; interface ChatUserListProps { conversations: ChatConversation[]; @@ -15,6 +16,7 @@ export const ChatUserList: React.FC = ({ }) => { const navigate = useNavigate(); const { userId: activeUserId } = useParams<{ userId: string }>(); + const { userStatuses } = useSocket(); const { user: currentUser } = useAuth(); if (!currentUser) return null; @@ -38,6 +40,8 @@ export const ChatUserList: React.FC = ({ const lastMessage = conversation.lastMessage; const isActive = activeUserId === otherUser.id; + const statusInfo = userStatuses[otherUser.id]; + const status = statusInfo?.status || "offline"; return (
= ({ src={otherUser.avatarUrl} alt={otherUser.name} size="md" - status={otherUser.isOnline ? "online" : "offline"} + status={status} className="mr-3 flex-shrink-0" /> diff --git a/src/components/chat/VideoCallModal.tsx b/src/components/chat/VideoCallModal.tsx new file mode 100644 index 000000000..8f3e131f3 --- /dev/null +++ b/src/components/chat/VideoCallModal.tsx @@ -0,0 +1,255 @@ +import React, { useEffect, useRef, useState } from "react"; +import { + X, + Mic, + MicOff, + Video as VideoIcon, + VideoOff, + PhoneOff, +} from "lucide-react"; +import { useSocket } from "../../context/SocketContext"; +import { Button } from "../ui/Button"; +import { useCallback, useMemo } from "react"; + +interface VideoCallModalProps { + roomId: string; + targetUserId: string; + onClose: () => void; + isIncoming?: boolean; + callType?: "video" | "voice"; +} + +export const VideoCallModal: React.FC = ({ + roomId, + targetUserId, + onClose, + isIncoming = false, + callType = "video", +}) => { + const { socket } = useSocket(); + const [localStream, setLocalStream] = useState(null); + const [remoteStream, setRemoteStream] = useState(null); + const [isAudioMuted, setIsAudioMuted] = useState(false); + const [isVideoOff, setIsVideoOff] = useState(callType === "voice"); + const peerConnection = useRef(null); + const localVideoRef = useRef(null); + const remoteVideoRef = useRef(null); + + const servers = useMemo( + () => ({ + iceServers: [ + { + urls: [ + "stun:stun1.l.google.com:19302", + "stun:stun2.l.google.com:19302", + ], + }, + ], + }), + [], + ); + + const handleEndCall = useCallback(() => { + localStream?.getTracks().forEach((track) => track.stop()); + peerConnection.current?.close(); + socket?.emit("end-call", roomId); + onClose(); + }, [localStream, roomId, socket, onClose]); + + useEffect(() => { + const startCall = async () => { + try { + const stream = await navigator.mediaDevices.getUserMedia({ + video: callType === "video", + audio: true, + }); + setLocalStream(stream); + if (localVideoRef.current) localVideoRef.current.srcObject = stream; + + peerConnection.current = new RTCPeerConnection(servers); + + stream.getTracks().forEach((track) => { + peerConnection.current?.addTrack(track, stream); + }); + + peerConnection.current.ontrack = (event) => { + setRemoteStream(event.streams[0]); + if (remoteVideoRef.current) + remoteVideoRef.current.srcObject = event.streams[0]; + }; + + peerConnection.current.onicecandidate = (event) => { + if (event.candidate) { + socket?.emit("ice-candidate", { + roomId, + targetUserId, + candidate: event.candidate, + }); + } + }; + + if (!isIncoming) { + const offer = await peerConnection.current.createOffer(); + await peerConnection.current.setLocalDescription(offer); + socket?.emit("offer", { roomId, targetUserId, offer }); + } + + socket?.on( + "offer", + async (data: { offer: RTCSessionDescriptionInit }) => { + const { offer } = data; + if (isIncoming && peerConnection.current) { + await peerConnection.current.setRemoteDescription( + new RTCSessionDescription(offer), + ); + const answer = await peerConnection.current.createAnswer(); + await peerConnection.current.setLocalDescription(answer); + socket.emit("answer", { roomId, targetUserId, answer }); + } + }, + ); + + socket?.on( + "answer", + async (data: { answer: RTCSessionDescriptionInit }) => { + const { answer } = data; + if (peerConnection.current) { + await peerConnection.current.setRemoteDescription( + new RTCSessionDescription(answer), + ); + } + }, + ); + + socket?.on( + "ice-candidate", + async (data: { candidate: RTCIceCandidateInit }) => { + const { candidate } = data; + if (peerConnection.current) { + await peerConnection.current.addIceCandidate( + new RTCIceCandidate(candidate), + ); + } + }, + ); + + socket?.on("call-ended", () => { + handleEndCall(); + }); + + socket?.emit("join-room", roomId); + } catch (err) { + console.error("Error accessing media devices:", err); + } + }; + + startCall(); + + return () => { + socket?.off("offer"); + socket?.off("answer"); + socket?.off("ice-candidate"); + socket?.off("call-ended"); + handleEndCall(); + }; + }, [ + roomId, + socket, + isIncoming, + handleEndCall, + servers, + targetUserId, + callType, + ]); + + const toggleAudio = () => { + if (localStream) { + localStream.getAudioTracks()[0].enabled = + !localStream.getAudioTracks()[0].enabled; + setIsAudioMuted(!isAudioMuted); + } + }; + + const toggleVideo = () => { + if (localStream) { + localStream.getVideoTracks()[0].enabled = + !localStream.getVideoTracks()[0].enabled; + setIsVideoOff(!isVideoOff); + } + }; + + return ( +
+
+ {/* Remote Video (Full Screen) */} +
+
+ + {/* Local Video (Floating) */} +
+
+ + {/* Controls */} +
+ + + + + +
+ + {/* Close Button */} + +
+
+ ); +}; diff --git a/src/components/collaboration/CollaborationRequestCard.tsx b/src/components/collaboration/CollaborationRequestCard.tsx index 192897711..87e165913 100644 --- a/src/components/collaboration/CollaborationRequestCard.tsx +++ b/src/components/collaboration/CollaborationRequestCard.tsx @@ -1,64 +1,66 @@ -import React from 'react'; -import { useNavigate } from 'react-router-dom'; -import { Check, X, MessageCircle } from 'lucide-react'; -import { CollaborationRequest } from '../../types'; -import { Card, CardBody, CardFooter } from '../ui/Card'; -import { Avatar } from '../ui/Avatar'; -import { Badge } from '../ui/Badge'; -import { Button } from '../ui/Button'; -import { findUserById } from '../../data/users'; -import { updateRequestStatus } from '../../data/collaborationRequests'; -import { formatDistanceToNow } from 'date-fns'; +import React from "react"; +import { useNavigate } from "react-router-dom"; +import { Check, X, MessageCircle } from "lucide-react"; +import { CollaborationRequest } from "../../types"; +import { Card, CardBody, CardFooter } from "../ui/Card"; +import { Avatar } from "../ui/Avatar"; +import { Badge } from "../ui/Badge"; +import { Button } from "../ui/Button"; +import { formatDistanceToNow } from "date-fns"; interface CollaborationRequestCardProps { request: CollaborationRequest; - onStatusUpdate?: (requestId: string, status: 'accepted' | 'rejected') => void; + onStatusUpdate?: (requestId: string, status: "accepted" | "rejected") => void; } -export const CollaborationRequestCard: React.FC = ({ - request, - onStatusUpdate -}) => { +export const CollaborationRequestCard: React.FC< + CollaborationRequestCardProps +> = ({ request, onStatusUpdate }) => { const navigate = useNavigate(); - const investor = findUserById(request.investorId); - - if (!investor) return null; - + const investor = request.partner || (request as any).investor; // Support populated partner or fallback + + if (!investor) { + console.warn( + "CollaborationRequestCard: No partner data found for request", + request.id, + ); + return null; + } + const handleAccept = () => { - updateRequestStatus(request.id, 'accepted'); if (onStatusUpdate) { - onStatusUpdate(request.id, 'accepted'); + onStatusUpdate(request.id, "accepted"); } }; - + const handleReject = () => { - updateRequestStatus(request.id, 'rejected'); if (onStatusUpdate) { - onStatusUpdate(request.id, 'rejected'); + onStatusUpdate(request.id, "rejected"); } }; - + const handleMessage = () => { navigate(`/chat/${investor.id}`); }; - + const handleViewProfile = () => { - navigate(`/profile/investor/${investor.id}`); + const role = investor.role === "investor" ? "investor" : "entrepreneur"; + navigate(`/profile/${role}/${investor.id}`); }; - + const getStatusBadge = () => { switch (request.status) { - case 'pending': + case "pending": return Pending; - case 'accepted': + case "accepted": return Accepted; - case 'rejected': + case "rejected": return Declined; default: return null; } }; - + return ( @@ -68,28 +70,32 @@ export const CollaborationRequestCard: React.FC = src={investor.avatarUrl} alt={investor.name} size="md" - status={investor.isOnline ? 'online' : 'offline'} + status={investor.isOnline ? "online" : "offline"} className="mr-3" /> - +
-

{investor.name}

+

+ {investor.name} +

- {formatDistanceToNow(new Date(request.createdAt), { addSuffix: true })} + {formatDistanceToNow(new Date(request.createdAt), { + addSuffix: true, + })}

- + {getStatusBadge()}
- +

{request.message}

- + - {request.status === 'pending' ? ( + {request.status === "pending" ? (
- + - -
@@ -142,4 +144,4 @@ export const CollaborationRequestCard: React.FC =
); -}; \ No newline at end of file +}; diff --git a/src/components/dashboard/PaymentModals.tsx b/src/components/dashboard/PaymentModals.tsx new file mode 100644 index 000000000..affb1cb53 --- /dev/null +++ b/src/components/dashboard/PaymentModals.tsx @@ -0,0 +1,308 @@ +import React, { useState, useEffect } from "react"; +import { X, DollarSign } from "lucide-react"; +import { Button } from "../ui/Button"; +import { Input } from "../ui/Input"; +import paymentService from "../../services/paymentService"; + +interface ModalProps { + isOpen: boolean; + onClose: () => void; + title: string; + children: React.ReactNode; +} + +const BaseModal: React.FC = ({ + isOpen, + onClose, + title, + children, +}) => { + if (!isOpen) return null; + + return ( +
+
+
+

{title}

+ +
+
{children}
+
+
+ ); +}; + +interface PaymentModalProps { + isOpen: boolean; + onClose: () => void; + onConfirm: (amount: number, extra?: string) => Promise | void; + isLoading?: boolean; +} + +export const DepositModal: React.FC = ({ + isOpen, + onClose, + onConfirm, + isLoading, +}) => { + const [amount, setAmount] = useState(""); + const [method, setMethod] = useState("Credit Card"); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + onConfirm(Number(amount), method); + }; + + return ( + +
+ setAmount(e.target.value)} + required + startAdornment={} + fullWidth + /> +
+ + +
+
+ + +
+ +
+ ); +}; + +export const WithdrawModal: React.FC< + PaymentModalProps & { balance: number } +> = ({ isOpen, onClose, onConfirm, isLoading, balance }) => { + const [amount, setAmount] = useState(""); + const [method, setMethod] = useState("Bank Account"); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + onConfirm(Number(amount), method); + }; + + return ( + +
+
+ Available Balance + + ${balance.toLocaleString()} + +
+ setAmount(e.target.value)} + required + startAdornment={} + fullWidth + error={Number(amount) > balance ? "Insufficient funds" : ""} + /> +
+ + +
+
+ + +
+ +
+ ); +}; + +export const TransferModal: React.FC< + PaymentModalProps & { balance: number } +> = ({ isOpen, onClose, onConfirm, isLoading, balance }) => { + const [amount, setAmount] = useState(""); + const [recipientId, setRecipientId] = useState(""); + const [description, setDescription] = useState(""); + const [connections, setConnections] = useState([]); + const [isFetchingConnections, setIsFetchingConnections] = useState(false); + + useEffect(() => { + if (isOpen) { + const fetchConnections = async () => { + setIsFetchingConnections(true); + try { + const data = await paymentService.getConnections(); + setConnections(data); + } catch (error) { + console.error("Failed to fetch connections:", error); + } finally { + setIsFetchingConnections(false); + } + }; + fetchConnections(); + } + }, [isOpen]); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + onConfirm(Number(amount), JSON.stringify({ recipientId, description })); + }; + + return ( + +
+
+ Available Balance + + ${balance.toLocaleString()} + +
+ +
+ + {isFetchingConnections ? ( +
+
+
+ ) : connections.length > 0 ? ( +
+ {connections.map((user) => ( + + ))} +
+ ) : ( +
+

+ No connected partners found. +

+

+ Establish collaborations to enable transfers. +

+
+ )} +
+ + setAmount(e.target.value)} + required + startAdornment={} + fullWidth + error={Number(amount) > balance ? "Insufficient funds" : ""} + /> + + setDescription(e.target.value)} + fullWidth + /> + +
+ + +
+ +
+ ); +}; diff --git a/src/components/dashboard/TransactionHistory.tsx b/src/components/dashboard/TransactionHistory.tsx new file mode 100644 index 000000000..b368ffcb8 --- /dev/null +++ b/src/components/dashboard/TransactionHistory.tsx @@ -0,0 +1,157 @@ +import React from "react"; +import { format } from "date-fns"; +import { + ArrowDownLeft, + ArrowUpRight, + ArrowRightLeft, + Clock, +} from "lucide-react"; +import { Transaction } from "../../types"; +import { Card, CardBody, CardHeader } from "../ui/Card"; +import { Badge } from "../ui/Badge"; + +interface TransactionHistoryProps { + transactions: Transaction[]; + loading?: boolean; +} + +export const TransactionHistory: React.FC = ({ + transactions, + loading = false, +}) => { + const getIcon = (type: string) => { + switch (type) { + case "deposit": + return ; + case "withdraw": + return ; + case "transfer": + return ; + default: + return ; + } + }; + + const getStatusColor = (status: string) => { + switch (status) { + case "completed": + return "success"; + case "pending": + return "warning"; + case "failed": + return "error"; + default: + return "gray"; + } + }; + + return ( + + +
+

+ Recent Transactions +

+ {transactions.length} total +
+
+ + {loading ? ( +
+ Loading transactions... +
+ ) : transactions.length === 0 ? ( +
+ No transactions yet. +
+ ) : ( +
+ + + + + + + + + + + + {transactions.map((transaction) => { + const txKey = + transaction.id || (transaction as any)._id || Math.random(); + return ( + + + + + + + + ); + })} + +
+ Type + + Description + + Date + + Amount + + Status +
+
+
+ {getIcon(transaction.type)} +
+ + {transaction.type} + +
+
+

+ {transaction.description} +

+
+

+ {format( + new Date(transaction.createdAt), + "MMM d, yyyy • HH:mm", + )} +

+
+ 0 + ? "text-green-600" + : "text-red-600" + }`} + > + {transaction.amount > 0 ? "+" : "-"} + {Math.abs(transaction.amount).toLocaleString( + undefined, + { + style: "currency", + currency: "USD", + }, + )} + + + + {transaction.status} + +
+
+ )} +
+
+ ); +}; diff --git a/src/components/dashboard/WalletCard.tsx b/src/components/dashboard/WalletCard.tsx new file mode 100644 index 000000000..5c785e0b4 --- /dev/null +++ b/src/components/dashboard/WalletCard.tsx @@ -0,0 +1,78 @@ +import React from "react"; +import { Wallet, Plus, ArrowUpRight, ArrowRight } from "lucide-react"; +import { Card, CardBody } from "../ui/Card"; +import { Button } from "../ui/Button"; + +interface WalletCardProps { + balance: number; + onDeposit: () => void; + onWithdraw: () => void; + onTransfer: () => void; +} + +export const WalletCard: React.FC = ({ + balance, + onDeposit, + onWithdraw, + onTransfer, +}) => { + return ( + +
+ +
+ +
+ +

Virtual Wallet

+
+ +
+

+ Total Balance +

+

+ $ + {balance.toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} +

+
+ +
+ + + +
+
+
+ ); +}; diff --git a/src/components/entrepreneur/EntrepreneurCard.tsx b/src/components/entrepreneur/EntrepreneurCard.tsx index 67f1fafe0..e9efafbf3 100644 --- a/src/components/entrepreneur/EntrepreneurCard.tsx +++ b/src/components/entrepreneur/EntrepreneurCard.tsx @@ -1,11 +1,12 @@ -import React from 'react'; -import { useNavigate } from 'react-router-dom'; -import { MessageCircle, ExternalLink } from 'lucide-react'; -import { Entrepreneur } from '../../types'; -import { Card, CardBody, CardFooter } from '../ui/Card'; -import { Avatar } from '../ui/Avatar'; -import { Badge } from '../ui/Badge'; -import { Button } from '../ui/Button'; +import React from "react"; +import { useNavigate } from "react-router-dom"; +import { MessageCircle, ExternalLink } from "lucide-react"; +import { Entrepreneur } from "../../types"; +import { Card, CardBody, CardFooter } from "../ui/Card"; +import { Avatar } from "../ui/Avatar"; +import { Badge } from "../ui/Badge"; +import { Button } from "../ui/Button"; +import { useSocket } from "../../context/SocketContext"; interface EntrepreneurCardProps { entrepreneur: Entrepreneur; @@ -14,22 +15,23 @@ interface EntrepreneurCardProps { export const EntrepreneurCard: React.FC = ({ entrepreneur, - showActions = true + showActions = true, }) => { const navigate = useNavigate(); - + const { userStatuses } = useSocket(); + const handleViewProfile = () => { navigate(`/profile/entrepreneur/${entrepreneur.id}`); }; - + const handleMessage = (e: React.MouseEvent) => { e.stopPropagation(); // Prevent card click navigate(`/chat/${entrepreneur.id}`); }; - + return ( - @@ -39,40 +41,58 @@ export const EntrepreneurCard: React.FC = ({ src={entrepreneur.avatarUrl} alt={entrepreneur.name} size="lg" - status={entrepreneur.isOnline ? 'online' : 'offline'} + status={userStatuses[entrepreneur.id]?.status || "offline"} className="mr-4" /> - +
-

{entrepreneur.name}

-

{entrepreneur.startupName}

- +

+ {entrepreneur.name} +

+

+ {entrepreneur.startupName} +

+
- {entrepreneur.industry} - {entrepreneur.location} - Founded {entrepreneur.foundedYear} + + {entrepreneur.industry} + + + {entrepreneur.location} + + + Founded {entrepreneur.foundedYear} +
- +
-

Pitch Summary

-

{entrepreneur.pitchSummary}

+

+ Pitch Summary +

+

+ {entrepreneur.pitchSummary} +

- +
Funding Need -

{entrepreneur.fundingNeeded}

+

+ {entrepreneur.fundingNeeded} +

- +
Team Size -

{entrepreneur.teamSize} people

+

+ {entrepreneur.teamSize} people +

- + {showActions && ( - +
- +
-

Investment Interests

+

+ Investment Interests +

{investor.investmentInterests.map((interest, index) => ( - {interest} + + {interest} + ))}
- +

{investor.bio}

- +
Investment Range -

{investor.minimumInvestment} - {investor.maximumInvestment}

+

+ {investor.minimumInvestment} - {investor.maximumInvestment} +

- + {showActions && ( - + - - + + - {user.name} + + {user.name} +
) : ( @@ -113,7 +154,7 @@ export const Navbar: React.FC = () => {
)} - + {/* Mobile menu button */}
- + {/* Mobile menu */} {isMenuOpen && (
@@ -141,14 +182,18 @@ export const Navbar: React.FC = () => { src={user.avatarUrl} alt={user.name} size="sm" - status={user.isOnline ? 'online' : 'offline'} + status={userStatuses[user.id]?.status || "offline"} />
-

{user.name}

-

{user.role}

+

+ {user.name} +

+

+ {user.role} +

- +
{navLinks.map((link, index) => ( { {link.text} ))} - + + - setIsMenuOpen(false)} > @@ -197,4 +244,4 @@ export const Navbar: React.FC = () => { )} ); -}; \ No newline at end of file +}; diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 905895777..0af55ee7a 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -12,6 +12,7 @@ import { Settings, HelpCircle, CalendarDays, + CreditCard, } from "lucide-react"; import { notificationService } from "../../services/notificationService"; import { messageService } from "../../services/messageService"; @@ -110,6 +111,11 @@ export const Sidebar: React.FC = () => { }, { to: "/requests", icon: , text: "Requests" }, { to: "/documents", icon: , text: "Documents" }, + { + to: "/transactions", + icon: , + text: "Transactions", + }, ]; const investorItems = [ @@ -139,6 +145,11 @@ export const Sidebar: React.FC = () => { }, { to: "/requests", icon: , text: "Requests" }, { to: "/deals", icon: , text: "Deals" }, + { + to: "/transactions", + icon: , + text: "Transactions", + }, ]; const sidebarItems = diff --git a/src/components/ui/Avatar.tsx b/src/components/ui/Avatar.tsx index fb0ab2ec7..0e92db12a 100644 --- a/src/components/ui/Avatar.tsx +++ b/src/components/ui/Avatar.tsx @@ -1,45 +1,46 @@ -import React from 'react'; +import React from "react"; -export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'; +export type AvatarSize = "xs" | "sm" | "md" | "lg" | "xl"; interface AvatarProps { src: string; alt: string; size?: AvatarSize; className?: string; - status?: 'online' | 'offline' | 'away' | 'busy'; + status?: "online" | "offline" | "away" | "busy" | "recently_active"; } export const Avatar: React.FC = ({ src, alt, - size = 'md', - className = '', + size = "md", + className = "", status, }) => { const sizeClasses = { - xs: 'h-6 w-6', - sm: 'h-8 w-8', - md: 'h-10 w-10', - lg: 'h-12 w-12', - xl: 'h-16 w-16', + xs: "h-6 w-6", + sm: "h-8 w-8", + md: "h-10 w-10", + lg: "h-12 w-12", + xl: "h-16 w-16", }; - + const statusColors = { - online: 'bg-success-500', - offline: 'bg-gray-400', - away: 'bg-warning-500', - busy: 'bg-error-500', + online: "bg-green-500", + offline: "bg-red-500", + recently_active: "bg-yellow-500", + away: "bg-warning-500", + busy: "bg-error-500", }; - + const statusSizes = { - xs: 'h-1.5 w-1.5', - sm: 'h-2 w-2', - md: 'h-2.5 w-2.5', - lg: 'h-3 w-3', - xl: 'h-4 w-4', + xs: "h-1.5 w-1.5", + sm: "h-2 w-2", + md: "h-2.5 w-2.5", + lg: "h-3 w-3", + xl: "h-4 w-4", }; - + return (
= ({ target.src = `https://ui-avatars.com/api/?name=${encodeURIComponent(alt)}&background=random`; }} /> - + {status && ( - )}
); -}; \ No newline at end of file +}; diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index 9a1c25b2f..7aaee7e9b 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react-refresh/only-export-components */ import React, { createContext, useState, useContext, useEffect } from "react"; import { User, UserRole, AuthContextType } from "../types"; import api from "../services/api"; @@ -50,6 +51,41 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ initAuth(); }, []); + // Listen for storage changes in other tabs + useEffect(() => { + const handleStorageChange = (e: StorageEvent) => { + if (e.key === TOKEN_STORAGE_KEY || e.key === USER_STORAGE_KEY) { + console.log( + "Storage change detected in another tab, syncing auth state...", + ); + const storedUser = localStorage.getItem(USER_STORAGE_KEY); + const token = localStorage.getItem(TOKEN_STORAGE_KEY); + + if (!token || !storedUser) { + // If token or user removed in another tab, logout here too + console.log("Auth cleared in another tab, logging out..."); + setUser(null); + } else { + try { + const parsedUser = JSON.parse(storedUser); + // Only update if it's actually different to avoid unnecessary re-renders + if (JSON.stringify(parsedUser) !== JSON.stringify(user)) { + console.log( + "User data changed in another tab, updating state...", + ); + setUser(parsedUser); + } + } catch (error) { + console.error("Failed to parse synced user data:", error); + } + } + } + }; + + window.addEventListener("storage", handleStorageChange); + return () => window.removeEventListener("storage", handleStorageChange); + }, [user]); + // Login function const login = async ( email: string, @@ -228,10 +264,10 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ return {children}; }; -export const useAuth = (): AuthContextType => { +export function useAuth() { const context = useContext(AuthContext); if (context === undefined) { throw new Error("useAuth must be used within an AuthProvider"); } return context; -}; +} diff --git a/src/context/SocketContext.tsx b/src/context/SocketContext.tsx new file mode 100644 index 000000000..90f952c31 --- /dev/null +++ b/src/context/SocketContext.tsx @@ -0,0 +1,93 @@ +/* eslint-disable react-refresh/only-export-components */ +import React, { createContext, useContext, useEffect, useState } from "react"; +import { io, type Socket } from "socket.io-client"; +import { useAuth } from "./AuthContext"; +import toast from "react-hot-toast"; + +interface SocketContextType { + socket: Socket | null; + onlineUsers: string[]; + userStatuses: Record< + string, + { status: "online" | "offline" | "recently_active"; lastSeen: string } + >; +} + +const SocketContext = createContext({ + socket: null, + onlineUsers: [], + userStatuses: {}, +}); + +export function useSocket() { + return useContext(SocketContext); +} + +export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + const [socket, setSocket] = useState(null); + const [onlineUsers, setOnlineUsers] = useState([]); + const [userStatuses, setUserStatuses] = useState< + Record< + string, + { status: "online" | "offline" | "recently_active"; lastSeen: string } + > + >({}); + const { user } = useAuth(); + + useEffect(() => { + if (user) { + const newSocket = io("http://localhost:3001"); + setSocket(newSocket); + + newSocket.on("get-online-users", (users: string[]) => { + setOnlineUsers(users); + }); + + newSocket.on( + "user-statuses", + ( + statuses: Record< + string, + { + status: "online" | "offline" | "recently_active"; + lastSeen: string; + } + >, + ) => { + setUserStatuses(statuses); + }, + ); + + newSocket.on( + "payment_received", + (data: { amount: number; senderName: string }) => { + toast.success( + `Received $${data.amount.toLocaleString()} from ${data.senderName}`, + { + duration: 5000, + icon: "💰", + }, + ); + // Dispatch custom event for dashboards to refresh data + window.dispatchEvent(new CustomEvent("payment-updated")); + }, + ); + + newSocket.emit("setup", user.id); + + return () => { + newSocket.close(); + }; + } else { + setSocket(null); + } + }, [user]); + + return ( + + {children} + + ); +}; diff --git a/src/pages/chat/ChatPage.tsx b/src/pages/chat/ChatPage.tsx index f4a8d5dbf..04fba57c8 100644 --- a/src/pages/chat/ChatPage.tsx +++ b/src/pages/chat/ChatPage.tsx @@ -11,6 +11,9 @@ import { Message, ChatConversation, User } from "../../types"; import { messageService } from "../../services/messageService"; import api from "../../services/api"; import { MessageCircle } from "lucide-react"; +import { VideoCallModal } from "../../components/chat/VideoCallModal"; +import { formatDistanceToNow } from "date-fns"; +import { useSocket } from "../../context/SocketContext"; export const ChatPage: React.FC = () => { const { userId } = useParams<{ userId: string }>(); @@ -19,7 +22,14 @@ export const ChatPage: React.FC = () => { const [newMessage, setNewMessage] = useState(""); const [conversations, setConversations] = useState([]); const [chatPartner, setChatPartner] = useState(null); + const [showVideoCall, setShowVideoCall] = useState(false); + const [incomingCall, setIncomingCall] = useState<{ + roomId: string; + fromName: string; + } | null>(null); + const [callType, setCallType] = useState<"video" | "voice">("video"); const messagesEndRef = useRef(null); + const { socket, userStatuses } = useSocket(); useEffect(() => { const fetchConversations = async () => { @@ -66,12 +76,50 @@ export const ChatPage: React.FC = () => { const interval = setInterval(fetchMessages, 3000); // Poll for new messages return () => clearInterval(interval); }, [currentUser, userId]); + useEffect(() => { + if (socket && currentUser) { + socket.on( + "offer", + (data: { from: string; offer: any; roomId: string }) => { + setIncomingCall({ + roomId: data.roomId, + fromName: chatPartner?.name || "Incoming Call", + }); + }, + ); + return () => { + socket.off("offer"); + }; + } + }, [socket, currentUser, userId]); useEffect(() => { // Scroll to bottom of messages messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); + const initiateVideoCall = () => { + if (userId && currentUser) { + setCallType("video"); + setShowVideoCall(true); + } + }; + + const initiateVoiceCall = () => { + if (userId && currentUser) { + setCallType("voice"); + setShowVideoCall(true); + } + }; + + const acceptCall = () => { + if (incomingCall) { + setShowVideoCall(true); + // Wait a bit for the modal to mount and register listeners + setIncomingCall(null); + } + }; + const handleSendMessage = async (e: React.FormEvent) => { e.preventDefault(); @@ -110,7 +158,7 @@ export const ChatPage: React.FC = () => { src={chatPartner.avatarUrl} alt={chatPartner.name} size="md" - status={chatPartner.isOnline ? "online" : "offline"} + status={userStatuses[chatPartner.id]?.status || "offline"} className="mr-3" /> @@ -119,7 +167,12 @@ export const ChatPage: React.FC = () => { {chatPartner.name}

- {chatPartner.isOnline ? "Online" : "Last seen recently"} + {userStatuses[chatPartner.id]?.status === "online" + ? "Online" + : userStatuses[chatPartner.id]?.status === + "recently_active" + ? `Recently active (${formatDistanceToNow(new Date(userStatuses[chatPartner.id].lastSeen), { addSuffix: true })})` + : `Active ${formatDistanceToNow(new Date(userStatuses[chatPartner.id]?.lastSeen || chatPartner.createdAt), { addSuffix: true })}`}

@@ -130,6 +183,7 @@ export const ChatPage: React.FC = () => { size="sm" className="rounded-full p-2" aria-label="Voice call" + onClick={initiateVoiceCall} > @@ -139,6 +193,7 @@ export const ChatPage: React.FC = () => { size="sm" className="rounded-full p-2" aria-label="Video call" + onClick={initiateVideoCall} >
+

Password Reset Request

+

Hello,

+

You have requested to reset your password for your Business Nexus account.

+
+

If the button above doesn't work, copy and paste the following link into your browser:

+

${resetLink}

+

This link will expire in 10 minutes.

+

If you didn't request a password reset, please ignore this email.

+

+ © 2026 Business Nexus. All rights reserved. +

+
+ `; + + return sendEmail(to, subject, html); +}; diff --git a/package-lock.json b/package-lock.json index 1293d407a..1d8636b01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,9 @@ "react-hot-toast": "^2.4.1", "react-router-dom": "^6.22.1", "socket.io": "^4.8.3", - "socket.io-client": "^4.8.3" + "socket.io-client": "^4.8.3", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1" }, "devDependencies": { "@eslint/js": "^9.9.1", @@ -37,6 +39,8 @@ "@types/nodemailer": "^7.0.9", "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.8", "@vitejs/plugin-react": "^4.3.1", "autoprefixer": "^10.4.18", "eslint": "^9.9.1", @@ -76,6 +80,50 @@ "node": ">=6.0.0" } }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", + "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", + "license": "MIT" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.0.6", + "@apidevtools/openapi-schemas": "^2.0.4", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "z-schema": "^5.0.1" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, "node_modules/@babel/code-frame": { "version": "7.25.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.25.7.tgz", @@ -1024,6 +1072,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "license": "MIT" + }, "node_modules/@mongodb-js/saslprep": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.5.tgz", @@ -1295,6 +1349,13 @@ "win32" ] }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, "node_modules/@socket.io/component-emitter": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", @@ -1420,8 +1481,7 @@ "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, "node_modules/@types/jsonwebtoken": { "version": "9.0.10", @@ -1520,6 +1580,24 @@ "@types/node": "*" } }, + "node_modules/@types/swagger-jsdoc": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz", + "integrity": "sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz", + "integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, "node_modules/@types/webidl-conversions": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", @@ -1913,8 +1991,7 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/asynckit": { "version": "0.4.0", @@ -1982,8 +2059,7 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/base64id": { "version": "2.0.0", @@ -2043,7 +2119,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2147,6 +2222,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "license": "MIT" + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2275,8 +2356,7 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "node_modules/content-disposition": { "version": "1.0.1", @@ -2436,6 +2516,18 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "dev": true }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/dotenv": { "version": "17.2.3", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", @@ -2921,7 +3013,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -3245,6 +3336,12 @@ "node": ">= 0.8" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3538,6 +3635,17 @@ "node": ">=0.8.19" } }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -3664,7 +3772,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "dependencies": { "argparse": "^2.0.1" }, @@ -3836,6 +3943,13 @@ "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "license": "MIT" }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -3848,6 +3962,13 @@ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", "license": "MIT" }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, "node_modules/lodash.isinteger": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", @@ -3878,6 +3999,12 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "license": "MIT" + }, "node_modules/lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", @@ -3995,7 +4122,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -4242,6 +4368,13 @@ "wrappy": "1" } }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "license": "MIT", + "peer": true + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -4325,6 +4458,15 @@ "node": ">=8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -5406,6 +5548,101 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swagger-jsdoc": { + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", + "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==", + "license": "MIT", + "dependencies": { + "commander": "6.2.0", + "doctrine": "3.0.0", + "glob": "7.1.6", + "lodash.mergewith": "^4.6.2", + "swagger-parser": "^10.0.3", + "yaml": "2.0.0-1" + }, + "bin": { + "swagger-jsdoc": "bin/swagger-jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/swagger-jsdoc/node_modules/commander": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", + "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/swagger-jsdoc/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/swagger-jsdoc/node_modules/yaml": { + "version": "2.0.0-1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", + "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==", + "license": "MIT", + "dependencies": { + "@apidevtools/swagger-parser": "10.0.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.31.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.31.0.tgz", + "integrity": "sha512-zSUTIck02fSga6rc0RZP3b7J7wgHXwLea8ZjgLA3Vgnb8QeOl3Wou2/j5QkzSGeoz6HusP/coYuJl33aQxQZpg==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, "node_modules/tailwindcss": { "version": "3.4.13", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.13.tgz", @@ -6447,6 +6684,36 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/z-schema": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", + "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", + "license": "MIT", + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "commander": "^9.4.1" + } + }, + "node_modules/z-schema/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.20.0 || >=14" + } } } } diff --git a/package.json b/package.json index 264593fd8..8ffc4a956 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,9 @@ "react-hot-toast": "^2.4.1", "react-router-dom": "^6.22.1", "socket.io": "^4.8.3", - "socket.io-client": "^4.8.3" + "socket.io-client": "^4.8.3", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1" }, "devDependencies": { "@eslint/js": "^9.9.1", @@ -40,6 +42,8 @@ "@types/nodemailer": "^7.0.9", "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.8", "@vitejs/plugin-react": "^4.3.1", "autoprefixer": "^10.4.18", "eslint": "^9.9.1", diff --git a/src/App.tsx b/src/App.tsx index 04d3b0bd1..46c632c88 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,6 +14,8 @@ import { DashboardLayout } from "./components/layout/DashboardLayout"; // Auth Pages import { LoginPage } from "./pages/auth/LoginPage"; import { RegisterPage } from "./pages/auth/RegisterPage"; +import { ForgotPasswordPage } from "./pages/auth/ForgotPasswordPage"; +import { ResetPasswordPage } from "./pages/auth/ResetPasswordPage"; // Dashboard Pages import { EntrepreneurDashboard } from "./pages/dashboard/EntrepreneurDashboard"; @@ -49,6 +51,8 @@ function App() { {/* Authentication Routes */} } /> } /> + } /> + } /> {/* Dashboard Routes */} }> diff --git a/src/components/collaboration/CollaborationRequestCard.tsx b/src/components/collaboration/CollaborationRequestCard.tsx index 87e165913..094a148f1 100644 --- a/src/components/collaboration/CollaborationRequestCard.tsx +++ b/src/components/collaboration/CollaborationRequestCard.tsx @@ -17,7 +17,7 @@ export const CollaborationRequestCard: React.FC< CollaborationRequestCardProps > = ({ request, onStatusUpdate }) => { const navigate = useNavigate(); - const investor = request.partner || (request as any).investor; // Support populated partner or fallback + const investor = request.partner; // Support populated partner if (!investor) { console.warn( diff --git a/src/components/dashboard/PaymentModals.tsx b/src/components/dashboard/PaymentModals.tsx index affb1cb53..f7553d2f3 100644 --- a/src/components/dashboard/PaymentModals.tsx +++ b/src/components/dashboard/PaymentModals.tsx @@ -174,7 +174,9 @@ export const TransferModal: React.FC< const [amount, setAmount] = useState(""); const [recipientId, setRecipientId] = useState(""); const [description, setDescription] = useState(""); - const [connections, setConnections] = useState([]); + const [connections, setConnections] = useState( + [], + ); const [isFetchingConnections, setIsFetchingConnections] = useState(false); useEffect(() => { diff --git a/src/components/dashboard/TransactionHistory.tsx b/src/components/dashboard/TransactionHistory.tsx index b368ffcb8..a20fad857 100644 --- a/src/components/dashboard/TransactionHistory.tsx +++ b/src/components/dashboard/TransactionHistory.tsx @@ -88,8 +88,7 @@ export const TransactionHistory: React.FC = ({ {transactions.map((transaction) => { - const txKey = - transaction.id || (transaction as any)._id || Math.random(); + const txKey = transaction.id || Math.random(); return ( = ({ {transaction.status} diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index 7aaee7e9b..8a3370e9c 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -91,13 +91,18 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ email: string, password: string, role: UserRole, - ): Promise => { + ): Promise<{ requires2FA?: boolean; tempToken?: string } | void> => { setIsLoading(true); console.log("Attempting login for:", email, "Role:", role); try { const response = await api.post("/auth/login", { email, password }); console.log("Login success:", response.data); - const { token, user: userData } = response.data; + const { token, user: userData, requires2FA, tempToken } = response.data; + + if (requires2FA) { + toast.success("Verification code required to continue"); + return { requires2FA, tempToken }; + } if (userData.role !== role) { console.warn( @@ -118,11 +123,23 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ toast.success("Successfully logged in!"); } catch (error: unknown) { const err = error as { - response?: { data?: { message?: string } }; + response?: { + data?: { message?: string; errors?: Array<{ msg: string }> }; + }; message?: string; }; - const message = + + let message = err.response?.data?.message || err.message || "Login failed"; + + if ( + err.response?.data?.errors && + Array.isArray(err.response.data.errors) + ) { + const detail = err.response.data.errors.map((e) => e.msg).join(". "); + message = `${message}: ${detail}`; + } + console.error("Login failed:", message); toast.error(message); throw new Error(message); @@ -239,12 +256,98 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ newPassword, }); toast.success("Password updated successfully"); + } catch (error: unknown) { + const err = error as { + response?: { + data?: { message?: string; errors?: Array<{ msg: string }> }; + }; + }; + + let message = err.response?.data?.message || "Failed to update password"; + + if ( + err.response?.data?.errors && + Array.isArray(err.response.data.errors) + ) { + const detail = err.response.data.errors.map((e) => e.msg).join(". "); + message = `${message}: ${detail}`; + } + + toast.error(message); + throw new Error(message); + } + }; + + // 2FA Setup + const setup2FA = async (): Promise => { + try { + await api.post("/auth/2fa/setup"); + toast.success("Verification code sent to your email"); + } catch (error: unknown) { + const err = error as { response?: { data?: { message?: string } } }; + const message = err.response?.data?.message || "Failed to send 2FA code"; + toast.error(message); + throw new Error(message); + } + }; + + // 2FA Enable + const enable2FA = async (otp: string): Promise => { + try { + await api.post("/auth/2fa/enable", { otp }); + const updatedUser = { ...user, isTwoFactorEnabled: true } as User; + setUser(updatedUser); + localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(updatedUser)); + toast.success("Two-factor authentication enabled"); + } catch (error: unknown) { + const err = error as { response?: { data?: { message?: string } } }; + const message = err.response?.data?.message || "Failed to enable 2FA"; + toast.error(message); + throw new Error(message); + } + }; + + // 2FA Disable + const disable2FA = async (): Promise => { + try { + await api.post("/auth/2fa/disable"); + const updatedUser = { ...user, isTwoFactorEnabled: false } as User; + setUser(updatedUser); + localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(updatedUser)); + toast.success("Two-factor authentication disabled"); + } catch (error: unknown) { + const err = error as { response?: { data?: { message?: string } } }; + const message = err.response?.data?.message || "Failed to disable 2FA"; + toast.error(message); + throw new Error(message); + } + }; + + // 2FA Validate Login + const validate2FALogin = async ( + tempToken: string, + otp: string, + ): Promise => { + setIsLoading(true); + try { + const response = await api.post("/auth/2fa/validate-login", { + tempToken, + otp, + }); + const { token, user: userData } = response.data; + + setUser(userData); + localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(userData)); + localStorage.setItem(TOKEN_STORAGE_KEY, token); + toast.success("Successfully logged in!"); } catch (error: unknown) { const err = error as { response?: { data?: { message?: string } } }; const message = - err.response?.data?.message || "Failed to update password"; + err.response?.data?.message || "Invalid verification code"; toast.error(message); throw new Error(message); + } finally { + setIsLoading(false); } }; @@ -257,6 +360,10 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ resetPassword, updateProfile, changePassword, + setup2FA, + enable2FA, + disable2FA, + validate2FALogin, isAuthenticated: !!user, isLoading, }; diff --git a/src/context/SocketContext.tsx b/src/context/SocketContext.tsx index 90f952c31..20fe616f5 100644 --- a/src/context/SocketContext.tsx +++ b/src/context/SocketContext.tsx @@ -38,7 +38,10 @@ export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ useEffect(() => { if (user) { - const newSocket = io("http://localhost:3001"); + const socketUrl = import.meta.env.VITE_API_URL + ? import.meta.env.VITE_API_URL.replace("/api", "") + : "http://localhost:3001"; + const newSocket = io(socketUrl); setSocket(newSocket); newSocket.on("get-online-users", (users: string[]) => { diff --git a/src/pages/auth/LoginPage.tsx b/src/pages/auth/LoginPage.tsx index bbd5b08a0..609d3ae5c 100644 --- a/src/pages/auth/LoginPage.tsx +++ b/src/pages/auth/LoginPage.tsx @@ -1,56 +1,115 @@ -import React, { useState } from 'react'; -import { Link, useNavigate } from 'react-router-dom'; -import { User, CircleDollarSign, Building2, LogIn, AlertCircle } from 'lucide-react'; -import { useAuth } from '../../context/AuthContext'; -import { Button } from '../../components/ui/Button'; -import { Input } from '../../components/ui/Input'; -import { UserRole } from '../../types'; +import React, { useState } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import { + User, + CircleDollarSign, + Building2, + LogIn, + AlertCircle, +} from "lucide-react"; +import { useAuth } from "../../context/AuthContext"; +import { Button } from "../../components/ui/Button"; +import { Input } from "../../components/ui/Input"; +import { UserRole } from "../../types"; export const LoginPage: React.FC = () => { - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [role, setRole] = useState('entrepreneur'); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [role, setRole] = useState("entrepreneur"); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); - - const { login } = useAuth(); + const [requires2FA, setRequires2FA] = useState(false); + const [tempToken, setTempToken] = useState(""); + const [otpCode, setOtpCode] = useState(""); + + const { login, validate2FALogin } = useAuth(); const navigate = useNavigate(); - + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(null); setIsLoading(true); - + try { - await login(email, password, role); + const result = await login(email, password, role); + if (result && result.requires2FA) { + setRequires2FA(true); + setTempToken(result.tempToken || ""); + setIsLoading(false); + return; + } // Redirect based on user role - navigate(role === 'entrepreneur' ? '/dashboard/entrepreneur' : '/dashboard/investor'); + navigate( + role === "entrepreneur" + ? "/dashboard/entrepreneur" + : "/dashboard/investor", + ); + } catch (err) { + setError((err as Error).message); + setIsLoading(false); + } + }; + + const handle2FAVerify = async (e: React.FormEvent) => { + e.preventDefault(); + if (!otpCode) { + setError("Please enter the verification code"); + return; + } + setError(null); + setIsLoading(true); + try { + await validate2FALogin(tempToken, otpCode); + navigate( + role === "entrepreneur" + ? "/dashboard/entrepreneur" + : "/dashboard/investor", + ); } catch (err) { setError((err as Error).message); setIsLoading(false); } }; - + // For demo purposes, pre-filled credentials const fillDemoCredentials = (userRole: UserRole) => { - if (userRole === 'entrepreneur') { - setEmail('sarah@techwave.io'); - setPassword('password123'); + if (userRole === "entrepreneur") { + setEmail("sarah@techwave.io"); + setPassword("password123"); } else { - setEmail('michael@vcinnovate.com'); - setPassword('password123'); + setEmail("michael@vcinnovate.com"); + setPassword("password123"); } setRole(userRole); }; - + return (
- - - + + +
@@ -70,119 +129,178 @@ export const LoginPage: React.FC = () => { {error}
)} - -
-
- -
- - - -
-
- - setEmail(e.target.value)} - required - fullWidth - startAdornment={} - /> - - setPassword(e.target.value)} - required - fullWidth - /> - -
-
- -
+
+ + +
+
+
+ )} diff --git a/src/services/api.ts b/src/services/api.ts index d9e1a7b24..d5cc901f4 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -1,6 +1,6 @@ import axios from "axios"; -const API_URL = "http://localhost:3001/api"; +const API_URL = import.meta.env.VITE_API_URL || "http://localhost:3001/api"; const api = axios.create({ baseURL: API_URL, diff --git a/src/services/paymentService.ts b/src/services/paymentService.ts index 54b86dce8..882915fe1 100644 --- a/src/services/paymentService.ts +++ b/src/services/paymentService.ts @@ -53,13 +53,14 @@ export const paymentService = { return response.data; }, searchUsers: async (query: string) => { - const response = await api.get("/users/search", { + const response = await api.get("/users/search", { params: { query }, }); return response.data; }, getConnections: async () => { - const response = await api.get("/users/connections"); + const response = + await api.get("/users/connections"); return response.data; }, }; diff --git a/src/types/index.ts b/src/types/index.ts index 203ee0591..85ceca8d3 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -11,6 +11,7 @@ export interface User { location?: string; isOnline?: boolean; walletBalance?: number; + isTwoFactorEnabled?: boolean; createdAt: string; // Entrepreneur fields @@ -180,7 +181,11 @@ export interface Transaction { export interface AuthContextType { user: User | null; - login: (email: string, password: string, role: UserRole) => Promise; + login: ( + email: string, + password: string, + role: UserRole, + ) => Promise<{ requires2FA?: boolean; tempToken?: string } | void>; register: ( name: string, email: string, @@ -195,6 +200,10 @@ export interface AuthContextType { currentPassword: string, newPassword: string, ) => Promise; + setup2FA: () => Promise; + enable2FA: (otp: string) => Promise; + disable2FA: () => Promise; + validate2FALogin: (tempToken: string, otp: string) => Promise; isAuthenticated: boolean; isLoading: boolean; } diff --git a/vercel.json b/vercel.json index 46fe2ba03..2b3aff9c6 100644 --- a/vercel.json +++ b/vercel.json @@ -1,4 +1,7 @@ { - "rewrites": [{ "source": "/(.*)", "destination": "/" }] - } - \ No newline at end of file + "version": 2, + "rewrites": [ + { "source": "/api/(.*)", "destination": "http://localhost:3001/api/$1" }, + { "source": "/(.*)", "destination": "/index.html" } + ] +} From 9722a6f5d28b6709ffaaecb103aa2a184d27da11 Mon Sep 17 00:00:00 2001 From: m-zeeshan-saeed Date: Sun, 15 Feb 2026 02:45:10 +0500 Subject: [PATCH 09/15] feat: conditionally auto-scroll chat messages based on user's current scroll position or if the last message was sent by the current user. --- src/pages/chat/ChatPage.tsx | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/pages/chat/ChatPage.tsx b/src/pages/chat/ChatPage.tsx index 0e81986cf..2e4832895 100644 --- a/src/pages/chat/ChatPage.tsx +++ b/src/pages/chat/ChatPage.tsx @@ -29,6 +29,7 @@ export const ChatPage: React.FC = () => { } | null>(null); const [callType, setCallType] = useState<"video" | "voice">("video"); const messagesEndRef = useRef(null); + const isAtBottom = useRef(true); const { socket, userStatuses } = useSocket(); useEffect(() => { @@ -93,10 +94,22 @@ export const ChatPage: React.FC = () => { }; } }, [socket, currentUser, userId, chatPartner?.name]); + const handleScroll = (e: React.UIEvent) => { + const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; + const isBottom = scrollHeight - scrollTop - clientHeight < 50; + isAtBottom.current = isBottom; + }; + useEffect(() => { - // Scroll to bottom of messages - messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); - }, [messages]); + // Scroll to bottom only if user is already at bottom or if it's a new message from current user + const lastMessage = messages[messages.length - 1]; + if ( + lastMessage && + (isAtBottom.current || lastMessage.senderId === currentUser?.id) + ) { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + } + }, [messages, currentUser]); const initiateVideoCall = () => { if (userId && currentUser) { @@ -210,7 +223,10 @@ export const ChatPage: React.FC = () => { {/* Messages container */} -
+
{messages.length > 0 ? (
{messages.map((message) => { From 3ba6d3dc62dc95ae2e6f68a2d622faccb8a0dc6e Mon Sep 17 00:00:00 2001 From: m-zeeshan-saeed Date: Sun, 15 Feb 2026 03:53:25 +0500 Subject: [PATCH 10/15] feat: Configure Socket.IO and CORS to use `CLIENT_URL` for origin and enable credentials. --- backend/server.ts | 16 +++------------- src/context/SocketContext.tsx | 10 ++++++---- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/backend/server.ts b/backend/server.ts index e53be223a..19e99c1d8 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -34,18 +34,7 @@ const allowedOrigins = [ app.use( cors({ - origin: (origin, callback) => { - // Allow requests with no origin (like mobile apps or curl requests) - if (!origin) return callback(null, true); - if ( - allowedOrigins.indexOf(origin) !== -1 || - process.env.NODE_ENV !== "production" - ) { - callback(null, true); - } else { - callback(new Error("Not allowed by CORS")); - } - }, + origin: process.env.CLIENT_URL, credentials: true, }), ); @@ -87,8 +76,9 @@ const httpServer = createServer(app); // Initialize Socket.IO const io = new Server(httpServer, { cors: { - origin: "*", // Adjust this for production + origin: process.env.CLIENT_URL, // Adjust this for production methods: ["GET", "POST"], + credentials: true, }, }); diff --git a/src/context/SocketContext.tsx b/src/context/SocketContext.tsx index 20fe616f5..7867c57b2 100644 --- a/src/context/SocketContext.tsx +++ b/src/context/SocketContext.tsx @@ -38,10 +38,12 @@ export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ useEffect(() => { if (user) { - const socketUrl = import.meta.env.VITE_API_URL - ? import.meta.env.VITE_API_URL.replace("/api", "") - : "http://localhost:3001"; - const newSocket = io(socketUrl); + const socketUrl = + import.meta.env.VITE_SOCKET_URL || "http://localhost:3001"; + const newSocket = io(socketUrl, { + transports: ["websocket"], + withCredentials: true, + }); setSocket(newSocket); newSocket.on("get-online-users", (users: string[]) => { From 76358ed17312182151ffecd8f00abde2f4434f4a Mon Sep 17 00:00:00 2001 From: m-zeeshan-saeed Date: Sun, 15 Feb 2026 04:04:38 +0500 Subject: [PATCH 11/15] feat: Configure Axios to use `withCredentials` and adjust the base API URL. --- src/services/api.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/services/api.ts b/src/services/api.ts index d5cc901f4..dcbfcf57c 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -1,17 +1,19 @@ import axios from "axios"; -const API_URL = import.meta.env.VITE_API_URL || "http://localhost:3001/api"; +const API_URL = import.meta.env.VITE_API_URL || "http://localhost:3001"; const api = axios.create({ baseURL: API_URL, + withCredentials: true, }); -// Add a request interceptor to attach the token api.interceptors.request.use((config) => { const token = localStorage.getItem("business_nexus_token"); + if (token) { config.headers.Authorization = `Bearer ${token}`; } + return config; }); From 690c799d2dbc1fa72427320dd1649ae58d6002ef Mon Sep 17 00:00:00 2001 From: m-zeeshan-saeed Date: Sun, 15 Feb 2026 05:17:54 +0500 Subject: [PATCH 12/15] feat: implement new chat modal with user search functionality and emoji picker integration --- backend/routes/user.routes.ts | 34 +- backend/server.ts | 5 - env.development | 19 + package-lock.json | 22 ++ package.json | 1 + src/components/chat/NewChatModal.tsx | 103 +++++ src/pages/chat/ChatPage.tsx | 568 +++++++++++++++++++++------ src/services/userService.ts | 9 + src/types/index.ts | 3 + 9 files changed, 625 insertions(+), 139 deletions(-) create mode 100644 env.development create mode 100644 src/components/chat/NewChatModal.tsx create mode 100644 src/services/userService.ts diff --git a/backend/routes/user.routes.ts b/backend/routes/user.routes.ts index dba9cc3e1..3963812d6 100644 --- a/backend/routes/user.routes.ts +++ b/backend/routes/user.routes.ts @@ -70,26 +70,24 @@ router.get( async (req: AuthRequest, res: Response) => { try { const { query } = req.query; - if (!query || typeof query !== "string") { - return res.status(400).json({ message: "Search query is required" }); + const searchQuery = (query as string) || ""; + + // Build search criteria + const searchCriteria: Record = { + id: { $ne: req.user?.userId }, // Exclude current user + }; + + if (searchQuery.trim()) { + searchCriteria.$or = [ + { id: searchQuery }, + { name: { $regex: searchQuery, $options: "i" } }, + { email: { $regex: searchQuery, $options: "i" } }, + ]; } - // Search for users by direct ID match or case-insensitive name/email match - // Filter out the current user from results - const users = await User.find({ - $and: [ - { id: { $ne: req.user?.userId } }, - { - $or: [ - { id: query }, - { name: { $regex: query, $options: "i" } }, - { email: { $regex: query, $options: "i" } }, - ], - }, - ], - }) - .select("id name email role") - .limit(10); + const users = await User.find(searchCriteria) + .select("id name email role avatarUrl") + .limit(20); res.json(users); } catch (error) { diff --git a/backend/server.ts b/backend/server.ts index 19e99c1d8..d1a43a6d6 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -26,11 +26,6 @@ const port = process.env.PORT || 3001; const MONGO_URL = process.env.MONGODB_URL; // Middleware -const allowedOrigins = [ - "http://localhost:5173", - "http://localhost:3000", - "https://business-nexus.vercel.app", // Example production domain -]; app.use( cors({ diff --git a/env.development b/env.development new file mode 100644 index 000000000..e3838c642 --- /dev/null +++ b/env.development @@ -0,0 +1,19 @@ +# Backend API URL (Localhost) +VITE_API_URL=http://localhost:3001/api + +# WebSocket URL (Localhost) +VITE_SOCKET_URL=http://localhost:3001 + +# Frontend URL for CORS (Localhost) +CLIENT_URL=http://localhost:5173 + +# Database & Secrets (Same as .env, but can be overridden) +MONGODB_URL=mongodb+srv://zeeshansheikh0313_db_user:Coolhero123@cluster0.2mcxgvw.mongodb.net/?appName=Cluster0 +JWT_SECRET=088bd22dc60db737b89b0b09608c40c4b631775ddf571ac3b0ceabf5794463a8 +PORT=3001 + +# SMTP Configuration +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_USER=coolhero444@gmail.com +SMTP_PASS=jfbm uawz wcya hkak diff --git a/package-lock.json b/package-lock.json index 1d8636b01..993a1fefa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "cors": "^2.8.6", "date-fns": "^3.3.1", "dotenv": "^17.2.3", + "emoji-picker-react": "^4.18.0", "express": "^5.2.1", "express-validator": "^7.3.1", "jsonwebtoken": "^9.0.3", @@ -2581,6 +2582,21 @@ "integrity": "sha512-+cYTcFB1QqD4j4LegwLfpCNxifb6dDFUAwk6RsLusCwIaZI6or2f+q8rs5tTB2YC53HhOlIbEaqHMAAC8IOIwA==", "dev": true }, + "node_modules/emoji-picker-react": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/emoji-picker-react/-/emoji-picker-react-4.18.0.tgz", + "integrity": "sha512-vLTrLfApXAIciguGE57pXPWs9lPLBspbEpPMiUq03TIli2dHZBiB+aZ0R9/Wat0xmTfcd4AuEzQgSYxEZ8C88Q==", + "license": "MIT", + "dependencies": { + "flairup": "1.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16" + } + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -3235,6 +3251,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/flairup": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/flairup/-/flairup-1.0.0.tgz", + "integrity": "sha512-IKlE+pNvL2R+kVL1kEhUYqRxVqeFnjiIvHWDMLFXNaqyUdFXQM2wte44EfMYJNHkW16X991t2Zg8apKkhv7OBA==", + "license": "MIT" + }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", diff --git a/package.json b/package.json index 8ffc4a956..9fb091c06 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "cors": "^2.8.6", "date-fns": "^3.3.1", "dotenv": "^17.2.3", + "emoji-picker-react": "^4.18.0", "express": "^5.2.1", "express-validator": "^7.3.1", "jsonwebtoken": "^9.0.3", diff --git a/src/components/chat/NewChatModal.tsx b/src/components/chat/NewChatModal.tsx new file mode 100644 index 000000000..26024bae6 --- /dev/null +++ b/src/components/chat/NewChatModal.tsx @@ -0,0 +1,103 @@ +import React, { useState, useEffect } from "react"; +import { X, Search, User as UserIcon } from "lucide-react"; +import { userService } from "../../services/userService"; +import { User } from "../../types"; +import { Avatar } from "../ui/Avatar"; + +interface NewChatModalProps { + onClose: () => void; + onSelectUser: (user: User) => void; +} + +export const NewChatModal: React.FC = ({ + onClose, + onSelectUser, +}) => { + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(true); + const [searchQuery, setSearchQuery] = useState(""); + + useEffect(() => { + const fetchUsers = async () => { + setLoading(true); + try { + const data = await userService.searchUsers(searchQuery); + setUsers(data); + } catch (error) { + console.error("Failed to fetch users:", error); + } finally { + setLoading(false); + } + }; + + const timeoutId = setTimeout(fetchUsers, 300); // Debounce + return () => clearTimeout(timeoutId); + }, [searchQuery]); + + return ( +
+
+ {/* Header */} +
+

+ New Chat +

+ +
+ + {/* Search */} +
+
+ + setSearchQuery(e.target.value)} + autoFocus + /> +
+
+ + {/* User List */} +
+ {loading ? ( +
+
+
+ ) : users.length > 0 ? ( +
+ {users.map((user) => ( +
onSelectUser(user)} + className="flex items-center p-3 hover:bg-gray-50 rounded-lg cursor-pointer transition-colors" + > + +
+

+ {user.name} +

+

+ {user.role} +

+
+
+ ))} +
+ ) : ( +
+

No users found.

+
+ )} +
+
+
+ ); +}; diff --git a/src/pages/chat/ChatPage.tsx b/src/pages/chat/ChatPage.tsx index 2e4832895..5b783b76f 100644 --- a/src/pages/chat/ChatPage.tsx +++ b/src/pages/chat/ChatPage.tsx @@ -1,42 +1,87 @@ import React, { useState, useEffect, useRef } from "react"; -import { useParams } from "react-router-dom"; -import { Send, Phone, Video, Info, Smile } from "lucide-react"; +import { useParams, useNavigate } from "react-router-dom"; +import { + Send, + Phone, + Video, + Smile, + Search, + Mic, + X, + Paperclip, + CheckCheck, +} from "lucide-react"; +import { NewChatModal } from "../../components/chat/NewChatModal"; +import EmojiPicker, { EmojiClickData, Theme } from "emoji-picker-react"; import { Avatar } from "../../components/ui/Avatar"; import { Button } from "../../components/ui/Button"; -import { Input } from "../../components/ui/Input"; -import { ChatMessage } from "../../components/chat/ChatMessage"; -import { ChatUserList } from "../../components/chat/ChatUserList"; import { useAuth } from "../../context/AuthContext"; import { Message, ChatConversation, User } from "../../types"; import { messageService } from "../../services/messageService"; import api from "../../services/api"; import { MessageCircle } from "lucide-react"; import { VideoCallModal } from "../../components/chat/VideoCallModal"; -import { formatDistanceToNow } from "date-fns"; +import { formatDistanceToNow, format } from "date-fns"; import { useSocket } from "../../context/SocketContext"; export const ChatPage: React.FC = () => { const { userId } = useParams<{ userId: string }>(); + const navigate = useNavigate(); const { user: currentUser } = useAuth(); const [messages, setMessages] = useState([]); const [newMessage, setNewMessage] = useState(""); const [conversations, setConversations] = useState([]); const [chatPartner, setChatPartner] = useState(null); + console.log( + "ChatPage Render. Current User:", + currentUser?.id, + "UserId Param:", + userId, + ); const [showVideoCall, setShowVideoCall] = useState(false); const [incomingCall, setIncomingCall] = useState<{ roomId: string; fromName: string; } | null>(null); const [callType, setCallType] = useState<"video" | "voice">("video"); + const [showEmojiPicker, setShowEmojiPicker] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + const [isRecording, setIsRecording] = useState(false); + const [showNewChat, setShowNewChat] = useState(false); + const [showChatSearch, setShowChatSearch] = useState(false); + const [chatSearchQuery, setChatSearchQuery] = useState(""); + const fileInputRef = useRef(null); + const mediaRecorderRef = useRef(null); + const audioChunksRef = useRef([]); + const messagesEndRef = useRef(null); const isAtBottom = useRef(true); + const emojiPickerRef = useRef(null); const { socket, userStatuses } = useSocket(); + // Close emoji picker when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + emojiPickerRef.current && + !emojiPickerRef.current.contains(event.target as Node) + ) { + setShowEmojiPicker(false); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, []); + useEffect(() => { const fetchConversations = async () => { if (currentUser) { try { const data = await messageService.getConversations(); + console.log("Fetched conversations:", data); setConversations(data); } catch (error) { console.error("Failed to fetch conversations:", error); @@ -53,6 +98,7 @@ export const ChatPage: React.FC = () => { if (currentUser && userId) { try { const data = await messageService.getMessages(userId); + console.log("Fetched messages:", data); setMessages(data); } catch (error) { console.error("Failed to fetch messages:", error); @@ -77,6 +123,7 @@ export const ChatPage: React.FC = () => { const interval = setInterval(fetchMessages, 3000); // Poll for new messages return () => clearInterval(interval); }, [currentUser, userId]); + useEffect(() => { if (socket && currentUser) { socket.on( @@ -94,6 +141,7 @@ export const ChatPage: React.FC = () => { }; } }, [socket, currentUser, userId, chatPartner?.name]); + const handleScroll = (e: React.UIEvent) => { const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; const isBottom = scrollHeight - scrollTop - clientHeight < 50; @@ -101,12 +149,10 @@ export const ChatPage: React.FC = () => { }; useEffect(() => { - // Scroll to bottom only if user is already at bottom or if it's a new message from current user + // Scroll to bottom ONLY if it's a new message sent by the current user + // This prevents auto-scrolling on initial load or when reading history const lastMessage = messages[messages.length - 1]; - if ( - lastMessage && - (isAtBottom.current || lastMessage.senderId === currentUser?.id) - ) { + if (lastMessage && lastMessage.senderId === currentUser?.id) { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); } }, [messages, currentUser]); @@ -133,6 +179,67 @@ export const ChatPage: React.FC = () => { } }; + const onEmojiClick = (emojiData: EmojiClickData) => { + setNewMessage((prev) => prev + emojiData.emoji); + }; + + const handleFileUpload = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + setNewMessage((prev) => + prev ? `${prev}\n[File: ${file.name}]` : `[File: ${file.name}]`, + ); + } + }; + + const handleMicClick = async () => { + if (isRecording) { + // Stop Recording + if ( + mediaRecorderRef.current && + mediaRecorderRef.current.state === "recording" + ) { + mediaRecorderRef.current.stop(); + } + setIsRecording(false); + } else { + // Start Recording + try { + const stream = await navigator.mediaDevices.getUserMedia({ + audio: true, + }); + const mediaRecorder = new MediaRecorder(stream); + mediaRecorderRef.current = mediaRecorder; + audioChunksRef.current = []; + + mediaRecorder.ondataavailable = (event) => { + if (event.data.size > 0) { + audioChunksRef.current.push(event.data); + } + }; + + mediaRecorder.onstop = () => { + // Create blob (unused for now until backend supports upload, but logic is ready) + // const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/webm' }); + setNewMessage((prev) => + prev ? `${prev}\n[Voice Message]` : `[Voice Message]`, + ); + + // Stop all tracks + stream.getTracks().forEach((track) => track.stop()); + }; + + mediaRecorder.start(); + setIsRecording(true); + } catch (error) { + console.error("Error accessing microphone:", error); + alert( + "Could not access microphone. Please ensure permissions are granted.", + ); + } + } + }; + const handleSendMessage = async (e: React.FormEvent) => { e.preventDefault(); @@ -142,6 +249,7 @@ export const ChatPage: React.FC = () => { const message = await messageService.sendMessage(userId, newMessage); setMessages([...messages, message]); setNewMessage(""); + setShowEmojiPicker(false); // Refresh conversations const data = await messageService.getConversations(); @@ -153,156 +261,369 @@ export const ChatPage: React.FC = () => { if (!currentUser) return null; + const filteredConversations = conversations.filter((c) => + c.partner?.name.toLowerCase().includes(searchQuery.toLowerCase()), + ); + return ( -
- {/* Conversations sidebar */} -
- +
+ {/* Sidebar - WhatsApp Style */} +
+ {/* Sidebar Header */} +
+ +
+ + +
+
+ + {/* Search Bar */} +
+
+ + setSearchQuery(e.target.value)} + /> +
+
+ + {/* Conversation List */} +
+ {conversations.length > 0 ? ( +
+ {filteredConversations.map((conversation) => ( +
+ conversation.partner?.id && + navigate(`/chat/${conversation.partner.id}`) + } + className={`flex items-center p-3 cursor-pointer hover:bg-gray-50 transition-colors border-b border-gray-100 ${ + userId === conversation.partner?.id + ? "bg-primary-50 border-l-4 border-primary-600" + : "border-l-4 border-transparent" + }`} + > + +
+
+

+ {conversation.partner?.name || "Unknown User"} +

+ {conversation.updatedAt && + !isNaN(new Date(conversation.updatedAt).getTime()) && ( + + {format(new Date(conversation.updatedAt), "HH:mm")} + + )} +
+
+

+ {conversation.lastMessage?.content} +

+
+
+
+ ))} +
+ ) : ( +
+

No conversations yet.

+

+ Search for users to start chatting. +

+
+ )} +
- {/* Main chat area */} -
- {/* Chat header */} + {/* Chat Area */} +
{chatPartner ? ( <> -
-
+ {/* Chat Header - Customize for Project Look */} +
+
- -
-

+
+ {chatPartner.name} -

-

+ + + {userStatuses[chatPartner.id]?.status === "online" && ( + + )} {userStatuses[chatPartner.id]?.status === "online" ? "Online" : userStatuses[chatPartner.id]?.status === "recently_active" - ? `Recently active (${formatDistanceToNow(new Date(userStatuses[chatPartner.id].lastSeen), { addSuffix: true })})` - : `Active ${formatDistanceToNow(new Date(userStatuses[chatPartner.id]?.lastSeen || chatPartner.createdAt), { addSuffix: true })}`} -

+ ? `Last seen ${ + userStatuses[chatPartner.id].lastSeen && + !isNaN( + new Date( + userStatuses[chatPartner.id].lastSeen, + ).getTime(), + ) + ? formatDistanceToNow( + new Date( + userStatuses[chatPartner.id].lastSeen, + ), + { addSuffix: true }, + ) + : "" + }` + : "Offline"} +
-
+
- - - + +
+ +
+ {showChatSearch ? ( +
+ + setChatSearchQuery(e.target.value)} + autoFocus + onBlur={() => + !chatSearchQuery && setShowChatSearch(false) + } + /> + +
+ ) : ( + + )} +
- {/* Messages container */} + {/* Messages Background - Clean, no pattern for modern look */} +
+ + {/* Messages Container */}
- {messages.length > 0 ? ( -
- {messages.map((message) => { - const isCurrentUser = message.senderId === currentUser.id; - const sender = isCurrentUser ? currentUser : chatPartner; - return ( - - ); - })} -
-
- ) : ( -
-
- -
-

- No messages yet -

-

- Send a message to start the conversation -

-
- )} +
+ + Messages are end-to-end encrypted. No one outside of this + chat, not even Business Nexus, can read or listen to them. + +
+ + {messages + .filter((msg) => + msg.content + .toLowerCase() + .includes(chatSearchQuery.toLowerCase()), + ) + .map((message) => { + const isCurrentUser = message.senderId === currentUser.id; + return ( +
+
+ {/* Removed triangles for cleaner modern UI */} + +
+ {message.content} +
+ +
+ + {message.createdAt && + !isNaN(new Date(message.createdAt).getTime()) + ? format(new Date(message.createdAt), "HH:mm") + : ""} + + {isCurrentUser && ( + + )} +
+
+
+ ); + })} +
- {/* Message input */} -
-
- + +
+ )} - setNewMessage(e.target.value)} - fullWidth - className="flex-1" - /> +
+ + +
- - + setNewMessage(e.target.value)} + /> + +
+ +
) : ( -
-
- + /* Empty State */ +
+
+ {/* Illustration Placeholder - could use an SVG or Image */} +
+ +
-

- Select a conversation +

+ Business Nexus Web

-

- Choose a contact from the list to start chatting +

+ Send and receive messages without keeping your phone online. +
+ Use Business Nexus on up to 4 linked devices and 1 phone.

+
+ + End-to-end encrypted +
)}
@@ -327,19 +648,34 @@ export const ChatPage: React.FC = () => {

Video calling you...

-
)} + + {showNewChat && ( + setShowNewChat(false)} + onSelectUser={(user) => { + setShowNewChat(false); + navigate(`/chat/${user.id}`); + }} + /> + )}
); }; diff --git a/src/services/userService.ts b/src/services/userService.ts new file mode 100644 index 000000000..20529f46c --- /dev/null +++ b/src/services/userService.ts @@ -0,0 +1,9 @@ +import api from "./api"; +import { User } from "../types"; + +export const userService = { + searchUsers: async (query: string = "") => { + const response = await api.get(`/users/search?query=${query}`); + return response.data; + }, +}; diff --git a/src/types/index.ts b/src/types/index.ts index 85ceca8d3..f085386ae 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -58,6 +58,7 @@ export interface Message { receiverId: string; content: string; timestamp: string; + createdAt: string; isRead: boolean; } @@ -71,6 +72,8 @@ export interface ChatConversation { avatarUrl: string; isOnline: boolean; }; + lastMessageDate?: string; + unreadCount: number; updatedAt: string; } From 14d7c352316928d3364c501b12d3aa8bdc4b0815 Mon Sep 17 00:00:00 2001 From: Zeeshan Saeed <84595628+m-zeeshan-saeed@users.noreply.github.com> Date: Sun, 15 Feb 2026 05:30:58 +0500 Subject: [PATCH 13/15] Create SECURITY.md for security policy Added a security policy document outlining supported versions and vulnerability reporting. --- SECURITY.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..034e84803 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,21 @@ +# Security Policy + +## Supported Versions + +Use this section to tell people about which versions of your project are +currently being supported with security updates. + +| Version | Supported | +| ------- | ------------------ | +| 5.1.x | :white_check_mark: | +| 5.0.x | :x: | +| 4.0.x | :white_check_mark: | +| < 4.0 | :x: | + +## Reporting a Vulnerability + +Use this section to tell people how to report a vulnerability. + +Tell them where to go, how often they can expect to get an update on a +reported vulnerability, what to expect if the vulnerability is accepted or +declined, etc. From 8709595dcf3bfd9788dff90be9c23982d5815338 Mon Sep 17 00:00:00 2001 From: Zeeshan Saeed <84595628+m-zeeshan-saeed@users.noreply.github.com> Date: Sun, 15 Feb 2026 05:32:07 +0500 Subject: [PATCH 14/15] Clean up env.development by removing sensitive data Removed sensitive information and updated environment variables. --- env.development | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/env.development b/env.development index e3838c642..b8f0cfb1d 100644 --- a/env.development +++ b/env.development @@ -1,19 +1,13 @@ # Backend API URL (Localhost) -VITE_API_URL=http://localhost:3001/api + # WebSocket URL (Localhost) -VITE_SOCKET_URL=http://localhost:3001 + # Frontend URL for CORS (Localhost) -CLIENT_URL=http://localhost:5173 + # Database & Secrets (Same as .env, but can be overridden) -MONGODB_URL=mongodb+srv://zeeshansheikh0313_db_user:Coolhero123@cluster0.2mcxgvw.mongodb.net/?appName=Cluster0 -JWT_SECRET=088bd22dc60db737b89b0b09608c40c4b631775ddf571ac3b0ceabf5794463a8 -PORT=3001 + # SMTP Configuration -SMTP_HOST=smtp.gmail.com -SMTP_PORT=587 -SMTP_USER=coolhero444@gmail.com -SMTP_PASS=jfbm uawz wcya hkak From c2a6f91892a67807ab7f34a0edee0205cdbb796a Mon Sep 17 00:00:00 2001 From: m-zeeshan-saeed Date: Sun, 15 Feb 2026 05:38:25 +0500 Subject: [PATCH 15/15] feat: Remove development environment file and enhance chat conversation list UI with improved styling, online status, and unread message indicators. --- env.development | 13 -------- src/pages/chat/ChatPage.tsx | 62 ++++++++++++++++++++++++++----------- 2 files changed, 44 insertions(+), 31 deletions(-) delete mode 100644 env.development diff --git a/env.development b/env.development deleted file mode 100644 index b8f0cfb1d..000000000 --- a/env.development +++ /dev/null @@ -1,13 +0,0 @@ -# Backend API URL (Localhost) - - -# WebSocket URL (Localhost) - - -# Frontend URL for CORS (Localhost) - - -# Database & Secrets (Same as .env, but can be overridden) - - -# SMTP Configuration diff --git a/src/pages/chat/ChatPage.tsx b/src/pages/chat/ChatPage.tsx index 5b783b76f..ad6ba5372 100644 --- a/src/pages/chat/ChatPage.tsx +++ b/src/pages/chat/ChatPage.tsx @@ -312,38 +312,64 @@ export const ChatPage: React.FC = () => { conversation.partner?.id && navigate(`/chat/${conversation.partner.id}`) } - className={`flex items-center p-3 cursor-pointer hover:bg-gray-50 transition-colors border-b border-gray-100 ${ + className={`flex flex-row items-center p-4 cursor-pointer transition-all duration-200 border-b border-gray-100 ${ userId === conversation.partner?.id - ? "bg-primary-50 border-l-4 border-primary-600" - : "border-l-4 border-transparent" + ? "bg-blue-50/60 border-l-4 border-l-primary-600 shadow-sm" + : "border-l-4 border-l-transparent hover:bg-gray-50" }`} > - -
+
+ + {/* Online Status Indicator */} + {userStatuses[conversation.partner?.id || ""]?.status === + "online" && ( + + )} +
+ +
-

+

{conversation.partner?.name || "Unknown User"}

{conversation.updatedAt && !isNaN(new Date(conversation.updatedAt).getTime()) && ( - + {format(new Date(conversation.updatedAt), "HH:mm")} )}
-

- {conversation.lastMessage?.content} +

0 + ? "font-semibold text-gray-800" + : "text-gray-500" + }`} + > + {conversation.lastMessage?.content || "No messages yet"}

+ {conversation.unreadCount > 0 && ( + + {conversation.unreadCount} + + )}