From 7a51d876a3d0fe79a40506d9aa2da14db298bd52 Mon Sep 17 00:00:00 2001 From: mzain4321 Date: Fri, 16 Jan 2026 16:48:42 +0500 Subject: [PATCH 01/14] stripe integrated for campaigns --- .env | 3 +- dev-dist/sw.js | 2 +- package-lock.json | 57 +++- package.json | 3 + src/components/camp/StripeDonationForm.tsx | 200 ++++++++++++ src/pages/admin/supporters.tsx | 288 ++++++++++-------- src/pages/campaignPage/CampaignDetailPage.tsx | 158 +++------- src/pages/campaignPage/CampaignPage.tsx | 160 +++------- .../dashCamp/DashboardCampaignDetail.tsx | 118 +++++-- src/pages/dashCamp/DashboardCampaigns.tsx | 129 ++++++-- 10 files changed, 689 insertions(+), 429 deletions(-) create mode 100644 src/components/camp/StripeDonationForm.tsx diff --git a/.env b/.env index 2e0184271..5b6b1154f 100644 --- a/.env +++ b/.env @@ -6,4 +6,5 @@ VITE_BACKEND_URL=http://localhost:5000 GOOGLE_CLIENT_ID="1087411543598-b2nh98usn3o29fpjn3s6mfljhkoo65s5.apps.googleusercontent.com" GOOGLE_CLIENT_SECRET="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" GOOGLE_CALLBACK_URL="http://localhost:5000/auth/google/callback" -TWO_FACTOR_ISSUER=TrustBridge \ No newline at end of file +TWO_FACTOR_ISSUER=TrustBridge +VITE_STRIPE_PUBLISHABLE_KEY=pk_test_51SfEwpJi6UXEfY8xdHehplpPVKfmEnlDV9dYtjiAF3x4aqDpsWiNh9WHiQMo7rRZYPipmCUmZFVkshvgDNiRSND800NKYu1LT1 \ No newline at end of file diff --git a/dev-dist/sw.js b/dev-dist/sw.js index 872a740bd..6c68293d7 100644 --- a/dev-dist/sw.js +++ b/dev-dist/sw.js @@ -82,7 +82,7 @@ define(['./workbox-d70286d7'], (function (workbox) { 'use strict'; "revision": "3ca0b8505b4bec776b69afdba2768812" }, { "url": "index.html", - "revision": "0.kh39an3nq1" + "revision": "0.nmsf0f9m8do" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { diff --git a/package-lock.json b/package-lock.json index f5882ffd0..6911e8e97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,8 @@ "dependencies": { "@botpress/webchat": "^4.0.4", "@hookform/resolvers": "^5.2.2", + "@stripe/react-stripe-js": "^5.4.1", + "@stripe/stripe-js": "^8.6.1", "agora-rtc-sdk-ng": "^4.24.0", "axios": "^1.13.2", "date-fns": "^3.3.1", @@ -26,6 +28,7 @@ "recharts": "^3.5.0", "socket.io-client": "^4.8.1", "sonner": "^2.0.7", + "stripe": "^20.1.2", "yup": "^1.7.1" }, "devDependencies": { @@ -3669,6 +3672,30 @@ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, + "node_modules/@stripe/react-stripe-js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-5.4.1.tgz", + "integrity": "sha512-ipeYcAHa4EPmjwfv0lFE+YDVkOQ0TMKkFWamW+BqmnSkEln/hO8rmxGPPWcd9WjqABx6Ro8Xg4pAS7evCcR9cw==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "@stripe/stripe-js": ">=8.0.0 <9.0.0", + "react": ">=16.8.0 <20.0.0", + "react-dom": ">=16.8.0 <20.0.0" + } + }, + "node_modules/@stripe/stripe-js": { + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-8.6.1.tgz", + "integrity": "sha512-UJ05U2062XDgydbUcETH1AoRQLNhigQ2KmDn1BG8sC3xfzu6JKg95Qt6YozdzFpxl1Npii/02m2LEWFt1RYjVA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12.16" + } + }, "node_modules/@surma/rollup-plugin-off-main-thread": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", @@ -3900,7 +3927,7 @@ "version": "24.3.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz", "integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==", - "dev": true, + "devOptional": true, "license": "MIT", "peer": true, "dependencies": { @@ -9155,9 +9182,9 @@ } }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -10564,6 +10591,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stripe": { + "version": "20.1.2", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-20.1.2.tgz", + "integrity": "sha512-qU+lQRRJnTxmyvglYBPE24/IepncmywsAg0GDTsTdP2pb+3e3RdREHJZjKgqCmv0phPxN/nmgNPnIPPH8w0P4A==", + "license": "MIT", + "dependencies": { + "qs": "^6.14.1" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@types/node": ">=16" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/style-to-js": { "version": "1.1.21", "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", @@ -11108,7 +11155,7 @@ "version": "7.10.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { diff --git a/package.json b/package.json index 1c9adc1ed..01fc5e340 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ "dependencies": { "@botpress/webchat": "^4.0.4", "@hookform/resolvers": "^5.2.2", + "@stripe/react-stripe-js": "^5.4.1", + "@stripe/stripe-js": "^8.6.1", "agora-rtc-sdk-ng": "^4.24.0", "axios": "^1.13.2", "date-fns": "^3.3.1", @@ -28,6 +30,7 @@ "recharts": "^3.5.0", "socket.io-client": "^4.8.1", "sonner": "^2.0.7", + "stripe": "^20.1.2", "yup": "^1.7.1" }, "devDependencies": { diff --git a/src/components/camp/StripeDonationForm.tsx b/src/components/camp/StripeDonationForm.tsx new file mode 100644 index 000000000..de6236f46 --- /dev/null +++ b/src/components/camp/StripeDonationForm.tsx @@ -0,0 +1,200 @@ +import React, { useState } from 'react'; +import { useStripe, useElements, CardElement } from '@stripe/react-stripe-js'; +import axios from 'axios'; +import toast from "react-hot-toast"; +import { Button } from '../ui/Button'; +import { useAuth } from '../../context/AuthContext'; + +interface StripeDonationFormProps { + amount: number; + campaignId: string; + onSuccess: () => void; + onCancel: () => void; +} + +export const StripeDonationForm: React.FC = ({ + amount, + campaignId, + onSuccess, + onCancel, +}) => { + const stripe = useStripe(); + const elements = useElements(); + const { user } = useAuth(); // Check if user is logged in + const [isProcessing, setIsProcessing] = useState(false); + const [guestName, setGuestName] = useState(""); + const [guestPhone, setGuestPhone] = useState(""); + const [guestEmail, setGuestEmail] = useState(""); + + const URL = import.meta.env.VITE_BACKEND_URL; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + + if (!stripe || !elements) { + return; + } + + // Validation for guest + if (!user && (!guestName.trim() || !guestPhone.trim() || !guestEmail.trim())) { + toast.error("Please enter your name, phone number and email"); + return; + } + + const token = localStorage.getItem('token'); + setIsProcessing(true); + + try { + const payload: any = { amount, campaignId }; + const headers: any = {}; + + if (token) { + headers.Authorization = `Bearer ${token}`; + } else { + payload.guestName = guestName; + payload.guestPhone = guestPhone; + payload.guestEmail = guestEmail; + } + + // 1. Create Payment Intent on backend + const { data: { clientSecret } } = await axios.post( + `${URL}/payment/create-payment-intent`, + payload, + { headers } + ); + + // 2. Confirm payment on frontend + const cardElement = elements.getElement(CardElement); + if (!cardElement) return; + + const { error, paymentIntent } = await stripe.confirmCardPayment(clientSecret, { + payment_method: { + card: cardElement, + billing_details: { + name: user ? user.name : guestName, + phone: user ? undefined : guestPhone, + email: user ? user.email : guestEmail + } + }, + }); + + if (error) { + toast.error(error.message || 'Payment failed'); + setIsProcessing(false); + return; + } + + if (paymentIntent.status === 'succeeded') { + // 3. Confirm and save on backend + const confirmPayload = { + ...payload, + paymentIntentId: paymentIntent.id + }; + + await axios.post( + `${URL}/payment/confirm-payment`, + confirmPayload, + { headers } + ); + + toast.success('Donation successful! Thank you for your support.'); + onSuccess(); + } + } catch (err: any) { + console.error('Payment Error:', err); + toast.error(err.response?.data?.message || 'Something went wrong with the payment'); + } finally { + setIsProcessing(false); + } + }; + + return ( +
+ + {!user && ( +
+
+ + setGuestName(e.target.value)} + className="w-full px-4 py-3 bg-gray-900/50 border border-gray-700 rounded-lg text-white focus:outline-none focus:border-orange-500 transition-colors" + placeholder="Enter your full name" + required + /> +
+
+ + setGuestEmail(e.target.value)} + className="w-full px-4 py-3 bg-gray-900/50 border border-gray-700 rounded-lg text-white focus:outline-none focus:border-orange-500 transition-colors" + placeholder="Enter your email address" + required + /> +
+
+ + setGuestPhone(e.target.value)} + className="w-full px-4 py-3 bg-gray-900/50 border border-gray-700 rounded-lg text-white focus:outline-none focus:border-orange-500 transition-colors" + placeholder="Enter your phone number" + required + /> +
+
+ )} + +
+ +
+ +
+ + +
+ +

