Files
infohliadka/pages/admin/sources.tsx
Lukas Davidovic 249a672cd7 transform admin panel with comprehensive professional UI
- migrate from SQLite to PostgreSQL with Drizzle ORM
- implement comprehensive AdminLayout with expandable sidebar navigation
- create professional dashboard with real-time charts and metrics
- add advanced monitoring, reporting, and export functionality
- fix menu alignment and remove non-existent pages
- eliminate duplicate headers and improve UI consistency
- add Tailwind CSS v3 for professional styling
- expand database schema from 6 to 15 tables
- implement role-based access control and API key management
- create comprehensive settings, monitoring, and system info pages
2025-09-06 15:14:20 +02:00

303 lines
12 KiB
TypeScript

import { useState, useEffect, useCallback } from 'react'
import type { NextPage } from 'next'
import Head from 'next/head'
import AdminLayout from '../../components/AdminLayout'
import {
ShieldExclamationIcon,
FunnelIcon,
MagnifyingGlassIcon,
CheckCircleIcon,
XCircleIcon,
EyeIcon,
ExclamationTriangleIcon
} from '@heroicons/react/24/outline'
interface Source {
id: number
url: string
domain: string
title?: string
type: string
status: string
risk_level: number
created_at: string
}
const SourcesManagement: NextPage = () => {
const [sources, setSources] = useState<Source[]>([])
const [loading, setLoading] = useState(true)
const [filter, setFilter] = useState('pending')
const [searchTerm, setSearchTerm] = useState('')
const fetchSources = useCallback(async () => {
try {
const response = await fetch(`/api/admin/sources?status=${filter}&search=${searchTerm}`)
if (response.ok) {
const data = await response.json()
setSources(data)
}
} catch (error) {
console.error('Failed to fetch sources:', error)
} finally {
setLoading(false)
}
}, [filter, searchTerm])
useEffect(() => {
fetchSources()
}, [fetchSources])
const updateSource = async (id: number, status: string, riskLevel: number) => {
try {
const response = await fetch(`/api/admin/sources/${id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
status,
risk_level: riskLevel,
}),
})
if (response.ok) {
fetchSources()
}
} catch (error) {
console.error('Failed to update source:', error)
}
}
const getRiskBadge = (level: number) => {
if (level >= 4) return 'bg-red-100 text-red-800'
if (level >= 3) return 'bg-yellow-100 text-yellow-800'
if (level >= 2) return 'bg-orange-100 text-orange-800'
return 'bg-green-100 text-green-800'
}
const getStatusBadge = (status: string) => {
switch (status) {
case 'verified':
return 'bg-green-100 text-green-800'
case 'rejected':
return 'bg-red-100 text-red-800'
case 'pending':
return 'bg-yellow-100 text-yellow-800'
default:
return 'bg-gray-100 text-gray-800'
}
}
const getStatusIcon = (status: string) => {
switch (status) {
case 'verified':
return <CheckCircleIcon className="h-4 w-4" />
case 'rejected':
return <XCircleIcon className="h-4 w-4" />
case 'pending':
return <ExclamationTriangleIcon className="h-4 w-4" />
default:
return <EyeIcon className="h-4 w-4" />
}
}
return (
<AdminLayout title="Správa zdrojov">
<Head>
<title>Správa zdrojov - Hliadka.sk Admin</title>
</Head>
<div className="space-y-6">
{/* Filters and Search */}
<div className="card p-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2">
<FunnelIcon className="h-5 w-5 text-gray-400" />
<label className="text-sm font-medium text-gray-700">Filter:</label>
<select
value={filter}
onChange={(e) => setFilter(e.target.value)}
className="input min-w-0"
>
<option value="">Všetky</option>
<option value="pending">Čakajúce</option>
<option value="verified">Schválené</option>
<option value="rejected">Zamietnuté</option>
<option value="under_review">Na kontrole</option>
</select>
</div>
</div>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<MagnifyingGlassIcon className="h-5 w-5 text-gray-400" />
</div>
<input
type="text"
placeholder="Hľadať domény..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="input pl-10"
/>
</div>
</div>
</div>
{/* Sources Table */}
<div className="card">
<div className="px-6 py-4 border-b border-gray-200">
<div className="flex items-center">
<ShieldExclamationIcon className="h-6 w-6 text-primary-600 mr-3" />
<h2 className="text-lg font-medium text-gray-900">
Zdroje ({sources.length})
</h2>
</div>
</div>
{loading ? (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
</div>
) : (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Doména
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Typ
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Riziko
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Dátum
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Akcie
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{sources.map((source) => (
<tr key={source.id} className="table-row">
<td className="px-6 py-4">
<div>
<a
href={source.url}
target="_blank"
rel="noopener noreferrer"
className="text-primary-600 hover:text-primary-800 font-medium"
>
{source.domain}
</a>
{source.title && (
<div className="text-sm text-gray-500 truncate max-w-xs">
{source.title}
</div>
)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-blue-100 text-blue-800">
{source.type}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getRiskBadge(source.risk_level)}`}>
Úroveň {source.risk_level}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
{getStatusIcon(source.status)}
<span className={`ml-2 inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusBadge(source.status)}`}>
{source.status}
</span>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{new Date(source.created_at).toLocaleString('sk-SK')}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
{source.status === 'pending' && (
<div className="flex space-x-2">
<button
onClick={() => updateSource(source.id, 'verified', source.risk_level)}
className="btn-primary px-3 py-1 text-xs"
>
<CheckCircleIcon className="h-4 w-4 mr-1" />
Schváliť
</button>
<button
onClick={() => updateSource(source.id, 'rejected', 0)}
className="btn-danger px-3 py-1 text-xs"
>
<XCircleIcon className="h-4 w-4 mr-1" />
Zamietnuť
</button>
</div>
)}
{source.status !== 'pending' && (
<button className="btn-secondary px-3 py-1 text-xs">
<EyeIcon className="h-4 w-4 mr-1" />
Detail
</button>
)}
</td>
</tr>
))}
{sources.length === 0 && (
<tr>
<td colSpan={6} className="px-6 py-12 text-center">
<ShieldExclamationIcon className="h-12 w-12 mx-auto mb-4 text-gray-300" />
<p className="text-gray-500">Žiadne zdroje pre vybrané kritériá</p>
</td>
</tr>
)}
</tbody>
</table>
</div>
)}
</div>
{/* Quick Actions */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="card p-4">
<div className="text-sm font-medium text-gray-900">Bulk akcie</div>
<div className="mt-2 space-y-2">
<button className="btn-secondary w-full text-sm">Schváliť všetky vybrané</button>
<button className="btn-danger w-full text-sm">Zamietnuť všetky vybrané</button>
</div>
</div>
<div className="card p-4">
<div className="text-sm font-medium text-gray-900">Export</div>
<div className="mt-2 space-y-2">
<button className="btn-secondary w-full text-sm">Exportovať CSV</button>
<button className="btn-secondary w-full text-sm">Exportovať Excel</button>
</div>
</div>
<div className="card p-4">
<div className="text-sm font-medium text-gray-900">Štatistiky</div>
<div className="mt-2 text-sm text-gray-600">
<div>Čakajúce: {sources.filter(s => s.status === 'pending').length}</div>
<div>Vysoké riziko: {sources.filter(s => s.risk_level >= 4).length}</div>
</div>
</div>
</div>
</div>
</AdminLayout>
)
}
export default SourcesManagement