feat(admin): restructure settings into subpages and hub overview
All checks were successful
Staging Build / build (push) Successful in 3m3s
All checks were successful
Staging Build / build (push) Successful in 3m3s
This commit is contained in:
226
shop/app/admin/einstellungen/system/page.tsx
Normal file
226
shop/app/admin/einstellungen/system/page.tsx
Normal file
@@ -0,0 +1,226 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Download, Upload, Loader2, Sliders, ShieldAlert, FileArchive } from 'lucide-react';
|
||||
import { createClient } from '@/lib/supabase/client';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function SystemPage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [demoActive, setDemoActive] = useState(true);
|
||||
const [exporting, setExporting] = useState(false);
|
||||
const [importing, setImporting] = useState(false);
|
||||
const [statusMsg, setStatusMsg] = useState('');
|
||||
const [statusType, setStatusType] = useState<'success' | 'error' | 'info' | ''>('');
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
async function loadData() {
|
||||
try {
|
||||
const supabase = createClient();
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) {
|
||||
router.push('/auth/login');
|
||||
return;
|
||||
}
|
||||
const state = localStorage.getItem('demo_banner_disabled') !== 'true';
|
||||
setDemoActive(state);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
loadData();
|
||||
}, [router]);
|
||||
|
||||
const toggleDemo = (checked: boolean) => {
|
||||
setDemoActive(checked);
|
||||
localStorage.setItem('demo_banner_disabled', (!checked).toString());
|
||||
window.dispatchEvent(new Event('storage_demo_changed'));
|
||||
};
|
||||
|
||||
const handleExport = async () => {
|
||||
setExporting(true);
|
||||
setStatusMsg('Export läuft...');
|
||||
setStatusType('info');
|
||||
try {
|
||||
const res = await fetch('/api/admin/db-backup');
|
||||
if (!res.ok) throw new Error('Export fehlgeschlagen');
|
||||
|
||||
const blob = await res.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `webshop_backup_${new Date().toISOString().split('T')[0]}.zip`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
setStatusMsg('Export erfolgreich abgeschlossen.');
|
||||
setStatusType('success');
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
setStatusMsg(`Fehler beim Export: ${err.message}`);
|
||||
setStatusType('error');
|
||||
} finally {
|
||||
setExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleImport = async () => {
|
||||
if (!selectedFile) return;
|
||||
if (!confirm('ACHTUNG: Dies löscht und überschreibt alle aktuellen Datenbankinhalte in dieser Instanz. Möchten Sie wirklich fortfahren?')) {
|
||||
return;
|
||||
}
|
||||
setImporting(true);
|
||||
setStatusMsg('Import läuft...');
|
||||
setStatusType('info');
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', selectedFile);
|
||||
|
||||
const res = await fetch('/api/admin/db-backup', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || 'Import fehlgeschlagen');
|
||||
|
||||
setStatusMsg('Datenbank erfolgreich importiert!');
|
||||
setStatusType('success');
|
||||
setSelectedFile(null);
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
setStatusMsg(`Fehler beim Import: ${err.message}`);
|
||||
setStatusType('error');
|
||||
} finally {
|
||||
setImporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div className="p-8 text-white flex justify-center items-center"><Loader2 className="w-8 h-8 animate-spin text-primary" /></div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-5xl mx-auto space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-extrabold tracking-tight text-white flex items-center gap-3">
|
||||
<Sliders className="w-8 h-8 text-amber-400" />
|
||||
System & Backup
|
||||
</h1>
|
||||
<p className="text-slate-400 text-xs mt-1">
|
||||
Verwaltung von System-Bannern, Datenbank-Sicherungen und Wiederherstellungen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{statusMsg && (
|
||||
<div className={`p-3.5 rounded-xl text-xs font-medium border ${
|
||||
statusType === 'success' ? 'bg-emerald-500/10 border-emerald-500/20 text-emerald-400' :
|
||||
statusType === 'error' ? 'bg-destructive/10 border-destructive/20 text-destructive' :
|
||||
'bg-sky-500/10 border-sky-500/20 text-sky-400'
|
||||
}`}>
|
||||
{statusMsg}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
<div className="p-5 rounded-2xl bg-slate-900/60 border border-slate-800 flex flex-col justify-between space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-sky-500/10 border border-sky-500/20 text-sky-400 flex items-center justify-center">
|
||||
<Sliders className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-base text-white">System Banner</h3>
|
||||
<p className="text-xs text-slate-400 mt-1 leading-relaxed">
|
||||
Gelbe Demo-Warnmeldungen im gesamten Shop aktivieren oder stummschalten.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between pt-3 border-t border-slate-800/80">
|
||||
<span className="text-xs font-semibold text-slate-300">Banner aktiv</span>
|
||||
<Switch checked={demoActive} onCheckedChange={toggleDemo} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-5 rounded-2xl bg-slate-900/60 border border-slate-800 flex flex-col justify-between space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-blue-500/10 border border-blue-500/20 text-blue-400 flex items-center justify-center">
|
||||
<Download className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-base text-white">DB Backup</h3>
|
||||
<p className="text-xs text-slate-400 mt-1 leading-relaxed">
|
||||
Lädt alle Produkte, Firmen, Lizenzen & Einstellungen als ZIP-Archiv herunter.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleExport}
|
||||
disabled={exporting || importing}
|
||||
className="w-full bg-blue-600 hover:bg-blue-500 text-white rounded-xl h-10 text-xs font-bold"
|
||||
>
|
||||
{exporting ? (
|
||||
<><Loader2 className="w-4 h-4 mr-2 animate-spin" /> Export läuft...</>
|
||||
) : (
|
||||
<><Download className="w-4 h-4 mr-2" /> ZIP Export</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2 p-5 rounded-2xl bg-slate-900/60 border border-slate-800 space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="w-10 h-10 rounded-xl bg-amber-500/10 border border-amber-500/20 text-amber-400 flex items-center justify-center">
|
||||
<Upload className="w-5 h-5" />
|
||||
</div>
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider text-amber-400 bg-amber-500/10 px-2.5 py-1 rounded-full border border-amber-500/20 flex items-center gap-1">
|
||||
<ShieldAlert className="w-3 h-3" /> Überschreibt DB
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-base text-white">Daten Wiederherstellung</h3>
|
||||
<p className="text-xs text-slate-400 mt-1 leading-relaxed">
|
||||
Spielen Sie eine gesicherte ZIP-Sicherungsdatei ein. Warnung: Überschreibt aktuelle Datenbankinhalte.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid sm:grid-cols-2 gap-3 pt-2">
|
||||
<div className="relative border border-dashed border-slate-700/80 rounded-xl p-3 hover:bg-slate-800/50 transition cursor-pointer flex items-center justify-center text-center">
|
||||
<input
|
||||
type="file"
|
||||
accept=".zip"
|
||||
onChange={(e) => setSelectedFile(e.target.files?.[0] || null)}
|
||||
disabled={exporting || importing}
|
||||
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer disabled:cursor-not-allowed"
|
||||
/>
|
||||
<span className="text-xs font-medium text-slate-300 truncate px-2 flex items-center gap-1.5">
|
||||
<FileArchive className="w-4 h-4 text-amber-400 shrink-0" />
|
||||
{selectedFile ? selectedFile.name : 'ZIP-Datei auswählen'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleImport}
|
||||
disabled={!selectedFile || exporting || importing}
|
||||
variant="outline"
|
||||
className="w-full border-amber-500/30 text-amber-300 hover:bg-amber-500/10 rounded-xl h-10 text-xs font-bold"
|
||||
>
|
||||
{importing ? (
|
||||
<><Loader2 className="w-4 h-4 mr-2 animate-spin" /> Importiert...</>
|
||||
) : (
|
||||
<><Upload className="w-4 h-4 mr-2" /> ZIP Einspielen</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user