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,91 @@
|
|
|
1
|
+
{% extends "admin/admin_base.html" %}
|
|
2
|
+
{% load i18n %}
|
|
3
|
+
|
|
4
|
+
{% block admin_title %}{% trans "Task Details" %}: {{ task.id }}{% endblock %}
|
|
5
|
+
|
|
6
|
+
{% block content %}
|
|
7
|
+
<nav class="mb-2">
|
|
8
|
+
<a href="{% url 'admin_task_list' %}" class="button transparent circle"><i>arrow_back</i></a>
|
|
9
|
+
<h5 class="max bold ml-2">{% trans "Task" %} {{ task.id|truncatechars:12 }}</h5>
|
|
10
|
+
</nav>
|
|
11
|
+
|
|
12
|
+
<div class="grid">
|
|
13
|
+
<div class="s12 m8">
|
|
14
|
+
<article class="round padding border">
|
|
15
|
+
<h6 class="bold mb-1">{% trans "General Information" %}</h6>
|
|
16
|
+
<div class="divider mb-1"></div>
|
|
17
|
+
|
|
18
|
+
<div class="row mb-1">
|
|
19
|
+
<div class="max bold">{% trans "Task Path" %}:</div>
|
|
20
|
+
<div class="max"><code>{{ task.task_path }}</code></div>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="row mb-1">
|
|
23
|
+
<div class="max bold">{% trans "Status" %}:</div>
|
|
24
|
+
<div class="max">
|
|
25
|
+
{% if task.status == 'SUCCEEDED' %}
|
|
26
|
+
<span class="chip tiny primary"><i>done</i> {{ task.status }}</span>
|
|
27
|
+
{% elif task.status == 'FAILED' %}
|
|
28
|
+
<span class="chip tiny error"><i>error</i> {{ task.status }}</span>
|
|
29
|
+
{% else %}
|
|
30
|
+
<span class="chip tiny surface"><i>schedule</i> {{ task.status }}</span>
|
|
31
|
+
{% endif %}
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="row mb-1">
|
|
35
|
+
<div class="max bold">{% trans "Queue" %}:</div>
|
|
36
|
+
<div class="max">{{ task.queue_name }}</div>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="row mb-1">
|
|
39
|
+
<div class="max bold">{% trans "Priority" %}:</div>
|
|
40
|
+
<div class="max">{{ task.priority }}</div>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div class="space"></div>
|
|
44
|
+
|
|
45
|
+
<h6 class="bold mb-1">{% trans "Timeline" %}</h6>
|
|
46
|
+
<div class="divider mb-1"></div>
|
|
47
|
+
<div class="row mb-1">
|
|
48
|
+
<div class="max bold">{% trans "Enqueued At" %}:</div>
|
|
49
|
+
<div class="max">{{ task.enqueued_at|date:"DATETIME_FORMAT" }}</div>
|
|
50
|
+
</div>
|
|
51
|
+
<div class="row mb-1">
|
|
52
|
+
<div class="max bold">{% trans "Started At" %}:</div>
|
|
53
|
+
<div class="max">{{ task.started_at|date:"DATETIME_FORMAT"|default:"-" }}</div>
|
|
54
|
+
</div>
|
|
55
|
+
<div class="row mb-1">
|
|
56
|
+
<div class="max bold">{% trans "Finished At" %}:</div>
|
|
57
|
+
<div class="max">{{ task.finished_at|date:"DATETIME_FORMAT"|default:"-" }}</div>
|
|
58
|
+
</div>
|
|
59
|
+
</article>
|
|
60
|
+
|
|
61
|
+
<div class="space"></div>
|
|
62
|
+
|
|
63
|
+
<article class="round padding border">
|
|
64
|
+
<h6 class="bold mb-1">{% trans "Arguments" %}</h6>
|
|
65
|
+
<div class="divider mb-1"></div>
|
|
66
|
+
<pre class="padding surface-variant round scroll" style="max-height: 200px;"><code>{{ task.args_kwargs }}</code></pre>
|
|
67
|
+
</article>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div class="s12 m4">
|
|
71
|
+
{% if task.return_value %}
|
|
72
|
+
<article class="round padding border primary-container">
|
|
73
|
+
<h6 class="bold mb-1">{% trans "Result" %}</h6>
|
|
74
|
+
<div class="divider mb-1"></div>
|
|
75
|
+
<div class="scroll" style="max-height: 300px;">
|
|
76
|
+
{{ task.return_value }}
|
|
77
|
+
</div>
|
|
78
|
+
</article>
|
|
79
|
+
{% endif %}
|
|
80
|
+
|
|
81
|
+
{% if task.traceback %}
|
|
82
|
+
<article class="round padding border error-container mt-1">
|
|
83
|
+
<h6 class="bold mb-1">{% trans "Error" %}</h6>
|
|
84
|
+
<div class="divider mb-1"></div>
|
|
85
|
+
<div class="bold mb-1">{{ task.exception_class_path }}</div>
|
|
86
|
+
<pre class="padding surface-variant round scroll small-text" style="max-height: 400px; white-space: pre-wrap;"><code>{{ task.traceback }}</code></pre>
|
|
87
|
+
</article>
|
|
88
|
+
{% endif %}
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
{% endblock %}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{% extends "admin/admin_base.html" %}
|
|
2
|
+
{% load i18n %}
|
|
3
|
+
|
|
4
|
+
{% block admin_title %}{% trans "Background Tasks" %}{% endblock %}
|
|
5
|
+
|
|
6
|
+
{% block content %}
|
|
7
|
+
<h5 class="bold mb-2">{% trans "Task Results" %}</h5>
|
|
8
|
+
|
|
9
|
+
<table class="border striped no-space">
|
|
10
|
+
<thead>
|
|
11
|
+
<tr>
|
|
12
|
+
<th class="min">{% trans "ID" %}</th>
|
|
13
|
+
<th>{% trans "Task Class" %}</th>
|
|
14
|
+
<th class="min">{% trans "Status" %}</th>
|
|
15
|
+
<th>{% trans "Finished" %}</th>
|
|
16
|
+
<th class="right-align">{% trans "Actions" %}</th>
|
|
17
|
+
</tr>
|
|
18
|
+
</thead>
|
|
19
|
+
<tbody>
|
|
20
|
+
{% for task in tasks %}
|
|
21
|
+
<tr class="pointer" onclick="location.href='{% url 'admin_task_detail' task.id %}'">
|
|
22
|
+
<td class="min">{{ task.id|truncatechars:12 }}</td>
|
|
23
|
+
<td><code>{{ task.task_class }}</code></td>
|
|
24
|
+
<td class="min">
|
|
25
|
+
{% if task.status == 'SUCCEEDED' %}
|
|
26
|
+
<span class="chip tiny primary"><i>done</i> {{ task.status }}</span>
|
|
27
|
+
{% elif task.status == 'FAILED' %}
|
|
28
|
+
<span class="chip tiny error"><i>error</i> {{ task.status }}</span>
|
|
29
|
+
{% else %}
|
|
30
|
+
<span class="chip tiny surface"><i>schedule</i> {{ task.status }}</span>
|
|
31
|
+
{% endif %}
|
|
32
|
+
</td>
|
|
33
|
+
<td>{{ task.finished_at|date:"SHORT_DATETIME_FORMAT"|default:"-" }}</td>
|
|
34
|
+
<td class="right-align">
|
|
35
|
+
<i class="grey-text">chevron_right</i>
|
|
36
|
+
</td>
|
|
37
|
+
</tr>
|
|
38
|
+
{% endfor %}
|
|
39
|
+
</tbody>
|
|
40
|
+
</table>
|
|
41
|
+
{% endblock %}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{% extends "admin/admin_base.html" %}
|
|
2
|
+
{% load i18n %}
|
|
3
|
+
|
|
4
|
+
{% block admin_title %}{% trans "Edit User" %}: {{ user_obj.username }}{% endblock %}
|
|
5
|
+
|
|
6
|
+
{% block content %}
|
|
7
|
+
<article class="round padding">
|
|
8
|
+
<form method="post" enctype="multipart/form-data">
|
|
9
|
+
{% csrf_token %}
|
|
10
|
+
<div class="grid">
|
|
11
|
+
{% for field in form %}
|
|
12
|
+
<div class="s12">
|
|
13
|
+
{% if field.field.widget.input_type == "checkbox" %}
|
|
14
|
+
<label class="checkbox">
|
|
15
|
+
{{ field }}
|
|
16
|
+
<span>{{ field.label }}</span>
|
|
17
|
+
</label>
|
|
18
|
+
{% else %}
|
|
19
|
+
<div class="field label border round">
|
|
20
|
+
{{ field }}
|
|
21
|
+
<label>{{ field.label }}</label>
|
|
22
|
+
</div>
|
|
23
|
+
{% endif %}
|
|
24
|
+
{% if field.errors %}
|
|
25
|
+
<span class="error">{{ field.errors|striptags }}</span>
|
|
26
|
+
{% endif %}
|
|
27
|
+
</div>
|
|
28
|
+
{% endfor %}
|
|
29
|
+
</div>
|
|
30
|
+
<div class="space"></div>
|
|
31
|
+
<nav class="right-align">
|
|
32
|
+
<a href="{% url 'admin_user_list' %}" class="button transparent">{% trans "Cancel" %}</a>
|
|
33
|
+
<button type="submit" class="button primary round">{% trans "Save" %}</button>
|
|
34
|
+
</nav>
|
|
35
|
+
</form>
|
|
36
|
+
</article>
|
|
37
|
+
{% endblock %}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{% extends "admin/admin_base.html" %}
|
|
2
|
+
{% load i18n %}
|
|
3
|
+
|
|
4
|
+
{% block admin_title %}{% trans "Users" %}{% endblock %}
|
|
5
|
+
|
|
6
|
+
{% block content %}
|
|
7
|
+
<h5 class="bold mb-2">{% trans "User Management" %}</h5>
|
|
8
|
+
|
|
9
|
+
<table class="border striped no-space">
|
|
10
|
+
<thead>
|
|
11
|
+
<tr>
|
|
12
|
+
<th class="min">{% trans "Avatar" %}</th>
|
|
13
|
+
<th class="max">{% trans "User" %}</th>
|
|
14
|
+
<th>{% trans "Email" %}</th>
|
|
15
|
+
<th class="min center-align">{% trans "Staff" %}</th>
|
|
16
|
+
<th class="min center-align">{% trans "Active" %}</th>
|
|
17
|
+
<th class="right-align">{% trans "Actions" %}</th>
|
|
18
|
+
</tr>
|
|
19
|
+
</thead>
|
|
20
|
+
<tbody>
|
|
21
|
+
{% for u in users %}
|
|
22
|
+
<tr class="pointer" onclick="location.href='{% url 'admin_user_edit' u.pk %}'">
|
|
23
|
+
<td class="min">
|
|
24
|
+
{% if u.avatar %}
|
|
25
|
+
<img src="{{ u.avatar.url }}" class="admin-thumb round">
|
|
26
|
+
{% else %}
|
|
27
|
+
<div class="admin-thumb round gray1 middle-align center-align">
|
|
28
|
+
<i class="extra">person</i>
|
|
29
|
+
</div>
|
|
30
|
+
{% endif %}
|
|
31
|
+
</td>
|
|
32
|
+
<td class="max">
|
|
33
|
+
<b>{{ u.username }}</b>
|
|
34
|
+
<div class="small-text">{{ u.get_full_name }}</div>
|
|
35
|
+
</td>
|
|
36
|
+
<td>{{ u.email }}</td>
|
|
37
|
+
<td class="min center-align">
|
|
38
|
+
{% if u.is_staff %}
|
|
39
|
+
<i class="primary-text">check_circle</i>
|
|
40
|
+
{% else %}
|
|
41
|
+
<i class="grey-text">cancel</i>
|
|
42
|
+
{% endif %}
|
|
43
|
+
</td>
|
|
44
|
+
<td class="min center-align">
|
|
45
|
+
{% if u.is_active %}
|
|
46
|
+
<i class="primary-text">check_circle</i>
|
|
47
|
+
{% else %}
|
|
48
|
+
<i class="grey-text">cancel</i>
|
|
49
|
+
{% endif %}
|
|
50
|
+
</td>
|
|
51
|
+
<td class="right-align">
|
|
52
|
+
{% if u != request.user %}
|
|
53
|
+
<a href="{% url 'admin_user_delete' u.pk %}" class="button circle transparent" onclick="event.stopPropagation();" title="{% trans 'Delete' %}"><i>delete</i></a>
|
|
54
|
+
{% endif %}
|
|
55
|
+
</td>
|
|
56
|
+
</tr>
|
|
57
|
+
{% endfor %}
|
|
58
|
+
</tbody>
|
|
59
|
+
</table>
|
|
60
|
+
{% endblock %}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{% load i18n %}
|
|
2
|
+
<!DOCTYPE html>
|
|
3
|
+
<html lang="{% get_current_language as LANGUAGE_CODE %}{{ LANGUAGE_CODE }}">
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
|
7
|
+
<title>{% block title %}Sandwitches{% endblock %}</title>
|
|
8
|
+
<style>
|
|
9
|
+
body {
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-direction: column;
|
|
12
|
+
min-height: 100vh;
|
|
13
|
+
}
|
|
14
|
+
main.container {
|
|
15
|
+
flex: 1;
|
|
16
|
+
padding-bottom: 80px;
|
|
17
|
+
}
|
|
18
|
+
/* Loading Sandwich CSS */
|
|
19
|
+
.loading-sandwich-container {
|
|
20
|
+
position: fixed;
|
|
21
|
+
top: 0;
|
|
22
|
+
left: 0;
|
|
23
|
+
width: 100%;
|
|
24
|
+
height: 100%;
|
|
25
|
+
background-color: rgba(255, 255, 255, 0.8); /* Semi-transparent white overlay */
|
|
26
|
+
display: flex;
|
|
27
|
+
justify-content: center;
|
|
28
|
+
align-items: center;
|
|
29
|
+
z-index: 9999;
|
|
30
|
+
visibility: hidden; /* Hidden by default */
|
|
31
|
+
opacity: 0;
|
|
32
|
+
transition: visibility 0s, opacity 0.3s linear;
|
|
33
|
+
}
|
|
34
|
+
.loading-sandwich-container.show {
|
|
35
|
+
visibility: visible;
|
|
36
|
+
opacity: 1;
|
|
37
|
+
}
|
|
38
|
+
.loading-sandwich-svg {
|
|
39
|
+
width: 80px;
|
|
40
|
+
height: 80px;
|
|
41
|
+
animation: spin 1.5s linear infinite;
|
|
42
|
+
}
|
|
43
|
+
@keyframes spin {
|
|
44
|
+
0% { transform: rotate(0deg); }
|
|
45
|
+
100% { transform: rotate(360deg); }
|
|
46
|
+
}
|
|
47
|
+
</style>
|
|
48
|
+
{% block extra_head %}{% endblock %}
|
|
49
|
+
</head>
|
|
50
|
+
<body class="{% block body_class %}{% endblock %}">
|
|
51
|
+
<div id="loading-sandwich" class="loading-sandwich-container">
|
|
52
|
+
<svg class="loading-sandwich-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
53
|
+
<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z" />
|
|
54
|
+
<path d="M12 9v6" />
|
|
55
|
+
<path d="M10 12h4" />
|
|
56
|
+
</svg>
|
|
57
|
+
</div>
|
|
58
|
+
{% block navbar %}{% endblock %}
|
|
59
|
+
<main class="container" role="main">{% block content %}{% endblock %}</main>
|
|
60
|
+
{% block footer %}{% endblock %}
|
|
61
|
+
{% block extra_scripts %}{% endblock %}
|
|
62
|
+
|
|
63
|
+
<script>
|
|
64
|
+
// JavaScript to show/hide the loading animation
|
|
65
|
+
const loadingSandwich = document.getElementById('loading-sandwich');
|
|
66
|
+
|
|
67
|
+
function showLoading() {
|
|
68
|
+
loadingSandwich.classList.add('show');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function hideLoading() {
|
|
72
|
+
loadingSandwich.classList.remove('show');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Example usage: You would call showLoading() before an async operation
|
|
76
|
+
// and hideLoading() after it completes.
|
|
77
|
+
// For demonstration, let's show it on page load for a brief moment.
|
|
78
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
79
|
+
// Can uncomment below for debugging purposes
|
|
80
|
+
// showLoading();
|
|
81
|
+
// setTimeout(hideLoading, 1000);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Intercept form submissions to show loading animation
|
|
85
|
+
document.querySelectorAll('form').forEach(form => {
|
|
86
|
+
form.addEventListener('submit', showLoading);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Hide loading animation on page load completion (after initial DOMContentLoaded)
|
|
90
|
+
window.addEventListener('load', hideLoading);
|
|
91
|
+
|
|
92
|
+
</script>
|
|
93
|
+
</body>
|
|
94
|
+
</html>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{% extends "base.html" %} {% load i18n static %} {% block extra_head %}
|
|
2
|
+
<link href="{% static "dist/main.css" %}" rel="stylesheet">
|
|
3
|
+
<script src="{% static "dist/main.js" %}" defer></script>
|
|
4
|
+
<link rel="icon" type="image/svg+xml" href="{% static "icons/favicon.svg" %}">
|
|
5
|
+
<style>
|
|
6
|
+
main.container {
|
|
7
|
+
padding-top: 2rem;
|
|
8
|
+
padding-bottom: 2rem;
|
|
9
|
+
}
|
|
10
|
+
</style>
|
|
11
|
+
{% endblock %}
|
|
12
|
+
|
|
13
|
+
{% block navbar %}
|
|
14
|
+
{% include "components/navbar.html" %}
|
|
15
|
+
{% include "components/language_dialog.html" %}
|
|
16
|
+
{% include "components/user_menu.html" %}
|
|
17
|
+
{% include "components/side_menu.html" %}
|
|
18
|
+
{% endblock %}
|
|
19
|
+
|
|
20
|
+
{% block footer %}
|
|
21
|
+
{% include "components/footer.html" %}
|
|
22
|
+
{% endblock %}
|
|
23
|
+
|
|
24
|
+
{% block extra_scripts %}
|
|
25
|
+
<script>
|
|
26
|
+
document.addEventListener('keydown', function(event) {
|
|
27
|
+
// Don't trigger hotkeys if an input field is focused
|
|
28
|
+
if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA' || event.target.tagName === 'SELECT') {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 'q' for search
|
|
33
|
+
if (event.key === 'q') {
|
|
34
|
+
const searchInput = document.getElementById('search');
|
|
35
|
+
if (searchInput) {
|
|
36
|
+
event.preventDefault(); // Prevent 'q' from being typed
|
|
37
|
+
searchInput.focus();
|
|
38
|
+
// If the advanced search is hidden, show it
|
|
39
|
+
const advancedSearch = document.getElementById('advanced-search');
|
|
40
|
+
if (advancedSearch && advancedSearch.style.display === 'none') {
|
|
41
|
+
toggleAdvanced(); // Assuming toggleAdvanced is a global function from index.html
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 'f' for favorite (only on recipe detail pages)
|
|
47
|
+
if (event.key === 'f') {
|
|
48
|
+
const favoriteButton = document.getElementById('favorite-toggle-button');
|
|
49
|
+
if (favoriteButton) {
|
|
50
|
+
event.preventDefault(); // Prevent default browser behavior (e.g., finding text)
|
|
51
|
+
favoriteButton.click();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
</script>
|
|
56
|
+
{% block page_scripts %}{% endblock %}
|
|
57
|
+
{% endblock %}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
(function() {
|
|
3
|
+
const carousel = document.getElementById('highlighted-carousel');
|
|
4
|
+
const scrollAmount = 320; // Approximately card width + margin
|
|
5
|
+
let autoScrollInterval;
|
|
6
|
+
|
|
7
|
+
window.scrollCarousel = function(direction) {
|
|
8
|
+
if (carousel) {
|
|
9
|
+
const currentScroll = carousel.scrollLeft;
|
|
10
|
+
const maxScroll = carousel.scrollWidth - carousel.clientWidth;
|
|
11
|
+
|
|
12
|
+
let newScroll = currentScroll + (direction * scrollAmount);
|
|
13
|
+
|
|
14
|
+
// Loop functionality for manual navigation?
|
|
15
|
+
// Maybe just stop at ends or loop? Let's just scroll.
|
|
16
|
+
carousel.scrollBy({ left: direction * scrollAmount, behavior: 'smooth' });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function autoScroll() {
|
|
21
|
+
if (!carousel) return;
|
|
22
|
+
|
|
23
|
+
const currentScroll = carousel.scrollLeft;
|
|
24
|
+
const maxScroll = carousel.scrollWidth - carousel.clientWidth;
|
|
25
|
+
|
|
26
|
+
// Tolerance for float calculation
|
|
27
|
+
if (currentScroll >= maxScroll - 10) {
|
|
28
|
+
// Loop back to start
|
|
29
|
+
carousel.scrollTo({ left: 0, behavior: 'smooth' });
|
|
30
|
+
} else {
|
|
31
|
+
carousel.scrollBy({ left: scrollAmount, behavior: 'smooth' });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function startAutoScroll() {
|
|
36
|
+
stopAutoScroll(); // Clear any existing
|
|
37
|
+
autoScrollInterval = setInterval(autoScroll, 5000); // 5 seconds
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function stopAutoScroll() {
|
|
41
|
+
if (autoScrollInterval) {
|
|
42
|
+
clearInterval(autoScrollInterval);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (carousel) {
|
|
47
|
+
// Start auto-scroll
|
|
48
|
+
startAutoScroll();
|
|
49
|
+
|
|
50
|
+
// Pause on interaction
|
|
51
|
+
carousel.addEventListener('mouseenter', stopAutoScroll);
|
|
52
|
+
carousel.addEventListener('mouseleave', startAutoScroll);
|
|
53
|
+
|
|
54
|
+
// Touch events for mobile
|
|
55
|
+
carousel.addEventListener('touchstart', stopAutoScroll);
|
|
56
|
+
carousel.addEventListener('touchend', startAutoScroll);
|
|
57
|
+
}
|
|
58
|
+
})();
|
|
59
|
+
</script>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{% load i18n %}
|
|
2
|
+
<form hx-get="{% url 'favorites' %}" 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">
|
|
7
|
+
<i class="front">search</i>
|
|
8
|
+
<input id="search" type="text" name="q" placeholder="{% trans 'Search favorites...' %}" 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">
|
|
60
|
+
<select name="tag">
|
|
61
|
+
<option value="">{% trans "All Tags" %}</option>
|
|
62
|
+
{% for t in tags %}
|
|
63
|
+
<option value="{{ t.name }}" {% if request.GET.tag == t.name %}selected{% endif %}>{{ t.name }}</option>
|
|
64
|
+
{% endfor %}
|
|
65
|
+
</select>
|
|
66
|
+
<label>{% trans "Tag" %}</label>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div class="s12 m6 l3">
|
|
71
|
+
<div class="field label border round">
|
|
72
|
+
<select name="sort">
|
|
73
|
+
<option value="date_desc" {% if request.GET.sort == 'date_desc' %}selected{% endif %}>{% trans "Newest" %}</option>
|
|
74
|
+
<option value="date_asc" {% if request.GET.sort == 'date_asc' %}selected{% endif %}>{% trans "Oldest" %}</option>
|
|
75
|
+
<option value="rating" {% if request.GET.sort == 'rating' %}selected{% endif %}>{% trans "Highest Rated" %}</option>
|
|
76
|
+
<option value="user" {% if request.GET.sort == 'user' %}selected{% endif %}>{% trans "Uploader (A-Z)" %}</option>
|
|
77
|
+
</select>
|
|
78
|
+
<label>{% trans "Sort By" %}</label>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</form>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<footer class="fixed bottom primary-container">
|
|
2
|
+
<div class="row align-center">
|
|
3
|
+
<div class="max">
|
|
4
|
+
<h6 class="small">Sandwitches</h6>
|
|
5
|
+
<div class="small-text">© {% now "Y" %} Sandwitches Inc.</div>
|
|
6
|
+
</div>
|
|
7
|
+
<nav>
|
|
8
|
+
version {{ version }}
|
|
9
|
+
<a href="https://github.com/martynvdijke/sandwitches" target="_blank" class="circle transparent">
|
|
10
|
+
<i>code</i>
|
|
11
|
+
</a>
|
|
12
|
+
</nav>
|
|
13
|
+
</div>
|
|
14
|
+
</footer>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{% load i18n static %}
|
|
2
|
+
|
|
3
|
+
<script>
|
|
4
|
+
async function scaleIngredients() {
|
|
5
|
+
const portionsInput = document.getElementById('portions');
|
|
6
|
+
const ingredientsDisplay = document.getElementById('ingredients-display');
|
|
7
|
+
const selector = document.querySelector('.portion-selector');
|
|
8
|
+
|
|
9
|
+
if (!portionsInput || !ingredientsDisplay || !selector) return;
|
|
10
|
+
|
|
11
|
+
const recipeId = selector.dataset.recipeId;
|
|
12
|
+
const targetServings = portionsInput.value;
|
|
13
|
+
|
|
14
|
+
ingredientsDisplay.innerHTML = '<div class="center-align padding"><progress class="circle"></progress></div>';
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const url = `/api/v1/recipes/${recipeId}/scale-ingredients?target_servings=${targetServings}`;
|
|
18
|
+
console.log("Fetching:", url);
|
|
19
|
+
const response = await fetch(url);
|
|
20
|
+
|
|
21
|
+
if (!response.ok) throw new Error(`Status: ${response.status}`);
|
|
22
|
+
|
|
23
|
+
const data = await response.json();
|
|
24
|
+
|
|
25
|
+
let html = '<div class="padding">';
|
|
26
|
+
data.forEach(item => {
|
|
27
|
+
html += `<div class="mb-1">${item.scaled_line}</div>`;
|
|
28
|
+
});
|
|
29
|
+
html += '</div>';
|
|
30
|
+
ingredientsDisplay.innerHTML = html;
|
|
31
|
+
} catch (e) {
|
|
32
|
+
console.error("Scaling failed:", e);
|
|
33
|
+
ingredientsDisplay.innerHTML = `<div class="padding error-text">{% trans "Error scaling ingredients:" %} ${e.message}</div>`;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function adjustPortions(delta) {
|
|
38
|
+
const input = document.getElementById('portions');
|
|
39
|
+
if (!input) return;
|
|
40
|
+
let val = parseInt(input.value) || 1;
|
|
41
|
+
val += delta;
|
|
42
|
+
if (val < 1) val = 1;
|
|
43
|
+
input.value = val;
|
|
44
|
+
scaleIngredients();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
48
|
+
scaleIngredients();
|
|
49
|
+
});
|
|
50
|
+
</script>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{% load i18n %}
|
|
2
|
+
<h5 class="primary-text">{% trans "Ingredients" %}</h5>
|
|
3
|
+
<div class="portion-selector" data-recipe-id="{{ recipe.id }}" data-original-servings="{{ recipe.servings }}">
|
|
4
|
+
<label for="portions">{% trans "Portions" %}:</label>
|
|
5
|
+
<button class="button circle transparent" onclick="adjustPortions(-1)">-</button>
|
|
6
|
+
<input type="number" id="portions" value="{{ recipe.servings }}" min="1" onchange="scaleIngredients()">
|
|
7
|
+
<button class="button circle transparent" onclick="adjustPortions(1)">+</button>
|
|
8
|
+
</div>
|
|
9
|
+
<div id="ingredients-display" class="card padding flat gray1">
|
|
10
|
+
<p class="text-center">{% trans "Loading ingredients..." %}</p>
|
|
11
|
+
</div>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{% load i18n markdown_extras %}
|
|
2
|
+
<h5 class="primary-text">{% trans "Instructions" %}</h5>
|
|
3
|
+
<div>
|
|
4
|
+
{% if recipe.instructions %}
|
|
5
|
+
{{ recipe.instructions|convert_markdown|safe }}
|
|
6
|
+
{% else %}
|
|
7
|
+
<p class="italic">{% trans "No instructions yet." %}</p>
|
|
8
|
+
{% endif %}
|
|
9
|
+
</div>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{% load i18n %}
|
|
2
|
+
<dialog id="language-menu">
|
|
3
|
+
<form method="post" action="{% url 'set_language' %}">
|
|
4
|
+
{% csrf_token %}
|
|
5
|
+
<h5 class="center-align">{% trans "Select Language" %}</h5>
|
|
6
|
+
<div class="grid center-align">
|
|
7
|
+
{% get_current_language as LANGUAGE_CODE %}
|
|
8
|
+
{% get_available_languages as LANGUAGES %}
|
|
9
|
+
{% for code, name in LANGUAGES %}
|
|
10
|
+
<div class="s6">
|
|
11
|
+
<button type="submit" name="language" value="{{ code }}" class="circle large {% if code == LANGUAGE_CODE %}primary{% else %}surface{% endif %}">
|
|
12
|
+
<span style="font-size: 2rem;">
|
|
13
|
+
{% if code == 'en' %}🇬🇧{% elif code == 'nl' %}🇳🇱{% else %}🌐{% endif %}
|
|
14
|
+
</span>
|
|
15
|
+
</button>
|
|
16
|
+
<div class="small-text">{{ name }}</div>
|
|
17
|
+
</div>
|
|
18
|
+
{% endfor %}
|
|
19
|
+
</div>
|
|
20
|
+
<input type="hidden" name="next" value="{{ request.get_full_path }}" />
|
|
21
|
+
</form>
|
|
22
|
+
<div class="space"></div>
|
|
23
|
+
<nav class="center-align">
|
|
24
|
+
<button class="transparent link" data-ui="#language-menu">{% trans "Close" %}</button>
|
|
25
|
+
</nav>
|
|
26
|
+
</dialog>
|