ocd-website/frontend/src/app/admin/members/page.tsx
TheMaddax 338f81f7ad Phase 7: Fix theme selector positioning and implement dynamic theme-aware styling
- 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
2025-06-03 17:42:14 -06:00

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>
);
}