- Fixed theme selector dropdown positioning issues with proper Tailwind classes - Replaced custom CSS classes with standard Tailwind utilities for v4 compatibility - Implemented dynamic theme-aware styling system for dropdown appearance - Added theme-specific styling for Light, Dark, High Contrast Light, High Contrast Dark - Enhanced UX with professional dropdown interface and visual consistency - Maintained 100% theme compliance and accessibility standards - Updated memory bank documentation with Phase 7 completion Key improvements: - Professional dropdown with proper shadows, borders, and backgrounds - Dynamic styling that adapts to current website theme - Enhanced accessibility with ARIA support and keyboard navigation - Seamless visual integration across all theme modes - Production-ready theme selector with polished UI
408 lines
13 KiB
TypeScript
408 lines
13 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import { useMembers, Member as MemberType, MemberFilter as MemberFilterType } from '../../../hooks/useMembers';
|
|
import Link from 'next/link';
|
|
|
|
// Member Filter Component
|
|
const MemberFilter = ({
|
|
onFilterChange
|
|
}: {
|
|
onFilterChange: (filter: { status: string; type: string; search: string }) => void
|
|
}) => {
|
|
const [status, setStatus] = useState('all');
|
|
const [type, setType] = useState('all');
|
|
const [search, setSearch] = useState('');
|
|
|
|
const handleFilterChange = () => {
|
|
onFilterChange({ status, type, search });
|
|
};
|
|
|
|
const handleReset = () => {
|
|
setStatus('all');
|
|
setType('all');
|
|
setSearch('');
|
|
onFilterChange({ status: 'all', type: 'all', search: '' });
|
|
};
|
|
|
|
return (
|
|
<div className="admin-card admin-gold-accent">
|
|
<h2 className="admin-card-title admin-gold-title text-lg">Filter Members</h2>
|
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-4">
|
|
<div>
|
|
<label htmlFor="status" className="admin-form-label">
|
|
Status
|
|
</label>
|
|
<select
|
|
id="status"
|
|
className="admin-form-select"
|
|
value={status}
|
|
onChange={(e) => setStatus(e.target.value)}
|
|
>
|
|
<option value="all">All Statuses</option>
|
|
<option value="active">Active</option>
|
|
<option value="expired">Expired</option>
|
|
<option value="pending">Pending</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="type" className="admin-form-label">
|
|
Membership Type
|
|
</label>
|
|
<select
|
|
id="type"
|
|
className="admin-form-select"
|
|
value={type}
|
|
onChange={(e) => setType(e.target.value)}
|
|
>
|
|
<option value="all">All Types</option>
|
|
<option value="regular">Regular</option>
|
|
<option value="family">Family</option>
|
|
<option value="lifetime">Lifetime</option>
|
|
<option value="honorary">Honorary</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="search" className="admin-form-label">
|
|
Search
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="search"
|
|
className="admin-form-input"
|
|
placeholder="Search by name, email, or phone"
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end space-x-3">
|
|
<button
|
|
type="button"
|
|
onClick={handleReset}
|
|
className="admin-btn-secondary"
|
|
>
|
|
Reset
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={handleFilterChange}
|
|
className="admin-btn-primary"
|
|
>
|
|
Apply Filters
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// Bulk actions component
|
|
const BulkActions = ({ selectedMembers, onAction }: { selectedMembers: string[], onAction: (action: string) => void }) => {
|
|
if (selectedMembers.length === 0) return null;
|
|
|
|
return (
|
|
<div className="admin-card admin-gold-accent p-4 mb-6 flex items-center justify-between">
|
|
<div className="admin-bulk-actions-text">
|
|
{selectedMembers.length} member{selectedMembers.length > 1 ? 's' : ''} selected
|
|
</div>
|
|
<div className="flex space-x-3">
|
|
<button
|
|
type="button"
|
|
onClick={() => onAction('email')}
|
|
className="admin-btn-secondary"
|
|
>
|
|
Email Selected
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => onAction('renew')}
|
|
className="admin-btn-primary"
|
|
>
|
|
Renew Selected
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default function MembersPage() {
|
|
const [filter, setFilter] = useState({ status: 'all', type: 'all', search: '' });
|
|
const [selectedMembers, setSelectedMembers] = useState<string[]>([]);
|
|
const [selectAll, setSelectAll] = useState(false);
|
|
|
|
// Use the members hook
|
|
const {
|
|
members,
|
|
loading,
|
|
error,
|
|
pagination,
|
|
fetchMembers,
|
|
bulkAction
|
|
} = useMembers();
|
|
|
|
// Apply filters when they change
|
|
useEffect(() => {
|
|
const apiFilter: MemberFilterType = {};
|
|
|
|
if (filter.status !== 'all') {
|
|
apiFilter.status = filter.status;
|
|
}
|
|
|
|
if (filter.type !== 'all') {
|
|
apiFilter.type = filter.type;
|
|
}
|
|
|
|
if (filter.search) {
|
|
apiFilter.search = filter.search;
|
|
}
|
|
|
|
fetchMembers(apiFilter);
|
|
}, [filter, fetchMembers]);
|
|
|
|
// Handle filter changes
|
|
const handleFilterChange = (newFilter: { status: string; type: string; search: string }) => {
|
|
setFilter(newFilter);
|
|
};
|
|
|
|
// Handle API errors
|
|
if (error) {
|
|
return (
|
|
<div className="admin-card">
|
|
<h2 className="text-lg font-semibold admin-error-text">Error Loading Members</h2>
|
|
<p className="admin-error-text mt-2">{error.message}</p>
|
|
<button
|
|
onClick={() => fetchMembers()}
|
|
className="mt-4 px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700"
|
|
>
|
|
Retry
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Handle bulk actions
|
|
const handleBulkAction = async (action: string) => {
|
|
if (selectedMembers.length === 0) return;
|
|
|
|
const result = await bulkAction(action, selectedMembers);
|
|
|
|
if (result) {
|
|
// Reset selection after successful action
|
|
setSelectedMembers([]);
|
|
setSelectAll(false);
|
|
|
|
// Could add a toast notification here
|
|
alert(`${result.message} - Affected: ${result.affected} members`);
|
|
} else {
|
|
alert('Failed to perform bulk action. Please try again.');
|
|
}
|
|
};
|
|
|
|
// Handle select all checkbox
|
|
const handleSelectAll = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
const checked = event.target.checked;
|
|
setSelectAll(checked);
|
|
|
|
if (checked) {
|
|
setSelectedMembers(members.map(member => member._id));
|
|
} else {
|
|
setSelectedMembers([]);
|
|
}
|
|
};
|
|
|
|
// Handle individual checkbox selection
|
|
const handleSelectMember = (memberId: string, checked: boolean) => {
|
|
if (checked) {
|
|
setSelectedMembers(prev => [...prev, memberId]);
|
|
} else {
|
|
setSelectedMembers(prev => prev.filter(id => id !== memberId));
|
|
}
|
|
};
|
|
|
|
// Function to get status badge styles - CSS Variable Strategy
|
|
const getStatusBadgeClasses = (status: string) => {
|
|
switch (status) {
|
|
case 'active':
|
|
return 'admin-badge admin-badge-success';
|
|
case 'expired':
|
|
return 'admin-badge admin-badge-error';
|
|
case 'pending':
|
|
return 'admin-badge admin-badge-warning';
|
|
default:
|
|
return 'admin-badge admin-badge-default';
|
|
}
|
|
};
|
|
|
|
// Format date for display
|
|
const formatDate = (dateString: string | null) => {
|
|
if (!dateString) return 'N/A';
|
|
|
|
const date = new Date(dateString);
|
|
return new Intl.DateTimeFormat('en-US', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric'
|
|
}).format(date);
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<div className="admin-page-header">
|
|
<div>
|
|
<h1 className="admin-page-header-title">Members</h1>
|
|
<p className="admin-page-header-subtitle">
|
|
Manage memberships, track expirations, and communicate with members.
|
|
</p>
|
|
</div>
|
|
<div className="mt-4 sm:mt-0">
|
|
<Link
|
|
href="/admin/members/add"
|
|
className="admin-btn-primary inline-flex items-center px-4 py-2 text-sm font-medium rounded-md"
|
|
>
|
|
<svg className="-ml-1 mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
|
</svg>
|
|
Add Member
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filter component */}
|
|
<MemberFilter onFilterChange={handleFilterChange} />
|
|
|
|
{/* Bulk actions */}
|
|
<BulkActions
|
|
selectedMembers={selectedMembers}
|
|
onAction={handleBulkAction}
|
|
/>
|
|
|
|
{/* Members table */}
|
|
<div className="admin-card p-0 overflow-hidden">
|
|
<div className="overflow-x-auto">
|
|
<table className="admin-table">
|
|
<thead className="admin-table-header">
|
|
<tr>
|
|
<th scope="col" className="admin-table-header-cell">
|
|
<div className="flex items-center">
|
|
<input
|
|
id="select-all"
|
|
type="checkbox"
|
|
className="h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded"
|
|
checked={selectAll}
|
|
onChange={handleSelectAll}
|
|
/>
|
|
<label htmlFor="select-all" className="sr-only">Select All</label>
|
|
</div>
|
|
</th>
|
|
<th scope="col" className="admin-table-header-cell">
|
|
Member
|
|
</th>
|
|
<th scope="col" className="admin-table-header-cell">
|
|
Contact
|
|
</th>
|
|
<th scope="col" className="admin-table-header-cell">
|
|
Membership
|
|
</th>
|
|
<th scope="col" className="admin-table-header-cell">
|
|
Dates
|
|
</th>
|
|
<th scope="col" className="admin-table-header-cell">
|
|
Status
|
|
</th>
|
|
<th scope="col" className="admin-table-header-cell text-right">
|
|
Actions
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="admin-table-body">
|
|
{loading ? (
|
|
<tr>
|
|
<td colSpan={7} className="px-6 py-12 text-center">
|
|
<div className="flex justify-center">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
|
</div>
|
|
<p className="mt-2 admin-text-light">Loading members...</p>
|
|
</td>
|
|
</tr>
|
|
) : (
|
|
members.map((member) => (
|
|
<tr key={member._id} className="admin-table-row">
|
|
<td className="admin-table-cell">
|
|
<div className="flex items-center">
|
|
<input
|
|
id={`select-${member._id}`}
|
|
type="checkbox"
|
|
className="h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded"
|
|
checked={selectedMembers.includes(member._id)}
|
|
onChange={(e) => handleSelectMember(member._id, e.target.checked)}
|
|
/>
|
|
<label htmlFor={`select-${member._id}`} className="sr-only">Select {member.firstName} {member.lastName}</label>
|
|
</div>
|
|
</td>
|
|
<td className="admin-table-cell">
|
|
<div className="flex items-center">
|
|
<div>
|
|
<div className="admin-table-cell-title">
|
|
{member.firstName} {member.lastName}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td className="admin-table-cell">
|
|
<div className="admin-table-cell-title">{member.email}</div>
|
|
<div className="admin-table-cell-subtitle">{member.phone}</div>
|
|
</td>
|
|
<td className="admin-table-cell">
|
|
<div className="admin-table-cell-title capitalize">{member.membershipType}</div>
|
|
</td>
|
|
<td className="admin-table-cell">
|
|
<div className="admin-table-cell-subtitle">
|
|
Joined: {formatDate(member.joinDate)}
|
|
</div>
|
|
<div className="admin-table-cell-subtitle">
|
|
Expires: {formatDate(member.expirationDate)}
|
|
</div>
|
|
</td>
|
|
<td className="admin-table-cell">
|
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusBadgeClasses(member.status)}`}>
|
|
{member.status.charAt(0).toUpperCase() + member.status.slice(1)}
|
|
</span>
|
|
</td>
|
|
<td className="admin-table-cell text-right">
|
|
<div className="flex justify-end space-x-2">
|
|
<Link href={`/admin/members/${member._id}`} className="admin-action-link">
|
|
View
|
|
</Link>
|
|
<Link href={`/admin/members/${member._id}/edit`} className="admin-action-link">
|
|
Edit
|
|
</Link>
|
|
<button
|
|
onClick={() => {
|
|
// In a real app, this would call an API
|
|
handleBulkAction('renew');
|
|
}}
|
|
className="admin-action-link"
|
|
>
|
|
Renew
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{!loading && members.length === 0 && (
|
|
<div className="py-12 text-center">
|
|
<p className="admin-text-light">No members found matching the current filters.</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|