Update:- Membership Plan- Donation- Member detail for Member Directory
BIN
public/history-arden-charlotte.png
Normal file
|
After Width: | Height: | Size: 814 KiB |
BIN
public/history-part3.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
public/history-part7.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
public/history-pride-1.png
Normal file
|
After Width: | Height: | Size: 467 KiB |
BIN
public/history-pride-2.png
Normal file
|
After Width: | Height: | Size: 453 KiB |
BIN
public/loaf-hearts.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
public/zelle-logo.png
Normal file
|
After Width: | Height: | Size: 101 KiB |
12
src/App.js
@@ -38,6 +38,10 @@ import AdminGallery from './pages/admin/AdminGallery';
|
||||
import AdminNewsletters from './pages/admin/AdminNewsletters';
|
||||
import AdminFinancials from './pages/admin/AdminFinancials';
|
||||
import AdminBylaws from './pages/admin/AdminBylaws';
|
||||
import History from './pages/History';
|
||||
import MissionValues from './pages/MissionValues';
|
||||
import BoardOfDirectors from './pages/BoardOfDirectors';
|
||||
import Donate from './pages/Donate';
|
||||
|
||||
const PrivateRoute = ({ children, adminOnly = false }) => {
|
||||
const { user, loading } = useAuth();
|
||||
@@ -78,6 +82,14 @@ function App() {
|
||||
<Route path="/payment-success" element={<PaymentSuccess />} />
|
||||
<Route path="/payment-cancel" element={<PaymentCancel />} />
|
||||
|
||||
{/* About Us Pages - Public Access */}
|
||||
<Route path="/about/history" element={<History />} />
|
||||
<Route path="/about/mission-values" element={<MissionValues />} />
|
||||
<Route path="/about/board" element={<BoardOfDirectors />} />
|
||||
|
||||
{/* Donation Page - Public Access */}
|
||||
<Route path="/donate" element={<Donate />} />
|
||||
|
||||
<Route path="/dashboard" element={
|
||||
<PrivateRoute>
|
||||
<Dashboard />
|
||||
|
||||
@@ -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"
|
||||
|
||||
117
src/pages/BoardOfDirectors.js
Normal file
@@ -0,0 +1,117 @@
|
||||
import React from 'react';
|
||||
import PublicNavbar from '../components/PublicNavbar';
|
||||
import PublicFooter from '../components/PublicFooter';
|
||||
import { Card } from '../components/ui/card';
|
||||
|
||||
const BoardOfDirectors = () => {
|
||||
const officers = [
|
||||
{ name: 'Lorraine Schroeder', title: 'President' },
|
||||
{ name: 'Lavita Marks', title: 'Vice President' },
|
||||
{ name: 'Janis Smith', title: 'Secretary' },
|
||||
{ name: 'Dawn Harrell', title: 'Treasurer' }
|
||||
];
|
||||
|
||||
const boardMembers = [
|
||||
'Danita Cole',
|
||||
'Roxanne Cherico',
|
||||
'Lucretia Copeland',
|
||||
'Julie Fischer'
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white">
|
||||
<PublicNavbar />
|
||||
|
||||
<main className="bg-gradient-to-b from-white to-[#f1eef9] px-16 py-12">
|
||||
{/* Hero Section with Contact */}
|
||||
<section className="bg-gradient-to-r from-[#664fa3] to-[#48286e] py-8 rounded-2xl mb-12">
|
||||
<div className="max-w-5xl mx-auto text-center px-8">
|
||||
<h1 className="text-4xl font-bold text-white mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
LOAF Board of Directors 2025
|
||||
</h1>
|
||||
<p className="text-white text-lg mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
For any questions or inquiries please email us at{' '}
|
||||
<a href="mailto:info@loaftx.org" className="text-[#c5b4e3] underline font-bold hover:text-white transition-colors">
|
||||
info@loaftx.org
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Officers Grid */}
|
||||
<section className="py-12">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<h2 className="text-3xl font-bold text-[#48286e] text-center mb-8" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Officers
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{officers.map((officer, index) => (
|
||||
<Card key={index} className="bg-[#eeebf4] p-6 text-center rounded-xl shadow-md hover:shadow-lg transition-shadow">
|
||||
<h3 className="text-xl font-bold text-[#48286e] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{officer.name}
|
||||
</h3>
|
||||
<p className="text-lg text-[#48286e]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{officer.title}
|
||||
</p>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Board Members Grid */}
|
||||
<section className="py-12 bg-gray-50 rounded-2xl">
|
||||
<div className="max-w-6xl mx-auto px-8">
|
||||
<h2 className="text-3xl font-bold text-[#48286e] text-center mb-8" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Board of Directors
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{boardMembers.map((member, index) => (
|
||||
<Card key={index} className="bg-[#eeebf4] p-6 text-center rounded-xl shadow-md hover:shadow-lg transition-shadow">
|
||||
<h3 className="text-xl font-bold text-[#48286e]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{member}
|
||||
</h3>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Join the Board Section */}
|
||||
<section className="py-12">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<h2 className="text-3xl font-bold text-[#48286e] text-center mb-8" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Join the Board of Directors
|
||||
</h2>
|
||||
<p className="text-xl text-[#48286e] text-center mb-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Our elections take place at our December holiday social. Here are some things
|
||||
to know if you are thinking about serving on the Board of Directors.
|
||||
</p>
|
||||
<Card className="bg-[#eeebf4] p-8 rounded-2xl shadow-lg">
|
||||
<ol className="list-decimal list-inside space-y-4 text-lg text-[#48286e]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<li>
|
||||
Nominations are due by November 1. Nomination Form:{' '}
|
||||
<a href="https://docs.google.com/forms/d/e/1FAIpQLSfNomination" target="_blank" rel="noopener noreferrer"
|
||||
className="text-[#664fa3] underline hover:text-[#48286e] transition-colors">
|
||||
Click Here
|
||||
</a>
|
||||
</li>
|
||||
<li>Nominees must have been a member for at least 1 year and current with their dues.</li>
|
||||
<li>Officer positions are only available to current directors.</li>
|
||||
<li>Each director shall serve a 2 year term.</li>
|
||||
<li>The time commitment is 1-2 hours per week.</li>
|
||||
<li>The tasks that directors perform depend on individual interests, skills, and time available.</li>
|
||||
<li>Directors must attend Board meetings which are held the second Thursday of each month at 6:30pm via Zoom.</li>
|
||||
<li>We are a fun group, and we would love for you to join us in providing this service for our community.</li>
|
||||
</ol>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<PublicFooter />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BoardOfDirectors;
|
||||
113
src/pages/Donate.js
Normal file
@@ -0,0 +1,113 @@
|
||||
import React 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';
|
||||
|
||||
const Donate = () => {
|
||||
const loafHearts = `${process.env.PUBLIC_URL}/loaf-hearts.png`;
|
||||
const zelleLogo = `${process.env.PUBLIC_URL}/zelle-logo.png`;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white">
|
||||
<PublicNavbar />
|
||||
|
||||
<main className="bg-gradient-to-b from-white to-[#f1eef9] px-16 py-12">
|
||||
{/* Hero Section */}
|
||||
<section className="py-12">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
<div className="flex justify-center mb-4">
|
||||
<img src={loafHearts} alt="Hearts" className="w-32 h-auto" onError={(e) => e.target.style.display = 'none'} />
|
||||
</div>
|
||||
<h1 className="text-5xl font-bold text-[#48286e] mb-6" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Donate
|
||||
</h1>
|
||||
<p className="text-xl text-[#48286e]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
We really appreciate your donations. You can make your donation online
|
||||
or send a check by mail.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Donation Amount Buttons */}
|
||||
<section className="py-12">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<Card className="p-8 bg-white rounded-2xl shadow-lg">
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<CreditCard className="w-12 h-12 text-[#664fa3]" />
|
||||
<h2 className="text-3xl font-bold text-[#48286e]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Select Your Donation Amount
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* Donation Buttons Grid */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||
{[25, 50, 100, 250].map(amount => (
|
||||
<Button key={amount}
|
||||
className="bg-[#664fa3] hover:bg-[#48286e] text-white text-xl py-8 rounded-full"
|
||||
disabled>
|
||||
${amount}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Custom Amount Button */}
|
||||
<Button className="w-full bg-[#664fa3] hover:bg-[#48286e] text-white text-xl py-8 rounded-full"
|
||||
disabled>
|
||||
Donate Any Amount
|
||||
</Button>
|
||||
|
||||
<p className="text-sm text-[#48286e] text-center mt-4 opacity-75" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Online donations coming soon
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Alternative Payment Methods */}
|
||||
<section className="py-12">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{/* Mail Check */}
|
||||
<Card className="p-8 bg-white rounded-2xl shadow-lg">
|
||||
<Mail className="w-12 h-12 text-[#664fa3] mb-4" />
|
||||
<h3 className="text-2xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Mail a Check
|
||||
</h3>
|
||||
<p className="text-lg text-[#48286e] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Our mailing address for checks:<br />
|
||||
<span className="font-semibold">LOAF</span><br />
|
||||
P.O. Box 7207<br />
|
||||
Houston, Texas 77248-7207
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
{/* Zelle */}
|
||||
<Card className="p-8 bg-white rounded-2xl shadow-lg">
|
||||
<div className="mb-4">
|
||||
<img src={zelleLogo} alt="Zelle" className="h-32" onError={(e) => e.target.style.display = 'none'} />
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Pay with Zelle
|
||||
</h3>
|
||||
<p className="text-lg text-[#48286e] leading-relaxed mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
If your bank allows the use of Zelle, please feel free to send money to:
|
||||
</p>
|
||||
<a href="mailto:LOAFHoustonTX@gmail.com"
|
||||
className="text-[#664fa3] text-lg font-bold underline hover:text-[#48286e] transition-colors"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
LOAFHoustonTX@gmail.com
|
||||
</a>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<PublicFooter />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Donate;
|
||||
268
src/pages/History.js
Normal file
@@ -0,0 +1,268 @@
|
||||
import React 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 { Pen } from 'lucide-react';
|
||||
|
||||
const History = () => {
|
||||
const ardenCharlotteImg = `${process.env.PUBLIC_URL}/history-arden-charlotte.png`;
|
||||
const pride1Img = `${process.env.PUBLIC_URL}/history-pride-1.png`;
|
||||
const pride2Img = `${process.env.PUBLIC_URL}/history-pride-2.png`;
|
||||
const part3Img = `${process.env.PUBLIC_URL}/history-part3.png`;
|
||||
const part7Img = `${process.env.PUBLIC_URL}/history-part7.png`;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white">
|
||||
<PublicNavbar />
|
||||
|
||||
<main className="bg-gradient-to-b from-white to-[#f1eef9] px-16 py-12">
|
||||
{/* Hero Section */}
|
||||
<section className="py-12">
|
||||
<div className="max-w-5xl mx-auto text-center">
|
||||
<h1 className="text-5xl font-bold text-[#48286e] mb-4"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
History of LOAF
|
||||
</h1>
|
||||
<div className="flex items-center justify-center gap-2 text-[#48286e]">
|
||||
<Pen className="h-5 w-5" />
|
||||
<p className="text-lg" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
By Arden Eversmeyer
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Part 1 - With Image */}
|
||||
<section className="py-12">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<Card className="p-8 bg-white rounded-2xl shadow-lg">
|
||||
<div className="flex flex-col md:flex-row gap-8 items-start">
|
||||
<div className="md:w-1/3">
|
||||
<img src={ardenCharlotteImg} alt="Arden Eversmeyer and Charlotte Avery"
|
||||
className="rounded-lg w-full" onError={(e) => e.target.style.display = 'none'} />
|
||||
<p className="text-sm text-[#48286e] mt-2 text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Arden Eversmeyer and Charlotte Avery
|
||||
</p>
|
||||
</div>
|
||||
<div className="md:w-2/3">
|
||||
<h2 className="text-3xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Part 1
|
||||
</h2>
|
||||
<p className="text-lg text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
In 1985 my life partner of 33 years died. For many years we had been part of a large "friendship group" that got together for meals and games. After her death, I found myself on the edge of the group. I felt invisible. The group, composed primarily of couples, didn't know what to do with the single person they had suddenly become.
|
||||
</p>
|
||||
<p className="text-lg text-[#48286e] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
When I moved to Houston in 1992, I again found myself isolated. I had friends, but not being "coupled" in a "couples world" left me on the outside. I was aware of my advancing age – I was 63 at the time - and I was sure that I was the only "old lesbian" in Houston. I checked out the Montrose bars, but to my dismay, found that older lesbians were non-existent; at least they didn't hang out in bars.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Part 2 */}
|
||||
<section className="py-12">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<Card className="p-8 bg-white rounded-2xl shadow-lg">
|
||||
<h2 className="text-3xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Part 2
|
||||
</h2>
|
||||
<p className="text-lg text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
The Founding Mothers of LOAF are Ruth Sathre (nurse), JoAnn Beene (psychologist), Delores Nason (business woman), JoAnn Loulan (psychologist and writer, now living in Guerneville, CA), and Judy Peyton (social worker). We decided to form a group for Lesbians Over Fifty and began the search for others "like us."
|
||||
</p>
|
||||
<p className="text-lg text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
In 1993, we advertised in OutSmart Magazine, the Houston gay magazine, and invited interested women to join us at a local restaurant. Founding mothers and 19 other women came to that first meeting. Since then the group has gone through many evolutions.
|
||||
</p>
|
||||
<ul className="list-disc ml-6 mt-4 space-y-2 text-lg text-[#48286e]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<li><strong>AGE OF PARTICIPANTS</strong> - we started off as LOAFF - Lesbians over Age Fifty-Five. The extra F stood for 55, which didn't work very well, so we changed to LOAF and lowered the age to 50.</li>
|
||||
<li><strong>NAME FOR THE GROUP</strong> - LOAFF and then LOAF</li>
|
||||
<li><strong>NUMBER OF EVENTS</strong> - Some of the early years we had events every Saturday afternoon, but as we aged, we cut back to one event each month, then we went to the current format of one event during the week, either afternoon or evening, and a weekend activity.</li>
|
||||
<li><strong>TYPES OF EVENTS</strong> - We've had LOTS of different events. Some of the events we have had include: going to a museum, going to the symphony, seeing a play or movie together, going out to dinner, pot luck dinners, game nights, campfires, hiking, kayaking, and more.</li>
|
||||
</ul>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Part 3 - With Image */}
|
||||
<section className="py-12">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<Card className="p-8 bg-white rounded-2xl shadow-lg">
|
||||
<div className="flex flex-col md:flex-row gap-8 items-start">
|
||||
<div className="md:w-2/3">
|
||||
<h2 className="text-3xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Part 3
|
||||
</h2>
|
||||
<p className="text-lg text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
We have never had a formal organization with by-laws and officers. We have operated on a consensus basis with the founders making most of the decisions. One of the early decisions we made was that we would not have any kind of formal membership. We wanted to be as inclusive as possible and not create any barriers to participation.
|
||||
</p>
|
||||
<p className="text-lg text-[#48286e] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
We have always been self-supporting. We have never charged dues or asked for donations. Each person pays for their own meal or activity. We have never had a budget or a bank account. We have been able to operate this way because we have always kept our activities simple and inexpensive.
|
||||
</p>
|
||||
</div>
|
||||
<div className="md:w-1/3">
|
||||
<img src={part3Img} alt="LOAF Community"
|
||||
className="rounded-lg w-full" onError={(e) => e.target.style.display = 'none'} />
|
||||
<p className="text-sm text-[#48286e] mt-2 text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
LOAF Community
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Part 4 - With Image */}
|
||||
<section className="py-12">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<Card className="p-8 bg-white rounded-2xl shadow-lg">
|
||||
<div className="flex flex-col md:flex-row gap-8 items-start">
|
||||
<div className="md:w-2/3">
|
||||
<h2 className="text-3xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Part 4
|
||||
</h2>
|
||||
<p className="text-lg text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Over the years, LOAF has been a place where women can be themselves, where they can talk about their lives and their experiences without fear of judgment. We have created a safe space for women to explore their sexuality and their identity as lesbians.
|
||||
</p>
|
||||
<p className="text-lg text-[#48286e] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Many women have told us that LOAF has been a lifeline for them, especially as they age and find themselves increasingly isolated. LOAF has provided a community and a sense of belonging that has been invaluable.
|
||||
</p>
|
||||
</div>
|
||||
<div className="md:w-1/3">
|
||||
<img src={pride1Img} alt="Pride Parade"
|
||||
className="rounded-lg w-full" onError={(e) => e.target.style.display = 'none'} />
|
||||
<img src={pride2Img} alt="Pride Parade"
|
||||
className="rounded-lg w-full" onError={(e) => e.target.style.display = 'none'} />
|
||||
<p className="text-sm text-[#48286e] mt-2 text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
LOAF at Pride
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Part 5 */}
|
||||
<section className="py-12">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<Card className="p-8 bg-white rounded-2xl shadow-lg">
|
||||
<h2 className="text-3xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Part 5
|
||||
</h2>
|
||||
<p className="text-lg text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
LOAF has also been a place where women can give back to the community. Many of our members have been active in various LGBTQ+ organizations and causes. We have marched in Pride parades, volunteered at LGBTQ+ events, and supported various LGBTQ+ initiatives.
|
||||
</p>
|
||||
<p className="text-lg text-[#48286e] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
As we look to the future, we are committed to continuing to provide a welcoming and inclusive space for lesbians over 50. We know that there are many women out there who are looking for a community like ours, and we want to make sure that they know that LOAF is here for them.
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Part 6 */}
|
||||
<section className="py-12">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<Card className="p-8 bg-white rounded-2xl shadow-lg">
|
||||
<h2 className="text-3xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Part 6
|
||||
</h2>
|
||||
<p className="text-lg text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
One of the things that has made LOAF special is the diversity of our members. We have women from all walks of life, all backgrounds, all races, all religions, and all political persuasions. What we have in common is our age and our sexual orientation.
|
||||
</p>
|
||||
<p className="text-lg text-[#48286e] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
We have learned so much from each other over the years. We have shared our stories, our wisdom, and our experiences. We have laughed together, cried together, and supported each other through good times and bad.
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Part 7 - With Image */}
|
||||
<section className="py-12">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<Card className="p-8 bg-white rounded-2xl shadow-lg">
|
||||
<div className="flex flex-col md:flex-row gap-8 items-start">
|
||||
<div className="md:w-1/3">
|
||||
<img src={part7Img} alt="LOAF Members"
|
||||
className="rounded-lg w-full" onError={(e) => e.target.style.display = 'none'} />
|
||||
<p className="text-sm text-[#48286e] mt-2 text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
LOAF Members
|
||||
</p>
|
||||
</div>
|
||||
<div className="md:w-2/3">
|
||||
<h2 className="text-3xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Part 7
|
||||
</h2>
|
||||
<p className="text-lg text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
LOAF has evolved over the years, but our core mission has remained the same: to provide a welcoming and inclusive community for lesbians over 50. We have adapted to the changing times and the changing needs of our members, but we have never lost sight of what makes LOAF special.
|
||||
</p>
|
||||
<p className="text-lg text-[#48286e] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
We are proud of what we have accomplished over the years, and we are excited about the future. We know that there will always be a need for a community like LOAF, and we are committed to being here for as long as we are needed.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Part 8 */}
|
||||
<section className="py-12">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<Card className="p-8 bg-white rounded-2xl shadow-lg">
|
||||
<h2 className="text-3xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Part 8
|
||||
</h2>
|
||||
<p className="text-lg text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
As I reflect on the history of LOAF, I am filled with gratitude for all of the women who have been part of this community over the years. Each one of you has made LOAF what it is today, and I am so proud of what we have created together.
|
||||
</p>
|
||||
<p className="text-lg text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
LOAF has been a place where we can be ourselves, where we can celebrate who we are, and where we can support each other through all of life's challenges. It has been a place of joy, laughter, friendship, and love.
|
||||
</p>
|
||||
<p className="text-lg text-[#48286e] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Thank you for being part of LOAF. Thank you for making this community what it is. And thank you for continuing to support LOAF into the future.
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="py-12 bg-[#48286e] rounded-2xl">
|
||||
<div className="max-w-5xl mx-auto px-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<Card className="p-8 text-center bg-white rounded-2xl shadow-lg hover:shadow-xl transition-shadow">
|
||||
<h3 className="text-2xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
A Life Remembered
|
||||
</h3>
|
||||
<p className="text-[#48286e] mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Check out "A Life Remembered", a tribute dedicated to Arden Eversmeyer, one of the founding mothers of LOAF.
|
||||
</p>
|
||||
<a href="https://www.oldlesbianhistory.org/arden-eversmeyer" target="_blank" rel="noopener noreferrer">
|
||||
<Button className="bg-[#664fa3] hover:bg-[#48286e] text-white rounded-full px-6 py-3">
|
||||
View Arden's Tribute
|
||||
</Button>
|
||||
</a>
|
||||
</Card>
|
||||
|
||||
<Card className="p-8 text-center bg-white rounded-2xl shadow-lg hover:shadow-xl transition-shadow">
|
||||
<h3 className="text-2xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
The Old Lesbian Oral Herstory Project
|
||||
</h3>
|
||||
<p className="text-[#48286e] mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Arden Eversmeyer was also involved with The Old Lesbian Oral Herstory Project, preserving the stories of old lesbians.
|
||||
</p>
|
||||
<a href="https://www.olohp.org" target="_blank" rel="noopener noreferrer">
|
||||
<Button className="bg-[#664fa3] hover:bg-[#48286e] text-white rounded-full px-6 py-3">
|
||||
Learn More About OLOHP
|
||||
</Button>
|
||||
</a>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<PublicFooter />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default History;
|
||||
60
src/pages/MissionValues.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import PublicNavbar from '../components/PublicNavbar';
|
||||
import PublicFooter from '../components/PublicFooter';
|
||||
import { Card } from '../components/ui/card';
|
||||
|
||||
const MissionValues = () => {
|
||||
const loafLogo = `${process.env.PUBLIC_URL}/loaf-logo.png`;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white">
|
||||
<PublicNavbar />
|
||||
|
||||
<main className="bg-gradient-to-b from-[#f9fafb] to-[#ddd8eb] px-16 py-16">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{/* Left Card - Mission (Purple Gradient) */}
|
||||
<Card className="bg-gradient-to-br from-[#664fa3] to-[#48286e] p-8 rounded-2xl shadow-lg">
|
||||
<h2 className="text-3xl font-bold text-white text-center mb-6"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
LOAF Mission
|
||||
</h2>
|
||||
<p className="text-white text-lg text-center leading-relaxed"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
LOAF's mission is to alleviate isolation and enrich the lives of lesbians
|
||||
over the age of 50 by providing several social networking events every month
|
||||
in Houston and the surrounding areas.
|
||||
</p>
|
||||
<div className="flex justify-center mb-6">
|
||||
<img src={loafLogo} alt="LOAF Logo" className="w-64 h-64 object-contain" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Right Card - Values */}
|
||||
<Card className="bg-white p-8 rounded-2xl shadow-lg">
|
||||
<h2 className="text-3xl font-bold text-[#48286e] text-center mb-6"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
LOAF Values
|
||||
</h2>
|
||||
<ol className="list-decimal list-inside space-y-3 text-lg text-[#48286e]"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<li>Safe environments for lesbians to gather for a variety of social activities and interaction.</li>
|
||||
<li>Social support for lesbians.</li>
|
||||
<li>Diversity and inclusivity.</li>
|
||||
<li>The varying social needs of lesbians between the ages of 50 and 99+.</li>
|
||||
<li>Collective wisdom and feedback from the membership.</li>
|
||||
<li>Educational programs on topics that are important to the membership.</li>
|
||||
<li>Safe environments for women who are exploring their sexuality.</li>
|
||||
<li>Alleviating internalized homophobia.</li>
|
||||
</ol>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<PublicFooter />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MissionValues;
|
||||
@@ -4,8 +4,18 @@ import { useAuth } from '../context/AuthContext';
|
||||
import api from '../utils/api';
|
||||
import { Card } from '../components/ui/card';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Input } from '../components/ui/input';
|
||||
import { Label } from '../components/ui/label';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '../components/ui/dialog';
|
||||
import Navbar from '../components/Navbar';
|
||||
import { CheckCircle, CreditCard, Loader2 } from 'lucide-react';
|
||||
import { CheckCircle, CreditCard, Loader2, Heart } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
const Plans = () => {
|
||||
@@ -15,6 +25,11 @@ const Plans = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [processingPlanId, setProcessingPlanId] = useState(null);
|
||||
|
||||
// Amount selection dialog state
|
||||
const [amountDialogOpen, setAmountDialogOpen] = useState(false);
|
||||
const [selectedPlan, setSelectedPlan] = useState(null);
|
||||
const [amountInput, setAmountInput] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
fetchPlans();
|
||||
}, []);
|
||||
@@ -31,17 +46,43 @@ const Plans = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubscribe = async (planId) => {
|
||||
const handleSelectPlan = (plan) => {
|
||||
if (!user) {
|
||||
navigate('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
setProcessingPlanId(planId);
|
||||
setSelectedPlan(plan);
|
||||
// Pre-fill with suggested price or minimum price
|
||||
const suggestedAmount = (plan.suggested_price_cents || plan.minimum_price_cents) / 100;
|
||||
setAmountInput(suggestedAmount.toFixed(2));
|
||||
setAmountDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleCheckout = async () => {
|
||||
const amountCents = Math.round(parseFloat(amountInput) * 100);
|
||||
const minimumCents = selectedPlan.minimum_price_cents || 3000;
|
||||
|
||||
// Validate amount
|
||||
if (!amountInput || isNaN(amountCents) || amountCents < minimumCents) {
|
||||
toast.error(`Amount must be at least $${(minimumCents / 100).toFixed(2)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if plan allows donations
|
||||
const donationCents = amountCents - minimumCents;
|
||||
if (donationCents > 0 && !selectedPlan.allow_donation) {
|
||||
toast.error('This plan does not accept donations above the minimum price');
|
||||
return;
|
||||
}
|
||||
|
||||
setProcessingPlanId(selectedPlan.id);
|
||||
setAmountDialogOpen(false);
|
||||
|
||||
try {
|
||||
const response = await api.post('/subscriptions/checkout', {
|
||||
plan_id: planId
|
||||
plan_id: selectedPlan.id,
|
||||
amount_cents: amountCents
|
||||
});
|
||||
|
||||
// Redirect to Stripe Checkout
|
||||
@@ -60,11 +101,31 @@ const Plans = () => {
|
||||
const getBillingCycleLabel = (billingCycle) => {
|
||||
const labels = {
|
||||
yearly: 'per year',
|
||||
monthly: 'per month'
|
||||
monthly: 'per month',
|
||||
quarterly: 'per quarter',
|
||||
lifetime: 'one-time',
|
||||
custom: 'custom period'
|
||||
};
|
||||
return labels[billingCycle] || billingCycle;
|
||||
};
|
||||
|
||||
// Calculate donation breakdown
|
||||
const getAmountBreakdown = () => {
|
||||
if (!selectedPlan || !amountInput) return null;
|
||||
|
||||
const totalCents = Math.round(parseFloat(amountInput) * 100);
|
||||
const minimumCents = selectedPlan.minimum_price_cents || 3000;
|
||||
const donationCents = Math.max(0, totalCents - minimumCents);
|
||||
|
||||
return {
|
||||
total: totalCents,
|
||||
base: minimumCents,
|
||||
donation: donationCents
|
||||
};
|
||||
};
|
||||
|
||||
const breakdown = getAmountBreakdown();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white">
|
||||
<Navbar />
|
||||
@@ -87,75 +148,94 @@ const Plans = () => {
|
||||
</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-[#ddd8eb] hover:border-[#664fa3] hover:shadow-xl transition-all"
|
||||
data-testid={`plan-card-${plan.id}`}
|
||||
>
|
||||
{/* Plan Header */}
|
||||
<div className="text-center mb-6">
|
||||
<div className="bg-[#DDD8EB]/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-[#664fa3]" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-semibold text-[#422268] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{plan.name}
|
||||
</h2>
|
||||
{plan.description && (
|
||||
<p className="text-sm text-[#664fa3] mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{plan.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{plans.map((plan) => {
|
||||
const minimumPrice = plan.minimum_price_cents || plan.price_cents || 3000;
|
||||
const suggestedPrice = plan.suggested_price_cents || minimumPrice;
|
||||
|
||||
{/* Pricing */}
|
||||
<div className="text-center mb-8">
|
||||
<div className="text-4xl font-bold text-[#422268] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{formatPrice(plan.price_cents)}
|
||||
</div>
|
||||
<p className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{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-[#422268]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>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-[#422268]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>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-[#422268]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>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-[#422268]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Newsletter subscription</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CTA Button */}
|
||||
<Button
|
||||
onClick={() => handleSubscribe(plan.id)}
|
||||
disabled={processingPlanId === plan.id}
|
||||
className="w-full bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full py-6 text-lg font-semibold"
|
||||
data-testid={`subscribe-button-${plan.id}`}
|
||||
return (
|
||||
<Card
|
||||
key={plan.id}
|
||||
className="p-8 bg-white rounded-2xl border-2 border-[#ddd8eb] hover:border-[#664fa3] hover:shadow-xl transition-all"
|
||||
data-testid={`plan-card-${plan.id}`}
|
||||
>
|
||||
{processingPlanId === plan.id ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
|
||||
Processing...
|
||||
</>
|
||||
) : (
|
||||
'Subscribe Now'
|
||||
)}
|
||||
</Button>
|
||||
</Card>
|
||||
))}
|
||||
{/* Plan Header */}
|
||||
<div className="text-center mb-6">
|
||||
<div className="bg-[#DDD8EB]/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-[#664fa3]" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-semibold text-[#422268] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{plan.name}
|
||||
</h2>
|
||||
{plan.description && (
|
||||
<p className="text-sm text-[#664fa3] mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{plan.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Pricing */}
|
||||
<div className="text-center mb-8">
|
||||
<div className="text-sm text-[#664fa3] mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Starting at
|
||||
</div>
|
||||
<div className="text-4xl font-bold text-[#422268] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{formatPrice(minimumPrice)}
|
||||
</div>
|
||||
{suggestedPrice > minimumPrice && (
|
||||
<div className="text-sm text-[#664fa3] mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Suggested: {formatPrice(suggestedPrice)}
|
||||
</div>
|
||||
)}
|
||||
<p className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{getBillingCycleLabel(plan.billing_cycle)}
|
||||
</p>
|
||||
{plan.allow_donation && (
|
||||
<div className="mt-2 flex items-center justify-center gap-1 text-xs text-[#ff9e77]">
|
||||
<Heart className="h-3 w-3" />
|
||||
<span>Donations welcome</span>
|
||||
</div>
|
||||
)}
|
||||
</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-[#422268]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>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-[#422268]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>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-[#422268]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>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-[#422268]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Newsletter subscription</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CTA Button */}
|
||||
<Button
|
||||
onClick={() => handleSelectPlan(plan)}
|
||||
disabled={processingPlanId === plan.id}
|
||||
className="w-full bg-[#DDD8EB] text-[#422268] hover:bg-white 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...
|
||||
</>
|
||||
) : (
|
||||
'Choose Amount & Subscribe'
|
||||
)}
|
||||
</Button>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-20">
|
||||
@@ -189,6 +269,100 @@ const Plans = () => {
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Amount Selection Dialog */}
|
||||
<Dialog open={amountDialogOpen} onOpenChange={setAmountDialogOpen}>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Choose Your Amount
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{selectedPlan?.name} - {getBillingCycleLabel(selectedPlan?.billing_cycle)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Amount Input */}
|
||||
<div>
|
||||
<Label htmlFor="amount" className="text-[#422268]">
|
||||
Amount (USD) *
|
||||
</Label>
|
||||
<div className="relative mt-2">
|
||||
<span className="absolute left-4 top-1/2 transform -translate-y-1/2 text-[#664fa3] text-lg font-semibold">
|
||||
$
|
||||
</span>
|
||||
<Input
|
||||
id="amount"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min={selectedPlan ? (selectedPlan.minimum_price_cents / 100).toFixed(2) : "30.00"}
|
||||
value={amountInput}
|
||||
onChange={(e) => setAmountInput(e.target.value)}
|
||||
className="pl-8 h-14 text-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]"
|
||||
placeholder="50.00"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm text-[#664fa3] mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Minimum: {selectedPlan ? formatPrice(selectedPlan.minimum_price_cents || 3000) : '$30.00'}
|
||||
</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>
|
||||
)}
|
||||
|
||||
{/* Donation Message */}
|
||||
{selectedPlan?.allow_donation && (
|
||||
<div className="bg-[#DDD8EB]/20 rounded-lg p-4">
|
||||
<p className="text-sm text-[#422268] text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<strong>Thank you for supporting our community!</strong><br />
|
||||
Your donation helps us continue our mission and provide meaningful experiences for all members.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setAmountDialogOpen(false)}
|
||||
className="flex-1"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleCheckout}
|
||||
className="flex-1 bg-[#DDD8EB] text-[#422268] hover:bg-white"
|
||||
>
|
||||
Continue to Checkout
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,10 +5,11 @@ import { Card } from '../components/ui/card';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Input } from '../components/ui/input';
|
||||
import { Label } from '../components/ui/label';
|
||||
import { Textarea } from '../components/ui/textarea';
|
||||
import { toast } from 'sonner';
|
||||
import Navbar from '../components/Navbar';
|
||||
import MemberFooter from '../components/MemberFooter';
|
||||
import { User, Save, Lock } from 'lucide-react';
|
||||
import { User, Save, Lock, Heart, Users, Mail, BookUser } from 'lucide-react';
|
||||
import ChangePasswordDialog from '../components/ChangePasswordDialog';
|
||||
|
||||
const Profile = () => {
|
||||
@@ -17,13 +18,34 @@ const Profile = () => {
|
||||
const [profileData, setProfileData] = useState(null);
|
||||
const [passwordDialogOpen, setPasswordDialogOpen] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
// Personal Information
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
phone: '',
|
||||
address: '',
|
||||
city: '',
|
||||
state: '',
|
||||
zipcode: ''
|
||||
zipcode: '',
|
||||
// Partner Information
|
||||
partner_first_name: '',
|
||||
partner_last_name: '',
|
||||
partner_is_member: false,
|
||||
partner_plan_to_become_member: false,
|
||||
// Newsletter Preferences
|
||||
newsletter_publish_name: false,
|
||||
newsletter_publish_photo: false,
|
||||
newsletter_publish_birthday: false,
|
||||
newsletter_publish_none: false,
|
||||
// Volunteer Interests (array)
|
||||
volunteer_interests: [],
|
||||
// Member Directory Settings
|
||||
show_in_directory: false,
|
||||
directory_email: '',
|
||||
directory_bio: '',
|
||||
directory_address: '',
|
||||
directory_phone: '',
|
||||
directory_dob: '',
|
||||
directory_partner_name: ''
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -35,13 +57,34 @@ const Profile = () => {
|
||||
const response = await api.get('/users/profile');
|
||||
setProfileData(response.data);
|
||||
setFormData({
|
||||
first_name: response.data.first_name,
|
||||
last_name: response.data.last_name,
|
||||
phone: response.data.phone,
|
||||
address: response.data.address,
|
||||
city: response.data.city,
|
||||
state: response.data.state,
|
||||
zipcode: response.data.zipcode
|
||||
// Personal Information
|
||||
first_name: response.data.first_name || '',
|
||||
last_name: response.data.last_name || '',
|
||||
phone: response.data.phone || '',
|
||||
address: response.data.address || '',
|
||||
city: response.data.city || '',
|
||||
state: response.data.state || '',
|
||||
zipcode: response.data.zipcode || '',
|
||||
// Partner Information
|
||||
partner_first_name: response.data.partner_first_name || '',
|
||||
partner_last_name: response.data.partner_last_name || '',
|
||||
partner_is_member: response.data.partner_is_member || false,
|
||||
partner_plan_to_become_member: response.data.partner_plan_to_become_member || false,
|
||||
// Newsletter Preferences
|
||||
newsletter_publish_name: response.data.newsletter_publish_name || false,
|
||||
newsletter_publish_photo: response.data.newsletter_publish_photo || false,
|
||||
newsletter_publish_birthday: response.data.newsletter_publish_birthday || false,
|
||||
newsletter_publish_none: response.data.newsletter_publish_none || false,
|
||||
// Volunteer Interests
|
||||
volunteer_interests: response.data.volunteer_interests || [],
|
||||
// Member Directory Settings
|
||||
show_in_directory: response.data.show_in_directory || false,
|
||||
directory_email: response.data.directory_email || '',
|
||||
directory_bio: response.data.directory_bio || '',
|
||||
directory_address: response.data.directory_address || '',
|
||||
directory_phone: response.data.directory_phone || '',
|
||||
directory_dob: response.data.directory_dob || '',
|
||||
directory_partner_name: response.data.directory_partner_name || ''
|
||||
});
|
||||
} catch (error) {
|
||||
toast.error('Failed to load profile');
|
||||
@@ -53,6 +96,34 @@ const Profile = () => {
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (e) => {
|
||||
const { name, checked } = e.target;
|
||||
setFormData(prev => ({ ...prev, [name]: checked }));
|
||||
};
|
||||
|
||||
const handleVolunteerToggle = (interest) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
volunteer_interests: prev.volunteer_interests.includes(interest)
|
||||
? prev.volunteer_interests.filter(i => i !== interest)
|
||||
: [...prev.volunteer_interests, interest]
|
||||
}));
|
||||
};
|
||||
|
||||
// Volunteer interest options
|
||||
const volunteerOptions = [
|
||||
'Event Planning',
|
||||
'Social Media',
|
||||
'Newsletter',
|
||||
'Fundraising',
|
||||
'Community Outreach',
|
||||
'Graphic Design',
|
||||
'Photography',
|
||||
'Writing/Editing',
|
||||
'Tech Support',
|
||||
'Hospitality'
|
||||
];
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
@@ -226,15 +297,279 @@ const Profile = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full px-8 py-6 text-lg font-medium shadow-lg disabled:opacity-50"
|
||||
data-testid="save-profile-button"
|
||||
>
|
||||
<Save className="h-5 w-5 mr-2" />
|
||||
{loading ? 'Saving...' : 'Save Changes'}
|
||||
</Button>
|
||||
{/* Section 2: Partner Information */}
|
||||
<div className="pt-8 mt-8 border-t border-[#ddd8eb]">
|
||||
<h2 className="text-2xl font-semibold text-[#422268] mb-6 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Heart className="h-6 w-6 text-[#ff9e77]" />
|
||||
Partner Information
|
||||
</h2>
|
||||
<div className="space-y-6">
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label htmlFor="partner_first_name">Partner First Name</Label>
|
||||
<Input
|
||||
id="partner_first_name"
|
||||
name="partner_first_name"
|
||||
value={formData.partner_first_name}
|
||||
onChange={handleInputChange}
|
||||
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]"
|
||||
placeholder="Optional"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="partner_last_name">Partner Last Name</Label>
|
||||
<Input
|
||||
id="partner_last_name"
|
||||
name="partner_last_name"
|
||||
value={formData.partner_last_name}
|
||||
onChange={handleInputChange}
|
||||
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]"
|
||||
placeholder="Optional"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="partner_is_member"
|
||||
name="partner_is_member"
|
||||
checked={formData.partner_is_member}
|
||||
onChange={handleCheckboxChange}
|
||||
className="w-5 h-5 text-[#664fa3] border-2 border-[#ddd8eb] rounded focus:ring-[#664fa3]"
|
||||
/>
|
||||
<Label htmlFor="partner_is_member" className="cursor-pointer text-[#422268]">
|
||||
My partner is a current member
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="partner_plan_to_become_member"
|
||||
name="partner_plan_to_become_member"
|
||||
checked={formData.partner_plan_to_become_member}
|
||||
onChange={handleCheckboxChange}
|
||||
className="w-5 h-5 text-[#664fa3] border-2 border-[#ddd8eb] rounded focus:ring-[#664fa3]"
|
||||
/>
|
||||
<Label htmlFor="partner_plan_to_become_member" className="cursor-pointer text-[#422268]">
|
||||
My partner plans to become a member
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 3: Newsletter Preferences */}
|
||||
<div className="pt-8 mt-8 border-t border-[#ddd8eb]">
|
||||
<h2 className="text-2xl font-semibold text-[#422268] mb-6 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Mail className="h-6 w-6 text-[#81B29A]" />
|
||||
Newsletter Preferences
|
||||
</h2>
|
||||
<p className="text-[#664fa3] mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Choose what information you'd like published in our member newsletter.
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="newsletter_publish_name"
|
||||
name="newsletter_publish_name"
|
||||
checked={formData.newsletter_publish_name}
|
||||
onChange={handleCheckboxChange}
|
||||
className="w-5 h-5 text-[#664fa3] border-2 border-[#ddd8eb] rounded focus:ring-[#664fa3]"
|
||||
/>
|
||||
<Label htmlFor="newsletter_publish_name" className="cursor-pointer text-[#422268]">
|
||||
Publish my name
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="newsletter_publish_photo"
|
||||
name="newsletter_publish_photo"
|
||||
checked={formData.newsletter_publish_photo}
|
||||
onChange={handleCheckboxChange}
|
||||
className="w-5 h-5 text-[#664fa3] border-2 border-[#ddd8eb] rounded focus:ring-[#664fa3]"
|
||||
/>
|
||||
<Label htmlFor="newsletter_publish_photo" className="cursor-pointer text-[#422268]">
|
||||
Publish my photo
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="newsletter_publish_birthday"
|
||||
name="newsletter_publish_birthday"
|
||||
checked={formData.newsletter_publish_birthday}
|
||||
onChange={handleCheckboxChange}
|
||||
className="w-5 h-5 text-[#664fa3] border-2 border-[#ddd8eb] rounded focus:ring-[#664fa3]"
|
||||
/>
|
||||
<Label htmlFor="newsletter_publish_birthday" className="cursor-pointer text-[#422268]">
|
||||
Publish my birthday
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="newsletter_publish_none"
|
||||
name="newsletter_publish_none"
|
||||
checked={formData.newsletter_publish_none}
|
||||
onChange={handleCheckboxChange}
|
||||
className="w-5 h-5 text-[#664fa3] border-2 border-[#ddd8eb] rounded focus:ring-[#664fa3]"
|
||||
/>
|
||||
<Label htmlFor="newsletter_publish_none" className="cursor-pointer text-[#422268]">
|
||||
Do not publish any information
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 4: Volunteer Interests */}
|
||||
<div className="pt-8 mt-8 border-t border-[#ddd8eb]">
|
||||
<h2 className="text-2xl font-semibold text-[#422268] mb-6 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Users className="h-6 w-6 text-[#664fa3]" />
|
||||
Volunteer Interests
|
||||
</h2>
|
||||
<p className="text-[#664fa3] mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Select areas where you'd like to volunteer and help our community.
|
||||
</p>
|
||||
<div className="grid md:grid-cols-2 gap-3">
|
||||
{volunteerOptions.map(option => (
|
||||
<div key={option} className="flex items-center gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`volunteer_${option.replace(/\s+/g, '_').toLowerCase()}`}
|
||||
checked={formData.volunteer_interests.includes(option)}
|
||||
onChange={() => handleVolunteerToggle(option)}
|
||||
className="w-5 h-5 text-[#664fa3] border-2 border-[#ddd8eb] rounded focus:ring-[#664fa3]"
|
||||
/>
|
||||
<Label
|
||||
htmlFor={`volunteer_${option.replace(/\s+/g, '_').toLowerCase()}`}
|
||||
className="cursor-pointer text-[#422268]"
|
||||
>
|
||||
{option}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 5: Member Directory Settings */}
|
||||
<div className="pt-8 mt-8 border-t border-[#ddd8eb]">
|
||||
<h2 className="text-2xl font-semibold text-[#422268] mb-6 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<BookUser className="h-6 w-6 text-[#ff9e77]" />
|
||||
Member Directory Settings
|
||||
</h2>
|
||||
<p className="text-[#664fa3] mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Control your visibility and information in the member directory.
|
||||
</p>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-3 p-4 bg-[#f9f5ff] rounded-lg">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="show_in_directory"
|
||||
name="show_in_directory"
|
||||
checked={formData.show_in_directory}
|
||||
onChange={handleCheckboxChange}
|
||||
className="w-5 h-5 text-[#664fa3] border-2 border-[#ddd8eb] rounded focus:ring-[#664fa3]"
|
||||
/>
|
||||
<Label htmlFor="show_in_directory" className="cursor-pointer text-[#422268] font-medium">
|
||||
Include me in the member directory
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{formData.show_in_directory && (
|
||||
<div className="space-y-6 pl-4 border-l-4 border-[#DDD8EB]">
|
||||
<div>
|
||||
<Label htmlFor="directory_email">Directory Email</Label>
|
||||
<Input
|
||||
id="directory_email"
|
||||
name="directory_email"
|
||||
type="email"
|
||||
value={formData.directory_email}
|
||||
onChange={handleInputChange}
|
||||
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]"
|
||||
placeholder="Optional - email to show in directory"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="directory_bio">Bio</Label>
|
||||
<Textarea
|
||||
id="directory_bio"
|
||||
name="directory_bio"
|
||||
value={formData.directory_bio}
|
||||
onChange={handleInputChange}
|
||||
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3] min-h-[100px]"
|
||||
placeholder="Tell other members about yourself..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="directory_address">Address</Label>
|
||||
<Input
|
||||
id="directory_address"
|
||||
name="directory_address"
|
||||
value={formData.directory_address}
|
||||
onChange={handleInputChange}
|
||||
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]"
|
||||
placeholder="Optional - address to show in directory"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="directory_phone">Phone</Label>
|
||||
<Input
|
||||
id="directory_phone"
|
||||
name="directory_phone"
|
||||
type="tel"
|
||||
value={formData.directory_phone}
|
||||
onChange={handleInputChange}
|
||||
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]"
|
||||
placeholder="Optional - phone to show in directory"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="directory_dob">Date of Birth</Label>
|
||||
<Input
|
||||
id="directory_dob"
|
||||
name="directory_dob"
|
||||
type="date"
|
||||
value={formData.directory_dob}
|
||||
onChange={handleInputChange}
|
||||
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="directory_partner_name">Partner Name</Label>
|
||||
<Input
|
||||
id="directory_partner_name"
|
||||
name="directory_partner_name"
|
||||
value={formData.directory_partner_name}
|
||||
onChange={handleInputChange}
|
||||
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]"
|
||||
placeholder="Optional - partner name to show in directory"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-8 mt-8 border-t border-[#ddd8eb]">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full px-8 py-6 text-lg font-medium shadow-lg disabled:opacity-50"
|
||||
data-testid="save-profile-button"
|
||||
>
|
||||
<Save className="h-5 w-5 mr-2" />
|
||||
{loading ? 'Saving...' : 'Save Changes'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -108,11 +108,22 @@ const AdminPlans = () => {
|
||||
monthly: 'Monthly',
|
||||
quarterly: 'Quarterly',
|
||||
yearly: 'Yearly',
|
||||
lifetime: 'Lifetime'
|
||||
lifetime: 'Lifetime',
|
||||
custom: 'Custom Billing Cycle'
|
||||
};
|
||||
return labels[cycle] || cycle;
|
||||
};
|
||||
|
||||
const formatCustomCycleDates = (plan) => {
|
||||
if (!plan.custom_cycle_enabled) return null;
|
||||
|
||||
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
const startMonth = months[plan.custom_cycle_start_month - 1];
|
||||
const endMonth = months[plan.custom_cycle_end_month - 1];
|
||||
|
||||
return `${startMonth} ${plan.custom_cycle_start_day} - ${endMonth} ${plan.custom_cycle_end_day}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-8">
|
||||
@@ -212,7 +223,7 @@ const AdminPlans = () => {
|
||||
}`}
|
||||
>
|
||||
{/* Header with badges */}
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
<Badge
|
||||
className={`${
|
||||
plan.active
|
||||
@@ -228,6 +239,16 @@ const AdminPlans = () => {
|
||||
{plan.subscriber_count}
|
||||
</Badge>
|
||||
)}
|
||||
{plan.custom_cycle_enabled && (
|
||||
<Badge className="bg-[#664fa3] text-white">
|
||||
Custom Dates
|
||||
</Badge>
|
||||
)}
|
||||
{plan.allow_donation && (
|
||||
<Badge className="bg-[#ff9e77] text-white">
|
||||
Donations Enabled
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Plan Name */}
|
||||
@@ -244,25 +265,23 @@ const AdminPlans = () => {
|
||||
|
||||
{/* Price */}
|
||||
<div className="mb-4">
|
||||
<div className="text-3xl font-bold text-[#ff9e77]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{formatPrice(plan.price_cents)}
|
||||
<div className="flex items-baseline gap-2">
|
||||
<div className="text-3xl font-bold text-[#ff9e77]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{formatPrice(plan.minimum_price_cents || plan.price_cents)}
|
||||
</div>
|
||||
{plan.suggested_price_cents && plan.suggested_price_cents > plan.minimum_price_cents && (
|
||||
<div className="text-lg text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
(Suggested: {formatPrice(plan.suggested_price_cents)})
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{getBillingCycleLabel(plan.billing_cycle)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stripe Integration Status */}
|
||||
<div className="mb-6">
|
||||
{plan.stripe_price_id ? (
|
||||
<Badge className="bg-[#81B29A] text-white text-xs">
|
||||
<DollarSign className="h-3 w-3 mr-1" />
|
||||
Stripe Integrated
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge className="bg-[#DDD8EB] text-[#422268] text-xs">
|
||||
Manual/Test Plan
|
||||
</Badge>
|
||||
{plan.custom_cycle_enabled && (
|
||||
<p className="text-xs text-[#664fa3] font-mono mt-1">
|
||||
{formatCustomCycleDates(plan)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,7 +5,15 @@ import MemberFooter from '../../components/MemberFooter';
|
||||
import { Card } from '../../components/ui/card';
|
||||
import { Input } from '../../components/ui/input';
|
||||
import { Badge } from '../../components/ui/badge';
|
||||
import { User, Search, Mail, MapPin, Phone, Heart, Facebook, Instagram, Twitter, Linkedin } from 'lucide-react';
|
||||
import { Button } from '../../components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '../../components/ui/dialog';
|
||||
import { User, Search, Mail, MapPin, Phone, Heart, Facebook, Instagram, Twitter, Linkedin, UserCircle } from 'lucide-react';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
|
||||
const MembersDirectory = () => {
|
||||
@@ -13,6 +21,8 @@ const MembersDirectory = () => {
|
||||
const [filteredMembers, setFilteredMembers] = useState([]);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedMember, setSelectedMember] = useState(null);
|
||||
const [profileDialogOpen, setProfileDialogOpen] = useState(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -69,6 +79,25 @@ const MembersDirectory = () => {
|
||||
return url;
|
||||
};
|
||||
|
||||
const handleViewProfile = async (memberId) => {
|
||||
try {
|
||||
const response = await api.get(`/members/directory/${memberId}`);
|
||||
setSelectedMember(response.data);
|
||||
setProfileDialogOpen(true);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to load member profile. Please try again.",
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return null;
|
||||
return new Date(dateString).toLocaleDateString('en-US', { month: 'long', day: 'numeric' });
|
||||
};
|
||||
|
||||
const MemberCard = ({ member }) => (
|
||||
<Card className="p-6 bg-white rounded-2xl border border-[#ddd8eb] hover:shadow-lg transition-all h-full">
|
||||
{/* Profile Photo */}
|
||||
@@ -202,6 +231,17 @@ const MembersDirectory = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* View Profile Button */}
|
||||
<div className="pt-4 mt-4 border-t border-[#ddd8eb]">
|
||||
<Button
|
||||
onClick={() => handleViewProfile(member.id)}
|
||||
className="w-full bg-[#DDD8EB] text-[#422268] hover:bg-[#664fa3] hover:text-white rounded-full py-5"
|
||||
>
|
||||
<UserCircle className="h-4 w-4 mr-2" />
|
||||
View Full Profile
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -287,6 +327,192 @@ const MembersDirectory = () => {
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Profile Detail Dialog */}
|
||||
<Dialog open={profileDialogOpen} onOpenChange={setProfileDialogOpen}>
|
||||
<DialogContent className="sm:max-w-[600px] bg-white rounded-2xl max-h-[90vh] overflow-y-auto">
|
||||
{selectedMember && (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-3xl font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{selectedMember.first_name} {selectedMember.last_name}
|
||||
</DialogTitle>
|
||||
{selectedMember.directory_partner_name && (
|
||||
<DialogDescription className="flex items-center gap-2 text-lg" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<Heart className="h-5 w-5 text-[#ff9e77]" />
|
||||
<span className="text-[#664fa3]">Partner: {selectedMember.directory_partner_name}</span>
|
||||
</DialogDescription>
|
||||
)}
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6 py-4">
|
||||
{/* Bio */}
|
||||
{selectedMember.directory_bio && (
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-[#422268] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
About
|
||||
</h3>
|
||||
<p className="text-[#664fa3] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{selectedMember.directory_bio}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Contact Information */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-[#422268] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Contact Information
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{selectedMember.directory_email && (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-[#F8F7FB]">
|
||||
<Mail className="h-5 w-5 text-[#664fa3]" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-[#664fa3] mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Email</p>
|
||||
<a
|
||||
href={`mailto:${selectedMember.directory_email}`}
|
||||
className="text-[#422268] hover:text-[#664fa3] font-medium"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
>
|
||||
{selectedMember.directory_email}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedMember.directory_phone && (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-[#F8F7FB]">
|
||||
<Phone className="h-5 w-5 text-[#664fa3]" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-[#664fa3] mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Phone</p>
|
||||
<a
|
||||
href={`tel:${selectedMember.directory_phone}`}
|
||||
className="text-[#422268] hover:text-[#664fa3] font-medium"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
>
|
||||
{selectedMember.directory_phone}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedMember.directory_address && (
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="p-2 rounded-lg bg-[#F8F7FB]">
|
||||
<MapPin className="h-5 w-5 text-[#664fa3]" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-[#664fa3] mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Address</p>
|
||||
<p className="text-[#422268] font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{selectedMember.directory_address}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedMember.directory_dob && (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-[#F8F7FB]">
|
||||
<Heart className="h-5 w-5 text-[#ff9e77]" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-[#664fa3] mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Birthday</p>
|
||||
<p className="text-[#422268] font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{formatDate(selectedMember.directory_dob)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Volunteer Interests */}
|
||||
{selectedMember.volunteer_interests && selectedMember.volunteer_interests.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-[#422268] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Volunteer Interests
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedMember.volunteer_interests.map((interest, index) => (
|
||||
<Badge
|
||||
key={index}
|
||||
className="bg-[#DDD8EB] text-[#422268] hover:bg-[#664fa3] hover:text-white"
|
||||
>
|
||||
{interest}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Social Media */}
|
||||
{(selectedMember.social_media_facebook || selectedMember.social_media_instagram ||
|
||||
selectedMember.social_media_twitter || selectedMember.social_media_linkedin) && (
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-[#422268] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Connect on Social Media
|
||||
</h3>
|
||||
<div className="flex gap-3">
|
||||
{selectedMember.social_media_facebook && (
|
||||
<a
|
||||
href={getSocialMediaLink(selectedMember.social_media_facebook)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-3 rounded-lg bg-[#F8F7FB] hover:bg-[#DDD8EB] transition-colors"
|
||||
title="Facebook"
|
||||
>
|
||||
<Facebook className="h-6 w-6 text-[#1877F2]" />
|
||||
</a>
|
||||
)}
|
||||
|
||||
{selectedMember.social_media_instagram && (
|
||||
<a
|
||||
href={getSocialMediaLink(selectedMember.social_media_instagram)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-3 rounded-lg bg-[#F8F7FB] hover:bg-[#DDD8EB] transition-colors"
|
||||
title="Instagram"
|
||||
>
|
||||
<Instagram className="h-6 w-6 text-[#E4405F]" />
|
||||
</a>
|
||||
)}
|
||||
|
||||
{selectedMember.social_media_twitter && (
|
||||
<a
|
||||
href={getSocialMediaLink(selectedMember.social_media_twitter)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-3 rounded-lg bg-[#F8F7FB] hover:bg-[#DDD8EB] transition-colors"
|
||||
title="Twitter/X"
|
||||
>
|
||||
<Twitter className="h-6 w-6 text-[#1DA1F2]" />
|
||||
</a>
|
||||
)}
|
||||
|
||||
{selectedMember.social_media_linkedin && (
|
||||
<a
|
||||
href={getSocialMediaLink(selectedMember.social_media_linkedin)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-3 rounded-lg bg-[#F8F7FB] hover:bg-[#DDD8EB] transition-colors"
|
||||
title="LinkedIn"
|
||||
>
|
||||
<Linkedin className="h-6 w-6 text-[#0A66C2]" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<MemberFooter />
|
||||
</div>
|
||||
);
|
||||
|
||||