espid2sensor in Hauptstack aufnehmen, verschachteltes Git-Repo aufgeloest
espid2sensor lief bisher nur per eigenem Dev-Compose mit isolierter Wegwerf-Mongo. Jetzt Teil von noisesensors/docker-compose.yml, Port 3004, Zugriff auf die dort laufende mongodb-Instanz (sensor_data). DEV_AUTO_LOGIN explizit aus, SESSION_SECRET ueber neue Env-Variable statt Code-Default. Dockerfile_esp2sensor an das Muster der anderen Services angeglichen (node:22-alpine, /opt/app, npm ci, tzdata/Europe-Berlin, deluser node). espid2sensor/.git entfernt (war eigenes verschachteltes Repo) und der komplette Verzeichnisinhalt ins laermsensor-stack-Repo uebernommen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,418 @@
|
||||
// Tab-Wechsel Funktion aus index.pug
|
||||
function showTab(tab) {
|
||||
document.getElementById('tabInputContent').style.display = tab === 'input' ? '' : 'none';
|
||||
document.getElementById('tabListContent').style.display = tab === 'list' ? '' : 'none';
|
||||
const tabUserContent = document.getElementById('tabUserContent');
|
||||
if (tabUserContent) tabUserContent.style.display = tab === 'user' ? '' : 'none';
|
||||
document.getElementById('tabInput').classList.toggle('active', tab === 'input');
|
||||
document.getElementById('tabList').classList.toggle('active', tab === 'list');
|
||||
const tabUser = document.getElementById('tabUser');
|
||||
if (tabUser) tabUser.classList.toggle('active', tab === 'user');
|
||||
}
|
||||
|
||||
// User-Tab Handling (nur für Admins)
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const userSaveBtn = document.getElementById('userSaveBtn');
|
||||
if (userSaveBtn) {
|
||||
userSaveBtn.addEventListener('click', async () => {
|
||||
const username = document.getElementById('username').value.trim();
|
||||
const password = document.getElementById('password').value.trim();
|
||||
const role = document.getElementById('role').value;
|
||||
const userResult = document.getElementById('userResult');
|
||||
if (!username || !password) {
|
||||
userResult.textContent = 'Benutzername und Passwort erforderlich.';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await fetch('/api/createUser', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password, role })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
userResult.textContent = 'User erfolgreich angelegt!';
|
||||
} else {
|
||||
userResult.textContent = data.error || 'Fehler beim Anlegen.';
|
||||
}
|
||||
} catch (err) {
|
||||
userResult.textContent = 'Serverfehler.';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function updateSortArrows() {
|
||||
const arrows = {
|
||||
sensorNr: document.getElementById('sortArrowSensorNr'),
|
||||
espId: document.getElementById('sortArrowEspId'),
|
||||
date: document.getElementById('sortArrowDate')
|
||||
};
|
||||
Object.entries(arrows).forEach(([key, el]) => {
|
||||
if (!el) return;
|
||||
// Aktiver Pfeil fett, andere ausgegraut
|
||||
el.textContent = currentSort.key === key
|
||||
? (currentSort.asc ? '↑' : '↓')
|
||||
: '↑';
|
||||
el.style.fontWeight = currentSort.key === key ? 'bold' : 'normal';
|
||||
el.style.opacity = currentSort.key === key ? '1' : '0.3';
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const saveBtn = document.getElementById('saveBtn');
|
||||
const refreshBtn = document.getElementById('refreshBtn');
|
||||
const espIdInput = document.getElementById('espId');
|
||||
const sensorNumberInput = document.getElementById('sensorNumber');
|
||||
const nameInput = document.getElementById('name');
|
||||
const descriptionInput = document.getElementById('description');
|
||||
const addressInput = document.getElementById('address');
|
||||
const pageInput = document.getElementById('page');
|
||||
const limitInput = document.getElementById('limit');
|
||||
const resultDiv = document.getElementById('result');
|
||||
const tableBody = document.querySelector('#entriesTable tbody');
|
||||
const tabInput = document.getElementById('tabInput');
|
||||
const tabList = document.getElementById('tabList');
|
||||
|
||||
|
||||
// Modal für Fehleranzeige
|
||||
function showModal(message, showCancelButton, callback) {
|
||||
// Remove previous modals
|
||||
document.querySelectorAll('.custom-modal-popup').forEach(m => m.remove());
|
||||
|
||||
let modal = document.createElement('div');
|
||||
modal.className = 'custom-modal-popup';
|
||||
|
||||
let box = document.createElement('div');
|
||||
box.className = 'custom-modal-box';
|
||||
|
||||
let msg = document.createElement('div');
|
||||
msg.className = 'custom-modal-msg';
|
||||
msg.textContent = message;
|
||||
box.appendChild(msg);
|
||||
|
||||
let btndiv = document.createElement('div')
|
||||
btndiv.className = 'twobuttons'
|
||||
|
||||
// Cancel Button (only if showCancelButton is true)
|
||||
if (showCancelButton) {
|
||||
let btnCancel = document.createElement('button');
|
||||
btnCancel.className = 'custom-modal-btn';
|
||||
btnCancel.textContent = 'Abbruch';
|
||||
btnCancel.onclick = () => {
|
||||
if (modal.parentNode) {
|
||||
modal.parentNode.removeChild(modal);
|
||||
}
|
||||
if (callback) callback(false); // Pass false for Cancel
|
||||
};
|
||||
btndiv.appendChild(btnCancel);
|
||||
}
|
||||
|
||||
// OK Button
|
||||
let btnOk = document.createElement('button');
|
||||
btnOk.className = 'custom-modal-btn';
|
||||
btnOk.textContent = 'OK';
|
||||
btnOk.onclick = () => {
|
||||
if (modal.parentNode) {
|
||||
modal.parentNode.removeChild(modal);
|
||||
}
|
||||
if (callback) callback(true); // Pass true for OK
|
||||
};
|
||||
|
||||
btndiv.appendChild(btnOk);
|
||||
box.appendChild(btndiv)
|
||||
|
||||
|
||||
modal.appendChild(box);
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Optional: Close modal when clicking outside
|
||||
modal.onclick = (e) => {
|
||||
if (e.target === modal) {
|
||||
if (modal.parentNode) {
|
||||
modal.parentNode.removeChild(modal);
|
||||
}
|
||||
if (callback) callback(false); // Treat as cancel
|
||||
}
|
||||
};
|
||||
}
|
||||
// Sensornummer nur Zahlen erlauben
|
||||
sensorNumberInput.addEventListener('input', () => {
|
||||
sensorNumberInput.value = sensorNumberInput.value.replace(/\D/g, '');
|
||||
});
|
||||
|
||||
// Adresse vom Server holen, wenn Enter oder Feld verlassen
|
||||
async function fetchAddressIfValid() {
|
||||
const value = sensorNumberInput.value.trim();
|
||||
if (value.length > 0) {
|
||||
try {
|
||||
const res = await fetch(`/api/address/${value}`);
|
||||
const data = await res.json();
|
||||
console.dir(data)
|
||||
if (!data.error && data.address) {
|
||||
addressInput.value = data.address;
|
||||
// Felder automatisch füllen, wenn props vorhanden
|
||||
if (!data.props.error) {
|
||||
if (Object.hasOwn(data.props, "chip")) {
|
||||
let pp = data.props.chip
|
||||
espIdInput.value = pp.id || ''
|
||||
nameInput.value = pp.name || ''
|
||||
descriptionInput.value = pp.description || ''
|
||||
// Weitere Felder nach Bedarf
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addressInput.value = '';
|
||||
sensorNumberInput.disabled = true;
|
||||
showModal('Sensor unbekannt', false, () => {
|
||||
sensorNumberInput.disabled = false;
|
||||
sensorNumberInput.focus();
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Abrufen der Adresse:', err);
|
||||
addressInput.value = '';
|
||||
sensorNumberInput.disabled = true;
|
||||
showModal('Sensor unbekannt', false, () => {
|
||||
sensorNumberInput.disabled = false;
|
||||
sensorNumberInput.focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enter-Taste
|
||||
sensorNumberInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
fetchAddressIfValid();
|
||||
}
|
||||
});
|
||||
|
||||
// Feld verlassen
|
||||
sensorNumberInput.addEventListener('blur', fetchAddressIfValid);
|
||||
|
||||
|
||||
async function saveEntry() {
|
||||
const espId = espIdInput.value.trim();
|
||||
const sensorNumber = sensorNumberInput.value.trim();
|
||||
const name = nameInput.value.trim();
|
||||
const description = descriptionInput.value.trim();
|
||||
const address = addressInput.value.trim();
|
||||
|
||||
if (!espId || !sensorNumber) {
|
||||
resultDiv.textContent = 'ESP-ID und Sensornummer sind Pflichtfelder.';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = '/api/save';
|
||||
const method = 'POST';
|
||||
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ espId, sensorNumber, name, description, address })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (data.error) {
|
||||
resultDiv.textContent = data.error;
|
||||
} else {
|
||||
resultDiv.textContent = 'OK!';
|
||||
setTimeout(() => {
|
||||
resultDiv.textContent = ''
|
||||
saveBtn.textContent = 'Speichern';
|
||||
}, 5000)
|
||||
clearForm(false);
|
||||
await loadEntries();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
resultDiv.textContent = 'Fehler beim Speichern.';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function clearForm(mitButton) {
|
||||
espIdInput.value = '';
|
||||
sensorNumberInput.value = '';
|
||||
nameInput.value = '';
|
||||
descriptionInput.value = '';
|
||||
addressInput.value = '';
|
||||
if (mitButton) {
|
||||
saveBtn.textContent = 'Speichern';
|
||||
}
|
||||
}
|
||||
|
||||
const clearUserForm = () => {
|
||||
document.getElementById('username').value = ''
|
||||
document.getElementById('password').value = ''
|
||||
document.getElementById('role').value = 'user'
|
||||
}
|
||||
|
||||
// Globale Sortier-Variable
|
||||
window.currentSort = window.currentSort || { key: null, asc: true };
|
||||
|
||||
async function loadEntries() {
|
||||
const page = parseInt(pageInput.value) || 1;
|
||||
const limit = parseInt(limitInput.value) || 50;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/list?page=${page}&limit=${limit}`);
|
||||
const erg = await res.json();
|
||||
const items = erg.items
|
||||
const gz = document.getElementById('gzahl');
|
||||
gz.innerHTML = `Gesamtzahl: ${erg.anzahl}`
|
||||
|
||||
let currentSort = window.currentSort || { key: null, asc: true };
|
||||
function renderTable(sortedItems) {
|
||||
tableBody.innerHTML = '';
|
||||
sortedItems.forEach(item => {
|
||||
const date = new Date(item.chip.lastUpdatedAt).toISOString().split('T')[0];
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td id="tdSensornumber">${item._id}</td>
|
||||
<td>${item.chip.id}</td>
|
||||
<td>${item.chip.name || ''}</td>
|
||||
<td id="tdBeschreibung">${item.chip.description || ''}</td>
|
||||
<td id="tdDate">${date}</td>
|
||||
<td>
|
||||
<div class="twobuttons">
|
||||
<button data-id="${item._id}" class="editBtn" title="Bearbeiten">✏️</button>
|
||||
<button data-id="${item._id}" class="deleteBtn" title="Löschen">🗑️</button>
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
tableBody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
function sortItems(items, key, asc) {
|
||||
return items.slice().sort((a, b) => {
|
||||
let valA, valB;
|
||||
if (key === 'sensorNr') {
|
||||
valA = a._id;
|
||||
valB = b._id;
|
||||
} else if (key === 'espId') {
|
||||
valA = a.chip.id;
|
||||
valB = b.chip.id;
|
||||
} else if (key === 'date') {
|
||||
valA = new Date(a.chip.lastUpdatedAt);
|
||||
valB = new Date(b.chip.lastUpdatedAt);
|
||||
}
|
||||
if (valA < valB) return asc ? -1 : 1;
|
||||
if (valA > valB) return asc ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
// Initial render: Standard nach SensorNr, ESP-ID oder Datum aufsteigend
|
||||
// Ändere hier die Spalte für die Standardsortierung:
|
||||
const defaultSortKey = window.currentSort && window.currentSort.key ? window.currentSort.key : 'sensorNr';
|
||||
const defaultSortAsc = window.currentSort && typeof window.currentSort.asc === 'boolean' ? window.currentSort.asc : true;
|
||||
currentSort.key = defaultSortKey;
|
||||
currentSort.asc = defaultSortAsc;
|
||||
window.currentSort = currentSort;
|
||||
renderTable(sortItems(items, defaultSortKey, defaultSortAsc));
|
||||
updateSortArrows();
|
||||
|
||||
// Add sort listeners
|
||||
document.getElementById('thSensorNr').onclick = () => {
|
||||
currentSort.asc = currentSort.key === 'sensorNr' ? !currentSort.asc : true;
|
||||
currentSort.key = 'sensorNr';
|
||||
window.currentSort = currentSort;
|
||||
renderTable(sortItems(items, 'sensorNr', currentSort.asc));
|
||||
updateSortArrows();
|
||||
};
|
||||
document.getElementById('thEspId').onclick = () => {
|
||||
currentSort.asc = currentSort.key === 'espId' ? !currentSort.asc : true;
|
||||
currentSort.key = 'espId';
|
||||
window.currentSort = currentSort;
|
||||
renderTable(sortItems(items, 'espId', currentSort.asc));
|
||||
updateSortArrows();
|
||||
};
|
||||
document.getElementById('thDate').onclick = () => {
|
||||
currentSort.asc = currentSort.key === 'date' ? !currentSort.asc : true;
|
||||
currentSort.key = 'date';
|
||||
window.currentSort = currentSort;
|
||||
renderTable(sortItems(items, 'date', currentSort.asc));
|
||||
updateSortArrows();
|
||||
};
|
||||
|
||||
document.querySelectorAll('.deleteBtn').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const id = btn.getAttribute('data-id');
|
||||
await deleteEntry(id);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.editBtn').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const id = btn.getAttribute('data-id');
|
||||
const res = await fetch(`/api/list?page=1&limit=1&id=${id}`);
|
||||
const items = await res.json();
|
||||
const item = items.find(e => e._id === parseInt(id));
|
||||
if (item) {
|
||||
espIdInput.value = item.chip.id;
|
||||
sensorNumberInput.value = item._id;
|
||||
nameInput.value = item.chip.name || '';
|
||||
descriptionInput.value = item.chip.description || '';
|
||||
addressInput.value = '';
|
||||
saveBtn.textContent = 'Aktualisieren';
|
||||
showTab('input')
|
||||
try {
|
||||
const rt = await fetch(`api/holAdresse/${item._id}`)
|
||||
const data = await rt.json();
|
||||
console.dir(data)
|
||||
if (!data.error && data.address) {
|
||||
addressInput.value = data.address;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Fehler beim Adresse holen", e)
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
resultDiv.textContent = 'Fehler beim Laden.';
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteEntry(id) {
|
||||
showModal('Wirklich löschen?', true, async (confirmed) => {
|
||||
if (confirmed) {
|
||||
try {
|
||||
const res = await fetch(`/api/delete/${id}`, { method: 'DELETE' });
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
await loadEntries();
|
||||
resultDiv.textContent = 'Eintrag gelöscht.';
|
||||
setTimeout(() => resultDiv.textContent = '', 3000);
|
||||
} else {
|
||||
resultDiv.textContent = 'Fehler beim Löschen.';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
resultDiv.textContent = 'Fehler beim Löschen.';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
saveBtn.addEventListener('click', saveEntry);
|
||||
refreshBtn.addEventListener('click', loadEntries);
|
||||
cancelBtn.addEventListener('click', () => clearForm(true));
|
||||
userCancelBtn.addEventListener('click', () => clearUserForm(true));
|
||||
|
||||
tabInput.addEventListener('click', () => showTab('input'))
|
||||
tabList.addEventListener('click', () => showTab('list'))
|
||||
const tabUser = document.getElementById('tabUser');
|
||||
if (tabUser) tabUser.addEventListener('click', () => showTab('user'))
|
||||
|
||||
loadEntries();
|
||||
});
|
||||
|
||||
window.showTab = showTab;
|
||||
@@ -0,0 +1,34 @@
|
||||
// public/login.js
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const emailInput = document.getElementById('email');
|
||||
const emailStatus = document.getElementById('emailStatus');
|
||||
let debounceTimeout;
|
||||
|
||||
emailInput.addEventListener('input', () => {
|
||||
clearTimeout(debounceTimeout);
|
||||
const email = emailInput.value.trim();
|
||||
|
||||
if (!email) {
|
||||
emailStatus.textContent = '';
|
||||
return;
|
||||
}
|
||||
|
||||
debounceTimeout = setTimeout(async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/check-email?email=${encodeURIComponent(email)}`);
|
||||
const data = await res.json();
|
||||
if (data.exists) {
|
||||
emailStatus.textContent = '✅ Benutzer existiert';
|
||||
emailStatus.style.color = 'green';
|
||||
} else {
|
||||
emailStatus.textContent = '❌ Benutzer nicht gefunden';
|
||||
emailStatus.style.color = 'red';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
emailStatus.textContent = 'Fehler bei der Prüfung';
|
||||
emailStatus.style.color = 'orange';
|
||||
}
|
||||
}, 300);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,246 @@
|
||||
/* Tab Navigation */
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.tab-btn {
|
||||
background: #eee;
|
||||
border: none;
|
||||
padding: 0.7rem 2rem;
|
||||
font-size: 1.1rem;
|
||||
border-radius: 6px 6px 0 0;
|
||||
cursor: pointer;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
}
|
||||
.tab-btn.active {
|
||||
background: #007bff;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.10);
|
||||
}
|
||||
|
||||
#tabUser {
|
||||
margin-left: 50px;
|
||||
}
|
||||
|
||||
/* Modal Fehlerfenster */
|
||||
.custom-modal-popup {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(0,0,0,0.4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.custom-modal-box {
|
||||
background: #fff;
|
||||
padding: 3rem;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.25);
|
||||
text-align: center;
|
||||
min-width: 350px;
|
||||
max-width: 90vw;
|
||||
}
|
||||
|
||||
.custom-modal-msg {
|
||||
margin-bottom: 2rem;
|
||||
font-size: 1.5rem;
|
||||
color: red;
|
||||
}
|
||||
|
||||
.custom-modal-btn {
|
||||
padding: 0.8rem 2.5rem;
|
||||
font-size: 1.1rem;
|
||||
background: #007bff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
body {
|
||||
font-family: system-ui, sans-serif;
|
||||
padding: 20px;
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
input, button {
|
||||
font-size: 1rem;
|
||||
padding: 8px;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 1px solid #ddd;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
background-color: antiquewhite;
|
||||
}
|
||||
|
||||
#result {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.controls input {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
th, td {
|
||||
text-align: left;
|
||||
padding: 4px;
|
||||
border-bottom: 1px solid #888;
|
||||
}
|
||||
|
||||
/* Spaltenbreiten über colgroup steuern */
|
||||
col.col-sensornumber { width: 7em; }
|
||||
col.col-espid {width: 9em}
|
||||
col.col-bezeichnung { width: 8em; }
|
||||
col.col-beschreibung{ width: 15em; }
|
||||
col.col-date { width: 10em; }
|
||||
col.col-aktionen { width: 2em; }
|
||||
|
||||
|
||||
.controls input#page,
|
||||
.controls input#limit {
|
||||
width: 50px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
form {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
background: #f9f9f9;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
background-color:burlywood
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 0.4rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.6rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.editBtn, .deleteBtn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.editBtn:hover {
|
||||
background: rgba(0, 123, 255, 0.1);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.deleteBtn:hover {
|
||||
background: rgba(220, 53, 69, 0.1);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.twobuttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
gap: 5px;
|
||||
}
|
||||
p.error {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start; /* Links bündig */
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.card form label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.card form input,
|
||||
.card form textarea {
|
||||
width: 100%;
|
||||
max-width: 400px; /* gleiche Breite */
|
||||
padding: 0.4rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.card form textarea {
|
||||
min-height: 60px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
#gzahl {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
#role {
|
||||
font-size: 12pt;
|
||||
padding: 5px 0 5px 3px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#version {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
font-size: 70%;
|
||||
color: #007bff;
|
||||
margin-top: 15px;
|
||||
}
|
||||
Reference in New Issue
Block a user