sandwitches 1.4.2__py3-none-any.whl → 2.0.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.
Files changed (67) hide show
  1. sandwitches/__init__.py +6 -0
  2. sandwitches/admin.py +21 -2
  3. sandwitches/api.py +112 -6
  4. sandwitches/feeds.py +23 -0
  5. sandwitches/forms.py +110 -7
  6. sandwitches/locale/nl/LC_MESSAGES/django.mo +0 -0
  7. sandwitches/locale/nl/LC_MESSAGES/django.po +784 -134
  8. sandwitches/migrations/0001_initial.py +255 -2
  9. sandwitches/migrations/0002_historicalrecipe_servings_recipe_servings.py +27 -0
  10. sandwitches/migrations/0003_setting.py +35 -0
  11. sandwitches/migrations/0004_alter_setting_ai_api_key_and_more.py +37 -0
  12. sandwitches/migrations/0005_rating_comment.py +17 -0
  13. sandwitches/models.py +48 -4
  14. sandwitches/settings.py +14 -5
  15. sandwitches/storage.py +44 -12
  16. sandwitches/templates/admin/admin_base.html +118 -0
  17. sandwitches/templates/admin/confirm_delete.html +23 -0
  18. sandwitches/templates/admin/dashboard.html +262 -0
  19. sandwitches/templates/admin/rating_list.html +38 -0
  20. sandwitches/templates/admin/recipe_form.html +184 -0
  21. sandwitches/templates/admin/recipe_list.html +64 -0
  22. sandwitches/templates/admin/tag_form.html +30 -0
  23. sandwitches/templates/admin/tag_list.html +37 -0
  24. sandwitches/templates/admin/task_detail.html +91 -0
  25. sandwitches/templates/admin/task_list.html +41 -0
  26. sandwitches/templates/admin/user_form.html +37 -0
  27. sandwitches/templates/admin/user_list.html +60 -0
  28. sandwitches/templates/base.html +80 -1
  29. sandwitches/templates/base_beer.html +57 -0
  30. sandwitches/templates/components/favorites_search_form.html +85 -0
  31. sandwitches/templates/components/footer.html +14 -0
  32. sandwitches/templates/components/ingredients_scripts.html +50 -0
  33. sandwitches/templates/components/ingredients_section.html +11 -0
  34. sandwitches/templates/components/instructions_section.html +9 -0
  35. sandwitches/templates/components/language_dialog.html +26 -0
  36. sandwitches/templates/components/navbar.html +27 -0
  37. sandwitches/templates/components/rating_section.html +66 -0
  38. sandwitches/templates/components/recipe_header.html +32 -0
  39. sandwitches/templates/components/search_form.html +106 -0
  40. sandwitches/templates/components/search_scripts.html +98 -0
  41. sandwitches/templates/components/side_menu.html +35 -0
  42. sandwitches/templates/components/user_menu.html +10 -0
  43. sandwitches/templates/detail.html +167 -110
  44. sandwitches/templates/favorites.html +42 -0
  45. sandwitches/templates/index.html +28 -61
  46. sandwitches/templates/partials/recipe_list.html +87 -0
  47. sandwitches/templates/recipe_form.html +119 -0
  48. sandwitches/templates/setup.html +1 -1
  49. sandwitches/templates/signup.html +114 -31
  50. sandwitches/templatetags/custom_filters.py +15 -0
  51. sandwitches/urls.py +56 -0
  52. sandwitches/utils.py +222 -0
  53. sandwitches/views.py +503 -14
  54. sandwitches-2.0.0.dist-info/METADATA +104 -0
  55. sandwitches-2.0.0.dist-info/RECORD +62 -0
  56. sandwitches/migrations/0002_historicalrecipe.py +0 -61
  57. sandwitches/migrations/0003_rating.py +0 -57
  58. sandwitches/migrations/0004_add_uploaded_by.py +0 -25
  59. sandwitches/migrations/0005_historicalrecipe_uploaded_by.py +0 -27
  60. sandwitches/migrations/0006_profile.py +0 -48
  61. sandwitches/migrations/0007_alter_rating_score.py +0 -23
  62. sandwitches/migrations/0008_delete_profile.py +0 -15
  63. sandwitches/templates/base_pico.html +0 -260
  64. sandwitches/templates/form.html +0 -16
  65. sandwitches-1.4.2.dist-info/METADATA +0 -25
  66. sandwitches-1.4.2.dist-info/RECORD +0 -35
  67. {sandwitches-1.4.2.dist-info → sandwitches-2.0.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,118 @@
1
+ {% extends "base.html" %}
2
+ {% load i18n static %}
3
+
4
+ {% block extra_head %}
5
+ <link href="https://cdn.jsdelivr.net/npm/beercss@3.7.12/dist/cdn/beer.min.css" rel="stylesheet">
6
+ <script type="module" src="https://cdn.jsdelivr.net/npm/beercss@3.7.12/dist/cdn/beer.min.js"></script>
7
+ <script type="module" src="https://cdn.jsdelivr.net/npm/material-dynamic-colors@1.1.2/dist/cdn/material-dynamic-colors.min.js"></script>
8
+ <script src="https://unpkg.com/htmx.org@2.0.8/dist/htmx.min.js"></script>
9
+ <link rel="icon" type="image/svg+xml" href="{% static "icons/favicon.svg" %}">
10
+ <style>
11
+ main.container {
12
+ padding-top: 2rem;
13
+ padding-bottom: 2rem;
14
+ }
15
+ .admin-thumb {
16
+ width: 80px;
17
+ height: 80px;
18
+ object-fit: cover;
19
+ }
20
+ </style>
21
+ {% endblock %}
22
+
23
+ {% block navbar %}
24
+ <nav class="top primary-container">
25
+ <button class="circle transparent" data-ui="#admin-menu">
26
+ <i>menu</i>
27
+ </button>
28
+ <a href="{% url 'admin_dashboard' %}" class="row align-center">
29
+ <img src="{% static 'icons/icon.svg' %}" class="circle small">
30
+ <h6 class="max">{% block admin_title %}{% trans "Sandwitches Admin" %}{% endblock %}</h6>
31
+ </a>
32
+ <div class="max"></div>
33
+ <button class="circle transparent" onclick="toggleMode()">
34
+ <i>dark_mode</i>
35
+ </button>
36
+
37
+ {% if user.avatar %}
38
+ <img src="{{ user.avatar.url }}" class="circle" data-ui="#user-menu">
39
+ {% else %}
40
+ <img src="https://www.w3schools.com/howto/img_avatar.png" class="circle" data-ui="#user-menu">
41
+ {% endif %}
42
+ </nav>
43
+
44
+ <dialog id="admin-menu" class="left">
45
+ <nav class="drawer vertical">
46
+ <header>
47
+ <img src="{% static 'icons/icon.svg' %}" class="circle large">
48
+ <h5 class="max">{% trans "Admin" %}</h5>
49
+ </header>
50
+ <div class="space"></div>
51
+ <a href="{% url 'admin_dashboard' %}" class="{% if request.resolver_match.url_name == 'admin_dashboard' %}active{% endif %}">
52
+ <i>dashboard</i>
53
+ <span>{% trans "Dashboard" %}</span>
54
+ </a>
55
+ <a href="{% url 'admin_recipe_list' %}" class="{% if request.resolver_match.url_name == 'admin_recipe_list' %}active{% endif %}">
56
+ <i>restaurant</i>
57
+ <span>{% trans "Recipes" %}</span>
58
+ </a>
59
+ <a href="{% url 'admin_user_list' %}" class="{% if request.resolver_match.url_name == 'admin_user_list' %}active{% endif %}">
60
+ <i>people</i>
61
+ <span>{% trans "Users" %}</span>
62
+ </a>
63
+ <a href="{% url 'admin_tag_list' %}" class="{% if request.resolver_match.url_name == 'admin_tag_list' %}active{% endif %}">
64
+ <i>label</i>
65
+ <span>{% trans "Tags" %}</span>
66
+ </a>
67
+ <a href="{% url 'admin_rating_list' %}" class="{% if request.resolver_match.url_name == 'admin_rating_list' %}active{% endif %}">
68
+ <i>star</i>
69
+ <span>{% trans "Ratings" %}</span>
70
+ </a>
71
+ <a href="{% url 'admin_task_list' %}" class="{% if request.resolver_match.url_name == 'admin_task_list' %}active{% endif %}">
72
+ <i>assignment</i>
73
+ <span>{% trans "Tasks" %}</span>
74
+ </a>
75
+ <div class="divider"></div>
76
+ <a href="{% url 'index' %}">
77
+ <i>home</i>
78
+ <span>{% trans "Public Site" %}</span>
79
+ </a>
80
+ </nav>
81
+ </dialog>
82
+
83
+ {% if user.is_authenticated %}
84
+ <menu id="user-menu" class="no-wrap left">
85
+ <a href="{% url 'admin:logout' %}" class="row"><i>logout</i>{% trans "Logout" %}</a>
86
+ </menu>
87
+ {% endif %}
88
+
89
+ {% if messages %}
90
+ <div class="padding">
91
+ {% for message in messages %}
92
+ <div class="snackbar active {% if message.tags == 'error' %}error{% else %}primary{% endif %}">
93
+ <i>{% if message.tags == 'error' %}error{% else %}info{% endif %}</i>
94
+ <span>{{ message }}</span>
95
+ </div>
96
+ {% endfor %}
97
+ </div>
98
+ {% endif %}
99
+ {% endblock %}
100
+
101
+ {% block footer %}
102
+ <footer class="padding">
103
+ <div class="row align-center">
104
+ <div class="max">
105
+ <h6 class="small">Sandwitches Admin</h6>
106
+ <div class="small-text">© {% now "Y" %} Sandwitches Inc.</div>
107
+ </div>
108
+ <nav>
109
+ version {{ version }}
110
+ </nav>
111
+ </div>
112
+ </footer>
113
+ {% endblock %}
114
+
115
+ {% block extra_scripts %}
116
+ {{ block.super }}
117
+ {% block admin_scripts %}{% endblock %}
118
+ {% endblock %}
@@ -0,0 +1,23 @@
1
+ {% extends "admin/admin_base.html" %}
2
+ {% load i18n %}
3
+
4
+ {% block admin_title %}{% trans "Confirm Delete" %}{% endblock %}
5
+
6
+ {% block content %}
7
+ <article class="round padding center-align">
8
+ <i class="extra error-text">warning</i>
9
+ <h5>{% trans "Are you sure?" %}</h5>
10
+ <p>{% trans "You are about to delete the following" %} {{ type }}: <b>{{ object }}</b></p>
11
+ <p>{% trans "This action cannot be undone." %}</p>
12
+
13
+ <div class="space"></div>
14
+
15
+ <form method="post">
16
+ {% csrf_token %}
17
+ <nav class="center-align">
18
+ <button type="button" class="button transparent" onclick="history.back()">{% trans "Cancel" %}</button>
19
+ <button type="submit" class="button error round">{% trans "Yes, Delete" %}</button>
20
+ </nav>
21
+ </form>
22
+ </article>
23
+ {% endblock %}
@@ -0,0 +1,262 @@
1
+ {% extends "admin/admin_base.html" %}
2
+ {% load i18n %}
3
+
4
+ {% block admin_title %}{% trans "Dashboard" %}{% endblock %}
5
+
6
+
7
+
8
+ {% block content %}
9
+
10
+ <div class="row align-center mb-2">
11
+
12
+ <div class="max"></div>
13
+
14
+ <form method="get" class="row no-space border round padding surface-variant">
15
+
16
+ <div class="field label small transparent">
17
+
18
+ <input type="date" name="start_date" value="{{ start_date }}">
19
+
20
+ <label>{% trans "Start Date" %}</label>
21
+
22
+ </div>
23
+
24
+ <div class="divider vertical"></div>
25
+
26
+ <div class="field label small transparent">
27
+
28
+ <input type="date" name="end_date" value="{{ end_date }}">
29
+
30
+ <label>{% trans "End Date" %}</label>
31
+
32
+ </div>
33
+
34
+ <button type="submit" class="button primary square"><i>refresh</i></button>
35
+
36
+ </form>
37
+
38
+ </div>
39
+
40
+
41
+
42
+ <div class="grid">
43
+
44
+ <div class="s12 m4">
45
+
46
+ <article class="round primary-container padding">
47
+
48
+ <div class="row align-center">
49
+
50
+ <i class="extra">restaurant</i>
51
+
52
+ <div class="max">
53
+
54
+ <h4 class="bold">{{ recipe_count }}</h4>
55
+
56
+ <div>{% trans "Recipes" %}</div>
57
+
58
+ </div>
59
+
60
+ </div>
61
+
62
+ <nav class="right-align">
63
+
64
+ <a href="{% url 'admin_recipe_list' %}" class="button transparent">{% trans "View all" %}</a>
65
+
66
+ </nav>
67
+
68
+ </article>
69
+
70
+ </div>
71
+
72
+ <div class="s12 m4">
73
+
74
+ <article class="round secondary-container padding">
75
+
76
+ <div class="row align-center">
77
+
78
+ <i class="extra">people</i>
79
+
80
+ <div class="max">
81
+
82
+ <h4 class="bold">{{ user_count }}</h4>
83
+
84
+ <div>{% trans "Users" %}</div>
85
+
86
+ </div>
87
+
88
+ </div>
89
+
90
+ <nav class="right-align">
91
+
92
+ <a href="{% url 'admin_user_list' %}" class="button transparent">{% trans "View all" %}</a>
93
+
94
+ </nav>
95
+
96
+ </article>
97
+
98
+ </div>
99
+
100
+ <div class="s12 m4">
101
+
102
+ <article class="round tertiary-container padding">
103
+
104
+ <div class="row align-center">
105
+
106
+ <i class="extra">label</i>
107
+
108
+ <div class="max">
109
+
110
+ <h4 class="bold">{{ tag_count }}</h4>
111
+
112
+ <div>{% trans "Tags" %}</div>
113
+
114
+ </div>
115
+
116
+ </div>
117
+
118
+ <nav class="right-align">
119
+
120
+ <a href="{% url 'admin_tag_list' %}" class="button transparent">{% trans "View all" %}</a>
121
+
122
+ </nav>
123
+
124
+ </article>
125
+
126
+ </div>
127
+
128
+
129
+
130
+ <!-- Charts Section -->
131
+
132
+ <div class="s12 m6">
133
+
134
+ <article class="round border padding">
135
+
136
+ <h6 class="bold mb-1">{% trans "Recipes Over Time (Last 30 Days)" %}</h6>
137
+
138
+ <canvas id="recipeChart" style="width:100%; max-height:300px;"></canvas>
139
+
140
+ </article>
141
+
142
+ </div>
143
+
144
+ <div class="s12 m6">
145
+
146
+ <article class="round border padding">
147
+
148
+ <h6 class="bold mb-1">{% trans "Average Rating Over Time (Last 30 Days)" %}</h6>
149
+
150
+ <canvas id="ratingChart" style="width:100%; max-height:300px;"></canvas>
151
+
152
+ </article>
153
+
154
+ </div>
155
+
156
+
157
+
158
+ <div class="s12">
159
+
160
+ <h5 class="bold">{% trans "Recent Recipes" %}</h5>
161
+
162
+ <table class="border striped no-space">
163
+
164
+ <thead>
165
+
166
+ <tr>
167
+
168
+ <th>{% trans "Title" %}</th>
169
+
170
+ <th>{% trans "Uploader" %}</th>
171
+
172
+ <th>{% trans "Created At" %}</th>
173
+
174
+ <th class="right-align">{% trans "Actions" %}</th>
175
+
176
+ </tr>
177
+
178
+ </thead>
179
+
180
+ <tbody>
181
+
182
+ {% for recipe in recent_recipes %}
183
+
184
+ <tr class="pointer" onclick="location.href='{% url 'admin_recipe_edit' recipe.pk %}'">
185
+
186
+ <td>{{ recipe.title }}</td>
187
+
188
+ <td>{{ recipe.uploaded_by.username|default:"-" }}</td>
189
+
190
+ <td>{{ recipe.created_at|date:"SHORT_DATETIME_FORMAT" }}</td>
191
+
192
+ <td class="right-align">
193
+
194
+ <a href="{% url 'admin_recipe_edit' recipe.pk %}" class="button circle transparent" onclick="event.stopPropagation();"><i>edit</i></a>
195
+
196
+ </td>
197
+
198
+ </tr>
199
+
200
+ {% endfor %}
201
+
202
+ </tbody>
203
+
204
+ </table>
205
+
206
+ </div>
207
+
208
+ </div>
209
+
210
+ {% endblock %}
211
+
212
+
213
+
214
+ {% block admin_scripts %}
215
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
216
+ <script>
217
+ document.addEventListener('DOMContentLoaded', function() {
218
+ const primaryColor = getComputedStyle(document.documentElement).getPropertyValue('--primary').trim() || '#5d4037';
219
+ const secondaryColor = getComputedStyle(document.documentElement).getPropertyValue('--secondary').trim() || '#ff7a18';
220
+
221
+ // Recipe Chart
222
+ new Chart(document.getElementById('recipeChart'), {
223
+ type: 'line',
224
+ data: {
225
+ labels: {{ recipe_labels|safe }},
226
+ datasets: [{
227
+ label: '{% trans "Recipes Created" %}',
228
+ data: {{ recipe_counts|safe }},
229
+ borderColor: primaryColor,
230
+ backgroundColor: primaryColor + '33',
231
+ fill: true,
232
+ tension: 0.4
233
+ }]
234
+ },
235
+ options: {
236
+ responsive: true,
237
+ plugins: { legend: { display: false } },
238
+ scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } } }
239
+ }
240
+ });
241
+
242
+ // Rating Chart
243
+ new Chart(document.getElementById('ratingChart'), {
244
+ type: 'bar',
245
+ data: {
246
+ labels: {{ rating_labels|safe }},
247
+ datasets: [{
248
+ label: '{% trans "Avg Rating" %}',
249
+ data: {{ rating_avgs|safe }},
250
+ backgroundColor: secondaryColor,
251
+ borderRadius: 4
252
+ }]
253
+ },
254
+ options: {
255
+ responsive: true,
256
+ plugins: { legend: { display: false } },
257
+ scales: { y: { min: 0, max: 10 } }
258
+ }
259
+ });
260
+ });
261
+ </script>
262
+ {% endblock %}
@@ -0,0 +1,38 @@
1
+ {% extends "admin/admin_base.html" %}
2
+ {% load i18n %}
3
+
4
+ {% block admin_title %}{% trans "Ratings" %}{% endblock %}
5
+
6
+ {% block content %}
7
+ <h5 class="bold mb-2">{% trans "Recipe Ratings" %}</h5>
8
+
9
+ <table class="border striped no-space">
10
+ <thead>
11
+ <tr>
12
+ <th>{% trans "Recipe" %}</th>
13
+ <th>{% trans "User" %}</th>
14
+ <th class="min center-align">{% trans "Score" %}</th>
15
+ <th>{% trans "Updated" %}</th>
16
+ <th class="right-align">{% trans "Actions" %}</th>
17
+ </tr>
18
+ </thead>
19
+ <tbody>
20
+ {% for r in ratings %}
21
+ <tr>
22
+ <td><b>{{ r.recipe.title }}</b></td>
23
+ <td>{{ r.user.username }}</td>
24
+ <td class="min center-align">
25
+ <div class="row align-center">
26
+ <i class="primary-text">star</i>
27
+ <span>{{ r.score }}</span>
28
+ </div>
29
+ </td>
30
+ <td>{{ r.updated_at|date:"SHORT_DATETIME_FORMAT" }}</td>
31
+ <td class="right-align">
32
+ <a href="{% url 'admin_rating_delete' r.pk %}" class="button circle transparent" title="{% trans 'Delete' %}"><i>delete</i></a>
33
+ </td>
34
+ </tr>
35
+ {% endfor %}
36
+ </tbody>
37
+ </table>
38
+ {% endblock %}
@@ -0,0 +1,184 @@
1
+ {% extends "admin/admin_base.html" %}
2
+ {% load i18n %}
3
+
4
+ {% block admin_title %}{{ title }}{% endblock %}
5
+
6
+ {% block extra_head %}
7
+ {{ block.super }}
8
+ <link rel="stylesheet" href="https://unpkg.com/easymde/dist/easymde.min.css">
9
+ <script src="https://unpkg.com/easymde/dist/easymde.min.js"></script>
10
+ <style>
11
+ /* Ensure EasyMDE fits well with BeerCSS containers */
12
+ .editor-toolbar {
13
+ background: var(--surface-variant) !important;
14
+ color: var(--on-surface-variant) !important;
15
+ border-color: var(--outline) !important;
16
+ opacity: 1 !important;
17
+ }
18
+ .CodeMirror {
19
+ background: var(--surface) !important;
20
+ color: var(--on-surface) !important;
21
+ border-color: var(--outline) !important;
22
+ }
23
+ .editor-toolbar button {
24
+ color: var(--on-surface-variant) !important;
25
+ }
26
+ .editor-toolbar button.active, .editor-toolbar button:hover {
27
+ background: var(--primary-container) !important;
28
+ color: var(--on-primary-container) !important;
29
+ }
30
+ /* Add spacing between sections */
31
+ .form-section {
32
+ margin-bottom: 2rem;
33
+ }
34
+ </style>
35
+ {% endblock %}
36
+
37
+ {% block content %}
38
+ <form method="post" enctype="multipart/form-data" id="recipe-form">
39
+ {% csrf_token %}
40
+ {{ form.rotation }}
41
+ <div class="grid">
42
+ <!-- Top Section: Title, Tags, and Image -->
43
+ <div class="s12 m8">
44
+ <article class="round padding border mb-2">
45
+ <div class="field label border round">
46
+ {{ form.title }}
47
+ <label>{{ form.title.label }}</label>
48
+ {% if form.title.errors %}<span class="error">{{ form.title.errors|striptags }}</span>{% endif %}
49
+ </div>
50
+
51
+ <div class="field label border round mt-1">
52
+ {{ form.tags_string }}
53
+ <label>{{ form.tags_string.label }}</label>
54
+ {% if form.tags_string.errors %}<span class="error">{{ form.tags_string.errors|striptags }}</span>{% endif %}
55
+ <span class="helper">{% trans "Separate tags with commas. New tags will be created automatically." %}</span>
56
+ </div>
57
+
58
+ <div class="field label border round mt-1">
59
+ {{ form.uploaded_by }}
60
+ <label>{{ form.uploaded_by.label }}</label>
61
+ {% if form.uploaded_by.errors %}<span class="error">{{ form.uploaded_by.errors|striptags }}</span>{% endif %}
62
+ </div>
63
+ </article>
64
+
65
+ <!-- Description -->
66
+ <div class="form-section">
67
+ <div class="row align-center mb-1">
68
+ <i class="primary-text">description</i>
69
+ <h6 class="bold ml-1">{% trans "Description" %}</h6>
70
+ </div>
71
+ <div class="card border no-padding">
72
+ {{ form.description }}
73
+ </div>
74
+ {% if form.description.errors %}<span class="error">{{ form.description.errors|striptags }}</span>{% endif %}
75
+ </div>
76
+ </div>
77
+
78
+ <div class="s12 m4">
79
+ <article class="round padding border center-align">
80
+ <h6 class="bold mb-1">{% trans "Recipe Image" %}</h6>
81
+
82
+ <div class="relative mb-1" style="overflow: hidden; min-height: 200px; display: flex; align-items: center; justify-content: center;">
83
+ {% if recipe.image %}
84
+ <img src="{{ recipe.image_medium.url }}" class="responsive round" id="image-preview" style="max-height: 300px; width: 100%; object-fit: contain; transition: transform 0.3s ease;">
85
+ {% else %}
86
+ <div class="medium-height middle-align center-align gray1 round" id="image-placeholder" style="width: 100%;">
87
+ <i class="extra">image</i>
88
+ </div>
89
+ {% endif %}
90
+ </div>
91
+
92
+ {% if recipe.image %}
93
+ <div class="row no-space border round mb-1">
94
+ <button type="button" class="button transparent max" onclick="rotatePreview(-90)" title="{% trans 'Rotate 90° CCW' %}">
95
+ <i>rotate_left</i>
96
+ </button>
97
+ <div class="divider vertical"></div>
98
+ <button type="button" class="button transparent max" onclick="rotatePreview(90)" title="{% trans 'Rotate 90° CW' %}">
99
+ <i>rotate_right</i>
100
+ </button>
101
+ </div>
102
+ {% endif %}
103
+
104
+ <div class="field file border round">
105
+ <input type="text" readonly>
106
+ {{ form.image }}
107
+ <label>{{ form.image.label }}</label>
108
+ <i>publish</i>
109
+ </div>
110
+ {% if form.image.errors %}<span class="error">{{ form.image.errors|striptags }}</span>{% endif %}
111
+ </article>
112
+ </div>
113
+
114
+ <!-- Ingredients and Instructions (Full Width) -->
115
+ <div class="s12">
116
+ <div class="divider mb-2"></div>
117
+
118
+ <div class="form-section">
119
+ <div class="row align-center mb-1">
120
+ <i class="primary-text">shopping_cart</i>
121
+ <h6 class="bold ml-1">{% trans "Ingredients" %}</h6>
122
+ </div>
123
+ <div class="card border no-padding">
124
+ {{ form.ingredients }}
125
+ </div>
126
+ {% if form.ingredients.errors %}<span class="error">{{ form.ingredients.errors|striptags }}</span>{% endif %}
127
+ </div>
128
+
129
+ <div class="form-section">
130
+ <div class="row align-center mb-1">
131
+ <i class="primary-text">list</i>
132
+ <h6 class="bold ml-1">{% trans "Instructions" %}</h6>
133
+ </div>
134
+ <div class="card border no-padding">
135
+ {{ form.instructions }}
136
+ </div>
137
+ {% if form.instructions.errors %}<span class="error">{{ form.instructions.errors|striptags }}</span>{% endif %}
138
+ </div>
139
+ </div>
140
+ </div>
141
+
142
+ <div class="large-space"></div>
143
+ <nav class="right-align padding surface-container border round">
144
+ <a href="{% url 'admin_recipe_list' %}" class="button transparent">{% trans "Cancel" %}</a>
145
+ <button type="submit" class="button primary round">
146
+ <i>save</i>
147
+ <span>{% trans "Save Recipe" %}</span>
148
+ </button>
149
+ </nav>
150
+ </form>
151
+ {% endblock %}
152
+
153
+ {% block admin_scripts %}
154
+ <script>
155
+ let currentRotation = 0;
156
+
157
+ function rotatePreview(angle) {
158
+ currentRotation = (currentRotation + angle) % 360;
159
+ const img = document.getElementById('image-preview');
160
+ if (img) {
161
+ img.style.transform = `rotate(${currentRotation}deg)`;
162
+ document.getElementsByName('rotation')[0].value = currentRotation;
163
+ }
164
+ }
165
+
166
+ document.addEventListener('DOMContentLoaded', function() {
167
+ const fields = ['id_description', 'id_ingredients', 'id_instructions'];
168
+ fields.forEach(id => {
169
+ const el = document.getElementById(id);
170
+ if (el) {
171
+ new EasyMDE({
172
+ element: el,
173
+ spellChecker: false,
174
+ autosave: {
175
+ enabled: false
176
+ },
177
+ status: false,
178
+ minHeight: "200px"
179
+ });
180
+ }
181
+ });
182
+ });
183
+ </script>
184
+ {% endblock %}