Initial commit: Open-Source Projektplattform für Schleswig-Holstein

- Flask-App mit SQLite, Projekt-Einreichung und Bewerbungen
- Suche und Filter nach Kategorie
- Modernes UI mit Bootstrap 5 und Custom CSS
- 6 Demo-Projekte via seed_demo_data.py
- Docker und docker-compose Support

Made-with: Cursor
This commit is contained in:
2026-03-05 19:57:42 +01:00
commit 21e4d86fef
15 changed files with 968 additions and 0 deletions

20
.dockerignore Normal file
View File

@@ -0,0 +1,20 @@
.venv/
venv/
env/
__pycache__/
*.py[cod]
*$py.class
instance/
*.db
.git/
.gitignore
.env
.env.*
*.log
.idea/
.vscode/
.cursor/
*.md
Dockerfile
docker-compose*.yml
.dockerignore

1
.flaskenv Normal file
View File

@@ -0,0 +1 @@
FLASK_APP=app.py

30
.gitignore vendored Normal file
View File

@@ -0,0 +1,30 @@
# Virtual Environment
.venv/
venv/
env/
# SQLite Database
instance/
*.db
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
*.egg-info/
.eggs/
dist/
build/
# Environment
.env
.env.local
*.log
# IDE
.idea/
.vscode/
*.swp
*.swo

21
Dockerfile Normal file
View File

@@ -0,0 +1,21 @@
FROM python:3.12-slim
WORKDIR /app
# Abhängigkeiten installieren
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt gunicorn
# Anwendung kopieren
COPY app.py models.py seed_demo_data.py ./
COPY templates/ templates/
COPY static/ static/
# Instance-Ordner für SQLite (wird per Volume gemountet)
ENV FLASK_APP=app.py
EXPOSE 5000
# Seed ausführen, dann Gunicorn starten
CMD python seed_demo_data.py 2>/dev/null || true && \
gunicorn --bind 0.0.0.0:5000 app:app

95
app.py Normal file
View File

@@ -0,0 +1,95 @@
import os
from flask import Flask, render_template, request, redirect, url_for
from models import db, Projekt, Bewerbung
app = Flask(__name__)
os.makedirs(app.instance_path, exist_ok=True)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///" + os.path.join(
app.instance_path, "projekte.db"
)
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "dev-secret-key")
db.init_app(app)
KATEGORIEN = [
"Bau & Planung",
"Verwaltung",
"Bürgerdienste",
"Demokratie & Partizipation",
"Bildung",
"Infrastruktur",
]
@app.route("/")
def index():
suche = request.args.get("suche", "").strip()
kategorie = request.args.get("kategorie", "").strip()
query = Projekt.query
if suche:
query = query.filter(
Projekt.titel.ilike(f"%{suche}%") | Projekt.beschreibung.ilike(f"%{suche}%")
)
if kategorie:
query = query.filter(Projekt.kategorie == kategorie)
projekte = query.order_by(Projekt.erstellt_am.desc()).all()
return render_template(
"index.html",
projekte=projekte,
kategorien=KATEGORIEN,
suche=suche,
aktuelle_kategorie=kategorie,
)
@app.route("/projekt/<int:id>")
def projekt_detail(id):
projekt = Projekt.query.get_or_404(id)
return render_template("projekt_detail.html", projekt=projekt)
@app.route("/projekt/einreichen", methods=["GET", "POST"])
def projekt_einreichen():
if request.method == "POST":
projekt = Projekt(
titel=request.form.get("titel", "").strip(),
beschreibung=request.form.get("beschreibung", "").strip(),
kategorie=request.form.get("kategorie", "").strip(),
behoerde=request.form.get("behoerde", "").strip(),
kontakt_email=request.form.get("kontakt_email", "").strip(),
)
if projekt.titel and projekt.beschreibung and projekt.kategorie and projekt.behoerde and projekt.kontakt_email:
db.session.add(projekt)
db.session.commit()
return redirect(url_for("index"))
return render_template("projekt_einreichen.html", kategorien=KATEGORIEN)
@app.route("/bewerbung", methods=["POST"])
def bewerbung_abgeben():
projekt_id = request.form.get("projekt_id", type=int)
projekt = Projekt.query.get_or_404(projekt_id)
bewerbung = Bewerbung(
projekt_id=projekt_id,
name=request.form.get("name", "").strip(),
email=request.form.get("email", "").strip(),
nachricht=request.form.get("nachricht", "").strip() or None,
)
if bewerbung.name and bewerbung.email:
db.session.add(bewerbung)
db.session.commit()
return redirect(url_for("bewerbung_erfolg"))
return redirect(url_for("projekt_detail", id=projekt_id))
@app.route("/bewerbung/erfolg")
def bewerbung_erfolg():
return render_template("bewerbung_erfolg.html")
with app.app_context():
db.create_all()

