Update:- Membership Plan- Donation- Member detail for Member Directory
This commit is contained in:
@@ -2,6 +2,13 @@ import React from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { Button } from './ui/button';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from './ui/dropdown-menu';
|
||||
|
||||
const Navbar = () => {
|
||||
const { user, logout } = useAuth();
|
||||
@@ -43,7 +50,7 @@ const Navbar = () => {
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
<Link to="/#donate">
|
||||
<Link to="/donate">
|
||||
<Button
|
||||
className="bg-[#ff9e77] hover:bg-[#ff8c64] text-[#48286e] rounded-[25px] px-[54px] py-[10px] text-[16.5px] font-semibold h-[41px]"
|
||||
style={{ fontFamily: "'Montserrat', sans-serif" }}
|
||||
@@ -66,6 +73,35 @@ const Navbar = () => {
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity flex items-center gap-1 bg-transparent border-none cursor-pointer"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
About Us
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="bg-white min-w-[220px]">
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/about/history" className="w-full px-3 py-2 text-[#48286e] hover:bg-[#f1eef9] cursor-pointer"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
History
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/about/mission-values" className="w-full px-3 py-2 text-[#48286e] hover:bg-[#f1eef9] cursor-pointer"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
Mission and Values
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/about/board" className="w-full px-3 py-2 text-[#48286e] hover:bg-[#f1eef9] cursor-pointer"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
Board of Directors
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Link
|
||||
to="/dashboard"
|
||||
className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity"
|
||||
|
||||
@@ -20,7 +20,8 @@ import {
|
||||
} from './ui/select';
|
||||
import api from '../utils/api';
|
||||
import { toast } from 'sonner';
|
||||
import { Calendar } from 'lucide-react';
|
||||
import { Calendar, Heart } from 'lucide-react';
|
||||
import { Card } from './ui/card';
|
||||
|
||||
const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -55,13 +56,36 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
|
||||
// Update amount when plan changes (unless manually edited)
|
||||
useEffect(() => {
|
||||
if (selectedPlan && !formData.amount) {
|
||||
// Pre-fill with suggested price, or minimum price if suggested not set
|
||||
const suggestedAmount = (selectedPlan.suggested_price_cents || selectedPlan.minimum_price_cents || selectedPlan.price_cents) / 100;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
amount: (selectedPlan.price_cents / 100).toFixed(2)
|
||||
amount: suggestedAmount.toFixed(2)
|
||||
}));
|
||||
}
|
||||
}, [selectedPlan]);
|
||||
|
||||
// Calculate donation breakdown
|
||||
const getAmountBreakdown = () => {
|
||||
if (!selectedPlan || !formData.amount) return null;
|
||||
|
||||
const totalCents = Math.round(parseFloat(formData.amount) * 100);
|
||||
const minimumCents = selectedPlan.minimum_price_cents || selectedPlan.price_cents || 3000;
|
||||
const donationCents = Math.max(0, totalCents - minimumCents);
|
||||
|
||||
return {
|
||||
total: totalCents,
|
||||
base: minimumCents,
|
||||
donation: donationCents
|
||||
};
|
||||
};
|
||||
|
||||
const formatPrice = (cents) => {
|
||||
return `$${(cents / 100).toFixed(2)}`;
|
||||
};
|
||||
|
||||
const breakdown = getAmountBreakdown();
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -74,6 +98,15 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
|
||||
toast.error('Please enter a valid payment amount');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate minimum amount
|
||||
const amountCents = Math.round(parseFloat(formData.amount) * 100);
|
||||
const minimumCents = selectedPlan.minimum_price_cents || selectedPlan.price_cents || 3000;
|
||||
if (amountCents < minimumCents) {
|
||||
toast.error(`Amount must be at least ${formatPrice(minimumCents)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (useCustomPeriod && (!formData.custom_period_start || !formData.custom_period_end)) {
|
||||
toast.error('Please specify both start and end dates for custom period');
|
||||
return;
|
||||
@@ -86,7 +119,7 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
|
||||
amount_cents: Math.round(parseFloat(formData.amount) * 100),
|
||||
payment_date: new Date(formData.payment_date).toISOString(),
|
||||
payment_method: formData.payment_method,
|
||||
use_custom_period: useCustomPeriod,
|
||||
override_plan_dates: useCustomPeriod,
|
||||
notes: formData.notes || null
|
||||
};
|
||||
|
||||
@@ -145,10 +178,12 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
|
||||
onValueChange={(value) => {
|
||||
const plan = plans.find(p => p.id === value);
|
||||
setSelectedPlan(plan);
|
||||
// Pre-fill with suggested price, or minimum price if suggested not set
|
||||
const suggestedAmount = plan ? (plan.suggested_price_cents || plan.minimum_price_cents || plan.price_cents) / 100 : '';
|
||||
setFormData({
|
||||
...formData,
|
||||
plan_id: value,
|
||||
amount: plan ? (plan.price_cents / 100).toFixed(2) : ''
|
||||
amount: suggestedAmount ? suggestedAmount.toFixed(2) : ''
|
||||
});
|
||||
}}
|
||||
>
|
||||
@@ -156,11 +191,15 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
|
||||
<SelectValue placeholder="Select subscription plan" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{plans.map(plan => (
|
||||
<SelectItem key={plan.id} value={plan.id}>
|
||||
{plan.name} - ${(plan.price_cents / 100).toFixed(2)}/{plan.billing_cycle}
|
||||
</SelectItem>
|
||||
))}
|
||||
{plans.map(plan => {
|
||||
const minPrice = (plan.minimum_price_cents || plan.price_cents) / 100;
|
||||
const sugPrice = plan.suggested_price_cents ? (plan.suggested_price_cents / 100) : null;
|
||||
return (
|
||||
<SelectItem key={plan.id} value={plan.id}>
|
||||
{plan.name} - ${minPrice.toFixed(2)}{sugPrice && sugPrice > minPrice ? ` (Suggested: $${sugPrice.toFixed(2)})` : ''}/{plan.billing_cycle}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{selectedPlan && (
|
||||
@@ -186,11 +225,38 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
|
||||
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]"
|
||||
required
|
||||
/>
|
||||
<p className="text-xs text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Amount can differ from plan price if offering a discount or partial payment
|
||||
</p>
|
||||
{selectedPlan && (
|
||||
<p className="text-xs text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Minimum: {formatPrice(selectedPlan.minimum_price_cents || selectedPlan.price_cents || 3000)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Breakdown Display */}
|
||||
{breakdown && breakdown.total >= breakdown.base && (
|
||||
<Card className="p-4 bg-[#f9f5ff] border border-[#DDD8EB]">
|
||||
<div className="space-y-2 text-sm" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<div className="flex justify-between text-[#422268]">
|
||||
<span>Membership Fee:</span>
|
||||
<span className="font-semibold">{formatPrice(breakdown.base)}</span>
|
||||
</div>
|
||||
{breakdown.donation > 0 && (
|
||||
<div className="flex justify-between text-[#ff9e77]">
|
||||
<span className="flex items-center gap-1">
|
||||
<Heart className="h-4 w-4" />
|
||||
Additional Donation:
|
||||
</span>
|
||||
<span className="font-semibold">{formatPrice(breakdown.donation)}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between text-[#422268] font-bold text-base pt-2 border-t border-[#DDD8EB]">
|
||||
<span>Total:</span>
|
||||
<span>{formatPrice(breakdown.total)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Payment Date */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="payment_date" className="text-[#422268] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
@@ -278,14 +344,34 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
|
||||
</div>
|
||||
) : (
|
||||
selectedPlan && (
|
||||
<p className="text-sm text-[#664fa3] bg-[#f1eef9] p-3 rounded-lg" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Will use plan's billing cycle: <span className="font-medium">{selectedPlan.billing_cycle}</span>
|
||||
<br />
|
||||
Starts today, ends {selectedPlan.billing_cycle === 'monthly' ? '30 days' :
|
||||
selectedPlan.billing_cycle === 'quarterly' ? '90 days' :
|
||||
selectedPlan.billing_cycle === 'yearly' ? '1 year' :
|
||||
selectedPlan.billing_cycle === 'lifetime' ? 'lifetime' : '1 year'} from now
|
||||
</p>
|
||||
<div className="text-sm text-[#664fa3] bg-[#f1eef9] p-3 rounded-lg space-y-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{selectedPlan.custom_cycle_enabled ? (
|
||||
<>
|
||||
<p>
|
||||
<span className="font-medium text-[#422268]">Plan uses custom billing cycle:</span>
|
||||
<br />
|
||||
{(() => {
|
||||
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
const startMonth = months[(selectedPlan.custom_cycle_start_month || 1) - 1];
|
||||
const endMonth = months[(selectedPlan.custom_cycle_end_month || 12) - 1];
|
||||
return `${startMonth} ${selectedPlan.custom_cycle_start_day} - ${endMonth} ${selectedPlan.custom_cycle_end_day} (recurring annually)`;
|
||||
})()}
|
||||
</p>
|
||||
<p className="text-xs">
|
||||
Subscription will end on the upcoming cycle end date based on today's date.
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p>
|
||||
Will use plan's billing cycle: <span className="font-medium">{selectedPlan.billing_cycle}</span>
|
||||
<br />
|
||||
Starts today, ends {selectedPlan.billing_cycle === 'monthly' ? '30 days' :
|
||||
selectedPlan.billing_cycle === 'quarterly' ? '90 days' :
|
||||
selectedPlan.billing_cycle === 'yearly' ? '1 year' :
|
||||
selectedPlan.billing_cycle === 'lifetime' ? 'lifetime' : '1 year'} from now
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -22,15 +22,40 @@ import { toast } from 'sonner';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import api from '../utils/api';
|
||||
|
||||
const MONTHS = [
|
||||
{ value: 1, label: 'January' },
|
||||
{ value: 2, label: 'February' },
|
||||
{ value: 3, label: 'March' },
|
||||
{ value: 4, label: 'April' },
|
||||
{ value: 5, label: 'May' },
|
||||
{ value: 6, label: 'June' },
|
||||
{ value: 7, label: 'July' },
|
||||
{ value: 8, label: 'August' },
|
||||
{ value: 9, label: 'September' },
|
||||
{ value: 10, label: 'October' },
|
||||
{ value: 11, label: 'November' },
|
||||
{ value: 12, label: 'December' }
|
||||
];
|
||||
|
||||
const PlanDialog = ({ open, onOpenChange, plan, onSuccess }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
description: '',
|
||||
price_cents: '',
|
||||
price_cents: 3000, // Legacy field, default $30
|
||||
billing_cycle: 'yearly',
|
||||
stripe_price_id: '',
|
||||
active: true
|
||||
active: true,
|
||||
// Custom billing cycle
|
||||
custom_cycle_enabled: false,
|
||||
custom_cycle_start_month: 1,
|
||||
custom_cycle_start_day: 1,
|
||||
custom_cycle_end_month: 12,
|
||||
custom_cycle_end_day: 31,
|
||||
// Dynamic pricing
|
||||
minimum_price_cents: 3000, // $30 minimum
|
||||
suggested_price_cents: 3000,
|
||||
allow_donation: true
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -38,19 +63,35 @@ const PlanDialog = ({ open, onOpenChange, plan, onSuccess }) => {
|
||||
setFormData({
|
||||
name: plan.name,
|
||||
description: plan.description || '',
|
||||
price_cents: plan.price_cents,
|
||||
price_cents: plan.price_cents || 3000,
|
||||
billing_cycle: plan.billing_cycle,
|
||||
stripe_price_id: plan.stripe_price_id || '',
|
||||
active: plan.active
|
||||
active: plan.active,
|
||||
custom_cycle_enabled: plan.custom_cycle_enabled || false,
|
||||
custom_cycle_start_month: plan.custom_cycle_start_month || 1,
|
||||
custom_cycle_start_day: plan.custom_cycle_start_day || 1,
|
||||
custom_cycle_end_month: plan.custom_cycle_end_month || 12,
|
||||
custom_cycle_end_day: plan.custom_cycle_end_day || 31,
|
||||
minimum_price_cents: plan.minimum_price_cents || 3000,
|
||||
suggested_price_cents: plan.suggested_price_cents || plan.minimum_price_cents || 3000,
|
||||
allow_donation: plan.allow_donation !== undefined ? plan.allow_donation : true
|
||||
});
|
||||
} else {
|
||||
setFormData({
|
||||
name: '',
|
||||
description: '',
|
||||
price_cents: '',
|
||||
price_cents: 3000,
|
||||
billing_cycle: 'yearly',
|
||||
stripe_price_id: '',
|
||||
active: true
|
||||
active: true,
|
||||
custom_cycle_enabled: false,
|
||||
custom_cycle_start_month: 1,
|
||||
custom_cycle_start_day: 1,
|
||||
custom_cycle_end_month: 12,
|
||||
custom_cycle_end_day: 31,
|
||||
minimum_price_cents: 3000,
|
||||
suggested_price_cents: 3000,
|
||||
allow_donation: true
|
||||
});
|
||||
}
|
||||
}, [plan, open]);
|
||||
@@ -60,6 +101,30 @@ const PlanDialog = ({ open, onOpenChange, plan, onSuccess }) => {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
// Validate minimum price
|
||||
if (formData.minimum_price_cents < 3000) {
|
||||
toast.error('Minimum price must be at least $30');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate suggested price
|
||||
if (formData.suggested_price_cents < formData.minimum_price_cents) {
|
||||
toast.error('Suggested price must be >= minimum price');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate custom cycle dates if enabled
|
||||
if (formData.custom_cycle_enabled) {
|
||||
if (!formData.custom_cycle_start_month || !formData.custom_cycle_start_day ||
|
||||
!formData.custom_cycle_end_month || !formData.custom_cycle_end_day) {
|
||||
toast.error('All custom cycle dates must be provided');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const endpoint = plan
|
||||
? `/admin/subscriptions/plans/${plan.id}`
|
||||
: '/admin/subscriptions/plans';
|
||||
@@ -68,7 +133,9 @@ const PlanDialog = ({ open, onOpenChange, plan, onSuccess }) => {
|
||||
|
||||
await api[method](endpoint, {
|
||||
...formData,
|
||||
price_cents: parseInt(formData.price_cents)
|
||||
price_cents: parseInt(formData.price_cents),
|
||||
minimum_price_cents: parseInt(formData.minimum_price_cents),
|
||||
suggested_price_cents: parseInt(formData.suggested_price_cents)
|
||||
});
|
||||
|
||||
toast.success(plan ? 'Plan updated successfully' : 'Plan created successfully');
|
||||
@@ -85,14 +152,14 @@ const PlanDialog = ({ open, onOpenChange, plan, onSuccess }) => {
|
||||
return (cents / 100).toFixed(2);
|
||||
};
|
||||
|
||||
const handlePriceChange = (e) => {
|
||||
const dollars = parseFloat(e.target.value) || 0;
|
||||
setFormData({ ...formData, price_cents: Math.round(dollars * 100) });
|
||||
const handlePriceChange = (field, value) => {
|
||||
const dollars = parseFloat(value) || 0;
|
||||
setFormData({ ...formData, [field]: Math.round(dollars * 100) });
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[600px]">
|
||||
<DialogContent className="sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{plan ? 'Edit Plan' : 'Create New Plan'}
|
||||
@@ -129,57 +196,167 @@ const PlanDialog = ({ open, onOpenChange, plan, onSuccess }) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Price & Billing Cycle - Side by side */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="price">Price (USD) *</Label>
|
||||
<Input
|
||||
id="price"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
value={formatPriceForDisplay(formData.price_cents)}
|
||||
onChange={handlePriceChange}
|
||||
placeholder="50.00"
|
||||
required
|
||||
className="mt-2"
|
||||
/>
|
||||
{/* Dynamic Pricing */}
|
||||
<div className="border-2 border-[#DDD8EB] rounded-lg p-4 space-y-4">
|
||||
<h3 className="font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Dynamic Pricing
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="minimum_price">Minimum Price (USD) *</Label>
|
||||
<Input
|
||||
id="minimum_price"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="30"
|
||||
value={formatPriceForDisplay(formData.minimum_price_cents)}
|
||||
onChange={(e) => handlePriceChange('minimum_price_cents', e.target.value)}
|
||||
placeholder="30.00"
|
||||
required
|
||||
className="mt-2"
|
||||
/>
|
||||
<p className="text-xs text-[#664fa3] mt-1">Minimum $30</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="suggested_price">Suggested Price (USD) *</Label>
|
||||
<Input
|
||||
id="suggested_price"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="30"
|
||||
value={formatPriceForDisplay(formData.suggested_price_cents)}
|
||||
onChange={(e) => handlePriceChange('suggested_price_cents', e.target.value)}
|
||||
placeholder="50.00"
|
||||
required
|
||||
className="mt-2"
|
||||
/>
|
||||
<p className="text-xs text-[#664fa3] mt-1">Pre-filled amount</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="billing_cycle">Billing Cycle *</Label>
|
||||
<Select
|
||||
value={formData.billing_cycle}
|
||||
onValueChange={(value) => setFormData({ ...formData, billing_cycle: value })}
|
||||
>
|
||||
<SelectTrigger className="mt-2">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="monthly">Monthly</SelectItem>
|
||||
<SelectItem value="quarterly">Quarterly</SelectItem>
|
||||
<SelectItem value="yearly">Yearly</SelectItem>
|
||||
<SelectItem value="lifetime">Lifetime</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{/* Allow Donation Toggle */}
|
||||
<div className="flex items-center justify-between pt-2">
|
||||
<div>
|
||||
<Label htmlFor="allow_donation">Allow Donations</Label>
|
||||
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Members can pay more than minimum
|
||||
</p>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="allow_donation"
|
||||
checked={formData.allow_donation}
|
||||
onChange={(e) => setFormData({ ...formData, allow_donation: e.target.checked })}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[#664fa3]/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[#81B29A]"></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stripe Price ID */}
|
||||
{/* Billing Cycle */}
|
||||
<div>
|
||||
<Label htmlFor="stripe_price_id">Stripe Price ID</Label>
|
||||
<Input
|
||||
id="stripe_price_id"
|
||||
value={formData.stripe_price_id}
|
||||
onChange={(e) => setFormData({ ...formData, stripe_price_id: e.target.value })}
|
||||
placeholder="price_xxxxxxxxxxxxx"
|
||||
className="mt-2 font-mono text-sm"
|
||||
/>
|
||||
<p className="text-sm text-[#664fa3] mt-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Optional. Leave empty for manual/test plans.
|
||||
</p>
|
||||
<Label htmlFor="billing_cycle">Billing Cycle *</Label>
|
||||
<Select
|
||||
value={formData.billing_cycle}
|
||||
onValueChange={(value) => setFormData({ ...formData, billing_cycle: value, custom_cycle_enabled: value === 'custom' })}
|
||||
>
|
||||
<SelectTrigger className="mt-2">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="monthly">Monthly</SelectItem>
|
||||
<SelectItem value="quarterly">Quarterly</SelectItem>
|
||||
<SelectItem value="yearly">Yearly</SelectItem>
|
||||
<SelectItem value="lifetime">Lifetime</SelectItem>
|
||||
<SelectItem value="custom">Custom Billing Cycle</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Custom Billing Cycle Dates */}
|
||||
{formData.billing_cycle === 'custom' && (
|
||||
<div className="border-2 border-[#DDD8EB] rounded-lg p-4 space-y-4">
|
||||
<h3 className="font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Custom Billing Period
|
||||
</h3>
|
||||
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Set recurring date range (e.g., Jan 1 - Dec 31 for calendar year)
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>Start Date</Label>
|
||||
<div className="grid grid-cols-2 gap-2 mt-2">
|
||||
<Select
|
||||
value={formData.custom_cycle_start_month?.toString()}
|
||||
onValueChange={(value) => setFormData({ ...formData, custom_cycle_start_month: parseInt(value) })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Month" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{MONTHS.map(month => (
|
||||
<SelectItem key={month.value} value={month.value.toString()}>
|
||||
{month.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
max="31"
|
||||
value={formData.custom_cycle_start_day}
|
||||
onChange={(e) => setFormData({ ...formData, custom_cycle_start_day: parseInt(e.target.value) || 1 })}
|
||||
placeholder="Day"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>End Date</Label>
|
||||
<div className="grid grid-cols-2 gap-2 mt-2">
|
||||
<Select
|
||||
value={formData.custom_cycle_end_month?.toString()}
|
||||
onValueChange={(value) => setFormData({ ...formData, custom_cycle_end_month: parseInt(value) })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Month" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{MONTHS.map(month => (
|
||||
<SelectItem key={month.value} value={month.value.toString()}>
|
||||
{month.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
max="31"
|
||||
value={formData.custom_cycle_end_day}
|
||||
onChange={(e) => setFormData({ ...formData, custom_cycle_end_day: parseInt(e.target.value) || 31 })}
|
||||
placeholder="Day"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#f9f5ff] border border-[#DDD8EB] rounded p-3">
|
||||
<p className="text-sm text-[#422268]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<strong>Example:</strong> Jan 1 - Dec 31 for calendar year, or Jul 1 - Jun 30 for fiscal year
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Active Toggle */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
|
||||
@@ -18,9 +18,9 @@ const PublicFooter = () => {
|
||||
<div className="pb-4">
|
||||
<p className="text-white text-base font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>About</p>
|
||||
</div>
|
||||
<a href="/#history" className="text-[#ddd8eb] text-base font-medium hover:text-white transition-colors" style={{ fontFamily: "'Inter', sans-serif" }}>History</a>
|
||||
<a href="/#mission" className="text-[#ddd8eb] text-base font-medium hover:text-white transition-colors" style={{ fontFamily: "'Inter', sans-serif" }}>Mission and Values</a>
|
||||
<a href="/#board" className="text-[#ddd8eb] text-base font-medium hover:text-white transition-colors" style={{ fontFamily: "'Inter', sans-serif" }}>Board of Directors</a>
|
||||
<Link to="/about/history" className="text-[#ddd8eb] text-base font-medium hover:text-white transition-colors" style={{ fontFamily: "'Inter', sans-serif" }}>History</Link>
|
||||
<Link to="/about/mission-values" className="text-[#ddd8eb] text-base font-medium hover:text-white transition-colors" style={{ fontFamily: "'Inter', sans-serif" }}>Mission and Values</Link>
|
||||
<Link to="/about/board" className="text-[#ddd8eb] text-base font-medium hover:text-white transition-colors" style={{ fontFamily: "'Inter', sans-serif" }}>Board of Directors</Link>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 w-[148px]">
|
||||
<div className="pb-4">
|
||||
@@ -32,9 +32,11 @@ const PublicFooter = () => {
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 items-center w-[271px]">
|
||||
<div className="pb-4 w-full">
|
||||
<Button className="bg-[#ff9e77] hover:bg-[#ff8c64] text-[#48286e] rounded-full px-6 py-3 text-lg font-medium w-[217px]">
|
||||
Donate
|
||||
</Button>
|
||||
<Link to="/donate">
|
||||
<Button className="bg-[#ff9e77] hover:bg-[#ff8c64] text-[#48286e] rounded-full px-6 py-3 text-lg font-medium w-[217px]">
|
||||
Donate
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<p className="text-[#ddd8eb] text-base font-medium text-center w-full" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
LOAF is supported by<br />the Hollyfield Foundation
|
||||
|
||||
@@ -2,6 +2,13 @@ import React from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { Button } from './ui/button';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from './ui/dropdown-menu';
|
||||
|
||||
const PublicNavbar = () => {
|
||||
const { user, logout } = useAuth();
|
||||
@@ -39,7 +46,7 @@ const PublicNavbar = () => {
|
||||
Register
|
||||
</Link>
|
||||
)}
|
||||
<Link to="/#donate">
|
||||
<Link to="/donate">
|
||||
<Button
|
||||
className="bg-[#ff9e77] hover:bg-[#ff8c64] text-[#48286e] rounded-[25px] px-[54px] py-[10px] text-[16.5px] font-semibold h-[41px]"
|
||||
style={{ fontFamily: "'Montserrat', sans-serif" }}
|
||||
@@ -62,13 +69,35 @@ const PublicNavbar = () => {
|
||||
>
|
||||
Welcome
|
||||
</Link>
|
||||
<Link
|
||||
to="/#about"
|
||||
className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||
>
|
||||
About Us
|
||||
</Link>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity flex items-center gap-1 bg-transparent border-none cursor-pointer"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
About Us
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="bg-white min-w-[220px]">
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/about/history" className="w-full px-3 py-2 text-[#48286e] hover:bg-[#f1eef9] cursor-pointer"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
History
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/about/mission-values" className="w-full px-3 py-2 text-[#48286e] hover:bg-[#f1eef9] cursor-pointer"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
Mission and Values
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/about/board" className="w-full px-3 py-2 text-[#48286e] hover:bg-[#f1eef9] cursor-pointer"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
Board of Directors
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Link
|
||||
to={user ? "/dashboard" : "/become-a-member"}
|
||||
className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity"
|
||||
|
||||
Reference in New Issue
Block a user