From bff3890043babc9b1b88bb4a02a1d06b0fa8a104 Mon Sep 17 00:00:00 2001 From: Danish-Butt Date: Wed, 20 Aug 2025 21:20:16 +0500 Subject: [PATCH 01/43] Auth routes created to request the backed --- package-lock.json | 10 + package.json | 11 +- src/App.tsx | 4 + src/context/AuthContext.tsx | 216 +++++++++++------- src/pages/auth/ForgotPasswordPage.tsx | 2 +- src/pages/auth/LoginPage.tsx | 2 +- src/pages/dashboard/EntrepreneurDashboard.tsx | 1 + 7 files changed, 157 insertions(+), 89 deletions(-) diff --git a/package-lock.json b/package-lock.json index bf00121ba..2f89c6b59 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "axios": "^1.6.7", "date-fns": "^3.3.1", + "jwt-decode": "^4.0.0", "lucide-react": "^0.344.0", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -2977,6 +2978,15 @@ "node": ">=6" } }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", diff --git a/package.json b/package.json index cdfd37b29..e9bf404e9 100644 --- a/package.json +++ b/package.json @@ -10,14 +10,15 @@ "preview": "vite preview" }, "dependencies": { + "axios": "^1.6.7", + "date-fns": "^3.3.1", + "jwt-decode": "^4.0.0", "lucide-react": "^0.344.0", "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", @@ -35,4 +36,4 @@ "typescript-eslint": "^8.3.0", "vite": "^5.4.2" } -} \ No newline at end of file +} diff --git a/src/App.tsx b/src/App.tsx index 51b12d8bf..053c442ed 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -29,6 +29,8 @@ import { DealsPage } from './pages/deals/DealsPage'; // Chat Pages import { ChatPage } from './pages/chat/ChatPage'; +import { ForgotPasswordPage } from './pages/auth/ForgotPasswordPage'; +import { ResetPasswordPage } from './pages/auth/ResetPasswordPage'; function App() { return ( @@ -38,6 +40,8 @@ function App() { {/* Authentication Routes */} } /> } /> + } /> + } /> {/* Dashboard Routes */} }> diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index 5d07bcf0a..f4c4df0f1 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -1,47 +1,78 @@ -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 { users } from "../data/users.js"; +import toast from "react-hot-toast"; +import axios from "axios"; +import { jwtDecode } from "jwt-decode"; // 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 RESET_TOKEN_KEY = "business_nexus_reset_token"; +const URL = "http://localhost:5000"; +interface TokenPayload { + userId: string; + email: string; + name: string; + exp: number; + iat: number; +} // 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 useEffect(() => { - const storedUser = localStorage.getItem(USER_STORAGE_KEY); - if (storedUser) { - setUser(JSON.parse(storedUser)); + const token = localStorage.getItem("token"); + if (token) { + try { + const decoded = jwtDecode(token); + // check expiry + if (decoded.exp * 1000 > Date.now()) { + setUser({ id: decoded.userId, email: decoded.email, name: decoded.name }); + } else { + localStorage.removeItem("token"); + } + } catch (err) { + console.log("Invalid token", err); + localStorage.removeItem("token"); + } } setIsLoading(false); }, []); // Mock login function - in a real app, this would make an API call - const login = async (email: string, password: string, role: UserRole): Promise => { + const login = async ( + email: string, + password: string, + role: UserRole + ): Promise => { setIsLoading(true); - + 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); + const res = await axios.post( + `${URL}/auth/login`, + { + email, + password, + role, + }, + { + withCredentials: true, + } + ); + const { token, user } = res.data; + + localStorage.setItem("token", token); + setUser(user); + toast.success("Successfully logged in!"); - 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'); - } } catch (error) { toast.error((error as Error).message); throw error; @@ -51,36 +82,31 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }; // 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 => { + const register = async ( + name: string, + email: string, + password: string, + role: UserRole + ): Promise => { setIsLoading(true); - + 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'); + const res = await axios.post( + `${URL}/auth/register`, + { + name, + email, + password, + role, + }, + { + withCredentials: true, + } + ); + if (res.status === 201) { + toast.success("Account created successfully!"); } - - // 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() - }; - - // 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; @@ -92,21 +118,37 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children // Mock 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'); - } - // 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'); + const resetLink = `http://localhost:5173/reset-password?token=${resetToken}`; + const message = ` +

To reset your password, please click the button below:

+ + Reset Password + +

If the button doesn’t work, copy and paste this link into your browser:

+

${resetLink}

