Compare commits

..

3 commits

Author SHA1 Message Date
ClaraCrazy
cc4e3ddfbd
[Chore]: SEO Part 2
Electric Boogaloo (hopefully not)
2025-12-30 11:55:58 +01:00
ClaraCrazy
12069300d0
[Feat]: Add loading animation on init
PRevents errors on very early inbox load (mostly by bots but oh well... perfectionTM)
2025-12-30 11:20:12 +01:00
ClaraCrazy
5226cb3c6b
[Chore]: Add Responsive Styles 2025-12-30 11:04:50 +01:00
10 changed files with 209 additions and 21 deletions

10
app.js
View file

@ -46,14 +46,20 @@ const mailProcessingService = new MailProcessingService(
)
debug('Mail processing service initialized')
// Track IMAP initialization state
let isImapReady = false
app.set('isImapReady', false)
// Put everything together:
imapService.on(ImapService.EVENT_NEW_MAIL, mail =>
mailProcessingService.onNewMail(mail)
)
debug('Bound IMAP new mail event handler')
imapService.on(ImapService.EVENT_INITIAL_LOAD_DONE, () =>
imapService.on(ImapService.EVENT_INITIAL_LOAD_DONE, () => {
mailProcessingService.onInitialLoadDone()
)
isImapReady = true
app.set('isImapReady', true)
})
debug('Bound IMAP initial load done event handler')
imapService.on(ImapService.EVENT_DELETED_MAIL, mail =>
mailProcessingService.onMailDeleted(mail)

View file

@ -281,11 +281,41 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
function initHamburgerMenu() {
const actionLinks = document.querySelector('.action-links');
if (!actionLinks) return;
// Create hamburger button
const hamburger = document.createElement('button');
hamburger.className = 'hamburger-menu';
hamburger.setAttribute('aria-label', 'Toggle menu');
hamburger.innerHTML = '<span></span><span></span><span></span>';
// Insert as first child
actionLinks.insertBefore(hamburger, actionLinks.firstChild);
actionLinks.classList.add('mobile-hidden');
hamburger.addEventListener('click', (e) => {
e.stopPropagation();
actionLinks.classList.toggle('mobile-hidden');
actionLinks.classList.toggle('mobile-open');
});
// Close menu when clicking outside
document.addEventListener('click', (e) => {
if (actionLinks.classList.contains('mobile-open') && !actionLinks.contains(e.target)) {
actionLinks.classList.remove('mobile-open');
actionLinks.classList.add('mobile-hidden');
}
});
}
// Expose utilities and run them
window.utils = { formatEmailDates, formatMailDate, initLockModals, initCopyAddress, initExpiryTimers, initQrModal };
window.utils = { formatEmailDates, formatMailDate, initLockModals, initCopyAddress, initExpiryTimers, initQrModal, initHamburgerMenu };
formatEmailDates();
formatMailDate();
initLockModals();
initCopyAddress();
initQrModal();
initHamburgerMenu();
});

View file

