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 (
<div className="space-y-6">
{/* Header */}
<div className="flex justify-between items-center">
<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-3xl font-bold text-gray-900">Device Management</h2>
{!isAdmin && (
<p className="text-sm text-gray-600 mt-1">Read-only view</p>
)}
<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-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
</button>
)}
</div>
</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>
)}

View File

@@ -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">
<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"
/>

View File

@@ -341,41 +341,68 @@ 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">
<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-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
</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"
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
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>
</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>
<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 && (
<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 className="space-y-6">
@@ -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 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-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>
<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-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>
</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
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>
<div className="space-y-2">
{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">
<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="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
</button>
</td>
</tr>
</div>
</div>
))}
</tbody>
</table>
</div>
) : (
<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>

View File

@@ -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>

View File

@@ -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
<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-gray-600 mb-8">
<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">

View File

@@ -224,58 +224,77 @@ export default function UsersPage() {
}
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>
{/* Header */}
<div className="flex justify-between items-center mb-6">
<h2 className="text-3xl font-bold text-gray-900">User Management</h2>
<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-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>
</div>
</div>
{/* Users Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{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"
? "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>
)}