(null);
+ const [isViewModalOpen, setIsViewModalOpen] = useState(false);
+
+ useEffect(() => {
+ fetchDeals();
+ }, []);
+
+ const fetchDeals = async () => {
+ try {
+ setLoading(true);
+ const res = await axios.get(`${URL}/deal/get-all-deals`);
+ setDeals(res.data);
+ } catch (error) {
+ console.error("Error fetching deals:", error);
+ toast.error("Failed to load deals.");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const openViewModal = (deal: any) => {
+ setSelectedDeal(deal);
+ setIsViewModalOpen(true);
+ };
+
+ const filteredDeals = deals.filter((deal) => {
+ const term = searchTerm.toLowerCase();
+ return (
+ deal.investorId?.name?.toLowerCase().includes(term) ||
+ deal.entrepreneurId?.name?.toLowerCase().includes(term) ||
+ deal.entrepreneurId?.startupName?.toLowerCase().includes(term) ||
+ deal.status?.toLowerCase().includes(term)
+ );
+ });
+
+ if (loading) return Loading deals...
;
+
+ return (
+
+
+
All Deal Records
+
+
+ setSearchTerm(e.target.value)}
+ />
+
+
+
+ {filteredDeals.length === 0 ? (
+
No deals found.
+ ) : (
+
+ {filteredDeals.map((deal) => (
+
+
+
+
+
Investor
+
+ {deal.investorId?.name || "Unknown"}
+
+
{deal.investorId?.email}
+
+
+
+
Entrepreneur
+
+ {deal.entrepreneurId?.name || "Unknown"}
+
+
{deal.entrepreneurId?.startupName}
+
+
+
+
+
+ {deal.status.charAt(0).toUpperCase() + deal.status.slice(1)}
+
+ {deal.paymentStatus === 'funds_released' && (
+
+ Payment Approved
+
+ )}
+
Created: {new Date(deal.createdAt).toLocaleDateString()}
+
+
+
+
+
+
Amount
+
${deal.investmentAmount?.toLocaleString()}
+
+
+
Equity
+
{deal.equityOffered}%
+
+
+
Valuation
+
${deal.postMoneyValuation?.toLocaleString()}
+
+
+
Type
+
{deal.investmentType}
+
+
+
+
+ {deal.negotiationHistory?.length > 0 && (
+
+ Negotiated
+ {deal.negotiationHistory.length} round(s) of negotiation
+
+ )}
+
+
+
+
+ ))}
+
+ )}
+
+ {/* View Modal (Read Only) */}
+ {isViewModalOpen && selectedDeal && (
+
setIsViewModalOpen(false)}
+ readOnly={true}
+ initialData={selectedDeal}
+ />
+ )}
+
+ );
+};
diff --git a/src/pages/admin/UserApprovals.tsx b/src/pages/admin/UserApprovals.tsx
index 861613e5f..feeaaa40c 100644
--- a/src/pages/admin/UserApprovals.tsx
+++ b/src/pages/admin/UserApprovals.tsx
@@ -79,6 +79,7 @@ export const UserApprovals: React.FC = () => {
show: false,
userId: null
});
+ const [emailStatus, setEmailStatus] = useState<{ [key: string]: { status: 'verifying' | 'exists' | 'not_exists' | 'error', reason?: string } }>({});
const URL = import.meta.env.VITE_BACKEND_URL;
const token = localStorage.getItem("token");
@@ -117,6 +118,36 @@ export const UserApprovals: React.FC = () => {
fetchApprovalData();
}, []);
+ const verifyEmail = async (email: string) => {
+ if (emailStatus[email]) return;
+
+ setEmailStatus(prev => ({ ...prev, [email]: { status: 'verifying' } }));
+ try {
+ const res = await axios.get(`${URL}/admin/verify-email/${email}`, {
+ headers: { Authorization: `Bearer ${token}` },
+ });
+ setEmailStatus(prev => ({
+ ...prev,
+ [email]: {
+ status: res.data.exists ? 'exists' : 'not_exists',
+ reason: res.data.reason
+ }
+ }));
+ } catch (error) {
+ setEmailStatus(prev => ({ ...prev, [email]: { status: 'error' } }));
+ }
+ };
+
+ useEffect(() => {
+ // Verify emails for all pending users
+ const allPending = [...pendingUsers.entrepreneurs, ...pendingUsers.investors];
+ allPending.forEach(user => {
+ if (!emailStatus[user.email]) {
+ verifyEmail(user.email);
+ }
+ });
+ }, [pendingUsers]);
+
// Approve user
const handleApprove = async (userId: string) => {
try {
@@ -219,6 +250,14 @@ export const UserApprovals: React.FC = () => {
{user.email}
+ {emailStatus[user.email]?.status === 'not_exists' && (
+
+ {emailStatus[user.email]?.reason === 'Disposable email' ? 'Fake Disposable' : 'Invalid/Fake Email'}
+
+ )}
diff --git a/src/pages/admin/supporters.tsx b/src/pages/admin/supporters.tsx
index 544b40c53..abba4299d 100644
--- a/src/pages/admin/supporters.tsx
+++ b/src/pages/admin/supporters.tsx
@@ -1,115 +1,112 @@
import React, { useEffect, useRef, useState } from "react";
import { Input } from "../../components/ui/Input";
-import { SearchIcon, Trash } from "lucide-react";
+import { SearchIcon } from "lucide-react";
import { Button } from "../../components/ui/Button";
import { ThreeDotsButton } from "../../components/ui/ThreeDotsButton";
+import axios from "axios";
+import toast from "react-hot-toast";
interface Supporter {
- _id: string;
+ id: string;
name: string;
email: string;
+ phone: string;
campaign: string;
amount: number;
+ date: string;
+ type: string;
}
export const Supporters: React.FC = () => {
- const [supporters, setSupportors] = useState([]);
- const [searchedSupportors, setSearchedSupportors] = useState([]);
+ const [supporters, setSupporters] = useState([]);
+ const [searchedSupporters, setSearchedSupporters] = useState([]);
const [query, setQuery] = useState("");
const [searched, setSearched] = useState("");
+ const [isLoading, setIsLoading] = useState(true);
+
+ const URL = import.meta.env.VITE_BACKEND_URL;
+
+ const fetchSupporters = async () => {
+ try {
+ setIsLoading(true);
+ const token = localStorage.getItem("token");
+ const res = await axios.get(`${URL}/payment/all-donations`, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ setSupporters(res.data);
+ } catch (err: any) {
+ console.error("Error fetching supporters:", err);
+ toast.error("Failed to fetch supporters data");
+ } finally {
+ setIsLoading(false);
+ }
+ };
useEffect(() => {
- // DUMMY SUPPORTERS DATA
- const dumySupporters: Supporter[] = [
- {
- _id: "1",
- name: "Ali Raza",
- email: "ali@example.com",
- campaign: "Eco-Friendly Water Bottles",
- amount: 5000,
- },
- {
- _id: "2",
- name: "Jessica Smith",
- email: "jessica@example.com",
- campaign: "AI Study Planner App",
- amount: 12000,
- },
- {
- _id: "3",
- name: "Hassan Khan",
- email: "hassan@example.com",
- campaign: "Organic Farming Project",
- amount: 3000,
- },
- ];
- setSupportors(dumySupporters);
+ fetchSupporters();
}, []);
+
const [showDialog, setShowDialog] = useState(false);
- const [index, setIndex] = useState(null);
- const dialogRef = useRef(null);
+ const [activeIndex, setActiveIndex] = useState(null);
+ const dialogRef = useRef(null);
// close on outside click
useEffect(() => {
- function handleClickOutside(e) {
- if (dialogRef.current && !dialogRef.current.contains(e.target)) {
+ function handleClickOutside(e: MouseEvent) {
+ if (dialogRef.current && !dialogRef.current.contains(e.target as Node)) {
setShowDialog(false);
- setIndex(null);
+ setActiveIndex(null);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
- const TableRow = ({ sup, idx }) => {
+ const TableRow = ({ sup, idx }: { sup: Supporter; idx: number }) => {
return (
-
- | {sup.name} |
- {sup.email} |
- {sup.campaign} |
-
-
-
- ${sup.amount.toLocaleString()}
+
+ | {sup.name} |
+ {sup.email} |
+ {sup.phone} |
+ {sup.campaign} |
+
+ ${sup.amount.toLocaleString()}
+ |
+
+ {new Date(sup.date).toLocaleDateString()}
+ |
+
+
+ {sup.type}
+ |
- {/* 3-dots button */}
+
{
e.stopPropagation();
- setIndex(idx);
+ setActiveIndex(idx);
setShowDialog((prev) => !prev);
}}
/>
- {/* dropdown menu */}
- {showDialog && index === idx && (
+ {showDialog && activeIndex === idx && (
-
-
)}
@@ -118,84 +115,109 @@ export const Supporters: React.FC = () => {
);
};
+ const handleSearch = (e: React.FormEvent) => {
+ e.preventDefault();
+ setSearched(query);
+ const filtered = supporters.filter((sup) =>
+ sup.name.toLowerCase().includes(query.toLowerCase()) ||
+ sup.email.toLowerCase().includes(query.toLowerCase()) ||
+ sup.campaign.toLowerCase().includes(query.toLowerCase()) ||
+ sup.phone.toLowerCase().includes(query.toLowerCase())
+ );
+ setSearchedSupporters(filtered);
+ };
+
return (
-
-
- Crowdfund Supporters
-
-
-
diff --git a/src/pages/campaignPage/CampaignPage.tsx b/src/pages/campaignPage/CampaignPage.tsx
index 2f80634a3..ea2ad7ab6 100644
--- a/src/pages/campaignPage/CampaignPage.tsx
+++ b/src/pages/campaignPage/CampaignPage.tsx
@@ -1,9 +1,14 @@
import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
+import axios from "axios";
+import { toast } from "react-hot-toast";
+import { loadStripe } from "@stripe/stripe-js";
+import { Elements } from "@stripe/react-stripe-js";
+import { StripeDonationForm } from "../../components/camp/StripeDonationForm";
import { Navbar } from "../../components/home/Navbar";
import { Button } from "../../components/ui/Button";
-import axios from "axios";
-import toast from "react-hot-toast";
+
+const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY);
interface CampaignCardProps {
_id: string;
@@ -16,6 +21,7 @@ interface CampaignCardProps {
organizer?: string;
endDate: string;
status: string;
+ isLifetime?: boolean;
supporters?: Array<{
supporterId: string;
amount: number;
@@ -37,6 +43,7 @@ export const CampaignsPage: React.FC = () => {
const URL = import.meta.env.VITE_BACKEND_URL;
const navigate = useNavigate();
const [campaigns, setCampaigns] = useState([]);
+ const [allCampaigns, setAllCampaigns] = useState([]);
const [filteredCampaigns, setFilteredCampaigns] = useState([]);
const [loading, setLoading] = useState(true);
const [selectedCategory, setSelectedCategory] = useState("All");
@@ -56,23 +63,25 @@ export const CampaignsPage: React.FC = () => {
const categories = ["Technology", "Health", "Education", "Environment", "Other"];
// Fetch active campaigns from database
- useEffect(() => {
- const fetchCampaigns = async () => {
- try {
- setLoading(true);
- const response = await axios.get(`${URL}/admin/campaigns`);
- // Filter only active campaigns
- const activeCampaigns = response.data.filter((campaign: CampaignCardProps) => campaign.status === "active");
- setCampaigns(activeCampaigns);
- setFilteredCampaigns(activeCampaigns);
- } catch (error) {
- console.error("Failed to fetch campaigns:", error);
- toast.error("Failed to load campaigns");
- } finally {
- setLoading(false);
- }
- };
+ const fetchCampaigns = async () => {
+ try {
+ setLoading(true);
+ const response = await axios.get(`${URL}/admin/campaigns`);
+ const data = response.data || [];
+ setAllCampaigns(data);
+ // Filter only active campaigns for the main display grid
+ const activeCampaigns = data.filter((campaign: CampaignCardProps) => campaign.status === "active");
+ setCampaigns(activeCampaigns);
+ setFilteredCampaigns(activeCampaigns);
+ } catch (error) {
+ console.error("Failed to fetch campaigns:", error);
+ toast.error("Failed to load campaigns");
+ } finally {
+ setLoading(false);
+ }
+ };
+ useEffect(() => {
fetchCampaigns();
}, [URL]);
@@ -103,7 +112,8 @@ export const CampaignsPage: React.FC = () => {
raisedAmount,
images,
organizer,
- endDate
+ endDate,
+ isLifetime
}) => {
const progress = (raisedAmount / goalAmount) * 100;
const daysLeft = calculateDaysLeft(endDate);
@@ -121,7 +131,7 @@ export const CampaignsPage: React.FC = () => {
{/* Days Left Badge */}
- ⏳ {daysLeft} days left
+ {isLifetime ? "⏳ Lifetime" : `⏳ ${daysLeft} days left`}
@@ -202,20 +212,6 @@ export const CampaignsPage: React.FC = () => {
);
};
- const handleDonationSubmit = (e: React.FormEvent) => {
- e.preventDefault();
- alert(`Thank you for your donation of $${donationForm.amount} to "${donationForm.campaignTitle}"!`);
- setShowDonationForm(false);
- setDonationForm({
- cardNumber: "",
- cardHolder: "",
- expiryDate: "",
- cvv: "",
- amount: 0,
- campaignId: "",
- campaignTitle: ""
- });
- };
const handleAmountClick = (amount: number) => {
setDonationForm(prev => ({ ...prev, amount }));
@@ -254,32 +250,29 @@ export const CampaignsPage: React.FC = () => {
Discover and donate to campaigns making a real difference in people's lives. Every contribution matters.
+
- {/* Stats */}
-
-
- {campaigns.length}
- Active Campaigns
-
-
-
- ${campaigns.reduce((sum, c) => sum + c.raisedAmount, 0).toLocaleString()}
-
- Total Raised
-
-
-
- {campaigns.reduce((sum, c) => sum + (c.supporters?.length || 0), 0)}+
+ {/* Stats Bar - Widened to full container */}
+
+ {[
+ { label: "Active Campaigns", value: campaigns.length, color: "text-blue-500" },
+ { label: "Total Campaigns", value: allCampaigns.length, color: "text-blue-500" },
+ { label: "Total Raised", value: `$${allCampaigns.reduce((sum, c) => sum + c.raisedAmount, 0).toLocaleString()}`, color: "text-green-500" },
+ { label: "Total Donors", value: `${allCampaigns.reduce((sum, c) => sum + (c.supporters?.length || 0), 0)}+`, color: "text-blue-500" },
+ { label: "Avg. Success Rate", value: `${allCampaigns.length > 0 ? Math.round((allCampaigns.filter(c => c.raisedAmount >= c.goalAmount).length / allCampaigns.length) * 100) : 0}%`, color: "text-purple-500" }
+ ].map((stat, idx) => (
+
+
+ {stat.label}
- Donors
-
-
-
- {campaigns.length > 0 ? Math.round((campaigns.filter(c => c.raisedAmount >= c.goalAmount).length / campaigns.length) * 100) : 0}%
+
+ {stat.value}
- Success Rate
-
+ ))}
@@ -429,108 +422,20 @@ export const CampaignsPage: React.FC = () => {
- {/* Payment Form */}
-
+ {/* Stripe Donation Form */}
+
+
+ {
+ handleCloseModal();
+ fetchCampaigns();
+ }}
+ onCancel={handleCloseModal}
+ />
+
+
diff --git a/src/pages/dashCamp/DashboardCampaignDetail.tsx b/src/pages/dashCamp/DashboardCampaignDetail.tsx
index 648dbfa32..5c6d7d9ba 100644
--- a/src/pages/dashCamp/DashboardCampaignDetail.tsx
+++ b/src/pages/dashCamp/DashboardCampaignDetail.tsx
@@ -4,8 +4,13 @@ import axios from "axios";
import toast from "react-hot-toast";
import { Button } from "../../components/ui/Button";
import { Card, CardBody } from "../../components/ui/Card";
-import { ArrowLeft, Calendar, Clock, Share2, ShieldCheck } from "lucide-react";
+import { ArrowLeft, Calendar, Clock, Share2, ShieldCheck, X, Rocket } from "lucide-react";
import { ShareModal } from "../../components/common/ShareModal";
+import { loadStripe } from "@stripe/stripe-js";
+import { Elements } from "@stripe/react-stripe-js";
+import { StripeDashboardPaymentForm } from "../../components/dashboard/StripeDashboardPaymentForm";
+
+const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY);
interface Campaign {
_id: string;
@@ -19,6 +24,7 @@ interface Campaign {
organizer?: string;
endDate: string;
startDate: string;
+ isLifetime?: boolean;
status: string;
supporters?: Array<{
supporterId: string;
@@ -36,29 +42,31 @@ export const DashboardCampaignDetail: React.FC = () => {
const [loading, setLoading] = useState(true);
const [currentImageIndex, setCurrentImageIndex] = useState(0);
const [isShareModalOpen, setIsShareModalOpen] = useState(false);
+ const [showDonationForm, setShowDonationForm] = useState(false);
+ const [donationAmount, setDonationAmount] = useState(0);
- useEffect(() => {
- const fetchCampaign = async () => {
- try {
- setLoading(true);
- const response = await axios.get(`${URL}/admin/campaigns`);
- const foundCampaign = response.data.find((c: Campaign) => c._id === id);
+ const fetchCampaign = async () => {
+ try {
+ setLoading(true);
+ const response = await axios.get(`${URL}/admin/campaigns`);
+ const foundCampaign = response.data.find((c: Campaign) => c._id === id);
- if (foundCampaign) {
- setCampaign(foundCampaign);
- } else {
- toast.error("Campaign not found");
- navigate("/dashboard/campaigns");
- }
- } catch (error) {
- console.error("Failed to fetch campaign:", error);
- toast.error("Failed to load campaign details");
+ if (foundCampaign) {
+ setCampaign(foundCampaign);
+ } else {
+ toast.error("Campaign not found");
navigate("/dashboard/campaigns");
- } finally {
- setLoading(false);
}
- };
+ } catch (error) {
+ console.error("Failed to fetch campaign:", error);
+ toast.error("Failed to load campaign details");
+ navigate("/dashboard/campaigns");
+ } finally {
+ setLoading(false);
+ }
+ };
+ useEffect(() => {
if (id) {
fetchCampaign();
}
@@ -97,199 +105,272 @@ export const DashboardCampaignDetail: React.FC = () => {
const daysLeft = calculateDaysLeft(campaign.endDate);
return (
-
- {/* Header */}
-
-
-
-
- {campaign.title}
-
+ <>
+
+ {/* Header */}
+
+
+
+
+ {campaign.title}
+
+
-
-
- }
- onClick={() => setIsShareModalOpen(true)}
- >
- Share
-
- {/*
+ {/* navigate("/All-Campaigns")} // Redirecting to landing page for donation as requested "go back to all campaigns" or "donate"
className="bg-primary-600 text-white"
>
Support Campaign
*/}
+
-
-
-
- {/* Left Column - Media & Description */}
-
- {/* Media Slider */}
-
- {campaign.images && campaign.images.length > 0 && (
-
- {campaign.images.map((img, idx) => (
- 
- ))}
- {/* Indicators */}
-
- {campaign.images.map((_, idx) => (
-
+ {/* Left Column - Media & Description */}
+
+ {/* Media Slider */}
+
+ {campaign.images && campaign.images.length > 0 && (
+
+ {campaign.images.map((img, idx) => (
+ ![]() setCurrentImageIndex(idx)}
- className={`w-2.5 h-2.5 rounded-full transition-all ${idx === currentImageIndex ? "bg-white w-8" : "bg-white/40"
+ src={`${URL}${img}`}
+ className={`absolute inset-0 w-full h-full object-contain transition-opacity duration-1000 ${idx === currentImageIndex ? "opacity-100" : "opacity-0"
}`}
+ alt={campaign.title}
/>
))}
-
- {/* Floating Labels */}
-
-
- {campaign.category}
-
-
- {daysLeft} Days Left
-
-
-
- )}
-
+ {/* Indicators */}
+
+ {campaign.images.map((_, idx) => (
+ setCurrentImageIndex(idx)}
+ className={`w-2.5 h-2.5 rounded-full transition-all ${idx === currentImageIndex ? "bg-white w-8" : "bg-white/40"
+ }`}
+ />
+ ))}
+
- {/* About Section */}
-
-
- About This Campaign
-
- {campaign.description}
-
-
-
+ {/* Floating Labels */}
+
+
+ {campaign.category}
+
+
+ {campaign.isLifetime ? "Lifetime" : `${daysLeft} Days Left`}
+
+
+
+ )}
+
- {/* Video Section if available */}
- {campaign.video && (
+ {/* About Section */}
- Campaign Video
-
-
+ About This Campaign
+
+ {campaign.description}
- )}
-
- {/* Right Column - Stats & Action */}
-
- {/* Progress Card - Now Light Themed */}
-
-
-
- Total Raised
-
- ${campaign.raisedAmount.toLocaleString()}
- USD
-
- Goal: ${campaign.goalAmount.toLocaleString()}
-
+ {/* Video Section if available */}
+ {campaign.video && (
+
+
+ Campaign Video
+
+
+
+
+
+ )}
+
-
-
- {progress.toFixed(1)}%
- {campaign.supporters?.length || 0} Supporters
-
-
-
+ {/* Right Column - Stats & Action */}
+
+ {/* Progress Card - Now Light Themed */}
+
+
+
+ Total Raised
+
+ ${campaign.raisedAmount.toLocaleString()}
+ USD
+
+ Goal: ${campaign.goalAmount.toLocaleString()}
-
-
-
- Donors
- {campaign.supporters?.length || 0}
+
+
+ {progress.toFixed(1)}%
+ {campaign.supporters?.length || 0} Supporters
+
+
-
- Days Left
- {daysLeft}
+
+
+
+ Donors
+ {campaign.supporters?.length || 0}
+
+
+ Days Left
+ {campaign.isLifetime ? "Lifetime" : daysLeft}
+
-
-
-
+
+
- {/* Meta Info */}
-
-
-
-
-
+ {/* Meta Info */}
+
+
+
+
+
+
+
+ Verified Organizer
+ {campaign.organizer || "TrustBridge Admin"}
+
-
- Verified Organizer
- {campaign.organizer || "TrustBridge Admin"}
+
+
+
+
+
+
+ Timeline
+
+ {new Date(campaign.startDate).toLocaleDateString()} - {campaign.isLifetime ? "Lifetime" : new Date(campaign.endDate).toLocaleDateString()}
+
+
+
+
+
+ {/* Call to Action Card - Now Light/Blue Themed */}
+
+
+ Make a global impact
+
+ Your contribution can help achieve the goal and change lives. Support this campaign directly on our main platform.
+
+ {
+ setDonationAmount(Math.max(25, Math.ceil((campaign.goalAmount - campaign.raisedAmount) * 0.01)));
+ setShowDonationForm(true);
+ }}
+ className="w-full font-bold shadow-md shadow-primary-200"
+ >
+ Contribute Now
+
+
+
+
+
+ setIsShareModalOpen(false)}
+ title={campaign.title}
+ url={window.location.href}
+ theme="light"
+ />
+
+ {/* Donation Form Modal */}
+ {showDonationForm && campaign && (
+
+
+ {/* Header */}
+
+
+
+
+ Support {campaign.title}
+
+ setShowDonationForm(false)}
+ className="p-2 rounded-full hover:bg-gray-100 text-gray-400 transition-colors"
+ >
+
+
+
-
-
-
+
+ {/* Donation Amount Selection */}
+
+
+
+ {[25, 50, 100, 250, 500, 1000].map((amount) => (
+ setDonationAmount(amount)}
+ className={`py-3 rounded-xl font-bold transition-all text-sm ${donationAmount === amount
+ ? 'bg-primary-600 text-white shadow-lg shadow-primary-200'
+ : 'bg-gray-50 text-gray-600 hover:bg-gray-100'
+ }`}
+ >
+ ${amount}
+
+ ))}
-
- Timeline
-
- {new Date(campaign.startDate).toLocaleDateString()} - {new Date(campaign.endDate).toLocaleDateString()}
-
+
+ $
+ setDonationAmount(parseInt(e.target.value) || 0)}
+ className="w-full pl-8 pr-4 py-3 bg-gray-50 border border-gray-100 rounded-xl text-gray-900 focus:outline-none focus:border-primary-500 transition-colors font-bold"
+ placeholder="Custom Amount"
+ />
-
-
- {/* Call to Action Card - Now Light/Blue Themed */}
-
-
- Make a global impact
-
- Your contribution can help achieve the goal and change lives. Support this campaign directly on our main platform.
-
- navigate("/All-Campaigns")}
- className="w-full font-bold shadow-md shadow-primary-200"
- >
- Contribute Now
-
-
+
+ {
+ setShowDonationForm(false);
+ fetchCampaign();
+ }}
+ onCancel={() => setShowDonationForm(false)}
+ />
+
+
+
-
-
- setIsShareModalOpen(false)}
- title={campaign.title}
- url={window.location.href}
- theme="light"
- />
-
+ )}
+ >
);
};
diff --git a/src/pages/dashCamp/DashboardCampaigns.tsx b/src/pages/dashCamp/DashboardCampaigns.tsx
index 12e243422..3708c61b7 100644
--- a/src/pages/dashCamp/DashboardCampaigns.tsx
+++ b/src/pages/dashCamp/DashboardCampaigns.tsx
@@ -4,7 +4,12 @@ import { Button } from "../../components/ui/Button";
import { Card, CardBody } from "../../components/ui/Card";
import axios from "axios";
import toast from "react-hot-toast";
-import { Rocket, Clock, Target, TrendingUp } from "lucide-react";
+import { Rocket, Clock, Target, TrendingUp, X } from "lucide-react";
+import { loadStripe } from "@stripe/stripe-js";
+import { Elements } from "@stripe/react-stripe-js";
+import { StripeDashboardPaymentForm } from "../../components/dashboard/StripeDashboardPaymentForm";
+
+const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY);
interface CampaignCardProps {
_id: string;
@@ -17,6 +22,7 @@ interface CampaignCardProps {
organizer?: string;
endDate: string;
status: string;
+ isLifetime?: boolean;
supporters?: Array<{
supporterId: string;
amount: number;
@@ -31,25 +37,28 @@ export const DashboardCampaigns: React.FC = () => {
const [filteredCampaigns, setFilteredCampaigns] = useState ([]);
const [loading, setLoading] = useState(true);
const [selectedCategory, setSelectedCategory] = useState("All");
+ const [showDonationForm, setShowDonationForm] = useState(false);
+ const [selectedCampaign, setSelectedCampaign] = useState(null);
+ const [donationAmount, setDonationAmount] = useState(0);
const categories = ["Technology", "Health", "Education", "Environment", "Other"];
- useEffect(() => {
- const fetchCampaigns = async () => {
- try {
- setLoading(true);
- const response = await axios.get(`${URL}/admin/campaigns`);
- const activeCampaigns = response.data.filter((campaign: CampaignCardProps) => campaign.status === "active");
- setCampaigns(activeCampaigns);
- setFilteredCampaigns(activeCampaigns);
- } catch (error) {
- console.error("Failed to fetch campaigns:", error);
- toast.error("Failed to load campaigns");
- } finally {
- setLoading(false);
- }
- };
+ const fetchCampaigns = async () => {
+ try {
+ setLoading(true);
+ const response = await axios.get(`${URL}/admin/campaigns`);
+ const activeCampaigns = response.data.filter((campaign: CampaignCardProps) => campaign.status === "active");
+ setCampaigns(activeCampaigns);
+ setFilteredCampaigns(activeCampaigns);
+ } catch (error) {
+ console.error("Failed to fetch campaigns:", error);
+ toast.error("Failed to load campaigns");
+ } finally {
+ setLoading(false);
+ }
+ };
+ useEffect(() => {
fetchCampaigns();
}, [URL]);
@@ -78,11 +87,12 @@ export const DashboardCampaigns: React.FC = () => {
raisedAmount,
images,
organizer,
- endDate
+ endDate,
+ isLifetime
}) => {
const progress = (raisedAmount / goalAmount) * 100;
const daysLeft = calculateDaysLeft(endDate);
- const displayImage = images && images.length > 0 ? `${URL}${images[0]}` : "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=800";
+ const displayImage = images && images.length > 0 ? `${URL}${images[0]} ` : "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=800";
return (
@@ -100,7 +110,7 @@ export const DashboardCampaigns: React.FC = () => {
- {daysLeft} days left
+ {isLifetime ? "Lifetime" : `${daysLeft} days left`}
@@ -129,7 +139,7 @@ export const DashboardCampaigns: React.FC = () => {
@@ -145,7 +155,11 @@ export const DashboardCampaigns: React.FC = () => {
View Details
navigate(`/All-Campaigns`)}
+ onClick={() => {
+ setSelectedCampaign({ _id, title, description, category, goalAmount, raisedAmount, images, organizer, endDate, status: "active" });
+ setDonationAmount(Math.max(25, Math.ceil((goalAmount - raisedAmount) * 0.01)));
+ setShowDonationForm(true);
+ }}
className="py-2.5 text-xs font-bold bg-primary-600 text-white hover:bg-primary-700 shadow-sm hover:shadow-primary-200 transition-all duration-300 transform hover:-translate-y-0.5"
>
Contribute
@@ -158,111 +172,184 @@ export const DashboardCampaigns: React.FC = () => {
};
return (
-
-
-
- Active Campaigns
- Discover and support life-changing causes within our community.
-
+ <>
+
+
+
+ Active Campaigns
+ Discover and support life-changing causes within our community.
+
-
- setSelectedCategory("All")}
- className={`px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap transition-all ${selectedCategory === "All"
- ? "bg-primary-600 text-white shadow-md shadow-primary-200"
- : "bg-white text-gray-600 border border-gray-200 hover:border-primary-300"
- }`}
- >
- All
-
- {categories.map((cat) => (
+
setSelectedCategory(cat)}
- className={`px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap transition-all ${selectedCategory === cat
+ onClick={() => setSelectedCategory("All")}
+ className={`px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap transition-all ${selectedCategory === "All"
? "bg-primary-600 text-white shadow-md shadow-primary-200"
: "bg-white text-gray-600 border border-gray-200 hover:border-primary-300"
}`}
>
- {cat}
+ All
- ))}
+ {categories.map((cat) => (
+ setSelectedCategory(cat)}
+ className={`px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap transition-all ${selectedCategory === cat
+ ? "bg-primary-600 text-white shadow-md shadow-primary-200"
+ : "bg-white text-gray-600 border border-gray-200 hover:border-primary-300"
+ }`}
+ >
+ {cat}
+
+ ))}
+
-
- {/* Stats Summary */}
-
-
-
-
-
-
-
- Active Campaigns
- {campaigns.length}
-
-
-
+ {/* Stats Summary */}
+
+
+
+
+
+
+
+ Active Campaigns
+ {campaigns.length}
+
+
+
-
-
-
-
-
-
- Total Goal
-
- ${campaigns.reduce((sum, c) => sum + c.goalAmount, 0).toLocaleString()}
-
-
-
-
+
+
+
+
+
+
+ Total Goal
+
+ ${campaigns.reduce((sum, c) => sum + c.goalAmount, 0).toLocaleString()}
+
+
+
+
-
-
-
-
-
-
- Total Raised
-
- ${campaigns.reduce((sum, c) => sum + c.raisedAmount, 0).toLocaleString()}
-
+
+
+
+
+
+
+ Total Raised
+
+ ${campaigns.reduce((sum, c) => sum + c.raisedAmount, 0).toLocaleString()}
+
+
+
+
+
+
+ {loading ? (
+
+ ) : filteredCampaigns.length === 0 ? (
+
+
+
-
-
+ No campaigns found
+
+ {selectedCategory === "All"
+ ? "There are no active campaigns at the moment."
+ : `No active campaigns in the ${selectedCategory} category.`}
+
+ setSelectedCategory("All")}
+ >
+ Clear Filters
+
+
+ ) : (
+
+ {filteredCampaigns.map((campaign) => (
+
+ ))}
+
+ )}
+
- {loading ? (
-
- ) : filteredCampaigns.length === 0 ? (
-
-
-
+
+ {/* Donation Form Modal */}
+ {showDonationForm && selectedCampaign && (
+
+
+ {/* Header */}
+
+
+
+
+ Support {selectedCampaign.title}
+
+
+ setShowDonationForm(false)}
+ className="p-2 rounded-full hover:bg-gray-100 text-gray-400 transition-colors"
+ >
+
+
+
+
+
+ {/* Donation Amount Selection */}
+
+
+
+ {[25, 50, 100, 250, 500, 1000].map((amount) => (
+ setDonationAmount(amount)}
+ className={`py-3 rounded-xl font-bold transition-all text-sm ${donationAmount === amount
+ ? 'bg-primary-600 text-white shadow-lg shadow-primary-200'
+ : 'bg-gray-50 text-gray-600 hover:bg-gray-100'
+ }`}
+ >
+ ${amount}
+
+ ))}
+
+
+ $
+ setDonationAmount(parseInt(e.target.value) || 0)}
+ className="w-full pl-8 pr-4 py-3 bg-gray-50 border border-gray-100 rounded-xl text-gray-900 focus:outline-none focus:border-primary-500 transition-colors font-bold"
+ placeholder="Custom Amount"
+ />
+
+
+
+
+ {
+ setShowDonationForm(false);
+ fetchCampaigns();
+ }}
+ onCancel={() => setShowDonationForm(false)}
+ />
+
+
- No campaigns found
-
- {selectedCategory === "All"
- ? "There are no active campaigns at the moment."
- : `No active campaigns in the ${selectedCategory} category.`}
-
- setSelectedCategory("All")}
- >
- Clear Filters
-
-
- ) : (
-
- {filteredCampaigns.map((campaign) => (
-
- ))}
)}
-
+ >
);
};
diff --git a/src/pages/dashboard/AdminDashboard.tsx b/src/pages/dashboard/AdminDashboard.tsx
index 09f155ae2..55efb82f8 100644
--- a/src/pages/dashboard/AdminDashboard.tsx
+++ b/src/pages/dashboard/AdminDashboard.tsx
@@ -2,9 +2,7 @@ import React, { useEffect, useState } from "react";
import {
Users,
TrendingUp,
- AlertTriangle,
Shield,
- MessageSquare,
UserX,
Ban,
UserCheck
@@ -18,7 +16,7 @@ import axios from "axios";
import { FraudAndRiskDetectionChart } from "../../components/admin/FraudAndRiskDetectionChart";
import { StartupGrowthChart } from "../../components/admin/StartupGrowthChart";
import { StartupIndustryChart } from "../../components/admin/StartupIndustryChart";
-import { FundingChart } from "../../components/admin/FundingChart";
+import { CampaignAnalyticsChart } from "../../components/admin/CampaignAnalyticsChart";
type ChartData = {
month: string;
@@ -31,12 +29,15 @@ export const AdminDashboard: React.FC = () => {
const { user } = useAuth();
const [stats, setStats] = useState({
approvedUsers: 0,
- supporters: 10,
+ supporters: 0,
campaigns: 0,
suspendedUsers: 0,
blockedUsers: 0,
});
const navigate = useNavigate();
+ const [campaignsChartData, setCampaignsChartData] = useState<
+ { name: string; raised: number; goal: number }[]
+ >([]);
useEffect(() => {
const fetchData = async () => {
@@ -45,43 +46,56 @@ export const AdminDashboard: React.FC = () => {
// Assuming the API returns total users, we'll need to calculate approved users
// For now, let's set a placeholder. In reality, you might need an API endpoint
// that specifically returns approved users count.
-
+
// First, let's fetch all users to calculate counts
const usersRes = await axios.get(`${URL}/admin/get-users`, {
headers: { Authorization: `Bearer ${localStorage.getItem("token")}` }
});
-
+
const allUsers = usersRes.data;
-
+
// Calculate counts based on user status
- const approvedCount = allUsers.filter((u: any) =>
- u.status === 'approved' ||
+ const approvedCount = allUsers.filter((u: any) =>
+ u.status === 'approved' ||
u.approvalStatus === 'approved' ||
u.isApproved === true ||
- (u.status !== 'pending' && u.status !== 'rejected' && !u.isBlocked && !u.isSuspended)
+ !(u.status !== 'pending' && u.status !== 'rejected')
).length;
-
- const suspendedCount = allUsers.filter((u: any) =>
- u.isSuspended === true ||
+
+ const suspendedCount = allUsers.filter((u: any) =>
+ u.isSuspended === true ||
u.suspended === true ||
u.status === 'suspended'
).length;
-
- const blockedCount = allUsers.filter((u: any) =>
- u.isBlocked === true ||
+
+ const blockedCount = allUsers.filter((u: any) =>
+ u.isBlocked === true ||
u.blocked === true ||
u.status === 'blocked'
).length;
-
+
// Update stats with calculated values
setStats({
approvedUsers: approvedCount,
- supporters: 10, // Keeping as static for now
+ supporters: res.data.supporters || 0,
campaigns: res.data.campaigns || 0,
suspendedUsers: suspendedCount,
blockedUsers: blockedCount
});
-
+
+ // Also fetch active campaigns for the chart
+ const campaignsRes = await axios.get(`${URL}/admin/campaigns`);
+ if (campaignsRes.data && Array.isArray(campaignsRes.data)) {
+ const activeCampaignsData = campaignsRes.data
+ .filter((c: any) => c.status === "active")
+ .map((c: any) => ({
+ name: c.title,
+ raised: c.raisedAmount,
+ goal: c.goalAmount,
+ }));
+ setCampaignsChartData(activeCampaignsData);
+ }
+
} catch (error) {
console.error("Error fetching admin stats:", error);
}
@@ -284,9 +298,9 @@ export const AdminDashboard: React.FC = () => {
-
+
- Fund GrowthRate
+ Campaign Progress Analytics
diff --git a/src/pages/dashboard/EntrepreneurDashboard.tsx b/src/pages/dashboard/EntrepreneurDashboard.tsx
index 214fd6d6d..37442502a 100644
--- a/src/pages/dashboard/EntrepreneurDashboard.tsx
+++ b/src/pages/dashboard/EntrepreneurDashboard.tsx
@@ -25,7 +25,7 @@ export const EntrepreneurDashboard: React.FC = () => {
CollaborationRequest[]
>([]);
const [recommendedInvestors, setRecommendedInvestors] = useState([]);
-
+
useEffect(() => {
const fetchData = async () => {
if (user) {
@@ -48,8 +48,8 @@ export const EntrepreneurDashboard: React.FC = () => {
const pendingRequests =
collaborationRequests.length > 0 &&
- Array.isArray(collaborationRequests) &&
- collaborationRequests.length > 0
+ Array.isArray(collaborationRequests) &&
+ collaborationRequests.length > 0
? collaborationRequests.filter((req) => req.requestStatus === "pending")
: [];
@@ -59,10 +59,10 @@ export const EntrepreneurDashboard: React.FC = () => {
) => {
setCollaborationRequests((prevRequests) =>
prevRequests.map((req) =>
- req._id === requestId ? { ...req, requestStatus:status } : req
+ req._id === requestId ? { ...req, requestStatus: status } : req
)
);
- updateRequestStatus(requestId,status)
+ updateRequestStatus(requestId, status)
};
return (
@@ -77,9 +77,16 @@ export const EntrepreneurDashboard: React.FC = () => {
-
- }>Find Investors
-
+
+
+ }>
+ Requests
+
+
+
+ }>Find Investors
+
+
{/* Summary cards */}
diff --git a/src/pages/dashboard/EntrepreneurRequests.tsx b/src/pages/dashboard/EntrepreneurRequests.tsx
new file mode 100644
index 000000000..858aa08c4
--- /dev/null
+++ b/src/pages/dashboard/EntrepreneurRequests.tsx
@@ -0,0 +1,127 @@
+import React, { useEffect, useState } from "react";
+import { useAuth } from "../../context/AuthContext";
+import {
+ getRequestsForEntrepreneur,
+ updateRequestStatus,
+} from "../../data/collaborationRequests";
+import { CollaborationRequest } from "../../types";
+import { Card, CardBody, CardHeader } from "../../components/ui/Card";
+import { Button } from "../../components/ui/Button";
+import { Avatar } from "../../components/ui/Avatar";
+import { Check, X, Clock } from "lucide-react";
+import toast from "react-hot-toast";
+
+export const EntrepreneurRequests: React.FC = () => {
+ const { user } = useAuth();
+ const [requests, setRequests] = useState ([]);
+ const [isLoading, setIsLoading] = useState(true);
+
+ const fetchRequests = async () => {
+ if (user?.userId) {
+ try {
+ const data = await getRequestsForEntrepreneur(user.userId);
+ setRequests(data);
+ } catch (error) {
+ console.error("Error fetching requests:", error);
+ } finally {
+ setIsLoading(false);
+ }
+ }
+ };
+
+ useEffect(() => {
+ fetchRequests();
+ }, [user?.userId]);
+
+ const handleStatusUpdate = async (
+ requestId: string,
+ newStatus: "accepted" | "rejected"
+ ) => {
+ try {
+ await updateRequestStatus(requestId, newStatus);
+ toast.success(`Request ${newStatus}`);
+ fetchRequests(); // Refresh list
+ } catch (error) {
+ toast.error("Failed to update status");
+ }
+ };
+
+ if (isLoading) {
+ return Loading requests... ;
+ }
+
+ return (
+
+ Collaboration Requests
+
+ {requests.length === 0 ? (
+
+
+ No collaboration requests found.
+
+
+ ) : (
+
+ {requests.map((request) => (
+
+
+
+
+
+
+ {request.inves_id?.name || "Unknown Investor"}
+
+
+ {new Date(request.time).toLocaleDateString()}
+
+
+ "{request.message}"
+
+
+
+
+
+ {request.requestStatus === "pending" ? (
+ <>
+ }
+ onClick={() => handleStatusUpdate(request._id, "rejected")}
+ >
+ Reject
+
+ }
+ onClick={() => handleStatusUpdate(request._id, "accepted")}
+ >
+ Accept
+
+ >
+ ) : (
+
+ {request.requestStatus === "accepted" ? (
+
+ ) : (
+
+ )}
+ {request.requestStatus.charAt(0).toUpperCase() + request.requestStatus.slice(1)}
+
+ )}
+
+
+
+ ))}
+
+ )}
+
+ );
+};
diff --git a/src/pages/dashboard/ManageTeam.tsx b/src/pages/dashboard/ManageTeam.tsx
new file mode 100644
index 000000000..5948cf049
--- /dev/null
+++ b/src/pages/dashboard/ManageTeam.tsx
@@ -0,0 +1,370 @@
+import React, { useEffect, useState, useRef } from "react";
+import { Link, useNavigate } from "react-router-dom";
+import { useAuth } from "../../context/AuthContext";
+import { getEnterpreneurById, addTeamMember, updateTeamMember, deleteTeamMember } from "../../data/users";
+import { Entrepreneur, TeamMember } from "../../types";
+import { Card, CardBody, CardHeader } from "../../components/ui/Card";
+import { Button } from "../../components/ui/Button";
+import { Avatar } from "../../components/ui/Avatar";
+import { Plus, Edit2, Trash2, X, Upload, ArrowLeft, ChevronDown } from "lucide-react";
+import toast from "react-hot-toast";
+
+export const ManageTeam: React.FC = () => {
+ const { user } = useAuth();
+ const navigate = useNavigate();
+ const [entrepreneur, setEntrepreneur] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const [editingMember, setEditingMember] = useState(null);
+ const [showDropdown, setShowDropdown] = useState(false);
+
+ // Form State
+ const [name, setName] = useState("");
+ const [roles, setRoles] = useState(""); // Comma separated string for input
+ const [avatarFile, setAvatarFile] = useState(null);
+ const [previewUrl, setPreviewUrl] = useState("");
+ const fileInputRef = useRef(null);
+
+ // Predefined roles for dropdown
+ const predefinedRoles = [
+ "CEO",
+ "CTO",
+ "CFO",
+ "COO",
+ "Founder",
+ "Co-Founder",
+ "Manager",
+ "Team Lead",
+ "Developer",
+ "Designer",
+ "Marketing Head",
+ "Sales Head",
+ "Product Manager",
+ "Operations Manager",
+ "HR Manager",
+ "Advisor",
+ "Investor",
+ "Board Member",
+ "Consultant"
+ ];
+
+ const fetchData = async () => {
+ if (user?.userId) {
+ try {
+ const data = await getEnterpreneurById(user.userId);
+ setEntrepreneur(data);
+ } catch (error) {
+ console.error(error);
+ toast.error("Failed to load profile");
+ } finally {
+ setIsLoading(false);
+ }
+ }
+ };
+
+ useEffect(() => {
+ fetchData();
+ }, [user]);
+
+ const handleOpenModal = (member?: TeamMember) => {
+ if (member) {
+ setEditingMember(member);
+ setName(member.name);
+ setRoles(member.role.join(", "));
+ setPreviewUrl(member.avatarUrl);
+ } else {
+ setEditingMember(null);
+ setName("");
+ setRoles("");
+ setAvatarFile(null);
+ setPreviewUrl("");
+ }
+ setIsModalOpen(true);
+ setShowDropdown(false);
+ };
+
+ const handleCloseModal = () => {
+ setIsModalOpen(false);
+ setEditingMember(null);
+ setShowDropdown(false);
+ };
+
+ const handleFileChange = (e: React.ChangeEvent) => {
+ if (e.target.files && e.target.files[0]) {
+ const file = e.target.files[0];
+ setAvatarFile(file);
+ setPreviewUrl(URL.createObjectURL(file));
+ }
+ };
+
+ const handleRoleSelect = (role: string) => {
+ const currentRoles = roles.split(",").map(r => r.trim()).filter(r => r !== "");
+
+ // Check if role already exists
+ if (currentRoles.includes(role)) {
+ // Remove role if already selected
+ const updatedRoles = currentRoles.filter(r => r !== role);
+ setRoles(updatedRoles.join(", "));
+ } else {
+ // Add role
+ currentRoles.push(role);
+ setRoles(currentRoles.join(", "));
+ }
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!user?.userId) return;
+
+ const formData = new FormData();
+ formData.append("name", name);
+
+ // Split roles by comma and trim
+ let rolesArray = roles.split(",").map(r => r.trim()).filter(r => r !== "");
+
+ // If no roles are provided, set default role
+ if (rolesArray.length === 0) {
+ rolesArray = ["Member"];
+ }
+
+ rolesArray.forEach(r => formData.append("role", r));
+
+ if (avatarFile) {
+ formData.append("avatarUrl", avatarFile);
+ }
+
+ try {
+ if (editingMember) {
+ if (editingMember._id) {
+ await updateTeamMember(user.userId, editingMember._id, formData);
+ toast.success("Team member updated successfully");
+ }
+ } else {
+ await addTeamMember(user.userId, formData);
+ toast.success("Team member added successfully");
+ }
+ setIsModalOpen(false);
+ fetchData(); // Refresh list
+ } catch (error) {
+ console.error(error);
+ toast.error("Failed to save team member");
+ }
+ };
+
+ const handleDelete = async (memberId: string) => {
+ if (!user?.userId) return;
+ if (window.confirm("Are you sure you want to remove this team member?")) {
+ try {
+ await deleteTeamMember(user.userId, memberId);
+ toast.success("Team member removed successfully");
+ fetchData();
+ } catch (error) {
+ console.error(error);
+ toast.error("Failed to remove team member");
+ }
+ }
+ };
+
+ if (isLoading) return Loading... ;
+
+ return (
+
+
+
+
+ }>Back to Profile
+
+ Manage Team
+
+ } onClick={() => handleOpenModal()}>
+ Add Team Member
+
+
+
+
+ {entrepreneur?.team && entrepreneur.team.length > 0 ? (
+ entrepreneur.team.map((member) => (
+
+
+
+
+
+
+ {member.name}
+
+ {member.role.map((r, idx) => (
+
+ {r}
+
+ ))}
+
+
+
+
+ handleOpenModal(member)}
+ className="p-2 text-gray-400 hover:text-blue-600 transition-colors"
+ >
+
+
+ member._id && handleDelete(member._id)}
+ className="p-2 text-gray-400 hover:text-red-600 transition-colors"
+ >
+
+
+
+
+
+
+ ))
+ ) : (
+
+ No team members added yet.
+ handleOpenModal()}>
+ Add your first team member
+
+
+ )}
+
+
+ {/* Modal */}
+ {isModalOpen && (
+
+
+
+
+
+
+
+ {editingMember ? "Edit Team Member" : "Add Team Member"}
+
+
+
+
+
+ )}
+
+ );
+};
\ No newline at end of file
diff --git a/src/pages/deals/DealsPage.tsx b/src/pages/deals/DealsPage.tsx
index dbd7a6613..54a3a932e 100644
--- a/src/pages/deals/DealsPage.tsx
+++ b/src/pages/deals/DealsPage.tsx
@@ -1,29 +1,79 @@
-import React, { useState } from "react";
+import React, { useState, useEffect } from "react";
+import axios from "axios";
import { Card, CardBody, CardHeader } from "../../components/ui/Card";
+import { Button } from "../../components/ui/Button";
+import { toast } from "react-hot-toast";
+import { useAuth } from "../../context/AuthContext";
+import { DealForm } from "../../components/DealForm";
+import { NegotiationModal } from "../../components/NegotiationModal";
+import { loadStripe } from "@stripe/stripe-js";
+import { Elements } from "@stripe/react-stripe-js";
+import { DealPaymentModal } from "../../components/DealPaymentModal";
+import { useNavigate } from "react-router-dom";
+
+// Initialize Stripe outside component to avoid recreation
+const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY);
+
+const URL = import.meta.env.VITE_BACKEND_URL;
export const DealsPage: React.FC = () => {
- const [deals, setDeals] = useState([
- {
- _id: "1",
- investorName: "John Doe",
- investorEmail: "john@example.com",
- businessName: "Acme Startup",
- amount: 50000,
- equity: 5,
- message: "We want to invest in your startup.",
- status: "accepted",
- },
- {
- _id: "2",
- investorName: "Jane Smith",
- investorEmail: "jane@example.com",
- businessName: "Acme Startup",
- amount: 100000,
- equity: 10,
- message: "We are interested in funding your growth.",
- status: "pending",
- },
- ]);
+ const { user } = useAuth();
+ const [deals, setDeals] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const navigate = useNavigate();
+ // Modal states
+ const [selectedDeal, setSelectedDeal] = useState(null);
+ const [isViewModalOpen, setIsViewModalOpen] = useState(false);
+ const [isNegotiationModalOpen, setIsNegotiationModalOpen] = useState(false);
+ const [isPaymentModalOpen, setIsPaymentModalOpen] = useState(false);
+
+ useEffect(() => {
+ fetchDeals();
+ }, [user]);
+
+ const fetchDeals = async () => {
+ if (!user?.userId) return;
+ try {
+ setLoading(true);
+ const res = await axios.get(`${URL}/deal/get-deals/${user.userId}`);
+ setDeals(res.data);
+ } catch (error) {
+ console.error("Error fetching deals:", error);
+ toast.error("Failed to load deals.");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleDealStatus = async (
+ dealId: string,
+ action: "accept" | "reject"
+ ) => {
+ try {
+ await axios.put(`${URL}/deal/update-deal/${dealId}`, {
+ action,
+ role: "investor",
+ });
+ toast.success(`Deal ${action === 'accept' ? 'accepted' : 'rejected'}!`);
+ fetchDeals(); // Refresh list
+ } catch (error) {
+ console.error(`Error ${action}ing deal:`, error);
+ toast.error(`Failed to ${action} deal.`);
+ }
+ };
+
+
+ const openViewModal = (deal: any) => {
+ setSelectedDeal(deal);
+ setIsViewModalOpen(true);
+ };
+
+ const openNegotiationModal = (deal: any) => {
+ setSelectedDeal(deal);
+ setIsNegotiationModalOpen(true);
+ };
+
+ if (loading) return Loading deals... ;
return (
@@ -38,40 +88,151 @@ export const DealsPage: React.FC = () => {
- {deal.investorName}
+ {deal.entrepreneurId?.startupName || "Startup"}
- {deal.investorEmail}
+ {deal.entrepreneurId?.name}
+
+ Sent {new Date(deal.createdAt).toLocaleDateString()}
+
- {deal.status}
+ {deal.status.charAt(0).toUpperCase() + deal.status.slice(1)}
- Business: {deal.businessName}
+ Investment Amount: ${deal.investmentAmount?.toLocaleString()}
- Investment Amount: ${deal.amount}
+ Valuation (Post-Money): ${deal.postMoneyValuation?.toLocaleString()}
- Requested Equity: {deal.equity}%
-
-
- Message: {deal.message}
+ Equity Offered: {deal.equityOffered}%
+
+
+ openViewModal(deal)}
+ >
+ See Details
+
+
+ {(deal.status === "negotiating" && deal.lastActionBy === 'entrepreneur') && (
+ <>
+ openNegotiationModal(deal)}
+ >
+ Review Counter Offer
+
+ handleDealStatus(deal._id, "accept")}
+ >
+ Accept Offer
+
+ handleDealStatus(deal._id, "reject")}
+ >
+ Reject
+
+ >
+ )}
+
+ {deal.status === 'accepted' && (
+ navigate(`/chat/${deal.entrepreneurId?._id}`)}>
+ Chat with Founder
+
+ )}
+
+ {deal.negotiationHistory &&
+ deal.negotiationHistory.length > 0 &&
+ deal.lastActionBy === "entrepreneur" &&
+ deal.status === "negotiating" && (
+ openNegotiationModal(deal)}
+ >
+ Review Counter Offer
+
+ )}
+
+ {deal.status === 'accepted' && deal.paymentStatus !== 'paid' && deal.paymentStatus !== 'funds_released' && (
+ {
+ setSelectedDeal(deal);
+ setIsPaymentModalOpen(true);
+ }}
+ >
+ Invest & Pay
+
+ )}
+
+ {(deal.paymentStatus === 'paid' || deal.paymentStatus === 'funds_released') && (
+
+ Payment {deal.paymentStatus === 'funds_released' ? 'Released' : 'Pending Approval'}
+
+ )}
+
))}
)}
+
+ {/* View Modal (Read Only) */}
+ {isViewModalOpen && selectedDeal && (
+ setIsViewModalOpen(false)}
+ readOnly={true}
+ initialData={selectedDeal}
+ />
+ )}
+
+ {/* Negotiation Modal */}
+ {isNegotiationModalOpen && selectedDeal && (
+ {
+ setIsNegotiationModalOpen(false);
+ fetchDeals();
+ }}
+ role="investor"
+ />
+ )}
+
+ {isPaymentModalOpen && selectedDeal && (
+
+ setIsPaymentModalOpen(false)}
+ onSuccess={() => {
+ fetchDeals();
+ }}
+ />
+
+ )}
);
};
diff --git a/src/pages/documents/DocumentsPage.tsx b/src/pages/documents/DocumentsPage.tsx
index d682db52c..c23e7b86c 100644
--- a/src/pages/documents/DocumentsPage.tsx
+++ b/src/pages/documents/DocumentsPage.tsx
@@ -1,165 +1,330 @@
-import React, { useState } from "react";
-import { FileText, Upload, Download, Trash2, Share2 } from "lucide-react";
+import React, { useState, useEffect } from "react";
+import { FileText, Upload, Download, Trash2, CheckCircle2, AlertCircle, FileUp, Loader2 } from "lucide-react";
import { Card, CardHeader, CardBody } from "../../components/ui/Card";
import { Button } from "../../components/ui/Button";
import { Badge } from "../../components/ui/Badge";
+import axios from "axios";
+import toast from "react-hot-toast";
+import { PdfPreviewModal } from "../../components/ui/PdfPreviewModal";
+import { Eye } from "lucide-react";
-interface Document {
- id: number;
- name: string;
+interface DBDocument {
+ _id: string;
type: string;
- size: string;
- lastModified: string;
- shared: boolean;
+ fileUrl: string;
+ fileName: string;
+ uploadedAt: string;
}
+const DOCUMENT_TYPES = [
+ {
+ id: "pitch_deck",
+ name: "Pitch Deck",
+ requirements: [
+ "Problem & Solution", "Product / Demo", "Market Size (TAM, SAM, SOM)",
+ "Business Model", "Traction & Growth", "Competition", "Team",
+ "Financial Highlights", "Funding Ask & Equity Offered", "Exit Strategy"
+ ]
+ },
+ {
+ id: "business_plan",
+ name: "Business Plan",
+ requirements: [
+ "Company Overview", "Vision & Mission", "Detailed Market Analysis",
+ "Product / Service Details", "Marketing & Sales Strategy", "Operations Plan",
+ "Team & Roles", "Legal Structure", "Risk Analysis", "Long-term Growth Plan"
+ ]
+ },
+ {
+ id: "financial_projections",
+ name: "Financial Projections",
+ requirements: [
+ "3–5 Year Revenue Forecast", "Expense Forecast", "Profit & Loss Statement",
+ "Cash Flow Projection", "Break-Even Analysis", "Burn Rate & Runway",
+ "Unit Economics (CAC, LTV)", "Valuation Assumptions"
+ ]
+ },
+ {
+ id: "legal_docs",
+ name: "Company Registration & Legal Docs",
+ requirements: [
+ "Certificate of Incorporation", "SECP Registration", "NTN / Tax Registration",
+ "Memorandum & Articles", "Shareholder Agreements"
+ ]
+ },
+ {
+ id: "revenue_traction",
+ name: "Revenue & Traction Report",
+ requirements: [
+ "Monthly Active Users", "Revenue Growth", "Customer Acquisition",
+ "Retention Rate", "Partnerships", "Contracts / LOIs"
+ ]
+ },
+ {
+ id: "use_of_funds",
+ name: "Use of Funds",
+ requirements: [
+ "Product Development", "Marketing", "Hiring", "Operations",
+ "Legal & Compliance", "Runway Timeline"
+ ]
+ }
+];
+
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]);
+ const [documents, setDocuments] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [uploading, setUploading] = useState(null);
+ const [previewDoc, setPreviewDoc] = useState<{ url: string; name: string } | null>(null);
+
+ const URL = import.meta.env.VITE_BACKEND_URL;
+ const token = localStorage.getItem("token");
+
+ const fetchDocuments = async () => {
+ try {
+ setLoading(true);
+ const res = await axios.get(`${URL}/document`, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ setDocuments(res.data.documents);
+ } catch (error) {
+ console.error(error);
+ toast.error("Failed to load documents");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchDocuments();
+ }, []);
+
+ const handleUpload = async (e: React.ChangeEvent, typeName: string) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+
+ if (file.type !== "application/pdf") {
+ toast.error("Format must be PDF");
+ return;
+ }
+
+ setUploading(typeName);
+ const formData = new FormData();
+ formData.append("file", file);
+ formData.append("type", typeName);
+
+ try {
+ await axios.post(`${URL}/document/upload`, formData, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ "Content-Type": "multipart/form-data"
+ }
+ });
+ toast.success(`${typeName} uploaded successfully`);
+ fetchDocuments();
+ } catch (error: any) {
+ console.error(error);
+ toast.error(error.response?.data?.message || "Upload failed");
+ } finally {
+ setUploading(null);
+ }
};
+ const handleDownload = async (fileUrl: string, fileName: string) => {
+ try {
+ const response = await axios.get(fileUrl, {
+ responseType: 'blob',
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ const url = window.URL.createObjectURL(new Blob([response.data]));
+ const link = document.createElement('a');
+ link.href = url;
+ link.setAttribute('download', fileName);
+ document.body.appendChild(link);
+ link.click();
+ link.parentNode?.removeChild(link);
+ window.URL.revokeObjectURL(url);
+ } catch (error) {
+ console.error("Download failed:", error);
+ toast.error("Download failed");
+ }
+ };
+
+ const handleDelete = async (id: string) => {
+ if (!window.confirm("Are you sure you want to delete this document?")) return;
+ try {
+ await axios.delete(`${URL}/document/${id}`, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ toast.success("Document deleted");
+ fetchDocuments();
+ } catch (error) {
+ console.error(error);
+ toast.error("Delete failed");
+ }
+ };
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
return (
-
+
- Documents
- Manage your startup's important files
+ Startup Documents
+ Complete your profile by uploading these key documents. You can upload them when you want.
-
-
-
- {/* Storage info */}
-
-
- Storage
-
-
-
-
- Used
- 12.5 GB
-
-
-
- Available
- 7.5 GB
-
-
-
-
-
- {/* Document list */}
-
-
-
-
- All Documents
-
-
-
- Sort by
-
-
- Filter
-
-
-
-
-
- {documents.map((doc) => (
-
-
-
-
+
+ {DOCUMENT_TYPES.map((docType) => {
+ const uploadedDoc = documents.find(d => d.type === docType.name);
-
-
-
- {doc.name}
-
- {doc.shared && (
-
- Shared
-
- )}
+ return (
+
+
+
+ {uploadedDoc ? (
+
+ Completed
+
+ ) : (
+
+ Pending
+
+ )}
+
+
+
+
+ What to Include:
+
+
+ {docType.requirements.map((req, idx) => (
+
+
+ {req}
+ ))}
+
+
-
- {doc.type}
- {doc.size}
- Modified {doc.lastModified}
+
+ {uploadedDoc ? (
+
+
+
+
+
+ {uploadedDoc.fileName}
+ Uploaded on {new Date(uploadedDoc.uploadedAt).toLocaleDateString()}
+
+
+
+ setPreviewDoc({ url: `${URL}${uploadedDoc.fileUrl}`, name: uploadedDoc.fileName })}
+ className="p-2 hover:bg-white rounded-lg text-gray-600 hover:text-blue-600 transition-colors border border-transparent hover:border-gray-200"
+ title="View PDF"
+ >
+
+
+ handleDownload(`${URL}${uploadedDoc.fileUrl}`, uploadedDoc.fileName)}
+ className="p-2 hover:bg-white rounded-lg text-gray-600 hover:text-green-600 transition-colors border border-transparent hover:border-gray-200"
+ title="Download PDF"
+ >
+
+
+ handleDelete(uploadedDoc._id)}
+ className="p-2 hover:bg-white rounded-lg text-red-400 hover:text-red-600 transition-colors border border-transparent hover:border-gray-200"
+ title="Delete"
+ >
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+ handleUpload(e, docType.name)}
+ disabled={uploading === docType.name}
+ />
+ document.getElementById(`replace-${docType.id}`)?.click()}
+ >
+ {uploading === docType.name ? : }
+ Replace PDF
+
+
+
+ ) : (
+
+ handleUpload(e, docType.name)}
+ disabled={uploading === docType.name}
+ />
document.getElementById(`upload-${docType.id}`)?.click()}
>
-
+ {uploading === docType.name ? (
+
+
+ Uploading...
+
+ ) : (
+
+
+ Upload {docType.name}
+
+ )}
-
- ))}
-
-
-
-
+ )}
+ * Only PDF format accepted
+
+
+
+ );
+ })}
+
+ {previewDoc && (
+ setPreviewDoc(null)}
+ fileUrl={previewDoc.url}
+ fileName={previewDoc.name}
+ />
+ )}
);
};
+
+export default DocumentsPage;
diff --git a/src/pages/entrepreneurs/EntrepreneursPage.tsx b/src/pages/entrepreneurs/EntrepreneursPage.tsx
index 2404d60d8..6c81d9c45 100644
--- a/src/pages/entrepreneurs/EntrepreneursPage.tsx
+++ b/src/pages/entrepreneurs/EntrepreneursPage.tsx
@@ -11,6 +11,7 @@ export const EntrepreneursPage: React.FC = () => {
const [searchQuery, setSearchQuery] = useState('');
const [selectedIndustries, setSelectedIndustries] = useState ([]);
const [selectedFundingRange, setSelectedFundingRange] = useState([]);
+ const [selectedLocations, setSelectedLocations] = useState([]);
const { user } = useAuth();
// Get unique industries and funding ranges
const [entrepreneurs, setEnterprenuers] = useState([]);
@@ -23,37 +24,44 @@ export const EntrepreneursPage: React.FC = () => {
}
}
fetchData();
- }, []);
- const allIndustries = Array.from(new Set(entrepreneurs.map(e => e.industry)));
+ }, [user]);
+
+ const allIndustries = Array.from(new Set(entrepreneurs.map(e => e.industry || 'Other').filter(Boolean)));
+ // Filter out undefined/null locations and get unique ones
+ const allLocations = Array.from(new Set(entrepreneurs.map(e => e.location).filter(Boolean)));
+
const fundingRanges = ['< $500K', '$500K - $1M', '$1M - $5M', '> $5M'];
// Filter entrepreneurs based on search and filters
const filteredEntrepreneurs = entrepreneurs.filter(entrepreneur => {
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());
+ (entrepreneur.startupName || '').toLowerCase().includes(searchQuery.toLowerCase()) ||
+ (entrepreneur.industry || '').toLowerCase().includes(searchQuery.toLowerCase()) ||
+ (entrepreneur.pitchSummary || '').toLowerCase().includes(searchQuery.toLowerCase());
const matchesIndustry = selectedIndustries.length === 0 ||
- selectedIndustries.includes(entrepreneur.industry);
+ selectedIndustries.includes(entrepreneur.industry || 'Other');
+
+ const matchesLocation = selectedLocations.length === 0 ||
+ selectedLocations.includes(entrepreneur.location);
// Simple funding range filter based on the amount string
const matchesFunding = selectedFundingRange.length === 0 ||
selectedFundingRange.some(range => {
- const amount = parseInt(entrepreneur.fundingNeeded.replace(/[^0-9]/g, ''));
+ const amount = entrepreneur.fundingNeeded || 0; // It's already a number
switch (range) {
- case '< $500K': return amount < 500;
- case '$500K - $1M': return amount >= 500 && amount <= 1000;
- case '$1M - $5M': return amount > 1000 && amount <= 5000;
- case '> $5M': return amount > 5000;
+ case '< $500K': return amount < 500000;
+ case '$500K - $1M': return amount >= 500000 && amount <= 1000000;
+ case '$1M - $5M': return amount > 1000000 && amount <= 5000000;
+ case '> $5M': return amount > 5000000;
default: return true;
}
});
const isApproved = entrepreneur.approvalStatus === 'approved';
- return matchesSearch && matchesIndustry && matchesFunding && isApproved;
+ return matchesSearch && matchesIndustry && matchesFunding && matchesLocation && isApproved;
});
const toggleIndustry = (industry: string) => {
@@ -72,6 +80,14 @@ export const EntrepreneursPage: React.FC = () => {
);
};
+ const toggleLocation = (location: string) => {
+ setSelectedLocations(prev =>
+ prev.includes(location)
+ ? prev.filter(l => l !== location)
+ : [...prev, location]
+ );
+ };
+
return (
@@ -102,6 +118,7 @@ export const EntrepreneursPage: React.FC = () => {
{industry}
))}
+ {allIndustries.length === 0 && No industries found. }
@@ -125,19 +142,24 @@ export const EntrepreneursPage: React.FC = () => {
Location
-
-
-
- San Francisco, CA
-
-
-
- New York, NY
-
-
-
- Boston, MA
-
+
+ {allLocations.length > 0 ? (
+ allLocations.map(location => (
+ toggleLocation(location)}
+ className={`flex items-center w-full text-left px-3 py-2 rounded-md text-sm ${selectedLocations.includes(location)
+ ? 'bg-primary-50 text-primary-700'
+ : 'text-gray-700 hover:bg-gray-50'
+ }`}
+ >
+
+ {location}
+
+ ))
+ ) : (
+ No locations found.
+ )}
@@ -166,7 +188,7 @@ export const EntrepreneursPage: React.FC = () => {
{filteredEntrepreneurs.map(entrepreneur => (
))}
diff --git a/src/pages/fundraises/Fundraises-Page.tsx b/src/pages/fundraises/Fundraises-Page.tsx
index 27367942a..ade20f9ff 100644
--- a/src/pages/fundraises/Fundraises-Page.tsx
+++ b/src/pages/fundraises/Fundraises-Page.tsx
@@ -1,390 +1,405 @@
-import React, { useState } from "react";
+import React, { useState, useEffect } from "react";
import { Navbar } from "../../components/home/Navbar";
import { Button } from "../../components/ui/Button";
import { Link, useNavigate } from "react-router-dom";
+import { useAuth } from "../../context/AuthContext";
+import axios from "axios";
+import { Entrepreneur } from "../../types";
+import {
+ Loader2,
+ ChevronLeft,
+ ChevronRight,
+ MapPin,
+ Users,
+ Target,
+ Layers,
+ TrendingUp,
+ Building2,
+ Clock,
+ Percent,
+ Sparkles,
+ Filter,
+ Zap,
+ ArrowRight,
+ ArrowLeft
+} from "lucide-react";
interface BusinessCardProps {
- id: number;
- name: string;
- description: string;
- industry: string;
- fundingGoal: number;
- fundsRaised: number;
- equityOffered: number;
- valuation: number;
- image: string;
- location: string;
- foundedYear: number;
- teamSize: number;
- stage: "Seed" | "Series A" | "Series B" | "Series C";
- investors: number;
+ entrepreneur: Entrepreneur;
}
-interface InvestmentModalData {
- businessId: number;
- businessName: string;
- investmentAmount: number;
- equityPercentage: number;
- estimatedReturn: number;
-}
+// Entrepreneurs display page with filters and showcase slider
export const FundraisePage: React.FC = () => {
const navigate = useNavigate();
- const [selectedBusiness, setSelectedBusiness] = useState (null);
- const [showInvestmentModal, setShowInvestmentModal] = useState(false);
- const [investmentData, setInvestmentData] = useState({
- businessId: 0,
- businessName: "",
- investmentAmount: 0,
- equityPercentage: 0,
- estimatedReturn: 0
+ const { user: currentUser } = useAuth();
+ const [entrepreneurs, setEntrepreneurs] = useState([]);
+ const [filteredEntrepreneurs, setFilteredEntrepreneurs] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [selectedIndustry, setSelectedIndustry] = useState("All Industries");
+ const [selectedStage, setSelectedStage] = useState("All Stages");
+ const [platformStats, setPlatformStats] = useState({
+ totalInvested: 0,
+ totalInvestors: 0,
+ activeDeals: 0
});
- const [isLoggedIn, setIsLoggedIn] = useState(false); // In real app, get from auth context
-
- // Sample business data
- const businesses: BusinessCardProps[] = [
- {
- id: 1,
- name: "Quantum AI",
- description: "Building quantum computing solutions for drug discovery and material science. Proprietary algorithms reduce simulation time by 90%.",
- industry: "Deep Tech",
- fundingGoal: 5000000,
- fundsRaised: 2500000,
- equityOffered: 15,
- valuation: 33333333,
- image: "https://images.unsplash.com/photo-1620712943543-bcc4688e7485?w=800",
- location: "San Francisco, CA",
- foundedYear: 2021,
- teamSize: 18,
- stage: "Series A",
- investors: 45
- },
- {
- id: 2,
- name: "NeuroTech",
- description: "Non-invasive brain-computer interface for treating neurological disorders. FDA approval pending for clinical trials.",
- industry: "HealthTech",
- fundingGoal: 3000000,
- fundsRaised: 1800000,
- equityOffered: 12,
- valuation: 25000000,
- image: "https://images.unsplash.com/photo-1559757148-5c350d0d3c56?w=800",
- location: "Boston, MA",
- foundedYear: 2020,
- teamSize: 12,
- stage: "Seed",
- investors: 32
- },
- {
- id: 3,
- name: "Green Energy Solutions",
- description: "Revolutionary solar panel technology with 40% higher efficiency using perovskite materials. Manufacturing plant ready for scale.",
- industry: "Clean Energy",
- fundingGoal: 8000000,
- fundsRaised: 5500000,
- equityOffered: 20,
- valuation: 40000000,
- image: "https://images.unsplash.com-1559757172-5c350d0d3c56?w=800",
- location: "Austin, TX",
- foundedYear: 2019,
- teamSize: 24,
- stage: "Series B",
- investors: 78
- },
- {
- id: 4,
- name: "AgriTech Robotics",
- description: "Autonomous farming robots that reduce water usage by 60% and increase crop yield by 35%. Currently deployed in 50+ farms.",
- industry: "AgriTech",
- fundingGoal: 4000000,
- fundsRaised: 2200000,
- equityOffered: 10,
- valuation: 40000000,
- image: "https://images.unsplash.com/photo-1581094794329-c8112a89af12?w=800",
- location: "Chicago, IL",
- foundedYear: 2020,
- teamSize: 16,
- stage: "Series A",
- investors: 41
- },
- {
- id: 5,
- name: "Space Logistics",
- description: "Orbital delivery systems for small satellite deployment. Contracted with SpaceX and NASA for upcoming missions.",
- industry: "Aerospace",
- fundingGoal: 12000000,
- fundsRaised: 8500000,
- equityOffered: 18,
- valuation: 66666666,
- image: "https://images.unsplash.com/photo-1446776653964-20c1d3a81b06?w=800",
- location: "Los Angeles, CA",
- foundedYear: 2018,
- teamSize: 42,
- stage: "Series C",
- investors: 112
- },
- {
- id: 6,
- name: "BioPrint Innovations",
- description: "3D bioprinting human tissues for pharmaceutical testing. Partnership with top 10 pharma companies secured.",
- industry: "Biotech",
- fundingGoal: 6000000,
- fundsRaised: 3200000,
- equityOffered: 14,
- valuation: 42857142,
- image: "https://images.unsplash.com/photo-1582719508461-905c673771fd?w=800",
- location: "San Diego, CA",
- foundedYear: 2021,
- teamSize: 22,
- stage: "Series A",
- investors: 56
+ const [currentPage, setCurrentPage] = useState(0);
+ const [itemsPerPage, setItemsPerPage] = useState(6); // 6 items for 2 rows of 3
+
+ const URL = import.meta.env.VITE_BACKEND_URL;
+
+ useEffect(() => {
+ const fetchData = async () => {
+ try {
+ setIsLoading(true);
+ const [entRes, statsRes] = await Promise.all([
+ axios.get(`${URL}/entrepreneur/get-entrepreneurs`),
+ axios.get(`${URL}/user/platform-stats`)
+ ]);
+
+ setEntrepreneurs(entRes.data.entrepreneurs || []);
+ setFilteredEntrepreneurs(entRes.data.entrepreneurs || []);
+
+ if (statsRes.data) {
+ setPlatformStats(statsRes.data);
+ }
+ } catch (error) {
+ console.error("Failed to fetch data:", error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ fetchData();
+ }, [URL]);
+
+ useEffect(() => {
+ let filtered = [...entrepreneurs];
+
+ if (selectedIndustry !== "All Industries") {
+ filtered = filtered.filter(ent => ent.industry === selectedIndustry);
}
- ];
-
- const BusinessCard: React.FC = ({
- id,
- name,
- description,
- industry,
- fundingGoal,
- fundsRaised,
- equityOffered,
- valuation,
- image,
- location,
- foundedYear,
- teamSize,
- stage,
- investors
- }) => {
- const progress = (fundsRaised / fundingGoal) * 100;
-
- const getStageColor = (stage: string) => {
- switch (stage) {
- case "Seed": return "bg-blue-500";
- case "Series A": return "bg-green-500";
- case "Series B": return "bg-yellow-500";
- case "Series C": return "bg-purple-500";
- default: return "bg-gray-500";
+
+ if (selectedStage !== "All Stages") {
+ filtered = filtered.filter(ent => {
+ if (selectedStage === "Pre-Seed") return ent.preSeedStatus === "completed" || ent.preSeedStatus === "in-progress";
+ if (selectedStage === "Seed") return ent.seedStatus === "completed" || ent.seedStatus === "in-progress";
+ if (selectedStage === "Series A") return ent.seriesAStatus === "completed" || ent.seriesAStatus === "in-progress";
+ return true;
+ });
+ }
+
+ setFilteredEntrepreneurs(filtered);
+ setCurrentPage(0); // Reset to first page when filters change
+ }, [selectedIndustry, selectedStage, entrepreneurs]);
+
+ // Pagination calculations
+ const totalPages = Math.ceil(filteredEntrepreneurs.length / itemsPerPage);
+ const paginatedEntrepreneurs = filteredEntrepreneurs.slice(
+ currentPage * itemsPerPage,
+ (currentPage + 1) * itemsPerPage
+ );
+
+ const nextPage = () => {
+ if (currentPage < totalPages - 1) {
+ setCurrentPage(prev => prev + 1);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }
+ };
+
+ const prevPage = () => {
+ if (currentPage > 0) {
+ setCurrentPage(prev => prev - 1);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }
+ };
+
+ const goToPage = (page: number) => {
+ setCurrentPage(page);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ };
+
+ // Adjust items per page based on screen size
+ useEffect(() => {
+ const updateItemsPerPage = () => {
+ if (window.innerWidth >= 1280) {
+ setItemsPerPage(6); // 2 rows of 3 for xl screens
+ } else if (window.innerWidth >= 1024) {
+ setItemsPerPage(4); // 2 rows of 2 for lg screens
+ } else if (window.innerWidth >= 768) {
+ setItemsPerPage(4); // 2 rows of 2 for md screens
+ } else {
+ setItemsPerPage(6); // 6 rows of 1 for mobile
}
};
- const handleInvestClick = () => {
- if (!isLoggedIn) {
- // Redirect to login page
- navigate("/login");
- return;
+ updateItemsPerPage();
+ window.addEventListener('resize', updateItemsPerPage);
+ return () => window.removeEventListener('resize', updateItemsPerPage);
+ }, []);
+
+ const industries = ["All Industries", ...new Set(entrepreneurs.map(ent => ent.industry).filter(Boolean) as string[])];
+ const stages = ["All Stages", "Pre-Seed", "Seed", "Series A"];
+
+ const BusinessCard: React.FC = ({ entrepreneur }) => {
+ const [currentImageIndex, setCurrentImageIndex] = useState(0);
+ const images = entrepreneur.businessThumbnails && entrepreneur.businessThumbnails.length > 0
+ ? entrepreneur.businessThumbnails
+ : [typeof entrepreneur.avatarUrl === 'string' ? entrepreneur.avatarUrl : "/placeholder-startup.jpg"];
+
+ useEffect(() => {
+ if (images.length <= 1) return;
+ const interval = setInterval(() => {
+ setCurrentImageIndex((prev) => (prev + 1) % images.length);
+ }, 3000);
+ return () => clearInterval(interval);
+ }, [images.length]);
+
+ const nextImage = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ setCurrentImageIndex((prev) => (prev + 1) % images.length);
+ };
+
+ const prevImage = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ setCurrentImageIndex((prev) => (prev - 1 + images.length) % images.length);
+ };
+
+ const getActiveStage = () => {
+ if (entrepreneur.seriesAStatus === "completed" || entrepreneur.seriesAStatus === "in-progress") return "Series A";
+ if (entrepreneur.seedStatus === "completed" || entrepreneur.seedStatus === "in-progress") return "Seed";
+ if (entrepreneur.preSeedStatus === "completed" || entrepreneur.preSeedStatus === "in-progress") return "Pre-Seed";
+ return "Concept";
+ };
+
+ const stage = getActiveStage();
+
+ const getStageColor = (stage: string) => {
+ switch (stage) {
+ case "Pre-Seed": return "bg-gradient-to-r from-blue-400 to-blue-500";
+ case "Seed": return "bg-gradient-to-r from-blue-600 to-blue-700";
+ case "Series A": return "bg-gradient-to-r from-emerald-500 to-emerald-600";
+ default: return "bg-gradient-to-r from-gray-500 to-gray-600";
}
-
- setSelectedBusiness({
- id, name, description, industry, fundingGoal, fundsRaised,
- equityOffered, valuation, image, location, foundedYear,
- teamSize, stage, investors
- });
-
- setInvestmentData(prev => ({
- ...prev,
- businessId: id,
- businessName: name,
- investmentAmount: Math.max(10000, Math.ceil((fundingGoal - fundsRaised) * 0.01)),
- equityPercentage: equityOffered,
- estimatedReturn: Math.ceil(valuation * 0.25) // Simplified calculation
- }));
-
- setShowInvestmentModal(true);
};
+ const fundingProgress = Math.round(((entrepreneur.totalRaised || 0) / (entrepreneur.fundingNeeded || 1)) * 100);
+
return (
-
+
+ {/* Glow Effect */}
+
+
{/* Stage Badge */}
-
+
+
{stage}
-
+
{/* Industry Badge */}
-
- 🏢 {industry}
+
+ {entrepreneur.industry || "General"}
-
- {/* Business Image */}
-
- 
-
+
+ {/* Image Slider */}
+
+ {images.map((img, idx) => (
+ 
+ ))}
+
+
+ {images.length > 1 && (
+ <>
+
+
+
+
+
+
+
+ {images.map((_, idx) => (
+
+ ))}
+
+ >
+ )}
-
- {/* Business Content */}
-
-
-
-
- {name}
-
- 📍 {location}
+
+ {/* Content */}
+
+
+
+
+
+ {entrepreneur.startupName || "Unnamed Startup"}
+
+
+
+ {entrepreneur.location || "Location not specified"}
+
+
+
+
+
+
+ ${((entrepreneur.valuation || 0) / 1000000).toFixed(1)}M
+
+
+
-
-
- {description}
+
+
+ {entrepreneur.pitchSummary || "No description provided."}
-
- {/* Business Stats */}
-
-
- 📅
- Founded {foundedYear}
+
+ {/* Stats Grid */}
+
+
+
+
+
+
+ {entrepreneur.teamSize || 0}
+ Team
+
-
- 👥
- {teamSize} team members
+
+
+
+
+
+
+ {entrepreneur.foundedYear || "N/A"}
+ Founded
+
-
- 🏦
- ${(valuation/1000000).toFixed(1)}M valuation
+
+
+
+
+
+
+
+ ${(entrepreneur.fundingNeeded || 0).toLocaleString()}
+
+ Target
+
-
- 🤝
- {investors} investors
+
+
+
+
+
+
+
+ ${(entrepreneur.totalRaised || 0).toLocaleString()}
+
+ Raised
+
-
-
- {/* Funding Progress */}
-
-
-
- ${(fundsRaised/1000000).toFixed(1)}M raised
-
-
- of ${(fundingGoal/1000000).toFixed(1)}M goal
-
-
-
-
- {progress.toFixed(1)}% funded
- 🏷️ {equityOffered}% equity offered
+
+ {/* Funding Progress */}
+
+
+
+ Funding Progress
+
+
+ {fundingProgress}%
+
+
+
+
+
+ ${(entrepreneur.totalRaised || 0).toLocaleString()} raised
+ ${(entrepreneur.fundingNeeded || 0).toLocaleString()} goal
+
+
-
- {/* Action Button */}
-
- 💼 Invest Now
-
-
- {/* Login Prompt for non-logged in users */}
- {!isLoggedIn && (
-
- Login required to invest
-
- )}
+
+
+
+ Login As Investor To Contact
+
+
);
};
- const handleInvestmentSubmit = (e: React.FormEvent) => {
- e.preventDefault();
- alert(`Investment of $${investmentData.investmentAmount.toLocaleString()} submitted for ${investmentData.businessName}!`);
- setShowInvestmentModal(false);
- setInvestmentData({
- businessId: 0,
- businessName: "",
- investmentAmount: 0,
- equityPercentage: 0,
- estimatedReturn: 0
- });
- };
-
- const handleAmountChange = (amount: number) => {
- const equity = (amount / selectedBusiness!.valuation) * 100;
- const estimatedReturn = amount * 2.5; // Simplified ROI calculation
-
- setInvestmentData(prev => ({
- ...prev,
- investmentAmount: amount,
- equityPercentage: parseFloat(equity.toFixed(2)),
- estimatedReturn: Math.ceil(estimatedReturn)
- }));
- };
-
const handleCloseModal = () => {
- setShowInvestmentModal(false);
- setSelectedBusiness(null);
- setInvestmentData({
- businessId: 0,
- businessName: "",
- investmentAmount: 0,
- equityPercentage: 0,
- estimatedReturn: 0
- });
+ // Unused
};
return (
-
+
{/* Hero Section */}
-
+
-
+
-
-
- Invest in the Future of Business
+
+
+
+ Invest in Innovation
+
+
+
+ Invest in the Future of Business
-
- Discover high-growth startups and innovative businesses seeking investment.
+
+ Discover high-growth startups and innovative businesses seeking investment.
Become a part of their journey and share in their success.
-
+
{/* Authentication Status */}
-
-
- {isLoggedIn ? (
+
+
+ {currentUser ? (
<>
-
-
- Ready to Invest
+
- setIsLoggedIn(false)}
- className="px-4 py-2 text-sm bg-transparent border border-gray-700 text-gray-400 rounded-lg hover:bg-white/10"
- >
- Logout
+
+ View Dashboard
>
) : (
<>
-
-
- Login Required to Invest
+
-
+
-
+
Login
-
- Sign Up
+
+ Register
@@ -392,24 +407,37 @@ export const FundraisePage: React.FC = () => {
)}
-
+
{/* Stats */}
-
-
- {businesses.length}+
- Active Deals
-
-
- $42M+
- Total Invested
+
+
+
+
+
+
+ {platformStats.activeDeals || entrepreneurs.length}+
+
+ Active Deals
-
- 364+
- Investors
+
+
+
+
+
+
+ ${(platformStats.totalInvested / 1000000).toFixed(1)}M+
+
+
+ Total Invested
-
- 3.2x
- Avg. ROI
+
+
+
+
+
+ {platformStats.totalInvestors}+
+
+ Investors
@@ -417,76 +445,245 @@ export const FundraisePage: React.FC = () => {
{/* Main Content */}
-
+
{/* Filter Tabs */}
-
-
-
-
- All Industries
-
- {["Deep Tech", "HealthTech", "Clean Energy", "AgriTech", "Aerospace", "Biotech"].map((ind) => (
-
- {ind}
-
- ))}
+
+
+
+ Explore Opportunities
+ Filter by industry and funding stage to find perfect matches
- {/* Stage Filter */}
-
- Stage:
- {["Seed", "Series A", "Series B", "Series C"].map((stage) => (
-
- {stage}
-
- ))}
+
+
+
+ {/* Industry Filter */}
+
+ Industry
+
+ {industries.slice(0, 5).map((ind) => (
+ setSelectedIndustry(ind)}
+ className={`px-4 py-2 rounded-lg font-medium transition-all ${selectedIndustry === ind
+ ? "bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg shadow-blue-500/30"
+ : "bg-gray-900 text-gray-300 hover:text-white hover:bg-gray-800 border border-gray-800"
+ }`}
+ >
+ {ind}
+
+ ))}
+
+
+
+ {/* Stage Filter */}
+
+ Stage
+
+ {stages.map((stage) => (
+ setSelectedStage(stage)}
+ className={`px-4 py-2 rounded-lg font-medium transition-all ${selectedStage === stage
+ ? "bg-gradient-to-r from-blue-600 to-blue-700 text-white shadow-lg shadow-blue-500/30"
+ : "bg-gray-900 text-gray-300 hover:text-white hover:bg-gray-800 border border-gray-800"
+ }`}
+ >
+ {stage}
+
+ ))}
+
+
+
- {/* Businesses Grid */}
-
- {businesses.map((business) => (
-
- ))}
+ {/* Results Summary */}
+
+
+ Showing {filteredEntrepreneurs.length} opportunities
+ {selectedIndustry !== "All Industries" && ` in ${selectedIndustry}`}
+ {selectedStage !== "All Stages" && ` at ${selectedStage} stage`}
+
+
+ Page {currentPage + 1} of {totalPages}
+
+ {/* Businesses Grid */}
+ {isLoading ? (
+
+
+
+ Loading investment opportunities...
+
+
+ ) : filteredEntrepreneurs.length > 0 ? (
+ <>
+
+ {paginatedEntrepreneurs.map((ent) => (
+
+
+
+ ))}
+
+
+ {/* Pagination Controls */}
+ {totalPages > 1 && (
+
+ {/* Page Info */}
+
+ Showing
+ {Math.min((currentPage * itemsPerPage) + 1, filteredEntrepreneurs.length)}-{Math.min((currentPage + 1) * itemsPerPage, filteredEntrepreneurs.length)}
+ of {filteredEntrepreneurs.length} opportunities
+
+
+ {/* Pagination Buttons */}
+
+ {/* Previous Button */}
+
+
+ Previous
+
+
+ {/* Page Numbers */}
+
+ {Array.from({ length: Math.min(5, totalPages) }).map((_, idx) => {
+ // Show 5 page numbers around current page
+ let pageNum;
+ if (totalPages <= 5) {
+ pageNum = idx;
+ } else if (currentPage <= 2) {
+ pageNum = idx;
+ } else if (currentPage >= totalPages - 3) {
+ pageNum = totalPages - 5 + idx;
+ } else {
+ pageNum = currentPage - 2 + idx;
+ }
+
+ return (
+ goToPage(pageNum)}
+ className={`w-10 h-10 rounded-xl font-medium transition-all flex items-center justify-center ${currentPage === pageNum
+ ? "bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg shadow-blue-500/30"
+ : "bg-gray-900 text-gray-400 hover:text-white hover:bg-gray-800 border border-gray-800"
+ }`}
+ >
+ {pageNum + 1}
+
+ );
+ })}
+
+ {totalPages > 5 && (
+ <>
+ ...
+ goToPage(totalPages - 1)}
+ className={`w-10 h-10 rounded-xl font-medium transition-all flex items-center justify-center ${currentPage === totalPages - 1
+ ? "bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg shadow-blue-500/30"
+ : "bg-gray-900 text-gray-400 hover:text-white hover:bg-gray-800 border border-gray-800"
+ }`}
+ >
+ {totalPages}
+
+ >
+ )}
+
+
+ {/* Next Button */}
+ = totalPages - 1}
+ className="flex items-center gap-2 px-5 py-2.5 bg-gray-900/50 backdrop-blur-sm border border-gray-800 text-gray-300 rounded-xl hover:bg-gray-800 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-300"
+ >
+ Next
+
+
+
+
+ {/* Items Per Page Selector */}
+
+ Show:
+
+ per page
+
+
+ )}
+ >
+ ) : (
+
+
+
+
+
+ No matches found
+ Try adjusting your filters to see more opportunities
+
+ setSelectedIndustry("All Industries")}
+ className="px-6 py-3 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-xl"
+ >
+ Clear Industry Filter
+
+ setSelectedStage("All Stages")}
+ className="px-6 py-3 bg-gray-900 text-white rounded-xl border border-gray-800"
+ >
+ Clear Stage Filter
+
+
+
+
+ )}
+
{/* CTA Section */}
-
-
-
- {isLoggedIn ? "Ready to Make Your First Investment?" : "Ready to Start Investing?"}
+
+
+
+ {currentUser ? "Ready to Make Your First Investment?" : "Ready to Start Investing?"}
-
- {isLoggedIn
+
+ {currentUser
? "Browse our curated selection of high-potential businesses and start building your investment portfolio today."
: "Join our community of investors and get access to exclusive investment opportunities in innovative startups."}
- {isLoggedIn ? (
+ {currentUser ? (
<>
-
+
Browse All Deals
-
+
View Portfolio
>
) : (
<>
-
- Create Investor Account
+
+ Create Account
-
+
Login to Invest
@@ -496,188 +693,6 @@ export const FundraisePage: React.FC = () => {
-
- {/* Investment Modal */}
- {showInvestmentModal && selectedBusiness && isLoggedIn && (
-
-
- {/* Header with Close Button */}
-
-
-
-
- Invest in {selectedBusiness.name}
-
-
-
- {/* Close Button */}
-
-
-
-
-
- {/* Scrollable Content */}
-
-
- {/* Business Info */}
-
-
- {selectedBusiness.description}
-
-
- {/* Key Metrics */}
-
-
- Valuation
-
- ${(selectedBusiness.valuation/1000000).toFixed(1)}M
-
-
-
- Equity Offered
-
- {selectedBusiness.equityOffered}%
-
-
-
-
- {/* Progress Info */}
-
-
-
- ${(selectedBusiness.fundsRaised/1000000).toFixed(1)}M raised
-
-
- of ${(selectedBusiness.fundingGoal/1000000).toFixed(1)}M goal
-
-
-
-
-
-
- {/* Investment Amount Selection */}
-
-
-
- {[10000, 25000, 50000, 100000, 250000, 500000].map((amount) => (
- handleAmountChange(amount)}
- className={`py-3 rounded-lg font-semibold transition-all text-sm sm:text-base ${
- investmentData.investmentAmount === amount
- ? 'bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg'
- : 'bg-white/5 text-gray-300 hover:bg-white/10'
- }`}
- >
- ${(amount/1000).toFixed(0)}K
-
- ))}
-
-
-
- $
- handleAmountChange(parseInt(e.target.value) || 0)}
- className="w-full pl-8 pr-4 py-3 bg-gray-900/50 border border-gray-700 rounded-lg text-white focus:outline-none focus:border-blue-500 transition-colors"
- placeholder="Enter custom amount (min $1,000)"
- />
-
-
-
- {/* Investment Summary */}
- {investmentData.investmentAmount > 0 && (
-
- Investment Summary
-
-
- Investment Amount:
-
- ${investmentData.investmentAmount.toLocaleString()}
-
-
-
- Equity Acquired:
-
- {investmentData.equityPercentage.toFixed(2)}%
-
-
-
- Estimated 5-Year Return:
-
- ${investmentData.estimatedReturn.toLocaleString()}
-
-
-
-
- Potential ROI:
-
- {((investmentData.estimatedReturn / investmentData.investmentAmount) * 100).toFixed(1)}%
-
-
-
-
-
- )}
-
- {/* Terms Agreement */}
-
-
-
-
- {/* Submit Button */}
-
- 💼 Submit Investment
-
-
- {/* Disclaimer */}
-
-
-
-
- Investments involve risks, including loss of principal. Past performance does not guarantee future results.
- Please consult with a financial advisor before making any investment decisions.
-
-
-
-
-
-
-
- )}
);
};
\ No newline at end of file
diff --git a/src/pages/home/HomePage.tsx b/src/pages/home/HomePage.tsx
index 4c5d45400..400d218dd 100644
--- a/src/pages/home/HomePage.tsx
+++ b/src/pages/home/HomePage.tsx
@@ -34,28 +34,59 @@ export const HomePage: React.FC = () => {
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState("");
const [recentCampaigns, setRecentCampaigns] = useState ([]);
+ const [recentFundraisers, setRecentFundraisers] = useState([]);
const [campaignsLoading, setCampaignsLoading] = useState(true);
-
- // Fetch recent active campaigns
+ const [fundraisersLoading, setFundraisersLoading] = useState(true);
+ const [stats, setStats] = useState({
+ totalStartups: 0,
+ totalFunded: 0,
+ successRate: 95
+ });
+
+ // Fetch recent active campaigns and platform stats
useEffect(() => {
- const fetchRecentCampaigns = async () => {
+ const fetchData = async () => {
try {
setCampaignsLoading(true);
- const response = await axios.get(`${URL}/admin/campaigns`);
- // Filter only active campaigns and take the last 3 (most recent)
- const activeCampaigns = response.data
+ setFundraisersLoading(true);
+
+ const [campaignsRes, entrepreneursRes, statsRes] = await Promise.all([
+ axios.get(`${URL}/admin/campaigns`),
+ axios.get(`${URL}/entrepreneur/get-entrepreneurs`),
+ axios.get(`${URL}/user/platform-stats`)
+ ]);
+
+ // Recent Campaigns - Sort by createdAt descending and slice to 3
+ const activeCampaigns = (campaignsRes.data || [])
.filter((c: any) => c.status === "active")
- .slice(-3)
- .reverse();
+ .sort((a: any, b: any) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
+ .slice(0, 3);
setRecentCampaigns(activeCampaigns);
+
+ // Recent Fundraisers - Ensure sorted by createdAt descending
+ const allEntrepreneurs = entrepreneursRes.data.entrepreneurs || [];
+ const sortedFundraisers = [...allEntrepreneurs]
+ .sort((a: any, b: any) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
+ .slice(0, 3);
+ setRecentFundraisers(sortedFundraisers);
+
+ // Stats
+ if (statsRes.data) {
+ setStats({
+ totalStartups: statsRes.data.totalStartups || 0,
+ totalFunded: statsRes.data.totalFunded || 0,
+ successRate: statsRes.data.successRate || 95
+ });
+ }
} catch (error) {
- console.error("Failed to fetch recent campaigns:", error);
+ console.error("Failed to fetch home page data:", error);
} finally {
setCampaignsLoading(false);
+ setFundraisersLoading(false);
}
};
- fetchRecentCampaigns();
+ fetchData();
}, [URL]);
const handleSubmit = async (e: React.FormEvent) => {
@@ -135,14 +166,27 @@ export const HomePage: React.FC = () => {
);
};
+ interface FundraiserProps {
+ id: string;
+ image: string;
+ fundNeeded: number;
+ company: string;
+ description: string;
+ totalRaised: number;
+ }
const FundraiserDiv: React.FC = ({
+ id,
image,
fundNeeded,
company,
description,
+ totalRaised
}) => {
return (
-
+ navigate(`/fundraises`)}
+ className="group cursor-pointer relative w-full h-44 md:h-48 hover:scale-[1.02] transition-all duration-300 mx-auto max-w-xs bg-gradient-to-br from-blue-900/80 to-purple-900/80 rounded-xl overflow-hidden border border-gray-700/50 shadow-lg hover:shadow-xl hover:border-blue-500/30"
+ >
 {
-
+
{company}
-
+
Active
-
+
{description}
-
- Need:
-
- ${fundNeeded}
-
+
+
+ Raised:
+
+ ${totalRaised.toLocaleString()}
+
+
+
+ Target:
+
+ ${fundNeeded.toLocaleString()}
+
+
+
@@ -256,7 +314,7 @@ export const HomePage: React.FC = () => {
Recent Campaigns
- 3 Live
+ {recentCampaigns.length} Live
@@ -318,18 +376,18 @@ export const HomePage: React.FC = () => {
- 500+
+ {stats.totalStartups || 0}+
Startups
- $50M+
+ ${(stats.totalFunded / 1000000).toFixed(1)}M+
Funded
- 95%
+ {stats.successRate}%
Success Rate
@@ -350,24 +408,34 @@ export const HomePage: React.FC = () => {
-
-
-
+ {fundraisersLoading ? (
+
+ ) : recentFundraisers.length === 0 ? (
+
+ No active fundraisers
+
+ ) : (
+ recentFundraisers.map((entrepreneur) => {
+ const businessImage = entrepreneur.businessThumbnails?.[0] ||
+ (entrepreneur.avatarUrl
+ ? (entrepreneur.avatarUrl.startsWith('http') ? entrepreneur.avatarUrl : `${URL}/${entrepreneur.avatarUrl}`)
+ : "app logo.jpeg");
+
+ return (
+
+ );
+ })
+ )}
diff --git a/src/pages/investors/InvestorsPage.tsx b/src/pages/investors/InvestorsPage.tsx
index 7a839fe3f..00913559f 100644
--- a/src/pages/investors/InvestorsPage.tsx
+++ b/src/pages/investors/InvestorsPage.tsx
@@ -13,6 +13,7 @@ export const InvestorsPage: React.FC = () => {
const [searchQuery, setSearchQuery] = useState("");
const [selectedStages, setSelectedStages] = useState([]);
const [selectedInterests, setSelectedInterests] = useState([]);
+ const [selectedLocation, setSelectedLocation] = useState(null);
const [investors, setInvestors] = useState([]);
const { user } = useAuth();
@@ -33,7 +34,10 @@ export const InvestorsPage: React.FC = () => {
);
const allInterests = Array.from(
new Set(investors.flatMap((i) => i.investmentInterests || ""))
- );
+ ).filter(Boolean);
+ const allLocations = Array.from(
+ new Set(investors.map((i) => i.location).filter(Boolean))
+ ) as string[];
// Filter investors based on search and filters
const filteredInvestors = investors && investors.filter((investor) => {
@@ -57,7 +61,9 @@ export const InvestorsPage: React.FC = () => {
const isApproved = investor.approvalStatus === 'approved';
- return matchesSearch && matchesStages && matchesInterests && isApproved;
+ const matchesLocation = !selectedLocation || investor.location === selectedLocation;
+
+ return matchesSearch && matchesStages && matchesInterests && isApproved && matchesLocation;
});
const toggleStage = (stage: string) => {
@@ -138,18 +144,23 @@ export const InvestorsPage: React.FC = () => {
Location
-
-
- San Francisco, CA
-
-
-
- New York, NY
-
-
-
- Boston, MA
-
+ {allLocations.length > 0 ? (
+ allLocations.map((location) => (
+ setSelectedLocation(selectedLocation === location ? null : location)}
+ className={`flex items-center w-full text-left px-3 py-2 rounded-md text-sm transition-colors ${selectedLocation === location
+ ? "bg-primary-50 text-primary-700 font-medium"
+ : "text-gray-700 hover:bg-gray-50"
+ }`}
+ >
+
+ {location}
+
+ ))
+ ) : (
+ No locations available
+ )}
@@ -177,7 +188,7 @@ export const InvestorsPage: React.FC = () => {
{filteredInvestors.map((investor) => (
-
+
))}
diff --git a/src/pages/profile/EntrepreneurProfile.tsx b/src/pages/profile/EntrepreneurProfile.tsx
index f50301ae5..30c379750 100644
--- a/src/pages/profile/EntrepreneurProfile.tsx
+++ b/src/pages/profile/EntrepreneurProfile.tsx
@@ -10,7 +10,21 @@ import {
FileText,
DollarSign,
Send,
+ X,
+ Clock,
+ Eye,
+ Edit2,
+ Save,
+ Loader2,
+ Check,
+ Upload,
+ Image as ImageIcon,
+ Trash2,
+ Plus,
} from "lucide-react";
+import axios from "axios";
+import toast from "react-hot-toast";
+import { PdfPreviewModal } from "../../components/ui/PdfPreviewModal";
import { Avatar } from "../../components/ui/Avatar";
import { Button } from "../../components/ui/Button";
import { Card, CardBody, CardHeader } from "../../components/ui/Card";
@@ -21,19 +35,36 @@ import {
createCollaborationRequest,
} from "../../data/collaborationRequests";
import { Entrepreneur } from "../../types";
-import { AmountMeasureWithTags, getEnterpreneurById } from "../../data/users";
+import { AmountMeasureWithTags, getEnterpreneurById, updateEntrepreneurData } from "../../data/users";
import { suspendUser, blockUser, unsuspendUser, unblockUser } from "../../data/admin"; // Import admin actions
+import { DealForm } from "../../components/DealForm";
type Props = {
userId?: string | undefined;
};
+
+interface DBDocument {
+ _id: string;
+ type: string;
+ fileUrl: string;
+ fileName: string;
+ uploadedAt: string;
+}
export const EntrepreneurProfile: React.FC = ({ userId }) => {
const { id } = useParams<{ id: string }>();
const { user: currentUser } = useAuth();
const [entrepreneur, setEnterpreneur] = useState();
- const [hasRequestedCollaboration, setHasRequestedCollaboration] =
- useState();
+ const [requestStatus, setRequestStatus] = useState<
+ "pending" | "accepted" | "rejected" | null
+ >(null);
const [isDealModalOpen, setIsDealModalOpen] = useState(false);
+ const [documents, setDocuments] = useState([]);
+ const [previewDoc, setPreviewDoc] = useState<{ url: string; name: string } | null>(null);
+ const [isEditingBio, setIsEditingBio] = useState(false);
+ const [editedBio, setEditedBio] = useState("");
+ const [isSavingBio, setIsSavingBio] = useState(false);
+ const [isUploadingThumbnails, setIsUploadingThumbnails] = useState(false);
+ const [isLoading, setIsLoading] = useState(true);
const [isSuspendModalOpen, setIsSuspendModalOpen] = useState(false);
const [isBlockModalOpen, setIsBlockModalOpen] = useState(false);
@@ -44,35 +75,42 @@ export const EntrepreneurProfile: React.FC = ({ userId }) => {
const navigate = useNavigate();
const [valuation, setValuation] = useState(0);
+ const URL = import.meta.env.VITE_BACKEND_URL;
+ const token = localStorage.getItem("token");
useEffect(() => {
const fetchEntrepreneur = async () => {
- if (id) {
- const entrepreneur = await getEnterpreneurById(id);
- setEnterpreneur(entrepreneur);
- } else {
- const entrepreneur = await getEnterpreneurById(userId);
- setEnterpreneur(entrepreneur);
+ const targetId = id || userId;
+ if (targetId) {
+ setIsLoading(true);
+ try {
+ const entrepreneur = await getEnterpreneurById(targetId);
+ setEnterpreneur(entrepreneur);
+
+ // Fetch documents
+ if (entrepreneur && entrepreneur.userId) {
+ const res = await axios.get(`${URL}/document/user/${entrepreneur.userId}`);
+ setDocuments(res.data.documents);
+ }
+ } catch (error) {
+ console.error("Failed to fetch documents", error);
+ } finally {
+ setIsLoading(false);
+ }
}
};
fetchEntrepreneur();
}, [id, userId]);
+
+
+ // Removed legacy auto-calculation of valuation that was overwriting backend data.
+ // Valuation and Status are now managed by backend events (e.g. Fund Release).
useEffect(() => {
- const calculateValuation = () => {
- if (entrepreneur?.growthRate && entrepreneur?.profitMargin)
- return (
- 5 *
- (1 +
- entrepreneur?.growthRate / 100 +
- entrepreneur?.profitMargin / 100)
- );
- };
- const nichevalue = calculateValuation();
- // ensure we multiply two numbers: use a numeric default for nichevalue and revenue
- const base = nichevalue ?? 1;
- const revenue = entrepreneur?.revenue ?? 0;
- setValuation(base * revenue);
+ if (entrepreneur) {
+ setValuation(entrepreneur.valuation);
+ setEditedBio(entrepreneur.bio || "");
+ }
}, [entrepreneur]);
useEffect(() => {
@@ -80,13 +118,25 @@ export const EntrepreneurProfile: React.FC = ({ userId }) => {
if (currentUser?.userId && id) {
const request = await checkRequestsFromInvestor(currentUser.userId, id);
console.log(request);
- setHasRequestedCollaboration(Boolean(request));
+ // The endpoint returns the status string directly or 'pending' if error/not found?
+ // Let's assume it returns the request object or status string as per `checkRequestsFromInvestor` implementation
+ // The data helper returns: return request.requestStatus;
+ setRequestStatus(request as any);
}
};
checkInvestor();
}, [currentUser?.userId, id]);
if (!currentUser) return null;
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
if (!entrepreneur || entrepreneur.role !== "entrepreneur") {
return (
@@ -97,7 +147,6 @@ export const EntrepreneurProfile: React.FC = ({ userId }) => {
The entrepreneur profile you're looking for doesn't exist or has been
removed.
- s
Back to Dashboard
@@ -108,6 +157,10 @@ export const EntrepreneurProfile: React.FC = ({ userId }) => {
}
const isCurrentUser = currentUser?.userId === entrepreneur?.userId;
+ console.log("CurrentUser:", currentUser?.userId);
+ console.log("EntrepreneurUser:", entrepreneur?.userId);
+ console.log("IsCurrentUser:", isCurrentUser);
+
const isInvestor = currentUser?.role === "investor";
const isAdmin = currentUser?.role === "admin";
// Check if the current investor has already sent a request to this entrepreneur
@@ -119,7 +172,7 @@ export const EntrepreneurProfile: React.FC = ({ userId }) => {
id,
`I'm interested in learning more about ${entrepreneur.startupName} and would like to explore potential investment opportunities.`
);
- await setHasRequestedCollaboration(true);
+ setRequestStatus("pending");
}
};
@@ -165,6 +218,72 @@ export const EntrepreneurProfile: React.FC = ({ userId }) => {
}
};
+ const handleSaveBio = async () => {
+ if (!entrepreneur?.userId) return;
+ setIsSavingBio(true);
+ try {
+ await axios.post(`${URL}/user/update-profile/${entrepreneur.userId}`, {
+ bio: editedBio
+ }, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ setEnterpreneur(prev => prev ? { ...prev, bio: editedBio } : undefined);
+ setIsEditingBio(false);
+ toast.success("Bio updated successfully");
+ } catch (error) {
+ console.error(error);
+ toast.error("Failed to update bio");
+ } finally {
+ setIsSavingBio(false);
+ }
+ };
+
+ const handleThumbnailUpload = async (e: React.ChangeEvent) => {
+ if (!e.target.files || e.target.files.length === 0) return;
+
+ const files = Array.from(e.target.files);
+ const currentCount = entrepreneur?.businessThumbnails?.length || 0;
+
+ if (currentCount + files.length > 3) {
+ toast.error("You can only have up to 3 thumbnails");
+ return;
+ }
+
+ const formData = new FormData();
+ files.forEach(file => {
+ formData.append("thumbnails", file);
+ });
+
+ try {
+ setIsUploadingThumbnails(true);
+ const res = await axios.post(`${URL}/entrepreneur/upload-thumbnails/${entrepreneur?.userId}`, formData, {
+ headers: {
+ "Content-Type": "multipart/form-data"
+ }
+ });
+ setEnterpreneur(prev => prev ? { ...prev, businessThumbnails: res.data.businessThumbnails } : undefined);
+ toast.success("Thumbnails uploaded successfully");
+ } catch (error: any) {
+ console.error("Upload failed", error);
+ toast.error(error.response?.data?.message || "Failed to upload thumbnails");
+ } finally {
+ setIsUploadingThumbnails(false);
+ }
+ };
+
+ const handleThumbnailDelete = async (imageUrl: string) => {
+ try {
+ const res = await axios.delete(`${URL}/entrepreneur/delete-thumbnail/${entrepreneur?.userId}`, {
+ data: { imageUrl }
+ });
+ setEnterpreneur(prev => prev ? { ...prev, businessThumbnails: res.data.businessThumbnails } : undefined);
+ toast.success("Thumbnail deleted");
+ } catch (error) {
+ console.error("Delete failed", error);
+ toast.error("Failed to delete thumbnail");
+ }
+ };
+
const fundAmount = valuation ?? 0;
return (
@@ -217,31 +336,44 @@ export const EntrepreneurProfile: React.FC = ({ userId }) => {
{!isAdmin ? (
!isCurrentUser && (
<>
-
- }
- >
- Message
-
-
+ {(!isInvestor || requestStatus === "accepted") && (
+
+ }
+ >
+ Message
+
+
+ )}
{isInvestor && (
}
- disabled={hasRequestedCollaboration}
+ leftIcon={
+ requestStatus === "pending" ? (
+
+ ) : requestStatus === "accepted" ? (
+
+ ) : (
+
+ )
+ }
+ disabled={requestStatus === "pending" || requestStatus === "accepted"}
onClick={handleSendRequest}
>
- {hasRequestedCollaboration
+ {requestStatus === "pending"
? "Request Sent"
- : "Request Collaboration"}
+ : requestStatus === "accepted"
+ ? "Request Accepted"
+ : "Request Collaboration"}
)}
>
)
) : (
- // Admin Actions
+ // Admin Actions ... (omitted for brevity, assume unchanged context)
+ {/* ... admin buttons ... */}
{entrepreneur.isSuspended ? (
= ({ userId }) => {
)}
- {hasRequestedCollaboration && (
+ {isInvestor && requestStatus === "accepted" && (
setIsDealModalOpen(true)}
@@ -288,92 +420,13 @@ export const EntrepreneurProfile: React.FC = ({ userId }) => {
Make a Deal
)}
- {isDealModalOpen && (
-
+ {isDealModalOpen && entrepreneur && currentUser && (
+ setIsDealModalOpen(false)}
+ />
)}
{isCurrentUser && (
@@ -451,13 +504,48 @@ export const EntrepreneurProfile: React.FC = ({ userId }) => {
{/* About */}
-
+
About
+ {isCurrentUser && (
+ isEditingBio ? handleSaveBio() : setIsEditingBio(true)}
+ disabled={isSavingBio}
+ className="p-1 hover:bg-gray-100 rounded-full transition-colors text-primary-600"
+ title={isEditingBio ? "Save bio" : "Edit bio"}
+ >
+ {isSavingBio ? (
+
+ ) : isEditingBio ? (
+
+ ) : (
+
+ )}
+
+ )}
-
- {entrepreneur.bio || "Say about yours..?"}
-
+ {isEditingBio ? (
+
+
+ ) : (
+
+ {entrepreneur.bio || "No information provided yet."}
+
+ )}
@@ -509,68 +597,197 @@ export const EntrepreneurProfile: React.FC = ({ userId }) => {
+ {/* Business Showcase - Only visible to Owner or Admin */}
+ {(isCurrentUser || currentUser?.role === "admin") && (
+
+
+
+
+
+ Business Showcase
+
+
+ {isCurrentUser && (entrepreneur?.businessThumbnails?.length || 0) < 3 && (
+
+
+
+ {isUploadingThumbnails ? : }
+ Upload
+
+
+ )}
+
+
+
+ {entrepreneur?.businessThumbnails && entrepreneur.businessThumbnails.length > 0 ? (
+ entrepreneur.businessThumbnails.map((url, idx) => (
+
+ 
+ {isCurrentUser && (
+
+ handleThumbnailDelete(url)}
+ className="p-2 bg-red-500 text-white rounded-full hover:bg-red-600 transition-colors shadow-lg"
+ title="Delete image"
+ >
+
+
+
+ )}
+
+ ))
+ ) : (
+
+
+ No business images uploaded yet.
+ {isCurrentUser && Upload up to 3 images to showcase your business. }
+
+ )}
+
+
+
+ )}
+
{/* Team */}
Team
-
- {entrepreneur.teamSize || 0} members
-
+
+
+ {entrepreneur.team?.length || 0} members
+
+ {isCurrentUser && (
+
+
+ Manage Team
+
+
+ )}
+
-
-
-
-
- {entrepreneur.name}
-
- Founder & CEO
-
-
-
-
-
-
-
- Alex Johnson
-
- CTO
-
-
-
-
-
-
-
- Jessica Chen
-
- Head of Product
-
-
-
- {entrepreneur.teamSize && entrepreneur?.teamSize > 3 && (
-
-
- + {entrepreneur?.teamSize - 3} more team members
-
+ {entrepreneur.team &&
+ entrepreneur.team.filter(member =>
+ !member.role.some(role => role.toLowerCase() === "member")
+ ).length > 0 ? (
+ entrepreneur.team
+ .filter(member =>
+ !member.role.some(role => role.toLowerCase() === "member")
+ )
+ .sort((a, b) => {
+ // Define role priority order
+ const rolePriority: { [key: string]: number } = {
+ "CEO": 1,
+ "Founder": 2,
+ "Co-Founder": 3,
+ "CTO": 4,
+ "CFO": 5,
+ "COO": 6,
+ "President": 7,
+ "VP": 8,
+ "Director": 9,
+ "Head": 10,
+ "Manager": 11,
+ "Lead": 12,
+ "Senior": 13
+ };
+
+ // Get the highest priority role for each member (lowest number = higher priority)
+ const getHighestPriority = (roles: string[]): number => {
+ let highestPriority = Infinity;
+ roles.forEach(role => {
+ // Check for exact matches first
+ if (rolePriority[role] && rolePriority[role] < highestPriority) {
+ highestPriority = rolePriority[role];
+ } else {
+ // Check for partial matches (e.g., "VP of Engineering" contains "VP")
+ for (const [key, priority] of Object.entries(rolePriority)) {
+ if (role.toLowerCase().includes(key.toLowerCase()) && priority < highestPriority) {
+ highestPriority = priority;
+ }
+ }
+ }
+ });
+ return highestPriority === Infinity ? 100 : highestPriority;
+ };
+
+ const priorityA = getHighestPriority(a.role);
+ const priorityB = getHighestPriority(b.role);
+
+ // Sort by priority (lower number = higher priority)
+ return priorityA - priorityB;
+ })
+ .slice(0, 4)
+ .map((member) => (
+
+
+
+
+ {member.name}
+
+
+ {member.role
+ .sort((roleA, roleB) => {
+ // Sort roles within member by priority too
+ const rolePriority: { [key: string]: number } = {
+ "CEO": 1, "Founder": 2, "Co-Founder": 3, "CTO": 4,
+ "CFO": 5, "COO": 6, "President": 7, "VP": 8,
+ "Director": 9, "Head": 10, "Manager": 11, "Lead": 12,
+ "Senior": 13
+ };
+
+ const getRolePriority = (role: string): number => {
+ if (rolePriority[role]) return rolePriority[role];
+ for (const [key, priority] of Object.entries(rolePriority)) {
+ if (role.toLowerCase().includes(key.toLowerCase())) {
+ return priority;
+ }
+ }
+ return 100;
+ };
+
+ return getRolePriority(roleA) - getRolePriority(roleB);
+ })
+ .join(", ")}
+
+
+
+ ))
+ ) : (
+
+ No team members with specific roles listed.
)}
+
+ {entrepreneur.team &&
+ entrepreneur.team.filter(member =>
+ !member.role.some(role => role.toLowerCase() === "member")
+ ).length > 4 && (
+
+
+ + {entrepreneur.team.filter(member =>
+ !member.role.some(role => role.toLowerCase() === "member")
+ ).length - 4} more team members with specific roles
+
+
+ )}
@@ -606,9 +823,19 @@ export const EntrepreneurProfile: React.FC = ({ userId }) => {
Previous Funding
-
- $750K Seed (2022)
-
+ {entrepreneur.fundingHistory && entrepreneur.fundingHistory.length > 0 ? (
+
+ {entrepreneur.fundingHistory.map((fund: any, index: number) => (
+
+ ${AmountMeasureWithTags(fund.amount)} {fund.stage} ({fund.year})
+
+ ))}
+
+ ) : (
+
+ N/A
+
+ )}
@@ -662,51 +889,51 @@ export const EntrepreneurProfile: React.FC = ({ userId }) => {
-
-
-
-
-
-
- Pitch Deck
-
-
- Updated 2 months ago
-
-
-
- View
-
-
-
-
-
-
-
-
-
- Business Plan
-
- Updated 1 month ago
-
-
- View
-
-
+ {documents.length > 0 ? (
+ documents.map((doc) => (
+
+
+
+
+
+
+ {doc.type}
+
+
+ Updated on {new Date(doc.uploadedAt).toLocaleDateString()}
+
+
+ setPreviewDoc({ url: `${URL}${doc.fileUrl}`, name: doc.fileName })}
+ >
+ View
+
+
+ ))
+ ) : (
+ No documents uploaded yet.
+ )}
+
-
-
-
-
-
-
- Financial Projections
-
- Updated 2 weeks ago
-
-
- View
-
+ {/* Investors Section */}
+
+ Investors
+
+ {entrepreneur.investors && entrepreneur.investors.length > 0 ? (
+ entrepreneur.investors.map((inv, idx) => (
+
+
+
+ {inv.name}
+ Verified Investor
+
+
+ ))
+ ) : (
+ No investors yet.
+ )}
@@ -717,13 +944,13 @@ export const EntrepreneurProfile: React.FC = ({ userId }) => {
sending a collaboration request.
- {!hasRequestedCollaboration ? (
+ {requestStatus !== "pending" && requestStatus !== "accepted" ? (
Request Collaboration
) : (
- Request Sent
+ {requestStatus === "accepted" ? "Access Granted" : "Request Sent"}
)}
@@ -731,7 +958,17 @@ export const EntrepreneurProfile: React.FC = ({ userId }) => {
+
+
+ {previewDoc && (
+ setPreviewDoc(null)}
+ fileUrl={previewDoc.url}
+ fileName={previewDoc.name}
+ />
+ )}
);
};
diff --git a/src/pages/profile/InvestorProfile.tsx b/src/pages/profile/InvestorProfile.tsx
index c5dde131b..1d115c25a 100644
--- a/src/pages/profile/InvestorProfile.tsx
+++ b/src/pages/profile/InvestorProfile.tsx
@@ -8,7 +8,12 @@ import {
BarChart3,
Briefcase,
DollarSign,
+ Edit2,
+ Save,
+ Loader2,
} from "lucide-react";
+import axios from "axios";
+import toast from "react-hot-toast";
import { Avatar } from "../../components/ui/Avatar";
import { Button } from "../../components/ui/Button";
import { Card, CardBody, CardHeader } from "../../components/ui/Card";
@@ -17,6 +22,7 @@ import { useAuth } from "../../context/AuthContext";
import { getInvestorById } from "../../data/users";
import { suspendUser, blockUser, unsuspendUser, unblockUser } from "../../data/admin";
import { Investor } from "../../types";
+import { AmountMeasureWithTags } from "../../data/users";
type Props = {
userId?: string | undefined;
@@ -32,16 +38,23 @@ export const InvestorProfile: React.FC = ({ userId }) => {
const [suspendReason, setSuspendReason] = useState("");
const [suspendDays, setSuspendDays] = useState(7);
const [blockReason, setBlockReason] = useState("");
+ const [isEditingBio, setIsEditingBio] = useState(false);
+ const [editedBio, setEditedBio] = useState("");
+ const [isSavingBio, setIsSavingBio] = useState(false);
+
+ const URL = import.meta.env.VITE_BACKEND_URL;
+ const token = localStorage.getItem("token");
// Fetch investor data
useEffect(() => {
const fetchInvestors = async () => {
- if (id) {
- const investor = await getInvestorById(id);
- setInvestor(investor);
- } else {
- const investor = await getInvestorById(userId);
- setInvestor(investor);
+ const targetId = id || userId;
+ if (targetId) {
+ const investor = await getInvestorById(targetId);
+ if (investor) {
+ setInvestor(investor);
+ setEditedBio(investor.bio || "");
+ }
}
};
fetchInvestors();
@@ -109,6 +122,26 @@ export const InvestorProfile: React.FC = ({ userId }) => {
}
};
+ const handleSaveBio = async () => {
+ if (!investor?.userId) return;
+ setIsSavingBio(true);
+ try {
+ await axios.post(`${URL}/user/update-profile/${investor.userId}`, {
+ bio: editedBio
+ }, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ setInvestor(prev => prev ? { ...prev, bio: editedBio } : undefined);
+ setIsEditingBio(false);
+ toast.success("Bio updated successfully");
+ } catch (error) {
+ console.error(error);
+ toast.error("Failed to update bio");
+ } finally {
+ setIsSavingBio(false);
+ }
+ };
+
return (
{/* Profile header */}
@@ -223,13 +256,48 @@ export const InvestorProfile: React.FC = ({ userId }) => {
{/* About */}
-
+
About
+ {isCurrentUser && (
+ isEditingBio ? handleSaveBio() : setIsEditingBio(true)}
+ disabled={isSavingBio}
+ className="p-1 hover:bg-gray-100 rounded-full transition-colors text-primary-600"
+ title={isEditingBio ? "Save bio" : "Edit bio"}
+ >
+ {isSavingBio ? (
+
+ ) : isEditingBio ? (
+
+ ) : (
+
+ )}
+
+ )}
-
- {investor.bio || "Say something about u..?"}
-
+ {isEditingBio ? (
+
+
+ ) : (
+
+ {investor.bio || "No information provided yet."}
+
+ )}
@@ -296,34 +364,51 @@ export const InvestorProfile: React.FC = ({ userId }) => {
Portfolio Companies
- {(investor.portfolioCompanies &&
- investor.portfolioCompanies.length) ||
+ {(investor.portfolio && investor.portfolio.length) ||
+ (investor.portfolioCompanies && investor.portfolioCompanies.length) ||
0}{" "}
companies
- {(investor.portfolioCompanies &&
- investor.portfolioCompanies.map((company, index) => (
- 0) ? (
+ investor.portfolio.map((company, index) => (
+
-
-
-
+
- {company}
+ {company.startupName}
-
- Invested in 2022
+
+ ${company.amount?.toLocaleString()} Invested
-
- ))) ||
- "You don't invest in any company yet.."}
+
+ ))
+ ) : (
+ investor.portfolioCompanies && investor.portfolioCompanies.length > 0 ? (
+ // Legacy fallback
+ investor.portfolioCompanies.map((company, index) => (
+
+
+
+
+
+ {company}
+ Legacy Entry
+
+
+ ))
+ ) : (
+ No investments yet.
+ )
+ )}
@@ -346,10 +431,10 @@ export const InvestorProfile: React.FC = ({ userId }) => {
- {(investor.minimumInvestment &&
+ {AmountMeasureWithTags((investor.minimumInvestment &&
investor.minimumInvestment) ||
- 0}{" "}
- - {investor.maximumInvestment || 0}
+ 0)}{" "}
+ - {AmountMeasureWithTags(investor.maximumInvestment) || 0}
@@ -358,7 +443,7 @@ export const InvestorProfile: React.FC = ({ userId }) => {
Total Investments
- {investor.totalInvestments || 0} companies
+ {investor.portfolio?.length || investor.totalInvestments || 0} companies
@@ -430,7 +515,7 @@ export const InvestorProfile: React.FC = ({ userId }) => {
Avg. ROI
- 3.2x
+ {((investor.portfolio?.length || 0) * 1.2 + 1.5).toFixed(1)}x
@@ -444,8 +529,9 @@ export const InvestorProfile: React.FC = ({ userId }) => {
Active Investments
- {(investor.portfolioCompanies &&
- investor.portfolioCompanies.length) ||
+ {(investor.portfolio &&
+ investor.portfolio.length) ||
+ (investor.portfolioCompanies?.length) ||
0}
diff --git a/src/pages/viewdeals/ViewDeal.tsx b/src/pages/viewdeals/ViewDeal.tsx
index 3e8e90155..d200f00be 100644
--- a/src/pages/viewdeals/ViewDeal.tsx
+++ b/src/pages/viewdeals/ViewDeal.tsx
@@ -1,43 +1,72 @@
-import React, { useState } from "react";
+import React, { useState, useEffect } from "react";
+import axios from "axios";
import { Card, CardBody, CardHeader } from "../../components/ui/Card";
import { Button } from "../../components/ui/Button";
import { toast } from "react-hot-toast";
+import { useAuth } from "../../context/AuthContext";
+import { DealForm } from "../../components/DealForm";
+import { NegotiationModal } from "../../components/NegotiationModal";
+import { useNavigate } from "react-router-dom";
+
+const URL = import.meta.env.VITE_BACKEND_URL;
export const ViewDeals: React.FC = () => {
- // Dummy static deals data
- const [deals, setDeals] = useState([
- {
- _id: "1",
- investorName: "John Doe",
- investorEmail: "john@example.com",
- businessName: "Acme Startup",
- amount: 50000,
- equity: 5,
- message: "We want to invest in your startup.",
- status: "pending",
- },
- {
- _id: "2",
- investorName: "Jane Smith",
- investorEmail: "jane@example.com",
- businessName: "Acme Startup",
- amount: 100000,
- equity: 10,
- message: "We are interested in funding your growth.",
- status: "pending",
- },
- ]);
+ const { user } = useAuth();
+ const [deals, setDeals] = useState ([]);
+ const [loading, setLoading] = useState(true);
+ const navigate = useNavigate();
+ // Modal states
+ const [selectedDeal, setSelectedDeal] = useState(null);
+ const [isViewModalOpen, setIsViewModalOpen] = useState(false);
+ const [isNegotiationModalOpen, setIsNegotiationModalOpen] = useState(false);
+
+ useEffect(() => {
+ fetchDeals();
+ }, [user]);
+
+ const fetchDeals = async () => {
+ if (!user?.userId) return;
+ try {
+ setLoading(true);
+ const res = await axios.get(`${URL}/deal/get-deals/${user.userId}`);
+ setDeals(res.data);
+ } catch (error) {
+ console.error("Error fetching deals:", error);
+ toast.error("Failed to load deals.");
+ } finally {
+ setLoading(false);
+ }
+ };
- const handleDealStatus = (
+ const handleDealStatus = async (
dealId: string,
- status: "accepted" | "rejected"
+ action: "accept" | "reject"
) => {
- setDeals((prev) =>
- prev.map((d) => (d._id === dealId ? { ...d, status } : d))
- );
- toast.success(`Deal ${status}`);
+ try {
+ await axios.put(`${URL}/deal/update-deal/${dealId}`, {
+ action,
+ role: "entrepreneur",
+ });
+ toast.success(`Deal ${action === 'accept' ? 'accepted' : 'rejected'}!`);
+ fetchDeals(); // Refresh list
+ } catch (error) {
+ console.error(`Error ${action}ing deal:`, error);
+ toast.error(`Failed to ${action} deal.`);
+ }
+ };
+
+ const openViewModal = (deal: any) => {
+ setSelectedDeal(deal);
+ setIsViewModalOpen(true);
};
+ const openNegotiationModal = (deal: any) => {
+ setSelectedDeal(deal);
+ setIsNegotiationModalOpen(true);
+ };
+
+ if (loading) return Loading deals... ;
+
return (
Investor Deals
@@ -51,57 +80,124 @@ export const ViewDeals: React.FC = () => {
- {deal.investorName}
+ {deal.investorId?.name || "Investor"}
- {deal.investorEmail}
+ {deal.investorId?.email}
+
+ Sent {new Date(deal.createdAt).toLocaleDateString()}
+
- {deal.status}
+ {deal.status.charAt(0).toUpperCase() + deal.status.slice(1)}
+ {deal.paymentStatus === 'funds_released' && (
+
+ Payment Approved
+
+ )}
- Business: {deal.businessName}
+ Startup: {deal.entrepreneurId?.startupName}
- Investment Amount: ${deal.amount}
+ Investment Amount: ${deal.investmentAmount?.toLocaleString()}
- Requested Equity: {deal.equity}%
+ Valuation (Post-Money): ${deal.postMoneyValuation?.toLocaleString()}
- Message: {deal.message}
+ Equity Offered: {deal.equityOffered}%
+
+
+ Type: {deal.investmentType}
- {deal.status === "pending" && (
-
- handleDealStatus(deal._id, "accepted")}
- >
- Accept
-
- handleDealStatus(deal._id, "rejected")}
- >
- Reject
+
+ openViewModal(deal)}
+ >
+ See Full Deal
+
+
+ {/* Only show actions if pending or negotiating (and last action wasn't us) */}
+ {(deal.status === "pending" || deal.status === "negotiating") && (
+ <>
+ {/* If negotiating and last action was US (entrepreneur), we act as 'waiting' */}
+ {deal.status === 'negotiating' && deal.lastActionBy === 'entrepreneur' ? (
+ Waiting for investor response...
+ ) : (
+ <>
+ openNegotiationModal(deal)}
+ >
+ Negotiate
+
+ handleDealStatus(deal._id, "accept")}
+ >
+ Accept
+
+ handleDealStatus(deal._id, "reject")}
+ >
+ Reject
+
+ >
+ )}
+ >
+ )}
+
+ {deal.status === 'accepted' && (
+ navigate(`/chat/${deal.investorId?._id}`)}>
+ Chat with Investor
-
- )}
+ )}
+
))}
)}
+
+ {/* View Modal (Read Only) */}
+ {isViewModalOpen && selectedDeal && (
+ setIsViewModalOpen(false)}
+ readOnly={true}
+ initialData={selectedDeal}
+ />
+ )}
+
+ {/* Negotiation Modal */}
+ {isNegotiationModalOpen && selectedDeal && (
+ {
+ setIsNegotiationModalOpen(false);
+ fetchDeals();
+ }}
+ role="entrepreneur"
+ />
+ )}
);
};
diff --git a/src/types/index.ts b/src/types/index.ts
index f08e9817e..ef17e077f 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -14,6 +14,13 @@ export interface User {
isSuspended?: boolean;
}
+export interface TeamMember {
+ _id?: string;
+ name: string;
+ role: string[];
+ avatarUrl: string;
+}
+
export interface Entrepreneur extends User {
startupName: string | undefined;
pitchSummary: string | undefined;
@@ -21,15 +28,34 @@ export interface Entrepreneur extends User {
industry: string | undefined;
foundedYear: number | undefined;
teamSize: number | undefined;
+ team?: TeamMember[];
revenue: string | undefined;
profitMargin: number | undefined;
growthRate: number | undefined;
marketOpportunity: string | undefined;
advantage: string | undefined;
+ valuation?: number;
+ preSeedStatus?: 'pending' | 'in-progress' | 'completed';
+ seedStatus?: 'pending' | 'in-progress' | 'completed';
+ seriesAStatus?: 'pending' | 'in-progress' | 'completed';
+ fundingHistory?: {
+ amount: number;
+ stage: string;
+ year: number;
+ date: Date;
+ }[];
+ businessThumbnails?: string[];
+ investors?: {
+ name: string;
+ avatarUrl: string;
+ userId: string;
+ }[];
+ totalRaised?: number;
}
export interface Investor extends User {
investmentInterests: string[] | undefined;
+ investmentStage: string[] | undefined;
portfolioCompanies: string[] | undefined;
totalInvestments: number | undefined;
minimumInvestment: number | undefined;
@@ -38,6 +64,13 @@ export interface Investor extends User {
successfullExits: number | undefined;
minTimline: number | undefined;
maxTimline: number | undefined;
+ portfolio?: {
+ startupName: string;
+ avatarUrl: string;
+ amount: number;
+ entrepreneurId: string;
+ userId: string;
+ }[];
}
export interface Message {
diff --git a/vite.config.ts.timestamp-1768834652432-06b607aa51a8.mjs b/vite.config.ts.timestamp-1768834652432-06b607aa51a8.mjs
new file mode 100644
index 000000000..8e4e721f9
--- /dev/null
+++ b/vite.config.ts.timestamp-1768834652432-06b607aa51a8.mjs
@@ -0,0 +1,82 @@
+// vite.config.ts
+import { defineConfig } from "file:///E:/final%20year%20project/frontend/Nexus/node_modules/vite/dist/node/index.js";
+import react from "file:///E:/final%20year%20project/frontend/Nexus/node_modules/@vitejs/plugin-react/dist/index.mjs";
+import { VitePWA } from "file:///E:/final%20year%20project/frontend/Nexus/node_modules/vite-plugin-pwa/dist/index.js";
+var vite_config_default = defineConfig({
+ plugins: [
+ react(),
+ VitePWA({
+ registerType: "autoUpdate",
+ devOptions: {
+ enabled: true
+ },
+ includeAssets: ["favicon.svg", "robots.txt"],
+ manifest: {
+ name: "TrustBridge AI",
+ short_name: "TrustBridge",
+ description: "Business Collaboration App",
+ theme_color: "#0f172a",
+ background_color: "#ffffff",
+ display: "standalone",
+ scope: "/",
+ start_url: "/login",
+ icons: [
+ {
+ src: "/logo-192x192.png",
+ sizes: "192x192",
+ type: "image/png"
+ },
+ {
+ src: "/logo-512x512.png",
+ sizes: "512x512",
+ type: "image/png"
+ }
+ ]
+ },
+ workbox: {
+ cleanupOutdatedCaches: true,
+ maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
+ // 5 MB
+ navigateFallbackDenylist: [
+ /^\/$/,
+ // landing
+ /^\/All-Campaigns(\/.*)?$/,
+ /^\/Fundraises(\/.*)?$/
+ ],
+ runtimeCaching: [
+ {
+ urlPattern: ({ request, url }) => {
+ if (request.destination !== "document") return false;
+ const excluded = [
+ "/",
+ "/All-Campaigns",
+ "/Fundraises"
+ ];
+ return !excluded.some(
+ (route) => url.pathname === route || url.pathname.startsWith(route + "/")
+ );
+ },
+ handler: "NetworkFirst",
+ options: {
+ cacheName: "pwa-pages-cache"
+ }
+ },
+ {
+ urlPattern: ({ request }) => request.destination === "script" || request.destination === "style",
+ handler: "StaleWhileRevalidate",
+ options: {
+ cacheName: "assets-cache"
+ }
+ }
+ ]
+ }
+ })
+ ],
+ optimizeDeps: {
+ exclude: ["lucide-react"]
+ }
+});
+export {
+ vite_config_default as default
+};
+//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCJFOlxcXFxmaW5hbCB5ZWFyIHByb2plY3RcXFxcZnJvbnRlbmRcXFxcTmV4dXNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIkU6XFxcXGZpbmFsIHllYXIgcHJvamVjdFxcXFxmcm9udGVuZFxcXFxOZXh1c1xcXFx2aXRlLmNvbmZpZy50c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vRTovZmluYWwlMjB5ZWFyJTIwcHJvamVjdC9mcm9udGVuZC9OZXh1cy92aXRlLmNvbmZpZy50c1wiO2ltcG9ydCB7IGRlZmluZUNvbmZpZyB9IGZyb20gJ3ZpdGUnXHJcbmltcG9ydCByZWFjdCBmcm9tICdAdml0ZWpzL3BsdWdpbi1yZWFjdCdcclxuaW1wb3J0IHsgVml0ZVBXQSB9IGZyb20gJ3ZpdGUtcGx1Z2luLXB3YSdcclxuXHJcbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XHJcbiAgcGx1Z2luczogW1xyXG4gICAgcmVhY3QoKSxcclxuICAgIFZpdGVQV0Eoe1xyXG4gICAgICByZWdpc3RlclR5cGU6ICdhdXRvVXBkYXRlJyxcclxuICAgICAgZGV2T3B0aW9uczoge1xyXG4gICAgICAgIGVuYWJsZWQ6IHRydWVcclxuICAgICAgfSxcclxuICAgICAgaW5jbHVkZUFzc2V0czogWydmYXZpY29uLnN2ZycsICdyb2JvdHMudHh0J10sXHJcbiAgICAgIG1hbmlmZXN0OiB7XHJcbiAgICAgICAgbmFtZTogJ1RydXN0QnJpZGdlIEFJJyxcclxuICAgICAgICBzaG9ydF9uYW1lOiAnVHJ1c3RCcmlkZ2UnLFxyXG4gICAgICAgIGRlc2NyaXB0aW9uOiAnQnVzaW5lc3MgQ29sbGFib3JhdGlvbiBBcHAnLFxyXG4gICAgICAgIHRoZW1lX2NvbG9yOiAnIzBmMTcyYScsXHJcbiAgICAgICAgYmFja2dyb3VuZF9jb2xvcjogJyNmZmZmZmYnLFxyXG4gICAgICAgIGRpc3BsYXk6ICdzdGFuZGFsb25lJyxcclxuICAgICAgICBzY29wZTogJy8nLFxyXG4gICAgICAgIHN0YXJ0X3VybDogJy9sb2dpbicsXHJcbiAgICAgICAgaWNvbnM6IFtcclxuICAgICAgICAgIHtcclxuICAgICAgICAgICAgc3JjOiAnL2xvZ28tMTkyeDE5Mi5wbmcnLFxyXG4gICAgICAgICAgICBzaXplczogJzE5MngxOTInLFxyXG4gICAgICAgICAgICB0eXBlOiAnaW1hZ2UvcG5nJ1xyXG4gICAgICAgICAgfSxcclxuICAgICAgICAgIHtcclxuICAgICAgICAgICAgc3JjOiAnL2xvZ28tNTEyeDUxMi5wbmcnLFxyXG4gICAgICAgICAgICBzaXplczogJzUxMng1MTInLFxyXG4gICAgICAgICAgICB0eXBlOiAnaW1hZ2UvcG5nJ1xyXG4gICAgICAgICAgfVxyXG4gICAgICAgIF1cclxuICAgICAgfSxcclxuICAgICAgd29ya2JveDoge1xyXG4gICAgICAgIGNsZWFudXBPdXRkYXRlZENhY2hlczogdHJ1ZSxcclxuICAgICAgICBtYXhpbXVtRmlsZVNpemVUb0NhY2hlSW5CeXRlczogNSAqIDEwMjQgKiAxMDI0LCAvLyA1IE1CXHJcbiAgICAgICAgbmF2aWdhdGVGYWxsYmFja0RlbnlsaXN0OiBbXHJcbiAgICAgICAgICAvXlxcLyQvLCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAvLyBsYW5kaW5nXHJcbiAgICAgICAgICAvXlxcL0FsbC1DYW1wYWlnbnMoXFwvLiopPyQvLFxyXG4gICAgICAgICAgL15cXC9GdW5kcmFpc2VzKFxcLy4qKT8kLyxcclxuICAgICAgICBdLFxyXG5cclxuICAgICAgICBydW50aW1lQ2FjaGluZzogW1xyXG4gICAgICAgICAge1xyXG4gICAgICAgICAgICB1cmxQYXR0ZXJuOiAoeyByZXF1ZXN0LCB1cmwgfSkgPT4ge1xyXG4gICAgICAgICAgICAgIGlmIChyZXF1ZXN0LmRlc3RpbmF0aW9uICE9PSAnZG9jdW1lbnQnKSByZXR1cm4gZmFsc2VcclxuXHJcbiAgICAgICAgICAgICAgY29uc3QgZXhjbHVkZWQgPSBbXHJcbiAgICAgICAgICAgICAgICAnLycsXHJcbiAgICAgICAgICAgICAgICAnL0FsbC1DYW1wYWlnbnMnLFxyXG4gICAgICAgICAgICAgICAgJy9GdW5kcmFpc2VzJ1xyXG4gICAgICAgICAgICAgIF1cclxuXHJcbiAgICAgICAgICAgICAgcmV0dXJuICFleGNsdWRlZC5zb21lKHJvdXRlID0+XHJcbiAgICAgICAgICAgICAgICB1cmwucGF0aG5hbWUgPT09IHJvdXRlIHx8IHVybC5wYXRobmFtZS5zdGFydHNXaXRoKHJvdXRlICsgJy8nKVxyXG4gICAgICAgICAgICAgIClcclxuICAgICAgICAgICAgfSxcclxuICAgICAgICAgICAgaGFuZGxlcjogJ05ldHdvcmtGaXJzdCcsXHJcbiAgICAgICAgICAgIG9wdGlvbnM6IHtcclxuICAgICAgICAgICAgICBjYWNoZU5hbWU6ICdwd2EtcGFnZXMtY2FjaGUnXHJcbiAgICAgICAgICAgIH1cclxuICAgICAgICAgIH0sXHJcbiAgICAgICAgICB7XHJcbiAgICAgICAgICAgIHVybFBhdHRlcm46ICh7IHJlcXVlc3QgfSkgPT5cclxuICAgICAgICAgICAgICByZXF1ZXN0LmRlc3RpbmF0aW9uID09PSAnc2NyaXB0JyB8fFxyXG4gICAgICAgICAgICAgIHJlcXVlc3QuZGVzdGluYXRpb24gPT09ICdzdHlsZScsXHJcbiAgICAgICAgICAgIGhhbmRsZXI6ICdTdGFsZVdoaWxlUmV2YWxpZGF0ZScsXHJcbiAgICAgICAgICAgIG9wdGlvbnM6IHtcclxuICAgICAgICAgICAgICBjYWNoZU5hbWU6ICdhc3NldHMtY2FjaGUnXHJcbiAgICAgICAgICAgIH1cclxuICAgICAgICAgIH1cclxuICAgICAgICBdXHJcbiAgICAgIH1cclxuICAgIH0pXHJcbiAgXSxcclxuICBvcHRpbWl6ZURlcHM6IHtcclxuICAgIGV4Y2x1ZGU6IFsnbHVjaWRlLXJlYWN0J11cclxuICB9XHJcbn0pXHJcbiJdLAogICJtYXBwaW5ncyI6ICI7QUFBMFMsU0FBUyxvQkFBb0I7QUFDdlUsT0FBTyxXQUFXO0FBQ2xCLFNBQVMsZUFBZTtBQUV4QixJQUFPLHNCQUFRLGFBQWE7QUFBQSxFQUMxQixTQUFTO0FBQUEsSUFDUCxNQUFNO0FBQUEsSUFDTixRQUFRO0FBQUEsTUFDTixjQUFjO0FBQUEsTUFDZCxZQUFZO0FBQUEsUUFDVixTQUFTO0FBQUEsTUFDWDtBQUFBLE1BQ0EsZUFBZSxDQUFDLGVBQWUsWUFBWTtBQUFBLE1BQzNDLFVBQVU7QUFBQSxRQUNSLE1BQU07QUFBQSxRQUNOLFlBQVk7QUFBQSxRQUNaLGFBQWE7QUFBQSxRQUNiLGFBQWE7QUFBQSxRQUNiLGtCQUFrQjtBQUFBLFFBQ2xCLFNBQVM7QUFBQSxRQUNULE9BQU87QUFBQSxRQUNQLFdBQVc7QUFBQSxRQUNYLE9BQU87QUFBQSxVQUNMO0FBQUEsWUFDRSxLQUFLO0FBQUEsWUFDTCxPQUFPO0FBQUEsWUFDUCxNQUFNO0FBQUEsVUFDUjtBQUFBLFVBQ0E7QUFBQSxZQUNFLEtBQUs7QUFBQSxZQUNMLE9BQU87QUFBQSxZQUNQLE1BQU07QUFBQSxVQUNSO0FBQUEsUUFDRjtBQUFBLE1BQ0Y7QUFBQSxNQUNBLFNBQVM7QUFBQSxRQUNQLHVCQUF1QjtBQUFBLFFBQ3ZCLCtCQUErQixJQUFJLE9BQU87QUFBQTtBQUFBLFFBQzFDLDBCQUEwQjtBQUFBLFVBQ3hCO0FBQUE7QUFBQSxVQUNBO0FBQUEsVUFDQTtBQUFBLFFBQ0Y7QUFBQSxRQUVBLGdCQUFnQjtBQUFBLFVBQ2Q7QUFBQSxZQUNFLFlBQVksQ0FBQyxFQUFFLFNBQVMsSUFBSSxNQUFNO0FBQ2hDLGtCQUFJLFFBQVEsZ0JBQWdCLFdBQVksUUFBTztBQUUvQyxvQkFBTSxXQUFXO0FBQUEsZ0JBQ2Y7QUFBQSxnQkFDQTtBQUFBLGdCQUNBO0FBQUEsY0FDRjtBQUVBLHFCQUFPLENBQUMsU0FBUztBQUFBLGdCQUFLLFdBQ3BCLElBQUksYUFBYSxTQUFTLElBQUksU0FBUyxXQUFXLFFBQVEsR0FBRztBQUFBLGNBQy9EO0FBQUEsWUFDRjtBQUFBLFlBQ0EsU0FBUztBQUFBLFlBQ1QsU0FBUztBQUFBLGNBQ1AsV0FBVztBQUFBLFlBQ2I7QUFBQSxVQUNGO0FBQUEsVUFDQTtBQUFBLFlBQ0UsWUFBWSxDQUFDLEVBQUUsUUFBUSxNQUNyQixRQUFRLGdCQUFnQixZQUN4QixRQUFRLGdCQUFnQjtBQUFBLFlBQzFCLFNBQVM7QUFBQSxZQUNULFNBQVM7QUFBQSxjQUNQLFdBQVc7QUFBQSxZQUNiO0FBQUEsVUFDRjtBQUFBLFFBQ0Y7QUFBQSxNQUNGO0FBQUEsSUFDRixDQUFDO0FBQUEsRUFDSDtBQUFBLEVBQ0EsY0FBYztBQUFBLElBQ1osU0FBUyxDQUFDLGNBQWM7QUFBQSxFQUMxQjtBQUNGLENBQUM7IiwKICAibmFtZXMiOiBbXQp9Cg==
| |