13
docker-compose.yml Normal file
View File

@@ -0,0 +1,13 @@
services:
web:
build: .
ports:
- "5000:5000"
volumes:
- instance_data:/app/instance
environment:
- SECRET_KEY=${SECRET_KEY:-docker-secret-key}
restart: unless-stopped
volumes:
instance_data:

34
models.py Normal file
View File

@@ -0,0 +1,34 @@
from datetime import datetime
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
db = SQLAlchemy()
class Projekt(db.Model):
__tablename__ = "projekt"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
titel: Mapped[str] = mapped_column(nullable=False)
beschreibung: Mapped[str] = mapped_column(nullable=False)
kategorie: Mapped[str] = mapped_column(nullable=False)
behoerde: Mapped[str] = mapped_column(nullable=False)
kontakt_email: Mapped[str] = mapped_column(nullable=False)
status: Mapped[str] = mapped_column(default="offen", nullable=False)
erstellt_am: Mapped[datetime] = mapped_column(default=datetime.utcnow)
bewerbungen = relationship("Bewerbung", back_populates="projekt", cascade="all, delete-orphan")
class Bewerbung(db.Model):
__tablename__ = "bewerbung"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
projekt_id: Mapped[int] = mapped_column(ForeignKey("projekt.id"), nullable=False)
name: Mapped[str] = mapped_column(nullable=False)
email: Mapped[str] = mapped_column(nullable=False)
nachricht: Mapped[str] = mapped_column(nullable=True)
erstellt_am: Mapped[datetime] = mapped_column(default=datetime.utcnow)
projekt = relationship("Projekt", back_populates="bewerbungen")

2
requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
Flask>=3.0.0
Flask-SQLAlchemy>=3.1.0

70
seed_demo_data.py Normal file
View File

