Files
bookhoard/templates/collections.templ
T
john-okeefe 033d66f503 feat(templates): Add hybrid SSR collection templates
- Add Collection template with SSR support
- Add CollectionDetail template for book display
- Templates accept pre-rendered data from server
2026-01-31 22:32:47 -05:00

368 lines
18 KiB
Templ
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package templates
templ Collection(user User, collections []CollectionData) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Collections - Bookmann</title>
<script src="/static/htmx.min.js"></script>
<script src="/static/toast.js"></script>
<script src="/static/header.js"></script>
<link href="/static/style.css" rel="stylesheet">
</head>
<body class="theme-{ user.Theme }">
@Header(user, "/collections")
<div class="max-w-7xl mx-auto 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>
<div id="collections-container" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
if len(collections) == 0 {
<div class="text-center py-16 col-span-full" style="color: var(--text-secondary)">
<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">
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>
</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>
<p class="text-xs" style="color: var(--text-secondary)">Created { col.CreatedAt }</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>
</body>
</html>
}
templ CollectionDetail(user User, collection CollectionDetailData, books []BookData) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{ collection.Name } - Bookmann</title>
<script src="/static/htmx.min.js"></script>
<script src="/static/toast.js"></script>
<script src="/static/header.js"></script>
<link href="/static/style.css" rel="stylesheet">
</head>
<body class="theme-{ user.Theme }">
@Header(user, "/collections")
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="mb-6">
<button onclick="backToCollections()" class="btn-secondary px-4 py-2 rounded-lg mb-4">
Back to Collections
</button>
<div class="flex items-center gap-4">
<div class="text-4xl" style="color: { collection.Color }">{ collection.Icon }</div>
<div>
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">{ collection.Name }</h1>
<p style="color: var(--text-secondary)">{ collection.Description }</p>
</div>
</div>
</div>
<div class="mb-6 flex justify-between items-center">
<h2 class="text-xl font-semibold" style="color: var(--text-primary)">Books in this Collection</h2>
<button onclick="showAddBooksModal()" class="btn-primary px-4 py-2 rounded-lg">
Add Books
</button>
</div>
<div id="books-container" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
if len(books) == 0 {
<div class="col-span-full text-center py-16" style="color: var(--text-secondary)">No books in this collection yet.</div>
}
for _, book := range books {
<div class="card p-4 rounded-lg border cursor-pointer hover:shadow-lg transition-shadow"
style="background-color: var(--bg-secondary); border-color: var(--border);">
<div class="aspect-w-3 aspect-h-4 mb-3 overflow-hidden rounded">
<img src="{ book.CoverImagePath }" alt="Cover"
class="w-full h-48 object-cover rounded"
onerror="this.src='/static/placeholder-book.svg'">
</div>
<h3 class="font-semibold text-lg mb-1 line-clamp-2" style="color: var(--text-primary)">{ book.Title }</h3>
if book.Author != "" {
<p class="text-sm" style="color: var(--text-secondary)">by { book.Author }</p>
}
<button onclick="removeBook('{ book.MediaItemID }')"
class="mt-2 px-3 py-1 text-sm border rounded hover:opacity-80"
style="border-color: var(--border); color: var(--text-secondary);">
Remove
</button>
</div>
}
</div>
</div>
<div id="add-books-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-2xl 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)">Add Books to Collection</h2>
<button onclick="hideAddBooksModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)"></button>
</div>
<p class="mb-4" style="color: var(--text-secondary)">Search and select books to add to this collection.</p>
<div class="mb-4">
<input type="text" id="book-search" placeholder="Search books..."
class="w-full px-4 py-2 border rounded-lg"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);">
</div>
<div id="book-results" class="max-h-64 overflow-y-auto mb-4"></div>
<div class="flex justify-end space-x-3">
<button type="button" onclick="hideAddBooksModal()" class="btn-secondary px-4 py-2 rounded-lg">
Cancel
</button>
<button type="button" onclick="addSelectedBooks()" class="btn-primary px-4 py-2 rounded-lg">
Add Selected Books
</button>
</div>
</div>
</div>
<script>
let collectionId = '{ collection.ID }';
function backToCollections() {
window.location.href = '/collections';
}
function showAddBooksModal() {
document.getElementById('add-books-modal').classList.remove('hidden');
searchBooks();
}
function hideAddBooksModal() {
document.getElementById('add-books-modal').classList.add('hidden');
document.getElementById('book-search').value = '';
document.getElementById('book-results').innerHTML = '';
selectedBooks.clear();
}
function searchBooks() {
const searchTerm = document.getElementById('book-search').value;
const container = document.getElementById('book-results');
if (searchTerm.length < 2) {
container.innerHTML = '<p class="text-sm" style="color: var(--text-secondary)">Enter at least 2 characters to search.</p>';
return;
}
// For now, this is a placeholder. We need a proper search endpoint.
// In a real implementation, you would call an API endpoint to search books.
container.innerHTML = '<p class="text-sm" style="color: var(--text-secondary)">Book search coming soon!</p>';
}
function addSelectedBooks() {
showToast('Add books feature coming soon!', 'info');
}
function removeBook(bookId) {
if (!confirm('Remove this book from the collection?')) return;
fetch(`/api/collections/${collectionId}/books/${bookId}`, {
method: 'DELETE',
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
})
.then(response => {
if (response.ok) {
showToast('Book removed from collection', 'success');
location.reload();
} else {
showToast('Failed to remove book', 'error');
}
})
.catch(error => {
showToast('Failed to remove book', 'error');
});
}
function logout() {
localStorage.removeItem('token');
window.location.href = '/login';
}
renderBooks();
</script>
</body>
</html>
}