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
This commit is contained in:
@@ -1,93 +1,204 @@
|
||||
import { useState } from "react"
|
||||
import type { NextPage } from "next"
|
||||
import Head from "next/head"
|
||||
import Link from "next/link"
|
||||
import { useState } from 'react'
|
||||
import type { NextPage } from 'next'
|
||||
import Head from 'next/head'
|
||||
import AdminLayout from '../../components/AdminLayout'
|
||||
import { CloudArrowUpIcon, DocumentArrowUpIcon, CheckCircleIcon } from '@heroicons/react/24/outline'
|
||||
|
||||
const BulkImport: NextPage = () => {
|
||||
const [importData, setImportData] = useState("")
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [result, setResult] = useState<any>(null)
|
||||
const [file, setFile] = useState<File | null>(null)
|
||||
const [importing, setImporting] = useState(false)
|
||||
const [results, setResults] = useState<any>(null)
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files && e.target.files[0]) {
|
||||
setFile(e.target.files[0])
|
||||
setResults(null)
|
||||
}
|
||||
}
|
||||
|
||||
const handleImport = async () => {
|
||||
if (!importData.trim()) return
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const lines = importData.trim().split('\n')
|
||||
const sources = lines.map(line => {
|
||||
const [domain, risk_level] = line.split(',')
|
||||
return {
|
||||
domain: domain?.trim(),
|
||||
risk_level: parseInt(risk_level?.trim()) || 3
|
||||
}
|
||||
}).filter(s => s.domain)
|
||||
if (!file) return
|
||||
|
||||
setImporting(true)
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/bulk-import', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sources })
|
||||
body: formData
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
setResult(data)
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setResults(data)
|
||||
setFile(null)
|
||||
} else {
|
||||
const error = await response.json()
|
||||
alert('Chyba pri importe: ' + error.error)
|
||||
}
|
||||
} catch (error) {
|
||||
setResult({ error: 'Import failed' })
|
||||
alert('Chyba pri nahrávaní súboru')
|
||||
} finally {
|
||||
setImporting(false)
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<>
|
||||
<Head>
|
||||
<title>Bulk Import - Infohliadka</title>
|
||||
<title>Hromadný import - Hliadka.sk Admin</title>
|
||||
</Head>
|
||||
|
||||
<div style={{ padding: '20px' }}>
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<Link href="/admin">← Back to Admin</Link>
|
||||
</div>
|
||||
|
||||
<h1>Bulk Import Sources</h1>
|
||||
<p>Import multiple sources at once. Format: domain,risk_level (one per line)</p>
|
||||
|
||||
<textarea
|
||||
value={importData}
|
||||
onChange={(e) => setImportData(e.target.value)}
|
||||
placeholder="example.com,4 badsite.sk,5 spam.org,2"
|
||||
rows={10}
|
||||
style={{ width: '100%', marginBottom: '10px' }}
|
||||
/>
|
||||
|
||||
<button
|
||||
onClick={handleImport}
|
||||
disabled={loading || !importData.trim()}
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
backgroundColor: loading ? '#ccc' : '#007bff',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px'
|
||||
}}
|
||||
>
|
||||
{loading ? 'Importing...' : 'Import Sources'}
|
||||
</button>
|
||||
|
||||
{result && (
|
||||
<div style={{ marginTop: '20px', padding: '15px', backgroundColor: '#f8f9fa', border: '1px solid #ddd' }}>
|
||||
<h3>Import Result:</h3>
|
||||
{result.error ? (
|
||||
<p style={{ color: 'red' }}>Error: {result.error}</p>
|
||||
) : (
|
||||
<div>
|
||||
<p>Total processed: {result.total}</p>
|
||||
<p>Successfully imported: {result.imported}</p>
|
||||
<p>Skipped (duplicates/invalid): {result.skipped}</p>
|
||||
<AdminLayout title="Hromadný import">
|
||||
<div className="px-4 py-6 sm:px-6 lg:px-8">
|
||||
<div className="sm:flex sm:items-center">
|
||||
<div className="sm:flex-auto">
|
||||
<p className="text-sm text-gray-700">
|
||||
Importujte viacero zdrojov naraz pomocou CSV súboru
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 max-w-3xl">
|
||||
<div className="card p-6">
|
||||
<div className="text-center">
|
||||
<CloudArrowUpIcon className="mx-auto h-12 w-12 text-gray-400" />
|
||||
<h3 className="mt-2 text-sm font-semibold text-gray-900">Nahranie súboru</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
Vyberte CSV súbor s URL adresami zdrojov na import
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<div className="flex justify-center px-6 py-10 border-2 border-gray-300 border-dashed rounded-md">
|
||||
<div className="text-center">
|
||||
<DocumentArrowUpIcon className="mx-auto h-12 w-12 text-gray-400" />
|
||||
<div className="mt-4">
|
||||
<label htmlFor="file-upload" className="cursor-pointer">
|
||||
<span className="mt-2 block text-sm font-semibold text-gray-900">
|
||||
Nahrať súbor
|
||||
</span>
|
||||
<input
|
||||
id="file-upload"
|
||||
name="file-upload"
|
||||
type="file"
|
||||
accept=".csv,.txt"
|
||||
className="sr-only"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
</label>
|
||||
<p className="text-sm text-gray-500">
|
||||
CSV alebo TXT súbor až do 10MB
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{file && (
|
||||
<div className="mt-4">
|
||||
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-md">
|
||||
<div className="flex items-center">
|
||||
<DocumentArrowUpIcon className="h-5 w-5 text-gray-400 mr-2" />
|
||||
<span className="text-sm font-medium text-gray-900">{file.name}</span>
|
||||
<span className="ml-2 text-sm text-gray-500">
|
||||
({Math.round(file.size / 1024)} KB)
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setFile(null)}
|
||||
className="text-sm text-red-600 hover:text-red-800"
|
||||
>
|
||||
Odstrániť
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{file && (
|
||||
<div className="mt-6 flex justify-end">
|
||||
<button
|
||||
onClick={handleImport}
|
||||
disabled={importing}
|
||||
className="btn-primary"
|
||||
>
|
||||
{importing ? (
|
||||
<>
|
||||
<svg className="animate-spin -ml-1 mr-3 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Importuje sa...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CloudArrowUpIcon className="h-4 w-4 mr-2" />
|
||||
Spustiť import
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{results && (
|
||||
<div className="mt-8">
|
||||
<div className="card p-6">
|
||||
<div className="flex items-center mb-4">
|
||||
<CheckCircleIcon className="h-6 w-6 text-green-500 mr-3" />
|
||||
<h3 className="text-lg font-medium text-gray-900">Import dokončený</h3>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<div className="bg-green-50 p-4 rounded-lg">
|
||||
<div className="text-2xl font-bold text-green-600">{results.imported || 0}</div>
|
||||
<div className="text-sm text-green-700">Úspešne importované</div>
|
||||
</div>
|
||||
<div className="bg-yellow-50 p-4 rounded-lg">
|
||||
<div className="text-2xl font-bold text-yellow-600">{results.skipped || 0}</div>
|
||||
<div className="text-sm text-yellow-700">Preskočené (duplikáty)</div>
|
||||
</div>
|
||||
<div className="bg-red-50 p-4 rounded-lg">
|
||||
<div className="text-2xl font-bold text-red-600">{results.failed || 0}</div>
|
||||
<div className="text-sm text-red-700">Neúspešné</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{results.errors && results.errors.length > 0 && (
|
||||
<div className="mt-6">
|
||||
<h4 className="text-sm font-medium text-gray-900 mb-2">Chyby pri importe:</h4>
|
||||
<div className="bg-red-50 rounded-md p-3">
|
||||
<ul className="text-sm text-red-700 space-y-1">
|
||||
{results.errors.map((error: string, idx: number) => (
|
||||
<li key={idx}>• {error}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-8">
|
||||
<div className="card p-6">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">Inštrukcie</h3>
|
||||
<div className="prose prose-sm text-gray-500">
|
||||
<ul>
|
||||
<li>CSV súbor by mal obsahovať jeden URL na riadok</li>
|
||||
<li>Prvý riadok môže obsahovať hlavičku (bude preskočený)</li>
|
||||
<li>Podporované formáty: .csv, .txt</li>
|
||||
<li>Maximálna veľkosť súboru: 10MB</li>
|
||||
<li>Duplikátne URL adresy budú automaticky preskočené</li>
|
||||
<li>Neplatné URL adresy budú označené ako chyby</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user