513 lines
14 KiB
HTML
513 lines
14 KiB
HTML
|
|
{% extends "base.html" %}
|
||
|
|
|
||
|
|
{% block title %}Mail - General Bots{% endblock %}
|
||
|
|
|
||
|
|
{% block content %}
|
||
|
|
<div class="mail-layout" id="mail-app">
|
||
|
|
<!-- Sidebar -->
|
||
|
|
<aside class="mail-sidebar">
|
||
|
|
<div class="sidebar-header">
|
||
|
|
<button class="compose-btn"
|
||
|
|
hx-get="/api/email/compose"
|
||
|
|
hx-target="#mail-content"
|
||
|
|
hx-swap="innerHTML">
|
||
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
|
|
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||
|
|
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||
|
|
</svg>
|
||
|
|
Compose
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Folder List -->
|
||
|
|
<nav class="mail-folders">
|
||
|
|
<a href="#inbox" class="folder-item{% if current_folder == "inbox" %} active{% endif %}"
|
||
|
|
hx-get="/api/email/list?folder=inbox"
|
||
|
|
hx-target="#mail-list"
|
||
|
|
hx-swap="innerHTML">
|
||
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
|
|
<polyline points="22 12 16 12 14 15 10 15 8 12 2 12"></polyline>
|
||
|
|
<path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"></path>
|
||
|
|
</svg>
|
||
|
|
<span>Inbox</span>
|
||
|
|
{% if unread_count > 0 %}
|
||
|
|
<span class="folder-badge">{{ unread_count }}</span>
|
||
|
|
{% endif %}
|
||
|
|
</a>
|
||
|
|
|
||
|
|
<a href="#sent" class="folder-item{% if current_folder == "sent" %} active{% endif %}"
|
||
|
|
hx-get="/api/email/list?folder=sent"
|
||
|
|
hx-target="#mail-list"
|
||
|
|
hx-swap="innerHTML">
|
||
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
|
|
<line x1="22" y1="2" x2="11" y2="13"></line>
|
||
|
|
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
|
||
|
|
</svg>
|
||
|
|
<span>Sent</span>
|
||
|
|
</a>
|
||
|
|
|
||
|
|
<a href="#drafts" class="folder-item{% if current_folder == "drafts" %} active{% endif %}"
|
||
|
|
hx-get="/api/email/list?folder=drafts"
|
||
|
|
hx-target="#mail-list"
|
||
|
|
hx-swap="innerHTML">
|
||
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
|
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||
|
|
<polyline points="14 2 14 8 20 8"></polyline>
|
||
|
|
</svg>
|
||
|
|
<span>Drafts</span>
|
||
|
|
{% if drafts_count > 0 %}
|
||
|
|
<span class="folder-badge secondary">{{ drafts_count }}</span>
|
||
|
|
{% endif %}
|
||
|
|
</a>
|
||
|
|
|
||
|
|
<a href="#starred" class="folder-item{% if current_folder == "starred" %} active{% endif %}"
|
||
|
|
hx-get="/api/email/list?folder=starred"
|
||
|
|
hx-target="#mail-list"
|
||
|
|
hx-swap="innerHTML">
|
||
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
|
|
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
|
||
|
|
</svg>
|
||
|
|
<span>Starred</span>
|
||
|
|
</a>
|
||
|
|
|
||
|
|
<a href="#archive" class="folder-item{% if current_folder == "archive" %} active{% endif %}"
|
||
|
|
hx-get="/api/email/list?folder=archive"
|
||
|
|
hx-target="#mail-list"
|
||
|
|
hx-swap="innerHTML">
|
||
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
|
|
<polyline points="21 8 21 21 3 21 3 8"></polyline>
|
||
|
|
<rect x="1" y="3" width="22" height="5"></rect>
|
||
|
|
<line x1="10" y1="12" x2="14" y2="12"></line>
|
||
|
|
</svg>
|
||
|
|
<span>Archive</span>
|
||
|
|
</a>
|
||
|
|
|
||
|
|
<a href="#trash" class="folder-item{% if current_folder == "trash" %} active{% endif %}"
|
||
|
|
hx-get="/api/email/list?folder=trash"
|
||
|
|
hx-target="#mail-list"
|
||
|
|
hx-swap="innerHTML">
|
||
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
|
|
<polyline points="3 6 5 6 21 6"></polyline>
|
||
|
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
|
||
|
|
</svg>
|
||
|
|
<span>Trash</span>
|
||
|
|
</a>
|
||
|
|
</nav>
|
||
|
|
|
||
|
|
<!-- Labels -->
|
||
|
|
<div class="mail-labels">
|
||
|
|
<div class="labels-header">
|
||
|
|
<span>Labels</span>
|
||
|
|
<button class="btn-icon-sm" title="Create label">
|
||
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
|
|
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||
|
|
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||
|
|
</svg>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
{% for label in labels %}
|
||
|
|
<a href="#label-{{ label.id }}" class="label-item"
|
||
|
|
hx-get="/api/email/list?label={{ label.id }}"
|
||
|
|
hx-target="#mail-list"
|
||
|
|
hx-swap="innerHTML">
|
||
|
|
<span class="label-dot" style="background: {{ label.color }}"></span>
|
||
|
|
<span>{{ label.name }}</span>
|
||
|
|
</a>
|
||
|
|
{% endfor %}
|
||
|
|
</div>
|
||
|
|
</aside>
|
||
|
|
|
||
|
|
<!-- Email List -->
|
||
|
|
<section class="mail-list-panel">
|
||
|
|
<div class="mail-list-header">
|
||
|
|
<div class="mail-list-actions">
|
||
|
|
<input type="checkbox" class="select-all" title="Select all">
|
||
|
|
<button class="btn-icon" title="Refresh"
|
||
|
|
hx-get="/api/email/list?folder={{ current_folder }}"
|
||
|
|
hx-target="#mail-list"
|
||
|
|
hx-swap="innerHTML">
|
||
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
|
|
<polyline points="23 4 23 10 17 10"></polyline>
|
||
|
|
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
|
||
|
|
</svg>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
<div class="mail-search">
|
||
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
|
|
<circle cx="11" cy="11" r="8"></circle>
|
||
|
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||
|
|
</svg>
|
||
|
|
<input type="text"
|
||
|
|
placeholder="Search mail..."
|
||
|
|
name="q"
|
||
|
|
hx-get="/api/email/search"
|
||
|
|
hx-trigger="keyup changed delay:300ms"
|
||
|
|
hx-target="#mail-list"
|
||
|
|
hx-swap="innerHTML">
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div id="mail-list" class="mail-list"
|
||
|
|
hx-get="/api/email/list?folder={{ current_folder }}"
|
||
|
|
hx-trigger="load"
|
||
|
|
hx-swap="innerHTML">
|
||
|
|
<div class="mail-loading">
|
||
|
|
<div class="spinner"></div>
|
||
|
|
<span>Loading emails...</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<!-- Email Content -->
|
||
|
|
<section class="mail-content-panel">
|
||
|
|
<div id="mail-content" class="mail-content">
|
||
|
|
<div class="mail-empty-state">
|
||
|
|
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||
|
|
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
|
||
|
|
<polyline points="22,6 12,13 2,6"></polyline>
|
||
|
|
</svg>
|
||
|
|
<h3>Select an email to read</h3>
|
||
|
|
<p>Choose from your inbox on the left</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<style>
|
||
|
|
.mail-layout {
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: 250px 350px 1fr;
|
||
|
|
height: calc(100vh - 64px);
|
||
|
|
background: var(--bg);
|
||
|
|
}
|
||
|
|
|
||
|
|
.mail-sidebar {
|
||
|
|
background: var(--surface);
|
||
|
|
border-right: 1px solid var(--border);
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
overflow-y: auto;
|
||
|
|
}
|
||
|
|
|
||
|
|
.sidebar-header {
|
||
|
|
padding: 1rem;
|
||
|
|
border-bottom: 1px solid var(--border);
|
||
|
|
}
|
||
|
|
|
||
|
|
.compose-btn {
|
||
|
|
width: 100%;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
gap: 0.5rem;
|
||
|
|
padding: 0.75rem 1rem;
|
||
|
|
background: var(--primary);
|
||
|
|
color: white;
|
||
|
|
border: none;
|
||
|
|
border-radius: 8px;
|
||
|
|
font-size: 0.9375rem;
|
||
|
|
font-weight: 500;
|
||
|
|
cursor: pointer;
|
||
|
|
transition: background 0.2s;
|
||
|
|
}
|
||
|
|
|
||
|
|
.compose-btn:hover {
|
||
|
|
background: var(--primary-hover);
|
||
|
|
}
|
||
|
|
|
||
|
|
.mail-folders {
|
||
|
|
padding: 0.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.folder-item {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 0.75rem;
|
||
|
|
padding: 0.625rem 0.75rem;
|
||
|
|
border-radius: 6px;
|
||
|
|
color: var(--text-secondary);
|
||
|
|
text-decoration: none;
|
||
|
|
font-size: 0.875rem;
|
||
|
|
transition: background 0.2s, color 0.2s;
|
||
|
|
}
|
||
|
|
|
||
|
|
.folder-item:hover {
|
||
|
|
background: var(--bg);
|
||
|
|
color: var(--text);
|
||
|
|
}
|
||
|
|
|
||
|
|
.folder-item.active {
|
||
|
|
background: rgba(59, 130, 246, 0.1);
|
||
|
|
color: var(--primary);
|
||
|
|
}
|
||
|
|
|
||
|
|
.folder-badge {
|
||
|
|
margin-left: auto;
|
||
|
|
padding: 0.125rem 0.5rem;
|
||
|
|
background: var(--primary);
|
||
|
|
color: white;
|
||
|
|
border-radius: 10px;
|
||
|
|
font-size: 0.75rem;
|
||
|
|
font-weight: 500;
|
||
|
|
}
|
||
|
|
|
||
|
|
.folder-badge.secondary {
|
||
|
|
background: var(--text-secondary);
|
||
|
|
}
|
||
|
|
|
||
|
|
.mail-labels {
|
||
|
|
padding: 0.5rem;
|
||
|
|
border-top: 1px solid var(--border);
|
||
|
|
margin-top: auto;
|
||
|
|
}
|
||
|
|
|
||
|
|
.labels-header {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: space-between;
|
||
|
|
padding: 0.5rem 0.75rem;
|
||
|
|
font-size: 0.75rem;
|
||
|
|
font-weight: 600;
|
||
|
|
color: var(--text-secondary);
|
||
|
|
text-transform: uppercase;
|
||
|
|
}
|
||
|
|
|
||
|
|
.label-item {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 0.5rem;
|
||
|
|
padding: 0.5rem 0.75rem;
|
||
|
|
border-radius: 6px;
|
||
|
|
color: var(--text-secondary);
|
||
|
|
text-decoration: none;
|
||
|
|
font-size: 0.8125rem;
|
||
|
|
transition: background 0.2s;
|
||
|
|
}
|
||
|
|
|
||
|
|
.label-item:hover {
|
||
|
|
background: var(--bg);
|
||
|
|
}
|
||
|
|
|
||
|
|
.label-dot {
|
||
|
|
width: 10px;
|
||
|
|
height: 10px;
|
||
|
|
border-radius: 50%;
|
||
|
|
}
|
||
|
|
|
||
|
|
.mail-list-panel {
|
||
|
|
border-right: 1px solid var(--border);
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
overflow: hidden;
|
||
|
|
}
|
||
|
|
|
||
|
|
.mail-list-header {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 0.5rem;
|
||
|
|
padding: 0.75rem;
|
||
|
|
border-bottom: 1px solid var(--border);
|
||
|
|
}
|
||
|
|
|
||
|
|
.mail-list-actions {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 0.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.mail-search {
|
||
|
|
flex: 1;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 0.5rem;
|
||
|
|
padding: 0.5rem 0.75rem;
|
||
|
|
background: var(--bg);
|
||
|
|
border-radius: 6px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.mail-search input {
|
||
|
|
flex: 1;
|
||
|
|
background: transparent;
|
||
|
|
border: none;
|
||
|
|
color: var(--text);
|
||
|
|
font-size: 0.875rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.mail-search input:focus {
|
||
|
|
outline: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.mail-search input::placeholder {
|
||
|
|
color: var(--text-secondary);
|
||
|
|
}
|
||
|
|
|
||
|
|
.mail-list {
|
||
|
|
flex: 1;
|
||
|
|
overflow-y: auto;
|
||
|
|
}
|
||
|
|
|
||
|
|
.mail-loading {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
padding: 3rem;
|
||
|
|
color: var(--text-secondary);
|
||
|
|
gap: 1rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.mail-content-panel {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
overflow: hidden;
|
||
|
|
}
|
||
|
|
|
||
|
|
.mail-content {
|
||
|
|
flex: 1;
|
||
|
|
overflow-y: auto;
|
||
|
|
padding: 1.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.mail-empty-state {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
height: 100%;
|
||
|
|
color: var(--text-secondary);
|
||
|
|
text-align: center;
|
||
|
|
}
|
||
|
|
|
||
|
|
.mail-empty-state svg {
|
||
|
|
margin-bottom: 1rem;
|
||
|
|
opacity: 0.5;
|
||
|
|
}
|
||
|
|
|
||
|
|
.mail-empty-state h3 {
|
||
|
|
font-size: 1.125rem;
|
||
|
|
font-weight: 500;
|
||
|
|
margin-bottom: 0.5rem;
|
||
|
|
color: var(--text);
|
||
|
|
}
|
||
|
|
|
||
|
|
.mail-empty-state p {
|
||
|
|
font-size: 0.875rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn-icon {
|
||
|
|
padding: 0.375rem;
|
||
|
|
background: transparent;
|
||
|
|
border: none;
|
||
|
|
color: var(--text-secondary);
|
||
|
|
border-radius: 4px;
|
||
|
|
cursor: pointer;
|
||
|
|
transition: background 0.2s, color 0.2s;
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn-icon:hover {
|
||
|
|
background: var(--bg);
|
||
|
|
color: var(--text);
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn-icon-sm {
|
||
|
|
padding: 0.25rem;
|
||
|
|
background: transparent;
|
||
|
|
border: none;
|
||
|
|
color: var(--text-secondary);
|
||
|
|
border-radius: 4px;
|
||
|
|
cursor: pointer;
|
||
|
|
}
|
||
|
|
|
||
|
|
.spinner {
|
||
|
|
width: 24px;
|
||
|
|
height: 24px;
|
||
|
|
border: 2px solid var(--border);
|
||
|
|
border-top-color: var(--primary);
|
||
|
|
border-radius: 50%;
|
||
|
|
animation: spin 0.8s linear infinite;
|
||
|
|
}
|
||
|
|
|
||
|
|
@keyframes spin {
|
||
|
|
to { transform: rotate(360deg); }
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (max-width: 1024px) {
|
||
|
|
.mail-layout {
|
||
|
|
grid-template-columns: 60px 280px 1fr;
|
||
|
|
}
|
||
|
|
|
||
|
|
.mail-sidebar {
|
||
|
|
padding: 0.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.compose-btn span,
|
||
|
|
.folder-item span,
|
||
|
|
.labels-header,
|
||
|
|
.label-item span {
|
||
|
|
display: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.folder-item {
|
||
|
|
justify-content: center;
|
||
|
|
padding: 0.75rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.folder-badge {
|
||
|
|
position: absolute;
|
||
|
|
top: 0;
|
||
|
|
right: 0;
|
||
|
|
transform: translate(25%, -25%);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (max-width: 768px) {
|
||
|
|
.mail-layout {
|
||
|
|
grid-template-columns: 1fr;
|
||
|
|
}
|
||
|
|
|
||
|
|
.mail-sidebar,
|
||
|
|
.mail-content-panel {
|
||
|
|
display: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.mail-sidebar.active,
|
||
|
|
.mail-content-panel.active {
|
||
|
|
display: flex;
|
||
|
|
position: fixed;
|
||
|
|
inset: 64px 0 0 0;
|
||
|
|
z-index: 100;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
// Email selection
|
||
|
|
document.addEventListener('click', (e) => {
|
||
|
|
const emailItem = e.target.closest('.email-item');
|
||
|
|
if (emailItem) {
|
||
|
|
document.querySelectorAll('.email-item.selected').forEach(el => el.classList.remove('selected'));
|
||
|
|
emailItem.classList.add('selected');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// Keyboard shortcuts
|
||
|
|
document.addEventListener('keydown', (e) => {
|
||
|
|
if (e.key === 'c' && !e.ctrlKey && !e.metaKey) {
|
||
|
|
const activeElement = document.activeElement;
|
||
|
|
if (activeElement.tagName !== 'INPUT' && activeElement.tagName !== 'TEXTAREA') {
|
||
|
|
e.preventDefault();
|
||
|
|
document.querySelector('.compose-btn').click();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (e.key === 'r' && !e.ctrlKey && !e.metaKey) {
|
||
|
|
const activeElement = document.activeElement;
|
||
|
|
if (activeElement.tagName !== 'INPUT' && activeElement.tagName !== 'TEXTAREA') {
|
||
|
|
e.preventDefault();
|
||
|
|
htmx.trigger('.mail-list', 'load');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
</script>
|
||
|
|
{% endblock %}
|