+ Secure payment powered by Stripe +

+
+ ); +}; 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 -

- -
-
{ - e.preventDefault(); - setSearched(query); - const filterSupportors = supporters.filter((ent) => - ent.name.toLowerCase().includes(query.toLowerCase()) - ); - - if (filterSupportors.length !== 0) - setSearchedSupportors([...filterSupportors]); - else setSearchedSupportors([]); - }} - > - { - setQuery(e.target.value); - if (e.target.value === "") { - setSearchedSupportors([]); - setSearched(""); - } - }} - /> -
+
+
+

+ Campaign Supporters +

+ +
+ +
+ { + setQuery(e.target.value); + if (e.target.value === "") { + setSearchedSupporters([]); + setSearched(""); + } + }} + /> + +
-
- + +
-
- - {searched - ? `Results '${searched}' searched count: ` - : "Total Supportors:"}{" "} - - {searched ? searchedSupportors.length : supporters.length} + +
+
+ {searched ? ( +

Showing {searchedSupporters.length} results for "{searched}"

+ ) : ( +

Total Supporters: {supporters.length}

+ )} +
+
-
- - - - - - - - - - - - {searchedSupportors.length !== 0 ? ( - searchedSupportors.map((user, idx) => ( - - )) - ) : searched ? ( -
No records found..
- ) : supporters.length !== 0 ? ( - supporters.map((user, idx) => ) - ) : ( -
No records found..
- )} -
-
NameEmailCampaignAmount Invested
+
+
+ + + + + + + + + + + + + + + + {isLoading ? ( + + + + ) : (searched ? searchedSupporters : supporters).length === 0 ? ( + + + + ) : ( + (searched ? searchedSupporters : supporters).map((sup, idx) => ( + + )) + )} + +
SupporterEmailPhoneCampaignAmountDateTypeActions
+
+
+ Loading supporters... +
+
+ No supporters found. +
+
); diff --git a/src/pages/campaignPage/CampaignDetailPage.tsx b/src/pages/campaignPage/CampaignDetailPage.tsx index 6c111c8c1..4f8a7bdea 100644 --- a/src/pages/campaignPage/CampaignDetailPage.tsx +++ b/src/pages/campaignPage/CampaignDetailPage.tsx @@ -6,6 +6,11 @@ import { Navbar } from "../../components/home/Navbar"; import { Button } from "../../components/ui/Button"; import { Share2 } from "lucide-react"; import { ShareModal } from "../../components/common/ShareModal"; +import { loadStripe } from "@stripe/stripe-js"; +import { Elements } from "@stripe/react-stripe-js"; +import { StripeDonationForm } from "../../components/camp/StripeDonationForm"; + +const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY); interface Campaign { _id: string; @@ -58,28 +63,28 @@ export const CampaignDetailPage: React.FC = () => { }); // Fetch campaign details - 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); - - if (foundCampaign) { - setCampaign(foundCampaign); - } else { - toast.error("Campaign not found"); - navigate("/All-Campaigns"); - } - } catch (error) { - console.error("Failed to fetch campaign:", error); - toast.error("Failed to load campaign details"); + 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("/All-Campaigns"); - } finally { - setLoading(false); } - }; + } catch (error) { + console.error("Failed to fetch campaign:", error); + toast.error("Failed to load campaign details"); + navigate("/All-Campaigns"); + } finally { + setLoading(false); + } + }; + useEffect(() => { if (id) { fetchCampaign(); } @@ -430,108 +435,19 @@ export const CampaignDetailPage: React.FC = () => {
- {/* Payment Form */} -
-
-
- - { - const value = e.target.value.replace(/\s/g, '').replace(/\D/g, ''); - if (value.length <= 16) { - setDonationForm(prev => ({ ...prev, cardNumber: value })); - } - }} - className="w-full px-4 py-3 bg-gray-900/50 border border-gray-700 rounded-lg text-white focus:outline-none focus:border-orange-500 transition-colors" - required - /> -
- -
- - setDonationForm(prev => ({ ...prev, cardHolder: e.target.value }))} - className="w-full px-4 py-3 bg-gray-900/50 border border-gray-700 rounded-lg text-white focus:outline-none focus:border-orange-500 transition-colors" - required - /> -
- -
-
- - { - let value = e.target.value.replace(/\D/g, ''); - if (value.length >= 2) { - value = value.slice(0, 2) + '/' + value.slice(2, 4); - } - if (value.length <= 5) { - setDonationForm(prev => ({ ...prev, expiryDate: value })); - } - }} - className="w-full px-4 py-3 bg-gray-900/50 border border-gray-700 rounded-lg text-white focus:outline-none focus:border-orange-500 transition-colors" - required - /> -
- -
- - { - const value = e.target.value.replace(/\D/g, ''); - if (value.length <= 4) { - setDonationForm(prev => ({ ...prev, cvv: value })); - } - }} - className="w-full px-4 py-3 bg-gray-900/50 border border-gray-700 rounded-lg text-white focus:outline-none focus:border-orange-500 transition-colors" - required - /> -
-
-
- - {/* Submit Button */} - - - {/* Security Notice */} -
-
- - - - Your donation is secure and encrypted -
-
-
+ {/* Stripe Payment Form */} + + setShowDonationForm(false)} + onSuccess={() => { + setShowDonationForm(false); + // Refresh campaign data in-place + fetchCampaign(); + }} + /> +
diff --git a/src/pages/campaignPage/CampaignPage.tsx b/src/pages/campaignPage/CampaignPage.tsx index 2f80634a3..54264c5bc 100644 --- a/src/pages/campaignPage/CampaignPage.tsx +++ b/src/pages/campaignPage/CampaignPage.tsx @@ -1,9 +1,9 @@ -import React, { useState, useEffect } from "react"; -import { useNavigate } from "react-router-dom"; -import { Navbar } from "../../components/home/Navbar"; -import { Button } from "../../components/ui/Button"; -import axios from "axios"; -import toast from "react-hot-toast"; +import { Share2 } from "lucide-react"; +import { loadStripe } from "@stripe/stripe-js"; +import { Elements } from "@stripe/react-stripe-js"; +import { StripeDonationForm } from "../../components/camp/StripeDonationForm"; + +const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY); interface CampaignCardProps { _id: string; @@ -56,23 +56,23 @@ 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`); + // 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); + } + }; + useEffect(() => { fetchCampaigns(); }, [URL]); @@ -429,108 +429,20 @@ export const CampaignsPage: React.FC = () => { - {/* Payment Form */} -
-
-
- - { - const value = e.target.value.replace(/\s/g, '').replace(/\D/g, ''); - if (value.length <= 16) { - setDonationForm(prev => ({ ...prev, cardNumber: value })); - } - }} - className="w-full px-4 py-3 bg-gray-900/50 border border-gray-700 rounded-lg text-white focus:outline-none focus:border-orange-500 transition-colors" - required - /> -
- -
- - setDonationForm(prev => ({ ...prev, cardHolder: e.target.value }))} - className="w-full px-4 py-3 bg-gray-900/50 border border-gray-700 rounded-lg text-white focus:outline-none focus:border-orange-500 transition-colors" - required - /> -
- -
-
- - { - let value = e.target.value.replace(/\D/g, ''); - if (value.length >= 2) { - value = value.slice(0, 2) + '/' + value.slice(2, 4); - } - if (value.length <= 5) { - setDonationForm(prev => ({ ...prev, expiryDate: value })); - } - }} - className="w-full px-4 py-3 bg-gray-900/50 border border-gray-700 rounded-lg text-white focus:outline-none focus:border-orange-500 transition-colors" - required - /> -
- -
- - { - const value = e.target.value.replace(/\D/g, ''); - if (value.length <= 4) { - setDonationForm(prev => ({ ...prev, cvv: value })); - } - }} - className="w-full px-4 py-3 bg-gray-900/50 border border-gray-700 rounded-lg text-white focus:outline-none focus:border-orange-500 transition-colors" - required - /> -
-
-
- - {/* Submit Button */} - - - {/* Security Notice */} -
-
- - - - Your donation is secure and encrypted -
-
-
+ {/* Stripe Donation Form */} +
+ + { + handleCloseModal(); + fetchCampaigns(); + }} + onCancel={handleCloseModal} + /> + +
diff --git a/src/pages/dashCamp/DashboardCampaignDetail.tsx b/src/pages/dashCamp/DashboardCampaignDetail.tsx index 648dbfa32..72e42d559 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 { StripeDonationForm } from "../../components/camp/StripeDonationForm"; + +const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY); interface Campaign { _id: string; @@ -36,29 +41,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(); } @@ -274,7 +281,10 @@ export const DashboardCampaignDetail: React.FC = () => { Your contribution can help achieve the goal and change lives. Support this campaign directly on our main platform.

+ + +
+ {/* Donation Amount Selection */} +
+ +
+ {[25, 50, 100, 250, 500, 1000].map((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); + fetchCampaign(); + }} + onCancel={() => setShowDonationForm(false)} + /> + +
+ + + )} ); }; diff --git a/src/pages/dashCamp/DashboardCampaigns.tsx b/src/pages/dashCamp/DashboardCampaigns.tsx index 12e243422..4f3a45d6f 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 { StripeDonationForm } from "../../components/camp/StripeDonationForm"; + +const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY); interface CampaignCardProps { _id: string; @@ -31,25 +36,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]); @@ -82,7 +90,7 @@ export const DashboardCampaigns: React.FC = () => { }) => { 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 ( @@ -129,7 +137,7 @@ export const DashboardCampaigns: React.FC = () => {
@@ -139,13 +147,17 @@ export const DashboardCampaigns: React.FC = () => {
@@ -179,10 +191,10 @@ export const DashboardCampaigns: React.FC = () => { @@ -263,6 +275,75 @@ export const DashboardCampaigns: React.FC = () => { ))}
)} + + {/* Donation Form Modal */} + {showDonationForm && selectedCampaign && ( +
+
+ {/* Header */} +
+
+ +

+ Support {selectedCampaign.title} +

+
+ +
+ +
+ {/* Donation Amount Selection */} +
+ +
+ {[25, 50, 100, 250, 500, 1000].map((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)} + /> + +
+
+
+ )}
); }; From 857cb83ed80073bfa078fb1fea31f65753af43a9 Mon Sep 17 00:00:00 2001 From: mzain4321 Date: Fri, 16 Jan 2026 19:43:21 +0500 Subject: [PATCH 02/14] d --- dev-dist/sw.js | 2 +- src/App.tsx | 2 - src/components/camp/CampForm.tsx | 62 +++- .../dashboard/StripeDashboardPaymentForm.tsx | 154 ++++++++ src/components/layout/Sidebar.tsx | 10 +- src/pages/campaignPage/CampaignPage.tsx | 6 + .../dashCamp/DashboardCampaignDetail.tsx | 336 +++++++++--------- src/pages/dashCamp/DashboardCampaigns.tsx | 204 +++++------ src/pages/dashboard/AdminDashboard.tsx | 24 +- 9 files changed, 495 insertions(+), 305 deletions(-) create mode 100644 src/components/dashboard/StripeDashboardPaymentForm.tsx diff --git a/dev-dist/sw.js b/dev-dist/sw.js index 6c68293d7..ad59b3b28 100644 --- a/dev-dist/sw.js +++ b/dev-dist/sw.js @@ -82,7 +82,7 @@ define(['./workbox-d70286d7'], (function (workbox) { 'use strict'; "revision": "3ca0b8505b4bec776b69afdba2768812" }, { "url": "index.html", - "revision": "0.nmsf0f9m8do" + "revision": "0.0ekk4lc4ck8" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { diff --git a/src/App.tsx b/src/App.tsx index 549bc00e8..d778d984a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -41,7 +41,6 @@ import { AudioCall } from "./pages/webRTC/AudioCall"; import { Toaster } from "react-hot-toast"; import { HomePage } from "./pages/home/HomePage"; import { Supporters } from "./pages/admin/supporters"; -import { FlaggedAccounts } from "./pages/admin/flaggedAccounts"; import { Users } from "./pages/admin/Users"; import { Campaigns } from "./pages/admin/Campaigns"; import { DealsPage } from "./pages/deals/DealsPage"; @@ -103,7 +102,6 @@ function App() { } /> } /> } /> - } /> } /> = ({ onSuccess, initialData }) => { startDate: "", endDate: "", category: "Other", + organizer: "", + isLifetime: false, }); const [images, setImages] = useState([]); @@ -37,8 +39,10 @@ const CampForm: React.FC = ({ onSuccess, initialData }) => { goalAmount: initialData.goalAmount || "", startDate: initialData.startDate ? new Date(initialData.startDate).toISOString().split('T')[0] : "", endDate: initialData.endDate ? new Date(initialData.endDate).toISOString().split('T')[0] : "", + category: initialData.category || "Other", organizer: initialData.organizer || "", + isLifetime: initialData.isLifetime || false, }); if (initialData.images && initialData.images.length > 0) { setExistingImages(initialData.images); @@ -55,7 +59,14 @@ const CampForm: React.FC = ({ onSuccess, initialData }) => { HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement > ) => { - const { name, value } = e.target; + const { name, value, type } = e.target; + // @ts-ignore + const checked = (e.target as HTMLInputElement).checked; + + if (type === "checkbox") { + setFormData({ ...formData, [name]: checked }); + return; + } // Title: only letters and spaces if (name === "title" && !/^[A-Za-z\s]*$/.test(value)) return; @@ -130,7 +141,7 @@ const CampForm: React.FC = ({ onSuccess, initialData }) => { }; const validateForm = () => { - const { title, description, goalAmount, startDate, endDate, category } = + const { title, description, goalAmount, startDate, endDate, category, isLifetime } = formData; if (!title.trim()) { @@ -172,14 +183,16 @@ const CampForm: React.FC = ({ onSuccess, initialData }) => { return false; } - if (!endDate) { - toast.error("End date is required"); - return false; - } + if (!isLifetime) { + if (!endDate) { + toast.error("End date is required"); + return false; + } - if (new Date(startDate) > new Date(endDate)) { - toast.error("Start date cannot be after end date"); - return false; + if (new Date(startDate) > new Date(endDate)) { + toast.error("Start date cannot be after end date"); + return false; + } } if (!category.trim()) { @@ -350,15 +363,28 @@ const CampForm: React.FC = ({ onSuccess, initialData }) => {
- +
+ + +
diff --git a/src/components/dashboard/StripeDashboardPaymentForm.tsx b/src/components/dashboard/StripeDashboardPaymentForm.tsx new file mode 100644 index 000000000..30bdfb439 --- /dev/null +++ b/src/components/dashboard/StripeDashboardPaymentForm.tsx @@ -0,0 +1,154 @@ +import React, { useState } from 'react'; +import { useStripe, useElements, CardElement } from '@stripe/react-stripe-js'; +import axios from 'axios'; +import toast from "react-hot-toast"; +import { Button } from '../ui/Button'; +import { useAuth } from '../../context/AuthContext'; + +interface StripeDashboardPaymentFormProps { + amount: number; + paymentType?: 'verification' | 'subscription' | 'other'; + campaignId?: string; + onSuccess: () => void; + onCancel: () => void; +} + +export const StripeDashboardPaymentForm: React.FC = ({ + amount, + paymentType, + campaignId, + onSuccess, + onCancel, +}) => { + const stripe = useStripe(); + const elements = useElements(); + const { user } = useAuth(); + const [isProcessing, setIsProcessing] = useState(false); + + const URL = import.meta.env.VITE_BACKEND_URL; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + + if (!stripe || !elements) { + return; + } + + if (!user) { + toast.error("Please log in to make a payment"); + return; + } + + const token = localStorage.getItem('token'); + setIsProcessing(true); + + try { + const payload: any = { + amount, + paymentType: paymentType || (campaignId ? 'donation' : 'other'), + campaignId, + description: campaignId + ? `Donation for campaign ${campaignId}` + : `Dashboard ${paymentType || 'other'} payment` + }; + const headers: any = { Authorization: `Bearer ${token}` }; + + // 1. Create Payment Intent on backend + // Note: Reusing create-payment-intent but without campaignId for dashboard payments + // This might need a slightly different endpoint if specific logic is needed + const { data: { clientSecret } } = await axios.post( + `${URL}/payment/create-payment-intent`, + payload, + { headers } + ); + + // 2. Confirm payment on frontend + const cardElement = elements.getElement(CardElement); + if (!cardElement) return; + + const { error, paymentIntent } = await stripe.confirmCardPayment(clientSecret, { + payment_method: { + card: cardElement, + billing_details: { + name: user.name, + email: user.email + } + }, + }); + + if (error) { + toast.error(error.message || 'Payment failed'); + setIsProcessing(false); + return; + } + + if (paymentIntent.status === 'succeeded') { + // 3. Confirm and save on backend + const confirmPayload = { + ...payload, + paymentIntentId: paymentIntent.id + }; + + await axios.post( + `${URL}/payment/confirm-payment`, + confirmPayload, + { headers } + ); + + toast.success('Payment successful!'); + onSuccess(); + } + } catch (err: any) { + console.error('Payment Error:', err); + toast.error(err.response?.data?.message || 'Something went wrong with the payment'); + } finally { + setIsProcessing(false); + } + }; + + return ( +
+
+ +
+ +
+ + +
+ +

+ Secure payment powered by Stripe +

+
+ ); +}; diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 615a4a718..413d54abe 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -126,11 +126,11 @@ export const Sidebar: React.FC = () => { icon: , text: "Fraud and Risk Detection", }, - { - to: "/admin/flaggedAccounts", - icon: , - text: "Flagged Accounts", - }, + // { + // to: "/admin/flaggedAccounts", + // icon: , + // text: "Flagged Accounts", + // }, { to: "/admin/send-notification", icon: , diff --git a/src/pages/campaignPage/CampaignPage.tsx b/src/pages/campaignPage/CampaignPage.tsx index 54264c5bc..8eb9f884c 100644 --- a/src/pages/campaignPage/CampaignPage.tsx +++ b/src/pages/campaignPage/CampaignPage.tsx @@ -1,7 +1,13 @@ +import React, { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import axios from "axios"; +import { toast } from "react-hot-toast"; import { Share2 } from "lucide-react"; 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"; const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY); diff --git a/src/pages/dashCamp/DashboardCampaignDetail.tsx b/src/pages/dashCamp/DashboardCampaignDetail.tsx index 72e42d559..57a69b784 100644 --- a/src/pages/dashCamp/DashboardCampaignDetail.tsx +++ b/src/pages/dashCamp/DashboardCampaignDetail.tsx @@ -8,7 +8,7 @@ import { ArrowLeft, Calendar, Clock, Share2, ShieldCheck, X, Rocket } from "luci import { ShareModal } from "../../components/common/ShareModal"; import { loadStripe } from "@stripe/stripe-js"; import { Elements } from "@stripe/react-stripe-js"; -import { StripeDonationForm } from "../../components/camp/StripeDonationForm"; +import { StripeDashboardPaymentForm } from "../../components/dashboard/StripeDashboardPaymentForm"; const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY); @@ -104,206 +104,208 @@ export const DashboardCampaignDetail: React.FC = () => { const daysLeft = calculateDaysLeft(campaign.endDate); return ( -
- {/* Header */} -
-
- -
-

{campaign.title}

- + <> +
+ {/* Header */} +
+
+ +
+

{campaign.title}

+ +
-
-
- - {/* + {/* */} +
-
-
- {/* Left Column - Media & Description */} -
- {/* Media Slider */} - - {campaign.images && campaign.images.length > 0 && ( -
- {campaign.images.map((img, idx) => ( - {campaign.title} - ))} - - {/* Indicators */} -
- {campaign.images.map((_, idx) => ( -
- {/* About Section */} - - -

About This Campaign

-
- {campaign.description} -
-
-
+ {/* Floating Labels */} +
+ + {campaign.category} + + + {daysLeft} Days Left + +
+
+ )} +
- {/* Video Section if available */} - {campaign.video && ( + {/* About Section */} -

Campaign Video

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

-
-
-

Days Left

-

{daysLeft}

+
+
+ {progress.toFixed(1)}% + {campaign.supporters?.length || 0} Supporters +
+
+
+
-
- - - {/* Meta Info */} - - -
-
- +
+
+

Donors

+

{campaign.supporters?.length || 0}

+
+
+

Days Left

+

{daysLeft}

+
-
-

Verified Organizer

-

{campaign.organizer || "TrustBridge Admin"}

-
-
+ + -
-
- + {/* Meta Info */} + + +
+
+ +
+
+

Verified Organizer

+

{campaign.organizer || "TrustBridge Admin"}

+
-
-

Timeline

-

- {new Date(campaign.startDate).toLocaleDateString()} - {new Date(campaign.endDate).toLocaleDateString()} -

+ +
+
+ +
+
+

Timeline

+

+ {new Date(campaign.startDate).toLocaleDateString()} - {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. -

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

+ + +
-
- setIsShareModalOpen(false)} - title={campaign.title} - url={window.location.href} - theme="light" - /> + setIsShareModalOpen(false)} + title={campaign.title} + url={window.location.href} + theme="light" + /> +
{/* Donation Form Modal */} {showDonationForm && campaign && ( -
-
+
+
{/* Header */}
@@ -354,7 +356,7 @@ export const DashboardCampaignDetail: React.FC = () => {
- { @@ -368,6 +370,6 @@ export const DashboardCampaignDetail: React.FC = () => {
)} -
+ ); }; diff --git a/src/pages/dashCamp/DashboardCampaigns.tsx b/src/pages/dashCamp/DashboardCampaigns.tsx index 4f3a45d6f..d20c59b78 100644 --- a/src/pages/dashCamp/DashboardCampaigns.tsx +++ b/src/pages/dashCamp/DashboardCampaigns.tsx @@ -7,7 +7,7 @@ import toast from "react-hot-toast"; import { Rocket, Clock, Target, TrendingUp, X } from "lucide-react"; import { loadStripe } from "@stripe/stripe-js"; import { Elements } from "@stripe/react-stripe-js"; -import { StripeDonationForm } from "../../components/camp/StripeDonationForm"; +import { StripeDashboardPaymentForm } from "../../components/dashboard/StripeDashboardPaymentForm"; const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY); @@ -147,7 +147,7 @@ export const DashboardCampaigns: React.FC = () => {
- {categories.map((cat) => ( +
- ))} + {categories.map((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.`} +

+ + + ) : ( +
+ {filteredCampaigns.map((campaign) => ( + + ))} +
+ )} +
- {loading ? ( -
-
-
- ) : filteredCampaigns.length === 0 ? ( - -
- -
-

No campaigns found

-

- {selectedCategory === "All" - ? "There are no active campaigns at the moment." - : `No active campaigns in the ${selectedCategory} category.`} -

- -
- ) : ( -
- {filteredCampaigns.map((campaign) => ( - - ))} -
- )} {/* Donation Form Modal */} {showDonationForm && selectedCampaign && ( -
-
+
+
{/* Header */}
@@ -308,10 +312,10 @@ export const DashboardCampaigns: React.FC = () => { key={amount} type="button" onClick={() => setDonationAmount(amount)} - className={`py - 3 rounded - xl font - bold transition - all text - sm ${donationAmount === 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} @@ -330,7 +334,7 @@ export const DashboardCampaigns: React.FC = () => {
- { @@ -344,6 +348,6 @@ export const DashboardCampaigns: React.FC = () => {
)} -
+ ); }; diff --git a/src/pages/dashboard/AdminDashboard.tsx b/src/pages/dashboard/AdminDashboard.tsx index 09f155ae2..a08be2619 100644 --- a/src/pages/dashboard/AdminDashboard.tsx +++ b/src/pages/dashboard/AdminDashboard.tsx @@ -45,34 +45,34 @@ 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' || 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, @@ -81,7 +81,7 @@ export const AdminDashboard: React.FC = () => { suspendedUsers: suspendedCount, blockedUsers: blockedCount }); - + } catch (error) { console.error("Error fetching admin stats:", error); } From 8eb13708bd5cec9f07d3e0c4e842ff4ee5872956 Mon Sep 17 00:00:00 2001 From: mzain4321 Date: Fri, 16 Jan 2026 20:47:28 +0500 Subject: [PATCH 03/14] stripe added --- dev-dist/sw.js | 2 +- src/components/camp/CampForm.tsx | 6 +++++- src/pages/campaignPage/CampaignDetailPage.tsx | 5 +++-- src/pages/campaignPage/CampaignPage.tsx | 6 ++++-- src/pages/dashCamp/DashboardCampaignDetail.tsx | 7 ++++--- src/pages/dashCamp/DashboardCampaigns.tsx | 6 ++++-- src/pages/fundraises/Fundraises-Page.tsx | 2 +- 7 files changed, 22 insertions(+), 12 deletions(-) diff --git a/dev-dist/sw.js b/dev-dist/sw.js index ad59b3b28..94e479efb 100644 --- a/dev-dist/sw.js +++ b/dev-dist/sw.js @@ -82,7 +82,7 @@ define(['./workbox-d70286d7'], (function (workbox) { 'use strict'; "revision": "3ca0b8505b4bec776b69afdba2768812" }, { "url": "index.html", - "revision": "0.0ekk4lc4ck8" + "revision": "0.7nsb4og9618" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { diff --git a/src/components/camp/CampForm.tsx b/src/components/camp/CampForm.tsx index b5366ec1e..41d372ef4 100644 --- a/src/components/camp/CampForm.tsx +++ b/src/components/camp/CampForm.tsx @@ -222,7 +222,11 @@ const CampForm: React.FC = ({ onSuccess, initialData }) => { setLoading(true); try { const data = new FormData(); - Object.entries(formData).forEach(([k, v]) => data.append(k, v)); + Object.entries(formData).forEach(([k, v]) => { + if (k === "endDate" && (formData.isLifetime || !v)) return; + // @ts-ignore + data.append(k, v); + }); images.forEach((file) => data.append("images", file)); existingImages.forEach((url) => data.append("existingImages", url)); diff --git a/src/pages/campaignPage/CampaignDetailPage.tsx b/src/pages/campaignPage/CampaignDetailPage.tsx index 4f8a7bdea..c06cdf802 100644 --- a/src/pages/campaignPage/CampaignDetailPage.tsx +++ b/src/pages/campaignPage/CampaignDetailPage.tsx @@ -25,6 +25,7 @@ interface Campaign { endDate: string; startDate: string; status: string; + isLifetime?: boolean; supporters?: Array<{ supporterId: string; amount: number; @@ -273,7 +274,7 @@ export const CampaignDetailPage: React.FC = () => { {campaign.category} - ⏳ {daysLeft} days left + {campaign.isLifetime ? "⏳ Lifetime" : `⏳ ${daysLeft} days left`}
@@ -241,7 +242,7 @@ export const DashboardCampaignDetail: React.FC = () => {

Days Left

-

{daysLeft}

+

{campaign.isLifetime ? "Lifetime" : daysLeft}

@@ -267,7 +268,7 @@ export const DashboardCampaignDetail: React.FC = () => {

Timeline

- {new Date(campaign.startDate).toLocaleDateString()} - {new Date(campaign.endDate).toLocaleDateString()} + {new Date(campaign.startDate).toLocaleDateString()} - {campaign.isLifetime ? "Lifetime" : new Date(campaign.endDate).toLocaleDateString()}

diff --git a/src/pages/dashCamp/DashboardCampaigns.tsx b/src/pages/dashCamp/DashboardCampaigns.tsx index d20c59b78..3708c61b7 100644 --- a/src/pages/dashCamp/DashboardCampaigns.tsx +++ b/src/pages/dashCamp/DashboardCampaigns.tsx @@ -22,6 +22,7 @@ interface CampaignCardProps { organizer?: string; endDate: string; status: string; + isLifetime?: boolean; supporters?: Array<{ supporterId: string; amount: number; @@ -86,7 +87,8 @@ export const DashboardCampaigns: React.FC = () => { raisedAmount, images, organizer, - endDate + endDate, + isLifetime }) => { const progress = (raisedAmount / goalAmount) * 100; const daysLeft = calculateDaysLeft(endDate); @@ -108,7 +110,7 @@ export const DashboardCampaigns: React.FC = () => {
- {daysLeft} days left + {isLifetime ? "Lifetime" : `${daysLeft} days left`}
diff --git a/src/pages/fundraises/Fundraises-Page.tsx b/src/pages/fundraises/Fundraises-Page.tsx index 27367942a..c90da05ff 100644 --- a/src/pages/fundraises/Fundraises-Page.tsx +++ b/src/pages/fundraises/Fundraises-Page.tsx @@ -482,7 +482,7 @@ export const FundraisePage: React.FC = () => { <> From 06c49180e9adf1700417b82998d169c14a20f858 Mon Sep 17 00:00:00 2001 From: mzain4321 Date: Sat, 17 Jan 2026 14:45:40 +0500 Subject: [PATCH 04/14] team add enter --- dev-dist/sw.js | 2 +- src/App.tsx | 10 + src/data/collaborationRequests.ts | 7 +- src/data/users.ts | 54 +++ src/pages/dashboard/EntrepreneurDashboard.tsx | 23 +- src/pages/dashboard/EntrepreneurRequests.tsx | 127 ++++++ src/pages/dashboard/ManageTeam.tsx | 370 ++++++++++++++++++ src/pages/profile/EntrepreneurProfile.tsx | 237 +++++++---- src/types/index.ts | 8 + 9 files changed, 753 insertions(+), 85 deletions(-) create mode 100644 src/pages/dashboard/EntrepreneurRequests.tsx create mode 100644 src/pages/dashboard/ManageTeam.tsx diff --git a/dev-dist/sw.js b/dev-dist/sw.js index 94e479efb..250de279e 100644 --- a/dev-dist/sw.js +++ b/dev-dist/sw.js @@ -82,7 +82,7 @@ define(['./workbox-d70286d7'], (function (workbox) { 'use strict'; "revision": "3ca0b8505b4bec776b69afdba2768812" }, { "url": "index.html", - "revision": "0.7nsb4og9618" + "revision": "0.neke7ha19ac" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { diff --git a/src/App.tsx b/src/App.tsx index d778d984a..d3da45bba 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,6 +17,8 @@ import { RegisterPage } from "./pages/auth/RegisterPage"; // Dashboard Pages import { EntrepreneurDashboard } from "./pages/dashboard/EntrepreneurDashboard"; +import { EntrepreneurRequests } from "./pages/dashboard/EntrepreneurRequests"; +import { ManageTeam } from "./pages/dashboard/ManageTeam"; import { InvestorDashboard } from "./pages/dashboard/InvestorDashboard"; // Profile Pages @@ -90,6 +92,14 @@ function App() { {/* Dashboard Routes */} }> } /> + } + /> + } + /> } /> } /> } /> diff --git a/src/data/collaborationRequests.ts b/src/data/collaborationRequests.ts index a5a0eb641..389cc5110 100644 --- a/src/data/collaborationRequests.ts +++ b/src/data/collaborationRequests.ts @@ -34,7 +34,7 @@ export const getRequestsFromInvestor = async ( export const checkRequestsFromInvestor = async ( inves_id: string | undefined, enter_id: string | undefined -): Promise => { +): Promise => { try { const body = { inves_id, enter_id }; const res = await axios.post( @@ -45,10 +45,11 @@ export const checkRequestsFromInvestor = async ( const { request } = res.data; console.log(request); - return request.requestStatus; // true if found, false otherwise + if (!request) return null; + return request.requestStatus; } catch (error) { console.error("checkRequestsFromInvestor error:", error); - return "pending"; + return null; } }; diff --git a/src/data/users.ts b/src/data/users.ts index f7e96997d..8182c62cf 100644 --- a/src/data/users.ts +++ b/src/data/users.ts @@ -164,3 +164,57 @@ export const AmountMeasureWithTags = (amount: number) => { } return "0"; }; +export const addTeamMember = async (userId: string, formData: FormData) => { + try { + const res = await axios.post( + `${URL}/entrepreneur/add-team-member/${userId}`, + formData, + { + withCredentials: true, + headers: { "Content-Type": "multipart/form-data" }, + } + ); + toast.success("Team member added successfully"); + return res.data; + } catch (err) { + console.error(err); + toast.error("Failed to add team member"); + throw err; + } +}; + +export const updateTeamMember = async (userId: string, memberId: string, formData: FormData) => { + try { + const res = await axios.put( + `${URL}/entrepreneur/update-team-member/${userId}/${memberId}`, + formData, + { + withCredentials: true, + headers: { "Content-Type": "multipart/form-data" }, + } + ); + toast.success("Team member updated successfully"); + return res.data; + } catch (err) { + console.error(err); + toast.error("Failed to update team member"); + throw err; + } +}; + +export const deleteTeamMember = async (userId: string, memberId: string) => { + try { + const res = await axios.delete( + `${URL}/entrepreneur/delete-team-member/${userId}/${memberId}`, + { + withCredentials: true, + } + ); + toast.success("Team member removed successfully"); + return res.data; + } catch (err) { + console.error(err); + toast.error("Failed to remove team member"); + throw err; + } +}; 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 = () => {

- - - +
+ + + + + + +
{/* 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" ? ( + <> + + + + ) : ( +
+ {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 ( +
+
+
+ + + +

Manage Team

+
+ +
+ +
+ {entrepreneur?.team && entrepreneur.team.length > 0 ? ( + entrepreneur.team.map((member) => ( + + +
+
+ +
+

{member.name}

+
+ {member.role.map((r, idx) => ( + + {r} + + ))} +
+
+
+
+ + +
+
+
+
+ )) + ) : ( +
+

No team members added yet.

+ +
+ )} +
+ + {/* Modal */} + {isModalOpen && ( +
+
+ + +

+ {editingMember ? "Edit Team Member" : "Add Team Member"} +

+ +
+ {/* Image Upload */} +
+
+ + +
+ +

Click icon to upload photo

+
+ +
+ + setName(e.target.value)} + className="w-full border border-gray-300 rounded-md p-2 focus:ring-2 focus:ring-primary-500 focus:border-primary-500 outline-none transition-all" + placeholder="e.g. John Doe" + /> +
+ +
+ +
+ setRoles(e.target.value)} + onClick={() => setShowDropdown(true)} + className="w-full border border-gray-300 rounded-md p-2 pr-10 focus:ring-2 focus:ring-primary-500 focus:border-primary-500 outline-none transition-all" + placeholder="e.g. CEO, CTO or type custom role" + /> + +
+ + {/* Selected roles preview */} + {roles && ( +
+ {roles.split(",").map(r => r.trim()).filter(r => r !== "").map((role, idx) => ( + + {role} + + + ))} +
+ )} + + {/* Dropdown */} + {showDropdown && ( +
+
+

Predefined Roles

+
+ {predefinedRoles.map((role, index) => { + const isSelected = roles.split(",").map(r => r.trim()).includes(role); + return ( +
handleRoleSelect(role)} + className={`px-3 py-2 cursor-pointer hover:bg-gray-50 flex items-center justify-between ${isSelected ? 'bg-primary-50' : ''}`} + > + + {role} + + {isSelected && ( + + ✓ + + )} +
+ ); + })} +
+

Click roles to select/deselect. You can also type custom roles.

+
+
+ )} +
+ +
+ + +
+
+
+
+ )} +
+ ); +}; \ No newline at end of file diff --git a/src/pages/profile/EntrepreneurProfile.tsx b/src/pages/profile/EntrepreneurProfile.tsx index f50301ae5..2ea1a5b3f 100644 --- a/src/pages/profile/EntrepreneurProfile.tsx +++ b/src/pages/profile/EntrepreneurProfile.tsx @@ -10,6 +10,9 @@ import { FileText, DollarSign, Send, + Check, + X, + Clock, } from "lucide-react"; import { Avatar } from "../../components/ui/Avatar"; import { Button } from "../../components/ui/Button"; @@ -31,8 +34,9 @@ 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 [isSuspendModalOpen, setIsSuspendModalOpen] = useState(false); @@ -80,7 +84,10 @@ 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(); @@ -108,6 +115,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 +130,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"); } }; @@ -217,31 +228,44 @@ export const EntrepreneurProfile: React.FC = ({ userId }) => { {!isAdmin ? ( !isCurrentUser && ( <> - - - + {(!isInvestor || requestStatus === "accepted") && ( + + + + )} {isInvestor && ( )} ) ) : ( - // Admin Actions + // Admin Actions ... (omitted for brevity, assume unchanged context)
+ {/* ... admin buttons ... */} {entrepreneur.isSuspended ? (
)} - {hasRequestedCollaboration && ( + {isInvestor && requestStatus === "accepted" && ( + + )} +
-
- -
-

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

+
+ )}
@@ -717,13 +808,13 @@ export const EntrepreneurProfile: React.FC = ({ userId }) => { sending a collaboration request.

- {!hasRequestedCollaboration ? ( + {requestStatus !== "pending" && requestStatus !== "accepted" ? ( ) : ( )}
diff --git a/src/types/index.ts b/src/types/index.ts index f08e9817e..480b66de6 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,6 +28,7 @@ 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; From 445eb3547391cde9d971f8651e6a6c23f826c23f Mon Sep 17 00:00:00 2001 From: mzain4321 Date: Sat, 17 Jan 2026 19:42:07 +0500 Subject: [PATCH 05/14] can add cards --- dev-dist/sw.js | 2 +- .../dashboard/StripeDashboardPaymentForm.tsx | 345 ++++++++++-- src/components/settings/BillingSettings.tsx | 526 +++++++++++++++--- 3 files changed, 769 insertions(+), 104 deletions(-) diff --git a/dev-dist/sw.js b/dev-dist/sw.js index 250de279e..738e22c81 100644 --- a/dev-dist/sw.js +++ b/dev-dist/sw.js @@ -82,7 +82,7 @@ define(['./workbox-d70286d7'], (function (workbox) { 'use strict'; "revision": "3ca0b8505b4bec776b69afdba2768812" }, { "url": "index.html", - "revision": "0.neke7ha19ac" + "revision": "0.uuk74r9hf78" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { diff --git a/src/components/dashboard/StripeDashboardPaymentForm.tsx b/src/components/dashboard/StripeDashboardPaymentForm.tsx index 30bdfb439..b24ee8110 100644 --- a/src/components/dashboard/StripeDashboardPaymentForm.tsx +++ b/src/components/dashboard/StripeDashboardPaymentForm.tsx @@ -1,9 +1,10 @@ -import React, { useState } from 'react'; -import { useStripe, useElements, CardElement } from '@stripe/react-stripe-js'; +import React, { useState, useEffect, useRef } from 'react'; +import { useStripe, useElements, CardElement, CardNumberElement, CardExpiryElement, CardCvcElement } from '@stripe/react-stripe-js'; import axios from 'axios'; import toast from "react-hot-toast"; import { Button } from '../ui/Button'; import { useAuth } from '../../context/AuthContext'; +import { CreditCard } from 'lucide-react'; interface StripeDashboardPaymentFormProps { amount: number; @@ -13,6 +14,15 @@ interface StripeDashboardPaymentFormProps { onCancel: () => void; } +interface DefaultCard { + id: string; + cardNumber: string; + cardholderName: string; + expiryMonth: string; + expiryYear: string; + cvv: string; +} + export const StripeDashboardPaymentForm: React.FC = ({ amount, paymentType, @@ -24,13 +34,69 @@ export const StripeDashboardPaymentForm: React.FC(null); + const [isLoadingCard, setIsLoadingCard] = useState(false); + const [useDefaultCard, setUseDefaultCard] = useState(false); + const [cvvError, setCvvError] = useState(null); + const cardNumberElementRef = useRef(null); + const cardExpiryElementRef = useRef(null); const URL = import.meta.env.VITE_BACKEND_URL; + // Fetch default card when component mounts (only for campaign contributions) + useEffect(() => { + const fetchDefaultCard = async () => { + if (!campaignId || !user) return; + + // Only for entrepreneur or investor roles + if (user.role !== 'entrepreneur' && user.role !== 'investor') return; + + try { + setIsLoadingCard(true); + const token = localStorage.getItem('token'); + const response = await axios.get(`${URL}/user/cards`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + const cards = response.data.cards || []; + const defaultCardData = cards.find((card: any) => card.isDefault); + + if (defaultCardData) { + // Fetch full card details + const cardResponse = await axios.get(`${URL}/user/cards/${defaultCardData.id}`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + const fullCard = cardResponse.data.card; + setDefaultCard({ + id: fullCard.id, + cardNumber: fullCard.cardNumber, + cardholderName: fullCard.cardholderName, + expiryMonth: fullCard.expiryMonth, + expiryYear: fullCard.expiryYear, + cvv: '', // CVV is never stored + }); + setUseDefaultCard(true); + } + } catch (error: any) { + console.error('Error fetching default card:', error); + // Silently fail - user can still use manual entry + } finally { + setIsLoadingCard(false); + } + }; + + fetchDefaultCard(); + }, [campaignId, user, URL]); + const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); - if (!stripe || !elements) { + if (!stripe) { return; } @@ -39,6 +105,20 @@ export const StripeDashboardPaymentForm: React.FC { + const cleaned = cardNumber.replace(/\s/g, ''); + if (cleaned.length < 4) return cardNumber; + const last4 = cleaned.slice(-4); + return `•••• •••• •••• ${last4}`; + }; + return ( -
+ + {isLoadingCard ? ( +
Loading card information...
+ ) : useDefaultCard && defaultCard ? ( + // Show default card with CVV input +
+
+
+
+ +
+
+

Default Card

+

{defaultCard.cardholderName}

+
+
+ +
+ {/* Display card info as read-only reference */} +
+

Your Default Card

+

{formatCardNumber(defaultCard.cardNumber)}

+

Expires: {defaultCard.expiryMonth}/{defaultCard.expiryYear}

+
+ + {/* Stripe Elements for card details - user needs to enter to match displayed card */} +
+
+ +
+ +
+
+ +
+
+ +
+ +
+
+ +
+ +
+ { + setCvvError(e.error?.message || null); + }} + /> +
+ {cvvError && ( +

{cvvError}

+ )} +
+
+
+ +

+ Please enter your card details above to match your default card, or use browser autofill. +

+
+
+ + +
+ ) : ( + // Show manual card entry
+ )} -
- - -
+
+ + +
-

- Secure payment powered by Stripe -

-
- ); +

+ Secure payment powered by Stripe +

+ +); }; diff --git a/src/components/settings/BillingSettings.tsx b/src/components/settings/BillingSettings.tsx index 652fb5333..dd5c18639 100644 --- a/src/components/settings/BillingSettings.tsx +++ b/src/components/settings/BillingSettings.tsx @@ -1,15 +1,20 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { Card, CardHeader, CardBody } from "../ui/Card"; import { Button } from "../ui/Button"; import { Badge } from "../ui/Badge"; -import { CreditCard, Download } from "lucide-react"; +import { Input } from "../ui/Input"; +import { CreditCard, Download, X, Edit } from "lucide-react"; +import { useAuth } from "../../context/AuthContext"; +import axios from "axios"; +import toast from "react-hot-toast"; interface PaymentMethod { id: string; - type: string; + cardNumber: string; last4: string; - expiryMonth: number; - expiryYear: number; + cardholderName: string; + expiryMonth: string; + expiryYear: string; isDefault: boolean; } @@ -21,25 +26,31 @@ interface Invoice { description: string; } +interface CardFormData { + cardNumber: string; + cardholderName: string; + cvv: string; + expiryMonth: string; + expiryYear: string; + isDefault: boolean; +} + export const BillingSettings: React.FC = () => { - const [paymentMethods, setPaymentMethods] = useState([ - { - id: "1", - type: "Visa", - last4: "4242", - expiryMonth: 12, - expiryYear: 2025, - isDefault: true, - }, - { - id: "2", - type: "Mastercard", - last4: "8888", - expiryMonth: 6, - expiryYear: 2026, - isDefault: false, - }, - ]); + const { user } = useAuth(); + const URL = import.meta.env.VITE_BACKEND_URL; + const [paymentMethods, setPaymentMethods] = useState([]); + const [isModalOpen, setIsModalOpen] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [isFetching, setIsFetching] = useState(true); + const [editingCardId, setEditingCardId] = useState(null); + const [formData, setFormData] = useState({ + cardNumber: "", + cardholderName: "", + cvv: "", + expiryMonth: "", + expiryYear: "", + isDefault: false, + }); const [invoices] = useState([ { @@ -65,17 +76,244 @@ export const BillingSettings: React.FC = () => { }, ]); - const handleSetDefault = (id: string) => { - setPaymentMethods((prev) => - prev.map((method) => ({ - ...method, - isDefault: method.id === id, - })) - ); + // Fetch cards from backend + useEffect(() => { + const fetchCards = async () => { + try { + setIsFetching(true); + const token = localStorage.getItem("token"); + const response = await axios.get(`${URL}/user/cards`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + setPaymentMethods(response.data.cards || []); + } catch (error: any) { + console.error("Error fetching cards:", error); + if (error.response?.status !== 404) { + toast.error("Failed to fetch cards"); + } + } finally { + setIsFetching(false); + } + }; + + fetchCards(); + }, [URL]); + + const handleSetDefault = async (id: string) => { + try { + const token = localStorage.getItem("token"); + await axios.patch( + `${URL}/user/cards/${id}/set-default`, + {}, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + + // Update local state + setPaymentMethods((prev) => + prev.map((method) => ({ + ...method, + isDefault: method.id === id, + })) + ); + toast.success("Card set as default successfully"); + } catch (error: any) { + console.error("Error setting default card:", error); + toast.error(error.response?.data?.message || "Failed to set default card"); + } }; - const handleRemoveCard = (id: string) => { - setPaymentMethods((prev) => prev.filter((method) => method.id !== id)); + const handleEditCard = async (id: string) => { + try { + setIsLoading(true); + const token = localStorage.getItem("token"); + const response = await axios.get(`${URL}/user/cards/${id}`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + const card = response.data.card; + setFormData({ + cardNumber: card.cardNumber, + cardholderName: card.cardholderName, + cvv: card.cvv, + expiryMonth: card.expiryMonth, + expiryYear: card.expiryYear, + isDefault: card.isDefault, + }); + setEditingCardId(id); + setIsModalOpen(true); + } catch (error: any) { + console.error("Error fetching card for edit:", error); + toast.error(error.response?.data?.message || "Failed to load card details"); + } finally { + setIsLoading(false); + } + }; + + const handleRemoveCard = async (id: string) => { + if (!window.confirm("Are you sure you want to remove this card?")) { + return; + } + + try { + const token = localStorage.getItem("token"); + await axios.delete(`${URL}/user/cards/${id}`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + setPaymentMethods((prev) => prev.filter((method) => method.id !== id)); + toast.success("Card removed successfully"); + } catch (error: any) { + console.error("Error removing card:", error); + toast.error(error.response?.data?.message || "Failed to remove card"); + } + }; + + const handleAddCard = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!formData.cardNumber || !formData.cardholderName || !formData.cvv || !formData.expiryMonth || !formData.expiryYear) { + toast.error("Please fill in all fields"); + return; + } + + // Validate card number (should be 16 digits) + const cardNumberDigits = formData.cardNumber.replace(/\s/g, ""); + if (cardNumberDigits.length !== 16 || !/^\d+$/.test(cardNumberDigits)) { + toast.error("Please enter a valid 16-digit card number"); + return; + } + + // Validate CVV (should be 3-4 digits) + if (!/^\d{3,4}$/.test(formData.cvv)) { + toast.error("Please enter a valid CVV (3-4 digits)"); + return; + } + + // Validate expiry month (01-12) + if (!/^(0[1-9]|1[0-2])$/.test(formData.expiryMonth)) { + toast.error("Please enter a valid month (01-12)"); + return; + } + + // Validate expiry year (should be 2 digits) + if (!/^\d{2}$/.test(formData.expiryYear)) { + toast.error("Please enter a valid year (2 digits)"); + return; + } + + try { + setIsLoading(true); + const token = localStorage.getItem("token"); + + if (editingCardId) { + // Update existing card + await axios.put( + `${URL}/user/cards/${editingCardId}`, + { + cardNumber: cardNumberDigits, + cardholderName: formData.cardholderName, + cvv: formData.cvv, + expiryMonth: formData.expiryMonth, + expiryYear: formData.expiryYear, + isDefault: formData.isDefault, + }, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + toast.success("Card updated successfully"); + } else { + // Add new card + await axios.post( + `${URL}/user/cards`, + { + cardNumber: cardNumberDigits, + cardholderName: formData.cardholderName, + cvv: formData.cvv, + expiryMonth: formData.expiryMonth, + expiryYear: formData.expiryYear, + isDefault: formData.isDefault, + }, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + toast.success("Card added successfully"); + } + + // Refresh cards list + const response = await axios.get(`${URL}/user/cards`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + setPaymentMethods(response.data.cards || []); + + // Reset form and close modal + setFormData({ + cardNumber: "", + cardholderName: "", + cvv: "", + expiryMonth: "", + expiryYear: "", + isDefault: false, + }); + setEditingCardId(null); + setIsModalOpen(false); + } catch (error: any) { + console.error("Error saving card:", error); + toast.error(error.response?.data?.message || `Failed to ${editingCardId ? "update" : "add"} card`); + } finally { + setIsLoading(false); + } + }; + + const handleCardNumberChange = (e: React.ChangeEvent) => { + let value = e.target.value.replace(/\s/g, ""); + if (value.length > 16) value = value.slice(0, 16); + // Add spaces every 4 digits + value = value.replace(/(.{4})/g, "$1 ").trim(); + setFormData({ ...formData, cardNumber: value }); + }; + + const handleOpenAddModal = () => { + setEditingCardId(null); + setFormData({ + cardNumber: "", + cardholderName: "", + cvv: "", + expiryMonth: "", + expiryYear: "", + isDefault: false, + }); + setIsModalOpen(true); + }; + + const handleCloseModal = () => { + setIsModalOpen(false); + setEditingCardId(null); + setFormData({ + cardNumber: "", + cardholderName: "", + cvv: "", + expiryMonth: "", + expiryYear: "", + isDefault: false, + }); }; const handleDownloadInvoice = (invoiceId: string) => { @@ -90,59 +328,213 @@ export const BillingSettings: React.FC = () => {

Payment Methods

-
-
- {paymentMethods.map((method) => ( -
-
-
- -
-
-
-

- {method.type} •••• {method.last4} -

- {method.isDefault && ( - Default - )} + {isFetching ? ( +
Loading cards...
+ ) : paymentMethods.length === 0 ? ( +
No cards added yet
+ ) : ( +
+ {paymentMethods.map((method) => ( +
+
+
+ +
+
+
+

+ {method.cardNumber} +

+ {method.isDefault && ( + Default + )} +
+

+ {method.cardholderName} • Expires {method.expiryMonth}/{method.expiryYear} +

-

- Expires {method.expiryMonth}/{method.expiryYear} -

-
-
- {!method.isDefault && ( +
+ + {!method.isDefault && ( + + )} - )} +
+
+ ))} +
+ )} + + + + {/* Add/Edit Card Modal */} + {isModalOpen && ( +
{ + if (e.target === e.currentTarget) { + handleCloseModal(); + } + }} + > +
+
+
+

+ {editingCardId ? "Edit Card" : "Add New Card"} +

+ +
+ +
+
+ +
+ +
+ + setFormData({ ...formData, cardholderName: e.target.value }) + } + fullWidth + required + /> +
+ +
+
+ { + const value = e.target.value.replace(/\D/g, "").slice(0, 4); + setFormData({ ...formData, cvv: value }); + }} + maxLength={4} + fullWidth + required + /> +
+
+ { + const value = e.target.value.replace(/\D/g, "").slice(0, 2); + setFormData({ ...formData, expiryMonth: value }); + }} + maxLength={2} + fullWidth + required + /> +
+
+ { + const value = e.target.value.replace(/\D/g, "").slice(0, 2); + setFormData({ ...formData, expiryYear: value }); + }} + maxLength={2} + fullWidth + required + /> +
+
+ +
+ + setFormData({ ...formData, isDefault: e.target.checked }) + } + className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded" + /> + +
+ +
+
-
- ))} + +
- - +
+ )} From 2c02af8f9e2053e08d2ed2f0d315508e040afbf8 Mon Sep 17 00:00:00 2001 From: mzain4321 Date: Sun, 18 Jan 2026 22:20:16 +0500 Subject: [PATCH 06/14] deal doen --- dev-dist/sw.js | 2 +- src/App.tsx | 9 +- src/components/DealForm.tsx | 403 ++++++++++++++++++++++ src/components/DealPaymentModal.tsx | 179 ++++++++++ src/components/NegotiationModal.tsx | 116 +++++++ src/components/layout/Sidebar.tsx | 10 + src/pages/admin/AdminDealPayments.tsx | 102 ++++++ src/pages/admin/AdminDealsPage.tsx | 168 +++++++++ src/pages/deals/DealsPage.tsx | 236 +++++++++++-- src/pages/profile/EntrepreneurProfile.tsx | 133 ++----- src/pages/viewdeals/ViewDeal.tsx | 213 ++++++++---- src/types/index.ts | 4 + 12 files changed, 1369 insertions(+), 206 deletions(-) create mode 100644 src/components/DealForm.tsx create mode 100644 src/components/DealPaymentModal.tsx create mode 100644 src/components/NegotiationModal.tsx create mode 100644 src/pages/admin/AdminDealPayments.tsx create mode 100644 src/pages/admin/AdminDealsPage.tsx diff --git a/dev-dist/sw.js b/dev-dist/sw.js index 738e22c81..7aa03caec 100644 --- a/dev-dist/sw.js +++ b/dev-dist/sw.js @@ -82,7 +82,7 @@ define(['./workbox-d70286d7'], (function (workbox) { 'use strict'; "revision": "3ca0b8505b4bec776b69afdba2768812" }, { "url": "index.html", - "revision": "0.uuk74r9hf78" + "revision": "0.dtk1f065j7s" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { diff --git a/src/App.tsx b/src/App.tsx index d3da45bba..b563bb75a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -68,6 +68,8 @@ import { CommunityGuidelines } from "./pages/legal/CommunityGuidelines"; import SendMassNotification from "./pages/admin/SendMassNotification"; import { DashboardCampaigns } from "./pages/dashCamp/DashboardCampaigns"; import { DashboardCampaignDetail } from "./pages/dashCamp/DashboardCampaignDetail"; +import { AdminDealsPage } from "./pages/admin/AdminDealsPage"; +import { AdminDealPayments } from "./pages/admin/AdminDealPayments"; function App() { return ( @@ -113,11 +115,10 @@ function App() { } /> } /> } /> - } - /> + } /> } /> + } /> + } /> }> diff --git a/src/components/DealForm.tsx b/src/components/DealForm.tsx new file mode 100644 index 000000000..a2d010430 --- /dev/null +++ b/src/components/DealForm.tsx @@ -0,0 +1,403 @@ +import React, { useState, useEffect } from "react"; +import axios from "axios"; +import { Button } from "./ui/Button"; +import { X } from "lucide-react"; +import toast from "react-hot-toast"; + +const URL = import.meta.env.VITE_BACKEND_URL; + +interface DealFormProps { + entrepreneur: any; + investor: any; + onClose: () => void; + valuation: number; + readOnly?: boolean; + initialData?: any; + isEmbedded?: boolean; + onSubmit?: (data: any) => void; +} + +export const DealForm: React.FC = ({ + entrepreneur, + investor, + onClose, + valuation, + readOnly = false, + initialData, + isEmbedded = false, + onSubmit, +}) => { + const [formData, setFormData] = useState({ + investmentAmount: initialData?.investmentAmount || "", + equityOffered: initialData?.equityOffered || "", + preMoneyValuation: initialData?.preMoneyValuation || valuation || 0, + postMoneyValuation: initialData?.postMoneyValuation || 0, + investmentType: initialData?.investmentType || "Equity", + boardSeat: initialData?.boardSeat || "No", + votingRights: initialData?.votingRights || "None", + dividends: initialData?.dividends || "On Exit Only", + rofr: initialData?.rofr || "No", + exitStrategy: initialData?.exitStrategy || "Acquisition", + exitTimeline: initialData?.exitTimeline || "3-5 years", + exitTimeline: initialData?.exitTimeline || "3-5 years", + additionalTerms: initialData?.additionalTerms || "", + stage: initialData?.stage || "Seed", // Default + }); + + // Auto-calculate Post-Money Valuation + useEffect(() => { + const amount = Number(formData.investmentAmount) || 0; + const preMoney = Number(formData.preMoneyValuation) || 0; + setFormData((prev) => ({ + ...prev, + postMoneyValuation: preMoney + amount, + })); + }, [formData.investmentAmount, formData.preMoneyValuation]); + + const handleChange = ( + e: React.ChangeEvent< + HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement + > + ) => { + const { name, value } = e.target; + setFormData((prev) => ({ ...prev, [name]: value })); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + const payload = { + investorId: investor.userId, + entrepreneurId: entrepreneur.userId, + ...formData, + investmentAmount: Number(formData.investmentAmount), + equityOffered: Number(formData.equityOffered), + preMoneyValuation: Number(formData.preMoneyValuation), + postMoneyValuation: Number(formData.postMoneyValuation), + }; + + if (onSubmit) { + onSubmit(payload); + return; + } + + try { + await axios.post(`${URL}/deal/create-deal`, payload, { + withCredentials: true, + }); + + toast.success("Deal proposal sent successfully!"); + onClose(); + } catch (error) { + console.error(error); + toast.error("Failed to send deal proposal."); + } + }; + + const content = ( +
+ {!isEmbedded && ( + + )} +

+ {readOnly ? (isEmbedded ? "Current Deal Terms" : "View Deal Proposal") : (isEmbedded ? "Proposed Counter Offer" : "Startup Investment Deal Form")} +

+ +
+ {/* ... form content ... */} + {/* Section 1: Investor Details */} +
+

+ Section 1: Investor Details +

+
+
+ + +
+
+ + +
+
+
+ + {/* Section 2: Entrepreneur Details */} +
+

+ Section 2: Entrepreneur Details +

+
+
+ + +
+
+ + +
+
+
+ + {/* Section 3: Investment Terms */} +
+

+ Section 3: Investment Terms +

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + {/* Section 4: Investor Rights & Preferences */} +
+

+ Section 4: Investor Rights & Preferences +

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + {/* Section 5: Exit Strategy */} +
+

+ Section 5: Exit Strategy +

+
+
+ + +
+
+ + +
+
+
+ + {/* Additional Terms */} +
+ +