refactor(templates): migrate collections page to HTMX modals
Refactor collections.templ to use HTMX-powered modals instead of
client-side JavaScript modals. This aligns with project guidelines
for server-side rendering and progressive enhancement.
Key changes:
1. Remove inline modal HTML and JavaScript:
- Delete hardcoded create-modal div with inline form
- Remove all inline JavaScript (showCreateModal, hideCreateModal,
selectColor, handleCreate, viewCollection, editCollection,
deleteCollection, logout)
2. Add HTMX modal infrastructure:
- Add modal container div: <div id="modal-container"></div>
- Load modals dynamically via hx-get attributes
- Remove JavaScript modal toggling functions
3. Refactor collection cards for event delegation:
- Change from <a> wrapper to <div> with onclick="navigateToCollection()"
- Add data-href attribute for navigation target
- Wrap edit/delete buttons in separate container to prevent
unwanted card navigation when clicking buttons
4. Update buttons to use HTMX:
- Create button: hx-get="/collections/create-modal"
- Edit button: hx-get="/collections/{id}/edit-modal"
- Delete button: hx-delete="/api/collections/{id}" with hx-confirm
- Restore System button: hx-get="/collections/restore-modal"
5. Remove redundant forms:
- Delete empty-state "Create Your First Collection" button's
inline onclick (now uses HTMX like the main create button)
6. Add external JavaScript:
- Load /static/collections.js for helper functions
(navigateToCollection, setupHTMXAuth, etc.)
Benefits:
- Smaller initial page load (modal HTML loaded on-demand)
- Server-side rendering follows project guidelines
- Progressive enhancement (page works without JavaScript)
- Consistent with auth page modal pattern
- Easier to maintain (modal logic separated into dedicated templates)
This commit is contained in:
+58
-173
@@ -11,19 +11,38 @@ templ Collection(user User, collections []CollectionData, errorMessage string) {
|
||||
<title>Collections - Bookhoard</title>
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<script src="/static/toast.js"></script>
|
||||
<script src="/static/collections.js"></script>
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
</head>
|
||||
<body class="theme-{ user.Theme }">
|
||||
@Header(user, "/collections")
|
||||
<!-- Modal Container -->
|
||||
<div id="modal-container"></div>
|
||||
<!-- Actual container page -->
|
||||
<div class="w-full px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="mb-8 flex justify-between items-center">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">My Collections</h1>
|
||||
<p style="color: var(--text-secondary)">Organize your books into custom collections</p>
|
||||
</div>
|
||||
<button onclick="showCreateModal()" class="btn-primary px-4 py-2 rounded-lg">
|
||||
➕ New Collection
|
||||
</button>
|
||||
<div class="flex gap-3">
|
||||
<button
|
||||
hx-get="/collections/restore-modal"
|
||||
hx-target="#modal-container"
|
||||
hx-swap="innerHTML"
|
||||
class="btn-secondary px-4 py-2 rounded-lg"
|
||||
>
|
||||
🔄 Restore System
|
||||
</button>
|
||||
<button
|
||||
hx-get="/collections/create-modal"
|
||||
hx-target="#modal-container"
|
||||
hx-swap="innerHTML"
|
||||
class="btn-primary px-4 py-2 rounded-lg"
|
||||
>
|
||||
➕ New Collection
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="collections-list" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
if len(collections) == 0 {
|
||||
@@ -31,187 +50,53 @@ templ Collection(user User, collections []CollectionData, errorMessage string) {
|
||||
<div class="text-6xl mb-4">📚</div>
|
||||
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No Collections Yet</h3>
|
||||
<p class="mb-4">Create collections to organize your books</p>
|
||||
<button onclick="showCreateModal()" class="btn-primary px-4 py-2 rounded-lg">
|
||||
<button
|
||||
hx-get="/collections/create-modal"
|
||||
hx-target="#modal-container"
|
||||
hx-swap="innerHTML"
|
||||
class="btn-primary px-4 py-2 rounded-lg"
|
||||
>
|
||||
Create Your First Collection
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
for _, col := range collections {
|
||||
<div
|
||||
class="card p-6 rounded-lg border cursor-pointer hover:shadow-lg transition-shadow"
|
||||
style="background-color: var(--bg-secondary); border-color: { col.Color }; border-left-width: 4px; border-left-style: solid;"
|
||||
onclick="viewCollection('{ col.ID }')"
|
||||
>
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div class="text-3xl">{ col.Icon }</div>
|
||||
<div class="flex space-x-2">
|
||||
<button
|
||||
onclick="event.stopPropagation(); editCollection('{ col.ID }')"
|
||||
class="p-2 hover:opacity-80 rounded"
|
||||
style="color: var(--text-secondary); background-color: var(--bg-primary);"
|
||||
>
|
||||
✏️
|
||||
</button>
|
||||
<button
|
||||
onclick="event.stopPropagation(); deleteCollection('{ col.ID }')"
|
||||
class="p-2 hover:opacity-80 rounded"
|
||||
style="color: var(--text-secondary); background-color: var(--bg-primary);"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
<div onclick="navigateToCollection(this)" data-href={ "/collections/" + col.ID } class="block">
|
||||
<div
|
||||
class="card p-6 rounded-lg border-l-4 cursor-pointer hover:shadow-lg transition-shadow"
|
||||
style="background-color: var(--bg-secondary);"
|
||||
data-color={ col.Color }
|
||||
>
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div class="text-3xl">{ col.Icon }</div>
|
||||
<div class="flex space-x-2">
|
||||
<button
|
||||
hx-get={ "/collections/" + col.ID + "/edit-modal" }
|
||||
hx-target="#modal-container"
|
||||
hx-swap="innerHTML"
|
||||
class="p-2 hover:opacity-80 rounded"
|
||||
style="color: var(--text-secondary); background-color: var(--bg-primary);"
|
||||
>
|
||||
✏️
|
||||
</button>
|
||||
<button
|
||||
hx-delete={ "/api/collections/" + col.ID + "" }
|
||||
hx-redirect="/collections"
|
||||
hx-confirm="Are you sure you want to delete this collection?"
|
||||
class="p-2 hover:opacity-80 rounded"
|
||||
style="color: var(--text-secondary); background-color: var(--bg-primary);"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold mb-2" style="color: var(--text-primary)">{ col.Name }</h3>
|
||||
<p class="text-sm mb-4" style="color: var(--text-secondary)">{ col.Description }</p>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold mb-2" style="color: var(--text-primary)">{ col.Name }</h3>
|
||||
<p class="text-sm mb-4" style="color: var(--text-secondary)">{ col.Description }</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div id="create-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7);">
|
||||
<div class="card rounded-lg p-6 w-full max-w-md mx-4" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Create Collection</h2>
|
||||
<button onclick="hideCreateModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)">✕</button>
|
||||
</div>
|
||||
<form id="create-form" onsubmit="handleCreate(event)">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="collection-name"
|
||||
required
|
||||
class="w-full px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
placeholder="My Reading List"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Description</label>
|
||||
<textarea
|
||||
id="collection-description"
|
||||
class="w-full px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
placeholder="Optional description"
|
||||
rows="3"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Color</label>
|
||||
<div class="flex gap-2">
|
||||
<button type="button" onclick="selectColor('#7aa2f7')" class="w-8 h-8 rounded-full color-option" style="background-color: #7aa2f7;"></button>
|
||||
<button type="button" onclick="selectColor('#f7768e')" class="w-8 h-8 rounded-full color-option" style="background-color: #f7768e;"></button>
|
||||
<button type="button" onclick="selectColor('#e0af68')" class="w-8 h-8 rounded-full color-option" style="background-color: #e0af68;"></button>
|
||||
<button type="button" onclick="selectColor('#9ece6a')" class="w-8 h-8 rounded-full color-option" style="background-color: #9ece6a;"></button>
|
||||
<button type="button" onclick="selectColor('#7dcfff')" class="w-8 h-8 rounded-full color-option" style="background-color: #7dcfff;"></button>
|
||||
<button type="button" onclick="selectColor('#bb9af7')" class="w-8 h-8 rounded-full color-option" style="background-color: #bb9af7;"></button>
|
||||
</div>
|
||||
<input type="hidden" id="collection-color" value="#7aa2f7"/>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" onclick="hideCreateModal()" class="btn-secondary px-4 py-2 rounded-lg">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" class="btn-primary px-4 py-2 rounded-lg">
|
||||
Create Collection
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
let selectedColor = '#7aa2f7';
|
||||
|
||||
function showCreateModal() {
|
||||
document.getElementById('create-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function hideCreateModal() {
|
||||
document.getElementById('create-modal').classList.add('hidden');
|
||||
document.getElementById('create-form').reset();
|
||||
selectedColor = '#7aa2f7';
|
||||
updateColorSelection();
|
||||
}
|
||||
|
||||
function selectColor(color) {
|
||||
selectedColor = color;
|
||||
document.getElementById('collection-color').value = color;
|
||||
updateColorSelection();
|
||||
}
|
||||
|
||||
function updateColorSelection() {
|
||||
document.querySelectorAll('.color-option').forEach(btn => {
|
||||
btn.style.outline = 'none';
|
||||
btn.style.outlineOffset = '0';
|
||||
});
|
||||
const selectedBtn = document.querySelector(`.color-option[onclick="selectColor('${selectedColor}')"]`);
|
||||
if (selectedBtn) {
|
||||
selectedBtn.style.outline = '3px solid var(--text-primary)';
|
||||
selectedBtn.style.outlineOffset = '3px';
|
||||
}
|
||||
}
|
||||
|
||||
function handleCreate(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const data = {
|
||||
name: document.getElementById('collection-name').value,
|
||||
description: document.getElementById('collection-description').value,
|
||||
color: selectedColor,
|
||||
icon: '📚'
|
||||
};
|
||||
|
||||
fetch('/api/collections', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
showToast('Collection created successfully', 'success');
|
||||
hideCreateModal();
|
||||
location.reload();
|
||||
})
|
||||
.catch(error => {
|
||||
showToast('Failed to create collection', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
function viewCollection(id) {
|
||||
window.location.href = `/collections/${id}`;
|
||||
}
|
||||
|
||||
function editCollection(id) {
|
||||
showToast('Edit feature coming soon!', 'info');
|
||||
}
|
||||
|
||||
function deleteCollection(id) {
|
||||
if (!confirm('Are you sure you want to delete this collection?')) return;
|
||||
|
||||
fetch(`/api/collections/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
showToast('Collection deleted successfully', 'success');
|
||||
location.reload();
|
||||
} else {
|
||||
showToast('Failed to delete collection', 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showToast('Failed to delete collection', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
function logout() {
|
||||
localStorage.removeItem('token');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
</script>
|
||||
@ErrorToast(errorMessage)
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+115
-63
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user