Files
3d.lehrstun.de/templates/projekte_katalog.html
Hans Puettmann 8a55e175a2 Style Guide v1.0.0: Indigo-Palette + lokale Web-Fonts
Komplette visuelle Umstellung nach dem generierten Style-Guide
(3d-lehrstun-de-v1.0.0). 3D-Druck-AG bekommt eine eigene Marke,
getrennt vom Teal der arduino.lehrstun.de-Schwester.

Farben: Teal #00979D -> Indigo #5B6EAE, warmes Off-White Surface
  #F0EEE9 statt Kuehlweiss, Lavendel-Akzent #9B8EC4.
Fonts: Raleway (Headings), Lato (Body), Inconsolata (Mono) — lokal
  gehostet in static/fonts/ (DSGVO-konform, kein Google-CDN),
  via @font-face + unicode-range (latin + latin-ext).
Form: Radius/Schatten/Spacing als Tokens (--radius-*, --shadow-*).

Technik:
  - base.html :root: neue --color-*/--font-*/--radius-* Tokens,
    alte Variablennamen (--primary etc.) als Aliase -> alle 33
    Templates ziehen sofort mit, kein Template-Rewrite noetig.
  - Semantische Banner-Tokens (--info/warn/success/error-bg+text+border)
    loesen ~130 Hardcoded-Pastelle ab (per sed gemappt).
  - Komponenten (Card, Button, Badge, Flash, Table, Code) auf Tokens.

Alle 13 Hauptseiten gegengeprueft (HTTP 200, keine Render-Fehler).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 12:52:14 +02:00

