bearbeiten Button für admin angepasst, zusage funktion auch hinzugefügt
This commit is contained in:
+77
@@ -108,6 +108,17 @@ def ensure_schema():
|
|||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
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:
|
with get_db_connection() as connection:
|
||||||
@@ -1090,6 +1101,22 @@ def concert_detail(request: Request, concert_id: int):
|
|||||||
)
|
)
|
||||||
photo_rows = cursor.fetchall()
|
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 = [
|
comments = [
|
||||||
{
|
{
|
||||||
"id": row[0],
|
"id": row[0],
|
||||||
@@ -1110,6 +1137,20 @@ def concert_detail(request: Request, concert_id: int):
|
|||||||
for row in photo_rows
|
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")
|
template = templates.get_template("concert_detail.html")
|
||||||
return template.render(
|
return template.render(
|
||||||
user=user,
|
user=user,
|
||||||
@@ -1118,6 +1159,9 @@ def concert_detail(request: Request, concert_id: int):
|
|||||||
photos=photos,
|
photos=photos,
|
||||||
can_edit=can_edit_concert(user, concert),
|
can_edit=can_edit_concert(user, concert),
|
||||||
can_delete=can_delete_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)
|
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("<h1>Ungültige Auswahl</h1>", status_code=400)
|
||||||
|
if not load_concert(concert_id):
|
||||||
|
return HTMLResponse("<h1>Konzert nicht gefunden</h1>", 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")
|
@app.post("/concerts/{concert_id}/comments")
|
||||||
def add_comment(
|
def add_comment(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|||||||
+90
-18
@@ -30,16 +30,6 @@ a {
|
|||||||
color: #ffffff;
|
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 {
|
.detail-actions {
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -51,26 +41,44 @@ a {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.delete-concert-button {
|
.concert-action {
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
padding: 10px 14px;
|
align-items: center;
|
||||||
background: #b42318;
|
justify-content: center;
|
||||||
|
min-height: 42px;
|
||||||
|
padding: 10px 16px;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 9px;
|
border-radius: 10px;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
line-height: 1;
|
||||||
|
text-decoration: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: transform 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.delete-concert-button:hover {
|
.concert-action:hover {
|
||||||
background: #dc2626;
|
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;
|
background: #2ea043;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.concert-action-delete {
|
||||||
|
background: #b42318;
|
||||||
|
}
|
||||||
|
|
||||||
|
.concert-action-delete:hover {
|
||||||
|
background: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
.concert-detail {
|
.concert-detail {
|
||||||
background: #161b22;
|
background: #161b22;
|
||||||
border: 1px solid #30363d;
|
border: 1px solid #30363d;
|
||||||
@@ -180,6 +188,70 @@ a {
|
|||||||
color: #8b949e;
|
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) {
|
@media (max-width: 600px) {
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
|
|||||||
@@ -173,8 +173,8 @@
|
|||||||
|
|
||||||
{% if can_edit %}
|
{% if can_edit %}
|
||||||
|
|
||||||
<div class="detail-actions">
|
<div class="detail-actions" aria-label="Konzert verwalten">
|
||||||
<a href="/concerts/{{ concert.id }}/edit" class="edit-button">
|
<a href="/concerts/{{ concert.id }}/edit" class="concert-action concert-action-edit">
|
||||||
✏️ Konzert bearbeiten
|
✏️ Konzert bearbeiten
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@@ -185,7 +185,7 @@
|
|||||||
action="/concerts/{{ concert.id }}/delete"
|
action="/concerts/{{ concert.id }}/delete"
|
||||||
onsubmit="return confirm('Dieses Konzert wirklich löschen?');"
|
onsubmit="return confirm('Dieses Konzert wirklich löschen?');"
|
||||||
>
|
>
|
||||||
<button type="submit" class="delete-concert-button">
|
<button type="submit" class="concert-action concert-action-delete">
|
||||||
🗑️ Konzert löschen
|
🗑️ Konzert löschen
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -196,16 +196,67 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
<section class="coming-soon">
|
<section class="attendance-section" id="attendance">
|
||||||
|
|
||||||
<h2>
|
<h2>
|
||||||
🐧 Community
|
🐧 Wer ist dabei?
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p>
|
<p>Wähle, wie es bei dir aussieht. Deine Auswahl kann jederzeit geändert werden.</p>
|
||||||
Wer geht hin, wer ist interessiert?
|
|
||||||
Kommentare und Teilnehmer folgen hier.
|
<div class="attendance-actions">
|
||||||
</p>
|
|
||||||
|
<form method="post" action="/concerts/{{ concert.id }}/attendance">
|
||||||
|
<input type="hidden" name="status" value="attending">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="attendance-option {% if current_attendance == 'attending' %}is-selected{% endif %}"
|
||||||
|
>
|
||||||
|
✓ Zugesagt
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form method="post" action="/concerts/{{ concert.id }}/attendance">
|
||||||
|
<input type="hidden" name="status" value="maybe">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="attendance-option {% if current_attendance == 'maybe' %}is-selected{% endif %}"
|
||||||
|
>
|
||||||
|
? Vielleicht
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form method="post" action="/concerts/{{ concert.id }}/attendance">
|
||||||
|
<input type="hidden" name="status" value="ticket_search">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="attendance-option {% if current_attendance == 'ticket_search' %}is-selected{% endif %}"
|
||||||
|
>
|
||||||
|
🎟️ Suche Ticket
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<details class="attendance-list">
|
||||||
|
<summary>
|
||||||
|
{{ attending_count }} Zusage{% if attending_count != 1 %}n{% endif %} anzeigen
|
||||||
|
</summary>
|
||||||
|
|
||||||
|
{% if attending_users %}
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{% for attendee in attending_users %}
|
||||||
|
<li>{{ attendee.name }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
<p>Noch niemand hat zugesagt.</p>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
</details>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,16 @@ CREATE TABLE concert_photos (
|
|||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE 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)
|
||||||
|
);
|
||||||
|
|
||||||
CREATE INDEX idx_concerts_start_datetime
|
CREATE INDEX idx_concerts_start_datetime
|
||||||
ON concerts(start_datetime);
|
ON concerts(start_datetime);
|
||||||
|
|
||||||
@@ -83,3 +93,6 @@ CREATE INDEX idx_concert_comments_concert
|
|||||||
|
|
||||||
CREATE INDEX idx_concert_photos_concert
|
CREATE INDEX idx_concert_photos_concert
|
||||||
ON concert_photos(concert_id, created_at);
|
ON concert_photos(concert_id, created_at);
|
||||||
|
|
||||||
|
CREATE INDEX idx_concert_attendance_concert
|
||||||
|
ON concert_attendance(concert_id, status);
|
||||||
|
|||||||
@@ -48,3 +48,16 @@ CREATE TABLE IF NOT EXISTS concert_photos (
|
|||||||
path TEXT NOT NULL,
|
path TEXT NOT NULL,
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
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)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_concert_attendance_concert
|
||||||
|
ON concert_attendance(concert_id, status);
|
||||||
|
|||||||
Reference in New Issue
Block a user