refactor(admin): optimize smtp settings with bento grid
This commit is contained in:
@@ -1,9 +1,13 @@
|
|||||||
"use client";
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
import { createClient } from '@/lib/supabase/client';
|
import { createClient } from '@/lib/supabase/client';
|
||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2, Mail, Server, ShieldCheck, Key, Send, CheckCircle2, AlertCircle } from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
|
||||||
interface Settings {
|
interface Settings {
|
||||||
host: string;
|
host: string;
|
||||||
@@ -13,39 +17,60 @@ interface Settings {
|
|||||||
pass: string;
|
pass: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const containerVariants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: { staggerChildren: 0.1 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const itemVariants = {
|
||||||
|
hidden: { opacity: 0, y: 15 },
|
||||||
|
visible: { opacity: 1, y: 0, transition: { type: 'spring' as const, stiffness: 80, damping: 15 } }
|
||||||
|
};
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const [settings, setSettings] = useState<Settings | null>(null);
|
const [settings, setSettings] = useState<Settings | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [message, setMessage] = useState('');
|
const [message, setMessage] = useState('');
|
||||||
|
const [msgType, setMsgType] = useState<'success' | 'error' | ''>('');
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [testing, setTesting] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
// Load current settings and verify access
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function checkAccessAndLoad() {
|
async function checkAccessAndLoad() {
|
||||||
|
try {
|
||||||
const supabase = createClient();
|
const supabase = createClient();
|
||||||
const { data: { user } } = await supabase.auth.getUser();
|
const { data: { user } } = await supabase.auth.getUser();
|
||||||
if (!user) {
|
if (!user) {
|
||||||
router.push('/auth/login');
|
router.push('/auth/login');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { data: userData } = await supabase
|
|
||||||
.from('users')
|
const [userRes, smtpRes] = await Promise.all([
|
||||||
.select('role')
|
supabase.from('users').select('role').eq('id', user.id).single(),
|
||||||
.eq('id', user.id)
|
fetch('/api/admin/smtp-settings').then(res => res.json())
|
||||||
.single();
|
]);
|
||||||
if (!userData || userData.role === 'verwaltung') {
|
|
||||||
|
if (!userRes.data || userRes.data.role === 'verwaltung') {
|
||||||
router.push('/admin');
|
router.push('/admin');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch('/api/admin/smtp-settings')
|
if (smtpRes.settings) {
|
||||||
.then((res) => res.json())
|
setSettings(smtpRes.settings as Settings);
|
||||||
.then((data) => {
|
} else {
|
||||||
if (data.settings) setSettings(data.settings as Settings);
|
setMessage('Fehler beim Laden der SMTP-Einstellungen.');
|
||||||
else setMessage('Failed to load settings');
|
setMsgType('error');
|
||||||
})
|
}
|
||||||
.catch(() => setMessage('Failed to load settings'))
|
} catch (err) {
|
||||||
.finally(() => setLoading(false));
|
setMessage('Fehler beim Laden der Einstellungen.');
|
||||||
|
setMsgType('error');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
checkAccessAndLoad();
|
checkAccessAndLoad();
|
||||||
}, [router]);
|
}, [router]);
|
||||||
@@ -62,23 +87,38 @@ export default function SettingsPage() {
|
|||||||
|
|
||||||
const handleSave = async (e: React.FormEvent) => {
|
const handleSave = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
setSaving(true);
|
||||||
setMessage('');
|
setMessage('');
|
||||||
|
try {
|
||||||
const res = await fetch('/api/admin/smtp-settings', {
|
const res = await fetch('/api/admin/smtp-settings', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(settings),
|
body: JSON.stringify(settings),
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (res.ok) setMessage('Settings saved');
|
if (res.ok) {
|
||||||
else setMessage(data.error || 'Error saving settings');
|
setMessage('SMTP-Einstellungen erfolgreich gespeichert.');
|
||||||
|
setMsgType('success');
|
||||||
|
} else {
|
||||||
|
setMessage(data.error || 'Fehler beim Speichern.');
|
||||||
|
setMsgType('error');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setMessage('Verbindungsfehler beim Speichern.');
|
||||||
|
setMsgType('error');
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTestEmail = async () => {
|
const handleTestEmail = async () => {
|
||||||
|
setTesting(true);
|
||||||
setMessage('');
|
setMessage('');
|
||||||
|
try {
|
||||||
const testPayload = {
|
const testPayload = {
|
||||||
to: settings?.user || '', // send to the configured user address
|
to: settings?.user || '',
|
||||||
subject: 'Test Email from Webshop Admin',
|
subject: 'Test-E-Mail aus dem CASPOS Webshop Admin',
|
||||||
text: 'This is a test email to verify SMTP configuration.',
|
text: 'Dies ist eine automatische Test-E-Mail zur Bestätigung der SMTP-Konfiguration.',
|
||||||
};
|
};
|
||||||
const res = await fetch('/api/admin/send-test-email', {
|
const res = await fetch('/api/admin/send-test-email', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -86,86 +126,156 @@ export default function SettingsPage() {
|
|||||||
body: JSON.stringify(testPayload),
|
body: JSON.stringify(testPayload),
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (res.ok) setMessage('Test email sent successfully');
|
if (res.ok) {
|
||||||
else setMessage(data.error || 'Failed to send test email');
|
setMessage('Test-E-Mail erfolgreich versendet.');
|
||||||
|
setMsgType('success');
|
||||||
|
} else {
|
||||||
|
setMessage(data.error || 'Versand der Test-E-Mail fehlgeschlagen.');
|
||||||
|
setMsgType('error');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setMessage('Fehler beim Senden der Test-E-Mail.');
|
||||||
|
setMsgType('error');
|
||||||
|
} finally {
|
||||||
|
setTesting(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>;
|
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 (
|
return (
|
||||||
<div className="max-w-2xl mx-auto p-8 bg-black/30 backdrop-blur-xl rounded-lg text-white">
|
<div className="p-6 max-w-5xl mx-auto text-slate-900 dark:text-white space-y-6">
|
||||||
<h1 className="text-2xl font-bold mb-6">SMTP‑Einstellungen</h1>
|
{/* Header */}
|
||||||
{message && <p className="mb-4 text-yellow-300">{message}</p>}
|
<motion.div initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} className="space-y-1">
|
||||||
<form onSubmit={handleSave} className="space-y-4">
|
<h1 className="text-3xl font-extrabold tracking-tight flex items-center gap-3">
|
||||||
<label className="flex flex-col">
|
<Mail className="w-8 h-8 text-sky-400" />
|
||||||
Host
|
SMTP-Einstellungen
|
||||||
<input
|
</h1>
|
||||||
|
<p className="text-slate-400 text-xs">
|
||||||
|
Konfigurieren Sie den E-Mail-Server für transaktionale Benachrichtigungen und Bestellbestätigungen.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Dynamic Status Message */}
|
||||||
|
{message && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, height: 0 }}
|
||||||
|
animate={{ opacity: 1, height: 'auto' }}
|
||||||
|
className={`p-3.5 rounded-xl text-xs font-semibold border flex items-center gap-2 ${
|
||||||
|
msgType === 'success'
|
||||||
|
? 'bg-emerald-500/10 border-emerald-500/20 text-emerald-400'
|
||||||
|
: 'bg-destructive/10 border-destructive/20 text-destructive'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{msgType === 'success' ? <CheckCircle2 className="w-4 h-4 shrink-0" /> : <AlertCircle className="w-4 h-4 shrink-0" />}
|
||||||
|
{message}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Bento Grid layout for SMTP */}
|
||||||
|
<form onSubmit={handleSave}>
|
||||||
|
<motion.div variants={containerVariants} initial="hidden" animate="visible" className="grid grid-cols-1 md:grid-cols-3 gap-5">
|
||||||
|
{/* Bento Box 1: Server Connection Details (2 Spalten) */}
|
||||||
|
<motion.div variants={itemVariants} className="md:col-span-2 p-6 rounded-2xl bg-slate-900/60 border border-slate-800 backdrop-blur-md space-y-4 shadow-md">
|
||||||
|
<div className="flex items-center gap-2 text-sky-400 font-bold text-sm border-b border-slate-800 pb-3">
|
||||||
|
<Server className="w-4 h-4" />
|
||||||
|
<span>Server & Serververbindung</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||||
|
<div className="sm:col-span-2 space-y-1.5">
|
||||||
|
<Label htmlFor="host" className="text-xs font-bold text-slate-300">SMTP Host</Label>
|
||||||
|
<Input
|
||||||
|
id="host"
|
||||||
name="host"
|
name="host"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
value={settings?.host ?? ''}
|
value={settings?.host ?? ''}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="mt-1 p-2 rounded bg-black/20 border border-white/20"
|
placeholder="smtp.beispiel.de"
|
||||||
|
className="bg-slate-950/80 border-slate-800 text-white rounded-xl font-mono text-sm"
|
||||||
/>
|
/>
|
||||||
</label>
|
</div>
|
||||||
<label className="flex flex-col">
|
|
||||||
Port
|
<div className="space-y-1.5">
|
||||||
<input
|
<Label htmlFor="port" className="text-xs font-bold text-slate-300">Port</Label>
|
||||||
|
<Input
|
||||||
|
id="port"
|
||||||
name="port"
|
name="port"
|
||||||
type="number"
|
type="number"
|
||||||
required
|
required
|
||||||
value={settings?.port ?? ''}
|
value={settings?.port ?? ''}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="mt-1 p-2 rounded bg-black/20 border border-white/20"
|
placeholder="587"
|
||||||
|
className="bg-slate-950/80 border-slate-800 text-white rounded-xl font-mono text-sm"
|
||||||
/>
|
/>
|
||||||
</label>
|
</div>
|
||||||
<label className="inline-flex items-center space-x-2">
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-2">
|
||||||
|
<label className="inline-flex items-center gap-2 cursor-pointer p-3 rounded-xl bg-slate-950/50 border border-slate-800/80 hover:bg-slate-800/40 transition">
|
||||||
<input
|
<input
|
||||||
name="secure"
|
name="secure"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={settings?.secure ?? false}
|
checked={settings?.secure ?? false}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="rounded"
|
className="w-4 h-4 rounded text-sky-500 bg-slate-900 border-slate-700"
|
||||||
/>
|
/>
|
||||||
<span>SSL / TLS (secure)</span>
|
<span className="text-xs font-semibold text-slate-200">SSL / TLS Verschlüsselung aktivieren</span>
|
||||||
</label>
|
</label>
|
||||||
<label className="flex flex-col">
|
</div>
|
||||||
Benutzer (SMTP‑User)
|
</motion.div>
|
||||||
<input
|
|
||||||
|
{/* Bento Box 2: Auth Credentials (1 Spalte) */}
|
||||||
|
<motion.div variants={itemVariants} className="p-6 rounded-2xl bg-slate-900/60 border border-slate-800 backdrop-blur-md space-y-4 shadow-md flex flex-col justify-between">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-2 text-sky-400 font-bold text-sm border-b border-slate-800 pb-3">
|
||||||
|
<Key className="w-4 h-4" />
|
||||||
|
<span>Zugangsdaten</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label htmlFor="user" className="text-xs font-bold text-slate-300">SMTP Benutzer</Label>
|
||||||
|
<Input
|
||||||
|
id="user"
|
||||||
name="user"
|
name="user"
|
||||||
type="email"
|
type="email"
|
||||||
required
|
required
|
||||||
value={settings?.user ?? ''}
|
value={settings?.user ?? ''}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="mt-1 p-2 rounded bg-black/20 border border-white/20"
|
placeholder="absender@beispiel.de"
|
||||||
|
className="bg-slate-950/80 border-slate-800 text-white rounded-xl font-mono text-sm"
|
||||||
/>
|
/>
|
||||||
</label>
|
</div>
|
||||||
<label className="flex flex-col">
|
|
||||||
Passwort
|
<div className="space-y-1.5">
|
||||||
<input
|
<Label htmlFor="pass" className="text-xs font-bold text-slate-300">Passwort</Label>
|
||||||
|
<Input
|
||||||
|
id="pass"
|
||||||
name="pass"
|
name="pass"
|
||||||
type="password"
|
type="password"
|
||||||
required
|
required
|
||||||
value={settings?.pass ?? ''}
|
value={settings?.pass ?? ''}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="mt-1 p-2 rounded bg-black/20 border border-white/20"
|
placeholder="••••••••••••"
|
||||||
|
className="bg-slate-950/80 border-slate-800 text-white rounded-xl font-mono text-sm"
|
||||||
/>
|
/>
|
||||||
</label>
|
|
||||||
<div className="flex space-x-4 mt-4">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="px-4 py-2 bg-primary rounded hover:bg-primary/80 transition"
|
|
||||||
>
|
|
||||||
Speichern
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleTestEmail}
|
|
||||||
className="px-4 py-2 bg-green-600 rounded hover:bg-green-500 transition"
|
|
||||||
>
|
|
||||||
Test‑Mail senden
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submit & Test Buttons */}
|
||||||
|
<div className="flex flex-col gap-2 pt-4 border-t border-slate-800">
|
||||||
|
<Button type="submit" disabled={saving || testing} className="w-full bg-sky-600 hover:bg-sky-500 text-white rounded-xl h-10 text-xs font-bold">
|
||||||
|
{saving ? <><Loader2 className="w-4 h-4 mr-2 animate-spin" /> Speichert...</> : 'Konfiguration speichern'}
|
||||||
|
</Button>
|
||||||
|
<Button type="button" onClick={handleTestEmail} disabled={saving || testing} variant="outline" className="w-full border-slate-700 text-slate-300 hover:text-white rounded-xl h-10 text-xs font-bold">
|
||||||
|
{testing ? <><Loader2 className="w-4 h-4 mr-2 animate-spin" /> Sendet...</> : <><Send className="w-3.5 h-3.5 mr-2 text-emerald-400" /> Test-Mail senden</>}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user