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
This commit is contained in:
2026-05-14 12:41:45 +02:00
parent 47d46e8e6f
commit 2d5ec8fc6d
8 changed files with 480 additions and 46 deletions
+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<text x="50%" y="54%" font-size="56" text-anchor="middle" dominant-baseline="central">🍺</text>
</svg>

After

Width:  |  Height:  |  Size: 170 B

+379
View File
@@ -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;
}
+9 -1
View File
@@ -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"])
+8 -2
View File
@@ -1,12 +1,18 @@
{% load static %}
<!doctype html>
<html lang="de">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta charset="utf-8" />
<title>Suff - Gähsnitz Open Air</title>
<link rel="icon" type="image/svg+xml" href="{% static 'suff/favicon.svg' %}" />
<link rel="stylesheet" href="{% static 'suff/style.css' %}" />
</head>
<body>
<h1>Suff</h1>
{% block content %}{% endblock %}
<main>
<p class="site-name">Gähsnitz Open Air</p>
<h1>Suff</h1>
{% block content %}{% endblock %}
</main>
</body>
</html>
+56 -33
View File
@@ -7,44 +7,67 @@
<p><i>Festival vorbei. Buchungen geschlossen, nur noch Anzeige.</i></p>
{% endif %}
<h3>Deine Rechnung: {{ total|floatformat:2 }} €</h3>
<h3>Bisher gebucht</h3>
{% if consumption_list %}
<ul>
{% for c in consumption_list %}
<li>
{{ 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 %}
</li>
{% endfor %}
</ul>
{% else %}
<p>Noch nichts gebucht.</p>
{% if booked_drink %}
<div class="toast" role="status">
Gebucht: +1 {{ booked_drink.name }}
</div>
{% endif %}
<section class="total-box">
<span class="total-label">Deine Rechnung</span>
<span class="total-value">{{ total|floatformat:2 }} €</span>
</section>
{% if phase == "booking" %}
<h3>Neues Getränk buchen</h3>
<p>Heutiger Tag: {{ current_day }}</p>
{% for drink in drinks %}
<form method="post" action="{% url 'suff:book' %}" style="display:inline">
{% csrf_token %}
<input type="hidden" name="drink_id" value="{{ drink.id }}" />
<button type="submit">
+1 {{ drink.name }} ({{ drink.sale_price_per_bottle|floatformat:2 }} €)
</button>
</form>
{% endfor %}
<section>
<h3>Neues Getränk buchen</h3>
<div class="drink-grid">
{% for drink in drinks %}
<form method="post" action="{% url 'suff:book' %}">
{% csrf_token %}
<input type="hidden" name="drink_id" value="{{ drink.id }}" />
<button type="submit" class="drink-btn">
<span class="drink-plus">+1</span>
<span class="drink-name">{{ drink.name }}</span>
<span class="drink-price">{{ drink.sale_price_per_bottle|floatformat:2 }} €</span>
</button>
</form>
{% endfor %}
</div>
</section>
{% endif %}
<hr />
<form method="post" action="{% url 'suff:logout' %}">
<section>
<h3>Bisher gebucht</h3>
{% if consumption_list %}
{% regroup consumption_list by get_day_display as day_groups %}
{% for group in day_groups %}
<div class="day-group">
<h4 class="day-heading">{{ group.grouper }}</h4>
<ul class="history">
{% for c in group.list %}
<li>
<span class="hist-when">{% if c.created_at %}{{ c.created_at|date:"H:i" }}{% else %}—{% endif %}</span>
<span class="hist-what">{{ c.drink.name }}</span>
<span class="hist-price">
{% if c.for_free %}gratis{% else %}{{ c.drink.sale_price_per_bottle|floatformat:2 }} €{% endif %}
</span>
</li>
{% endfor %}
</ul>
</div>
{% endfor %}
{% else %}
<div class="empty-state">
<p class="empty-emoji">🍺</p>
<p>Noch nichts gebucht.</p>
{% if phase == "booking" %}<p class="muted">Tipp dich rein, sobald du was trinkst!</p>{% endif %}
</div>
{% endif %}
</section>
<form method="post" action="{% url 'suff:logout' %}" class="logout-form">
{% csrf_token %}
<button type="submit">Logout</button>
<button type="submit" class="btn-secondary">Logout</button>
</form>
{% endblock %}
+3 -3
View File
@@ -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.
</p>
{% if error %}<p><b>{{ error }}</b></p>{% endif %}
{% if error %}<p class="error">{{ error }}</p>{% endif %}
<form method="post" action="{% url 'suff:name' %}">
{% csrf_token %}
<label>
Name:
<input type="text" name="name" autofocus required />
Name
<input type="text" name="name" autofocus required autocomplete="off" />
</label>
<button type="submit">Weiter</button>
</form>
+4 -5
View File
@@ -13,9 +13,8 @@
<p>
Bitte einen Admin bitten, die PIN über das Admin-Panel zu setzen.
</p>
<p>
<a href="{% url 'suff:name' %}">Zurück</a>
&middot;
<a href="/admin/">Admin-Panel</a>
</p>
<div class="link-row">
<a href="{% url 'suff:name' %}" class="link-btn">Zurück</a>
<a href="/admin/" class="link-btn link-btn-secondary">Admin-Panel</a>
</div>
{% endblock %}
+18 -2
View File
@@ -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`)