Add heavy metal visual theme and venue badge levels
This commit is contained in:
+32
-19
@@ -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)
|
||||||
|
|||||||
@@ -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 |
@@ -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; }
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
Reference in New Issue
Block a user