+ `; + + const sub = "Password reset instructions"; + const res = await axios.post( + `${URL}/auth/send-mail`, + { email, message, sub }, + { + withCredentials: true, + } + ); + const {user} = res.data; + localStorage.setItem("user",JSON.stringify(user)); + console.log(user); + toast.success("Password reset instructions sent to your email"); } catch (error) { toast.error((error as Error).message); throw error; @@ -114,20 +156,27 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }; // Mock reset password function - const resetPassword = async (token: string, newPassword: string): Promise => { + 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'); + throw new Error("Invalid or expired reset token"); } + const user = JSON.parse(localStorage.getItem("user")); + await axios.patch( + `${URL}/auth/update-password/${user._id}`, + { newPassword}, + { withCredentials: true } + ); + // In a real app, this would update the user's password in the database localStorage.removeItem(RESET_TOKEN_KEY); - toast.success('Password reset successfully'); + localStorage.removeItem("user"); + toast.success("Password reset successfully"); } catch (error) { toast.error((error as Error).message); throw error; @@ -137,32 +186,35 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children // Logout function const logout = (): void => { setUser(null); - localStorage.removeItem(USER_STORAGE_KEY); - toast.success('Logged out successfully'); + localStorage.removeItem("token"); + 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)); - + await new Promise((resolve) => setTimeout(resolve, 1000)); + // Update user in mock data - const userIndex = users.findIndex(u => u.id === userId); + const userIndex = users.findIndex((u) => u.id === userId); if (userIndex === -1) { - throw new Error('User not found'); + 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'); + + toast.success("Profile updated successfully"); } catch (error) { toast.error((error as Error).message); throw error; @@ -178,7 +230,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children resetPassword, updateProfile, isAuthenticated: !!user, - isLoading + isLoading, }; return {children}; @@ -188,7 +240,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children 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..5949bcacf 100644 --- a/src/pages/auth/ForgotPasswordPage.tsx +++ b/src/pages/auth/ForgotPasswordPage.tsx @@ -20,7 +20,7 @@ export const ForgotPasswordPage: React.FC = () => { await forgotPassword(email); setIsSubmitted(true); } catch (error) { - // Error is handled by the AuthContext + console.log(error); } finally { setIsLoading(false); } diff --git a/src/pages/auth/LoginPage.tsx b/src/pages/auth/LoginPage.tsx index bbd5b08a0..e95bff70a 100644 --- a/src/pages/auth/LoginPage.tsx +++ b/src/pages/auth/LoginPage.tsx @@ -138,7 +138,7 @@ export const LoginPage: React.FC = () => { diff --git a/src/pages/dashboard/EntrepreneurDashboard.tsx b/src/pages/dashboard/EntrepreneurDashboard.tsx index 6c1dd3b17..54b8cebd3 100644 --- a/src/pages/dashboard/EntrepreneurDashboard.tsx +++ b/src/pages/dashboard/EntrepreneurDashboard.tsx @@ -13,6 +13,7 @@ import { investors } from '../../data/users'; export const EntrepreneurDashboard: React.FC = () => { const { user } = useAuth(); + console.log(user); const [collaborationRequests, setCollaborationRequests] = useState([]); const [recommendedInvestors, setRecommendedInvestors] = useState(investors.slice(0, 3)); From a1ddd6e79b617016710c58de9dfc5329608920a5 Mon Sep 17 00:00:00 2001 From: Danish-Butt Date: Fri, 22 Aug 2025 19:57:15 +0500 Subject: [PATCH 02/43] moved to EnterPre page --- src/App.tsx | 1 - src/components/chat/ChatMessage.tsx | 1 - src/components/chat/ChatUserList.tsx | 1 - .../CollaborationRequestCard.tsx | 1 - src/components/layout/DashboardLayout.tsx | 61 +++---- src/components/ui/Input.tsx | 2 + src/context/AuthContext.tsx | 102 ++++++----- src/data/users.ts | 160 +++------------- src/pages/chat/ChatPage.tsx | 1 - src/pages/dashboard/EntrepreneurDashboard.tsx | 172 +++++++++++------- src/pages/dashboard/InvestorDashboard.tsx | 55 +++--- src/pages/entrepreneurs/EntrepreneursPage.tsx | 1 - src/pages/investors/InvestorsPage.tsx | 1 - src/pages/profile/EntrepreneurProfile.tsx | 1 - src/pages/profile/InvestorProfile.tsx | 2 +- src/pages/settings/SettingsPage.tsx | 172 +++++++++++------- src/types/index.ts | 3 +- 17 files changed, 358 insertions(+), 379 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 053c442ed..20074cc4b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; import { AuthProvider } from './context/AuthContext'; diff --git a/src/components/chat/ChatMessage.tsx b/src/components/chat/ChatMessage.tsx index c3cf93ef4..d24ae5ba6 100644 --- a/src/components/chat/ChatMessage.tsx +++ b/src/components/chat/ChatMessage.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { formatDistanceToNow } from 'date-fns'; import { Message } from '../../types'; import { Avatar } from '../ui/Avatar'; -import { findUserById } from '../../data/users'; interface ChatMessageProps { message: Message; diff --git a/src/components/chat/ChatUserList.tsx b/src/components/chat/ChatUserList.tsx index 62a295101..6f1fb52f6 100644 --- a/src/components/chat/ChatUserList.tsx +++ b/src/components/chat/ChatUserList.tsx @@ -4,7 +4,6 @@ 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'; interface ChatUserListProps { diff --git a/src/components/collaboration/CollaborationRequestCard.tsx b/src/components/collaboration/CollaborationRequestCard.tsx index 192897711..7a47eca70 100644 --- a/src/components/collaboration/CollaborationRequestCard.tsx +++ b/src/components/collaboration/CollaborationRequestCard.tsx @@ -6,7 +6,6 @@ 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'; diff --git a/src/components/layout/DashboardLayout.tsx b/src/components/layout/DashboardLayout.tsx index f99043426..be574357b 100644 --- a/src/components/layout/DashboardLayout.tsx +++ b/src/components/layout/DashboardLayout.tsx @@ -1,37 +1,36 @@ -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 ( +
+
+
+ ); + } + if (!isAuthenticated) { + return ; + } + - if (isLoading) { return ( -
-
+
+ + +
+ + +
+
+ +
+
+
); - } - - if (!isAuthenticated) { - return ; - } - - return ( -
- - -
- - -
-
- -
-
-
-
- ); -}; \ No newline at end of file +}; diff --git a/src/components/ui/Input.tsx b/src/components/ui/Input.tsx index 2e16a6973..9646e51d0 100644 --- a/src/components/ui/Input.tsx +++ b/src/components/ui/Input.tsx @@ -17,6 +17,7 @@ export const Input = forwardRef(({ endAdornment, fullWidth = false, className = '', + onChange, ...props }, ref) => { @@ -44,6 +45,7 @@ export const Input = forwardRef(({ diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index f4c4df0f1..a32e2c194 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -1,9 +1,7 @@ import React, { createContext, useState, useContext, useEffect } from "react"; import { User, UserRole, AuthContextType } from "../types"; -import { users } from "../data/users.js"; import toast from "react-hot-toast"; import axios from "axios"; -import { jwtDecode } from "jwt-decode"; // Create Auth Context const AuthContext = createContext(undefined); @@ -12,38 +10,36 @@ const AuthContext = createContext(undefined); const USER_STORAGE_KEY = "business_nexus_user"; const RESET_TOKEN_KEY = "business_nexus_reset_token"; const URL = "http://localhost:5000"; -interface TokenPayload { - userId: string; - email: string; - name: string; - exp: number; - iat: number; -} -// Auth Provider Component export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children, }) => { const [user, setUser] = useState(null); + const [userData, setUserData] = useState(null); const [isLoading, setIsLoading] = useState(true); // Check for stored user on initial load useEffect(() => { + setIsLoading(true); const token = localStorage.getItem("token"); if (token) { - try { - const decoded = jwtDecode(token); - // check expiry - if (decoded.exp * 1000 > Date.now()) { - setUser({ id: decoded.userId, email: decoded.email, name: decoded.name }); - } else { - localStorage.removeItem("token"); + axios + .get("http://localhost:5000/auth/verify", { + headers: { Authorization: `Bearer ${token}` }, + }) + .then((res) => { + const {user} = res.data; + if (! (user.exp * 1000 > Date.now())) { + localStorage.removeItem("token"); } - } catch (err) { - console.log("Invalid token", err); - localStorage.removeItem("token"); - } + }) + .catch(() => { + localStorage.removeItem("token"); + setUser(null); + }) + .finally(() => setIsLoading(false)); + } else { + setIsLoading(false); } - setIsLoading(false); }, []); // Mock login function - in a real app, this would make an API call @@ -71,6 +67,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ localStorage.setItem("token", token); setUser(user); + setUserData(user); toast.success("Successfully logged in!"); } catch (error) { @@ -106,6 +103,10 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ ); if (res.status === 201) { toast.success("Account created successfully!"); + const { token, user } = res.data; + localStorage.setItem("token", token); + setUserData(user); + setUser(user); } } catch (error) { toast.error((error as Error).message); @@ -116,7 +117,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ }; // Mock forgot password function - const forgotPassword = async (email: string): Promise => { + const forgotPassword = async (email: string,role:string): Promise => { try { // Generate reset token (in a real app, this would be a secure token) const resetToken = Math.random().toString(36).substring(2, 15); @@ -140,14 +141,13 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ const sub = "Password reset instructions"; const res = await axios.post( `${URL}/auth/send-mail`, - { email, message, sub }, + { email, message, sub,role }, { withCredentials: true, } ); const {user} = res.data; localStorage.setItem("user",JSON.stringify(user)); - console.log(user); toast.success("Password reset instructions sent to your email"); } catch (error) { toast.error((error as Error).message); @@ -169,7 +169,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ await axios.patch( `${URL}/auth/update-password/${user._id}`, - { newPassword}, + { newPassword,role:user.role}, { withCredentials: true } ); @@ -186,6 +186,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ // Logout function const logout = (): void => { setUser(null); + setUserData(null); localStorage.removeItem("token"); toast.success("Logged out successfully"); }; @@ -193,36 +194,37 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ // Update user profile const updateProfile = async ( userId: string, - updates: Partial + userData: User ): 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"); + if(userData.location==="" && userData.bio==="" && userData.avatarUrl === ""){ + console.log(userData); + alert("Make changes to update profile.."); + return; } - - 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 formData = new FormData(); + formData.append("name", userData.name); + formData.append("location", userData.location); + formData.append("email", userData.email); + formData.append("bio", userData.bio); + formData.append("avatarUrl", userData.avatarUrl); + console.log(formData); + await axios + .post(`${URL}/user/update-profile/${userId}`, formData, { + withCredentials: true, + }) + .then((res) => { + toast.success("profile updated successfully."); + const {user} = res.data; + setUserData(user); + }) + .catch((err) => { + console.log(err); + }); }; const value = { user, + userData, login, register, logout, diff --git a/src/data/users.ts b/src/data/users.ts index b46cc84cf..58af7a58c 100644 --- a/src/data/users.ts +++ b/src/data/users.ts @@ -1,136 +1,30 @@ -import { Entrepreneur, Investor } from '../types'; +import axios from "axios"; +const URL = "http://localhost:5000"; -export const entrepreneurs: Entrepreneur[] = [ - { - id: 'e1', - name: 'Sarah Johnson', - email: 'sarah@techwave.io', - role: 'entrepreneur', - avatarUrl: 'https://images.pexels.com/photos/774909/pexels-photo-774909.jpeg', - bio: 'Serial entrepreneur with 10+ years of experience in SaaS and fintech.', - startupName: 'TechWave AI', - pitchSummary: 'AI-powered financial analytics platform helping SMBs make data-driven decisions.', - fundingNeeded: '$1.5M', - industry: 'FinTech', - location: 'San Francisco, CA', - foundedYear: 2021, - teamSize: 12, - isOnline: true, - createdAt: '2023-01-15T09:24:00Z' - }, - { - id: 'e2', - name: 'David Chen', - email: 'david@greenlife.co', - role: 'entrepreneur', - avatarUrl: 'https://images.pexels.com/photos/614810/pexels-photo-614810.jpeg', - bio: 'Environmental scientist turned entrepreneur. Passionate about sustainable solutions.', - startupName: 'GreenLife Solutions', - pitchSummary: 'Biodegradable packaging alternatives for consumer goods and food industry.', - fundingNeeded: '$2M', - industry: 'CleanTech', - location: 'Portland, OR', - foundedYear: 2020, - teamSize: 8, - isOnline: false, - createdAt: '2022-03-10T14:35:00Z' - }, - { - id: 'e3', - name: 'Maya Patel', - email: 'maya@healthpulse.com', - role: 'entrepreneur', - avatarUrl: 'https://images.pexels.com/photos/415829/pexels-photo-415829.jpeg', - bio: 'Former healthcare professional with an MBA. Building tech to improve patient care.', - startupName: 'HealthPulse', - pitchSummary: 'Mobile platform connecting patients with mental health professionals in real-time.', - fundingNeeded: '$800K', - industry: 'HealthTech', - location: 'Boston, MA', - foundedYear: 2022, - teamSize: 5, - isOnline: true, - createdAt: '2022-07-22T11:42:00Z' - }, - { - id: 'e4', - name: 'James Wilson', - email: 'james@urbanfarm.io', - role: 'entrepreneur', - avatarUrl: 'https://images.pexels.com/photos/220453/pexels-photo-220453.jpeg', - bio: 'Agricultural engineer focused on urban farming solutions and food security.', - startupName: 'UrbanFarm', - pitchSummary: 'IoT-enabled vertical farming systems for urban environments and food deserts.', - fundingNeeded: '$3M', - industry: 'AgTech', - location: 'Chicago, IL', - foundedYear: 2019, - teamSize: 14, - isOnline: false, - createdAt: '2021-11-05T16:18:00Z' - } -]; - -export const investors: Investor[] = [ - { - id: 'i1', - name: 'Michael Rodriguez', - email: 'michael@vcinnovate.com', - role: 'investor', - avatarUrl: 'https://images.pexels.com/photos/2379004/pexels-photo-2379004.jpeg', - bio: 'Early-stage investor with focus on B2B SaaS and fintech. Previously founded and exited two startups.', - investmentInterests: ['FinTech', 'SaaS', 'AI/ML'], - investmentStage: ['Seed', 'Series A'], - portfolioCompanies: ['PayStream', 'DataSense', 'CloudSecure'], - totalInvestments: 12, - minimumInvestment: '$250K', - maximumInvestment: '$1.5M', - isOnline: true, - createdAt: '2020-05-18T10:15:00Z' - }, - { - id: 'i2', - name: 'Jennifer Lee', - email: 'jennifer@impactvc.org', - role: 'investor', - avatarUrl: 'https://images.pexels.com/photos/1181686/pexels-photo-1181686.jpeg', - bio: 'Impact investor focused on climate tech, sustainable agriculture, and clean energy.', - investmentInterests: ['CleanTech', 'AgTech', 'Sustainability'], - investmentStage: ['Seed', 'Series A', 'Series B'], - portfolioCompanies: ['SolarFlow', 'EcoPackage', 'CleanWater Solutions'], - totalInvestments: 18, - minimumInvestment: '$500K', - maximumInvestment: '$3M', - isOnline: false, - createdAt: '2019-08-30T15:40:00Z' - }, - { - id: 'i3', - name: 'Robert Torres', - email: 'robert@healthventures.com', - role: 'investor', - avatarUrl: 'https://images.pexels.com/photos/834863/pexels-photo-834863.jpeg', - bio: 'Healthcare-focused investor with medical background. Looking for innovations in patient care and biotech.', - investmentInterests: ['HealthTech', 'BioTech', 'Medical Devices'], - investmentStage: ['Series A', 'Series B'], - portfolioCompanies: ['MediTrack', 'BioGenics', 'Patient+'], - totalInvestments: 9, - minimumInvestment: '$1M', - maximumInvestment: '$5M', - isOnline: true, - createdAt: '2021-02-12T09:30:00Z' - } -]; - -// Combined user data for lookup -export const users = [...entrepreneurs, ...investors]; - -// Helper function to find a user by ID -export const findUserById = (id: string) => { - return users.find(user => user.id === id) || null; +export const getInvestorsFromDb = async () => { + await axios + .get(URL+"/user/get-investors", { + withCredentials: true, + }) + .then((res) => { + const { users } = res.data; + return users; + }) + .catch((err) => { + console.log(err); + }); }; -// Helper function to get a user by role -export const getUsersByRole = (role: 'entrepreneur' | 'investor') => { - return users.filter(user => user.role === role); -}; \ No newline at end of file +export const getEnterprenuerFromDb = async () => { + await axios + .get(URL+"/user/get-enterpreneurs", { + withCredentials: true, + }) + .then((res) => { + const { users } = res.data; + return users; + }) + .catch((err) => { + console.log(err); + }); +}; diff --git a/src/pages/chat/ChatPage.tsx b/src/pages/chat/ChatPage.tsx index 10c55b077..9c99583c3 100644 --- a/src/pages/chat/ChatPage.tsx +++ b/src/pages/chat/ChatPage.tsx @@ -8,7 +8,6 @@ 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'; diff --git a/src/pages/dashboard/EntrepreneurDashboard.tsx b/src/pages/dashboard/EntrepreneurDashboard.tsx index 54b8cebd3..ca3762049 100644 --- a/src/pages/dashboard/EntrepreneurDashboard.tsx +++ b/src/pages/dashboard/EntrepreneurDashboard.tsx @@ -1,59 +1,77 @@ -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, + 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 { getInvestorsFromDb } from "../../data/users"; export const EntrepreneurDashboard: React.FC = () => { const { user } = useAuth(); - console.log(user); - const [collaborationRequests, setCollaborationRequests] = useState([]); - const [recommendedInvestors, setRecommendedInvestors] = useState(investors.slice(0, 3)); - + const [collaborationRequests, setCollaborationRequests] = useState< + CollaborationRequest[] + >([]); + const [recommendedInvestors, setRecommendedInvestors] = useState([]); + useEffect(() => { - if (user) { - // Load collaboration requests - const requests = getRequestsForEntrepreneur(user.id); - setCollaborationRequests(requests); - } - }, [user]); - - const handleRequestStatusUpdate = (requestId: string, status: 'accepted' | 'rejected') => { - setCollaborationRequests(prevRequests => - prevRequests.map(req => + const fetchData = async () => { + if (user) { + const investors = await getInvestorsFromDb(); + setRecommendedInvestors(investors); + + const requests = getRequestsForEntrepreneur(user.id); + setCollaborationRequests(requests); + } + }; + fetchData(); + }, []); + + const handleRequestStatusUpdate = ( + requestId: string, + status: "accepted" | "rejected" + ) => { + setCollaborationRequests((prevRequests) => + prevRequests.map((req) => req.id === requestId ? { ...req, status } : req ) ); }; - + if (!user) return null; - - const pendingRequests = collaborationRequests.filter(req => req.status === 'pending'); - + + const pendingRequests = collaborationRequests.filter( + (req) => req.status === "pending" + ); + 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 */}
@@ -63,13 +81,17 @@ export const EntrepreneurDashboard: React.FC = () => {
-

Pending Requests

-

{pendingRequests.length}

+

+ Pending Requests +

+

+ {pendingRequests.length} +

- +
@@ -77,15 +99,21 @@ export const EntrepreneurDashboard: React.FC = () => {
-

Total Connections

+

+ Total Connections +

- {collaborationRequests.filter(req => req.status === 'accepted').length} + { + collaborationRequests.filter( + (req) => req.status === "accepted" + ).length + }

- +
@@ -93,13 +121,15 @@ export const EntrepreneurDashboard: React.FC = () => {
-

Upcoming Meetings

+

+ Upcoming Meetings +

2

- +
@@ -107,27 +137,31 @@ export const EntrepreneurDashboard: React.FC = () => {
-

Profile Views

+

+ Profile Views +

24

- +
{/* 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 +

)}
- + {/* Recommended investors */}
-

Recommended Investors

- +

+ Recommended Investors +

+ View all
- + - {recommendedInvestors.map(investor => ( - - ))} + {recommendedInvestors && recommendedInvestors.length > 0 ? ( + recommendedInvestors.map((investor, i) => ( +
{investor.name}
+ )) + ) : ( +
No investors found
+ )}
); -}; \ No newline at end of file +}; diff --git a/src/pages/dashboard/InvestorDashboard.tsx b/src/pages/dashboard/InvestorDashboard.tsx index b72480abc..af7542a84 100644 --- a/src/pages/dashboard/InvestorDashboard.tsx +++ b/src/pages/dashboard/InvestorDashboard.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; import { Users, PieChart, Filter, Search, PlusCircle } from 'lucide-react'; import { Button } from '../../components/ui/Button'; @@ -7,9 +7,8 @@ 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 { getEnterprenuerFromDb } from '../../data/users'; export const InvestorDashboard: React.FC = () => { const { user } = useAuth(); @@ -20,26 +19,37 @@ export const InvestorDashboard: React.FC = () => { // Get collaboration requests sent by this investor const sentRequests = getRequestsFromInvestor(user.id); - const requestedEntrepreneurIds = sentRequests.map(req => req.entrepreneurId); - + // const requestedEntrepreneurIds = sentRequests.map(req => req.entrepreneurId); + const [entrepreneurs,setEnterprenuers] = useState([{}]); + + useEffect(() => { + const fetchData = async()=>{ + if (user) { + const entrepreneurs = getEnterprenuerFromDb(); + setEnterprenuers(entrepreneurs); + } + } + fetchData(); + }, []); // Filter entrepreneurs based on search and industry filters - const filteredEntrepreneurs = entrepreneurs.filter(entrepreneur => { - // Search filter - const matchesSearch = searchQuery === '' || - entrepreneur.name.toLowerCase().includes(searchQuery.toLowerCase()) || - entrepreneur.startupName.toLowerCase().includes(searchQuery.toLowerCase()) || - entrepreneur.industry.toLowerCase().includes(searchQuery.toLowerCase()) || - entrepreneur.pitchSummary.toLowerCase().includes(searchQuery.toLowerCase()); + const filteredEntrepreneurs = entrepreneurs; + // const filteredEntrepreneurs = entrepreneurs.filter(entrepreneur => { + // // Search filter + // const matchesSearch = searchQuery === '' || + // entrepreneur.name.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 || - selectedIndustries.includes(entrepreneur.industry); + // // Industry filter + // const matchesIndustry = selectedIndustries.length === 0 || + // selectedIndustries.includes(entrepreneur.industry); - return matchesSearch && matchesIndustry; - }); + // return matchesSearch && matchesIndustry; + // }); // Get unique industries for filter - const industries = Array.from(new Set(entrepreneurs.map(e => e.industry))); + const industries = []; // Toggle industry selection const toggleIndustry = (industry: string) => { @@ -157,12 +167,9 @@ export const InvestorDashboard: React.FC = () => { {filteredEntrepreneurs.length > 0 ? (
- {filteredEntrepreneurs.map(entrepreneur => ( - - ))} + {/* {filteredEntrepreneurs.map(entrepreneur => ( +
+ ))} */}
) : (
diff --git a/src/pages/entrepreneurs/EntrepreneursPage.tsx b/src/pages/entrepreneurs/EntrepreneursPage.tsx index bbcf8bf31..1f1c097af 100644 --- a/src/pages/entrepreneurs/EntrepreneursPage.tsx +++ b/src/pages/entrepreneurs/EntrepreneursPage.tsx @@ -4,7 +4,6 @@ import { Input } from '../../components/ui/Input'; import { Card, CardHeader, CardBody } from '../../components/ui/Card'; import { Badge } from '../../components/ui/Badge'; import { EntrepreneurCard } from '../../components/entrepreneur/EntrepreneurCard'; -import { entrepreneurs } from '../../data/users'; export const EntrepreneursPage: React.FC = () => { const [searchQuery, setSearchQuery] = useState(''); diff --git a/src/pages/investors/InvestorsPage.tsx b/src/pages/investors/InvestorsPage.tsx index 0f234cf4d..35a77a05d 100644 --- a/src/pages/investors/InvestorsPage.tsx +++ b/src/pages/investors/InvestorsPage.tsx @@ -4,7 +4,6 @@ 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'; export const InvestorsPage: React.FC = () => { const [searchQuery, setSearchQuery] = useState(''); diff --git a/src/pages/profile/EntrepreneurProfile.tsx b/src/pages/profile/EntrepreneurProfile.tsx index 3c05a18e6..c1f76d680 100644 --- a/src/pages/profile/EntrepreneurProfile.tsx +++ b/src/pages/profile/EntrepreneurProfile.tsx @@ -6,7 +6,6 @@ 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'; diff --git a/src/pages/profile/InvestorProfile.tsx b/src/pages/profile/InvestorProfile.tsx index 22b722d62..acd41a42d 100644 --- a/src/pages/profile/InvestorProfile.tsx +++ b/src/pages/profile/InvestorProfile.tsx @@ -6,7 +6,7 @@ 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'; export const InvestorProfile: React.FC = () => { diff --git a/src/pages/settings/SettingsPage.tsx b/src/pages/settings/SettingsPage.tsx index bb6d5f17f..9a179a1d1 100644 --- a/src/pages/settings/SettingsPage.tsx +++ b/src/pages/settings/SettingsPage.tsx @@ -1,54 +1,82 @@ -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, 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 { Navigate } from "react-router-dom"; export const SettingsPage: React.FC = () => { - const { user } = useAuth(); - - if (!user) return null; - + const { user,updateProfile,userData } = useAuth(); + + if (!user || !userData) return null; + console.log(userData) + const [userDetails, setUserDetails] = useState({ + name: userData.name, + email: userData.email, + role: userData.role, + bio: userData.bio || "", + location: userData.location || "", + avatarUrl: userData.avatarUrl || "", + }); + const [isFileUploaded, setIsFileUploaded] = useState(false); + const handleChange = (e: Event) => { + const { name, value, files } = e.target; + if (name === "avatarUrl") { + setUserDetails({ ...userDetails, [name]: files[0] }); + setIsFileUploaded(true); + } else { + setUserDetails({ ...userDetails, [name]: value }); + } + }; + const handleSubmit = async (e:Event) => { + e.preventDefault(); + + updateProfile(userData.userId,userDetails); + }; return (

Settings

-

Manage your account preferences and settings

+

+ Manage your account preferences and settings +

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

Profile Settings

+

+ Profile Settings +

- - + +
+ + +

JPG, GIF or PNG. Max size of 800K

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

Security Settings

+

+ Security Settings +

-

Two-Factor Authentication

+

+ Two-Factor Authentication +

Add an extra layer of security to your account

- Not Enabled + + Not Enabled +
- +
-

Change Password

+

+ Change Password +

- - - - - - + + + + + +
@@ -172,4 +212,4 @@ export const SettingsPage: React.FC = () => {
); -}; \ No newline at end of file +}; diff --git a/src/types/index.ts b/src/types/index.ts index 02212bbf9..38f221709 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -6,6 +6,7 @@ export interface User { email: string; role: UserRole; avatarUrl: string; + location:string; bio: string; isOnline?: boolean; createdAt: string; @@ -17,7 +18,6 @@ export interface Entrepreneur extends User { pitchSummary: string; fundingNeeded: string; industry: string; - location: string; foundedYear: number; teamSize: number; } @@ -70,6 +70,7 @@ export interface Document { export interface AuthContextType { user: User | null; + userData:User | null; login: (email: string, password: string, role: UserRole) => Promise; register: (name: string, email: string, password: string, role: UserRole) => Promise; logout: () => void; From be9f805790e5767b5b97cb6e6398a17b1a644bf7 Mon Sep 17 00:00:00 2001 From: Danish-Butt Date: Sat, 23 Aug 2025 23:20:30 +0500 Subject: [PATCH 03/43] profiles developing --- src/App.tsx | 2 +- src/components/investor/InvestorCard.tsx | 72 ++-- src/components/layout/Sidebar.tsx | 2 +- src/data/collaborationRequests.ts | 2 +- src/data/users.ts | 91 +++++- src/pages/dashboard/EntrepreneurDashboard.tsx | 5 +- src/pages/dashboard/InvestorDashboard.tsx | 2 +- src/pages/profile/EntrepreneurProfile.tsx | 309 ++++++++++++------ src/pages/profile/InvestorProfile.tsx | 14 +- src/pages/settings/SettingsPage.tsx | 1 + src/types/index.ts | 4 +- 11 files changed, 353 insertions(+), 151 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 20074cc4b..d35daa896 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -97,7 +97,7 @@ function App() { } /> {/* Catch all other routes and redirect to login */} - } /> + } /> diff --git a/src/components/investor/InvestorCard.tsx b/src/components/investor/InvestorCard.tsx index ca383f6a0..b7d1b7f8f 100644 --- a/src/components/investor/InvestorCard.tsx +++ b/src/components/investor/InvestorCard.tsx @@ -1,11 +1,11 @@ -import React from 'react'; -import { useNavigate } from 'react-router-dom'; -import { MessageCircle, ExternalLink } from 'lucide-react'; -import { Investor } 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 { Investor } from "../../types"; +import { Card, CardBody, CardFooter } from "../ui/Card"; +import { Avatar } from "../ui/Avatar"; +import { Badge } from "../ui/Badge"; +import { Button } from "../ui/Button"; interface InvestorCardProps { investor: Investor; @@ -14,22 +14,22 @@ interface InvestorCardProps { export const InvestorCard: React.FC = ({ investor, - showActions = true + showActions = true, }) => { const navigate = useNavigate(); - + const handleViewProfile = () => { navigate(`/profile/investor/${investor.id}`); }; - + const handleMessage = (e: React.MouseEvent) => { e.stopPropagation(); // Prevent card click navigate(`/chat/${investor.id}`); }; - + return ( - @@ -39,43 +39,55 @@ export const InvestorCard: React.FC = ({ src={investor.avatarUrl} alt={investor.name} size="lg" - status={investor.isOnline ? 'online' : 'offline'} + status={investor.isOnline ? "online" : "offline"} className="mr-4" /> - +
-

{investor.name}

-

Investor • {investor.totalInvestments} investments

- +

+ {investor.name} +

+

+ Investor • {investor.totalInvestments} investments +

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

Investment Interests

+

+ Investment Interests +

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

{investor.bio}

- +
Investment Range -

{investor.minimumInvestment} - {investor.maximumInvestment}

+

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

- + {showActions && ( - + +
); } - - const isCurrentUser = currentUser?.id === entrepreneur.id; - const isInvestor = currentUser?.role === 'investor'; - + + const isCurrentUser = currentUser?.userId === entrepreneur.userId; + 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 hasRequestedCollaboration = + isInvestor && id + ? getRequestsFromInvestor(currentUser.userId).some( + (req) => req.entrepreneurId === id + ) + : false; + const handleSendRequest = () => { if (isInvestor && currentUser && id) { createCollaborationRequest( - currentUser.id, + currentUser.userId, 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(); } }; - + const handleChange = (e: React.ChangeEvent) => { + setFormData({ ...formData, [e.target.name]: e.target.value }); +}; + + + const EditingPopUp = () => { + return ( +
+
+
+ +
+ +
+
+
+
+ ); + }; + return (
+ {isEditing && } {/* Profile header */} @@ -60,17 +125,19 @@ 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} @@ -88,11 +155,11 @@ export const EntrepreneurProfile: React.FC = () => {
- +
{!isCurrentUser && ( <> - + - + {isInvestor && ( )} )} - + {isCurrentUser && ( @@ -124,7 +197,7 @@ export const EntrepreneurProfile: React.FC = () => {
- +
{/* Main content - left side */}
@@ -137,50 +210,67 @@ export const EntrepreneurProfile: React.FC = () => {

{entrepreneur.bio}

- + {/* Startup Description */} -

Startup Overview

+

+ Startup Overview +

-

Problem Statement

+

+ Problem Statement +

- {entrepreneur?.pitchSummary?.split('.')[0]}. + {entrepreneur?.pitchSummary?.split(".")[0]}.

- +
-

Solution

+

+ Solution +

{entrepreneur.pitchSummary}

- +
-

Market Opportunity

+

+ 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. + 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

+

+ Competitive Advantage +

- Unlike our competitors, we offer a unique approach that combines innovative technology with deep industry expertise, resulting in superior outcomes for our customers. + Unlike our competitors, we offer a unique approach that + combines innovative technology with deep industry expertise, + resulting in superior outcomes for our customers.

- + {/* Team */}

Team

- {entrepreneur.teamSize} members + + {entrepreneur.teamSize} members +
@@ -192,11 +282,13 @@ export const EntrepreneurProfile: React.FC = () => { className="mr-3" />
-

{entrepreneur.name}

+

+ {entrepreneur.name} +

Founder & CEO

- +
{ className="mr-3" />
-

Alex Johnson

+

+ Alex Johnson +

CTO

- +
{ className="mr-3" />
-

Jessica Chen

+

+ Jessica Chen +

Head of Product

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

+ {entrepreneur.teamSize - 3} more team members

+

+ + {entrepreneur.teamSize - 3} more team members +

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

{entrepreneur.fundingNeeded}

+

+ {entrepreneur.fundingNeeded} +

- +
Valuation -

$8M - $12M

+

+ $8M - $12M +

- +
- Previous Funding -

$750K Seed (2022)

+ + Previous Funding + +

+ $750K Seed (2022) +

- +
- Funding Timeline + + Funding Timeline +
Pre-seed - Completed + + Completed +
Seed - Completed + + Completed +
Series A - In Progress + + In Progress +
- + {/* Documents */} @@ -293,53 +407,62 @@ export const EntrepreneurProfile: React.FC = () => {
-

Pitch Deck

-

Updated 2 months ago

+

+ Pitch Deck +

+

+ Updated 2 months ago +

- + - +
-

Business Plan

+

+ Business Plan +

Updated 1 month ago

- +
- +
-

Financial Projections

+

+ 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 ? ( - ) : ( - )} @@ -351,4 +474,4 @@ export const EntrepreneurProfile: React.FC = () => {
); -}; \ No newline at end of file +}; diff --git a/src/pages/profile/InvestorProfile.tsx b/src/pages/profile/InvestorProfile.tsx index acd41a42d..3ac670162 100644 --- a/src/pages/profile/InvestorProfile.tsx +++ b/src/pages/profile/InvestorProfile.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } 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'; @@ -7,14 +7,22 @@ import { Card, CardBody, CardHeader } from '../../components/ui/Card'; import { Badge } from '../../components/ui/Badge'; import { useAuth } from '../../context/AuthContext'; +import { getInvestorById } from '../../data/users'; import { Investor } from '../../types'; export const InvestorProfile: React.FC = () => { const { id } = useParams<{ id: string }>(); const { user: currentUser } = useAuth(); - + let investor :Investor; // Fetch investor data - const investor = findUserById(id || '') as Investor | null; + useEffect(()=>{ + const fetchInvestors = async() =>{ + investor = await getInvestorById(id); + } + fetchInvestors(); + },[]); + + if (!investor || investor.role !== 'investor') { return ( diff --git a/src/pages/settings/SettingsPage.tsx b/src/pages/settings/SettingsPage.tsx index 9a179a1d1..e1a032c48 100644 --- a/src/pages/settings/SettingsPage.tsx +++ b/src/pages/settings/SettingsPage.tsx @@ -140,6 +140,7 @@ export const SettingsPage: React.FC = () => { diff --git a/src/types/index.ts b/src/types/index.ts index 38f221709..a1c910957 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,7 +1,7 @@ export type UserRole = 'entrepreneur' | 'investor'; export interface User { - id: string; + userId: string; name: string; email: string; role: UserRole; @@ -9,10 +9,10 @@ export interface User { location:string; bio: string; isOnline?: boolean; - createdAt: string; } export interface Entrepreneur extends User { + userId:string; role: 'entrepreneur'; startupName: string; pitchSummary: string; From fc2383bd6a5eab5bb2253d5bc7693aa166386797 Mon Sep 17 00:00:00 2001 From: Danish-Butt Date: Tue, 26 Aug 2025 23:40:01 +0500 Subject: [PATCH 04/43] about to end auth --- .../entrepreneur/EntrepreneurCard.tsx | 4 +- src/components/investor/InvestorCard.tsx | 4 +- src/components/layout/Sidebar.tsx | 2 +- src/components/ui/Input.tsx | 2 +- src/context/AuthContext.tsx | 79 ++--- src/data/users.ts | 73 ++++- src/pages/dashboard/InvestorDashboard.tsx | 11 +- src/pages/profile/EntrepreneurProfile.tsx | 130 ++++++--- src/pages/profile/InvestorProfile.tsx | 269 +++++++++++------- src/pages/settings/SettingsPage.tsx | 51 ++-- src/types/index.ts | 2 - 11 files changed, 407 insertions(+), 220 deletions(-) diff --git a/src/components/entrepreneur/EntrepreneurCard.tsx b/src/components/entrepreneur/EntrepreneurCard.tsx index 67f1fafe0..a2cdaeee7 100644 --- a/src/components/entrepreneur/EntrepreneurCard.tsx +++ b/src/components/entrepreneur/EntrepreneurCard.tsx @@ -19,12 +19,12 @@ export const EntrepreneurCard: React.FC = ({ const navigate = useNavigate(); const handleViewProfile = () => { - navigate(`/profile/entrepreneur/${entrepreneur.id}`); + navigate(`/profile/entrepreneur/${entrepreneur._id}`); }; const handleMessage = (e: React.MouseEvent) => { e.stopPropagation(); // Prevent card click - navigate(`/chat/${entrepreneur.id}`); + navigate(`/chat/${entrepreneur._id}`); }; return ( diff --git a/src/components/investor/InvestorCard.tsx b/src/components/investor/InvestorCard.tsx index b7d1b7f8f..756da07d2 100644 --- a/src/components/investor/InvestorCard.tsx +++ b/src/components/investor/InvestorCard.tsx @@ -19,7 +19,7 @@ export const InvestorCard: React.FC = ({ const navigate = useNavigate(); const handleViewProfile = () => { - navigate(`/profile/investor/${investor.id}`); + navigate(`/profile/investor/${investor._id}`); }; const handleMessage = (e: React.MouseEvent) => { @@ -52,7 +52,7 @@ export const InvestorCard: React.FC = ({

- {investor.investmentStage.map((stage, index) => ( + {investor.investmentStage && investor.investmentStage.map((stage, index) => ( {stage} diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 8498f2a20..53d4eaad4 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -47,7 +47,7 @@ export const Sidebar: React.FC = () => { const investorItems = [ { to: '/dashboard/investor', icon: , text: 'Dashboard' }, - { to: '/profile/investor/' + user.id, icon: , text: 'My Portfolio' }, + { to: '/profile/investor/' + user.userId, icon: , text: 'My Portfolio' }, { to: '/entrepreneurs', icon: , text: 'Find Startups' }, { to: '/messages', icon: , text: 'Messages' }, { to: '/notifications', icon: , text: 'Notifications' }, diff --git a/src/components/ui/Input.tsx b/src/components/ui/Input.tsx index 9646e51d0..48535d659 100644 --- a/src/components/ui/Input.tsx +++ b/src/components/ui/Input.tsx @@ -44,7 +44,7 @@ export const Input = forwardRef(({ diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index a32e2c194..cb1337fa3 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -22,15 +22,15 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ setIsLoading(true); const token = localStorage.getItem("token"); if (token) { - axios + axios .get("http://localhost:5000/auth/verify", { headers: { Authorization: `Bearer ${token}` }, }) .then((res) => { - const {user} = res.data; - if (! (user.exp * 1000 > Date.now())) { - localStorage.removeItem("token"); - } + const { user } = res.data; + if (!(user.exp * 1000 > Date.now())) { + localStorage.removeItem("token"); + } }) .catch(() => { localStorage.removeItem("token"); @@ -69,7 +69,6 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ setUser(user); setUserData(user); toast.success("Successfully logged in!"); - } catch (error) { toast.error((error as Error).message); throw error; @@ -104,7 +103,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ if (res.status === 201) { toast.success("Account created successfully!"); const { token, user } = res.data; - localStorage.setItem("token", token); + localStorage.setItem("token", token); setUserData(user); setUser(user); } @@ -117,7 +116,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ }; // Mock forgot password function - const forgotPassword = async (email: string,role:string): Promise => { + const forgotPassword = async (email: string, role: string): Promise => { try { // Generate reset token (in a real app, this would be a secure token) const resetToken = Math.random().toString(36).substring(2, 15); @@ -141,13 +140,13 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ const sub = "Password reset instructions"; const res = await axios.post( `${URL}/auth/send-mail`, - { email, message, sub,role }, + { email, message, sub, role }, { withCredentials: true, } ); - const {user} = res.data; - localStorage.setItem("user",JSON.stringify(user)); + const { user } = res.data; + localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(user)); toast.success("Password reset instructions sent to your email"); } catch (error) { toast.error((error as Error).message); @@ -165,11 +164,11 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ if (token !== storedToken) { throw new Error("Invalid or expired reset token"); } - const user = JSON.parse(localStorage.getItem("user")); - + const user = localStorage.getItem(USER_STORAGE_KEY); + await axios.patch( `${URL}/auth/update-password/${user._id}`, - { newPassword,role:user.role}, + { newPassword, role: user.role }, { withCredentials: true } ); @@ -196,30 +195,34 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ userId: string, userData: User ): Promise => { - if(userData.location==="" && userData.bio==="" && userData.avatarUrl === ""){ - console.log(userData); - alert("Make changes to update profile.."); - return; - } - const formData = new FormData(); - formData.append("name", userData.name); - formData.append("location", userData.location); - formData.append("email", userData.email); - formData.append("bio", userData.bio); - formData.append("avatarUrl", userData.avatarUrl); - console.log(formData); - await axios - .post(`${URL}/user/update-profile/${userId}`, formData, { - withCredentials: true, - }) - .then((res) => { - toast.success("profile updated successfully."); - const {user} = res.data; - setUserData(user); - }) - .catch((err) => { - console.log(err); - }); + if ( + userData.location === "" && + userData.bio === "" && + userData.avatarUrl === "" + ) { + console.log(userData); + alert("Make changes to update profile.."); + return; + } + const formData = new FormData(); + formData.append("name", userData.name); + formData.append("location", userData.location); + formData.append("email", userData.email); + formData.append("bio", userData.bio); + formData.append("avatarUrl", userData.avatarUrl); + console.log(formData); + await axios + .post(`${URL}/user/update-profile/${userId}`, formData, { + withCredentials: true, + }) + .then((res) => { + toast.success("profile updated successfully."); + const { user } = res.data; + setUserData(user); + }) + .catch((err) => { + console.log(err); + }); }; const value = { diff --git a/src/data/users.ts b/src/data/users.ts index 194e6078b..d55e02f78 100644 --- a/src/data/users.ts +++ b/src/data/users.ts @@ -1,10 +1,11 @@ import axios from "axios"; -import { Entrepreneur } from "../types"; +import { Entrepreneur, Investor } from "../types"; +import toast from "react-hot-toast"; const URL = "http://localhost:5000"; export const getInvestorsFromDb = async () => { try { - const res = await axios.get(URL + "/user/get-investors", { + const res = await axios.get(URL + "/investor/get-investors", { withCredentials: true, }); const { users } = res.data; @@ -16,11 +17,12 @@ export const getInvestorsFromDb = async () => { export const getInvestorById = async (id) => { try { - const res = await axios.get(URL + "/user/get-investors/" + id, { + const res = await axios.get(URL + "/investor/get-investor-by-id/" + id, { withCredentials: true, }); const { user } = res.data; - return user; + const filteredUser = filterInvestor(user); + return filteredUser; } catch (err) { console.log(err); } @@ -28,7 +30,7 @@ export const getInvestorById = async (id) => { export const getEnterprenuerFromDb = async () => { try { - const res = await axios.get(URL + "/user/get-enterpreneurs", { + const res = await axios.get(URL + "/entrepreneur/get-entrepreneurs", { withCredentials: true, }); const { users } = res.data; @@ -40,17 +42,66 @@ export const getEnterprenuerFromDb = async () => { export const getEnterpreneurById = async (id) => { try { - const res = await axios.get(URL + "/user/get-enterpreneurs/" + id, { - withCredentials: true, - }); - const { user } = res.data; - const filteredUser = filterEntrepreneur(user); - return filteredUser; + const res = await axios.get( + URL + "/entrepreneur/get-entrepreneur-by-id/" + id, + { + withCredentials: true, + } + ); + const { entrepreneur } = res.data; + return entrepreneur; } catch (err) { console.log(err); } }; +export const updateEntrepreneurData = async (formData: Entrepreneur) => { + try { + await axios.put( + `${URL}/entrepreneur/update-profile/${formData.userId}`, + formData, + { + withCredentials: true, + } + ); + toast.success("User data updated successfully."); + } catch (err) { + console.log(err); + } +}; +function filterInvestor(obj: Investor): Investor { + const { + userId, + name, + bio, + role, + location, + email, + avatarUrl, + investmentInterests, + investmentStage, + portfolioCompanies, + totalInvestments, + minimumInvestment, + maximumInvestment, + } = obj; + return { + userId, + name, + bio, + role, + email, + location, + avatarUrl, + investmentInterests, + investmentStage, + portfolioCompanies, + totalInvestments, + minimumInvestment, + maximumInvestment, + }; +} + function filterEntrepreneur(obj: Entrepreneur): Entrepreneur { const { userId, diff --git a/src/pages/dashboard/InvestorDashboard.tsx b/src/pages/dashboard/InvestorDashboard.tsx index c78bf65f2..c8d9a2e80 100644 --- a/src/pages/dashboard/InvestorDashboard.tsx +++ b/src/pages/dashboard/InvestorDashboard.tsx @@ -165,11 +165,14 @@ export const InvestorDashboard: React.FC = () => { - {filteredEntrepreneurs.length > 0 ? ( + {entrepreneurs.length > 0 ? (
- {/* {filteredEntrepreneurs.map(entrepreneur => ( -
- ))} */} + + {entrepreneurs.map(entrepreneur => ( +
+ +
+ ))}
) : (
diff --git a/src/pages/profile/EntrepreneurProfile.tsx b/src/pages/profile/EntrepreneurProfile.tsx index 1bc37a372..6b633bbe2 100644 --- a/src/pages/profile/EntrepreneurProfile.tsx +++ b/src/pages/profile/EntrepreneurProfile.tsx @@ -20,7 +20,7 @@ import { createCollaborationRequest, getRequestsFromInvestor, } from "../../data/collaborationRequests"; -import { getEnterpreneurById } from "../../data/users"; +import { getEnterpreneurById, updateEntrepreneurData } from "../../data/users"; import { Entrepreneur } from "../../types"; import { Input } from "../../components/ui/Input"; @@ -29,22 +29,34 @@ export const EntrepreneurProfile: React.FC = () => { const { user: currentUser } = useAuth(); const [isEditing, setIsEditing] = useState(false); const [entrepreneur, setEnterpreneur] = useState(); - const [formData, setFormData] = useState({ - name: "", - bio: "", - location: "", - }); + const initalData = { + userId: id, + startupName: entrepreneur?.startupName, + pitchSummary: entrepreneur?.pitchSummary, + fundingNeeded: entrepreneur?.fundingNeeded, + industry: entrepreneur?.industry, + foundedYear: entrepreneur?.foundedYear, + teamSize: entrepreneur?.teamSize, + }; + const [formData, setFormData] = useState(initalData); // Fetch entrepreneur data useEffect(() => { - const fetchInvestors = async () => { + const fetchEntrepreneur = async () => { const entrepreneur = await getEnterpreneurById(id); + console.log(entrepreneur); + console.log(currentUser) setEnterpreneur(entrepreneur); }; - fetchInvestors(); + fetchEntrepreneur(); }, []); + + useEffect(() => { + setFormData(initalData); + }, [entrepreneur]); + if (!currentUser) return null; - if (!entrepreneur || entrepreneur.role !== "entrepreneur") { + if (!entrepreneur) { return (

@@ -87,43 +99,87 @@ export const EntrepreneurProfile: React.FC = () => { window.location.reload(); } }; - const handleChange = (e: React.ChangeEvent) => { - setFormData({ ...formData, [e.target.name]: e.target.value }); -}; + const handleChange = (e) => { + const { name, value } = e.target; + setFormData((prev) => ({ ...prev, [name]: value })); + }; - const EditingPopUp = () => { - return ( -
-
-
- -
- -
-
-
-
- ); + const handleSubmit = async (e) => { + e.preventDefault(); + updateEntrepreneurData(formData); + setFormData(initalData); + setIsEditing(false); }; return (
- {isEditing && } + {isEditing && ( +
+
+
+ + + + + + +
+ + +
+
+
+
+ )} {/* Profile header */}
{

- {entrepreneur.name} + {entrepreneur.userId.name}

@@ -142,7 +198,7 @@ export const EntrepreneurProfile: React.FC = () => { {entrepreneur.industry} - {entrepreneur.location} + {entrepreneur.userId.location} @@ -207,7 +263,7 @@ export const EntrepreneurProfile: React.FC = () => {

About

-

{entrepreneur.bio}

+

{entrepreneur.userId.bio}

diff --git a/src/pages/profile/InvestorProfile.tsx b/src/pages/profile/InvestorProfile.tsx index 3ac670162..3cca86981 100644 --- a/src/pages/profile/InvestorProfile.tsx +++ b/src/pages/profile/InvestorProfile.tsx @@ -1,43 +1,54 @@ -import React, { useEffect } 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 { getInvestorById } from '../../data/users'; -import { Investor } from '../../types'; +import React, { useEffect, useState } 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 { getInvestorById } from "../../data/users"; +import { Investor } from "../../types"; export const InvestorProfile: React.FC = () => { const { id } = useParams<{ id: string }>(); const { user: currentUser } = useAuth(); - let investor :Investor; + const [investor, setInvestor] = useState(); // Fetch investor data - useEffect(()=>{ - const fetchInvestors = async() =>{ - investor = await getInvestorById(id); - } - fetchInvestors(); - },[]); - - - - if (!investor || investor.role !== 'investor') { + useEffect(() => { + const fetchInvestors = async () => { + const investor = await getInvestorById(id); + setInvestor(investor); + }; + fetchInvestors(); + }, []); + + if (!currentUser) return null; + 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; - + + const isCurrentUser = currentUser?.userId === investor.userId; + return (
{/* Profile header */} @@ -48,52 +59,50 @@ 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

- +
San Francisco, CA - {investor.investmentStage.map((stage, index) => ( - {stage} - ))} + {investor.investmentStage && + investor.investmentStage.map((stage, index) => ( + + {stage} + + ))}
- +
{!isCurrentUser && ( - - + + )} - + {isCurrentUser && ( - )}
- +
{/* Main content - left side */}
@@ -106,34 +115,48 @@ export const InvestorProfile: React.FC = () => {

{investor.bio}

- + {/* Investment Interests */} -

Investment Interests

+

+ Investment Interests +

-

Industries

+

+ Industries +

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

Investment Stages

+

+ Investment Stages +

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

Investment Criteria

+

+ Investment Criteria +

  • @@ -156,76 +179,112 @@ export const InvestorProfile: React.FC = () => {
- + {/* Portfolio Companies */} -

Portfolio Companies

- {investor.portfolioCompanies.length} companies +

+ Portfolio Companies +

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

{company}

-

Invested in 2022

+ {investor.portfolioCompanies && + 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 && investor.minimumInvestment} -{" "} + {investor.maximumInvestment && investor.maximumInvestment}

- +
- Total Investments -

{investor.totalInvestments} companies

+ + Total Investments + +

+ {investor.totalInvestments} companies +

- +
- Typical Investment Timeline + + Typical Investment Timeline +

3-5 years

- +
- Investment Focus + + Investment Focus +
SaaS & B2B
-
+
FinTech
-
+
HealthTech
-
+
@@ -233,39 +292,53 @@ export const InvestorProfile: React.FC = () => {
- + {/* Stats */} -

Investment Stats

+

+ Investment Stats +

-

Successful Exits

-

4

+

+ Successful Exits +

+

+ 4 +

- +
-

Avg. ROI

-

3.2x

+

+ Avg. ROI +

+

+ 3.2x +

- +
-

Active Investments

-

{investor.portfolioCompanies.length}

+

+ Active Investments +

+

+ {investor.portfolioCompanies && investor.portfolioCompanies.length} +

@@ -277,4 +350,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 e1a032c48..d0391131f 100644 --- a/src/pages/settings/SettingsPage.tsx +++ b/src/pages/settings/SettingsPage.tsx @@ -9,18 +9,19 @@ import { useAuth } from "../../context/AuthContext"; import { Navigate } from "react-router-dom"; export const SettingsPage: React.FC = () => { - const { user,updateProfile,userData } = useAuth(); + const { user, updateProfile, userData } = useAuth(); if (!user || !userData) return null; - console.log(userData) - const [userDetails, setUserDetails] = useState({ + + const initialValues = { name: userData.name, email: userData.email, role: userData.role, bio: userData.bio || "", location: userData.location || "", avatarUrl: userData.avatarUrl || "", - }); + }; + const [userDetails, setUserDetails] = useState(initialValues); const [isFileUploaded, setIsFileUploaded] = useState(false); const handleChange = (e: Event) => { const { name, value, files } = e.target; @@ -31,10 +32,14 @@ export const SettingsPage: React.FC = () => { setUserDetails({ ...userDetails, [name]: value }); } }; - const handleSubmit = async (e:Event) => { + const handleSubmit = async (e: Event) => { e.preventDefault(); - - updateProfile(userData.userId,userDetails); + + updateProfile(userData.userId, userDetails); + }; + const handleCancel = (e) => { + e.preventDefault(); + setUserDetails(initialValues); }; return (
@@ -50,9 +55,12 @@ export const SettingsPage: React.FC = () => {
@@ -155,12 +156,14 @@ export const SettingsPage: React.FC = () => { rows={4} name="bio" onChange={handleChange} - defaultValue={userDetails.bio} + value={userDetails.bio} >
- +
diff --git a/src/types/index.ts b/src/types/index.ts index a1c910957..355b88cf1 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -12,8 +12,6 @@ export interface User { } export interface Entrepreneur extends User { - userId:string; - role: 'entrepreneur'; startupName: string; pitchSummary: string; fundingNeeded: string; From 1c645cc1018cc21ac3b74f03e200cccb5f88fa1f Mon Sep 17 00:00:00 2001 From: Danish-Butt Date: Fri, 29 Aug 2025 23:13:28 +0500 Subject: [PATCH 05/43] profile updation --- .../entrepreneur/EntrepreneurCard.tsx | 4 +- src/components/ui/Badge.tsx | 3 + src/data/users.ts | 96 ++--- src/pages/dashboard/InvestorDashboard.tsx | 14 +- src/pages/entrepreneurs/EntrepreneursPage.tsx | 20 +- src/pages/investors/InvestorsPage.tsx | 138 ++++--- src/pages/messages/MessagesPage.tsx | 2 +- src/pages/profile/EntrepreneurProfile.tsx | 182 +++++--- src/pages/profile/InvestorProfile.tsx | 389 +++++++++++++++--- src/types/index.ts | 50 ++- 10 files changed, 628 insertions(+), 270 deletions(-) diff --git a/src/components/entrepreneur/EntrepreneurCard.tsx b/src/components/entrepreneur/EntrepreneurCard.tsx index a2cdaeee7..bd284146c 100644 --- a/src/components/entrepreneur/EntrepreneurCard.tsx +++ b/src/components/entrepreneur/EntrepreneurCard.tsx @@ -19,12 +19,12 @@ export const EntrepreneurCard: React.FC = ({ const navigate = useNavigate(); const handleViewProfile = () => { - navigate(`/profile/entrepreneur/${entrepreneur._id}`); + navigate(`/profile/entrepreneur/${entrepreneur.userId ||entrepreneur._id}`); }; const handleMessage = (e: React.MouseEvent) => { e.stopPropagation(); // Prevent card click - navigate(`/chat/${entrepreneur._id}`); + navigate(`/chat/${entrepreneur.userId ||entrepreneur._id}`); }; return ( diff --git a/src/components/ui/Badge.tsx b/src/components/ui/Badge.tsx index 8c84dc3ed..ee33fa675 100644 --- a/src/components/ui/Badge.tsx +++ b/src/components/ui/Badge.tsx @@ -9,6 +9,7 @@ interface BadgeProps { size?: BadgeSize; rounded?: boolean; className?: string; + onClick?:EventListener; } export const Badge: React.FC = ({ @@ -17,6 +18,7 @@ export const Badge: React.FC = ({ size = 'md', rounded = false, className = '', + onClick={}, }) => { const variantClasses = { primary: 'bg-primary-100 text-primary-800', @@ -39,6 +41,7 @@ export const Badge: React.FC = ({ return ( {children} diff --git a/src/data/users.ts b/src/data/users.ts index d55e02f78..04e1b4556 100644 --- a/src/data/users.ts +++ b/src/data/users.ts @@ -8,8 +8,8 @@ export const getInvestorsFromDb = async () => { const res = await axios.get(URL + "/investor/get-investors", { withCredentials: true, }); - const { users } = res.data; - return users; + const { investors } = res.data; + return investors; } catch (err) { console.log(err); } @@ -20,9 +20,8 @@ export const getInvestorById = async (id) => { const res = await axios.get(URL + "/investor/get-investor-by-id/" + id, { withCredentials: true, }); - const { user } = res.data; - const filteredUser = filterInvestor(user); - return filteredUser; + const { investor } = res.data; + return investor; } catch (err) { console.log(err); } @@ -33,8 +32,8 @@ export const getEnterprenuerFromDb = async () => { const res = await axios.get(URL + "/entrepreneur/get-entrepreneurs", { withCredentials: true, }); - const { users } = res.data; - return users; + const { entrepreneurs } = res.data; + return entrepreneurs; } catch (err) { console.log(err); } @@ -48,14 +47,15 @@ export const getEnterpreneurById = async (id) => { withCredentials: true, } ); - const { entrepreneur } = res.data; + const {entrepreneur} = res.data; + console.log(entrepreneur); return entrepreneur; } catch (err) { console.log(err); } }; -export const updateEntrepreneurData = async (formData: Entrepreneur) => { +export const updateEntrepreneurData = async (formData: any) => { try { await axios.put( `${URL}/entrepreneur/update-profile/${formData.userId}`, @@ -69,68 +69,18 @@ export const updateEntrepreneurData = async (formData: Entrepreneur) => { console.log(err); } }; -function filterInvestor(obj: Investor): Investor { - const { - userId, - name, - bio, - role, - location, - email, - avatarUrl, - investmentInterests, - investmentStage, - portfolioCompanies, - totalInvestments, - minimumInvestment, - maximumInvestment, - } = obj; - return { - userId, - name, - bio, - role, - email, - location, - avatarUrl, - investmentInterests, - investmentStage, - portfolioCompanies, - totalInvestments, - minimumInvestment, - maximumInvestment, - }; -} -function filterEntrepreneur(obj: Entrepreneur): Entrepreneur { - const { - userId, - name, - bio, - role, - startupName, - pitchSummary, - fundingNeeded, - industry, - teamSize, - location, - foundedYear, - email, - avatarUrl, - } = obj; - return { - userId, - name, - bio, - role, - foundedYear, - email, - startupName, - pitchSummary, - fundingNeeded, - industry, - teamSize, - location, - avatarUrl, - }; -} +export const updateInvestorData = async (formData: any) => { + try { + await axios.put( + `${URL}/investor/update-profile/${formData.userId}`, + formData, + { + withCredentials: true, + } + ); + toast.success("User data updated successfully."); + } catch (err) { + console.log(err); + } +}; diff --git a/src/pages/dashboard/InvestorDashboard.tsx b/src/pages/dashboard/InvestorDashboard.tsx index c8d9a2e80..cb96e5989 100644 --- a/src/pages/dashboard/InvestorDashboard.tsx +++ b/src/pages/dashboard/InvestorDashboard.tsx @@ -20,13 +20,18 @@ export const InvestorDashboard: React.FC = () => { // Get collaboration requests sent by this investor const sentRequests = getRequestsFromInvestor(user.id); // const requestedEntrepreneurIds = sentRequests.map(req => req.entrepreneurId); - const [entrepreneurs,setEnterprenuers] = useState([{}]); + const [entrepreneurs,setEnterprenuers] = useState([]); + const industries = []; useEffect(() => { const fetchData = async()=>{ if (user) { const entrepreneurs = await getEnterprenuerFromDb(); setEnterprenuers(entrepreneurs); + entrepreneurs.map(e=>{ + industries.push(e.industry); + }); + console.log(industries); } } fetchData(); @@ -49,7 +54,6 @@ export const InvestorDashboard: React.FC = () => { // }); // Get unique industries for filter - const industries = []; // Toggle industry selection const toggleIndustry = (industry: string) => { @@ -120,7 +124,7 @@ export const InvestorDashboard: React.FC = () => {

Total Startups

-

{entrepreneurs.length}

+

{entrepreneurs && entrepreneurs.length}

@@ -165,10 +169,10 @@ export const InvestorDashboard: React.FC = () => { - {entrepreneurs.length > 0 ? ( + {entrepreneurs && entrepreneurs.length > 0 ? (
- {entrepreneurs.map(entrepreneur => ( + {entrepreneurs && entrepreneurs.map(entrepreneur => (
diff --git a/src/pages/entrepreneurs/EntrepreneursPage.tsx b/src/pages/entrepreneurs/EntrepreneursPage.tsx index 1f1c097af..60037acc0 100644 --- a/src/pages/entrepreneurs/EntrepreneursPage.tsx +++ b/src/pages/entrepreneurs/EntrepreneursPage.tsx @@ -1,16 +1,30 @@ -import React, { useState } from 'react'; +import React, { useEffect, 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 { EntrepreneurCard } from '../../components/entrepreneur/EntrepreneurCard'; +import { useAuth } from '../../context/AuthContext'; +import { getEnterprenuerFromDb } from '../../data/users'; +import { Entrepreneur } from '../../types'; export const EntrepreneursPage: React.FC = () => { const [searchQuery, setSearchQuery] = useState(''); const [selectedIndustries, setSelectedIndustries] = useState([]); const [selectedFundingRange, setSelectedFundingRange] = useState([]); - + const {user} =useAuth(); // Get unique industries and funding ranges + const [entrepreneurs,setEnterprenuers] = useState([]); + + useEffect(() => { + const fetchData = async()=>{ + if (user) { + const entrepreneurs = await getEnterprenuerFromDb(); + setEnterprenuers(entrepreneurs); + } + } + fetchData(); + }, []); const allIndustries = Array.from(new Set(entrepreneurs.map(e => e.industry))); const fundingRanges = ['< $500K', '$500K - $1M', '$1M - $5M', '> $5M']; @@ -153,7 +167,7 @@ export const EntrepreneursPage: React.FC = () => {
{filteredEntrepreneurs.map(entrepreneur => ( ))} diff --git a/src/pages/investors/InvestorsPage.tsx b/src/pages/investors/InvestorsPage.tsx index 35a77a05d..230a7e2cc 100644 --- a/src/pages/investors/InvestorsPage.tsx +++ b/src/pages/investors/InvestorsPage.tsx @@ -1,60 +1,89 @@ -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 React, { useEffect, 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 { useAuth } from "../../context/AuthContext"; +import { getInvestorsFromDb } from "../../data/users"; +import { getRequestsForEntrepreneur } from "../../data/collaborationRequests"; +import { Investor } from "../../types"; export const InvestorsPage: React.FC = () => { - const [searchQuery, setSearchQuery] = useState(''); + const [searchQuery, setSearchQuery] = useState(""); const [selectedStages, setSelectedStages] = useState([]); const [selectedInterests, setSelectedInterests] = useState([]); - + + const [investors, setInvestors] = useState([]); + const { user } = useAuth(); + + useEffect(() => { + const fetchData = async () => { + if (user) { + const investors = await getInvestorsFromDb(); + console.log(investors); + setInvestors(investors ? investors : []); + + const requests = getRequestsForEntrepreneur(user.id); + } + }; + fetchData(); + }, []); + if (!user) return null; // 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 => + 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)); - + + const matchesStages = + selectedStages.length === 0 || + investor.investmentStage?.some((stage) => selectedStages.includes(stage)); + + const matchesInterests = + selectedInterests.length === 0 || + 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.filter((i) => i !== interest) : [...prev, interest] ); }; - + return (

Find Investors

-

Connect with investors who match your startup's needs

+

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

- +
{/* Filters sidebar */}
@@ -64,16 +93,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)} > @@ -97,9 +134,11 @@ export const InvestorsPage: React.FC = () => { ))}
- +
-

Location

+

+ Location +

- + {/* Main content */}
@@ -129,7 +168,7 @@ export const InvestorsPage: React.FC = () => { startAdornment={} fullWidth /> - +
@@ -137,17 +176,14 @@ export const InvestorsPage: React.FC = () => {
- +
- {filteredInvestors.map(investor => ( - + {filteredInvestors.map((investor) => ( + ))}
); -}; \ No newline at end of file +}; diff --git a/src/pages/messages/MessagesPage.tsx b/src/pages/messages/MessagesPage.tsx index c7589fb80..070249a2b 100644 --- a/src/pages/messages/MessagesPage.tsx +++ b/src/pages/messages/MessagesPage.tsx @@ -11,7 +11,7 @@ export const MessagesPage: React.FC = () => { if (!user) return null; - const conversations = getConversationsForUser(user.id); + const conversations = getConversationsForUser(user.userId); return (
diff --git a/src/pages/profile/EntrepreneurProfile.tsx b/src/pages/profile/EntrepreneurProfile.tsx index 6b633bbe2..b354735fb 100644 --- a/src/pages/profile/EntrepreneurProfile.tsx +++ b/src/pages/profile/EntrepreneurProfile.tsx @@ -29,7 +29,7 @@ export const EntrepreneurProfile: React.FC = () => { const { user: currentUser } = useAuth(); const [isEditing, setIsEditing] = useState(false); const [entrepreneur, setEnterpreneur] = useState(); - const initalData = { + const initialData = { userId: id, startupName: entrepreneur?.startupName, pitchSummary: entrepreneur?.pitchSummary, @@ -37,26 +37,28 @@ export const EntrepreneurProfile: React.FC = () => { industry: entrepreneur?.industry, foundedYear: entrepreneur?.foundedYear, teamSize: entrepreneur?.teamSize, + minValuation: entrepreneur?.minValuation, + maxValuation: entrepreneur?.maxValuation, + marketOpportunity: entrepreneur?.marketOpportunity, + advantage: entrepreneur?.advantage, }; - const [formData, setFormData] = useState(initalData); + const [formData, setFormData] = useState(initialData); // Fetch entrepreneur data useEffect(() => { const fetchEntrepreneur = async () => { const entrepreneur = await getEnterpreneurById(id); - console.log(entrepreneur); - console.log(currentUser) setEnterpreneur(entrepreneur); }; fetchEntrepreneur(); }, []); useEffect(() => { - setFormData(initalData); + setFormData(initialData); }, [entrepreneur]); if (!currentUser) return null; - if (!entrepreneur) { + if (!entrepreneur || entrepreneur.role !== "entrepreneur") { return (

@@ -75,7 +77,8 @@ export const EntrepreneurProfile: React.FC = () => { ); } - const isCurrentUser = currentUser?.userId === entrepreneur.userId; + const isCurrentUser = + currentUser?.userId === (entrepreneur.userId || entrepreneur._id); const isInvestor = currentUser?.role === "investor"; // Check if the current investor has already sent a request to this entrepreneur @@ -93,10 +96,6 @@ export const EntrepreneurProfile: React.FC = () => { 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(); } }; @@ -108,7 +107,32 @@ export const EntrepreneurProfile: React.FC = () => { const handleSubmit = async (e) => { e.preventDefault(); updateEntrepreneurData(formData); - setFormData(initalData); + const { + startupName, + pitchSummary, + fundingNeeded, + industry, + foundedYear, + teamSize, + marketOpportunity, + advantage, + minValuation, + maxValuation, + } = formData; + setEnterpreneur({ + ...entrepreneur, + startupName, + pitchSummary, + fundingNeeded, + industry, + foundedYear, + teamSize, + marketOpportunity, + advantage, + minValuation, + maxValuation, + }); + setFormData(initialData); setIsEditing(false); }; @@ -116,44 +140,81 @@ export const EntrepreneurProfile: React.FC = () => {
{isEditing && (
-
-
- - - - - - +
+

+ Profile Update +

+ +
+
+ + + + + + +
+
+ + + + + +
+

- 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. + {entrepreneur.marketOpportunity}

@@ -310,11 +368,7 @@ export const EntrepreneurProfile: React.FC = () => {

Competitive Advantage

-

- Unlike our competitors, we offer a unique approach that - combines innovative technology with deep industry expertise, - resulting in superior outcomes for our customers. -

+

{entrepreneur.advantage}

@@ -409,7 +463,7 @@ export const EntrepreneurProfile: React.FC = () => {
Valuation

- $8M - $12M + ${entrepreneur.minValuation} - ${entrepreneur.maxValuation}

diff --git a/src/pages/profile/InvestorProfile.tsx b/src/pages/profile/InvestorProfile.tsx index 3cca86981..874786345 100644 --- a/src/pages/profile/InvestorProfile.tsx +++ b/src/pages/profile/InvestorProfile.tsx @@ -7,19 +7,38 @@ import { UserCircle, BarChart3, Briefcase, + Eraser, + DollarSign, } 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 { getInvestorById } from "../../data/users"; +import { getInvestorById, updateInvestorData } from "../../data/users"; import { Investor } from "../../types"; +import { Input } from "../../components/ui/Input"; export const InvestorProfile: React.FC = () => { const { id } = useParams<{ id: string }>(); + const [isEditing, setIsEditing] = useState(false); const { user: currentUser } = useAuth(); const [investor, setInvestor] = useState(); + const initialData = { + userId: id, + investmentInterests: investor?.investmentInterests || [], + investmentStage: investor?.investmentStage || [], + minimumInvestment: investor?.minimumInvestment, + totalInvestments: investor?.totalInvestments, + maximumInvestment: investor?.maximumInvestment, + investmentCriteria: investor?.investmentCriteria || [], + successfullExits: investor?.successfullExits, + minTimline: investor?.minTimline, + maxTimline: investor?.maxTimline, + interest: "", + criteria: "", + }; + const [formData, setFormData] = useState(initialData); // Fetch investor data useEffect(() => { const fetchInvestors = async () => { @@ -29,6 +48,10 @@ export const InvestorProfile: React.FC = () => { fetchInvestors(); }, []); + useEffect(() => { + setFormData(initialData); + }, [investor]); + if (!currentUser) return null; if (!investor || investor.role !== "investor") { return ( @@ -38,7 +61,7 @@ export const InvestorProfile: React.FC = () => { The investor profile you're looking for doesn't exist or has been removed.

- + @@ -47,10 +70,280 @@ export const InvestorProfile: React.FC = () => { ); } - const isCurrentUser = currentUser?.userId === investor.userId; + const isCurrentUser = + currentUser?.userId === (investor.userId || investor._id); + + const handleChange = (e) => { + const { checked, name, value } = e.target; + if (name === "investmentStage") { + setFormData((prev) => { + const current = prev.investmentStage; + + if (checked) { + return { ...prev, investmentStage: [...current, value] }; + } else { + return { + ...prev, + investmentStage: current.filter((item) => item !== value), + }; + } + }); + } else { + setFormData({ ...formData, [name]: value }); + } + }; + const handleSubmit = async (e) => { + e.preventDefault(); + if ( + formData.maximumInvestment === "" || + formData.minimumInvestment === "" || + formData.investmentInterests?.length === 0 || + formData.investmentStage?.length === 0 + ) { + alert("Please input the rquired data..."); + return; + } + updateInvestorData(formData); + const { + investmentInterests, + investmentStage, + minimumInvestment, + maximumInvestment, + investmentCriteria, + successfullExits, + minTimline, + maxTimline, + } = formData; + setInvestor({ + ...investor, + investmentInterests, + investmentStage, + minimumInvestment, + maximumInvestment, + investmentCriteria, + successfullExits, + minTimline, + maxTimline, + }); + setFormData(initialData); + setIsEditing(false); + }; + + const handleInterests = (e) => { + e.preventDefault(); + const updatedInterests = [ + ...formData.investmentInterests, + formData.interest, + ]; + setFormData({ + ...formData, + investmentInterests: updatedInterests, + interest: "", + }); + }; + + const handleCriteria = (e) => { + e.preventDefault(); + const updateCriteria = [...formData.investmentCriteria, formData.criteria]; + setFormData({ + ...formData, + investmentCriteria: updateCriteria, + criteria: "", + }); + console.log(updateCriteria); + }; return (
+ {isEditing && ( +
+
+

+ Profile Update +

+ +
+
+ + +
+
+ {formData.investmentInterests?.map((item, idx) => ( +
+ {item} + +
+ ))} +
+ + +
+ + + + + + +
+ + +
+
+
+ = 5} + helperText={`${ + 5 - formData.investmentCriteria.length + } Investment Criteria rules can be added..`} + fullWidth + /> + +
+
+ {formData.investmentCriteria?.map((item, idx) => ( +
+ {item} + +
+ ))} +
+ + + + +
+ + +
+
+ +
+
+ )} {/* Profile header */} @@ -95,7 +388,14 @@ export const InvestorProfile: React.FC = () => { )} {isCurrentUser && ( - )} @@ -144,7 +444,7 @@ export const InvestorProfile: React.FC = () => { Investment Stages

- {investor.investmentStage&& + {investor.investmentStage && investor.investmentStage.map((stage, index) => ( {stage} @@ -158,22 +458,12 @@ export const InvestorProfile: React.FC = () => { 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 -
  • + {investor.investmentCriteria?.map((ic, idx) => ( +
  • + + {ic} +
  • + ))}
@@ -233,8 +523,10 @@ export const InvestorProfile: React.FC = () => { Investment Range -

- {investor.minimumInvestment && investor.minimumInvestment} -{" "} +

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

@@ -252,7 +544,9 @@ export const InvestorProfile: React.FC = () => { Typical Investment Timeline -

3-5 years

+

+ {investor.minTimline}-{investor.maxTimline} years +

@@ -260,33 +554,23 @@ export const InvestorProfile: React.FC = () => { Investment Focus
-
- SaaS & B2B -
-
-
-
-
- FinTech -
-
-
-
-
- HealthTech -
+ {investor.investmentInterests && + investor.investmentInterests.map((interest, index) => (
-
-
+ key={index} + className="flex justify-between items-center" + > + + {interest} + +
+
+
+
+ ))}
@@ -309,7 +593,7 @@ export const InvestorProfile: React.FC = () => { Successful Exits

- 4 + {formData.successfullExits}

@@ -337,7 +621,8 @@ export const InvestorProfile: React.FC = () => { Active Investments

- {investor.portfolioCompanies && investor.portfolioCompanies.length} + {investor.portfolioCompanies && + investor.portfolioCompanies.length}

diff --git a/src/types/index.ts b/src/types/index.ts index 355b88cf1..8f782b5c8 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,4 +1,4 @@ -export type UserRole = 'entrepreneur' | 'investor'; +export type UserRole = "entrepreneur" | "investor"; export interface User { userId: string; @@ -6,28 +6,35 @@ export interface User { email: string; role: UserRole; avatarUrl: string; - location:string; + location: string; bio: string; isOnline?: boolean; } export interface Entrepreneur extends User { - startupName: string; - pitchSummary: string; - fundingNeeded: string; - industry: string; - foundedYear: number; - teamSize: number; + startupName: string | undefined; + pitchSummary: string | undefined; + fundingNeeded: string | undefined; + industry: string | undefined; + foundedYear: number | undefined; + teamSize: number | undefined; + minValuation:string | undefined; + maxValuation:string | undefined; + marketOpportunity:string | undefined; + advantage:string | undefined; } export interface Investor extends User { - role: 'investor'; - investmentInterests: string[]; - investmentStage: string[]; - portfolioCompanies: string[]; - totalInvestments: number; - minimumInvestment: string; - maximumInvestment: string; + investmentInterests: string[] | undefined; + investmentStage: string[] | undefined; + portfolioCompanies: string[] | undefined; + totalInvestments: number | undefined; + minimumInvestment: string | undefined; + maximumInvestment: string | undefined; + investmentCriteria: string[] | undefined; + successfullExits: number | undefined; + minTimline:number | undefined, + maxTimline:number | undefined, } export interface Message { @@ -51,7 +58,7 @@ export interface CollaborationRequest { investorId: string; entrepreneurId: string; message: string; - status: 'pending' | 'accepted' | 'rejected'; + status: "pending" | "accepted" | "rejected"; createdAt: string; } @@ -68,13 +75,18 @@ export interface Document { export interface AuthContextType { user: User | null; - userData:User | null; + userData: 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; isAuthenticated: boolean; isLoading: boolean; -} \ No newline at end of file +} From 0f360417676d129583c6b775e2c4695e5d08c0b8 Mon Sep 17 00:00:00 2001 From: Danish-Butt Date: Sat, 30 Aug 2025 22:34:27 +0500 Subject: [PATCH 06/43] setting up the socket.io --- package-lock.json | 92 +++++++++++++++++- package.json | 3 +- src/App.tsx | 1 - src/components/investor/InvestorCard.tsx | 4 +- src/data/messages.ts | 47 ++++----- src/data/users.ts | 13 ++- src/pages/chat/ChatPage.tsx | 119 +++++++++++++---------- 7 files changed, 190 insertions(+), 89 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2f89c6b59..7d980fc21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,8 @@ "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-client": "^4.8.1" }, "devDependencies": { "@eslint/js": "^9.9.1", @@ -1218,6 +1219,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", @@ -1966,7 +1973,6 @@ "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, "dependencies": { "ms": "^2.1.3" }, @@ -2038,6 +2044,28 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, + "node_modules/engine.io-client": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "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/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -3149,8 +3177,7 @@ "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", @@ -3854,6 +3881,34 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "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.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -4423,6 +4478,35 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "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 e9bf404e9..a9be5f4f6 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "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-client": "^4.8.1" }, "devDependencies": { "@eslint/js": "^9.9.1", diff --git a/src/App.tsx b/src/App.tsx index d35daa896..c3684194b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -89,7 +89,6 @@ function App() { {/* Chat Routes */} }> - } /> } /> diff --git a/src/components/investor/InvestorCard.tsx b/src/components/investor/InvestorCard.tsx index 756da07d2..42bb0d59a 100644 --- a/src/components/investor/InvestorCard.tsx +++ b/src/components/investor/InvestorCard.tsx @@ -19,12 +19,12 @@ export const InvestorCard: React.FC = ({ const navigate = useNavigate(); const handleViewProfile = () => { - navigate(`/profile/investor/${investor._id}`); + navigate(`/profile/investor/${investor.userId || investor._id}`); }; const handleMessage = (e: React.MouseEvent) => { e.stopPropagation(); // Prevent card click - navigate(`/chat/${investor.id}`); + navigate(`/chat/${investor.userId || investor._id}`); }; return ( diff --git a/src/data/messages.ts b/src/data/messages.ts index 895d3dcd7..2cc6d9b55 100644 --- a/src/data/messages.ts +++ b/src/data/messages.ts @@ -1,3 +1,4 @@ +import axios from 'axios'; import { Message, ChatConversation } from '../types'; export const messages: Message[] = [ @@ -88,41 +89,29 @@ export const messages: Message[] = [ } ]; +const URL = "http://localhost:5000"; // Helper function to get messages between two users -export const getMessagesBetweenUsers = (user1Id: string, user2Id: string): Message[] => { - return messages.filter( - message => - (message.senderId === user1Id && message.receiverId === user2Id) || - (message.senderId === user2Id && message.receiverId === user1Id) - ).sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()); +export const getMessagesBetweenUsers = async(user1Id: string, user2Id: string): Message[] => { + const res = await axios.get(`${URL}/message/get-message-btw-users/sender?=${user1Id}receiver?=${user2Id}`,{ + withCredentials:true + }); + const {messages} = res.data; + return messages; }; +export const saveMessagesBetweenUsers = async(messages:Any)=> { + await axios.post(`${URL}/message/save-message-btw-users`,messages,{ + withCredentials:true + }); +}; // Helper function to get conversations for a user -export const getConversationsForUser = (userId: string): ChatConversation[] => { +export const getConversationsForUser = async(userId: string): ChatConversation[] => { // Get unique conversation partners - const conversationPartners = new Set(); - - messages.forEach(message => { - if (message.senderId === userId) { - conversationPartners.add(message.receiverId); - } - if (message.receiverId === userId) { - conversationPartners.add(message.senderId); - } + const res = await axios.get(`${URL}/conversation/get-conversations-for-user/${userId}`,{ + withCredentials:true }); - - // Create conversation objects - return Array.from(conversationPartners).map(partnerId => { - const conversationMessages = getMessagesBetweenUsers(userId, partnerId); - const lastMessage = conversationMessages[conversationMessages.length - 1]; - - return { - id: `conv-${userId}-${partnerId}`, - participants: [userId, partnerId], - lastMessage, - updatedAt: lastMessage?.timestamp || new Date().toISOString() - }; - }).sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()); + const {conversation} = res.data; + return conversation; }; // Helper function to send a new message diff --git a/src/data/users.ts b/src/data/users.ts index 04e1b4556..c96322810 100644 --- a/src/data/users.ts +++ b/src/data/users.ts @@ -1,5 +1,4 @@ import axios from "axios"; -import { Entrepreneur, Investor } from "../types"; import toast from "react-hot-toast"; const URL = "http://localhost:5000"; @@ -47,7 +46,7 @@ export const getEnterpreneurById = async (id) => { withCredentials: true, } ); - const {entrepreneur} = res.data; + const { entrepreneur } = res.data; console.log(entrepreneur); return entrepreneur; } catch (err) { @@ -84,3 +83,13 @@ export const updateInvestorData = async (formData: any) => { console.log(err); } }; + +export const getUserFromDb = async (id) => { + try { + const res = await axios.get(`${URL}/user/get-user-by-id/${id}`); + const { user } = res.data; + return user; + } catch (err) { + console.log(err); + } +}; diff --git a/src/pages/chat/ChatPage.tsx b/src/pages/chat/ChatPage.tsx index 9c99583c3..1eb8fb6f7 100644 --- a/src/pages/chat/ChatPage.tsx +++ b/src/pages/chat/ChatPage.tsx @@ -1,72 +1,83 @@ -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 { 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, User } from "../../types"; +import { + getMessagesBetweenUsers, + sendMessage, + getConversationsForUser, +} from "../../data/messages"; +import { MessageCircle } from "lucide-react"; +import { getUserFromDb } from "../../data/users"; export const ChatPage: React.FC = () => { const { userId } = useParams<{ userId: string }>(); const { user: currentUser } = useAuth(); const [messages, setMessages] = useState([]); - const [newMessage, setNewMessage] = useState(''); + const [newMessage, setNewMessage] = useState(""); const [conversations, setConversations] = useState([]); const messagesEndRef = useRef(null); - - const chatPartner = userId ? findUserById(userId) : null; - + const [chatPartner, setChatPartner] = useState(null); + useEffect(() => { + // Load user Data + const fetchUserData = async () => { + const Partner = await getUserFromDb(userId); + setChatPartner(Partner || null); + }; + fetchUserData(); + // Load conversations if (currentUser) { - setConversations(getConversationsForUser(currentUser.id)); + setConversations(getConversationsForUser(currentUser.userId)); } }, [currentUser]); - + useEffect(() => { // Load messages between users if (currentUser && userId) { - setMessages(getMessagesBetweenUsers(currentUser.id, userId)); + setMessages(getMessagesBetweenUsers(currentUser.userId, userId)); } }, [currentUser, userId]); - + useEffect(() => { // Scroll to bottom of messages - messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); - + const handleSendMessage = (e: React.FormEvent) => { e.preventDefault(); - + if (!newMessage.trim() || !currentUser || !userId) return; - + const message = sendMessage({ - senderId: currentUser.id, + senderId: currentUser.userId, receiverId: userId, - content: newMessage + content: newMessage, }); - + setMessages([...messages, message]); - setNewMessage(''); - + setNewMessage(""); + // Update conversations - setConversations(getConversationsForUser(currentUser.id)); + setConversations(getConversationsForUser(currentUser.userId)); }; - + if (!currentUser) return null; - + return (
{/* Conversations sidebar */}
- + {/* Main chat area */}
{/* Chat header */} @@ -78,18 +89,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) => ( ))}
@@ -138,12 +151,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 */}
@@ -156,7 +173,7 @@ export const ChatPage: React.FC = () => { > - + { fullWidth className="flex-1" /> - +
); -}; \ No newline at end of file +}; From f7b758df74cddd36ae21e7e6cc9ce0696a57a0ca Mon Sep 17 00:00:00 2001 From: Danish-Butt Date: Sun, 31 Aug 2025 23:22:50 +0500 Subject: [PATCH 07/43] real chat enabled --- src/components/chat/ChatMessage.tsx | 17 ++- src/data/messages.ts | 195 +++++++++++++++------------- src/pages/chat/ChatPage.tsx | 102 ++++++++++++--- src/types/index.ts | 2 - 4 files changed, 202 insertions(+), 114 deletions(-) diff --git a/src/components/chat/ChatMessage.tsx b/src/components/chat/ChatMessage.tsx index d24ae5ba6..5da1fac80 100644 --- a/src/components/chat/ChatMessage.tsx +++ b/src/components/chat/ChatMessage.tsx @@ -1,7 +1,8 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { formatDistanceToNow } from 'date-fns'; -import { Message } from '../../types'; +import { Message, User } from '../../types'; import { Avatar } from '../ui/Avatar'; +import { getUserFromDb } from '../../data/users'; interface ChatMessageProps { message: Message; @@ -9,7 +10,15 @@ interface ChatMessageProps { } export const ChatMessage: React.FC = ({ message, isCurrentUser }) => { - const user = findUserById(message.senderId); + const [user, setUser] = useState(null); + useEffect(() => { + // Load partner Data + const fetchUserData = async () => { + const user = await getUserFromDb(message.senderId); + setUser(user || null); + }; + fetchUserData(); + }, []); if (!user) return null; @@ -38,7 +47,7 @@ export const ChatMessage: React.FC = ({ message, isCurrentUser - {formatDistanceToNow(new Date(message.timestamp), { addSuffix: true })} + {formatDistanceToNow(new Date(message.time), { addSuffix: true })} diff --git a/src/data/messages.ts b/src/data/messages.ts index 2cc6d9b55..9b2f32506 100644 --- a/src/data/messages.ts +++ b/src/data/messages.ts @@ -1,128 +1,143 @@ -import axios from 'axios'; -import { Message, ChatConversation } from '../types'; +import axios from "axios"; +import { Message, ChatConversation } from "../types"; export const messages: Message[] = [ // Conversation between Sarah (e1) and Michael (i1) { - id: 'm1', - senderId: 'e1', - receiverId: 'i1', - content: 'Thanks for connecting. Id love to discuss how our AI platform can revolutionize financial analytics for SMBs.', - timestamp: '2023-08-15T10:15:00Z', - isRead: true + id: "m1", + senderId: "e1", + receiverId: "i1", + content: + "Thanks for connecting. Id love to discuss how our AI platform can revolutionize financial analytics for SMBs.", + timestamp: "2023-08-15T10:15:00Z", + isRead: true, }, { - id: 'm2', - senderId: 'i1', - receiverId: 'e1', - content: 'Im interested in learning more about your tech stack and ML models. Are you available for a call this week?', - timestamp: '2023-08-15T10:30:00Z', - isRead: true + id: "m2", + senderId: "i1", + receiverId: "e1", + content: + "Im interested in learning more about your tech stack and ML models. Are you available for a call this week?", + timestamp: "2023-08-15T10:30:00Z", + isRead: true, }, { - id: 'm3', - senderId: 'e1', - receiverId: 'i1', - content: 'Absolutely! I can walk you through our technology and current traction. How does Thursday at 2pm PT work?', - timestamp: '2023-08-15T10:45:00Z', - isRead: true + id: "m3", + senderId: "e1", + receiverId: "i1", + content: + "Absolutely! I can walk you through our technology and current traction. How does Thursday at 2pm PT work?", + timestamp: "2023-08-15T10:45:00Z", + isRead: true, }, { - id: 'm4', - senderId: 'i1', - receiverId: 'e1', - content: 'Thursday works great. Ill send a calendar invite. Looking forward to it!', - timestamp: '2023-08-15T11:00:00Z', - isRead: false + id: "m4", + senderId: "i1", + receiverId: "e1", + content: + "Thursday works great. Ill send a calendar invite. Looking forward to it!", + timestamp: "2023-08-15T11:00:00Z", + isRead: false, }, // Conversation between Maya (e3) and Jennifer (i2) { - id: 'm5', - senderId: 'i2', - receiverId: 'e3', - content: 'I saw your pitch for HealthPulse and Im intrigued by your approach to mental healthcare accessibility.', - timestamp: '2023-08-16T09:00:00Z', - isRead: true + id: "m5", + senderId: "i2", + receiverId: "e3", + content: + "I saw your pitch for HealthPulse and Im intrigued by your approach to mental healthcare accessibility.", + timestamp: "2023-08-16T09:00:00Z", + isRead: true, }, { - id: 'm6', - senderId: 'e3', - receiverId: 'i2', - content: 'Thank you, Jennifer! Mental health services need to be more accessible, especially in underserved communities.', - timestamp: '2023-08-16T09:15:00Z', - isRead: true + id: "m6", + senderId: "e3", + receiverId: "i2", + content: + "Thank you, Jennifer! Mental health services need to be more accessible, especially in underserved communities.", + timestamp: "2023-08-16T09:15:00Z", + isRead: true, }, { - id: 'm7', - senderId: 'i2', - receiverId: 'e3', - content: 'I completely agree. Could you share more about your user acquisition strategy and current metrics?', - timestamp: '2023-08-16T09:30:00Z', - isRead: false + id: "m7", + senderId: "i2", + receiverId: "e3", + content: + "I completely agree. Could you share more about your user acquisition strategy and current metrics?", + timestamp: "2023-08-16T09:30:00Z", + isRead: false, }, // Conversation between David (e2) and Robert (i3) { - id: 'm8', - senderId: 'e2', - receiverId: 'i3', - content: 'Hello Robert, I noticed you invest in healthcare. While GreenLife is focused on sustainable packaging, we have some applications in medical supplies.', - timestamp: '2023-08-17T14:00:00Z', - isRead: true + id: "m8", + senderId: "e2", + receiverId: "i3", + content: + "Hello Robert, I noticed you invest in healthcare. While GreenLife is focused on sustainable packaging, we have some applications in medical supplies.", + timestamp: "2023-08-17T14:00:00Z", + isRead: true, }, { - id: 'm9', - senderId: 'i3', - receiverId: 'e2', - content: 'Interesting crossover, David. Id be interested in learning more about your biodegradable materials and how they could be used in healthcare.', - timestamp: '2023-08-17T15:30:00Z', - isRead: true + id: "m9", + senderId: "i3", + receiverId: "e2", + content: + "Interesting crossover, David. Id be interested in learning more about your biodegradable materials and how they could be used in healthcare.", + timestamp: "2023-08-17T15:30:00Z", + isRead: true, }, { - id: 'm10', - senderId: 'e2', - receiverId: 'i3', - content: 'Great! Weve been developing materials that can safely package medical devices while being eco-friendly. Our tests show 40% less environmental impact.', - timestamp: '2023-08-17T16:45:00Z', - isRead: false - } + id: "m10", + senderId: "e2", + receiverId: "i3", + content: + "Great! Weve been developing materials that can safely package medical devices while being eco-friendly. Our tests show 40% less environmental impact.", + timestamp: "2023-08-17T16:45:00Z", + isRead: false, + }, ]; const URL = "http://localhost:5000"; // Helper function to get messages between two users -export const getMessagesBetweenUsers = async(user1Id: string, user2Id: string): Message[] => { - const res = await axios.get(`${URL}/message/get-message-btw-users/sender?=${user1Id}receiver?=${user2Id}`,{ - withCredentials:true - }); - const {messages} = res.data; +export const getMessagesBetweenUsers = async ( + user1Id: string, + user2Id: string +) => { + const res = await axios.get( + `${URL}/message/get-messages-btw-users?sender=${user1Id}&receiver=${user2Id}`, + { + withCredentials: true, + } + ); + const { messages } = res.data; return messages; }; -export const saveMessagesBetweenUsers = async(messages:Any)=> { - await axios.post(`${URL}/message/save-message-btw-users`,messages,{ - withCredentials:true - }); +export const saveMessagesBetweenUsers = async (newMessage: Any) => { + try { + const res = await axios.post(`${URL}/message/save-message`, newMessage, { + withCredentials: true, + }); + console.log("message saved"); + + const { message } = res.data; + return message; + } catch (err) { + console.log(err); + } }; // Helper function to get conversations for a user -export const getConversationsForUser = async(userId: string): ChatConversation[] => { +export const getConversationsForUser = async (userId: string): any[] => { // Get unique conversation partners - const res = await axios.get(`${URL}/conversation/get-conversations-for-user/${userId}`,{ - withCredentials:true - }); - const {conversation} = res.data; - return conversation; + const res = await axios.get( + `${URL}/conversation/get-conversations-for-user/${userId}`, + { + withCredentials: true, + } + ); + const { conversations } = res.data; + return conversations; }; -// Helper function to send a new message -export const sendMessage = (newMessage: Omit): Message => { - const message: Message = { - ...newMessage, - id: `m${messages.length + 1}`, - timestamp: new Date().toISOString(), - isRead: false - }; - - messages.push(message); - return message; -}; \ No newline at end of file diff --git a/src/pages/chat/ChatPage.tsx b/src/pages/chat/ChatPage.tsx index 1eb8fb6f7..ba6b394f4 100644 --- a/src/pages/chat/ChatPage.tsx +++ b/src/pages/chat/ChatPage.tsx @@ -10,11 +10,12 @@ import { useAuth } from "../../context/AuthContext"; import { Message, User } from "../../types"; import { getMessagesBetweenUsers, - sendMessage, getConversationsForUser, + saveMessagesBetweenUsers, } from "../../data/messages"; import { MessageCircle } from "lucide-react"; import { getUserFromDb } from "../../data/users"; +import { io } from "socket.io-client"; export const ChatPage: React.FC = () => { const { userId } = useParams<{ userId: string }>(); @@ -24,49 +25,114 @@ export const ChatPage: React.FC = () => { const [conversations, setConversations] = useState([]); const messagesEndRef = useRef(null); const [chatPartner, setChatPartner] = useState(null); + const socket = useRef(); useEffect(() => { - // Load user Data + // Load partner Data const fetchUserData = async () => { const Partner = await getUserFromDb(userId); setChatPartner(Partner || null); }; fetchUserData(); - // Load conversations - if (currentUser) { - setConversations(getConversationsForUser(currentUser.userId)); - } - }, [currentUser]); + // Load messages + const fetchMessages = async () => { + if (currentUser && userId) { + const messages = await getMessagesBetweenUsers( + currentUser?.userId, + userId + ); + setMessages(messages.length > 0 ? messages : []); + } + }; + fetchMessages(); + }, []); + // Load conversations useEffect(() => { - // Load messages between users - if (currentUser && userId) { - setMessages(getMessagesBetweenUsers(currentUser.userId, userId)); - } - }, [currentUser, userId]); + const fetchConversations = async () => { + if (currentUser) { + const conversations = getConversationsForUser(currentUser.userId); + setConversations(conversations.length > 0 ? conversations : []); + } + }; + fetchConversations(); + }, [currentUser?.userId]); + + // Load messages between users + // useEffect(() => { + // const fetchMessages = async () => { + // if (currentUser && userId) { + // const messages = await getMessagesBetweenUsers( + // currentUser?.userId, + // userId + // ); + // setMessages(messages.length > 0 ? messages : []); + // } + // }; + // fetchMessages(); + // },[]); + + // connect socket.io client + useEffect(() => { + socket.current = io("http://localhost:5000", { + withCredentials: true, + }); + + const handleConnect = () => { + socket.current.emit("join", currentUser?.userId); + }; + + // connect user + socket.current.on("connect", handleConnect); + + // when user receive message + socket.current.on("received-message", (message) => { + console.log(message); + console.log(messages); + setMessages((prev) => [...prev, message]); + }); + + // when user got hi + socket.current.on("hi", () => { + alert("hi"); + }); + + return () => { + if (socket.current) { + socket.current.off("connect", handleConnect); + socket.current.off("hi"); + socket.current.disconnect(); + } + }; + }, [currentUser?.userId]); useEffect(() => { // Scroll to bottom of messages messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); - const handleSendMessage = (e: React.FormEvent) => { + // Hanlde send message + const handleSendMessage = async (e: React.FormEvent) => { e.preventDefault(); if (!newMessage.trim() || !currentUser || !userId) return; - const message = sendMessage({ + const message = { senderId: currentUser.userId, receiverId: userId, content: newMessage, - }); + isRead: false, + }; + const msg = await saveMessagesBetweenUsers(message); + socket.current.emit("send-message", msg); + setMessages((prev) => [...prev, msg]); - setMessages([...messages, message]); setNewMessage(""); // Update conversations - setConversations(getConversationsForUser(currentUser.userId)); + const conversations = getConversationsForUser(currentUser.userId); + setConversations(conversations.length > 0 ? conversations : []); }; if (!currentUser) return null; @@ -139,7 +205,7 @@ export const ChatPage: React.FC = () => {
{messages.map((message) => ( diff --git a/src/types/index.ts b/src/types/index.ts index 8f782b5c8..dabae5ede 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -38,11 +38,9 @@ export interface Investor extends User { } export interface Message { - id: string; senderId: string; receiverId: string; content: string; - timestamp: string; isRead: boolean; } From 58d087cb7998bebe27385c50c032e3082a7f1c58 Mon Sep 17 00:00:00 2001 From: Danish-Butt Date: Tue, 2 Sep 2025 00:00:52 +0500 Subject: [PATCH 08/43] conversations will update dynamically --- src/components/chat/ChatUserList.tsx | 112 +++++++++++++++++---------- src/components/ui/Badge.tsx | 2 +- src/data/messages.ts | 18 ++++- src/pages/auth/LoginPage.tsx | 8 +- src/pages/chat/ChatPage.tsx | 82 ++++++++++---------- 5 files changed, 134 insertions(+), 88 deletions(-) diff --git a/src/components/chat/ChatUserList.tsx b/src/components/chat/ChatUserList.tsx index 6f1fb52f6..970568d5d 100644 --- a/src/components/chat/ChatUserList.tsx +++ b/src/components/chat/ChatUserList.tsx @@ -1,22 +1,46 @@ -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'; +import React, { useEffect, useState } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { formatDistanceToNow } from "date-fns"; +import { ChatConversation, User } from "../../types"; +import { Avatar } from "../ui/Avatar"; +import { Badge } from "../ui/Badge"; +import { useAuth } from "../../context/AuthContext"; +import { getUserFromDb } from "../../data/users"; interface ChatUserListProps { - conversations: ChatConversation[]; + conversation: ChatConversation; } -export const ChatUserList: React.FC = ({ conversations }) => { +export const ChatUserList: React.FC = ({ conversation }) => { const navigate = useNavigate(); const { userId: activeUserId } = useParams<{ userId: string }>(); const { user: currentUser } = useAuth(); - + + const [participants, setParticipants] = useState([]); + + useEffect(() => { + const fetchParticipants = async () => { + if (!conversation?.participants) return; + + // filter out current user + const otherIds = conversation.participants.filter( + (id) => id !== currentUser?.userId + ); + + // fetch all users in parallel + const users = await Promise.all( + otherIds.map(async (id) => await getUserFromDb(id)) + ); + + // filter nulls (if any user not found) + setParticipants(users.filter(Boolean) as User[]); + }; + + fetchParticipants(); + }, [conversation, currentUser]); + + if (conversation === null) return; if (!currentUser) return null; - const handleSelectUser = (userId: string) => { navigate(`/chat/${userId}`); }; @@ -24,63 +48,67 @@ 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); - if (!otherUser) return null; - + {participants.length > 0 ? ( + participants.map((user) => { const lastMessage = conversation.lastMessage; - const isActive = activeUserId === otherParticipantId; - + const isActive = user._id === activeUserId; // highlight current open chat + return (
handleSelectUser(otherUser.id)} + onClick={() => handleSelectUser(user._id)} > - +

- {otherUser.name} + {user.name}

- + {lastMessage && ( - {formatDistanceToNow(new Date(lastMessage.timestamp), { addSuffix: false })} + {formatDistanceToNow( + new Date(conversation?.lastModified), + { addSuffix: false } + )} )}
- +
{lastMessage && (

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

)} - - {lastMessage && !lastMessage.isRead && lastMessage.senderId !== currentUser.id && ( - New - )} + + {lastMessage && + !lastMessage.isRead && + lastMessage.senderId !== currentUser.userId && ( + + New + + )}
@@ -95,4 +123,4 @@ export const ChatUserList: React.FC = ({ conversations }) =>
); -}; \ No newline at end of file +}; diff --git a/src/components/ui/Badge.tsx b/src/components/ui/Badge.tsx index ee33fa675..6fd01081b 100644 --- a/src/components/ui/Badge.tsx +++ b/src/components/ui/Badge.tsx @@ -18,7 +18,7 @@ export const Badge: React.FC = ({ size = 'md', rounded = false, className = '', - onClick={}, + onClick, }) => { const variantClasses = { primary: 'bg-primary-100 text-primary-800', diff --git a/src/data/messages.ts b/src/data/messages.ts index 9b2f32506..8ac7f3f13 100644 --- a/src/data/messages.ts +++ b/src/data/messages.ts @@ -137,7 +137,21 @@ export const getConversationsForUser = async (userId: string): any[] => { withCredentials: true, } ); - const { conversations } = res.data; - return conversations; + const { conversation } = res.data; + console.log(conversation) + return conversation; }; +export const addConversationsForUser = async (con: object): any[] => { + // Get unique conversation partners + const res = await axios.post( + `${URL}/conversation/add-conversations-for-user`, + con, + { + withCredentials: true, + } + ); + const { conversation } = res.data; + console.log(conversation) + return conversation; +}; diff --git a/src/pages/auth/LoginPage.tsx b/src/pages/auth/LoginPage.tsx index e95bff70a..898d57912 100644 --- a/src/pages/auth/LoginPage.tsx +++ b/src/pages/auth/LoginPage.tsx @@ -34,11 +34,11 @@ export const LoginPage: React.FC = () => { // For demo purposes, pre-filled credentials const fillDemoCredentials = (userRole: UserRole) => { if (userRole === 'entrepreneur') { - setEmail('sarah@techwave.io'); - setPassword('password123'); + setEmail('en@gmail.com'); + setPassword('123'); } else { - setEmail('michael@vcinnovate.com'); - setPassword('password123'); + setEmail('in@gmail.com'); + setPassword('123'); } setRole(userRole); }; diff --git a/src/pages/chat/ChatPage.tsx b/src/pages/chat/ChatPage.tsx index ba6b394f4..8310ade4f 100644 --- a/src/pages/chat/ChatPage.tsx +++ b/src/pages/chat/ChatPage.tsx @@ -12,6 +12,7 @@ import { getMessagesBetweenUsers, getConversationsForUser, saveMessagesBetweenUsers, + addConversationsForUser, } from "../../data/messages"; import { MessageCircle } from "lucide-react"; import { getUserFromDb } from "../../data/users"; @@ -22,21 +23,24 @@ export const ChatPage: React.FC = () => { const { user: currentUser } = useAuth(); const [messages, setMessages] = useState([]); const [newMessage, setNewMessage] = useState(""); - const [conversations, setConversations] = useState([]); + const [conversation, setConversation] = useState(); const messagesEndRef = useRef(null); const [chatPartner, setChatPartner] = useState(null); const socket = useRef(); useEffect(() => { - // Load partner Data const fetchUserData = async () => { + // Load conversations + const conv = await getConversationsForUser(currentUser?.userId); + if (conv) { + setConversation(conv); + } + + // Load partner Data const Partner = await getUserFromDb(userId); setChatPartner(Partner || null); - }; - fetchUserData(); - // Load messages - const fetchMessages = async () => { + // Load messages if (currentUser && userId) { const messages = await getMessagesBetweenUsers( currentUser?.userId, @@ -45,33 +49,9 @@ export const ChatPage: React.FC = () => { setMessages(messages.length > 0 ? messages : []); } }; - fetchMessages(); - }, []); - - // Load conversations - useEffect(() => { - const fetchConversations = async () => { - if (currentUser) { - const conversations = getConversationsForUser(currentUser.userId); - setConversations(conversations.length > 0 ? conversations : []); - } - }; - fetchConversations(); - }, [currentUser?.userId]); + fetchUserData(); + }, [userId]); - // Load messages between users - // useEffect(() => { - // const fetchMessages = async () => { - // if (currentUser && userId) { - // const messages = await getMessagesBetweenUsers( - // currentUser?.userId, - // userId - // ); - // setMessages(messages.length > 0 ? messages : []); - // } - // }; - // fetchMessages(); - // },[]); // connect socket.io client useEffect(() => { @@ -88,8 +68,6 @@ export const ChatPage: React.FC = () => { // when user receive message socket.current.on("received-message", (message) => { - console.log(message); - console.log(messages); setMessages((prev) => [...prev, message]); }); @@ -124,15 +102,41 @@ export const ChatPage: React.FC = () => { content: newMessage, isRead: false, }; + const msg = await saveMessagesBetweenUsers(message); socket.current.emit("send-message", msg); setMessages((prev) => [...prev, msg]); - setNewMessage(""); - // Update conversations - const conversations = getConversationsForUser(currentUser.userId); - setConversations(conversations.length > 0 ? conversations : []); + // Update conversation + try { + let updatedConv: any; + + if (conversation && conversation.participants?.length > 0) { + for (const partner of conversation.participants) { + if (partner.userId !== userId) { + const updatedConv = await addConversationsForUser({ + senderId: currentUser?.userId, + receiverId: chatPartner?._id, + lastMessage: msg, + }); + setConversation(updatedConv); + } + } + } else { + updatedConv = await addConversationsForUser({ + senderId: currentUser.userId, + receiverId: chatPartner?._id, + lastMessage: msg, + }); + } + console.log(updatedConv) + if (updatedConv) { + setConversation(updatedConv); + } + } catch (err) { + console.error("Failed to update conversation", err); + } }; if (!currentUser) return null; @@ -141,7 +145,7 @@ export const ChatPage: React.FC = () => {
{/* Conversations sidebar */}
- +
{/* Main chat area */} From d3c69cf6b4fc6aee125c6430c95342e0b9362ef9 Mon Sep 17 00:00:00 2001 From: Danish-Butt Date: Tue, 2 Sep 2025 22:44:31 +0500 Subject: [PATCH 09/43] dynamically messages changed --- src/components/chat/ChatMessage.tsx | 14 +---- src/components/chat/ChatUserList.tsx | 21 ++++--- src/data/messages.ts | 23 +++++-- src/pages/chat/ChatPage.tsx | 89 ++++++++++++++++------------ 4 files changed, 86 insertions(+), 61 deletions(-) diff --git a/src/components/chat/ChatMessage.tsx b/src/components/chat/ChatMessage.tsx index 5da1fac80..da9803d65 100644 --- a/src/components/chat/ChatMessage.tsx +++ b/src/components/chat/ChatMessage.tsx @@ -2,24 +2,14 @@ import React, { useEffect, useState } from 'react'; import { formatDistanceToNow } from 'date-fns'; import { Message, User } from '../../types'; import { Avatar } from '../ui/Avatar'; -import { getUserFromDb } from '../../data/users'; interface ChatMessageProps { message: Message; + user:User | undefined isCurrentUser: boolean; } -export const ChatMessage: React.FC = ({ message, isCurrentUser }) => { - const [user, setUser] = useState(null); - useEffect(() => { - // Load partner Data - const fetchUserData = async () => { - const user = await getUserFromDb(message.senderId); - setUser(user || null); - }; - fetchUserData(); - }, []); - +export const ChatMessage: React.FC = ({ message,user, isCurrentUser }) => { if (!user) return null; return ( diff --git a/src/components/chat/ChatUserList.tsx b/src/components/chat/ChatUserList.tsx index 970568d5d..ec925fc87 100644 --- a/src/components/chat/ChatUserList.tsx +++ b/src/components/chat/ChatUserList.tsx @@ -17,27 +17,34 @@ export const ChatUserList: React.FC = ({ conversation }) => { const { user: currentUser } = useAuth(); const [participants, setParticipants] = useState([]); + const [lastMessage, setLastMessage] = useState({}); useEffect(() => { const fetchParticipants = async () => { if (!conversation?.participants) return; - // filter out current user - const otherIds = conversation.participants.filter( - (id) => id !== currentUser?.userId + const allIds = Array.from( + new Set(conversation.participants.flatMap((i) => i.receiverId || "")) ); + console.log(allIds); // fetch all users in parallel const users = await Promise.all( - otherIds.map(async (id) => await getUserFromDb(id)) + allIds.map(async (id) => await getUserFromDb(id)) ); // filter nulls (if any user not found) setParticipants(users.filter(Boolean) as User[]); + + const lastMessageIndex = conversation.participants.findIndex((part)=>{ + return part.receiverId === activeUserId + }); + + setLastMessage({...conversation.participants[lastMessageIndex].lastMessage}); }; fetchParticipants(); - }, [conversation, currentUser]); + }, [conversation, currentUser,activeUserId]); if (conversation === null) return; if (!currentUser) return null; @@ -55,7 +62,6 @@ export const ChatUserList: React.FC = ({ conversation }) => {
{participants.length > 0 ? ( participants.map((user) => { - const lastMessage = conversation.lastMessage; const isActive = user._id === activeUserId; // highlight current open chat return ( @@ -79,7 +85,8 @@ export const ChatUserList: React.FC = ({ conversation }) => {

- {user.name} + {user.name.slice(0, 5)} + {"..."}

{lastMessage && ( diff --git a/src/data/messages.ts b/src/data/messages.ts index 8ac7f3f13..4555c0be1 100644 --- a/src/data/messages.ts +++ b/src/data/messages.ts @@ -129,7 +129,7 @@ export const saveMessagesBetweenUsers = async (newMessage: Any) => { } }; // Helper function to get conversations for a user -export const getConversationsForUser = async (userId: string): any[] => { +export const getConversationsForUser = async (userId: string | undefined) => { // Get unique conversation partners const res = await axios.get( `${URL}/conversation/get-conversations-for-user/${userId}`, @@ -151,7 +151,22 @@ export const addConversationsForUser = async (con: object): any[] => { withCredentials: true, } ); - const { conversation } = res.data; - console.log(conversation) - return conversation; + const { conversationForSender } = res.data; + console.log(conversationForSender) + return conversationForSender; }; + + +export const updateConversationsForUser = async (con: object): any[] => { + // Get unique conversation partners + const res = await axios.post( + `${URL}/conversation/update-conversations-for-user`, + con, + { + withCredentials: true, + } + ); + const { conversationForSender } = res.data; + console.log(conversationForSender) + return conversationForSender; +}; \ No newline at end of file diff --git a/src/pages/chat/ChatPage.tsx b/src/pages/chat/ChatPage.tsx index 8310ade4f..35d1a8dc3 100644 --- a/src/pages/chat/ChatPage.tsx +++ b/src/pages/chat/ChatPage.tsx @@ -12,7 +12,7 @@ import { getMessagesBetweenUsers, getConversationsForUser, saveMessagesBetweenUsers, - addConversationsForUser, + updateConversationsForUser, } from "../../data/messages"; import { MessageCircle } from "lucide-react"; import { getUserFromDb } from "../../data/users"; @@ -26,32 +26,58 @@ export const ChatPage: React.FC = () => { const [conversation, setConversation] = useState(); const messagesEndRef = useRef(null); const [chatPartner, setChatPartner] = useState(null); + const [users, setUsers] = useState<[string , User][]>([]); const socket = useRef(); + useEffect(()=>{ + const fetchConversation = async()=>{ + // Load conversations + const conv = await getConversationsForUser(currentUser?.userId); + if (conv) setConversation(conv); + } + fetchConversation(); + },[userId,currentUser?.userId,messages]) + useEffect(() => { + if (!currentUser?.userId || !userId) return; // guard clause const fetchUserData = async () => { - // Load conversations - const conv = await getConversationsForUser(currentUser?.userId); - if (conv) { - setConversation(conv); - } + try { + + // Load partner Data + const partner = await getUserFromDb(userId); + setChatPartner(partner || null); - // Load partner Data - const Partner = await getUserFromDb(userId); - setChatPartner(Partner || null); - - // Load messages - if (currentUser && userId) { + // Load messages const messages = await getMessagesBetweenUsers( - currentUser?.userId, + currentUser.userId, userId ); setMessages(messages.length > 0 ? messages : []); + } catch (err) { + console.error("Error fetching user data:", err); } }; + fetchUserData(); - }, [userId]); + }, [currentUser?.userId, userId]); + + useEffect(() => { + const fetchUsers = async () => { + const uniqueIds = Array.from(new Set(messages.map((m) => m.senderId))); + const usersData = await Promise.all( + uniqueIds.map(async (id) => { + const user = await getUserFromDb(id); + return [id, user] as [string, User]; + }) + ); + + setUsers(Object.fromEntries(usersData)); + }; + if (messages.length > 0) { + fetchUsers(); + } + }, [messages]); // connect socket.io client useEffect(() => { @@ -110,27 +136,13 @@ export const ChatPage: React.FC = () => { // Update conversation try { - let updatedConv: any; - - if (conversation && conversation.participants?.length > 0) { - for (const partner of conversation.participants) { - if (partner.userId !== userId) { - const updatedConv = await addConversationsForUser({ - senderId: currentUser?.userId, - receiverId: chatPartner?._id, - lastMessage: msg, - }); - setConversation(updatedConv); - } - } - } else { - updatedConv = await addConversationsForUser({ - senderId: currentUser.userId, - receiverId: chatPartner?._id, - lastMessage: msg, - }); + const con = { + senderId:currentUser?.userId, + receiverId:userId, + lastMessage:{...msg} } - console.log(updatedConv) + const updatedConv = await updateConversationsForUser(con); + console.log(updatedConv); if (updatedConv) { setConversation(updatedConv); } @@ -207,11 +219,12 @@ export const ChatPage: React.FC = () => {
{messages.length > 0 ? (
- {messages.map((message) => ( + {messages.map((msg) => ( ))}
From f8ec362f95f5ba44c232b77de44e2826c170ce65 Mon Sep 17 00:00:00 2001 From: Danish-Butt Date: Wed, 3 Sep 2025 23:37:15 +0500 Subject: [PATCH 10/43] message read updation --- src/components/chat/ChatMessage.tsx | 2 +- src/components/chat/ChatUserList.tsx | 38 +++++++++++------- src/data/messages.ts | 14 +++---- src/pages/chat/ChatPage.tsx | 60 +++++++++++++++++++--------- src/types/index.ts | 5 ++- 5 files changed, 75 insertions(+), 44 deletions(-) diff --git a/src/components/chat/ChatMessage.tsx b/src/components/chat/ChatMessage.tsx index da9803d65..c238686d5 100644 --- a/src/components/chat/ChatMessage.tsx +++ b/src/components/chat/ChatMessage.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { formatDistanceToNow } from 'date-fns'; import { Message, User } from '../../types'; import { Avatar } from '../ui/Avatar'; diff --git a/src/components/chat/ChatUserList.tsx b/src/components/chat/ChatUserList.tsx index ec925fc87..c98d9ea05 100644 --- a/src/components/chat/ChatUserList.tsx +++ b/src/components/chat/ChatUserList.tsx @@ -16,8 +16,7 @@ export const ChatUserList: React.FC = ({ conversation }) => { const { userId: activeUserId } = useParams<{ userId: string }>(); const { user: currentUser } = useAuth(); - const [participants, setParticipants] = useState([]); - const [lastMessage, setLastMessage] = useState({}); + const [chatPartners, setChatPartners] = useState([]); useEffect(() => { const fetchParticipants = async () => { @@ -27,27 +26,23 @@ export const ChatUserList: React.FC = ({ conversation }) => { new Set(conversation.participants.flatMap((i) => i.receiverId || "")) ); - console.log(allIds); - // fetch all users in parallel const users = await Promise.all( allIds.map(async (id) => await getUserFromDb(id)) ); // filter nulls (if any user not found) - setParticipants(users.filter(Boolean) as User[]); + setChatPartners(users.filter(Boolean) as User[]); - const lastMessageIndex = conversation.participants.findIndex((part)=>{ - return part.receiverId === activeUserId - }); - - setLastMessage({...conversation.participants[lastMessageIndex].lastMessage}); + // find the index of active user last message }; fetchParticipants(); - }, [conversation, currentUser,activeUserId]); + }, [conversation, currentUser, activeUserId]); if (conversation === null) return; + if (!currentUser) return null; + const handleSelectUser = (userId: string) => { navigate(`/chat/${userId}`); }; @@ -60,9 +55,22 @@ export const ChatUserList: React.FC = ({ conversation }) => {
- {participants.length > 0 ? ( - participants.map((user) => { + {chatPartners.length > 0 ? ( + chatPartners.map((user) => { const isActive = user._id === activeUserId; // highlight current open chat + let lastMessage; + const lastMessageIndex = conversation.participants.findIndex( + (part) => { + return part.receiverId === user._id; + } + ); + if (lastMessageIndex === -1) { + return; + } else { + lastMessage = { + ...conversation.participants[lastMessageIndex].lastMessage, + }; + } return (
= ({ conversation }) => { {"..."} - {lastMessage && ( + {Object.keys(lastMessage).length !== 0 && ( {formatDistanceToNow( - new Date(conversation?.lastModified), + new Date(lastMessage.time || "Text first"), { addSuffix: false } )} diff --git a/src/data/messages.ts b/src/data/messages.ts index 4555c0be1..4d7e1a4eb 100644 --- a/src/data/messages.ts +++ b/src/data/messages.ts @@ -121,7 +121,6 @@ export const saveMessagesBetweenUsers = async (newMessage: Any) => { withCredentials: true, }); console.log("message saved"); - const { message } = res.data; return message; } catch (err) { @@ -129,16 +128,18 @@ export const saveMessagesBetweenUsers = async (newMessage: Any) => { } }; // Helper function to get conversations for a user -export const getConversationsForUser = async (userId: string | undefined) => { +export const getConversationsForUser = async ( + currentUserId: string | undefined, + partnerId: string | undefined +) => { // Get unique conversation partners const res = await axios.get( - `${URL}/conversation/get-conversations-for-user/${userId}`, + `${URL}/conversation/get-conversations-for-user?currentUserId=${currentUserId}&partnerId=${partnerId}`, { withCredentials: true, } ); const { conversation } = res.data; - console.log(conversation) return conversation; }; @@ -152,11 +153,9 @@ export const addConversationsForUser = async (con: object): any[] => { } ); const { conversationForSender } = res.data; - console.log(conversationForSender) return conversationForSender; }; - export const updateConversationsForUser = async (con: object): any[] => { // Get unique conversation partners const res = await axios.post( @@ -167,6 +166,5 @@ export const updateConversationsForUser = async (con: object): any[] => { } ); const { conversationForSender } = res.data; - console.log(conversationForSender) return conversationForSender; -}; \ No newline at end of file +}; diff --git a/src/pages/chat/ChatPage.tsx b/src/pages/chat/ChatPage.tsx index 35d1a8dc3..922923314 100644 --- a/src/pages/chat/ChatPage.tsx +++ b/src/pages/chat/ChatPage.tsx @@ -23,26 +23,27 @@ export const ChatPage: React.FC = () => { const { user: currentUser } = useAuth(); const [messages, setMessages] = useState([]); const [newMessage, setNewMessage] = useState(""); + const [isTyping, setIsTyping] = useState(false); + const [checkStatus, setCheckStatus] = useState(false); const [conversation, setConversation] = useState(); const messagesEndRef = useRef(null); const [chatPartner, setChatPartner] = useState(null); - const [users, setUsers] = useState<[string , User][]>([]); + const [users, setUsers] = useState<[string, User][]>([]); const socket = useRef(); - useEffect(()=>{ - const fetchConversation = async()=>{ - // Load conversations - const conv = await getConversationsForUser(currentUser?.userId); - if (conv) setConversation(conv); - } + useEffect(() => { + const fetchConversation = async () => { + // Load conversations + const conv = await getConversationsForUser(currentUser?.userId,userId); + if (conv) setConversation(conv); + }; fetchConversation(); - },[userId,currentUser?.userId,messages]) + }, [userId, currentUser?.userId, messages]); useEffect(() => { if (!currentUser?.userId || !userId) return; // guard clause const fetchUserData = async () => { try { - // Load partner Data const partner = await getUserFromDb(userId); setChatPartner(partner || null); @@ -97,11 +98,23 @@ export const ChatPage: React.FC = () => { setMessages((prev) => [...prev, message]); }); + // when user get typing + socket.current.on("is-typing", () => { + setIsTyping(true); + setTimeout(() => { + setIsTyping(false); + }, 2000); + }); + // when user got hi socket.current.on("hi", () => { alert("hi"); }); - + + // when user offline or status changed + socket.current.on("check-user-status",()=>{ + setCheckStatus(!checkStatus); + }) return () => { if (socket.current) { socket.current.off("connect", handleConnect); @@ -137,12 +150,11 @@ export const ChatPage: React.FC = () => { // Update conversation try { const con = { - senderId:currentUser?.userId, - receiverId:userId, - lastMessage:{...msg} - } + sender: currentUser?.userId, + receiver: userId, + lastMessage: { ...msg }, + }; const updatedConv = await updateConversationsForUser(con); - console.log(updatedConv); if (updatedConv) { setConversation(updatedConv); } @@ -179,8 +191,16 @@ export const ChatPage: React.FC = () => {

{chatPartner.name}

-

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

+ {isTyping + ? "is typing" + : chatPartner.isOnline + ? "online" + : "Last seen recently"}

@@ -261,7 +281,11 @@ export const ChatPage: React.FC = () => { type="text" placeholder="Type a message..." value={newMessage} - onChange={(e) => setNewMessage(e.target.value)} + onChange={(e) => { + e.preventDefault(); + socket.current.emit("typing", chatPartner._id); + setNewMessage(e.target.value); + }} fullWidth className="flex-1" /> diff --git a/src/types/index.ts b/src/types/index.ts index dabae5ede..9e9421b5e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -38,10 +38,11 @@ export interface Investor extends User { } export interface Message { - senderId: string; - receiverId: string; + sender: string; + receiver: string; content: string; isRead: boolean; + time:Date, } export interface ChatConversation { From 93b52fda984c1cd15015d31583f94bba76d9012d Mon Sep 17 00:00:00 2001 From: Danish-Butt Date: Sat, 6 Sep 2025 22:33:09 +0500 Subject: [PATCH 11/43] video call implemented --- package-lock.json | 8 +- package.json | 2 +- src/App.tsx | 198 +++++++++++--------- src/components/layout/Navbar.tsx | 3 +- src/components/ui/Avatar.tsx | 4 +- src/components/webRTC/AudioCall.tsx | 88 +++++++++ src/components/webRTC/Videocall.tsx | 189 +++++++++++++++++++ src/components/webrtc/IncomingCallModal.tsx | 36 ++++ src/context/AuthContext.tsx | 9 +- src/context/SocketContext.tsx | 49 +++++ src/pages/chat/ChatPage.tsx | 111 +++++++---- src/pages/dashboard/InvestorDashboard.tsx | 4 +- src/pages/messages/MessagesPage.tsx | 6 +- src/pages/settings/SettingsPage.tsx | 18 +- src/types/index.ts | 4 + 15 files changed, 580 insertions(+), 149 deletions(-) create mode 100644 src/components/webRTC/AudioCall.tsx create mode 100644 src/components/webRTC/Videocall.tsx create mode 100644 src/components/webrtc/IncomingCallModal.tsx create mode 100644 src/context/SocketContext.tsx diff --git a/package-lock.json b/package-lock.json index 7d980fc21..47d13bbc1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-dropzone": "^14.2.3", - "react-hot-toast": "^2.4.1", + "react-hot-toast": "^2.6.0", "react-router-dom": "^6.22.1", "socket.io-client": "^4.8.1" }, @@ -3653,9 +3653,9 @@ } }, "node_modules/react-hot-toast": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.5.2.tgz", - "integrity": "sha512-Tun3BbCxzmXXM7C+NI4qiv6lT0uwGh4oAfeJyNOjYUejTsm35mK9iCaYLGv8cBz9L5YxZLx/2ii7zsIwPtPUdw==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz", + "integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==", "license": "MIT", "dependencies": { "csstype": "^3.1.3", diff --git a/package.json b/package.json index a9be5f4f6..146a53e15 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-dropzone": "^14.2.3", - "react-hot-toast": "^2.4.1", + "react-hot-toast": "^2.6.0", "react-router-dom": "^6.22.1", "socket.io-client": "^4.8.1" }, diff --git a/src/App.tsx b/src/App.tsx index c3684194b..88ecbd68e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,106 +1,132 @@ -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"; +import { SocketProvider } from "./context/SocketContext"; // 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"; // Chat Pages -import { ChatPage } from './pages/chat/ChatPage'; -import { ForgotPasswordPage } from './pages/auth/ForgotPasswordPage'; -import { ResetPasswordPage } from './pages/auth/ResetPasswordPage'; +import { ChatPage } from "./pages/chat/ChatPage"; +import { ForgotPasswordPage } from "./pages/auth/ForgotPasswordPage"; +import { ResetPasswordPage } from "./pages/auth/ResetPasswordPage"; +import { VideoCall } from "./components/webRTC/Videocall"; +import { AudioCall } from "./components/webRTC/AudioCall"; +import { Toaster } from "react-hot-toast"; function App() { return ( + //
+ //

WebRTC Test

+ // + //
- - - {/* 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 */} + }> + } /> + + + }> + } /> + + + }> + } /> + + + }> + } /> + + + }> + } /> + + + }> + } /> + + + }> + } /> + + + }> + + }> + } /> + + + {/* Chat Routes */} + }> + } /> + } /> + } /> + + + {/* Redirect root to login */} + } /> + + {/* Catch all other routes and redirect to login */} + } + /> + + + + ); } -export default App; \ No newline at end of file +export default App; diff --git a/src/components/layout/Navbar.tsx b/src/components/layout/Navbar.tsx index 511c56842..c6fff0434 100644 --- a/src/components/layout/Navbar.tsx +++ b/src/components/layout/Navbar.tsx @@ -7,6 +7,7 @@ import { Button } from '../ui/Button'; export const Navbar: React.FC = () => { const [isMenuOpen, setIsMenuOpen] = useState(false); + const { user, logout } = useAuth(); const navigate = useNavigate(); @@ -26,7 +27,7 @@ export const Navbar: React.FC = () => { // User profile route based on role and ID const profileRoute = user - ? `/profile/${user.role}/${user.id}` + ? `/profile/${user.role}/${user.userId}` : '/login'; const navLinks = [ diff --git a/src/components/ui/Avatar.tsx b/src/components/ui/Avatar.tsx index fb0ab2ec7..1c918486a 100644 --- a/src/components/ui/Avatar.tsx +++ b/src/components/ui/Avatar.tsx @@ -7,7 +7,7 @@ interface AvatarProps { alt: string; size?: AvatarSize; className?: string; - status?: 'online' | 'offline' | 'away' | 'busy'; + status?: "online" | "offline"; } export const Avatar: React.FC = ({ @@ -28,8 +28,6 @@ export const Avatar: React.FC = ({ const statusColors = { online: 'bg-success-500', offline: 'bg-gray-400', - away: 'bg-warning-500', - busy: 'bg-error-500', }; const statusSizes = { diff --git a/src/components/webRTC/AudioCall.tsx b/src/components/webRTC/AudioCall.tsx new file mode 100644 index 000000000..df3791f34 --- /dev/null +++ b/src/components/webRTC/AudioCall.tsx @@ -0,0 +1,88 @@ +import React, { useEffect, useRef, useState } from "react"; +import { useParams } from "react-router-dom"; +import io from "socket.io-client"; + +const socket = io("http://localhost:5000"); + +export const AudioCall = () => { + const {roomId} = useParams(); + const localAudioRef = useRef(null); + const remoteAudioRef = useRef(null); + const pcRef = useRef(null); + + const [joined, setJoined] = useState(false); + + useEffect(() => { + pcRef.current = new RTCPeerConnection(); + + // Handle remote stream + pcRef.current.ontrack = (event) => { + remoteAudioRef.current.srcObject = event.streams[0]; + }; + + // connect the user + if(!joined){ + joinRoom(); + } + + // Send ICE candidate to peer + pcRef.current.onicecandidate = (event) => { + if (event.candidate) { + socket.emit("ice-candidate", { candidate: event.candidate, roomId }); + } + }; + + // Socket listeners + socket.on("offer", async ({ offer }) => { + await pcRef.current.setRemoteDescription( + new RTCSessionDescription(offer) + ); + const answer = await pcRef.current.createAnswer(); + await pcRef.current.setLocalDescription(answer); + socket.emit("answer", { roomId, answer }); + }); + + socket.on("answer", async ({ answer }) => { + await pcRef.current.setRemoteDescription( + new RTCSessionDescription(answer) + ); + }); + + socket.on("ice-candidate", async ({ candidate }) => { + try { + await pcRef.current.addIceCandidate(new RTCIceCandidate(candidate)); + } catch (err) { + console.error("Error adding ICE candidate:", err); + } + }); + return () => { + socket.off("offer"); + socket.off("answer"); + socket.off("ice-candidate"); + }; + }, [roomId]); + + const joinRoom = async() => { + setJoined(true); + + socket.emit("join-room", roomId); + + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + localAudioRef.current.srcObject = stream; + + stream.getTracks().forEach((track) => { + pcRef.current.addTrack(track, stream); + }); + + const offer = await pcRef.current.createOffer(); + await pcRef.current.setLocalDescription(offer); + socket.emit("offer", { roomId, offer }); + }; + + return ( +
+
+ ); +}; diff --git a/src/components/webRTC/Videocall.tsx b/src/components/webRTC/Videocall.tsx new file mode 100644 index 000000000..a21e7949f --- /dev/null +++ b/src/components/webRTC/Videocall.tsx @@ -0,0 +1,189 @@ +// client/src/components/VideoCall.js +import React, { useEffect, useRef, useState } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { useAuth } from "../../context/AuthContext"; +import { useSocket } from "../../context/SocketContext"; +import toast from "react-hot-toast"; + + +export const VideoCall: React.FC = () => { + const { roomId, userId } = useParams(); + + const {socket} = useSocket(); + const { user } = useAuth(); + const localVideoRef = useRef(null); + const remoteVideoRef = useRef(null); + const pcRef = useRef(null); + const navigate = useNavigate(); + + const [joined, setJoined] = useState(false); + + useEffect(() => { + pcRef.current = new RTCPeerConnection(); + + // Handle remote stream + pcRef.current.ontrack = (event) => { + if (remoteVideoRef.current) { + remoteVideoRef.current.srcObject = event.streams[0]; + } + }; + + // Connect user + if (!joined) { + joinRoom(); + } + + // Send ICE candidates to peer + pcRef.current.onicecandidate = (event) => { + if (event.candidate) { + socket?.emit("ice-candidate", { roomId, candidate: event.candidate }); + } + }; + + // *****Socket listeners***** + socket?.on("offer", async ({ offer }) => { + if (pcRef.current) { + await pcRef.current.setRemoteDescription( + new RTCSessionDescription(offer) + ); + const answer = await pcRef.current.createAnswer(); + await pcRef.current.setLocalDescription(answer); + socket?.emit("answer", { roomId, answer }); + } + }); + + // Offer Answer + socket?.on("answer", async ({ answer }) => { + if (pcRef.current) { + await pcRef.current.setRemoteDescription( + new RTCSessionDescription(answer) + ); + } + }); + + // receiver ICE + socket?.on("ice-candidate", async ({ candidate }) => { + try { + await pcRef.current.addIceCandidate(new RTCIceCandidate(candidate)); + } catch (err) { + console.error("Error adding ICE candidate:", err); + } + }); + + // Accepted call + socket?.on("call-accepted", () => { + toast.success("user joined"); + }); + + // Call ended + socket?.on("call-ended", () => { + toast.success("call ended"); + navigate(`/chat/${userId}`); + }); + +// Call rejected + socket?.on("call-rejected", () => { + console.log("uff"); + toast.error("Your call was rejected."); + navigate(`/chat/${userId}`); + }); + + + return () => { + socket?.off("offer"); + socket?.off("call-rejected"); + socket?.off("call-accepted"); + socket?.off("answer"); + socket?.off("ice-candidate"); + }; + }, [roomId]); + + const joinRoom = async () => { + setJoined(true); + socket?.emit("join-room", { roomId }); + + try { + // Get media + const stream = await navigator.mediaDevices.getUserMedia({ + video: true, + }); + if (localVideoRef.current) { + localVideoRef.current.srcObject = stream; + } + + // Add tracks to connection + stream.getTracks().forEach((track) => { + if (pcRef.current) { + pcRef.current.addTrack(track, stream); + } + }); + + // Create offer + if (pcRef.current) { + const offer = await pcRef.current.createOffer(); + await pcRef.current.setLocalDescription(offer); + socket?.emit("offer", { roomId, offer }); + } + + socket?.emit("start-call", { + from: user?.userId, + to: userId, + roomId: roomId, + }); + } catch (error) { + console.log("video call error : " + error); + } + }; + + return ( +
+ {/* Video Area */} +
+ {/* Local Video (small in corner) */} +
+
+ + {/* Remote Video (big) */} +
+ {remoteVideoRef.current !== null ? ( +
+
+ + {/* Control Bar */} +
+ + + +
+
+ ); +}; diff --git a/src/components/webrtc/IncomingCallModal.tsx b/src/components/webrtc/IncomingCallModal.tsx new file mode 100644 index 000000000..788c0037d --- /dev/null +++ b/src/components/webrtc/IncomingCallModal.tsx @@ -0,0 +1,36 @@ +// IncomingCallModal.tsx +import React from "react"; + +type Props = { + from: string; + roomId: string; + onAccept: () => void; + onReject: () => void; +}; + +const IncomingCallModal: React.FC = ({ from, onAccept, onReject }) => { + return ( +
+
+

📞 Incoming Call

+

User {from} is calling you

+
+ + +
+
+
+ ); +}; + +export default IncomingCallModal; diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index cb1337fa3..72991a644 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -14,7 +14,6 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children, }) => { const [user, setUser] = useState(null); - const [userData, setUserData] = useState(null); const [isLoading, setIsLoading] = useState(true); // Check for stored user on initial load @@ -66,8 +65,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ const { token, user } = res.data; localStorage.setItem("token", token); - setUser(user); - setUserData(user); + setUser({...user,isOnline:true}); toast.success("Successfully logged in!"); } catch (error) { toast.error((error as Error).message); @@ -104,7 +102,6 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ toast.success("Account created successfully!"); const { token, user } = res.data; localStorage.setItem("token", token); - setUserData(user); setUser(user); } } catch (error) { @@ -185,7 +182,6 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ // Logout function const logout = (): void => { setUser(null); - setUserData(null); localStorage.removeItem("token"); toast.success("Logged out successfully"); }; @@ -218,7 +214,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ .then((res) => { toast.success("profile updated successfully."); const { user } = res.data; - setUserData(user); + setUser(user); }) .catch((err) => { console.log(err); @@ -227,7 +223,6 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ const value = { user, - userData, login, register, logout, diff --git a/src/context/SocketContext.tsx b/src/context/SocketContext.tsx new file mode 100644 index 000000000..ee87d0ff6 --- /dev/null +++ b/src/context/SocketContext.tsx @@ -0,0 +1,49 @@ +// SocketContext.tsx +import React, { createContext, useContext, useEffect, useState } from "react"; +import { io, Socket } from "socket.io-client"; +import { useAuth } from "./AuthContext"; // your existing auth context + +type SocketContextType = { + socket: Socket | null; +}; + +const SocketContext = createContext({ socket: null }); +export const useSocket = () => useContext(SocketContext); + +export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + const { user } = useAuth(); // logged in user + const [socket, setSocket] = useState(null); + + useEffect(() => { + if (user) { + // connect socket after login + const s = io("http://localhost:5000", { + withCredentials: true, + transports: ["websocket", "polling"], + }); + + s.on("connect", () => { + console.log("Connected to socket:", s.id); + // tell backend this userId is online + s.emit("join", user.userId); + }); + + setSocket(s); + + // cleanup on unmount or logout + return () => { + s.disconnect(); + setSocket(null); + console.log(" Socket disconnected"); + }; + } + }, [user]); + + return ( + + {children} + + ); +}; diff --git a/src/pages/chat/ChatPage.tsx b/src/pages/chat/ChatPage.tsx index 922923314..cefd46763 100644 --- a/src/pages/chat/ChatPage.tsx +++ b/src/pages/chat/ChatPage.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useRef } from "react"; -import { useParams } from "react-router-dom"; +import { useNavigate, 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"; @@ -16,7 +16,8 @@ import { } from "../../data/messages"; import { MessageCircle } from "lucide-react"; import { getUserFromDb } from "../../data/users"; -import { io } from "socket.io-client"; +import { useSocket } from "../../context/SocketContext"; +import IncomingCallModal from "../../components/webrtc/IncomingCallModal"; export const ChatPage: React.FC = () => { const { userId } = useParams<{ userId: string }>(); @@ -29,17 +30,41 @@ export const ChatPage: React.FC = () => { const messagesEndRef = useRef(null); const [chatPartner, setChatPartner] = useState(null); const [users, setUsers] = useState<[string, User][]>([]); - const socket = useRef(); + const { socket } = useSocket(); + const [incomingCall, setIncomingCall] = useState<{ + from: string; + roomId: string; + } | null>(null); + const navigate = useNavigate(); + + const acceptCall = () => { + if (incomingCall) { + socket?.emit("accept-call", { + from: incomingCall.from, + }); + navigate(`video-call/${incomingCall.roomId}`); + setIncomingCall(null); + } + }; + + const rejectCall = () => { + if (incomingCall) { + socket?.emit("reject-call", { from: incomingCall.from }); + setIncomingCall(null); + } + }; + + // Load conversations useEffect(() => { const fetchConversation = async () => { - // Load conversations - const conv = await getConversationsForUser(currentUser?.userId,userId); + const conv = await getConversationsForUser(currentUser?.userId, userId); if (conv) setConversation(conv); }; fetchConversation(); }, [userId, currentUser?.userId, messages]); + // Fetch Partner Data, messages useEffect(() => { if (!currentUser?.userId || !userId) return; // guard clause const fetchUserData = async () => { @@ -62,6 +87,7 @@ export const ChatPage: React.FC = () => { fetchUserData(); }, [currentUser?.userId, userId]); + // Set Last messages for each conversation useEffect(() => { const fetchUsers = async () => { const uniqueIds = Array.from(new Set(messages.map((m) => m.senderId))); @@ -80,47 +106,35 @@ export const ChatPage: React.FC = () => { } }, [messages]); - // connect socket.io client + // Connect socket.io client useEffect(() => { - socket.current = io("http://localhost:5000", { - withCredentials: true, + socket?.on("incoming-call", ({ from, roomId }) => { + setIncomingCall({ from, roomId }); }); - const handleConnect = () => { - socket.current.emit("join", currentUser?.userId); - }; - - // connect user - socket.current.on("connect", handleConnect); - // when user receive message - socket.current.on("received-message", (message) => { + socket?.on("received-message", (message) => { setMessages((prev) => [...prev, message]); }); // when user get typing - socket.current.on("is-typing", () => { + socket?.on("is-typing", () => { setIsTyping(true); setTimeout(() => { setIsTyping(false); }, 2000); }); - // when user got hi - socket.current.on("hi", () => { - alert("hi"); - }); - // when user offline or status changed - socket.current.on("check-user-status",()=>{ + socket?.on("check-user-status", () => { setCheckStatus(!checkStatus); - }) + }); return () => { - if (socket.current) { - socket.current.off("connect", handleConnect); - socket.current.off("hi"); - socket.current.disconnect(); - } + socket?.off("send-messsage"); + socket?.off("received-messsage"); + socket?.off("incoming-call"); + socket?.off("accept-call"); + socket?.off("reject-call"); }; }, [currentUser?.userId]); @@ -143,7 +157,7 @@ export const ChatPage: React.FC = () => { }; const msg = await saveMessagesBetweenUsers(message); - socket.current.emit("send-message", msg); + socket?.emit("send-message", msg); setMessages((prev) => [...prev, msg]); setNewMessage(""); @@ -167,6 +181,19 @@ export const ChatPage: React.FC = () => { return (
+ {incomingCall && ( +
+ {/* Chat UI */} +

Chat & Calls

+ + +
+ )} {/* Conversations sidebar */}
@@ -193,7 +220,9 @@ export const ChatPage: React.FC = () => {

{isTyping @@ -211,6 +240,15 @@ export const ChatPage: React.FC = () => { size="sm" className="rounded-full p-2" aria-label="Voice call" + onClick={(e) => { + e.preventDefault(); + navigate( + `audio-call/${currentUser?.userId.slice( + 0, + 5 + )}&${chatPartner?._id.slice(0, 5)}` + ); + }} > @@ -220,6 +258,15 @@ export const ChatPage: React.FC = () => { size="sm" className="rounded-full p-2" aria-label="Video call" + onClick={(e) => { + e.preventDefault(); + navigate( + `video-call/${currentUser?.userId.slice( + 0, + 5 + )}&${chatPartner?._id.slice(0, 5)}` + ); + }} >

- {/* */} +

No messages yet

diff --git a/src/pages/settings/SettingsPage.tsx b/src/pages/settings/SettingsPage.tsx index d0391131f..9d11cdf66 100644 --- a/src/pages/settings/SettingsPage.tsx +++ b/src/pages/settings/SettingsPage.tsx @@ -9,17 +9,17 @@ import { useAuth } from "../../context/AuthContext"; import { Navigate } from "react-router-dom"; export const SettingsPage: React.FC = () => { - const { user, updateProfile, userData } = useAuth(); + const { user, updateProfile} = useAuth(); - if (!user || !userData) return null; + if (!user ) return null; const initialValues = { - name: userData.name, - email: userData.email, - role: userData.role, - bio: userData.bio || "", - location: userData.location || "", - avatarUrl: userData.avatarUrl || "", + name: user?.name, + email: user?.email, + role: user?.role, + bio: user?.bio || "", + location: user?.location || "", + avatarUrl: user?.avatarUrl || "", }; const [userDetails, setUserDetails] = useState(initialValues); const [isFileUploaded, setIsFileUploaded] = useState(false); @@ -35,7 +35,7 @@ export const SettingsPage: React.FC = () => { const handleSubmit = async (e: Event) => { e.preventDefault(); - updateProfile(userData.userId, userDetails); + updateProfile(user?.userId, userDetails); }; const handleCancel = (e) => { e.preventDefault(); diff --git a/src/types/index.ts b/src/types/index.ts index 9e9421b5e..fafe06fc3 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -89,3 +89,7 @@ export interface AuthContextType { isAuthenticated: boolean; isLoading: boolean; } + +export interface Socketcontext{ + socket:string | null; +} \ No newline at end of file From debc0e26dc3ba9dda1691ce00f045bfbbf8e6c17 Mon Sep 17 00:00:00 2001 From: Danish-Butt Date: Sun, 7 Sep 2025 22:20:05 +0500 Subject: [PATCH 12/43] video call ended except design --- src/components/webRTC/Videocall.tsx | 203 ++++++++++++++++++---------- src/pages/chat/ChatPage.tsx | 4 +- 2 files changed, 130 insertions(+), 77 deletions(-) diff --git a/src/components/webRTC/Videocall.tsx b/src/components/webRTC/Videocall.tsx index a21e7949f..607e74b41 100644 --- a/src/components/webRTC/Videocall.tsx +++ b/src/components/webRTC/Videocall.tsx @@ -1,22 +1,21 @@ -// client/src/components/VideoCall.js import React, { useEffect, useRef, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { useAuth } from "../../context/AuthContext"; import { useSocket } from "../../context/SocketContext"; import toast from "react-hot-toast"; - export const VideoCall: React.FC = () => { const { roomId, userId } = useParams(); - - const {socket} = useSocket(); + const { socket } = useSocket(); const { user } = useAuth(); - const localVideoRef = useRef(null); - const remoteVideoRef = useRef(null); - const pcRef = useRef(null); + + const localVideoRef = useRef(null); + const remoteVideoRef = useRef(null); + const pcRef = useRef(null); const navigate = useNavigate(); const [joined, setJoined] = useState(false); + const [isMuted, setIsMuted] = useState(false); useEffect(() => { pcRef.current = new RTCPeerConnection(); @@ -28,73 +27,78 @@ export const VideoCall: React.FC = () => { } }; - // Connect user - if (!joined) { - joinRoom(); - } - - // Send ICE candidates to peer + // ICE candidates → send to other peer pcRef.current.onicecandidate = (event) => { if (event.candidate) { socket?.emit("ice-candidate", { roomId, candidate: event.candidate }); } }; - // *****Socket listeners***** + // ===== SOCKET LISTENERS ===== socket?.on("offer", async ({ offer }) => { - if (pcRef.current) { - await pcRef.current.setRemoteDescription( + try { + await pcRef.current?.setRemoteDescription( new RTCSessionDescription(offer) ); - const answer = await pcRef.current.createAnswer(); - await pcRef.current.setLocalDescription(answer); - socket?.emit("answer", { roomId, answer }); + // Create answer + const answer = await pcRef.current?.createAnswer(); + await pcRef.current?.setLocalDescription(answer); + socket.emit("answer", { roomId, answer }); + } catch (err) { + console.error("Error handling offer:", err); } }); - // Offer Answer socket?.on("answer", async ({ answer }) => { - if (pcRef.current) { - await pcRef.current.setRemoteDescription( - new RTCSessionDescription(answer) - ); + try { + if (pcRef.current && !pcRef.current.remoteDescription) { + await pcRef.current.setRemoteDescription( + new RTCSessionDescription(answer) + ); + } + } catch (err) { + console.error("Error setting remote description:", err); } }); - // receiver ICE socket?.on("ice-candidate", async ({ candidate }) => { try { - await pcRef.current.addIceCandidate(new RTCIceCandidate(candidate)); + if (pcRef.current?.remoteDescription) { + await pcRef.current.addIceCandidate(new RTCIceCandidate(candidate)); + } } catch (err) { console.error("Error adding ICE candidate:", err); } }); - // Accepted call socket?.on("call-accepted", () => { - toast.success("user joined"); + toast.success("Call accepted"); }); - // Call ended socket?.on("call-ended", () => { - toast.success("call ended"); + toast.success("Call ended"); navigate(`/chat/${userId}`); }); -// Call rejected socket?.on("call-rejected", () => { - console.log("uff"); - toast.error("Your call was rejected."); + toast.error("Your call is declined."); navigate(`/chat/${userId}`); }); + socket?.on("receiver-offline", () => { + toast.error("The receiver is offline."); + navigate(`/chat/${userId}`); + }); + + if (!joined) joinRoom(); return () => { socket?.off("offer"); - socket?.off("call-rejected"); - socket?.off("call-accepted"); socket?.off("answer"); socket?.off("ice-candidate"); + socket?.off("call-accepted"); + socket?.off("call-rejected"); + socket?.off("call-ended"); }; }, [roomId]); @@ -106,32 +110,29 @@ export const VideoCall: React.FC = () => { // Get media const stream = await navigator.mediaDevices.getUserMedia({ video: true, + audio: true, }); if (localVideoRef.current) { localVideoRef.current.srcObject = stream; } - // Add tracks to connection - stream.getTracks().forEach((track) => { - if (pcRef.current) { - pcRef.current.addTrack(track, stream); - } - }); + // Add tracks + if (pcRef.current?.getSenders().length === 0) { + stream + .getTracks() + .forEach((track) => pcRef.current?.addTrack(track, stream)); + } - // Create offer - if (pcRef.current) { - const offer = await pcRef.current.createOffer(); - await pcRef.current.setLocalDescription(offer); + // Caller creates offer + if (user?.userId !== userId) { + // Only caller + const offer = await pcRef.current?.createOffer(); + await pcRef.current?.setLocalDescription(offer); socket?.emit("offer", { roomId, offer }); + socket?.emit("start-call", { from: user?.userId, to: userId, roomId }); } - - socket?.emit("start-call", { - from: user?.userId, - to: userId, - roomId: roomId, - }); } catch (error) { - console.log("video call error : " + error); + console.log("Video call error:", error); } }; @@ -139,49 +140,101 @@ export const VideoCall: React.FC = () => {

{/* Video Area */}
- {/* Local Video (small in corner) */} -
+ {/* Local Video */} +
- {/* Remote Video (big) */} + {/* Remote Video */}
- {remoteVideoRef.current !== null ? ( -
{/* Control Bar */}
- - -
diff --git a/src/pages/chat/ChatPage.tsx b/src/pages/chat/ChatPage.tsx index cefd46763..5f867f73f 100644 --- a/src/pages/chat/ChatPage.tsx +++ b/src/pages/chat/ChatPage.tsx @@ -41,7 +41,7 @@ export const ChatPage: React.FC = () => { const acceptCall = () => { if (incomingCall) { socket?.emit("accept-call", { - from: incomingCall.from, + to: incomingCall.from, }); navigate(`video-call/${incomingCall.roomId}`); setIncomingCall(null); @@ -50,7 +50,7 @@ export const ChatPage: React.FC = () => { const rejectCall = () => { if (incomingCall) { - socket?.emit("reject-call", { from: incomingCall.from }); + socket?.emit("reject-call", { to: incomingCall.from }); setIncomingCall(null); } }; From 8a0335642c11b98cd107a7be77c9dfbb1b0e3f4d Mon Sep 17 00:00:00 2001 From: Danish-Butt Date: Mon, 8 Sep 2025 21:49:20 +0500 Subject: [PATCH 13/43] calls added --- src/components/webRTC/AudioCall.tsx | 237 ++++++++++++++++++++++------ src/components/webRTC/Videocall.tsx | 104 ++++++------ src/pages/chat/ChatPage.tsx | 2 +- 3 files changed, 237 insertions(+), 106 deletions(-) diff --git a/src/components/webRTC/AudioCall.tsx b/src/components/webRTC/AudioCall.tsx index df3791f34..0d7eac472 100644 --- a/src/components/webRTC/AudioCall.tsx +++ b/src/components/webRTC/AudioCall.tsx @@ -1,88 +1,227 @@ import React, { useEffect, useRef, useState } from "react"; -import { useParams } from "react-router-dom"; -import io from "socket.io-client"; +import { useNavigate, useParams } from "react-router-dom"; +import { useAuth } from "../../context/AuthContext"; +import { useSocket } from "../../context/SocketContext"; +import toast from "react-hot-toast"; -const socket = io("http://localhost:5000"); +export const AudioCall: React.FC = () => { + const { roomId, userId } = useParams(); + const { socket } = useSocket(); + const { user } = useAuth(); -export const AudioCall = () => { - const {roomId} = useParams(); - const localAudioRef = useRef(null); - const remoteAudioRef = useRef(null); - const pcRef = useRef(null); + const localAudioRef = useRef(null); + const remoteAudioRef = useRef(null); + const pcRef = useRef(null); + const navigate = useNavigate(); const [joined, setJoined] = useState(false); + const [isMuted, setIsMuted] = useState(false); useEffect(() => { pcRef.current = new RTCPeerConnection(); - // Handle remote stream + // Handle remote stream pcRef.current.ontrack = (event) => { - remoteAudioRef.current.srcObject = event.streams[0]; + if (remoteAudioRef.current) { + remoteAudioRef.current.srcObject = event.streams[0]; + } }; - // connect the user - if(!joined){ - joinRoom(); - } - - // Send ICE candidate to peer + // ICE candidates → send to other peer pcRef.current.onicecandidate = (event) => { if (event.candidate) { - socket.emit("ice-candidate", { candidate: event.candidate, roomId }); + socket?.emit("ice-candidate", { roomId, candidate: event.candidate }); + } + }; + + const stopStream = () => { + const stream = localAudioRef.current?.srcObject as MediaStream; + if (stream) { + stream.getTracks().forEach((track) => { + track.stop(); // stops both mic and camera + }); + if (localAudioRef.current) { + localAudioRef.current.srcObject = null; + } } + + // Close peer connection + pcRef.current?.close(); + pcRef.current = null; + socket?.emit("end-call", { to: userId, roomId }); }; - // Socket listeners - socket.on("offer", async ({ offer }) => { - await pcRef.current.setRemoteDescription( - new RTCSessionDescription(offer) - ); - const answer = await pcRef.current.createAnswer(); - await pcRef.current.setLocalDescription(answer); - socket.emit("answer", { roomId, answer }); + // ===== SOCKET LISTENERS ===== + socket?.on("offer", async ({ offer }) => { + try { + await pcRef.current?.setRemoteDescription( + new RTCSessionDescription(offer) + ); + // Create answer + const answer = await pcRef.current?.createAnswer(); + await pcRef.current?.setLocalDescription(answer); + socket.emit("answer", { roomId, answer }); + } catch (err) { + console.error("Error handling offer:", err); + } }); - socket.on("answer", async ({ answer }) => { - await pcRef.current.setRemoteDescription( - new RTCSessionDescription(answer) - ); + socket?.on("answer", async ({ answer }) => { + try { + if (pcRef.current && !pcRef.current.remoteDescription) { + await pcRef.current.setRemoteDescription( + new RTCSessionDescription(answer) + ); + } + } catch (err) { + console.error("Error setting remote description:", err); + } }); - socket.on("ice-candidate", async ({ candidate }) => { + socket?.on("ice-candidate", async ({ candidate }) => { try { - await pcRef.current.addIceCandidate(new RTCIceCandidate(candidate)); + if (pcRef.current?.remoteDescription) { + await pcRef.current.addIceCandidate(new RTCIceCandidate(candidate)); + } } catch (err) { console.error("Error adding ICE candidate:", err); } }); + + socket?.on("call-accepted", () => { + toast.success("Call accepted"); + }); + + socket?.on("call-ended", () => { + toast.success("Call ended"); + navigate(`/chat/${userId}`); + }); + + socket?.on("call-rejected", () => { + toast.error("Your call is declined."); + navigate(`/chat/${userId}`); + }); + + socket?.on("receiver-offline", () => { + toast.error("The receiver is offline."); + navigate(`/chat/${userId}`); + }); + + if (!joined) joinRoom(); + return () => { - socket.off("offer"); - socket.off("answer"); - socket.off("ice-candidate"); + socket?.off("offer"); + socket?.off("answer"); + socket?.off("ice-candidate"); + socket?.off("call-accepted"); + socket?.off("call-rejected"); + socket?.off("call-ended"); }; }, [roomId]); - const joinRoom = async() => { + const joinRoom = async () => { setJoined(true); + socket?.emit("join-room", { roomId }); + + try { + // Get media + const stream = await navigator.mediaDevices.getUserMedia({ + audio: true, + }); + if (localAudioRef.current) { + localAudioRef.current.srcObject = stream; + } - socket.emit("join-room", roomId); - - const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); - localAudioRef.current.srcObject = stream; - - stream.getTracks().forEach((track) => { - pcRef.current.addTrack(track, stream); - }); + // Add tracks + if (pcRef.current?.getSenders().length === 0) { + stream + .getTracks() + .forEach((track) => pcRef.current?.addTrack(track, stream)); + } - const offer = await pcRef.current.createOffer(); - await pcRef.current.setLocalDescription(offer); - socket.emit("offer", { roomId, offer }); + // Caller creates offer + if (user?.userId !== userId) { + // Only caller + const offer = await pcRef.current?.createOffer(); + await pcRef.current?.setLocalDescription(offer); + socket?.emit("offer", { roomId, offer }); + socket?.emit("start-call", { from: user?.userId, to: userId, roomId }); + } + } catch (error) { + console.log("Audio call error:", error); + } }; return ( -
-
@@ -174,67 +191,42 @@ export const VideoCall: React.FC = () => {
- {/* Mic Toggle */} - - - {/* Camera Toggle */} -
diff --git a/src/pages/chat/ChatPage.tsx b/src/pages/chat/ChatPage.tsx index 5f867f73f..411afd24d 100644 --- a/src/pages/chat/ChatPage.tsx +++ b/src/pages/chat/ChatPage.tsx @@ -43,7 +43,7 @@ export const ChatPage: React.FC = () => { socket?.emit("accept-call", { to: incomingCall.from, }); - navigate(`video-call/${incomingCall.roomId}`); + navigate(`audio-call/${incomingCall.roomId}`); setIncomingCall(null); } }; From eb81a752433b39ad2f366b5e3bd054d53ae5cbcd Mon Sep 17 00:00:00 2001 From: Danish-Butt Date: Mon, 8 Sep 2025 22:13:35 +0500 Subject: [PATCH 14/43] incoming modal over app --- src/App.tsx | 8 +- src/components/layout/DashboardLayout.tsx | 96 ++++++++++++++----- src/pages/chat/ChatPage.tsx | 62 +++++------- .../webRTC/AudioCall.tsx | 0 .../webRTC/Videocall.tsx | 0 5 files changed, 97 insertions(+), 69 deletions(-) rename src/{components => pages}/webRTC/AudioCall.tsx (100%) rename src/{components => pages}/webRTC/Videocall.tsx (100%) diff --git a/src/App.tsx b/src/App.tsx index 88ecbd68e..60cc78901 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -36,16 +36,12 @@ import { DealsPage } from "./pages/deals/DealsPage"; import { ChatPage } from "./pages/chat/ChatPage"; import { ForgotPasswordPage } from "./pages/auth/ForgotPasswordPage"; import { ResetPasswordPage } from "./pages/auth/ResetPasswordPage"; -import { VideoCall } from "./components/webRTC/Videocall"; -import { AudioCall } from "./components/webRTC/AudioCall"; +import { VideoCall } from "./pages/webRTC/Videocall"; +import { AudioCall } from "./pages/webRTC/AudioCall"; import { Toaster } from "react-hot-toast"; function App() { return ( - //
- //

WebRTC Test

- // - //
diff --git a/src/components/layout/DashboardLayout.tsx b/src/components/layout/DashboardLayout.tsx index be574357b..201d5d6f3 100644 --- a/src/components/layout/DashboardLayout.tsx +++ b/src/components/layout/DashboardLayout.tsx @@ -1,36 +1,80 @@ -import React from "react"; -import { Outlet, Navigate } from "react-router-dom"; +import React, { useEffect, useState } from "react"; +import { Outlet, Navigate, useNavigate } from "react-router-dom"; import { useAuth } from "../../context/AuthContext"; import { Navbar } from "./Navbar"; import { Sidebar } from "./Sidebar"; +import { useSocket } from "../../context/SocketContext"; +import IncomingCallModal from "../webrtc/IncomingCallModal"; export const DashboardLayout: React.FC = () => { const { isAuthenticated, isLoading } = useAuth(); - if (isLoading) { - return ( -
-
-
- ); - } - if (!isAuthenticated) { - return ; - } - - + const { socket } = useSocket(); + const navigate = useNavigate(); + + const [incomingCall, setIncomingCall] = useState<{ + from: string; + roomId: string; + } | null>(null); + + if (isLoading) { return ( -
- - -
- - -
-
- -
-
-
+
+
); + } + if (!isAuthenticated) { + return ; + } + + const acceptCall = () => { + if (incomingCall) { + socket?.emit("accept-call", { to: incomingCall.from }); + navigate(`/chat/${incomingCall.from}/audio-call/${incomingCall.roomId}`); + setIncomingCall(null); + } + }; + + const rejectCall = () => { + if (incomingCall) { + socket?.emit("reject-call", { to: incomingCall.from }); + setIncomingCall(null); + } + }; + + useEffect(() => { + const handleIncoming = ({ from, roomId }: { from: string; roomId: string }) => { + setIncomingCall({ from, roomId }); + }; + + socket?.on("incoming-call", handleIncoming); + return () => { + socket?.off("incoming-call", handleIncoming); + }; + }, [socket]); + + return ( +
+ + {incomingCall && ( + + )} + + + +
+ +
+
+ +
+
+
+
+ ); }; diff --git a/src/pages/chat/ChatPage.tsx b/src/pages/chat/ChatPage.tsx index 411afd24d..529802d12 100644 --- a/src/pages/chat/ChatPage.tsx +++ b/src/pages/chat/ChatPage.tsx @@ -32,28 +32,28 @@ export const ChatPage: React.FC = () => { const [users, setUsers] = useState<[string, User][]>([]); const { socket } = useSocket(); - const [incomingCall, setIncomingCall] = useState<{ - from: string; - roomId: string; - } | null>(null); + // const [incomingCall, setIncomingCall] = useState<{ + // from: string; + // roomId: string; + // } | null>(null); const navigate = useNavigate(); - const acceptCall = () => { - if (incomingCall) { - socket?.emit("accept-call", { - to: incomingCall.from, - }); - navigate(`audio-call/${incomingCall.roomId}`); - setIncomingCall(null); - } - }; - - const rejectCall = () => { - if (incomingCall) { - socket?.emit("reject-call", { to: incomingCall.from }); - setIncomingCall(null); - } - }; + // const acceptCall = () => { + // if (incomingCall) { + // socket?.emit("accept-call", { + // to: incomingCall.from, + // }); + // navigate(`audio-call/${incomingCall.roomId}`); + // setIncomingCall(null); + // } + // }; + + // const rejectCall = () => { + // if (incomingCall) { + // socket?.emit("reject-call", { to: incomingCall.from }); + // setIncomingCall(null); + // } + // }; // Load conversations useEffect(() => { @@ -108,9 +108,9 @@ export const ChatPage: React.FC = () => { // Connect socket.io client useEffect(() => { - socket?.on("incoming-call", ({ from, roomId }) => { - setIncomingCall({ from, roomId }); - }); + // socket?.on("incoming-call", ({ from, roomId }) => { + // setIncomingCall({ from, roomId }); + // }); // when user receive message socket?.on("received-message", (message) => { @@ -132,7 +132,7 @@ export const ChatPage: React.FC = () => { return () => { socket?.off("send-messsage"); socket?.off("received-messsage"); - socket?.off("incoming-call"); + // socket?.off("incoming-call"); socket?.off("accept-call"); socket?.off("reject-call"); }; @@ -181,19 +181,7 @@ export const ChatPage: React.FC = () => { return (
- {incomingCall && ( -
- {/* Chat UI */} -

Chat & Calls

- - -
- )} + {/* Conversations sidebar */}
diff --git a/src/components/webRTC/AudioCall.tsx b/src/pages/webRTC/AudioCall.tsx similarity index 100% rename from src/components/webRTC/AudioCall.tsx rename to src/pages/webRTC/AudioCall.tsx diff --git a/src/components/webRTC/Videocall.tsx b/src/pages/webRTC/Videocall.tsx similarity index 100% rename from src/components/webRTC/Videocall.tsx rename to src/pages/webRTC/Videocall.tsx From c7169062ccfe02c49099eaae9d3787f71014fe93 Mon Sep 17 00:00:00 2001 From: Danish-Butt Date: Tue, 9 Sep 2025 16:43:30 +0500 Subject: [PATCH 15/43] collaboration requests added --- .../CollaborationRequestCard.tsx | 35 ++-- src/data/collaborationRequests.ts | 175 +++++++++++------- src/pages/dashboard/EntrepreneurDashboard.tsx | 46 +++-- src/pages/dashboard/InvestorDashboard.tsx | 164 +++++++++------- src/pages/profile/EntrepreneurProfile.tsx | 152 +++++++-------- src/types/index.ts | 9 +- 6 files changed, 347 insertions(+), 234 deletions(-) diff --git a/src/components/collaboration/CollaborationRequestCard.tsx b/src/components/collaboration/CollaborationRequestCard.tsx index 7a47eca70..32c0b6176 100644 --- a/src/components/collaboration/CollaborationRequestCard.tsx +++ b/src/components/collaboration/CollaborationRequestCard.tsx @@ -1,13 +1,14 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Check, X, MessageCircle } from 'lucide-react'; -import { CollaborationRequest } from '../../types'; +import { CollaborationRequest, Investor } 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 { updateRequestStatus } from '../../data/collaborationRequests'; import { formatDistanceToNow } from 'date-fns'; +import { getInvestorById } from '../../data/users'; interface CollaborationRequestCardProps { request: CollaborationRequest; @@ -19,34 +20,42 @@ export const CollaborationRequestCard: React.FC = onStatusUpdate }) => { const navigate = useNavigate(); - const investor = findUserById(request.investorId); + const [investor, setInvestor] = useState(); + useEffect(() => { + const fetchInvestors = async () => { + const investor = await getInvestorById(request.inves_id); + setInvestor(investor); + }; + fetchInvestors(); + }, []); + if (!investor) return null; - const handleAccept = () => { - updateRequestStatus(request.id, 'accepted'); +const handleAccept = () => { + updateRequestStatus(request._id, 'accepted'); if (onStatusUpdate) { - onStatusUpdate(request.id, 'accepted'); + onStatusUpdate(request._id, 'accepted'); } }; const handleReject = () => { - updateRequestStatus(request.id, 'rejected'); + updateRequestStatus(request._id, 'rejected'); if (onStatusUpdate) { - onStatusUpdate(request.id, 'rejected'); + onStatusUpdate(request._id, 'rejected'); } }; const handleMessage = () => { - navigate(`/chat/${investor.id}`); + navigate(`/chat/${investor.userId}`); }; const handleViewProfile = () => { - navigate(`/profile/investor/${investor.id}`); + navigate(`/profile/investor/${investor.userId}`); }; const getStatusBadge = () => { - switch (request.status) { + switch (request.requestStatus) { case 'pending': return Pending; case 'accepted': @@ -74,7 +83,7 @@ export const CollaborationRequestCard: React.FC =

{investor.name}

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

@@ -88,7 +97,7 @@ export const CollaborationRequestCard: React.FC = - {request.status === 'pending' ? ( + {request.requestStatus === 'pending' ? (
+
- + {/* Filters and search */}
@@ -92,17 +103,21 @@ export const InvestorDashboard: React.FC = () => { startAdornment={} />
- +
- Filter by: - + + Filter by: + +
- {industries.map(industry => ( + {industries.map((industry) => ( toggleIndustry(industry)} > @@ -113,7 +128,7 @@ export const InvestorDashboard: React.FC = () => {
- + {/* Stats summary */}
@@ -123,13 +138,17 @@ export const InvestorDashboard: React.FC = () => {
-

Total Startups

-

{entrepreneurs && entrepreneurs.length}

+

+ Total Startups +

+

+ {entrepreneurs && entrepreneurs.length} +

- +
@@ -137,13 +156,17 @@ export const InvestorDashboard: React.FC = () => {
-

Industries

-

{industries.length}

+

+ Industries +

+

+ {industries.length} +

- +
@@ -151,41 +174,48 @@ export const InvestorDashboard: React.FC = () => {
-

Your Connections

+

+ Your Connections +

- {sentRequests.filter(req => req.status === 'accepted').length} + {sentRequests && + sentRequests.filter( + (req) => req.requestStatus === "accepted" + ).length}

- + {/* Entrepreneurs grid */}
-

Featured Startups

+

+ Featured Startups +

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

No startups match your filters

-
); -}; \ No newline at end of file +}; diff --git a/src/pages/profile/EntrepreneurProfile.tsx b/src/pages/profile/EntrepreneurProfile.tsx index b354735fb..dfd0229dc 100644 --- a/src/pages/profile/EntrepreneurProfile.tsx +++ b/src/pages/profile/EntrepreneurProfile.tsx @@ -17,8 +17,8 @@ import { Card, CardBody, CardHeader } from "../../components/ui/Card"; import { Badge } from "../../components/ui/Badge"; import { useAuth } from "../../context/AuthContext"; import { + checkRequestsFromInvestor, createCollaborationRequest, - getRequestsFromInvestor, } from "../../data/collaborationRequests"; import { getEnterpreneurById, updateEntrepreneurData } from "../../data/users"; import { Entrepreneur } from "../../types"; @@ -29,6 +29,8 @@ export const EntrepreneurProfile: React.FC = () => { const { user: currentUser } = useAuth(); const [isEditing, setIsEditing] = useState(false); const [entrepreneur, setEnterpreneur] = useState(); + const [hasRequestedCollaboration, setHasRequestedCollaboration] = + useState(); const initialData = { userId: id, startupName: entrepreneur?.startupName, @@ -50,9 +52,18 @@ export const EntrepreneurProfile: React.FC = () => { const entrepreneur = await getEnterpreneurById(id); setEnterpreneur(entrepreneur); }; + fetchEntrepreneur(); }, []); + useEffect(() => { + const checkInvestor = async()=>{ + const request = await checkRequestsFromInvestor(currentUser?.userId,id); + setHasRequestedCollaboration(request); + } + checkInvestor(); + }, [currentUser?.userId,id]); + useEffect(() => { setFormData(initialData); }, [entrepreneur]); @@ -82,20 +93,15 @@ export const EntrepreneurProfile: React.FC = () => { const isInvestor = currentUser?.role === "investor"; // Check if the current investor has already sent a request to this entrepreneur - const hasRequestedCollaboration = - isInvestor && id - ? getRequestsFromInvestor(currentUser.userId).some( - (req) => req.entrepreneurId === id - ) - : false; - - const handleSendRequest = () => { + + const handleSendRequest = async () => { if (isInvestor && currentUser && id) { createCollaborationRequest( currentUser.userId, id, `I'm interested in learning more about ${entrepreneur.startupName} and would like to explore potential investment opportunities.` ); + await setHasRequestedCollaboration(true); } }; @@ -149,71 +155,71 @@ export const EntrepreneurProfile: React.FC = () => { className="gap-5 flex flex-col text-sm justify-center items-center" >
-
- - - - - - +
+ + + + + + +
+
+ + + + +
-
- - - - - -
- - {/* Stats */} -
- - -
-
- -
-
-

