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:
@@ -190,28 +190,35 @@ export default function DevicesPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold text-gray-900">Device Management</h2>
|
||||
{!isAdmin && (
|
||||
<p className="text-sm text-gray-600 mt-1">Read-only view</p>
|
||||
<div className="space-y-8">
|
||||
{/* Hero Section with Gradient */}
|
||||
<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>
|
||||
<h2 className="text-4xl font-bold text-white mb-2">Device Management</h2>
|
||||
<p className="text-purple-100 text-lg">
|
||||
{!isAdmin ? "Read-only view" : "Verwalte deine Tracking-Geräte"}
|
||||
</p>
|
||||
</div>
|
||||
{isAdmin && (
|
||||
<button
|
||||
onClick={() => setShowAddModal(true)}
|
||||
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
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{isAdmin && (
|
||||
<button
|
||||
onClick={() => setShowAddModal(true)}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 font-medium"
|
||||
>
|
||||
+ Add Device
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
|
||||
{error}
|
||||
<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">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xl">⚠️</span>
|
||||
<span className="font-semibold">{error}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -228,32 +235,33 @@ export default function DevicesPage() {
|
||||
return (
|
||||
<div
|
||||
key={device.id}
|
||||
className="bg-white rounded-lg shadow-md p-6 space-y-4 border-2"
|
||||
style={{ borderColor: device.isActive ? device.color : "#ccc" }}
|
||||
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"
|
||||
>
|
||||
<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 items-center gap-3">
|
||||
<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 }}
|
||||
>
|
||||
<span className="text-white text-2xl">📱</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
<h3 className="text-lg font-bold text-gray-900">
|
||||
{device.name}
|
||||
</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>
|
||||
<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
|
||||
? "bg-green-100 text-green-800"
|
||||
: "bg-gray-100 text-gray-800"
|
||||
? "bg-gradient-to-r from-green-500 to-emerald-600 text-white"
|
||||
: "bg-gradient-to-r from-gray-400 to-gray-500 text-white"
|
||||
}`}
|
||||
>
|
||||
{isRecent ? "Online" : "Offline"}
|
||||
{isRecent ? "🟢 Online" : "⚫ Offline"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -262,7 +270,7 @@ export default function DevicesPage() {
|
||||
)}
|
||||
|
||||
{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">
|
||||
<span className="text-gray-600 flex items-center gap-2">
|
||||
<span className="text-lg">🕒</span>
|
||||
@@ -322,13 +330,13 @@ export default function DevicesPage() {
|
||||
<div className="flex gap-2 pt-2">
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
@@ -340,8 +348,10 @@ export default function DevicesPage() {
|
||||
</div>
|
||||
|
||||
{devices.length === 0 && (
|
||||
<div className="bg-white rounded-lg shadow p-8 text-center text-gray-500">
|
||||
No devices found. Add a device to get started.
|
||||
<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">
|
||||
<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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -48,28 +48,42 @@ export default function EmailsPage() {
|
||||
const previewUrl = `/api/admin/emails/preview?template=${selectedTemplate}`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold text-gray-900 mb-6">Email Templates</h2>
|
||||
<div className="space-y-8">
|
||||
{/* 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 */}
|
||||
{message && (
|
||||
<div
|
||||
className={`mb-6 p-4 rounded ${
|
||||
className={`p-5 rounded-xl shadow-lg flex items-center gap-3 ${
|
||||
message.type === 'success'
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-red-100 text-red-800'
|
||||
? 'bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-200 text-green-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 className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Template List */}
|
||||
<div className="lg:col-span-1">
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Templates</h3>
|
||||
<div className="lg:col-span-1 space-y-4">
|
||||
<div className="bg-white rounded-2xl shadow-lg overflow-hidden">
|
||||
<div className="bg-gradient-to-r from-pink-50 to-rose-50 px-6 py-4 border-b border-pink-100">
|
||||
<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 className="p-4">
|
||||
<div className="space-y-2">
|
||||
@@ -77,16 +91,16 @@ export default function EmailsPage() {
|
||||
<button
|
||||
key={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
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-50 hover:bg-gray-100 text-gray-900'
|
||||
? 'bg-gradient-to-r from-pink-600 to-rose-600 text-white shadow-lg scale-105'
|
||||
: '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 ${
|
||||
selectedTemplate === template.name
|
||||
? 'text-blue-100'
|
||||
? 'text-pink-100'
|
||||
: 'text-gray-600'
|
||||
}`}>
|
||||
{template.description}
|
||||
@@ -100,25 +114,33 @@ export default function EmailsPage() {
|
||||
{/* Send Test Button */}
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Preview */}
|
||||
<div className="lg:col-span-2">
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Preview</h3>
|
||||
<span className="text-sm text-gray-600">
|
||||
{EMAIL_TEMPLATES.find(t => t.name === selectedTemplate)?.subject}
|
||||
</span>
|
||||
<div className="bg-white rounded-2xl shadow-lg overflow-hidden">
|
||||
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 px-6 py-4 border-b border-blue-100">
|
||||
<div className="flex justify-between items-center">
|
||||
<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}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<iframe
|
||||
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' }}
|
||||
title="Email Preview"
|
||||
/>
|
||||
|
||||
@@ -341,39 +341,66 @@ export default function MqttPage() {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-8">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-3xl font-bold">MQTT Provisioning</h1>
|
||||
<div className="flex gap-4 items-center">
|
||||
{syncStatus && syncStatus.pending_changes > 0 && (
|
||||
<span className="px-3 py-1 bg-yellow-100 text-yellow-800 rounded-md text-sm">
|
||||
{syncStatus.pending_changes} ausstehende Änderungen
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={handleSync}
|
||||
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"
|
||||
>
|
||||
{syncing ? "Synchronisiere..." : "MQTT Sync"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowAddModal(true)}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
||||
>
|
||||
Device Provisionieren
|
||||
</button>
|
||||
<div className="space-y-8">
|
||||
{/* Hero Section with Gradient */}
|
||||
<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="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 && (
|
||||
<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
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={handleSync}
|
||||
disabled={syncing || !syncStatus || syncStatus.pending_changes === 0}
|
||||
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"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowAddModal(true)}
|
||||
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
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{syncStatus && (
|
||||
<div className="mb-6 p-4 bg-gray-100 rounded-md">
|
||||
<h3 className="font-semibold mb-2">Sync Status</h3>
|
||||
<div className="text-sm">
|
||||
<p>Status: <span className={syncStatus.last_sync_status === 'success' ? 'text-green-600' : 'text-red-600'}>{syncStatus.last_sync_status}</span></p>
|
||||
{syncStatus.last_sync_at && (
|
||||
<p>Letzter Sync: {new Date(syncStatus.last_sync_at).toLocaleString('de-DE')}</p>
|
||||
)}
|
||||
<div className="bg-white rounded-2xl shadow-lg overflow-hidden">
|
||||
<div className="bg-gradient-to-r from-slate-50 to-gray-50 px-6 py-4 border-b border-gray-200">
|
||||
<div className="flex items-center gap-3">
|
||||
<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 && (
|
||||
<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>
|
||||
)}
|
||||
@@ -383,26 +410,48 @@ export default function MqttPage() {
|
||||
const deviceRules = aclRules[cred.device_id] || [];
|
||||
|
||||
return (
|
||||
<div key={cred.id} className="border rounded-lg p-6 bg-white shadow-sm">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold">{cred.device_name}</h3>
|
||||
<p className="text-sm text-gray-500">Device ID: {cred.device_id}</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>
|
||||
<p className="text-xs text-gray-500 mt-1">Erstellt: {new Date(cred.created_at).toLocaleString('de-DE')}</p>
|
||||
<div key={cred.id} className="bg-white rounded-2xl shadow-lg overflow-hidden hover:shadow-2xl transition-shadow">
|
||||
<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>
|
||||
<h3 className="text-xl font-bold text-gray-900">{cred.device_name}</h3>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleToggleEnabled(cred.device_id, !cred.enabled)}
|
||||
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'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
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'}`}
|
||||
>
|
||||
{cred.enabled ? 'Aktiviert' : 'Deaktiviert'}
|
||||
</button>
|
||||
</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
|
||||
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
|
||||
onClick={() => {
|
||||
@@ -414,60 +463,61 @@ export default function MqttPage() {
|
||||
});
|
||||
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
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<h4 className="font-semibold mb-2 text-sm">ACL Regeln:</h4>
|
||||
<div>
|
||||
<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 ? (
|
||||
<table className="w-full text-sm">
|
||||
<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 => (
|
||||
<tr key={rule.id} className="border-t">
|
||||
<td className="px-4 py-2"><code className="bg-gray-100 px-2 py-1 rounded">{rule.topic_pattern}</code></td>
|
||||
<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>
|
||||
<td className="px-4 py-2 text-gray-500">{new Date(rule.created_at).toLocaleString('de-DE')}</td>
|
||||
<td className="px-4 py-2">
|
||||
<button
|
||||
onClick={() => handleDeleteAclRule(rule.id, cred.device_id)}
|
||||
className="text-red-600 hover:text-red-800 text-xs"
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="space-y-2">
|
||||
{deviceRules.map(rule => (
|
||||
<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">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-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
|
||||
onClick={() => handleDeleteAclRule(rule.id, cred.device_id)}
|
||||
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
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-500">Keine ACL Regeln definiert</p>
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{credentials.length === 0 && (
|
||||
<div className="text-center py-12 text-gray-500">
|
||||
Noch keine Devices provisioniert
|
||||
<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">
|
||||
<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>
|
||||
|
||||
@@ -144,21 +144,29 @@ export default function SettingsPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold text-gray-900 mb-6">Settings</h2>
|
||||
<div className="space-y-8">
|
||||
{/* 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 */}
|
||||
<div className="border-b border-gray-200 mb-6">
|
||||
<nav className="flex gap-4">
|
||||
<div className="bg-white rounded-2xl shadow-lg overflow-hidden">
|
||||
<nav className="flex gap-2 p-2 bg-gradient-to-r from-gray-50 to-slate-50">
|
||||
<button
|
||||
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'
|
||||
? 'border-blue-600 text-blue-600'
|
||||
: 'border-transparent text-gray-600 hover:text-gray-900'
|
||||
? 'bg-gradient-to-r from-blue-600 to-indigo-600 text-white shadow-lg'
|
||||
: 'text-gray-600 hover:bg-white hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
SMTP Settings
|
||||
📧 SMTP Settings
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
@@ -166,25 +174,32 @@ export default function SettingsPage() {
|
||||
{/* Status Message */}
|
||||
{message && (
|
||||
<div
|
||||
className={`mb-6 p-4 rounded ${
|
||||
className={`p-5 rounded-xl shadow-lg flex items-center gap-3 ${
|
||||
message.type === 'success'
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-red-100 text-red-800'
|
||||
? 'bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-200 text-green-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>
|
||||
)}
|
||||
|
||||
{/* Config Source Info */}
|
||||
<div className="mb-6 p-4 bg-blue-50 border border-blue-200 rounded">
|
||||
<p className="text-sm text-blue-900">
|
||||
<strong>Current source:</strong> {source === 'database' ? 'Database (Custom)' : 'Environment (.env)'}
|
||||
</p>
|
||||
<div className="bg-gradient-to-br from-blue-50 to-indigo-50 border-2 border-blue-200 rounded-2xl p-5 shadow-md">
|
||||
<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>
|
||||
<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>
|
||||
|
||||
{/* 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">
|
||||
{/* Host */}
|
||||
<div>
|
||||
@@ -343,28 +358,28 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="flex gap-3 mt-6">
|
||||
<div className="flex gap-3 mt-8 pt-6 border-t border-gray-200">
|
||||
<button
|
||||
type="submit"
|
||||
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
|
||||
type="button"
|
||||
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>
|
||||
{source === 'database' && (
|
||||
<button
|
||||
type="button"
|
||||
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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -12,26 +12,38 @@ export default function SetupGuidePage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="bg-white rounded-lg shadow-lg p-8">
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-4">
|
||||
📱 OwnTracks App Setup Anleitung
|
||||
</h1>
|
||||
<p className="text-gray-600 mb-8">
|
||||
Diese Anleitung erklärt Schritt-für-Schritt, wie Sie die OwnTracks App
|
||||
auf Ihrem Smartphone installieren und mit dem Location Tracker System verbinden.
|
||||
</p>
|
||||
<div className="max-w-5xl mx-auto space-y-8">
|
||||
{/* Hero Section with Gradient */}
|
||||
<div className="relative overflow-hidden rounded-2xl bg-gradient-to-br from-emerald-600 via-teal-700 to-cyan-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">
|
||||
<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>
|
||||
<p className="text-emerald-100 text-lg">
|
||||
Diese Anleitung erklärt Schritt-für-Schritt, wie Sie die OwnTracks App
|
||||
auf Ihrem Smartphone installieren und mit dem Location Tracker System verbinden.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl shadow-lg p-8">
|
||||
|
||||
{/* Table of Contents */}
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-6 mb-8">
|
||||
<h2 className="text-xl font-bold text-gray-900 mb-3">📋 Inhaltsverzeichnis</h2>
|
||||
<ul className="space-y-2 text-sm">
|
||||
<li><a href="#installation" className="text-blue-600 hover:underline">1. Installation</a></li>
|
||||
<li><a href="#credentials" className="text-blue-600 hover:underline">2. MQTT Credentials erhalten</a></li>
|
||||
<li><a href="#configuration" className="text-blue-600 hover:underline">3. App Konfiguration</a></li>
|
||||
<li><a href="#testing" className="text-blue-600 hover:underline">5. Verbindung testen</a></li>
|
||||
<li><a href="#ports" className="text-blue-600 hover:underline">6. Port 1883 vs. 9001</a></li>
|
||||
<li><a href="#troubleshooting" className="text-blue-600 hover:underline">7. Troubleshooting</a></li>
|
||||
<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-4 flex items-center gap-2">
|
||||
<span className="text-2xl">📋</span>
|
||||
Inhaltsverzeichnis
|
||||
</h2>
|
||||
<ul className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<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="#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="#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>
|
||||
</div>
|
||||
|
||||
@@ -44,8 +56,11 @@ export default function SetupGuidePage() {
|
||||
onToggle={() => toggleSection("1")}
|
||||
>
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div className="border rounded-lg p-4">
|
||||
<h4 className="font-bold text-lg mb-2">🍎 iOS (iPhone/iPad)</h4>
|
||||
<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-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">
|
||||
<li>Öffnen Sie den <strong>App Store</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"
|
||||
target="_blank"
|
||||
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
|
||||
</a>
|
||||
</div>
|
||||
<div className="border rounded-lg p-4">
|
||||
<h4 className="font-bold text-lg mb-2">🤖 Android</h4>
|
||||
<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-3 flex items-center gap-2">
|
||||
<span className="text-2xl">🤖</span>
|
||||
Android
|
||||
</h4>
|
||||
<ol className="list-decimal list-inside space-y-2 text-sm">
|
||||
<li>Öffnen Sie den <strong>Google Play Store</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"
|
||||
target="_blank"
|
||||
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
|
||||
</a>
|
||||
@@ -87,8 +105,11 @@ export default function SetupGuidePage() {
|
||||
isOpen={openSections["2"]}
|
||||
onToggle={() => toggleSection("2")}
|
||||
>
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-4">
|
||||
<p className="text-sm font-semibold">⚠️ Wichtig: Bevor Sie die App konfigurieren, benötigen Sie MQTT-Zugangsdaten!</p>
|
||||
<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-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>
|
||||
<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>
|
||||
@@ -234,14 +255,20 @@ export default function SetupGuidePage() {
|
||||
</Section>
|
||||
|
||||
{/* Quick Start Checklist */}
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-6 mt-8">
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-4">✅ Schnellstart-Checkliste</h3>
|
||||
<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 flex items-center gap-2">
|
||||
<span className="text-2xl">✅</span>
|
||||
Schnellstart-Checkliste
|
||||
</h3>
|
||||
<ChecklistItems />
|
||||
</div>
|
||||
|
||||
{/* Support Section */}
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-lg p-6 mt-8">
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-3">📞 Weiterführende Informationen</h3>
|
||||
<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-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>
|
||||
<h4 className="font-semibold mb-2">OwnTracks Dokumentation:</h4>
|
||||
@@ -282,19 +309,20 @@ function Section({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
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
|
||||
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">
|
||||
{icon} {title}
|
||||
<h2 className="text-2xl font-bold text-gray-900 flex items-center gap-3">
|
||||
<span className="text-3xl">{icon}</span>
|
||||
{title}
|
||||
</h2>
|
||||
<span className="text-2xl text-gray-400">
|
||||
<span className={`text-3xl font-bold transition-transform ${isOpen ? 'rotate-180' : ''}`}>
|
||||
{isOpen ? "−" : "+"}
|
||||
</span>
|
||||
</button>
|
||||
{isOpen && <div className="mt-4">{children}</div>}
|
||||
{isOpen && <div className="p-6 bg-white border-t border-gray-200">{children}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -345,7 +373,7 @@ function ConfigTable() {
|
||||
function PortComparison() {
|
||||
return (
|
||||
<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>
|
||||
<ul className="space-y-2 text-sm">
|
||||
<li>✅ <strong>Protokoll:</strong> Standard MQTT (TCP)</li>
|
||||
@@ -360,7 +388,7 @@ function PortComparison() {
|
||||
Websockets: DEAKTIVIERT
|
||||
</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>
|
||||
<ul className="space-y-2 text-sm">
|
||||
<li>✅ <strong>Protokoll:</strong> MQTT über WebSocket</li>
|
||||
@@ -416,8 +444,11 @@ function TroubleshootingSection() {
|
||||
|
||||
function TroubleshootingItem({ problem, solutions }: { problem: string; solutions: string[] }) {
|
||||
return (
|
||||
<div className="border border-gray-300 rounded-lg p-4">
|
||||
<h4 className="font-bold text-red-600 mb-2">❌ {problem}</h4>
|
||||
<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-700 mb-3 text-lg flex items-center gap-2">
|
||||
<span className="text-xl">❌</span>
|
||||
{problem}
|
||||
</h4>
|
||||
<ul className="space-y-1 text-sm">
|
||||
{solutions.map((solution, i) => (
|
||||
<li key={i} className="flex items-start gap-2">
|
||||
|
||||
@@ -224,19 +224,26 @@ export default function UsersPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-3xl font-bold text-gray-900">User Management</h2>
|
||||
<button
|
||||
onClick={() => {
|
||||
setFormData({ username: "", email: "", password: "", role: "VIEWER" });
|
||||
setShowAddModal(true);
|
||||
}}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
||||
>
|
||||
Add User
|
||||
</button>
|
||||
<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>
|
||||
<h2 className="text-4xl font-bold text-white mb-2">User Management</h2>
|
||||
<p className="text-violet-100 text-lg">Verwalte Benutzerkonten und Berechtigungen</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setFormData({ username: "", email: "", password: "", role: "VIEWER" });
|
||||
setShowAddModal(true);
|
||||
}}
|
||||
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
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Users Grid */}
|
||||
@@ -244,38 +251,50 @@ export default function UsersPage() {
|
||||
{users.map((user) => (
|
||||
<div
|
||||
key={user.id}
|
||||
className="bg-white rounded-lg shadow-md p-6 border-l-4"
|
||||
style={{
|
||||
borderLeftColor: user.role === "ADMIN" ? "#ef4444" : "#3b82f6",
|
||||
}}
|
||||
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"
|
||||
>
|
||||
<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">
|
||||
<span
|
||||
className={`px-2 py-1 text-xs font-medium rounded ${
|
||||
<div className="flex items-center gap-3">
|
||||
<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"
|
||||
? "bg-red-100 text-red-800"
|
||||
: "bg-blue-100 text-blue-800"
|
||||
}`}
|
||||
>
|
||||
{user.role}
|
||||
</span>
|
||||
? "bg-gradient-to-br from-red-500 to-orange-600"
|
||||
: "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}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-sm mb-4">
|
||||
<p>
|
||||
<span className="font-medium text-gray-700">Username:</span>{" "}
|
||||
<span className="text-gray-900">{user.username}</span>
|
||||
<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 className="flex items-center justify-between">
|
||||
<span className="font-semibold text-gray-700">Username:</span>
|
||||
<span className="text-gray-900 font-mono bg-white px-2 py-0.5 rounded">{user.username}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-medium text-gray-700">Email:</span>{" "}
|
||||
<p className="flex items-center justify-between">
|
||||
<span className="font-semibold text-gray-700">Email:</span>
|
||||
<span className="text-gray-900">{user.email || "—"}</span>
|
||||
</p>
|
||||
<p className="text-gray-600">
|
||||
Created: {new Date(user.createdAt).toLocaleDateString()}
|
||||
<p className="flex items-center justify-between text-gray-600">
|
||||
<span>Created:</span>
|
||||
<span>{new Date(user.createdAt).toLocaleDateString()}</span>
|
||||
</p>
|
||||
{user.lastLoginAt && (
|
||||
<p className="text-gray-600">
|
||||
Last login: {new Date(user.lastLoginAt).toLocaleString()}
|
||||
<p className="flex items-center justify-between text-gray-600">
|
||||
<span>Last login:</span>
|
||||
<span>{new Date(user.lastLoginAt).toLocaleString()}</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -283,13 +302,13 @@ export default function UsersPage() {
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
@@ -300,15 +319,15 @@ export default function UsersPage() {
|
||||
<div className="flex gap-2 mt-2">
|
||||
<button
|
||||
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
|
||||
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>
|
||||
</div>
|
||||
)}
|
||||
@@ -317,8 +336,10 @@ export default function UsersPage() {
|
||||
</div>
|
||||
|
||||
{users.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-gray-600">No users found. Create your first user!</p>
|
||||
<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">
|
||||
<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>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user