aboutsummaryrefslogtreecommitdiff
path: root/templates
diff options
context:
space:
mode:
authorBobby <[email protected]>2026-03-08 05:43:04 +0530
committerBobby <[email protected]>2026-03-08 05:43:04 +0530
commit662dd2069dc8590e8b54823a33726464cf10c4e7 (patch)
tree55a740e6114440d7e311afd3f5ba79a7101965f8 /templates
parentd21ea918864a8b18fef94bbfaec8097444be1b17 (diff)
downloaddove-662dd2069dc8590e8b54823a33726464cf10c4e7.tar.xz
dove-662dd2069dc8590e8b54823a33726464cf10c4e7.zip
feat(domains): enhance TLD and domain management with edit and delete functionality
- Added edit and delete buttons for TLDs in the TLD management interface. - Implemented a modal confirmation for delete actions across TLDs, mailboxes, users, and aliases. - Created separate edit pages for domains and TLDs with forms for updating their details. - Improved user experience by adding alerts for error messages and success notifications. feat(mail): streamline mailbox management with alias support - Introduced alias creation and deletion functionality for mailboxes. - Enhanced mailbox edit interface to include alias management. - Added dropdowns for selecting users and domains when creating aliases. fix(alerts): implement alert system for error messages - Developed a reusable alert component to display error messages. - Integrated alert dismiss functionality with automatic timeout for user notifications. refactor: general code improvements and organization - Updated error handling in the backend to support HTMX requests. - Refactored redirect functions to handle HTMX redirects appropriately.
Diffstat (limited to 'templates')
-rw-r--r--templates/domains/editdomain.django5
-rw-r--r--templates/domains/edittld.django5
-rw-r--r--templates/domains/htmx/domains.htmx.django29
-rw-r--r--templates/domains/htmx/editdomain.htmx.django49
-rw-r--r--templates/domains/htmx/edittld.htmx.django22
-rw-r--r--templates/domains/htmx/tlds.htmx.django8
-rw-r--r--templates/layouts/base.django9
-rw-r--r--templates/layouts/dashboard.django2
-rw-r--r--templates/mail/editmailbox.django5
-rw-r--r--templates/mail/edituser.django5
-rw-r--r--templates/mail/htmx/editmailbox.htmx.django139
-rw-r--r--templates/mail/htmx/edituser.htmx.django26
-rw-r--r--templates/mail/htmx/mailboxes.htmx.django55
-rw-r--r--templates/mail/htmx/users.htmx.django31
-rw-r--r--templates/partials/alert.django17
15 files changed, 389 insertions, 18 deletions
diff --git a/templates/domains/editdomain.django b/templates/domains/editdomain.django
new file mode 100644
index 0000000..7b10b37
--- /dev/null
+++ b/templates/domains/editdomain.django
@@ -0,0 +1,5 @@
+{% extends "layouts/dashboard.django" %}
+
+{% block dashboard %}
+{% include "domains/htmx/editdomain.htmx.django" %}
+{% endblock %}
diff --git a/templates/domains/edittld.django b/templates/domains/edittld.django
new file mode 100644
index 0000000..a466b56
--- /dev/null
+++ b/templates/domains/edittld.django
@@ -0,0 +1,5 @@
+{% extends "layouts/dashboard.django" %}
+
+{% block dashboard %}
+{% include "domains/htmx/edittld.htmx.django" %}
+{% endblock %}
diff --git a/templates/domains/htmx/domains.htmx.django b/templates/domains/htmx/domains.htmx.django
index 82a4820..11843f6 100644
--- a/templates/domains/htmx/domains.htmx.django
+++ b/templates/domains/htmx/domains.htmx.django
@@ -21,6 +21,12 @@
</div>
<p class="text-sm text-zinc-200">{{ domain.Name }}.{{ domain.TLD.Name }}</p>
</div>
+ <div class="flex items-center gap-4">
+ {% url "domains.manage.edit" id=domain.ID as edit_domain_path %}
+ <a href="{{ edit_domain_path }}" hx-get="{{ edit_domain_path }}" hx-target="#content" hx-swap="innerHTML" hx-push-url="true" class="text-xs text-zinc-600 hover:text-zinc-300 transition-colors duration-150">Edit</a>
+ {% url "domains.manage.delete" id=domain.ID as delete_domain_path %}
+ <button data-confirm-trigger data-confirm-title="Delete {{ domain.Name }}.{{ domain.TLD.Name }}" data-confirm-message="This domain and all associated data will be permanently removed. This action cannot be undone." data-confirm-action="{{ delete_domain_path }}" class="text-xs text-zinc-600 hover:text-red-400 transition-colors duration-150">Delete</button>
+ </div>
</div>
{% endfor %}
</div>
@@ -36,4 +42,25 @@
</div>
{% endif %}
</div>
-</div> \ No newline at end of file
+
+ <div id="confirm-modal" class="fixed inset-0 z-50 hidden">
+ <div class="absolute inset-0 bg-black/60 backdrop-blur-sm" data-confirm-backdrop></div>
+ <div class="flex items-center justify-center min-h-screen p-4">
+ <div class="relative glass rounded-xl glow-border w-full max-w-sm p-6 space-y-4">
+ <div class="flex items-center gap-3">
+ <div class="flex items-center justify-center w-10 h-10 rounded-xl bg-red-500/10">
+ <svg class="w-5 h-5 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
+ </svg>
+ </div>
+ <h3 id="confirm-title" class="text-sm font-medium text-zinc-100"></h3>
+ </div>
+ <p id="confirm-message" class="text-xs text-zinc-400 leading-relaxed"></p>
+ <div class="flex items-center justify-end gap-3 pt-2">
+ <button data-confirm-cancel class="px-4 py-2 text-xs text-zinc-400 hover:text-zinc-200 rounded-lg bg-surface-800 border border-white/[0.06] hover:border-white/[0.1] transition-all duration-150">Cancel</button>
+ <button id="confirm-action" class="px-4 py-2 text-xs text-white rounded-lg bg-red-500/80 hover:bg-red-500 border border-red-400/20 transition-all duration-150">Delete</button>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/templates/domains/htmx/editdomain.htmx.django b/templates/domains/htmx/editdomain.htmx.django
new file mode 100644
index 0000000..f26dbce
--- /dev/null
+++ b/templates/domains/htmx/editdomain.htmx.django
@@ -0,0 +1,49 @@
+<h1 id="page-title" class="text-sm font-medium text-zinc-100" hx-swap-oob="true">{{ PageTitle }}</h1>
+<div class="slide-up flex items-start justify-center pt-12">
+ <div class="glass rounded-xl glow-border w-full max-w-lg">
+ <div class="px-5 py-4 border-b border-white/[0.04]">
+ <h2 class="text-sm font-medium text-zinc-200">Edit Domain</h2>
+ </div>
+ <div class="p-5">
+ {% url "domains.manage.update" id=domain.ID as update_path %}
+ <form hx-put="{{ update_path }}" hx-swap="none" class="space-y-4">
+ <div>
+ <label class="block text-xs font-medium text-zinc-400 mb-1.5 ml-1">Domain Name</label>
+ <input type="text" name="name" value="{{ domain.Name }}" required autocomplete="off" class="input-field">
+ </div>
+ <div>
+ <label class="block text-xs font-medium text-zinc-400 mb-1.5 ml-1">TLD</label>
+ <div class="dropdown" data-dropdown>
+ <input type="hidden" name="tld_name" value="{{ domain.TLD.Name }}" data-dropdown-value>
+ <button type="button" data-dropdown-trigger class="input-field text-left flex items-center justify-between">
+ <span class="truncate" data-dropdown-label>.{{ domain.TLD.Name }}</span>
+ <svg class="w-4 h-4 text-zinc-500 shrink-0 ml-2 transition-transform duration-150" data-dropdown-chevron fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
+ </svg>
+ </button>
+ <div class="dropdown-menu" data-dropdown-menu>
+ <div class="p-2 border-b border-white/[0.04]">
+ <input type="text" placeholder="Search TLDs..." class="dropdown-search" data-dropdown-search>
+ </div>
+ <div class="dropdown-options" data-dropdown-options>
+ {% for tld in tlds %}
+ <button type="button" class="dropdown-option" data-dropdown-option data-value="{{ tld.Name }}" data-label=".{{ tld.Name }}">
+ <p class="text-sm text-zinc-200">.{{ tld.Name }}</p>
+ </button>
+ {% endfor %}
+ </div>
+ <div class="dropdown-empty hidden" data-dropdown-empty>
+ <p class="text-xs text-zinc-500 text-center py-3">No TLDs found</p>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="flex items-center gap-3 pt-2">
+ <button type="submit" class="btn-primary">Save Changes</button>
+ {% url "domains.manage" as domains_path %}
+ <a href="{{ domains_path }}" hx-get="{{ domains_path }}" hx-target="#content" hx-swap="innerHTML" hx-push-url="true" class="text-xs text-zinc-500 hover:text-zinc-300 transition-colors duration-150">Cancel</a>
+ </div>
+ </form>
+ </div>
+ </div>
+</div> \ No newline at end of file
diff --git a/templates/domains/htmx/edittld.htmx.django b/templates/domains/htmx/edittld.htmx.django
new file mode 100644
index 0000000..773578b
--- /dev/null
+++ b/templates/domains/htmx/edittld.htmx.django
@@ -0,0 +1,22 @@
+<h1 id="page-title" class="text-sm font-medium text-zinc-100" hx-swap-oob="true">{{ PageTitle }}</h1>
+<div class="slide-up flex items-start justify-center pt-12">
+ <div class="glass rounded-xl glow-border w-full max-w-lg">
+ <div class="px-5 py-4 border-b border-white/[0.04]">
+ <h2 class="text-sm font-medium text-zinc-200">Edit TLD</h2>
+ </div>
+ <div class="p-5">
+ {% url "domains.tlds.update" id=tld.ID as update_path %}
+ <form hx-put="{{ update_path }}" hx-swap="none" class="space-y-4">
+ <div>
+ <label class="block text-xs font-medium text-zinc-400 mb-1.5 ml-1">TLD Name</label>
+ <input type="text" name="name" value="{{ tld.Name }}" required autocomplete="off" class="input-field">
+ </div>
+ <div class="flex items-center gap-3 pt-2">
+ <button type="submit" class="btn-primary">Save Changes</button>
+ {% url "domains.tlds" as tlds_path %}
+ <a href="{{ tlds_path }}" hx-get="{{ tlds_path }}" hx-target="#content" hx-swap="innerHTML" hx-push-url="true" class="text-xs text-zinc-500 hover:text-zinc-300 transition-colors duration-150">Cancel</a>
+ </div>
+ </form>
+ </div>
+ </div>
+</div> \ No newline at end of file
diff --git a/templates/domains/htmx/tlds.htmx.django b/templates/domains/htmx/tlds.htmx.django
index 0f4972d..13705d8 100644
--- a/templates/domains/htmx/tlds.htmx.django
+++ b/templates/domains/htmx/tlds.htmx.django
@@ -29,8 +29,12 @@
</div>
</div>
{% if not tld.IsDefault %}
- {% url "domains.tlds.delete" name=tld.Name as delete_tld_path %}
- <button data-confirm-trigger data-confirm-title="Delete .{{ tld.Name }}" data-confirm-message="This TLD and all associated data will be permanently removed. This action cannot be undone." data-confirm-action="{{ delete_tld_path }}" class="text-xs text-zinc-600 hover:text-red-400 transition-colors duration-150">Delete</button>
+ <div class="flex items-center gap-4">
+ {% url "domains.tlds.edit" id=tld.ID as edit_tld_path %}
+ <a href="{{ edit_tld_path }}" hx-get="{{ edit_tld_path }}" hx-target="#content" hx-swap="innerHTML" hx-push-url="true" class="text-xs text-zinc-600 hover:text-zinc-300 transition-colors duration-150">Edit</a>
+ {% url "domains.tlds.delete" name=tld.Name as delete_tld_path %}
+ <button data-confirm-trigger data-confirm-title="Delete .{{ tld.Name }}" data-confirm-message="This TLD and all associated data will be permanently removed. This action cannot be undone." data-confirm-action="{{ delete_tld_path }}" class="text-xs text-zinc-600 hover:text-red-400 transition-colors duration-150">Delete</button>
+ </div>
{% endif %}
</div>
{% endfor %}
diff --git a/templates/layouts/base.django b/templates/layouts/base.django
index c858bce..a5212f9 100644
--- a/templates/layouts/base.django
+++ b/templates/layouts/base.django
@@ -15,9 +15,18 @@
</div>
{% block content %}{% endblock %}
<script src="/static/js/htmx.min.js"></script>
+ <script>
+ document.body.addEventListener("htmx:beforeOnLoad", function(event) {
+ if (event.detail.xhr.status >= 400) {
+ event.detail.shouldSwap = true;
+ event.detail.isError = false;
+ }
+ });
+ </script>
<script src="/static/js/sidebar.js"></script>
<script src="/static/js/dropdown.js"></script>
<script src="/static/js/confirm.js"></script>
+ <script src="/static/js/alert.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>
diff --git a/templates/layouts/dashboard.django b/templates/layouts/dashboard.django
index b8713a6..26aad2e 100644
--- a/templates/layouts/dashboard.django
+++ b/templates/layouts/dashboard.django
@@ -11,5 +11,7 @@
{% block dashboard %}{% endblock %}
</main>
</div>
+
+ <div id="alert-container" class="fixed top-4 right-4 z-[100] flex flex-col gap-2"></div>
</div>
{% endblock %}
diff --git a/templates/mail/editmailbox.django b/templates/mail/editmailbox.django
new file mode 100644
index 0000000..e573921
--- /dev/null
+++ b/templates/mail/editmailbox.django
@@ -0,0 +1,5 @@
+{% extends "layouts/dashboard.django" %}
+
+{% block dashboard %}
+{% include "mail/htmx/editmailbox.htmx.django" %}
+{% endblock %}
diff --git a/templates/mail/edituser.django b/templates/mail/edituser.django
new file mode 100644
index 0000000..8599b50
--- /dev/null
+++ b/templates/mail/edituser.django
@@ -0,0 +1,5 @@
+{% extends "layouts/dashboard.django" %}
+
+{% block dashboard %}
+{% include "mail/htmx/edituser.htmx.django" %}
+{% endblock %}
diff --git a/templates/mail/htmx/editmailbox.htmx.django b/templates/mail/htmx/editmailbox.htmx.django
new file mode 100644
index 0000000..b25563f
--- /dev/null
+++ b/templates/mail/htmx/editmailbox.htmx.django
@@ -0,0 +1,139 @@
+<h1 id="page-title" class="text-sm font-medium text-zinc-100" hx-swap-oob="true">{{ PageTitle }}</h1>
+<div class="slide-up space-y-6 max-w-lg mx-auto pt-12">
+ <div class="glass rounded-xl glow-border">
+ <div class="px-5 py-4 border-b border-white/[0.04]">
+ <h2 class="text-sm font-medium text-zinc-200">Edit Mailbox</h2>
+ </div>
+ <div class="p-5">
+ {% url "mail.mailboxes.update" id=mailbox.ID as update_path %}
+ <form hx-put="{{ update_path }}" hx-swap="none" class="space-y-4">
+ <div>
+ <label class="block text-xs font-medium text-zinc-400 mb-1.5 ml-1">Address</label>
+ <input type="text" value="{{ mailbox.Address }}" disabled class="input-field opacity-50 cursor-not-allowed">
+ </div>
+ <div>
+ <label class="block text-xs font-medium text-zinc-400 mb-1.5 ml-1">Owner</label>
+ <div class="dropdown" data-dropdown>
+ <input type="hidden" name="user_id" value="{{ mailbox.UserID }}" data-dropdown-value>
+ <button type="button" class="input-field text-left flex items-center justify-between" data-dropdown-trigger>
+ <span class="truncate" data-dropdown-label>{{ mailbox.User.DisplayName }} ({{ mailbox.User.Username }})</span>
+ <svg class="w-4 h-4 text-zinc-500 shrink-0 ml-2 transition-transform duration-150" data-dropdown-chevron fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
+ </svg>
+ </button>
+ <div class="dropdown-menu" data-dropdown-menu>
+ <div class="p-2 border-b border-white/[0.04]">
+ <input type="text" placeholder="Search users..." class="dropdown-search" data-dropdown-search>
+ </div>
+ <div class="dropdown-options" data-dropdown-options>
+ {% for user in users %}
+ <button type="button" class="dropdown-option" data-dropdown-option data-value="{{ user.ID }}" data-label="{{ user.DisplayName }} ({{ user.Username }})">
+ <div>
+ <p class="text-sm text-zinc-200">{{ user.DisplayName }}</p>
+ <p class="text-xs text-zinc-500">{{ user.Username }}</p>
+ </div>
+ </button>
+ {% endfor %}
+ </div>
+ <div class="dropdown-empty hidden" data-dropdown-empty>
+ <p class="text-xs text-zinc-500 text-center py-3">No users found</p>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="flex items-center gap-3 pt-2">
+ <button type="submit" class="btn-primary">Save Changes</button>
+ {% url "mail.mailboxes" as mailboxes_path %}
+ <a href="{{ mailboxes_path }}" hx-get="{{ mailboxes_path }}" hx-target="#content" hx-swap="innerHTML" hx-push-url="true" class="text-xs text-zinc-500 hover:text-zinc-300 transition-colors duration-150">Cancel</a>
+ </div>
+ </form>
+ </div>
+ </div>
+
+ <div class="glass rounded-xl glow-border">
+ <div class="flex items-center justify-between px-5 py-4 border-b border-white/[0.04]">
+ <h2 class="text-sm font-medium text-zinc-200">Aliases</h2>
+ <span class="text-xs text-zinc-600">{{ aliases|length }} total</span>
+ </div>
+ {% if aliases %}
+ <div class="divide-y divide-white/[0.04]">
+ {% for alias in aliases %}
+ <div class="flex items-center justify-between px-5 py-3">
+ <div class="flex items-center gap-3">
+ <div class="flex items-center justify-center w-8 h-8 rounded-lg bg-purple-500/10">
+ <svg class="w-4 h-4 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244" />
+ </svg>
+ </div>
+ <p class="text-sm text-zinc-200">{{ alias.SourceAddress }}</p>
+ </div>
+ {% url "mail.aliases.delete" id=mailbox.ID alias_id=alias.ID as delete_alias_path %}
+ <button data-confirm-trigger data-confirm-title="Delete {{ alias.SourceAddress }}" data-confirm-message="This alias will be permanently removed. Emails sent to this address will no longer be forwarded." data-confirm-action="{{ delete_alias_path }}" class="text-xs text-zinc-600 hover:text-red-400 transition-colors duration-150">Remove</button>
+ </div>
+ {% endfor %}
+ </div>
+ {% else %}
+ <div class="flex flex-col items-center justify-center py-8 text-center">
+ <p class="text-xs text-zinc-500">No aliases configured</p>
+ </div>
+ {% endif %}
+ <div class="px-5 py-4 border-t border-white/[0.04]">
+ {% url "mail.aliases.create" id=mailbox.ID as create_alias_path %}
+ <form hx-post="{{ create_alias_path }}" hx-swap="none" class="space-y-4">
+ <div>
+ <label class="block text-xs font-medium text-zinc-400 mb-1.5 ml-1">New Alias</label>
+ <div class="flex items-center gap-2">
+ <input type="text" name="local_part" required autocomplete="off" placeholder="alias" class="input-field flex-1">
+ <span class="text-sm text-zinc-500">@</span>
+ <div class="dropdown flex-1" data-dropdown>
+ <input type="hidden" name="domain_id" data-dropdown-value>
+ <button type="button" class="input-field text-left flex items-center justify-between" data-dropdown-trigger>
+ <span class="truncate" data-dropdown-label>Select domain</span>
+ <svg class="w-4 h-4 text-zinc-500 shrink-0 ml-2 transition-transform duration-150" data-dropdown-chevron fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
+ </svg>
+ </button>
+ <div class="dropdown-menu" data-dropdown-menu>
+ <div class="p-2 border-b border-white/[0.04]">
+ <input type="text" placeholder="Search domains..." class="dropdown-search" data-dropdown-search>
+ </div>
+ <div class="dropdown-options" data-dropdown-options>
+ {% for domain in domains %}
+ <button type="button" class="dropdown-option" data-dropdown-option data-value="{{ domain.ID }}" data-label="{{ domain.Name }}.{{ domain.TLD.Name }}">
+ <p class="text-sm text-zinc-200">{{ domain.Name }}.{{ domain.TLD.Name }}</p>
+ </button>
+ {% endfor %}
+ </div>
+ <div class="dropdown-empty hidden" data-dropdown-empty>
+ <p class="text-xs text-zinc-500 text-center py-3">No domains found</p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <button type="submit" class="btn-primary">Add Alias</button>
+ </form>
+ </div>
+ </div>
+
+ <div id="confirm-modal" class="fixed inset-0 z-50 hidden">
+ <div class="absolute inset-0 bg-black/60 backdrop-blur-sm" data-confirm-backdrop></div>
+ <div class="flex items-center justify-center min-h-screen p-4">
+ <div class="relative glass rounded-xl glow-border w-full max-w-sm p-6 space-y-4">
+ <div class="flex items-center gap-3">
+ <div class="flex items-center justify-center w-10 h-10 rounded-xl bg-red-500/10">
+ <svg class="w-5 h-5 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
+ </svg>
+ </div>
+ <h3 id="confirm-title" class="text-sm font-medium text-zinc-100"></h3>
+ </div>
+ <p id="confirm-message" class="text-xs text-zinc-400 leading-relaxed"></p>
+ <div class="flex items-center justify-end gap-3 pt-2">
+ <button data-confirm-cancel class="px-4 py-2 text-xs text-zinc-400 hover:text-zinc-200 rounded-lg bg-surface-800 border border-white/[0.06] hover:border-white/[0.1] transition-all duration-150">Cancel</button>
+ <button id="confirm-action" class="px-4 py-2 text-xs text-white rounded-lg bg-red-500/80 hover:bg-red-500 border border-red-400/20 transition-all duration-150">Delete</button>
+ </div>
+ </div>
+ </div>
+ </div>
+</div> \ No newline at end of file
diff --git a/templates/mail/htmx/edituser.htmx.django b/templates/mail/htmx/edituser.htmx.django
new file mode 100644
index 0000000..8b22afc
--- /dev/null
+++ b/templates/mail/htmx/edituser.htmx.django
@@ -0,0 +1,26 @@
+<h1 id="page-title" class="text-sm font-medium text-zinc-100" hx-swap-oob="true">{{ PageTitle }}</h1>
+<div class="slide-up flex items-start justify-center pt-12">
+ <div class="glass rounded-xl glow-border w-full max-w-lg">
+ <div class="px-5 py-4 border-b border-white/[0.04]">
+ <h2 class="text-sm font-medium text-zinc-200">Edit User</h2>
+ </div>
+ <div class="p-5">
+ {% url "mail.users.update" id=user.ID as update_path %}
+ <form hx-put="{{ update_path }}" hx-swap="none" class="space-y-4">
+ <div>
+ <label class="block text-xs font-medium text-zinc-400 mb-1.5 ml-1">Username</label>
+ <input type="text" value="{{ user.Username }}" disabled class="input-field opacity-50 cursor-not-allowed">
+ </div>
+ <div>
+ <label class="block text-xs font-medium text-zinc-400 mb-1.5 ml-1">Display Name</label>
+ <input type="text" name="display_name" value="{{ user.DisplayName }}" required autocomplete="off" class="input-field">
+ </div>
+ <div class="flex items-center gap-3 pt-2">
+ <button type="submit" class="btn-primary">Save Changes</button>
+ {% url "mail.users" as users_path %}
+ <a href="{{ users_path }}" hx-get="{{ users_path }}" hx-target="#content" hx-swap="innerHTML" hx-push-url="true" class="text-xs text-zinc-500 hover:text-zinc-300 transition-colors duration-150">Cancel</a>
+ </div>
+ </form>
+ </div>
+ </div>
+</div> \ No newline at end of file
diff --git a/templates/mail/htmx/mailboxes.htmx.django b/templates/mail/htmx/mailboxes.htmx.django
index 96094f3..c0200af 100644
--- a/templates/mail/htmx/mailboxes.htmx.django
+++ b/templates/mail/htmx/mailboxes.htmx.django
@@ -12,20 +12,28 @@
{% if items %}
<div class="divide-y divide-white/[0.04]">
{% for mailbox in items %}
- {% url "mail.mailbox" address=mailbox.Address as mailbox_path %}
- <a href="{{ mailbox_path }}" hx-get="{{ mailbox_path }}" hx-target="#content" hx-swap="innerHTML" hx-push-url="true" class="flex items-center justify-between px-5 py-3 hover:bg-white/[0.02] transition-colors duration-150">
+ <div class="flex items-center justify-between px-5 py-3">
<div class="flex items-center gap-3">
- <div class="flex items-center justify-center w-8 h-8 rounded-lg bg-accent-500/10">
- <svg class="w-4 h-4 text-accent-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
- <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 13.5h3.86a2.25 2.25 0 0 1 2.012 1.244l.256.512a2.25 2.25 0 0 0 2.013 1.244h2.21a2.25 2.25 0 0 0 2.013-1.244l.256-.512a2.25 2.25 0 0 1 2.013-1.244h3.859m-19.5.338V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18v-4.162c0-.224-.034-.447-.1-.661L19.24 5.338a2.25 2.25 0 0 0-2.15-1.588H6.911a2.25 2.25 0 0 0-2.15 1.588L2.35 13.177a2.25 2.25 0 0 0-.1.661Z" />
- </svg>
- </div>
- <div>
- <p class="text-sm text-zinc-200">{{ mailbox.Address }}</p>
- <p class="text-xs text-zinc-600">{{ mailbox.User.DisplayName }}</p>
- </div>
+ {% url "mail.mailbox" address=mailbox.Address as mailbox_path %}
+ <a href="{{ mailbox_path }}" hx-get="{{ mailbox_path }}" hx-target="#content" hx-swap="innerHTML" hx-push-url="true" class="flex items-center gap-3 hover:opacity-80 transition-opacity duration-150">
+ <div class="flex items-center justify-center w-8 h-8 rounded-lg bg-accent-500/10">
+ <svg class="w-4 h-4 text-accent-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 13.5h3.86a2.25 2.25 0 0 1 2.012 1.244l.256.512a2.25 2.25 0 0 0 2.013 1.244h2.21a2.25 2.25 0 0 0 2.013-1.244l.256-.512a2.25 2.25 0 0 1 2.013-1.244h3.859m-19.5.338V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18v-4.162c0-.224-.034-.447-.1-.661L19.24 5.338a2.25 2.25 0 0 0-2.15-1.588H6.911a2.25 2.25 0 0 0-2.15 1.588L2.35 13.177a2.25 2.25 0 0 0-.1.661Z" />
+ </svg>
+ </div>
+ <div>
+ <p class="text-sm text-zinc-200">{{ mailbox.Address }}</p>
+ <p class="text-xs text-zinc-600">{{ mailbox.User.DisplayName }}</p>
+ </div>
+ </a>
+ </div>
+ <div class="flex items-center gap-4">
+ {% url "mail.mailboxes.edit" id=mailbox.ID as edit_mailbox_path %}
+ <a href="{{ edit_mailbox_path }}" hx-get="{{ edit_mailbox_path }}" hx-target="#content" hx-swap="innerHTML" hx-push-url="true" class="text-xs text-zinc-600 hover:text-zinc-300 transition-colors duration-150">Edit</a>
+ {% url "mail.mailboxes.delete" id=mailbox.ID as delete_mailbox_path %}
+ <button data-confirm-trigger data-confirm-title="Delete {{ mailbox.Address }}" data-confirm-message="This mailbox and all its emails will be permanently removed. This action cannot be undone." data-confirm-action="{{ delete_mailbox_path }}" class="text-xs text-zinc-600 hover:text-red-400 transition-colors duration-150">Delete</button>
</div>
- </a>
+ </div>
{% endfor %}
</div>
{% else %}
@@ -36,8 +44,29 @@
</svg>
</div>
<p class="text-sm text-zinc-400">No mailboxes yet</p>
- <p class="mt-1 text-xs text-zinc-600">Send an email to start receiving mail</p>
+ <p class="mt-1 text-xs text-zinc-600">Create a mailbox to get started</p>
</div>
{% endif %}
</div>
+
+ <div id="confirm-modal" class="fixed inset-0 z-50 hidden">
+ <div class="absolute inset-0 bg-black/60 backdrop-blur-sm" data-confirm-backdrop></div>
+ <div class="flex items-center justify-center min-h-screen p-4">
+ <div class="relative glass rounded-xl glow-border w-full max-w-sm p-6 space-y-4">
+ <div class="flex items-center gap-3">
+ <div class="flex items-center justify-center w-10 h-10 rounded-xl bg-red-500/10">
+ <svg class="w-5 h-5 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
+ </svg>
+ </div>
+ <h3 id="confirm-title" class="text-sm font-medium text-zinc-100"></h3>
+ </div>
+ <p id="confirm-message" class="text-xs text-zinc-400 leading-relaxed"></p>
+ <div class="flex items-center justify-end gap-3 pt-2">
+ <button data-confirm-cancel class="px-4 py-2 text-xs text-zinc-400 hover:text-zinc-200 rounded-lg bg-surface-800 border border-white/[0.06] hover:border-white/[0.1] transition-all duration-150">Cancel</button>
+ <button id="confirm-action" class="px-4 py-2 text-xs text-white rounded-lg bg-red-500/80 hover:bg-red-500 border border-red-400/20 transition-all duration-150">Delete</button>
+ </div>
+ </div>
+ </div>
+ </div>
</div>
diff --git a/templates/mail/htmx/users.htmx.django b/templates/mail/htmx/users.htmx.django
index 7ec040c..8929907 100644
--- a/templates/mail/htmx/users.htmx.django
+++ b/templates/mail/htmx/users.htmx.django
@@ -24,7 +24,13 @@
<p class="text-xs text-zinc-600">{{ user.Username }}</p>
</div>
</div>
- <span class="text-xs text-zinc-600">{{ user.Mailboxes|length }} mailbox{{ user.Mailboxes|length|pluralize:"es" }}</span>
+ <div class="flex items-center gap-4">
+ <span class="text-xs text-zinc-600">{{ user.Mailboxes|length }} mailbox{{ user.Mailboxes|length|pluralize:"es" }}</span>
+ {% url "mail.users.edit" id=user.ID as edit_user_path %}
+ <a href="{{ edit_user_path }}" hx-get="{{ edit_user_path }}" hx-target="#content" hx-swap="innerHTML" hx-push-url="true" class="text-xs text-zinc-600 hover:text-zinc-300 transition-colors duration-150">Edit</a>
+ {% url "mail.users.delete" id=user.ID as delete_user_path %}
+ <button data-confirm-trigger data-confirm-title="Delete {{ user.DisplayName }}" data-confirm-message="This user and all associated data will be permanently removed. This action cannot be undone." data-confirm-action="{{ delete_user_path }}" class="text-xs text-zinc-600 hover:text-red-400 transition-colors duration-150">Delete</button>
+ </div>
</div>
{% endfor %}
</div>
@@ -36,8 +42,29 @@
</svg>
</div>
<p class="text-sm text-zinc-400">No users yet</p>
- <p class="mt-1 text-xs text-zinc-600">Users are created when emails are received</p>
+ <p class="mt-1 text-xs text-zinc-600">Create a user to get started</p>
</div>
{% endif %}
</div>
+
+ <div id="confirm-modal" class="fixed inset-0 z-50 hidden">
+ <div class="absolute inset-0 bg-black/60 backdrop-blur-sm" data-confirm-backdrop></div>
+ <div class="flex items-center justify-center min-h-screen p-4">
+ <div class="relative glass rounded-xl glow-border w-full max-w-sm p-6 space-y-4">
+ <div class="flex items-center gap-3">
+ <div class="flex items-center justify-center w-10 h-10 rounded-xl bg-red-500/10">
+ <svg class="w-5 h-5 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
+ </svg>
+ </div>
+ <h3 id="confirm-title" class="text-sm font-medium text-zinc-100"></h3>
+ </div>
+ <p id="confirm-message" class="text-xs text-zinc-400 leading-relaxed"></p>
+ <div class="flex items-center justify-end gap-3 pt-2">
+ <button data-confirm-cancel class="px-4 py-2 text-xs text-zinc-400 hover:text-zinc-200 rounded-lg bg-surface-800 border border-white/[0.06] hover:border-white/[0.1] transition-all duration-150">Cancel</button>
+ <button id="confirm-action" class="px-4 py-2 text-xs text-white rounded-lg bg-red-500/80 hover:bg-red-500 border border-red-400/20 transition-all duration-150">Delete</button>
+ </div>
+ </div>
+ </div>
+ </div>
</div>
diff --git a/templates/partials/alert.django b/templates/partials/alert.django
new file mode 100644
index 0000000..c2c8052
--- /dev/null
+++ b/templates/partials/alert.django
@@ -0,0 +1,17 @@
+<div id="alert-container" class="fixed top-4 right-4 z-[100] flex flex-col gap-2" hx-swap-oob="true">
+ <div class="alert-toast" data-alert>
+ <div class="flex items-center gap-3">
+ <div class="flex items-center justify-center w-8 h-8 rounded-lg bg-red-500/10 shrink-0">
+ <svg class="w-4 h-4 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z" />
+ </svg>
+ </div>
+ <p class="text-sm text-zinc-200">{{ ErrorMessage }}</p>
+ <button data-alert-dismiss class="ml-auto text-zinc-500 hover:text-zinc-300 transition-colors duration-150 shrink-0">
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
+ </svg>
+ </button>
+ </div>
+ </div>
+</div>