Add tabs for all, own and favourite recipes
- Tabs "Alle", "Eigene Rezepte", "Favoriten" above the recipe list; search and tag filters stay within the active tab - Menu link to own recipes; empty states per tab - Fix recipe card images rendering portrait instead of 4:3 Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c76fd119cf
commit
23adb882bd
+1
-1
@@ -138,7 +138,7 @@ class SearchForm(forms.Form):
|
||||
ansicht = forms.ChoiceField(
|
||||
label="Anzeigen",
|
||||
required=False,
|
||||
choices=[("", "Alle Rezepte"), ("meine", "Meine Rezepte"), ("favoriten", "Favoriten")],
|
||||
choices=[("", "Alle Rezepte"), ("meine", "Eigene Rezepte"), ("favoriten", "Favoriten")],
|
||||
)
|
||||
|
||||
def ingredient_terms(self):
|
||||
|
||||
+2
-1
@@ -74,8 +74,9 @@ def recipe_list(request):
|
||||
page = Paginator(qs, PAGE_SIZE).get_page(request.GET.get("seite"))
|
||||
tags = Tag.visible_to(request.user)
|
||||
return render(request, "recipes/list.html", {
|
||||
"form": form, "page": page, "tags": tags, "active_tag": data.get("tag", ""),
|
||||
"form": form, "page": page, "tags": tags, "active_tag": data.get("tag", ""), "active_view": view,
|
||||
"filtered": bool(data.get("q") or data.get("zutat") or data.get("tag") or view),
|
||||
"only_view": bool(view and not (data.get("q") or data.get("zutat") or data.get("tag"))),
|
||||
})
|
||||
|
||||
|
||||
|
||||
+10
-1
@@ -209,7 +209,7 @@ input[type="search"] {
|
||||
display: block; height: 100%; overflow: hidden; text-decoration: none; color: var(--text);
|
||||
background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
|
||||
}
|
||||
.recipe-card img, .recipe-card .placeholder { display: block; width: 100%; aspect-ratio: 4 / 3; object-fit: cover; }
|
||||
.recipe-card img, .recipe-card .placeholder { display: block; width: 100%; height: auto; aspect-ratio: 4 / 3; object-fit: cover; }
|
||||
.recipe-card .placeholder { display: grid; place-items: center; font-size: 2.5rem; background: var(--border); }
|
||||
.recipe-card-body { padding: 0.6rem 0.75rem 0.75rem; display: flex; flex-direction: column; gap: 0.2rem; line-height: 1.3; }
|
||||
.recipe-card-body .muted { font-size: 0.82rem; }
|
||||
@@ -283,3 +283,12 @@ code { font-size: 0.9em; }
|
||||
.help-box { border-left: 4px solid var(--accent); }
|
||||
.help-box ol { padding-left: 1.2rem; margin: 0; }
|
||||
.help-box li { margin-bottom: 0.4rem; }
|
||||
|
||||
/* --- Tabs ---------------------------------------------------------------------------- */
|
||||
.tabs { display: flex; gap: 0.25rem; margin: 0 0 1rem; border-bottom: 1px solid var(--border); overflow-x: auto; scrollbar-width: none; }
|
||||
.tabs::-webkit-scrollbar { display: none; }
|
||||
.tab {
|
||||
flex: 0 0 auto; padding: 0.6rem 0.9rem; min-height: var(--tap); display: flex; align-items: center;
|
||||
color: var(--muted); text-decoration: none; font-weight: 600; border-bottom: 3px solid transparent; margin-bottom: -1px;
|
||||
}
|
||||
.tab[aria-current="page"] { color: var(--accent); border-bottom-color: var(--accent); }
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
<summary aria-label="Menü">☰</summary>
|
||||
<nav>
|
||||
<a href="{% url 'recipes:list' %}">Rezepte</a>
|
||||
<a href="{% url 'recipes:list' %}?ansicht=meine">Eigene Rezepte</a>
|
||||
<a href="{% url 'recipes:list' %}?ansicht=favoriten">Favoriten</a>
|
||||
<a href="{% url 'recipes:import' %}">Importieren</a>
|
||||
<a href="{% url 'accounts:profile' %}">Mein Konto</a>
|
||||
|
||||
@@ -9,16 +9,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="tabs" aria-label="Ansicht">
|
||||
<a class="tab" href="{% querystring ansicht=None seite=None %}"{% if not active_view %} aria-current="page"{% endif %}>Alle</a>
|
||||
<a class="tab" href="{% querystring ansicht="meine" seite=None %}"{% if active_view == "meine" %} aria-current="page"{% endif %}>Eigene Rezepte</a>
|
||||
<a class="tab" href="{% querystring ansicht="favoriten" seite=None %}"{% if active_view == "favoriten" %} aria-current="page"{% endif %}>Favoriten</a>
|
||||
</nav>
|
||||
|
||||
<form method="get" class="search" role="search">
|
||||
<div class="search-row">
|
||||
{{ form.q }}
|
||||
<button type="submit" class="button">Suchen</button>
|
||||
</div>
|
||||
<details class="filters"{% if form.zutat.value or form.ansicht.value %} open{% endif %}>
|
||||
<details class="filters"{% if form.zutat.value %} open{% endif %}>
|
||||
<summary>Filter</summary>
|
||||
<div class="field"><label for="{{ form.zutat.id_for_label }}">{{ form.zutat.label }}</label>{{ form.zutat }}</div>
|
||||
<div class="field"><label for="{{ form.ansicht.id_for_label }}">{{ form.ansicht.label }}</label>{{ form.ansicht }}</div>
|
||||
</details>
|
||||
{% if active_view %}<input type="hidden" name="ansicht" value="{{ active_view }}">{% endif %}
|
||||
{% if active_tag %}<input type="hidden" name="tag" value="{{ active_tag }}">{% endif %}
|
||||
</form>
|
||||
|
||||
@@ -65,8 +71,15 @@
|
||||
{% if page.has_next %}<a class="button secondary" href="{% querystring seite=page.next_page_number %}">Weiter →</a>{% endif %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% elif filtered %}
|
||||
<p class="muted">Keine Rezepte gefunden. <a href="{% url 'recipes:list' %}">Filter zurücksetzen</a></p>
|
||||
{% elif filtered and not only_view %}
|
||||
<p class="muted">Keine Rezepte gefunden. <a href="{% querystring q=None zutat=None tag=None seite=None %}">Filter zurücksetzen</a></p>
|
||||
{% elif active_view == "favoriten" %}
|
||||
<div class="card empty"><p>Noch keine Favoriten. Tippe bei einem Rezept auf „☆ Merken“.</p></div>
|
||||
{% elif active_view == "meine" %}
|
||||
<div class="card empty">
|
||||
<p>Du hast noch keine eigenen Rezepte.</p>
|
||||
<a class="button" href="{% url 'recipes:create' %}">Rezept anlegen</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card empty">
|
||||
<p>Noch keine Rezepte. Leg das erste an!</p>
|
||||
|
||||
@@ -204,3 +204,28 @@ def test_imported_draft_defaults_to_public(user_client, settings):
|
||||
def test_private_can_still_be_chosen(user_client):
|
||||
user_client.post(reverse("recipes:create"), form_data(visibility="private"))
|
||||
assert Recipe.objects.get().visibility == "private"
|
||||
|
||||
|
||||
def test_tabs_and_own_recipes_tab(user_client, user, other_user):
|
||||
make_recipe(user, title="Meins", visibility=Recipe.Visibility.PRIVATE)
|
||||
make_recipe(other_user, title="Deins")
|
||||
response = user_client.get(reverse("recipes:list"), {"ansicht": "meine"})
|
||||
html = response.content.decode()
|
||||
assert "Eigene Rezepte" in html and 'aria-current="page">Eigene Rezepte' in html
|
||||
assert [r.title for r in response.context["page"].object_list] == ["Meins"]
|
||||
all_titles = {r.title for r in user_client.get(reverse("recipes:list")).context["page"].object_list}
|
||||
assert all_titles == {"Meins", "Deins"}
|
||||
|
||||
|
||||
def test_search_stays_in_own_tab(user_client, user, other_user):
|
||||
make_recipe(user, title="Linsensuppe")
|
||||
make_recipe(other_user, title="Linsensalat")
|
||||
response = user_client.get(reverse("recipes:list"), {"ansicht": "meine", "q": "linsen"})
|
||||
assert [r.title for r in response.context["page"].object_list] == ["Linsensuppe"]
|
||||
assert '<input type="hidden" name="ansicht" value="meine">' in response.content.decode()
|
||||
|
||||
|
||||
def test_own_tab_empty_state(user_client, other_user):
|
||||
make_recipe(other_user, title="Deins")
|
||||
html = user_client.get(reverse("recipes:list"), {"ansicht": "meine"}).content.decode()
|
||||
assert "noch keine eigenen Rezepte" in html and "Deins" not in html
|
||||
|
||||
Reference in New Issue
Block a user