533 lines
22 KiB
JavaScript
533 lines
22 KiB
JavaScript
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();
|
|
|
|
useEffect(() => {
|
|
fetchMembers();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
filterMembers();
|
|
}, [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 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 MemberCard = ({ member }) => (
|
|
<Card className="p-6 bg-white rounded-2xl border border-[#ddd8eb] 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-[#ddd8eb]"
|
|
/>
|
|
) : (
|
|
<div className="w-32 h-32 rounded-full bg-[#DDD8EB] border-4 border-[#ddd8eb] flex items-center justify-center">
|
|
<span className="text-4xl font-semibold text-[#664fa3]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
{getInitials(member.first_name, member.last_name)}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Name */}
|
|
<h3 className="text-2xl font-semibold text-[#422268] 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-[#ff9e77]" />
|
|
<span className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
Partner: {member.directory_partner_name}
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Bio */}
|
|
{member.directory_bio && (
|
|
<p className="text-[#664fa3] text-center mb-4 line-clamp-3" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
{member.directory_bio}
|
|
</p>
|
|
)}
|
|
|
|
{/* Member Since */}
|
|
<div className="flex items-center justify-center gap-2 mb-4">
|
|
<Calendar className="h-4 w-4 text-[#664fa3]" />
|
|
<span className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
Member since {new Date(member.member_since || 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-[#664fa3] flex-shrink-0" />
|
|
<a
|
|
href={`mailto:${member.directory_email}`}
|
|
className="text-[#664fa3] hover:text-[#422268] 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-[#664fa3] flex-shrink-0" />
|
|
<a
|
|
href={`tel:${member.directory_phone}`}
|
|
className="text-[#664fa3] hover:text-[#422268]"
|
|
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-[#664fa3] flex-shrink-0 mt-0.5" />
|
|
<span className="text-[#664fa3]" 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-[#ddd8eb]">
|
|
<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-[#F8F7FB] hover:bg-[#DDD8EB] 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-[#F8F7FB] hover:bg-[#DDD8EB] 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-[#F8F7FB] hover:bg-[#DDD8EB] 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-[#F8F7FB] hover:bg-[#DDD8EB] 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-[#ddd8eb]">
|
|
<Button
|
|
onClick={() => handleViewProfile(member.id)}
|
|
className="w-full bg-[#DDD8EB] text-[#422268] hover:bg-[#664fa3] 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-white">
|
|
<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>
|
|
|
|
{/* 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'}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Members Grid */}
|
|
{loading ? (
|
|
<div className="text-center py-20">
|
|
<p className="text-[#664fa3]" 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">
|
|
{filteredMembers.map((member) => (
|
|
<MemberCard key={member.id} member={member} />
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-20">
|
|
<User className="h-20 w-20 text-[#ddd8eb] mx-auto mb-6" />
|
|
<h3 className="text-2xl font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
{searchQuery ? 'No Members Found' : 'No Members in Directory'}
|
|
</h3>
|
|
<p className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
{searchQuery
|
|
? 'Try adjusting your search query.'
|
|
: 'Members who opt in to the directory will appear here.'}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Info Card */}
|
|
{!loading && members.length > 0 && (
|
|
<Card className="mt-12 p-6 bg-[#F8F7FB] border-[#ddd8eb]">
|
|
<div className="flex items-start gap-4">
|
|
<div className="bg-[#DDD8EB]/20 p-3 rounded-lg">
|
|
<User className="h-6 w-6 text-[#664fa3]" />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-lg font-semibold text-[#422268] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
Want to appear in the directory?
|
|
</h3>
|
|
<p className="text-[#664fa3]" 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-[#ff9e77] 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-white rounded-2xl max-h-[90vh] overflow-y-auto">
|
|
{selectedMember && (
|
|
<>
|
|
<DialogHeader>
|
|
<DialogTitle className="text-3xl font-semibold text-[#422268]" 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-[#ff9e77]" />
|
|
<span className="text-[#664fa3]">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-[#422268] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
About
|
|
</h3>
|
|
<p className="text-[#664fa3] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
{selectedMember.directory_bio}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Contact Information */}
|
|
<div>
|
|
<h3 className="text-lg font-semibold text-[#422268] 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-[#F8F7FB]">
|
|
<Mail className="h-5 w-5 text-[#664fa3]" />
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-[#664fa3] mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Email</p>
|
|
<a
|
|
href={`mailto:${selectedMember.directory_email}`}
|
|
className="text-[#422268] hover:text-[#664fa3] 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-[#F8F7FB]">
|
|
<Phone className="h-5 w-5 text-[#664fa3]" />
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-[#664fa3] mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Phone</p>
|
|
<a
|
|
href={`tel:${selectedMember.directory_phone}`}
|
|
className="text-[#422268] hover:text-[#664fa3] 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-[#F8F7FB]">
|
|
<MapPin className="h-5 w-5 text-[#664fa3]" />
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-[#664fa3] mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Address</p>
|
|
<p className="text-[#422268] 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-[#F8F7FB]">
|
|
<Heart className="h-5 w-5 text-[#ff9e77]" />
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-[#664fa3] mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Birthday</p>
|
|
<p className="text-[#422268] 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-[#422268] 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-[#DDD8EB] text-[#422268] hover:bg-[#664fa3] 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-[#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_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>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
<MemberFooter />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default MembersDirectory;
|