You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
107 lines
4.3 KiB
107 lines
4.3 KiB
{{define "content"}} |
|
<article class="ebooks-page"> |
|
<header class="page-header"> |
|
<h1>E-Books</h1> |
|
<p class="page-summary">Top-level publications (index events) from Nostr. These are publications that are not contained in any higher-level index event.</p> |
|
</header> |
|
|
|
<div class="ebooks-container"> |
|
<table id="ebooks-table" class="ebooks-table" aria-label="E-Books listing"> |
|
<thead> |
|
<tr> |
|
<th data-sort="title" class="sortable">Title <span class="sort-indicator">↕</span></th> |
|
<th data-sort="author" class="sortable">Author <span class="sort-indicator">↕</span></th> |
|
<th data-sort="type" class="sortable">Type <span class="sort-indicator">↕</span></th> |
|
<th data-sort="created" class="sortable">Created <span class="sort-indicator">↕</span></th> |
|
<th>Actions</th> |
|
</tr> |
|
</thead> |
|
<tbody> |
|
{{range .EBooks}} |
|
<tr> |
|
<td> |
|
<strong>{{.Title}}</strong> |
|
{{if .Summary}}<br><small class="text-muted">{{.Summary}}</small>{{end}} |
|
</td> |
|
<td><code class="pubkey">{{.Author}}</code></td> |
|
<td>{{if .Type}}{{.Type}}{{else}}—{{end}}</td> |
|
<td> |
|
<time datetime="{{.TimeISO}}">{{.Time}}</time> |
|
</td> |
|
<td> |
|
<a href="https://alexandria.gitcitadel.eu/naddr/{{.Naddr}}" target="_blank" rel="noopener noreferrer" class="btn btn-sm">View</a> |
|
</td> |
|
</tr> |
|
{{else}} |
|
<tr> |
|
<td colspan="5" class="text-center">No e-books found.</td> |
|
</tr> |
|
{{end}} |
|
</tbody> |
|
</table> |
|
</div> |
|
</article> |
|
|
|
<script> |
|
(function() { |
|
const table = document.getElementById('ebooks-table'); |
|
if (!table) return; |
|
|
|
const tbody = table.querySelector('tbody'); |
|
const headers = table.querySelectorAll('th.sortable'); |
|
let currentSort = { column: null, direction: 'asc' }; |
|
|
|
headers.forEach(header => { |
|
header.addEventListener('click', function() { |
|
const column = this.dataset.sort; |
|
const direction = currentSort.column === column && currentSort.direction === 'asc' ? 'desc' : 'asc'; |
|
|
|
// Update sort indicators |
|
headers.forEach(h => { |
|
const indicator = h.querySelector('.sort-indicator'); |
|
if (h === this) { |
|
indicator.textContent = direction === 'asc' ? '↑' : '↓'; |
|
} else { |
|
indicator.textContent = '↕'; |
|
} |
|
}); |
|
|
|
// Sort rows |
|
const rows = Array.from(tbody.querySelectorAll('tr')); |
|
rows.sort((a, b) => { |
|
let aVal, bVal; |
|
const aCell = a.cells[Array.from(headers).indexOf(this)]; |
|
const bCell = b.cells[Array.from(headers).indexOf(this)]; |
|
|
|
if (column === 'title') { |
|
aVal = aCell.querySelector('strong')?.textContent || ''; |
|
bVal = bCell.querySelector('strong')?.textContent || ''; |
|
} else if (column === 'created') { |
|
aVal = aCell.querySelector('time')?.getAttribute('datetime') || ''; |
|
bVal = bCell.querySelector('time')?.getAttribute('datetime') || ''; |
|
} else { |
|
aVal = aCell.textContent.trim(); |
|
bVal = bCell.textContent.trim(); |
|
} |
|
|
|
if (direction === 'asc') { |
|
return aVal.localeCompare(bVal); |
|
} else { |
|
return bVal.localeCompare(aVal); |
|
} |
|
}); |
|
|
|
// Re-append sorted rows |
|
rows.forEach(row => tbody.appendChild(row)); |
|
|
|
currentSort = { column, direction }; |
|
}); |
|
|
|
// Add cursor pointer style |
|
header.style.cursor = 'pointer'; |
|
}); |
|
})(); |
|
</script> |
|
{{end}} |
|
|
|
{{/* Feed is defined in components.html */}}
|
|
|