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:
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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. */
|
||||
@@ -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 |
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
Reference in New Issue
Block a user