diff --git a/app/main.py b/app/main.py
index e27463a..0c27d48 100644
--- a/app/main.py
+++ b/app/main.py
@@ -108,6 +108,17 @@ def ensure_schema():
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
)
""",
+ """
+ CREATE TABLE IF NOT EXISTS concert_attendance (
+ concert_id INTEGER NOT NULL REFERENCES concerts(id) ON DELETE CASCADE,
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ status VARCHAR(20) NOT NULL CHECK (
+ status IN ('attending', 'maybe', 'ticket_search')
+ ),
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (concert_id, user_id)
+ )
+ """,
]
with get_db_connection() as connection:
@@ -1090,6 +1101,22 @@ def concert_detail(request: Request, concert_id: int):
)
photo_rows = cursor.fetchall()
+ cursor.execute(
+ """
+ SELECT
+ concert_attendance.user_id,
+ concert_attendance.status,
+ users.display_name,
+ users.username
+ FROM concert_attendance
+ JOIN users ON users.id = concert_attendance.user_id
+ WHERE concert_attendance.concert_id = %s
+ ORDER BY users.display_name NULLS LAST, users.username
+ """,
+ (concert_id,),
+ )
+ attendance_rows = cursor.fetchall()
+
comments = [
{
"id": row[0],
@@ -1110,6 +1137,20 @@ def concert_detail(request: Request, concert_id: int):
for row in photo_rows
]
+ attendance = [
+ {
+ "user_id": row[0],
+ "status": row[1],
+ "name": row[2] or row[3],
+ }
+ for row in attendance_rows
+ ]
+ attending_users = [item for item in attendance if item["status"] == "attending"]
+ current_attendance = next(
+ (item["status"] for item in attendance if item["user_id"] == user["id"]),
+ None,
+ )
+
template = templates.get_template("concert_detail.html")
return template.render(
user=user,
@@ -1118,6 +1159,9 @@ def concert_detail(request: Request, concert_id: int):
photos=photos,
can_edit=can_edit_concert(user, concert),
can_delete=can_delete_concert(user, concert),
+ current_attendance=current_attendance,
+ attending_users=attending_users,
+ attending_count=len(attending_users),
)
@@ -1404,6 +1448,39 @@ def delete_concert(request: Request, concert_id: int):
return RedirectResponse("/", status_code=303)
+@app.post("/concerts/{concert_id}/attendance")
+def set_attendance(
+ request: Request,
+ concert_id: int,
+ status: str = Form(...),
+):
+ user = get_current_user(request)
+
+ if not user:
+ return login_redirect(f"/concerts/{concert_id}")
+ if status not in {"attending", "maybe", "ticket_search"}:
+ return HTMLResponse("
Ungültige Auswahl
", status_code=400)
+ if not load_concert(concert_id):
+ return HTMLResponse("Konzert nicht gefunden
", status_code=404)
+
+ with get_db_connection() as connection:
+ with connection.cursor() as cursor:
+ cursor.execute(
+ """
+ INSERT INTO concert_attendance (concert_id, user_id, status)
+ VALUES (%s, %s, %s)
+ ON CONFLICT (concert_id, user_id)
+ DO UPDATE SET
+ status = EXCLUDED.status,
+ updated_at = CURRENT_TIMESTAMP
+ """,
+ (concert_id, user["id"], status),
+ )
+ connection.commit()
+
+ return RedirectResponse(f"/concerts/{concert_id}#attendance", status_code=303)
+
+
@app.post("/concerts/{concert_id}/comments")
def add_comment(
request: Request,
diff --git a/app/static/style.css b/app/static/style.css
index 2a3ba45..803fbb9 100644
--- a/app/static/style.css
+++ b/app/static/style.css
@@ -30,16 +30,6 @@ a {
color: #ffffff;
}
-.edit-button {
- display: inline-block;
- padding: 10px 14px;
- background: #238636;
- color: #ffffff;
- border-radius: 9px;
- text-decoration: none;
- font-weight: bold;
-}
-
.detail-actions {
margin-top: 30px;
display: flex;
@@ -51,26 +41,44 @@ a {
margin: 0;
}
-.delete-concert-button {
- display: inline-block;
- padding: 10px 14px;
- background: #b42318;
+.concert-action {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 42px;
+ padding: 10px 16px;
color: #ffffff;
border: 0;
- border-radius: 9px;
+ border-radius: 10px;
font: inherit;
font-weight: bold;
+ line-height: 1;
+ text-decoration: none;
cursor: pointer;
+ transition: transform 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
}
-.delete-concert-button:hover {
- background: #dc2626;
+.concert-action:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 5px 16px rgba(0, 0, 0, 0.25);
}
-.edit-button:hover {
+.concert-action-edit {
+ background: #238636;
+}
+
+.concert-action-edit:hover {
background: #2ea043;
}
+.concert-action-delete {
+ background: #b42318;
+}
+
+.concert-action-delete:hover {
+ background: #dc2626;
+}
+
.concert-detail {
background: #161b22;
border: 1px solid #30363d;
@@ -180,6 +188,70 @@ a {
color: #8b949e;
}
+.attendance-section {
+ margin-top: 35px;
+ padding: 25px;
+ background: #0d1117;
+ border: 1px solid #30363d;
+ border-radius: 12px;
+}
+
+.attendance-section h2 {
+ margin-top: 0;
+}
+
+.attendance-section > p {
+ color: #8b949e;
+}
+
+.attendance-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ margin-top: 18px;
+}
+
+.attendance-actions form {
+ margin: 0;
+}
+
+.attendance-option {
+ padding: 10px 14px;
+ background: #161b22;
+ border: 1px solid #3d4856;
+ border-radius: 9px;
+ color: #e6edf3;
+ font: inherit;
+ font-weight: bold;
+ cursor: pointer;
+}
+
+.attendance-option:hover,
+.attendance-option.is-selected {
+ border-color: #58a6ff;
+ background: #1b3554;
+}
+
+.attendance-option.is-selected {
+ color: #ffffff;
+}
+
+.attendance-list {
+ margin-top: 20px;
+ color: #b8c1cc;
+}
+
+.attendance-list summary {
+ color: #58a6ff;
+ font-weight: bold;
+ cursor: pointer;
+}
+
+.attendance-list ul {
+ margin: 12px 0 0;
+ padding-left: 22px;
+}
+
@media (max-width: 600px) {
.container {
diff --git a/app/templates/concert_detail.html b/app/templates/concert_detail.html
index b1fa240..e027279 100644
--- a/app/templates/concert_detail.html
+++ b/app/templates/concert_detail.html
@@ -173,8 +173,8 @@
{% if can_edit %}
-
-
+
+
✏️ Konzert bearbeiten
@@ -185,7 +185,7 @@
action="/concerts/{{ concert.id }}/delete"
onsubmit="return confirm('Dieses Konzert wirklich löschen?');"
>
-