@@ -0,0 +1,70 @@
"""Demo-Projekte für die Open-Source Projektplattform laden."""
import os
import sys
# Projekt-Root zum Python-Pfad hinzufügen
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from app import app
from models import db, Projekt
DEMO_PROJEKTE = [
{
"titel": "Digitale Antragsstellung für Baugenehmigungen",
"beschreibung": "Entwicklung einer Open-Source-Webanwendung, die Bürgerinnen und Bürgern ermöglicht, Baugenehmigungsanträge digital einzureichen. Die Lösung soll Formularvalidierung, Dokumenten-Upload und Status-Tracking umfassen sowie mit bestehenden Fachverfahren kommunizieren können.",
"kategorie": "Bau & Planung",
"behoerde": "Kreis Rendsburg-Eckernförde",
"kontakt_email": "bauamt@kreis-rd.de",
},
{
"titel": "Open-Source Dokumentenmanagement für Kommunen",
"beschreibung": "Gemeinsame Entwicklung eines DMS für kleine und mittlere Kommunen in Schleswig-Holstein. Fokus auf Aktenführung, Ablage, Volltextsuche und revisionssichere Archivierung. Soll DSGVO-konform und barrierefrei sein.",
"kategorie": "Verwaltung",
"behoerde": "Stadt Neumünster",
"kontakt_email": "it@neumuenster.de",
},
{
"titel": "Barrierefreie Terminbuchung für Bürgerämter",
"beschreibung": "Terminbuchungssystem für Bürgerämter, das WCAG 2.1 AA erfüllt und mit Screenreadern nutzbar ist. Integration in bestehende Webauftritte, Kalender-Synchronisation und Warteschlangen-Management.",
"kategorie": "Bürgerdienste",
"behoerde": "Hansestadt Lübeck",
"kontakt_email": "buergerservice@luebeck.de",
},
{
"titel": "Transparenzportal für Ratsbeschlüsse",
"beschreibung": "Plattform zur Veröffentlichung und Durchsuchbarkeit von Ratsbeschlüssen, Sitzungsunterlagen und Protokollen. Ziel: mehr Transparenz und einfacher Zugang für Bürgerinnen und Bürger sowie Presse.",
"kategorie": "Demokratie & Partizipation",
"behoerde": "Kreis Nordfriesland",
"kontakt_email": "ratsbuero@nordfriesland.de",
},
{
"titel": "Schulverwaltungssoftware für Stundenpläne",
"beschreibung": "Open-Source-Tool zur Erstellung und Verwaltung von Stundenplänen für allgemeinbildende Schulen. Berücksichtigung von Lehrkräften, Räumen, Fächern und Nebenbedingungen. Export für digitale Schwarze Bretter.",
"kategorie": "Bildung",
"behoerde": "Ministerium für Allgemeine und Berufliche Bildung",
"kontakt_email": "digitalisierung@bildung.landsh.de",
},
{
"titel": "Meldeportal für Straßenschäden",
"beschreibung": "Bürger-App und Webportal zum Melden von Straßenschäden (Schlaglöcher, Risse, defekte Beleuchtung). Mit Foto-Upload, Geoposition und Status-Updates. Anbindung an bestehende Straßenunterhaltungs-Systeme.",
"kategorie": "Infrastruktur",
"behoerde": "Landeshauptstadt Kiel",
"kontakt_email": "strassenbau@kiel.de",
},
]
def main():
with app.app_context():
if Projekt.query.count() > 0:
print("Datenbank enthält bereits Projekte. Überspringe Seed.")
return
for p in DEMO_PROJEKTE:
db.session.add(Projekt(**p))
db.session.commit()
print(f"{len(DEMO_PROJEKTE)} Demo-Projekte wurden angelegt.")
if __name__ == "__main__":
main()

456
static/style.css Normal file
View File

