sandwitches 2.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sandwitches/__init__.py +6 -0
- sandwitches/admin.py +69 -0
- sandwitches/api.py +207 -0
- sandwitches/asgi.py +16 -0
- sandwitches/feeds.py +23 -0
- sandwitches/forms.py +196 -0
- sandwitches/locale/nl/LC_MESSAGES/django.mo +0 -0
- sandwitches/locale/nl/LC_MESSAGES/django.po +1010 -0
- sandwitches/migrations/0001_initial.py +328 -0
- sandwitches/migrations/0002_historicalrecipe_servings_recipe_servings.py +27 -0
- sandwitches/migrations/0003_setting.py +35 -0
- sandwitches/migrations/0004_alter_setting_ai_api_key_and_more.py +37 -0
- sandwitches/migrations/0005_rating_comment.py +17 -0
- sandwitches/migrations/0006_historicalrecipe_is_highlighted_and_more.py +22 -0
- sandwitches/migrations/__init__.py +0 -0
- sandwitches/models.py +218 -0
- sandwitches/settings.py +220 -0
- sandwitches/storage.py +114 -0
- sandwitches/tasks.py +115 -0
- sandwitches/templates/admin/admin_base.html +118 -0
- sandwitches/templates/admin/confirm_delete.html +23 -0
- sandwitches/templates/admin/dashboard.html +262 -0
- sandwitches/templates/admin/rating_list.html +38 -0
- sandwitches/templates/admin/recipe_form.html +184 -0
- sandwitches/templates/admin/recipe_list.html +64 -0
- sandwitches/templates/admin/tag_form.html +30 -0
- sandwitches/templates/admin/tag_list.html +37 -0
- sandwitches/templates/admin/task_detail.html +91 -0
- sandwitches/templates/admin/task_list.html +41 -0
- sandwitches/templates/admin/user_form.html +37 -0
- sandwitches/templates/admin/user_list.html +60 -0
- sandwitches/templates/base.html +94 -0
- sandwitches/templates/base_beer.html +57 -0
- sandwitches/templates/components/carousel_scripts.html +59 -0
- sandwitches/templates/components/favorites_search_form.html +85 -0
- sandwitches/templates/components/footer.html +14 -0
- sandwitches/templates/components/ingredients_scripts.html +50 -0
- sandwitches/templates/components/ingredients_section.html +11 -0
- sandwitches/templates/components/instructions_section.html +9 -0
- sandwitches/templates/components/language_dialog.html +26 -0
- sandwitches/templates/components/navbar.html +27 -0
- sandwitches/templates/components/rating_section.html +66 -0
- sandwitches/templates/components/recipe_header.html +32 -0
- sandwitches/templates/components/search_form.html +106 -0
- sandwitches/templates/components/search_scripts.html +98 -0
- sandwitches/templates/components/side_menu.html +35 -0
- sandwitches/templates/components/user_menu.html +10 -0
- sandwitches/templates/detail.html +178 -0
- sandwitches/templates/favorites.html +42 -0
- sandwitches/templates/index.html +76 -0
- sandwitches/templates/login.html +57 -0
- sandwitches/templates/partials/recipe_list.html +87 -0
- sandwitches/templates/recipe_form.html +119 -0
- sandwitches/templates/setup.html +105 -0
- sandwitches/templates/signup.html +133 -0
- sandwitches/templatetags/__init__.py +0 -0
- sandwitches/templatetags/custom_filters.py +15 -0
- sandwitches/templatetags/markdown_extras.py +17 -0
- sandwitches/urls.py +109 -0
- sandwitches/utils.py +222 -0
- sandwitches/views.py +647 -0
- sandwitches/wsgi.py +16 -0
- sandwitches-2.2.0.dist-info/METADATA +104 -0
- sandwitches-2.2.0.dist-info/RECORD +65 -0
- sandwitches-2.2.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{% load i18n static %}
|
|
2
|
+
<nav class="top primary-container">
|
|
3
|
+
<button class="circle transparent large" onclick="ui('#menu')">
|
|
4
|
+
<i>menu</i>
|
|
5
|
+
</button>
|
|
6
|
+
<a href="{% url 'index' %}" class="row align-center">
|
|
7
|
+
<img src="{% static 'icons/icon.svg' %}" class="circle small">
|
|
8
|
+
</a>
|
|
9
|
+
<div class="max"></div>
|
|
10
|
+
<button class="circle transparent" onclick="ui('mode', ui('mode') == 'dark' ? 'light' : 'dark')">
|
|
11
|
+
<i>dark_mode</i>
|
|
12
|
+
</button>
|
|
13
|
+
<button class="circle transparent" data-ui="#language-menu">
|
|
14
|
+
<i>language</i>
|
|
15
|
+
</button>
|
|
16
|
+
|
|
17
|
+
{% if user.is_authenticated %}
|
|
18
|
+
{% if user.avatar %}
|
|
19
|
+
<img src="{{ user.avatar.url }}" class="circle" data-ui="#user-menu">
|
|
20
|
+
{% else %}
|
|
21
|
+
<img src="https://www.w3schools.com/howto/img_avatar.png" class="circle" data-ui="#user-menu">
|
|
22
|
+
{% endif %}
|
|
23
|
+
{% else %}
|
|
24
|
+
<a href="{% url 'login' %}"><button class="chip transparent border white-text">{% trans "Login" %}</button></a>
|
|
25
|
+
<a href="{% url 'signup' %}"><button class="chip primary">{% trans "Sign up" %}</button></a>
|
|
26
|
+
{% endif %}
|
|
27
|
+
</nav>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{% load i18n %}
|
|
2
|
+
<h6 class="bold">{% trans "Rating" %}</h6>
|
|
3
|
+
<div class="row align-center">
|
|
4
|
+
<i class="primary-text">star</i>
|
|
5
|
+
<span class="h5 ml-1">{{ avg_rating|floatformat:1 }}</span>
|
|
6
|
+
<span class="small-text ml-1">({{ rating_count }} {% trans "vote" %}{% if rating_count != 1 %}s{% endif %})</span>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<div class="space"></div>
|
|
10
|
+
|
|
11
|
+
{% if user.is_authenticated %}
|
|
12
|
+
<article class="border round no-elevate secondary-container">
|
|
13
|
+
{% if user_rating %}
|
|
14
|
+
<div class="row align-center">
|
|
15
|
+
<i class="primary-text">check_circle</i>
|
|
16
|
+
<span class="ml-2">{% trans "Your rating:" %} <b>{{ user_rating.score }}</b></span>
|
|
17
|
+
</div>
|
|
18
|
+
{% if user_rating.comment %}
|
|
19
|
+
<p class="small-text ml-2">{{ user_rating.comment }}</p>
|
|
20
|
+
{% endif %}
|
|
21
|
+
{% endif %}
|
|
22
|
+
|
|
23
|
+
<form method="post" action="{% url 'recipe_rate' pk=recipe.pk %}">
|
|
24
|
+
{% csrf_token %}
|
|
25
|
+
<label class="bold">{% trans "Rate this sandwich" %}</label>
|
|
26
|
+
<div class="field middle-align">
|
|
27
|
+
<label class="slider">
|
|
28
|
+
<input type="range"
|
|
29
|
+
name="{{ rating_form.score.name }}"
|
|
30
|
+
min="0"
|
|
31
|
+
max="10"
|
|
32
|
+
step="0.1"
|
|
33
|
+
value="{{ user_rating.score|default:'5.0' }}"
|
|
34
|
+
oninput="document.getElementById('score-output').innerText = parseFloat(this.value).toFixed(1)">
|
|
35
|
+
<span></span>
|
|
36
|
+
</label>
|
|
37
|
+
<span id="score-output" class="bold ml-2">{{ user_rating.score|default:"5.0" }}</span>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="field textarea label border round mt-2">
|
|
40
|
+
<textarea name="{{ rating_form.comment.name }}" placeholder="{% trans "Add a comment (optional)" %}">{{ user_rating.comment|default:"" }}</textarea>
|
|
41
|
+
<label>{% trans "Comment" %}</label>
|
|
42
|
+
</div>
|
|
43
|
+
<button type="submit" class="button primary round mt-2">{% trans "Submit Rating" %}</button>
|
|
44
|
+
</form>
|
|
45
|
+
</article>
|
|
46
|
+
{% else %}
|
|
47
|
+
<p><a href="{% url 'login' %}" class="link">{% trans "Log in" %}</a> {% trans "to rate this recipe." %}</p>
|
|
48
|
+
{% endif %}
|
|
49
|
+
|
|
50
|
+
{% if all_ratings %}
|
|
51
|
+
<div class="space"></div>
|
|
52
|
+
<h6 class="bold">{% trans "All Ratings" %}</h6>
|
|
53
|
+
<div class="collection">
|
|
54
|
+
{% for rating in all_ratings %}
|
|
55
|
+
<div class="row">
|
|
56
|
+
<div class="max">
|
|
57
|
+
<span class="bold">{{ rating.user.username }}</span>
|
|
58
|
+
<span class="ml-2">{{ rating.score|floatformat:1 }} <i class="small">star</i></span>
|
|
59
|
+
</div>
|
|
60
|
+
{% if rating.comment %}
|
|
61
|
+
<p class="small-text">{{ rating.comment }}</p>
|
|
62
|
+
{% endif %}
|
|
63
|
+
</div>
|
|
64
|
+
{% endfor %}
|
|
65
|
+
</div>
|
|
66
|
+
{% endif %}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{% load i18n %}
|
|
2
|
+
<div class="row align-center">
|
|
3
|
+
<h4 class="bold max">{{ recipe.title }}</h4>
|
|
4
|
+
{% if user.is_authenticated %}
|
|
5
|
+
<a href="{% url 'toggle_favorite' recipe.pk %}" id="favorite-toggle-button" class="button circle transparent" title="{% trans 'Toggle Favorite' %}">
|
|
6
|
+
<i class="{% if recipe in user.favorites.all %}primary-text{% else %}black-text{% endif %}">
|
|
7
|
+
{% if recipe in user.favorites.all %}favorite{% else %}favorite_border{% endif %}
|
|
8
|
+
</i>
|
|
9
|
+
</a>
|
|
10
|
+
{% endif %}
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<div class="row align-center">
|
|
14
|
+
{% if recipe.uploaded_by %}
|
|
15
|
+
<a href="{% url 'index' %}?uploader={{ recipe.uploaded_by.username }}" class="row align-center" style="color: inherit; text-decoration: none;">
|
|
16
|
+
{% if recipe.uploaded_by.avatar %}
|
|
17
|
+
<img src="{{ recipe.uploaded_by.avatar.url }}" class="circle small" alt="{{ recipe.uploaded_by.username }}">
|
|
18
|
+
{% else %}
|
|
19
|
+
<i class="small circle surface">person</i>
|
|
20
|
+
{% endif %}
|
|
21
|
+
<div class="ml-2">
|
|
22
|
+
<span class="small-text">{% trans "Uploaded by" %}</span>
|
|
23
|
+
<div class="bold">{{ recipe.uploaded_by.get_full_name|default:recipe.uploaded_by.username }}</div>
|
|
24
|
+
</div>
|
|
25
|
+
</a>
|
|
26
|
+
{% else %}
|
|
27
|
+
<i class="small circle surface">person_off</i>
|
|
28
|
+
<div class="ml-2">
|
|
29
|
+
<span class="small-text">{% trans "Unknown uploader" %}</span>
|
|
30
|
+
</div>
|
|
31
|
+
{% endif %}
|
|
32
|
+
</div>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
{% load i18n %}
|
|
2
|
+
<form hx-get="{% url 'index' %}" hx-target="#recipe-grid" hx-swap="outerHTML" hx-push-url="true" hx-trigger="change delay:500ms, keyup delay:500ms from:#search, submit, uploaderChange from:body">
|
|
3
|
+
<div class="grid">
|
|
4
|
+
<!-- Main Search Bar -->
|
|
5
|
+
<div class="s12">
|
|
6
|
+
<div class="field prefix suffix border round primary-container">
|
|
7
|
+
<i class="front">search</i>
|
|
8
|
+
<input id="search" type="text" name="q" placeholder="{% trans 'Search by title...' %}" value="{{ request.GET.q|default:'' }}">
|
|
9
|
+
<a class="circle plain" onclick="toggleAdvanced()" title="{% trans 'Advanced Search' %}">
|
|
10
|
+
<i id="advanced-icon">filter_list</i>
|
|
11
|
+
</a>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<!-- Collapsible Advanced Search Panel -->
|
|
16
|
+
<div id="advanced-search" class="s12 hidden" style="display: none;">
|
|
17
|
+
<div class="padding surface border round">
|
|
18
|
+
<div class="grid">
|
|
19
|
+
<div class="s12 m6 l3">
|
|
20
|
+
<div class="field label border round">
|
|
21
|
+
<input type="date" name="date_start" value="{{ request.GET.date_start|default:'' }}">
|
|
22
|
+
<label>{% trans "Start Date" %}</label>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
<div class="s12 m6 l3">
|
|
26
|
+
<div class="field label border round">
|
|
27
|
+
<input type="date" name="date_end" value="{{ request.GET.date_end|default:'' }}">
|
|
28
|
+
<label>{% trans "End Date" %}</label>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<!-- Custom Uploader Dropdown with Avatar -->
|
|
33
|
+
<div class="s12 m6 l3">
|
|
34
|
+
<div class="field label border round suffix relative" onclick="toggleUploaderMenu(event)">
|
|
35
|
+
<input type="text" id="uploader-display" readonly value="{{ request.GET.uploader|default:'' }}" placeholder="{% trans 'All Uploaders' %}" style="cursor: pointer;">
|
|
36
|
+
<label>{% trans "Uploader" %}</label>
|
|
37
|
+
<i class="chevron-down">arrow_drop_down</i>
|
|
38
|
+
|
|
39
|
+
<div id="uploader-menu" class="absolute surface elevate round left top-round bottom-round scroll" style="top: 100%; left: 0; right: 0; max-height: 300px; display: none; z-index: 20;">
|
|
40
|
+
<a class="row padding hover pointer" onclick="selectUploader('', '{% trans 'All Uploaders' %}')">
|
|
41
|
+
<span>{% trans "All Uploaders" %}</span>
|
|
42
|
+
</a>
|
|
43
|
+
{% for u in uploaders %}
|
|
44
|
+
<a class="row padding hover pointer align-center" onclick="selectUploader('{{ u.username }}', '{{ u.username }}')">
|
|
45
|
+
{% if u.avatar %}
|
|
46
|
+
<img src="{{ u.avatar.url }}" class="circle tiny margin-right">
|
|
47
|
+
{% else %}
|
|
48
|
+
<i class="tiny circle surface margin-right">person</i>
|
|
49
|
+
{% endif %}
|
|
50
|
+
<span>{{ u.username }}</span>
|
|
51
|
+
</a>
|
|
52
|
+
{% endfor %}
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
<input type="hidden" name="uploader" id="uploader-hidden" value="{{ request.GET.uploader|default:'' }}">
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div class="s12 m6 l3">
|
|
59
|
+
<div class="field label border round suffix relative" onclick="toggleTagMenu(event)">
|
|
60
|
+
<input type="text" id="tag-display" readonly placeholder="{% trans 'All Tags' %}" style="cursor: pointer;">
|
|
61
|
+
<label>{% trans "Tags" %}</label>
|
|
62
|
+
<i class="chevron-down">arrow_drop_down</i>
|
|
63
|
+
|
|
64
|
+
<div id="tag-menu" class="absolute surface elevate round left top-round bottom-round scroll" style="top: 100%; left: 0; right: 0; max-height: 300px; display: none; z-index: 20;" onclick="event.stopPropagation()">
|
|
65
|
+
{% for t in tags %}
|
|
66
|
+
<label class="row padding hover pointer align-center">
|
|
67
|
+
<input type="checkbox" class="margin-right" onchange="updateSelectedTags()" value="{{ t.name }}" {% if t.name in selected_tags %}checked{% endif %}>
|
|
68
|
+
<span>{{ t.name }}</span>
|
|
69
|
+
</label>
|
|
70
|
+
{% endfor %}
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
<div id="tag-hidden-container">
|
|
74
|
+
{% for t in selected_tags %}
|
|
75
|
+
<input type="hidden" name="tag" value="{{ t }}">
|
|
76
|
+
{% endfor %}
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
{% if user.is_authenticated %}
|
|
81
|
+
<div class="s12 m6 l3">
|
|
82
|
+
<div class="field middle-align">
|
|
83
|
+
<label class="checkbox">
|
|
84
|
+
<input type="checkbox" name="favorites" {% if request.GET.favorites == 'on' %}checked{% endif %}>
|
|
85
|
+
<span>{% trans "My Favorites" %}</span>
|
|
86
|
+
</label>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
{% endif %}
|
|
90
|
+
|
|
91
|
+
<div class="s12 m6 l3">
|
|
92
|
+
<div class="field label border round">
|
|
93
|
+
<select name="sort">
|
|
94
|
+
<option value="date_desc" {% if request.GET.sort == 'date_desc' %}selected{% endif %}>{% trans "Newest" %}</option>
|
|
95
|
+
<option value="date_asc" {% if request.GET.sort == 'date_asc' %}selected{% endif %}>{% trans "Oldest" %}</option>
|
|
96
|
+
<option value="rating" {% if request.GET.sort == 'rating' %}selected{% endif %}>{% trans "Highest Rated" %}</option>
|
|
97
|
+
<option value="user" {% if request.GET.sort == 'user' %}selected{% endif %}>{% trans "Uploader (A-Z)" %}</option>
|
|
98
|
+
</select>
|
|
99
|
+
<label>{% trans "Sort By" %}</label>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</form>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
function toggleAdvanced() {
|
|
3
|
+
var el = document.getElementById('advanced-search');
|
|
4
|
+
if (el.style.display === 'none') {
|
|
5
|
+
el.style.display = 'block';
|
|
6
|
+
} else {
|
|
7
|
+
el.style.display = 'none';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function toggleUploaderMenu(event) {
|
|
12
|
+
event.stopPropagation(); // Prevent closing immediately
|
|
13
|
+
var el = document.getElementById('uploader-menu');
|
|
14
|
+
if (el.style.display === 'none') {
|
|
15
|
+
// Close others if needed, but for now just toggle
|
|
16
|
+
el.style.display = 'block';
|
|
17
|
+
} else {
|
|
18
|
+
el.style.display = 'none';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function selectUploader(username, displayName) {
|
|
23
|
+
document.getElementById('uploader-hidden').value = username;
|
|
24
|
+
document.getElementById('uploader-display').value = username ? username : ''; // Or displayName
|
|
25
|
+
|
|
26
|
+
// Trigger HTMX
|
|
27
|
+
document.body.dispatchEvent(new Event('uploaderChange'));
|
|
28
|
+
|
|
29
|
+
// Close menu (handled by toggle or outside click)
|
|
30
|
+
// document.getElementById('uploader-menu').style.display = 'none';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function toggleTagMenu(event) {
|
|
34
|
+
event.stopPropagation();
|
|
35
|
+
var el = document.getElementById('tag-menu');
|
|
36
|
+
if (el.style.display === 'none') {
|
|
37
|
+
el.style.display = 'block';
|
|
38
|
+
} else {
|
|
39
|
+
el.style.display = 'none';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function updateSelectedTags(triggerEvent = true) {
|
|
44
|
+
var menu = document.getElementById('tag-menu');
|
|
45
|
+
var checkboxes = menu.querySelectorAll('input[type="checkbox"]');
|
|
46
|
+
var hiddenContainer = document.getElementById('tag-hidden-container');
|
|
47
|
+
var displayInput = document.getElementById('tag-display');
|
|
48
|
+
|
|
49
|
+
hiddenContainer.innerHTML = ''; // Clear existing
|
|
50
|
+
var selectedNames = [];
|
|
51
|
+
|
|
52
|
+
checkboxes.forEach(function(cb) {
|
|
53
|
+
if (cb.checked) {
|
|
54
|
+
selectedNames.push(cb.value);
|
|
55
|
+
var input = document.createElement('input');
|
|
56
|
+
input.type = 'hidden';
|
|
57
|
+
input.name = 'tag';
|
|
58
|
+
input.value = cb.value;
|
|
59
|
+
hiddenContainer.appendChild(input);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (selectedNames.length > 0) {
|
|
64
|
+
displayInput.value = selectedNames.join(', ');
|
|
65
|
+
} else {
|
|
66
|
+
displayInput.value = '';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (triggerEvent) {
|
|
70
|
+
// Trigger HTMX
|
|
71
|
+
document.body.dispatchEvent(new Event('uploaderChange'));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Initialize display value
|
|
76
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
77
|
+
updateSelectedTags(false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Close menus on clicking outside
|
|
81
|
+
document.addEventListener('click', function(event) {
|
|
82
|
+
var uploaderMenu = document.getElementById('uploader-menu');
|
|
83
|
+
var uploaderField = document.getElementById('uploader-display').parentElement;
|
|
84
|
+
if (uploaderMenu && uploaderMenu.style.display === 'block') {
|
|
85
|
+
if (!uploaderField.contains(event.target)) {
|
|
86
|
+
uploaderMenu.style.display = 'none';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
var tagMenu = document.getElementById('tag-menu');
|
|
91
|
+
var tagField = document.getElementById('tag-display').parentElement;
|
|
92
|
+
if (tagMenu && tagMenu.style.display === 'block') {
|
|
93
|
+
if (!tagField.contains(event.target)) {
|
|
94
|
+
tagMenu.style.display = 'none';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
</script>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{% load i18n static %}
|
|
2
|
+
<dialog id="menu" class="left">
|
|
3
|
+
<nav class="drawer vertical">
|
|
4
|
+
<header>
|
|
5
|
+
<img src="{% static 'icons/icon.svg' %}" class="circle large">
|
|
6
|
+
<h5 class="max">Sandwitches</h5>
|
|
7
|
+
</header>
|
|
8
|
+
<div class="space"></div>
|
|
9
|
+
<a href="{% url 'index' %}" class="padding {% if request.resolver_match.url_name == 'index' %}active{% endif %}">
|
|
10
|
+
<i class="extra padding">home</i>
|
|
11
|
+
<span class="large-text">{% trans "Home" %}</span>
|
|
12
|
+
</a>
|
|
13
|
+
{% if user.is_authenticated %}
|
|
14
|
+
<a href="{% url 'favorites' %}" class="padding {% if request.resolver_match.url_name == 'favorites' %}active{% endif %}">
|
|
15
|
+
<i class="extra padding">favorite</i>
|
|
16
|
+
<span class="large-text">{% trans "Favorites" %}</span>
|
|
17
|
+
</a>
|
|
18
|
+
{% endif %}
|
|
19
|
+
<a href="/api/docs" class="padding">
|
|
20
|
+
<i class="extra padding">api</i>
|
|
21
|
+
<span class="large-text">{% trans "API Docs" %}</span>
|
|
22
|
+
</a>
|
|
23
|
+
<a href="{% url 'latest_recipes_feed' %}" class="padding">
|
|
24
|
+
<i class="extra padding">rss_feed</i>
|
|
25
|
+
<span class="large-text">{% trans "RSS Feed" %}</span>
|
|
26
|
+
</a>
|
|
27
|
+
{% if user.is_staff %}
|
|
28
|
+
<div class="divider"></div>
|
|
29
|
+
<a href="{% url 'admin_dashboard' %}" class="padding">
|
|
30
|
+
<i class="extra padding">admin_panel_settings</i>
|
|
31
|
+
<span class="large-text">{% trans "Admin Dashboard" %}</span>
|
|
32
|
+
</a>
|
|
33
|
+
{% endif %}
|
|
34
|
+
</nav>
|
|
35
|
+
</dialog>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{% load i18n %}
|
|
2
|
+
{% if user.is_authenticated %}
|
|
3
|
+
<menu id="user-menu" class="no-wrap left">
|
|
4
|
+
{% if user.is_staff %}
|
|
5
|
+
<a href="{% url 'admin_dashboard' %}" class="row"><i>admin_panel_settings</i>{% trans "Admin" %}</a>
|
|
6
|
+
<div class="divider"></div>
|
|
7
|
+
{% endif %}
|
|
8
|
+
<a href="{% url 'admin:logout' %}" class="row"><i>logout</i>{% trans "Logout" %}</a>
|
|
9
|
+
</menu>
|
|
10
|
+
{% endif %}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
{% extends "base_beer.html" %}
|
|
2
|
+
{% block title %}{{ recipe.title }} — Sandwitch{% endblock %}
|
|
3
|
+
|
|
4
|
+
{% block extra_head %}
|
|
5
|
+
{{ block.super }}
|
|
6
|
+
<style>
|
|
7
|
+
@media only screen and (min-width: 992px) {
|
|
8
|
+
main.container {
|
|
9
|
+
padding-left: 10%;
|
|
10
|
+
padding-right: 10%;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
.portion-selector {
|
|
14
|
+
display: flex;
|
|
15
|
+
align-items: center;
|
|
16
|
+
gap: 10px;
|
|
17
|
+
margin-bottom: 20px;
|
|
18
|
+
}
|
|
19
|
+
.portion-selector input[type="number"] {
|
|
20
|
+
width: 60px;
|
|
21
|
+
text-align: center;
|
|
22
|
+
}
|
|
23
|
+
</style>
|
|
24
|
+
{% load custom_filters %} {# Load your custom filters #}
|
|
25
|
+
<script type="application/ld+json">
|
|
26
|
+
{
|
|
27
|
+
"@context": "https://schema.org",
|
|
28
|
+
"@type": "Recipe",
|
|
29
|
+
"name": "{{ recipe.title }}",
|
|
30
|
+
{% if recipe.image %}
|
|
31
|
+
"image": "{{ request.scheme }}://{{ request.get_host }}{{ recipe.image_medium.url }}",
|
|
32
|
+
{% endif %}
|
|
33
|
+
"author": {
|
|
34
|
+
"@type": "Person",
|
|
35
|
+
"name": "{% if recipe.uploaded_by %}{{ recipe.uploaded_by.get_full_name|default:recipe.uploaded_by.username }}{% else %}Anonymous{% endif %}"
|
|
36
|
+
},
|
|
37
|
+
"datePublished": "{{ recipe.created_at|date:"Y-m-d" }}",
|
|
38
|
+
"description": "{{ recipe.description|striptags|truncatechars:200 }}",
|
|
39
|
+
{% if recipe.tags.all %}
|
|
40
|
+
"recipeCategory": [{% for tag in recipe.tags.all %}"{{ tag.name }}"{% if not forloop.last %},{% endif %}{% endfor %}],
|
|
41
|
+
{% endif %}
|
|
42
|
+
"recipeIngredient": [
|
|
43
|
+
{% for ingredient in recipe.ingredients|linebreaksbr|striptags|split:"\n" %}
|
|
44
|
+
"{{ ingredient|escapejs }}"{% if not forloop.last %},{% endif %}
|
|
45
|
+
{% endfor %}
|
|
46
|
+
],
|
|
47
|
+
"recipeInstructions": {
|
|
48
|
+
"@type": "ItemList",
|
|
49
|
+
"itemListElement": [
|
|
50
|
+
{% for instruction in recipe.instructions|linebreaksbr|striptags|split:"\n" %}
|
|
51
|
+
{
|
|
52
|
+
"@type": "HowToStep",
|
|
53
|
+
"text": "{{ instruction|escapejs }}"
|
|
54
|
+
}{% if not forloop.last %},{% endif %}
|
|
55
|
+
{% endfor %}
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
{% if avg_rating > 0 %}
|
|
59
|
+
"aggregateRating": {
|
|
60
|
+
"@type": "AggregateRating",
|
|
61
|
+
"ratingValue": "{{ avg_rating|floatformat:1 }}",
|
|
62
|
+
"reviewCount": "{{ rating_count }}"
|
|
63
|
+
}
|
|
64
|
+
{% endif %}
|
|
65
|
+
}
|
|
66
|
+
</script>
|
|
67
|
+
{% endblock %}
|
|
68
|
+
|
|
69
|
+
{% block content %}
|
|
70
|
+
|
|
71
|
+
{% load i18n markdown_extras %} {# Keep existing load tags #}
|
|
72
|
+
|
|
73
|
+
<div class="space"></div>
|
|
74
|
+
|
|
75
|
+
<nav>
|
|
76
|
+
<a href="{% url 'index' %}" class="button transparent circle">
|
|
77
|
+
<i>arrow_back</i>
|
|
78
|
+
</a>
|
|
79
|
+
<h5 class="max ml-2">{% trans "Back to all" %}</h5>
|
|
80
|
+
</nav>
|
|
81
|
+
|
|
82
|
+
<div class="large-space"></div>
|
|
83
|
+
|
|
84
|
+
<div class="grid">
|
|
85
|
+
<!-- Left Column: Image and Main Info -->
|
|
86
|
+
<div class="s12 m12 l5">
|
|
87
|
+
<article class="round no-padding elevate">
|
|
88
|
+
{% if recipe.image %}
|
|
89
|
+
<img src="{{ recipe.image_large.url }}"
|
|
90
|
+
srcset="{{ recipe.image_medium.url }} 700w, {{ recipe.image_large.url }} 1200w"
|
|
91
|
+
sizes="(max-width: 768px) 95vw, 600px"
|
|
92
|
+
alt="{{ recipe.title }}"
|
|
93
|
+
loading="lazy"
|
|
94
|
+
class="responsive top-round">
|
|
95
|
+
{% else %}
|
|
96
|
+
<div class="primary medium-height top-round middle-align center-align" style="height:300px;">
|
|
97
|
+
<i class="extra">lunch_dining</i>
|
|
98
|
+
</div>
|
|
99
|
+
{% endif %}
|
|
100
|
+
<div class="padding">
|
|
101
|
+
{% include "components/recipe_header.html" %}
|
|
102
|
+
|
|
103
|
+
<div class="space"></div>
|
|
104
|
+
<div class="divider"></div>
|
|
105
|
+
<div class="space"></div>
|
|
106
|
+
|
|
107
|
+
<h6 class="bold">{% trans "Tags" %}</h6>
|
|
108
|
+
<div class="row wrap">
|
|
109
|
+
{% for tag in recipe.tags.all %}
|
|
110
|
+
<a href="{% url 'index' %}?tag={{ tag.name|urlencode }}" class="chip round surface">{{ tag.name }}</a>
|
|
111
|
+
{% endfor %}
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div class="space"></div>
|
|
115
|
+
<div class="divider"></div>
|
|
116
|
+
<div class="space"></div>
|
|
117
|
+
|
|
118
|
+
<div class="row wrap">
|
|
119
|
+
{% if recipe.favorited_by.all %}
|
|
120
|
+
<span class="tiny-text">
|
|
121
|
+
{% if recipe.favorited_by.count == 1 %}
|
|
122
|
+
{% trans "Liked by" %} {{ recipe.favorited_by.first.username }}
|
|
123
|
+
{% else %}
|
|
124
|
+
{% trans "Liked by" %} {{ recipe.favorited_by.first.username }} {% trans "and" %} {{ recipe.favorited_by.count|add:"-1" }} {% trans "others" %}
|
|
125
|
+
{% endif %}
|
|
126
|
+
</span>
|
|
127
|
+
{% endif %}
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<div class="space"></div>
|
|
131
|
+
<div class="divider"></div>
|
|
132
|
+
<div class="space"></div>
|
|
133
|
+
|
|
134
|
+
{% include "components/rating_section.html" %}
|
|
135
|
+
|
|
136
|
+
{% if user.is_authenticated and user.is_staff %}
|
|
137
|
+
<div class="space"></div>
|
|
138
|
+
<a href="/admin/sandwitches/recipe/{{ recipe.pk }}/change/" class="button transparent border round width-100 center-align">
|
|
139
|
+
<i>edit</i>
|
|
140
|
+
<span>{% trans "Edit Recipe" %}</span>
|
|
141
|
+
</a>
|
|
142
|
+
{% endif %}
|
|
143
|
+
</div>
|
|
144
|
+
</article>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<!-- Right Column: Details -->
|
|
148
|
+
<div class="s12 m12 l7">
|
|
149
|
+
<div class="padding">
|
|
150
|
+
|
|
151
|
+
<h5 class="primary-text">{% trans "Description" %}</h5>
|
|
152
|
+
<div class="large-text">
|
|
153
|
+
{% if recipe.description %}
|
|
154
|
+
{{ recipe.description|convert_markdown|safe }}
|
|
155
|
+
{% else %}
|
|
156
|
+
<p class="italic">{% trans "No description yet." %}</p>
|
|
157
|
+
{% endif %}
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<div class="large-space"></div>
|
|
161
|
+
|
|
162
|
+
{% include "components/ingredients_section.html" %}
|
|
163
|
+
|
|
164
|
+
<div class="large-space"></div>
|
|
165
|
+
|
|
166
|
+
{% include "components/instructions_section.html" %}
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
{% endblock %}
|
|
171
|
+
|
|
172
|
+
{% block extra_scripts %}
|
|
173
|
+
|
|
174
|
+
{{ block.super }}
|
|
175
|
+
|
|
176
|
+
{% include "components/ingredients_scripts.html" %}
|
|
177
|
+
|
|
178
|
+
{% endblock %}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{% extends "base_beer.html" %}
|
|
2
|
+
{% load i18n static %}
|
|
3
|
+
|
|
4
|
+
{% block title %}{% trans 'Favorites' %}{% endblock %}
|
|
5
|
+
|
|
6
|
+
{% block extra_head %}
|
|
7
|
+
{{ block.super }}
|
|
8
|
+
<style>
|
|
9
|
+
@media only screen and (min-width: 992px) {
|
|
10
|
+
main.container {
|
|
11
|
+
padding-left: 10%;
|
|
12
|
+
padding-right: 10%;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
</style>
|
|
16
|
+
{% endblock %}
|
|
17
|
+
|
|
18
|
+
{% block content %}
|
|
19
|
+
<div class="large-space"></div>
|
|
20
|
+
|
|
21
|
+
<article class="round primary s12 m12 l12">
|
|
22
|
+
<div class="row align-center">
|
|
23
|
+
<div class="max">
|
|
24
|
+
<h4 class="upper">{% trans 'Your Favorites' %}</h4>
|
|
25
|
+
<p class="large-text">{% trans 'The recipes you love the most.' %}</p>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</article>
|
|
29
|
+
|
|
30
|
+
<div class="space"></div>
|
|
31
|
+
|
|
32
|
+
{% include "components/favorites_search_form.html" %}
|
|
33
|
+
|
|
34
|
+
<div class="large-space"></div>
|
|
35
|
+
|
|
36
|
+
{% include "partials/recipe_list.html" %}
|
|
37
|
+
|
|
38
|
+
{% endblock %}
|
|
39
|
+
|
|
40
|
+
{% block page_scripts %}
|
|
41
|
+
{% include "components/search_scripts.html" %}
|
|
42
|
+
{% endblock %}
|