Rebrand application as MetalCircle
This commit is contained in:
+189
-8
@@ -21,7 +21,8 @@ except ImportError:
|
||||
Image = ImageOps = UnidentifiedImageError = None
|
||||
|
||||
from fastapi import FastAPI, File, Form, Request, UploadFile
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||
@@ -77,8 +78,8 @@ INITIAL_ADMIN_PASSWORD = os.environ.get("INITIAL_ADMIN_PASSWORD")
|
||||
INITIAL_ADMIN_EMAIL = os.environ.get("INITIAL_ADMIN_EMAIL")
|
||||
|
||||
BADGE_DEFINITIONS = (
|
||||
("founder", "Gründer", "⚔️", None, "Von Anfang an dabei und Pingu Concerts mit aufgebaut", "special"),
|
||||
("admin", "Admin", "🏴☠️", None, "Verantwortung für Pingu Concerts", "special"),
|
||||
("founder", "Gründer", "⚔️", None, "Von Anfang an dabei und MetalCircle mit aufgebaut", "special"),
|
||||
("admin", "Admin", "🏴☠️", None, "Verantwortung für MetalCircle", "special"),
|
||||
("beta_tester", "Beta Tester", "🧪", None, "In der Beta dabei", "beta"),
|
||||
("first_gig", "Erster Gig", "🎸", 1, "Dein erstes besuchtes Konzert", "attendance"),
|
||||
("regular", "Stammgast", "🤘", 5, "5 Konzerte am selben Veranstaltungsort besucht", "attendance"),
|
||||
@@ -405,7 +406,7 @@ async def lifespan(_app: FastAPI):
|
||||
yield
|
||||
|
||||
|
||||
app = FastAPI(title="Pingu Concerts", lifespan=lifespan)
|
||||
app = FastAPI(title="MetalCircle", lifespan=lifespan)
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
@@ -458,6 +459,8 @@ async def require_login(request: Request, call_next):
|
||||
or request.url.path == "/register"
|
||||
or request.url.path.startswith("/register/")
|
||||
or request.url.path.startswith("/password-reset")
|
||||
or request.url.path in {"/impressum", "/datenschutz"}
|
||||
or request.url.path == "/profile/export"
|
||||
or request.url.path.startswith("/static/")
|
||||
):
|
||||
return await call_next(request)
|
||||
@@ -2037,6 +2040,18 @@ def register_page(token: str):
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Rechtliche Hinweise
|
||||
|
||||
@app.get("/impressum", response_class=HTMLResponse)
|
||||
def impressum_page():
|
||||
return templates.get_template("impressum.html").render()
|
||||
|
||||
|
||||
@app.get("/datenschutz", response_class=HTMLResponse)
|
||||
def privacy_page():
|
||||
return templates.get_template("datenschutz.html").render()
|
||||
|
||||
|
||||
# Login
|
||||
# ============================================================
|
||||
|
||||
@@ -2280,6 +2295,134 @@ def own_profile(request: Request, saved: str = ""):
|
||||
)
|
||||
|
||||
|
||||
@app.get("/profile/export")
|
||||
def export_profile_data(request: Request):
|
||||
"""Download the authenticated user's application data as JSON."""
|
||||
user = get_current_user(request)
|
||||
if not user:
|
||||
return login_redirect("/profile/export")
|
||||
|
||||
user_id = user["id"]
|
||||
with get_db_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT id, username, email, display_name, avatar_path,
|
||||
instagram_url, profile_visibility, is_admin, created_at
|
||||
FROM users WHERE id = %s
|
||||
""",
|
||||
(user_id,),
|
||||
)
|
||||
account = cursor.fetchone()
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT c.id, c.artist, c.event_type, c.start_datetime, c.end_datetime,
|
||||
c.venue_id, c.description, c.ticket_url, c.ticket_price,
|
||||
c.flyer_path, c.flyer_url, c.visibility, c.created_at
|
||||
FROM concerts c WHERE c.created_by = %s ORDER BY c.start_datetime
|
||||
""",
|
||||
(user_id,),
|
||||
)
|
||||
created_events = cursor.fetchall()
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT concert_id, status, updated_at
|
||||
FROM concert_attendance WHERE user_id = %s ORDER BY updated_at
|
||||
""",
|
||||
(user_id,),
|
||||
)
|
||||
attendance = cursor.fetchall()
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT id, requester_id, addressee_id, status, created_at, updated_at
|
||||
FROM friendships WHERE requester_id = %s OR addressee_id = %s
|
||||
ORDER BY created_at
|
||||
""",
|
||||
(user_id, user_id),
|
||||
)
|
||||
friendships = cursor.fetchall()
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT id, sender_id, recipient_id, body, read_at, created_at
|
||||
FROM direct_messages WHERE sender_id = %s OR recipient_id = %s
|
||||
ORDER BY created_at
|
||||
""",
|
||||
(user_id, user_id),
|
||||
)
|
||||
messages = cursor.fetchall()
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT concert_id, invited_by, viewed_at, created_at
|
||||
FROM event_invitations WHERE user_id = %s ORDER BY created_at
|
||||
""",
|
||||
(user_id,),
|
||||
)
|
||||
invitations = cursor.fetchall()
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT id, concert_id, body, created_at
|
||||
FROM concert_comments WHERE user_id = %s ORDER BY created_at
|
||||
""",
|
||||
(user_id,),
|
||||
)
|
||||
comments = cursor.fetchall()
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT id, concert_id, path, created_at
|
||||
FROM concert_photos WHERE user_id = %s ORDER BY created_at
|
||||
""",
|
||||
(user_id,),
|
||||
)
|
||||
photos = cursor.fetchall()
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT badge_code, awarded_at FROM user_badges
|
||||
WHERE user_id = %s ORDER BY awarded_at
|
||||
""",
|
||||
(user_id,),
|
||||
)
|
||||
badges = cursor.fetchall()
|
||||
|
||||
def rows_to_dicts(rows, keys):
|
||||
return [dict(zip(keys, row)) for row in rows]
|
||||
|
||||
data = {
|
||||
"export_version": 1,
|
||||
"exported_at": datetime.now(),
|
||||
"account": dict(zip(
|
||||
("id", "username", "email", "display_name", "avatar_path",
|
||||
"instagram_url", "profile_visibility", "is_admin", "created_at"),
|
||||
account,
|
||||
)) if account else None,
|
||||
"created_events": rows_to_dicts(
|
||||
created_events,
|
||||
("id", "artist", "event_type", "start_datetime", "end_datetime", "venue_id",
|
||||
"description", "ticket_url", "ticket_price", "flyer_path", "flyer_url",
|
||||
"visibility", "created_at"),
|
||||
),
|
||||
"attendance": rows_to_dicts(attendance, ("concert_id", "status", "updated_at")),
|
||||
"friendships": rows_to_dicts(friendships, ("id", "requester_id", "addressee_id", "status", "created_at", "updated_at")),
|
||||
"messages": rows_to_dicts(messages, ("id", "sender_id", "recipient_id", "body", "read_at", "created_at")),
|
||||
"event_invitations": rows_to_dicts(invitations, ("concert_id", "invited_by", "viewed_at", "created_at")),
|
||||
"comments": rows_to_dicts(comments, ("id", "concert_id", "body", "created_at")),
|
||||
"photos": rows_to_dicts(photos, ("id", "concert_id", "path", "created_at")),
|
||||
"badges": rows_to_dicts(badges, ("badge_code", "awarded_at")),
|
||||
}
|
||||
filename = re.sub(r"[^A-Za-z0-9_-]", "_", user["username"])
|
||||
return JSONResponse(
|
||||
content=jsonable_encoder(data),
|
||||
headers={"Content-Disposition": f'attachment; filename="pingu-concerts-{filename}-daten.json"'},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/users/{username}", response_class=HTMLResponse)
|
||||
def user_profile(request: Request, username: str):
|
||||
return render_profile(request, username)
|
||||
@@ -2375,7 +2518,7 @@ def send_friend_request(request: Request, username: str):
|
||||
|
||||
|
||||
@app.post("/friendships/{friendship_id}/{action}")
|
||||
def manage_friendship(request: Request, friendship_id: int, action: str):
|
||||
def manage_friendship(request: Request, friendship_id: int, action: str, return_to: str = Form("")):
|
||||
user = get_current_user(request)
|
||||
if action not in {"accept", "decline", "remove"}:
|
||||
return HTMLResponse("Ungültige Aktion.", status_code=400)
|
||||
@@ -2405,6 +2548,8 @@ def manage_friendship(request: Request, friendship_id: int, action: str):
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("SELECT username FROM users WHERE id = %s", (other_id,))
|
||||
other = cursor.fetchone()
|
||||
if return_to == "/messages":
|
||||
return RedirectResponse("/messages", status_code=303)
|
||||
return RedirectResponse(f"/users/{other[0]}" if other else "/", status_code=303)
|
||||
|
||||
|
||||
@@ -2440,8 +2585,41 @@ def load_chat_partner(cursor, user, username: str):
|
||||
def message_inbox(request: Request):
|
||||
user = get_current_user(request)
|
||||
conversations = []
|
||||
friend_requests = []
|
||||
event_invitations = []
|
||||
with get_db_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT f.id, u.username, COALESCE(u.display_name, u.username), f.created_at
|
||||
FROM friendships f JOIN users u ON u.id = f.requester_id
|
||||
WHERE f.addressee_id = %s AND f.status = 'pending'
|
||||
ORDER BY f.created_at DESC
|
||||
""",
|
||||
(user["id"],),
|
||||
)
|
||||
friend_requests = [
|
||||
{"id": row[0], "username": row[1], "display_name": row[2],
|
||||
"created_at": row[3].strftime("%d.%m.%Y %H:%M")}
|
||||
for row in cursor.fetchall()
|
||||
]
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT ei.concert_id, c.artist, c.start_datetime,
|
||||
COALESCE(u.display_name, u.username)
|
||||
FROM event_invitations ei
|
||||
JOIN concerts c ON c.id = ei.concert_id
|
||||
LEFT JOIN users u ON u.id = ei.invited_by
|
||||
WHERE ei.user_id = %s AND ei.viewed_at IS NULL
|
||||
ORDER BY ei.created_at DESC
|
||||
""",
|
||||
(user["id"],),
|
||||
)
|
||||
event_invitations = [
|
||||
{"concert_id": row[0], "artist": row[1],
|
||||
"date": row[2].strftime("%d.%m.%Y %H:%M"), "invited_by": row[3] or "Ein Mitglied"}
|
||||
for row in cursor.fetchall()
|
||||
]
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT u.id, u.username, COALESCE(u.display_name, u.username), u.avatar_path
|
||||
@@ -2475,11 +2653,14 @@ def message_inbox(request: Request):
|
||||
"id": row[0], "username": row[1], "display_name": row[2],
|
||||
"avatar_path": row[3], "last_message": latest[0] if latest else None,
|
||||
"last_at": latest[1].strftime("%d.%m.%Y %H:%M") if latest else None,
|
||||
"last_at_raw": latest[1] if latest else None,
|
||||
"last_from_me": bool(latest and latest[2] == user["id"]),
|
||||
"unread_count": latest[3] if latest else 0,
|
||||
})
|
||||
conversations.sort(key=lambda item: (item["unread_count"] > 0, item["last_at_raw"] or datetime.min), reverse=True)
|
||||
template = templates.get_template("messages.html")
|
||||
return template.render(user=user, conversations=conversations, partner=None, messages=[])
|
||||
return template.render(user=user, conversations=conversations, friend_requests=friend_requests,
|
||||
event_invitations=event_invitations, partner=None, messages=[])
|
||||
|
||||
|
||||
@app.get("/messages/{username}", response_class=HTMLResponse)
|
||||
@@ -3375,7 +3556,7 @@ def legacy_search_venues(q: str):
|
||||
if not results:
|
||||
|
||||
headers = {
|
||||
"User-Agent": "PinguConcerts/1.0"
|
||||
"User-Agent": "MetalCircle/1.0"
|
||||
}
|
||||
|
||||
external_query = q
|
||||
@@ -3811,7 +3992,7 @@ def search_venues(q: str):
|
||||
"namedetails": 1,
|
||||
"limit": 25,
|
||||
},
|
||||
headers={"User-Agent": "PinguConcerts/1.0"},
|
||||
headers={"User-Agent": "MetalCircle/1.0"},
|
||||
timeout=8,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Metal+Mania&family=Roboto+Condensed:wght@400;500;700&display=swap');
|
||||
|
||||
:root {
|
||||
--bg: #050505;
|
||||
--surface: #101010;
|
||||
@@ -59,11 +57,16 @@ header {
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 800;
|
||||
font-family: Impact, Haettenschweiler, "Arial Narrow Bold", sans-serif;
|
||||
letter-spacing: .06em;
|
||||
text-transform: uppercase;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
display: block;
|
||||
width: min(300px, 48vw);
|
||||
height: auto;
|
||||
max-height: 74px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.logo span {
|
||||
@@ -378,6 +381,14 @@ form .button {
|
||||
.admin-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
|
||||
.admin-danger { padding: 9px 12px; color: #fecaca; border: 1px solid #7f1d1d; background: #450a0a; border-radius: 8px; cursor: pointer; }
|
||||
.admin-danger:disabled { opacity: .4; cursor: not-allowed; }
|
||||
.data-export { margin: 18px 0 0; text-align: right; font-size: .8rem; }
|
||||
.data-export a { color: var(--muted); text-decoration: underline; }
|
||||
.data-export a:hover { color: var(--text); }
|
||||
.legal-copy { max-width: 820px; margin-left: auto; margin-right: auto; line-height: 1.65; }
|
||||
.legal-copy h2 { margin-top: 28px; color: #fca5a5; }
|
||||
.legal-copy h2:first-child { margin-top: 0; }
|
||||
.legal-copy a { color: #f87171; text-decoration: underline; }
|
||||
.legal-note { margin-top: 28px; padding: 12px 14px; color: var(--muted); background: rgba(5,5,5,.45); border-left: 3px solid var(--accent); font-size: .9rem; }
|
||||
|
||||
/* Heavy-Metal-Theme: Poster, Bühne und Backstage-Pass statt Standard-UI */
|
||||
body::after {
|
||||
@@ -397,12 +408,12 @@ body::after {
|
||||
header, main, .container { position: relative; z-index: 1; }
|
||||
|
||||
h1, h2, h3, .logo, .page-title h1 {
|
||||
font-family: "Metal Mania", Impact, Haettenschweiler, "Arial Narrow Bold", sans-serif;
|
||||
font-family: "Nimbus Sans Narrow", Impact, Haettenschweiler, "Arial Narrow Bold", sans-serif;
|
||||
letter-spacing: .055em;
|
||||
}
|
||||
|
||||
.button, button {
|
||||
font-family: "Roboto Condensed", "Nimbus Sans Narrow", system-ui, sans-serif;
|
||||
font-family: "Nimbus Sans Narrow", system-ui, sans-serif;
|
||||
letter-spacing: .06em;
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
@@ -2,9 +2,10 @@
|
||||
<summary>☠ {{ user.display_name }}{% if user.notification_count %} <span class="metal-notification" title="{{ user.notification_count }} neue Nachricht(en) oder Anfrage(n)" aria-label="Neue Benachrichtigungen">🤘<b>{{ user.notification_count }}</b></span>{% endif %}</summary>
|
||||
<div class="user-menu-panel">
|
||||
<a href="/profile">Mein Profil</a>
|
||||
<a href="/messages">Nachrichten{% if user.unread_message_count %} <span class="menu-count">{{ user.unread_message_count }}</span>{% endif %}</a>
|
||||
{% if user.event_invitation_count %}<a href="/#event-invitations">Veranstaltungseinladungen <span class="menu-count">{{ user.event_invitation_count }}</span></a>{% endif %}
|
||||
<a href="/messages">Nachrichten{% if user.notification_count %} <span class="metal-notification" title="Neue Nachrichten oder Anfragen" aria-label="Neue Nachrichten oder Anfragen">🤘<b>{{ user.notification_count }}</b></span>{% endif %}</a>
|
||||
{% if user.is_admin %}<a href="/admin">⚙️ Verwaltung</a>{% endif %}
|
||||
<a href="/datenschutz">Datenschutz</a>
|
||||
<a href="/impressum">Impressum</a>
|
||||
<form method="post" action="/logout">
|
||||
<button type="submit">Abmelden</button>
|
||||
</form>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html><html lang="de"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Account-Link · Pingu Concerts</title><link rel="stylesheet" href="/static/css/style.css"></head><body>
|
||||
<header><div class="header-inner"><a href="/" class="logo">🎸 Pingu <span>Concerts</span></a>{% include '_user_menu.html' %}</div></header>
|
||||
<!DOCTYPE html><html lang="de"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Account-Link · MetalCircle</title><link rel="icon" type="image/png" href="/static/images/metalcircle-circle.png"><link rel="stylesheet" href="/static/css/style.css"></head><body>
|
||||
<header><div class="header-inner"><a href="/" class="logo"><img class="brand-logo" src="/static/images/metalcircle-full.png" alt="MetalCircle"></a>{% include '_user_menu.html' %}</div></header>
|
||||
<main><div class="page-title"><h1>Passwort zurücksetzen</h1><p>Reset-Link für @{{ target_username }} · {{ target_email }}</p></div>
|
||||
<section class="admin-section"><p>Diesen persönlichen Link sicher an den User senden. Er ist zwei Stunden gültig und nur einmal verwendbar.</p><label class="invite-link" style="display:block;overflow-wrap:anywhere">{{ account_url }}</label><p><a class="button button-secondary" href="/admin/users">Zurück</a></p></section></main></body></html>
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Patch-Bilder · Pingu Concerts</title>
|
||||
<title>Patch-Bilder · MetalCircle</title>
|
||||
<link rel="icon" type="image/png" href="/static/images/metalcircle-circle.png">
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<style>
|
||||
.patch-admin-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 14px; }
|
||||
@@ -15,7 +16,7 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header><div class="header-inner"><a href="/" class="logo">🎸 Pingu <span>Concerts</span></a>{% include '_user_menu.html' %}</div></header>
|
||||
<header><div class="header-inner"><a href="/" class="logo"><img class="brand-logo" src="/static/images/metalcircle-full.png" alt="MetalCircle"></a>{% include '_user_menu.html' %}</div></header>
|
||||
<main>
|
||||
<div class="page-title"><h1>🧵 Patch-Bilder</h1><p>Grafiken für die Patches auf der virtuellen Kutte verwalten.</p></div>
|
||||
{% set admin_section = 'patches' %}{% include '_admin_nav.html' %}
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Benutzerverwaltung · Pingu Concerts</title>
|
||||
<title>Benutzerverwaltung · MetalCircle</title>
|
||||
<link rel="icon" type="image/png" href="/static/images/metalcircle-circle.png">
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<style>
|
||||
.invite-link { display: block; margin-top: 14px; padding: 12px; overflow-wrap: anywhere; background: #0f1420; border: 1px solid var(--border); border-radius: 9px; color: #c4b5fd; }
|
||||
@@ -17,7 +18,7 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header><div class="header-inner"><a href="/" class="logo">🎸 Pingu <span>Concerts</span></a>{% include '_user_menu.html' %}</div></header>
|
||||
<header><div class="header-inner"><a href="/" class="logo"><img class="brand-logo" src="/static/images/metalcircle-full.png" alt="MetalCircle"></a>{% include '_user_menu.html' %}</div></header>
|
||||
<main>
|
||||
<div class="page-title"><h1>⚙️ Verwaltung</h1><p>Benutzerkonten und Einladungen verwalten.</p></div>
|
||||
{% set admin_section = 'users' %}{% include '_admin_nav.html' %}
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Veranstaltungsorte · Pingu Concerts</title>
|
||||
<title>Veranstaltungsorte · MetalCircle</title>
|
||||
<link rel="icon" type="image/png" href="/static/images/metalcircle-circle.png">
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<style>
|
||||
.venue-fields { display: grid; grid-template-columns: 2fr 2fr .8fr 1.4fr 1.2fr; gap: 8px; }
|
||||
@@ -29,7 +30,7 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header><div class="header-inner"><a href="/" class="logo">🎸 Pingu <span>Concerts</span></a>{% include '_user_menu.html' %}</div></header>
|
||||
<header><div class="header-inner"><a href="/" class="logo"><img class="brand-logo" src="/static/images/metalcircle-full.png" alt="MetalCircle"></a>{% include '_user_menu.html' %}</div></header>
|
||||
<main>
|
||||
<div class="page-title"><h1>📍 Veranstaltungsorte</h1><p>Orte korrigieren, bestätigen, zusammenführen oder entfernen.</p></div>
|
||||
{% set admin_section = 'venues' %}{% include '_admin_nav.html' %}
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
content="width=device-width, initial-scale=1.0"
|
||||
>
|
||||
|
||||
<title>{{ concert.artist }} | Pingu Concerts</title>
|
||||
<title>{{ concert.artist }} | MetalCircle</title>
|
||||
<link rel="icon" type="image/png" href="/static/images/metalcircle-circle.png">
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Datenschutz · MetalCircle</title><link rel="icon" type="image/png" href="/static/images/metalcircle-circle.png"><link rel="stylesheet" href="/static/css/style.css"></head>
|
||||
<body>
|
||||
<header><div class="header-inner"><a href="/" class="logo"><img class="brand-logo" src="/static/images/metalcircle-full.png" alt="MetalCircle"></a></div></header>
|
||||
<main>
|
||||
<div class="page-title"><h1>Datenschutz</h1><p>Hinweise zur Verarbeitung deiner Daten</p></div>
|
||||
<section class="admin-section legal-copy">
|
||||
<h2>Verantwortlicher</h2>
|
||||
<p>Kai Piekny, Fleyerstr. 33, 58097 Hagen<br><a href="mailto:konzert@pinguholic.de">konzert@pinguholic.de</a></p>
|
||||
<h2>Welche Daten werden gespeichert?</h2>
|
||||
<p>Für die Nutzung werden insbesondere Benutzername, E-Mail-Adresse, Passwort-Hash, Anzeigename, optionale Profil- und Instagram-Angaben, Profilbild, Freundschaften, Nachrichten, Veranstaltungsteilnahmen, Kommentare, Fotos, Patches sowie von dir angelegte Veranstaltungen gespeichert.</p>
|
||||
<h2>Wofür werden sie verwendet?</h2>
|
||||
<p>Die Daten werden ausschließlich für Anmeldung, Kontoverwaltung, Veranstaltungsfunktionen, Freundschaften, Nachrichten, Benachrichtigungen und die von dir gewählten Sichtbarkeitseinstellungen verarbeitet.</p>
|
||||
<h2>Rechtsgrundlage und Speicherdauer</h2>
|
||||
<p>Die Verarbeitung erfolgt im geschlossenen Projektbetrieb zur Bereitstellung der gewünschten Funktionen und – soweit erforderlich – auf Grundlage deiner Einwilligung. Daten bleiben gespeichert, solange dein Konto besteht oder gesetzliche Aufbewahrungspflichten gelten. Nicht mehr benötigte Daten werden gelöscht.</p>
|
||||
<h2>Weitergabe und externe Dienste</h2>
|
||||
<p>Es werden keine Werbe- oder Trackingdienste eingesetzt. Bei der Veranstaltungsortsuche können Suchanfragen an einen externen Geocoding-Dienst übermittelt werden. Externe Flyer- und Instagram-Links werden beim Aufruf direkt von deinem Browser geladen; dafür gelten die Datenschutzbestimmungen des jeweiligen Anbieters.</p>
|
||||
<h2>Deine Rechte</h2>
|
||||
<p>Du kannst Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung und – soweit anwendbar – Datenübertragbarkeit verlangen. Einen Export deiner gespeicherten Anwendungsdaten findest du klein am Ende des eigenen Profilbereichs. Anfragen bitte an <a href="mailto:konzert@pinguholic.de">konzert@pinguholic.de</a>.</p>
|
||||
<h2>Cookies und Sicherheit</h2>
|
||||
<p>Für die Anmeldung wird ausschließlich ein technisch notwendiges, HttpOnly-Sitzungscookie verwendet. Im späteren HTTPS-Betrieb wird es zusätzlich mit dem Secure-Flag gesetzt. Passwörter werden nicht im Klartext gespeichert.</p>
|
||||
<p class="legal-note">Diese Information ist eine technische Projektgrundlage und ersetzt keine individuelle rechtliche Prüfung. Vor einer öffentlichen Veröffentlichung sollten Hostinganbieter, Auftragsverarbeiter, Löschfristen und der externe Geocoding-Dienst konkret ergänzt und geprüft werden.</p>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -3,13 +3,14 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ concert.artist }} bearbeiten · Pingu Concerts</title>
|
||||
<title>{{ concert.artist }} bearbeiten · MetalCircle</title>
|
||||
<link rel="icon" type="image/png" href="/static/images/metalcircle-circle.png">
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="header-inner">
|
||||
<a href="/" class="logo">🎸 Pingu <span>Concerts</span></a>
|
||||
<a href="/" class="logo"><img class="brand-logo" src="/static/images/metalcircle-full.png" alt="MetalCircle"></a>
|
||||
{% include '_user_menu.html' %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Impressum · MetalCircle</title><link rel="icon" type="image/png" href="/static/images/metalcircle-circle.png"><link rel="stylesheet" href="/static/css/style.css"></head>
|
||||
<body>
|
||||
<header><div class="header-inner"><a href="/" class="logo"><img class="brand-logo" src="/static/images/metalcircle-full.png" alt="MetalCircle"></a></div></header>
|
||||
<main>
|
||||
<div class="page-title"><h1>Impressum</h1><p>Anbieterkennzeichnung</p></div>
|
||||
<section class="admin-section legal-copy">
|
||||
<h2>MetalCircle</h2>
|
||||
<p><strong>Verantwortlich für den Inhalt</strong></p>
|
||||
<p>Kai Piekny<br>Fleyerstr. 33<br>58097 Hagen<br>Deutschland</p>
|
||||
<p>E-Mail: <a href="mailto:konzert@pinguholic.de">konzert@pinguholic.de</a></p>
|
||||
<p class="legal-note">Dieses private Projekt befindet sich derzeit im geschlossenen Betrieb. Vor einer Veröffentlichung werden die Angaben und rechtlichen Anforderungen nochmals geprüft.</p>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -10,7 +10,8 @@
|
||||
content="width=device-width, initial-scale=1.0"
|
||||
>
|
||||
|
||||
<title>Pingu Concerts</title>
|
||||
<title>MetalCircle</title>
|
||||
<link rel="icon" type="image/png" href="/static/images/metalcircle-circle.png">
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
@@ -175,7 +176,7 @@
|
||||
}
|
||||
|
||||
.page-header { padding: 24px; border: 1px solid #5f1515; border-left: 4px solid #dc2626; border-radius: 14px; background: linear-gradient(110deg, rgba(10,10,10,.9), rgba(69,10,10,.28)); box-shadow: 0 12px 30px rgba(0,0,0,.3); }
|
||||
.page-header h1 { font-family: "Metal Mania", Impact, Haettenschweiler, "Arial Narrow Bold", sans-serif; letter-spacing: .05em; text-transform: uppercase; text-shadow: 0 2px 18px rgba(185, 28, 28, .45); }
|
||||
.page-header h1 { font-family: "Nimbus Sans Narrow", Impact, Haettenschweiler, "Arial Narrow Bold", sans-serif; letter-spacing: .05em; text-transform: uppercase; text-shadow: 0 2px 18px rgba(185, 28, 28, .45); }
|
||||
.new-concert-button { background: #991b1b; border: 1px solid #dc2626; }
|
||||
.new-concert-button:hover { background: #b91c1c; }
|
||||
.admin-button:hover { border-color: #dc2626; }
|
||||
@@ -232,11 +233,11 @@
|
||||
<div>
|
||||
|
||||
<h1>
|
||||
{% if archive %}☠ Vergangene Veranstaltungen{% else %}🐧 Pingu Concerts{% endif %}
|
||||
{% if archive %}☠ Vergangene Veranstaltungen{% else %}<img class="brand-logo" src="/static/images/metalcircle-full.png" alt="MetalCircle">{% endif %}
|
||||
</h1>
|
||||
|
||||
<div class="subtitle">
|
||||
{% if archive %}Das Archiv vergangener Nächte{% else %}Konzerte, Festivals und alles davor{% endif %}
|
||||
<strong>Enter the Pit. Join the Circle.</strong>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Anmelden · Pingu Concerts</title>
|
||||
<title>Anmelden · MetalCircle</title>
|
||||
<link rel="icon" type="image/png" href="/static/images/metalcircle-circle.png">
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="header-inner">
|
||||
<a href="/" class="logo">🎸 Pingu <span>Concerts</span></a>
|
||||
<a href="/" class="logo"><img class="brand-logo" src="/static/images/metalcircle-full.png" alt="MetalCircle"></a>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
@@ -33,7 +34,6 @@
|
||||
</p>
|
||||
<button type="submit" class="button">Anmelden</button>
|
||||
<p><small>Passwort vergessen? Bitte einen Admin um einen zeitlich begrenzten Reset-Link.</small></p>
|
||||
<a href="/" class="button button-secondary">Abbrechen</a>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Nachrichten · Pingu Concerts</title>
|
||||
<title>Nachrichten · MetalCircle</title>
|
||||
<link rel="icon" type="image/png" href="/static/images/metalcircle-circle.png">
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<style>
|
||||
.conversation-list { display: grid; gap: 10px; }
|
||||
@@ -23,11 +24,23 @@
|
||||
.chat-form { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 10px; margin-top: 12px; max-width: none; }
|
||||
.chat-form textarea { min-height: 76px; margin: 0; resize: vertical; }
|
||||
.empty-chat { color: var(--muted); text-align: center; margin: auto; }
|
||||
.message-notifications { margin-bottom: 24px; padding: 16px; background: rgba(69,10,10,.45); border: 1px solid #7f1d1d; border-radius: 14px; }
|
||||
.message-notifications h2, .conversation-heading { margin: 0 0 12px; font-size: 1.15rem; }
|
||||
.notification-row { display: flex; align-items: center; gap: 11px; padding: 11px; color: var(--text); border-top: 1px solid rgba(127,29,29,.55); }
|
||||
a.notification-row:hover { background: rgba(127,29,29,.25); }
|
||||
.notification-icon { flex: 0 0 auto; font-size: 1.35rem; }
|
||||
.notification-row > span:nth-child(2) { min-width: 0; flex: 1; }
|
||||
.notification-row small { display: block; margin-top: 3px; color: var(--muted); }
|
||||
.notification-actions { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.notification-actions form { margin: 0; }
|
||||
.notification-actions .button { padding: 7px 10px; font-size: .82rem; }
|
||||
.notification-link { color: #fca5a5; white-space: nowrap; }
|
||||
.conversation-heading { margin-top: 20px; }
|
||||
@media (max-width: 600px) { .chat-form { grid-template-columns: 1fr; } .chat-bubble { max-width: 90%; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header><div class="header-inner"><a href="/" class="logo">🎸 Pingu <span>Concerts</span></a>{% include '_user_menu.html' %}</div></header>
|
||||
<header><div class="header-inner"><a href="/" class="logo"><img class="brand-logo" src="/static/images/metalcircle-full.png" alt="MetalCircle"></a>{% include '_user_menu.html' %}</div></header>
|
||||
<main>
|
||||
{% if partner %}
|
||||
<div class="chat-header">
|
||||
@@ -45,6 +58,26 @@
|
||||
<p><a class="button button-secondary" href="/messages">← Alle Nachrichten</a></p>
|
||||
{% else %}
|
||||
<div class="page-title"><h1>🤘 Nachrichten</h1><p>Private Chats mit deinen Freunden.</p></div>
|
||||
{% if friend_requests or event_invitations %}
|
||||
<section class="message-notifications">
|
||||
<h2>Neue Anfragen</h2>
|
||||
{% for request in friend_requests %}
|
||||
<div class="notification-row">
|
||||
<span class="notification-icon">🤝</span>
|
||||
<span><strong>{{ request.display_name }}</strong> möchte sich mit dir befreunden.<small>{{ request.created_at }}</small></span>
|
||||
<span class="notification-actions"><form method="post" action="/friendships/{{ request.id }}/accept"><input type="hidden" name="return_to" value="/messages"><button class="button" type="submit">Annehmen</button></form><form method="post" action="/friendships/{{ request.id }}/decline"><input type="hidden" name="return_to" value="/messages"><button class="button button-secondary" type="submit">Ablehnen</button></form></span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% for invitation in event_invitations %}
|
||||
<a class="notification-row" href="/concerts/{{ invitation.concert_id }}">
|
||||
<span class="notification-icon">✉️</span>
|
||||
<span><strong>{{ invitation.artist }}</strong> – Einladung von {{ invitation.invited_by }}<small>{{ invitation.date }}</small></span>
|
||||
<span class="notification-link">Ansehen →</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endif %}
|
||||
<h2 class="conversation-heading">Unterhaltungen</h2>
|
||||
<div class="conversation-list">
|
||||
{% for conversation in conversations %}
|
||||
<a class="conversation-card" href="/messages/{{ conversation.username }}">
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
content="width=device-width, initial-scale=1.0"
|
||||
>
|
||||
|
||||
<title>Veranstaltung hinzufügen · Pingu Concerts</title>
|
||||
<title>Veranstaltung hinzufügen · MetalCircle</title>
|
||||
<link rel="icon" type="image/png" href="/static/images/metalcircle-circle.png">
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
@@ -75,7 +76,7 @@
|
||||
<div class="header-inner">
|
||||
|
||||
<a href="/" class="logo">
|
||||
🎸 Pingu <span>Concerts</span>
|
||||
<img class="brand-logo" src="/static/images/metalcircle-full.png" alt="MetalCircle">
|
||||
</a>
|
||||
|
||||
{% include '_user_menu.html' %}
|
||||
@@ -322,7 +323,7 @@
|
||||
|
||||
<label for="flyer-url">🔗 Flyer extern verlinken <small>(bevorzugt)</small></label>
|
||||
<input type="url" id="flyer-url" name="flyer_url" placeholder="https://veranstalter.example/flyer.jpg">
|
||||
<div class="flyer-help">Bitte möglichst den offiziellen Flyer des Veranstalters oder der Band verlinken. Das Bild wird nicht auf Pingu Concerts kopiert; der Link wird als Quelle ausgewiesen.</div>
|
||||
<div class="flyer-help">Bitte möglichst den offiziellen Flyer des Veranstalters oder der Band verlinken. Das Bild wird nicht auf MetalCircle kopiert; der Link wird als Quelle ausgewiesen.</div>
|
||||
|
||||
<label for="flyer">
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html><html lang="de"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Passwort zurücksetzen · Pingu Concerts</title><link rel="stylesheet" href="/static/css/style.css"></head><body>
|
||||
<header><div class="header-inner"><a href="/" class="logo">🎸 Pingu <span>Concerts</span></a></div></header><main><div class="page-title"><h1>Passwort zurücksetzen</h1></div><section class="empty">
|
||||
<!DOCTYPE html><html lang="de"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Passwort zurücksetzen · MetalCircle</title><link rel="icon" type="image/png" href="/static/images/metalcircle-circle.png"><link rel="stylesheet" href="/static/css/style.css"></head><body>
|
||||
<header><div class="header-inner"><a href="/" class="logo"><img class="brand-logo" src="/static/images/metalcircle-full.png" alt="MetalCircle"></a></div></header><main><div class="page-title"><h1>Passwort zurücksetzen</h1></div><section class="empty">
|
||||
{% if error %}<p role="alert">{{ error }}</p>{% endif %}<form method="post" action="/password-reset/{{ token }}"><label>Neues Passwort<br><input type="password" name="password" minlength="10" required autocomplete="new-password"></label><br><label>Passwort wiederholen<br><input type="password" name="password_repeat" minlength="10" required autocomplete="new-password"></label><br><button class="button" type="submit">Passwort speichern</button></form>
|
||||
</section></main></body></html>
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ profile.display_name }} · Pingu Concerts</title>
|
||||
<title>{{ profile.display_name }} · MetalCircle</title>
|
||||
<link rel="icon" type="image/png" href="/static/images/metalcircle-circle.png">
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<style>
|
||||
.profile-layout { display: grid; grid-template-columns: minmax(240px, .8fr) minmax(0, 1.4fr); gap: 24px; align-items: start; }
|
||||
@@ -57,7 +58,7 @@
|
||||
<body>
|
||||
<header>
|
||||
<div class="header-inner">
|
||||
<a href="/" class="logo">🎸 Pingu <span>Concerts</span></a>
|
||||
<a href="/" class="logo"><img class="brand-logo" src="/static/images/metalcircle-full.png" alt="MetalCircle"></a>
|
||||
{% include '_user_menu.html' %}
|
||||
</div>
|
||||
</header>
|
||||
@@ -186,6 +187,7 @@
|
||||
<span class="avatar-upload-status" id="avatar-upload-status">Große Bilder werden vor dem Upload automatisch optimiert.</span>
|
||||
<button class="button" type="submit">Profil speichern</button>
|
||||
</form>
|
||||
<p class="data-export"><a href="/profile/export">Meine gespeicherten Daten exportieren (JSON)</a></p>
|
||||
</section>
|
||||
{% endif %}
|
||||
</main>
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<title>Pingu Concerts - Registrierung</title>
|
||||
<title>MetalCircle - Registrierung</title>
|
||||
<link rel="icon" type="image/png" href="/static/images/metalcircle-circle.png">
|
||||
|
||||
<style>
|
||||
body {
|
||||
@@ -31,12 +32,12 @@
|
||||
|
||||
<body>
|
||||
|
||||
<h1>🎸 Pingu Concerts</h1>
|
||||
<p><img src="/static/images/metalcircle-full.png" alt="MetalCircle" style="display:block;max-width:100%;height:auto;margin:0 auto 24px"></p>
|
||||
|
||||
<h2>Einladung angenommen</h2>
|
||||
|
||||
<p>
|
||||
Erstelle deinen Account für Pingu Concerts.
|
||||
Erstelle deinen Account für MetalCircle.
|
||||
</p>
|
||||
|
||||
<form method="post" action="/register">
|
||||
|
||||
Reference in New Issue
Block a user