Enhancements
This commit is contained in:
@@ -0,0 +1,835 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Server,
|
||||
Database,
|
||||
Users,
|
||||
Activity,
|
||||
RefreshCw,
|
||||
Plus,
|
||||
Edit3,
|
||||
Trash2,
|
||||
Eye,
|
||||
AlertCircle,
|
||||
CheckCircle,
|
||||
XCircle
|
||||
} from 'lucide-react';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
function App() {
|
||||
useEffect(() => {
|
||||
const ws = new WebSocket('ws://localhost:8000/ws/logs');
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.type === 'log') {
|
||||
console.log(`[Backend Log]: ${data.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('WebSocket connection closed');
|
||||
};
|
||||
|
||||
return () => {
|
||||
ws.close();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <div>Your React App</div>;
|
||||
}
|
||||
|
||||
const Dashboard = () => {
|
||||
const [apiUrl, setApiUrl] = useState('http://127.0.0.1:8000');
|
||||
const [connectionStatus, setConnectionStatus] = useState('disconnected');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState('dashboard');
|
||||
const [members, setMembers] = useState([]);
|
||||
const [totalMembers, setTotalMembers] = useState(0);
|
||||
const [activeMembersCount, setActiveMembersCount] = useState(0);
|
||||
const [inactiveMembersCount, setInactiveMembersCount] = useState(0);
|
||||
const [deactivatedMembersCount, setDeactivatedMembersCount] = useState(0);
|
||||
const [loadingData, setLoadingData] = useState(false);
|
||||
|
||||
const testConnection = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
console.log('Testing connection to:', apiUrl);
|
||||
// Test with a simple endpoint first - try /member/1 since /member/100 works
|
||||
const response = await fetch(`${apiUrl}/member/100`);
|
||||
|
||||
if (response.ok) {
|
||||
setConnectionStatus('connected');
|
||||
console.log('Connection successful!');
|
||||
// Load members data when connection is successful
|
||||
await loadMembersCount();
|
||||
} else {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Connection error:', error);
|
||||
setConnectionStatus('error');
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const loadMembersCount = async () => {
|
||||
if (connectionStatus !== 'connected' && !isLoading) return;
|
||||
|
||||
setLoadingData(true);
|
||||
console.log('Attempting to load active members count...');
|
||||
|
||||
// const fetchActiveMembersCount = async () => {
|
||||
try {
|
||||
const response = await fetch(`${apiUrl}/activememberscount`);
|
||||
if (response.ok) {
|
||||
const count = await response.json(); // Single number return
|
||||
setTotalMembers(count);
|
||||
console.log('Total active members count:', count);
|
||||
} else {
|
||||
console.log('Failed to fetch active members count:', response.statusText);
|
||||
setTotalMembers(0);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Error fetching active members count:', error);
|
||||
setMembers([]);
|
||||
setTotalMembers(0);
|
||||
}
|
||||
//}
|
||||
|
||||
setLoadingData(false);
|
||||
};
|
||||
|
||||
// Fetch member counts
|
||||
const fetchMemberCounts = async () => {
|
||||
if (connectionStatus !== 'connected' && !isLoading) return;
|
||||
|
||||
try {
|
||||
// Fetch active members count
|
||||
const activeResponse = await fetch(`${apiUrl}/activememberscount`, {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
if (activeResponse.ok) {
|
||||
const activeCount = await activeResponse.json();
|
||||
setActiveMembersCount(activeCount);
|
||||
console.log('activememberscount: ', activeCount);
|
||||
}
|
||||
|
||||
// Fetch inactive members count
|
||||
const inactiveResponse = await fetch(`${apiUrl}/inactivememberscount`, {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
if (inactiveResponse.ok) {
|
||||
const inactiveCount = await inactiveResponse.json();
|
||||
setInactiveMembersCount(inactiveCount);
|
||||
}
|
||||
|
||||
// Fetch deactivated members count
|
||||
const deactivatedResponse = await fetch(`${apiUrl}/deactivatedmemberscount`, {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
if (deactivatedResponse.ok) {
|
||||
const deactivatedCount = await deactivatedResponse.json();
|
||||
setDeactivatedMembersCount(deactivatedCount);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching member counts:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch data from API
|
||||
const fetchData = async (endpoint = 'items') => {
|
||||
if (connectionStatus !== 'connected' && !isLoading) return;
|
||||
|
||||
setLoadingData(true);
|
||||
try {
|
||||
const response = await fetch(`${apiUrl}/${endpoint}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
setData(Array.isArray(result) ? result : [result]);
|
||||
} else {
|
||||
throw new Error(`Failed to fetch data: ${response.statusText}`);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoadingData(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadMembers = async () => {
|
||||
if (connectionStatus !== 'connected' && !isLoading) return;
|
||||
|
||||
setLoadingData(true);
|
||||
try {
|
||||
// Since /members/ gives 500 error, let's try different approaches
|
||||
console.log('Attempting to load active members count...');
|
||||
|
||||
// Option 1: Try individual member requests (if you know IDs)
|
||||
// For now, let's try to get a few members by ID
|
||||
const memberIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; // Adjust these IDs as needed
|
||||
const memberPromises = memberIds.map(async (id) => {
|
||||
try {
|
||||
const response = await fetch(`${apiUrl}/member/${id}`);
|
||||
if (response.ok) {
|
||||
return await response.json();
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.log(`Member ${id} not found or error:`, error);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const results = await Promise.all(memberPromises);
|
||||
const validMembers = results.filter(member => member !== null);
|
||||
|
||||
console.log('Found members:', validMembers);
|
||||
setMembers(validMembers);
|
||||
setTotalMembers(validMembers.length);
|
||||
|
||||
if (validMembers.length === 0) {
|
||||
console.warn('No members found with IDs 1-10. You may need to adjust the ID range or fix the /members/ endpoint.');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading members:', error);
|
||||
setMembers([]);
|
||||
setTotalMembers(0);
|
||||
}
|
||||
setLoadingData(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (connectionStatus === 'connected') {
|
||||
// loadMembersCount();
|
||||
fetchMemberCounts();
|
||||
if (activeTab === 'data') {
|
||||
fetchData();
|
||||
}
|
||||
}
|
||||
}, [connectionStatus]);
|
||||
|
||||
const getStatusColor = () => {
|
||||
switch (connectionStatus) {
|
||||
case 'connected': return '#10b981';
|
||||
case 'error': return '#ef4444';
|
||||
default: return '#f59e0b';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = () => {
|
||||
switch (connectionStatus) {
|
||||
case 'connected': return <CheckCircle size={16} />;
|
||||
case 'error': return <XCircle size={16} />;
|
||||
default: return <AlertCircle size={16} />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = () => {
|
||||
switch (connectionStatus) {
|
||||
case 'connected': return 'Connected';
|
||||
case 'error': return 'Connection Error';
|
||||
default: return 'Disconnected';
|
||||
}
|
||||
};
|
||||
|
||||
const getFullName = (member) => {
|
||||
const firstName = member.member_firstname || '';
|
||||
const lastName = member.member_surname || '';
|
||||
const memberSuffix = member.member_suffix || '';
|
||||
const fullName = `${firstName} ${lastName} ${memberSuffix}`.trim();
|
||||
return fullName || 'N/A';
|
||||
};
|
||||
|
||||
const MEMBER_STATE_COLORS = {
|
||||
'A': '#10B981', // Green - Active
|
||||
'I': '#F59E0B', // Amber/Orange - Inactive
|
||||
'D': '#EF4444' // Red - Deactivated
|
||||
};
|
||||
|
||||
const getMemberStateColor = (stateCode) => {
|
||||
return MEMBER_STATE_COLORS[stateCode] || '#6B7280'; // Gray as fallback
|
||||
};
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
minHeight: '100vh',
|
||||
backgroundColor: '#f9fafb',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
|
||||
},
|
||||
header: {
|
||||
backgroundColor: 'white',
|
||||
boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.1)',
|
||||
borderBottom: '1px solid #e5e7eb'
|
||||
},
|
||||
headerContent: {
|
||||
maxWidth: '1280px',
|
||||
margin: '0 auto',
|
||||
padding: '0 24px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingTop: '24px',
|
||||
paddingBottom: '24px'
|
||||
},
|
||||
title: {
|
||||
fontSize: '30px',
|
||||
fontWeight: 'bold',
|
||||
color: '#111827',
|
||||
margin: 0
|
||||
},
|
||||
subtitle: {
|
||||
color: '#6b7280',
|
||||
marginTop: '4px',
|
||||
fontSize: '16px'
|
||||
},
|
||||
statusContainer: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
color: getStatusColor(),
|
||||
fontWeight: '500'
|
||||
},
|
||||
mainContent: {
|
||||
maxWidth: '1280px',
|
||||
margin: '0 auto',
|
||||
padding: '32px 24px'
|
||||
},
|
||||
card: {
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.1)',
|
||||
border: '1px solid #e5e7eb',
|
||||
padding: '24px',
|
||||
marginBottom: '32px'
|
||||
},
|
||||
cardTitle: {
|
||||
fontSize: '18px',
|
||||
fontWeight: '600',
|
||||
color: '#111827',
|
||||
marginBottom: '16px'
|
||||
},
|
||||
connectionForm: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '16px'
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
outline: 'none'
|
||||
},
|
||||
button: {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
padding: '8px 16px',
|
||||
backgroundColor: '#2563eb',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
cursor: 'pointer',
|
||||
transition: 'background-color 0.2s'
|
||||
},
|
||||
buttonDisabled: {
|
||||
opacity: 0.5,
|
||||
cursor: 'not-allowed'
|
||||
},
|
||||
errorAlert: {
|
||||
marginTop: '16px',
|
||||
padding: '16px',
|
||||
backgroundColor: '#fef2f2',
|
||||
border: '1px solid #fecaca',
|
||||
borderRadius: '6px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
},
|
||||
errorText: {
|
||||
color: '#991b1b'
|
||||
},
|
||||
tabs: {
|
||||
borderBottom: '1px solid #e5e7eb',
|
||||
marginBottom: '32px'
|
||||
},
|
||||
tabsNav: {
|
||||
display: 'flex',
|
||||
gap: '32px'
|
||||
},
|
||||
tab: {
|
||||
padding: '8px 4px',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
border: 'none',
|
||||
background: 'none',
|
||||
cursor: 'pointer',
|
||||
borderBottom: '2px solid transparent',
|
||||
transition: 'all 0.2s'
|
||||
},
|
||||
tabActive: {
|
||||
borderBottom: '2px solid #2563eb',
|
||||
color: '#2563eb'
|
||||
},
|
||||
tabInactive: {
|
||||
color: '#6b7280'
|
||||
},
|
||||
statsGrid: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))',
|
||||
gap: '24px',
|
||||
marginBottom: '32px'
|
||||
},
|
||||
statCard: {
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.1)',
|
||||
border: '1px solid #e5e7eb',
|
||||
padding: '24px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
statContent: {
|
||||
flex: 1
|
||||
},
|
||||
statLabel: {
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
color: '#6b7280'
|
||||
},
|
||||
statValue: {
|
||||
fontSize: '24px',
|
||||
fontWeight: 'bold',
|
||||
color: '#111827',
|
||||
marginTop: '8px'
|
||||
},
|
||||
statIcon: {
|
||||
padding: '12px',
|
||||
borderRadius: '50%'
|
||||
},
|
||||
iconOrange: {
|
||||
backgroundColor: '#fed7aa',
|
||||
color: '#ea580c'
|
||||
},
|
||||
iconBlue: {
|
||||
backgroundColor: '#dbeafe',
|
||||
color: '#2563eb'
|
||||
},
|
||||
iconGreen: {
|
||||
backgroundColor: '#dcfce7',
|
||||
color: '#16a34a'
|
||||
},
|
||||
iconPurple: {
|
||||
backgroundColor: '#e9d5ff',
|
||||
color: '#9333ea'
|
||||
},
|
||||
actionButtons: {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '16px'
|
||||
},
|
||||
actionButton: {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
padding: '8px 16px',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
cursor: 'pointer',
|
||||
transition: 'background-color 0.2s'
|
||||
},
|
||||
actionButtonGray: {
|
||||
backgroundColor: '#f3f4f6',
|
||||
color: '#374151'
|
||||
},
|
||||
actionButtonGreen: {
|
||||
backgroundColor: '#dcfce7',
|
||||
color: '#166534'
|
||||
},
|
||||
actionButtonBlue: {
|
||||
backgroundColor: '#dbeafe',
|
||||
color: '#1e40af'
|
||||
},
|
||||
emptyState: {
|
||||
textAlign: 'center',
|
||||
padding: '32px'
|
||||
},
|
||||
emptyStateIcon: {
|
||||
margin: '0 auto 16px',
|
||||
color: '#9ca3af'
|
||||
},
|
||||
emptyStateText: {
|
||||
color: '#6b7280'
|
||||
},
|
||||
cardHeader: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: '20px'
|
||||
},
|
||||
|
||||
refreshButton: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '8px 16px',
|
||||
backgroundColor: '#3B82F6',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500'
|
||||
},
|
||||
|
||||
loadingState: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
padding: '40px',
|
||||
color: '#6B7280'
|
||||
},
|
||||
|
||||
loadingIcon: {
|
||||
marginBottom: '10px',
|
||||
color: '#3B82F6'
|
||||
},
|
||||
|
||||
dataContainer: {
|
||||
marginTop: '20px'
|
||||
},
|
||||
|
||||
dataHeader: {
|
||||
marginBottom: '16px',
|
||||
padding: '12px',
|
||||
backgroundColor: '#F3F4F6',
|
||||
borderRadius: '6px'
|
||||
},
|
||||
|
||||
dataCount: {
|
||||
margin: 0,
|
||||
fontWeight: '600',
|
||||
color: '#374151'
|
||||
},
|
||||
|
||||
tableContainer: {
|
||||
overflowX: 'auto',
|
||||
border: '1px solid #E5E7EB',
|
||||
borderRadius: '8px'
|
||||
},
|
||||
|
||||
table: {
|
||||
width: '100%',
|
||||
borderCollapse: 'collapse'
|
||||
},
|
||||
|
||||
tableHeaderRow: {
|
||||
backgroundColor: '#F9FAFB'
|
||||
},
|
||||
|
||||
tableHeader: {
|
||||
padding: '12px',
|
||||
textAlign: 'left',
|
||||
fontWeight: '600',
|
||||
color: '#374151',
|
||||
borderBottom: '1px solid #E5E7EB'
|
||||
},
|
||||
|
||||
tableRow: {
|
||||
borderBottom: '1px solid #F3F4F6'
|
||||
},
|
||||
|
||||
tableCell: {
|
||||
padding: '12px',
|
||||
color: '#6B7280'
|
||||
},
|
||||
|
||||
statusBadge: {
|
||||
padding: '4px 8px',
|
||||
borderRadius: '12px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '500'
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
{/* Header */}
|
||||
<div style={styles.header}>
|
||||
<div style={styles.headerContent}>
|
||||
<div>
|
||||
<h1 style={styles.title}>FastAPI Dashboard</h1>
|
||||
<p style={styles.subtitle}>Manage your backend data and monitor API status</p>
|
||||
</div>
|
||||
<div style={styles.statusContainer}>
|
||||
{getStatusIcon()}
|
||||
<span>{getStatusText()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div style={styles.mainContent}>
|
||||
{/* Connection Section */}
|
||||
<div style={styles.card}>
|
||||
<h2 style={styles.cardTitle}>API Connection</h2>
|
||||
<div style={styles.connectionForm}>
|
||||
<input
|
||||
type="text"
|
||||
value={apiUrl}
|
||||
onChange={(e) => setApiUrl(e.target.value)}
|
||||
style={styles.input}
|
||||
placeholder="Enter API URL"
|
||||
/>
|
||||
<button
|
||||
onClick={testConnection}
|
||||
disabled={isLoading}
|
||||
style={{
|
||||
...styles.button,
|
||||
...(isLoading ? styles.buttonDisabled : {}),
|
||||
':hover': { backgroundColor: '#1d4ed8' }
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isLoading) e.target.style.backgroundColor = '#1d4ed8';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isLoading) e.target.style.backgroundColor = '#2563eb';
|
||||
}}
|
||||
>
|
||||
<RefreshCw size={16} style={isLoading ? { animation: 'spin 1s linear infinite' } : {}} />
|
||||
Test Connection
|
||||
</button>
|
||||
</div>
|
||||
{connectionStatus === 'error' && (
|
||||
<div style={styles.errorAlert}>
|
||||
<XCircle size={20} style={{ color: '#dc2626' }} />
|
||||
<span style={styles.errorText}>NetworkError when attempting to fetch resource.</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Navigation Tabs */}
|
||||
<div style={styles.tabs}>
|
||||
<div style={styles.tabsNav}>
|
||||
<button
|
||||
onClick={() => setActiveTab('dashboard')}
|
||||
style={{
|
||||
...styles.tab,
|
||||
...(activeTab === 'dashboard' ? styles.tabActive : styles.tabInactive)
|
||||
}}
|
||||
>
|
||||
Dashboard
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('data')}
|
||||
style={{
|
||||
...styles.tab,
|
||||
...(activeTab === 'data' ? styles.tabActive : styles.tabInactive)
|
||||
}}
|
||||
>
|
||||
Data
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<div style={styles.statsGrid}>
|
||||
{/* API Status */}
|
||||
<div style={styles.statCard}>
|
||||
<div style={styles.statContent}>
|
||||
<p style={styles.statLabel}>API Status</p>
|
||||
<p style={styles.statValue}>
|
||||
{connectionStatus === 'connected' ? 'Online' : 'Offline'}
|
||||
</p>
|
||||
</div>
|
||||
<div style={{...styles.statIcon, ...styles.iconOrange}}>
|
||||
<Server size={24} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Active Users */}
|
||||
<div style={styles.statCard}>
|
||||
<div style={styles.statContent}>
|
||||
<p style={styles.statLabel}>Active Users</p>
|
||||
<p style={styles.statValue}>1</p>
|
||||
</div>
|
||||
<div style={{...styles.statIcon, ...styles.iconGreen}}>
|
||||
<Users size={24} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Activity */}
|
||||
<div style={styles.statCard}>
|
||||
<div style={styles.statContent}>
|
||||
<p style={styles.statLabel}>Activity</p>
|
||||
<p style={styles.statValue}>Live</p>
|
||||
</div>
|
||||
<div style={{...styles.statIcon, ...styles.iconPurple}}>
|
||||
<Activity size={24} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-r from-green-500 to-green-600 p-6 rounded-xl text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-green-100">Active Members</p>
|
||||
<p className="text-2xl font-bold">{activeMembersCount}</p>
|
||||
</div>
|
||||
<Users className="w-8 h-8 text-green-200" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-r from-yellow-500 to-yellow-600 p-6 rounded-xl text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-yellow-100">Inactive Members</p>
|
||||
<p className="text-2xl font-bold">{inactiveMembersCount}</p>
|
||||
</div>
|
||||
<Users className="w-8 h-8 text-yellow-200" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-r from-red-500 to-red-600 p-6 rounded-xl text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-red-100">Deactivated Members</p>
|
||||
<p className="text-2xl font-bold">{deactivatedMembersCount}</p>
|
||||
</div>
|
||||
<Users className="w-8 h-8 text-red-200" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div style={styles.card}>
|
||||
<h2 style={styles.cardTitle}>Quick Actions</h2>
|
||||
<div style={styles.actionButtons}>
|
||||
<button
|
||||
style={{...styles.actionButton, ...styles.actionButtonGray}}
|
||||
onMouseEnter={(e) => e.target.style.backgroundColor = '#e5e7eb'}
|
||||
onMouseLeave={(e) => e.target.style.backgroundColor = '#f3f4f6'}
|
||||
>
|
||||
<Eye size={16} />
|
||||
View Data
|
||||
</button>
|
||||
<button
|
||||
style={{...styles.actionButton, ...styles.actionButtonGreen}}
|
||||
onMouseEnter={(e) => e.target.style.backgroundColor = '#bbf7d0'}
|
||||
onMouseLeave={(e) => e.target.style.backgroundColor = '#dcfce7'}
|
||||
>
|
||||
<Plus size={16} />
|
||||
Add Item
|
||||
</button>
|
||||
<button
|
||||
style={{...styles.actionButton, ...styles.actionButtonBlue}}
|
||||
onMouseEnter={(e) => e.target.style.backgroundColor = '#bfdbfe'}
|
||||
onMouseLeave={(e) => e.target.style.backgroundColor = '#dbeafe'}
|
||||
>
|
||||
<RefreshCw size={16} />
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content based on active tab */}
|
||||
{activeTab === 'data' && (
|
||||
<div style={styles.card}>
|
||||
<div style={styles.cardHeader}>
|
||||
<h2 style={styles.cardTitle}>Data Management</h2>
|
||||
<button
|
||||
onClick={loadMembers}
|
||||
style={styles.refreshButton}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<RefreshCw size={16} style={{ marginRight: '8px' }} />
|
||||
{isLoading ? 'Loading...' : 'Load Members'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div style={styles.loadingState}>
|
||||
<RefreshCw size={24} style={{ ...styles.loadingIcon, animation: 'spin 1s linear infinite' }} />
|
||||
<p>Loading members...</p>
|
||||
</div>
|
||||
) : members.length === 0 ? (
|
||||
<div style={styles.emptyState}>
|
||||
<Database size={48} style={styles.emptyStateIcon} />
|
||||
<p style={styles.emptyStateText}>No members found. Click "Load Members" to fetch data.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div style={styles.dataContainer}>
|
||||
<div style={styles.dataHeader}>
|
||||
<p style={styles.dataCount}>Total Members: {members.length}</p>
|
||||
</div>
|
||||
|
||||
<div style={styles.tableContainer}>
|
||||
<table style={styles.table}>
|
||||
<thead>
|
||||
<tr style={styles.tableHeaderRow}>
|
||||
<th style={styles.tableHeader}>ID</th>
|
||||
<th style={styles.tableHeader}>Name</th>
|
||||
<th style={styles.tableHeader}>Email</th>
|
||||
<th style={styles.tableHeader}>Active</th>
|
||||
<th style={styles.tableHeader}>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{members.map((member, index) => (
|
||||
<tr key={member.id || index} style={styles.tableRow}>
|
||||
<td style={styles.tableCell}>{member.id}</td>
|
||||
<td style={styles.tableCell}>{getFullName(member)}</td>
|
||||
<td style={styles.tableCell}>{member.member_email || 'N/A'}</td>
|
||||
<td style={styles.tableCell}>
|
||||
<span style={{
|
||||
...styles.statusBadge,
|
||||
backgroundColor: getMemberStateColor(member.current_state.state_code),
|
||||
color: 'white'
|
||||
}}>
|
||||
{member.current_state.state_name}
|
||||
</span>
|
||||
</td>
|
||||
<td style={styles.tableCell}>
|
||||
<button style={styles.actionButton}>
|
||||
<Edit3 size={14} />
|
||||
</button>
|
||||
<button style={styles.actionButton}>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
<style>
|
||||
{`
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
Reference in New Issue
Block a user