django-adminflow 1.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.
- adminflow/__init__.py +1 -0
- adminflow/apps.py +22 -0
- adminflow/context_processors.py +72 -0
- adminflow/forms.py +89 -0
- adminflow/import_export.py +224 -0
- adminflow/static/adminflow/img/logo.png +0 -0
- adminflow/templates/admin/actions.html +79 -0
- adminflow/templates/admin/base.html +1263 -0
- adminflow/templates/admin/base_site.html +5 -0
- adminflow/templates/admin/change_form.html +315 -0
- adminflow/templates/admin/change_list.html +216 -0
- adminflow/templates/admin/change_list_results.html +211 -0
- adminflow/templates/admin/delete_confirmation.html +55 -0
- adminflow/templates/admin/delete_selected_confirmation.html +118 -0
- adminflow/templates/admin/edit_inline/stacked.html +85 -0
- adminflow/templates/admin/edit_inline/tabular.html +81 -0
- adminflow/templates/admin/filter.html +17 -0
- adminflow/templates/admin/import_export/change_list_export_item.html +8 -0
- adminflow/templates/admin/import_export/change_list_import_item.html +8 -0
- adminflow/templates/admin/import_export/export.html +199 -0
- adminflow/templates/admin/includes/fieldset.html +73 -0
- adminflow/templates/admin/includes/toast.html +102 -0
- adminflow/templates/admin/index.html +227 -0
- adminflow/templates/admin/logged_out.html +38 -0
- adminflow/templates/admin/login.html +144 -0
- adminflow/templates/admin/object_history.html +60 -0
- adminflow/templates/admin/pagination.html +30 -0
- adminflow/templates/admin/search_form.html +28 -0
- adminflow/templates/admin/submit_line.html +48 -0
- adminflow/templates/admin/verify_2fa.html +280 -0
- adminflow/templates/otp/email/token.html +64 -0
- adminflow/templates/otp/email/token.txt +11 -0
- adminflow/templates/registration/logged_out.html +38 -0
- adminflow/templates/registration/password_change_form.html +272 -0
- adminflow/templates/simple_history/object_history.html +186 -0
- adminflow/templates/simple_history/object_history_form.html +103 -0
- adminflow/templates/simple_history/submit_line.html +17 -0
- adminflow/templatetags/__init__.py +1 -0
- adminflow/templatetags/__pycache__/__init__.cpython-314.pyc +0 -0
- adminflow/templatetags/__pycache__/adminflow_tags.cpython-314.pyc +0 -0
- adminflow/templatetags/adminflow_tags.py +233 -0
- adminflow/user_admin.py +662 -0
- adminflow/views.py +207 -0
- django_adminflow-1.0.0.dist-info/METADATA +398 -0
- django_adminflow-1.0.0.dist-info/RECORD +47 -0
- django_adminflow-1.0.0.dist-info/WHEEL +5 -0
- django_adminflow-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
{% extends "admin/base_site.html" %}
|
|
2
|
+
{% load i18n admin_urls static import_export_tags adminflow_tags %}
|
|
3
|
+
|
|
4
|
+
{% block extrastyle %}
|
|
5
|
+
{{ block.super }}
|
|
6
|
+
{{ form.media }}
|
|
7
|
+
<style>
|
|
8
|
+
.field-chip input[type="checkbox"] { display: none; }
|
|
9
|
+
.field-chip label {
|
|
10
|
+
display: inline-flex; align-items: center; gap: 5px;
|
|
11
|
+
padding: 5px 11px; border-radius: 8px; cursor: pointer;
|
|
12
|
+
font-size: 12px; font-weight: 600; user-select: none; transition: all .15s;
|
|
13
|
+
border: 1px solid var(--color-outline-variant, #cbd5e1);
|
|
14
|
+
background: var(--color-surface-low, #f1f5f9);
|
|
15
|
+
color: var(--color-on-surface, #0f172a);
|
|
16
|
+
}
|
|
17
|
+
.field-chip input:checked + label {
|
|
18
|
+
background: var(--color-primary, #2563eb);
|
|
19
|
+
border-color: var(--color-primary, #2563eb);
|
|
20
|
+
color: #fff;
|
|
21
|
+
}
|
|
22
|
+
.field-chip label:hover { filter: brightness(.95); }
|
|
23
|
+
.info-chip {
|
|
24
|
+
display: inline-flex; align-items: center; gap: 5px;
|
|
25
|
+
padding: 5px 11px; border-radius: 8px;
|
|
26
|
+
font-size: 12px; font-weight: 500;
|
|
27
|
+
border: 1px dashed var(--color-outline-variant, #cbd5e1);
|
|
28
|
+
background: var(--color-surface-lowest, #fff);
|
|
29
|
+
color: var(--color-on-surface-variant, #64748b);
|
|
30
|
+
}
|
|
31
|
+
</style>
|
|
32
|
+
{% endblock %}
|
|
33
|
+
|
|
34
|
+
{% block extrahead %}
|
|
35
|
+
{{ block.super }}
|
|
36
|
+
<script type="text/javascript" src="{% url 'admin:jsi18n' %}"></script>
|
|
37
|
+
{% endblock %}
|
|
38
|
+
|
|
39
|
+
{% block breadcrumbs %}
|
|
40
|
+
<nav class="flex items-center gap-1.5 text-xs text-on-surface-variant/70 mb-4">
|
|
41
|
+
<a href="{% url 'admin:index' %}" class="hover:text-primary transition-colors flex items-center">
|
|
42
|
+
<span class="material-symbols-outlined text-[16px]">home</span>
|
|
43
|
+
</a>
|
|
44
|
+
<span class="material-symbols-outlined text-[14px]">chevron_right</span>
|
|
45
|
+
<a href="{% url 'admin:app_list' app_label=opts.app_label %}" class="hover:text-primary capitalize font-medium">{{ opts.app_config.verbose_name }}</a>
|
|
46
|
+
<span class="material-symbols-outlined text-[14px]">chevron_right</span>
|
|
47
|
+
<a href="{% url opts|admin_urlname:'changelist' %}" class="hover:text-primary font-medium">{{ opts.verbose_name_plural|capfirst }}</a>
|
|
48
|
+
<span class="material-symbols-outlined text-[14px]">chevron_right</span>
|
|
49
|
+
<span class="text-primary font-bold">{% translate "Export" %}</span>
|
|
50
|
+
</nav>
|
|
51
|
+
{% endblock %}
|
|
52
|
+
|
|
53
|
+
{% block content %}
|
|
54
|
+
<div class="space-y-5 max-w-3xl">
|
|
55
|
+
|
|
56
|
+
<!-- Header -->
|
|
57
|
+
<div>
|
|
58
|
+
<h2 class="text-xl font-black text-on-surface tracking-tight">
|
|
59
|
+
{% translate "Export" %} {{ opts.verbose_name_plural|capfirst }}
|
|
60
|
+
</h2>
|
|
61
|
+
<p class="text-xs text-on-surface-variant mt-1">
|
|
62
|
+
{% if sub_sheets %}
|
|
63
|
+
{% translate "XLSX → one file with multiple sheets · JSON → one nested JSON file" %}
|
|
64
|
+
{% else %}
|
|
65
|
+
{% translate "Select fields and format, then click Export." %}
|
|
66
|
+
{% endif %}
|
|
67
|
+
</p>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
{% if form.non_field_errors %}
|
|
71
|
+
<div class="p-3 bg-red-50 border border-red-200 text-red-700 rounded-lg text-xs font-semibold">
|
|
72
|
+
{{ form.non_field_errors }}
|
|
73
|
+
</div>
|
|
74
|
+
{% endif %}
|
|
75
|
+
|
|
76
|
+
<form action="{{ export_url }}" method="POST">
|
|
77
|
+
{% csrf_token %}
|
|
78
|
+
|
|
79
|
+
<!-- ── Primary resource: selectable checkboxes ── -->
|
|
80
|
+
<div class="border border-outline-variant bg-surface-lowest rounded-xl shadow-sm overflow-hidden mb-4">
|
|
81
|
+
<div class="px-5 py-3 bg-surface-low/40 border-b border-outline-variant flex items-center justify-between">
|
|
82
|
+
<div class="flex items-center gap-2 text-xs font-bold text-on-surface">
|
|
83
|
+
<span class="material-symbols-outlined text-[16px] text-primary">table_chart</span>
|
|
84
|
+
{% if primary_sheet_title %}{{ primary_sheet_title }}{% else %}{% translate "Fields to export" %}{% endif %}
|
|
85
|
+
<span class="text-[10px] font-normal text-on-surface-variant">— click to toggle</span>
|
|
86
|
+
</div>
|
|
87
|
+
<label class="flex items-center gap-1.5 text-xs font-semibold text-on-surface-variant cursor-pointer hover:text-on-surface">
|
|
88
|
+
<input type="checkbox" id="select-all-primary" class="rounded accent-primary" checked
|
|
89
|
+
onchange="toggleAll(this)">
|
|
90
|
+
{% translate "Select all" %}
|
|
91
|
+
</label>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<div class="p-5 flex flex-wrap gap-2" id="primary-fields">
|
|
95
|
+
{% for field in form.visible_fields %}
|
|
96
|
+
{% if field.field.is_selectable_field %}
|
|
97
|
+
<span class="field-chip">
|
|
98
|
+
{{ field }}
|
|
99
|
+
<label for="{{ field.id_for_label }}">
|
|
100
|
+
<span class="material-symbols-outlined text-[12px]">check</span>
|
|
101
|
+
{{ field.label }}
|
|
102
|
+
</label>
|
|
103
|
+
</span>
|
|
104
|
+
{% endif %}
|
|
105
|
+
{% endfor %}
|
|
106
|
+
{% if form.export_items.errors %}
|
|
107
|
+
<p class="w-full text-xs text-red-600 font-semibold mt-1">{{ form.export_items.errors }}</p>
|
|
108
|
+
{% endif %}
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<!-- ── Sub-sheets: always exported, shown as info ── -->
|
|
113
|
+
{% for sheet in sub_sheets %}
|
|
114
|
+
<div class="border border-outline-variant bg-surface-lowest rounded-xl shadow-sm overflow-hidden mb-4">
|
|
115
|
+
<div class="px-5 py-3 bg-surface-low/40 border-b border-outline-variant flex items-center gap-2">
|
|
116
|
+
<span class="material-symbols-outlined text-[16px] text-emerald-600">table_rows</span>
|
|
117
|
+
<span class="text-xs font-bold text-on-surface">{{ sheet.title }}</span>
|
|
118
|
+
<span class="ml-auto flex items-center gap-1 text-[10px] font-semibold text-emerald-700 bg-emerald-50 border border-emerald-200 px-2 py-0.5 rounded-full">
|
|
119
|
+
<span class="material-symbols-outlined text-[11px]">check_circle</span>
|
|
120
|
+
Always included in XLSX
|
|
121
|
+
</span>
|
|
122
|
+
</div>
|
|
123
|
+
<div class="p-5 flex flex-wrap gap-2">
|
|
124
|
+
{% for col in sheet.fields %}
|
|
125
|
+
<span class="info-chip">
|
|
126
|
+
<span class="material-symbols-outlined text-[12px]">drag_indicator</span>
|
|
127
|
+
{{ col|title }}
|
|
128
|
+
</span>
|
|
129
|
+
{% endfor %}
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
{% endfor %}
|
|
133
|
+
|
|
134
|
+
<!-- Hidden fields -->
|
|
135
|
+
{% for field in form.hidden_fields %}{{ field }}{% endfor %}
|
|
136
|
+
|
|
137
|
+
<!-- Format & non-selectable fields -->
|
|
138
|
+
<div class="border border-outline-variant bg-surface-lowest rounded-xl shadow-sm p-5 space-y-4 mb-5">
|
|
139
|
+
{% for field in form.visible_fields %}
|
|
140
|
+
{% if not field.field.is_selectable_field and not field.field.initial_field %}
|
|
141
|
+
<div class="flex flex-col gap-1">
|
|
142
|
+
<label class="text-xs font-bold text-on-surface" for="{{ field.id_for_label }}">{{ field.label }}</label>
|
|
143
|
+
<div class="[&_select]:max-w-xs [&_select]:w-full [&_select]:px-3 [&_select]:py-2
|
|
144
|
+
[&_select]:border [&_select]:border-outline-variant [&_select]:rounded-lg
|
|
145
|
+
[&_select]:bg-surface-low [&_select]:text-sm [&_select]:text-on-surface
|
|
146
|
+
[&_select]:focus:outline-none [&_select]:focus:ring-2 [&_select]:focus:ring-primary/30">
|
|
147
|
+
{{ field }}
|
|
148
|
+
</div>
|
|
149
|
+
{% if field.field.help_text %}<p class="text-[11px] text-on-surface-variant">{{ field.field.help_text|safe }}</p>{% endif %}
|
|
150
|
+
{{ field.errors }}
|
|
151
|
+
</div>
|
|
152
|
+
{% endif %}
|
|
153
|
+
{% endfor %}
|
|
154
|
+
{% if sub_sheets %}
|
|
155
|
+
<p class="text-[11px] text-on-surface-variant/70 border-t border-outline-variant/40 pt-3">
|
|
156
|
+
<span class="material-symbols-outlined text-[13px] align-middle text-primary">info</span>
|
|
157
|
+
<strong>xlsx</strong> → one file, one sheet per section ·
|
|
158
|
+
<strong>json</strong> → one <em>.json</em> file with all sections nested inside.
|
|
159
|
+
</p>
|
|
160
|
+
{% endif %}
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<!-- Submit -->
|
|
164
|
+
<div class="flex items-center gap-3">
|
|
165
|
+
<button type="submit"
|
|
166
|
+
class="flex items-center gap-2 px-5 py-2.5 bg-primary text-white text-xs font-bold rounded-lg shadow-md hover:bg-primary/90 transition-all active:scale-[0.98]">
|
|
167
|
+
<span class="material-symbols-outlined text-[16px]">download</span>
|
|
168
|
+
{% translate "Export" %}
|
|
169
|
+
</button>
|
|
170
|
+
<a href="{% url opts|admin_urlname:'changelist' %}"
|
|
171
|
+
class="text-xs text-on-surface-variant hover:text-on-surface transition-colors font-medium">
|
|
172
|
+
{% translate "Cancel" %}
|
|
173
|
+
</a>
|
|
174
|
+
</div>
|
|
175
|
+
</form>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<script>
|
|
179
|
+
function toggleAll(masterCb) {
|
|
180
|
+
document.querySelectorAll('#primary-fields .field-chip input[type="checkbox"]').forEach(cb => {
|
|
181
|
+
cb.checked = masterCb.checked;
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Sync select-all on individual change
|
|
186
|
+
document.addEventListener('DOMContentLoaded', function () {
|
|
187
|
+
// Ensure all primary checkboxes start checked
|
|
188
|
+
document.querySelectorAll('#primary-fields .field-chip input[type="checkbox"]').forEach(cb => {
|
|
189
|
+
cb.checked = true;
|
|
190
|
+
cb.addEventListener('change', syncMaster);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
function syncMaster() {
|
|
195
|
+
const all = [...document.querySelectorAll('#primary-fields .field-chip input[type="checkbox"]')];
|
|
196
|
+
document.getElementById('select-all-primary').checked = all.every(c => c.checked);
|
|
197
|
+
}
|
|
198
|
+
</script>
|
|
199
|
+
{% endblock %}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{% load i18n adminflow_tags %}
|
|
2
|
+
<fieldset class="space-y-4 {% if not is_inline %}border border-outline-variant bg-surface-lowest p-4 sm:p-6 rounded-xl shadow-sm{% endif %}">
|
|
3
|
+
{% if fieldset.name %}
|
|
4
|
+
{% if is_inline %}
|
|
5
|
+
<h4 class="text-xs font-bold text-on-surface uppercase tracking-wider mb-2 px-1">
|
|
6
|
+
{{ fieldset.name }}
|
|
7
|
+
</h4>
|
|
8
|
+
{% else %}
|
|
9
|
+
<legend class="text-xs font-black text-on-surface uppercase tracking-wider px-2 bg-surface-lowest">
|
|
10
|
+
{{ fieldset.name }}
|
|
11
|
+
</legend>
|
|
12
|
+
{% endif %}
|
|
13
|
+
{% endif %}
|
|
14
|
+
|
|
15
|
+
{% if fieldset.description %}
|
|
16
|
+
<p class="text-xs text-on-surface-variant italic mb-4">{{ fieldset.description|safe }}</p>
|
|
17
|
+
{% endif %}
|
|
18
|
+
|
|
19
|
+
<div class="space-y-4">
|
|
20
|
+
{% for line in fieldset %}
|
|
21
|
+
<div class="grid grid-cols-1 {% if line.fields|length > 1 %}md:grid-cols-{{ line.fields|length }}{% endif %} gap-4">
|
|
22
|
+
{% for field in line %}
|
|
23
|
+
<div class="space-y-1.5 {% if field.errors %}text-danger{% endif %}">
|
|
24
|
+
<!-- Label -->
|
|
25
|
+
<div class="flex items-center justify-between">
|
|
26
|
+
<label class="text-[10px] font-bold text-on-surface-variant uppercase tracking-wider" for="{{ field.field.id_for_label }}">
|
|
27
|
+
{{ field.field.label }}
|
|
28
|
+
{% if field.field.required %}<span class="text-danger">*</span>{% endif %}
|
|
29
|
+
</label>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<!-- Input Control -->
|
|
33
|
+
<div class="relative">
|
|
34
|
+
{% if field.is_readonly %}
|
|
35
|
+
<div class="px-3 py-2 bg-surface-low border border-outline-variant rounded-lg text-xs text-on-surface font-mono">
|
|
36
|
+
{{ field.contents }}
|
|
37
|
+
</div>
|
|
38
|
+
{% else %}
|
|
39
|
+
{% if field.field|is_checkbox %}
|
|
40
|
+
<div class="flex items-center gap-2 py-2">
|
|
41
|
+
{{ field.field|add_class:"w-4 h-4 rounded border-outline-variant text-primary focus:ring-primary cursor-pointer dark:bg-slate-800 dark:border-slate-700" }}
|
|
42
|
+
<span class="text-xs text-on-surface-variant font-medium select-none">{{ field.field.help_text|safe }}</span>
|
|
43
|
+
</div>
|
|
44
|
+
{% elif field.field|is_textarea %}
|
|
45
|
+
{{ field.field|add_class:"w-full px-3 py-2 rounded-lg border border-outline-variant bg-surface-lowest text-xs text-on-surface focus:ring-2 focus:ring-primary/10 focus:border-primary transition-all outline-none dark:bg-slate-900 dark:border-slate-800" }}
|
|
46
|
+
{% elif field.field|is_select %}
|
|
47
|
+
{{ field.field|add_class:"w-full pl-3 pr-8 py-2 bg-surface-lowest border border-outline-variant rounded-lg text-xs focus:ring-2 focus:ring-primary/10 focus:border-primary transition-all outline-none dark:bg-slate-900 dark:border-slate-800" }}
|
|
48
|
+
{% elif field.field|is_file %}
|
|
49
|
+
{{ field.field|add_class:"text-xs text-on-surface-variant file:mr-4 file:py-1.5 file:px-3 file:rounded-md file:border-0 file:text-xs file:font-semibold file:bg-primary/10 file:text-primary hover:file:bg-primary hover:file:text-white transition-all cursor-pointer" }}
|
|
50
|
+
{% else %}
|
|
51
|
+
{{ field.field|add_class:"w-full h-10 px-3 rounded-lg border border-outline-variant bg-surface-lowest text-xs text-on-surface focus:ring-2 focus:ring-primary/10 focus:border-primary transition-all outline-none dark:bg-slate-900 dark:border-slate-800" }}
|
|
52
|
+
{% endif %}
|
|
53
|
+
{% endif %}
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<!-- Field Errors -->
|
|
57
|
+
{% if field.errors %}
|
|
58
|
+
<p class="text-[11px] text-danger font-semibold flex items-center gap-1">
|
|
59
|
+
<span class="material-symbols-outlined text-sm">error</span>
|
|
60
|
+
{{ field.errors|striptags }}
|
|
61
|
+
</p>
|
|
62
|
+
{% endif %}
|
|
63
|
+
|
|
64
|
+
<!-- Help Text (Only for non-checkboxes where it is shown inline above) -->
|
|
65
|
+
{% if field.field.help_text and not field.field|is_checkbox %}
|
|
66
|
+
<p class="text-[10px] text-on-surface-variant opacity-80">{{ field.field.help_text|safe }}</p>
|
|
67
|
+
{% endif %}
|
|
68
|
+
</div>
|
|
69
|
+
{% endfor %}
|
|
70
|
+
</div>
|
|
71
|
+
{% endfor %}
|
|
72
|
+
</div>
|
|
73
|
+
</fieldset>
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
{% load i18n %}
|
|
2
|
+
<!-- AdminFlow Unified Reusable Floating Toast Notifications -->
|
|
3
|
+
{% if messages or form.errors or form.non_field_errors or errors %}
|
|
4
|
+
<div id="toast-container" class="fixed top-6 right-6 z-[9999] space-y-3 max-w-md w-full px-4 pointer-events-none">
|
|
5
|
+
{% if messages %}
|
|
6
|
+
{% for message in messages %}
|
|
7
|
+
<div class="toast-item pointer-events-auto relative overflow-hidden flex items-start gap-3 p-4 bg-white/95 dark:bg-slate-900/95 backdrop-blur-md rounded-2xl shadow-2xl border transition-all transform translate-y-0 opacity-100 duration-300 animate-slide-in {% if message.tags == 'warning' %}border-amber-400/80 text-amber-950 dark:text-amber-200 bg-amber-50/90{% elif message.tags == 'error' %}border-rose-400/80 text-rose-950 dark:text-rose-200 bg-rose-50/90{% else %}border-emerald-400/80 text-emerald-950 dark:text-emerald-200 bg-emerald-50/90{% endif %}" role="alert">
|
|
8
|
+
<div class="p-2 rounded-xl shrink-0 {% if message.tags == 'warning' %}bg-amber-500/20 text-amber-700 dark:text-amber-400{% elif message.tags == 'error' %}bg-rose-500/20 text-rose-700 dark:text-rose-400{% else %}bg-emerald-500/20 text-emerald-700 dark:text-emerald-400{% endif %}">
|
|
9
|
+
<span class="material-symbols-outlined text-[22px]">
|
|
10
|
+
{% if message.tags == 'warning' %}warning{% elif message.tags == 'error' %}error{% else %}check_circle{% endif %}
|
|
11
|
+
</span>
|
|
12
|
+
</div>
|
|
13
|
+
<div class="flex-1 pt-0.5">
|
|
14
|
+
<h4 class="text-[11px] font-bold uppercase tracking-wider mb-0.5 {% if message.tags == 'warning' %}text-amber-800{% elif message.tags == 'error' %}text-rose-800{% else %}text-emerald-800{% endif %}">
|
|
15
|
+
{% if message.tags == 'warning' %}Security Alert{% elif message.tags == 'error' %}Error Notification{% else %}Success Notification{% endif %}
|
|
16
|
+
</h4>
|
|
17
|
+
<p class="text-xs font-semibold leading-relaxed text-slate-800 dark:text-slate-100">{{ message }}</p>
|
|
18
|
+
</div>
|
|
19
|
+
<button type="button" onclick="dismissToast(this.closest('.toast-item'))" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 p-1 rounded-lg hover:bg-slate-200/50 dark:hover:bg-slate-800 transition-colors shrink-0 cursor-pointer">
|
|
20
|
+
<span class="material-symbols-outlined text-[18px]">close</span>
|
|
21
|
+
</button>
|
|
22
|
+
|
|
23
|
+
<!-- Progress Bar -->
|
|
24
|
+
<div class="absolute bottom-0 left-0 right-0 h-[3px] bg-slate-200/50 dark:bg-slate-800/50">
|
|
25
|
+
<div class="toast-progress h-full {% if message.tags == 'warning' %}bg-amber-500{% elif message.tags == 'error' %}bg-rose-500{% else %}bg-emerald-500{% endif %}"></div>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
{% endfor %}
|
|
29
|
+
{% endif %}
|
|
30
|
+
|
|
31
|
+
{% if form.non_field_errors %}
|
|
32
|
+
{% for error in form.non_field_errors %}
|
|
33
|
+
<div class="toast-item pointer-events-auto relative overflow-hidden flex items-start gap-3 p-4 bg-white/95 dark:bg-slate-900/95 backdrop-blur-md rounded-2xl shadow-2xl border border-rose-400/80 text-rose-950 dark:text-rose-200 bg-rose-50/90 transition-all transform translate-y-0 opacity-100 duration-300 animate-slide-in" role="alert">
|
|
34
|
+
<div class="p-2 rounded-xl shrink-0 bg-rose-500/20 text-rose-700 dark:text-rose-400">
|
|
35
|
+
<span class="material-symbols-outlined text-[22px]">error</span>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="flex-1 pt-0.5">
|
|
38
|
+
<h4 class="text-[11px] font-bold uppercase tracking-wider mb-0.5 text-rose-800">Form Error</h4>
|
|
39
|
+
<p class="text-xs font-semibold leading-relaxed text-slate-800 dark:text-slate-100">{{ error }}</p>
|
|
40
|
+
</div>
|
|
41
|
+
<button type="button" onclick="dismissToast(this.closest('.toast-item'))" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 p-1 rounded-lg hover:bg-slate-200/50 dark:hover:bg-slate-800 transition-colors shrink-0 cursor-pointer">
|
|
42
|
+
<span class="material-symbols-outlined text-[18px]">close</span>
|
|
43
|
+
</button>
|
|
44
|
+
<div class="absolute bottom-0 left-0 right-0 h-[3px] bg-slate-200/50 dark:bg-slate-800/50">
|
|
45
|
+
<div class="toast-progress h-full bg-rose-500"></div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
{% endfor %}
|
|
49
|
+
{% endif %}
|
|
50
|
+
|
|
51
|
+
{% if errors and not form.non_field_errors %}
|
|
52
|
+
<div class="toast-item pointer-events-auto relative overflow-hidden flex items-start gap-3 p-4 bg-white/95 dark:bg-slate-900/95 backdrop-blur-md rounded-2xl shadow-2xl border border-rose-400/80 text-rose-950 dark:text-rose-200 bg-rose-50/90 transition-all transform translate-y-0 opacity-100 duration-300 animate-slide-in" role="alert">
|
|
53
|
+
<div class="p-2 rounded-xl shrink-0 bg-rose-500/20 text-rose-700 dark:text-rose-400">
|
|
54
|
+
<span class="material-symbols-outlined text-[22px]">error</span>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="flex-1 pt-0.5">
|
|
57
|
+
<h4 class="text-[11px] font-bold uppercase tracking-wider mb-0.5 text-rose-800">Validation Error</h4>
|
|
58
|
+
<p class="text-xs font-semibold leading-relaxed text-slate-800 dark:text-slate-100">
|
|
59
|
+
{% blocktranslate count counter=errors|length %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktranslate %}
|
|
60
|
+
</p>
|
|
61
|
+
</div>
|
|
62
|
+
<button type="button" onclick="dismissToast(this.closest('.toast-item'))" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 p-1 rounded-lg hover:bg-slate-200/50 dark:hover:bg-slate-800 transition-colors shrink-0 cursor-pointer">
|
|
63
|
+
<span class="material-symbols-outlined text-[18px]">close</span>
|
|
64
|
+
</button>
|
|
65
|
+
<div class="absolute bottom-0 left-0 right-0 h-[3px] bg-slate-200/50 dark:bg-slate-800/50">
|
|
66
|
+
<div class="toast-progress h-full bg-rose-500"></div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
{% endif %}
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<style>
|
|
73
|
+
@keyframes slideInRight {
|
|
74
|
+
from { transform: translateX(120%); opacity: 0; }
|
|
75
|
+
to { transform: translateX(0); opacity: 1; }
|
|
76
|
+
}
|
|
77
|
+
@keyframes toastProgress {
|
|
78
|
+
from { width: 100%; }
|
|
79
|
+
to { width: 0%; }
|
|
80
|
+
}
|
|
81
|
+
.animate-slide-in {
|
|
82
|
+
animation: slideInRight 0.35s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
|
83
|
+
}
|
|
84
|
+
.toast-progress {
|
|
85
|
+
animation: toastProgress 7s linear forwards;
|
|
86
|
+
}
|
|
87
|
+
</style>
|
|
88
|
+
|
|
89
|
+
<script>
|
|
90
|
+
function dismissToast(el) {
|
|
91
|
+
if (!el) return;
|
|
92
|
+
el.style.transform = 'translateX(120%)';
|
|
93
|
+
el.style.opacity = '0';
|
|
94
|
+
setTimeout(() => el.remove(), 300);
|
|
95
|
+
}
|
|
96
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
97
|
+
setTimeout(function() {
|
|
98
|
+
document.querySelectorAll('.toast-item').forEach(dismissToast);
|
|
99
|
+
}, 7000);
|
|
100
|
+
});
|
|
101
|
+
</script>
|
|
102
|
+
{% endif %}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
{% extends "admin/base_site.html" %}
|
|
2
|
+
{% load i18n static adminflow_tags %}
|
|
3
|
+
|
|
4
|
+
{% block bodyclass %}dashboard-page{% endblock %}
|
|
5
|
+
|
|
6
|
+
{% block content %}
|
|
7
|
+
<div class="space-y-6 max-w-[1600px] mx-auto w-full">
|
|
8
|
+
<!-- Welcome Header -->
|
|
9
|
+
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
|
10
|
+
<div>
|
|
11
|
+
<h2 class="text-2xl md:text-3xl font-extrabold text-on-surface tracking-tight">Good day, {{ user.get_username|capfirst }}</h2>
|
|
12
|
+
<p class="text-xs text-on-surface-variant mt-1">Here is a quick overview of your admin console.</p>
|
|
13
|
+
</div>
|
|
14
|
+
<div class="flex items-center gap-2">
|
|
15
|
+
<button onclick="openCommandPalette()" class="flex items-center gap-2 px-3 py-2 rounded-lg border border-outline-variant text-primary font-bold text-xs hover:bg-surface-low transition-all bg-surface-lowest shadow-sm">
|
|
16
|
+
<span class="material-symbols-outlined text-[16px]">search</span>
|
|
17
|
+
{% translate "Global Search" %}
|
|
18
|
+
</button>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<!-- Section 1: KPI Stats Cards (2x2 on mobile, 4 cols on desktop) -->
|
|
23
|
+
<div class="grid grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4">
|
|
24
|
+
<!-- Users Card -->
|
|
25
|
+
{% get_user_stats as user_stats %}
|
|
26
|
+
<div class="glass-card p-5 rounded-xl shadow-sm hover:shadow-md transition-shadow" title="System Users — Active: {{ user_stats.active }}, Inactive: {{ user_stats.inactive }}, Staff: {{ user_stats.staff }}, Super Admins: {{ user_stats.superuser }}, Total: {{ user_stats.total }}">
|
|
27
|
+
<div class="flex justify-between items-start">
|
|
28
|
+
<div class="p-2 bg-primary/10 rounded-lg text-primary">
|
|
29
|
+
<span class="material-symbols-outlined">group</span>
|
|
30
|
+
</div>
|
|
31
|
+
<span class="text-xs text-primary font-bold">
|
|
32
|
+
{{ user_stats.total }} Total
|
|
33
|
+
</span>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="mt-4">
|
|
36
|
+
<p class="text-[10px] text-on-surface-variant font-bold uppercase tracking-wider">{% translate "System Users" %}</p>
|
|
37
|
+
<div class="flex items-center gap-3 mt-2 pt-2 border-t border-outline-variant/40">
|
|
38
|
+
<div class="flex items-center gap-1" title="Active Users">
|
|
39
|
+
<span class="material-symbols-outlined text-[14px] text-success">check_circle</span>
|
|
40
|
+
<span class="text-sm font-bold text-success">{{ user_stats.active }}</span>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="flex items-center gap-1" title="Inactive Users">
|
|
43
|
+
<span class="material-symbols-outlined text-[14px] text-danger">cancel</span>
|
|
44
|
+
<span class="text-sm font-bold text-danger">{{ user_stats.inactive }}</span>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="flex items-center gap-1" title="Staff Users">
|
|
47
|
+
<span class="material-symbols-outlined text-[14px] text-primary">badge</span>
|
|
48
|
+
<span class="text-sm font-bold text-primary">{{ user_stats.staff }}</span>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="flex items-center gap-1" title="Super Admins">
|
|
51
|
+
<span class="material-symbols-outlined text-[14px] text-warning">shield_person</span>
|
|
52
|
+
<span class="text-sm font-bold text-warning">{{ user_stats.superuser }}</span>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<!-- Database Info -->
|
|
59
|
+
{% get_system_stats as system_stats %}
|
|
60
|
+
<div class="glass-card p-5 rounded-xl shadow-sm hover:shadow-md transition-shadow" title="Database Engine: {{ system_stats.db_engine }} — File: {{ system_stats.db_name }}">
|
|
61
|
+
<div class="flex justify-between items-start">
|
|
62
|
+
<div class="p-2 bg-success/10 rounded-lg text-success">
|
|
63
|
+
<span class="material-symbols-outlined">database</span>
|
|
64
|
+
</div>
|
|
65
|
+
<span class="text-xs text-success font-bold flex items-center gap-1">
|
|
66
|
+
<span class="material-symbols-outlined text-[12px]">check_circle</span> Connected
|
|
67
|
+
</span>
|
|
68
|
+
</div>
|
|
69
|
+
<div class="mt-4">
|
|
70
|
+
<p class="text-[10px] text-on-surface-variant font-bold uppercase tracking-wider">{% translate "Database Engine" %}</p>
|
|
71
|
+
<h3 class="text-xl md:text-2xl font-black text-on-surface mt-1">{{ system_stats.db_engine }}</h3>
|
|
72
|
+
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<!-- Framework & Runtime -->
|
|
77
|
+
<div class="glass-card p-5 rounded-xl shadow-sm hover:shadow-md transition-shadow" title="Django v{{ system_stats.django_version }} running on Python {{ system_stats.python_version }}">
|
|
78
|
+
<div class="flex justify-between items-start">
|
|
79
|
+
<div class="p-2 bg-warning/10 rounded-lg text-warning">
|
|
80
|
+
<span class="material-symbols-outlined">code</span>
|
|
81
|
+
</div>
|
|
82
|
+
<span class="text-xs text-primary font-bold">
|
|
83
|
+
Python {{ system_stats.python_version }}
|
|
84
|
+
</span>
|
|
85
|
+
</div>
|
|
86
|
+
<div class="mt-4">
|
|
87
|
+
<p class="text-[10px] text-on-surface-variant font-bold uppercase tracking-wider">{% translate "Django Version" %}</p>
|
|
88
|
+
<h3 class="text-xl md:text-2xl font-black text-on-surface mt-1">v{{ system_stats.django_version }}</h3>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<!-- Admin Registry -->
|
|
93
|
+
<div class="glass-card p-5 rounded-xl shadow-sm hover:shadow-md transition-shadow" title="Registered Models ({{ system_stats.model_count }}): {{ system_stats.model_names }}">
|
|
94
|
+
<div class="flex justify-between items-start">
|
|
95
|
+
<div class="p-2 bg-info/10 rounded-lg text-info">
|
|
96
|
+
<span class="material-symbols-outlined">widgets</span>
|
|
97
|
+
</div>
|
|
98
|
+
<span class="text-xs text-info font-bold flex items-center gap-1">
|
|
99
|
+
{{ system_stats.app_count }} {% translate "Apps" %}
|
|
100
|
+
</span>
|
|
101
|
+
</div>
|
|
102
|
+
<div class="mt-4">
|
|
103
|
+
<p class="text-[10px] text-on-surface-variant font-bold uppercase tracking-wider">{% translate "Registered Models" %}</p>
|
|
104
|
+
<h3 class="text-xl md:text-2xl font-black text-on-surface mt-1">{{ system_stats.model_count }}</h3>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<!-- Section 2: Apps Grid & Recent Actions Split Layout -->
|
|
110
|
+
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
111
|
+
<!-- Left: App Index Card Grid -->
|
|
112
|
+
<div class="lg:col-span-2 space-y-6">
|
|
113
|
+
{% get_admin_app_list as apps %}
|
|
114
|
+
{% for app in apps %}
|
|
115
|
+
<div class="border border-outline-variant bg-surface-lowest rounded-xl shadow-sm overflow-hidden">
|
|
116
|
+
<!-- App header -->
|
|
117
|
+
<div class="px-5 py-4 bg-surface-low border-b border-outline-variant flex justify-between items-center">
|
|
118
|
+
<h3 class="text-sm font-bold text-on-surface tracking-tight">{{ app.name }}</h3>
|
|
119
|
+
<a href="{{ app.app_url }}" class="text-xs font-semibold text-primary hover:underline" title="{% blocktranslate with name=app.name %}View all models in {{ name }}{% endblocktranslate %}">
|
|
120
|
+
{% translate "Manage app" %} →
|
|
121
|
+
</a>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<!-- Models list -->
|
|
125
|
+
<div class="divide-y divide-outline-variant">
|
|
126
|
+
{% for model in app.models %}
|
|
127
|
+
<div class="px-5 py-3 flex items-center justify-between hover:bg-surface-low/30 transition-colors">
|
|
128
|
+
<div class="flex items-center gap-3">
|
|
129
|
+
<span class="material-symbols-outlined text-primary text-lg">{{ model|adminflow_icon }}</span>
|
|
130
|
+
<div>
|
|
131
|
+
<a href="{{ model.admin_url }}" class="text-xs font-bold text-on-surface hover:text-primary transition-colors">
|
|
132
|
+
{{ model.name }}
|
|
133
|
+
</a>
|
|
134
|
+
<p class="text-[10px] text-on-surface-variant">Class: {{ model.object_name }}</p>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<div class="flex items-center gap-2">
|
|
139
|
+
{% if model.add_url %}
|
|
140
|
+
<a href="{{ model.add_url }}" class="flex items-center gap-1 px-2 py-1 rounded bg-primary/10 text-primary hover:bg-primary hover:text-white transition-all text-[11px] font-bold">
|
|
141
|
+
<span class="material-symbols-outlined text-xs">add</span>
|
|
142
|
+
{% translate "Add" %}
|
|
143
|
+
</a>
|
|
144
|
+
{% endif %}
|
|
145
|
+
|
|
146
|
+
{% if model.admin_url %}
|
|
147
|
+
<a href="{{ model.admin_url }}" class="flex items-center gap-1 px-2 py-1 rounded border border-outline-variant text-on-surface-variant hover:bg-surface-low transition-all text-[11px] font-bold bg-surface-lowest">
|
|
148
|
+
<span class="material-symbols-outlined text-xs">visibility</span>
|
|
149
|
+
{% translate "View" %}
|
|
150
|
+
</a>
|
|
151
|
+
{% endif %}
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
{% endfor %}
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
{% empty %}
|
|
158
|
+
<div class="p-8 text-center bg-surface-lowest rounded-xl border border-outline-variant space-y-3">
|
|
159
|
+
<span class="material-symbols-outlined text-on-surface-variant text-5xl opacity-40">inbox</span>
|
|
160
|
+
<p class="text-xs text-on-surface-variant">{% translate "You don’t have permission to view or edit anything." %}</p>
|
|
161
|
+
</div>
|
|
162
|
+
{% endfor %}
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<!-- Right: Recent Actions Timeline -->
|
|
166
|
+
<div class="border border-outline-variant bg-surface-lowest rounded-xl shadow-sm overflow-hidden h-fit">
|
|
167
|
+
<div class="px-5 py-4 bg-surface-low border-b border-outline-variant">
|
|
168
|
+
<h3 class="text-sm font-bold text-on-surface">{% translate "Recent Activity" %}</h3>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<div class="p-5 max-h-[500px] overflow-y-auto custom-scrollbar">
|
|
172
|
+
{% load log %}
|
|
173
|
+
{% get_admin_log 10 as admin_log for_user user %}
|
|
174
|
+
|
|
175
|
+
{% if not admin_log %}
|
|
176
|
+
<p class="text-xs text-on-surface-variant text-center py-6">{% translate "No recent activity found." %}</p>
|
|
177
|
+
{% else %}
|
|
178
|
+
<ul class="space-y-4 relative before:absolute before:top-2 before:bottom-2 before:left-[17px] before:w-0.5 before:bg-outline-variant">
|
|
179
|
+
{% for entry in admin_log %}
|
|
180
|
+
<li class="flex items-start gap-3 relative z-10">
|
|
181
|
+
<!-- Icon indicator based on action flag -->
|
|
182
|
+
<div class="w-9 h-9 rounded-full flex items-center justify-center border shadow-sm shrink-0
|
|
183
|
+
{% if entry.is_addition %}bg-green-50 border-green-200 text-green-600 dark:bg-green-950/20 dark:border-green-800/30{% elif entry.is_change %}bg-amber-50 border-amber-200 text-amber-600 dark:bg-amber-950/20 dark:border-amber-800/30{% else %}bg-rose-50 border-rose-200 text-rose-600 dark:bg-rose-950/20 dark:border-rose-800/30{% endif %}">
|
|
184
|
+
<span class="material-symbols-outlined text-sm">
|
|
185
|
+
{% if entry.is_addition %}add{% elif entry.is_change %}edit{% else %}delete{% endif %}
|
|
186
|
+
</span>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<!-- Content -->
|
|
190
|
+
<div class="flex-1 space-y-1">
|
|
191
|
+
<div class="flex justify-between items-center gap-2">
|
|
192
|
+
<span class="text-xs font-bold text-on-surface break-all">
|
|
193
|
+
{% if entry.is_deletion or not entry.get_admin_url %}
|
|
194
|
+
{{ entry.object_repr }}
|
|
195
|
+
{% else %}
|
|
196
|
+
<a href="{{ entry.get_admin_url }}" class="hover:text-primary transition-colors">{{ entry.object_repr }}</a>
|
|
197
|
+
{% endif %}
|
|
198
|
+
</span>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
<p class="text-[10px] text-on-surface-variant leading-relaxed">
|
|
202
|
+
{% if entry.content_type %}
|
|
203
|
+
<span class="font-semibold">{{ entry.content_type.name|capfirst }}</span>
|
|
204
|
+
{% else %}
|
|
205
|
+
{% translate "Unknown content" %}
|
|
206
|
+
{% endif %}
|
|
207
|
+
·
|
|
208
|
+
<span class="font-mono text-[9px] bg-surface-low px-1.5 py-0.5 rounded border border-outline-variant">
|
|
209
|
+
{% if entry.is_addition %}{% translate "Added" %}{% elif entry.is_change %}{% translate "Modified" %}{% else %}{% translate "Deleted" %}{% endif %}
|
|
210
|
+
</span>
|
|
211
|
+
</p>
|
|
212
|
+
|
|
213
|
+
{% if entry.change_message %}
|
|
214
|
+
<p class="text-[10px] italic text-on-surface-variant bg-surface-low/50 p-1.5 rounded border border-dashed border-outline-variant">
|
|
215
|
+
{{ entry.change_message }}
|
|
216
|
+
</p>
|
|
217
|
+
{% endif %}
|
|
218
|
+
</div>
|
|
219
|
+
</li>
|
|
220
|
+
{% endfor %}
|
|
221
|
+
</ul>
|
|
222
|
+
{% endif %}
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
{% endblock %}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{% extends "admin/base_site.html" %}
|
|
2
|
+
{% load i18n static %}
|
|
3
|
+
|
|
4
|
+
{% block bodyclass %}bg-background h-full flex items-center justify-center p-4 md:p-8{% endblock %}
|
|
5
|
+
|
|
6
|
+
{% block content %}
|
|
7
|
+
<main class="w-full max-w-md flex flex-col overflow-hidden rounded-2xl shadow-xl bg-surface-lowest border border-outline-variant mx-auto my-[10vh] text-center p-8 md:p-12 space-y-6">
|
|
8
|
+
<!-- Icon Header -->
|
|
9
|
+
<div class="flex justify-center">
|
|
10
|
+
<div class="h-16 w-16 rounded-2xl bg-primary/10 border border-primary/20 flex items-center justify-center text-primary shadow-inner">
|
|
11
|
+
<span class="material-symbols-outlined text-3xl">logout</span>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<!-- Title & Subtitle -->
|
|
16
|
+
<div class="space-y-2">
|
|
17
|
+
<h1 class="text-2xl font-extrabold text-on-surface tracking-tight">{% translate "Logged Out" %}</h1>
|
|
18
|
+
<p class="text-xs text-on-surface-variant leading-relaxed max-w-xs mx-auto">
|
|
19
|
+
{% translate "Thanks for spending some quality time with the web site today." %}
|
|
20
|
+
</p>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<!-- Actions -->
|
|
24
|
+
<div class="pt-4 space-y-3">
|
|
25
|
+
<a href="{% url 'admin:login' %}" class="w-full h-11 bg-primary text-white rounded-xl text-xs font-bold hover:bg-primary/95 active:scale-[0.99] transition-all shadow-md flex items-center justify-center gap-2">
|
|
26
|
+
<span class="material-symbols-outlined text-[18px]">login</span>
|
|
27
|
+
<span>{% translate "Log in again" %}</span>
|
|
28
|
+
</a>
|
|
29
|
+
|
|
30
|
+
{% if site_url %}
|
|
31
|
+
<a href="{{ site_url }}" class="w-full h-10 border border-outline-variant bg-surface hover:bg-surface-low text-on-surface-variant rounded-xl text-xs font-semibold transition-all flex items-center justify-center gap-2">
|
|
32
|
+
<span class="material-symbols-outlined text-[16px]">home</span>
|
|
33
|
+
<span>{% translate "Return to site" %}</span>
|
|
34
|
+
</a>
|
|
35
|
+
{% endif %}
|
|
36
|
+
</div>
|
|
37
|
+
</main>
|
|
38
|
+
{% endblock %}
|