@ -158,6 +158,39 @@ text-muted {
flex-wrap: nowrap;
/* ensures they stay in one line */
text-align: right;
display: flex;
gap: 10px;
}
.hamburger-menu {
display: none;
background: var(--overlay-purple-20);
border: 1px solid var(--overlay-purple-30);
border-radius: 12px;
padding: 10px;
cursor: pointer;
color: var(--color-accent-purple-light);
transition: all 0.3s ease;
flex-direction: column;
gap: 4px;
width: 40px;
height: 40px;
align-items: center;
justify-content: center;
}
.hamburger-menu span {
width: 24px;
height: 2px;
background: currentColor;
border-radius: 2px;
transition: all 0.3s ease;
}
.hamburger-menu:hover {
background: var(--overlay-purple-30);
border-color: var(--overlay-purple-40);
box-shadow: 0 4px 15px var(--overlay-purple-25);
}
.action-links a {
@ -341,9 +374,11 @@ label {
text-align: center;
margin-bottom: 30px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 12px;
flex-wrap: wrap;
}
.inbox-title {
@ -735,7 +770,9 @@ label {
display: none;
color: var(--color-accent-purple-alt);
font-size: 0.9em;
margin-left: 10px;
position: absolute;
margin-left: 0;
margin-top: 50px;
}
.email-expiry .expiry-timer[style*="color: #b00"] {
@ -835,6 +872,7 @@ label {
}
.qr-modal-content h3 {
margin-left: 16.516px;
margin-bottom: 24px;
}
@ -853,4 +891,82 @@ label {
font-size: 1.2rem;
margin: 0;
word-break: break-all;
}
/* Loading Page */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 80vh;
text-align: center;
}
.loading-spinner {
margin-bottom: 30px;
}
.loading-title {
color: var(--color-text-primary);
margin-bottom: 15px;
font-size: 2rem;
}
.loading-message {
color: var(--color-text-secondary);
font-size: 1.2rem;
}
.loading-submessage {
color: var(--color-text-muted);
font-size: 1rem;
margin-top: 10px;
}
body.loading-page .logo {
display: none;
}
/* Responsive Styles */
@media (max-width: 768px) {
.action-links {
position: relative;
}
.hamburger-menu {
display: flex;
}
.action-links.mobile-hidden>a,
.action-links.mobile-hidden>button:not(.hamburger-menu) {
display: none;
}
.action-links.mobile-open {
flex-direction: column;
position: absolute;
right: 20px;
top: 20px;
background: var(--color-bg-dark);
border: 1px solid var(--overlay-purple-30);
border-radius: 15px;
padding: 15px;
box-shadow: 0 10px 40px var(--overlay-black-40);
z-index: 1000;
min-width: 200px;
}
.action-links.mobile-open>.hamburger-menu {
display: none;
}
.action-links.mobile-open>a,
.action-links.mobile-open>button:not(.hamburger-menu) {
display: block;
width: 100%;
text-align: center;
}
.qr-icon-btn {
display: none;
}
}

View file

@ -3,9 +3,9 @@
{% block header %}
<div class="action-links">
{% if showUnlockButton %}
<a href="#" id="unlockBtn">Unlock</a>
<a href="#" id="unlockBtn" aria-label="Unlock inbox">Unlock</a>
{% endif %}
<a href="/">Logout</a>
<a href="/" aria-label="Return to home">Logout</a>
</div>
{% endblock %}

View file

@ -4,18 +4,18 @@
<div class="action-links">
{% if lockEnabled %}
{% if isLocked and hasAccess %}
<a href="#" id="removeLockBtn">Remove Lock</a>
<a href="#" id="removeLockBtn" aria-label="Remove password lock">Remove Lock</a>
{% elseif isLocked %}
<a href="#" id="unlockBtn">Unlock</a>
<a href="#" id="unlockBtn" aria-label="Unlock inbox">Unlock</a>
{% else %}
<a href="#" id="lockBtn">Protect Inbox</a>
<a href="#" id="lockBtn" aria-label="Protect inbox with password">Protect Inbox</a>
{% endif %}
{% endif %}
<a href="/inbox/{{ address }}/delete-all">Wipe Inbox</a>
<a href="/inbox/{{ address }}/delete-all" aria-label="Delete all emails">Wipe Inbox</a>
{% if lockEnabled and hasAccess %}
<a href="/lock/logout">Logout</a>
<a href="/lock/logout" aria-label="Logout">Logout</a>
{% else %}
<a href="/logout">Logout</a>
<a href="/logout" aria-label="Logout">Logout</a>
{% endif %}
</div>
{% endblock %}

View file

@ -75,11 +75,11 @@
<script src="/javascripts/notifications.js" defer="true"></script>
</head>
<body>
<body{% if bodyClass %} class="{{ bodyClass }}"{% endif %}>
<main>
<div class="header">
<a href="/">
<img src="/images/logo.png" class="logo" style="max-width: 75px">
<a href="/" aria-label="48hr.email home">
<img src="/images/logo.png" class="logo" alt="48hr.email logo" style="max-width: 75px">
</a>
{% block header %}{% endblock %}
</div>

View file

@ -0,0 +1,27 @@
{% extends "layout.twig" %}
{% set bodyClass = 'loading-page' %}
{% block header %}{% endblock %}
{% block footer %}{% endblock %}
{% block body %}
<div class="loading-container">
<div class="loading-spinner">
<svg width="80" height="80" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="40" stroke="var(--color-accent-purple-light)" stroke-width="8" fill="none" stroke-dasharray="63 188" stroke-linecap="round">
<animateTransform attributeName="transform" type="rotate" from="0 50 50" to="360 50 50" dur="1.5s" repeatCount="indefinite"/>
</circle>
</svg>
</div>
<h2 class="loading-title">Loading Mail Service</h2>
<p class="loading-message">Connecting to IMAP server and loading messages...</p>
<p class="loading-submessage">This may take a few moments on first startup</p>
</div>
<script>
// Auto-refresh every 2 seconds to check if IMAP is ready
setTimeout(() => {
window.location.reload();
}, 2000);
</script>
{% endblock %}

View file

@ -2,13 +2,13 @@
{% block header %}
<div class="action-links">
<a href="/inbox/{{ address }}">← Return to inbox</a>
<a href="/inbox/{{ address }}/{{ uid }}/delete">Delete Email</a>
<a href="/inbox/{{ address }}/{{ uid }}/raw" target="_blank">View Raw</a>
<a href="/inbox/{{ address }}" aria-label="Return to inbox">← Return to inbox</a>
<a href="/inbox/{{ address }}/{{ uid }}/delete" aria-label="Delete this email">Delete Email</a>
<a href="/inbox/{{ address }}/{{ uid }}/raw" target="_blank" aria-label="View raw email">View Raw</a>
{% if lockEnabled and isLocked and hasAccess %}
<a href="/lock/logout">Logout</a>
<a href="/lock/logout" aria-label="Logout">Logout</a>
{% else %}
<a href="/logout">Logout</a>
<a href="/logout" aria-label="Logout">Logout</a>
{% endif %}
</div>
{% endblock %}

View file

@ -85,6 +85,15 @@ app.use(
)
Twig.extendFilter('sanitizeHtml', sanitizeHtmlTwigFilter)
// Middleware to show loading page until IMAP is ready
app.use((req, res, next) => {
const isImapReady = req.app.get('isImapReady')
if (!isImapReady && !req.path.startsWith('/images') && !req.path.startsWith('/javascripts') && !req.path.startsWith('/stylesheets') && !req.path.startsWith('/dependencies')) {
return res.render('loading')
}
next()
})
app.use('/', loginRouter)
app.use('/inbox', inboxRouter)
app.use('/error', errorRouter)

View file

@ -1,6 +1,6 @@
{
"name": "48hr.email",
"version": "1.7.2",
"version": "1.7.3",
"private": false,
"description": "48hr.email is your favorite open-source tempmail client.",
"keywords": [