Add heavy metal visual theme and venue badge levels

This commit is contained in:
kai
2026-08-27 09:41:15 +02:00
parent fd8c216cc8
commit 0343d9c966
5 changed files with 120 additions and 26 deletions
+32 -19
View File
@@ -81,11 +81,11 @@ BADGE_DEFINITIONS = (
("admin", "Admin", "🏴‍☠️", None, "Verantwortung für Pingu Concerts", "special"), ("admin", "Admin", "🏴‍☠️", None, "Verantwortung für Pingu Concerts", "special"),
("beta_tester", "Beta Tester", "🧪", None, "In der Beta dabei", "beta"), ("beta_tester", "Beta Tester", "🧪", None, "In der Beta dabei", "beta"),
("first_gig", "Erster Gig", "🎸", 1, "Dein erstes besuchtes Konzert", "attendance"), ("first_gig", "Erster Gig", "🎸", 1, "Dein erstes besuchtes Konzert", "attendance"),
("regular", "Stammgast", "🤘", 5, "5 Konzerte besucht", "attendance"), ("regular", "Stammgast", "🤘", 5, "5 Konzerte am selben Veranstaltungsort besucht", "attendance"),
("ten_gigs", "Zehnerrunde", "🔥", 10, "10 Konzerte besucht", "attendance"), ("ten_gigs", "Stammgast · Level 10", "🔥", 10, "10 Konzerte am selben Veranstaltungsort besucht", "attendance"),
("tour_veteran", "Tourveteran", "", 25, "25 Konzerte besucht", "attendance"), ("tour_veteran", "Stammgast · Level 25", "", 25, "25 Konzerte am selben Veranstaltungsort besucht", "attendance"),
("fifty_gigs", "Fünfzig Konzerte", "💀", 50, "50 Konzerte besucht", "attendance"), ("fifty_gigs", "Stammgast · Level 50", "💀", 50, "50 Konzerte am selben Veranstaltungsort besucht", "attendance"),
("hundred_gigs", "Hunderterclub", "👑", 100, "100 Konzerte besucht", "attendance"), ("hundred_gigs", "Stammgast · Level 100", "👑", 100, "100 Konzerte am selben Veranstaltungsort besucht", "attendance"),
) )
ATTENDANCE_BADGE_CODES = tuple( ATTENDANCE_BADGE_CODES = tuple(
badge_code badge_code
@@ -1085,31 +1085,43 @@ def remove_uploaded_file(path: str | None, destination_dir: str, url_prefix: str
return True return True
def attended_concert_count(user_id: int) -> int: def attended_concert_stats(user_id: int) -> tuple[int, int]:
with get_db_connection() as connection: with get_db_connection() as connection:
with connection.cursor() as cursor: with connection.cursor() as cursor:
cursor.execute( cursor.execute(
""" """
SELECT COUNT(*) WITH attended AS (
FROM concert_attendance SELECT concerts.venue_id
JOIN concerts ON concerts.id = concert_attendance.concert_id , concerts.event_type
WHERE concert_attendance.user_id = %s FROM concert_attendance
AND concert_attendance.status = 'attending' JOIN concerts ON concerts.id = concert_attendance.concert_id
AND COALESCE( WHERE concert_attendance.user_id = %s
concerts.end_datetime, AND concert_attendance.status = 'attending'
concerts.start_datetime AND COALESCE(concerts.end_datetime, concerts.start_datetime) < CURRENT_TIMESTAMP
) < CURRENT_TIMESTAMP ), venue_counts AS (
SELECT venue_id, COUNT(*) AS visit_count
FROM attended
WHERE venue_id IS NOT NULL AND event_type <> 'other'
GROUP BY venue_id
)
SELECT
(SELECT COUNT(*) FROM attended),
COALESCE((SELECT MAX(visit_count) FROM venue_counts), 0)
""", """,
(user_id,), (user_id,),
) )
return cursor.fetchone()[0] total, max_same_venue = cursor.fetchone()
return total, max_same_venue
def grant_earned_badges(user_id: int, attended_count: int, registered_at): def grant_earned_badges(user_id: int, attended_count: int, max_same_venue, registered_at):
highest_attendance_badge = None highest_attendance_badge = None
for badge_code, _name, _icon, threshold, _description, category in BADGE_DEFINITIONS: for badge_code, _name, _icon, threshold, _description, category in BADGE_DEFINITIONS:
if category == "attendance" and attended_count >= threshold: qualifies = category == "attendance" and attended_count >= threshold
if category == "attendance" and threshold >= 5:
qualifies = max_same_venue >= threshold
if qualifies:
highest_attendance_badge = badge_code highest_attendance_badge = badge_code
with get_db_connection() as connection: with get_db_connection() as connection:
@@ -2205,10 +2217,11 @@ def render_profile(
connections["incoming"].append(item) connections["incoming"].append(item)
else: else:
connections["outgoing"].append(item) connections["outgoing"].append(item)
attended_count = attended_concert_count(profile["id"]) attended_count, max_same_venue = attended_concert_stats(profile["id"])
grant_earned_badges( grant_earned_badges(
profile["id"], profile["id"],
attended_count, attended_count,
max_same_venue,
profile["registered_at"], profile["registered_at"],
) )
profile = load_profile(username) profile = load_profile(username)
+79 -1
View File
@@ -23,10 +23,20 @@ body {
"Segoe UI", "Segoe UI",
sans-serif; sans-serif;
background: radial-gradient(circle at 50% -15%, #320909 0, transparent 32rem), var(--bg); background:
linear-gradient(rgba(4, 4, 6, .58), rgba(4, 4, 6, .72)),
url('/static/images/crowd-stage-background.webp') center top / cover fixed no-repeat,
var(--bg);
color: var(--text); color: var(--text);
} }
@media (max-width: 700px) {
body {
background-attachment: scroll;
background-position: center top;
}
}
a { a {
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;
@@ -366,6 +376,74 @@ form .button {
.admin-danger { padding: 9px 12px; color: #fecaca; border: 1px solid #7f1d1d; background: #450a0a; border-radius: 8px; cursor: pointer; } .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; } .admin-danger:disabled { opacity: .4; cursor: not-allowed; }
/* Heavy-Metal-Theme: Poster, Bühne und Backstage-Pass statt Standard-UI */
body::after {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
z-index: 0;
opacity: .16;
background-image:
radial-gradient(circle at 18% 22%, rgba(255,255,255,.12) 0 1px, transparent 1px),
radial-gradient(circle at 73% 64%, rgba(255,255,255,.08) 0 1px, transparent 1px);
background-size: 7px 9px, 11px 13px;
mix-blend-mode: screen;
}
header, main, .container { position: relative; z-index: 1; }
h1, h2, h3, .logo, .button, button, .page-title h1 {
font-family: Impact, Haettenschweiler, "Arial Narrow Bold", sans-serif;
letter-spacing: .045em;
}
h1, .page-title h1 {
text-transform: uppercase;
text-shadow: 2px 2px 0 #250707, 0 0 18px rgba(220,38,38,.24);
}
.page-title {
border-left: 4px solid var(--accent);
padding-left: 16px;
}
.button {
border: 1px solid #ef4444;
box-shadow: 0 3px 0 #450a0a, 0 0 14px rgba(185,28,28,.2);
text-transform: uppercase;
}
.button:hover { transform: translateY(-1px); box-shadow: 0 4px 0 #450a0a, 0 0 20px rgba(220,38,38,.35); }
.concert-card, .admin-section, .profile-card, .badges-card, .profile-edit {
box-shadow: 0 12px 28px rgba(0,0,0,.3), inset 0 1px rgba(255,255,255,.025);
}
.concert-card { position: relative; border-left: 3px solid #7f1d1d; }
.concert-card::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
border-radius: inherit;
background: linear-gradient(120deg, rgba(255,255,255,.035), transparent 32%);
}
.event-category {
padding: 3px 8px;
border: 1px solid #5f1515;
border-radius: 999px;
background: rgba(127,29,29,.3);
}
.empty-state { background: rgba(16,16,16,.86); border-style: solid; }
@media (max-width: 600px) {
.page-title { padding-left: 11px; }
.button { min-height: 42px; }
}
@media (max-width: 700px) { @media (max-width: 700px) {
.admin-nav { display: grid; grid-template-columns: 1fr; } .admin-nav { display: grid; grid-template-columns: 1fr; }
.admin-section { padding: 16px; } .admin-section { padding: 16px; }
Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

+6 -3
View File
@@ -14,7 +14,7 @@
<link <link
rel="stylesheet" rel="stylesheet"
href="/static/style.css" href="/static/css/style.css"
> >
<style> <style>
@@ -167,11 +167,14 @@
.user-result-name span { color: #a8a29e; font-size: .86rem; } .user-result-name span { color: #a8a29e; font-size: .86rem; }
body { body {
background: radial-gradient(circle at 50% -20%, #3a0909 0, transparent 34rem), #050505; background:
linear-gradient(rgba(4, 4, 6, .58), rgba(4, 4, 6, .72)),
url('/static/images/crowd-stage-background.webp') center top / cover fixed no-repeat,
#050505;
min-height: 100vh; min-height: 100vh;
} }
.page-header { padding-bottom: 22px; border-bottom: 1px solid #3f2424; } .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: 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: 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 { background: #991b1b; border: 1px solid #dc2626; }
.new-concert-button:hover { background: #b91c1c; } .new-concert-button:hover { background: #b91c1c; }
+3 -3
View File
@@ -7,8 +7,8 @@
<link rel="stylesheet" href="/static/css/style.css"> <link rel="stylesheet" href="/static/css/style.css">
<style> <style>
.profile-layout { display: grid; grid-template-columns: minmax(240px, .8fr) minmax(0, 1.4fr); gap: 24px; align-items: start; } .profile-layout { display: grid; grid-template-columns: minmax(240px, .8fr) minmax(0, 1.4fr); gap: 24px; align-items: start; }
.profile-card, .badges-card, .profile-edit { padding: 24px; background: var(--surface); border: 1px solid var(--border); border-radius: 16px; } .profile-card, .badges-card, .profile-edit { padding: 24px; background: linear-gradient(145deg, rgba(24,24,24,.96), rgba(45,12,12,.8)); border: 1px solid var(--border); border-radius: 16px; box-shadow: 0 14px 32px rgba(0,0,0,.35); }
.profile-card { text-align: center; } .profile-card { text-align: center; border-top: 4px solid var(--accent); }
.avatar, .avatar-fallback { width: 132px; height: 132px; margin: 0 auto 16px; border-radius: 50%; border: 3px solid var(--accent); object-fit: cover; } .avatar, .avatar-fallback { width: 132px; height: 132px; margin: 0 auto 16px; border-radius: 50%; border: 3px solid var(--accent); object-fit: cover; }
.avatar-fallback { display: grid; place-items: center; background: #283048; font-size: 3rem; font-weight: 800; } .avatar-fallback { display: grid; place-items: center; background: #283048; font-size: 3rem; font-weight: 800; }
.profile-name { margin: 0; font-size: 1.7rem; } .profile-name { margin: 0; font-size: 1.7rem; }
@@ -19,7 +19,7 @@
.profile-success { margin-bottom: 18px; padding: 12px 14px; color: #bbf7d0; background: #052e16; border: 1px solid #15803d; border-radius: 9px; } .profile-success { margin-bottom: 18px; padding: 12px 14px; color: #bbf7d0; background: #052e16; border: 1px solid #15803d; border-radius: 9px; }
.instagram-current { display: block; margin: -8px 0 14px; color: #f87171; } .instagram-current { display: block; margin: -8px 0 14px; color: #f87171; }
.avatar-upload-status { display: block; min-height: 1.2em; margin: -8px 0 12px; color: var(--muted); font-size: .85rem; } .avatar-upload-status { display: block; min-height: 1.2em; margin: -8px 0 12px; color: var(--muted); font-size: .85rem; }
.stat { margin-top: 22px; padding: 16px; background: #0f1420; border-radius: 12px; } .stat { margin-top: 22px; padding: 16px; background: rgba(5,5,5,.72); border: 1px solid #5f1515; border-radius: 12px; }
.stat strong { display: block; color: #c4b5fd; font-size: 2.2rem; } .stat strong { display: block; color: #c4b5fd; font-size: 2.2rem; }
.kutte { position: relative; min-height: 320px; padding: 58px 36px 28px; background-image: linear-gradient(90deg, rgba(0,0,0,.2) 0 46%, rgba(255,255,255,.035) 46% 54%, rgba(0,0,0,.2) 54%), url('/static/images/black-denim-texture.webp'); background-size: 100% 100%, 520px auto; background-repeat: no-repeat, repeat; border: 4px solid #292524; border-radius: 28px 28px 14px 14px; box-shadow: inset 0 0 28px 8px rgba(0,0,0,.8), 0 10px 30px rgba(0,0,0,.45); } .kutte { position: relative; min-height: 320px; padding: 58px 36px 28px; background-image: linear-gradient(90deg, rgba(0,0,0,.2) 0 46%, rgba(255,255,255,.035) 46% 54%, rgba(0,0,0,.2) 54%), url('/static/images/black-denim-texture.webp'); background-size: 100% 100%, 520px auto; background-repeat: no-repeat, repeat; border: 4px solid #292524; border-radius: 28px 28px 14px 14px; box-shadow: inset 0 0 28px 8px rgba(0,0,0,.8), 0 10px 30px rgba(0,0,0,.45); }
.kutte::before, .kutte::after { content: ""; position: absolute; top: 0; width: 30%; height: 62px; background: #111; border-bottom: 3px solid #404040; } .kutte::before, .kutte::after { content: ""; position: absolute; top: 0; width: 30%; height: 62px; background: #111; border-bottom: 3px solid #404040; }