diff options
| author | Bobby <[email protected]> | 2025-12-23 10:57:04 +0530 |
|---|---|---|
| committer | Bobby <[email protected]> | 2025-12-23 10:57:04 +0530 |
| commit | 3036abbc4af1e85e3f3d473c9facdeefb94916c7 (patch) | |
| tree | 149363c254b9a52b85854cb2d32eb95c8cefa542 | |
| parent | ecce99fde86b01efffd1825688ee950f7e7f6b33 (diff) | |
| download | lain-3036abbc4af1e85e3f3d473c9facdeefb94916c7.tar.xz lain-3036abbc4af1e85e3f3d473c9facdeefb94916c7.zip | |
email templates, routes and utils
| -rw-r--r-- | router/auth.go | 13 | ||||
| -rw-r--r-- | router/base.go | 10 | ||||
| -rw-r--r-- | router/mail.go | 10 | ||||
| -rw-r--r-- | templates/auth/login.django | 36 | ||||
| -rw-r--r-- | templates/error.django | 10 | ||||
| -rw-r--r-- | templates/layouts/generic.django | 17 | ||||
| -rw-r--r-- | templates/layouts/mailbox.django | 17 | ||||
| -rw-r--r-- | templates/layouts/main.django | 59 | ||||
| -rw-r--r-- | templates/mail/folder.django | 5 | ||||
| -rw-r--r-- | templates/partials/filters.django | 129 | ||||
| -rw-r--r-- | templates/partials/footer.django | 3 | ||||
| -rw-r--r-- | templates/partials/navbar.django | 124 | ||||
| -rw-r--r-- | templates/partials/pane.django | 32 | ||||
| -rw-r--r-- | templates/partials/preview.django | 46 | ||||
| -rw-r--r-- | templates/partials/sidebar.django | 28 | ||||
| -rw-r--r-- | utils/email/helpers.go | 9 |
16 files changed, 508 insertions, 40 deletions
diff --git a/router/auth.go b/router/auth.go index 728e985..665b84e 100644 --- a/router/auth.go +++ b/router/auth.go @@ -3,14 +3,23 @@ package router import ( "lain/controllers" "lain/types" + "lain/utils/auth" + "lain/utils/shortcuts" "lain/utils/urls" + + "github.com/gofiber/fiber/v2" ) func init() { urls.SetNamespace("auth") - urls.Path(types.GET, "/login", controllers.LoginPage, "login") - urls.Path(types.GET, "/logout", controllers.Logout, "logout") + urls.Path(types.GET, "/login", func(c *fiber.Ctx) error { + if auth.IsAuthenticated(c) { + return shortcuts.Redirect(c, "mail.inbox") + } + return controllers.LoginPage(c) + }, "login") + urls.Path(types.GET, "/logout", controllers.Logout, "logout") urls.Path(types.POST, "/login", controllers.Login, "login.submit") } diff --git a/router/base.go b/router/base.go index a5e8e96..ece2df6 100644 --- a/router/base.go +++ b/router/base.go @@ -2,12 +2,20 @@ package router import ( "lain/types" + "lain/utils/auth" "lain/utils/shortcuts" "lain/utils/urls" + + "github.com/gofiber/fiber/v2" ) func init() { urls.SetNamespace("") - urls.Path(types.GET, "/", shortcuts.RedirectTo("auth.login"), "home") + urls.Path(types.GET, "/", func(c *fiber.Ctx) error { + if auth.IsAuthenticated(c) { + return shortcuts.Redirect(c, "mail.inbox") + } + return shortcuts.Redirect(c, "auth.login") + }, "home") } diff --git a/router/mail.go b/router/mail.go index 8584f05..9076d97 100644 --- a/router/mail.go +++ b/router/mail.go @@ -1,19 +1,15 @@ package router import ( - "lain/session" + "lain/controllers" "lain/types" "lain/utils/auth" "lain/utils/urls" - - "github.com/gofiber/fiber/v2" ) func init() { urls.SetNamespace("mail") - urls.Path(types.GET, "/inbox", auth.RequireAuthentication(func(c *fiber.Ctx) error { - email, _ := session.GetSessionEmail(c) - return c.SendString("Inbox for " + email) - }), "inbox") + urls.Path(types.GET, "/inbox", auth.RequireAuthentication(controllers.Mailbox), "inbox") + urls.Path(types.GET, "/*", auth.RequireAuthentication(controllers.Mailbox), "folder") } diff --git a/templates/auth/login.django b/templates/auth/login.django index 33ed81a..355c7ba 100644 --- a/templates/auth/login.django +++ b/templates/auth/login.django @@ -1,25 +1,27 @@ {% extends 'layouts/generic.django' %} {% block content %} - <div class="login-container"> - <h1>{{ AppName }}</h1> - <p class="subtitle">{{ AppDescription }}</p> + <div class="login-page"> + <div class="login-container"> + <h1>{{ AppName }}</h1> + <p class="subtitle">{{ AppDescription }}</p> - {% if Error %} - <div class="error">{{ Error }}</div> - {% endif %} + {% if Error %} + <div class="error">{{ Error }}</div> + {% endif %} - <form method="POST" action="{% url 'auth.login' %}"> - <div class="field"> - <label>Email</label> - <input type="email" name="email" required autofocus /> - </div> + <form method="POST" action="{% url 'auth.login' %}"> + <div class="field"> + <label>Email</label> + <input type="email" name="email" required autofocus /> + </div> - <div class="field"> - <label>Password</label> - <input type="password" name="password" required /> - </div> + <div class="field"> + <label>Password</label> + <input type="password" name="password" required /> + </div> - <button type="submit">Login</button> - </form> + <button type="submit">Login</button> + </form> + </div> </div> {% endblock %} diff --git a/templates/error.django b/templates/error.django index f766688..d38b972 100644 --- a/templates/error.django +++ b/templates/error.django @@ -1,8 +1,10 @@ {% extends 'layouts/generic.django' %} {% block content %} - <div class="error-container"> - <h1>{{ ErrorTitle }}</h1> - <p>{{ ErrorMessage }}</p> - <a href="{% url 'auth.login' %}">Go back home</a> + <div class="error-page"> + <div class="error-container"> + <h1>{{ ErrorTitle }}</h1> + <p>{{ ErrorMessage }}</p> + <a href="{% url 'auth.login' %}">Go back home</a> + </div> </div> {% endblock %} diff --git a/templates/layouts/generic.django b/templates/layouts/generic.django index 3ca183b..3222516 100644 --- a/templates/layouts/generic.django +++ b/templates/layouts/generic.django @@ -2,24 +2,23 @@ <html lang="en"> <head> <meta charset="UTF-8" /> - <title>{{ Title }} - {{ Appname }}</title> + <title>{{ Title }}</title> <link rel="stylesheet" href="/static/css/main.css" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <link rel="manifest" href="/static/extra/site.webmanifest" /> {% block head %} {% endblock %} </head> <body> - <main class="content"> - {% block content %} + {% block body %} + <main class="content"> + {% block content %} - {% endblock %} - </main> + {% endblock %} + </main> + {% endblock %} - <footer> - {{ AppName }} - Powered by {{ AppEngine }} - © <a href="https://shi.foo" target="_blank" rel="noopener noreferrer">shi.foo</a> 2025 - </footer> + {% include 'partials/footer.django' %} </body> {% block scripts %} diff --git a/templates/layouts/mailbox.django b/templates/layouts/mailbox.django new file mode 100644 index 0000000..ebb0352 --- /dev/null +++ b/templates/layouts/mailbox.django @@ -0,0 +1,17 @@ +{% extends 'layouts/main.django' %} + +{% block content %} + <div class="mailbox"> + <aside class="sidebar"> + {% include 'partials/sidebar.django' %} + </aside> + + <section class="pane"> + {% include 'partials/pane.django' %} + </section> + + <section class="preview"> + {% include 'partials/preview.django' %} + </section> + </div> +{% endblock %} diff --git a/templates/layouts/main.django b/templates/layouts/main.django new file mode 100644 index 0000000..929f656 --- /dev/null +++ b/templates/layouts/main.django @@ -0,0 +1,59 @@ +{% extends 'layouts/generic.django' %} + +{% block body %} + {% include 'partials/navbar.django' %} + <main> + {% block content %} + + {% endblock %} + </main> +{% endblock %} + +{% block scripts %} + <script> + document.addEventListener('DOMContentLoaded', function () { + // Handle dropdown clicks + document.querySelectorAll('.options-subitem > a').forEach(function (item) { + item.addEventListener('click', function (e) { + e.preventDefault() + const parent = this.parentElement + + if (parent.classList.contains('disabled')) { + return + } + + document.querySelectorAll('.options-subitem.open').forEach(function (other) { + if (other !== parent) { + other.classList.remove('open') + } + }) + + parent.classList.toggle('open') + }) + }) + + document.addEventListener('click', function (e) { + if (!e.target.closest('.options-subitem')) { + document.querySelectorAll('.options-subitem.open').forEach(function (item) { + item.classList.remove('open') + }) + } + }) + + // Toggle search filters + const toggleBtn = document.getElementById('toggle-filters') + const filters = document.getElementById('search-filters') + + if (toggleBtn && filters) { + toggleBtn.addEventListener('click', function (e) { + e.preventDefault() + if (filters.style.display === 'none') { + filters.style.display = 'block' + } else { + filters.style.display = 'none' + } + }) + } + }) + </script> +{% endblock %} diff --git a/templates/mail/folder.django b/templates/mail/folder.django new file mode 100644 index 0000000..4cdce72 --- /dev/null +++ b/templates/mail/folder.django @@ -0,0 +1,5 @@ +{% extends 'layouts/mailbox.django' %} + +{% block scripts %} + <script src="{% static 'js/filters.js' %}"></script> +{% endblock %} diff --git a/templates/partials/filters.django b/templates/partials/filters.django new file mode 100644 index 0000000..98765be --- /dev/null +++ b/templates/partials/filters.django @@ -0,0 +1,129 @@ +<div class="filters" id="filters" style="display: none;"> + <div class="filter-header"> + <span>Advanced Search</span> + <button type="button" class="close-filters" id="close-filters">×</button> + </div> + + <div class="filter-section"> + <div class="filter-label-row"> + <label>From</label> + <select name="from_operator"> + <option value="includes">includes</option> + <option value="excludes">excludes</option> + <option value="equals">equals</option> + </select> + </div> + <div class="tag-input-container"> + <div class="tags" id="from-tags"></div> + <input type="text" class="tag-input" id="from-input" placeholder="Add email..." autocomplete="off" /> + <input type="hidden" name="from[]" id="from-hidden" /> + </div> + </div> + + <div class="filter-section"> + <div class="filter-label-row"> + <label>To</label> + <select name="to_operator"> + <option value="includes">includes</option> + <option value="excludes">excludes</option> + <option value="equals">equals</option> + </select> + </div> + <div class="tag-input-container"> + <div class="tags" id="to-tags"></div> + <input type="text" class="tag-input" id="to-input" placeholder="Add email..." autocomplete="off" /> + <input type="hidden" name="to[]" id="to-hidden" /> + </div> + </div> + + <div class="filter-section"> + <div class="filter-label-row"> + <label>Subject</label> + <select name="subject_operator"> + <option value="includes">includes</option> + <option value="excludes">excludes</option> + <option value="equals">equals</option> + <option value="startswith">starts</option> + <option value="endswith">ends</option> + </select> + </div> + <input type="text" name="subject" class="filter-input" placeholder="Search..." /> + </div> + + <div class="filter-section"> + <div class="filter-label-row"> + <label>Body</label> + <select name="body_operator"> + <option value="contains">contains</option> + <option value="excludes">excludes</option> + </select> + </div> + <input type="text" name="body" class="filter-input" placeholder="Search..." /> + </div> + + <div class="filter-section"> + <div class="filter-label-row"> + <label>Attachments</label> + <select name="filename_operator"> + <option value="includes">includes</option> + <option value="excludes">excludes</option> + <option value="equals">equals</option> + </select> + </div> + <div class="tag-input-container"> + <div class="tags" id="filename-tags"></div> + <input type="text" class="tag-input" id="filename-input" placeholder="Add filename..." autocomplete="off" /> + <input type="hidden" name="filename[]" id="filename-hidden" /> + </div> + </div> + + <div class="filter-section"> + <div class="filter-label-row"> + <label>Flags</label> + </div> + <div class="flag-checkboxes"> + <label class="flag-option"><input type="checkbox" name="has_attachment" value="1" /><span>Attachment</span></label> + <label class="flag-option"><input type="checkbox" name="is_flagged" value="1" /><span>Flagged</span></label> + <label class="flag-option"><input type="checkbox" name="is_unread" value="1" /><span>Unread</span></label> + <label class="flag-option"><input type="checkbox" name="is_answered" value="1" /><span>Answered</span></label> + </div> + </div> + + <div class="filter-section"> + <div class="filter-label-row"> + <label>Date</label> + </div> + <select name="date_preset" class="filter-input" id="date-preset"> + <option value="">Any time</option> + <option value="today">Today</option> + <option value="yesterday">Yesterday</option> + <option value="last7days">Last 7 days</option> + <option value="last30days">Last 30 days</option> + <option value="custom">Custom range</option> + </select> + <div id="custom-date-range" style="display: none; margin-top: 5px;"> + <input type="date" name="date_from" style="margin-bottom: 4px;" /> + <input type="date" name="date_to" /> + </div> + </div> + + <div class="filter-section"> + <div class="filter-label-row"> + <label>Search In</label> + </div> + <select name="scope" class="filter-input" id="scope-select"> + <option value="current">Current folder</option> + <option value="recursive">Current + subfolders</option> + <option value="all">All folders</option> + <option value="custom">Custom folders</option> + </select> + <input type="text" name="custom_folders" class="filter-input" placeholder="Folder1, Folder2..." style="margin-top: 5px; display: none;" id="custom-folders-input" /> + </div> + + <div class="filter-actions"> + <button type="button" class="btn-clear" id="clear-filters">Clear</button> + <button type="submit" class="btn-apply">Apply</button> + </div> +</div> + +<div class="autocomplete-dropdown" id="autocomplete-dropdown" style="display: none;"></div> diff --git a/templates/partials/footer.django b/templates/partials/footer.django new file mode 100644 index 0000000..3900a0c --- /dev/null +++ b/templates/partials/footer.django @@ -0,0 +1,3 @@ +<footer> + {{ AppName }} - Powered by {{ AppEngine }} - © <a href="https://shi.foo" target="_blank" rel="noopener noreferrer">shi.foo</a> 2025 +</footer> diff --git a/templates/partials/navbar.django b/templates/partials/navbar.django new file mode 100644 index 0000000..7b18bc9 --- /dev/null +++ b/templates/partials/navbar.django @@ -0,0 +1,124 @@ +<nav class="navbar"> + <nav class="topnav"> + <div class="nav-title"> + <div class="nav-item"> + <a href="{% url 'mail.inbox' %}">{{ AppName }}</a> + </div> + </div> + <div class="nav-links"> + <div class="nav-item"> + <a href="{% url 'mail.inbox' %}">Mail</a> + </div> + <div class="nav-item"> + <a href="#">Contacts</a> + </div> + <div class="nav-item"> + <a href="#">Calendar</a> + </div> + <div class="nav-item"> + <a href="#">Settings</a> + </div> + <div class="nav-item"> + <a href="{% url 'auth.logout' %}">Logout</a> + </div> + </div> + </nav> + <nav class="subnav"> + <div class="nav-links"> + <div class="nav-subitem"> + <a href="{% url 'mail.inbox' %}">Refresh</a> + </div> + <div class="nav-subitem"> + <a href="#">Compose</a> + </div> + <div class="nav-subitem disabled"> + <a href="#">Reply</a> + </div> + <div class="nav-subitem disabled"> + <a href="#">Reply All</a> + </div> + <div class="nav-subitem disabled options-subitem"> + <a href="#">Forward</a> + <div class="options-dropdown"> + <div class="options-dropdown-item"> + <a href="#">Forward as Attachment</a> + </div> + <div class="options-dropdown-item"> + <a href="#">Forward Inline</a> + </div> + <div class="options-dropdown-item"> + <a href="#">Resend (bounce)</a> + </div> + </div> + </div> + <div class="nav-subitem disabled"> + <a href="#">Delete</a> + </div> + <div class="nav-subitem disabled"> + <a href="#">Archive</a> + </div> + <div class="nav-subitem disabled"> + <a href="#">Junk</a> + </div> + <div class="nav-subitem disabled options-subitem"> + <a href="#">Mark</a> + <div class="options-dropdown"> + <div class="options-dropdown-item"> + <a href="#">As Read</a> + </div> + <div class="options-dropdown-item"> + <a href="#">As Unread</a> + </div> + <div class="options-dropdown-item"> + <a href="#">As Flagged</a> + </div> + <div class="options-dropdown-item"> + <a href="#">As Unflagged</a> + </div> + </div> + </div> + <div class="nav-subitem disabled options-subitem"> + <a href="#">More</a> + <div class="options-dropdown"> + <div class="options-dropdown-item"> + <a href="#">Print this message</a> + </div> + <div class="options-dropdown-item flyout-subitem"> + <a href="#">Download</a> + <div class="flyout-dropdown"> + <div class="flyout-dropdown-item"> + <a href="#">Source (.eml)</a> + </div> + <div class="flyout-dropdown-item"> + <a href="#">Mbox (as .zip)</a> + </div> + <div class="flyout-dropdown-item"> + <a href="#">Maildir (as .zip)</a> + </div> + </div> + </div> + <div class="options-dropdown-item"> + <a href="#">Edit as New</a> + </div> + <div class="options-dropdown-item"> + <a href="#">View Source</a> + </div> + <div class="options-dropdown-item"> + <a href="#">Open in New Window</a> + </div> + <div class="options-dropdown-item"> + <a href="#">Create Filter</a> + </div> + </div> + </div> + </div> + <div class="nav-search"> + <form action="#" method="get"> + <input type="text" name="q" placeholder="Search" class="search-input" /> + <button type="button" id="toggle-filters" class="btn-filters">Filters</button> + <button type="submit" class="btn-search">Search</button> + {% include 'partials/filters.django' %} + </form> + </div> + </nav> +</nav> diff --git a/templates/partials/pane.django b/templates/partials/pane.django new file mode 100644 index 0000000..873e9de --- /dev/null +++ b/templates/partials/pane.django @@ -0,0 +1,32 @@ +<div class="pane"> + {% if Emails %} + {% for email in Emails %} + <div class="email-row {% if not email.IsRead %}unread{% endif %} {% if email.Active %}active{% endif %}" data-email-id="{{ email.ID }}"> + <div class="email-checkbox"> + <input type="checkbox" /> + </div> + + <div class="email-flag {% if email.IsFlagged %}flagged{% endif %}"> + {% comment %} <img src="{% static 'icons/flag.svg' %}" alt="Flag" /> (We will write the static filter later){% endcomment %} + </div> + + <div class="email-info"> + <div class="email-from">{{ email.FromName }}</div> + <div class="email-subject">{{ email.Subject }}</div> + <div class="email-preview">{{ email.Preview }}</div> + </div> + + <div class="email-meta"> + {% if email.HasAttachments %} + <span class="attachment-icon">📎</span> + {% endif %} + <span class="email-date">{{ email.Date }}</span> + </div> + </div> + {% endfor %} + {% else %} + <div class="empty-state"> + <p>No emails in this folder</p> + </div> + {% endif %} +</div> diff --git a/templates/partials/preview.django b/templates/partials/preview.django new file mode 100644 index 0000000..dd40109 --- /dev/null +++ b/templates/partials/preview.django @@ -0,0 +1,46 @@ +{% if Email %} + <div class="email-header"> + <h2 class="email-subject">{{ Email.Subject }}</h2> + + <div class="email-actions"> + <button class="btn-icon" title="Reply">↶</button> + <button class="btn-icon" title="Reply All">⇄</button> + <button class="btn-icon" title="Forward">→</button> + <button class="btn-icon" title="Archive">▼</button> + <button class="btn-icon" title="Delete">×</button> + </div> + </div> + + <div class="email-sender"> + <div class="sender-info"> + <strong>{{ Email.FromName }}</strong> <{{ Email.FromAddress }}> + </div> + <div class="email-date">{{ Email.Date }}</div> + </div> + + <div class="email-recipients"> + <div> + <strong>To:</strong> {{ Email.To }} + </div> + {% if Email.Cc %} + <div> + <strong>Cc:</strong> {{ Email.Cc }} + </div> + {% endif %} + </div> + + {% if Email.Attachments %} + <div class="email-attachments"> + <strong>Attachments:</strong> + {% for attachment in Email.Attachments %} + <a href="#" class="attachment">{{ attachment.Filename }} ({{ attachment.Size }})</a> + {% endfor %} + </div> + {% endif %} + + <div class="email-body">{{ Email.Body|safe }}</div> +{% else %} + <div class="no-email-selected"> + <p>Select an email to view</p> + </div> +{% endif %} diff --git a/templates/partials/sidebar.django b/templates/partials/sidebar.django new file mode 100644 index 0000000..ea0a72d --- /dev/null +++ b/templates/partials/sidebar.django @@ -0,0 +1,28 @@ +<ul class="folder-list"> + {% for folder in Folders %} + <li class="folder-item {% if folder.Active %}active{% endif %}"> + <a href="/mail/{{ folder.IMAPName|lower }}"> + <img src="{{ folder.Icon }}" alt="{{ folder.Name }}" class="folder-icon" /> + <span class="folder-name">{{ folder.Name }}</span> + {% if folder.UnreadCount > 0 %} + <span class="folder-count">{{ folder.UnreadCount }}</span> + {% endif %} + </a> + {% if folder.Subfolders %} + <ul class="subfolder-list"> + {% for subfolder in folder.Subfolders %} + <li class="folder-item subfolder {% if subfolder.Active %}active{% endif %}"> + <a href="/mail/{{ subfolder.IMAPName|lower }}"> + <img src="{{ subfolder.Icon }}" alt="{{ subfolder.Name }}" class="folder-icon" /> + <span class="folder-name">{{ subfolder.Name }}</span> + {% if subfolder.UnreadCount > 0 %} + <span class="folder-count">{{ subfolder.UnreadCount }}</span> + {% endif %} + </a> + </li> + {% endfor %} + </ul> + {% endif %} + </li> + {% endfor %} +</ul> diff --git a/utils/email/helpers.go b/utils/email/helpers.go new file mode 100644 index 0000000..eb95726 --- /dev/null +++ b/utils/email/helpers.go @@ -0,0 +1,9 @@ +package email + +import ( + "slices" +) + +func hasAttribute(attrs []string, attr string) bool { + return slices.Contains(attrs, attr) +} |
