486 lines
18 KiB
TypeScript
486 lines
18 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import Link from 'next/link';
|
|
import {
|
|
Settings,
|
|
Key,
|
|
BarChart3,
|
|
Globe,
|
|
ArrowLeft,
|
|
Save,
|
|
RefreshCw,
|
|
Eye,
|
|
EyeOff,
|
|
Check,
|
|
AlertCircle,
|
|
} from 'lucide-react';
|
|
import { ApiStatsChart } from './_components/api-stats-chart';
|
|
|
|
interface SettingsData {
|
|
id: string;
|
|
apiKey: string | null;
|
|
selectedLeague: string;
|
|
leagueId: number;
|
|
season: number;
|
|
siteName: string;
|
|
primaryColor: string;
|
|
secondaryColor: string;
|
|
showLiveMatches: boolean;
|
|
showStandings: boolean;
|
|
showTopScorers: boolean;
|
|
showRecentResults: boolean;
|
|
showUpcoming: boolean;
|
|
refreshInterval: number;
|
|
}
|
|
|
|
interface ApiStats {
|
|
totalRequests: number;
|
|
requestsToday: number;
|
|
requestsByEndpoint: { endpoint: string; count: number }[];
|
|
chartData: { time: string; count: number }[];
|
|
avgResponseTime: number;
|
|
successRate: number;
|
|
}
|
|
|
|
export default function AdminPage() {
|
|
const [settings, setSettings] = useState<SettingsData | null>(null);
|
|
const [stats, setStats] = useState<ApiStats | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [saving, setSaving] = useState(false);
|
|
const [showApiKey, setShowApiKey] = useState(false);
|
|
const [saveSuccess, setSaveSuccess] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
fetchData();
|
|
}, []);
|
|
|
|
const fetchData = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const [settingsRes, statsRes] = await Promise.all([
|
|
fetch('/api/admin/settings'),
|
|
fetch('/api/admin/stats'),
|
|
]);
|
|
|
|
if (settingsRes.ok) {
|
|
const settingsData = await settingsRes.json();
|
|
setSettings(settingsData);
|
|
}
|
|
|
|
if (statsRes.ok) {
|
|
const statsData = await statsRes.json();
|
|
setStats(statsData);
|
|
}
|
|
} catch (err) {
|
|
setError('Failed to load data');
|
|
console.error(err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const saveSettings = async () => {
|
|
if (!settings) return;
|
|
|
|
try {
|
|
setSaving(true);
|
|
setError(null);
|
|
|
|
const res = await fetch('/api/admin/settings', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(settings),
|
|
});
|
|
|
|
if (res.ok) {
|
|
const updatedSettings = await res.json();
|
|
setSettings(updatedSettings);
|
|
setSaveSuccess(true);
|
|
setTimeout(() => setSaveSuccess(false), 3000);
|
|
} else {
|
|
setError('Failed to save settings');
|
|
}
|
|
} catch (err) {
|
|
setError('Failed to save settings');
|
|
console.error(err);
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
const updateSetting = <K extends keyof SettingsData>(
|
|
key: K,
|
|
value: SettingsData[K]
|
|
) => {
|
|
if (settings) {
|
|
setSettings({ ...settings, [key]: value });
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-gray-900 to-gray-800 flex items-center justify-center">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-4 border-green-500 border-t-transparent"></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-gray-900 to-gray-800">
|
|
{/* Header */}
|
|
<header className="bg-gray-900/80 backdrop-blur-md border-b border-gray-700 sticky top-0 z-50">
|
|
<div className="max-w-7xl mx-auto px-4 py-4 flex items-center justify-between">
|
|
<div className="flex items-center gap-4">
|
|
<Link
|
|
href="/"
|
|
className="flex items-center gap-2 text-gray-400 hover:text-white transition-colors"
|
|
>
|
|
<ArrowLeft className="h-5 w-5" />
|
|
<span>Back to Site</span>
|
|
</Link>
|
|
<div className="h-6 w-px bg-gray-700" />
|
|
<h1 className="text-xl font-bold text-white flex items-center gap-2">
|
|
<Settings className="h-6 w-6 text-green-500" />
|
|
Backoffice
|
|
</h1>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<button
|
|
onClick={fetchData}
|
|
className="flex items-center gap-2 px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-lg transition-colors"
|
|
>
|
|
<RefreshCw className="h-4 w-4" />
|
|
Refresh
|
|
</button>
|
|
<button
|
|
onClick={saveSettings}
|
|
disabled={saving}
|
|
className="flex items-center gap-2 px-4 py-2 bg-green-600 hover:bg-green-500 text-white rounded-lg transition-colors disabled:opacity-50"
|
|
>
|
|
{saving ? (
|
|
<RefreshCw className="h-4 w-4 animate-spin" />
|
|
) : saveSuccess ? (
|
|
<Check className="h-4 w-4" />
|
|
) : (
|
|
<Save className="h-4 w-4" />
|
|
)}
|
|
{saveSuccess ? 'Saved!' : 'Save Changes'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{error && (
|
|
<div className="max-w-7xl mx-auto px-4 pt-4">
|
|
<div className="bg-red-500/20 border border-red-500 text-red-400 px-4 py-3 rounded-lg flex items-center gap-2">
|
|
<AlertCircle className="h-5 w-5" />
|
|
{error}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<main className="max-w-7xl mx-auto px-4 py-8">
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
{/* Layout Settings */}
|
|
<section className="bg-gray-800/50 backdrop-blur-sm rounded-xl border border-gray-700 p-6">
|
|
<h2 className="text-lg font-semibold text-white flex items-center gap-2 mb-6">
|
|
<Settings className="h-5 w-5 text-green-500" />
|
|
Layout Settings
|
|
</h2>
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Site Name
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={settings?.siteName || ''}
|
|
onChange={(e) => updateSetting('siteName', e.target.value)}
|
|
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Primary Color
|
|
</label>
|
|
<div className="flex items-center gap-2">
|
|
<input
|
|
type="color"
|
|
value={settings?.primaryColor || '#E42518'}
|
|
onChange={(e) =>
|
|
updateSetting('primaryColor', e.target.value)
|
|
}
|
|
className="w-12 h-10 rounded cursor-pointer"
|
|
/>
|
|
<input
|
|
type="text"
|
|
value={settings?.primaryColor || ''}
|
|
onChange={(e) =>
|
|
updateSetting('primaryColor', e.target.value)
|
|
}
|
|
className="flex-1 px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white text-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Secondary Color
|
|
</label>
|
|
<div className="flex items-center gap-2">
|
|
<input
|
|
type="color"
|
|
value={settings?.secondaryColor || '#006600'}
|
|
onChange={(e) =>
|
|
updateSetting('secondaryColor', e.target.value)
|
|
}
|
|
className="w-12 h-10 rounded cursor-pointer"
|
|
/>
|
|
<input
|
|
type="text"
|
|
value={settings?.secondaryColor || ''}
|
|
onChange={(e) =>
|
|
updateSetting('secondaryColor', e.target.value)
|
|
}
|
|
className="flex-1 px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white text-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Refresh Interval (seconds)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
min="60"
|
|
max="3600"
|
|
value={settings?.refreshInterval || 300}
|
|
onChange={(e) =>
|
|
updateSetting('refreshInterval', parseInt(e.target.value))
|
|
}
|
|
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-3 pt-2">
|
|
<label className="block text-sm font-medium text-gray-300">
|
|
Visible Sections
|
|
</label>
|
|
{[
|
|
{ key: 'showLiveMatches', label: 'Live Matches' },
|
|
{ key: 'showStandings', label: 'Standings Table' },
|
|
{ key: 'showTopScorers', label: 'Top Scorers' },
|
|
{ key: 'showRecentResults', label: 'Recent Results' },
|
|
{ key: 'showUpcoming', label: 'Upcoming Fixtures' },
|
|
].map((item) => (
|
|
<label
|
|
key={item.key}
|
|
className="flex items-center gap-3 cursor-pointer"
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
checked={
|
|
settings?.[item.key as keyof SettingsData] as boolean
|
|
}
|
|
onChange={(e) =>
|
|
updateSetting(
|
|
item.key as keyof SettingsData,
|
|
e.target.checked as never
|
|
)
|
|
}
|
|
className="w-5 h-5 rounded bg-gray-700 border-gray-600 text-green-500 focus:ring-green-500"
|
|
/>
|
|
<span className="text-gray-300">{item.label}</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* API Key Management */}
|
|
<section className="bg-gray-800/50 backdrop-blur-sm rounded-xl border border-gray-700 p-6">
|
|
<h2 className="text-lg font-semibold text-white flex items-center gap-2 mb-6">
|
|
<Key className="h-5 w-5 text-yellow-500" />
|
|
API Key Management
|
|
</h2>
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
API-Football API Key
|
|
</label>
|
|
<div className="relative">
|
|
<input
|
|
type={showApiKey ? 'text' : 'password'}
|
|
value={settings?.apiKey || ''}
|
|
onChange={(e) => updateSetting('apiKey', e.target.value)}
|
|
placeholder="Enter your API key"
|
|
className="w-full px-4 py-2 pr-12 bg-gray-700 border border-gray-600 rounded-lg text-white focus:ring-2 focus:ring-yellow-500 focus:border-transparent font-mono"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowApiKey(!showApiKey)}
|
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-white"
|
|
>
|
|
{showApiKey ? (
|
|
<EyeOff className="h-5 w-5" />
|
|
) : (
|
|
<Eye className="h-5 w-5" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
<p className="text-sm text-gray-500 mt-2">
|
|
Get your API key from{' '}
|
|
<a
|
|
href="https://www.api-football.com/"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-green-500 hover:underline"
|
|
>
|
|
api-football.com
|
|
</a>
|
|
</p>
|
|
</div>
|
|
|
|
<div className="bg-gray-700/50 rounded-lg p-4">
|
|
<h3 className="text-sm font-medium text-gray-300 mb-2">
|
|
API Status
|
|
</h3>
|
|
<div className="flex items-center gap-2">
|
|
{settings?.apiKey ? (
|
|
<>
|
|
<div className="w-3 h-3 rounded-full bg-green-500"></div>
|
|
<span className="text-green-400">API Key Configured</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<div className="w-3 h-3 rounded-full bg-red-500"></div>
|
|
<span className="text-red-400">No API Key Set</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* League Switcher */}
|
|
<section className="bg-gray-800/50 backdrop-blur-sm rounded-xl border border-gray-700 p-6">
|
|
<h2 className="text-lg font-semibold text-white flex items-center gap-2 mb-6">
|
|
<Globe className="h-5 w-5 text-blue-500" />
|
|
League Selection
|
|
</h2>
|
|
|
|
<div className="space-y-4">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<button
|
|
onClick={() => updateSetting('selectedLeague', 'primeira_liga')}
|
|
className={`p-4 rounded-xl border-2 transition-all ${
|
|
settings?.selectedLeague === 'primeira_liga'
|
|
? 'border-green-500 bg-green-500/20'
|
|
: 'border-gray-600 bg-gray-700/50 hover:border-gray-500'
|
|
}`}
|
|
>
|
|
<div className="text-2xl mb-2">🇵🇹</div>
|
|
<div className="font-semibold text-white">Primeira Liga</div>
|
|
<div className="text-sm text-gray-400">Portuguese Championship</div>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => updateSetting('selectedLeague', 'euro_2026')}
|
|
className={`p-4 rounded-xl border-2 transition-all ${
|
|
settings?.selectedLeague === 'euro_2026'
|
|
? 'border-blue-500 bg-blue-500/20'
|
|
: 'border-gray-600 bg-gray-700/50 hover:border-gray-500'
|
|
}`}
|
|
>
|
|
<div className="text-2xl mb-2">🇪🇺</div>
|
|
<div className="font-semibold text-white">UEFA Euro</div>
|
|
<div className="text-sm text-gray-400">European Championship</div>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="bg-gray-700/50 rounded-lg p-4">
|
|
<h3 className="text-sm font-medium text-gray-300 mb-2">
|
|
Current Configuration
|
|
</h3>
|
|
<div className="grid grid-cols-2 gap-2 text-sm">
|
|
<div className="text-gray-400">League ID:</div>
|
|
<div className="text-white font-mono">{settings?.leagueId}</div>
|
|
<div className="text-gray-400">Season:</div>
|
|
<div className="text-white font-mono">{settings?.season}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* API Statistics */}
|
|
<section className="bg-gray-800/50 backdrop-blur-sm rounded-xl border border-gray-700 p-6">
|
|
<h2 className="text-lg font-semibold text-white flex items-center gap-2 mb-6">
|
|
<BarChart3 className="h-5 w-5 text-purple-500" />
|
|
API Statistics
|
|
</h2>
|
|
|
|
<div className="grid grid-cols-2 gap-4 mb-6">
|
|
<div className="bg-gray-700/50 rounded-lg p-4">
|
|
<div className="text-3xl font-bold text-white">
|
|
{stats?.totalRequests || 0}
|
|
</div>
|
|
<div className="text-sm text-gray-400">Total Requests</div>
|
|
</div>
|
|
<div className="bg-gray-700/50 rounded-lg p-4">
|
|
<div className="text-3xl font-bold text-white">
|
|
{stats?.requestsToday || 0}
|
|
</div>
|
|
<div className="text-sm text-gray-400">Requests Today</div>
|
|
</div>
|
|
<div className="bg-gray-700/50 rounded-lg p-4">
|
|
<div className="text-3xl font-bold text-white">
|
|
{stats?.avgResponseTime || 0}ms
|
|
</div>
|
|
<div className="text-sm text-gray-400">Avg Response Time</div>
|
|
</div>
|
|
<div className="bg-gray-700/50 rounded-lg p-4">
|
|
<div className="text-3xl font-bold text-green-400">
|
|
{stats?.successRate || 100}%
|
|
</div>
|
|
<div className="text-sm text-gray-400">Success Rate</div>
|
|
</div>
|
|
</div>
|
|
|
|
<ApiStatsChart data={stats?.chartData || []} />
|
|
|
|
{stats?.requestsByEndpoint && stats.requestsByEndpoint.length > 0 && (
|
|
<div className="mt-6">
|
|
<h3 className="text-sm font-medium text-gray-300 mb-3">
|
|
Requests by Endpoint
|
|
</h3>
|
|
<div className="space-y-2">
|
|
{stats.requestsByEndpoint.map((item) => (
|
|
<div
|
|
key={item.endpoint}
|
|
className="flex items-center justify-between bg-gray-700/50 rounded-lg px-3 py-2"
|
|
>
|
|
<span className="text-gray-300 font-mono text-sm">
|
|
{item.endpoint}
|
|
</span>
|
|
<span className="text-white font-semibold">
|
|
{item.count}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</section>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|