Apply modern SaaS design to all admin pages

Modernize all admin interface pages with consistent design language:
- Add hero sections with gradient backgrounds and blur effects
- Implement modern card designs with hover animations
- Use gradient buttons with shadow effects
- Add emoji icons in colored containers
- Apply consistent color themes per page
- Enhance user experience with smooth transitions

Pages updated:
- /admin/devices (purple theme)
- /admin/mqtt (cyan/blue theme)
- /admin/setup (emerald theme)
- /admin/users (violet theme)
- /admin/settings (indigo theme)
- /admin/emails (pink/rose theme)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-29 23:31:25 +00:00
parent 5f637817ce
commit a39b53151e
6 changed files with 392 additions and 243 deletions

View File

@@ -190,28 +190,35 @@ export default function DevicesPage() {
} }
return ( return (
<div className="space-y-6"> <div className="space-y-8">
{/* Header */} {/* Hero Section with Gradient */}
<div className="flex justify-between items-center"> <div className="relative overflow-hidden rounded-2xl bg-gradient-to-br from-purple-600 via-purple-700 to-indigo-800 p-8 shadow-xl">
<div className="absolute top-0 right-0 -mt-4 -mr-4 h-40 w-40 rounded-full bg-white/10 blur-3xl"></div>
<div className="absolute bottom-0 left-0 -mb-4 -ml-4 h-40 w-40 rounded-full bg-white/10 blur-3xl"></div>
<div className="relative flex justify-between items-center">
<div> <div>
<h2 className="text-3xl font-bold text-gray-900">Device Management</h2> <h2 className="text-4xl font-bold text-white mb-2">Device Management</h2>
{!isAdmin && ( <p className="text-purple-100 text-lg">
<p className="text-sm text-gray-600 mt-1">Read-only view</p> {!isAdmin ? "Read-only view" : "Verwalte deine Tracking-Geräte"}
)} </p>
</div> </div>
{isAdmin && ( {isAdmin && (
<button <button
onClick={() => setShowAddModal(true)} onClick={() => setShowAddModal(true)}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 font-medium" className="px-6 py-3 bg-white text-purple-700 rounded-xl hover:bg-purple-50 font-semibold shadow-lg hover:shadow-xl transition-all transform hover:-translate-y-0.5"
> >
+ Add Device + Add Device
</button> </button>
)} )}
</div> </div>
</div>
{error && ( {error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded"> <div className="bg-gradient-to-r from-red-50 to-red-100 border border-red-200 text-red-700 px-6 py-4 rounded-xl shadow-md">
{error} <div className="flex items-center gap-2">
<span className="text-xl"></span>
<span className="font-semibold">{error}</span>
</div>
</div> </div>
)} )}
@@ -228,32 +235,33 @@ export default function DevicesPage() {
return ( return (
<div <div
key={device.id} key={device.id}
className="bg-white rounded-lg shadow-md p-6 space-y-4 border-2" className="group relative overflow-hidden bg-white rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-1 p-6 space-y-4"
style={{ borderColor: device.isActive ? device.color : "#ccc" }}
> >
<div className="absolute inset-0 bg-gradient-to-br from-blue-50 to-indigo-50 opacity-0 group-hover:opacity-100 transition-opacity"></div>
<div className="relative">
<div className="flex justify-between items-start"> <div className="flex justify-between items-start">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div <div
className="w-12 h-12 rounded-full border-2 border-white shadow-md flex items-center justify-center" className="w-14 h-14 rounded-xl shadow-lg flex items-center justify-center ring-2 ring-white transform group-hover:scale-110 transition-transform"
style={{ backgroundColor: device.color }} style={{ backgroundColor: device.color }}
> >
<span className="text-white text-2xl">📱</span> <span className="text-white text-2xl">📱</span>
</div> </div>
<div> <div>
<h3 className="text-lg font-semibold text-gray-900"> <h3 className="text-lg font-bold text-gray-900">
{device.name} {device.name}
</h3> </h3>
<p className="text-sm text-gray-500">ID: {device.id}</p> <p className="text-xs text-gray-500 font-mono bg-gray-100 px-2 py-0.5 rounded">ID: {device.id}</p>
</div> </div>
</div> </div>
<span <span
className={`px-3 py-1 rounded-full text-xs font-medium ${ className={`px-3 py-1.5 rounded-lg text-xs font-bold shadow-md ${
isRecent isRecent
? "bg-green-100 text-green-800" ? "bg-gradient-to-r from-green-500 to-emerald-600 text-white"
: "bg-gray-100 text-gray-800" : "bg-gradient-to-r from-gray-400 to-gray-500 text-white"
}`} }`}
> >
{isRecent ? "Online" : "Offline"} {isRecent ? "🟢 Online" : "Offline"}
</span> </span>
</div> </div>
@@ -262,7 +270,7 @@ export default function DevicesPage() {
)} )}
{device.latestLocation && ( {device.latestLocation && (
<div className="border-t border-gray-200 pt-4 space-y-2"> <div className="bg-gradient-to-br from-gray-50 to-slate-50 rounded-xl p-4 space-y-2 border border-gray-200">
<div className="flex items-center justify-between text-sm"> <div className="flex items-center justify-between text-sm">
<span className="text-gray-600 flex items-center gap-2"> <span className="text-gray-600 flex items-center gap-2">
<span className="text-lg">🕒</span> <span className="text-lg">🕒</span>
@@ -322,13 +330,13 @@ export default function DevicesPage() {
<div className="flex gap-2 pt-2"> <div className="flex gap-2 pt-2">
<button <button
onClick={() => openEditModal(device)} onClick={() => openEditModal(device)}
className="flex-1 px-3 py-2 bg-blue-100 text-blue-700 rounded-md hover:bg-blue-200 text-sm font-medium" className="flex-1 px-4 py-2.5 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-lg hover:from-blue-700 hover:to-blue-800 text-sm font-semibold shadow-md hover:shadow-lg transition-all"
> >
Edit Edit
</button> </button>
<button <button
onClick={() => openDeleteModal(device)} onClick={() => openDeleteModal(device)}
className="flex-1 px-3 py-2 bg-red-100 text-red-700 rounded-md hover:bg-red-200 text-sm font-medium" className="flex-1 px-4 py-2.5 bg-gradient-to-r from-red-600 to-red-700 text-white rounded-lg hover:from-red-700 hover:to-red-800 text-sm font-semibold shadow-md hover:shadow-lg transition-all"
> >
Delete Delete
</button> </button>
@@ -340,8 +348,10 @@ export default function DevicesPage() {
</div> </div>
{devices.length === 0 && ( {devices.length === 0 && (
<div className="bg-white rounded-lg shadow p-8 text-center text-gray-500"> <div className="relative overflow-hidden bg-gradient-to-br from-gray-50 to-slate-50 rounded-2xl shadow-lg p-12 text-center border border-gray-200">
No devices found. Add a device to get started. <div className="absolute top-0 right-0 text-9xl opacity-5">📱</div>
<p className="text-xl font-semibold text-gray-600 mb-2">Keine Devices gefunden</p>
<p className="text-gray-500">Füge dein erstes Device hinzu, um zu starten.</p>
</div> </div>
)} )}

View File

@@ -48,28 +48,42 @@ export default function EmailsPage() {
const previewUrl = `/api/admin/emails/preview?template=${selectedTemplate}`; const previewUrl = `/api/admin/emails/preview?template=${selectedTemplate}`;
return ( return (
<div> <div className="space-y-8">
<h2 className="text-3xl font-bold text-gray-900 mb-6">Email Templates</h2> {/* Hero Section with Gradient */}
<div className="relative overflow-hidden rounded-2xl bg-gradient-to-br from-pink-600 via-rose-700 to-red-800 p-8 shadow-xl">
<div className="absolute top-0 right-0 -mt-4 -mr-4 h-40 w-40 rounded-full bg-white/10 blur-3xl"></div>
<div className="absolute bottom-0 left-0 -mb-4 -ml-4 h-40 w-40 rounded-full bg-white/10 blur-3xl"></div>
<div className="relative">
<h2 className="text-4xl font-bold text-white mb-2">Email Templates</h2>
<p className="text-pink-100 text-lg">Verwalte und teste E-Mail-Vorlagen</p>
</div>
</div>
{/* Status Message */} {/* Status Message */}
{message && ( {message && (
<div <div
className={`mb-6 p-4 rounded ${ className={`p-5 rounded-xl shadow-lg flex items-center gap-3 ${
message.type === 'success' message.type === 'success'
? 'bg-green-100 text-green-800' ? 'bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-200 text-green-800'
: 'bg-red-100 text-red-800' : 'bg-gradient-to-r from-red-50 to-orange-50 border-2 border-red-200 text-red-800'
}`} }`}
> >
{message.text} <span className="text-2xl">{message.type === 'success' ? '✓' : '⚠️'}</span>
<span className="font-semibold">{message.text}</span>
</div> </div>
)} )}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Template List */} {/* Template List */}
<div className="lg:col-span-1"> <div className="lg:col-span-1 space-y-4">
<div className="bg-white rounded-lg shadow"> <div className="bg-white rounded-2xl shadow-lg overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200"> <div className="bg-gradient-to-r from-pink-50 to-rose-50 px-6 py-4 border-b border-pink-100">
<h3 className="text-lg font-semibold text-gray-900">Templates</h3> <div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-pink-600 to-rose-600 flex items-center justify-center text-white text-xl">
📧
</div>
<h3 className="text-lg font-bold text-gray-900">Templates</h3>
</div>
</div> </div>
<div className="p-4"> <div className="p-4">
<div className="space-y-2"> <div className="space-y-2">
@@ -77,16 +91,16 @@ export default function EmailsPage() {
<button <button
key={template.name} key={template.name}
onClick={() => setSelectedTemplate(template.name)} onClick={() => setSelectedTemplate(template.name)}
className={`w-full text-left px-4 py-3 rounded-md transition-colors ${ className={`w-full text-left px-4 py-3 rounded-xl transition-all transform ${
selectedTemplate === template.name selectedTemplate === template.name
? 'bg-blue-600 text-white' ? 'bg-gradient-to-r from-pink-600 to-rose-600 text-white shadow-lg scale-105'
: 'bg-gray-50 hover:bg-gray-100 text-gray-900' : 'bg-gradient-to-br from-gray-50 to-slate-50 hover:from-pink-50 hover:to-rose-50 text-gray-900 hover:shadow-md'
}`} }`}
> >
<p className="font-medium">{template.subject}</p> <p className="font-bold">{template.subject}</p>
<p className={`text-sm mt-1 ${ <p className={`text-sm mt-1 ${
selectedTemplate === template.name selectedTemplate === template.name
? 'text-blue-100' ? 'text-pink-100'
: 'text-gray-600' : 'text-gray-600'
}`}> }`}>
{template.description} {template.description}
@@ -100,25 +114,33 @@ export default function EmailsPage() {
{/* Send Test Button */} {/* Send Test Button */}
<button <button
onClick={() => setShowSendModal(true)} onClick={() => setShowSendModal(true)}
className="w-full mt-4 px-4 py-3 bg-green-600 text-white rounded-md hover:bg-green-700 font-medium" className="w-full px-6 py-4 bg-gradient-to-r from-green-500 to-emerald-600 text-white rounded-xl hover:from-green-600 hover:to-emerald-700 font-bold shadow-lg hover:shadow-xl transition-all flex items-center justify-center gap-2"
> >
<span className="text-xl">📨</span>
Send Test Email Send Test Email
</button> </button>
</div> </div>
{/* Preview */} {/* Preview */}
<div className="lg:col-span-2"> <div className="lg:col-span-2">
<div className="bg-white rounded-lg shadow"> <div className="bg-white rounded-2xl shadow-lg overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200 flex justify-between items-center"> <div className="bg-gradient-to-r from-blue-50 to-indigo-50 px-6 py-4 border-b border-blue-100">
<h3 className="text-lg font-semibold text-gray-900">Preview</h3> <div className="flex justify-between items-center">
<span className="text-sm text-gray-600"> <div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-blue-600 to-indigo-600 flex items-center justify-center text-white text-xl">
👁
</div>
<h3 className="text-lg font-bold text-gray-900">Preview</h3>
</div>
<span className="text-sm font-semibold text-gray-700 bg-white px-3 py-1.5 rounded-lg shadow-sm">
{EMAIL_TEMPLATES.find(t => t.name === selectedTemplate)?.subject} {EMAIL_TEMPLATES.find(t => t.name === selectedTemplate)?.subject}
</span> </span>
</div> </div>
</div>
<div className="p-4"> <div className="p-4">
<iframe <iframe
src={previewUrl} src={previewUrl}
className="w-full border border-gray-300 rounded" className="w-full border-2 border-gray-200 rounded-xl shadow-inner"
style={{ height: '600px' }} style={{ height: '600px' }}
title="Email Preview" title="Email Preview"
/> />

View File

@@ -341,41 +341,68 @@ export default function MqttPage() {
); );
return ( return (
<div className="p-8"> <div className="space-y-8">
<div className="flex justify-between items-center mb-6"> {/* Hero Section with Gradient */}
<h1 className="text-3xl font-bold">MQTT Provisioning</h1> <div className="relative overflow-hidden rounded-2xl bg-gradient-to-br from-cyan-600 via-blue-700 to-indigo-800 p-8 shadow-xl">
<div className="flex gap-4 items-center"> <div className="absolute top-0 right-0 -mt-4 -mr-4 h-40 w-40 rounded-full bg-white/10 blur-3xl"></div>
<div className="absolute bottom-0 left-0 -mb-4 -ml-4 h-40 w-40 rounded-full bg-white/10 blur-3xl"></div>
<div className="relative">
<div className="flex justify-between items-center mb-4">
<div>
<h2 className="text-4xl font-bold text-white mb-2">MQTT Provisioning</h2>
<p className="text-cyan-100 text-lg">Verwalte MQTT-Zugangsdaten und Berechtigungen</p>
</div>
<div className="flex gap-3">
{syncStatus && syncStatus.pending_changes > 0 && ( {syncStatus && syncStatus.pending_changes > 0 && (
<span className="px-3 py-1 bg-yellow-100 text-yellow-800 rounded-md text-sm"> <span className="px-4 py-2 bg-yellow-400 text-yellow-900 rounded-lg text-sm font-bold shadow-lg">
{syncStatus.pending_changes} ausstehende Änderungen {syncStatus.pending_changes} ausstehende Änderungen
</span> </span>
)} )}
<button <button
onClick={handleSync} onClick={handleSync}
disabled={syncing || !syncStatus || syncStatus.pending_changes === 0} disabled={syncing || !syncStatus || syncStatus.pending_changes === 0}
className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed" className="px-5 py-2.5 bg-gradient-to-r from-green-500 to-emerald-600 text-white rounded-xl hover:from-green-600 hover:to-emerald-700 disabled:from-gray-400 disabled:to-gray-500 font-semibold shadow-lg hover:shadow-xl transition-all"
> >
{syncing ? "Synchronisiere..." : "MQTT Sync"} {syncing ? "Synchronisiere..." : "🔄 MQTT Sync"}
</button> </button>
<button <button
onClick={() => setShowAddModal(true)} onClick={() => setShowAddModal(true)}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700" className="px-5 py-2.5 bg-white text-cyan-700 rounded-xl hover:bg-cyan-50 font-semibold shadow-lg hover:shadow-xl transition-all"
> >
Device Provisionieren + Device Provisionieren
</button> </button>
</div> </div>
</div> </div>
</div>
</div>
{syncStatus && ( {syncStatus && (
<div className="mb-6 p-4 bg-gray-100 rounded-md"> <div className="bg-white rounded-2xl shadow-lg overflow-hidden">
<h3 className="font-semibold mb-2">Sync Status</h3> <div className="bg-gradient-to-r from-slate-50 to-gray-50 px-6 py-4 border-b border-gray-200">
<div className="text-sm"> <div className="flex items-center gap-3">
<p>Status: <span className={syncStatus.last_sync_status === 'success' ? 'text-green-600' : 'text-red-600'}>{syncStatus.last_sync_status}</span></p> <div className="w-10 h-10 rounded-lg bg-gradient-to-br from-slate-600 to-slate-700 flex items-center justify-center text-white text-xl">
🔄
</div>
<h3 className="text-xl font-bold text-gray-900">Sync Status</h3>
</div>
</div>
<div className="p-6">
<div className="grid grid-cols-2 gap-4">
<div className="bg-gradient-to-br from-blue-50 to-indigo-50 p-4 rounded-xl border border-blue-100">
<p className="text-sm font-semibold text-blue-700 uppercase tracking-wide mb-1">Status</p>
<p className={`text-2xl font-bold ${syncStatus.last_sync_status === 'success' ? 'text-green-600' : 'text-red-600'}`}>
{syncStatus.last_sync_status === 'success' ? '✓ Erfolgreich' : '✗ Fehler'}
</p>
</div>
{syncStatus.last_sync_at && ( {syncStatus.last_sync_at && (
<p>Letzter Sync: {new Date(syncStatus.last_sync_at).toLocaleString('de-DE')}</p> <div className="bg-gradient-to-br from-purple-50 to-violet-50 p-4 rounded-xl border border-purple-100">
<p className="text-sm font-semibold text-purple-700 uppercase tracking-wide mb-1">Letzter Sync</p>
<p className="text-lg font-bold text-purple-900">{new Date(syncStatus.last_sync_at).toLocaleString('de-DE')}</p>
</div>
)} )}
</div> </div>
</div> </div>
</div>
)} )}
<div className="space-y-6"> <div className="space-y-6">
@@ -383,26 +410,48 @@ export default function MqttPage() {
const deviceRules = aclRules[cred.device_id] || []; const deviceRules = aclRules[cred.device_id] || [];
return ( return (
<div key={cred.id} className="border rounded-lg p-6 bg-white shadow-sm"> <div key={cred.id} className="bg-white rounded-2xl shadow-lg overflow-hidden hover:shadow-2xl transition-shadow">
<div className="flex justify-between items-start mb-4"> <div className="bg-gradient-to-r from-blue-50 to-indigo-50 px-6 py-5 border-b border-blue-100">
<div className="flex justify-between items-start">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-blue-600 to-indigo-600 flex items-center justify-center text-white text-2xl shadow-lg">
📱
</div>
<div> <div>
<h3 className="text-xl font-semibold">{cred.device_name}</h3> <h3 className="text-xl font-bold text-gray-900">{cred.device_name}</h3>
<p className="text-sm text-gray-500">Device ID: {cred.device_id}</p> <p className="text-sm text-gray-600">Device ID: <code className="bg-white px-2 py-0.5 rounded font-mono text-xs">{cred.device_id}</code></p>
<p className="text-sm text-gray-600 mt-1">Username: <code className="bg-gray-100 px-2 py-1 rounded">{cred.mqtt_username}</code></p> </div>
<p className="text-xs text-gray-500 mt-1">Erstellt: {new Date(cred.created_at).toLocaleString('de-DE')}</p>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
onClick={() => handleToggleEnabled(cred.device_id, !cred.enabled)} onClick={() => handleToggleEnabled(cred.device_id, !cred.enabled)}
className={`px-3 py-1 rounded text-sm ${cred.enabled ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`} className={`px-4 py-2 rounded-lg text-sm font-bold shadow-md ${cred.enabled ? 'bg-gradient-to-r from-green-500 to-emerald-600 text-white' : 'bg-gradient-to-r from-red-500 to-red-600 text-white'}`}
> >
{cred.enabled ? 'Aktiviert' : 'Deaktiviert'} {cred.enabled ? 'Aktiviert' : 'Deaktiviert'}
</button> </button>
</div>
</div>
</div>
<div className="p-6">
<div className="bg-gradient-to-br from-gray-50 to-slate-50 rounded-xl p-4 mb-4 border border-gray-200">
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-1">Username</p>
<code className="text-sm font-mono text-gray-900 bg-white px-3 py-1.5 rounded-lg border border-gray-200 inline-block">{cred.mqtt_username}</code>
</div>
<div>
<p className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-1">Erstellt</p>
<p className="text-sm text-gray-700">{new Date(cred.created_at).toLocaleString('de-DE')}</p>
</div>
</div>
</div>
<div className="flex gap-2 mb-4">
<button <button
onClick={() => handleRegeneratePassword(cred.device_id)} onClick={() => handleRegeneratePassword(cred.device_id)}
className="px-3 py-1 bg-yellow-100 text-yellow-800 rounded text-sm hover:bg-yellow-200" className="flex-1 px-4 py-2.5 bg-gradient-to-r from-yellow-500 to-orange-500 text-white rounded-lg hover:from-yellow-600 hover:to-orange-600 text-sm font-semibold shadow-md hover:shadow-lg transition-all"
> >
Passwort Reset 🔑 Passwort Reset
</button> </button>
<button <button
onClick={() => { onClick={() => {
@@ -414,60 +463,61 @@ export default function MqttPage() {
}); });
setShowAclModal(true); setShowAclModal(true);
}} }}
className="px-3 py-1 bg-blue-100 text-blue-800 rounded text-sm hover:bg-blue-200" className="flex-1 px-4 py-2.5 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-lg hover:from-blue-700 hover:to-blue-800 text-sm font-semibold shadow-md hover:shadow-lg transition-all"
> >
ACL Hinzufügen + ACL Hinzufügen
</button> </button>
<button <button
onClick={() => handleDeleteCredentials(cred.device_id)} onClick={() => handleDeleteCredentials(cred.device_id)}
className="px-3 py-1 bg-red-100 text-red-800 rounded text-sm hover:bg-red-200" className="px-4 py-2.5 bg-gradient-to-r from-red-600 to-red-700 text-white rounded-lg hover:from-red-700 hover:to-red-800 text-sm font-semibold shadow-md hover:shadow-lg transition-all"
> >
Löschen 🗑 Löschen
</button> </button>
</div> </div>
</div>
<div className="mt-4"> <div>
<h4 className="font-semibold mb-2 text-sm">ACL Regeln:</h4> <h4 className="font-bold mb-3 text-gray-900 flex items-center gap-2">
<span className="text-lg">🔐</span>
ACL Regeln
</h4>
{deviceRules.length > 0 ? ( {deviceRules.length > 0 ? (
<table className="w-full text-sm"> <div className="space-y-2">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-2 text-left">Topic Pattern</th>
<th className="px-4 py-2 text-left">Berechtigung</th>
<th className="px-4 py-2 text-left">Erstellt</th>
<th className="px-4 py-2"></th>
</tr>
</thead>
<tbody>
{deviceRules.map(rule => ( {deviceRules.map(rule => (
<tr key={rule.id} className="border-t"> <div key={rule.id} className="bg-gradient-to-br from-gray-50 to-slate-50 rounded-lg p-4 border border-gray-200 hover:border-blue-300 transition-colors">
<td className="px-4 py-2"><code className="bg-gray-100 px-2 py-1 rounded">{rule.topic_pattern}</code></td> <div className="flex items-center justify-between">
<td className="px-4 py-2"><span className="px-2 py-1 bg-blue-100 text-blue-800 rounded text-xs">{rule.permission}</span></td> <div className="flex-1">
<td className="px-4 py-2 text-gray-500">{new Date(rule.created_at).toLocaleString('de-DE')}</td> <div className="flex items-center gap-3 mb-2">
<td className="px-4 py-2"> <code className="text-sm font-mono bg-white px-3 py-1.5 rounded-lg border border-gray-200">{rule.topic_pattern}</code>
<span className="px-3 py-1 bg-gradient-to-r from-blue-500 to-indigo-600 text-white rounded-lg text-xs font-bold">{rule.permission}</span>
</div>
<p className="text-xs text-gray-500">Erstellt: {new Date(rule.created_at).toLocaleString('de-DE')}</p>
</div>
<button <button
onClick={() => handleDeleteAclRule(rule.id, cred.device_id)} onClick={() => handleDeleteAclRule(rule.id, cred.device_id)}
className="text-red-600 hover:text-red-800 text-xs" className="ml-4 px-3 py-2 bg-red-100 text-red-700 rounded-lg hover:bg-red-200 text-xs font-semibold"
> >
Löschen Löschen
</button> </button>
</td> </div>
</tr> </div>
))} ))}
</tbody> </div>
</table>
) : ( ) : (
<div className="bg-gradient-to-br from-gray-50 to-slate-50 rounded-lg p-6 text-center border border-gray-200">
<p className="text-sm text-gray-500">Keine ACL Regeln definiert</p> <p className="text-sm text-gray-500">Keine ACL Regeln definiert</p>
</div>
)} )}
</div> </div>
</div> </div>
</div>
); );
})} })}
{credentials.length === 0 && ( {credentials.length === 0 && (
<div className="text-center py-12 text-gray-500"> <div className="relative overflow-hidden bg-gradient-to-br from-gray-50 to-slate-50 rounded-2xl shadow-lg p-12 text-center border border-gray-200">
Noch keine Devices provisioniert <div className="absolute top-0 right-0 text-9xl opacity-5">🔐</div>
<p className="text-xl font-semibold text-gray-600 mb-2">Noch keine Devices provisioniert</p>
<p className="text-gray-500">Erstelle MQTT-Credentials für dein erstes Device.</p>
</div> </div>
)} )}
</div> </div>

View File

@@ -144,21 +144,29 @@ export default function SettingsPage() {
} }
return ( return (
<div> <div className="space-y-8">
<h2 className="text-3xl font-bold text-gray-900 mb-6">Settings</h2> {/* Hero Section with Gradient */}
<div className="relative overflow-hidden rounded-2xl bg-gradient-to-br from-indigo-600 via-blue-700 to-purple-800 p-8 shadow-xl">
<div className="absolute top-0 right-0 -mt-4 -mr-4 h-40 w-40 rounded-full bg-white/10 blur-3xl"></div>
<div className="absolute bottom-0 left-0 -mb-4 -ml-4 h-40 w-40 rounded-full bg-white/10 blur-3xl"></div>
<div className="relative">
<h2 className="text-4xl font-bold text-white mb-2">Settings</h2>
<p className="text-indigo-100 text-lg">Konfiguriere System-Einstellungen und Integrationen</p>
</div>
</div>
{/* Tab Navigation */} {/* Tab Navigation */}
<div className="border-b border-gray-200 mb-6"> <div className="bg-white rounded-2xl shadow-lg overflow-hidden">
<nav className="flex gap-4"> <nav className="flex gap-2 p-2 bg-gradient-to-r from-gray-50 to-slate-50">
<button <button
onClick={() => setActiveTab('smtp')} onClick={() => setActiveTab('smtp')}
className={`px-4 py-2 border-b-2 font-medium ${ className={`px-6 py-3 rounded-xl font-semibold transition-all ${
activeTab === 'smtp' activeTab === 'smtp'
? 'border-blue-600 text-blue-600' ? 'bg-gradient-to-r from-blue-600 to-indigo-600 text-white shadow-lg'
: 'border-transparent text-gray-600 hover:text-gray-900' : 'text-gray-600 hover:bg-white hover:text-gray-900'
}`} }`}
> >
SMTP Settings 📧 SMTP Settings
</button> </button>
</nav> </nav>
</div> </div>
@@ -166,25 +174,32 @@ export default function SettingsPage() {
{/* Status Message */} {/* Status Message */}
{message && ( {message && (
<div <div
className={`mb-6 p-4 rounded ${ className={`p-5 rounded-xl shadow-lg flex items-center gap-3 ${
message.type === 'success' message.type === 'success'
? 'bg-green-100 text-green-800' ? 'bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-200 text-green-800'
: 'bg-red-100 text-red-800' : 'bg-gradient-to-r from-red-50 to-orange-50 border-2 border-red-200 text-red-800'
}`} }`}
> >
{message.text} <span className="text-2xl">{message.type === 'success' ? '✓' : '⚠️'}</span>
<span className="font-semibold">{message.text}</span>
</div> </div>
)} )}
{/* Config Source Info */} {/* Config Source Info */}
<div className="mb-6 p-4 bg-blue-50 border border-blue-200 rounded"> <div className="bg-gradient-to-br from-blue-50 to-indigo-50 border-2 border-blue-200 rounded-2xl p-5 shadow-md">
<p className="text-sm text-blue-900"> <div className="flex items-center gap-3">
<strong>Current source:</strong> {source === 'database' ? 'Database (Custom)' : 'Environment (.env)'} <div className="w-10 h-10 rounded-lg bg-gradient-to-br from-blue-600 to-indigo-600 flex items-center justify-center text-white text-xl">
</p> 📊
</div>
<div>
<p className="text-sm font-bold text-blue-900">Current Source</p>
<p className="text-blue-700">{source === 'database' ? 'Database (Custom)' : 'Environment (.env)'}</p>
</div>
</div>
</div> </div>
{/* SMTP Form */} {/* SMTP Form */}
<form onSubmit={handleSave} className="bg-white rounded-lg shadow p-6"> <form onSubmit={handleSave} className="bg-white rounded-2xl shadow-lg p-8">
<div className="space-y-4"> <div className="space-y-4">
{/* Host */} {/* Host */}
<div> <div>
@@ -343,28 +358,28 @@ export default function SettingsPage() {
</div> </div>
{/* Buttons */} {/* Buttons */}
<div className="flex gap-3 mt-6"> <div className="flex gap-3 mt-8 pt-6 border-t border-gray-200">
<button <button
type="submit" type="submit"
disabled={saving} disabled={saving}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-gray-400" className="px-6 py-3 bg-gradient-to-r from-blue-600 to-indigo-600 text-white rounded-xl hover:from-blue-700 hover:to-indigo-700 disabled:from-gray-400 disabled:to-gray-500 font-semibold shadow-lg hover:shadow-xl transition-all"
> >
{saving ? 'Saving...' : 'Save Settings'} {saving ? '⚙️ Saving...' : '💾 Save Settings'}
</button> </button>
<button <button
type="button" type="button"
onClick={() => setShowTestModal(true)} onClick={() => setShowTestModal(true)}
className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700" className="px-6 py-3 bg-gradient-to-r from-green-500 to-emerald-600 text-white rounded-xl hover:from-green-600 hover:to-emerald-700 font-semibold shadow-lg hover:shadow-xl transition-all"
> >
Test Connection Test Connection
</button> </button>
{source === 'database' && ( {source === 'database' && (
<button <button
type="button" type="button"
onClick={handleReset} onClick={handleReset}
className="px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-100" className="px-6 py-3 bg-gradient-to-r from-gray-100 to-slate-100 border-2 border-gray-300 rounded-xl hover:from-gray-200 hover:to-slate-200 font-semibold shadow-md hover:shadow-lg transition-all"
> >
Reset to Defaults Reset to Defaults
</button> </button>
)} )}
</div> </div>

View File

@@ -12,26 +12,38 @@ export default function SetupGuidePage() {
}; };
return ( return (
<div className="max-w-4xl mx-auto"> <div className="max-w-5xl mx-auto space-y-8">
<div className="bg-white rounded-lg shadow-lg p-8"> {/* Hero Section with Gradient */}
<h1 className="text-4xl font-bold text-gray-900 mb-4"> <div className="relative overflow-hidden rounded-2xl bg-gradient-to-br from-emerald-600 via-teal-700 to-cyan-800 p-8 shadow-xl">
📱 OwnTracks App Setup Anleitung <div className="absolute top-0 right-0 -mt-4 -mr-4 h-40 w-40 rounded-full bg-white/10 blur-3xl"></div>
<div className="absolute bottom-0 left-0 -mb-4 -ml-4 h-40 w-40 rounded-full bg-white/10 blur-3xl"></div>
<div className="relative">
<h1 className="text-4xl font-bold text-white mb-3 flex items-center gap-3">
<span className="text-5xl">📱</span>
OwnTracks App Setup Anleitung
</h1> </h1>
<p className="text-gray-600 mb-8"> <p className="text-emerald-100 text-lg">
Diese Anleitung erklärt Schritt-für-Schritt, wie Sie die OwnTracks App Diese Anleitung erklärt Schritt-für-Schritt, wie Sie die OwnTracks App
auf Ihrem Smartphone installieren und mit dem Location Tracker System verbinden. auf Ihrem Smartphone installieren und mit dem Location Tracker System verbinden.
</p> </p>
</div>
</div>
<div className="bg-white rounded-2xl shadow-lg p-8">
{/* Table of Contents */} {/* Table of Contents */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-6 mb-8"> <div className="bg-gradient-to-br from-blue-50 to-indigo-50 border-2 border-blue-200 rounded-2xl p-6 mb-8 shadow-md">
<h2 className="text-xl font-bold text-gray-900 mb-3">📋 Inhaltsverzeichnis</h2> <h2 className="text-xl font-bold text-gray-900 mb-4 flex items-center gap-2">
<ul className="space-y-2 text-sm"> <span className="text-2xl">📋</span>
<li><a href="#installation" className="text-blue-600 hover:underline">1. Installation</a></li> Inhaltsverzeichnis
<li><a href="#credentials" className="text-blue-600 hover:underline">2. MQTT Credentials erhalten</a></li> </h2>
<li><a href="#configuration" className="text-blue-600 hover:underline">3. App Konfiguration</a></li> <ul className="grid grid-cols-1 md:grid-cols-2 gap-3">
<li><a href="#testing" className="text-blue-600 hover:underline">5. Verbindung testen</a></li> <li><a href="#installation" className="flex items-center gap-2 text-blue-700 hover:text-blue-900 font-semibold hover:underline bg-white px-4 py-2 rounded-lg shadow-sm hover:shadow-md transition-all"> 1. Installation</a></li>
<li><a href="#ports" className="text-blue-600 hover:underline">6. Port 1883 vs. 9001</a></li> <li><a href="#credentials" className="flex items-center gap-2 text-blue-700 hover:text-blue-900 font-semibold hover:underline bg-white px-4 py-2 rounded-lg shadow-sm hover:shadow-md transition-all"> 2. MQTT Credentials erhalten</a></li>
<li><a href="#troubleshooting" className="text-blue-600 hover:underline">7. Troubleshooting</a></li> <li><a href="#configuration" className="flex items-center gap-2 text-blue-700 hover:text-blue-900 font-semibold hover:underline bg-white px-4 py-2 rounded-lg shadow-sm hover:shadow-md transition-all"> 3. App Konfiguration</a></li>
<li><a href="#testing" className="flex items-center gap-2 text-blue-700 hover:text-blue-900 font-semibold hover:underline bg-white px-4 py-2 rounded-lg shadow-sm hover:shadow-md transition-all"> 5. Verbindung testen</a></li>
<li><a href="#ports" className="flex items-center gap-2 text-blue-700 hover:text-blue-900 font-semibold hover:underline bg-white px-4 py-2 rounded-lg shadow-sm hover:shadow-md transition-all"> 6. Port 1883 vs. 9001</a></li>
<li><a href="#troubleshooting" className="flex items-center gap-2 text-blue-700 hover:text-blue-900 font-semibold hover:underline bg-white px-4 py-2 rounded-lg shadow-sm hover:shadow-md transition-all"> 7. Troubleshooting</a></li>
</ul> </ul>
</div> </div>
@@ -44,8 +56,11 @@ export default function SetupGuidePage() {
onToggle={() => toggleSection("1")} onToggle={() => toggleSection("1")}
> >
<div className="grid md:grid-cols-2 gap-6"> <div className="grid md:grid-cols-2 gap-6">
<div className="border rounded-lg p-4"> <div className="bg-gradient-to-br from-blue-50 to-indigo-50 border-2 border-blue-200 rounded-xl p-5 shadow-md hover:shadow-lg transition-shadow">
<h4 className="font-bold text-lg mb-2">🍎 iOS (iPhone/iPad)</h4> <h4 className="font-bold text-lg mb-3 flex items-center gap-2">
<span className="text-2xl">🍎</span>
iOS (iPhone/iPad)
</h4>
<ol className="list-decimal list-inside space-y-2 text-sm"> <ol className="list-decimal list-inside space-y-2 text-sm">
<li>Öffnen Sie den <strong>App Store</strong></li> <li>Öffnen Sie den <strong>App Store</strong></li>
<li>Suchen Sie nach <strong>"OwnTracks"</strong></li> <li>Suchen Sie nach <strong>"OwnTracks"</strong></li>
@@ -55,13 +70,16 @@ export default function SetupGuidePage() {
href="https://apps.apple.com/app/owntracks/id692424691" href="https://apps.apple.com/app/owntracks/id692424691"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="inline-block mt-3 text-blue-600 hover:underline text-sm" className="inline-block mt-3 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm font-semibold shadow-md hover:shadow-lg transition-all"
> >
App Store Link App Store Link
</a> </a>
</div> </div>
<div className="border rounded-lg p-4"> <div className="bg-gradient-to-br from-green-50 to-emerald-50 border-2 border-green-200 rounded-xl p-5 shadow-md hover:shadow-lg transition-shadow">
<h4 className="font-bold text-lg mb-2">🤖 Android</h4> <h4 className="font-bold text-lg mb-3 flex items-center gap-2">
<span className="text-2xl">🤖</span>
Android
</h4>
<ol className="list-decimal list-inside space-y-2 text-sm"> <ol className="list-decimal list-inside space-y-2 text-sm">
<li>Öffnen Sie den <strong>Google Play Store</strong></li> <li>Öffnen Sie den <strong>Google Play Store</strong></li>
<li>Suchen Sie nach <strong>"OwnTracks"</strong></li> <li>Suchen Sie nach <strong>"OwnTracks"</strong></li>
@@ -71,7 +89,7 @@ export default function SetupGuidePage() {
href="https://play.google.com/store/apps/details?id=org.owntracks.android" href="https://play.google.com/store/apps/details?id=org.owntracks.android"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="inline-block mt-3 text-blue-600 hover:underline text-sm" className="inline-block mt-3 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 text-sm font-semibold shadow-md hover:shadow-lg transition-all"
> >
Play Store Link Play Store Link
</a> </a>
@@ -87,8 +105,11 @@ export default function SetupGuidePage() {
isOpen={openSections["2"]} isOpen={openSections["2"]}
onToggle={() => toggleSection("2")} onToggle={() => toggleSection("2")}
> >
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-4"> <div className="bg-gradient-to-r from-yellow-50 to-orange-50 border-2 border-yellow-300 rounded-xl p-5 mb-6 shadow-md">
<p className="text-sm font-semibold"> Wichtig: Bevor Sie die App konfigurieren, benötigen Sie MQTT-Zugangsdaten!</p> <p className="text-sm font-bold text-yellow-900 flex items-center gap-2">
<span className="text-xl"></span>
Wichtig: Bevor Sie die App konfigurieren, benötigen Sie MQTT-Zugangsdaten!
</p>
</div> </div>
<ol className="list-decimal list-inside space-y-3 text-sm"> <ol className="list-decimal list-inside space-y-3 text-sm">
<li>Navigieren Sie zu <a href="/admin/mqtt" className="text-blue-600 hover:underline font-semibold">MQTT Provisioning</a></li> <li>Navigieren Sie zu <a href="/admin/mqtt" className="text-blue-600 hover:underline font-semibold">MQTT Provisioning</a></li>
@@ -234,14 +255,20 @@ export default function SetupGuidePage() {
</Section> </Section>
{/* Quick Start Checklist */} {/* Quick Start Checklist */}
<div className="bg-green-50 border border-green-200 rounded-lg p-6 mt-8"> <div className="bg-gradient-to-br from-green-50 to-emerald-50 border-2 border-green-200 rounded-2xl shadow-lg p-6 mt-8">
<h3 className="text-xl font-bold text-gray-900 mb-4"> Schnellstart-Checkliste</h3> <h3 className="text-xl font-bold text-gray-900 mb-4 flex items-center gap-2">
<span className="text-2xl"></span>
Schnellstart-Checkliste
</h3>
<ChecklistItems /> <ChecklistItems />
</div> </div>
{/* Support Section */} {/* Support Section */}
<div className="bg-gray-50 border border-gray-200 rounded-lg p-6 mt-8"> <div className="bg-gradient-to-br from-slate-50 to-gray-50 border-2 border-gray-200 rounded-2xl shadow-lg p-6 mt-8">
<h3 className="text-xl font-bold text-gray-900 mb-3">📞 Weiterführende Informationen</h3> <h3 className="text-xl font-bold text-gray-900 mb-4 flex items-center gap-2">
<span className="text-2xl">📞</span>
Weiterführende Informationen
</h3>
<div className="grid md:grid-cols-2 gap-4 text-sm"> <div className="grid md:grid-cols-2 gap-4 text-sm">
<div> <div>
<h4 className="font-semibold mb-2">OwnTracks Dokumentation:</h4> <h4 className="font-semibold mb-2">OwnTracks Dokumentation:</h4>
@@ -282,19 +309,20 @@ function Section({
children: React.ReactNode; children: React.ReactNode;
}) { }) {
return ( return (
<div id={id} className="border-b border-gray-200 py-6"> <div id={id} className="bg-white rounded-2xl shadow-lg overflow-hidden mb-6 border border-gray-200">
<button <button
onClick={onToggle} onClick={onToggle}
className="flex items-center justify-between w-full text-left hover:bg-gray-50 p-2 rounded" className="flex items-center justify-between w-full text-left bg-gradient-to-r from-gray-50 to-slate-50 hover:from-blue-50 hover:to-indigo-50 px-6 py-5 transition-all"
> >
<h2 className="text-2xl font-bold text-gray-900"> <h2 className="text-2xl font-bold text-gray-900 flex items-center gap-3">
{icon} {title} <span className="text-3xl">{icon}</span>
{title}
</h2> </h2>
<span className="text-2xl text-gray-400"> <span className={`text-3xl font-bold transition-transform ${isOpen ? 'rotate-180' : ''}`}>
{isOpen ? "" : "+"} {isOpen ? "" : "+"}
</span> </span>
</button> </button>
{isOpen && <div className="mt-4">{children}</div>} {isOpen && <div className="p-6 bg-white border-t border-gray-200">{children}</div>}
</div> </div>
); );
} }
@@ -345,7 +373,7 @@ function ConfigTable() {
function PortComparison() { function PortComparison() {
return ( return (
<div className="grid md:grid-cols-2 gap-6"> <div className="grid md:grid-cols-2 gap-6">
<div className="border-2 border-green-500 rounded-lg p-4 bg-green-50"> <div className="border-2 border-green-400 rounded-xl p-6 bg-gradient-to-br from-green-50 to-emerald-50 shadow-lg hover:shadow-xl transition-shadow">
<h4 className="font-bold text-lg mb-3 text-green-800">Port 1883 (Standard MQTT)</h4> <h4 className="font-bold text-lg mb-3 text-green-800">Port 1883 (Standard MQTT)</h4>
<ul className="space-y-2 text-sm"> <ul className="space-y-2 text-sm">
<li> <strong>Protokoll:</strong> Standard MQTT (TCP)</li> <li> <strong>Protokoll:</strong> Standard MQTT (TCP)</li>
@@ -360,7 +388,7 @@ function PortComparison() {
Websockets: DEAKTIVIERT Websockets: DEAKTIVIERT
</div> </div>
</div> </div>
<div className="border-2 border-blue-500 rounded-lg p-4 bg-blue-50"> <div className="border-2 border-blue-400 rounded-xl p-6 bg-gradient-to-br from-blue-50 to-indigo-50 shadow-lg hover:shadow-xl transition-shadow">
<h4 className="font-bold text-lg mb-3 text-blue-800">Port 9001 (MQTT over WebSockets)</h4> <h4 className="font-bold text-lg mb-3 text-blue-800">Port 9001 (MQTT over WebSockets)</h4>
<ul className="space-y-2 text-sm"> <ul className="space-y-2 text-sm">
<li> <strong>Protokoll:</strong> MQTT über WebSocket</li> <li> <strong>Protokoll:</strong> MQTT über WebSocket</li>
@@ -416,8 +444,11 @@ function TroubleshootingSection() {
function TroubleshootingItem({ problem, solutions }: { problem: string; solutions: string[] }) { function TroubleshootingItem({ problem, solutions }: { problem: string; solutions: string[] }) {
return ( return (
<div className="border border-gray-300 rounded-lg p-4"> <div className="bg-gradient-to-br from-red-50 to-orange-50 border-2 border-red-200 rounded-xl p-5 shadow-md hover:shadow-lg transition-shadow">
<h4 className="font-bold text-red-600 mb-2"> {problem}</h4> <h4 className="font-bold text-red-700 mb-3 text-lg flex items-center gap-2">
<span className="text-xl"></span>
{problem}
</h4>
<ul className="space-y-1 text-sm"> <ul className="space-y-1 text-sm">
{solutions.map((solution, i) => ( {solutions.map((solution, i) => (
<li key={i} className="flex items-start gap-2"> <li key={i} className="flex items-start gap-2">

View File

@@ -224,58 +224,77 @@ export default function UsersPage() {
} }
return ( return (
<div className="space-y-8">
{/* Hero Section with Gradient */}
<div className="relative overflow-hidden rounded-2xl bg-gradient-to-br from-violet-600 via-purple-700 to-fuchsia-800 p-8 shadow-xl">
<div className="absolute top-0 right-0 -mt-4 -mr-4 h-40 w-40 rounded-full bg-white/10 blur-3xl"></div>
<div className="absolute bottom-0 left-0 -mb-4 -ml-4 h-40 w-40 rounded-full bg-white/10 blur-3xl"></div>
<div className="relative flex justify-between items-center">
<div> <div>
{/* Header */} <h2 className="text-4xl font-bold text-white mb-2">User Management</h2>
<div className="flex justify-between items-center mb-6"> <p className="text-violet-100 text-lg">Verwalte Benutzerkonten und Berechtigungen</p>
<h2 className="text-3xl font-bold text-gray-900">User Management</h2> </div>
<button <button
onClick={() => { onClick={() => {
setFormData({ username: "", email: "", password: "", role: "VIEWER" }); setFormData({ username: "", email: "", password: "", role: "VIEWER" });
setShowAddModal(true); setShowAddModal(true);
}} }}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700" className="px-6 py-3 bg-white text-violet-700 rounded-xl hover:bg-violet-50 font-semibold shadow-lg hover:shadow-xl transition-all transform hover:-translate-y-0.5"
> >
Add User + Add User
</button> </button>
</div> </div>
</div>
{/* Users Grid */} {/* Users Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{users.map((user) => ( {users.map((user) => (
<div <div
key={user.id} key={user.id}
className="bg-white rounded-lg shadow-md p-6 border-l-4" className="group relative overflow-hidden bg-white rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-1 p-6"
style={{
borderLeftColor: user.role === "ADMIN" ? "#ef4444" : "#3b82f6",
}}
> >
<div className={`absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity bg-gradient-to-br ${
user.role === "ADMIN" ? "from-red-50 to-orange-50" : "from-blue-50 to-indigo-50"
}`}></div>
<div className="relative">
<div className="flex items-start justify-between mb-4"> <div className="flex items-start justify-between mb-4">
<span <div className="flex items-center gap-3">
className={`px-2 py-1 text-xs font-medium rounded ${ <div className={`w-12 h-12 rounded-xl shadow-lg flex items-center justify-center text-2xl ring-2 ring-white transform group-hover:scale-110 transition-transform ${
user.role === "ADMIN" user.role === "ADMIN"
? "bg-red-100 text-red-800" ? "bg-gradient-to-br from-red-500 to-orange-600"
: "bg-blue-100 text-blue-800" : "bg-gradient-to-br from-blue-500 to-indigo-600"
}`}>
{user.role === "ADMIN" ? "👑" : "👤"}
</div>
<span
className={`px-3 py-1.5 text-xs font-bold rounded-lg shadow-md ${
user.role === "ADMIN"
? "bg-gradient-to-r from-red-500 to-orange-600 text-white"
: "bg-gradient-to-r from-blue-500 to-indigo-600 text-white"
}`} }`}
> >
{user.role} {user.role}
</span> </span>
</div> </div>
</div>
<div className="space-y-2 text-sm mb-4"> <div className="bg-gradient-to-br from-gray-50 to-slate-50 rounded-xl p-4 mb-4 border border-gray-200 space-y-2 text-sm">
<p> <p className="flex items-center justify-between">
<span className="font-medium text-gray-700">Username:</span>{" "} <span className="font-semibold text-gray-700">Username:</span>
<span className="text-gray-900">{user.username}</span> <span className="text-gray-900 font-mono bg-white px-2 py-0.5 rounded">{user.username}</span>
</p> </p>
<p> <p className="flex items-center justify-between">
<span className="font-medium text-gray-700">Email:</span>{" "} <span className="font-semibold text-gray-700">Email:</span>
<span className="text-gray-900">{user.email || "—"}</span> <span className="text-gray-900">{user.email || "—"}</span>
</p> </p>
<p className="text-gray-600"> <p className="flex items-center justify-between text-gray-600">
Created: {new Date(user.createdAt).toLocaleDateString()} <span>Created:</span>
<span>{new Date(user.createdAt).toLocaleDateString()}</span>
</p> </p>
{user.lastLoginAt && ( {user.lastLoginAt && (
<p className="text-gray-600"> <p className="flex items-center justify-between text-gray-600">
Last login: {new Date(user.lastLoginAt).toLocaleString()} <span>Last login:</span>
<span>{new Date(user.lastLoginAt).toLocaleString()}</span>
</p> </p>
)} )}
</div> </div>
@@ -283,13 +302,13 @@ export default function UsersPage() {
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
onClick={() => openEditModal(user)} onClick={() => openEditModal(user)}
className="flex-1 px-3 py-2 bg-blue-600 text-white text-sm rounded-md hover:bg-blue-700" className="flex-1 px-4 py-2.5 bg-gradient-to-r from-blue-600 to-blue-700 text-white text-sm font-semibold rounded-lg hover:from-blue-700 hover:to-blue-800 shadow-md hover:shadow-lg transition-all"
> >
Edit Edit
</button> </button>
<button <button
onClick={() => openDeleteModal(user)} onClick={() => openDeleteModal(user)}
className="flex-1 px-3 py-2 bg-red-600 text-white text-sm rounded-md hover:bg-red-700" className="flex-1 px-4 py-2.5 bg-gradient-to-r from-red-600 to-red-700 text-white text-sm font-semibold rounded-lg hover:from-red-700 hover:to-red-800 shadow-md hover:shadow-lg transition-all"
> >
Delete Delete
</button> </button>
@@ -300,15 +319,15 @@ export default function UsersPage() {
<div className="flex gap-2 mt-2"> <div className="flex gap-2 mt-2">
<button <button
onClick={() => handleResendWelcome(user)} onClick={() => handleResendWelcome(user)}
className="flex-1 px-3 py-2 bg-green-600 text-white text-xs rounded-md hover:bg-green-700" className="flex-1 px-3 py-2 bg-gradient-to-r from-green-500 to-emerald-600 text-white text-xs font-semibold rounded-lg hover:from-green-600 hover:to-emerald-700 shadow-md hover:shadow-lg transition-all"
> >
Resend Welcome 📧 Resend Welcome
</button> </button>
<button <button
onClick={() => handleSendPasswordReset(user)} onClick={() => handleSendPasswordReset(user)}
className="flex-1 px-3 py-2 bg-orange-600 text-white text-xs rounded-md hover:bg-orange-700" className="flex-1 px-3 py-2 bg-gradient-to-r from-orange-500 to-red-500 text-white text-xs font-semibold rounded-lg hover:from-orange-600 hover:to-red-600 shadow-md hover:shadow-lg transition-all"
> >
Reset Password 🔑 Reset Password
</button> </button>
</div> </div>
)} )}
@@ -317,8 +336,10 @@ export default function UsersPage() {
</div> </div>
{users.length === 0 && ( {users.length === 0 && (
<div className="text-center py-12"> <div className="relative overflow-hidden bg-gradient-to-br from-gray-50 to-slate-50 rounded-2xl shadow-lg p-12 text-center border border-gray-200">
<p className="text-gray-600">No users found. Create your first user!</p> <div className="absolute top-0 right-0 text-9xl opacity-5">👤</div>
<p className="text-xl font-semibold text-gray-600 mb-2">Keine Benutzer gefunden</p>
<p className="text-gray-500">Erstelle deinen ersten Benutzer!</p>
</div> </div>
)} )}