Initial Commit

This commit is contained in:
Koncept Kit
2025-12-05 16:40:33 +07:00
parent 0834f12410
commit 94c7d5aec0
91 changed files with 20446 additions and 0 deletions

196
src/pages/Plans.js Normal file
View File

@@ -0,0 +1,196 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import api from '../utils/api';
import { Card } from '../components/ui/card';
import { Button } from '../components/ui/button';
import Navbar from '../components/Navbar';
import { CheckCircle, CreditCard, Loader2 } from 'lucide-react';
import { toast } from 'sonner';
const Plans = () => {
const { user } = useAuth();
const navigate = useNavigate();
const [plans, setPlans] = useState([]);
const [loading, setLoading] = useState(true);
const [processingPlanId, setProcessingPlanId] = useState(null);
useEffect(() => {
fetchPlans();
}, []);
const fetchPlans = async () => {
try {
const response = await api.get('/subscriptions/plans');
setPlans(response.data);
} catch (error) {
console.error('Failed to fetch plans:', error);
toast.error('Failed to load subscription plans');
} finally {
setLoading(false);
}
};
const handleSubscribe = async (planId) => {
if (!user) {
navigate('/login');
return;
}
setProcessingPlanId(planId);
try {
const response = await api.post('/subscriptions/checkout', {
plan_id: planId
});
// Redirect to Stripe Checkout
window.location.href = response.data.checkout_url;
} catch (error) {
console.error('Failed to create checkout session:', error);
toast.error(error.response?.data?.detail || 'Failed to start checkout process');
setProcessingPlanId(null);
}
};
const formatPrice = (cents) => {
return `$${(cents / 100).toFixed(2)}`;
};
const getBillingCycleLabel = (billingCycle) => {
const labels = {
yearly: 'per year',
monthly: 'per month'
};
return labels[billingCycle] || billingCycle;
};
return (
<div className="min-h-screen bg-[#FDFCF8]">
<Navbar />
<div className="max-w-7xl mx-auto px-6 py-12">
{/* Header */}
<div className="mb-12 text-center">
<h1 className="text-4xl md:text-5xl font-semibold fraunces text-[#3D405B] mb-4">
Membership Plans
</h1>
<p className="text-lg text-[#6B708D] max-w-2xl mx-auto">
Choose the membership plan that works best for you and become part of our vibrant community.
</p>
</div>
{loading ? (
<div className="text-center py-20">
<Loader2 className="h-12 w-12 text-[#E07A5F] mx-auto mb-4 animate-spin" />
<p className="text-[#6B708D]">Loading plans...</p>
</div>
) : plans.length > 0 ? (
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-5xl mx-auto">
{plans.map((plan) => (
<Card
key={plan.id}
className="p-8 bg-white rounded-2xl border-2 border-[#EAE0D5] hover:border-[#E07A5F] hover:shadow-xl transition-all"
data-testid={`plan-card-${plan.id}`}
>
{/* Plan Header */}
<div className="text-center mb-6">
<div className="bg-[#F2CC8F]/20 p-4 rounded-full w-16 h-16 mx-auto mb-4 flex items-center justify-center">
<CreditCard className="h-8 w-8 text-[#E07A5F]" />
</div>
<h2 className="text-2xl font-semibold fraunces text-[#3D405B] mb-2">
{plan.name}
</h2>
{plan.description && (
<p className="text-sm text-[#6B708D] mb-4">
{plan.description}
</p>
)}
</div>
{/* Pricing */}
<div className="text-center mb-8">
<div className="text-4xl font-bold fraunces text-[#3D405B] mb-2">
{formatPrice(plan.price_cents)}
</div>
<p className="text-[#6B708D]">
{getBillingCycleLabel(plan.billing_cycle)}
</p>
</div>
{/* Features */}
<div className="space-y-3 mb-8">
<div className="flex items-start gap-3">
<CheckCircle className="h-5 w-5 text-[#81B29A] flex-shrink-0 mt-0.5" />
<span className="text-sm text-[#3D405B]">Access to all member events</span>
</div>
<div className="flex items-start gap-3">
<CheckCircle className="h-5 w-5 text-[#81B29A] flex-shrink-0 mt-0.5" />
<span className="text-sm text-[#3D405B]">Community directory access</span>
</div>
<div className="flex items-start gap-3">
<CheckCircle className="h-5 w-5 text-[#81B29A] flex-shrink-0 mt-0.5" />
<span className="text-sm text-[#3D405B]">Exclusive member benefits</span>
</div>
<div className="flex items-start gap-3">
<CheckCircle className="h-5 w-5 text-[#81B29A] flex-shrink-0 mt-0.5" />
<span className="text-sm text-[#3D405B]">Newsletter subscription</span>
</div>
</div>
{/* CTA Button */}
<Button
onClick={() => handleSubscribe(plan.id)}
disabled={processingPlanId === plan.id}
className="w-full bg-[#E07A5F] text-white hover:bg-[#D0694E] rounded-full py-6 text-lg font-semibold"
data-testid={`subscribe-button-${plan.id}`}
>
{processingPlanId === plan.id ? (
<>
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
Processing...
</>
) : (
'Subscribe Now'
)}
</Button>
</Card>
))}
</div>
) : (
<div className="text-center py-20">
<CreditCard className="h-20 w-20 text-[#EAE0D5] mx-auto mb-6" />
<h3 className="text-2xl font-semibold fraunces text-[#3D405B] mb-4">
No Plans Available
</h3>
<p className="text-[#6B708D]">
Membership plans are not currently available. Please check back later!
</p>
</div>
)}
{/* Info Section */}
<div className="mt-16 max-w-3xl mx-auto">
<Card className="p-8 bg-gradient-to-br from-[#F2CC8F]/20 to-[#E07A5F]/20 rounded-2xl border border-[#EAE0D5]">
<h3 className="text-xl font-semibold fraunces text-[#3D405B] mb-4 text-center">
Need Help Choosing?
</h3>
<p className="text-[#6B708D] text-center mb-4">
If you have any questions about our membership plans or need assistance, please contact us.
</p>
<div className="text-center">
<a
href="mailto:support@loaf.org"
className="text-[#E07A5F] hover:text-[#D0694E] font-medium"
>
support@loaf.org
</a>
</div>
</Card>
</div>
</div>
</div>
);
};
export default Plans;