Enhancements

This commit is contained in:
2026-06-25 11:19:08 +02:00
parent 8804d21c32
commit 9343657d20
62 changed files with 10210 additions and 976 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,334 @@
import React, { useState, useEffect } from 'react';
import { BarChart3, Users, CreditCard, FileText, Settings, LogOut } from 'lucide-react';
interface Member {
id: number;
member_firstname: string;
member_surname: string;
member_email: string;
member_active: boolean;
member_count: number;
}
interface Payment {
id: number;
member_id: number;
payment_date: string;
payment_amount: number;
payment_year: string;
}
interface DashboardStats {
total_members: number;
active_members: number;
total_payments: number;
total_amount: number;
}
const FastAPIFrontend: React.FC = () => {
const [activeTab, setActiveTab] = useState<'dashboard' | 'members' | 'payments' | 'settings'>('dashboard');
const [members, setMembers] = useState<Member[]>([]);
const [payments, setPayments] = useState<Payment[]>([]);
const [stats, setStats] = useState<DashboardStats>({
total_members: 0,
active_members: 0,
total_payments: 0,
total_amount: 0,
});
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const apiUrl = 'http://localhost:8000';
useEffect(() => {
fetchDashboardData();
}, []);
const fetchDashboardData = async () => {
setLoading(true);
setError(null);
try {
const [membersRes, paymentsRes] = await Promise.all([
fetch(`${apiUrl}/members/`),
fetch(`${apiUrl}/payments/`),
]);
if (membersRes.ok && paymentsRes.ok) {
const membersData = await membersRes.json();
const paymentsData = await paymentsRes.json();
setMembers(membersData);
setPayments(paymentsData);
// Calculate stats
const activeCount = membersData.filter((m: Member) => m.member_active).length;
const totalAmount = paymentsData.reduce((sum: number, p: Payment) => sum + p.payment_amount, 0);
setStats({
total_members: membersData.length,
active_members: activeCount,
total_payments: paymentsData.length,
total_amount: totalAmount,
});
} else {
setError('Failed to fetch data from server');
}
} catch (err) {
setError('Error connecting to server: ' + (err instanceof Error ? err.message : 'Unknown error'));
} finally {
setLoading(false);
}
};
const DashboardView = () => (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<StatCard
title="Total Members"
value={stats.total_members}
icon={<Users className="w-8 h-8" />}
color="blue"
/>
<StatCard
title="Active Members"
value={stats.active_members}
icon={<Users className="w-8 h-8" />}
color="green"
/>
<StatCard
title="Total Payments"
value={stats.total_payments}
icon={<CreditCard className="w-8 h-8" />}
color="purple"
/>
<StatCard
title="Total Amount"
value={`${stats.total_amount.toFixed(2)}`}
icon={<BarChart3 className="w-8 h-8" />}
color="orange"
/>
</div>
);
const MembersView = () => (
<div className="bg-white rounded-lg shadow overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200">
<h2 className="text-lg font-semibold text-gray-900">Members List</h2>
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Email</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Count</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{members.map((member) => (
<tr key={member.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
{member.member_firstname} {member.member_surname}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{member.member_email}</td>
<td className="px-6 py-4 whitespace-nowrap">
<span
className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${
member.member_active
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}
>
{member.member_active ? 'Active' : 'Inactive'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{member.member_count}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
const PaymentsView = () => (
<div className="bg-white rounded-lg shadow overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200">
<h2 className="text-lg font-semibold text-gray-900">Payments List</h2>
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Member ID</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Amount</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Year</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{payments.map((payment) => (
<tr key={payment.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{payment.member_id}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{new Date(payment.payment_date).toLocaleDateString()}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{payment.payment_amount.toFixed(2)}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{payment.payment_year}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
const SettingsView = () => (
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Settings</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700">API Server</label>
<input
type="text"
value={apiUrl}
disabled
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md bg-gray-50 text-gray-500"
/>
</div>
<button
onClick={fetchDashboardData}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700"
>
Refresh Data
</button>
</div>
</div>
);
return (
<div className="min-h-screen bg-gray-900">
{/* Header */}
<header className="bg-gray-800 border-b border-gray-700">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<BarChart3 className="w-8 h-8 text-blue-500" />
<h1 className="text-2xl font-bold text-white">SUST Dashboard</h1>
</div>
<button className="inline-flex items-center px-3 py-2 border border-gray-600 rounded-md text-sm font-medium text-gray-300 hover:bg-gray-700">
<LogOut className="w-4 h-4 mr-2" />
Logout
</button>
</div>
</div>
</header>
{/* Main Content */}
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Error Alert */}
{error && (
<div className="mb-6 p-4 bg-red-900 border border-red-700 rounded-lg text-red-100">
{error}
</div>
)}
{/* Navigation Tabs */}
<div className="flex space-x-4 mb-8 border-b border-gray-700">
<TabButton
label="Dashboard"
icon={<BarChart3 className="w-4 h-4" />}
isActive={activeTab === 'dashboard'}
onClick={() => setActiveTab('dashboard')}
/>
<TabButton
label="Members"
icon={<Users className="w-4 h-4" />}
isActive={activeTab === 'members'}
onClick={() => setActiveTab('members')}
/>
<TabButton
label="Payments"
icon={<CreditCard className="w-4 h-4" />}
isActive={activeTab === 'payments'}
onClick={() => setActiveTab('payments')}
/>
<TabButton
label="Settings"
icon={<Settings className="w-4 h-4" />}
isActive={activeTab === 'settings'}
onClick={() => setActiveTab('settings')}
/>
</div>
{/* Loading State */}
{loading && (
<div className="flex justify-center items-center py-12">
<div className="text-gray-400">Loading...</div>
</div>
)}
{/* Tab Content */}
{!loading && (
<>
{activeTab === 'dashboard' && <DashboardView />}
{activeTab === 'members' && <MembersView />}
{activeTab === 'payments' && <PaymentsView />}
{activeTab === 'settings' && <SettingsView />}
</>
)}
</main>
</div>
);
};
interface StatCardProps {
title: string;
value: string | number;
icon: React.ReactNode;
color: 'blue' | 'green' | 'purple' | 'orange';
}
const StatCard: React.FC<StatCardProps> = ({ title, value, icon, color }) => {
const colorClasses = {
blue: 'bg-blue-500',
green: 'bg-green-500',
purple: 'bg-purple-500',
orange: 'bg-orange-500',
};
return (
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-gray-600 text-sm font-medium">{title}</p>
<p className="text-3xl font-bold text-gray-900 mt-2">{value}</p>
</div>
<div className={`${colorClasses[color]} p-3 rounded-full text-white`}>{icon}</div>
</div>
</div>
);
};
interface TabButtonProps {
label: string;
icon: React.ReactNode;
isActive: boolean;
onClick: () => void;
}
const TabButton: React.FC<TabButtonProps> = ({ label, icon, isActive, onClick }) => (
<button
onClick={onClick}
className={`flex items-center space-x-2 px-4 py-3 border-b-2 font-medium text-sm transition-colors ${
isActive
? 'border-blue-500 text-blue-500'
: 'border-transparent text-gray-400 hover:text-gray-300'
}`}
>
{icon}
<span>{label}</span>
</button>
);
export default FastAPIFrontend;
@@ -0,0 +1,573 @@
import React, { useState, useEffect } from 'react';
import { Search, Plus, Edit3, Trash2, RefreshCw, AlertCircle, CheckCircle, Server, Database, Users, Activity } from 'lucide-react';
const FastAPIFrontend = () => {
const [apiUrl, setApiUrl] = useState('http://localhost:8000');
const [isConnected, setIsConnected] = useState(false);
const [loading, setLoading] = useState(false);
const [data, setData] = useState([]);
const [error, setError] = useState(null);
const [activeTab, setActiveTab] = useState('dashboard');
const [members, setMembers] = useState([]);
const [activeMembersCount, setActiveMembersCount] = useState(0);
const [inactiveMembersCount, setInactiveMembersCount] = useState(0);
const [deactivatedMembersCount, setDeactivatedMembersCount] = useState(0);
const [formData, setFormData] = useState({});
const [showModal, setShowModal] = useState(false);
const [editingItem, setEditingItem] = useState(null);
// Test API connection
const testConnection = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(`${apiUrl}/health`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (response.ok) {
setIsConnected(true);
setError(null);
} else {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
} catch (err) {
setIsConnected(false);
setError(err.message || 'Connection failed');
} finally {
setLoading(false);
}
};
// Fetch member counts
const fetchMemberCounts = async () => {
if (!isConnected) 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);
}
// 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 (!isConnected) return;
setLoading(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 {
setLoading(false);
}
};
// Load members data
const loadMembers = async () => {
setLoading(true);
try {
const response = await fetch(`${apiUrl}/members/`);
if (response.ok) {
const data = await response.json();
setMembers(data);
} else {
console.error('Failed to fetch members');
setMembers([]);
}
} catch (error) {
console.error('Error loading members:', error);
setMembers([]);
} finally {
setLoading(false);
}
};
// Create new item
const createItem = async (itemData) => {
try {
const response = await fetch(`${apiUrl}/items`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(itemData),
});
if (response.ok) {
await fetchData();
setShowModal(false);
setFormData({});
} else {
throw new Error('Failed to create item');
}
} catch (err) {
setError(err.message);
}
};
// Update item
const updateItem = async (id, itemData) => {
try {
const response = await fetch(`${apiUrl}/items/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(itemData),
});
if (response.ok) {
await fetchData();
setShowModal(false);
setEditingItem(null);
setFormData({});
} else {
throw new Error('Failed to update item');
}
} catch (err) {
setError(err.message);
}
};
// Delete item
const deleteItem = async (id) => {
if (!window.confirm('Are you sure you want to delete this item?')) return;
try {
const response = await fetch(`${apiUrl}/items/${id}`, {
method: 'DELETE',
});
if (response.ok) {
await fetchData();
} else {
throw new Error('Failed to delete item');
}
} catch (err) {
setError(err.message);
}
};
// Handle form submission
const handleSubmit = (e) => {
e.preventDefault();
if (editingItem) {
updateItem(editingItem.id, formData);
} else {
createItem(formData);
}
};
// Open edit modal
const startEdit = (item) => {
setEditingItem(item);
setFormData(item);
setShowModal(true);
};
useEffect(() => {
testConnection();
}, [apiUrl]);
useEffect(() => {
if (isConnected) {
fetchMemberCounts();
if (activeTab === 'data') {
fetchData();
}
}
}, [isConnected, activeTab]);
const ConnectionStatus = () => (
<div className="flex items-center space-x-2 mb-6">
<div className={`w-3 h-3 rounded-full ${isConnected ? 'bg-green-400' : 'bg-red-400'}`}></div>
<span className={`text-sm font-medium ${isConnected ? 'text-green-600' : 'text-red-600'}`}>
{isConnected ? 'Connected' : 'Disconnected'}
</span>
<span className="text-gray-500"></span>
<span className="text-sm text-gray-600">{apiUrl}</span>
</div>
);
const Dashboard = () => (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="bg-gradient-to-r from-blue-500 to-blue-600 p-6 rounded-xl text-white">
<div className="flex items-center justify-between">
<div>
<p className="text-blue-100">API Status</p>
<p className="text-2xl font-bold">{isConnected ? 'Online' : 'Offline'}</p>
</div>
<Server className="w-8 h-8 text-blue-200" />
</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 className="bg-gradient-to-r from-purple-500 to-purple-600 p-6 rounded-xl text-white">
<div className="flex items-center justify-between">
<div>
<p className="text-purple-100">Total Members</p>
<p className="text-2xl font-bold">{activeMembersCount + inactiveMembersCount + deactivatedMembersCount}</p>
</div>
<Users className="w-8 h-8 text-purple-200" />
</div>
</div>
<div className="bg-gradient-to-r from-orange-500 to-orange-600 p-6 rounded-xl text-white">
<div className="flex items-center justify-between">
<div>
<p className="text-orange-100">Activity</p>
<p className="text-2xl font-bold">Live</p>
</div>
<Activity className="w-8 h-8 text-orange-200" />
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-4">Member Statistics</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div className="text-center p-4 bg-green-50 rounded-lg border border-green-200">
<div className="text-2xl font-bold text-green-600">{activeMembersCount}</div>
<div className="text-sm text-green-700">Active Members</div>
<div className="text-xs text-green-600 mt-1">
{activeMembersCount + inactiveMembersCount + deactivatedMembersCount > 0
? `${Math.round((activeMembersCount / (activeMembersCount + inactiveMembersCount + deactivatedMembersCount)) * 100)}%`
: '0%'
} of total
</div>
</div>
<div className="text-center p-4 bg-yellow-50 rounded-lg border border-yellow-200">
<div className="text-2xl font-bold text-yellow-600">{inactiveMembersCount}</div>
<div className="text-sm text-yellow-700">Inactive Members</div>
<div className="text-xs text-yellow-600 mt-1">
{activeMembersCount + inactiveMembersCount + deactivatedMembersCount > 0
? `${Math.round((inactiveMembersCount / (activeMembersCount + inactiveMembersCount + deactivatedMembersCount)) * 100)}%`
: '0%'
} of total
</div>
</div>
<div className="text-center p-4 bg-red-50 rounded-lg border border-red-200">
<div className="text-2xl font-bold text-red-600">{deactivatedMembersCount}</div>
<div className="text-sm text-red-700">Deactivated Members</div>
<div className="text-xs text-red-600 mt-1">
{activeMembersCount + inactiveMembersCount + deactivatedMembersCount > 0
? `${Math.round((deactivatedMembersCount / (activeMembersCount + inactiveMembersCount + deactivatedMembersCount)) * 100)}%`
: '0%'
} of total
</div>
</div>
</div>
<h3 className="text-lg font-semibold text-gray-800 mb-4">Quick Actions</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<button
onClick={() => setActiveTab('data')}
className="p-4 border-2 border-dashed border-gray-200 rounded-lg hover:border-blue-300 hover:bg-blue-50 transition-colors"
>
<Database className="w-6 h-6 text-gray-400 mx-auto mb-2" />
<p className="text-sm font-medium text-gray-600">View All Members</p>
</button>
<button
onClick={fetchMemberCounts}
className="p-4 border-2 border-dashed border-gray-200 rounded-lg hover:border-green-300 hover:bg-green-50 transition-colors"
>
<RefreshCw className="w-6 h-6 text-gray-400 mx-auto mb-2" />
<p className="text-sm font-medium text-gray-600">Refresh Counts</p>
</button>
<button
onClick={testConnection}
className="p-4 border-2 border-dashed border-gray-200 rounded-lg hover:border-purple-300 hover:bg-purple-50 transition-colors"
>
<Activity className="w-6 h-6 text-gray-400 mx-auto mb-2" />
<p className="text-sm font-medium text-gray-600">Test Connection</p>
</button>
</div>
</div>
</div>
);
const DataView = () => (
<div className="space-y-6">
<div className="flex justify-between items-center">
<h2 className="text-xl font-semibold text-gray-800">Data Management</h2>
<button
onClick={() => {
setFormData({});
setEditingItem(null);
setShowModal(true);
}}
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors flex items-center space-x-2"
>
<Plus className="w-4 h-4" />
<span>Add New</span>
</button>
</div>
{loading ? (
<div className="flex justify-center items-center py-12">
<RefreshCw className="w-6 h-6 animate-spin text-blue-600" />
<span className="ml-2 text-gray-600">Loading...</span>
</div>
) : (
<div className="bg-white rounded-xl shadow-sm border overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Description</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{data.length === 0 ? (
<tr>
<td colSpan="4" className="px-6 py-12 text-center text-gray-500">
No data available. Add some items to get started.
</td>
</tr>
) : (
data.map((item, index) => (
<tr key={item.id || index} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{item.id || index + 1}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
{item.name || item.title || 'N/A'}
</td>
<td className="px-6 py-4 text-sm text-gray-600 max-w-xs truncate">
{item.description || item.content || 'No description'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div className="flex space-x-2">
<button
onClick={() => startEdit(item)}
className="text-blue-600 hover:text-blue-900 p-1 rounded"
>
<Edit3 className="w-4 h-4" />
</button>
<button
onClick={() => deleteItem(item.id)}
className="text-red-600 hover:text-red-900 p-1 rounded"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
)}
</div>
);
const Modal = () => (
showModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl max-w-md w-full p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-4">
{editingItem ? 'Edit Item' : 'Add New Item'}
</h3>
<div onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Name</label>
<input
type="text"
value={formData.name || ''}
onChange={(e) => setFormData({...formData, name: e.target.value})}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Enter name"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Description</label>
<textarea
value={formData.description || ''}
onChange={(e) => setFormData({...formData, description: e.target.value})}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
rows="3"
placeholder="Enter description"
/>
</div>
<div className="flex space-x-3 pt-4">
<button
type="button"
onClick={() => {
setShowModal(false);
setEditingItem(null);
setFormData({});
}}
className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
>
Cancel
</button>
<button
onClick={handleSubmit}
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
{editingItem ? 'Update' : 'Create'}
</button>
</div>
</div>
</div>
</div>
)
);
return (
<div className="min-h-screen bg-gray-100">
<div className="bg-white shadow-sm border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center py-4">
<div>
<h1 className="text-2xl font-bold text-gray-900">FastAPI Dashboard</h1>
<p className="text-sm text-gray-600">Manage your backend data and monitor API status</p>
</div>
<div className="flex items-center space-x-4">
<input
type="text"
value={apiUrl}
onChange={(e) => setApiUrl(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="API URL"
/>
<button
onClick={testConnection}
disabled={loading}
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 flex items-center space-x-2"
>
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
<span>Test</span>
</button>
</div>
</div>
<div className="flex space-x-8">
<button
onClick={() => setActiveTab('dashboard')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'dashboard'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
Dashboard
</button>
<button
onClick={() => setActiveTab('data')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'data'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
Data
</button>
</div>
</div>
</div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<ConnectionStatus />
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6 flex items-center space-x-2">
<AlertCircle className="w-5 h-5 text-red-600" />
<span className="text-red-700">{error}</span>
</div>
)}
{activeTab === 'dashboard' ? <Dashboard /> : <DataView />}
</div>
<Modal />
</div>
);
};
export default FastAPIFrontend;
@@ -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;
@@ -0,0 +1,762 @@
import React, { useState, useEffect } from 'react';
import { Save, X, User, Calendar, Phone, Mail, MapPin, Users, Edit, Plus, Trash2 } from 'lucide-react';
// Member Edit Form Component
const MemberEditForm = ({ member = null, onSave, onCancel, isOpen }) => {
const [formData, setFormData] = useState({
id: '',
member_count: 1,
member_firstname: '',
member_firstname_spouse: '',
member_surname: '',
member_suffix: '',
member_street: '',
member_streetnumber: '',
member_email: '',
member_zip: '',
member_city: '',
member_gender: '',
member_died: '',
member_membership_start: '',
member_phone: '',
current_state_code: '',
current_state_changed_at: ''
});
const [errors, setErrors] = useState({});
const [isLoading, setIsLoading] = useState(false);
const genderOptions = [
{ value: '', label: 'Select Gender' },
{ value: 'M', label: 'Male' },
{ value: 'F', label: 'Female' },
{ value: 'O', label: 'Other' }
];
const stateCodes = [
'AL', 'AK', 'AZ', 'AR', 'CA', 'CO', 'CT', 'DE', 'FL', 'GA',
'HI', 'ID', 'IL', 'IN', 'IA', 'KS', 'KY', 'LA', 'ME', 'MD',
'MA', 'MI', 'MN', 'MS', 'MO', 'MT', 'NE', 'NV', 'NH', 'NJ',
'NM', 'NY', 'NC', 'ND', 'OH', 'OK', 'OR', 'PA', 'RI', 'SC',
'SD', 'TN', 'TX', 'UT', 'VT', 'VA', 'WA', 'WV', 'WI', 'WY'
];
useEffect(() => {
if (member) {
setFormData({
id: member.id || '',
member_count: member.member_count || 1,
member_firstname: member.member_firstname || '',
member_firstname_spouse: member.member_firstname_spouse || '',
member_surname: member.member_surname || '',
member_suffix: member.member_suffix || '',
member_street: member.member_street || '',
member_streetnumber: member.member_streetnumber || '',
member_email: member.member_email || '',
member_zip: member.member_zip || '',
member_city: member.member_city || '',
member_gender: member.member_gender || '',
member_died: member.member_died ? new Date(member.member_died).toISOString().split('T')[0] : '',
member_membership_start: member.member_membership_start ? new Date(member.member_membership_start).toISOString().split('T')[0] : '',
member_phone: member.member_phone || '',
current_state_code: member.current_state_code || '',
current_state_changed_at: member.current_state_changed_at ? new Date(member.current_state_changed_at).toISOString().slice(0, 16) : ''
});
} else {
// Reset form for new member
setFormData({
id: '',
member_count: 1,
member_firstname: '',
member_firstname_spouse: '',
member_surname: '',
member_suffix: '',
member_street: '',
member_streetnumber: '',
member_email: '',
member_zip: '',
member_city: '',
member_gender: '',
member_died: '',
member_membership_start: '',
member_phone: '',
current_state_code: '',
current_state_changed_at: ''
});
}
setErrors({});
}, [member]);
const handleInputChange = (e) => {
const { name, value, type } = e.target;
setFormData(prev => ({
...prev,
[name]: type === 'number' ? (value === '' ? '' : parseInt(value)) : value
}));
if (errors[name]) {
setErrors(prev => ({
...prev,
[name]: ''
}));
}
};
const validateForm = () => {
const newErrors = {};
if (!formData.member_firstname.trim()) {
newErrors.member_firstname = 'First name is required';
}
if (!formData.member_surname.trim()) {
newErrors.member_surname = 'Surname is required';
}
if (formData.member_email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.member_email)) {
newErrors.member_email = 'Please enter a valid email address';
}
if (formData.member_zip && (formData.member_zip < 10000 || formData.member_zip > 99999)) {
newErrors.member_zip = 'Please enter a valid 5-digit ZIP code';
}
if (formData.member_phone && formData.member_phone.length < 10) {
newErrors.member_phone = 'Please enter a valid phone number';
}
if (formData.member_count < 1) {
newErrors.member_count = 'Member count must be at least 1';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async () => {
if (!validateForm()) {
return;
}
setIsLoading(true);
try {
const submitData = {
...formData,
updated_date: new Date().toISOString(),
member_died: formData.member_died ? new Date(formData.member_died).toISOString() : null,
member_membership_start: formData.member_membership_start ? new Date(formData.member_membership_start).toISOString() : null,
current_state_changed_at: formData.current_state_changed_at ? new Date(formData.current_state_changed_at).toISOString() : null
};
if (onSave) {
await onSave(submitData);
}
} catch (error) {
console.error('Error saving member:', error);
setErrors({ submit: 'Failed to save member. Please try again.' });
} finally {
setIsLoading(false);
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-lg max-w-4xl w-full max-h-[90vh] overflow-y-auto">
<div className="p-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold text-gray-800 flex items-center gap-2">
<User className="h-6 w-6" />
{member ? 'Edit Member' : 'Add New Member'}
</h2>
<button
onClick={onCancel}
className="p-2 text-gray-500 hover:text-gray-700 rounded-full hover:bg-gray-100"
>
<X className="h-5 w-5" />
</button>
</div>
<div className="space-y-6">
{errors.submit && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
{errors.submit}
</div>
)}
{/* Personal Information */}
<div className="bg-gray-50 p-4 rounded-lg">
<h3 className="text-lg font-semibold text-gray-700 mb-4 flex items-center gap-2">
<User className="h-5 w-5" />
Personal Information
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
First Name *
</label>
<input
type="text"
name="member_firstname"
value={formData.member_firstname}
onChange={handleInputChange}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.member_firstname ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="Enter first name"
/>
{errors.member_firstname && (
<p className="text-red-500 text-xs mt-1">{errors.member_firstname}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Spouse First Name
</label>
<input
type="text"
name="member_firstname_spouse"
value={formData.member_firstname_spouse}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter spouse first name"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Surname *
</label>
<input
type="text"
name="member_surname"
value={formData.member_surname}
onChange={handleInputChange}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.member_surname ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="Enter surname"
/>
{errors.member_surname && (
<p className="text-red-500 text-xs mt-1">{errors.member_surname}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Suffix
</label>
<input
type="text"
name="member_suffix"
value={formData.member_suffix}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Jr., Sr., III, etc."
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Gender
</label>
<select
name="member_gender"
value={formData.member_gender}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{genderOptions.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1 flex items-center gap-1">
<Users className="h-4 w-4" />
Member Count
</label>
<input
type="number"
name="member_count"
value={formData.member_count}
onChange={handleInputChange}
min="1"
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.member_count ? 'border-red-500' : 'border-gray-300'
}`}
/>
{errors.member_count && (
<p className="text-red-500 text-xs mt-1">{errors.member_count}</p>
)}
</div>
</div>
</div>
{/* Contact Information */}
<div className="bg-gray-50 p-4 rounded-lg">
<h3 className="text-lg font-semibold text-gray-700 mb-4 flex items-center gap-2">
<Mail className="h-5 w-5" />
Contact Information
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1 flex items-center gap-1">
<Mail className="h-4 w-4" />
Email
</label>
<input
type="email"
name="member_email"
value={formData.member_email}
onChange={handleInputChange}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.member_email ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="member@example.com"
/>
{errors.member_email && (
<p className="text-red-500 text-xs mt-1">{errors.member_email}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1 flex items-center gap-1">
<Phone className="h-4 w-4" />
Phone
</label>
<input
type="tel"
name="member_phone"
value={formData.member_phone}
onChange={handleInputChange}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.member_phone ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="(555) 123-4567"
/>
{errors.member_phone && (
<p className="text-red-500 text-xs mt-1">{errors.member_phone}</p>
)}
</div>
</div>
</div>
{/* Address Information */}
<div className="bg-gray-50 p-4 rounded-lg">
<h3 className="text-lg font-semibold text-gray-700 mb-4 flex items-center gap-2">
<MapPin className="h-5 w-5" />
Address Information
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Street Number
</label>
<input
type="text"
name="member_streetnumber"
value={formData.member_streetnumber}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="123"
/>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Street
</label>
<input
type="text"
name="member_street"
value={formData.member_street}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Main Street"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
ZIP Code
</label>
<input
type="number"
name="member_zip"
value={formData.member_zip}
onChange={handleInputChange}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.member_zip ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="12345"
/>
{errors.member_zip && (
<p className="text-red-500 text-xs mt-1">{errors.member_zip}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
City
</label>
<input
type="text"
name="member_city"
value={formData.member_city}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Anytown"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
State
</label>
<select
name="current_state_code"
value={formData.current_state_code}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Select State</option>
{stateCodes.map(code => (
<option key={code} value={code}>{code}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
State Changed At
</label>
<input
type="datetime-local"
name="current_state_changed_at"
value={formData.current_state_changed_at}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
</div>
{/* Membership Information */}
<div className="bg-gray-50 p-4 rounded-lg">
<h3 className="text-lg font-semibold text-gray-700 mb-4 flex items-center gap-2">
<Calendar className="h-5 w-5" />
Membership Information
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Membership Start Date
</label>
<input
type="date"
name="member_membership_start"
value={formData.member_membership_start}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Date of Death
</label>
<input
type="date"
name="member_died"
value={formData.member_died}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
</div>
{/* Form Actions */}
<div className="flex justify-end gap-3 pt-6 border-t">
<button
type="button"
onClick={onCancel}
className="px-6 py-2 text-gray-700 bg-gray-200 hover:bg-gray-300 rounded-md transition-colors"
disabled={isLoading}
>
Cancel
</button>
<button
type="button"
onClick={handleSubmit}
disabled={isLoading}
className="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
>
<Save className="h-4 w-4" />
{isLoading ? 'Saving...' : 'Save Member'}
</button>
</div>
</div>
</div>
</div>
</div>
);
};
// Main FastAPI Frontend Component
const FastAPIFrontend = () => {
const [members, setMembers] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [showMemberForm, setShowMemberForm] = useState(false);
const [editingMember, setEditingMember] = useState(null);
// Your existing API base URL - adjust this to match your FastAPI server
const API_BASE_URL = 'http://localhost:8000'; // Change this to your FastAPI URL
// Fetch members from your FastAPI backend
const fetchMembers = async () => {
setLoading(true);
setError('');
try {
const response = await fetch(`${API_BASE_URL}/members`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
setMembers(data);
} catch (err) {
setError('Failed to fetch members: ' + err.message);
} finally {
setLoading(false);
}
};
// Save member (create or update)
const saveMember = async (memberData) => {
try {
const isEdit = memberData.id && memberData.id !== '';
const url = isEdit ? `${API_BASE_URL}/members/${memberData.id}` : `${API_BASE_URL}/members`;
const method = isEdit ? 'PUT' : 'POST';
const response = await fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(memberData),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const savedMember = await response.json();
// Update local state
if (isEdit) {
setMembers(prev => prev.map(m => m.id === savedMember.id ? savedMember : m));
} else {
setMembers(prev => [...prev, savedMember]);
}
setShowMemberForm(false);
setEditingMember(null);
alert(isEdit ? 'Member updated successfully!' : 'Member created successfully!');
} catch (err) {
throw new Error('Failed to save member: ' + err.message);
}
};
// Delete member
const deleteMember = async (memberId) => {
if (!confirm('Are you sure you want to delete this member?')) {
return;
}
try {
const response = await fetch(`${API_BASE_URL}/members/${memberId}`, {
method: 'DELETE',
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
setMembers(prev => prev.filter(m => m.id !== memberId));
alert('Member deleted successfully!');
} catch (err) {
alert('Failed to delete member: ' + err.message);
}
};
// Handle edit member
const handleEditMember = (member) => {
setEditingMember(member);
setShowMemberForm(true);
};
// Handle add new member
const handleAddMember = () => {
setEditingMember(null);
setShowMemberForm(true);
};
// Handle close form
const handleCloseForm = () => {
setShowMemberForm(false);
setEditingMember(null);
};
// Load members on component mount
useEffect(() => {
fetchMembers();
}, []);
return (
<div className="min-h-screen bg-gray-100">
<div className="container mx-auto px-4 py-8">
<div className="bg-white shadow-lg rounded-lg p-6">
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold text-gray-800">Member Management</h1>
<button
onClick={handleAddMember}
className="px-6 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors flex items-center gap-2"
>
<Plus className="h-5 w-5" />
Add New Member
</button>
</div>
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded mb-4">
{error}
</div>
)}
{loading ? (
<div className="text-center py-8">
<div className="text-lg text-gray-600">Loading members...</div>
</div>
) : (
<div className="overflow-x-auto">
<table className="min-w-full table-auto">
<thead>
<tr className="bg-gray-50">
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Name
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Email
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Phone
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
City
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Member Count
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{members.map((member) => (
<tr key={member.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">
{member.member_firstname} {member.member_surname} {member.member_suffix}
</div>
{member.member_firstname_spouse && (
<div className="text-sm text-gray-500">
Spouse: {member.member_firstname_spouse}
</div>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{member.member_email || '-'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{member.member_phone || '-'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{member.member_city || '-'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{member.member_count}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div className="flex space-x-2">
<button
onClick={() => handleEditMember(member)}
className="text-blue-600 hover:text-blue-900 p-1 rounded hover:bg-blue-100"
title="Edit Member"
>
<Edit className="h-4 w-4" />
</button>
<button
onClick={() => deleteMember(member.id)}
className="text-red-600 hover:text-red-900 p-1 rounded hover:bg-red-100"
title="Delete Member"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</td>
</tr>
))}
{members.length === 0 && !loading && (
<tr>
<td colSpan="6" className="text-center py-8 text-gray-500">
No members found. Click "Add New Member" to get started.
</td>
</tr>
)}
</tbody>
</table>
</div>
)}
<div className="mt-6 flex justify-between items-center">
<div className="text-sm text-gray-700">
Total Members: {members.length}
</div>
<button
onClick={fetchMembers}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors text-sm"
>
Refresh
</button>
</div>
</div>
</div>
{/* Member Edit Form Modal */}
<MemberEditForm
member={editingMember}
onSave={saveMember}
onCancel={handleCloseForm}
isOpen={showMemberForm}
/>
</div>
);
};
export default FastAPIFrontend;
+1 -1
View File
@@ -1,6 +1,6 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import FastAPIFrontend from './FastAPIFrontend';
import FastAPIFrontend from './FastAPIFrontend.tsx';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
@@ -0,0 +1,599 @@
import React, { useState, useEffect } from 'react';
import { Save, X, User, Calendar, Phone, Mail, MapPin, Users } from 'lucide-react';
const MemberEditForm = ({ member = null, onSave, onCancel }) => {
const [formData, setFormData] = useState({
id: '',
member_count: 1,
member_firstname: '',
member_firstname_spouse: '',
member_surname: '',
member_suffix: '',
member_street: '',
member_streetnumber: '',
member_email: '',
member_zip: '',
member_city: '',
member_gender: '',
member_died: '',
member_membership_start: '',
member_phone: '',
current_state_code: '',
current_state_changed_at: ''
});
const [errors, setErrors] = useState({});
const [isLoading, setIsLoading] = useState(false);
// Gender options based on your enum
const genderOptions = [
{ value: '', label: 'Select Gender' },
{ value: 'M', label: 'Male' },
{ value: 'F', label: 'Female' },
{ value: 'O', label: 'Other' }
];
// US State codes
const stateCodes = [
'AL', 'AK', 'AZ', 'AR', 'CA', 'CO', 'CT', 'DE', 'FL', 'GA',
'HI', 'ID', 'IL', 'IN', 'IA', 'KS', 'KY', 'LA', 'ME', 'MD',
'MA', 'MI', 'MN', 'MS', 'MO', 'MT', 'NE', 'NV', 'NH', 'NJ',
'NM', 'NY', 'NC', 'ND', 'OH', 'OK', 'OR', 'PA', 'RI', 'SC',
'SD', 'TN', 'TX', 'UT', 'VT', 'VA', 'WA', 'WV', 'WI', 'WY'
];
// Load member data when component mounts or member prop changes
useEffect(() => {
if (member) {
setFormData({
id: member.id || '',
member_count: member.member_count || 1,
member_firstname: member.member_firstname || '',
member_firstname_spouse: member.member_firstname_spouse || '',
member_surname: member.member_surname || '',
member_suffix: member.member_suffix || '',
member_street: member.member_street || '',
member_streetnumber: member.member_streetnumber || '',
member_email: member.member_email || '',
member_zip: member.member_zip || '',
member_city: member.member_city || '',
member_gender: member.member_gender || '',
member_died: member.member_died ? new Date(member.member_died).toISOString().split('T')[0] : '',
member_membership_start: member.member_membership_start ? new Date(member.member_membership_start).toISOString().split('T')[0] : '',
member_phone: member.member_phone || '',
current_state_code: member.current_state_code || '',
current_state_changed_at: member.current_state_changed_at ? new Date(member.current_state_changed_at).toISOString().slice(0, 16) : ''
});
}
}, [member]);
const handleInputChange = (e) => {
const { name, value, type } = e.target;
setFormData(prev => ({
...prev,
[name]: type === 'number' ? (value === '' ? '' : parseInt(value)) : value
}));
// Clear error when user starts typing
if (errors[name]) {
setErrors(prev => ({
...prev,
[name]: ''
}));
}
};
const validateForm = () => {
const newErrors = {};
// Required field validations
if (!formData.member_firstname.trim()) {
newErrors.member_firstname = 'First name is required';
}
if (!formData.member_surname.trim()) {
newErrors.member_surname = 'Surname is required';
}
// Email validation
if (formData.member_email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.member_email)) {
newErrors.member_email = 'Please enter a valid email address';
}
// ZIP code validation
if (formData.member_zip && (formData.member_zip < 10000 || formData.member_zip > 99999)) {
newErrors.member_zip = 'Please enter a valid 5-digit ZIP code';
}
// Phone validation (basic)
if (formData.member_phone && formData.member_phone.length < 10) {
newErrors.member_phone = 'Please enter a valid phone number';
}
// Member count validation
if (formData.member_count < 1) {
newErrors.member_count = 'Member count must be at least 1';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async () => {
if (!validateForm()) {
return;
}
setIsLoading(true);
try {
// Prepare data for submission
const submitData = {
...formData,
updated_date: new Date().toISOString(),
// Convert date strings back to proper format for database
member_died: formData.member_died ? new Date(formData.member_died).toISOString() : null,
member_membership_start: formData.member_membership_start ? new Date(formData.member_membership_start).toISOString() : null,
current_state_changed_at: formData.current_state_changed_at ? new Date(formData.current_state_changed_at).toISOString() : null
};
if (onSave) {
await onSave(submitData);
}
} catch (error) {
console.error('Error saving member:', error);
setErrors({ submit: 'Failed to save member. Please try again.' });
} finally {
setIsLoading(false);
}
};
const formatDateTime = (dateString) => {
if (!dateString) return '';
return new Date(dateString).toISOString().slice(0, 16);
};
return (
<div className="max-w-4xl mx-auto bg-white shadow-lg rounded-lg p-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold text-gray-800 flex items-center gap-2">
<User className="h-6 w-6" />
{member ? 'Edit Member' : 'Add New Member'}
</h2>
{onCancel && (
<button
onClick={onCancel}
className="p-2 text-gray-500 hover:text-gray-700 rounded-full hover:bg-gray-100"
>
<X className="h-5 w-5" />
</button>
)}
</div>
<div className="space-y-6">
{errors.submit && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
{errors.submit}
</div>
)}
{/* Personal Information */}
<div className="bg-gray-50 p-4 rounded-lg">
<h3 className="text-lg font-semibold text-gray-700 mb-4 flex items-center gap-2">
<User className="h-5 w-5" />
Personal Information
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
First Name *
</label>
<input
type="text"
name="member_firstname"
value={formData.member_firstname}
onChange={handleInputChange}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.member_firstname ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="Enter first name"
/>
{errors.member_firstname && (
<p className="text-red-500 text-xs mt-1">{errors.member_firstname}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Spouse First Name
</label>
<input
type="text"
name="member_firstname_spouse"
value={formData.member_firstname_spouse}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter spouse first name"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Surname *
</label>
<input
type="text"
name="member_surname"
value={formData.member_surname}
onChange={handleInputChange}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.member_surname ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="Enter surname"
/>
{errors.member_surname && (
<p className="text-red-500 text-xs mt-1">{errors.member_surname}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Suffix
</label>
<input
type="text"
name="member_suffix"
value={formData.member_suffix}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Jr., Sr., III, etc."
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Gender
</label>
<select
name="member_gender"
value={formData.member_gender}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{genderOptions.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1 flex items-center gap-1">
<Users className="h-4 w-4" />
Member Count
</label>
<input
type="number"
name="member_count"
value={formData.member_count}
onChange={handleInputChange}
min="1"
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.member_count ? 'border-red-500' : 'border-gray-300'
}`}
/>
{errors.member_count && (
<p className="text-red-500 text-xs mt-1">{errors.member_count}</p>
)}
</div>
</div>
</div>
{/* Contact Information */}
<div className="bg-gray-50 p-4 rounded-lg">
<h3 className="text-lg font-semibold text-gray-700 mb-4 flex items-center gap-2">
<Mail className="h-5 w-5" />
Contact Information
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1 flex items-center gap-1">
<Mail className="h-4 w-4" />
Email
</label>
<input
type="email"
name="member_email"
value={formData.member_email}
onChange={handleInputChange}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.member_email ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="member@example.com"
/>
{errors.member_email && (
<p className="text-red-500 text-xs mt-1">{errors.member_email}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1 flex items-center gap-1">
<Phone className="h-4 w-4" />
Phone
</label>
<input
type="tel"
name="member_phone"
value={formData.member_phone}
onChange={handleInputChange}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.member_phone ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="(555) 123-4567"
/>
{errors.member_phone && (
<p className="text-red-500 text-xs mt-1">{errors.member_phone}</p>
)}
</div>
</div>
</div>
{/* Address Information */}
<div className="bg-gray-50 p-4 rounded-lg">
<h3 className="text-lg font-semibold text-gray-700 mb-4 flex items-center gap-2">
<MapPin className="h-5 w-5" />
Address Information
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Street Number
</label>
<input
type="text"
name="member_streetnumber"
value={formData.member_streetnumber}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="123"
/>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Street
</label>
<input
type="text"
name="member_street"
value={formData.member_street}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Main Street"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
ZIP Code
</label>
<input
type="number"
name="member_zip"
value={formData.member_zip}
onChange={handleInputChange}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.member_zip ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="12345"
/>
{errors.member_zip && (
<p className="text-red-500 text-xs mt-1">{errors.member_zip}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
City
</label>
<input
type="text"
name="member_city"
value={formData.member_city}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Anytown"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
State
</label>
<select
name="current_state_code"
value={formData.current_state_code}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Select State</option>
{stateCodes.map(code => (
<option key={code} value={code}>{code}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
State Changed At
</label>
<input
type="datetime-local"
name="current_state_changed_at"
value={formData.current_state_changed_at}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
</div>
{/* Membership Information */}
<div className="bg-gray-50 p-4 rounded-lg">
<h3 className="text-lg font-semibold text-gray-700 mb-4 flex items-center gap-2">
<Calendar className="h-5 w-5" />
Membership Information
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Membership Start Date
</label>
<input
type="date"
name="member_membership_start"
value={formData.member_membership_start}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Date of Death
</label>
<input
type="date"
name="member_died"
value={formData.member_died}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
</div>
{/* Form Actions */}
<div className="flex justify-end gap-3 pt-6 border-t">
{onCancel && (
<button
type="button"
onClick={onCancel}
className="px-6 py-2 text-gray-700 bg-gray-200 hover:bg-gray-300 rounded-md transition-colors"
disabled={isLoading}
>
Cancel
</button>
)}
<button
type="button"
onClick={handleSubmit}
disabled={isLoading}
className="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
>
<Save className="h-4 w-4" />
{isLoading ? 'Saving...' : 'Save Member'}
</button>
</div>
</form>
</div>
);
};
// Example usage component
const App = () => {
const [currentMember, setCurrentMember] = useState(null);
const [showForm, setShowForm] = useState(false);
// Example member data
const exampleMember = {
id: 1,
member_count: 2,
member_firstname: 'John',
member_firstname_spouse: 'Jane',
member_surname: 'Doe',
member_suffix: 'Jr.',
member_street: 'Main Street',
member_streetnumber: '123',
member_email: 'john.doe@example.com',
member_zip: 12345,
member_city: 'Anytown',
member_gender: 'M',
member_died: null,
member_membership_start: '2020-01-15',
member_phone: '(555) 123-4567',
current_state_code: 'CA',
current_state_changed_at: '2020-01-15T10:00:00'
};
const handleSave = async (memberData) => {
console.log('Saving member data:', memberData);
// Here you would typically make an API call to save the data
// await saveMemberToAPI(memberData);
alert('Member saved successfully!');
setShowForm(false);
};
const handleCancel = () => {
setShowForm(false);
setCurrentMember(null);
};
const handleEdit = () => {
setCurrentMember(exampleMember);
setShowForm(true);
};
const handleAddNew = () => {
setCurrentMember(null);
setShowForm(true);
};
if (showForm) {
return (
<MemberEditForm
member={currentMember}
onSave={handleSave}
onCancel={handleCancel}
/>
);
}
return (
<div className="max-w-4xl mx-auto p-6">
<div className="bg-white shadow-lg rounded-lg p-6">
<h1 className="text-3xl font-bold text-gray-800 mb-6">Member Management</h1>
<div className="space-y-4">
<div className="p-4 border border-gray-200 rounded-lg">
<h3 className="text-lg font-semibold text-gray-700 mb-2">John Doe Jr.</h3>
<p className="text-gray-600">Email: john.doe@example.com</p>
<p className="text-gray-600">Phone: (555) 123-4567</p>
<p className="text-gray-600">Address: 123 Main Street, Anytown, CA 12345</p>
<button
onClick={handleEdit}
className="mt-3 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
>
Edit Member
</button>
</div>
<button
onClick={handleAddNew}
className="px-6 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors"
>
Add New Member
</button>
</div>
</div>
</div>
);
};
export default App;