From d43a12acf393b06abb5842519225e24626523359 Mon Sep 17 00:00:00 2001 From: MetalCircle Codex Bot Date: Tue, 15 Sep 2026 14:20:34 +0200 Subject: [PATCH] Fix two-column badge layout and descriptions --- app/locales/en.json | 16 ++++++++++++++++ app/main.py | 19 +++++++++++++++++++ app/templates/profile.html | 15 +++++++++------ app/tests/test_i18n.py | 26 +++++++++++++++++++++++--- 4 files changed, 67 insertions(+), 9 deletions(-) diff --git a/app/locales/en.json b/app/locales/en.json index e6a634e..1e9aaab 100644 --- a/app/locales/en.json +++ b/app/locales/en.json @@ -412,6 +412,22 @@ "Pro Tagebucheintrag sind maximal drei Bilder möglich.": "Each diary entry can have a maximum of three images.", "Ein Mitglied": "A member", "Von Anfang an dabei und MetalCircle aufgebaut": "Here from the start and helped build MetalCircle", + "Dieser Patch würdigt dich als Teil der ersten Crew, die MetalCircle möglich gemacht hat. Du hast die Community von Anfang an begleitet und beim Aufbau mitgewirkt.": "This patch recognizes you as part of the first crew that made MetalCircle possible. You have been with the community from the beginning and helped build it.", + "Dieser Patch steht für die Verantwortung, MetalCircle zu verwalten und die Community im Alltag zu unterstützen.": "This patch represents the responsibility of managing MetalCircle and supporting the community in its day-to-day work.", + "Ein besonderer Patch für die treue Gefährtin des Captains – als sichtbares Zeichen für Vertrauen und ihren festen Platz in der Crew.": "A special patch for the Captain's trusted companion, recognizing the trust and the valued place she holds in the crew.", + "Du hast MetalCircle bereits in der Alpha begleitet. Mit deinen frühen Erfahrungen und Rückmeldungen hast du geholfen, die Plattform vor der Beta weiterzuentwickeln.": "You joined MetalCircle during the alpha. Your early experience and feedback helped shape the platform before the beta.", + "Du bist während der geschlossenen Beta Teil der Crew geworden und hast MetalCircle in einer frühen Phase begleitet.": "You joined the crew during the closed beta and supported MetalCircle in an early stage of its journey.", + "Du bist früh zur MetalCircle-Community gestoßen und gehörst zu den ersten, die gemeinsam den Weg der Plattform mitgehen.": "You joined the MetalCircle community early and are among the first members to help the platform grow.", + "Der erste Schritt zählt: Dieser Patch markiert das erste Konzert, das du in MetalCircle als besucht festgehalten hast.": "Every journey starts somewhere. This patch marks the first concert you recorded as attended in MetalCircle.", + "Fünf Besuche am selben Veranstaltungsort machen diesen Ort zu mehr als einem Eintrag im Kalender. Dieser Patch würdigt deine wiederkehrenden Abende dort.": "Five visits turn a venue into more than a name on the calendar. This patch celebrates the many nights you have returned there for live music.", + "Zehn besuchte Konzerte sind ein erster kräftiger Meilenstein deiner Konzertgeschichte. Jeder Eintrag steht für einen Abend, den du live erlebt hast.": "Ten attended concerts are a strong first milestone in your concert history. Each one marks a night of live music you experienced in person.", + "Mit 25 besuchten Konzerten wächst aus einzelnen Abenden eine echte Konzertgeschichte. Dieser Patch hält fest, wie oft du schon mitten im Publikum standest.": "With 25 attended concerts, a collection of nights out becomes a real concert history. This patch marks all the times you have been there in the crowd.", + "50 besuchte Konzerte sind ein großer Meilenstein. Dieser Patch würdigt viele Abende mit lauter Musik, besonderen Erinnerungen und gemeinsamen Erlebnissen.": "Fifty attended concerts are a major milestone. This patch celebrates years of loud music, lasting memories, and nights shared with others.", + "100 besuchte Konzerte: Dieser Patch steht für eine beeindruckende Konzertgeschichte und unzählige Stunden voller Live-Musik.": "One hundred attended concerts make for an impressive history and countless hours spent enjoying live music.", + "Dein Konzertbesuch hat dich zum ersten Mal über die Landesgrenze geführt. Dieser Patch erinnert an den Auftakt deiner internationalen Konzertreise.": "Your first concert abroad took you across a national border. This patch marks the beginning of your international concert journey.", + "Du hast Konzerte in fünf unterschiedlichen Ländern erlebt. Dieser Patch steht für eine Konzertreise, die weit über den eigenen Heimatraum hinausgeht.": "You have attended concerts in five different countries. This patch celebrates a concert journey that has taken you well beyond home.", + "An zwei aufeinanderfolgenden Tagen ein Konzert zu besuchen, zeigt echte Ausdauer. Dieser Patch markiert genau diese Konzertserie.": "Attending concerts on two consecutive days takes dedication. This patch marks that back-to-back run of live music.", + "Drei Konzerte an einem Wochenende: Dieser Patch würdigt ein besonders intensives Wochenende voller Live-Musik.": "Three concerts in one weekend make for an unforgettable run. This patch celebrates a weekend packed with live music.", "In der Beta dabei": "Joined during the beta", "Erster Gig": "First gig", "Stammgast": "Regular", diff --git a/app/main.py b/app/main.py index de8caa2..4917ac8 100644 --- a/app/main.py +++ b/app/main.py @@ -116,6 +116,24 @@ BADGE_DEFINITIONS = ( ("double_trouble", "DOUBLE TROUBLE", "⚡", 2, "Zwei Konzerte an zwei aufeinanderfolgenden Tagen", "streak"), ("iron_weekend", "IRON WEEKEND", "🤘", 3, "Drei Konzerte innerhalb eines Wochenendes", "streak"), ) +BADGE_LONG_DESCRIPTIONS = { + "founder": "Dieser Patch würdigt dich als Teil der ersten Crew, die MetalCircle möglich gemacht hat. Du hast die Community von Anfang an begleitet und beim Aufbau mitgewirkt.", + "admin": "Dieser Patch steht für die Verantwortung, MetalCircle zu verwalten und die Community im Alltag zu unterstützen.", + "captns_mate": "Ein besonderer Patch für die treue Gefährtin des Captains – als sichtbares Zeichen für Vertrauen und ihren festen Platz in der Crew.", + "alpha_tester": "Du hast MetalCircle bereits in der Alpha begleitet. Mit deinen frühen Erfahrungen und Rückmeldungen hast du geholfen, die Plattform vor der Beta weiterzuentwickeln.", + "beta_tester": "Du bist während der geschlossenen Beta Teil der Crew geworden und hast MetalCircle in einer frühen Phase begleitet.", + "early_bird": "Du bist früh zur MetalCircle-Community gestoßen und gehörst zu den ersten, die gemeinsam den Weg der Plattform mitgehen.", + "first_gig": "Der erste Schritt zählt: Dieser Patch markiert das erste Konzert, das du in MetalCircle als besucht festgehalten hast.", + "regular": "Fünf Besuche am selben Veranstaltungsort machen diesen Ort zu mehr als einem Eintrag im Kalender. Dieser Patch würdigt deine wiederkehrenden Abende dort.", + "ten_gigs": "Zehn besuchte Konzerte sind ein erster kräftiger Meilenstein deiner Konzertgeschichte. Jeder Eintrag steht für einen Abend, den du live erlebt hast.", + "tour_veteran": "Mit 25 besuchten Konzerten wächst aus einzelnen Abenden eine echte Konzertgeschichte. Dieser Patch hält fest, wie oft du schon mitten im Publikum standest.", + "fifty_gigs": "50 besuchte Konzerte sind ein großer Meilenstein. Dieser Patch würdigt viele Abende mit lauter Musik, besonderen Erinnerungen und gemeinsamen Erlebnissen.", + "hundred_gigs": "100 besuchte Konzerte: Dieser Patch steht für eine beeindruckende Konzertgeschichte und unzählige Stunden voller Live-Musik.", + "border_breaker": "Dein Konzertbesuch hat dich zum ersten Mal über die Landesgrenze geführt. Dieser Patch erinnert an den Auftakt deiner internationalen Konzertreise.", + "globe_banger": "Du hast Konzerte in fünf unterschiedlichen Ländern erlebt. Dieser Patch steht für eine Konzertreise, die weit über den eigenen Heimatraum hinausgeht.", + "double_trouble": "An zwei aufeinanderfolgenden Tagen ein Konzert zu besuchen, zeigt echte Ausdauer. Dieser Patch markiert genau diese Konzertserie.", + "iron_weekend": "Drei Konzerte an einem Wochenende: Dieser Patch würdigt ein besonders intensives Wochenende voller Live-Musik.", +} ATTENDANCE_BADGE_CODES = tuple( badge_code for badge_code, _name, _icon, _threshold, _description, category in BADGE_DEFINITIONS @@ -2736,6 +2754,7 @@ def render_profile( "icon": icon, "threshold": threshold, "description": description, + "long_description": BADGE_LONG_DESCRIPTIONS[code], "category": category, "earned": code in profile["earned_codes"] and (category != "attendance" or code == highest_visible_attendance), "image_path": badge_assets.get(code), diff --git a/app/templates/profile.html b/app/templates/profile.html index 88cffe2..5e6a609 100644 --- a/app/templates/profile.html +++ b/app/templates/profile.html @@ -26,17 +26,20 @@ .kutte::before, .kutte::after { content: ""; position: absolute; top: 0; width: 30%; height: 62px; background: #111; border-bottom: 3px solid #404040; } .kutte::before { left: 0; border-radius: 24px 0 45% 0; } .kutte::after { right: 0; border-radius: 0 24px 0 45%; } - .patch-grid { display: flex; flex-wrap: wrap; gap: 14px; justify-content: flex-start; align-items: flex-start; } + .patch-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); grid-auto-flow: row; gap: 14px; align-items: start; } + .patch-grid > p { grid-column: 1 / -1; } + .patch-button { display: flex; width: 100%; min-width: 0; justify-content: center; padding:0; border:0; background:transparent; color:inherit; cursor:pointer; } .patch { padding: 5px; display: grid; place-items: center; text-align: center; background: #242424; border: 2px dashed #78716c; border-radius: 9px; color: #f8fafc; } - .patch-image-frame { width: fit-content; height: fit-content; } + .patch-image-frame { width: 100%; height: 140px; } .patch-icon-frame { width: 105px; height: 105px; } .patch-icon { font-size: 2.5rem; } - .patch-image { width: auto; height: auto; max-width: 140px; max-height: 140px; object-fit: contain; display: block; } + .patch-image { width: 100%; height: 100%; max-width: 140px; max-height: 140px; object-fit: contain; display: block; } .patch.locked { opacity: .38; filter: grayscale(1); } .patch.earned { border: 0; background: transparent; box-shadow: 2px 3px 7px rgba(0,0,0,.7); } - .patch-button { padding:0; border:0; background:transparent; color:inherit; cursor:pointer; } .patch-dialog { width:min(92vw,460px); padding:24px; color:#f8fafc; background:#171717; border:1px solid var(--border); border-radius:14px; box-shadow:0 20px 60px #000; } .patch-dialog::backdrop { background:rgba(0,0,0,.78); } + .patch-dialog-description { line-height:1.55; } + .patch-dialog-reason { padding-top:10px; border-top:1px solid var(--border); color:var(--muted); } .patch-dialog-preview { max-width:150px; max-height:150px; object-fit:contain; } .patch-dialog-close { float:right; margin:0; } .profile-edit { margin-top: 24px; } @@ -150,9 +153,9 @@
{% if badge.image_path %}Patch {{ badge.name | t }}{% else %}
{{ badge.icon }}
{% endif %} -

{{ badge.name | t }}

{{ badge.description | t }}

+

{{ badge.name | t }}

{{ badge.long_description | t }}

{{ _('Erhalten am:') }} {{ badge.awarded_at }}

- {% if badge.trigger_concert %}

{{ _('Ausgelöst durch:') }} {{ badge.trigger_concert.artist }} · {{ badge.trigger_concert.date }}

{% else %}

{{ _('Grund:') }} {{ badge.description | t }}

{% endif %} + {% if badge.trigger_concert %}

{{ _('Ausgelöst durch:') }} {{ badge.trigger_concert.artist }} · {{ badge.trigger_concert.date }}

{% else %}

{{ _('Grund:') }} {{ badge.description | t }}

{% endif %}
{% endif %} {% endfor %} diff --git a/app/tests/test_i18n.py b/app/tests/test_i18n.py index 7658e0e..e3d2eee 100644 --- a/app/tests/test_i18n.py +++ b/app/tests/test_i18n.py @@ -108,17 +108,32 @@ class TranslationTests(unittest.TestCase): self.assertIn('My profile', page) self.assertIn('new message(s) or request(s)', page) + def test_every_patch_has_a_long_description_with_an_english_translation(self): + codes = {definition[0] for definition in main.BADGE_DEFINITIONS} + self.assertEqual(set(main.BADGE_LONG_DESCRIPTIONS), codes) + current_language.set('en') + for description in main.BADGE_LONG_DESCRIPTIONS.values(): + with self.subTest(description=description): + self.assertNotEqual(main._(description), description) + def test_every_page_renders_with_populated_content_in_both_languages(self): from jinja2 import ChainableUndefined import os env = main.templates.overlay(undefined=ChainableUndefined) member = dict(id=1, username='sample', display_name='Nachrichten ', email='sample@example.invalid', is_admin=True, notification_count=2, created_at='14.09.2026') event = dict(id=1, concert_id=1, artist='Nachrichten ', title='Nachrichten ', venue='Venue', city='Hagen', country='Deutschland', date='14.09.2026', time='20:00', event_type='concert', event_type_label='Konzert', visibility='friends', is_past=True, is_verified=True, concert_count=2, name='Venue', rating=4, favorite_song='Nachrichten ', notes='Nachrichten ', photos=[dict(id=1, path='/photo.jpg')], bands=[]) - badge = dict(code='founder', name='Gründer', description='Von Anfang an dabei und MetalCircle aufgebaut', earned=True, icon='⚔️', image_path='/patch.jpg') + badge = dict(code='founder', name='Gründer', description='Von Anfang an dabei und MetalCircle aufgebaut', long_description=main.BADGE_LONG_DESCRIPTIONS['founder'], earned=True, icon='⚔️', image_path='/patch.jpg') + profile_badges = [badge] + for code in ('alpha_tester', 'beta_tester', 'early_bird'): + name, icon, _threshold, reason, _category = main.BADGE_BY_CODE[code] + profile_badges.append(dict(code=code, name=name, description=reason, + long_description=main.BADGE_LONG_DESCRIPTIONS[code], + earned=True, icon=icon, image_path='/patch.jpg', + awarded_at='14.09.2026', trigger_concert=None)) event['badges'] = [badge] event['linked_events'] = [] - profile = dict(member, attended_concerts=1) - context = dict(user=member, users=[member], managed_user=member, concert=event, profile=profile, filters={}, stats={}, connections=dict(friends=[member], incoming=[member], outgoing=[member], blocked=[member]), badges=[badge], patch_groups=[dict(label='⚔️ Spezial-Patches', patches=[badge])], venues=[event, dict(event, id=2)], entries=[event], upcoming_concerts=[event], past_concerts=[event], events=[event], followed_bands=[dict(name='Nachrichten ', key='test')], followed_venues=[event], linkable_events=[event], invitable_users=[member], invited_user_ids=[1], attending_users=[member], maybe_users=[member], ticket_seekers=[member], ticket_offers=[member], attending_count=2, maybe_count=1, ticket_seeker_count=1, ticket_offer_count=1, comments=[dict(member, body='Nachrichten ')], is_own_profile=True, can_view_details=True, can_edit_title=True, can_edit_details=True, can_manage_access=True, can_delete=True, can_edit=True, can_manage=True, user_results=[member], friend_requests=[member], event_invitations=[dict(event, invited_by='Nachrichten ')], conversations=[dict(member, unread_count=1, last_message='Nachrichten ')], messages=[dict(body='Nachrichten ')]) + profile = dict(member, attended_concerts=1, earned_codes={badge['code'] for badge in profile_badges}) + context = dict(user=member, users=[member], managed_user=member, concert=event, profile=profile, filters={}, stats={}, connections=dict(friends=[member], incoming=[member], outgoing=[member], blocked=[member]), badges=profile_badges, patch_groups=[dict(label='⚔️ Spezial-Patches', patches=[badge])], venues=[event, dict(event, id=2)], entries=[event], upcoming_concerts=[event], past_concerts=[event], events=[event], followed_bands=[dict(name='Nachrichten ', key='test')], followed_venues=[event], linkable_events=[event], invitable_users=[member], invited_user_ids=[1], attending_users=[member], maybe_users=[member], ticket_seekers=[member], ticket_offers=[member], attending_count=2, maybe_count=1, ticket_seeker_count=1, ticket_offer_count=1, comments=[dict(member, body='Nachrichten ')], is_own_profile=True, can_view_details=True, can_edit_title=True, can_edit_details=True, can_manage_access=True, can_delete=True, can_edit=True, can_manage=True, user_results=[member], friend_requests=[member], event_invitations=[dict(event, invited_by='Nachrichten ')], conversations=[dict(member, unread_count=1, last_message='Nachrichten ')], messages=[dict(body='Nachrichten ')]) for language in ('de', 'en'): current_language.set(language) for path in Path(main.BASE_DIR, 'templates').glob('*.html'): @@ -128,6 +143,11 @@ class TranslationTests(unittest.TestCase): if not path.name.startswith('_'): self.assertIn(f'', page) self.assertIn('class="language-switch"', page) + if path.name == 'profile.html': + self.assertIn('grid-template-columns: repeat(2, minmax(0, 1fr))', page) + self.assertIn('This patch recognizes you as part of the first crew' if language == 'en' else 'Dieser Patch würdigt dich als Teil der ersten Crew', page) + self.assertIn('Reason:' if language == 'en' else 'Grund:', page) + self.assertEqual(page.count('class="patch-button"'), len(profile_badges)) if os.environ.get('I18N_RENDER_DIR'): target = Path(os.environ['I18N_RENDER_DIR'], language) target.mkdir(parents=True, exist_ok=True)