Total Investment

-

$4.3M

-
-
-
-
- - - -
-
- -
-
-

Active Deals

-

8

-
-
-
-
- - - -
-
- -
-
-

Portfolio Companies

-

12

-
-
-
-
- + + {/* Add Deal Form */} + {showForm && ( - -
-
- -
-
-

Closed This Month

-

2

-
-
+ +

New Deal

+
+ + setNewDeal({ ...newDeal, name: e.target.value })} + /> + setNewDeal({ ...newDeal, logo: e.target.value })} + /> + + setNewDeal({ ...newDeal, industry: e.target.value }) + } + /> + + setNewDeal({ ...newDeal, amount: e.target.value }) + } + /> + + setNewDeal({ ...newDeal, equity: e.target.value }) + } + /> + setNewDeal({ ...newDeal, stage: e.target.value })} + /> + + +
-
- + )} + {/* Filters */}
@@ -162,15 +192,19 @@ export const DealsPage: React.FC = () => { fullWidth />
- +
- {statuses.map(status => ( + {statuses.map((status) => ( toggleStatus(status)} > @@ -181,92 +215,96 @@ export const DealsPage: React.FC = () => {
- + {/* Deals table */}

Active Deals

-
- - - - - - - - - - - - - - {deals.map(deal => ( - - + + + + + + + + ))} + +
- Startup - - Amount - - Equity - - Status - - Stage - - Last Activity - - Actions -
-
- -
-
- {deal.startup.name} -
-
- {deal.startup.industry} + {filteredDeals.length === 0 ? ( +

No deals found.

+ ) : ( +
+ + + + + + + + + + + + + + {filteredDeals.map((deal) => ( + + - - - - - - - - ))} - -
+ Startup + + Amount + + Equity + + Status + + Stage + + Last Activity + + Actions +
+
+ +
+
+ {deal.startup.name} +
+
+ {deal.startup.industry} +
- -
-
{deal.amount}
-
-
{deal.equity}
-
- - {deal.status} - - -
{deal.stage}
-
-
- {new Date(deal.lastActivity).toLocaleDateString()} -
-
- -
-
+
+
{deal.amount}
+
+
{deal.equity}
+
+ + {deal.status} + + +
{deal.stage}
+
+
+ {new Date(deal.lastActivity).toLocaleDateString()} +
+
+ +
+
+ )}
); -}; \ No newline at end of file +}; diff --git a/src/pages/documents/DocumentsPage.tsx b/src/pages/documents/DocumentsPage.tsx index 1e325d3c9..f3e5ffa2d 100644 --- a/src/pages/documents/DocumentsPage.tsx +++ b/src/pages/documents/DocumentsPage.tsx @@ -1,45 +1,37 @@ -import React from 'react'; -import { FileText, Upload, Download, Trash2, Share2 } from 'lucide-react'; -import { Card, CardHeader, CardBody } from '../../components/ui/Card'; -import { Button } from '../../components/ui/Button'; -import { Badge } from '../../components/ui/Badge'; +import React, { useState } from "react"; +import { FileText, Upload, Download, Trash2, Share2 } from "lucide-react"; +import { Card, CardHeader, CardBody } from "../../components/ui/Card"; +import { Button } from "../../components/ui/Button"; +import { Badge } from "../../components/ui/Badge"; -const documents = [ - { - id: 1, - name: 'Pitch Deck 2024.pdf', - type: 'PDF', - size: '2.4 MB', - lastModified: '2024-02-15', - shared: true - }, - { - id: 2, - name: 'Financial Projections.xlsx', - type: 'Spreadsheet', - size: '1.8 MB', - lastModified: '2024-02-10', - shared: false - }, - { - id: 3, - name: 'Business Plan.docx', - type: 'Document', - size: '3.2 MB', - lastModified: '2024-02-05', - shared: true - }, - { - id: 4, - name: 'Market Research.pdf', - type: 'PDF', - size: '5.1 MB', - lastModified: '2024-01-28', - shared: false - } -]; +interface Document { + id: number; + name: string; + type: string; + size: string; + lastModified: string; + shared: boolean; +} export const DocumentsPage: React.FC = () => { + const [documents, setDocuments] = useState([]); + + const handleUpload = (e: React.ChangeEvent) => { + const files = e.target.files; + if (!files) return; + + const uploadedDocs: Document[] = Array.from(files).map((file, idx) => ({ + id: documents.length + idx + 1, + name: file.name, + type: file.type || "Unknown", + size: (file.size / (1024 * 1024)).toFixed(2) + " MB", + lastModified: new Date(file.lastModified).toISOString().split("T")[0], + shared: false, + })); + + setDocuments((prev) => [...prev, ...uploadedDocs]); + }; + return (
@@ -47,12 +39,20 @@ export const DocumentsPage: React.FC = () => {

Documents

Manage your startup's important files

- - + +
- +
{/* Storage info */} @@ -66,39 +66,26 @@ export const DocumentsPage: React.FC = () => { 12.5 GB
-
+
Available 7.5 GB
- -
-

Quick Access

-
- - - - -
-
- + {/* Document list */}
-

All Documents

+

+ All Documents +

- + - +
); -}; \ No newline at end of file +}; diff --git a/src/pages/investors/InvestorsPage.tsx b/src/pages/investors/InvestorsPage.tsx index 230a7e2cc..30ef59387 100644 --- a/src/pages/investors/InvestorsPage.tsx +++ b/src/pages/investors/InvestorsPage.tsx @@ -21,10 +21,7 @@ export const InvestorsPage: React.FC = () => { const fetchData = async () => { if (user) { const investors = await getInvestorsFromDb(); - console.log(investors); setInvestors(investors ? investors : []); - - const requests = getRequestsForEntrepreneur(user.id); } }; fetchData(); @@ -39,13 +36,13 @@ export const InvestorsPage: React.FC = () => { ); // Filter investors based on search and filters - const filteredInvestors = investors.filter((investor) => { + const filteredInvestors = investors && investors.filter((investor) => { const matchesSearch = searchQuery === "" || - investor.name.toLowerCase().includes(searchQuery.toLowerCase()) || - investor.bio.toLowerCase().includes(searchQuery.toLowerCase()) || + investor.name?.toLowerCase().includes(searchQuery.toLowerCase()) || + investor.bio?.toLowerCase().includes(searchQuery.toLowerCase()) || investor.investmentInterests?.some((interest) => - interest.toLowerCase().includes(searchQuery.toLowerCase()) + interest?.toLowerCase().includes(searchQuery.toLowerCase()) ); const matchesStages = diff --git a/src/pages/messages/MessagesPage.tsx b/src/pages/messages/MessagesPage.tsx index 1ee7b2295..b6d9c31e8 100644 --- a/src/pages/messages/MessagesPage.tsx +++ b/src/pages/messages/MessagesPage.tsx @@ -1,20 +1,33 @@ -import React from 'react'; -import { useAuth } from '../../context/AuthContext'; -import { getConversationsForUser } from '../../data/messages'; -import { ChatUserList } from '../../components/chat/ChatUserList'; -import { MessageCircle } from 'lucide-react'; +import React, { useEffect, useState } from "react"; +import { useAuth } from "../../context/AuthContext"; +import { getConversationsForUser } from "../../data/messages"; +import { ChatUserList } from "../../components/chat/ChatUserList"; +import { MessageCircle } from "lucide-react"; +import { ChatConversation } from "../../types"; export const MessagesPage: React.FC = () => { const { user } = useAuth(); - + const [conversation, setConversation] = useState(); + if (!user) return null; - - const conversations = getConversationsForUser(user.userId); - + + useEffect(() => { + const fetchConversation = async () => { + const conv = await getConversationsForUser(user?.userId); + if (conv) { + setConversation(conv); + } + }; + + fetchConversation(); + }, [user?.userId]); + return ( -
- {conversations.length > 0 ? ( - +
+ {conversation? ( +
+ +
) : (
@@ -22,10 +35,11 @@ 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 + conversation

)}
); -}; \ No newline at end of file +}; diff --git a/src/pages/notifications/NotificationsPage.tsx b/src/pages/notifications/NotificationsPage.tsx index 3bb806c25..5872c07eb 100644 --- a/src/pages/notifications/NotificationsPage.tsx +++ b/src/pages/notifications/NotificationsPage.tsx @@ -1,112 +1,90 @@ -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 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"; +import { useAuth } from "../../context/AuthContext"; export const NotificationsPage: React.FC = () => { + const { user } = useAuth(); + const notifications = user?.notifications || []; + const getNotificationIcon = (type: string) => { switch (type) { - case 'message': + case "message": return ; - case 'connection': + case "connection": return ; - case 'investment': + case "investment": return ; default: return ; } }; - + return (

Notifications

-

Stay updated with your network activity

+

+ Stay updated with your 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: any) => ( + + + + +
+
+ + {notification.user.name} + + {notification.unread && ( + + New + + )} +
+ +

{notification.content}

+ +
+ {getNotificationIcon(notification.type)} + {notification.time} +
-
- - - ))} + + + )) + ) : ( +

+ No notifications found 🚀 +

+ )}
); -}; \ No newline at end of file +}; diff --git a/src/pages/profile/InvestorProfile.tsx b/src/pages/profile/InvestorProfile.tsx index 874786345..8f3480914 100644 --- a/src/pages/profile/InvestorProfile.tsx +++ b/src/pages/profile/InvestorProfile.tsx @@ -150,7 +150,6 @@ export const InvestorProfile: React.FC = () => { investmentCriteria: updateCriteria, criteria: "", }); - console.log(updateCriteria); }; return ( From b36bd651b2d7bac5903636f9e202527986a63a53 Mon Sep 17 00:00:00 2001 From: Danish-Butt Date: Wed, 10 Sep 2025 13:43:53 +0500 Subject: [PATCH 20/43] time error resolved --- src/components/chat/ChatMessage.tsx | 47 +++++++++++++++++----------- src/components/chat/ChatUserList.tsx | 16 +++++----- 2 files changed, 37 insertions(+), 26 deletions(-) diff --git a/src/components/chat/ChatMessage.tsx b/src/components/chat/ChatMessage.tsx index c238686d5..f94922007 100644 --- a/src/components/chat/ChatMessage.tsx +++ b/src/components/chat/ChatMessage.tsx @@ -1,20 +1,26 @@ -import React from 'react'; -import { formatDistanceToNow } from 'date-fns'; -import { Message, User } from '../../types'; -import { Avatar } from '../ui/Avatar'; +import React from "react"; +import { formatDistanceToNow } from "date-fns"; +import { Message, User } from "../../types"; +import { Avatar } from "../ui/Avatar"; interface ChatMessageProps { message: Message; - user:User | undefined + user: User | undefined; isCurrentUser: boolean; } -export const ChatMessage: React.FC = ({ message,user, isCurrentUser }) => { +export const ChatMessage: React.FC = ({ + message, + user, + isCurrentUser, +}) => { if (!user) return null; - + return (
{!isCurrentUser && ( = ({ message,user, isCurren className="mr-2 self-end" /> )} - -
+ +

{message.content}

- - - {formatDistanceToNow(new Date(message.time), { addSuffix: true })} - + {message?.time && ( + + {formatDistanceToNow(new Date(message.time), { addSuffix: true })} + + )}
- + {isCurrentUser && ( = ({ message,user, isCurren )}
); -}; \ No newline at end of file +}; diff --git a/src/components/chat/ChatUserList.tsx b/src/components/chat/ChatUserList.tsx index 851208747..2745b7e3e 100644 --- a/src/components/chat/ChatUserList.tsx +++ b/src/components/chat/ChatUserList.tsx @@ -96,14 +96,14 @@ export const ChatUserList: React.FC = ({ conversation }) => { {"..."} - {Object.keys(lastMessage).length !== 0 && ( - - {formatDistanceToNow( - new Date(lastMessage?.time || "Text first"), - { addSuffix: false } - )} - - )} + {Object.keys(lastMessage).length !== 0 && + lastMessage?.time && ( + + {formatDistanceToNow(new Date(lastMessage.time), { + addSuffix: false, + })} + + )}
From 38602aee25eb996d086f230ac76fe2e83e68d505 Mon Sep 17 00:00:00 2001 From: Danish-Butt Date: Tue, 30 Sep 2025 13:29:08 +0500 Subject: [PATCH 21/43] agora RTC implemented for audio video call --- package-lock.json | 172 ++++++++ package.json | 6 + src/App.tsx | 10 +- .../entrepreneur/EntrepreneurCard.tsx | 2 +- src/components/investor/InvestorCard.tsx | 28 +- src/components/layout/DashboardLayout.tsx | 27 +- src/components/webrtc/IncomingCallModal.tsx | 10 +- src/context/AuthContext.tsx | 24 +- src/context/SocketContext.tsx | 3 +- src/data/messages.ts | 21 +- src/pages/chat/ChatPage.tsx | 5 +- src/pages/dashboard/EntrepreneurDashboard.tsx | 1 + src/pages/investors/InvestorsPage.tsx | 2 +- src/pages/profile/InvestorProfile.tsx | 2 +- src/pages/webRTC/AudioCall.tsx | 342 +++++++-------- src/pages/webRTC/Videocall.tsx | 397 ++++++++++-------- 16 files changed, 654 insertions(+), 398 deletions(-) diff --git a/package-lock.json b/package-lock.json index d5ae710ea..f12e42e7b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "business-nexus", "version": "0.1.0", "dependencies": { + "agora-rtc-sdk-ng": "^4.24.0", "axios": "^1.6.7", "date-fns": "^3.3.1", "jwt-decode": "^4.0.0", @@ -37,6 +38,39 @@ "vite": "^5.4.2" } }, + "node_modules/@agora-js/media": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@agora-js/media/-/media-4.24.0.tgz", + "integrity": "sha512-foii2klr5+qonLznxN0ZZFejoxLt/W8do79wmIsADPZLw2uZjRP35m0lqUGiLXBKeQ8u3i4UygPzEdFaY26hrw==", + "license": "MIT", + "dependencies": { + "@agora-js/report": "4.24.0", + "@agora-js/shared": "4.24.0", + "agora-rte-extension": "^1.2.4", + "axios": "^1.8.3", + "webrtc-adapter": "8.2.0" + } + }, + "node_modules/@agora-js/report": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@agora-js/report/-/report-4.24.0.tgz", + "integrity": "sha512-MYbtkdY1Ls0KW0iagUzrPzyvqMWlyCWSC5odEb1SQaraAl7DJeDUkf91a3wxKzrjVah+LCxFxsS4lCFDxvKgNA==", + "license": "MIT", + "dependencies": { + "@agora-js/shared": "4.24.0", + "axios": "^1.8.3" + } + }, + "node_modules/@agora-js/shared": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@agora-js/shared/-/shared-4.24.0.tgz", + "integrity": "sha512-Vj67ZcTHZI+1ctWusrEPSSGLM3l6CFiAze/Bi8r7YHRMLivzhZR79nV6GiKvHS3muLAON2YAExznvjPIly6lcg==", + "license": "MIT", + "dependencies": { + "axios": "^1.8.3", + "ua-parser-js": "^0.7.34" + } + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -1584,6 +1618,29 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agora-rtc-sdk-ng": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/agora-rtc-sdk-ng/-/agora-rtc-sdk-ng-4.24.0.tgz", + "integrity": "sha512-2apG/07EtsuX21ncSF77q+dr6/kDgu9B/RpKtstCtaq46l4/Eraoecewi4zXRUCY3Im+8dzTIXx6jUwyPdxdHQ==", + "license": "MIT", + "dependencies": { + "@agora-js/media": "4.24.0", + "@agora-js/report": "4.24.0", + "@agora-js/shared": "4.24.0", + "agora-rte-extension": "^1.2.4", + "axios": "^1.8.3", + "formdata-polyfill": "^4.0.7", + "pako": "^2.1.0", + "ua-parser-js": "^0.7.34", + "webrtc-adapter": "8.2.0" + } + }, + "node_modules/agora-rte-extension": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/agora-rte-extension/-/agora-rte-extension-1.2.4.tgz", + "integrity": "sha512-0ovZz1lbe30QraG1cU+ji7EnQ8aUu+Hf3F+a8xPml3wPOyUQEK6CTdxV9kMecr9t+fIDrGeW7wgJTsM1DQE7Nw==", + "license": "ISC" + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2483,6 +2540,29 @@ "reusify": "^1.0.4" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -2605,6 +2685,18 @@ "node": ">= 6" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -3225,6 +3317,26 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-releases": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", @@ -3319,6 +3431,12 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -3850,6 +3968,12 @@ "loose-envify": "^1.1.0" } }, + "node_modules/sdp": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.1.tgz", + "integrity": "sha512-lwsAIzOPlH8/7IIjjz3K0zYBk7aBVVcvjMwt3M4fLxpjMYyy7i3I97SLHebgn4YBjirkzfp3RvRDWSKsh/+WFw==", + "license": "MIT" + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -4240,6 +4364,32 @@ } } }, + "node_modules/ua-parser-js": { + "version": "0.7.41", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.41.tgz", + "integrity": "sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, "node_modules/undici-types": { "version": "7.10.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", @@ -4351,6 +4501,28 @@ } } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webrtc-adapter": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-8.2.0.tgz", + "integrity": "sha512-umxCMgedPAVq4Pe/jl3xmelLXLn4XZWFEMR5Iipb5wJ+k1xMX0yC4ZY9CueZUU1MjapFxai1tFGE7R/kotH6Ww==", + "license": "BSD-3-Clause", + "dependencies": { + "sdp": "^3.0.2" + }, + "engines": { + "node": ">=6.0.0", + "npm": ">=3.10.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index b50efb5f1..b88b4b271 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "agora-rtc-sdk-ng": "^4.24.0", "axios": "^1.6.7", "date-fns": "^3.3.1", "jwt-decode": "^4.0.0", @@ -37,5 +38,10 @@ "typescript": "^5.5.3", "typescript-eslint": "^8.3.0", "vite": "^5.4.2" + }, + "husky": { + "hooks": { + "pre-commit": "npm run lint" + } } } diff --git a/src/App.tsx b/src/App.tsx index 60cc78901..5938ad00a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -104,8 +104,14 @@ function App() { {/* Chat Routes */} }> - } /> - } /> + } + /> + } + /> } /> diff --git a/src/components/entrepreneur/EntrepreneurCard.tsx b/src/components/entrepreneur/EntrepreneurCard.tsx index bd284146c..d1c13a2d6 100644 --- a/src/components/entrepreneur/EntrepreneurCard.tsx +++ b/src/components/entrepreneur/EntrepreneurCard.tsx @@ -49,7 +49,7 @@ export const EntrepreneurCard: React.FC = ({
{entrepreneur.industry} - {entrepreneur.location} + {entrepreneur.teamSize} Founded {entrepreneur.foundedYear}
diff --git a/src/components/investor/InvestorCard.tsx b/src/components/investor/InvestorCard.tsx index 42bb0d59a..fa81f4277 100644 --- a/src/components/investor/InvestorCard.tsx +++ b/src/components/investor/InvestorCard.tsx @@ -19,12 +19,12 @@ export const InvestorCard: React.FC = ({ const navigate = useNavigate(); const handleViewProfile = () => { - navigate(`/profile/investor/${investor.userId || investor._id}`); + navigate(`/profile/investor/${investor.userId}`); }; const handleMessage = (e: React.MouseEvent) => { e.stopPropagation(); // Prevent card click - navigate(`/chat/${investor.userId || investor._id}`); + navigate(`/chat/${investor.userId}`); }; return ( @@ -48,15 +48,16 @@ export const InvestorCard: React.FC = ({ {investor.name}

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

- {investor.investmentStage && investor.investmentStage.map((stage, index) => ( - - {stage} - - ))} + {investor.investmentStage && + investor.investmentStage.map((stage, index) => ( + + {stage} + + ))}
@@ -66,11 +67,12 @@ export const InvestorCard: React.FC = ({ Investment Interests
- {investor.investmentInterests && investor.investmentInterests.map((interest, index) => ( - - {interest} - - ))} + {investor.investmentInterests && + investor.investmentInterests.map((interest, index) => ( + + {interest} + + ))}
diff --git a/src/components/layout/DashboardLayout.tsx b/src/components/layout/DashboardLayout.tsx index 201d5d6f3..c76cdf569 100644 --- a/src/components/layout/DashboardLayout.tsx +++ b/src/components/layout/DashboardLayout.tsx @@ -14,6 +14,8 @@ export const DashboardLayout: React.FC = () => { const [incomingCall, setIncomingCall] = useState<{ from: string; roomId: string; + callType: string; + fromName: string; } | null>(null); if (isLoading) { @@ -30,7 +32,11 @@ export const DashboardLayout: React.FC = () => { const acceptCall = () => { if (incomingCall) { socket?.emit("accept-call", { to: incomingCall.from }); - navigate(`/chat/${incomingCall.from}/audio-call/${incomingCall.roomId}`); + navigate( + `/chat/${incomingCall.from}/${incomingCall.callType}-call/${ + incomingCall.roomId + }/${true}` + ); setIncomingCall(null); } }; @@ -43,8 +49,18 @@ export const DashboardLayout: React.FC = () => { }; useEffect(() => { - const handleIncoming = ({ from, roomId }: { from: string; roomId: string }) => { - setIncomingCall({ from, roomId }); + const handleIncoming = ({ + from, + roomId, + callType, + fromName, + }: { + from: string; + roomId: string; + callType: string; + fromName: string; + }) => { + setIncomingCall({ from, roomId, callType, fromName }); }; socket?.on("incoming-call", handleIncoming); @@ -55,11 +71,10 @@ export const DashboardLayout: React.FC = () => { return (
- {incomingCall && ( diff --git a/src/components/webrtc/IncomingCallModal.tsx b/src/components/webrtc/IncomingCallModal.tsx index 788c0037d..f2ddec55a 100644 --- a/src/components/webrtc/IncomingCallModal.tsx +++ b/src/components/webrtc/IncomingCallModal.tsx @@ -2,18 +2,18 @@ import React from "react"; type Props = { - from: string; - roomId: string; + callType:string; + fromName:string; onAccept: () => void; onReject: () => void; }; -const IncomingCallModal: React.FC = ({ from, onAccept, onReject }) => { +const IncomingCallModal: React.FC = ({ callType,fromName, onAccept, onReject }) => { return (
-
+

📞 Incoming Call

-

User {from} is calling you

+

{fromName} request you for {callType} call

+ + {/* Mic Control */} + {isMute ? ( + + ) : ( + + )} +
- - {/* Control Bar */} -
- -
); }; diff --git a/src/pages/webRTC/Videocall.tsx b/src/pages/webRTC/Videocall.tsx index e66d086f8..cb1414c6f 100644 --- a/src/pages/webRTC/Videocall.tsx +++ b/src/pages/webRTC/Videocall.tsx @@ -3,91 +3,164 @@ import { useNavigate, useParams } from "react-router-dom"; import { useAuth } from "../../context/AuthContext"; import { useSocket } from "../../context/SocketContext"; import toast from "react-hot-toast"; +import AgoraRTC, { ILocalTrack } from "agora-rtc-sdk-ng"; +import axios from "axios"; export const VideoCall: React.FC = () => { - const { roomId, userId } = useParams(); + const { roomId, userId, isIncommingCall } = useParams(); + const [isMute, setIsMute] = useState(false); + const [isVideoOn, setIsVideoOn] = useState(false); const { socket } = useSocket(); const { user } = useAuth(); - - const localVideoRef = useRef(null); - const remoteVideoRef = useRef(null); - const pcRef = useRef(null); const navigate = useNavigate(); - + const APP_ID = "5e3db4d74aaa43ff8eeee3ad9f08efd8"; + const CHANNEL = String(roomId); const [joined, setJoined] = useState(false); - const [isMuted, setIsMuted] = useState(false); + const localVideoRef = useRef(null); + const remoteVideoRef = useRef(null); + const localTracksRef = useRef([]); + const clientRef = useRef(null); + const [token, setToken] = useState(null); + const [uid, setUid] = useState(null); + const URL = import.meta.env.VITE_BACKEND_URL; useEffect(() => { - pcRef.current = new RTCPeerConnection(); + const fetchToken = async () => { + // generate uid (string or number) + const uid = String(Date.now()); // simple unique uid + setUid(uid); - // Handle remote stream - pcRef.current.ontrack = (event) => { - if (remoteVideoRef.current) { - remoteVideoRef.current.srcObject = event.streams[0]; + try { + const res = await axios.get(`${URL}/agora/rtc/${CHANNEL}/${uid}`, { + withCredentials: true, + }); + setToken(res.data.token); + } catch (err) { + console.error("Failed to fetch token:", err); } }; - // ICE candidates → send to other peer - pcRef.current.onicecandidate = (event) => { - if (event.candidate) { - socket?.emit("ice-candidate", { roomId, candidate: event.candidate }); - } - }; + fetchToken(); + }, [CHANNEL, URL]); - const stopStream = () => { - const stream = localVideoRef.current?.srcObject as MediaStream; - if (stream) { - stream.getTracks().forEach((track) => { - track.stop(); // stops both mic and camera - }); - if (localVideoRef.current) { - localVideoRef.current.srcObject = null; + useEffect(() => { + if (!token || !uid) return; + + AgoraRTC.setLogLevel(0); + + const init = async () => { + const client = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" }); + clientRef.current = client; + + // 🔹 Set up listeners BEFORE joining/publishing + client.on("user-published", async (user, mediaType) => { + if (user.uid === uid) return; // skip own stream + await client.subscribe(user, mediaType); + console.log("Subscribed to:", user.uid); + + if (mediaType === "video") { + user.videoTrack?.play(remoteVideoRef.current!); } - } + if (mediaType === "audio") { + user.audioTrack?.play(); + } + }); - // Close peer connection - pcRef.current?.close(); - pcRef.current = null; - socket?.emit("end-call", { to: userId, roomId }); - }; + client.on("user-unpublished", (user, mediaType) => { + if (mediaType === "video" && remoteVideoRef.current) { + remoteVideoRef.current.innerHTML = ""; + } + }); - // ===== SOCKET LISTENERS ===== - socket?.on("offer", async ({ offer }) => { try { - await pcRef.current?.setRemoteDescription( - new RTCSessionDescription(offer) - ); - // Create answer - const answer = await pcRef.current?.createAnswer(); - await pcRef.current?.setLocalDescription(answer); - socket.emit("answer", { roomId, answer }); - } catch (err) { - console.error("Error handling offer:", err); - } - }); + await client.join(APP_ID, CHANNEL, token, uid); - socket?.on("answer", async ({ answer }) => { - try { - if (pcRef.current && !pcRef.current.remoteDescription) { - await pcRef.current.setRemoteDescription( - new RTCSessionDescription(answer) - ); - } + // create local tracks + localTracksRef.current = + await AgoraRTC.createMicrophoneAndCameraTracks(); + + // publish AFTER listeners are ready + await client.publish(localTracksRef.current); + + localTracksRef.current[1].play(localVideoRef.current!); + setIsVideoOn(true); + setJoined(true); } catch (err) { - console.error("Error setting remote description:", err); + console.error("Agora join error:", err); } - }); - socket?.on("ice-candidate", async ({ candidate }) => { - try { - if (pcRef.current?.remoteDescription) { - await pcRef.current.addIceCandidate(new RTCIceCandidate(candidate)); - } - } catch (err) { - console.error("Error adding ICE candidate:", err); + // Outgoing call + const isIncoming = isIncommingCall === "true"; + if (!isIncoming) { + socket?.emit("start-call", { + from: user?.userId, + to: userId, + roomId: CHANNEL, + callType:"video", + fromName:user?.name + }); } + }; + + init(); + + return () => { + cleanup(); + }; + }, [CHANNEL, isIncommingCall, user, userId, token, uid, socket]); + + // === Cleanup === + const cleanup = async () => { + localTracksRef.current.forEach((track) => { + track.stop(); + track.close(); }); + if (clientRef.current) { + clientRef.current.removeAllListeners(); // ✅ avoid duplicate handlers + await clientRef.current.leave(); + } + setJoined(false); + }; + + // Mute mic + const muteMic = async () => { + if (localTracksRef.current[0]) { + await localTracksRef.current[0].setEnabled(false); + setIsMute(true); + console.log("Microphone muted"); + } + }; + + // Unmute mic + const unmuteMic = async () => { + if (localTracksRef.current[0]) { + await localTracksRef.current[0].setEnabled(true); + + setIsMute(false); + console.log("Microphone unmuted"); + } + }; + + // Stop video + const stopVideo = async () => { + if (localTracksRef.current[1]) { + await localTracksRef.current[1].setEnabled(false); + + setIsVideoOn(false); + console.log("Video stopped"); + } + }; + + // Resume video + const resumeVideo = async () => { + if (localTracksRef.current[1]) { + await localTracksRef.current[1].setEnabled(true); + setIsVideoOn(true); + console.log("Video resumed"); + } + }; + useEffect(() => { socket?.on("call-accepted", () => { toast.success("Call accepted"); }); @@ -102,132 +175,108 @@ export const VideoCall: React.FC = () => { navigate(`/chat/${userId}`); }); - socket?.on("receiver-offline", () => { + socket?.on("receiver-offline", async () => { toast.error("The receiver is offline."); + await cleanup(); navigate(`/chat/${userId}`); }); - if (!joined) joinRoom(); - return () => { - socket?.off("offer"); - socket?.off("answer"); - socket?.off("ice-candidate"); socket?.off("call-accepted"); + socket?.off("receiver-offline"); socket?.off("call-rejected"); socket?.off("call-ended"); }; - }, [roomId]); - - const joinRoom = async () => { - setJoined(true); - socket?.emit("join-room", { roomId }); - - try { - // Get media - const stream = await navigator.mediaDevices.getUserMedia({ - video: true, - audio: true, - }); - if (localVideoRef.current) { - localVideoRef.current.srcObject = stream; - } - - // Add tracks - if (pcRef.current?.getSenders().length === 0) { - stream - .getTracks() - .forEach((track) => pcRef.current?.addTrack(track, stream)); - } - - // Caller creates offer - if (user?.userId !== userId) { - // Only caller - const offer = await pcRef.current?.createOffer(); - await pcRef.current?.setLocalDescription(offer); - socket?.emit("offer", { roomId, offer }); - socket?.emit("start-call", { from: user?.userId, to: userId, roomId }); - } - } catch (error) { - console.log("Video call error:", error); - } - }; - + }, [socket, userId]); return ( -
- {/* Video Area */} -
- {/* Local Video */} -
-

Agora 1-to-1 Audio Call

From b543396a39279962e4372d1a61a898d756312868 Mon Sep 17 00:00:00 2001 From: Danish-Butt Date: Sat, 11 Oct 2025 21:23:46 +0500 Subject: [PATCH 23/43] linkedin and google oAuth integrated --- public/app logo.jpeg | Bin 0 -> 62816 bytes src/App.tsx | 5 +- src/context/AuthContext.tsx | 33 +++++ src/pages/auth/LoginPage.tsx | 178 ++++++++++++++++++-------- src/pages/auth/LoginWithOAuthPage.tsx | 116 +++++++++++++++++ src/types/index.ts | 21 +-- 6 files changed, 291 insertions(+), 62 deletions(-) create mode 100644 public/app logo.jpeg create mode 100644 src/pages/auth/LoginWithOAuthPage.tsx diff --git a/public/app logo.jpeg b/public/app logo.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..9f3abdca7b91217b22b81d18c768948a9648f3bb GIT binary patch literal 62816 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
6^0x<&E&tCd>29 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 literal 0 HcmV?d00001 diff --git a/src/App.tsx b/src/App.tsx index 5938ad00a..ea2ef75c8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -39,6 +39,8 @@ import { ResetPasswordPage } from "./pages/auth/ResetPasswordPage"; import { VideoCall } from "./pages/webRTC/Videocall"; import { AudioCall } from "./pages/webRTC/AudioCall"; import { Toaster } from "react-hot-toast"; +import { LoginWithOAuthPage } from "./pages/auth/LoginWithOAuthPage"; +import { HomePage } from "./pages/home/HomePage"; function App() { return ( @@ -51,6 +53,7 @@ function App() { } /> } /> } /> + } /> {/* Dashboard Routes */} }> @@ -116,7 +119,7 @@ function App() { {/* Redirect root to login */} - } /> + } /> {/* Catch all other routes and redirect to login */} = ({ setIsLoading(false); } }; + const loginWithOauth = async ( + userToken: string, + role: UserRole + ): Promise => { + setIsLoading(true); + + console.log("hello"); + try { + const res = await axios.post( + `${URL}/auth/login-with-oauth`, + { + userToken, + role, + }, + { + headers: { "Content-Type": "application/json" }, + withCredentials: true, + } + ); + const { token, user } = res.data; + + console.log(user); + localStorage.setItem("token", token); + setUser({ ...user, isOnline: true }); + toast.success("Successfully logged in!"); + } catch (error) { + toast.error((error as Error).message); + throw error; + } finally { + setIsLoading(false); + } + }; // Mock register function - in a real app, this would make an API call const register = async ( @@ -228,6 +260,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ const value = { user, login, + loginWithOauth, register, logout, forgotPassword, diff --git a/src/pages/auth/LoginPage.tsx b/src/pages/auth/LoginPage.tsx index 898d57912..0e74ac9f5 100644 --- a/src/pages/auth/LoginPage.tsx +++ b/src/pages/auth/LoginPage.tsx @@ -1,56 +1,92 @@ -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, + Globe, + Linkedin, +} 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 URL = import.meta.env.VITE_BACKEND_URL; + const { login } = useAuth(); const navigate = useNavigate(); - + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(null); setIsLoading(true); - + try { await login(email, password, role); // 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); } }; - + // For demo purposes, pre-filled credentials const fillDemoCredentials = (userRole: UserRole) => { - if (userRole === 'entrepreneur') { - setEmail('en@gmail.com'); - setPassword('123'); + if (userRole === "entrepreneur") { + setEmail("en@gmail.com"); + setPassword("123"); } else { - setEmail('in@gmail.com'); - setPassword('123'); + setEmail("in@gmail.com"); + setPassword("123"); } setRole(userRole); }; - + + const loginWith = (provider: string) => { + window.location.href = `${URL}/auth/${provider}`; + }; + return (
- - - + + +
@@ -70,7 +106,7 @@ export const LoginPage: React.FC = () => { {error}
)} - +
- + { fullWidth startAdornment={} /> - + { required fullWidth /> - +
- + - +
- Demo Accounts + + Demo Accounts +
- + + {/* To be deleted part */}
- +
- +
@@ -192,18 +237,49 @@ export const LoginPage: React.FC = () => { Or
- + + {/* end */} +

- Don't have an account?{' '} - + Don't have an account?{" "} + Sign up

+ +
+
+
+
+
+ Or +
+
+ +
+ + +
); -}; \ No newline at end of file +}; diff --git a/src/pages/auth/LoginWithOAuthPage.tsx b/src/pages/auth/LoginWithOAuthPage.tsx new file mode 100644 index 000000000..886fe4bb6 --- /dev/null +++ b/src/pages/auth/LoginWithOAuthPage.tsx @@ -0,0 +1,116 @@ +import { Building2, CircleDollarSign, LogIn } from "lucide-react"; +import React, { useEffect, useState } from "react"; +import { Button } from "../../components/ui/Button"; +import { useAuth } from "../../context/AuthContext"; +import { useNavigate } from "react-router-dom"; +import { UserRole } from "../../types"; + +export const LoginWithOAuthPage: React.FC = () => { + const [role, setRole] = useState("investor"); + const { isLoading, loginWithOauth } = useAuth(); + const navigate = useNavigate(); + const params = new URLSearchParams(window.location.search); + const token = params.get("token"); + + console.log(token); + useEffect(() => { + if (!token) { + alert("Missing token"); + return; + } + }, [token]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!token) { + alert("Missing token"); + return; + } + // token is guaranteed to be a string here + await loginWithOauth(token, role); + navigate( + role === "entrepreneur" + ? "/dashboard/entrepreneur" + : "/dashboard/investor" + ); + }; + + return ( +
+
+
+
+
+ + + + +
+
+
+ +
+ + + +
+
+ +
+
+
+
+
+ ); +}; diff --git a/src/types/index.ts b/src/types/index.ts index 4b75aeffc..be2cc181d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -18,10 +18,10 @@ export interface Entrepreneur extends User { industry: string | undefined; foundedYear: number | undefined; teamSize: number | undefined; - minValuation:string | undefined; - maxValuation:string | undefined; - marketOpportunity:string | undefined; - advantage:string | undefined; + minValuation: string | undefined; + maxValuation: string | undefined; + marketOpportunity: string | undefined; + advantage: string | undefined; } export interface Investor extends User { @@ -33,8 +33,8 @@ export interface Investor extends User { maximumInvestment: string | undefined; investmentCriteria: string[] | undefined; successfullExits: number | undefined; - minTimline:number | undefined, - maxTimline:number | undefined, + minTimline: number | undefined; + maxTimline: number | undefined; } export interface Message { @@ -42,7 +42,7 @@ export interface Message { receiver: string; content: string; isRead: boolean; - time:Date, + time: Date; } export interface ChatConversation { @@ -85,10 +85,11 @@ export interface AuthContextType { forgotPassword: (email: string) => Promise; resetPassword: (token: string, newPassword: string) => Promise; updateProfile: (userId: string, updates: Partial) => Promise; + loginWithOauth: (userToken: string, role: UserRole) => Promise; isAuthenticated: boolean; isLoading: boolean; } -export interface Socketcontext{ - socket:string | null; -} \ No newline at end of file +export interface Socketcontext { + socket: string | null; +} From ff05efe91267104b165363ad1fb4b79c9ed34385 Mon Sep 17 00:00:00 2001 From: Danish-Butt Date: Sun, 12 Oct 2025 17:38:44 +0500 Subject: [PATCH 24/43] landing page almost designed --- public/app logo.jpeg | Bin 0 -> 62816 bytes src/App.tsx | 3 +- src/components/home/Navbar.tsx | 51 ++++++++++ src/pages/home/HomePage.tsx | 173 +++++++++++++++++++++++++++++++++ 4 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 public/app logo.jpeg create mode 100644 src/components/home/Navbar.tsx create mode 100644 src/pages/home/HomePage.tsx diff --git a/public/app logo.jpeg b/public/app logo.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..9f3abdca7b91217b22b81d18c768948a9648f3bb GIT binary patch literal 62816 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 literal 0 HcmV?d00001 diff --git a/src/App.tsx b/src/App.tsx index 5938ad00a..6b1ea1963 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -39,6 +39,7 @@ import { ResetPasswordPage } from "./pages/auth/ResetPasswordPage"; import { VideoCall } from "./pages/webRTC/Videocall"; import { AudioCall } from "./pages/webRTC/AudioCall"; import { Toaster } from "react-hot-toast"; +import { HomePage } from "./pages/home/HomePage"; function App() { return ( @@ -116,7 +117,7 @@ function App() { {/* Redirect root to login */} - } /> + } /> {/* Catch all other routes and redirect to login */} { + const ListItem = ({ data }) => { + return ( +
  • + {data} +
  • + ); + }; + const navList = ["Get started", "Compaigns", "FundRaise"]; + return ( +
    +
    +
    + + + + +
    +

    Trust Bridge AI

    +
    +
    +
      + {navList.map((item, idx) => ( + + ))} +
    +
    +
    + ); +}; diff --git a/src/pages/home/HomePage.tsx b/src/pages/home/HomePage.tsx new file mode 100644 index 000000000..8f4265490 --- /dev/null +++ b/src/pages/home/HomePage.tsx @@ -0,0 +1,173 @@ +import React from "react"; +import { Navbar } from "../../components/home/Navbar"; + +export const HomePage: React.FC = () => { + const CompaignDiv = ({ image, title }) => { + return ( +
    + pic +
    +
    +

    + {title} +

    +
    +
    + ); + }; + const FundeRaiserDiv = ({ image, fundNeeded, company, description }) => { + return ( +
    + pic +
    +
    +

    {company}

    +

    {description}

    +
    + + Fund Needed: $ + +

    {fundNeeded}

    +
    +
    +
    +
    + ); + }; + const SuccessfulCompanyDiv = ({ image, company, description, exits }) => { + return ( +
    +
    + pic + {/*
    */} +
    +

    + Company: + {company} +

    +

    + Description: + {description} +

    +
    + Total exits: +

    {exits}

    +
    + {/*
    */} +
    +
    +
    + ); + }; + return ( +
    + +
    + +
    +
    +
    +
    +

    + Recent Compaigns.. +

    +
    + + + +
    +
    + +
    +
    +

    + The plateform where Entrepreneur and investors can + collaborate,make deals and can make secure & successful + transactions.{" "} +

    +
    + + +
    +
    +
    + +
    +

    + Fundraiser Companies.. +

    +
    + + + +
    +
    +
    +
    +

    + Successful Companies.. +

    +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +
    + ); +}; From bb73efe05df124b53f0b4bd9900fbb7a7d3c0752 Mon Sep 17 00:00:00 2001 From: mzain4321 Date: Tue, 14 Oct 2025 14:39:33 +0500 Subject: [PATCH 25/43] feature/admindashboard&campaig pages --- .env | 5 + package-lock.json | 18 +- package.json | 2 +- src/App.tsx | 26 ++- src/components/camp/CampForm.tsx | 167 +++++++++++++++++ src/components/layout/Navbar.tsx | 153 ++++++++------- src/components/layout/Sidebar.tsx | 35 +++- src/pages/admin/UserManagement.tsx | 3 + src/pages/admin/activities.tsx | 3 + src/pages/admin/campaigns.tsx | 104 +++++++++++ src/pages/admin/entrepreneur.tsx | 94 ++++++++++ src/pages/admin/investors.tsx | 95 ++++++++++ src/pages/auth/LoginPage.tsx | 178 +++++++++++------- src/pages/auth/RegisterPage.tsx | 79 +++++--- src/pages/dashboard/AdminDashboard.tsx | 248 +++++++++++++++++++++++++ src/types/index.ts | 3 +- 16 files changed, 1027 insertions(+), 186 deletions(-) create mode 100644 .env create mode 100644 src/components/camp/CampForm.tsx create mode 100644 src/pages/admin/UserManagement.tsx create mode 100644 src/pages/admin/activities.tsx create mode 100644 src/pages/admin/campaigns.tsx create mode 100644 src/pages/admin/entrepreneur.tsx create mode 100644 src/pages/admin/investors.tsx create mode 100644 src/pages/dashboard/AdminDashboard.tsx diff --git a/.env b/.env new file mode 100644 index 000000000..a90afdb33 --- /dev/null +++ b/.env @@ -0,0 +1,5 @@ +DB_URL="mongodb://localhost:27017/TrustBridge" +JWT_SECRET="NetDpVFFq8H8tYtWT3bKjKzgTmsXPZ7mylWw/Z4t2PKC09bhJuL9aud0u4GPuGBojCDZ/XHrpHDBmPokRoTA==" +FRONTEND_URL=http://localhost:5173 +REDIS_URL=redis://localhost:6379 +VITE_BACKEND_URL=http://localhost:5000 diff --git a/package-lock.json b/package-lock.json index d5ae710ea..4f7b8028d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,7 @@ "tailwindcss": "^3.4.1", "typescript": "^5.5.3", "typescript-eslint": "^8.3.0", - "vite": "^5.4.2" + "vite": "^5.4.20" } }, "node_modules/@alloc/quick-lru": { @@ -1822,9 +1822,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.30001747", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001747.tgz", + "integrity": "sha512-mzFa2DGIhuc5490Nd/G31xN1pnBnYMadtkyTjefPI7wzypqgCEpeWu9bJr0OnDsyKrW75zA9ZAt7pbQFmwLsQg==", "dev": true, "funding": [ { @@ -1839,7 +1839,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/chalk": { "version": "2.4.2", @@ -4293,10 +4294,11 @@ "dev": true }, "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.20", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.20.tgz", + "integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==", "dev": true, + "license": "MIT", "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", diff --git a/package.json b/package.json index b50efb5f1..92f43c0cd 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,6 @@ "tailwindcss": "^3.4.1", "typescript": "^5.5.3", "typescript-eslint": "^8.3.0", - "vite": "^5.4.2" + "vite": "^5.4.20" } } diff --git a/src/App.tsx b/src/App.tsx index 60cc78901..f28954aa9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,6 +17,7 @@ import { RegisterPage } from "./pages/auth/RegisterPage"; // Dashboard Pages import { EntrepreneurDashboard } from "./pages/dashboard/EntrepreneurDashboard"; import { InvestorDashboard } from "./pages/dashboard/InvestorDashboard"; +import { AdminDashboard } from "./pages/dashboard/AdminDashboard"; // Profile Pages import { EntrepreneurProfile } from "./pages/profile/EntrepreneurProfile"; @@ -36,12 +37,21 @@ import { DealsPage } from "./pages/deals/DealsPage"; import { ChatPage } from "./pages/chat/ChatPage"; import { ForgotPasswordPage } from "./pages/auth/ForgotPasswordPage"; import { ResetPasswordPage } from "./pages/auth/ResetPasswordPage"; -import { VideoCall } from "./pages/webRTC/Videocall"; -import { AudioCall } from "./pages/webRTC/AudioCall"; +//import { VideoCall } from "./components/webRTC/Videocall"; +//import { AudioCall } from "./components/webRTC/AudioCall"; import { Toaster } from "react-hot-toast"; +import { UserManagement } from "./pages/admin/UserManagement"; +import { Activities } from "./pages/admin/activities"; +import { Entrepreneurj } from "./pages/admin/entrepreneur"; +import { Investors } from "./pages/admin/investors"; +import { Campaigns } from "./pages/admin/campaigns"; function App() { return ( + //
    + //

    WebRTC Test

    + // + //
    @@ -56,6 +66,15 @@ function App() { }> } /> } /> + } /> + + + }> + } /> + } /> + } /> + } /> + } /> {/* Profile Routes */} @@ -104,8 +123,7 @@ function App() { {/* Chat Routes */} }> - } /> - } /> + } /> diff --git a/src/components/camp/CampForm.tsx b/src/components/camp/CampForm.tsx new file mode 100644 index 000000000..a474e71c0 --- /dev/null +++ b/src/components/camp/CampForm.tsx @@ -0,0 +1,167 @@ +"use client"; +import React, { useState } from "react"; +import axios from "axios"; +import toast from "react-hot-toast"; + +interface CampFormProps { + onSuccess: () => void; +} + +const CampForm: React.FC = ({ onSuccess }) => { + const [formData, setFormData] = useState({ + title: "", + description: "", + goalAmount: "", + startDate: "", + endDate: "", + category: "Other", + }); + const [images, setImages] = useState(null); + const [previewUrls, setPreviewUrls] = useState([]); + const [loading, setLoading] = useState(false); + + const handleFileChange = (e: React.ChangeEvent) => { + if (e.target.files) { + setImages(e.target.files); + const urls = Array.from(e.target.files).map((file) => + URL.createObjectURL(file) + ); + setPreviewUrls(urls); + } + }; + + const handleChange = (e: React.ChangeEvent) => { + setFormData({ ...formData, [e.target.name]: e.target.value }); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + + try { + const data = new FormData(); + Object.entries(formData).forEach(([key, value]) => data.append(key, value)); + if (images) { + for (let i = 0; i < images.length; i++) { + data.append("images", images[i]); + } + } + + await axios.post("http://localhost:5000/admin/campaigns", data, { + headers: { "Content-Type": "multipart/form-data" }, + }); + + toast.success("Campaign created!"); + setFormData({ + title: "", + description: "", + goalAmount: "", + startDate: "", + endDate: "", + category: "Other", + }); + setImages(null); + setPreviewUrls([]); + onSuccess(); + } catch (error: any) { + console.error(error); +toast.error(error.response?.data?.message || error.message || "Failed to create campaign"); + + } finally { + setLoading(false); + } + }; + + return ( +
    +

    Add New Campaign

    + + + +
    + +
    + +
    + + + + {/* Update profile */} + {currentUser?.role !== "admin" && ( + + +

    + Profile Details +

    +
    + + {currentUser?.role === "investor" ? ( + + ) : ( + + )} + +
    + )} +
    + ); +}; \ No newline at end of file diff --git a/src/components/settings/SecuritySettings.tsx b/src/components/settings/SecuritySettings.tsx new file mode 100644 index 000000000..67b2d75e7 --- /dev/null +++ b/src/components/settings/SecuritySettings.tsx @@ -0,0 +1,327 @@ +import React, { useState } from "react"; +import { Card, CardHeader, CardBody } from "../ui/Card"; +import { Input } from "../ui/Input"; +import { Button } from "../ui/Button"; +import { Badge } from "../ui/Badge"; +import { Shield, Smartphone, LogOut, AlertCircle } from "lucide-react"; + +interface Session { + id: string; + device: string; + browser: string; + location: string; + lastActive: string; + isCurrent: boolean; +} + +export const SecuritySettings: React.FC = () => { + const [passwords, setPasswords] = useState({ + current: "", + new: "", + confirm: "", + }); + + const [passwordError, setPasswordError] = useState(""); + const [passwordSuccess, setPasswordSuccess] = useState(false); + const [isUpdatingPassword, setIsUpdatingPassword] = useState(false); + + const [twoFactorEnabled, setTwoFactorEnabled] = useState(false); + const [showTwoFactorSetup, setShowTwoFactorSetup] = useState(false); + const [verificationCode, setVerificationCode] = useState(""); + + const [sessions, setSessions] = useState([ + { + id: "1", + device: "Windows PC", + browser: "Chrome 119", + location: "Lahore, Punjab, PK", + lastActive: "Active now", + isCurrent: true, + }, + { + id: "2", + device: "iPhone 15", + browser: "Safari", + location: "Lahore, Punjab, PK", + lastActive: "2 hours ago", + isCurrent: false, + }, + { + id: "3", + device: "MacBook Pro", + browser: "Chrome 118", + location: "Karachi, Sindh, PK", + lastActive: "Yesterday", + isCurrent: false, + }, + ]); + + const handlePasswordChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setPasswords((prev) => ({ ...prev, [name]: value })); + setPasswordError(""); + setPasswordSuccess(false); + }; + + const validatePassword = (): boolean => { + if (!passwords.current || !passwords.new || !passwords.confirm) { + setPasswordError("All password fields are required"); + return false; + } + + if (passwords.new.length < 8) { + setPasswordError("New password must be at least 8 characters"); + return false; + } + + if (passwords.new !== passwords.confirm) { + setPasswordError("New passwords do not match"); + return false; + } + + if (passwords.current === passwords.new) { + setPasswordError("New password must be different from current password"); + return false; + } + + return true; + }; + + const handlePasswordUpdate = async () => { + if (!validatePassword()) return; + + setIsUpdatingPassword(true); + setPasswordError(""); + + try { + // Simulate API call + await new Promise((resolve) => setTimeout(resolve, 1500)); + + // TODO: Replace with actual API call + // await updatePassword(passwords.current, passwords.new); + + setPasswordSuccess(true); + setPasswords({ current: "", new: "", confirm: "" }); + + setTimeout(() => setPasswordSuccess(false), 5000); + } catch (error) { + setPasswordError("Failed to update password. Please try again."); + } finally { + setIsUpdatingPassword(false); + } + }; + + const handleToggleTwoFactor = () => { + if (twoFactorEnabled) { + // Disable 2FA + setTwoFactorEnabled(false); + setShowTwoFactorSetup(false); + } else { + // Show 2FA setup + setShowTwoFactorSetup(true); + } + }; + + const handleVerifyTwoFactor = async () => { + if (verificationCode.length !== 6) { + return; + } + + try { + // Simulate API call + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // TODO: Replace with actual API call + // await verifyTwoFactorCode(verificationCode); + + setTwoFactorEnabled(true); + setShowTwoFactorSetup(false); + setVerificationCode(""); + } catch (error) { + console.error("Failed to verify 2FA code"); + } + }; + + const handleRevokeSession = (sessionId: string) => { + setSessions((prev) => prev.filter((session) => session.id !== sessionId)); + }; + + const handleRevokeAllSessions = () => { + setSessions((prev) => prev.filter((session) => session.isCurrent)); + }; + + return ( +
    + {/* Two-Factor Authentication */} + + +
    + +

    + Two-Factor Authentication +

    +
    +
    + +
    +
    +

    + Add an extra layer of security to your account by requiring a + verification code in addition to your password. +

    + + {twoFactorEnabled ? "Enabled" : "Not Enabled"} + +
    + +
    + + {showTwoFactorSetup && !twoFactorEnabled && ( +
    +
    + +
    +

    + Set Up Two-Factor Authentication +

    +
      +
    1. 1. Download an authenticator app (Google Authenticator, Authy, etc.)
    2. +
    3. 2. Scan this QR code with your authenticator app
    4. +
    5. 3. Enter the 6-digit code below to verify
    6. +
    + + {/* Mock QR Code */} +
    +
    + QR Code +
    + Placeholder +
    +
    + +
    + setVerificationCode(e.target.value)} + maxLength={6} + /> +
    + + +
    +
    +
    +
    +
    + )} +
    +
    + + {/* Change Password */} + + +

    Change Password

    +
    + + {passwordError && ( +
    + +

    {passwordError}

    +
    + )} + + {passwordSuccess && ( +
    + +

    + Password updated successfully! +

    +
    + )} + + + + + + + +
    + +
    +
    +
    + + {/* Login History */} + + +

    + Recent Login Activity +

    +
    + +
    + {[ + { date: "Nov 26, 2025 09:45 AM", location: "Lahore, PK", status: "success" }, + { date: "Nov 25, 2025 02:30 PM", location: "Lahore, PK", status: "success" }, + { date: "Nov 24, 2025 11:20 AM", location: "Karachi, PK", status: "success" }, + { date: "Nov 23, 2025 08:15 PM", location: "Unknown", status: "failed" }, + ].map((activity, index) => ( +
    +
    +

    {activity.location}

    +

    {activity.date}

    +
    + + {activity.status === "success" ? "Success" : "Failed"} + +
    + ))} +
    +
    +
    +
    + ); +}; \ No newline at end of file diff --git a/src/components/user/UserDetails.tsx b/src/components/user/UserDetails.tsx index 1fa65d54e..9d1f639ad 100644 --- a/src/components/user/UserDetails.tsx +++ b/src/components/user/UserDetails.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useState } from "react"; import { InvestorSettings } from "../settings/InvestorSettings"; import { EntrepreneurSettings } from "../settings/EntrepreneurSettings"; import { useAuth } from "../../context/AuthContext"; +import { Card } from "../ui/Card"; export const UserDetails: React.FC = () => { const [role, setRole] = useState(null); @@ -25,17 +26,21 @@ export const UserDetails: React.FC = () => { return ( <>
    - background + {/* background */}
    -
    -
    +
    + {role === "investor" ? ( ) : ( )} -
    +
    diff --git a/src/pages/admin/FraudAndRiskDetection.tsx b/src/pages/admin/FraudAndRiskDetection.tsx new file mode 100644 index 000000000..b97b6da85 --- /dev/null +++ b/src/pages/admin/FraudAndRiskDetection.tsx @@ -0,0 +1,3 @@ +export const FraudAndRiskDetection: React.FC = () => { + return

    FraudAndRiskDetection page

    ; +}; diff --git a/src/pages/admin/UserManagement.tsx b/src/pages/admin/UserManagement.tsx deleted file mode 100644 index 9e60fb4aa..000000000 --- a/src/pages/admin/UserManagement.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export const UserManagement: React.FC = () => { - return

    User Management Page

    ; -}; diff --git a/src/pages/profile/EntrepreneurProfile.tsx b/src/pages/profile/EntrepreneurProfile.tsx index d834c5010..146c333b6 100644 --- a/src/pages/profile/EntrepreneurProfile.tsx +++ b/src/pages/profile/EntrepreneurProfile.tsx @@ -24,7 +24,7 @@ import { Entrepreneur } from "../../types"; import { AmountMeasureWithTags, getEnterpreneurById } from "../../data/users"; type Props = { - userId: string | undefined; + userId?: string | undefined; }; export const EntrepreneurProfile: React.FC = ({ userId }) => { const { id } = useParams<{ id: string }>(); diff --git a/src/pages/profile/InvestorProfile.tsx b/src/pages/profile/InvestorProfile.tsx index 1682270fa..8f8031245 100644 --- a/src/pages/profile/InvestorProfile.tsx +++ b/src/pages/profile/InvestorProfile.tsx @@ -18,7 +18,7 @@ import { getInvestorById } from "../../data/users"; import { Investor } from "../../types"; type Props = { - userId: string | undefined; + userId?: string | undefined; }; export const InvestorProfile: React.FC = ({ userId }) => { const { id } = useParams<{ id: string }>(); diff --git a/src/pages/settings/SettingsPage.tsx b/src/pages/settings/SettingsPage.tsx index f9c5d48a2..73d9bd1a2 100644 --- a/src/pages/settings/SettingsPage.tsx +++ b/src/pages/settings/SettingsPage.tsx @@ -1,20 +1,20 @@ import React, { useEffect, useState } from "react"; -import { User, Lock, Bell, Globe, Palette, CreditCard } from "lucide-react"; +import { User, Lock, 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 { Navigate } from "react-router-dom"; import { getEnterpreneurById, getInvestorById } from "../../data/users"; -import { Entrepreneur, Investor, UserRole } from "../../types"; -import { InvestorSettings } from "../../components/settings/InvestorSettings"; -import { EntrepreneurSettings } from "../../components/settings/EntrepreneurSettings"; +import { Entrepreneur, Investor } from "../../types"; +import { ProfileSettings } from "../../components/settings/ProfileSettings"; +import { SecuritySettings } from "../../components/settings/SecuritySettings"; +import { AppearanceSettings } from "../../components/settings/AppearanceSettings"; +import { BillingSettings } from "../../components/settings/BillingSettings"; + +type SettingsTab = "profile" | "security" | "appearance" | "billing"; export const SettingsPage: React.FC = () => { - const { user: currentUser, updateProfile } = useAuth(); + const { user: currentUser } = useAuth(); const [user, setUser] = useState(); + const [activeTab, setActiveTab] = useState("profile"); useEffect(() => { const fetchUser = async () => { @@ -29,48 +29,28 @@ export const SettingsPage: React.FC = () => { fetchUser(); }, [currentUser]); - type UserDetails = { - name?: string; - email?: string; - role?: UserRole | undefined; - bio: string; - location: string; - avatarUrl?: string | File | null; - }; - - const initialValues: UserDetails = { - name: currentUser?.name, - email: currentUser?.email, - role: currentUser?.role, - bio: currentUser?.bio || "", - location: currentUser?.location || "", - avatarUrl: currentUser?.avatarUrl || "", - }; - - const [userDetails, setUserDetails] = useState(initialValues); - const [isFileUploaded, setIsFileUploaded] = useState(false); - - const handleChange = ( - e: React.ChangeEvent - ) => { - const target = e.target as HTMLInputElement & { files?: FileList }; - const { name, value, files } = target; - if (name === "avatarUrl") { - const file = files?.[0] ?? null; - setUserDetails((prev) => ({ ...prev, avatarUrl: file })); - setIsFileUploaded(!!file); - } else { - setUserDetails((prev) => ({ ...prev, [name]: value } as UserDetails)); + const navItems = [ + { id: "profile" as SettingsTab, label: "Profile", icon: User }, + { id: "security" as SettingsTab, label: "Security", icon: Lock }, + { id: "appearance" as SettingsTab, label: "Appearance", icon: Palette }, + { id: "billing" as SettingsTab, label: "Billing", icon: CreditCard }, + ]; + + const renderContent = () => { + switch (activeTab) { + case "profile": + return ; + case "security": + return ; + case "appearance": + return ; + case "billing": + return ; + default: + return ; } }; - const handleSubmit = async (e: React.MouseEvent) => { - e.preventDefault(); - if (!user) return; - // userId guaranteed because of the guard - updateProfile(user.userId, userDetails); - }; - return (
    @@ -81,182 +61,67 @@ export const SettingsPage: React.FC = () => {
    - {/* Settings navigation */} - - + {/* Desktop Settings Navigation */} + + +

    Settings

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

    - Profile Settings -

    -
    - -
    - - -
    - - - - -

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

    -
    -
    - -
    - - - - - - -
    - -
    - - -
    - -
    - -
    -
    -
    - - {/* Update profile */} - - -

    - Profile Details -

    -
    - - {currentUser?.role === "investor" ? ( - - ) : ( - - )} - -
    - - {/* Security Settings */} - - -

    - Security Settings -

    -
    - -
    -

    - Two-Factor Authentication -

    -
    -
    -

    - Add an extra layer of security to your account -

    - - Not Enabled - -
    - -
    -
    - -
    -

    - Change Password -

    -
    - - - - - - -
    - -
    -
    -
    + {/* Mobile Settings Navigation - Icon Only */} +
    + + +
    + + {/* Main settings content */} +
    {renderContent()}
    ); -}; +}; \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index 9b89e9954..4e0782d98 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,8 +1,7 @@ -export type UserRole = 'entrepreneur' | 'investor' | 'admin'; - +export type UserRole = "entrepreneur" | "investor" | "admin"; export interface User { - userId: string; + userId: string | undefined; name: string; email: string; role: UserRole; @@ -19,19 +18,19 @@ export interface Entrepreneur extends User { industry: string | undefined; foundedYear: number | undefined; teamSize: number | undefined; - revenue:string | undefined; - profitMargin:number | undefined; - growthRate:number | undefined; - marketOpportunity:string | undefined; - advantage:string | undefined; + revenue: string | undefined; + profitMargin: number | undefined; + growthRate: number | undefined; + marketOpportunity: string | undefined; + advantage: string | undefined; } export interface Investor extends User { investmentInterests: string[] | undefined; portfolioCompanies: string[] | undefined; totalInvestments: number | undefined; - minimumInvestment: string | undefined; - maximumInvestment: string | undefined; + minimumInvestment: number | undefined; + maximumInvestment: number | undefined; investmentCriteria: string[] | undefined; successfullExits: number | undefined; minTimline: number | undefined;