@@ -0,0 +1,456 @@
:root {
--font-sans: "Plus Jakarta Sans", system-ui, sans-serif;
--color-bg: #f8fafc;
--color-surface: #ffffff;
--color-text: #1e293b;
--color-text-muted: #64748b;
--color-primary: #0ea5e9;
--color-primary-hover: #0284c7;
--color-accent: #06b6d4;
--color-border: #e2e8f0;
--color-success: #10b981;
--color-warning: #f59e0b;
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-xl: 24px;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.07), 0 2px 4px -2px rgba(0, 0, 0, 0.05);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.08), 0 4px 6px -4px rgba(0, 0, 0, 0.05);
--shadow-glow: 0 0 40px rgba(14, 165, 233, 0.15);
--transition: 0.2s ease;
}
* {
box-sizing: border-box;
}
body {
font-family: var(--font-sans);
background: var(--color-bg);
color: var(--color-text);
line-height: 1.6;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.main-content {
flex: 1;
}
/* Navbar */
.navbar {
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(12px);
box-shadow: var(--shadow-sm);
padding: 1rem 0;
}
.navbar-brand {
font-weight: 700;
font-size: 1.25rem;
color: var(--color-text) !important;
}
.nav-link {
font-weight: 500;
color: var(--color-text-muted) !important;
transition: color var(--transition);
}
.nav-link:hover {
color: var(--color-primary) !important;
}
.btn-nav-cta {
background: linear-gradient(135deg, var(--color-primary), var(--color-accent)) !important;
color: white !important;
padding: 0.5rem 1.25rem !important;
border-radius: var(--radius-md) !important;
margin-left: 0.5rem;
}
.btn-nav-cta:hover {
opacity: 0.95;
color: white !important;
}
/* Hero */
.hero {
background: linear-gradient(135deg, #0f172a 0%, #1e3a5f 50%, #0ea5e9 100%);
color: white;
padding: 4rem 0;
margin-bottom: 2rem;
}
.hero-title {
font-size: clamp(1.75rem, 4vw, 2.5rem);
font-weight: 700;
margin-bottom: 0.75rem;
letter-spacing: -0.02em;
}
.hero-subtitle {
font-size: 1.125rem;
opacity: 0.9;
max-width: 600px;
}
/* Search */
.search-section {
margin-top: -1.5rem;
margin-bottom: 2rem;
}
.search-form {
background: var(--color-surface);
padding: 1.5rem;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
}
.form-label {
font-weight: 600;
color: var(--color-text);
margin-bottom: 0.375rem;
}
.form-control, .form-select {
border-radius: var(--radius-md);
border-color: var(--color-border);
transition: border-color var(--transition), box-shadow var(--transition);
}
.form-control:focus, .form-select:focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.2);
}
/* Project Grid */
.projects-section {
padding-bottom: 4rem;
}
.section-title {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 1.5rem;
color: var(--color-text);
}
.project-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 1.5rem;
}
.project-card {
background: var(--color-surface);
border-radius: var(--radius-lg);
padding: 1.5rem;
box-shadow: var(--shadow-md);
transition: transform var(--transition), box-shadow var(--transition);
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
}
.project-card::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, var(--color-primary), var(--color-accent));
opacity: 0;
transition: opacity var(--transition);
}
.project-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg), var(--shadow-glow);
}
.project-card:hover::before {
opacity: 1;
}
.project-card-badge {
display: inline-block;
font-size: 0.75rem;
font-weight: 600;
color: var(--color-primary);
background: rgba(14, 165, 233, 0.1);
padding: 0.25rem 0.75rem;
border-radius: 999px;
margin-bottom: 0.75rem;
}
.project-card-title {
font-size: 1.125rem;
font-weight: 700;
margin-bottom: 0.5rem;
color: var(--color-text);
line-height: 1.3;
}
.project-card-desc {
font-size: 0.9375rem;
color: var(--color-text-muted);
margin-bottom: 1rem;
flex: 1;
}
.project-card-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
font-size: 0.875rem;
}
.project-card-behoerde {
color: var(--color-text-muted);
}
.project-card-status {
font-weight: 600;
padding: 0.2rem 0.5rem;
border-radius: var(--radius-sm);
}
.status-offen {
background: rgba(16, 185, 129, 0.15);
color: var(--color-success);
}
.status-vergeben {
background: rgba(245, 158, 11, 0.15);
color: var(--color-warning);
}
.status-abgeschlossen {
background: rgba(100, 116, 139, 0.15);
color: var(--color-text-muted);
}
.project-card-link {
font-weight: 600;
color: var(--color-primary);
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 0.25rem;
transition: gap var(--transition), color var(--transition);
}
.project-card-link:hover {
color: var(--color-primary-hover);
gap: 0.5rem;
}
/* Empty State */
.empty-state {
text-align: center;
padding: 4rem 2rem;
background: var(--color-surface);
border-radius: var(--radius-lg);
color: var(--color-text-muted);
}
.empty-state a {
color: var(--color-primary);
font-weight: 600;
}
/* Detail Page */
.detail-hero {
background: linear-gradient(135deg, #0f172a 0%, #1e3a5f 100%);
color: white;
padding: 3rem 0;
margin-bottom: 2rem;
}
.back-link {
display: inline-block;
color: rgba(255, 255, 255, 0.8);
text-decoration: none;
font-size: 0.9375rem;
margin-bottom: 1rem;
transition: color var(--transition);
}
.back-link:hover {
color: white;
}
.detail-badge {
display: inline-block;
font-size: 0.75rem;
font-weight: 600;
background: rgba(255, 255, 255, 0.2);
padding: 0.25rem 0.75rem;
border-radius: 999px;
margin-bottom: 0.75rem;
}
.detail-title {
font-size: clamp(1.5rem, 3vw, 2rem);
font-weight: 700;
margin-bottom: 0.5rem;
}
.detail-meta {
display: flex;
gap: 1rem;
align-items: center;
font-size: 0.9375rem;
opacity: 0.9;
}
.detail-content {
padding-bottom: 4rem;
}
.detail-card {
background: var(--color-surface);
border-radius: var(--radius-lg);
padding: 2rem;
box-shadow: var(--shadow-md);
margin-bottom: 1.5rem;
}
.detail-card h2 {
font-size: 1.25rem;
font-weight: 700;
margin-bottom: 1rem;
}
.detail-description {
color: var(--color-text);
white-space: pre-wrap;
}
.apply-card {
background: var(--color-surface);
border-radius: var(--radius-lg);
padding: 1.5rem;
box-shadow: var(--shadow-lg);
position: sticky;
top: 100px;
}
.apply-card h3 {
font-size: 1.125rem;
font-weight: 700;
margin-bottom: 1.25rem;
}
.contact-info {
margin-top: 1rem;
padding: 1rem;
background: rgba(14, 165, 233, 0.08);
border-radius: var(--radius-md);
}
.contact-info a {
color: var(--color-primary);
font-weight: 500;
}
/* Form Page */
.form-hero {
background: linear-gradient(135deg, #0f172a 0%, #1e3a5f 100%);
color: white;
padding: 3rem 0;
margin-bottom: 2rem;
}
.form-hero-title {
font-size: clamp(1.5rem, 3vw, 2rem);
font-weight: 700;
margin-bottom: 0.5rem;
}
.form-hero-subtitle {
opacity: 0.9;
max-width: 560px;
}
.form-section {
padding-bottom: 4rem;
}
.form-card {
background: var(--color-surface);
border-radius: var(--radius-lg);
padding: 2.5rem;
box-shadow: var(--shadow-lg);
max-width: 640px;
}
/* Success Page */
.success-section {
padding: 4rem 0;
}
.success-card {
text-align: center;
background: var(--color-surface);
border-radius: var(--radius-xl);
padding: 3rem;
box-shadow: var(--shadow-lg);
max-width: 480px;
margin: 0 auto;
}
.success-icon {
width: 64px;
height: 64px;
background: linear-gradient(135deg, var(--color-success), #34d399);
color: white;
font-size: 2rem;
font-weight: 700;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 1.5rem;
}
.success-title {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 0.75rem;
}
.success-text {
color: var(--color-text-muted);
margin-bottom: 1.5rem;
}
/* Buttons */
.btn-primary {
background: linear-gradient(135deg, var(--color-primary), var(--color-accent));
border: none;
border-radius: var(--radius-md);
font-weight: 600;
padding: 0.625rem 1.25rem;
transition: opacity var(--transition), transform var(--transition);
}
.btn-primary:hover {
opacity: 0.95;
transform: translateY(-1px);
}
/* Footer */
.footer {
background: var(--color-text);
color: rgba(255, 255, 255, 0.7);
padding: 1.5rem 0;
font-size: 0.875rem;
}

46
templates/base.html Normal file
View File

@@ -0,0 +1,46 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Offene Innovation SH{% endblock %} | DigitalHub.SH</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<nav class="navbar navbar-expand-lg sticky-top">
<div class="container">
<a class="navbar-brand" href="{{ url_for('index') }}">Offene Innovation SH</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navCollapse" aria-controls="navCollapse" aria-expanded="false" aria-label="Navigation umschalten">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navCollapse">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('index') }}">Projekte</a>
</li>
<li class="nav-item">
<a class="nav-link btn-nav-cta" href="{{ url_for('projekt_einreichen') }}">Projekt einreichen</a>
</li>
</ul>
</div>
</div>
</nav>
<main class="main-content">
{% block content %}{% endblock %}
</main>
<footer class="footer">
<div class="container">
<p class="mb-0">Landesprogramm Offene Innovation Schleswig-Holstein</p>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block title %}Bewerbung eingegangen{% endblock %}
{% block content %}
<section class="success-section">
<div class="container">
<div class="success-card">
<div class="success-icon"></div>
<h1 class="success-title">Bewerbung erfolgreich eingereicht</h1>
<p class="success-text">Vielen Dank! Die zuständige Behörde wird sich in Kürze bei Ihnen melden.</p>
<a href="{{ url_for('index') }}" class="btn btn-primary btn-lg">Weitere Projekte ansehen</a>
</div>
</div>
</section>
{% endblock %}

