Files
infohliadka/pages/admin/settings/general.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

421 lines
15 KiB
TypeScript

import { useState, useEffect } from 'react'
import type { NextPage } from 'next'
import Head from 'next/head'
import AdminLayout from '../../../components/AdminLayout'
import {
Cog6ToothIcon,
GlobeAltIcon,
ShieldCheckIcon,
ClockIcon,
DocumentTextIcon,
EnvelopeIcon,
BellIcon
} from '@heroicons/react/24/outline'
interface SystemSetting {
key: string
value: string
type: 'string' | 'number' | 'boolean' | 'json'
description: string
category: string
}
const GeneralSettings: NextPage = () => {
const [settings, setSettings] = useState<SystemSetting[]>([])
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
useEffect(() => {
fetchSettings()
}, [])
const fetchSettings = async () => {
try {
const response = await fetch('/api/admin/settings')
if (response.ok) {
const data = await response.json()
setSettings(data)
}
} catch (error) {
console.error('Failed to fetch settings:', error)
} finally {
setLoading(false)
}
}
const updateSetting = async (key: string, value: string) => {
setSaving(true)
try {
const response = await fetch('/api/admin/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key, value })
})
if (response.ok) {
setSettings(prev => prev.map(s => s.key === key ? { ...s, value } : s))
}
} catch (error) {
console.error('Failed to update setting:', error)
} finally {
setSaving(false)
}
}
const getSetting = (key: string) => settings.find(s => s.key === key)
const SettingField = ({ setting }: { setting: SystemSetting }) => {
const [value, setValue] = useState(setting.value)
const handleSave = () => {
if (value !== setting.value) {
updateSetting(setting.key, value)
}
}
if (setting.type === 'boolean') {
return (
<div className="flex items-center justify-between">
<div className="flex-1">
<label className="text-sm font-medium text-gray-700">{setting.description}</label>
</div>
<button
type="button"
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 ${
value === 'true' ? 'bg-primary-600' : 'bg-gray-200'
}`}
onClick={() => {
const newValue = value === 'true' ? 'false' : 'true'
setValue(newValue)
updateSetting(setting.key, newValue)
}}
>
<span
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
value === 'true' ? 'translate-x-5' : 'translate-x-0'
}`}
/>
</button>
</div>
)
}
return (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{setting.description}
</label>
<div className="flex space-x-2">
<input
type={setting.type === 'number' ? 'number' : 'text'}
value={value}
onChange={(e) => setValue(e.target.value)}
className="input flex-1"
/>
{value !== setting.value && (
<button
onClick={handleSave}
disabled={saving}
className="btn-primary px-3 py-2"
>
Uložiť
</button>
)}
</div>
</div>
)
}
if (loading) {
return (
<AdminLayout title="Všeobecné nastavenia">
<Head>
<title>Všeobecné nastavenia - Hliadka.sk Admin</title>
</Head>
<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>
</AdminLayout>
)
}
const generalSettings = settings.filter(s => s.category === 'general')
const apiSettings = settings.filter(s => s.category === 'api')
const emailSettings = settings.filter(s => s.category === 'email')
const moderationSettings = settings.filter(s => s.category === 'moderation')
return (
<AdminLayout title="Všeobecné nastavenia">
<Head>
<title>Všeobecné nastavenia - Hliadka.sk Admin</title>
</Head>
<div className="space-y-8">
{/* System Information */}
<div className="card p-6">
<div className="flex items-center mb-6">
<GlobeAltIcon className="h-6 w-6 text-primary-600 mr-3" />
<h2 className="text-xl font-semibold text-gray-900">Systémové informácie</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div className="stat-card">
<div className="text-sm font-medium text-gray-500">Verzia aplikácie</div>
<div className="text-lg font-semibold text-gray-900">v1.0.0</div>
</div>
<div className="stat-card">
<div className="text-sm font-medium text-gray-500">Verzia databázy</div>
<div className="text-lg font-semibold text-gray-900">PostgreSQL 14.2</div>
</div>
<div className="stat-card">
<div className="text-sm font-medium text-gray-500">Posledná aktualizácia</div>
<div className="text-lg font-semibold text-gray-900">6.9.2025</div>
</div>
</div>
</div>
{/* General Settings */}
<div className="card p-6">
<div className="flex items-center mb-6">
<Cog6ToothIcon className="h-6 w-6 text-primary-600 mr-3" />
<h2 className="text-xl font-semibold text-gray-900">Všeobecné nastavenia</h2>
</div>
<div className="space-y-6">
<SettingField setting={{
key: 'site_name',
value: getSetting('site_name')?.value || 'Hliadka.sk',
type: 'string',
description: 'Názov aplikácie',
category: 'general'
}} />
<SettingField setting={{
key: 'site_description',
value: getSetting('site_description')?.value || 'Platforma na monitorovanie problematických webových zdrojov',
type: 'string',
description: 'Popis aplikácie',
category: 'general'
}} />
<SettingField setting={{
key: 'admin_email',
value: getSetting('admin_email')?.value || 'admin@hliadka.sk',
type: 'string',
description: 'Administrátorský email',
category: 'general'
}} />
<SettingField setting={{
key: 'maintenance_mode',
value: getSetting('maintenance_mode')?.value || 'false',
type: 'boolean',
description: 'Režim údržby',
category: 'general'
}} />
<SettingField setting={{
key: 'registration_enabled',
value: getSetting('registration_enabled')?.value || 'false',
type: 'boolean',
description: 'Povoliť registráciu nových moderátorov',
category: 'general'
}} />
</div>
</div>
{/* API Settings */}
<div className="card p-6">
<div className="flex items-center mb-6">
<ShieldCheckIcon className="h-6 w-6 text-primary-600 mr-3" />
<h2 className="text-xl font-semibold text-gray-900">API nastavenia</h2>
</div>
<div className="space-y-6">
<SettingField setting={{
key: 'api_rate_limit_default',
value: getSetting('api_rate_limit_default')?.value || '1000',
type: 'number',
description: 'Predvolený rate limit pre API kľúče (požiadavky/hodinu)',
category: 'api'
}} />
<SettingField setting={{
key: 'api_cache_duration',
value: getSetting('api_cache_duration')?.value || '300',
type: 'number',
description: 'Doba cachovania API odpovedí (sekundy)',
category: 'api'
}} />
<SettingField setting={{
key: 'api_require_auth',
value: getSetting('api_require_auth')?.value || 'true',
type: 'boolean',
description: 'Vyžadovať autentifikáciu pre všetky API endpointy',
category: 'api'
}} />
<SettingField setting={{
key: 'api_cors_enabled',
value: getSetting('api_cors_enabled')?.value || 'true',
type: 'boolean',
description: 'Povoliť CORS pre API',
category: 'api'
}} />
</div>
</div>
{/* Email Settings */}
<div className="card p-6">
<div className="flex items-center mb-6">
<EnvelopeIcon className="h-6 w-6 text-primary-600 mr-3" />
<h2 className="text-xl font-semibold text-gray-900">Email nastavenia</h2>
</div>
<div className="space-y-6">
<SettingField setting={{
key: 'smtp_host',
value: getSetting('smtp_host')?.value || 'localhost',
type: 'string',
description: 'SMTP server',
category: 'email'
}} />
<SettingField setting={{
key: 'smtp_port',
value: getSetting('smtp_port')?.value || '587',
type: 'number',
description: 'SMTP port',
category: 'email'
}} />
<SettingField setting={{
key: 'smtp_from_email',
value: getSetting('smtp_from_email')?.value || 'noreply@hliadka.sk',
type: 'string',
description: 'Odosielateľský email',
category: 'email'
}} />
<SettingField setting={{
key: 'email_notifications_enabled',
value: getSetting('email_notifications_enabled')?.value || 'true',
type: 'boolean',
description: 'Povoliť email notifikácie',
category: 'email'
}} />
</div>
</div>
{/* Moderation Settings */}
<div className="card p-6">
<div className="flex items-center mb-6">
<DocumentTextIcon className="h-6 w-6 text-primary-600 mr-3" />
<h2 className="text-xl font-semibold text-gray-900">Nastavenia moderovania</h2>
</div>
<div className="space-y-6">
<SettingField setting={{
key: 'auto_assign_reports',
value: getSetting('auto_assign_reports')?.value || 'true',
type: 'boolean',
description: 'Automaticky priradiť hlásenia moderátorom',
category: 'moderation'
}} />
<SettingField setting={{
key: 'require_evidence_for_reports',
value: getSetting('require_evidence_for_reports')?.value || 'false',
type: 'boolean',
description: 'Vyžadovať dôkazy pre hlásenia',
category: 'moderation'
}} />
<SettingField setting={{
key: 'max_reports_per_ip',
value: getSetting('max_reports_per_ip')?.value || '10',
type: 'number',
description: 'Maximálny počet hlásení z jednej IP denne',
category: 'moderation'
}} />
<SettingField setting={{
key: 'high_risk_threshold',
value: getSetting('high_risk_threshold')?.value || '4',
type: 'number',
description: 'Prah pre označenie ako vysoké riziko (1-5)',
category: 'moderation'
}} />
</div>
</div>
{/* Notification Settings */}
<div className="card p-6">
<div className="flex items-center mb-6">
<BellIcon className="h-6 w-6 text-primary-600 mr-3" />
<h2 className="text-xl font-semibold text-gray-900">Nastavenia notifikácií</h2>
</div>
<div className="space-y-6">
<SettingField setting={{
key: 'notify_new_reports',
value: getSetting('notify_new_reports')?.value || 'true',
type: 'boolean',
description: 'Notifikovať o nových hláseniach',
category: 'notifications'
}} />
<SettingField setting={{
key: 'notify_high_risk_sources',
value: getSetting('notify_high_risk_sources')?.value || 'true',
type: 'boolean',
description: 'Notifikovať o zdrojoch s vysokým rizikom',
category: 'notifications'
}} />
<SettingField setting={{
key: 'notification_frequency',
value: getSetting('notification_frequency')?.value || 'realtime',
type: 'string',
description: 'Frekvencia notifikácií (realtime, hourly, daily)',
category: 'notifications'
}} />
</div>
</div>
{/* System Maintenance */}
<div className="card p-6">
<div className="flex items-center mb-6">
<ClockIcon className="h-6 w-6 text-primary-600 mr-3" />
<h2 className="text-xl font-semibold text-gray-900">Údržba systému</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<button className="btn-secondary p-4 text-left">
<div className="text-sm font-medium text-gray-900">Vyčistiť cache</div>
<div className="text-sm text-gray-500">Vymazať všetky cached dáta</div>
</button>
<button className="btn-secondary p-4 text-left">
<div className="text-sm font-medium text-gray-900">Optimalizovať databázu</div>
<div className="text-sm text-gray-500">Spustiť optimalizáciu databázy</div>
</button>
<button className="btn-secondary p-4 text-left">
<div className="text-sm font-medium text-gray-900">Vyčistiť logy</div>
<div className="text-sm text-gray-500">Vymazať staré log súbory</div>
</button>
<button className="btn-secondary p-4 text-left">
<div className="text-sm font-medium text-gray-900">Záloha databázy</div>
<div className="text-sm text-gray-500">Vytvoriť manuálnu zálohu</div>
</button>
</div>
</div>
</div>
</AdminLayout>
)
}
export default GeneralSettings