Refactor mindmap visualization and enhance user authentication UI: Implement API calls to load mindmap data dynamically, process hierarchical data into nodes and links, and improve error handling. Update login and registration templates for a modern design with enhanced validation and user experience. Remove obsolete network background images.

This commit is contained in:
2025-04-27 07:08:38 +02:00
parent 0705ecce59
commit 11ab15127c
6 changed files with 395 additions and 226 deletions

View File

@@ -282,21 +282,25 @@ class MindMapVisualization {
// Zeige Lade-Animation
this.showLoading();
// Demo-Logik: Verwende direkt die Standardknoten
this.nodes = this.defaultNodes;
this.links = this.defaultLinks;
// API-Aufruf durchführen, um die Kategorien und ihre Knoten zu laden
const response = await fetch('/api/mindmap/public');
if (!response.ok) {
throw new Error('API-Fehler: ' + response.statusText);
}
// Simuliere einen API-Aufruf (in einer echten Anwendung würde hier ein Fetch stehen)
await new Promise(resolve => setTimeout(resolve, 1000));
const data = await response.json();
console.log('Geladene Mindmap-Daten:', data);
// Verarbeite die hierarchischen Daten in flache Knoten und Links
const processed = this.processApiData(data);
this.nodes = processed.nodes;
this.links = processed.links;
// Visualisierung aktualisieren
this.updateVisualization();
// Lade-Animation ausblenden
this.hideLoading();
// Zufällige Knoten pulsieren lassen
this.pulseRandomNodes();
} catch (error) {
console.error('Fehler beim Laden der Mindmap-Daten:', error);
@@ -304,64 +308,140 @@ class MindMapVisualization {
this.nodes = this.defaultNodes;
this.links = this.defaultLinks;
// Fehler anzeigen
this.showError('Mindmap-Daten konnten nicht geladen werden. Verwende Standarddaten.');
// Visualisierung auch im Fehlerfall aktualisieren
this.updateVisualization();
this.hideLoading();
}
}
// Startet ein zufälliges Pulsen von Knoten für visuelle Aufmerksamkeit
pulseRandomNodes() {
// Zufälligen Knoten auswählen
const randomNode = () => {
const randomIndex = Math.floor(Math.random() * this.nodes.length);
return this.nodes[randomIndex];
// Verarbeitet die API-Daten in das benötigte Format
processApiData(apiData) {
// Erstelle einen Root-Knoten, der alle Kategorien verbindet
const rootNode = {
id: "root",
name: "Wissen",
description: "Zentrale Wissensbasis",
thought_count: 0
};
// Initiales Pulsen starten
const initialPulse = () => {
const node = randomNode();
this.pulseNode(node);
let nodes = [rootNode];
let links = [];
// Für jede Kategorie Knoten und Verbindungen erstellen
apiData.forEach(category => {
// Kategorie als Knoten hinzufügen
const categoryNode = {
id: `category_${category.id}`,
name: category.name,
description: category.description,
color_code: category.color_code,
icon: category.icon,
thought_count: 0,
type: 'category'
};
// Nächstes Pulsen in 3-7 Sekunden
setTimeout(() => {
const nextNode = randomNode();
this.pulseNode(nextNode);
// Regelmäßig wiederholen
setInterval(() => {
const pulseNode = randomNode();
this.pulseNode(pulseNode);
}, 5000 + Math.random() * 5000);
}, 3000 + Math.random() * 4000);
};
nodes.push(categoryNode);
// Mit Root-Knoten verbinden
links.push({
source: "root",
target: categoryNode.id
});
// Alle Knoten aus dieser Kategorie hinzufügen
if (category.nodes && category.nodes.length > 0) {
category.nodes.forEach(node => {
// Zähle die Gedanken für die Kategorie
categoryNode.thought_count += node.thought_count || 0;
const mindmapNode = {
id: `node_${node.id}`,
name: node.name,
description: node.description || '',
color_code: node.color_code || category.color_code,
thought_count: node.thought_count || 0,
type: 'node',
categoryId: category.id
};
nodes.push(mindmapNode);
// Mit Kategorie-Knoten verbinden
links.push({
source: categoryNode.id,
target: mindmapNode.id
});
});
}
// Rekursiv Unterkategorien verarbeiten
if (category.children && category.children.length > 0) {
this.processSubcategories(category.children, nodes, links, categoryNode.id);
}
});
// Verzögertes Starten nach vollständigem Laden
setTimeout(initialPulse, 1000);
// Root-Knoten-Gedankenzähler aktualisieren
rootNode.thought_count = nodes.reduce((sum, node) => sum + (node.thought_count || 0), 0);
return { nodes, links };
}
// Lässt einen Knoten pulsieren für visuelle Hervorhebung
pulseNode(node) {
if (!this.nodeElements) return;
const nodeElement = this.nodeElements.filter(d => d.id === node.id);
if (nodeElement.size() > 0) {
const circle = nodeElement.select('circle');
// Verarbeitet Unterkategorien rekursiv
processSubcategories(subcategories, nodes, links, parentId) {
subcategories.forEach(category => {
// Kategorie als Knoten hinzufügen
const categoryNode = {
id: `category_${category.id}`,
name: category.name,
description: category.description,
color_code: category.color_code,
icon: category.icon,
thought_count: 0,
type: 'subcategory'
};
// Speichern des ursprünglichen Radius
const originalRadius = circle.attr('r');
nodes.push(categoryNode);
// Animiertes Pulsieren
circle.transition()
.duration(600)
.attr('r', originalRadius * 1.3)
.attr('filter', 'url(#pulse-effect)')
.transition()
.duration(600)
.attr('r', originalRadius)
.attr('filter', 'url(#glass-effect)');
}
// Mit Eltern-Kategorie verbinden
links.push({
source: parentId,
target: categoryNode.id
});
// Alle Knoten aus dieser Kategorie hinzufügen
if (category.nodes && category.nodes.length > 0) {
category.nodes.forEach(node => {
// Zähle die Gedanken für die Kategorie
categoryNode.thought_count += node.thought_count || 0;
const mindmapNode = {
id: `node_${node.id}`,
name: node.name,
description: node.description || '',
color_code: node.color_code || category.color_code,
thought_count: node.thought_count || 0,
type: 'node',
categoryId: category.id
};
nodes.push(mindmapNode);
// Mit Kategorie-Knoten verbinden
links.push({
source: categoryNode.id,
target: mindmapNode.id
});
});
}
// Rekursiv Unterkategorien verarbeiten
if (category.children && category.children.length > 0) {
this.processSubcategories(category.children, nodes, links, categoryNode.id);
}
});
}
// Zeigt den Ladebildschirm an
@@ -586,11 +666,20 @@ class MindMapVisualization {
}
}
// Farbe basierend auf Knotentyp erhalten
// Bestimmt die Farbe eines Knotens basierend auf seinem Typ oder direkt angegebener Farbe
getNodeColor(node) {
// Verwende die ID als Typ, falls vorhanden
const nodeType = node.id.toLowerCase();
return this.colorPalette[nodeType] || this.colorPalette.default;
// Direkt angegebene Farbe verwenden, wenn vorhanden
if (node.color_code) {
return node.color_code;
}
// Kategorietyp-basierte Färbung
if (node.type === 'category' || node.type === 'subcategory') {
return this.colorPalette.root;
}
// Fallback für verschiedene Knotentypen
return this.colorPalette[node.id] || this.colorPalette.default;
}
// Aktualisiert die Positionen in jedem Simulationsschritt
@@ -1031,7 +1120,7 @@ class MindMapVisualization {
// Verzögerung für Animation
setTimeout(() => {
// API-Aufruf simulieren (später durch echten Aufruf ersetzen)
// API-Aufruf für echte Daten aus der Datenbank
this.fetchThoughtsForNode(node.id)
.then(thoughts => {
// Ladeanimation ausblenden
@@ -1056,6 +1145,128 @@ class MindMapVisualization {
}, 600); // Verzögerung für bessere UX
}
// Holt Gedanken für einen Knoten aus der Datenbank
async fetchThoughtsForNode(nodeId) {
try {
// Extrahiere die tatsächliche ID aus dem nodeId Format (z.B. "node_123" oder "category_456")
const id = nodeId.toString().split('_')[1];
if (!id) {
console.warn('Ungültige Node-ID: ', nodeId);
return [];
}
// API-Aufruf an den entsprechenden Endpunkt
const response = await fetch(`/api/nodes/${id}/thoughts`);
if (!response.ok) {
throw new Error(`API-Fehler: ${response.statusText}`);
}
const thoughts = await response.json();
console.log('Geladene Gedanken für Knoten:', thoughts);
return thoughts;
} catch (error) {
console.error('Fehler beim Laden der Gedanken für Knoten:', error);
return [];
}
}
// Rendert die Gedanken in der UI
renderThoughts(thoughts, container) {
if (!container) return;
container.innerHTML = '';
thoughts.forEach(thought => {
const thoughtCard = document.createElement('div');
thoughtCard.className = 'thought-card';
thoughtCard.setAttribute('data-id', thought.id);
const cardColor = thought.color_code || this.colorPalette.default;
thoughtCard.innerHTML = `
<div class="thought-card-header" style="border-left: 4px solid ${cardColor}">
<h3 class="thought-title">${thought.title}</h3>
<div class="thought-meta">
<span class="thought-date">${new Date(thought.created_at).toLocaleDateString('de-DE')}</span>
${thought.author ? `<span class="thought-author">von ${thought.author.username}</span>` : ''}
</div>
</div>
<div class="thought-content">
<p>${thought.abstract || thought.content.substring(0, 150) + '...'}</p>
</div>
<div class="thought-footer">
<div class="thought-keywords">
${thought.keywords ? thought.keywords.split(',').map(kw =>
`<span class="keyword">${kw.trim()}</span>`).join('') : ''}
</div>
<a href="/thoughts/${thought.id}" class="thought-link">Mehr lesen →</a>
</div>
`;
// Event-Listener für Klick auf Gedanken
thoughtCard.addEventListener('click', (e) => {
// Verhindern, dass der Link-Klick den Kartenklick auslöst
if (e.target.tagName === 'A') return;
window.location.href = `/thoughts/${thought.id}`;
});
container.appendChild(thoughtCard);
});
}
// Rendert eine Leermeldung, wenn keine Gedanken vorhanden sind
renderEmptyThoughts(container, node) {
if (!container) return;
const emptyState = document.createElement('div');
emptyState.className = 'empty-thoughts-state';
emptyState.innerHTML = `
<div class="empty-icon">
<i class="fas fa-lightbulb"></i>
</div>
<h3>Keine Gedanken verknüpft</h3>
<p>Zu "${node.name}" sind noch keine Gedanken verknüpft.</p>
<div class="empty-actions">
<a href="/add-thought?node=${node.id}" class="btn btn-primary">
<i class="fas fa-plus-circle"></i> Gedanken hinzufügen
</a>
</div>
`;
container.appendChild(emptyState);
}
// Rendert einen Fehlerzustand
renderErrorState(container) {
if (!container) return;
const errorState = document.createElement('div');
errorState.className = 'error-thoughts-state';
errorState.innerHTML = `
<div class="error-icon">
<i class="fas fa-exclamation-triangle"></i>
</div>
<h3>Fehler beim Laden</h3>
<p>Die Gedanken konnten nicht geladen werden. Bitte versuche es später erneut.</p>
<button class="btn btn-secondary retry-button">
<i class="fas fa-redo"></i> Erneut versuchen
</button>
`;
// Event-Listener für Retry-Button
const retryButton = errorState.querySelector('.retry-button');
if (retryButton && this.selectedNode) {
retryButton.addEventListener('click', () => {
this.loadThoughtsForNode(this.selectedNode);
});
}
container.appendChild(errorState);
}
// Zentriert einen Knoten in der Ansicht
centerNodeInView(node) {
// Sanfter Übergang zur Knotenzentrierüng

View File

@@ -1,7 +0,0 @@
/* Dies ist ein Platzhalter für das Netzwerk-Hintergrundbild mit Base64-Kodierung.
Das eigentliche Bild sollte hier durch eine echte JPG-Datei ersetzt werden.
Empfohlene Bildgröße: mindestens 1920x1080px
Optimaler Stil: Dunkler Hintergrund mit abstrakten Verbindungslinien und Punkten
Da wir keine echte JPG-Datei erstellen können, verwende stattdessen eine SVG-Datei mit dem gleichen Namen. */

View File

@@ -1,99 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1920" height="1080" viewBox="0 0 1920 1080">
<defs>
<linearGradient id="bg-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#0e1220" />
<stop offset="100%" stop-color="#1a1f38" />
</linearGradient>
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="4" result="blur" />
<feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter>
</defs>
<!-- Background -->
<rect width="100%" height="100%" fill="url(#bg-gradient)" />
<!-- Connection Lines -->
<g stroke-opacity="0.3" filter="url(#glow)">
<line x1="200" y1="300" x2="500" y2="200" stroke="#8b5cf6" stroke-width="1.5" />
<line x1="500" y1="200" x2="800" y2="400" stroke="#8b5cf6" stroke-width="1.5" />
<line x1="800" y1="400" x2="1100" y2="300" stroke="#58a9ff" stroke-width="1.5" />
<line x1="1100" y1="300" x2="1400" y2="500" stroke="#58a9ff" stroke-width="1.5" />
<line x1="1400" y1="500" x2="1600" y2="200" stroke="#7e3ff2" stroke-width="1.5" />
<line x1="200" y1="300" x2="400" y2="600" stroke="#7e3ff2" stroke-width="1.5" />
<line x1="400" y1="600" x2="700" y2="800" stroke="#8b5cf6" stroke-width="1.5" />
<line x1="700" y1="800" x2="1000" y2="700" stroke="#58a9ff" stroke-width="1.5" />
<line x1="1000" y1="700" x2="1300" y2="800" stroke="#8b5cf6" stroke-width="1.5" />
<line x1="1300" y1="800" x2="1600" y2="600" stroke="#7e3ff2" stroke-width="1.5" />
<line x1="1600" y1="600" x2="1700" y2="900" stroke="#58a9ff" stroke-width="1.5" />
<line x1="400" y1="200" x2="600" y2="350" stroke="#7e3ff2" stroke-width="1.5" />
<line x1="600" y1="350" x2="900" y2="250" stroke="#8b5cf6" stroke-width="1.5" />
<line x1="900" y1="250" x2="1200" y2="400" stroke="#58a9ff" stroke-width="1.5" />
<line x1="1200" y1="400" x2="1500" y2="300" stroke="#8b5cf6" stroke-width="1.5" />
<line x1="300" y1="700" x2="550" y2="550" stroke="#7e3ff2" stroke-width="1.5" />
<line x1="550" y1="550" x2="800" y2="650" stroke="#58a9ff" stroke-width="1.5" />
<line x1="800" y1="650" x2="1100" y2="550" stroke="#8b5cf6" stroke-width="1.5" />
<line x1="1100" y1="550" x2="1400" y2="650" stroke="#7e3ff2" stroke-width="1.5" />
<line x1="1400" y1="650" x2="1650" y2="450" stroke="#58a9ff" stroke-width="1.5" />
<line x1="1650" y1="450" x2="1800" y2="500" stroke="#8b5cf6" stroke-width="1.5" />
</g>
<!-- Connection Points -->
<g fill-opacity="0.8" filter="url(#glow)">
<circle cx="200" cy="300" r="4" fill="#8b5cf6" />
<circle cx="500" cy="200" r="5" fill="#8b5cf6" />
<circle cx="800" cy="400" r="6" fill="#8b5cf6" />
<circle cx="1100" cy="300" r="5" fill="#58a9ff" />
<circle cx="1400" cy="500" r="4" fill="#58a9ff" />
<circle cx="1600" cy="200" r="6" fill="#7e3ff2" />
<circle cx="400" cy="600" r="5" fill="#7e3ff2" />
<circle cx="700" cy="800" r="4" fill="#8b5cf6" />
<circle cx="1000" cy="700" r="6" fill="#58a9ff" />
<circle cx="1300" cy="800" r="5" fill="#8b5cf6" />
<circle cx="1600" cy="600" r="4" fill="#7e3ff2" />
<circle cx="1700" cy="900" r="6" fill="#58a9ff" />
<circle cx="400" cy="200" r="4" fill="#7e3ff2" />
<circle cx="600" cy="350" r="5" fill="#7e3ff2" />
<circle cx="900" cy="250" r="6" fill="#8b5cf6" />
<circle cx="1200" cy="400" r="4" fill="#58a9ff" />
<circle cx="1500" cy="300" r="5" fill="#8b5cf6" />
<circle cx="300" cy="700" r="6" fill="#7e3ff2" />
<circle cx="550" cy="550" r="4" fill="#7e3ff2" />
<circle cx="800" cy="650" r="5" fill="#58a9ff" />
<circle cx="1100" cy="550" r="6" fill="#8b5cf6" />
<circle cx="1400" cy="650" r="4" fill="#7e3ff2" />
<circle cx="1650" cy="450" r="5" fill="#58a9ff" />
<circle cx="1800" cy="500" r="6" fill="#8b5cf6" />
</g>
<!-- Stars/Dots in Background -->
<g fill="#ffffff" fill-opacity="0.3">
<circle cx="250" cy="150" r="1" />
<circle cx="450" cy="350" r="1" />
<circle cx="650" cy="150" r="1" />
<circle cx="850" cy="350" r="1" />
<circle cx="1050" cy="150" r="1" />
<circle cx="1250" cy="350" r="1" />
<circle cx="1450" cy="150" r="1" />
<circle cx="1650" cy="350" r="1" />
<circle cx="1850" cy="150" r="1" />
<circle cx="250" cy="550" r="1" />
<circle cx="450" cy="750" r="1" />
<circle cx="650" cy="550" r="1" />
<circle cx="850" cy="750" r="1" />
<circle cx="1050" cy="550" r="1" />
<circle cx="1250" cy="750" r="1" />
<circle cx="1450" cy="550" r="1" />
<circle cx="1650" cy="750" r="1" />
<circle cx="1850" cy="550" r="1" />
<circle cx="250" cy="950" r="1" />
<circle cx="450" cy="850" r="1" />
<circle cx="650" cy="950" r="1" />
<circle cx="850" cy="850" r="1" />
<circle cx="1050" cy="950" r="1" />
<circle cx="1250" cy="850" r="1" />
<circle cx="1450" cy="950" r="1" />
<circle cx="1650" cy="850" r="1" />
<circle cx="1850" cy="950" r="1" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -3,45 +3,52 @@
{% block title %}Anmelden{% endblock %}
{% block content %}
<div class="row justify-content-center mt-5">
<div class="col-md-6 col-lg-5">
<div class="glass fade-in p-4 p-md-5">
<h2 class="text-center mb-4 text-gray-800">
<i class="fas fa-sign-in-alt me-2"></i>
Anmelden
</h2>
<form method="POST" action="{{ url_for('login') }}">
<div class="mb-3">
<label for="username" class="form-label text-gray-700">Benutzername</label>
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-user"></i>
</span>
<input type="text" class="form-control" id="username" name="username" required>
<div class="flex justify-center items-center min-h-screen px-4 py-12">
<div class="w-full max-w-md">
<div class="bg-white bg-opacity-20 backdrop-blur-lg rounded-xl shadow-xl overflow-hidden transition-all duration-300 hover:shadow-2xl">
<div class="p-6 sm:p-8">
<h2 class="text-center text-2xl font-bold text-gray-800 mb-6">
<i class="fas fa-sign-in-alt mr-2"></i>
Anmelden
</h2>
<form method="POST" action="{{ url_for('login') }}" class="space-y-6">
<div class="space-y-2">
<label for="username" class="block text-sm font-medium text-gray-700">Benutzername</label>
<div class="relative rounded-md shadow-sm">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-user text-gray-400"></i>
</div>
<input type="text" id="username" name="username" required
class="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
placeholder="Benutzername eingeben">
</div>
</div>
</div>
<div class="mb-4">
<label for="password" class="form-label text-gray-700">Passwort</label>
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-lock"></i>
</span>
<input type="password" class="form-control" id="password" name="password" required>
<div class="space-y-2">
<label for="password" class="block text-sm font-medium text-gray-700">Passwort</label>
<div class="relative rounded-md shadow-sm">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-lock text-gray-400"></i>
</div>
<input type="password" id="password" name="password" required
class="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
placeholder="Passwort eingeben">
</div>
</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">
<i class="fas fa-sign-in-alt me-2"></i> Anmelden
</button>
</div>
<div class="text-center mt-4 text-gray-700">
<p>Noch kein Konto? <a href="{{ url_for('register') }}" class="text-blue-600 hover:text-blue-800">Registrieren</a></p>
</div>
</form>
<div>
<button type="submit"
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200">
<i class="fas fa-sign-in-alt mr-2"></i> Anmelden
</button>
</div>
<div class="text-center text-sm text-gray-600">
<p>Noch kein Konto? <a href="{{ url_for('register') }}" class="font-medium text-blue-600 hover:text-blue-500 transition-colors">Registrieren</a></p>
</div>
</form>
</div>
</div>
</div>
</div>

View File

@@ -3,56 +3,113 @@
{% block title %}Registrieren{% endblock %}
{% block content %}
<div class="row justify-content-center mt-5">
<div class="col-md-6 col-lg-5">
<div class="glass fade-in p-4 p-md-5">
<h2 class="text-center mb-4 text-gray-800">
<i class="fas fa-user-plus me-2"></i>
<div class="flex justify-center items-center mt-10 px-4">
<div class="w-full max-w-md">
<div class="bg-white bg-opacity-80 backdrop-blur-lg rounded-lg shadow-md border border-white border-opacity-30 p-6 md:p-8 transition-all duration-300 transform hover:shadow-lg">
<h2 class="text-center mb-6 text-gray-800 font-bold text-2xl flex items-center justify-center">
<i class="fas fa-user-plus mr-2 text-blue-600"></i>
Registrieren
</h2>
<form method="POST" action="{{ url_for('register') }}">
<div class="mb-3">
<label for="username" class="form-label text-gray-700">Benutzername</label>
<div class="input-group">
<span class="input-group-text">
<form method="POST" action="{{ url_for('register') }}" class="needs-validation space-y-6" novalidate>
<div class="space-y-2">
<label for="username" class="block text-gray-700 font-medium text-sm">Benutzername</label>
<div class="relative flex items-center">
<span class="absolute left-3 text-blue-600">
<i class="fas fa-user"></i>
</span>
<input type="text" class="form-control" id="username" name="username" required>
<input type="text" class="pl-10 w-full rounded-md border border-gray-300 py-2 px-4 focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 transition-all duration-200"
id="username" name="username" placeholder="Dein Benutzername" required>
</div>
<div class="invalid-feedback text-red-600 text-sm hidden">
Bitte gib einen Benutzernamen ein.
</div>
</div>
<div class="mb-3">
<label for="email" class="form-label text-gray-700">E-Mail</label>
<div class="input-group">
<span class="input-group-text">
<div class="space-y-2">
<label for="email" class="block text-gray-700 font-medium text-sm">E-Mail</label>
<div class="relative flex items-center">
<span class="absolute left-3 text-blue-600">
<i class="fas fa-envelope"></i>
</span>
<input type="email" class="form-control" id="email" name="email" required>
<input type="email" class="pl-10 w-full rounded-md border border-gray-300 py-2 px-4 focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 transition-all duration-200"
id="email" name="email" placeholder="name@beispiel.de" required>
</div>
<div class="invalid-feedback text-red-600 text-sm hidden">
Bitte gib eine gültige E-Mail-Adresse ein.
</div>
</div>
<div class="mb-4">
<label for="password" class="form-label text-gray-700">Passwort</label>
<div class="input-group">
<span class="input-group-text">
<div class="space-y-2">
<label for="password" class="block text-gray-700 font-medium text-sm">Passwort</label>
<div class="relative flex items-center">
<span class="absolute left-3 text-blue-600">
<i class="fas fa-lock"></i>
</span>
<input type="password" class="form-control" id="password" name="password" required>
<input type="password" class="pl-10 w-full rounded-md border border-gray-300 py-2 px-4 focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 transition-all duration-200"
id="password" name="password" placeholder="Mindestens 8 Zeichen" required>
<button class="absolute right-2 text-gray-500 hover:text-gray-700 focus:outline-none" type="button" id="togglePassword">
<i class="fas fa-eye"></i>
</button>
</div>
<div class="invalid-feedback text-red-600 text-sm hidden">
Bitte gib ein sicheres Passwort ein.
</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">
<i class="fas fa-user-plus me-2"></i> Konto erstellen
<div class="pt-2">
<button type="submit" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-md shadow-sm transition-all duration-200 transform hover:scale-[1.02] focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50">
<i class="fas fa-user-plus mr-2"></i> Konto erstellen
</button>
</div>
<div class="text-center mt-4 text-gray-700">
<p>Bereits registriert? <a href="{{ url_for('login') }}" class="text-blue-600 hover:text-blue-800">Anmelden</a></p>
<div class="text-center mt-4 text-gray-700 text-sm">
<p>Bereits registriert? <a href="{{ url_for('login') }}" class="text-blue-600 hover:text-blue-800 font-medium transition-colors duration-200">Anmelden</a></p>
</div>
</form>
</div>
</div>
</div>
<script>
// Formularvalidierung aktivieren
(function() {
'use strict';
var forms = document.querySelectorAll('.needs-validation');
Array.prototype.slice.call(forms).forEach(function(form) {
form.addEventListener('submit', function(event) {
if (!form.checkValidity()) {
event.preventDefault();
event.stopPropagation();
// Zeige Fehlermeldungen an
form.querySelectorAll(':invalid').forEach(function(input) {
input.parentNode.nextElementSibling.classList.remove('hidden');
});
}
form.classList.add('was-validated');
}, false);
// Verstecke Fehlermeldungen bei Eingabe
form.querySelectorAll('input').forEach(function(input) {
input.addEventListener('input', function() {
if (this.checkValidity()) {
this.parentNode.nextElementSibling.classList.add('hidden');
}
});
});
});
// Passwort-Sichtbarkeit umschalten
const togglePassword = document.querySelector('#togglePassword');
const password = document.querySelector('#password');
togglePassword.addEventListener('click', function() {
const type = password.getAttribute('type') === 'password' ? 'text' : 'password';
password.setAttribute('type', type);
this.querySelector('i').classList.toggle('fa-eye');
this.querySelector('i').classList.toggle('fa-eye-slash');
});
})();
</script>
{% endblock %}