From 2d5ec8fc6d49ef08db7408113507c6b54bd7915d Mon Sep 17 00:00:00 2001
From: Flo Ha
Date: Thu, 14 May 2026 12:41:45 +0200
Subject: [PATCH] Style suff frontend mobile-first
Dark theme matching GOA palette, standalone microsite (no nav).
- Landing/login: GOA subhead + big "Suff" wordmark, large tap targets
- me page: 2-col 4:3 drink grid, bordered total box, day-grouped
history with zebra rows, emoji empty-state
- Booking confirmation toast (amber, 5s, then 800ms CSS collapse)
- Touch feedback via :active scale, SVG beer favicon
- no_pin.html link-buttons styled
---
gaehsnitz/static/suff/favicon.svg | 3 +
gaehsnitz/static/suff/style.css | 379 +++++++++++++++++++++++++++
gaehsnitz/suff.py | 10 +-
gaehsnitz/templates/suff/base.html | 10 +-
gaehsnitz/templates/suff/me.html | 89 ++++---
gaehsnitz/templates/suff/name.html | 6 +-
gaehsnitz/templates/suff/no_pin.html | 9 +-
suff.md | 20 +-
8 files changed, 480 insertions(+), 46 deletions(-)
create mode 100644 gaehsnitz/static/suff/favicon.svg
create mode 100644 gaehsnitz/static/suff/style.css
diff --git a/gaehsnitz/static/suff/favicon.svg b/gaehsnitz/static/suff/favicon.svg
new file mode 100644
index 0000000..db09544
--- /dev/null
+++ b/gaehsnitz/static/suff/favicon.svg
@@ -0,0 +1,3 @@
+
+ 🍺
+
diff --git a/gaehsnitz/static/suff/style.css b/gaehsnitz/static/suff/style.css
new file mode 100644
index 0000000..fa430d4
--- /dev/null
+++ b/gaehsnitz/static/suff/style.css
@@ -0,0 +1,379 @@
+*, *::before, *::after {
+ box-sizing: border-box;
+}
+
+html, body, h1, h2, p, form, label, input, button {
+ margin: 0;
+ padding: 0;
+ border: none;
+}
+
+html, body {
+ width: 100%;
+ min-height: 100%;
+ font-size: 16px;
+ font-family: system-ui, -apple-system, sans-serif;
+}
+
+body {
+ background-color: #161616;
+ color: #EEEEEE;
+ padding: 24px 16px 48px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+main {
+ width: 100%;
+ max-width: 480px;
+ display: flex;
+ flex-direction: column;
+ gap: 18px;
+}
+
+.site-name {
+ text-align: center;
+ color: #885522;
+ font-size: 0.95rem;
+ letter-spacing: 0.05em;
+ margin-bottom: -4px;
+}
+
+h1 {
+ font-size: 2.4rem;
+ font-weight: bold;
+ color: #EE9933;
+ text-shadow: 0 0 16px #CC6611;
+ text-align: center;
+ margin-bottom: 8px;
+}
+
+h2 {
+ font-size: 1.3rem;
+ font-weight: normal;
+ color: #FFCC77;
+}
+
+p {
+ line-height: 1.5rem;
+ color: #DDDDDD;
+}
+
+a {
+ color: #EE9933;
+ text-decoration: none;
+}
+
+a:hover, a:focus {
+ color: #EEEEEE;
+}
+
+.error {
+ color: #EE6622;
+ font-weight: bold;
+}
+
+form {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+label {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ color: #FFCC77;
+ font-size: 0.95rem;
+}
+
+input[type="text"] {
+ background-color: rgba(80, 40, 10, 0.4);
+ color: #EEEEEE;
+ border: 2px solid #885522;
+ border-radius: 6px;
+ padding: 14px 12px;
+ font-size: 1.1rem;
+ width: 100%;
+}
+
+input[type="text"]:focus {
+ outline: none;
+ border-color: #EE9933;
+}
+
+button {
+ background-color: #EE9933;
+ color: #161616;
+ font-size: 1.1rem;
+ font-weight: bold;
+ padding: 14px 20px;
+ border-radius: 6px;
+ cursor: pointer;
+ width: 100%;
+ min-height: 52px;
+}
+
+button:hover, button:focus {
+ background-color: #FFCC77;
+}
+
+button {
+ transition: transform 80ms ease, background-color 100ms ease;
+}
+
+button:active {
+ transform: scale(0.96);
+}
+
+.muted {
+ font-size: 0.9rem;
+ color: #AAAAAA;
+ text-align: center;
+}
+
+.muted-left {
+ font-size: 0.9rem;
+ color: #AAAAAA;
+}
+
+h3 {
+ font-size: 1.1rem;
+ font-weight: normal;
+ color: #FFCC77;
+ margin-bottom: 10px;
+}
+
+section {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.total-box {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 4px;
+ padding: 18px;
+ background-color: rgba(80, 40, 10, 0.4);
+ border: 2px solid #885522;
+ border-radius: 8px;
+}
+
+.total-label {
+ color: #FFCC77;
+ font-size: 0.95rem;
+}
+
+.total-value {
+ color: #EE9933;
+ font-size: 2rem;
+ font-weight: bold;
+ text-shadow: 0 0 12px #CC6611;
+}
+
+.drink-grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 12px;
+}
+
+.drink-grid form {
+ margin: 0;
+}
+
+.drink-btn {
+ width: 100%;
+ aspect-ratio: 4 / 3;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ padding: 12px 8px;
+ line-height: 1.2;
+}
+
+.drink-plus {
+ font-size: 1.6rem;
+ font-weight: bold;
+}
+
+.drink-name {
+ font-size: 1rem;
+ font-weight: bold;
+ text-align: center;
+ word-break: break-word;
+}
+
+.drink-price {
+ font-size: 0.85rem;
+ font-weight: normal;
+ opacity: 0.8;
+}
+
+.history {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.history li {
+ display: grid;
+ grid-template-columns: auto 1fr auto;
+ gap: 10px;
+ padding: 10px 12px;
+ background-color: rgba(80, 40, 10, 0.2);
+ border-radius: 4px;
+ font-size: 0.95rem;
+}
+
+.history li:nth-child(odd) {
+ background-color: rgba(80, 40, 10, 0.35);
+}
+
+.hist-when {
+ color: #FFCC77;
+ white-space: nowrap;
+}
+
+.hist-price {
+ color: #DDDDDD;
+ white-space: nowrap;
+}
+
+.btn-secondary {
+ background-color: transparent;
+ color: #885522;
+ border: 2px solid #885522;
+}
+
+.btn-secondary:hover, .btn-secondary:focus {
+ background-color: rgba(80, 40, 10, 0.4);
+ color: #FFCC77;
+}
+
+.logout-form {
+ margin-top: 24px;
+}
+
+.toast {
+ background-color: rgba(238, 153, 51, 0.15);
+ border: 2px solid #EE9933;
+ color: #FFCC77;
+ padding: 12px 16px;
+ border-radius: 6px;
+ text-align: center;
+ font-weight: bold;
+ overflow: hidden;
+ max-height: 200px;
+ animation: toast-in 200ms ease-out, toast-collapse 800ms ease-in-out 5s forwards;
+}
+
+@keyframes toast-in {
+ from { opacity: 0; transform: translateY(-8px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+@keyframes toast-collapse {
+ 0% {
+ opacity: 1;
+ max-height: 200px;
+ padding-top: 12px;
+ padding-bottom: 12px;
+ margin-bottom: 0;
+ border-width: 2px;
+ }
+ 40% {
+ opacity: 0;
+ max-height: 200px;
+ padding-top: 12px;
+ padding-bottom: 12px;
+ border-width: 2px;
+ }
+ 100% {
+ opacity: 0;
+ max-height: 0;
+ padding-top: 0;
+ padding-bottom: 0;
+ margin-bottom: calc(-1 * var(--gap, 18px));
+ border-width: 0;
+ }
+}
+
+.day-group {
+ margin-bottom: 14px;
+}
+
+.day-heading {
+ font-size: 0.95rem;
+ font-weight: bold;
+ color: #EE9933;
+ margin: 0 0 6px 4px;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.empty-state {
+ text-align: center;
+ padding: 24px 12px;
+ background-color: rgba(80, 40, 10, 0.15);
+ border: 1px dashed #885522;
+ border-radius: 8px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.empty-emoji {
+ font-size: 2.5rem;
+ margin: 0 0 8px;
+ line-height: 1;
+}
+
+.empty-state p {
+ margin: 0;
+}
+
+.link-row {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ margin-top: 8px;
+}
+
+.link-btn {
+ display: block;
+ text-align: center;
+ background-color: #EE9933;
+ color: #161616;
+ font-weight: bold;
+ padding: 14px 20px;
+ border-radius: 6px;
+ transition: transform 80ms ease, background-color 100ms ease;
+}
+
+.link-btn:hover, .link-btn:focus {
+ background-color: #FFCC77;
+ color: #161616;
+}
+
+.link-btn:active {
+ transform: scale(0.96);
+}
+
+.link-btn-secondary {
+ background-color: transparent;
+ color: #885522;
+ border: 2px solid #885522;
+}
+
+.link-btn-secondary:hover, .link-btn-secondary:focus {
+ background-color: rgba(80, 40, 10, 0.4);
+ color: #FFCC77;
+}
diff --git a/gaehsnitz/suff.py b/gaehsnitz/suff.py
index 000d86c..7fe5a28 100644
--- a/gaehsnitz/suff.py
+++ b/gaehsnitz/suff.py
@@ -166,12 +166,20 @@ def pin_view(request):
def me_view(request):
phase = _require_open(request)
drinks = Drink.objects.order_by("name") if phase == "booking" else Drink.objects.none()
+ booked_drink = None
+ booked_id = request.GET.get("booked")
+ if booked_id:
+ try:
+ booked_drink = Drink.objects.get(pk=int(booked_id))
+ except (Drink.DoesNotExist, TypeError, ValueError):
+ booked_drink = None
context = _tab_context(request.user)
context.update(
{
"phase": phase,
"drinks": drinks,
"current_day": _current_festival_day(),
+ "booked_drink": booked_drink,
}
)
return render(request, "suff/me.html", context)
@@ -197,7 +205,7 @@ def book_view(request):
day=_current_festival_day(),
for_free=False,
)
- return HttpResponseRedirect(reverse("suff:me"))
+ return HttpResponseRedirect(f"{reverse('suff:me')}?booked={drink.id}")
@require_http_methods(["POST"])
diff --git a/gaehsnitz/templates/suff/base.html b/gaehsnitz/templates/suff/base.html
index 7cb03a9..8410adc 100644
--- a/gaehsnitz/templates/suff/base.html
+++ b/gaehsnitz/templates/suff/base.html
@@ -1,12 +1,18 @@
+{% load static %}
Suff - Gähsnitz Open Air
+
+
- Suff
- {% block content %}{% endblock %}
+
+ Gähsnitz Open Air
+ Suff
+ {% block content %}{% endblock %}
+
diff --git a/gaehsnitz/templates/suff/me.html b/gaehsnitz/templates/suff/me.html
index 625ba1d..738af69 100644
--- a/gaehsnitz/templates/suff/me.html
+++ b/gaehsnitz/templates/suff/me.html
@@ -7,44 +7,67 @@
Festival vorbei. Buchungen geschlossen, nur noch Anzeige.
{% endif %}
-Deine Rechnung: {{ total|floatformat:2 }} €
-
-Bisher gebucht
-{% if consumption_list %}
-
- {% for c in consumption_list %}
-
- {{ c.get_day_display }}{% if c.created_at %} {{ c.created_at|date:"H:i" }}{% endif %}:
- {{ c.amount }}Ă— {{ c.drink.name }}
- {% if c.for_free %}
- (gratis)
- {% else %}
- à {{ c.drink.sale_price_per_bottle|floatformat:2 }} €
- {% endif %}
-
- {% endfor %}
-
-{% else %}
- Noch nichts gebucht.
+{% if booked_drink %}
+
+ Gebucht: +1 {{ booked_drink.name }}
+
{% endif %}
+
+ Deine Rechnung
+ {{ total|floatformat:2 }} €
+
+
{% if phase == "booking" %}
- Neues Getränk buchen
- Heutiger Tag: {{ current_day }}
- {% for drink in drinks %}
-
- {% endfor %}
+
+ Neues Getränk buchen
+
+ {% for drink in drinks %}
+
+ {% endfor %}
+
+
{% endif %}
-
-
{% endblock %}
diff --git a/gaehsnitz/templates/suff/name.html b/gaehsnitz/templates/suff/name.html
index 9abca9a..f4ccd00 100644
--- a/gaehsnitz/templates/suff/name.html
+++ b/gaehsnitz/templates/suff/name.html
@@ -7,12 +7,12 @@
entfernt (z.B. wird aus "Flo Hä!" → "flo-ha"). Merk dir den Namen so,
wie er hier nach dem Anlegen angezeigt wird.
-{% if error %}{{ error }}
{% endif %}
+{% if error %}{{ error }}
{% endif %}
diff --git a/gaehsnitz/templates/suff/no_pin.html b/gaehsnitz/templates/suff/no_pin.html
index 7016642..4dc171e 100644
--- a/gaehsnitz/templates/suff/no_pin.html
+++ b/gaehsnitz/templates/suff/no_pin.html
@@ -13,9 +13,8 @@
Bitte einen Admin bitten, die PIN ĂĽber das Admin-Panel zu setzen.
-
- ZurĂĽck
- ·
- Admin-Panel
-
+
{% endblock %}
diff --git a/suff.md b/suff.md
index 29bedc8..b1f4175 100644
--- a/suff.md
+++ b/suff.md
@@ -40,9 +40,25 @@ Self-service drink tab for festival attendees. Lives at `/suff/`. Plain Django,
- `gaehsnitz/admin.py` — `SetPinForm` + `set_pin_view`
- `gaehsnitz/templates/suff/{base,name,pin,no_pin,me}.html`
- `gaehsnitz/templates/admin/gaehsnitz/user/set_pin.html`
+- `gaehsnitz/static/suff/{style.css,favicon.svg}`
- `gaehsnitz/migrations/0003_consumption_created_at_user_pin.py`
- Edits: `gaehsnitzproject/settings.py`, `gaehsnitzproject/urls.py`, `gaehsnitz/models.py`
-## Next step
+## Frontend
-Mobile-first styling. Currently zero CSS. Big tap targets for drink buttons, sticky/large running total, dark-mode friendly for outdoor evening use. Visual feedback after booking, day-grouped history, friendlier empty state, bigger PIN input. Decide whether `/suff/` integrates into the GOA site header or stays a separate microsite.
+Mobile-first styled. Dark theme matching GOA palette (`#161616` bg, `#EE9933`/`#FFCC77` amber accents, `#885522` brown borders). Standalone microsite — no nav to main GOA page.
+
+- Landing/login: GOA subhead + big "Suff" wordmark, `name` and `pin` forms with stacked label/input, large tap targets
+- `me` page: 2-col drink button grid (4:3 aspect), stacked +1 / name / price; bordered total box; day-grouped history with zebra rows; emoji empty-state
+- Booking confirmation: amber toast, 5s display, then 800ms collapse animation (pure CSS, no JS)
+- `:active` scale(0.96) feedback on buttons + link-buttons
+- `no_pin.html` link-buttons styled (primary + secondary)
+- SVG favicon (🍺)
+
+## Further ideas
+
+- Color-code drink buttons (per-drink accent border or bg — Bier amber, Wasser blue, etc.) for fast visual recognition in dim light
+- Drink icons/emoji per type
+- Style phase pages (`before` / `closed` if non-404)
+- PWA manifest for add-to-homescreen
+- Donation/free-drink flow if needed (currently admin-only via `for_free`)