millas 0.1.9 β 0.2.0
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.
- package/package.json +1 -1
- package/src/admin/Admin.js +36 -11
- package/src/admin/views/layouts/base.njk +867 -246
- package/src/admin/views/pages/dashboard.njk +32 -26
- package/src/admin/views/pages/form.njk +228 -111
- package/src/admin/views/pages/list.njk +226 -50
|
@@ -1,61 +1,66 @@
|
|
|
1
1
|
{% extends "layouts/base.njk" %}
|
|
2
|
-
|
|
3
2
|
{% block title %}Dashboard{% endblock %}
|
|
4
3
|
|
|
5
4
|
{% block content %}
|
|
6
5
|
<div class="breadcrumb">
|
|
7
|
-
<span
|
|
6
|
+
<span class="icon icon-13" style="color:var(--text-xmuted)"><svg viewBox="0 0 24 24"><use href="#ic-grid"/></svg></span>
|
|
8
7
|
<span class="breadcrumb-sep">βΊ</span>
|
|
9
|
-
<span>Dashboard</span>
|
|
8
|
+
<span class="breadcrumb-current">Dashboard</span>
|
|
10
9
|
</div>
|
|
11
10
|
|
|
12
11
|
{# ββ Stat cards ββ #}
|
|
12
|
+
{% if resources | length %}
|
|
13
13
|
<div class="stats-grid">
|
|
14
14
|
{% for resource in resources %}
|
|
15
|
-
<a href="{{ adminPrefix }}/{{ resource.slug }}" style="
|
|
16
|
-
<div class="stat-
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
<div class="stat-sub">Total records</div>
|
|
15
|
+
<a href="{{ adminPrefix }}/{{ resource.slug }}" class="stat-card" style="cursor:pointer">
|
|
16
|
+
<div class="stat-icon-wrap">
|
|
17
|
+
<span class="icon icon-18">
|
|
18
|
+
<svg viewBox="0 0 24 24"><use href="#ic-table"/></svg>
|
|
19
|
+
</span>
|
|
21
20
|
</div>
|
|
21
|
+
<div class="stat-label">{{ resource.label }}</div>
|
|
22
|
+
<div class="stat-value">{{ resource.count if resource.count is defined else 'β' }}</div>
|
|
23
|
+
<div class="stat-sub">Total records</div>
|
|
22
24
|
</a>
|
|
23
|
-
{% else %}
|
|
24
|
-
<div class="stat-card">
|
|
25
|
-
<div class="stat-icon">π</div>
|
|
26
|
-
<div class="stat-label">Resources</div>
|
|
27
|
-
<div class="stat-value">0</div>
|
|
28
|
-
<div class="stat-sub">None registered yet</div>
|
|
29
|
-
</div>
|
|
30
25
|
{% endfor %}
|
|
31
26
|
</div>
|
|
27
|
+
{% endif %}
|
|
32
28
|
|
|
33
|
-
{# ββ
|
|
29
|
+
{# ββ No resources welcome state ββ #}
|
|
34
30
|
{% if not resources | length %}
|
|
35
31
|
<div class="card">
|
|
36
32
|
<div class="card-body">
|
|
37
33
|
<div class="empty-state">
|
|
38
|
-
<div class="empty-icon"
|
|
34
|
+
<div class="empty-icon">
|
|
35
|
+
<span class="icon icon-24"><svg viewBox="0 0 24 24"><use href="#ic-database"/></svg></span>
|
|
36
|
+
</div>
|
|
39
37
|
<div class="empty-title">Welcome to Millas Admin</div>
|
|
40
38
|
<div class="empty-desc">
|
|
41
39
|
Register your models to start managing your data.
|
|
42
|
-
Add this to your <code>AppServiceProvider.boot()</code>:
|
|
40
|
+
Add this to your <code style="font-family:'DM Mono',monospace;background:var(--surface3);padding:1px 5px;border-radius:3px;font-size:12px">AppServiceProvider.boot()</code>:
|
|
43
41
|
</div>
|
|
44
|
-
<pre style="background:var(--surface2);border:1px solid var(--border);border-radius:
|
|
45
|
-
const { Admin, AdminResource } = resolveMillas();
|
|
42
|
+
<pre style="background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius);padding:16px;text-align:left;font-family:'DM Mono',monospace;font-size:12px;margin:0 auto;max-width:480px;overflow-x:auto;color:var(--text-soft)">const { Admin } = resolveMillas();
|
|
46
43
|
Admin.register(UserResource);
|
|
47
|
-
Admin.mount(
|
|
44
|
+
Admin.mount(expressApp);</pre>
|
|
48
45
|
</div>
|
|
49
46
|
</div>
|
|
50
47
|
</div>
|
|
48
|
+
|
|
51
49
|
{% else %}
|
|
52
|
-
|
|
50
|
+
|
|
51
|
+
{# ββ Recent records per resource ββ #}
|
|
53
52
|
{% for resource in resources %}
|
|
54
53
|
{% if resource.recent | length %}
|
|
55
|
-
<div class="card mb-
|
|
54
|
+
<div class="card mb-5">
|
|
56
55
|
<div class="card-header">
|
|
57
|
-
<span class="card-title">
|
|
58
|
-
|
|
56
|
+
<span class="card-title">
|
|
57
|
+
<span class="icon icon-15" style="color:var(--primary)"><svg viewBox="0 0 24 24"><use href="#ic-table"/></svg></span>
|
|
58
|
+
Recent {{ resource.label }}
|
|
59
|
+
</span>
|
|
60
|
+
<a href="{{ adminPrefix }}/{{ resource.slug }}" class="btn btn-ghost btn-sm">
|
|
61
|
+
View all
|
|
62
|
+
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-chevron-right"/></svg></span>
|
|
63
|
+
</a>
|
|
59
64
|
</div>
|
|
60
65
|
<div class="table-wrap">
|
|
61
66
|
<table>
|
|
@@ -80,5 +85,6 @@ Admin.mount(Route, expressApp);</pre>
|
|
|
80
85
|
</div>
|
|
81
86
|
{% endif %}
|
|
82
87
|
{% endfor %}
|
|
88
|
+
|
|
83
89
|
{% endif %}
|
|
84
90
|
{% endblock %}
|
|
@@ -1,145 +1,262 @@
|
|
|
1
1
|
{% extends "layouts/base.njk" %}
|
|
2
2
|
|
|
3
3
|
{% block title %}{{ 'Edit' if isEdit else 'New' }} {{ resource.singular }}{% endblock %}
|
|
4
|
-
{% block topbar_title %}
|
|
4
|
+
{% block topbar_title %}
|
|
5
|
+
<span class="icon icon-16" style="color:var(--text-muted)">
|
|
6
|
+
<svg viewBox="0 0 24 24"><use href="#ic-{{ 'edit' if isEdit else 'plus' }}"/></svg>
|
|
7
|
+
</span>
|
|
8
|
+
{{ 'Edit' if isEdit else 'New' }} {{ resource.singular }}
|
|
9
|
+
{% endblock %}
|
|
10
|
+
|
|
11
|
+
{% block topbar_actions %}
|
|
12
|
+
<a href="{{ adminPrefix }}/{{ resource.slug }}" class="btn btn-ghost btn-sm">
|
|
13
|
+
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-arrow-left"/></svg></span>
|
|
14
|
+
Back to {{ resource.label }}
|
|
15
|
+
</a>
|
|
16
|
+
{% endblock %}
|
|
5
17
|
|
|
6
18
|
{% block content %}
|
|
7
19
|
<div class="breadcrumb">
|
|
8
|
-
<a href="{{ adminPrefix }}/">
|
|
20
|
+
<a href="{{ adminPrefix }}/">
|
|
21
|
+
<span class="icon icon-12"><svg viewBox="0 0 24 24"><use href="#ic-grid"/></svg></span>
|
|
22
|
+
</a>
|
|
9
23
|
<span class="breadcrumb-sep">βΊ</span>
|
|
10
24
|
<a href="{{ adminPrefix }}/{{ resource.slug }}">{{ resource.label }}</a>
|
|
11
25
|
<span class="breadcrumb-sep">βΊ</span>
|
|
12
|
-
<span>{{ 'Edit #' + record.id if isEdit else 'New' }}</span>
|
|
26
|
+
<span class="breadcrumb-current">{{ 'Edit #' + record.id if isEdit else 'New ' + resource.singular }}</span>
|
|
13
27
|
</div>
|
|
14
28
|
|
|
15
|
-
<div
|
|
16
|
-
<div class="card
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
29
|
+
<div style="max-width:760px">
|
|
30
|
+
<div class="card">
|
|
31
|
+
<div class="card-header">
|
|
32
|
+
<span class="card-title">
|
|
33
|
+
<span class="icon icon-15" style="color:var(--primary)">
|
|
34
|
+
<svg viewBox="0 0 24 24"><use href="#ic-{{ 'edit' if isEdit else 'file' }}"/></svg>
|
|
35
|
+
</span>
|
|
36
|
+
{{ ('Edit ' + resource.singular + ' #' + record.id) if isEdit else ('Create ' + resource.singular) }}
|
|
37
|
+
</span>
|
|
21
38
|
{% if isEdit %}
|
|
22
|
-
<
|
|
39
|
+
<span class="badge badge-gray text-xs">ID: {{ record.id }}</span>
|
|
23
40
|
{% endif %}
|
|
41
|
+
</div>
|
|
24
42
|
|
|
43
|
+
<div class="card-body">
|
|
44
|
+
|
|
45
|
+
{# ββ Validation errors summary ββ #}
|
|
25
46
|
{% if errors | length %}
|
|
26
|
-
<div class="alert alert-error
|
|
47
|
+
<div class="alert alert-error mb-5">
|
|
48
|
+
<span class="icon icon-16"><svg viewBox="0 0 24 24"><use href="#ic-alert-circle"/></svg></span>
|
|
27
49
|
<div>
|
|
28
50
|
<strong>Please fix the following errors:</strong>
|
|
29
|
-
<ul style="margin-top:
|
|
51
|
+
<ul style="margin-top:5px;padding-left:14px;list-style:disc">
|
|
30
52
|
{% for field, msgs in errors %}
|
|
31
|
-
<li>{{ msgs[0] }}</li>
|
|
53
|
+
<li style="font-size:12.5px;margin-top:2px">{{ msgs[0] if msgs is iterable and msgs[0] is string else msgs }}</li>
|
|
32
54
|
{% endfor %}
|
|
33
55
|
</ul>
|
|
34
56
|
</div>
|
|
35
57
|
</div>
|
|
36
58
|
{% endif %}
|
|
37
59
|
|
|
38
|
-
<
|
|
39
|
-
{%
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
{%
|
|
53
|
-
|
|
54
|
-
{
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
60
|
+
<form method="POST" action="{{ formAction }}" id="record-form" novalidate>
|
|
61
|
+
{% if isEdit %}
|
|
62
|
+
<input type="hidden" name="_method" value="PUT">
|
|
63
|
+
{% endif %}
|
|
64
|
+
|
|
65
|
+
<div class="form-grid">
|
|
66
|
+
{% for field in formFields %}
|
|
67
|
+
<div class="form-group {% if field.type == 'textarea' or field.type == 'json' %}full{% endif %}">
|
|
68
|
+
<label class="form-label" for="field-{{ field.name }}">
|
|
69
|
+
{{ field.label }}
|
|
70
|
+
{% if field.required %}<span class="required">*</span>{% endif %}
|
|
71
|
+
</label>
|
|
72
|
+
|
|
73
|
+
{% set val = record[field.name] if record[field.name] is defined else '' %}
|
|
74
|
+
{% set hasError = errors[field.name] is defined %}
|
|
75
|
+
|
|
76
|
+
{# ββ Select ββ #}
|
|
77
|
+
{% if field.type == 'select' and field.options %}
|
|
78
|
+
<select
|
|
79
|
+
id="field-{{ field.name }}"
|
|
80
|
+
name="{{ field.name }}"
|
|
81
|
+
class="form-control{% if hasError %} error{% endif %}">
|
|
82
|
+
<option value="">β Select β</option>
|
|
83
|
+
{% for opt in field.options %}
|
|
84
|
+
<option value="{{ opt }}" {% if val == opt %}selected{% endif %}>{{ opt }}</option>
|
|
85
|
+
{% endfor %}
|
|
86
|
+
</select>
|
|
87
|
+
|
|
88
|
+
{# ββ Boolean ββ #}
|
|
89
|
+
{% elif field.type == 'boolean' %}
|
|
90
|
+
<div class="check-group" style="padding:8px 0">
|
|
91
|
+
<input
|
|
92
|
+
type="checkbox"
|
|
93
|
+
id="field-{{ field.name }}"
|
|
94
|
+
name="{{ field.name }}"
|
|
95
|
+
class="check-input"
|
|
96
|
+
value="1"
|
|
97
|
+
{% if val %}checked{% endif %}>
|
|
98
|
+
<label class="check-label" for="field-{{ field.name }}">
|
|
99
|
+
{{ field.label }}
|
|
100
|
+
</label>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
{# ββ Textarea ββ #}
|
|
104
|
+
{% elif field.type == 'textarea' %}
|
|
105
|
+
<textarea
|
|
106
|
+
id="field-{{ field.name }}"
|
|
107
|
+
name="{{ field.name }}"
|
|
108
|
+
class="form-control{% if hasError %} error{% endif %}"
|
|
109
|
+
placeholder="{{ field.placeholder or '' }}"
|
|
110
|
+
rows="4">{{ val }}</textarea>
|
|
111
|
+
|
|
112
|
+
{# ββ JSON ββ #}
|
|
113
|
+
{% elif field.type == 'json' %}
|
|
114
|
+
<textarea
|
|
115
|
+
id="field-{{ field.name }}"
|
|
116
|
+
name="{{ field.name }}"
|
|
117
|
+
class="form-control{% if hasError %} error{% endif %}"
|
|
118
|
+
placeholder='{"key": "value"}'
|
|
119
|
+
style="font-family:'DM Mono',monospace;font-size:12px"
|
|
120
|
+
rows="5">{{ val | dump if val else '' }}</textarea>
|
|
121
|
+
|
|
122
|
+
{# ββ Password ββ #}
|
|
123
|
+
{% elif field.type == 'password' %}
|
|
124
|
+
<div style="position:relative">
|
|
125
|
+
<input
|
|
126
|
+
type="password"
|
|
127
|
+
id="field-{{ field.name }}"
|
|
128
|
+
name="{{ field.name }}"
|
|
129
|
+
class="form-control{% if hasError %} error{% endif %}"
|
|
130
|
+
placeholder="{{ 'Leave blank to keep current' if isEdit else '' }}"
|
|
131
|
+
style="padding-right:40px">
|
|
132
|
+
<button type="button"
|
|
133
|
+
onclick="togglePassword(this)"
|
|
134
|
+
style="position:absolute;right:10px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;color:var(--text-muted);display:flex;align-items:center">
|
|
135
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
136
|
+
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>
|
|
137
|
+
</svg>
|
|
138
|
+
</button>
|
|
139
|
+
</div>
|
|
116
140
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
141
|
+
{# ββ Email ββ #}
|
|
142
|
+
{% elif field.type == 'email' %}
|
|
143
|
+
<input
|
|
144
|
+
type="email"
|
|
145
|
+
id="field-{{ field.name }}"
|
|
146
|
+
name="{{ field.name }}"
|
|
147
|
+
value="{{ val }}"
|
|
148
|
+
class="form-control{% if hasError %} error{% endif %}"
|
|
149
|
+
placeholder="{{ field.placeholder or 'email@example.com' }}">
|
|
150
|
+
|
|
151
|
+
{# ββ Number ββ #}
|
|
152
|
+
{% elif field.type == 'number' %}
|
|
153
|
+
<input
|
|
154
|
+
type="number"
|
|
155
|
+
id="field-{{ field.name }}"
|
|
156
|
+
name="{{ field.name }}"
|
|
157
|
+
value="{{ val }}"
|
|
158
|
+
class="form-control{% if hasError %} error{% endif %}"
|
|
159
|
+
placeholder="{{ field.placeholder or '0' }}">
|
|
160
|
+
|
|
161
|
+
{# ββ Date ββ #}
|
|
162
|
+
{% elif field.type == 'date' %}
|
|
163
|
+
<input
|
|
164
|
+
type="date"
|
|
165
|
+
id="field-{{ field.name }}"
|
|
166
|
+
name="{{ field.name }}"
|
|
167
|
+
value="{{ val }}"
|
|
168
|
+
class="form-control{% if hasError %} error{% endif %}">
|
|
169
|
+
|
|
170
|
+
{# ββ Datetime ββ #}
|
|
171
|
+
{% elif field.type == 'datetime' %}
|
|
172
|
+
<input
|
|
173
|
+
type="datetime-local"
|
|
174
|
+
id="field-{{ field.name }}"
|
|
175
|
+
name="{{ field.name }}"
|
|
176
|
+
value="{{ val }}"
|
|
177
|
+
class="form-control{% if hasError %} error{% endif %}">
|
|
178
|
+
|
|
179
|
+
{# ββ Default text ββ #}
|
|
180
|
+
{% else %}
|
|
181
|
+
<input
|
|
182
|
+
type="text"
|
|
183
|
+
id="field-{{ field.name }}"
|
|
184
|
+
name="{{ field.name }}"
|
|
185
|
+
value="{{ val }}"
|
|
186
|
+
class="form-control{% if hasError %} error{% endif %}"
|
|
187
|
+
placeholder="{{ field.placeholder or '' }}">
|
|
188
|
+
{% endif %}
|
|
189
|
+
|
|
190
|
+
{# ββ Error / help ββ #}
|
|
191
|
+
{% if hasError %}
|
|
192
|
+
<span class="form-error">
|
|
193
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
|
194
|
+
{{ errors[field.name][0] if errors[field.name] is iterable and errors[field.name][0] is string else errors[field.name] }}
|
|
195
|
+
</span>
|
|
196
|
+
{% elif field.help %}
|
|
197
|
+
<span class="form-help">{{ field.help }}</span>
|
|
198
|
+
{% endif %}
|
|
199
|
+
</div>
|
|
200
|
+
{% endfor %}
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
{# ββ Form footer ββ #}
|
|
204
|
+
<div class="flex items-center gap-2" style="margin-top:24px;padding-top:20px;border-top:1px solid var(--border-soft)">
|
|
205
|
+
<button type="submit" class="btn btn-primary" id="submit-btn">
|
|
206
|
+
<span class="icon icon-14"><svg viewBox="0 0 24 24"><use href="#ic-save"/></svg></span>
|
|
207
|
+
{{ 'Save Changes' if isEdit else 'Create ' + resource.singular }}
|
|
208
|
+
</button>
|
|
209
|
+
<a href="{{ adminPrefix }}/{{ resource.slug }}" class="btn btn-ghost">Cancel</a>
|
|
210
|
+
|
|
211
|
+
{% if isEdit and resource.canDelete %}
|
|
212
|
+
<button type="button" class="btn btn-danger" style="margin-left:auto"
|
|
213
|
+
onclick="confirmDelete('{{ adminPrefix }}/{{ resource.slug }}/{{ record.id }}/delete', '{{ resource.singular }} #{{ record.id }}')">
|
|
214
|
+
<span class="icon icon-14"><svg viewBox="0 0 24 24"><use href="#ic-trash"/></svg></span>
|
|
215
|
+
Delete
|
|
216
|
+
</button>
|
|
121
217
|
{% endif %}
|
|
122
218
|
</div>
|
|
123
|
-
|
|
124
|
-
|
|
219
|
+
</form>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
125
222
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
223
|
+
{# ββ Timestamps readonly ββ #}
|
|
224
|
+
{% if isEdit and (record.created_at or record.updated_at) %}
|
|
225
|
+
<div class="flex gap-4" style="margin-top:14px;padding:0 2px">
|
|
226
|
+
{% if record.created_at %}
|
|
227
|
+
<span class="text-xs text-muted flex items-center gap-1">
|
|
228
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
|
229
|
+
Created {{ record.created_at }}
|
|
230
|
+
</span>
|
|
231
|
+
{% endif %}
|
|
232
|
+
{% if record.updated_at %}
|
|
233
|
+
<span class="text-xs text-muted flex items-center gap-1">
|
|
234
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/></svg>
|
|
235
|
+
Updated {{ record.updated_at }}
|
|
236
|
+
</span>
|
|
237
|
+
{% endif %}
|
|
139
238
|
</div>
|
|
239
|
+
{% endif %}
|
|
140
240
|
</div>
|
|
141
241
|
|
|
242
|
+
<script>
|
|
243
|
+
function togglePassword(btn) {
|
|
244
|
+
const input = btn.previousElementSibling || btn.closest('div').querySelector('input');
|
|
245
|
+
if (!input) return;
|
|
246
|
+
input.type = input.type === 'password' ? 'text' : 'password';
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Prevent double-submit
|
|
250
|
+
document.getElementById('record-form')?.addEventListener('submit', function() {
|
|
251
|
+
const btn = document.getElementById('submit-btn');
|
|
252
|
+
if (btn) {
|
|
253
|
+
btn.disabled = true;
|
|
254
|
+
btn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="animation:spin 1s linear infinite"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/></svg> Savingβ¦`;
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
</script>
|
|
258
|
+
|
|
142
259
|
<style>
|
|
143
|
-
|
|
260
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
144
261
|
</style>
|
|
145
262
|
{% endblock %}
|