Enhance Admin Dashboard and Members Directory with improved layout, pagination, and member details display.
Applied requested changes to UI
This commit is contained in:
@@ -12,10 +12,10 @@ const BoardOfDirectors = () => {
|
||||
];
|
||||
|
||||
const boardMembers = [
|
||||
{ name: 'Danita Cole' },
|
||||
{ name: 'Roxanne Cherico' },
|
||||
{ name: 'Lucretia Copeland' },
|
||||
{ name: 'Julie Fischer' }
|
||||
{ name: 'Danita Cole', title: 'Director' },
|
||||
{ name: 'Roxanne Cherico', title: 'Director' },
|
||||
{ name: 'Lucretia Copeland', title: 'Director' },
|
||||
{ name: 'Julie Fischer', title: 'Director' }
|
||||
];
|
||||
|
||||
|
||||
@@ -112,50 +112,50 @@ const BoardOfDirectors = () => {
|
||||
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 */}
|
||||
<Card className="bg-[#eeebf4] p-8 rounded-2xl shadow-lg mx-auto border border-white/70">
|
||||
<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, however it is possible to be elected prior to 1 year, but start the term on the 1 year anniversary.</li>
|
||||
<li>Officer positions are only available to current directors.</li>
|
||||
<Card className="bg-[#eeebf4] p-8 rounded-2xl shadow-lg mx-auto border border-white/70">
|
||||
<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, however it is possible to be elected prior to 1 year, but start the term on the 1 year anniversary.</li>
|
||||
<li>Officer positions are only available to current directors.</li>
|
||||
|
||||
<li>Each director shall serve a 2-year term.</li>
|
||||
<li>Each director shall serve a 2-year term.</li>
|
||||
|
||||
<li>The time commitment is approximately 1–2 hours per week.</li>
|
||||
<li>The time commitment is approximately 1–2 hours per week.</li>
|
||||
|
||||
<li>
|
||||
The tasks that directors perform depend on individual interests. Recent
|
||||
tasks include researching how to obtain an extra PO Box key, ordering
|
||||
Welcome Team name tags, taking pictures at events, researching new venues
|
||||
for holiday socials, and monitoring Facebook posts. For more information
|
||||
about director duties, see Article 2 of the bylaws in the Members Only
|
||||
section of the website:
|
||||
<a
|
||||
href="https://loaftx.org/bylaws/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[#48286e] underline"
|
||||
>
|
||||
https://loaftx.org/bylaws/
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
The tasks that directors perform depend on individual interests. Recent
|
||||
tasks include researching how to obtain an extra PO Box key, ordering
|
||||
Welcome Team name tags, taking pictures at events, researching new venues
|
||||
for holiday socials, and monitoring Facebook posts. For more information
|
||||
about director duties, see Article 2 of the bylaws in the Members Only
|
||||
section of the website:
|
||||
<a
|
||||
href="https://loaftx.org/bylaws/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[#48286e] underline"
|
||||
>
|
||||
https://loaftx.org/bylaws/
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
Directors must attend Board meetings held on the second Thursday of each
|
||||
month at 6:30pm via Zoom.
|
||||
</li>
|
||||
<li>
|
||||
Directors must attend Board meetings held on 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>
|
||||
<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>
|
||||
|
||||
@@ -4,7 +4,7 @@ import api from '../../utils/api';
|
||||
import { Card } from '../../components/ui/card';
|
||||
import { Button } from '../../components/ui/button';
|
||||
import { Badge } from '../../components/ui/badge';
|
||||
import { Users, Calendar, Clock, CheckCircle, Mail, AlertCircle } from 'lucide-react';
|
||||
import { Users, Calendar, Clock, CheckCircle, Mail, AlertCircle,Globe } from 'lucide-react';
|
||||
|
||||
const AdminDashboard = () => {
|
||||
const [stats, setStats] = useState({
|
||||
@@ -42,8 +42,8 @@ const AdminDashboard = () => {
|
||||
}).map(u => ({
|
||||
...u,
|
||||
totalReminders: (u.email_verification_reminders_sent || 0) +
|
||||
(u.event_attendance_reminders_sent || 0) +
|
||||
(u.payment_reminders_sent || 0)
|
||||
(u.event_attendance_reminders_sent || 0) +
|
||||
(u.payment_reminders_sent || 0)
|
||||
})).sort((a, b) => b.totalReminders - a.totalReminders).slice(0, 5); // Top 5
|
||||
|
||||
setUsersNeedingAttention(needingAttention);
|
||||
@@ -56,173 +56,183 @@ const AdminDashboard = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Admin Dashboard
|
||||
</h1>
|
||||
<p className="text-lg text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Manage users, events, and membership applications.
|
||||
</p>
|
||||
<div className='flex justify-between items-center'>
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Admin Dashboard
|
||||
</h1>
|
||||
<p className="text-lg text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Manage users, events, and membership applications.
|
||||
</p>
|
||||
</div>
|
||||
<Link to={'/'}>
|
||||
<Button
|
||||
className="bg-[#664fa3] text-white hover:bg-[#533a82] rounded-full flex items-center gap-2"
|
||||
>
|
||||
<Globe />
|
||||
View Public Site
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 mb-12">
|
||||
<Card className="p-6 bg-white rounded-2xl border border-[#ddd8eb]" data-testid="stat-total-users">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="bg-[#DDD8EB]/20 p-3 rounded-lg">
|
||||
<Users className="h-6 w-6 text-[#664fa3]" />
|
||||
</div>
|
||||
{/* Stats Grid */}
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 mb-12">
|
||||
<Card className="p-6 bg-white rounded-2xl border border-[#ddd8eb]" data-testid="stat-total-users">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="bg-[#DDD8EB]/20 p-3 rounded-lg">
|
||||
<Users className="h-6 w-6 text-[#664fa3]" />
|
||||
</div>
|
||||
<p className="text-3xl font-semibold text-[#422268] mb-1" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{loading ? '-' : stats.totalMembers}
|
||||
</p>
|
||||
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Total Members</p>
|
||||
</Card>
|
||||
</div>
|
||||
<p className="text-3xl font-semibold text-[#422268] mb-1" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{loading ? '-' : stats.totalMembers}
|
||||
</p>
|
||||
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Total Members</p>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6 bg-white rounded-2xl border border-[#ddd8eb]" data-testid="stat-pending-validations">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="bg-orange-100 p-3 rounded-lg">
|
||||
<Clock className="h-6 w-6 text-orange-600" />
|
||||
</div>
|
||||
<Card className="p-6 bg-white rounded-2xl border border-[#ddd8eb]" data-testid="stat-pending-validations">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="bg-orange-100 p-3 rounded-lg">
|
||||
<Clock className="h-6 w-6 text-orange-600" />
|
||||
</div>
|
||||
<p className="text-3xl font-semibold text-[#422268] mb-1" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{loading ? '-' : stats.pendingValidations}
|
||||
</p>
|
||||
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Pending Validations</p>
|
||||
</Card>
|
||||
</div>
|
||||
<p className="text-3xl font-semibold text-[#422268] mb-1" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{loading ? '-' : stats.pendingValidations}
|
||||
</p>
|
||||
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Pending Validations</p>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6 bg-white rounded-2xl border border-[#ddd8eb]" data-testid="stat-active-members">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="bg-[#81B29A]/20 p-3 rounded-lg">
|
||||
<CheckCircle className="h-6 w-6 text-[#81B29A]" />
|
||||
</div>
|
||||
<Card className="p-6 bg-white rounded-2xl border border-[#ddd8eb]" data-testid="stat-active-members">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="bg-[#81B29A]/20 p-3 rounded-lg">
|
||||
<CheckCircle className="h-6 w-6 text-[#81B29A]" />
|
||||
</div>
|
||||
<p className="text-3xl font-semibold text-[#422268] mb-1" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{loading ? '-' : stats.activeMembers}
|
||||
</div>
|
||||
<p className="text-3xl font-semibold text-[#422268] mb-1" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{loading ? '-' : stats.activeMembers}
|
||||
</p>
|
||||
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Active Members</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="grid md:grid-cols-2 gap-8">
|
||||
<Link to="/admin/members">
|
||||
<Card className="p-8 bg-white rounded-2xl border border-[#ddd8eb] hover:shadow-lg hover:-translate-y-1 transition-all cursor-pointer" data-testid="quick-action-users">
|
||||
<Users className="h-12 w-12 text-[#664fa3] mb-4" />
|
||||
<h3 className="text-xl font-semibold text-[#422268] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Manage Members
|
||||
</h3>
|
||||
<p className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
View and manage paying members and their subscription status.
|
||||
</p>
|
||||
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Active Members</p>
|
||||
<Button
|
||||
className="mt-4 bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full"
|
||||
data-testid="manage-users-button"
|
||||
>
|
||||
Go to Members
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="grid md:grid-cols-2 gap-8">
|
||||
<Link to="/admin/members">
|
||||
<Card className="p-8 bg-white rounded-2xl border border-[#ddd8eb] hover:shadow-lg hover:-translate-y-1 transition-all cursor-pointer" data-testid="quick-action-users">
|
||||
<Users className="h-12 w-12 text-[#664fa3] mb-4" />
|
||||
<h3 className="text-xl font-semibold text-[#422268] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Manage Members
|
||||
</h3>
|
||||
<p className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
View and manage paying members and their subscription status.
|
||||
</p>
|
||||
<Button
|
||||
className="mt-4 bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full"
|
||||
data-testid="manage-users-button"
|
||||
>
|
||||
Go to Members
|
||||
</Button>
|
||||
</Card>
|
||||
</Link>
|
||||
<Link to="/admin/validations">
|
||||
<Card className="p-8 bg-white rounded-2xl border border-[#ddd8eb] hover:shadow-lg hover:-translate-y-1 transition-all cursor-pointer" data-testid="quick-action-validations">
|
||||
<Clock className="h-12 w-12 text-orange-600 mb-4" />
|
||||
<h3 className="text-xl font-semibold text-[#422268] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Validation Queue
|
||||
</h3>
|
||||
<p className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Review and validate pending membership applications.
|
||||
</p>
|
||||
<Button
|
||||
className="mt-4 bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full"
|
||||
data-testid="manage-validations-button"
|
||||
>
|
||||
View Validations
|
||||
</Button>
|
||||
</Card>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Link to="/admin/validations">
|
||||
<Card className="p-8 bg-white rounded-2xl border border-[#ddd8eb] hover:shadow-lg hover:-translate-y-1 transition-all cursor-pointer" data-testid="quick-action-validations">
|
||||
<Clock className="h-12 w-12 text-orange-600 mb-4" />
|
||||
<h3 className="text-xl font-semibold text-[#422268] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Validation Queue
|
||||
</h3>
|
||||
<p className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Review and validate pending membership applications.
|
||||
</p>
|
||||
<Button
|
||||
className="mt-4 bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full"
|
||||
data-testid="manage-validations-button"
|
||||
>
|
||||
View Validations
|
||||
</Button>
|
||||
</Card>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Users Needing Attention Widget */}
|
||||
{usersNeedingAttention.length > 0 && (
|
||||
<div className="mt-12">
|
||||
<Card className="p-8 bg-white rounded-2xl border-2 border-[#ff9e77] shadow-lg">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="bg-[#ff9e77]/20 p-3 rounded-lg">
|
||||
<AlertCircle className="h-6 w-6 text-[#ff9e77]" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-2xl font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Members Needing Personal Outreach
|
||||
</h3>
|
||||
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
These members have received multiple reminder emails. Consider calling them directly.
|
||||
</p>
|
||||
</div>
|
||||
{/* Users Needing Attention Widget */}
|
||||
{usersNeedingAttention.length > 0 && (
|
||||
<div className="mt-12">
|
||||
<Card className="p-8 bg-white rounded-2xl border-2 border-[#ff9e77] shadow-lg">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="bg-[#ff9e77]/20 p-3 rounded-lg">
|
||||
<AlertCircle className="h-6 w-6 text-[#ff9e77]" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{usersNeedingAttention.map(user => (
|
||||
<Link key={user.id} to={`/admin/users/${user.id}`}>
|
||||
<div className="p-4 bg-[#F8F7FB] rounded-xl border border-[#ddd8eb] hover:border-[#ff9e77] hover:shadow-md transition-all cursor-pointer">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h4 className="font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{user.first_name} {user.last_name}
|
||||
</h4>
|
||||
<Badge className="bg-[#ff9e77] text-white px-3 py-1 rounded-full text-xs">
|
||||
{user.totalReminders} reminder{user.totalReminders !== 1 ? 's' : ''}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p>Email: {user.email}</p>
|
||||
<p>Phone: {user.phone || 'N/A'}</p>
|
||||
<p className="capitalize">Status: {user.status.replace('_', ' ')}</p>
|
||||
{user.email_verification_reminders_sent > 0 && (
|
||||
<p>
|
||||
<Mail className="inline h-3 w-3 mr-1" />
|
||||
{user.email_verification_reminders_sent} email verification reminder{user.email_verification_reminders_sent !== 1 ? 's' : ''}
|
||||
</p>
|
||||
)}
|
||||
{user.event_attendance_reminders_sent > 0 && (
|
||||
<p>
|
||||
<Calendar className="inline h-3 w-3 mr-1" />
|
||||
{user.event_attendance_reminders_sent} event reminder{user.event_attendance_reminders_sent !== 1 ? 's' : ''}
|
||||
</p>
|
||||
)}
|
||||
{user.payment_reminders_sent > 0 && (
|
||||
<p>
|
||||
<Clock className="inline h-3 w-3 mr-1" />
|
||||
{user.payment_reminders_sent} payment reminder{user.payment_reminders_sent !== 1 ? 's' : ''}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className="bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full text-sm"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
window.location.href = `tel:${user.phone}`;
|
||||
}}
|
||||
>
|
||||
Call Member
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 p-4 bg-[#DDD8EB]/20 rounded-lg border border-[#ddd8eb]">
|
||||
<div>
|
||||
<h3 className="text-2xl font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Members Needing Personal Outreach
|
||||
</h3>
|
||||
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<strong>💡 Tip for helping older members:</strong> Many of our members are older ladies who may struggle with email.
|
||||
A friendly phone call can help them complete the registration process and feel more welcomed to the community.
|
||||
These members have received multiple reminder emails. Consider calling them directly.
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{usersNeedingAttention.map(user => (
|
||||
<Link key={user.id} to={`/admin/users/${user.id}`}>
|
||||
<div className="p-4 bg-[#F8F7FB] rounded-xl border border-[#ddd8eb] hover:border-[#ff9e77] hover:shadow-md transition-all cursor-pointer">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h4 className="font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{user.first_name} {user.last_name}
|
||||
</h4>
|
||||
<Badge className="bg-[#ff9e77] text-white px-3 py-1 rounded-full text-xs">
|
||||
{user.totalReminders} reminder{user.totalReminders !== 1 ? 's' : ''}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p>Email: {user.email}</p>
|
||||
<p>Phone: {user.phone || 'N/A'}</p>
|
||||
<p className="capitalize">Status: {user.status.replace('_', ' ')}</p>
|
||||
{user.email_verification_reminders_sent > 0 && (
|
||||
<p>
|
||||
<Mail className="inline h-3 w-3 mr-1" />
|
||||
{user.email_verification_reminders_sent} email verification reminder{user.email_verification_reminders_sent !== 1 ? 's' : ''}
|
||||
</p>
|
||||
)}
|
||||
{user.event_attendance_reminders_sent > 0 && (
|
||||
<p>
|
||||
<Calendar className="inline h-3 w-3 mr-1" />
|
||||
{user.event_attendance_reminders_sent} event reminder{user.event_attendance_reminders_sent !== 1 ? 's' : ''}
|
||||
</p>
|
||||
)}
|
||||
{user.payment_reminders_sent > 0 && (
|
||||
<p>
|
||||
<Clock className="inline h-3 w-3 mr-1" />
|
||||
{user.payment_reminders_sent} payment reminder{user.payment_reminders_sent !== 1 ? 's' : ''}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className="bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full text-sm"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
window.location.href = `tel:${user.phone}`;
|
||||
}}
|
||||
>
|
||||
Call Member
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 p-4 bg-[#DDD8EB]/20 rounded-lg border border-[#ddd8eb]">
|
||||
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<strong>💡 Tip for helping older members:</strong> Many of our members are older ladies who may struggle with email.
|
||||
A friendly phone call can help them complete the registration process and feel more welcomed to the community.
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -118,7 +118,7 @@ const AdminStaff = () => {
|
||||
const getStatusBadge = (status) => {
|
||||
const config = {
|
||||
active: { label: 'Active', className: 'bg-[#81B29A] text-white' },
|
||||
inactive: { label: 'Inactive', className: 'bg-gray-400 text-white' }
|
||||
inactive: { label: 'Inactive', className: 'bg-gray-400 text-white ' }
|
||||
};
|
||||
|
||||
const statusConfig = config[status] || config.inactive;
|
||||
|
||||
@@ -24,6 +24,8 @@ const MembersDirectory = () => {
|
||||
const [selectedMember, setSelectedMember] = useState(null);
|
||||
const [profileDialogOpen, setProfileDialogOpen] = useState(false);
|
||||
const { toast } = useToast();
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const pageSize = 12;
|
||||
|
||||
useEffect(() => {
|
||||
fetchMembers();
|
||||
@@ -33,6 +35,10 @@ const MembersDirectory = () => {
|
||||
filterMembers();
|
||||
}, [searchQuery, members]);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentPage(1);
|
||||
}, [searchQuery, members]);
|
||||
|
||||
const fetchMembers = async () => {
|
||||
try {
|
||||
const response = await api.get('/members/directory');
|
||||
@@ -66,6 +72,14 @@ const MembersDirectory = () => {
|
||||
setFilteredMembers(filtered);
|
||||
};
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(filteredMembers.length / pageSize));
|
||||
|
||||
const pageStart = (currentPage - 1) * pageSize;
|
||||
|
||||
const paginatedMembers = filteredMembers.slice(pageStart, pageStart + pageSize);
|
||||
|
||||
const totalMembers = members.length;
|
||||
|
||||
const getInitials = (firstName, lastName) => {
|
||||
return `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase();
|
||||
};
|
||||
@@ -97,9 +111,15 @@ const MembersDirectory = () => {
|
||||
if (!dateString) return null;
|
||||
return new Date(dateString).toLocaleDateString('en-US', { month: 'long', day: 'numeric' });
|
||||
};
|
||||
|
||||
const Border = ({ yaxis = false }) => {
|
||||
return (
|
||||
yaxis ?
|
||||
<div className=' border-2 w-full border-[#664FA3] my-24' />
|
||||
: <div className=' border-2 w-full border-[#664FA3] mb-24' />
|
||||
)
|
||||
}
|
||||
const MemberCard = ({ member }) => (
|
||||
<Card className="p-6 bg-white rounded-2xl border border-[#ddd8eb] hover:shadow-lg transition-all h-full">
|
||||
<Card className="p-6 bg-white rounded-3xl border border-[#ddd8eb] hover:shadow-lg transition-all h-full">
|
||||
{/* Profile Photo */}
|
||||
<div className="flex justify-center mb-4">
|
||||
{member.profile_photo_url ? (
|
||||
@@ -259,39 +279,48 @@ const MembersDirectory = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white">
|
||||
<div className="min-h-screen bg-gradient-to-bl from-[#F9FAFB] to-[#DDD8EB]">
|
||||
<Navbar />
|
||||
|
||||
<div className="max-w-7xl mx-auto px-6 py-12">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Members Directory
|
||||
</h1>
|
||||
<p className="text-lg text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Connect with fellow LOAF members in our community.
|
||||
</p>
|
||||
</div>
|
||||
<div className="max-w-7xl mx-auto py-12">
|
||||
|
||||
{/* Search Bar */}
|
||||
<div className="mb-8">
|
||||
<div className="relative max-w-xl">
|
||||
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 h-5 w-5 text-[#664fa3]" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search by name or bio..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-12 pr-4 py-6 text-lg border-[#ddd8eb] rounded-xl focus:border-[#664fa3] focus:ring-[#664fa3]"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
/>
|
||||
</div>
|
||||
{searchQuery && (
|
||||
<p className="mt-3 text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Found {filteredMembers.length} {filteredMembers.length === 1 ? 'member' : 'members'}
|
||||
{/* Header and Search bar */}
|
||||
<div className='px-9'>
|
||||
|
||||
{/* Header */}
|
||||
<div className="m-8 mt-14 flex flex-col sm:flex-row justify-between items-center ">
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-[#422268] mb-4" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
LOAF Members
|
||||
</h1>
|
||||
<p className="text-lg " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<span className='text-foreground'>Number of current memebers in the directory: </span> <span className='text-[#664fa3] font-medium'>{totalMembers}</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Search Bar */}
|
||||
<div className="mb-24 mx-10">
|
||||
<div className="relative w-full ">
|
||||
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 h-5 w-5 text-[#664fa3]" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search by name or bio..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-12 pr-4 py-6 text-3xl font-medium bg-background border-foreground rounded-full focus:border-[#664fa3] focus:ring-[#664fa3]"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
/>
|
||||
</div>
|
||||
{searchQuery && (
|
||||
<p className="mt-3 text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Found {filteredMembers.length} {filteredMembers.length === 1 ? 'member' : 'members'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{/* Border Decoration */}
|
||||
|
||||
<Border />
|
||||
|
||||
{/* Members Grid */}
|
||||
{loading ? (
|
||||
@@ -300,7 +329,7 @@ const MembersDirectory = () => {
|
||||
</div>
|
||||
) : filteredMembers.length > 0 ? (
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{filteredMembers.map((member) => (
|
||||
{paginatedMembers.map((member) => (
|
||||
<MemberCard key={member.id} member={member} />
|
||||
))}
|
||||
</div>
|
||||
@@ -318,6 +347,11 @@ const MembersDirectory = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
{/* Border Decoration */}
|
||||
<Border yaxis="true" />
|
||||
|
||||
{/* Info Card */}
|
||||
{!loading && members.length > 0 && (
|
||||
<Card className="mt-12 p-6 bg-[#F8F7FB] border-[#ddd8eb]">
|
||||
@@ -465,67 +499,113 @@ const MembersDirectory = () => {
|
||||
{/* 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>
|
||||
)}
|
||||
<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_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_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>
|
||||
)}
|
||||
{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>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
|
||||
|
||||
{/* Pagination */}
|
||||
{!loading && filteredMembers.length > 0 && (
|
||||
<div className="mt-10 flex flex-col items-center gap-4 pb-12">
|
||||
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Showing {pageStart + 1}–{Math.min(pageStart + pageSize, filteredMembers.length)} of {filteredMembers.length}
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center justify-center gap-2">
|
||||
<Button
|
||||
onClick={() => setCurrentPage((page) => Math.max(1, page - 1))}
|
||||
disabled={currentPage === 1}
|
||||
className="bg-[#DDD8EB] rounded-full text-[#422268] hover:bg-[#664fa3] hover:text-white"
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
|
||||
{Array.from({ length: totalPages }, (_, index) => {
|
||||
const pageNumber = index + 1;
|
||||
const isActive = pageNumber === currentPage;
|
||||
return (
|
||||
<Button
|
||||
key={pageNumber}
|
||||
onClick={() => setCurrentPage(pageNumber)}
|
||||
className={
|
||||
isActive
|
||||
? "bg-[#664fa3] text-white hover:bg-[#422268] rounded-full"
|
||||
: "bg-[#DDD8EB] text-[#422268] hover:bg-[#664fa3] hover:text-white rounded-full"
|
||||
}
|
||||
>
|
||||
{pageNumber}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
|
||||
<Button
|
||||
onClick={() => setCurrentPage((page) => Math.min(totalPages, page + 1))}
|
||||
disabled={currentPage === totalPages}
|
||||
className="bg-[#DDD8EB] text-[#422268] hover:bg-[#664fa3] rounded-full hover:text-white"
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<MemberFooter />
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user