diff --git a/README.md b/README.md index 3c2f651..c620aaf 100644 --- a/README.md +++ b/README.md @@ -999,3 +999,182 @@ api.interceptors.response.use( **Last Updated**: December 18, 2024 **Version**: 1.0.0 **Maintainer**: LOAF Development Team + +**Backend API** + +**Auth** +- POST `/api/auth/register` +- GET `/api/auth/verify-email` +- POST `/api/auth/resend-verification-email` +- POST `/api/auth/login` +- POST `/api/auth/forgot-password` +- POST `/api/auth/reset-password` +- GET `/api/auth/me` +- GET `/api/auth/permissions` + +**Users** +- PUT `/api/users/change-password` +- GET `/api/users/profile` +- PUT `/api/users/profile` + +**Members** +- GET `/api/members/directory` (defined twice in code) +- GET `/api/members/directory/{user_id}` +- GET `/api/members/profile` +- PUT `/api/members/profile` +- POST `/api/members/profile/upload-photo` +- DELETE `/api/members/profile/delete-photo` +- GET `/api/members/calendar/events` +- GET `/api/members/gallery` +- GET `/api/members/event-activity` + +**Events (public/member)** +- GET `/api/events` +- GET `/api/events/{event_id}` +- GET `/api/events/{event_id}/gallery` +- POST `/api/events/{event_id}/rsvp` +- GET `/api/events/{event_id}/download.ics` + +**Calendars** +- GET `/api/calendars/subscribe.ics` +- GET `/api/calendars/all-events.ics` + +**Newsletters (public)** +- GET `/api/newsletters` +- GET `/api/newsletters/years` + +**Financials (public)** +- GET `/api/financials` + +**Bylaws (public)** +- GET `/api/bylaws/current` +- GET `/api/bylaws/history` + +**Config/Diagnostics** +- GET `/api/config` +- GET `/api/config/limits` +- GET `/api/diagnostics/cors` + +**Invitations** +- GET `/api/invitations/verify/{token}` +- POST `/api/invitations/accept` + +**Subscriptions** +- GET `/api/subscriptions/plans` +- POST `/api/subscriptions/checkout` + +**Donations** +- POST `/api/donations/checkout` + +**Contact** +- POST `/api/contact` + +**Admin – Calendar** +- POST `/api/admin/calendar/sync/{event_id}` +- DELETE `/api/admin/calendar/unsync/{event_id}` + +**Admin – Event Gallery** +- POST `/api/admin/events/{event_id}/gallery` +- DELETE `/api/admin/event-gallery/{image_id}` +- PUT `/api/admin/event-gallery/{image_id}` + +**Admin – Events** +- POST `/api/admin/events` +- PUT `/api/admin/events/{event_id}` +- GET `/api/admin/events/{event_id}` +- GET `/api/admin/events/{event_id}/rsvps` +- PUT `/api/admin/events/{event_id}/attendance` +- GET `/api/admin/events` +- DELETE `/api/admin/events/{event_id}` + +**Admin – Storage** +- GET `/api/admin/storage/usage` +- GET `/api/admin/storage/breakdown` + +**Admin – Users & Invitations** +- GET `/api/admin/users` +- GET `/api/admin/users/invitations` +- GET `/api/admin/users/export` +- GET `/api/admin/users/{user_id}` +- PUT `/api/admin/users/{user_id}` +- PUT `/api/admin/users/{user_id}/validate` +- PUT `/api/admin/users/{user_id}/status` +- POST `/api/admin/users/{user_id}/reject` +- POST `/api/admin/users/{user_id}/activate-payment` +- PUT `/api/admin/users/{user_id}/reset-password` +- PUT `/api/admin/users/{user_id}/role` +- POST `/api/admin/users/{user_id}/resend-verification` +- POST `/api/admin/users/{user_id}/upload-photo` +- DELETE `/api/admin/users/{user_id}/delete-photo` +- POST `/api/admin/users/create` +- POST `/api/admin/users/invite` +- POST `/api/admin/users/invitations/{invitation_id}/resend` +- DELETE `/api/admin/users/invitations/{invitation_id}` +- POST `/api/admin/users/import` +- GET `/api/admin/users/import-jobs` +- GET `/api/admin/users/import-jobs/{job_id}` + +**Admin – Imports** +- POST `/api/admin/import/upload-csv` +- GET `/api/admin/import/{job_id}/preview` +- POST `/api/admin/import/{job_id}/execute` +- POST `/api/admin/import/{job_id}/rollback` +- GET `/api/admin/import/{job_id}/status` +- GET `/api/admin/import/{job_id}/errors/download` + +**Admin – Subscriptions** +- GET `/api/admin/subscriptions/plans` +- GET `/api/admin/subscriptions/plans/{plan_id}` +- POST `/api/admin/subscriptions/plans` +- PUT `/api/admin/subscriptions/plans/{plan_id}` +- DELETE `/api/admin/subscriptions/plans/{plan_id}` +- GET `/api/admin/subscriptions` +- GET `/api/admin/subscriptions/stats` +- PUT `/api/admin/subscriptions/{subscription_id}` +- POST `/api/admin/subscriptions/{subscription_id}/cancel` +- GET `/api/admin/subscriptions/export` + +**Admin – Donations** +- GET `/api/admin/donations` +- GET `/api/admin/donations/stats` +- GET `/api/admin/donations/export` + +**Admin – Newsletters** +- POST `/api/admin/newsletters` +- PUT `/api/admin/newsletters/{newsletter_id}` +- DELETE `/api/admin/newsletters/{newsletter_id}` + +**Admin – Financials** +- POST `/api/admin/financials` +- PUT `/api/admin/financials/{report_id}` +- DELETE `/api/admin/financials/{report_id}` + +**Admin – Bylaws** +- POST `/api/admin/bylaws` +- PUT `/api/admin/bylaws/{bylaws_id}` +- DELETE `/api/admin/bylaws/{bylaws_id}` + +**Admin – Roles** +- GET `/api/admin/roles` +- GET `/api/admin/roles/assignable` +- POST `/api/admin/roles` +- GET `/api/admin/roles/{role_id}` +- PUT `/api/admin/roles/{role_id}` +- DELETE `/api/admin/roles/{role_id}` +- GET `/api/admin/roles/{role_id}/permissions` +- PUT `/api/admin/roles/{role_id}/permissions` + +**Admin – Permissions** +- GET `/api/admin/permissions` +- GET `/api/admin/permissions/modules` +- GET `/api/admin/permissions/roles/{role}` +- PUT `/api/admin/permissions/roles/{role}` +- POST `/api/admin/permissions/seed` + +**Admin – Stripe Settings** +- GET `/api/admin/settings/stripe/status` +- POST `/api/admin/settings/stripe/test-connection` +- PUT `/api/admin/settings/stripe` + +**Webhooks** +- POST `/api/webhooks/stripe` \ No newline at end of file diff --git a/package.json b/package.json index 5663868..13ec4bf 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@radix-ui/react-tooltip": "^1.2.4", "@stripe/react-stripe-js": "^2.0.0", "@stripe/stripe-js": "^2.0.0", + "@tailwindcss/line-clamp": "^0.4.4", "axios": "^1.8.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/src/App.js b/src/App.js index 7023eb9..82e87c1 100644 --- a/src/App.js +++ b/src/App.js @@ -23,6 +23,7 @@ import AdminStaff from './pages/admin/AdminStaff'; import AdminMembers from './pages/admin/AdminMembers'; import AdminPermissions from './pages/admin/AdminPermissions'; import AdminSettings from './pages/admin/AdminSettings'; +import AdminMemberTiers from './pages/admin/AdminMemberTiers'; import AdminRoles from './pages/admin/AdminRoles'; import AdminEvents from './pages/admin/AdminEvents'; import AdminEventAttendance from './pages/admin/AdminEventAttendance'; @@ -31,6 +32,7 @@ import AdminPlans from './pages/admin/AdminPlans'; import AdminSubscriptions from './pages/admin/AdminSubscriptions'; import AdminDonations from './pages/admin/AdminDonations'; import AdminLayout from './layouts/AdminLayout'; +import SettingsLayout from './layouts/SettingsLayout'; import { AuthProvider, useAuth } from './context/AuthContext'; import MemberRoute from './components/MemberRoute'; import MemberCalendar from './pages/members/MemberCalendar'; @@ -286,18 +288,21 @@ function App() { } /> - - - + } /> - + - } /> + }> + } /> + } /> + } /> + } /> + {/* 404 - Catch all undefined routes */} } /> diff --git a/src/components/AdminSidebar.js b/src/components/AdminSidebar.js index 12401d5..415f015 100644 --- a/src/components/AdminSidebar.js +++ b/src/components/AdminSidebar.js @@ -169,13 +169,7 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => { path: '/admin/bylaws', disabled: false }, - { - name: 'Permissions', - icon: Shield, - path: '/admin/permissions', - disabled: false, - superadminOnly: true - }, + { name: 'Settings', icon: Settings, diff --git a/src/components/ChangePasswordDialog.js b/src/components/ChangePasswordDialog.js index 916bdf1..4530e03 100644 --- a/src/components/ChangePasswordDialog.js +++ b/src/components/ChangePasswordDialog.js @@ -66,7 +66,7 @@ const ChangePasswordDialog = ({ open, onOpenChange }) => { return ( - +
diff --git a/src/components/ChangeRoleDialog.js b/src/components/ChangeRoleDialog.js index dafa8d3..736a005 100644 --- a/src/components/ChangeRoleDialog.js +++ b/src/components/ChangeRoleDialog.js @@ -77,7 +77,7 @@ export default function ChangeRoleDialog({ open, onClose, user, onSuccess }) { return ( - + diff --git a/src/components/CreateMemberDialog.js b/src/components/CreateMemberDialog.js index 02b077e..4d3308b 100644 --- a/src/components/CreateMemberDialog.js +++ b/src/components/CreateMemberDialog.js @@ -181,7 +181,7 @@ const CreateMemberDialog = ({ open, onOpenChange, onSuccess }) => { value={formData.first_name} onChange={(e) => handleChange('first_name', e.target.value)} className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple " - placeholder="John" + placeholder="Jane" /> {errors.first_name && (

{errors.first_name}

diff --git a/src/components/CreateStaffDialog.js b/src/components/CreateStaffDialog.js index 166806f..01f36c4 100644 --- a/src/components/CreateStaffDialog.js +++ b/src/components/CreateStaffDialog.js @@ -106,7 +106,7 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => { return ( - + @@ -165,7 +165,7 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => { value={formData.first_name} onChange={(e) => handleChange('first_name', e.target.value)} className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple " - placeholder="John" + placeholder="Jane" /> {errors.first_name && (

{errors.first_name}

diff --git a/src/components/InviteStaffDialog.js b/src/components/InviteStaffDialog.js index 17e62e9..a37276e 100644 --- a/src/components/InviteStaffDialog.js +++ b/src/components/InviteStaffDialog.js @@ -123,7 +123,7 @@ const InviteStaffDialog = ({ open, onOpenChange, onSuccess }) => { return ( - + @@ -196,7 +196,7 @@ const InviteStaffDialog = ({ open, onOpenChange, onSuccess }) => { value={formData.first_name} onChange={(e) => handleChange('first_name', e.target.value)} className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple " - placeholder="John" + placeholder="Jane" />
diff --git a/src/components/MemberBadge.js b/src/components/MemberBadge.js new file mode 100644 index 0000000..a7e1387 --- /dev/null +++ b/src/components/MemberBadge.js @@ -0,0 +1,19 @@ +// src/components/MemberBadge.js +import React from 'react'; +import { Badge } from './ui/badge'; +import { getTierForMember } from '../utils/member-tiers'; +import { MEMBER_TIER_ICONS } from '../config/memberTierIcons'; + +const MemberBadge = ({ memberSince, tiers }) => { + const tier = getTierForMember(memberSince, tiers); + const Icon = MEMBER_TIER_ICONS[tier.icon] || MEMBER_TIER_ICONS.FaUser; + + return ( + + + {tier.label} + + ); +}; + +export default MemberBadge; \ No newline at end of file diff --git a/src/components/MemberCard.js b/src/components/MemberCard.js new file mode 100644 index 0000000..40bb13f --- /dev/null +++ b/src/components/MemberCard.js @@ -0,0 +1,186 @@ +import React from 'react' +import { Card } from './ui/card'; +import { Button } from './ui/button'; +import { Heart, Calendar, Mail, Phone, MapPin, Facebook, Instagram, Twitter, Linkedin, UserCircle } from 'lucide-react'; + +// Helper function to get initials +const getInitials = (firstName, lastName) => { + return `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase(); +}; + +// Helper function to ensure social media URLs have proper protocol +const getSocialMediaLink = (url) => { + if (!url) return null; + if (!url.startsWith('http://') && !url.startsWith('https://')) { + return `https://${url}`; + } + return url; +}; + +const MemberCard = ({ member, onViewProfile }) => { + const joinedDate = member.created_at; + return ( + + {/* Profile Photo */} +
+ member since badge +
+
+ {member.profile_photo_url ? ( + {`${member.first_name} + ) : ( +
+ + {getInitials(member.first_name, member.last_name)} + +
+ )} +
+ + {/* Name */} +

+ {member.first_name} {member.last_name} +

+ + {/* Partner Name */} + {member.directory_partner_name && ( +
+ + + Partner: {member.directory_partner_name} + +
+ )} + + {/* Bio */} + {member.directory_bio && ( +

+ {member.directory_bio} +

+ )} + + {/* Member Since */} + {joinedDate && ( +
+ + + Member since {new Date(joinedDate).toLocaleDateString('en-US', { + month: 'long', + year: 'numeric' + })} + +
+ )} + + {/* Contact Information */} +
+ {member.directory_email && ( + + )} + + {member.directory_phone && ( + + )} + + {member.directory_address && ( +
+ + + {member.directory_address} + +
+ )} +
+ + {/* Social Media Links */} + {(member.social_media_facebook || member.social_media_instagram || member.social_media_twitter || member.social_media_linkedin) && ( +
+
+ {member.social_media_facebook && ( + + + + )} + + {member.social_media_instagram && ( + + + + )} + + {member.social_media_twitter && ( + + + + )} + + {member.social_media_linkedin && ( + + + + )} +
+
+ )} + + {/* View Profile Button */} +
+ +
+
+ ); +}; + +export default MemberCard diff --git a/src/components/PaymentActivationDialog.js b/src/components/PaymentActivationDialog.js index 34074ac..aeab9c9 100644 --- a/src/components/PaymentActivationDialog.js +++ b/src/components/PaymentActivationDialog.js @@ -156,8 +156,8 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => { if (!user) return null; return ( - - + + Activate Manual Payment diff --git a/src/components/PublicNavbar.js b/src/components/PublicNavbar.js index 132ce49..0575131 100644 --- a/src/components/PublicNavbar.js +++ b/src/components/PublicNavbar.js @@ -75,8 +75,24 @@ const PublicNavbar = () => {
-
- +
+ {user && ( + + Welcome, {user.first_name} + + )} + {(user?.role === 'admin' || user?.role === 'superadmin') && ( + + Dashboard + + )} + + + + + Newsletters + + + + + Financials + + + + + Bylaws + + + + + + )} + {/* { style={{ fontFamily: "'Nunito Sans', sans-serif" }} > Contact Us - + */}
@@ -219,6 +299,18 @@ const PublicNavbar = () => {
+ {/* User Info */} + {user && ( +
+

+ Welcome, +

+

+ {user.first_name} {user.last_name} +

+
+ )} + {/* Navigation Links */}
+ ); }; diff --git a/src/layouts/SettingsLayout.js b/src/layouts/SettingsLayout.js new file mode 100644 index 0000000..0f800fb --- /dev/null +++ b/src/layouts/SettingsLayout.js @@ -0,0 +1,16 @@ +import React from 'react'; +import { Outlet } from 'react-router-dom'; +import SettingsSidebar from '../components/SettingsSidebar'; + +const SettingsLayout = () => { + return ( +
+ +
+ +
+
+ ); +}; + +export default SettingsLayout; diff --git a/src/pages/AcceptInvitation.js b/src/pages/AcceptInvitation.js index 8f3b489..334a0e9 100644 --- a/src/pages/AcceptInvitation.js +++ b/src/pages/AcceptInvitation.js @@ -387,7 +387,7 @@ const AcceptInvitation = () => { value={formData.first_name} onChange={(e) => handleChange('first_name', e.target.value)} className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple " - placeholder="John" + placeholder="Jane" /> {formErrors.first_name && (

{formErrors.first_name}

diff --git a/src/pages/Dashboard.js b/src/pages/Dashboard.js index eb9429a..ee8ad38 100644 --- a/src/pages/Dashboard.js +++ b/src/pages/Dashboard.js @@ -7,7 +7,7 @@ import { Button } from '../components/ui/button'; import { Badge } from '../components/ui/badge'; import Navbar from '../components/Navbar'; import MemberFooter from '../components/MemberFooter'; -import { Calendar, User, CheckCircle, Clock, AlertCircle, Mail, Users, Image, FileText, DollarSign, Scale } from 'lucide-react'; +import { Calendar, User, CheckCircle, Clock, AlertCircle, Mail, Users, Image, FileText, DollarSign, Scale, Receipt, Heart, CreditCard } from 'lucide-react'; import { toast } from 'sonner'; const Dashboard = () => { @@ -17,6 +17,7 @@ const Dashboard = () => { const [resendLoading, setResendLoading] = useState(false); const [eventActivity, setEventActivity] = useState(null); const [activityLoading, setActivityLoading] = useState(true); + const [activeTransactionTab, setActiveTransactionTab] = useState('all'); const joinedDate = user?.member_since || user?.created_at; useEffect(() => { @@ -180,7 +181,7 @@ const Dashboard = () => { {/* Grid Layout */} -
+
{/* Quick Stats */}

@@ -219,19 +220,41 @@ const Dashboard = () => { )}

+ +

+ Membership Info +

+
+ + {user?.subscription_start_date && user?.subscription_end_date && ( + <> +
+

Membership Period

+

+ {new Date(user.subscription_start_date).toLocaleDateString()} - {new Date(user.subscription_end_date).toLocaleDateString()} +

+
+
+

Days Remaining

+

+ {Math.max(0, Math.ceil((new Date(user.subscription_end_date) - new Date()) / (1000 * 60 * 60 * 24)))} days +

+
+ + )} +
+
{/* Upcoming Events */} - +

- Upcoming Events + My Event Activity

-
@@ -313,155 +336,129 @@ const Dashboard = () => {
)} - {/* Event Activity Section */} -
-
-

- My Event Activity + {/* Transaction History Section */} + + {/* Header */} +
+
+ +
+

+ Transaction History

- {activityLoading ? ( -

Loading event activity...

- ) : eventActivity ? ( -
- {/* Stats Cards */} -
- -
-
- -
-
-

Total RSVPs

-

- {eventActivity.total_rsvps} -

-
-
-
- -
-
- -
-
-

Events Attended

-

- {eventActivity.total_attended} -

-
-
-
+ {/* Stats Row */} +
+
+
+ + + Total Subscriptions +
- - {/* Upcoming RSVP'd Events */} - {eventActivity.upcoming_events && eventActivity.upcoming_events.length > 0 && ( - -

- Upcoming Events (RSVP'd) -

-
- {eventActivity.upcoming_events.map((event) => ( - -
-
-
-

{event.title}

-

- {new Date(event.start_at).toLocaleDateString()} at{' '} - {new Date(event.start_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} -

-

{event.location}

-
- - {event.rsvp_status === 'yes' ? 'Going' : - event.rsvp_status === 'maybe' ? 'Maybe' : 'Not Going'} - -
-
- - ))} -
-
- )} - - {/* Past Events & Attendance */} - {eventActivity.past_events && eventActivity.past_events.length > 0 && ( - -

- Past Events -

-
- {eventActivity.past_events.slice(0, 5).map((event) => ( -
-
-
-

{event.title}

-

- {new Date(event.start_at).toLocaleDateString()} at{' '} - {new Date(event.start_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} -

-
-
- - {event.attended ? 'Attended' : 'Did not attend'} - - {event.attended && event.attended_at && ( -

- Checked in: {new Date(event.attended_at).toLocaleDateString()} -

- )} -
-
-
- ))} -
- {eventActivity.past_events.length > 5 && ( -

- Showing 5 of {eventActivity.past_events.length} past events -

- )} -
- )} - - {/* No Events Message */} - {(!eventActivity.upcoming_events || eventActivity.upcoming_events.length === 0) && - (!eventActivity.past_events || eventActivity.past_events.length === 0) && ( - -
- -

- No Event Activity Yet -

-

- Browse upcoming events and RSVP to start building your event history! -

- - - -
-
- )} +

+ $30.00 +

+

+ 1 payment(s) +

- ) : ( - -
- -

- Failed to load event activity. Please try refreshing the page. +

+
+ + + Total Donations + +
+

+ $0.00 +

+

+ 0 donation(s) +

+
+
+ + {/* Filter Tabs */} +
+ + + +
+ + {/* Transaction List */} +
+ {(activeTransactionTab === 'all' || activeTransactionTab === 'subscriptions') && ( +
+
+
+
+
+ + Annual Membership + + + active + +
+
+ + Dec 16, 2025 + + Custom +
+ + Manual Payment + +
+
+ + $30.00 + +
+ )} + {activeTransactionTab === 'donations' && ( +
+ +

+ No donations yet

- - )} -
+ )} +
+ +
diff --git a/src/pages/admin/AdminBylaws.js b/src/pages/admin/AdminBylaws.js index cee3c44..ad29aeb 100644 --- a/src/pages/admin/AdminBylaws.js +++ b/src/pages/admin/AdminBylaws.js @@ -330,7 +330,7 @@ const AdminBylaws = () => { {/* Create/Edit Dialog */} - + {selectedBylaws ? 'Edit Bylaws' : 'Add Bylaws Version'} diff --git a/src/pages/admin/AdminDashboard.js b/src/pages/admin/AdminDashboard.js index fef95b2..8c0a4c2 100644 --- a/src/pages/admin/AdminDashboard.js +++ b/src/pages/admin/AdminDashboard.js @@ -60,7 +60,7 @@ const AdminDashboard = () => { return ( <> -
+

Admin Dashboard @@ -69,9 +69,9 @@ const AdminDashboard = () => { Manage users, events, and membership applications.

- +