286 lines
13 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
{% extends "base.html" %}
{% block title %}Projekte{% endblock %}
{% block content %}
<style>
.kat-tabelle { width: 100%; border-collapse: collapse; }
.kat-tabelle th {
background: var(--surface-muted); padding: 0.6rem 0.5rem;
text-align: left; font-size: 0.85rem; cursor: pointer;
user-select: none; white-space: nowrap;
}
.kat-tabelle th:hover { background: var(--surface-hover); }
.kat-tabelle th .sort-pfeil {
display: inline-block; margin-left: 0.3rem; opacity: 0.3; font-size: 0.7rem;
}
.kat-tabelle th.sort-aktiv .sort-pfeil { opacity: 1; color: var(--primary); }
.kat-tabelle td {
padding: 0.5rem; border-bottom: 1px solid var(--border);
vertical-align: middle; font-size: 0.9rem;
}
.kat-zeile { cursor: pointer; }
.kat-zeile:hover { background: var(--surface-soft); }
.kat-zeile.offen { background: var(--info-bg); }
.kat-thumb {
width: 40px; height: 40px; border-radius: 6px; object-fit: cover;
background: linear-gradient(135deg, var(--surface-muted), var(--surface-hover));
display: flex; align-items: center; justify-content: center;
font-size: 1.2rem; color: var(--text-light);
}
.kat-thumb img { width: 100%; height: 100%; object-fit: cover; border-radius: 6px; }
.kat-detail { display: none; background: var(--surface-soft); }
.kat-detail.offen { display: table-row; }
.kat-detail-inner {
display: flex; gap: 1.5rem; padding: 1rem 0.5rem; flex-wrap: wrap;
}
.kat-detail-bild img {
max-width: 260px; max-height: 260px; border-radius: 8px;
border: 1px solid var(--border); background: #fff;
}
.kat-detail-bild-platzhalter {
width: 260px; height: 200px; border-radius: 8px;
background: linear-gradient(135deg, var(--surface-muted), var(--surface-hover));
display: flex; align-items: center; justify-content: center;
font-size: 3rem; color: var(--text-light);
}
.kat-detail-info { flex: 1; min-width: 260px; }
.kat-detail-info h3 { margin-top: 0; }
.kat-detail-info .meta {
font-size: 0.85rem; color: var(--text-light); margin-bottom: 0.6rem;
}
.kat-dateien { margin-top: 0.5rem; }
.kat-dateien a {
display: inline-block; margin: 0.2rem 0.4rem 0.2rem 0;
padding: 0.2rem 0.6rem; background: #fff; border: 1px solid var(--border);
border-radius: 4px; text-decoration: none; font-size: 0.85rem; color: var(--text);
}
.kat-dateien a:hover { background: var(--surface-muted); }
</style>
<h1>Projekte</h1>
<p style="color: var(--text-light);">
Was die AG gerade macht — laufende und fertige Projekte. Klick auf eine Zeile fuer Details.
Spalten-Header klicken zum Sortieren.
</p>
{% if projekte %}
<div class="card" style="padding: 0;">
<table class="kat-tabelle" id="kat-tabelle">
<thead>
<tr>
<th></th>
<th data-sort="titel">Titel<span class="sort-pfeil">▲▼</span></th>
<th data-sort="person">Wer<span class="sort-pfeil">▲▼</span></th>
<th data-sort="status">Status<span class="sort-pfeil">▲▼</span></th>
<th data-sort="erfolg">Erfolg<span class="sort-pfeil">▲▼</span></th>
<th data-sort="datum">Datum<span class="sort-pfeil">▲▼</span></th>
<th title="MakerWorld">MW</th>
</tr>
</thead>
<tbody>
{% for p in projekte %}
{# ====== Kompakte Zeile ====== #}
<tr class="kat-zeile" data-projekt="{{ p.id }}"
data-titel="{{ p.titel|lower }}"
data-person="{{ (p.vorname or '')|lower }}"
data-status="{{ p.status_lebenszyklus }}"
data-erfolg="{% if p.selbsteinschaetzung == 'zufrieden' %}1{% elif p.selbsteinschaetzung == 'mittel' %}2{% elif p.selbsteinschaetzung == 'gelernt' %}3{% else %}9{% endif %}"
data-datum="{{ p.abgeschlossen_am or p.geaendert_am or '' }}">
<td>
<div class="kat-thumb">
{% if p.hat_cover %}
<img src="{{ url_for('api.projekt_cover', projekt_id=p.id) }}" alt="">
{% else %}🎯{% endif %}
</div>
</td>
<td><strong>{{ p.titel }}</strong></td>
<td>{{ tier_emoji(p.tier or '') }} {{ p.vorname or '—' }}</td>
<td>
{% if p.status_lebenszyklus == 'in_arbeit' %}<span class="badge badge-mittel">🔧 laeuft</span>
{% else %}<span class="badge badge-anfaenger">✅ fertig</span>{% endif %}
</td>
<td>
{% if p.selbsteinschaetzung == 'zufrieden' %}🟢
{% elif p.selbsteinschaetzung == 'mittel' %}🟡
{% elif p.selbsteinschaetzung == 'gelernt' %}🔴
{% else %}—{% endif %}
</td>
<td style="font-size: 0.8rem; color: var(--text-light); white-space: nowrap;">
{{ (p.abgeschlossen_am or p.geaendert_am or '')|iso_format('%d.%m.%y') }}
</td>
<td style="text-align: center;">
{% if p.makerworld_url %}
<a href="{{ p.makerworld_url }}" target="_blank" rel="noopener noreferrer"
onclick="event.stopPropagation();" title="Auf MakerWorld" style="text-decoration: none;">🌐</a>
{% endif %}
</td>
</tr>
{# ====== Detail-Zeile (versteckt bis Klick) ====== #}
<tr class="kat-detail" data-fuer="{{ p.id }}">
<td colspan="7">
<div class="kat-detail-inner">
<div class="kat-detail-bild">
{% if p.hat_cover %}
<img src="{{ url_for('api.projekt_cover', projekt_id=p.id) }}" alt="">
{% else %}
<div class="kat-detail-bild-platzhalter">🎯</div>
{% endif %}
</div>
<div class="kat-detail-info">
<h3>{{ p.titel }}</h3>
<div class="meta">
{{ tier_emoji(p.tier or '') }} <strong>{{ p.vorname or '—' }}</strong>
{% if p.software_titel %} · 🖥️ {{ p.software_titel }}{% endif %}
{% if p.drucker_name %} · 🖨️ {{ p.drucker_name }}{% endif %}
{% if p.abgeschlossen_am %} · abgeschlossen {{ p.abgeschlossen_am|iso_format('%d.%m.%Y') }}{% endif %}
</div>
{% if p.beschreibung_md %}
<div style="margin-bottom: 0.8rem;">{{ p.beschreibung_md|markdown }}</div>
{% else %}
<p style="color: var(--text-light);"><em>Keine Beschreibung.</em></p>
{% endif %}
{% if p.dateien %}
<div class="kat-dateien">
<strong style="font-size: 0.85rem;">Dateien &amp; Links:</strong>
{% for d in p.dateien %}
{% if d.extern_url %}
<a href="{{ d.extern_url }}" target="_blank" rel="noopener noreferrer" onclick="event.stopPropagation();">
🔗 {{ d.dateiname }} ↗
</a>
{% else %}
<a href="{{ url_for('api.projekt_datei_download', projekt_id=p.id, datei_id=d.id) }}" onclick="event.stopPropagation();">
📎 {{ d.dateiname }}
<span style="color: var(--text-light); font-size: 0.75rem;">
({{ d.art }}, {{ (d.groesse_bytes // 1024) if d.groesse_bytes else 0 }} KB)
</span>
</a>
{% endif %}
{% endfor %}
</div>
{% endif %}
{% if p.makerworld_url %}
<div style="margin-top: 0.8rem;">
<a href="{{ p.makerworld_url }}" target="_blank" rel="noopener noreferrer"
class="btn btn-secondary" style="font-size: 0.85rem;">
🌐 Auf MakerWorld ansehen ↗
</a>
</div>
{% endif %}
{# ====== "Sowas mache ich auch"-Button ====== #}
<div style="margin-top: 1rem; padding-top: 0.8rem; border-top: 1px solid var(--border);">
{% if p.profil_id == eigene_profil_id %}
<span style="font-size: 0.85rem; color: var(--text-light); font-style: italic;">
Dein eigenes Projekt.
</span>
{% elif darf_wiederholen %}
<form method="post"
action="{{ url_for('profil.projekt_aus_vorlage', vorlage_id=p.id) }}"
onclick="event.stopPropagation();"
onsubmit="return confirm('Neues Projekt mit Vorlage \'{{ p.titel }}\' anlegen?')">
<button class="btn btn-primary" style="font-size: 0.9rem;">
+ Sowas mache ich auch
</button>
</form>
{% else %}
<span style="font-size: 0.85rem; color: var(--text-light);">
{{ wiederhol_sperr_grund }}
</span>
{% endif %}
</div>
</div>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="card">
<p style="color: var(--text-light);">Noch keine Projekte im Katalog.</p>
</div>
{% endif %}
{% endblock %}
{% block scripts %}
<script>
(function() {
var tabelle = document.getElementById('kat-tabelle');
if (!tabelle) return;
var tbody = tabelle.querySelector('tbody');
// ===== Aufklappen =====
tabelle.querySelectorAll('.kat-zeile').forEach(function(zeile) {
zeile.addEventListener('click', function(e) {
if (e.target.tagName === 'A') return; // MakerWorld-Link nicht abfangen
var pid = zeile.dataset.projekt;
var detail = tabelle.querySelector('.kat-detail[data-fuer="' + pid + '"]');
if (!detail) return;
var offen = detail.classList.toggle('offen');
zeile.classList.toggle('offen', offen);
});
});
// ===== Sortieren =====
var sortRichtung = {}; // key -> 'asc' | 'desc'
function sortiereNach(key) {
var richtung = sortRichtung[key] === 'asc' ? 'desc' : 'asc';
sortRichtung = {}; // nur eine Sortierung aktiv
sortRichtung[key] = richtung;
// Zeilen-Paare einsammeln: [kat-zeile, kat-detail]
var paare = [];
tabelle.querySelectorAll('.kat-zeile').forEach(function(zeile) {
var pid = zeile.dataset.projekt;
var detail = tabelle.querySelector('.kat-detail[data-fuer="' + pid + '"]');
paare.push({ zeile: zeile, detail: detail, key: zeile.dataset[key] || '' });
});
// Sortier-Helper: erfolg + status sind numerisch/string-codiert, titel/person/datum string-lexikografisch
paare.sort(function(a, b) {
var va = a.key, vb = b.key;
if (key === 'erfolg') { va = parseInt(va) || 9; vb = parseInt(vb) || 9; }
if (va < vb) return richtung === 'asc' ? -1 : 1;
if (va > vb) return richtung === 'asc' ? 1 : -1;
return 0;
});
// Neu in tbody einhaengen (paar fuer paar — Detail folgt direkt)
paare.forEach(function(p) {
tbody.appendChild(p.zeile);
if (p.detail) tbody.appendChild(p.detail);
});
// Sortier-Indikator
tabelle.querySelectorAll('th[data-sort]').forEach(function(th) {
th.classList.remove('sort-aktiv');
var pfeil = th.querySelector('.sort-pfeil');
if (pfeil) pfeil.textContent = '▲▼';
});
var aktivTh = tabelle.querySelector('th[data-sort="' + key + '"]');
if (aktivTh) {
aktivTh.classList.add('sort-aktiv');
aktivTh.querySelector('.sort-pfeil').textContent = richtung === 'asc' ? '▲' : '▼';
}
}
tabelle.querySelectorAll('th[data-sort]').forEach(function(th) {
th.addEventListener('click', function() {
sortiereNach(th.dataset.sort);
});
});
// Initial: nach Datum absteigend (neueste oben)
sortiereNach('datum');
sortiereNach('datum'); // zweimal -> desc
})();
</script>
{% endblock %}