9a848e8c53
Implement a command palette accessible via CMD+K/Ctrl+K using the ninja-keys web component. Features include: - New /api/cmd-k endpoint serving dynamic CV entries (experiences, projects, courses) - Language-aware responses with 1-hour cache headers - Scroll-to-section functionality for quick navigation - Enhanced keyboard shortcuts modal with CMD+K documentation - Comprehensive test coverage for API and UI interactions Also includes cleanup of deprecated debug test files and various UI polish improvements to contact form, themes, and action bar components.
195 lines
8.7 KiB
HTML
195 lines
8.7 KiB
HTML
{{define "contact-modal"}}
|
|
<!-- Contact Form Modal - Native Dialog -->
|
|
<dialog id="contact-modal" class="info-modal no-print"
|
|
_="on click call closeOnBackdrop(me, event)
|
|
on show
|
|
-- Reset form state when modal opens
|
|
set form to getElementById('contact-form')
|
|
if form
|
|
call form.reset()
|
|
set formFields to querySelectorAll('.form-group') in form
|
|
repeat for field in formFields
|
|
remove .hidden from field
|
|
end
|
|
remove .hidden from querySelector('.form-actions') in form
|
|
remove .hidden from querySelector('.form-note') in form
|
|
set responseDiv to getElementById('contact-response')
|
|
if responseDiv set responseDiv.innerHTML to ''
|
|
end">
|
|
<div class="info-modal-content">
|
|
<button class="info-modal-close" onclick="document.getElementById('contact-modal').close()" aria-label="{{.UI.ContactModal.Close}}">
|
|
<iconify-icon icon="mdi:close" width="24" height="24"></iconify-icon>
|
|
</button>
|
|
|
|
<div class="info-modal-header">
|
|
<h2>{{.UI.ContactModal.Title}}</h2>
|
|
<div class="info-modal-cv-title">
|
|
<iconify-icon icon="mdi:email-outline" width="32" height="32"></iconify-icon>
|
|
{{.UI.ContactModal.Subtitle}}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="info-modal-body">
|
|
<p class="contact-modal-description">
|
|
{{.UI.ContactModal.Description}}
|
|
</p>
|
|
|
|
<form id="contact-form"
|
|
hx-post="/api/contact?lang={{.Lang}}"
|
|
hx-target="#contact-response"
|
|
hx-swap="innerHTML"
|
|
hx-indicator="#contact-spinner"
|
|
hx-headers='{"X-Requested-With": "XMLHttpRequest"}'
|
|
_="on htmx:afterRequest
|
|
-- Check if response contains success message (not validation error)
|
|
set responseDiv to document.getElementById('contact-response')
|
|
if responseDiv is not null and responseDiv.querySelector('.contact-success') is not null
|
|
-- Hide all form fields and show only success message
|
|
set formFields to me.querySelectorAll('.form-group')
|
|
repeat for field in formFields
|
|
add .hidden to field
|
|
end
|
|
add .hidden to me.querySelector('.form-actions')
|
|
add .hidden to me.querySelector('.form-note')
|
|
-- Close modal after 3 seconds
|
|
wait 3s then call document.getElementById('contact-modal').close()
|
|
end">
|
|
|
|
<!-- Honeypot field - hidden, should be empty -->
|
|
<div style="position: absolute; left: -9999px;" aria-hidden="true">
|
|
<label for="contact-website">Website</label>
|
|
<input type="text"
|
|
name="website"
|
|
id="contact-website"
|
|
tabindex="-1"
|
|
autocomplete="off">
|
|
</div>
|
|
|
|
<!-- Timing field - set via JavaScript on page load -->
|
|
<input type="hidden" name="form_loaded_at" id="contact-form-loaded-at">
|
|
|
|
<!-- Email (required) -->
|
|
<div class="form-group">
|
|
<label for="contact-email" class="form-label">
|
|
{{.UI.ContactModal.Form.Email}} <span class="required-indicator">*</span>
|
|
</label>
|
|
<input type="email"
|
|
id="contact-email"
|
|
name="email"
|
|
class="form-input"
|
|
required
|
|
autocomplete="email"
|
|
placeholder="{{.UI.ContactModal.Form.EmailPlaceholder}}"
|
|
aria-required="true">
|
|
</div>
|
|
|
|
<!-- Name (optional) -->
|
|
<div class="form-group">
|
|
<label for="contact-name" class="form-label">
|
|
{{.UI.ContactModal.Form.Name}}
|
|
</label>
|
|
<input type="text"
|
|
id="contact-name"
|
|
name="name"
|
|
class="form-input"
|
|
autocomplete="name"
|
|
placeholder="{{.UI.ContactModal.Form.NamePlaceholder}}">
|
|
</div>
|
|
|
|
<!-- Company (optional) -->
|
|
<div class="form-group">
|
|
<label for="contact-company" class="form-label">
|
|
{{.UI.ContactModal.Form.Company}}
|
|
</label>
|
|
<input type="text"
|
|
id="contact-company"
|
|
name="company"
|
|
class="form-input"
|
|
autocomplete="organization"
|
|
placeholder="{{.UI.ContactModal.Form.CompanyPlaceholder}}">
|
|
</div>
|
|
|
|
<!-- Subject (optional) -->
|
|
<div class="form-group">
|
|
<label for="contact-subject" class="form-label">
|
|
{{.UI.ContactModal.Form.Subject}}
|
|
</label>
|
|
<input type="text"
|
|
id="contact-subject"
|
|
name="subject"
|
|
class="form-input"
|
|
placeholder="{{.UI.ContactModal.Form.SubjectPlaceholder}}">
|
|
</div>
|
|
|
|
<!-- Message (required) -->
|
|
<div class="form-group">
|
|
<label for="contact-message" class="form-label">
|
|
{{.UI.ContactModal.Form.Message}} <span class="required-indicator">*</span>
|
|
</label>
|
|
<textarea id="contact-message"
|
|
name="message"
|
|
class="form-textarea"
|
|
required
|
|
rows="5"
|
|
placeholder="{{.UI.ContactModal.Form.MessagePlaceholder}}"
|
|
aria-required="true"></textarea>
|
|
</div>
|
|
|
|
<!-- Response area for success/error messages -->
|
|
<div id="contact-response" class="contact-response" role="status" aria-live="polite"></div>
|
|
|
|
<!-- Submit button with loading indicator -->
|
|
<div class="form-actions">
|
|
<button type="submit" class="contact-submit-btn">
|
|
<iconify-icon icon="mdi:send" width="20" height="20"></iconify-icon>
|
|
<span>{{.UI.ContactModal.Form.Submit}}</span>
|
|
<iconify-icon id="contact-spinner"
|
|
icon="mdi:loading"
|
|
class="htmx-indicator spinning"
|
|
width="20"
|
|
height="20"
|
|
aria-label="{{.UI.ContactModal.Form.Sending}}"></iconify-icon>
|
|
</button>
|
|
</div>
|
|
|
|
<p class="form-note">{{.UI.ContactModal.Form.Note}}</p>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</dialog>
|
|
|
|
<!-- Initialize form timestamp on page load and modal open -->
|
|
<script>
|
|
(function() {
|
|
'use strict';
|
|
|
|
function resetContactFormTimestamp() {
|
|
const timestampField = document.getElementById('contact-form-loaded-at');
|
|
if (timestampField) {
|
|
timestampField.value = Date.now();
|
|
}
|
|
}
|
|
|
|
// Set initial timestamp on page load
|
|
document.addEventListener('DOMContentLoaded', resetContactFormTimestamp);
|
|
|
|
// Reset timestamp when modal opens (using MutationObserver to catch dialog open)
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const contactModal = document.getElementById('contact-modal');
|
|
if (contactModal) {
|
|
// Observer watches for the 'open' attribute being added
|
|
const observer = new MutationObserver(function(mutations) {
|
|
mutations.forEach(function(mutation) {
|
|
if (mutation.attributeName === 'open' && contactModal.hasAttribute('open')) {
|
|
resetContactFormTimestamp();
|
|
}
|
|
});
|
|
});
|
|
|
|
observer.observe(contactModal, { attributes: true });
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
{{end}}
|