fastapi-admin-kit 0.1.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.
- fastapi_admin_kit/__init__.py +73 -0
- fastapi_admin_kit/actions/__init__.py +63 -0
- fastapi_admin_kit/actions/base.py +68 -0
- fastapi_admin_kit/actions/registry.py +43 -0
- fastapi_admin_kit/admin/__init__.py +17 -0
- fastapi_admin_kit/admin/admin_config.py +95 -0
- fastapi_admin_kit/admin/admin_database.py +138 -0
- fastapi_admin_kit/admin/admin_router.py +74 -0
- fastapi_admin_kit/admin/admin_template.py +203 -0
- fastapi_admin_kit/admin/builtin_models.py +284 -0
- fastapi_admin_kit/admin/core.py +1036 -0
- fastapi_admin_kit/admin/decorators.py +70 -0
- fastapi_admin_kit/admin/state.py +76 -0
- fastapi_admin_kit/admin.py +728 -0
- fastapi_admin_kit/api/__init__.py +44 -0
- fastapi_admin_kit/api/auth.py +342 -0
- fastapi_admin_kit/api/crud.py +128 -0
- fastapi_admin_kit/api/deps.py +79 -0
- fastapi_admin_kit/api/roles.py +128 -0
- fastapi_admin_kit/api/schema_generator.py +171 -0
- fastapi_admin_kit/api/schemas.py +81 -0
- fastapi_admin_kit/api/search.py +132 -0
- fastapi_admin_kit/audit/__init__.py +36 -0
- fastapi_admin_kit/audit/context.py +62 -0
- fastapi_admin_kit/audit/diff.py +77 -0
- fastapi_admin_kit/audit/event_bus.py +96 -0
- fastapi_admin_kit/audit/events.py +48 -0
- fastapi_admin_kit/audit/listener.py +159 -0
- fastapi_admin_kit/audit/logger.py +28 -0
- fastapi_admin_kit/audit/middleware.py +39 -0
- fastapi_admin_kit/audit/models.py +53 -0
- fastapi_admin_kit/audit/sqlalchemy_logger.py +58 -0
- fastapi_admin_kit/auth/__init__.py +34 -0
- fastapi_admin_kit/auth/backend.py +95 -0
- fastapi_admin_kit/auth/csrf.py +240 -0
- fastapi_admin_kit/auth/dependencies.py +150 -0
- fastapi_admin_kit/auth/identity.py +181 -0
- fastapi_admin_kit/auth/models.py +246 -0
- fastapi_admin_kit/auth/password.py +35 -0
- fastapi_admin_kit/auth/permissions.py +205 -0
- fastapi_admin_kit/auth/protocol.py +22 -0
- fastapi_admin_kit/auth/ratelimit.py +88 -0
- fastapi_admin_kit/auth/router.py +10 -0
- fastapi_admin_kit/auth/session.py +79 -0
- fastapi_admin_kit/auth/totp.py +83 -0
- fastapi_admin_kit/auth/views.py +165 -0
- fastapi_admin_kit/cli.py +229 -0
- fastapi_admin_kit/config/__init__.py +19 -0
- fastapi_admin_kit/config/audit.py +18 -0
- fastapi_admin_kit/config/auth.py +54 -0
- fastapi_admin_kit/config/behavior.py +27 -0
- fastapi_admin_kit/config/nav.py +32 -0
- fastapi_admin_kit/config/storage.py +22 -0
- fastapi_admin_kit/config/theme.py +215 -0
- fastapi_admin_kit/config/ui.py +147 -0
- fastapi_admin_kit/dashboard/__init__.py +64 -0
- fastapi_admin_kit/db.py +133 -0
- fastapi_admin_kit/exceptions.py +5 -0
- fastapi_admin_kit/field_types.py +81 -0
- fastapi_admin_kit/filters/__init__.py +21 -0
- fastapi_admin_kit/filters/base.py +170 -0
- fastapi_admin_kit/filters/registry.py +68 -0
- fastapi_admin_kit/flash.py +45 -0
- fastapi_admin_kit/form/__init__.py +1 -0
- fastapi_admin_kit/form/pipeline.py +106 -0
- fastapi_admin_kit/inspection/__init__.py +117 -0
- fastapi_admin_kit/inspection/registry.py +253 -0
- fastapi_admin_kit/inspection.py +115 -0
- fastapi_admin_kit/modeladmin.py +375 -0
- fastapi_admin_kit/models/__init__.py +7 -0
- fastapi_admin_kit/models/base.py +7 -0
- fastapi_admin_kit/nav.py +208 -0
- fastapi_admin_kit/pagination/__init__.py +14 -0
- fastapi_admin_kit/pagination/base.py +40 -0
- fastapi_admin_kit/pagination/cursor.py +97 -0
- fastapi_admin_kit/pagination/dynamic.py +48 -0
- fastapi_admin_kit/pagination/offset.py +42 -0
- fastapi_admin_kit/plugins/__init__.py +1 -0
- fastapi_admin_kit/py.typed +0 -0
- fastapi_admin_kit/registry/__init__.py +5 -0
- fastapi_admin_kit/registry/core.py +287 -0
- fastapi_admin_kit/registry/validation.py +107 -0
- fastapi_admin_kit/registry.py +15 -0
- fastapi_admin_kit/router.py +335 -0
- fastapi_admin_kit/static/css/admin.css +4736 -0
- fastapi_admin_kit/static/css/presets.css +317 -0
- fastapi_admin_kit/static/css/tokens.css +217 -0
- fastapi_admin_kit/static/css/variables.css +74 -0
- fastapi_admin_kit/static/icons/heroicons.svg +160 -0
- fastapi_admin_kit/static/js/admin.js +692 -0
- fastapi_admin_kit/static/js/htmx-config.js +42 -0
- fastapi_admin_kit/storage/__init__.py +6 -0
- fastapi_admin_kit/storage/base.py +48 -0
- fastapi_admin_kit/storage/local.py +73 -0
- fastapi_admin_kit/templates/base.html +142 -0
- fastapi_admin_kit/templates/macros/form_fields.html +660 -0
- fastapi_admin_kit/templates/macros/icons.html +50 -0
- fastapi_admin_kit/templates/macros/table.html +108 -0
- fastapi_admin_kit/templates/macros/widgets.html +159 -0
- fastapi_admin_kit/templates/pages/2fa/setup.html +122 -0
- fastapi_admin_kit/templates/pages/2fa/verify.html +55 -0
- fastapi_admin_kit/templates/pages/audit_detail.html +122 -0
- fastapi_admin_kit/templates/pages/audit_log.html +102 -0
- fastapi_admin_kit/templates/pages/dashboard.html +295 -0
- fastapi_admin_kit/templates/pages/detail.html +183 -0
- fastapi_admin_kit/templates/pages/form.html +119 -0
- fastapi_admin_kit/templates/pages/list.html +277 -0
- fastapi_admin_kit/templates/pages/login.html +85 -0
- fastapi_admin_kit/templates/pages/profile/password.html +78 -0
- fastapi_admin_kit/templates/pages/profile/profile.html +73 -0
- fastapi_admin_kit/templates/pages/role_form.html +75 -0
- fastapi_admin_kit/templates/pages/roles/form.html +117 -0
- fastapi_admin_kit/templates/pages/roles/list.html +69 -0
- fastapi_admin_kit/templates/pages/roles.html +77 -0
- fastapi_admin_kit/templates/pages/settings/theme.html +255 -0
- fastapi_admin_kit/templates/pages/users/form.html +229 -0
- fastapi_admin_kit/templates/pages/users/list.html +83 -0
- fastapi_admin_kit/templates/partials/command_palette.html +52 -0
- fastapi_admin_kit/templates/partials/field_wrapper.html +2 -0
- fastapi_admin_kit/templates/partials/flash_messages.html +39 -0
- fastapi_admin_kit/templates/partials/head.html +21 -0
- fastapi_admin_kit/templates/partials/head_minimal.html +18 -0
- fastapi_admin_kit/templates/partials/list_table.html +178 -0
- fastapi_admin_kit/templates/partials/mobile_backdrop.html +2 -0
- fastapi_admin_kit/templates/partials/pagination.html +82 -0
- fastapi_admin_kit/templates/partials/permission_widget.html +86 -0
- fastapi_admin_kit/templates/partials/scripts.html +13 -0
- fastapi_admin_kit/templates/partials/sidebar.html +94 -0
- fastapi_admin_kit/templates/partials/topbar.html +95 -0
- fastapi_admin_kit/types.py +145 -0
- fastapi_admin_kit/validation.py +43 -0
- fastapi_admin_kit/views/__init__.py +78 -0
- fastapi_admin_kit/views/audit.py +134 -0
- fastapi_admin_kit/views/bulk.py +28 -0
- fastapi_admin_kit/views/class_views.py +1040 -0
- fastapi_admin_kit/views/context.py +588 -0
- fastapi_admin_kit/views/dashboard.py +162 -0
- fastapi_admin_kit/views/delete.py +31 -0
- fastapi_admin_kit/views/extra.py +65 -0
- fastapi_admin_kit/views/factory.py +667 -0
- fastapi_admin_kit/views/form.py +159 -0
- fastapi_admin_kit/views/list.py +28 -0
- fastapi_admin_kit/views/profile.py +219 -0
- fastapi_admin_kit/views/protocols.py +54 -0
- fastapi_admin_kit/views/renderers.py +634 -0
- fastapi_admin_kit/views/roles.py +230 -0
- fastapi_admin_kit/views/search.py +31 -0
- fastapi_admin_kit/views/settings.py +31 -0
- fastapi_admin_kit/views/sidebar.py +101 -0
- fastapi_admin_kit/views/totp.py +249 -0
- fastapi_admin_kit/views/users.py +347 -0
- fastapi_admin_kit/views.py +117 -0
- fastapi_admin_kit/widgets/__init__.py +44 -0
- fastapi_admin_kit/widgets/base.py +44 -0
- fastapi_admin_kit/widgets/inputs.py +363 -0
- fastapi_admin_kit/widgets/registry.py +110 -0
- fastapi_admin_kit/widgets/relation.py +70 -0
- fastapi_admin_kit/widgets/resolver.py +102 -0
- fastapi_admin_kit-0.1.0.dist-info/METADATA +210 -0
- fastapi_admin_kit-0.1.0.dist-info/RECORD +163 -0
- fastapi_admin_kit-0.1.0.dist-info/WHEEL +4 -0
- fastapi_admin_kit-0.1.0.dist-info/entry_points.txt +3 -0
- fastapi_admin_kit-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
{# pages/form.html — create/edit form with fieldsets #}
|
|
2
|
+
{% extends "base.html" %}
|
|
3
|
+
{% from "macros/form_fields.html" import render_field %}
|
|
4
|
+
|
|
5
|
+
{% block title %}{% if obj %}Edit{% else %}Create{% endif %} {{ registered.verbose_name }} — {{ title | default("Admin") }}{% endblock %}
|
|
6
|
+
|
|
7
|
+
{% block breadcrumb %}
|
|
8
|
+
{{ super() }}
|
|
9
|
+
<span class="breadcrumb-separator">/</span>
|
|
10
|
+
<a href="{{ admin_path | default('/admin') }}/{{ registered.table_name }}/" class="breadcrumb-segment hover:text-[var(--color-primary-600)] transition-colors">{{ registered.verbose_name_plural }}</a>
|
|
11
|
+
<span class="breadcrumb-separator">/</span>
|
|
12
|
+
<span class="breadcrumb-current">{% if obj %}Edit {{ obj }}{% else %}Create{% endif %}</span>
|
|
13
|
+
{% endblock %}
|
|
14
|
+
|
|
15
|
+
{% block content %}
|
|
16
|
+
<div x-data="{ unsaved: false }" @change="unsaved = true">
|
|
17
|
+
<div class="page-header">
|
|
18
|
+
<div class="page-header-left">
|
|
19
|
+
<a href="{{ admin_path | default('/admin') }}/{{ registered.table_name }}/" class="page-header-back">
|
|
20
|
+
{{ icon("chevron-left", size="16px") }}
|
|
21
|
+
{{ registered.verbose_name_plural }}
|
|
22
|
+
</a>
|
|
23
|
+
<h1 class="page-header-title">
|
|
24
|
+
{% if obj %}Edit {{ registered.verbose_name }}{% else %}New {{ registered.verbose_name }}{% endif %}
|
|
25
|
+
</h1>
|
|
26
|
+
{% if obj %}
|
|
27
|
+
<p class="page-header-subtitle">
|
|
28
|
+
<span class="mono">#{{ obj.id }}</span>
|
|
29
|
+
{% if obj.__str__() %} — {{ obj.__str__() }}{% endif %}
|
|
30
|
+
</p>
|
|
31
|
+
{% endif %}
|
|
32
|
+
</div>
|
|
33
|
+
<div class="page-header-right">
|
|
34
|
+
{% if obj and permissions.can_delete %}
|
|
35
|
+
<form method="post" action="{{ admin_path | default('/admin') }}/{{ registered.table_name }}/{{ obj.id }}/delete">
|
|
36
|
+
<input type="hidden" name="csrf_token" value="{{ get_csrf_token(request) }}">
|
|
37
|
+
<button type="button" class="btn btn-danger btn-sm"
|
|
38
|
+
onclick="confirmAction('Delete', 'Are you sure you want to delete this {{ registered.verbose_name | lower }}?', () => this.closest('form').submit())">Delete</button>
|
|
39
|
+
</form>
|
|
40
|
+
{% endif %}
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
{% if errors %}
|
|
45
|
+
<div class="mb-6 p-4 rounded-lg bg-red-50 border border-red-200">
|
|
46
|
+
<div class="flex items-center gap-2 text-red-700">
|
|
47
|
+
{{ icon("exclamation-triangle", size="20px") }}
|
|
48
|
+
<span class="font-medium">Please correct the errors below</span>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
{% endif %}
|
|
52
|
+
|
|
53
|
+
{% set _ui = ui_config | default({}) %}
|
|
54
|
+
<form method="post"
|
|
55
|
+
action="{{ admin_path | default('/admin') }}/{{ registered.table_name }}{% if obj %}/{{ obj.id }}{% else %}/create{% endif %}"
|
|
56
|
+
@input="unsaved = true"
|
|
57
|
+
class="space-y-6 form--{{ _ui.form_layout | default('two-column') }} form--{{ _ui.form_spacing | default('normal') }}">
|
|
58
|
+
<input type="hidden" name="csrf_token" value="{{ get_csrf_token(request) }}">
|
|
59
|
+
|
|
60
|
+
{% if fieldsets %}
|
|
61
|
+
{% for fieldset in fieldsets %}
|
|
62
|
+
<div class="fieldset" x-data="{ open: true }">
|
|
63
|
+
<button type="button" @click="open = !open"
|
|
64
|
+
class="fieldset__header" style="cursor: pointer; background: none; border: none; width: 100%; text-align: left;">
|
|
65
|
+
<span class="fieldset__title">{{ fieldset.title or "General" }}</span>
|
|
66
|
+
<span class="fieldset__toggle" :style="open ? 'transform: rotate(180deg)' : ''">
|
|
67
|
+
{{ icon("chevron-right", size="16px") }}
|
|
68
|
+
</span>
|
|
69
|
+
</button>
|
|
70
|
+
<div x-show="open" x-collapse class="fieldset__fields">
|
|
71
|
+
{% for field in fieldset.fields %}
|
|
72
|
+
{{ render_field(field, registered.table_name) }}
|
|
73
|
+
{% endfor %}
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
{% endfor %}
|
|
77
|
+
{% else %}
|
|
78
|
+
<div class="fieldset">
|
|
79
|
+
<div class="fieldset__fields">
|
|
80
|
+
{% for field in form_fields %}
|
|
81
|
+
{{ render_field(field, registered.table_name) }}
|
|
82
|
+
{% endfor %}
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
{% endif %}
|
|
86
|
+
|
|
87
|
+
{% if perm_data is defined and search_url is defined %}
|
|
88
|
+
<div class="fieldset">
|
|
89
|
+
<div class="fieldset__header">
|
|
90
|
+
<h3 class="fieldset__title">Direct Permissions</h3>
|
|
91
|
+
</div>
|
|
92
|
+
<div class="fieldset__fields">
|
|
93
|
+
<p class="field-help" style="color: var(--text-secondary); font-size: 0.75rem; margin-bottom: 0.5rem;">
|
|
94
|
+
Override role permissions for specific tables. These are merged with role permissions using OR logic.
|
|
95
|
+
</p>
|
|
96
|
+
{% include "partials/permission_widget.html" with context %}
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
{% endif %}
|
|
100
|
+
|
|
101
|
+
<div class="sticky bottom-0 flex items-center justify-between p-4" style="background: var(--surface-base); border: 1px solid var(--surface-border); border-radius: var(--radius-lg);">
|
|
102
|
+
<div class="text-sm" style="color: var(--text-secondary);" x-show="unsaved" x-transition>
|
|
103
|
+
You have unsaved changes
|
|
104
|
+
</div>
|
|
105
|
+
<div class="flex items-center gap-3 ml-auto">
|
|
106
|
+
<a href="{{ admin_path | default('/admin') }}/{{ registered.table_name }}/"
|
|
107
|
+
class="btn btn-secondary">
|
|
108
|
+
Cancel
|
|
109
|
+
</a>
|
|
110
|
+
<button type="submit"
|
|
111
|
+
@click="unsaved = false"
|
|
112
|
+
class="btn btn-primary">
|
|
113
|
+
{% if obj %}Save Changes{% else %}Create {{ registered.verbose_name }}{% endif %}
|
|
114
|
+
</button>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</form>
|
|
118
|
+
</div>
|
|
119
|
+
{% endblock %}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
{# pages/list.html — model list view with type-aware filters #}
|
|
2
|
+
{% extends "base.html" %}
|
|
3
|
+
|
|
4
|
+
{% block title %}{{ registered.verbose_name_plural }} — {{ title | default("Admin") }}{% endblock %}
|
|
5
|
+
|
|
6
|
+
{% block content %}
|
|
7
|
+
<div class="space-y-6">
|
|
8
|
+
{# Breadcrumb #}
|
|
9
|
+
<nav class="breadcrumb">
|
|
10
|
+
<a href="{{ admin_path | default('/admin') }}/" class="breadcrumb-segment">
|
|
11
|
+
{{ icon("home", size="16px") }}
|
|
12
|
+
Dashboard
|
|
13
|
+
</a>
|
|
14
|
+
<span class="breadcrumb-separator">/</span>
|
|
15
|
+
<span class="breadcrumb-current">{{ registered.verbose_name_plural }}</span>
|
|
16
|
+
</nav>
|
|
17
|
+
|
|
18
|
+
<div class="page-header">
|
|
19
|
+
<div class="page-header-left">
|
|
20
|
+
<h1 class="page-header-title">{{ registered.verbose_name_plural }}</h1>
|
|
21
|
+
<p class="page-header-subtitle">Manage your {{ registered.verbose_name_plural | lower }} and inventory</p>
|
|
22
|
+
</div>
|
|
23
|
+
{% if permissions.can_create %}
|
|
24
|
+
<div class="page-header-right">
|
|
25
|
+
<a href="{{ admin_path | default('/admin') }}/{{ registered.table_name }}/create" class="btn btn-primary">
|
|
26
|
+
+ New {{ registered.verbose_name }}
|
|
27
|
+
</a>
|
|
28
|
+
</div>
|
|
29
|
+
{% endif %}
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
{# Filter Bar #}
|
|
33
|
+
<div class="filter-bar" x-data="filterBar()">
|
|
34
|
+
<div class="filter-bar-search">
|
|
35
|
+
{{ icon("magnifying-glass", class="filter-bar-search-icon", size="20px") }}
|
|
36
|
+
<input type="text" name="q" value="{{ search_query | default('') }}"
|
|
37
|
+
placeholder="Search {{ registered.verbose_name_plural | lower }}..."
|
|
38
|
+
hx-get="{{ admin_path | default('/admin') }}/{{ registered.table_name }}/"
|
|
39
|
+
hx-trigger="keyup changed delay:300ms"
|
|
40
|
+
hx-target="#table-wrapper"
|
|
41
|
+
hx-swap="outerHTML"
|
|
42
|
+
hx-include="[name='q']"
|
|
43
|
+
class="filter-bar-search-input"
|
|
44
|
+
x-ref="searchInput"
|
|
45
|
+
@input="hasSearch = $refs.searchInput.value.length > 0">
|
|
46
|
+
<button type="button" class="filter-bar-clear"
|
|
47
|
+
x-show="hasSearch" x-transition
|
|
48
|
+
@click="$refs.searchInput.value = ''; hasSearch = false; $refs.searchInput.dispatchEvent(new KeyboardEvent('keyup', {bubbles: true}))">
|
|
49
|
+
{{ icon("x-mark", size="16px") }}
|
|
50
|
+
</button>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
{% if filter_fields %}
|
|
54
|
+
<div class="filter-bar-chips">
|
|
55
|
+
{% for field_name, field_info in filter_fields.items() %}
|
|
56
|
+
<div class="relative" x-data="{ open: false }">
|
|
57
|
+
|
|
58
|
+
{% if field_info.field_type == "date" %}
|
|
59
|
+
<div class="filter-chip-wrap {% if active_filters.get(field_name) %}filter-chip--active{% endif %}">
|
|
60
|
+
<label class="filter-chip-label">{{ field_name | replace('_', ' ') | title }}</label>
|
|
61
|
+
<input type="date" name="filter_{{ field_name }}"
|
|
62
|
+
value="{{ active_filters.get(field_name, '') }}"
|
|
63
|
+
hx-get="{{ admin_path | default('/admin') }}/{{ registered.table_name }}/"
|
|
64
|
+
hx-trigger="change"
|
|
65
|
+
hx-target="#table-wrapper"
|
|
66
|
+
hx-swap="outerHTML"
|
|
67
|
+
hx-include="[name='q'], [name^='filter_']"
|
|
68
|
+
class="filter-input-date">
|
|
69
|
+
{% if active_filters.get(field_name) %}
|
|
70
|
+
<button type="button" class="filter-clear-btn" title="Clear"
|
|
71
|
+
hx-get="{{ admin_path | default('/admin') }}/{{ registered.table_name }}/"
|
|
72
|
+
hx-target="#table-wrapper"
|
|
73
|
+
hx-swap="outerHTML"
|
|
74
|
+
hx-include="[name='q']">×</button>
|
|
75
|
+
{% endif %}
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
{% elif field_info.field_type == "datetime" %}
|
|
79
|
+
<div class="filter-chip-wrap {% if active_filters.get(field_name) %}filter-chip--active{% endif %}">
|
|
80
|
+
<label class="filter-chip-label">{{ field_name | replace('_', ' ') | title }}</label>
|
|
81
|
+
<input type="datetime-local" name="filter_{{ field_name }}"
|
|
82
|
+
value="{{ active_filters.get(field_name, '') }}"
|
|
83
|
+
hx-get="{{ admin_path | default('/admin') }}/{{ registered.table_name }}/"
|
|
84
|
+
hx-trigger="change"
|
|
85
|
+
hx-target="#table-wrapper"
|
|
86
|
+
hx-swap="outerHTML"
|
|
87
|
+
hx-include="[name='q'], [name^='filter_']"
|
|
88
|
+
class="filter-input-datetime">
|
|
89
|
+
{% if active_filters.get(field_name) %}
|
|
90
|
+
<button type="button" class="filter-clear-btn" title="Clear"
|
|
91
|
+
hx-get="{{ admin_path | default('/admin') }}/{{ registered.table_name }}/"
|
|
92
|
+
hx-target="#table-wrapper"
|
|
93
|
+
hx-swap="outerHTML"
|
|
94
|
+
hx-include="[name='q']">×</button>
|
|
95
|
+
{% endif %}
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
{% elif field_info.field_type == "time" %}
|
|
99
|
+
<div class="filter-chip-wrap {% if active_filters.get(field_name) %}filter-chip--active{% endif %}">
|
|
100
|
+
<label class="filter-chip-label">{{ field_name | replace('_', ' ') | title }}</label>
|
|
101
|
+
<input type="time" name="filter_{{ field_name }}"
|
|
102
|
+
value="{{ active_filters.get(field_name, '') }}"
|
|
103
|
+
hx-get="{{ admin_path | default('/admin') }}/{{ registered.table_name }}/"
|
|
104
|
+
hx-trigger="change"
|
|
105
|
+
hx-target="#table-wrapper"
|
|
106
|
+
hx-swap="outerHTML"
|
|
107
|
+
hx-include="[name='q'], [name^='filter_']"
|
|
108
|
+
class="filter-input-time">
|
|
109
|
+
{% if active_filters.get(field_name) %}
|
|
110
|
+
<button type="button" class="filter-clear-btn" title="Clear"
|
|
111
|
+
hx-get="{{ admin_path | default('/admin') }}/{{ registered.table_name }}/"
|
|
112
|
+
hx-target="#table-wrapper"
|
|
113
|
+
hx-swap="outerHTML"
|
|
114
|
+
hx-include="[name='q']">×</button>
|
|
115
|
+
{% endif %}
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
{% else %}
|
|
119
|
+
{# boolean, enum, relation, text — render as dropdown #}
|
|
120
|
+
<button type="button" class="filter-chip"
|
|
121
|
+
:class="{ 'active': activeFilters['{{ field_name }}'] }"
|
|
122
|
+
@click="open = !open">
|
|
123
|
+
{{ field_name | replace('_', ' ') | title }}
|
|
124
|
+
<template x-if="activeFilters['{{ field_name }}']">
|
|
125
|
+
<span class="filter-chip-count" x-text="activeFilters['{{ field_name }}']"></span>
|
|
126
|
+
</template>
|
|
127
|
+
{{ icon("chevron-right", size="16px") }}
|
|
128
|
+
</button>
|
|
129
|
+
<div class="filter-dropdown" x-show="open" @click.outside="open = false" x-transition>
|
|
130
|
+
<div class="filter-dropdown-header">
|
|
131
|
+
<span>{{ field_name | replace('_', ' ') | title }}</span>
|
|
132
|
+
<button type="button" class="filter-dropdown-clear"
|
|
133
|
+
@click="clearFilter('{{ field_name }}'); open = false">Clear</button>
|
|
134
|
+
</div>
|
|
135
|
+
{% for value, label in field_info.choices %}
|
|
136
|
+
<div class="filter-dropdown-item">
|
|
137
|
+
<input type="radio" name="filter_{{ field_name }}" value="{{ value }}"
|
|
138
|
+
:checked="activeFilters['{{ field_name }}'] === '{{ value }}'"
|
|
139
|
+
@change="setFilter('{{ field_name }}', '{{ value }}'); open = false"
|
|
140
|
+
id="filter_{{ field_name }}_{{ value }}">
|
|
141
|
+
<label for="filter_{{ field_name }}_{{ value }}">{{ label }}</label>
|
|
142
|
+
</div>
|
|
143
|
+
{% endfor %}
|
|
144
|
+
</div>
|
|
145
|
+
{% endif %}
|
|
146
|
+
|
|
147
|
+
</div>
|
|
148
|
+
{% endfor %}
|
|
149
|
+
</div>
|
|
150
|
+
{% endif %}
|
|
151
|
+
|
|
152
|
+
<div class="filter-bar-right">
|
|
153
|
+
{% if filter_fields %}
|
|
154
|
+
<button type="button" class="filter-bar-clear-all"
|
|
155
|
+
x-show="Object.keys(activeFilters).length > 0"
|
|
156
|
+
x-transition
|
|
157
|
+
@click="clearAllFilters()">
|
|
158
|
+
Clear all filters
|
|
159
|
+
</button>
|
|
160
|
+
<div class="filter-bar-divider" x-show="Object.keys(activeFilters).length > 0"></div>
|
|
161
|
+
{% endif %}
|
|
162
|
+
|
|
163
|
+
<button type="button" class="filter-bar-action" title="Toggle column visibility"
|
|
164
|
+
@click="showColumnPicker = !showColumnPicker">
|
|
165
|
+
{{ icon("eye", size="20px") }}
|
|
166
|
+
</button>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
{# Bulk Actions Bar — Django-style: always visible above table #}
|
|
171
|
+
{% if permissions.can_delete or list_actions %}
|
|
172
|
+
<div class="bulk-bar">
|
|
173
|
+
<div class="bulk-bar-left">
|
|
174
|
+
<label class="bulk-bar-label" for="bulk-action-select">Action:</label>
|
|
175
|
+
<select id="bulk-action-select" name="bulk_action" class="bulk-bar-select">
|
|
176
|
+
<option value="">---------</option>
|
|
177
|
+
{% if list_actions %}
|
|
178
|
+
{% for action in list_actions %}
|
|
179
|
+
<option value="{{ action.name }}">{{ action.label | default(action.name) }}</option>
|
|
180
|
+
{% endfor %}
|
|
181
|
+
{% endif %}
|
|
182
|
+
{% if permissions.can_delete %}
|
|
183
|
+
<option value="delete_selected">Delete selected</option>
|
|
184
|
+
{% endif %}
|
|
185
|
+
</select>
|
|
186
|
+
</div>
|
|
187
|
+
<div class="bulk-bar-right">
|
|
188
|
+
<button type="button" class="btn btn-primary btn-sm" onclick="executeBulkAction()">Go</button>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
{% endif %}
|
|
192
|
+
|
|
193
|
+
{% include "partials/list_table.html" %}
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
<script>
|
|
197
|
+
function filterBar() {
|
|
198
|
+
return {
|
|
199
|
+
hasSearch: false,
|
|
200
|
+
activeFilters: {{ active_filters | tojson }},
|
|
201
|
+
showColumnPicker: false,
|
|
202
|
+
init() {
|
|
203
|
+
},
|
|
204
|
+
setFilter(field, value) {
|
|
205
|
+
if (value) {
|
|
206
|
+
this.activeFilters[field] = value;
|
|
207
|
+
} else {
|
|
208
|
+
delete this.activeFilters[field];
|
|
209
|
+
}
|
|
210
|
+
this.applyFilters();
|
|
211
|
+
},
|
|
212
|
+
clearFilter(field) {
|
|
213
|
+
delete this.activeFilters[field];
|
|
214
|
+
this.applyFilters();
|
|
215
|
+
},
|
|
216
|
+
clearAllFilters() {
|
|
217
|
+
this.activeFilters = {};
|
|
218
|
+
this.applyFilters();
|
|
219
|
+
},
|
|
220
|
+
applyFilters() {
|
|
221
|
+
const params = new URLSearchParams(window.location.search);
|
|
222
|
+
Object.entries(this.activeFilters).forEach(([key, val]) => {
|
|
223
|
+
if (val) params.set('filter_' + key, val);
|
|
224
|
+
else params.delete('filter_' + key);
|
|
225
|
+
});
|
|
226
|
+
const search = this.$refs.searchInput?.value;
|
|
227
|
+
if (search) params.set('q', search);
|
|
228
|
+
else params.delete('q');
|
|
229
|
+
const url = window.location.pathname + (params.toString() ? '?' + params.toString() : '');
|
|
230
|
+
htmx.ajax('GET', url, { target: '#table-wrapper', swap: 'outerHTML' });
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function _sendBulkAction(actionValue, selectedIds) {
|
|
236
|
+
var params = new URLSearchParams();
|
|
237
|
+
params.append('action', actionValue);
|
|
238
|
+
params.append('csrf_token', document.querySelector('meta[name="csrf-token"]').content);
|
|
239
|
+
selectedIds.forEach(function(id) {
|
|
240
|
+
params.append('ids[]', id);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
var xhr = new XMLHttpRequest();
|
|
244
|
+
xhr.open('POST', '{{ admin_path | default("/admin") }}/{{ registered.table_name }}/bulk', true);
|
|
245
|
+
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
|
246
|
+
xhr.setRequestHeader('HX-Request', 'true');
|
|
247
|
+
xhr.onload = function() { window.location.reload(); };
|
|
248
|
+
xhr.onerror = function() { window.location.reload(); };
|
|
249
|
+
xhr.send(params.toString());
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function executeBulkAction() {
|
|
253
|
+
var actionSelect = document.getElementById('bulk-action-select');
|
|
254
|
+
var selected = document.querySelectorAll('input[name="ids[]"]:checked');
|
|
255
|
+
|
|
256
|
+
if (selected.length === 0) {
|
|
257
|
+
alert('Please select at least one item.');
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (!actionSelect.value) {
|
|
261
|
+
alert('Please select an action.');
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
var selectedIds = Array.from(selected).map(function(cb) { return cb.value; });
|
|
266
|
+
|
|
267
|
+
if (actionSelect.value === 'delete_selected') {
|
|
268
|
+
confirmAction('Delete selected', 'Are you sure you want to delete the selected items?', function() {
|
|
269
|
+
_sendBulkAction(actionSelect.value, selectedIds);
|
|
270
|
+
});
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
_sendBulkAction(actionSelect.value, selectedIds);
|
|
275
|
+
}
|
|
276
|
+
</script>
|
|
277
|
+
{% endblock %}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>{{ title | default("Admin") }} — Login</title>
|
|
7
|
+
|
|
8
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
|
+
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
11
|
+
|
|
12
|
+
<link rel="stylesheet" href="/static/css/tokens.css">
|
|
13
|
+
<link rel="stylesheet" href="/static/css/admin.css">
|
|
14
|
+
|
|
15
|
+
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
16
|
+
|
|
17
|
+
<style>
|
|
18
|
+
:root {
|
|
19
|
+
--primary-500: {{ admin_config.primary_color | default('') }};
|
|
20
|
+
--primary-600: {{ admin_config.primary_color_dark | default('') }};
|
|
21
|
+
}
|
|
22
|
+
</style>
|
|
23
|
+
</head>
|
|
24
|
+
<body class="bg-[var(--surface-overlay)] text-[var(--text-primary)] font-sans antialiased min-h-screen flex items-center justify-center p-4">
|
|
25
|
+
|
|
26
|
+
<div class="w-full max-w-md">
|
|
27
|
+
<div class="text-center mb-6">
|
|
28
|
+
{% if logo_url %}
|
|
29
|
+
<img src="{{ logo_url }}" alt="Logo" class="h-10 mx-auto mb-2">
|
|
30
|
+
{% else %}
|
|
31
|
+
<div class="w-10 h-10 mx-auto mb-2 rounded-lg bg-[var(--primary-500)] flex items-center justify-center">
|
|
32
|
+
{{ icon("home", css_class="text-white", size="20px") }}
|
|
33
|
+
</div>
|
|
34
|
+
{% endif %}
|
|
35
|
+
<h1 class="text-xl font-bold text-[var(--text-primary)]" style="margin: 0;">{{ title | default("Admin") }}</h1>
|
|
36
|
+
<p class="text-sm text-[var(--text-secondary)] mt-0.5">Sign in to your account</p>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
{% set messages = get_flash_messages(request) | default([]) %}
|
|
40
|
+
{% if messages %}
|
|
41
|
+
<div class="mb-4 space-y-2">
|
|
42
|
+
{% for msg in messages %}
|
|
43
|
+
<div class="p-3 rounded-lg text-sm
|
|
44
|
+
{% if msg.level == 'error' %}bg-[var(--danger-50)] text-[var(--danger-500)] border border-[var(--danger-100)]
|
|
45
|
+
{% elif msg.level == 'success' %}bg-[var(--success-50)] text-[var(--success-500)] border border-[var(--success-100)]
|
|
46
|
+
{% elif msg.level == 'warning' %}bg-[var(--warning-50)] text-[var(--warning-500)] border border-[var(--warning-100)]
|
|
47
|
+
{% else %}bg-[var(--info-50)] text-[var(--info-500)] border border-[var(--info-100)]{% endif %}">
|
|
48
|
+
{{ msg.text }}
|
|
49
|
+
</div>
|
|
50
|
+
{% endfor %}
|
|
51
|
+
</div>
|
|
52
|
+
{% endif %}
|
|
53
|
+
|
|
54
|
+
<form method="post" action="{{ admin_path | default('/admin') }}/login{% if next %}?next={{ next }}{% endif %}"
|
|
55
|
+
class="bg-white rounded-xl shadow-md border border-[var(--surface-border)] p-8">
|
|
56
|
+
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
|
57
|
+
{% if error %}
|
|
58
|
+
<div class="mb-4 p-3 rounded-lg bg-[var(--danger-50)] text-[var(--danger-500)] text-sm border border-[var(--danger-100)]">
|
|
59
|
+
{{ error }}
|
|
60
|
+
</div>
|
|
61
|
+
{% endif %}
|
|
62
|
+
|
|
63
|
+
<div class="mb-4">
|
|
64
|
+
<label for="email" class="block text-sm font-medium text-[var(--text-secondary)] mb-1.5">Email address</label>
|
|
65
|
+
<input type="email" id="email" name="email" required autofocus
|
|
66
|
+
class="form-input w-full px-4 py-2.5 rounded-lg border border-[var(--surface-border)] bg-white text-[var(--text-primary)] placeholder-[var(--text-disabled)] focus:ring-2 focus:ring-[var(--primary-500)] focus:border-[var(--primary-500)] transition-all"
|
|
67
|
+
placeholder="admin@example.com">
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div class="mb-6">
|
|
71
|
+
<label for="password" class="block text-sm font-medium text-[var(--text-secondary)] mb-1.5">Password</label>
|
|
72
|
+
<input type="password" id="password" name="password" required
|
|
73
|
+
class="form-input w-full px-4 py-2.5 rounded-lg border border-[var(--surface-border)] bg-white text-[var(--text-primary)] placeholder-[var(--text-disabled)] focus:ring-2 focus:ring-[var(--primary-500)] focus:border-[var(--primary-500)] transition-all"
|
|
74
|
+
placeholder="Enter your password">
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<button type="submit"
|
|
78
|
+
class="w-full px-4 py-3 rounded-lg bg-[var(--primary-500)] text-white font-medium hover:bg-[var(--primary-600)] focus:ring-2 focus:ring-[var(--primary-500)] focus:ring-offset-2 transition-all cursor-pointer">
|
|
79
|
+
Sign in
|
|
80
|
+
</button>
|
|
81
|
+
</form>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
</body>
|
|
85
|
+
</html>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{# pages/profile/password.html — change password #}
|
|
2
|
+
{% extends "base.html" %}
|
|
3
|
+
|
|
4
|
+
{% block title %}Change Password — {{ title | default("Admin") }}{% endblock %}
|
|
5
|
+
|
|
6
|
+
{% block breadcrumb %}
|
|
7
|
+
{{ super() }}
|
|
8
|
+
<span class="text-[var(--color-gray-400)]">/</span>
|
|
9
|
+
<a href="{{ admin_path | default('/admin') }}/profile" class="hover:text-[var(--color-primary-600)] transition-colors">Profile</a>
|
|
10
|
+
<span class="text-[var(--color-gray-400)]">/</span>
|
|
11
|
+
<span class="text-[var(--color-gray-900)] font-medium">Change Password</span>
|
|
12
|
+
{% endblock %}
|
|
13
|
+
|
|
14
|
+
{% block content %}
|
|
15
|
+
<div class="space-y-6">
|
|
16
|
+
<div class="page-header">
|
|
17
|
+
<div class="page-header-left">
|
|
18
|
+
<a href="{{ admin_path | default('/admin') }}/profile" class="page-header-back">
|
|
19
|
+
{{ icon("chevron-left") }}
|
|
20
|
+
Profile
|
|
21
|
+
</a>
|
|
22
|
+
<h1 class="page-header-title">Change Password</h1>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
{% if error %}
|
|
27
|
+
<div class="p-4 rounded-lg bg-red-50 border border-red-200 text-red-700 text-sm">
|
|
28
|
+
{{ error }}
|
|
29
|
+
</div>
|
|
30
|
+
{% endif %}
|
|
31
|
+
|
|
32
|
+
<form method="post" action="{{ admin_path | default('/admin') }}/profile/password" class="space-y-6">
|
|
33
|
+
<input type="hidden" name="csrf_token" value="{{ get_csrf_token(request) }}">
|
|
34
|
+
|
|
35
|
+
<div class="bg-white rounded-xl border border-[var(--color-gray-200)] p-6">
|
|
36
|
+
<h2 class="text-lg font-semibold text-[var(--color-gray-900)] mb-4">Password Requirements</h2>
|
|
37
|
+
<ul class="text-sm text-[var(--color-gray-600)] space-y-1 list-disc list-inside">
|
|
38
|
+
<li>At least 12 characters</li>
|
|
39
|
+
<li>At least one uppercase letter</li>
|
|
40
|
+
<li>At least one lowercase letter</li>
|
|
41
|
+
<li>At least one digit</li>
|
|
42
|
+
<li>At least one special character</li>
|
|
43
|
+
</ul>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div class="bg-white rounded-xl border border-[var(--color-gray-200)] p-6">
|
|
47
|
+
<div class="grid grid-cols-1 gap-6">
|
|
48
|
+
<div>
|
|
49
|
+
<label for="current_password" class="block text-sm font-medium text-[var(--color-gray-700)] mb-1">Current Password</label>
|
|
50
|
+
<input type="password" id="current_password" name="current_password" required
|
|
51
|
+
class="form-input w-full px-4 py-2.5 rounded-lg border border-[var(--color-gray-300)] bg-white text-[var(--color-gray-900)] focus:ring-2 focus:ring-[var(--color-primary-500)] focus:border-[var(--color-primary-500)] transition-colors">
|
|
52
|
+
</div>
|
|
53
|
+
<div>
|
|
54
|
+
<label for="new_password" class="block text-sm font-medium text-[var(--color-gray-700)] mb-1">New Password</label>
|
|
55
|
+
<input type="password" id="new_password" name="new_password" required
|
|
56
|
+
class="form-input w-full px-4 py-2.5 rounded-lg border border-[var(--color-gray-300)] bg-white text-[var(--color-gray-900)] focus:ring-2 focus:ring-[var(--color-primary-500)] focus:border-[var(--color-primary-500)] transition-colors">
|
|
57
|
+
</div>
|
|
58
|
+
<div>
|
|
59
|
+
<label for="confirm_password" class="block text-sm font-medium text-[var(--color-gray-700)] mb-1">Confirm New Password</label>
|
|
60
|
+
<input type="password" id="confirm_password" name="confirm_password" required
|
|
61
|
+
class="form-input w-full px-4 py-2.5 rounded-lg border border-[var(--color-gray-300)] bg-white text-[var(--color-gray-900)] focus:ring-2 focus:ring-[var(--color-primary-500)] focus:border-[var(--color-primary-500)] transition-colors">
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div class="sticky bottom-0 bg-white border border-[var(--color-gray-200)] rounded-xl p-4 flex items-center justify-end gap-3">
|
|
67
|
+
<a href="{{ admin_path | default('/admin') }}/profile"
|
|
68
|
+
class="px-4 py-2 rounded-lg border border-[var(--color-gray-300)] text-sm text-[var(--color-gray-700)] hover:bg-[var(--color-gray-50)] transition-colors">
|
|
69
|
+
Cancel
|
|
70
|
+
</a>
|
|
71
|
+
<button type="submit"
|
|
72
|
+
class="btn-primary px-4 py-2 rounded-lg bg-[var(--color-primary-600)] text-white text-sm font-medium hover:bg-[var(--color-primary-500)] transition-colors">
|
|
73
|
+
Change Password
|
|
74
|
+
</button>
|
|
75
|
+
</div>
|
|
76
|
+
</form>
|
|
77
|
+
</div>
|
|
78
|
+
{% endblock %}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{# pages/profile/profile.html — user profile #}
|
|
2
|
+
{% extends "base.html" %}
|
|
3
|
+
|
|
4
|
+
{% block title %}Profile — {{ title | default("Admin") }}{% endblock %}
|
|
5
|
+
|
|
6
|
+
{% block breadcrumb %}
|
|
7
|
+
{{ super() }}
|
|
8
|
+
<span class="text-[var(--color-gray-400)]">/</span>
|
|
9
|
+
<span class="text-[var(--color-gray-900)] font-medium">Profile</span>
|
|
10
|
+
{% endblock %}
|
|
11
|
+
|
|
12
|
+
{% block content %}
|
|
13
|
+
<div class="space-y-6">
|
|
14
|
+
<div class="page-header">
|
|
15
|
+
<div class="page-header-left">
|
|
16
|
+
<h1 class="page-header-title">My Profile</h1>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
{% if error %}
|
|
21
|
+
<div class="p-4 rounded-lg bg-red-50 border border-red-200 text-red-700 text-sm">
|
|
22
|
+
{{ error }}
|
|
23
|
+
</div>
|
|
24
|
+
{% endif %}
|
|
25
|
+
|
|
26
|
+
<form method="post" action="{{ admin_path | default('/admin') }}/profile" class="space-y-6">
|
|
27
|
+
<input type="hidden" name="csrf_token" value="{{ get_csrf_token(request) }}">
|
|
28
|
+
|
|
29
|
+
<div class="bg-white rounded-xl border border-[var(--color-gray-200)] p-6">
|
|
30
|
+
<h2 class="text-lg font-semibold text-[var(--color-gray-900)] mb-4">Account Information</h2>
|
|
31
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
32
|
+
<div>
|
|
33
|
+
<label for="email" class="block text-sm font-medium text-[var(--color-gray-700)] mb-1">Email</label>
|
|
34
|
+
<input type="email" id="email" name="email" value="{{ profile_user.email }}" required
|
|
35
|
+
class="form-input w-full px-4 py-2.5 rounded-lg border border-[var(--color-gray-300)] bg-white text-[var(--color-gray-900)] focus:ring-2 focus:ring-[var(--color-primary-500)] focus:border-[var(--color-primary-500)] transition-colors">
|
|
36
|
+
</div>
|
|
37
|
+
<div>
|
|
38
|
+
<label for="full_name" class="block text-sm font-medium text-[var(--color-gray-700)] mb-1">Full Name</label>
|
|
39
|
+
<input type="text" id="full_name" name="full_name" value="{{ profile_user.full_name | default('') }}"
|
|
40
|
+
class="form-input w-full px-4 py-2.5 rounded-lg border border-[var(--color-gray-300)] bg-white text-[var(--color-gray-900)] focus:ring-2 focus:ring-[var(--color-primary-500)] focus:border-[var(--color-primary-500)] transition-colors">
|
|
41
|
+
</div>
|
|
42
|
+
<div>
|
|
43
|
+
<label for="password" class="block text-sm font-medium text-[var(--color-gray-700)] mb-1">Confirm Password</label>
|
|
44
|
+
<input type="password" id="password" name="password" required
|
|
45
|
+
class="form-input w-full px-4 py-2.5 rounded-lg border border-[var(--color-gray-300)] bg-white text-[var(--color-gray-900)] focus:ring-2 focus:ring-[var(--color-primary-500)] focus:border-[var(--color-primary-500)] transition-colors"
|
|
46
|
+
placeholder="Enter password to save changes">
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div class="bg-white rounded-xl border border-[var(--color-gray-200)] p-6">
|
|
52
|
+
<h2 class="text-lg font-semibold text-[var(--color-gray-900)] mb-2">Security</h2>
|
|
53
|
+
<div class="flex items-center gap-4">
|
|
54
|
+
<a href="{{ admin_path | default('/admin') }}/profile/password"
|
|
55
|
+
class="px-4 py-2 rounded-lg border border-[var(--color-gray-300)] text-sm text-[var(--color-gray-700)] hover:bg-[var(--color-gray-50)] transition-colors">
|
|
56
|
+
Change Password
|
|
57
|
+
</a>
|
|
58
|
+
<a href="{{ admin_path | default('/admin') }}/profile/2fa"
|
|
59
|
+
class="px-4 py-2 rounded-lg border border-[var(--color-gray-300)] text-sm text-[var(--color-gray-700)] hover:bg-[var(--color-gray-50)] transition-colors">
|
|
60
|
+
Two-Factor Authentication
|
|
61
|
+
</a>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div class="sticky bottom-0 bg-white border border-[var(--color-gray-200)] rounded-xl p-4 flex items-center justify-end">
|
|
66
|
+
<button type="submit"
|
|
67
|
+
class="btn-primary px-4 py-2 rounded-lg bg-[var(--color-primary-600)] text-white text-sm font-medium hover:bg-[var(--color-primary-500)] transition-colors">
|
|
68
|
+
Save Changes
|
|
69
|
+
</button>
|
|
70
|
+
</div>
|
|
71
|
+
</form>
|
|
72
|
+
</div>
|
|
73
|
+
{% endblock %}
|