You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

516 lines
18 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import Layout from '../components/Layout';
import { fredApi, FredUsageStats, FredSeriesData, FredObservationsData } from '../lib/api';
interface QuotaDisplayProps {
stats: FredUsageStats;
isLoading: boolean;
}
const QuotaDisplay: React.FC<QuotaDisplayProps> = ({ stats, isLoading }) => {
if (isLoading) {
return (
<div className="bg-white rounded-lg shadow p-6 mb-6">
<div className="animate-pulse">
<div className="h-4 bg-gray-200 rounded w-1/4 mb-2"></div>
<div className="h-8 bg-gray-200 rounded w-1/2"></div>
</div>
</div>
);
}
const getQuotaColor = (percentage: number) => {
if (percentage < 50) return 'bg-green-500';
if (percentage < 80) return 'bg-yellow-500';
return 'bg-red-500';
};
const getQuotaTextColor = (percentage: number) => {
if (percentage < 50) return 'text-green-600';
if (percentage < 80) return 'text-yellow-600';
return 'text-red-600';
};
return (
<div className="bg-white rounded-lg shadow p-6 mb-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">📊 FRED API Daily Quota</h3>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-4">
<div className="bg-gray-50 rounded-lg p-4">
<div className="text-sm text-gray-500">Used Today</div>
<div className={`text-2xl font-bold ${getQuotaTextColor(stats.usage_percentage)}`}>
{stats.used_today}
</div>
</div>
<div className="bg-gray-50 rounded-lg p-4">
<div className="text-sm text-gray-500">Remaining</div>
<div className="text-2xl font-bold text-blue-600">
{stats.remaining_today}
</div>
</div>
<div className="bg-gray-50 rounded-lg p-4">
<div className="text-sm text-gray-500">Daily Limit</div>
<div className="text-2xl font-bold text-gray-900">
{stats.daily_limit}
</div>
</div>
<div className="bg-gray-50 rounded-lg p-4">
<div className="text-sm text-gray-500">Usage %</div>
<div className={`text-2xl font-bold ${getQuotaTextColor(stats.usage_percentage)}`}>
{stats.usage_percentage.toFixed(1)}%
</div>
</div>
</div>
{/* Progress Bar */}
<div className="mb-4">
<div className="flex justify-between text-sm text-gray-600 mb-1">
<span>API Usage Progress</span>
<span>{stats.used_today} / {stats.daily_limit}</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className={`h-2 rounded-full ${getQuotaColor(stats.usage_percentage)}`}
style={{ width: `${Math.min(stats.usage_percentage, 100)}%` }}
></div>
</div>
</div>
{/* Status */}
<div className="flex items-center gap-2">
<div className={`w-3 h-3 rounded-full ${stats.can_make_requests ? 'bg-green-500' : 'bg-red-500'}`}></div>
<span className={`text-sm font-medium ${stats.can_make_requests ? 'text-green-600' : 'text-red-600'}`}>
{stats.can_make_requests ? 'API Available' : 'Daily Limit Reached'}
</span>
</div>
</div>
);
};
interface CacheStatsProps {
stats: FredUsageStats;
}
const CacheStats: React.FC<CacheStatsProps> = ({ stats }) => {
return (
<div className="bg-white rounded-lg shadow p-6 mb-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">💾 Database Cache Statistics</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-blue-50 rounded-lg p-4">
<div className="text-sm text-blue-600">Cached Series</div>
<div className="text-2xl font-bold text-blue-900">
{stats.cache_stats.cached_series.toLocaleString()}
</div>
<div className="text-xs text-blue-500 mt-1">Economic data series</div>
</div>
<div className="bg-green-50 rounded-lg p-4">
<div className="text-sm text-green-600">Cached Observations</div>
<div className="text-2xl font-bold text-green-900">
{stats.cache_stats.cached_observations.toLocaleString()}
</div>
<div className="text-xs text-green-500 mt-1">Historical data points</div>
</div>
<div className="bg-purple-50 rounded-lg p-4">
<div className="text-sm text-purple-600">Cache Duration</div>
<div className="text-2xl font-bold text-purple-900">
{stats.cache_stats.cache_duration_hours}h
</div>
<div className="text-xs text-purple-500 mt-1">Auto-refresh period</div>
</div>
</div>
<div className="mt-4 p-4 bg-gray-50 rounded-lg">
<h4 className="font-medium text-gray-900 mb-2">🔧 Proxy Information</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
<div>
<span className="text-gray-600">Mode:</span>
<span className="ml-2 font-medium">{stats.proxy_info.mode}</span>
</div>
<div>
<span className="text-gray-600">Smart Caching:</span>
<span className="ml-2 font-medium">{stats.proxy_info.smart_caching ? 'Enabled' : 'Disabled'}</span>
</div>
<div>
<span className="text-gray-600">Permanent Storage:</span>
<span className="ml-2 font-medium">{stats.proxy_info.permanent_storage ? 'Yes' : 'No'}</span>
</div>
<div>
<span className="text-gray-600">Supported Endpoints:</span>
<span className="ml-2 font-medium">{stats.proxy_info.supported_endpoints}</span>
</div>
</div>
</div>
</div>
);
};
interface EndpointStatsProps {
stats: FredUsageStats;
}
const EndpointStats: React.FC<EndpointStatsProps> = ({ stats }) => {
const topEndpoints = stats.endpoint_stats.slice(0, 10);
return (
<div className="bg-white rounded-lg shadow p-6 mb-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">🔥 Most Used Endpoints</h3>
{topEndpoints.length > 0 ? (
<div className="space-y-3">
{topEndpoints.map((endpoint, index) => (
<div key={endpoint.endpoint} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div className="flex items-center gap-3">
<div className="w-6 h-6 bg-blue-100 text-blue-600 rounded-full flex items-center justify-center text-xs font-bold">
{index + 1}
</div>
<code className="text-sm font-mono bg-gray-200 px-2 py-1 rounded">
{endpoint.endpoint}
</code>
</div>
<div className="text-right">
<div className="text-lg font-bold text-gray-900">{endpoint.call_count}</div>
<div className="text-xs text-gray-500">calls</div>
</div>
</div>
))}
</div>
) : (
<div className="text-gray-500 text-center py-8">
No endpoint usage data available
</div>
)}
</div>
);
};
interface PopularSeriesProps {
series: FredSeriesData[];
isLoading: boolean;
onSelectSeries: (seriesId: string) => void;
}
const PopularSeries: React.FC<PopularSeriesProps> = ({ series, isLoading, onSelectSeries }) => {
if (isLoading) {
return (
<div className="bg-white rounded-lg shadow p-6 mb-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">📈 Popular Economic Indicators</h3>
<div className="space-y-3">
{[...Array(5)].map((_, i) => (
<div key={i} className="animate-pulse p-4 bg-gray-50 rounded-lg">
<div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
<div className="h-3 bg-gray-200 rounded w-1/2"></div>
</div>
))}
</div>
</div>
);
}
return (
<div className="bg-white rounded-lg shadow p-6 mb-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">📈 Popular Economic Indicators</h3>
<div className="space-y-2">
{series.map((seriesData) => (
<button
key={seriesData.id}
onClick={() => onSelectSeries(seriesData.id)}
className="w-full text-left p-4 bg-gray-50 hover:bg-blue-50 rounded-lg transition-colors border border-transparent hover:border-blue-200"
>
<div className="flex justify-between items-start">
<div className="flex-1">
<div className="font-medium text-gray-900 flex items-center gap-2">
<code className="text-sm bg-gray-200 px-2 py-1 rounded">{seriesData.id}</code>
{seriesData.cached && (
<span className="text-xs bg-green-100 text-green-600 px-2 py-1 rounded">Cached</span>
)}
</div>
<div className="text-sm text-gray-600 mt-1 line-clamp-2">{seriesData.title}</div>
<div className="text-xs text-gray-500 mt-1">
{seriesData.frequency} {seriesData.units}
</div>
</div>
<div className="text-xs text-gray-400 ml-4">
Updated: {new Date(seriesData.last_updated).toLocaleDateString()}
</div>
</div>
</button>
))}
</div>
</div>
);
};
interface SeriesSearchProps {
onSelectSeries: (seriesId: string) => void;
}
const SeriesSearch: React.FC<SeriesSearchProps> = ({ onSelectSeries }) => {
const [searchTerm, setSearchTerm] = useState('');
const [searchResults, setSearchResults] = useState<FredSeriesData[]>([]);
const [isSearching, setIsSearching] = useState(false);
const handleSearch = async () => {
if (!searchTerm.trim()) return;
setIsSearching(true);
try {
const results = await fredApi.searchSeries(searchTerm, 15);
setSearchResults(results);
} catch (error) {
console.error('Search failed:', error);
setSearchResults([]);
} finally {
setIsSearching(false);
}
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
handleSearch();
}
};
return (
<div className="bg-white rounded-lg shadow p-6 mb-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">🔍 Search Economic Data</h3>
<div className="flex gap-2 mb-4">
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="Search for economic indicators (e.g., unemployment, inflation, GDP)"
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
<button
onClick={handleSearch}
disabled={!searchTerm.trim() || isSearching}
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSearching ? 'Searching...' : 'Search'}
</button>
</div>
{searchResults.length > 0 && (
<div className="space-y-2 max-h-96 overflow-y-auto">
{searchResults.map((seriesData) => (
<button
key={seriesData.id}
onClick={() => onSelectSeries(seriesData.id)}
className="w-full text-left p-3 bg-gray-50 hover:bg-blue-50 rounded-lg transition-colors border border-transparent hover:border-blue-200"
>
<div className="flex justify-between items-start">
<div className="flex-1">
<div className="font-medium text-gray-900">
<code className="text-sm bg-gray-200 px-2 py-1 rounded mr-2">{seriesData.id}</code>
</div>
<div className="text-sm text-gray-600 mt-1 line-clamp-2">{seriesData.title}</div>
<div className="text-xs text-gray-500 mt-1">
{seriesData.frequency} {seriesData.units}
</div>
</div>
</div>
</button>
))}
</div>
)}
{searchTerm && searchResults.length === 0 && !isSearching && (
<div className="text-gray-500 text-center py-4">
No results found for "{searchTerm}"
</div>
)}
</div>
);
};
interface SeriesDataDisplayProps {
seriesId: string;
observations: FredObservationsData | null;
isLoading: boolean;
onClose: () => void;
}
const SeriesDataDisplay: React.FC<SeriesDataDisplayProps> = ({ seriesId, observations, isLoading, onClose }) => {
if (isLoading) {
return (
<div className="bg-white rounded-lg shadow p-6">
<div className="flex justify-between items-center mb-4">
<div className="h-6 bg-gray-200 rounded w-1/3 animate-pulse"></div>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600"></button>
</div>
<div className="space-y-2">
{[...Array(10)].map((_, i) => (
<div key={i} className="flex justify-between items-center p-2 animate-pulse">
<div className="h-4 bg-gray-200 rounded w-24"></div>
<div className="h-4 bg-gray-200 rounded w-16"></div>
</div>
))}
</div>
</div>
);
}
if (!observations) return null;
return (
<div className="bg-white rounded-lg shadow p-6">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
📊 <code className="text-sm bg-gray-200 px-2 py-1 rounded">{seriesId}</code>
{observations.cached && (
<span className="text-xs bg-green-100 text-green-600 px-2 py-1 rounded">From Cache</span>
)}
</h3>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 text-xl"
>
</button>
</div>
<div className="mb-4 text-sm text-gray-600">
{observations.count} observations Last 20 shown
{observations.cached_at && (
<span className="ml-2"> Cached: {new Date(observations.cached_at).toLocaleString()}</span>
)}
</div>
<div className="max-h-96 overflow-y-auto">
<table className="w-full text-sm">
<thead className="bg-gray-50 sticky top-0">
<tr>
<th className="px-4 py-2 text-left">Date</th>
<th className="px-4 py-2 text-right">Value</th>
</tr>
</thead>
<tbody>
{observations.observations.slice(0, 20).map((obs, index) => (
<tr key={index} className="border-t border-gray-100">
<td className="px-4 py-2 text-gray-900">{obs.date}</td>
<td className="px-4 py-2 text-right font-mono">
{obs.value === '.' ? 'N/A' : parseFloat(obs.value).toLocaleString()}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
};
const FredPage: React.FC = () => {
const [stats, setStats] = useState<FredUsageStats | null>(null);
const [popularSeries, setPopularSeries] = useState<FredSeriesData[]>([]);
const [selectedSeriesId, setSelectedSeriesId] = useState<string | null>(null);
const [seriesObservations, setSeriesObservations] = useState<FredObservationsData | null>(null);
const [isLoadingStats, setIsLoadingStats] = useState(true);
const [isLoadingPopular, setIsLoadingPopular] = useState(true);
const [isLoadingObservations, setIsLoadingObservations] = useState(false);
// Load initial data
useEffect(() => {
const loadInitialData = async () => {
try {
// Load usage stats
const statsData = await fredApi.getUsageStats(7, true);
setStats(statsData);
setIsLoadingStats(false);
// Load popular series
const popularData = await fredApi.getPopularSeries();
setPopularSeries(popularData);
setIsLoadingPopular(false);
} catch (error) {
console.error('Failed to load initial data:', error);
setIsLoadingStats(false);
setIsLoadingPopular(false);
}
};
loadInitialData();
}, []);
// Load series observations when selected
const handleSelectSeries = async (seriesId: string) => {
setSelectedSeriesId(seriesId);
setIsLoadingObservations(true);
try {
const observationsData = await fredApi.getSeriesObservations(seriesId, 50);
setSeriesObservations(observationsData);
} catch (error) {
console.error('Failed to load series observations:', error);
setSeriesObservations(null);
} finally {
setIsLoadingObservations(false);
}
};
const handleCloseSeriesData = () => {
setSelectedSeriesId(null);
setSeriesObservations(null);
};
return (
<Layout>
<div className="space-y-6">
<div className="flex justify-between items-center">
<h1 className="text-2xl font-bold text-gray-900">🏦 FRED Economic Data</h1>
<div className="text-sm text-gray-500">
Federal Reserve Economic Data (FRED) API Access
</div>
</div>
{/* Quota Display */}
{stats && <QuotaDisplay stats={stats} isLoading={isLoadingStats} />}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Left Column */}
<div>
{/* Popular Series */}
<PopularSeries
series={popularSeries}
isLoading={isLoadingPopular}
onSelectSeries={handleSelectSeries}
/>
{/* Search */}
<SeriesSearch onSelectSeries={handleSelectSeries} />
</div>
{/* Right Column */}
<div>
{/* Cache Stats */}
{stats && <CacheStats stats={stats} />}
{/* Endpoint Stats */}
{stats && <EndpointStats stats={stats} />}
</div>
</div>
{/* Series Data Display */}
{selectedSeriesId && (
<SeriesDataDisplay
seriesId={selectedSeriesId}
observations={seriesObservations}
isLoading={isLoadingObservations}
onClose={handleCloseSeriesData}
/>
)}
</div>
</Layout>
);
};
export default FredPage;