style: Remove team management functionality and associated templates for a cleaner codebase
This commit is contained in:
-185
@@ -41,7 +41,6 @@ def set_security_headers(response):
|
|||||||
|
|
||||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
INVOICE_UPLOAD_DIR = os.path.join(BASE_DIR, "static", "uploads", "invoices")
|
INVOICE_UPLOAD_DIR = os.path.join(BASE_DIR, "static", "uploads", "invoices")
|
||||||
TEAM_UPLOAD_DIR = os.path.join(BASE_DIR, "static", "uploads", "team")
|
|
||||||
MONGO_URI = os.environ.get("MONGO_URI", "mongodb://localhost:27017")
|
MONGO_URI = os.environ.get("MONGO_URI", "mongodb://localhost:27017")
|
||||||
MONGO_DB_NAME = os.environ.get("MONGO_DB_NAME", "Invario_Website")
|
MONGO_DB_NAME = os.environ.get("MONGO_DB_NAME", "Invario_Website")
|
||||||
INSTANCE_REPO_URL = os.environ.get("INSTANCE_REPO_URL", "https://github.com/AIIrondev/legendary-octo-garbanzo")
|
INSTANCE_REPO_URL = os.environ.get("INSTANCE_REPO_URL", "https://github.com/AIIrondev/legendary-octo-garbanzo")
|
||||||
@@ -118,30 +117,6 @@ def _save_invoice_pdf(file_obj, invoice_number: str) -> str | None:
|
|||||||
file_obj.save(target)
|
file_obj.save(target)
|
||||||
return f"uploads/invoices/{unique_name}"
|
return f"uploads/invoices/{unique_name}"
|
||||||
|
|
||||||
|
|
||||||
def _save_team_photo(file_obj, identifier: str) -> str | None:
|
|
||||||
if not file_obj or not file_obj.filename:
|
|
||||||
return None
|
|
||||||
original_name = secure_filename(file_obj.filename)
|
|
||||||
if not _is_allowed_image_filename(original_name):
|
|
||||||
return None
|
|
||||||
os.makedirs(TEAM_UPLOAD_DIR, exist_ok=True)
|
|
||||||
# If a photo for this identifier already exists, keep the first one and return it.
|
|
||||||
safe_identifier = secure_filename(identifier or "member")
|
|
||||||
try:
|
|
||||||
for existing in sorted(os.listdir(TEAM_UPLOAD_DIR)):
|
|
||||||
if existing.startswith(f"team_{safe_identifier}_"):
|
|
||||||
return f"uploads/team/{existing}"
|
|
||||||
except Exception:
|
|
||||||
# If listing fails for any reason, proceed to save new file.
|
|
||||||
pass
|
|
||||||
extension = os.path.splitext(original_name)[1].lower() or ".jpg"
|
|
||||||
unique_name = f"team_{safe_identifier}_{datetime.utcnow().strftime('%Y%m%d%H%M%S')}{extension}"
|
|
||||||
target = os.path.join(TEAM_UPLOAD_DIR, unique_name)
|
|
||||||
file_obj.save(target)
|
|
||||||
return f"uploads/team/{unique_name}"
|
|
||||||
|
|
||||||
|
|
||||||
def _get_mongo_client() -> MongoClient:
|
def _get_mongo_client() -> MongoClient:
|
||||||
global _MONGO_CLIENT
|
global _MONGO_CLIENT
|
||||||
if _MONGO_CLIENT is not None:
|
if _MONGO_CLIENT is not None:
|
||||||
@@ -1504,42 +1479,6 @@ def _sanitize_html(html_content: str, max_length: int = 50000) -> str:
|
|||||||
return bleach.clean(html_content, tags=allowed_tags, attributes=allowed_attrs, strip=True)
|
return bleach.clean(html_content, tags=allowed_tags, attributes=allowed_attrs, strip=True)
|
||||||
|
|
||||||
|
|
||||||
def _get_team_members() -> list:
|
|
||||||
members = []
|
|
||||||
client = None
|
|
||||||
try:
|
|
||||||
client, col = _get_collection("team_members")
|
|
||||||
members = list(col.find().sort([("sort_order", 1), ("created_at", 1)]))
|
|
||||||
except PyMongoError:
|
|
||||||
return []
|
|
||||||
finally:
|
|
||||||
if client:
|
|
||||||
client.close()
|
|
||||||
|
|
||||||
normalized = []
|
|
||||||
for member in members:
|
|
||||||
public_id = str(member.get("_id") or "")
|
|
||||||
sort_raw = member.get("sort_order", 999)
|
|
||||||
try:
|
|
||||||
sort_order = int(sort_raw)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
sort_order = 999
|
|
||||||
normalized.append(
|
|
||||||
{
|
|
||||||
"id": public_id,
|
|
||||||
"sort_order": sort_order,
|
|
||||||
"name": _sanitize_text(member.get("name") or "", 120),
|
|
||||||
"role": _sanitize_text(member.get("role") or "", 120),
|
|
||||||
"work": _sanitize_text(member.get("work") or "", 220),
|
|
||||||
"bio": _sanitize_text(member.get("bio") or "", 500),
|
|
||||||
"photo": _sanitize_text(member.get("photo") or "", 255),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
normalized.sort(key=lambda value: (value.get("sort_order", 999), value.get("name", "")))
|
|
||||||
return normalized
|
|
||||||
|
|
||||||
|
|
||||||
def _validate_username(username: str) -> bool:
|
def _validate_username(username: str) -> bool:
|
||||||
"""Validate username format: alphanumeric, underscore, dash, 3-30 chars."""
|
"""Validate username format: alphanumeric, underscore, dash, 3-30 chars."""
|
||||||
if not username or not isinstance(username, str):
|
if not username or not isinstance(username, str):
|
||||||
@@ -1689,12 +1628,6 @@ def inventarsystem():
|
|||||||
return render_template("inventarsystem.html")
|
return render_template("inventarsystem.html")
|
||||||
|
|
||||||
|
|
||||||
@app.route('/team')
|
|
||||||
def team():
|
|
||||||
team_members = _get_team_members()
|
|
||||||
return render_template("team.html", team_members=team_members)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/kontakt')
|
@app.route('/kontakt')
|
||||||
def kontakt():
|
def kontakt():
|
||||||
return render_template("kontakt.html")
|
return render_template("kontakt.html")
|
||||||
@@ -2581,124 +2514,6 @@ def admin_users():
|
|||||||
instances_by_owner = _list_instances_grouped_by_owner()
|
instances_by_owner = _list_instances_grouped_by_owner()
|
||||||
return render_template("admin_users.html", users=users, instances_by_owner=instances_by_owner)
|
return render_template("admin_users.html", users=users, instances_by_owner=instances_by_owner)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/admin/team', methods=['GET', 'POST'])
|
|
||||||
@admin_required
|
|
||||||
def admin_team():
|
|
||||||
if request.method == 'POST':
|
|
||||||
action = _sanitize_text(request.form.get("action") or "upsert", 20)
|
|
||||||
member_id = _sanitize_text(request.form.get("member_id") or "", 64)
|
|
||||||
|
|
||||||
client = None
|
|
||||||
try:
|
|
||||||
client, col = _get_collection("team_members")
|
|
||||||
|
|
||||||
if action == "delete":
|
|
||||||
try:
|
|
||||||
result = col.delete_one({"_id": ObjectId(member_id)})
|
|
||||||
except Exception:
|
|
||||||
flash("Ungültige Team-ID.", "error")
|
|
||||||
return redirect(url_for("admin_team"))
|
|
||||||
if result.deleted_count:
|
|
||||||
flash("Teammitglied entfernt.", "success")
|
|
||||||
else:
|
|
||||||
flash("Teammitglied nicht gefunden.", "error")
|
|
||||||
return redirect(url_for("admin_team"))
|
|
||||||
|
|
||||||
name = _sanitize_text(request.form.get("name") or "", 120)
|
|
||||||
role = _sanitize_text(request.form.get("role") or "", 120)
|
|
||||||
work = _sanitize_text(request.form.get("work") or "", 220)
|
|
||||||
bio = _sanitize_text(request.form.get("bio") or "", 500)
|
|
||||||
sort_raw = _sanitize_text(request.form.get("sort_order") or "999", 10)
|
|
||||||
|
|
||||||
try:
|
|
||||||
sort_order = int(sort_raw)
|
|
||||||
except ValueError:
|
|
||||||
sort_order = 999
|
|
||||||
sort_order = max(1, min(sort_order, 999))
|
|
||||||
|
|
||||||
if not name or not role or not work:
|
|
||||||
flash("Bitte Name, Rolle und Arbeit ausfüllen.", "error")
|
|
||||||
return redirect(url_for("admin_team"))
|
|
||||||
|
|
||||||
current = None
|
|
||||||
if member_id:
|
|
||||||
try:
|
|
||||||
current = col.find_one({"_id": ObjectId(member_id)})
|
|
||||||
except Exception:
|
|
||||||
flash("Ungültige Team-ID.", "error")
|
|
||||||
return redirect(url_for("admin_team"))
|
|
||||||
|
|
||||||
photo_identifier = member_id or name[:20]
|
|
||||||
photo_path = _save_team_photo(request.files.get("photo"), photo_identifier)
|
|
||||||
|
|
||||||
if request.files.get("photo") and request.files.get("photo").filename and not photo_path:
|
|
||||||
flash("Foto muss JPG, JPEG, PNG oder WEBP sein.", "error")
|
|
||||||
return redirect(url_for("admin_team"))
|
|
||||||
|
|
||||||
if not photo_path:
|
|
||||||
photo_path = (current or {}).get("photo", "")
|
|
||||||
|
|
||||||
if action == "create":
|
|
||||||
if not photo_path:
|
|
||||||
flash("Bitte ein Foto für das neue Teammitglied hochladen.", "error")
|
|
||||||
return redirect(url_for("admin_team"))
|
|
||||||
col.insert_one(
|
|
||||||
{
|
|
||||||
"name": name,
|
|
||||||
"role": role,
|
|
||||||
"work": work,
|
|
||||||
"bio": bio,
|
|
||||||
"photo": photo_path,
|
|
||||||
"sort_order": sort_order,
|
|
||||||
"created_by": session.get("username") or "admin",
|
|
||||||
"created_at": _utc_now_iso(),
|
|
||||||
"updated_at": _utc_now_iso(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
flash("Teammitglied hinzugefuegt.", "success")
|
|
||||||
elif action == "update":
|
|
||||||
if not member_id:
|
|
||||||
flash("Team-ID fehlt.", "error")
|
|
||||||
return redirect(url_for("admin_team"))
|
|
||||||
try:
|
|
||||||
result = col.update_one(
|
|
||||||
{"_id": ObjectId(member_id)},
|
|
||||||
{
|
|
||||||
"$set": {
|
|
||||||
"name": name,
|
|
||||||
"role": role,
|
|
||||||
"work": work,
|
|
||||||
"bio": bio,
|
|
||||||
"photo": photo_path,
|
|
||||||
"sort_order": sort_order,
|
|
||||||
"updated_by": session.get("username") or "admin",
|
|
||||||
"updated_at": _utc_now_iso(),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
flash("Ungültige Team-ID.", "error")
|
|
||||||
return redirect(url_for("admin_team"))
|
|
||||||
if result.matched_count == 0:
|
|
||||||
flash("Teammitglied nicht gefunden.", "error")
|
|
||||||
return redirect(url_for("admin_team"))
|
|
||||||
flash("Teammitglied aktualisiert.", "success")
|
|
||||||
else:
|
|
||||||
flash("Ungültige Aktion.", "error")
|
|
||||||
return redirect(url_for("admin_team"))
|
|
||||||
except PyMongoError:
|
|
||||||
flash("Teamdaten konnten nicht gespeichert werden.", "error")
|
|
||||||
finally:
|
|
||||||
if client:
|
|
||||||
client.close()
|
|
||||||
|
|
||||||
return redirect(url_for("admin_team"))
|
|
||||||
|
|
||||||
team_members = _get_team_members()
|
|
||||||
return render_template("admin_team.html", team_members=team_members)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/admin/chats', methods=['GET', 'POST'])
|
@app.route('/admin/chats', methods=['GET', 'POST'])
|
||||||
@admin_required
|
@admin_required
|
||||||
def admin_chats():
|
def admin_chats():
|
||||||
|
|||||||
@@ -1,180 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block title %}Admin | Team Cards{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<section class="panel">
|
|
||||||
<h1>Team Cards verwalten</h1>
|
|
||||||
<p>Beliebig viele Teammitglieder können hinzugefügt und sortiert werden. Jedes Mitglied hat Name, Rolle, Arbeit, Kurztext und ein Foto.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="card add-card">
|
|
||||||
<h2>Neues Teammitglied hinzufügen</h2>
|
|
||||||
<form method="post" enctype="multipart/form-data" class="form-grid">
|
|
||||||
<input type="hidden" name="action" value="create">
|
|
||||||
|
|
||||||
<label>Name</label>
|
|
||||||
<input name="name" maxlength="120" required>
|
|
||||||
|
|
||||||
<label>Rolle</label>
|
|
||||||
<input name="role" maxlength="120" required>
|
|
||||||
|
|
||||||
<label>Arbeit</label>
|
|
||||||
<input name="work" maxlength="220" required>
|
|
||||||
|
|
||||||
<label>Kurzbeschreibung</label>
|
|
||||||
<textarea name="bio" rows="3" maxlength="500"></textarea>
|
|
||||||
|
|
||||||
<label>Reihenfolge (1-999)</label>
|
|
||||||
<input type="number" name="sort_order" min="1" max="999" value="100" required>
|
|
||||||
|
|
||||||
<label>Foto (JPG, JPEG, PNG, WEBP)</label>
|
|
||||||
<input type="file" name="photo" accept=".jpg,.jpeg,.png,.webp" required>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<button type="submit" class="btn primary">Hinzufügen</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="card-grid">
|
|
||||||
{% for member in team_members %}
|
|
||||||
<article class="card">
|
|
||||||
<h2>{{ member.name }}</h2>
|
|
||||||
<form method="post" enctype="multipart/form-data" class="form-grid">
|
|
||||||
<input type="hidden" name="action" value="update">
|
|
||||||
<input type="hidden" name="member_id" value="{{ member.id }}">
|
|
||||||
|
|
||||||
<label>Name</label>
|
|
||||||
<input name="name" value="{{ member.name }}" maxlength="120" required>
|
|
||||||
|
|
||||||
<label>Rolle</label>
|
|
||||||
<input name="role" value="{{ member.role }}" maxlength="120" required>
|
|
||||||
|
|
||||||
<label>Arbeit</label>
|
|
||||||
<input name="work" value="{{ member.work }}" maxlength="220" required>
|
|
||||||
|
|
||||||
<label>Kurzbeschreibung</label>
|
|
||||||
<textarea name="bio" rows="3" maxlength="500">{{ member.bio }}</textarea>
|
|
||||||
|
|
||||||
<label>Reihenfolge (1-999)</label>
|
|
||||||
<input type="number" name="sort_order" min="1" max="999" value="{{ member.sort_order or 100 }}" required>
|
|
||||||
|
|
||||||
<label>Foto (JPG, JPEG, PNG, WEBP)</label>
|
|
||||||
<input type="file" name="photo" accept=".jpg,.jpeg,.png,.webp">
|
|
||||||
|
|
||||||
{% if member.photo %}
|
|
||||||
<img class="preview" src="{{ url_for('static', filename=member.photo) }}" alt="Teamfoto {{ member.name }}">
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<button type="submit" class="btn primary">Speichern</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<form method="post" class="delete-form" onsubmit="return confirm('Mitglied wirklich entfernen?');">
|
|
||||||
<input type="hidden" name="action" value="delete">
|
|
||||||
<input type="hidden" name="member_id" value="{{ member.id }}">
|
|
||||||
<button type="submit" class="btn danger">Entfernen</button>
|
|
||||||
</form>
|
|
||||||
</article>
|
|
||||||
{% else %}
|
|
||||||
<article class="card">
|
|
||||||
<h2>Noch keine Teammitglieder</h2>
|
|
||||||
<p>Über das Formular oben kann jetzt das erste Teammitglied erstellt werden.</p>
|
|
||||||
</article>
|
|
||||||
{% endfor %}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.panel {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(290px, 1fr));
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #d8e1e8;
|
|
||||||
border-radius: 14px;
|
|
||||||
padding: 1rem;
|
|
||||||
box-shadow: 0 10px 24px rgba(20, 40, 55, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-card {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card h2 {
|
|
||||||
margin: 0 0 0.7rem;
|
|
||||||
color: #113d59;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-grid {
|
|
||||||
display: grid;
|
|
||||||
gap: 0.45rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
font-size: 0.86rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #4b677c;
|
|
||||||
}
|
|
||||||
|
|
||||||
input,
|
|
||||||
textarea {
|
|
||||||
border: 1px solid #bdd0dd;
|
|
||||||
border-radius: 9px;
|
|
||||||
padding: 0.5rem 0.6rem;
|
|
||||||
font: inherit;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
resize: vertical;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview {
|
|
||||||
margin-top: 0.4rem;
|
|
||||||
width: 100%;
|
|
||||||
max-height: 200px;
|
|
||||||
object-fit: cover;
|
|
||||||
border: 1px solid #d0d9de;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row {
|
|
||||||
margin-top: 0.4rem;
|
|
||||||
display: flex;
|
|
||||||
gap: 0.45rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
border: none;
|
|
||||||
border-radius: 999px;
|
|
||||||
padding: 0.5rem 0.85rem;
|
|
||||||
font-weight: 700;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn.primary {
|
|
||||||
color: #fff;
|
|
||||||
background: linear-gradient(120deg, #0b5b89 0%, #0b4262 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn.danger {
|
|
||||||
color: #842f2f;
|
|
||||||
background: #fff4f4;
|
|
||||||
border: 1px solid #e7b9b9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.delete-form {
|
|
||||||
margin-top: 0.8rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -518,12 +518,11 @@
|
|||||||
<div class="nav-links" id="mainNav" aria-label="Main Navigation" aria-hidden="true">
|
<div class="nav-links" id="mainNav" aria-label="Main Navigation" aria-hidden="true">
|
||||||
<a href="{{ url_for('default') }}">Start</a>
|
<a href="{{ url_for('default') }}">Start</a>
|
||||||
<a href="{{ url_for('dienstleistungen') }}">Für Schulen</a>
|
<a href="{{ url_for('dienstleistungen') }}">Für Schulen</a>
|
||||||
<a href="{{ url_for('team') }}">Team</a>
|
<a href="{{ url_for('preise') }}">Preise</a>
|
||||||
<a href="{{ url_for('kontakt') }}">Kontakt</a>
|
<a href="{{ url_for('kontakt') }}">Kontakt</a>
|
||||||
{% if session.get('is_admin') %}
|
{% if session.get('is_admin') %}
|
||||||
<a class="nav-user-link" href="{{ url_for('admin_invoices') }}">Rechnungsverwaltung</a>
|
<a class="nav-user-link" href="{{ url_for('admin_invoices') }}">Rechnungsverwaltung</a>
|
||||||
<a class="nav-user-link" href="{{ url_for('admin_blog') }}">Blog verwalten</a>
|
<a class="nav-user-link" href="{{ url_for('admin_blog') }}">Blog verwalten</a>
|
||||||
<a class="nav-user-link" href="{{ url_for('admin_team') }}">Team Cards</a>
|
|
||||||
<a class="nav-user-link" href="{{ url_for('admin_chats') }}">Admin-Chat</a>
|
<a class="nav-user-link" href="{{ url_for('admin_chats') }}">Admin-Chat</a>
|
||||||
<a class="nav-user-link" href="{{ url_for('admin_tickets') }}">Support-Center</a>
|
<a class="nav-user-link" href="{{ url_for('admin_tickets') }}">Support-Center</a>
|
||||||
<a class="nav-user-link" href="{{ url_for('admin_system_tools') }}">System-Tools</a>
|
<a class="nav-user-link" href="{{ url_for('admin_system_tools') }}">System-Tools</a>
|
||||||
@@ -550,7 +549,6 @@
|
|||||||
<a href="{{ url_for('admin_tickets') }}">Support-Center</a>
|
<a href="{{ url_for('admin_tickets') }}">Support-Center</a>
|
||||||
<a href="{{ url_for('admin_system_tools') }}">System-Tools</a>
|
<a href="{{ url_for('admin_system_tools') }}">System-Tools</a>
|
||||||
<a href="{{ url_for('admin_blog') }}">Blog verwalten</a>
|
<a href="{{ url_for('admin_blog') }}">Blog verwalten</a>
|
||||||
<a href="{{ url_for('admin_team') }}">Team Cards</a>
|
|
||||||
<a href="{{ url_for('admin_instances') }}">Schul-Instanzen</a>
|
<a href="{{ url_for('admin_instances') }}">Schul-Instanzen</a>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
@@ -597,7 +595,6 @@
|
|||||||
<ul class="footer-links">
|
<ul class="footer-links">
|
||||||
<li><a href="{{ url_for('default') }}">Start</a></li>
|
<li><a href="{{ url_for('default') }}">Start</a></li>
|
||||||
<li><a href="{{ url_for('dienstleistungen') }}">Leistungsübersicht</a></li>
|
<li><a href="{{ url_for('dienstleistungen') }}">Leistungsübersicht</a></li>
|
||||||
<li><a href="{{ url_for('team') }}">Team</a></li>
|
|
||||||
<li><a href="{{ url_for('blog') }}">Neuigkeiten</a></li>
|
<li><a href="{{ url_for('blog') }}">Neuigkeiten</a></li>
|
||||||
<li><a href="{{ url_for('preise') }}">Preise</a></li>
|
<li><a href="{{ url_for('preise') }}">Preise</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -1,171 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block title %}Team | Invario{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<section class="page-hero">
|
|
||||||
<h1>Unser Team für Schulen</h1>
|
|
||||||
<p>Unsere Ansprechpartner mit klaren Verantwortungen.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="team-grid" aria-label="Team Cards">
|
|
||||||
{% if team_members and team_members|length > 0 %}
|
|
||||||
{% for member in team_members %}
|
|
||||||
<article class="member-card">
|
|
||||||
{% if member and member.photo %}
|
|
||||||
<img class="member-photo" src="{{ url_for('static', filename=member.photo) }}" alt="Teamfoto {{ member.name }}">
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="member-content">
|
|
||||||
<p class="member-slot">Teammitglied</p>
|
|
||||||
<h2>{{ member.name }}</h2>
|
|
||||||
<p class="member-role">{{ member.role }}</p>
|
|
||||||
<p><strong>Arbeit:</strong> {{ member.work }}</p>
|
|
||||||
{% if member.bio %}
|
|
||||||
<p>{{ member.bio }}</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{% if not team_members or team_members|length == 0 %}
|
|
||||||
<p>Derzeit sind keine Teammitglieder eingetragen. </p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<section class="values">
|
|
||||||
<h2>Arbeitsprinzipien</h2>
|
|
||||||
<p>Direkte Kommunikation, saubere Einführung und verbindliche Betreuung im laufenden Betrieb.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.page-hero {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.team-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
||||||
gap: 0.75rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-card {
|
|
||||||
background: #ffffff;
|
|
||||||
border: 1px solid #d8dfd7;
|
|
||||||
border-radius: 16px;
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 12px 24px rgba(25, 45, 41, 0.06);
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: auto 1fr;
|
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-card:hover {
|
|
||||||
box-shadow: 0 20px 32px rgba(25, 45, 41, 0.12);
|
|
||||||
transform: translateY(-4px);
|
|
||||||
border-color: #c0ccc8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-photo {
|
|
||||||
width: 100%;
|
|
||||||
aspect-ratio: 3 / 4;
|
|
||||||
object-fit: cover;
|
|
||||||
object-position: center 20%;
|
|
||||||
background: linear-gradient(135deg, #eef2ee 0%, #e0e8e4 100%);
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-content {
|
|
||||||
padding: 0.75rem;
|
|
||||||
display: grid;
|
|
||||||
gap: 0.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-slot {
|
|
||||||
margin: 0;
|
|
||||||
color: #5f7479;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-content h2 {
|
|
||||||
margin: 0;
|
|
||||||
color: #264f56;
|
|
||||||
font-size: 1.4rem;
|
|
||||||
line-height: 1.1;
|
|
||||||
margin-top: 0.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-role {
|
|
||||||
margin: 0;
|
|
||||||
color: #3b6168;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-content p:last-child {
|
|
||||||
margin: 0.25rem 0 0 0;
|
|
||||||
color: #505550;
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.values {
|
|
||||||
margin-top: 3rem;
|
|
||||||
background: #ffffff;
|
|
||||||
border: 1px solid #d8dfd7;
|
|
||||||
border-radius: 14px;
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.values h2 {
|
|
||||||
color: #265057;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
font-size: clamp(1.25rem, 2vw, 1.5rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.values p {
|
|
||||||
color: #505550;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.team-grid {
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-photo {
|
|
||||||
height: 240px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.team-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-card {
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-content {
|
|
||||||
padding: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-photo {
|
|
||||||
height: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.values {
|
|
||||||
padding: 1.5rem;
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
|
||||||
Reference in New Issue
Block a user