Files
membership-fe/src/pages/members/MembersDirectory.js
kayela 4ba44d8997 Refactor Members Directory and Newsletter Archive styles to use new color palette
- Updated color classes in MembersDirectory.js to use new color variables for borders, backgrounds, and text.
- Enhanced visual consistency by replacing hardcoded colors with Tailwind CSS color utilities.
- Modified NewsletterArchive.js to align with the new design system, ensuring a cohesive look across components.
- Added new color variables in tailwind.config.js for better maintainability and scalability.
2026-01-07 11:36:07 -06:00

629 lines
25 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } from 'react';
import api from '../../utils/api';
import Navbar from '../../components/Navbar';
import MemberFooter from '../../components/MemberFooter';
import { Card } from '../../components/ui/card';
import { Input } from '../../components/ui/input';
import { Badge } from '../../components/ui/badge';
import { Button } from '../../components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '../../components/ui/dialog';
import { User, Search, Mail, MapPin, Phone, Heart, Facebook, Instagram, Twitter, Linkedin, UserCircle, Calendar } from 'lucide-react';
import { useToast } from '../../hooks/use-toast';
const MembersDirectory = () => {
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;
useEffect(() => {
fetchMembers();
}, []);
useEffect(() => {
filterMembers();
}, [searchQuery, members]);
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;
const paginatedMembers = filteredMembers.slice(pageStart, pageStart + pageSize);
const totalMembers = members.length;
const getInitials = (firstName, lastName) => {
return `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase();
};
const getSocialMediaLink = (url) => {
if (!url) return null;
// Ensure URL has protocol
if (!url.startsWith('http://') && !url.startsWith('https://')) {
return `https://${url}`;
}
return url;
};
const handleViewProfile = async (memberId) => {
try {
const response = await api.get(`/members/directory/${memberId}`);
setSelectedMember(response.data);
setProfileDialogOpen(true);
} catch (error) {
toast({
title: "Error",
description: "Failed to load member profile. Please try again.",
variant: "destructive"
});
}
};
const formatDate = (dateString) => {
if (!dateString) return null;
return new Date(dateString).toLocaleDateString('en-US', { month: 'long', day: 'numeric' });
};
const Border = ({ yaxis = false }) => {
return (
yaxis ?
<div className=' border-2 w-full border-muted-foreground my-24' />
: <div className=' border-2 w-full border-muted-foreground mb-24' />
)
}
const MemberCard = ({ member }) => (
<Card className="p-6 bg-background rounded-3xl border border-chart-6 hover:shadow-lg transition-all h-full">
{/* Profile Photo */}
<div className="flex justify-center mb-4">
{member.profile_photo_url ? (
<img
src={member.profile_photo_url}
alt={`${member.first_name} ${member.last_name}`}
className="w-32 h-32 rounded-full object-cover border-4 border-chart-6"
/>
) : (
<div className="w-32 h-32 rounded-full bg-chart-6 border-4 border-chart-6 flex items-center justify-center">
<span className="text-4xl font-semibold text-muted-foreground" style={{ fontFamily: "'Inter', sans-serif" }}>
{getInitials(member.first_name, member.last_name)}
</span>
</div>
)}
</div>
{/* Name */}
<h3 className="text-2xl font-semibold text-primary text-center mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
{member.first_name} {member.last_name}
</h3>
{/* Partner Name */}
{member.directory_partner_name && (
<div className="flex items-center justify-center gap-2 mb-4">
<Heart className="h-4 w-4 text-accent" />
<span className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Partner: {member.directory_partner_name}
</span>
</div>
)}
{/* Bio */}
{member.directory_bio && (
<p className="text-muted-foreground text-center mb-4 line-clamp-3" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{member.directory_bio}
</p>
)}
{/* Member Since */}
{member.created_at && (
<div className="flex items-center justify-center gap-2 mb-4">
<Calendar className="h-4 w-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Member since {new Date(member.created_at).toLocaleDateString('en-US', {
month: 'long',
year: 'numeric'
})}
</span>
</div>
)}
{/* Contact Information */}
<div className="space-y-3 mb-4">
{member.directory_email && (
<div className="flex items-center gap-2 text-sm">
<Mail className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<a
href={`mailto:${member.directory_email}`}
className="text-muted-foreground hover:text-primary truncate"
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
>
{member.directory_email}
</a>
</div>
)}
{member.directory_phone && (
<div className="flex items-center gap-2 text-sm">
<Phone className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<a
href={`tel:${member.directory_phone}`}
className="text-muted-foreground hover:text-primary"
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
>
{member.directory_phone}
</a>
</div>
)}
{member.directory_address && (
<div className="flex items-start gap-2 text-sm">
<MapPin className="h-4 w-4 text-muted-foreground flex-shrink-0 mt-0.5" />
<span className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{member.directory_address}
</span>
</div>
)}
</div>
{/* Social Media Links */}
{(member.social_media_facebook || member.social_media_instagram || member.social_media_twitter || member.social_media_linkedin) && (
<div className="pt-4 border-t border-chart-6">
<div className="flex justify-center gap-3">
{member.social_media_facebook && (
<a
href={getSocialMediaLink(member.social_media_facebook)}
target="_blank"
rel="noopener noreferrer"
className="p-2 rounded-lg bg-chart-7 hover:bg-chart-6 transition-colors"
title="Facebook"
>
<Facebook className="h-5 w-5 text-[#1877F2]" />
</a>
)}
{member.social_media_instagram && (
<a
href={getSocialMediaLink(member.social_media_instagram)}
target="_blank"
rel="noopener noreferrer"
className="p-2 rounded-lg bg-chart-7 hover:bg-chart-6 transition-colors"
title="Instagram"
>
<Instagram className="h-5 w-5 text-[#E4405F]" />
</a>
)}
{member.social_media_twitter && (
<a
href={getSocialMediaLink(member.social_media_twitter)}
target="_blank"
rel="noopener noreferrer"
className="p-2 rounded-lg bg-chart-7 hover:bg-chart-6 transition-colors"
title="Twitter/X"
>
<Twitter className="h-5 w-5 text-[#1DA1F2]" />
</a>
)}
{member.social_media_linkedin && (
<a
href={getSocialMediaLink(member.social_media_linkedin)}
target="_blank"
rel="noopener noreferrer"
className="p-2 rounded-lg bg-chart-7 hover:bg-chart-6 transition-colors"
title="LinkedIn"
>
<Linkedin className="h-5 w-5 text-[#0A66C2]" />
</a>
)}
</div>
</div>
)}
{/* View Profile Button */}
<div className="pt-4 mt-4 border-t border-chart-6">
<Button
onClick={() => handleViewProfile(member.id)}
className="w-full bg-chart-6 text-primary hover:bg-muted-foreground hover:text-white rounded-full py-5"
>
<UserCircle className="h-4 w-4 mr-2" />
View Full Profile
</Button>
</div>
</Card>
);
return (
<div className="min-h-screen bg-gradient-to-bl from-[#F9FAFB] to-chart-6">
<Navbar />
<div className="max-w-7xl mx-auto py-12">
{/* 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-primary 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-muted-foreground 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-muted-foreground" />
<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-muted-foreground focus:ring-muted-foreground"
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
/>
</div>
{searchQuery && (
<p className="mt-3 text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Found {filteredMembers.length} {filteredMembers.length === 1 ? 'member' : 'members'}
</p>
)}
</div>
</div>
{/* Border Decoration */}
<Border />
{/* Members Grid */}
{loading ? (
<div className="text-center py-20">
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading members...</p>
</div>
) : filteredMembers.length > 0 ? (
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
{paginatedMembers.map((member) => (
<MemberCard key={member.id} member={member} />
))}
</div>
) : (
<div className="text-center py-20">
<User className="h-20 w-20 text-chart-6 mx-auto mb-6" />
<h3 className="text-2xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
{searchQuery ? 'No Members Found' : 'No Members in Directory'}
</h3>
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{searchQuery
? 'Try adjusting your search query.'
: 'Members who opt in to the directory will appear here.'}
</p>
</div>
)}
{/* Border Decoration */}
<Border yaxis="true" />
{/* Info Card */}
{!loading && members.length > 0 && (
<Card className="mt-12 p-6 bg-chart-7 border-chart-6">
<div className="flex items-start gap-4">
<div className="bg-chart-6/20 p-3 rounded-lg">
<User className="h-6 w-6 text-muted-foreground" />
</div>
<div>
<h3 className="text-lg font-semibold text-primary mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
Want to appear in the directory?
</h3>
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Update your profile settings to show in the directory and add your photo, bio, and contact information.{' '}
<a href="/members/profile" className="text-accent hover:underline font-medium">
Edit your profile
</a>
</p>
</div>
</div>
</Card>
)}
</div>
{/* Profile Detail Dialog */}
<Dialog open={profileDialogOpen} onOpenChange={setProfileDialogOpen}>
<DialogContent className="sm:max-w-[600px] bg-background rounded-2xl max-h-[90vh] overflow-y-auto">
{selectedMember && (
<>
<DialogHeader>
<DialogTitle className="text-3xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
{selectedMember.first_name} {selectedMember.last_name}
</DialogTitle>
{selectedMember.directory_partner_name && (
<DialogDescription className="flex items-center gap-2 text-lg" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<Heart className="h-5 w-5 text-accent" />
<span className="text-muted-foreground">Partner: {selectedMember.directory_partner_name}</span>
</DialogDescription>
)}
</DialogHeader>
<div className="space-y-6 py-4">
{/* Bio */}
{selectedMember.directory_bio && (
<div>
<h3 className="text-lg font-semibold text-primary mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
About
</h3>
<p className="text-muted-foreground leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{selectedMember.directory_bio}
</p>
</div>
)}
{/* Contact Information */}
<div>
<h3 className="text-lg font-semibold text-primary mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
Contact Information
</h3>
<div className="space-y-3">
{selectedMember.directory_email && (
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-chart-7">
<Mail className="h-5 w-5 text-muted-foreground" />
</div>
<div>
<p className="text-xs text-muted-foreground mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Email</p>
<a
href={`mailto:${selectedMember.directory_email}`}
className="text-primary hover:text-muted-foreground font-medium"
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
>
{selectedMember.directory_email}
</a>
</div>
</div>
)}
{selectedMember.directory_phone && (
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-chart-7">
<Phone className="h-5 w-5 text-muted-foreground" />
</div>
<div>
<p className="text-xs text-muted-foreground mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Phone</p>
<a
href={`tel:${selectedMember.directory_phone}`}
className="text-primary hover:text-muted-foreground font-medium"
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
>
{selectedMember.directory_phone}
</a>
</div>
</div>
)}
{selectedMember.directory_address && (
<div className="flex items-start gap-3">
<div className="p-2 rounded-lg bg-chart-7">
<MapPin className="h-5 w-5 text-muted-foreground" />
</div>
<div>
<p className="text-xs text-muted-foreground mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Address</p>
<p className="text-primary font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{selectedMember.directory_address}
</p>
</div>
</div>
)}
{selectedMember.directory_dob && (
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-chart-7">
<Heart className="h-5 w-5 text-accent" />
</div>
<div>
<p className="text-xs text-muted-foreground mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Birthday</p>
<p className="text-primary font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{formatDate(selectedMember.directory_dob)}
</p>
</div>
</div>
)}
</div>
</div>
{/* Volunteer Interests */}
{selectedMember.volunteer_interests && selectedMember.volunteer_interests.length > 0 && (
<div>
<h3 className="text-lg font-semibold text-primary mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
Volunteer Interests
</h3>
<div className="flex flex-wrap gap-2">
{selectedMember.volunteer_interests.map((interest, index) => (
<Badge
key={index}
className="bg-chart-6 text-primary hover:bg-muted-foreground hover:text-white"
>
{interest}
</Badge>
))}
</div>
</div>
)}
{/* Social Media */}
{(selectedMember.social_media_facebook || selectedMember.social_media_instagram ||
selectedMember.social_media_twitter || selectedMember.social_media_linkedin) && (
<div>
<h3 className="text-lg font-semibold text-primary 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-chart-7 hover:bg-chart-6 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-chart-7 hover:bg-chart-6 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-chart-7 hover:bg-chart-6 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-chart-7 hover:bg-chart-6 transition-colors"
title="LinkedIn"
>
<Linkedin className="h-6 w-6 text-[#0A66C2]" />
</a>
)}
</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-muted-foreground" 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(1)}
disabled={currentPage === 1}
className="bg-chart-6 rounded-full text-primary hover:bg-muted-foreground hover:text-white"
>
First Page
</Button>
<Button
onClick={() => setCurrentPage((page) => Math.max(1, page - 1))}
disabled={currentPage === 1}
className="bg-chart-6 rounded-full text-primary hover:bg-muted-foreground 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-muted-foreground text-white hover:bg-primary rounded-full"
: "bg-chart-6 text-primary hover:bg-muted-foreground hover:text-white rounded-full"
}
>
{pageNumber}
</Button>
);
})}
<Button
onClick={() => setCurrentPage((page) => Math.min(totalPages, page + 1))}
disabled={currentPage === totalPages}
className="bg-chart-6 text-primary hover:bg-muted-foreground rounded-full hover:text-white"
>
Next
</Button>
<Button
onClick={() => setCurrentPage(totalPages)}
disabled={currentPage === totalPages}
className="bg-chart-6 text-primary hover:bg-muted-foreground rounded-full hover:text-white"
>
Last Page
</Button>
</div>
</div>
)}
<MemberFooter />
</div>
);
};
export default MembersDirectory;