aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBobby <[email protected]>2026-03-07 19:11:59 +0530
committerBobby <[email protected]>2026-03-07 19:11:59 +0530
commit547384c41181c034a5eaf340c5e569d36eb013be (patch)
tree345341ac0df478fe51d11eeb6c45f2265afd7619
parent96c136f046d78c51210927e61483a36a220fedcb (diff)
downloaddove-547384c41181c034a5eaf340c5e569d36eb013be.tar.xz
dove-547384c41181c034a5eaf340c5e569d36eb013be.zip
feat: implement mailbox and user creation features with validation and dropdowns
-rw-r--r--controllers/mailbox.go24
-rw-r--r--controllers/user.go24
-rw-r--r--messages/mailbox.go9
-rw-r--r--messages/user.go8
-rw-r--r--pages/mailboxes.go5
-rw-r--r--pages/users.go5
-rw-r--r--repositories/user.go16
-rw-r--r--router/dashboard.go5
-rw-r--r--services/mailbox.go42
-rw-r--r--services/user.go38
-rw-r--r--static/css/tailwind.css110
-rw-r--r--static/js/dropdown.js62
-rw-r--r--templates/dashboard/htmx/mailboxes.htmx.django6
-rw-r--r--templates/dashboard/htmx/newmailbox.htmx.django52
-rw-r--r--templates/dashboard/htmx/newuser.htmx.django26
-rw-r--r--templates/dashboard/htmx/users.htmx.django6
-rw-r--r--templates/dashboard/newmailbox.django5
-rw-r--r--templates/dashboard/newuser.django5
-rw-r--r--templates/layouts/base.django1
-rw-r--r--types/mailbox.go11
-rw-r--r--types/user.go6
21 files changed, 462 insertions, 4 deletions
diff --git a/controllers/mailbox.go b/controllers/mailbox.go
new file mode 100644
index 0000000..0878bd3
--- /dev/null
+++ b/controllers/mailbox.go
@@ -0,0 +1,24 @@
+package controllers
+
+import (
+ "dove/services"
+ "dove/types"
+ "dove/utils/meta"
+ "dove/utils/shortcuts"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+func CreateMailbox(context *fiber.Ctx) error {
+ body, parseError := meta.Body[types.CreateMailboxRequest](context)
+ if parseError != nil {
+ return shortcuts.BadRequest(context, parseError)
+ }
+
+ serviceError := services.CreateMailbox(body)
+ if serviceError != nil {
+ return shortcuts.HandleError(context, serviceError)
+ }
+
+ return shortcuts.Redirect(context, "dashboard.mailboxes")
+} \ No newline at end of file
diff --git a/controllers/user.go b/controllers/user.go
new file mode 100644
index 0000000..ea2008b
--- /dev/null
+++ b/controllers/user.go
@@ -0,0 +1,24 @@
+package controllers
+
+import (
+ "dove/services"
+ "dove/types"
+ "dove/utils/meta"
+ "dove/utils/shortcuts"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+func CreateUser(context *fiber.Ctx) error {
+ body, parseError := meta.Body[types.CreateUserRequest](context)
+ if parseError != nil {
+ return shortcuts.BadRequest(context, parseError)
+ }
+
+ serviceError := services.CreateUser(body)
+ if serviceError != nil {
+ return shortcuts.HandleError(context, serviceError)
+ }
+
+ return shortcuts.Redirect(context, "dashboard.users")
+} \ No newline at end of file
diff --git a/messages/mailbox.go b/messages/mailbox.go
index e193bf7..9ead4f0 100644
--- a/messages/mailbox.go
+++ b/messages/mailbox.go
@@ -1,6 +1,11 @@
package messages
const (
- MailboxAutoCreated = "Auto-created mailbox for %s."
- MailboxNotRegistered = "No registered mailbox or alias for address: %s"
+ MailboxAutoCreated = "Auto-created mailbox for %s."
+ MailboxNotRegistered = "No registered mailbox or alias for address: %s"
+ MailboxAddressRequired = "Mailbox address is required."
+ MailboxUserRequired = "A user must be selected for the mailbox."
+ MailboxAlreadyExists = "A mailbox with this address already exists."
+ MailboxCreationFailed = "Failed to create mailbox."
+ MailboxUserNotFound = "The selected user does not exist."
)
diff --git a/messages/user.go b/messages/user.go
new file mode 100644
index 0000000..5c3fe93
--- /dev/null
+++ b/messages/user.go
@@ -0,0 +1,8 @@
+package messages
+
+const (
+ UserUsernameRequired = "Username is required."
+ UserDisplayNameRequired = "Display name is required."
+ UserAlreadyExists = "A user with this username already exists."
+ UserCreationFailed = "Failed to create user."
+)
diff --git a/pages/mailboxes.go b/pages/mailboxes.go
index 317d9c9..611c2cb 100644
--- a/pages/mailboxes.go
+++ b/pages/mailboxes.go
@@ -8,6 +8,11 @@ import (
"github.com/gofiber/fiber/v2"
)
+func NewMailbox(context *fiber.Ctx) error {
+ meta.SetPageTitle(context, "New Mailbox")
+ return shortcuts.Render(context, "dashboard/newmailbox", services.MailboxFormData())
+}
+
func Mailboxes(context *fiber.Ctx) error {
meta.SetPageTitle(context, "Mailboxes")
diff --git a/pages/users.go b/pages/users.go
index 723b544..1fc6fcc 100644
--- a/pages/users.go
+++ b/pages/users.go
@@ -8,6 +8,11 @@ import (
"github.com/gofiber/fiber/v2"
)
+func NewUser(context *fiber.Ctx) error {
+ meta.SetPageTitle(context, "New User")
+ return shortcuts.Render(context, "dashboard/newuser", nil)
+}
+
func Users(context *fiber.Ctx) error {
meta.SetPageTitle(context, "Users")
diff --git a/repositories/user.go b/repositories/user.go
index 0147f55..7a3e5e6 100644
--- a/repositories/user.go
+++ b/repositories/user.go
@@ -8,6 +8,16 @@ import (
"gorm.io/gorm"
)
+func FindUserByID(userID uint) *models.User {
+ var user models.User
+ result := database.DB.First(&user, userID)
+ if result.Error == gorm.ErrRecordNotFound {
+ return nil
+ }
+
+ return &user
+}
+
func FindUserByUsername(username string) *models.User {
var user models.User
result := database.DB.Where("username = ?", username).First(&user)
@@ -39,6 +49,12 @@ func ListUsers(pagination meta.Pagination, sorting meta.Sorting, search string)
return users, total
}
+func AllUsers() []models.User {
+ var users []models.User
+ database.DB.Order("username ASC").Find(&users)
+ return users
+}
+
func CountUsers() int64 {
var count int64
database.DB.Model(&models.User{}).Count(&count)
diff --git a/router/dashboard.go b/router/dashboard.go
index 2a593dc..ff7aa52 100644
--- a/router/dashboard.go
+++ b/router/dashboard.go
@@ -1,6 +1,7 @@
package router
import (
+ "dove/controllers"
"dove/enums"
"dove/pages"
"dove/utils/auth"
@@ -12,6 +13,10 @@ func init() {
urls.Path(enums.Get, "/", auth.RequireAuthentication(pages.Dashboard), "index")
urls.Path(enums.Get, "/mailboxes", auth.RequireAuthentication(pages.Mailboxes), "mailboxes")
+ urls.Path(enums.Get, "/mailboxes/new", auth.RequireAuthentication(pages.NewMailbox), "mailboxes.new")
+ urls.Path(enums.Post, "/mailboxes", auth.RequireAuthentication(controllers.CreateMailbox), "mailboxes.create")
urls.Path(enums.Get, "/mailboxes/:address", auth.RequireAuthentication(pages.Mailbox), "mailbox")
urls.Path(enums.Get, "/users", auth.RequireAuthentication(pages.Users), "users")
+ urls.Path(enums.Get, "/users/new", auth.RequireAuthentication(pages.NewUser), "users.new")
+ urls.Path(enums.Post, "/users", auth.RequireAuthentication(controllers.CreateUser), "users.create")
} \ No newline at end of file
diff --git a/services/mailbox.go b/services/mailbox.go
index b84e19b..d1e954d 100644
--- a/services/mailbox.go
+++ b/services/mailbox.go
@@ -1,10 +1,15 @@
package services
import (
+ "strings"
+
+ "dove/enums"
+ "dove/messages"
"dove/models"
"dove/repositories"
"dove/types"
"dove/utils/meta"
+ "dove/utils/shortcuts"
)
func ListMailboxes(pagination meta.Pagination, sorting meta.Sorting, search string) types.PaginatedResponse {
@@ -12,6 +17,43 @@ func ListMailboxes(pagination meta.Pagination, sorting meta.Sorting, search stri
return pagination.Response(mailboxes, total)
}
+func MailboxFormData() types.MailboxFormResponse {
+ return types.MailboxFormResponse{
+ Users: repositories.AllUsers(),
+ }
+}
+
+func CreateMailbox(request types.CreateMailboxRequest) *types.ServiceError {
+ address := strings.TrimSpace(request.Address)
+
+ if address == "" {
+ return shortcuts.ServiceError(enums.BadRequest, messages.MailboxAddressRequired)
+ }
+
+ if request.UserID == 0 {
+ return shortcuts.ServiceError(enums.BadRequest, messages.MailboxUserRequired)
+ }
+
+ if repositories.FindUserByID(request.UserID) == nil {
+ return shortcuts.ServiceError(enums.Unprocessable, messages.MailboxUserNotFound)
+ }
+
+ if repositories.FindMailboxByAddress(address) != nil {
+ return shortcuts.ServiceError(enums.Unprocessable, messages.MailboxAlreadyExists)
+ }
+
+ mailbox := &models.Mailbox{
+ Address: address,
+ UserID: request.UserID,
+ }
+
+ if createError := repositories.CreateMailbox(mailbox); createError != nil {
+ return shortcuts.ServiceError(enums.Internal, messages.MailboxCreationFailed)
+ }
+
+ return nil
+}
+
func ResolveMailboxes(recipientAddresses []string) []models.Mailbox {
var resolvedMailboxes []models.Mailbox
diff --git a/services/user.go b/services/user.go
index 392aaa1..b2d43cf 100644
--- a/services/user.go
+++ b/services/user.go
@@ -1,12 +1,50 @@
package services
import (
+ "strings"
+
+ "dove/enums"
+ "dove/messages"
+ "dove/models"
"dove/repositories"
"dove/types"
"dove/utils/meta"
+ "dove/utils/shortcuts"
)
func ListUsers(pagination meta.Pagination, sorting meta.Sorting, search string) types.PaginatedResponse {
users, total := repositories.ListUsers(pagination, sorting, search)
return pagination.Response(users, total)
}
+
+func CreateUser(request types.CreateUserRequest) *types.ServiceError {
+ username := strings.TrimSpace(request.Username)
+ displayName := strings.TrimSpace(request.DisplayName)
+
+ if username == "" {
+ return shortcuts.ServiceError(enums.BadRequest, messages.UserUsernameRequired)
+ }
+
+ if displayName == "" {
+ return shortcuts.ServiceError(enums.BadRequest, messages.UserDisplayNameRequired)
+ }
+
+ if repositories.FindUserByUsername(username) != nil {
+ return shortcuts.ServiceError(enums.Unprocessable, messages.UserAlreadyExists)
+ }
+
+ user := &models.User{
+ Username: username,
+ DisplayName: displayName,
+ }
+
+ if createError := repositories.CreateUser(user); createError != nil {
+ return shortcuts.ServiceError(enums.Internal, messages.UserCreationFailed)
+ }
+
+ return nil
+}
+
+func AllUsers() []models.User {
+ return repositories.AllUsers()
+}
diff --git a/static/css/tailwind.css b/static/css/tailwind.css
index 477fdcd..a325d92 100644
--- a/static/css/tailwind.css
+++ b/static/css/tailwind.css
@@ -83,6 +83,31 @@
}
}
+@utility btn-small {
+ padding: 0.375rem 0.75rem;
+ border-radius: 0.5rem;
+ font-size: 0.75rem;
+ font-weight: 500;
+ color: white;
+ background: linear-gradient(
+ 135deg,
+ var(--color-accent-500),
+ var(--color-accent-600)
+ );
+ outline: none;
+ transition: all 0.2s ease;
+ cursor: pointer;
+ &:hover {
+ box-shadow:
+ 0 0 20px rgba(99, 102, 241, 0.3),
+ 0 2px 10px rgba(99, 102, 241, 0.2);
+ transform: translateY(-1px);
+ }
+ &:active {
+ transform: translateY(0);
+ }
+}
+
@utility fade-in {
animation: fadeIn 0.4s ease-out;
}
@@ -198,6 +223,91 @@
background: rgba(255, 255, 255, 0.06);
}
+.dropdown {
+ position: relative;
+}
+
+.dropdown-menu {
+ display: none;
+ position: absolute;
+ top: calc(100% + 0.375rem);
+ left: 0;
+ right: 0;
+ z-index: 50;
+ border-radius: 0.75rem;
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ background: var(--color-surface-800);
+ box-shadow:
+ 0 10px 40px rgba(0, 0, 0, 0.4),
+ 0 0 20px rgba(0, 0, 0, 0.2);
+ overflow: hidden;
+}
+
+.dropdown.open .dropdown-menu {
+ display: block;
+}
+
+.dropdown.open [data-dropdown-chevron] {
+ transform: rotate(180deg);
+}
+
+.dropdown-search {
+ width: 100%;
+ padding: 0.5rem 0.75rem;
+ border: 1px solid rgba(255, 255, 255, 0.06);
+ border-radius: 0.5rem;
+ background: var(--color-surface-900);
+ color: #e4e4e7;
+ font-size: 0.8125rem;
+ outline: none;
+ transition: border-color 0.2s ease;
+}
+
+.dropdown-search::placeholder {
+ color: #52525b;
+}
+
+.dropdown-search:focus {
+ border-color: var(--color-accent-500);
+}
+
+.dropdown-options {
+ max-height: 12rem;
+ overflow-y: auto;
+ padding: 0.25rem;
+}
+
+.dropdown-options::-webkit-scrollbar {
+ width: 4px;
+}
+
+.dropdown-options::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+.dropdown-options::-webkit-scrollbar-thumb {
+ background: rgba(255, 255, 255, 0.1);
+ border-radius: 2px;
+}
+
+.dropdown-option {
+ display: block;
+ width: 100%;
+ text-align: left;
+ padding: 0.5rem 0.75rem;
+ border-radius: 0.5rem;
+ cursor: pointer;
+ transition: background 0.15s ease;
+}
+
+.dropdown-option:hover {
+ background: rgba(255, 255, 255, 0.04);
+}
+
+.dropdown-option.selected {
+ background: rgba(99, 102, 241, 0.1);
+}
+
::selection {
background-color: var(--color-accent-500);
color: white;
diff --git a/static/js/dropdown.js b/static/js/dropdown.js
new file mode 100644
index 0000000..0bfcc88
--- /dev/null
+++ b/static/js/dropdown.js
@@ -0,0 +1,62 @@
+function initDropdowns() {
+ document.querySelectorAll("[data-dropdown]").forEach(function (dropdown) {
+ if (dropdown.dataset.initialized) return;
+ dropdown.dataset.initialized = "true";
+
+ var trigger = dropdown.querySelector("[data-dropdown-trigger]");
+ var menu = dropdown.querySelector("[data-dropdown-menu]");
+ var search = dropdown.querySelector("[data-dropdown-search]");
+ var options = dropdown.querySelectorAll("[data-dropdown-option]");
+ var empty = dropdown.querySelector("[data-dropdown-empty]");
+ var hiddenInput = dropdown.querySelector("[data-dropdown-value]");
+ var label = dropdown.querySelector("[data-dropdown-label]");
+
+ trigger.addEventListener("click", function () {
+ dropdown.classList.toggle("open");
+ if (dropdown.classList.contains("open")) {
+ search.value = "";
+ filterOptions("");
+ search.focus();
+ }
+ });
+
+ search.addEventListener("input", function () {
+ filterOptions(search.value.toLowerCase());
+ });
+
+ options.forEach(function (option) {
+ option.addEventListener("click", function () {
+ hiddenInput.value = option.dataset.value;
+ label.textContent = option.dataset.label;
+ label.classList.remove("text-zinc-500");
+ label.classList.add("text-zinc-200");
+
+ options.forEach(function (other) {
+ other.classList.remove("selected");
+ });
+ option.classList.add("selected");
+
+ dropdown.classList.remove("open");
+ });
+ });
+
+ function filterOptions(query) {
+ var visibleCount = 0;
+ options.forEach(function (option) {
+ var matches = option.dataset.label.toLowerCase().indexOf(query) !== -1;
+ option.style.display = matches ? "" : "none";
+ if (matches) visibleCount++;
+ });
+ empty.classList.toggle("hidden", visibleCount > 0);
+ }
+
+ document.addEventListener("click", function (event) {
+ if (!dropdown.contains(event.target)) {
+ dropdown.classList.remove("open");
+ }
+ });
+ });
+}
+
+document.addEventListener("DOMContentLoaded", initDropdowns);
+document.body.addEventListener("htmx:afterSwap", initDropdowns);
diff --git a/templates/dashboard/htmx/mailboxes.htmx.django b/templates/dashboard/htmx/mailboxes.htmx.django
index 9ccedb1..1816951 100644
--- a/templates/dashboard/htmx/mailboxes.htmx.django
+++ b/templates/dashboard/htmx/mailboxes.htmx.django
@@ -3,7 +3,11 @@
<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">All Mailboxes</h2>
- <span class="text-xs text-zinc-600">{{ total }} total</span>
+ <div class="flex items-center gap-3">
+ <span class="text-xs text-zinc-600">{{ total }} total</span>
+ {% url "dashboard.mailboxes.new" as new_mailbox_path %}
+ <a href="{{ new_mailbox_path }}" hx-get="{{ new_mailbox_path }}" hx-target="#content" hx-swap="innerHTML" hx-push-url="true" class="btn-small">New Mailbox</a>
+ </div>
</div>
{% if items %}
<div class="divide-y divide-white/[0.04]">
diff --git a/templates/dashboard/htmx/newmailbox.htmx.django b/templates/dashboard/htmx/newmailbox.htmx.django
new file mode 100644
index 0000000..4f201cd
--- /dev/null
+++ b/templates/dashboard/htmx/newmailbox.htmx.django
@@ -0,0 +1,52 @@
+<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">Create Mailbox</h2>
+ </div>
+ <div class="p-5">
+ {% url "dashboard.mailboxes.create" as create_path %}
+ <form method="POST" action="{{ create_path }}" class="space-y-4">
+ <div>
+ <label class="block text-xs font-medium text-zinc-400 mb-1.5 ml-1">Address</label>
+ <input type="email" name="address" required autocomplete="off" class="input-field">
+ </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" 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 a user</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">Create Mailbox</button>
+ {% url "dashboard.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>
diff --git a/templates/dashboard/htmx/newuser.htmx.django b/templates/dashboard/htmx/newuser.htmx.django
new file mode 100644
index 0000000..826dc72
--- /dev/null
+++ b/templates/dashboard/htmx/newuser.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">Create User</h2>
+ </div>
+ <div class="p-5">
+ {% url "dashboard.users.create" as create_path %}
+ <form method="POST" action="{{ create_path }}" 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" name="username" required autocomplete="off" class="input-field">
+ </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" required autocomplete="off" class="input-field">
+ </div>
+ <div class="flex items-center gap-3 pt-2">
+ <button type="submit" class="btn-primary">Create User</button>
+ {% url "dashboard.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>
diff --git a/templates/dashboard/htmx/users.htmx.django b/templates/dashboard/htmx/users.htmx.django
index 6578ba2..edcf72a 100644
--- a/templates/dashboard/htmx/users.htmx.django
+++ b/templates/dashboard/htmx/users.htmx.django
@@ -3,7 +3,11 @@
<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">All Users</h2>
- <span class="text-xs text-zinc-600">{{ total }} total</span>
+ <div class="flex items-center gap-3">
+ <span class="text-xs text-zinc-600">{{ total }} total</span>
+ {% url "dashboard.users.new" as new_user_path %}
+ <a href="{{ new_user_path }}" hx-get="{{ new_user_path }}" hx-target="#content" hx-swap="innerHTML" hx-push-url="true" class="btn-small">New User</a>
+ </div>
</div>
{% if items %}
<div class="divide-y divide-white/[0.04]">
diff --git a/templates/dashboard/newmailbox.django b/templates/dashboard/newmailbox.django
new file mode 100644
index 0000000..179ca88
--- /dev/null
+++ b/templates/dashboard/newmailbox.django
@@ -0,0 +1,5 @@
+{% extends "layouts/dashboard.django" %}
+
+{% block dashboard %}
+{% include "dashboard/htmx/newmailbox.htmx.django" %}
+{% endblock %}
diff --git a/templates/dashboard/newuser.django b/templates/dashboard/newuser.django
new file mode 100644
index 0000000..efb3176
--- /dev/null
+++ b/templates/dashboard/newuser.django
@@ -0,0 +1,5 @@
+{% extends "layouts/dashboard.django" %}
+
+{% block dashboard %}
+{% include "dashboard/htmx/newuser.htmx.django" %}
+{% endblock %}
diff --git a/templates/layouts/base.django b/templates/layouts/base.django
index 0a884dd..bcfab20 100644
--- a/templates/layouts/base.django
+++ b/templates/layouts/base.django
@@ -16,6 +16,7 @@
{% block content %}{% endblock %}
<script src="/static/js/htmx.min.js"></script>
<script src="/static/js/navigation.js"></script>
+ <script src="/static/js/dropdown.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>
diff --git a/types/mailbox.go b/types/mailbox.go
index 5d213fd..adf1ce5 100644
--- a/types/mailbox.go
+++ b/types/mailbox.go
@@ -1,5 +1,16 @@
package types
+import "dove/models"
+
type Mailbox struct {
Address string
}
+
+type CreateMailboxRequest struct {
+ Address string `form:"address"`
+ UserID uint `form:"user_id"`
+}
+
+type MailboxFormResponse struct {
+ Users []models.User `json:"users"`
+}
diff --git a/types/user.go b/types/user.go
new file mode 100644
index 0000000..6686567
--- /dev/null
+++ b/types/user.go
@@ -0,0 +1,6 @@
+package types
+
+type CreateUserRequest struct {
+ Username string `form:"username"`
+ DisplayName string `form:"display_name"`
+}