63
templates/index.html Normal file
View File

@@ -0,0 +1,63 @@
{% extends "base.html" %}
{% block title %}Projekte durchsuchen{% endblock %}
{% block content %}
<section class="hero">
<div class="container">
<h1 class="hero-title">Open-Source Projekte für die Verwaltung</h1>
<p class="hero-subtitle">Finden Sie passende Projekte und bewerben Sie sich als Mitarbeitende oder reichen Sie Ihr eigenes Projekt ein.</p>
</div>
</section>
<section class="search-section">
<div class="container">
<form method="get" action="{{ url_for('index') }}" class="search-form">
<div class="row g-3 align-items-end">
<div class="col-md-6">
<label for="suche" class="form-label">Suche</label>
<input type="text" id="suche" name="suche" class="form-control form-control-lg" placeholder="Titel oder Beschreibung durchsuchen..." value="{{ suche }}">
</div>
<div class="col-md-4">
<label for="kategorie" class="form-label">Kategorie</label>
<select id="kategorie" name="kategorie" class="form-select form-select-lg">
<option value="">Alle Kategorien</option>
{% for k in kategorien %}
<option value="{{ k }}" {% if aktuelle_kategorie == k %}selected{% endif %}>{{ k }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary btn-lg w-100">Suchen</button>
</div>
</div>
</form>
</div>
</section>
<section class="projects-section">
<div class="container">
<h2 class="section-title">Verfügbare Projekte</h2>
{% if projekte %}
<div class="project-grid">
{% for projekt in projekte %}
<article class="project-card">
<span class="project-card-badge">{{ projekt.kategorie }}</span>
<h3 class="project-card-title">{{ projekt.titel }}</h3>
<p class="project-card-desc">{{ projekt.beschreibung[:180] }}{% if projekt.beschreibung|length > 180 %}…{% endif %}</p>
<div class="project-card-meta">
<span class="project-card-behoerde">{{ projekt.behoerde }}</span>
<span class="project-card-status status-{{ projekt.status }}">{{ projekt.status }}</span>
</div>
<a href="{{ url_for('projekt_detail', id=projekt.id) }}" class="project-card-link">Details & Bewerben</a>
</article>
{% endfor %}
</div>
{% else %}
<div class="empty-state">
<p>Keine Projekte gefunden. Versuchen Sie andere Suchbegriffe oder <a href="{{ url_for('projekt_einreichen') }}">reichen Sie ein Projekt ein</a>.</p>
</div>
{% endif %}
</div>
</section>
{% endblock %}

View File

@@ -0,0 +1,54 @@
{% extends "base.html" %}
{% block title %}{{ projekt.titel }}{% endblock %}
{% block content %}
<section class="detail-hero">
<div class="container">
<a href="{{ url_for('index') }}" class="back-link">&larr; Zurück zur Übersicht</a>
<span class="detail-badge">{{ projekt.kategorie }}</span>
<h1 class="detail-title">{{ projekt.titel }}</h1>
<div class="detail-meta">
<span class="detail-behoerde">{{ projekt.behoerde }}</span>
<span class="detail-status status-{{ projekt.status }}">{{ projekt.status }}</span>
</div>
</div>
</section>
<section class="detail-content">
<div class="container">
<div class="row">
<div class="col-lg-8">
<div class="detail-card">
<h2>Projektbeschreibung</h2>
<p class="detail-description">{{ projekt.beschreibung }}</p>
</div>
</div>
<div class="col-lg-4">
<div class="apply-card">
<h3>Als Mitarbeitende bewerben</h3>
<form method="post" action="{{ url_for('bewerbung_abgeben') }}">
<input type="hidden" name="projekt_id" value="{{ projekt.id }}">
<div class="mb-3">
<label for="name" class="form-label">Name</label>
<input type="text" id="name" name="name" class="form-control" required placeholder="Ihr Name">
</div>
<div class="mb-3">
<label for="email" class="form-label">E-Mail</label>
<input type="email" id="email" name="email" class="form-control" required placeholder="ihre@email.de">
</div>
<div class="mb-4">
<label for="nachricht" class="form-label">Nachricht (optional)</label>
<textarea id="nachricht" name="nachricht" class="form-control" rows="4" placeholder="Kurze Vorstellung und Motivation..."></textarea>
</div>
<button type="submit" class="btn btn-primary btn-lg w-100">Bewerbung absenden</button>
</form>
</div>
<div class="contact-info">
<p><strong>Kontakt:</strong> <a href="mailto:{{ projekt.kontakt_email }}">{{ projekt.kontakt_email }}</a></p>
</div>
</div>
</div>
</div>
</section>
{% endblock %}

View File

@@ -0,0 +1,47 @@
{% extends "base.html" %}
{% block title %}Projekt einreichen{% endblock %}
{% block content %}
<section class="form-hero">
<div class="container">
<h1 class="form-hero-title">Projekt einreichen</h1>
<p class="form-hero-subtitle">Beschreiben Sie Ihre Idee für eine Open-Source-Lösung. Die Fachjury prüft eingereichte Projekte im Rahmen des Call for Concepts.</p>
</div>
</section>
<section class="form-section">
<div class="container">
<div class="form-card">
<form method="post" action="{{ url_for('projekt_einreichen') }}">
<div class="mb-4">
<label for="titel" class="form-label">Projekttitel</label>
<input type="text" id="titel" name="titel" class="form-control form-control-lg" required placeholder="Kurzer, prägnanter Titel">
</div>
<div class="mb-4">
<label for="beschreibung" class="form-label">Projektbeschreibung</label>
<textarea id="beschreibung" name="beschreibung" class="form-control" rows="6" required placeholder="Beschreiben Sie das Problem, die geplante Lösung und den Nutzen..."></textarea>
</div>
<div class="mb-4">
<label for="kategorie" class="form-label">Kategorie</label>
<select id="kategorie" name="kategorie" class="form-select form-select-lg" required>
<option value="">Bitte wählen</option>
{% for k in kategorien %}
<option value="{{ k }}">{{ k }}</option>
{% endfor %}
</select>
</div>
<div class="mb-4">
<label for="behoerde" class="form-label">Behörde / Organisation</label>
<input type="text" id="behoerde" name="behoerde" class="form-control form-control-lg" required placeholder="z.B. Stadt Kiel, Kreis Rendsburg-Eckernförde">
</div>
<div class="mb-4">
<label for="kontakt_email" class="form-label">Kontakt-E-Mail</label>
<input type="email" id="kontakt_email" name="kontakt_email" class="form-control form-control-lg" required placeholder="kontakt@behoerde.de">
</div>
<button type="submit" class="btn btn-primary btn-lg">Projekt einreichen</button>
</form>
</div>
</div>
</section>
{% endblock %}