Files
shape3d.it/public/index.html
2026-01-22 00:17:23 +01:00

266 lines
10 KiB
HTML
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>3D-Druck Materialmatrix</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://cdn.tailwindcss.com"></script>
<style>
body {
background: radial-gradient(circle at top, #e2e8f0 0%, #f8fafc 45%, #e2e8f0 90%);
}
thead th {
position: sticky;
top: 0;
backdrop-filter: blur(4px);
}
th[data-printer],
td[data-printer] {
background: rgba(148, 163, 184, 0.05);
}
#disclaimer {
box-shadow: inset 0 1px 0 rgba(148, 163, 184, 0.25);
}
</style>
</head>
<body class="bg-slate-100 min-h-screen">
<div class="w-full sm:w-[90%] mx-auto py-6 space-y-6">
<header class="flex items-center justify-between gap-4">
<div>
<h1 class="text-2xl font-bold tracking-tight text-slate-900">3D-Druck Materialmatrix</h1>
<p class="text-xs text-slate-500">Schnell prüfen, welche Filamente auf welchen Druckern laufen.</p>
</div>
<span id="status" class="text-xs text-slate-500"></span>
</header>
<div class="bg-white/80 backdrop-blur rounded-lg shadow flex gap-6 p-4 min-h-[420px]">
<!-- Sidebar -->
<aside class="w-72 space-y-5 border-r pr-4 border-slate-100">
<div>
<h2 class="text-sm font-semibold text-slate-700 mb-2">Drucker auswählen</h2>
<label class="block text-xs font-medium mb-1 text-slate-600">Einzelansicht</label>
<select id="printerSelect" class="w-full border rounded px-2 py-1 mb-2 text-sm">
<option value=""> wird geladen </option>
</select>
<p class="text-xs text-slate-500">
Zeigt die Kompatibilität nur für diesen Drucker.
</p>
</div>
<div>
<label class="block text-xs font-medium mb-1 text-slate-600">Vergleich (mehrere)</label>
<select id="printerCompare" multiple class="w-full border rounded px-2 py-1 h-32 text-sm"></select>
<p class="text-xs text-slate-500">
Strg/⌘ gedrückt halten, um mehrere zu wählen.
</p>
</div>
<div class="text-xs text-slate-400">
Tipp: Im Vergleich werden die ausgewählten Drucker rechts als separate Spalten eingefärbt.
</div>
</aside>
<!-- Main -->
<main class="flex-1 flex flex-col gap-3">
<div class="overflow-auto max-h-[70vh] rounded border bg-white" id="tableWrap">
<table class="min-w-full text-sm" id="matTable">
<thead class="bg-slate-50">
<tr id="tableHead">
<th class="px-3 py-2 text-left">Material</th>
<th class="px-3 py-2 text-left">Eigenschaften</th>
<th class="px-3 py-2 text-left">Tg °C</th>
<th class="px-3 py-2 text-left">Düse</th>
<th class="px-3 py-2 text-left">Platte</th>
<th class="px-3 py-2 text-left">Zusatz</th>
<th class="px-3 py-2 text-left">Anwendung</th>
<th class="px-3 py-2 text-left">Kinder</th>
<th class="px-3 py-2 text-left">Emission</th>
</tr>
</thead>
<tbody id="matBody"></tbody>
</table>
</div>
<div id="errorBox" class="hidden rounded bg-rose-50 border border-rose-200 text-rose-700 text-sm px-3 py-2"></div>
<!-- Hinweisblock unten -->
<section id="disclaimer" class="mt-2 rounded-lg border border-slate-200 bg-slate-50 p-4 text-xs text-slate-700 leading-snug">
<p>
<strong>Hinweis:</strong> Dieses Projekt wird privat betrieben und befindet sich im Aufbau.
Es sind noch nicht alle Drucker und Materialien eingetragen.
Alle Angaben erfolgen nach bestem Wissen, jedoch <u>ohne Gewähr auf Vollständigkeit oder Richtigkeit</u>.
</p>
</section>
</main>
</div>
</div>
<script>
const API_BASE = './api';
const printerSelect = document.getElementById('printerSelect');
const printerCompare = document.getElementById('printerCompare');
const matBody = document.getElementById('matBody');
const tableHead = document.getElementById('tableHead');
const statusEl = document.getElementById('status');
const errorBox = document.getElementById('errorBox');
function showError(msg) {
errorBox.textContent = msg;
errorBox.classList.remove('hidden');
}
function clearError() {
errorBox.classList.add('hidden');
}
async function fetchJSON(url) {
const res = await fetch(url);
if (!res.ok) throw new Error('HTTP ' + res.status + ' for ' + url);
return await res.json();
}
async function loadPrinters() {
try {
statusEl.textContent = 'Lade Drucker …';
const data = await fetchJSON(`${API_BASE}/printers.php`);
printerSelect.innerHTML = '';
printerCompare.innerHTML = '';
if (!data.length) {
printerSelect.innerHTML = '<option value="">(keine Drucker gefunden)</option>';
statusEl.textContent = 'Keine Drucker gefunden';
return;
}
data.forEach(p => {
printerSelect.appendChild(new Option(p.name, p.id));
printerCompare.appendChild(new Option(p.name, p.id));
});
// ersten Drucker anzeigen
loadSinglePrinter(data[0].id);
statusEl.textContent = 'Drucker geladen';
} catch (err) {
console.error(err);
showError('Konnte Drucker nicht laden. Prüfe public/api/printers.php.');
}
}
async function loadSinglePrinter(id) {
if (!id) return;
clearError();
statusEl.textContent = 'Lade Materialien …';
try {
const data = await fetchJSON(`${API_BASE}/printer-materials.php?id=${id}`);
renderTable([data]);
statusEl.textContent = 'Fertig';
} catch (err) {
console.error(err);
showError('Konnte Materialien für Drucker nicht laden.');
statusEl.textContent = 'Fehler';
}
}
async function loadMultiplePrinters(ids) {
if (!ids.length) return;
clearError();
statusEl.textContent = 'Lade Vergleich …';
try {
const datasets = await Promise.all(ids.map(id => fetchJSON(`${API_BASE}/printer-materials.php?id=${id}`)));
renderTable(datasets);
statusEl.textContent = 'Vergleich geladen';
} catch (err) {
console.error(err);
showError('Konnte einen der gewählten Drucker nicht laden.');
statusEl.textContent = 'Fehler';
}
}
function renderTable(datasets) {
// Kopf neu aufbauen
const baseHead = `
<th class="px-3 py-2 text-left">Material</th>
<th class="px-3 py-2 text-left">Eigenschaften</th>
<th class="px-3 py-2 text-left">Tg °C</th>
<th class="px-3 py-2 text-left">Düse</th>
<th class="px-3 py-2 text-left">Platte</th>
<th class="px-3 py-2 text-left">Zusatz</th>
<th class="px-3 py-2 text-left">Anwendung</th>
<th class="px-3 py-2 text-left">Kinder</th>
<th class="px-3 py-2 text-left">Emission</th>
`;
let printerCols = '';
datasets.forEach(ds => {
printerCols += `<th class="px-3 py-2 text-left bg-slate-100" data-printer="${ds.printer.id}">${ds.printer.name}</th>`;
});
tableHead.innerHTML = baseHead + printerCols;
const materials = datasets[0]?.materials ?? [];
matBody.innerHTML = '';
materials.forEach((m, idx) => {
const tr = document.createElement('tr');
tr.className = idx % 2 === 0 ? 'bg-white' : 'bg-slate-50/60';
const kid = m.kid_safety === 'safe' ? '🌿' : (m.kid_safety === 'limited' ? '🟡' : '🔴');
const em = m.emission === 'low' ? '✅' : (m.emission === 'medium' ? '⚠️' : '⛔');
let html = `
<td class="px-3 py-2 font-medium">${m.code}<div class="text-xs text-slate-500">${m.short_desc ?? ''}</div></td>
<td class="px-3 py-2">${m.properties ?? ''}</td>
<td class="px-3 py-2">${m.tg_celsius ?? ''}</td>
<td class="px-3 py-2">${m.nozzle_req ?? ''}</td>
<td class="px-3 py-2">${m.plate_req ?? ''}</td>
<td class="px-3 py-2">${m.extra_req ?? ''}</td>
<td class="px-3 py-2">${m.application ?? ''}</td>
<td class="px-3 py-2">${kid}</td>
<td class="px-3 py-2">${em}</td>
`;
// Drucker-Spalten
datasets.forEach(ds => {
const match = ds.materials.find(x => x.id === m.id || x.code === m.code);
if (!match || !match.support_level) {
html += `<td class="px-3 py-2" data-printer="${ds.printer.id}"><span class="px-2 py-1 rounded bg-slate-200 text-xs">unbekannt</span></td>`;
} else {
let badge = '';
if (match.support_level === 'full') {
badge = '<span class="px-2 py-1 rounded bg-green-100 text-green-800 text-xs">✓ voll</span>';
} else if (match.support_level === 'partial') {
badge = '<span class="px-2 py-1 rounded bg-amber-100 text-amber-800 text-xs">⚠ teilw.</span>';
} else if (match.support_level === 'with_addon') {
badge = '<span class="px-2 py-1 rounded bg-sky-100 text-sky-800 text-xs">⚙ Zusatz</span>';
} else {
badge = '<span class="px-2 py-1 rounded bg-rose-100 text-rose-800 text-xs">✗ nein</span>';
}
const note = match.partial_reason
? `<div class="text-xs text-slate-400">${match.partial_reason}</div>`
: (match.extra_info ? `<div class="text-xs text-slate-400">${match.extra_info}</div>` : '');
html += `<td class="px-3 py-2" data-printer="${ds.printer.id}">${badge}${note}</td>`;
}
});
tr.innerHTML = html;
matBody.appendChild(tr);
});
}
// Events
printerSelect.addEventListener('change', e => {
const id = e.target.value;
if (id) {
loadSinglePrinter(id);
// Vergleich leeren
printerCompare.selectedIndex = -1;
}
});
printerCompare.addEventListener('change', e => {
const ids = Array.from(e.target.selectedOptions).map(o => o.value);
if (ids.length) {
loadMultiplePrinters(ids);
}
});
// Start
loadPrinters();
</script>
</body>
</html>