From 44f2be5d84a859d9723e0d202d1594f932768bcc Mon Sep 17 00:00:00 2001 From: Koncept Kit <63216427+konceptkit@users.noreply.github.com> Date: Thu, 11 Dec 2025 23:14:23 +0700 Subject: [PATCH] Donation page update and Subscription update on Admin Dashboard --- src/App.js | 16 +- src/components/AdminSidebar.js | 9 +- src/pages/Donate.js | 146 +++++++- src/pages/DonationSuccess.js | 102 ++++++ src/pages/Plans.js | 95 ++++- src/pages/admin/AdminSubscriptions.js | 506 ++++++++++++++++++++++++++ 6 files changed, 858 insertions(+), 16 deletions(-) create mode 100644 src/pages/DonationSuccess.js create mode 100644 src/pages/admin/AdminSubscriptions.js diff --git a/src/App.js b/src/App.js index c48718b..928ac78 100644 --- a/src/App.js +++ b/src/App.js @@ -24,6 +24,7 @@ import AdminMembers from './pages/admin/AdminMembers'; import AdminEvents from './pages/admin/AdminEvents'; import AdminApprovals from './pages/admin/AdminApprovals'; import AdminPlans from './pages/admin/AdminPlans'; +import AdminSubscriptions from './pages/admin/AdminSubscriptions'; import AdminLayout from './layouts/AdminLayout'; import { AuthProvider, useAuth } from './context/AuthContext'; import MemberRoute from './components/MemberRoute'; @@ -42,6 +43,7 @@ import History from './pages/History'; import MissionValues from './pages/MissionValues'; import BoardOfDirectors from './pages/BoardOfDirectors'; import Donate from './pages/Donate'; +import DonationSuccess from './pages/DonationSuccess'; const PrivateRoute = ({ children, adminOnly = false }) => { const { user, loading } = useAuth(); @@ -77,7 +79,11 @@ function App() { } /> - } /> + + + + } /> } /> } /> } /> @@ -89,6 +95,7 @@ function App() { {/* Donation Page - Public Access */} } /> + } /> @@ -209,6 +216,13 @@ function App() { } /> + + + + + + } /> diff --git a/src/components/AdminSidebar.js b/src/components/AdminSidebar.js index efde1e4..622579a 100644 --- a/src/components/AdminSidebar.js +++ b/src/components/AdminSidebar.js @@ -20,7 +20,8 @@ import { FileText, DollarSign, Scale, - HardDrive + HardDrive, + Repeat } from 'lucide-react'; const AdminSidebar = ({ isOpen, onToggle, isMobile }) => { @@ -116,6 +117,12 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => { path: '/admin/plans', disabled: false }, + { + name: 'Subscriptions', + icon: Repeat, + path: '/admin/subscriptions', + disabled: false + }, { name: 'Events', icon: Calendar, diff --git a/src/pages/Donate.js b/src/pages/Donate.js index 1ce1024..278ce2f 100644 --- a/src/pages/Donate.js +++ b/src/pages/Donate.js @@ -1,14 +1,59 @@ -import React from 'react'; +import React, { useState } from 'react'; import PublicNavbar from '../components/PublicNavbar'; import PublicFooter from '../components/PublicFooter'; import { Button } from '../components/ui/button'; import { Card } from '../components/ui/card'; -import { CreditCard, Mail } from 'lucide-react'; +import { Input } from '../components/ui/input'; +import { Label } from '../components/ui/label'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '../components/ui/dialog'; +import { CreditCard, Mail, Heart, Loader2 } from 'lucide-react'; +import api from '../utils/api'; +import { toast } from 'sonner'; const Donate = () => { const loafHearts = `${process.env.PUBLIC_URL}/loaf-hearts.png`; const zelleLogo = `${process.env.PUBLIC_URL}/zelle-logo.png`; + const [customAmountDialogOpen, setCustomAmountDialogOpen] = useState(false); + const [customAmount, setCustomAmount] = useState(''); + const [processingAmount, setProcessingAmount] = useState(null); + + const handleDonateAmount = async (amountCents) => { + setProcessingAmount(amountCents); + try { + const response = await api.post('/donations/checkout', { + amount_cents: amountCents + }); + + // Redirect to Stripe Checkout + window.location.href = response.data.checkout_url; + } catch (error) { + console.error('Failed to process donation:', error); + toast.error(error.response?.data?.detail || 'Failed to process donation. Please try again.'); + setProcessingAmount(null); + } + }; + + const handleCustomDonate = () => { + const amount = parseFloat(customAmount); + + if (!customAmount || isNaN(amount) || amount < 1) { + toast.error('Please enter a valid amount (minimum $1.00)'); + return; + } + + const amountCents = Math.round(amount * 100); + setCustomAmountDialogOpen(false); + handleDonateAmount(amountCents); + }; + return (
@@ -44,22 +89,33 @@ const Donate = () => { {/* Donation Buttons Grid */}
{[25, 50, 100, 250].map(amount => ( - ))}
{/* Custom Amount Button */} - -

- Online donations coming soon +

+ Secure donation processing powered by Stripe

@@ -106,6 +162,76 @@ const Donate = () => { + + {/* Custom Amount Dialog */} + + + + + Enter Donation Amount + + + Choose how much you'd like to donate to support our community + + + +
+
+ +
+ + $ + + setCustomAmount(e.target.value)} + placeholder="50.00" + className="pl-10 h-14 text-xl border-2 border-[#ddd8eb] focus:border-[#664fa3] rounded-xl" + onKeyPress={(e) => { + if (e.key === 'Enter') { + handleCustomDonate(); + } + }} + /> +
+

+ Minimum donation: $1.00 +

+
+ +
+

+ Thank you for supporting LOAF!
+ Your donation helps us continue our mission and provide meaningful experiences for our community. +

+
+
+ + + + + +
+
); }; diff --git a/src/pages/DonationSuccess.js b/src/pages/DonationSuccess.js new file mode 100644 index 0000000..0a49e26 --- /dev/null +++ b/src/pages/DonationSuccess.js @@ -0,0 +1,102 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import PublicNavbar from '../components/PublicNavbar'; +import PublicFooter from '../components/PublicFooter'; +import { Card } from '../components/ui/card'; +import { Button } from '../components/ui/button'; +import { CheckCircle, Heart } from 'lucide-react'; + +const DonationSuccess = () => { + const navigate = useNavigate(); + const loafHearts = `${process.env.PUBLIC_URL}/loaf-hearts.png`; + + return ( +
+ + +
+
+ + {/* Success Icon */} +
+ Hearts e.target.style.display = 'none'} + /> +
+
+ +
+ + {/* Title */} +

+ Thank You for Your Donation! +

+ + {/* Message */} +
+

+ Your generous contribution helps support our community and continue our mission. +

+ +
+
+ + + Your Support Makes a Difference + +
+

+ A receipt for your donation has been sent to your email address. +

+
+ +

+ We deeply appreciate your support and commitment to LOAF's mission of building a vibrant, inclusive community. +

+
+ + {/* Actions */} +
+ + +
+
+ + {/* Additional Info */} +
+

+ Have questions about your donation? +

+ + Contact us at support@loaf.org + +
+
+
+ + +
+ ); +}; + +export default DonationSuccess; diff --git a/src/pages/Plans.js b/src/pages/Plans.js index d9c88f3..47c6bd1 100644 --- a/src/pages/Plans.js +++ b/src/pages/Plans.js @@ -15,15 +15,16 @@ import { DialogTitle, } from '../components/ui/dialog'; import Navbar from '../components/Navbar'; -import { CheckCircle, CreditCard, Loader2, Heart } from 'lucide-react'; +import { CheckCircle, CreditCard, Loader2, Heart, AlertCircle } from 'lucide-react'; import { toast } from 'sonner'; const Plans = () => { - const { user } = useAuth(); + const { user, loading: authLoading } = useAuth(); const navigate = useNavigate(); const [plans, setPlans] = useState([]); const [loading, setLoading] = useState(true); const [processingPlanId, setProcessingPlanId] = useState(null); + const [statusInfo, setStatusInfo] = useState(null); // Amount selection dialog state const [amountDialogOpen, setAmountDialogOpen] = useState(false); @@ -34,6 +35,65 @@ const Plans = () => { fetchPlans(); }, []); + // Status-based access control + useEffect(() => { + if (!authLoading && user) { + // Define status-to-message mapping + const statusMessages = { + pending_email: { + title: "Email Verification Required", + message: "Please verify your email address before selecting a membership plan. Check your inbox for the verification link.", + action: null, + canView: true, + canSubscribe: false + }, + pending_approval: { + title: "Application Under Review", + message: "Your application is being reviewed by our admin team. You'll receive an email once approved to proceed with payment.", + action: null, + canView: true, + canSubscribe: false + }, + pre_approved: { + title: "Application Under Review", + message: "Your application is being reviewed by our admin team. You'll receive an email once approved to proceed with payment.", + action: null, + canView: true, + canSubscribe: false + }, + payment_pending: { + title: null, + message: null, + canView: true, + canSubscribe: true + }, + active: { + title: "Already a Member", + message: "You already have an active membership. Visit your dashboard to view your membership details.", + action: "Go to Dashboard", + actionLink: "/dashboard", + canView: true, + canSubscribe: false + }, + inactive: { + title: "Membership Inactive", + message: "Your membership has expired. Please select a plan below to renew your membership.", + action: null, + canView: true, + canSubscribe: true + } + }; + + const userStatus = statusMessages[user.status]; + setStatusInfo(userStatus || { + title: "Access Restricted", + message: "Please contact support for assistance with your account.", + canView: true, + canSubscribe: false + }); + } + }, [user, authLoading]); + const fetchPlans = async () => { try { const response = await api.get('/subscriptions/plans'); @@ -141,6 +201,31 @@ const Plans = () => {

+ {/* Status Banner */} + {statusInfo && statusInfo.title && ( + +
+ +
+

+ {statusInfo.title} +

+

+ {statusInfo.message} +

+ {statusInfo.action && statusInfo.actionLink && ( + + )} +
+
+
+ )} + {loading ? (
@@ -220,8 +305,8 @@ const Plans = () => { {/* CTA Button */} + + + + +
+ ); +}; + +export default AdminSubscriptions;