Add frontend, member_state_log, etc.
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>FastAPI Dashboard</title>
|
||||
<link href="./dist/output.css" rel="stylesheet" />
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+1817
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "react-frontend",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"serve": "vite preview"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"lucide-react": "^0.525.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"vite": "^7.0.6"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,748 @@
|
||||
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 [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);
|
||||
};
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
// Load members when component mounts if already connected
|
||||
useEffect(() => {
|
||||
if (connectionStatus === 'connected') {
|
||||
loadMembersCount();
|
||||
}
|
||||
}, [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 Members */}
|
||||
<div style={styles.statCard}>
|
||||
<div style={styles.statContent}>
|
||||
<p style={styles.statLabel}>Active Members</p>
|
||||
<p style={styles.statValue}>{totalMembers}</p>
|
||||
</div>
|
||||
<div style={{...styles.statIcon, ...styles.iconBlue}}>
|
||||
<Database 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>
|
||||
|
||||
{/* 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,458 @@
|
||||
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 [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 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);
|
||||
}
|
||||
};
|
||||
|
||||
// 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 && 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">Total Items</p>
|
||||
<p className="text-2xl font-bold">{data.length}</p>
|
||||
</div>
|
||||
<Database className="w-8 h-8 text-green-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">Active Users</p>
|
||||
<p className="text-2xl font-bold">1</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">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 Data</p>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setFormData({});
|
||||
setEditingItem(null);
|
||||
setShowModal(true);
|
||||
}}
|
||||
className="p-4 border-2 border-dashed border-gray-200 rounded-lg hover:border-green-300 hover:bg-green-50 transition-colors"
|
||||
>
|
||||
<Plus className="w-6 h-6 text-gray-400 mx-auto mb-2" />
|
||||
<p className="text-sm font-medium text-gray-600">Add Item</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"
|
||||
>
|
||||
<RefreshCw className="w-6 h-6 text-gray-400 mx-auto mb-2" />
|
||||
<p className="text-sm font-medium text-gray-600">Refresh</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,233 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Server,
|
||||
Database,
|
||||
Users,
|
||||
Activity,
|
||||
RefreshCw,
|
||||
Plus,
|
||||
Eye,
|
||||
AlertCircle,
|
||||
CheckCircle,
|
||||
XCircle
|
||||
} from 'lucide-react';
|
||||
|
||||
const Dashboard = () => {
|
||||
const [apiUrl, setApiUrl] = useState('http://localhost:8000');
|
||||
const [connectionStatus, setConnectionStatus] = useState('disconnected');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState('dashboard');
|
||||
|
||||
const testConnection = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// Simulate API call
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
setConnectionStatus('connected');
|
||||
} catch (error) {
|
||||
setConnectionStatus('error');
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const getStatusColor = () => {
|
||||
switch (connectionStatus) {
|
||||
case 'connected': return 'text-green-600';
|
||||
case 'error': return 'text-red-600';
|
||||
default: return 'text-orange-600';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = () => {
|
||||
switch (connectionStatus) {
|
||||
case 'connected': return <CheckCircle className="w-4 h-4" />;
|
||||
case 'error': return <XCircle className="w-4 h-4" />;
|
||||
default: return <AlertCircle className="w-4 h-4" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = () => {
|
||||
switch (connectionStatus) {
|
||||
case 'connected': return 'Connected';
|
||||
case 'error': return 'Connection Error';
|
||||
default: return 'Disconnected';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* Header */}
|
||||
<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-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">FastAPI Dashboard</h1>
|
||||
<p className="text-gray-600 mt-1">Manage your backend data and monitor API status</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className={`flex items-center space-x-2 ${getStatusColor()}`}>
|
||||
{getStatusIcon()}
|
||||
<span className="font-medium">{getStatusText()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Connection Section */}
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="bg-white rounded-lg shadow-sm border p-6 mb-8">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">API Connection</h2>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex-1">
|
||||
<input
|
||||
type="text"
|
||||
value={apiUrl}
|
||||
onChange={(e) => setApiUrl(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="Enter API URL"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={testConnection}
|
||||
disabled={isLoading}
|
||||
className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isLoading ? (
|
||||
<RefreshCw className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
Test Connection
|
||||
</button>
|
||||
</div>
|
||||
{connectionStatus === 'error' && (
|
||||
<div className="mt-4 p-4 bg-red-50 border border-red-200 rounded-md">
|
||||
<div className="flex items-center">
|
||||
<XCircle className="w-5 h-5 text-red-400 mr-2" />
|
||||
<span className="text-red-800">NetworkError when attempting to fetch resource.</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Navigation Tabs */}
|
||||
<div className="mb-8">
|
||||
<div className="border-b border-gray-200">
|
||||
<nav 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 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
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 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
Data
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
{/* API Status */}
|
||||
<div className="bg-white rounded-lg shadow-sm border p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">API Status</p>
|
||||
<p className="text-2xl font-bold text-gray-900 mt-2">
|
||||
{connectionStatus === 'connected' ? 'Online' : 'Offline'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-orange-100 p-3 rounded-full">
|
||||
<Server className="w-6 h-6 text-orange-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Total Items */}
|
||||
<div className="bg-white rounded-lg shadow-sm border p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Total Items</p>
|
||||
<p className="text-2xl font-bold text-gray-900 mt-2">0</p>
|
||||
</div>
|
||||
<div className="bg-blue-100 p-3 rounded-full">
|
||||
<Database className="w-6 h-6 text-blue-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Active Users */}
|
||||
<div className="bg-white rounded-lg shadow-sm border p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Active Users</p>
|
||||
<p className="text-2xl font-bold text-gray-900 mt-2">1</p>
|
||||
</div>
|
||||
<div className="bg-green-100 p-3 rounded-full">
|
||||
<Users className="w-6 h-6 text-green-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Activity */}
|
||||
<div className="bg-white rounded-lg shadow-sm border p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Activity</p>
|
||||
<p className="text-2xl font-bold text-gray-900 mt-2">Live</p>
|
||||
</div>
|
||||
<div className="bg-purple-100 p-3 rounded-full">
|
||||
<Activity className="w-6 h-6 text-purple-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="bg-white rounded-lg shadow-sm border p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">Quick Actions</h2>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<button className="inline-flex items-center px-4 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2">
|
||||
<Eye className="w-4 h-4 mr-2" />
|
||||
View Data
|
||||
</button>
|
||||
<button className="inline-flex items-center px-4 py-2 bg-green-100 text-green-700 rounded-md hover:bg-green-200 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Add Item
|
||||
</button>
|
||||
<button className="inline-flex items-center px-4 py-2 bg-blue-100 text-blue-700 rounded-md hover:bg-blue-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content based on active tab */}
|
||||
{activeTab === 'data' && (
|
||||
<div className="mt-8 bg-white rounded-lg shadow-sm border p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">Data Management</h2>
|
||||
<div className="text-center py-8">
|
||||
<Database className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
||||
<p className="text-gray-500">No data available. Connect to your API to view data.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
@@ -0,0 +1,515 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Server,
|
||||
Database,
|
||||
Users,
|
||||
Activity,
|
||||
RefreshCw,
|
||||
Plus,
|
||||
Eye,
|
||||
AlertCircle,
|
||||
CheckCircle,
|
||||
XCircle
|
||||
} from 'lucide-react';
|
||||
|
||||
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 [loadingData, setLoadingData] = useState(false);
|
||||
|
||||
const testConnection = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
console.log('Testing connection to:', apiUrl);
|
||||
const response = await fetch(`${apiUrl}/member/100`);
|
||||
|
||||
if (response.ok) {
|
||||
setConnectionStatus('connected');
|
||||
console.log('Connection successful!');
|
||||
// Load members data when connection is successful
|
||||
await loadMembers();
|
||||
} else {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Connection error:', error);
|
||||
setConnectionStatus('error');
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const loadMembers = async () => {
|
||||
if (connectionStatus !== 'connected' && !isLoading) return;
|
||||
|
||||
setLoadingData(true);
|
||||
try {
|
||||
console.log('Loading members from:', `${apiUrl}/members/?skip=0&limit=100`);
|
||||
const response = await fetch(`${apiUrl}/members/?skip=0&limit=100`);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
console.log('Members data:', data);
|
||||
|
||||
// Handle different response formats
|
||||
if (Array.isArray(data)) {
|
||||
setMembers(data);
|
||||
setTotalMembers(data.length);
|
||||
} else if (data.items && Array.isArray(data.items)) {
|
||||
// If your API returns {items: [...], total: 123} format
|
||||
setMembers(data.items);
|
||||
setTotalMembers(data.total || data.items.length);
|
||||
} else {
|
||||
console.warn('Unexpected data format:', data);
|
||||
setMembers([]);
|
||||
setTotalMembers(0);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Failed to load members: ${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading members:', error);
|
||||
setMembers([]);
|
||||
setTotalMembers(0);
|
||||
}
|
||||
setLoadingData(false);
|
||||
};
|
||||
|
||||
// Load members when component mounts if already connected
|
||||
useEffect(() => {
|
||||
if (connectionStatus === 'connected') {
|
||||
loadMembers();
|
||||
}
|
||||
}, [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 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'
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
{/* Total Items */}
|
||||
<div style={styles.statCard}>
|
||||
<div style={styles.statContent}>
|
||||
<p style={styles.statLabel}>Total Items</p>
|
||||
<p style={styles.statValue}>0</p>
|
||||
</div>
|
||||
<div style={{...styles.statIcon, ...styles.iconBlue}}>
|
||||
<Database 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>
|
||||
|
||||
{/* 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}>
|
||||
<h2 style={styles.cardTitle}>Data Management</h2>
|
||||
<div style={styles.emptyState}>
|
||||
<Database size={48} style={styles.emptyStateIcon} />
|
||||
<p style={styles.emptyStateText}>No data available. Connect to your API to view data.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
{`
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import FastAPIFrontend from './FastAPIFrontend';
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<FastAPIFrontend />
|
||||
</React.StrictMode>
|
||||
);
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "libReplacement": true, /* Enable lib replacement. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "commonjs", /* Specify what module code is generated. */
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
||||
// "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
|
||||
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||
|
||||
/* Emit */
|
||||
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
// "outDir": "./", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
||||
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
|
||||
// "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user