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/src/components/MemberCard.js b/src/components/MemberCard.js index 93f82be..8c82666 100644 --- a/src/components/MemberCard.js +++ b/src/components/MemberCard.js @@ -9,14 +9,22 @@ const getInitials = (firstName, lastName) => { return `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase(); }; -const MemberCard = ({ member }) => { - const joinedDate = member.member_since || member.created_at; +// 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 */}
- {/* todo: get correct status to pass to StatusBadge */} - +
{member.profile_photo_url ? ( @@ -165,7 +173,7 @@ const MemberCard = ({ member }) => { {/* View Profile Button */}
- - Menu - -
+ {/* Mobile Overlay */} + {isMobile && sidebarOpen && ( +
)} -
- {children} -
- -
+ + {/* Main Content Area */} +
+ {isMobile && ( +
+ + + Menu + +
+ )} +
+ {children} +
+
+
+ ); }; diff --git a/src/pages/admin/AdminMembers.js b/src/pages/admin/AdminMembers.js index 25d2e3d..4b36ef2 100644 --- a/src/pages/admin/AdminMembers.js +++ b/src/pages/admin/AdminMembers.js @@ -21,7 +21,7 @@ import InviteStaffDialog from '../../components/InviteStaffDialog'; import WordPressImportWizard from '../../components/WordPressImportWizard'; import StatusBadge from '../../components/StatusBadge'; import { StatCard } from '@/components/StatCard'; -import useMembers from '../../hooks/use-members'; +import { useMembers } from '../../hooks/use-users'; const AdminMembers = () => { const navigate = useNavigate(); @@ -35,7 +35,7 @@ const AdminMembers = () => { setSearchQuery, filterValue: statusFilter, setFilterValue: setStatusFilter, - fetchMembers, + refetch, } = useMembers(); const [paymentDialogOpen, setPaymentDialogOpen] = useState(false); const [selectedUserForPayment, setSelectedUserForPayment] = useState(null); @@ -53,7 +53,7 @@ const AdminMembers = () => { }; const handlePaymentSuccess = () => { - fetchMembers(); // Refresh list + refetch(); // Refresh list }; const handleStatusChangeRequest = (userId, currentStatus, newStatus, user) => { @@ -74,7 +74,7 @@ const AdminMembers = () => { try { await api.put(`/admin/users/${userId}/status`, { status: newStatus }); toast.success('Member status updated successfully'); - fetchMembers(); // Refresh list + refetch(); // Refresh list } catch (error) { toast.error(error.response?.data?.detail || 'Failed to update status'); } finally { @@ -520,19 +520,19 @@ const AdminMembers = () => { ); diff --git a/src/pages/admin/AdminStaff.js b/src/pages/admin/AdminStaff.js index 46a461f..92f6130 100644 --- a/src/pages/admin/AdminStaff.js +++ b/src/pages/admin/AdminStaff.js @@ -15,10 +15,7 @@ import { UserCog, Search, Shield, UserPlus, Mail, Edit, Eye, Trash2, UserCheck, import StatusBadge from '../../components/StatusBadge'; import { StatCard } from '@/components/StatCard'; import { CircleMinus, CreditCard, Users } from 'lucide-react'; -import useMembers from '../../hooks/use-members'; - -// Staff roles (non-guest, non-member) - includes all admin-type roles -const STAFF_ROLES = ['admin', 'superadmin', 'finance']; +import { useStaff } from '../../hooks/use-users'; const AdminStaff = () => { const navigate = useNavigate(); @@ -31,12 +28,10 @@ const AdminStaff = () => { setSearchQuery, filterValue: roleFilter, setFilterValue: setRoleFilter, - fetchMembers, - } = useMembers({ + refetch, + } = useStaff({ initialFilter: 'all', filterKey: 'role', - allowedRoles: STAFF_ROLES, - fetchErrorMessage: 'Failed to fetch staff', }); const [createDialogOpen, setCreateDialogOpen] = useState(false); const [inviteDialogOpen, setInviteDialogOpen] = useState(false); @@ -48,7 +43,7 @@ const AdminStaff = () => { try { await api.put(`/admin/users/${userId}/status`, { status: newStatus }); toast.success(`User ${newStatus === 'active' ? 'activated' : 'deactivated'} successfully`); - fetchMembers(); // Refresh list + refetch(); // Refresh list } catch (error) { toast.error(error.response?.data?.detail || 'Failed to update user status'); } @@ -62,7 +57,7 @@ const AdminStaff = () => { try { await api.delete(`/admin/users/${userId}`); toast.success('User deleted successfully'); - fetchMembers(); // Refresh list + refetch(); // Refresh list } catch (error) { toast.error(error.response?.data?.detail || 'Failed to delete user'); } @@ -114,7 +109,7 @@ const AdminStaff = () => {
['admin', 'superadmin', 'finance', 'staff', 'media', 'moderator'].includes(u.role)).length} icon={Users} @@ -304,7 +299,7 @@ const AdminStaff = () => { { - const [members, setMembers] = useState([]); - 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(); const [currentPage, setCurrentPage] = useState(1); const pageSize = 12; + const allowedRoles = useMemo(() => [], []); + const searchAccessor = useCallback( + (member) => [ + `${member.first_name} ${member.last_name}`, + member.directory_bio || '' + ], + [] + ); + const handleFetchError = useCallback(() => { + toast({ + title: "Error", + description: "Failed to load members directory. Please try again.", + variant: "destructive" + }); + }, [toast]); - useEffect(() => { - fetchMembers(); - }, []); - - useEffect(() => { - filterMembers(); - }, [searchQuery, members]); + const { + users: members, + filteredUsers: filteredMembers, + loading, + searchQuery, + setSearchQuery, + } = useMembers({ + endpoint: '/members/directory', + initialFilter: 'active', + filterKey: 'status', + allowedRoles, + searchAccessor, + fetchErrorMessage: 'Failed to load members directory. Please try again.', + onFetchError: handleFetchError + }); useEffect(() => { setCurrentPage(1); }, [searchQuery, members]); - const fetchMembers = async () => { - try { - const response = await api.get('/members/directory'); - setMembers(response.data); - setFilteredMembers(response.data); - } catch (error) { - console.error('Failed to fetch members:', error); - toast({ - title: "Error", - description: "Failed to load members directory. Please try again.", - variant: "destructive" - }); - } finally { - setLoading(false); - } - }; - - const filterMembers = () => { - if (!searchQuery.trim()) { - setFilteredMembers(members); - return; - } - - const query = searchQuery.toLowerCase(); - const filtered = members.filter(member => { - const fullName = `${member.first_name} ${member.last_name}`.toLowerCase(); - const bio = (member.directory_bio || '').toLowerCase(); - return fullName.includes(query) || bio.includes(query); - }); - - setFilteredMembers(filtered); - }; - const totalPages = Math.max(1, Math.ceil(filteredMembers.length / pageSize)); const pageStart = (currentPage - 1) * pageSize; @@ -171,7 +158,7 @@ const MembersDirectory = () => { ) : filteredMembers.length > 0 ? (
{paginatedMembers.map((member) => ( - + ))}
) : ( @@ -223,8 +210,7 @@ const MembersDirectory = () => { {selectedMember.first_name} {selectedMember.last_name} - {/* todo: figure out the correct selection to get the status of the user and pass into badge */} - + {selectedMember.directory_partner_name && ( diff --git a/tailwind.config.js b/tailwind.config.js index 0538059..9ebfc77 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -102,6 +102,5 @@ module.exports = { plugins: [ require("tailwindcss-animate"), require("@tailwindcss/typography"), - require('@tailwindcss/line-clamp') ], }; \ No newline at end of file