diff --git a/public/history-arden-charlotte.png b/public/history-arden-charlotte.png new file mode 100644 index 0000000..b13d4cd Binary files /dev/null and b/public/history-arden-charlotte.png differ diff --git a/public/history-part3.png b/public/history-part3.png new file mode 100644 index 0000000..891e5a4 Binary files /dev/null and b/public/history-part3.png differ diff --git a/public/history-part7.png b/public/history-part7.png new file mode 100644 index 0000000..503ce4f Binary files /dev/null and b/public/history-part7.png differ diff --git a/public/history-pride-1.png b/public/history-pride-1.png new file mode 100644 index 0000000..316aa64 Binary files /dev/null and b/public/history-pride-1.png differ diff --git a/public/history-pride-2.png b/public/history-pride-2.png new file mode 100644 index 0000000..6c1780f Binary files /dev/null and b/public/history-pride-2.png differ diff --git a/public/loaf-hearts.png b/public/loaf-hearts.png new file mode 100644 index 0000000..7b6218d Binary files /dev/null and b/public/loaf-hearts.png differ diff --git a/public/zelle-logo.png b/public/zelle-logo.png new file mode 100644 index 0000000..f8fed1f Binary files /dev/null and b/public/zelle-logo.png differ diff --git a/src/App.js b/src/App.js index cb100e8..c48718b 100644 --- a/src/App.js +++ b/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() { } /> } /> + {/* About Us Pages - Public Access */} + } /> + } /> + } /> + + {/* Donation Page - Public Access */} + } /> + diff --git a/src/components/Navbar.js b/src/components/Navbar.js index 806b2e1..0904c09 100644 --- a/src/components/Navbar.js +++ b/src/components/Navbar.js @@ -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 - + + + + + + History + + + + + Mission and Values + + + + + Board of Directors + + + + { 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 }) => { - {plans.map(plan => ( - - {plan.name} - ${(plan.price_cents / 100).toFixed(2)}/{plan.billing_cycle} - - ))} + {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 ( + + {plan.name} - ${minPrice.toFixed(2)}{sugPrice && sugPrice > minPrice ? ` (Suggested: $${sugPrice.toFixed(2)})` : ''}/{plan.billing_cycle} + + ); + })} {selectedPlan && ( @@ -186,11 +225,38 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => { className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" required /> -

- Amount can differ from plan price if offering a discount or partial payment -

+ {selectedPlan && ( +

+ Minimum: {formatPrice(selectedPlan.minimum_price_cents || selectedPlan.price_cents || 3000)} +

+ )} + {/* Breakdown Display */} + {breakdown && breakdown.total >= breakdown.base && ( + +
+
+ Membership Fee: + {formatPrice(breakdown.base)} +
+ {breakdown.donation > 0 && ( +
+ + + Additional Donation: + + {formatPrice(breakdown.donation)} +
+ )} +
+ Total: + {formatPrice(breakdown.total)} +
+
+
+ )} + {/* Payment Date */}
) : ( selectedPlan && ( -

- Will use plan's billing cycle: {selectedPlan.billing_cycle} -
- 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 -

+
+ {selectedPlan.custom_cycle_enabled ? ( + <> +

+ Plan uses custom billing cycle: +
+ {(() => { + 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)`; + })()} +

+

+ Subscription will end on the upcoming cycle end date based on today's date. +

+ + ) : ( +

+ Will use plan's billing cycle: {selectedPlan.billing_cycle} +
+ 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 +

+ )} +
) )} diff --git a/src/components/PlanDialog.js b/src/components/PlanDialog.js index 3124ba6..72c06a0 100644 --- a/src/components/PlanDialog.js +++ b/src/components/PlanDialog.js @@ -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 ( - + {plan ? 'Edit Plan' : 'Create New Plan'} @@ -129,57 +196,167 @@ const PlanDialog = ({ open, onOpenChange, plan, onSuccess }) => { /> - {/* Price & Billing Cycle - Side by side */} -
-
- - + {/* Dynamic Pricing */} +
+

+ Dynamic Pricing +

+ +
+
+ + handlePriceChange('minimum_price_cents', e.target.value)} + placeholder="30.00" + required + className="mt-2" + /> +

Minimum $30

+
+ +
+ + handlePriceChange('suggested_price_cents', e.target.value)} + placeholder="50.00" + required + className="mt-2" + /> +

Pre-filled amount

+
-
- - + {/* Allow Donation Toggle */} +
+
+ +

+ Members can pay more than minimum +

+
+
- {/* Stripe Price ID */} + {/* Billing Cycle */}
- - setFormData({ ...formData, stripe_price_id: e.target.value })} - placeholder="price_xxxxxxxxxxxxx" - className="mt-2 font-mono text-sm" - /> -

- Optional. Leave empty for manual/test plans. -

+ +
+ {/* Custom Billing Cycle Dates */} + {formData.billing_cycle === 'custom' && ( +
+

+ Custom Billing Period +

+

+ Set recurring date range (e.g., Jan 1 - Dec 31 for calendar year) +

+ +
+
+ +
+ + + setFormData({ ...formData, custom_cycle_start_day: parseInt(e.target.value) || 1 })} + placeholder="Day" + /> +
+
+ +
+ +
+ + + setFormData({ ...formData, custom_cycle_end_day: parseInt(e.target.value) || 31 })} + placeholder="Day" + /> +
+
+
+ +
+

+ Example: Jan 1 - Dec 31 for calendar year, or Jul 1 - Jun 30 for fiscal year +

+
+
+ )} + {/* Active Toggle */}
diff --git a/src/components/PublicFooter.js b/src/components/PublicFooter.js index e191dea..f02f7ba 100644 --- a/src/components/PublicFooter.js +++ b/src/components/PublicFooter.js @@ -18,9 +18,9 @@ const PublicFooter = () => {

About

- History - Mission and Values - Board of Directors + History + Mission and Values + Board of Directors
@@ -32,9 +32,11 @@ const PublicFooter = () => {
- + + +

LOAF is supported by
the Hollyfield Foundation diff --git a/src/components/PublicNavbar.js b/src/components/PublicNavbar.js index d6567fb..e5fa8b0 100644 --- a/src/components/PublicNavbar.js +++ b/src/components/PublicNavbar.js @@ -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 )} - + + + + + + History + + + + + Mission and Values + + + + + Board of Directors + + + + { + 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 ( +

+ + +
+ {/* Hero Section with Contact */} +
+
+

+ LOAF Board of Directors 2025 +

+

+ For any questions or inquiries please email us at{' '} + + info@loaftx.org + +

+
+
+ + {/* Officers Grid */} +
+
+

+ Officers +

+
+ {officers.map((officer, index) => ( + +

+ {officer.name} +

+

+ {officer.title} +

+
+ ))} +
+
+
+ + {/* Board Members Grid */} +
+
+

+ Board of Directors +

+
+ {boardMembers.map((member, index) => ( + +

+ {member} +

+
+ ))} +
+
+
+ + {/* Join the Board Section */} +
+
+

+ Join the Board of Directors +

+

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

+ +
    +
  1. + Nominations are due by November 1. Nomination Form:{' '} + + Click Here + +
  2. +
  3. Nominees must have been a member for at least 1 year and current with their dues.
  4. +
  5. Officer positions are only available to current directors.
  6. +
  7. Each director shall serve a 2 year term.
  8. +
  9. The time commitment is 1-2 hours per week.
  10. +
  11. The tasks that directors perform depend on individual interests, skills, and time available.
  12. +
  13. Directors must attend Board meetings which are held the second Thursday of each month at 6:30pm via Zoom.
  14. +
  15. We are a fun group, and we would love for you to join us in providing this service for our community.
  16. +
+
+
+
+
+ + +
+ ); +}; + +export default BoardOfDirectors; diff --git a/src/pages/Donate.js b/src/pages/Donate.js new file mode 100644 index 0000000..1ce1024 --- /dev/null +++ b/src/pages/Donate.js @@ -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 ( +
+ + +
+ {/* Hero Section */} +
+
+
+ Hearts e.target.style.display = 'none'} /> +
+

+ Donate +

+

+ We really appreciate your donations. You can make your donation online + or send a check by mail. +

+
+
+ + {/* Donation Amount Buttons */} +
+
+ +
+ +

+ Select Your Donation Amount +

+
+ + {/* Donation Buttons Grid */} +
+ {[25, 50, 100, 250].map(amount => ( + + ))} +
+ + {/* Custom Amount Button */} + + +

+ Online donations coming soon +

+
+
+
+ + {/* Alternative Payment Methods */} +
+
+
+ {/* Mail Check */} + + +

+ Mail a Check +

+

+ Our mailing address for checks:
+ LOAF
+ P.O. Box 7207
+ Houston, Texas 77248-7207 +

+
+ + {/* Zelle */} + +
+ Zelle e.target.style.display = 'none'} /> +
+

+ Pay with Zelle +

+

+ If your bank allows the use of Zelle, please feel free to send money to: +

+ + LOAFHoustonTX@gmail.com + +
+
+
+
+
+ + +
+ ); +}; + +export default Donate; diff --git a/src/pages/History.js b/src/pages/History.js new file mode 100644 index 0000000..7a14b30 --- /dev/null +++ b/src/pages/History.js @@ -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 ( +
+ + +
+ {/* Hero Section */} +
+
+

+ History of LOAF +

+
+ +

+ By Arden Eversmeyer +

+
+
+
+ + {/* Part 1 - With Image */} +
+
+ +
+
+ Arden Eversmeyer and Charlotte Avery e.target.style.display = 'none'} /> +

+ Arden Eversmeyer and Charlotte Avery +

+
+
+

+ Part 1 +

+

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

+

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

+
+
+
+
+
+ + {/* Part 2 */} +
+
+ +

+ Part 2 +

+

+ 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." +

+

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

+
    +
  • AGE OF PARTICIPANTS - 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.
  • +
  • NAME FOR THE GROUP - LOAFF and then LOAF
  • +
  • NUMBER OF EVENTS - 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.
  • +
  • TYPES OF EVENTS - 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.
  • +
+
+
+
+ + {/* Part 3 - With Image */} +
+
+ +
+
+

+ Part 3 +

+

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

+

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

+
+
+ LOAF Community e.target.style.display = 'none'} /> +

+ LOAF Community +

+
+
+
+
+
+ + {/* Part 4 - With Image */} +
+
+ +
+
+

+ Part 4 +

+

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

+

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

+
+
+ Pride Parade e.target.style.display = 'none'} /> + Pride Parade e.target.style.display = 'none'} /> +

+ LOAF at Pride +

+
+
+
+
+
+ + {/* Part 5 */} +
+
+ +

+ Part 5 +

+

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

+

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

+
+
+
+ + {/* Part 6 */} +
+
+ +

+ Part 6 +

+

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

+

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

+
+
+
+ + {/* Part 7 - With Image */} +
+
+ +
+
+ LOAF Members e.target.style.display = 'none'} /> +

+ LOAF Members +

+
+
+

+ Part 7 +

+

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

+

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

+
+
+
+
+
+ + {/* Part 8 */} +
+
+ +

+ Part 8 +

+

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

+

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

+

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

+
+
+
+ + {/* CTA Section */} +
+
+
+ +

+ A Life Remembered +

+

+ Check out "A Life Remembered", a tribute dedicated to Arden Eversmeyer, one of the founding mothers of LOAF. +

+ + + +
+ + +

+ The Old Lesbian Oral Herstory Project +

+

+ Arden Eversmeyer was also involved with The Old Lesbian Oral Herstory Project, preserving the stories of old lesbians. +

+ + + +
+
+
+
+
+ + +
+ ); +}; + +export default History; diff --git a/src/pages/MissionValues.js b/src/pages/MissionValues.js new file mode 100644 index 0000000..664cff7 --- /dev/null +++ b/src/pages/MissionValues.js @@ -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 ( +
+ + +
+
+
+ {/* Left Card - Mission (Purple Gradient) */} + +

+ LOAF Mission +

+

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

+
+ LOAF Logo +
+
+ + {/* Right Card - Values */} + +

+ LOAF Values +

+
    +
  1. Safe environments for lesbians to gather for a variety of social activities and interaction.
  2. +
  3. Social support for lesbians.
  4. +
  5. Diversity and inclusivity.
  6. +
  7. The varying social needs of lesbians between the ages of 50 and 99+.
  8. +
  9. Collective wisdom and feedback from the membership.
  10. +
  11. Educational programs on topics that are important to the membership.
  12. +
  13. Safe environments for women who are exploring their sexuality.
  14. +
  15. Alleviating internalized homophobia.
  16. +
+
+
+
+
+ + +
+ ); +}; + +export default MissionValues; diff --git a/src/pages/Plans.js b/src/pages/Plans.js index e969e97..d9c88f3 100644 --- a/src/pages/Plans.js +++ b/src/pages/Plans.js @@ -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 (
@@ -87,75 +148,94 @@ const Plans = () => {
) : plans.length > 0 ? (
- {plans.map((plan) => ( - - {/* Plan Header */} -
-
- -
-

- {plan.name} -

- {plan.description && ( -

- {plan.description} -

- )} -
+ {plans.map((plan) => { + const minimumPrice = plan.minimum_price_cents || plan.price_cents || 3000; + const suggestedPrice = plan.suggested_price_cents || minimumPrice; - {/* Pricing */} -
-
- {formatPrice(plan.price_cents)} -
-

- {getBillingCycleLabel(plan.billing_cycle)} -

-
- - {/* Features */} -
-
- - Access to all member events -
-
- - Community directory access -
-
- - Exclusive member benefits -
-
- - Newsletter subscription -
-
- - {/* CTA Button */} - -
- ))} + {/* Plan Header */} +
+
+ +
+

+ {plan.name} +

+ {plan.description && ( +

+ {plan.description} +

+ )} +
+ + {/* Pricing */} +
+
+ Starting at +
+
+ {formatPrice(minimumPrice)} +
+ {suggestedPrice > minimumPrice && ( +
+ Suggested: {formatPrice(suggestedPrice)} +
+ )} +

+ {getBillingCycleLabel(plan.billing_cycle)} +

+ {plan.allow_donation && ( +
+ + Donations welcome +
+ )} +
+ + {/* Features */} +
+
+ + Access to all member events +
+
+ + Community directory access +
+
+ + Exclusive member benefits +
+
+ + Newsletter subscription +
+
+ + {/* CTA Button */} + + + ); + })}
) : (
@@ -189,6 +269,100 @@ const Plans = () => {
+ + {/* Amount Selection Dialog */} + + + + + Choose Your Amount + + + {selectedPlan?.name} - {getBillingCycleLabel(selectedPlan?.billing_cycle)} + + + +
+ {/* Amount Input */} +
+ +
+ + $ + + setAmountInput(e.target.value)} + className="pl-8 h-14 text-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" + placeholder="50.00" + /> +
+

+ Minimum: {selectedPlan ? formatPrice(selectedPlan.minimum_price_cents || 3000) : '$30.00'} +

+
+ + {/* Breakdown Display */} + {breakdown && breakdown.total >= breakdown.base && ( + +
+
+ Membership Fee: + {formatPrice(breakdown.base)} +
+ {breakdown.donation > 0 && ( +
+ + + Additional Donation: + + {formatPrice(breakdown.donation)} +
+ )} +
+ Total: + {formatPrice(breakdown.total)} +
+
+
+ )} + + {/* Donation Message */} + {selectedPlan?.allow_donation && ( +
+

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

+
+ )} +
+ + + + + +
+
); }; diff --git a/src/pages/Profile.js b/src/pages/Profile.js index 3f69e74..0fe551f 100644 --- a/src/pages/Profile.js +++ b/src/pages/Profile.js @@ -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 = () => {
- + {/* Section 2: Partner Information */} +
+

+ + Partner Information +

+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + {/* Section 3: Newsletter Preferences */} +
+

+ + Newsletter Preferences +

+

+ Choose what information you'd like published in our member newsletter. +

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + {/* Section 4: Volunteer Interests */} +
+

+ + Volunteer Interests +

+

+ Select areas where you'd like to volunteer and help our community. +

+
+ {volunteerOptions.map(option => ( +
+ handleVolunteerToggle(option)} + className="w-5 h-5 text-[#664fa3] border-2 border-[#ddd8eb] rounded focus:ring-[#664fa3]" + /> + +
+ ))} +
+
+ + {/* Section 5: Member Directory Settings */} +
+

+ + Member Directory Settings +

+

+ Control your visibility and information in the member directory. +

+ +
+
+ + +
+ + {formData.show_in_directory && ( +
+
+ + +
+ +
+ +