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>
286 lines
13 KiB
HTML
286 lines
13 KiB
HTML
{% 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 & 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 %}
|