Phase 2 & 3: Update frontend with sorting and filtering UI
- Expand sort dropdown with 12 sorting options - Add collapsible filter panel with 6 filter types - Implement JavaScript filter logic (applyFilters, loadMediaItemsWithFilters, displayActiveFilters, clearFilters) - Add URL state management for shareable filtered/sorted views - Add active filter chips display with clear functionality - Support filtering by author, genre, series, language, year range, has_cover - Restore filters from URL on page load
This commit is contained in:
1 parent
3b2075fc70
commit
ffb09ccc5e
2 files changed
+275
-4
No files matched your search
+266
-3
@@ -100,14 +100,94 @@ templ Dashboard(user User) {
|
|||||||
</button>
|
</button>
|
||||||
<input type="text" id="search-input" placeholder="Search media..." class="flex-1 px-4 py-2 border rounded-lg" style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)" onkeyup="filterMedia()">
|
<input type="text" id="search-input" placeholder="Search media..." class="flex-1 px-4 py-2 border rounded-lg" style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)" onkeyup="filterMedia()">
|
||||||
<select id="sort-select" class="px-4 py-2 border rounded-lg" style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)" onchange="loadMediaItems()">
|
<select id="sort-select" class="px-4 py-2 border rounded-lg" style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)" onchange="loadMediaItems()">
|
||||||
<option value="created_at DESC">Newest First</option>
|
<option value="created_at DESC">Newest Added</option>
|
||||||
|
<option value="created_at ASC">Oldest Added</option>
|
||||||
<option value="title ASC">Title A-Z</option>
|
<option value="title ASC">Title A-Z</option>
|
||||||
|
<option value="title DESC">Title Z-A</option>
|
||||||
<option value="author ASC">Author A-Z</option>
|
<option value="author ASC">Author A-Z</option>
|
||||||
<option value="created_at ASC">Oldest First</option>
|
<option value="author DESC">Author Z-A</option>
|
||||||
|
<option value="series ASC">Series Order</option>
|
||||||
|
<option value="date_published DESC">Newest Published</option>
|
||||||
|
<option value="copyright_year DESC">Copyright Year (Newest)</option>
|
||||||
|
<option value="page_count ASC">Shortest First</option>
|
||||||
|
<option value="page_count DESC">Longest First</option>
|
||||||
|
<option value="genre ASC">Genre A-Z</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Filters Panel -->
|
||||||
|
<div class="filter-panel mb-6">
|
||||||
|
<details>
|
||||||
|
<summary class="cursor-pointer font-semibold mb-4 flex items-center gap-2" style="color: var(--text-primary)">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"></path>
|
||||||
|
</svg>
|
||||||
|
Filters
|
||||||
|
</summary>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm mb-1" style="color: var(--text-secondary)">Author</label>
|
||||||
|
<select id="filter-author" class="w-full px-3 py-2 border rounded-lg" style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)">
|
||||||
|
<option value="">All Authors</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm mb-1" style="color: var(--text-secondary)">Genre</label>
|
||||||
|
<select id="filter-genre" class="w-full px-3 py-2 border rounded-lg" style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)">
|
||||||
|
<option value="">All Genres</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm mb-1" style="color: var(--text-secondary)">Series</label>
|
||||||
|
<select id="filter-series" class="w-full px-3 py-2 border rounded-lg" style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)">
|
||||||
|
<option value="">All Series</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm mb-1" style="color: var(--text-secondary)">Language</label>
|
||||||
|
<select id="filter-language" class="w-full px-3 py-2 border rounded-lg" style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)">
|
||||||
|
<option value="">All Languages</option>
|
||||||
|
<option value="en">English</option>
|
||||||
|
<option value="es">Spanish</option>
|
||||||
|
<option value="fr">French</option>
|
||||||
|
<option value="de">German</option>
|
||||||
|
<option value="ja">Japanese</option>
|
||||||
|
<option value="zh">Chinese</option>
|
||||||
|
<option value="ko">Korean</option>
|
||||||
|
<option value="ru">Russian</option>
|
||||||
|
<option value="it">Italian</option>
|
||||||
|
<option value="pt">Portuguese</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm mb-1" style="color: var(--text-secondary)">Year Range</label>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<input type="number" id="filter-year-min" placeholder="From" class="w-1/2 px-3 py-2 border rounded-lg" style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)" min="1800" max="2100">
|
||||||
|
<input type="number" id="filter-year-max" placeholder="To" class="w-1/2 px-3 py-2 border rounded-lg" style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)" min="1800" max="2100">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-end">
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="checkbox" id="filter-has-cover" class="w-4 h-4 rounded">
|
||||||
|
<span class="text-sm" style="color: var(--text-secondary)">Has Cover Image</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button onclick="applyFilters()" class="btn-primary px-4 py-2 rounded-lg font-medium">Apply Filters</button>
|
||||||
|
<button onclick="clearFilters()" class="btn-secondary px-4 py-2 rounded-lg font-medium">Clear All</button>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Active Filters Display -->
|
||||||
|
<div id="active-filters" class="hidden mb-4">
|
||||||
|
<div class="flex flex-wrap gap-2" id="active-filters-container">
|
||||||
|
<!-- Active filter chips added here dynamically -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Media Grid -->
|
<!-- Media Grid -->
|
||||||
<div id="media-container" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div id="media-container" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
<!-- Media items will be loaded here -->
|
<!-- Media items will be loaded here -->
|
||||||
@@ -382,15 +462,198 @@ templ Dashboard(user User) {
|
|||||||
mediaItems = tempItems;
|
mediaItems = tempItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyFilters() {
|
||||||
|
const filters = {
|
||||||
|
author: document.getElementById('filter-author').value,
|
||||||
|
genre: document.getElementById('filter-genre').value,
|
||||||
|
series: document.getElementById('filter-series').value,
|
||||||
|
language: document.getElementById('filter-language').value,
|
||||||
|
yearMin: parseInt(document.getElementById('filter-year-min').value) || 0,
|
||||||
|
yearMax: parseInt(document.getElementById('filter-year-max').value) || 0,
|
||||||
|
hasCover: document.getElementById('filter-has-cover').checked
|
||||||
|
};
|
||||||
|
|
||||||
|
const sort = document.getElementById('sort-select').value;
|
||||||
|
|
||||||
|
// Update URL for shareability
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.set('library_id', currentLibrary.id);
|
||||||
|
params.set('sort', sort);
|
||||||
|
Object.entries(filters).forEach(([key, value]) => {
|
||||||
|
if (value && value !== 0) {
|
||||||
|
if (key === 'hasCover') {
|
||||||
|
if (value === true) params.set(`has_cover`, 'true');
|
||||||
|
} else {
|
||||||
|
params.set(`${key}_filter`, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.history.pushState({}, '', `?${params.toString()}`);
|
||||||
|
|
||||||
|
loadMediaItemsWithFilters(filters, sort);
|
||||||
|
displayActiveFilters(filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadMediaItemsWithFilters(filters, sort) {
|
||||||
|
if (!currentLibrary) return;
|
||||||
|
|
||||||
|
const loading = document.getElementById('loading');
|
||||||
|
const container = document.getElementById('media-container');
|
||||||
|
|
||||||
|
loading.style.display = 'block';
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.set('library_id', currentLibrary.id);
|
||||||
|
params.set('sort', sort);
|
||||||
|
params.set('limit', 50);
|
||||||
|
params.set('offset', 0);
|
||||||
|
|
||||||
|
// Only add filter params if they have values
|
||||||
|
if (filters.author) params.set('author_filter', filters.author);
|
||||||
|
if (filters.series) params.set('series_filter', filters.series);
|
||||||
|
if (filters.genre) params.set('genre_filter', filters.genre);
|
||||||
|
if (filters.language) params.set('language_filter', filters.language);
|
||||||
|
if (filters.yearMin) params.set('year_min', filters.yearMin);
|
||||||
|
if (filters.yearMax) params.set('year_max', filters.yearMax);
|
||||||
|
if (filters.hasCover) params.set('has_cover', 'true');
|
||||||
|
|
||||||
|
fetch(`/api/media-items/filtered?${params.toString()}`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + localStorage.getItem('token'),
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
loading.style.display = 'none';
|
||||||
|
mediaItems = data.data || [];
|
||||||
|
renderMediaItems();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
loading.style.display = 'none';
|
||||||
|
console.error('Error loading filtered media items:', error);
|
||||||
|
container.innerHTML = '<div class="col-span-full text-center py-8" style="color: var(--text-secondary)">Error loading media items. Please try again.</div>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayActiveFilters(filters) {
|
||||||
|
const container = document.getElementById('active-filters');
|
||||||
|
const chipsContainer = document.getElementById('active-filters-container');
|
||||||
|
chipsContainer.innerHTML = '';
|
||||||
|
|
||||||
|
let hasFilters = false;
|
||||||
|
|
||||||
|
const filterLabels = {
|
||||||
|
author: 'Author',
|
||||||
|
genre: 'Genre',
|
||||||
|
series: 'Series',
|
||||||
|
language: 'Language',
|
||||||
|
yearMin: 'Year From',
|
||||||
|
yearMax: 'Year To',
|
||||||
|
hasCover: 'Has Cover'
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.entries(filters).forEach(([key, value]) => {
|
||||||
|
if (value && value !== 0) {
|
||||||
|
hasFilters = true;
|
||||||
|
const label = filterLabels[key] || key;
|
||||||
|
const displayValue = key === 'hasCover' ? 'Yes' : value;
|
||||||
|
|
||||||
|
const chip = document.createElement('span');
|
||||||
|
chip.className = 'px-3 py-1 rounded-full text-sm flex items-center gap-2 cursor-pointer';
|
||||||
|
chip.style.cssText = 'background-color: var(--accent); color: var(--bg-primary)';
|
||||||
|
chip.innerHTML = `${label}: ${displayValue} <button class="font-bold hover:opacity-80">×</button>`;
|
||||||
|
chip.onclick = () => clearFilter(key);
|
||||||
|
chipsContainer.appendChild(chip);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
container.classList.toggle('hidden', !hasFilters);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFilter(filterKey) {
|
||||||
|
switch (filterKey) {
|
||||||
|
case 'author':
|
||||||
|
document.getElementById('filter-author').value = '';
|
||||||
|
break;
|
||||||
|
case 'genre':
|
||||||
|
document.getElementById('filter-genre').value = '';
|
||||||
|
break;
|
||||||
|
case 'series':
|
||||||
|
document.getElementById('filter-series').value = '';
|
||||||
|
break;
|
||||||
|
case 'language':
|
||||||
|
document.getElementById('filter-language').value = '';
|
||||||
|
break;
|
||||||
|
case 'yearMin':
|
||||||
|
document.getElementById('filter-year-min').value = '';
|
||||||
|
break;
|
||||||
|
case 'yearMax':
|
||||||
|
document.getElementById('filter-year-max').value = '';
|
||||||
|
break;
|
||||||
|
case 'hasCover':
|
||||||
|
document.getElementById('filter-has-cover').checked = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
applyFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFilters() {
|
||||||
|
document.getElementById('filter-author').value = '';
|
||||||
|
document.getElementById('filter-genre').value = '';
|
||||||
|
document.getElementById('filter-series').value = '';
|
||||||
|
document.getElementById('filter-language').value = '';
|
||||||
|
document.getElementById('filter-year-min').value = '';
|
||||||
|
document.getElementById('filter-year-max').value = '';
|
||||||
|
document.getElementById('filter-has-cover').checked = false;
|
||||||
|
|
||||||
|
// Clear URL params
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.set('library_id', currentLibrary.id);
|
||||||
|
window.history.pushState({}, '', `?${params.toString()}`);
|
||||||
|
|
||||||
|
loadMediaItems();
|
||||||
|
document.getElementById('active-filters').classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
function viewMediaItem(mediaId) {
|
function viewMediaItem(mediaId) {
|
||||||
// TODO: Implement detailed media view
|
// TODO: Implement detailed media view
|
||||||
alert('Media viewer coming soon!');
|
alert('Media viewer coming soon!');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load libraries on page load
|
// Load filters from URL on page load
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
loadTheme();
|
loadTheme();
|
||||||
loadLibraries();
|
loadLibraries();
|
||||||
|
|
||||||
|
// Restore filters from URL
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
if (params.has('author_filter')) {
|
||||||
|
document.getElementById('filter-author').value = params.get('author_filter');
|
||||||
|
}
|
||||||
|
if (params.has('genre_filter')) {
|
||||||
|
document.getElementById('filter-genre').value = params.get('genre_filter');
|
||||||
|
}
|
||||||
|
if (params.has('series_filter')) {
|
||||||
|
document.getElementById('filter-series').value = params.get('series_filter');
|
||||||
|
}
|
||||||
|
if (params.has('language_filter')) {
|
||||||
|
document.getElementById('filter-language').value = params.get('language_filter');
|
||||||
|
}
|
||||||
|
if (params.has('year_min')) {
|
||||||
|
document.getElementById('filter-year-min').value = params.get('year_min');
|
||||||
|
}
|
||||||
|
if (params.has('year_max')) {
|
||||||
|
document.getElementById('filter-year-max').value = params.get('year_max');
|
||||||
|
}
|
||||||
|
if (params.has('has_cover')) {
|
||||||
|
document.getElementById('filter-has-cover').checked = params.get('has_cover') === 'true';
|
||||||
|
}
|
||||||
|
if (params.has('sort')) {
|
||||||
|
document.getElementById('sort-select').value = params.get('sort');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -29,7 +29,15 @@ func Dashboard(user User) templ.Component {
|
|||||||
templ_7745c5c3_Var1 = templ.NopComponent
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
}
|
}
|
||||||
ctx = templ.ClearChildren(ctx)
|
ctx = templ.ClearChildren(ctx)
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Dashboard - 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\"><style>\n :root {\n --bg-primary: #1a1b26;\n --bg-secondary: #16161e;\n --text-primary: #a9b1d6;\n --text-secondary: #565f89;\n --accent: #7aa2f7;\n --border: #414868;\n }\n .theme-tokyo-night {\n --bg-primary: #1a1b26;\n --bg-secondary: #16161e;\n --text-primary: #a9b1d6;\n --text-secondary: #565f89;\n --accent: #7aa2f7;\n --border: #414868;\n }\n body {\n background-color: var(--bg-primary);\n color: var(--text-primary);\n }\n .card {\n background-color: var(--bg-secondary);\n border-color: var(--border);\n }\n .btn-primary {\n background-color: var(--accent);\n color: var(--bg-primary);\n }\n .btn-primary:hover {\n opacity: 0.8;\n }\n .rating-star {\n display: inline-block;\n transition: all 0.2s ease;\n cursor: pointer;\n position: relative;\n }\n .rating-star:hover {\n transform: scale(1.1);\n }\n .half-star {\n position: relative;\n display: inline-block;\n }\n .half-star::before {\n content: '★';\n position: absolute;\n left: 0;\n top: 0;\n width: 50%;\n overflow: hidden;\n color: #facc15; /* yellow-400 */\n }\n .rating-text {\n font-size: 0.875rem;\n margin-left: 0.5rem;\n opacity: 0.8;\n }\n </style></head><body class=\"theme-tokyo-night\">Header(user, \"/dashboard\")<div class=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8\"><!-- Header Section --><div class=\"mb-8\"><div><h2 class=\"text-3xl font-bold\" style=\"color: var(--text-primary)\">Your Media Library</h2><p style=\"color: var(--text-secondary)\">Browse and manage your media collections</p></div></div><!-- Libraries Section --><div id=\"libraries-container\" class=\"mb-8\"><!-- Libraries will be loaded here --></div><!-- Media Items Section --><div id=\"media-section\" class=\"hidden\"><!-- Search and Filter --><div class=\"mb-6\"><div class=\"flex flex-col sm:flex-row gap-4\"><button onclick=\"backToLibraries()\" class=\"btn-secondary px-4 py-2 rounded-lg\">← Back to Libraries</button> <input type=\"text\" id=\"search-input\" placeholder=\"Search media...\" class=\"flex-1 px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)\" onkeyup=\"filterMedia()\"> <select id=\"sort-select\" class=\"px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)\" onchange=\"loadMediaItems()\"><option value=\"created_at DESC\">Newest First</option> <option value=\"title ASC\">Title A-Z</option> <option value=\"author ASC\">Author A-Z</option> <option value=\"created_at ASC\">Oldest First</option></select></div></div><!-- Media Grid --><div id=\"media-container\" class=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\"><!-- Media items will be loaded here --></div></div><!-- Loading indicator --><div id=\"loading\" class=\"text-center py-8\" style=\"color: var(--text-secondary)\">Loading your media libraries...</div></div><script>\n let libraries = [];\n let currentLibrary = null;\n let mediaItems = [];\n\n function loadLibraries() {\n const loading = document.getElementById('loading');\n const librariesContainer = document.getElementById('libraries-container');\n \n loading.style.display = 'block';\n librariesContainer.innerHTML = '';\n\n fetch('/api/libraries/visible', {\n headers: {\n 'Authorization': 'Bearer ' + localStorage.getItem('token'),\n 'Content-Type': 'application/json'\n }\n })\n .then(response => response.json())\n .then(data => {\n loading.style.display = 'none';\n libraries = data;\n Line truncated
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Dashboard - Bookmann</title><script src=\"/static/htmx.min.js\"></script><script src=\"/static/toast.js\"></script><script src=\"/static/header.js\"></script><script src=\"/static/search.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"><style>\n :root {\n --bg-primary: #1a1b26;\n --bg-secondary: #16161e;\n --text-primary: #a9b1d6;\n --text-secondary: #565f89;\n --accent: #7aa2f7;\n --border: #414868;\n }\n .theme-tokyo-night {\n --bg-primary: #1a1b26;\n --bg-secondary: #16161e;\n --text-primary: #a9b1d6;\n --text-secondary: #565f89;\n --accent: #7aa2f7;\n --border: #414868;\n }\n body {\n background-color: var(--bg-primary);\n color: var(--text-primary);\n }\n .card {\n background-color: var(--bg-secondary);\n border-color: var(--border);\n }\n .btn-primary {\n background-color: var(--accent);\n color: var(--bg-primary);\n }\n .btn-primary:hover {\n opacity: 0.8;\n }\n .rating-star {\n display: inline-block;\n transition: all 0.2s ease;\n cursor: pointer;\n position: relative;\n }\n .rating-star:hover {\n transform: scale(1.1);\n }\n .half-star {\n position: relative;\n display: inline-block;\n }\n .half-star::before {\n content: '★';\n position: absolute;\n left: 0;\n top: 0;\n width: 50%;\n overflow: hidden;\n color: #facc15; /* yellow-400 */\n }\n .rating-text {\n font-size: 0.875rem;\n margin-left: 0.5rem;\n opacity: 0.8;\n }\n </style></head><body class=\"theme-tokyo-night\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = Header(user, "/dashboard").Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8\"><!-- Header Section --><div class=\"mb-8\"><div><h2 class=\"text-3xl font-bold\" style=\"color: var(--text-primary)\">Your Media Library</h2><p style=\"color: var(--text-secondary)\">Browse and manage your media collections</p></div></div><!-- Libraries Section --><div id=\"libraries-container\" class=\"mb-8\"><!-- Libraries will be loaded here --></div><!-- Media Items Section --><div id=\"media-section\" class=\"hidden\"><!-- Search and Filter --><div class=\"mb-6\"><div class=\"flex flex-col sm:flex-row gap-4\"><button onclick=\"backToLibraries()\" class=\"btn-secondary px-4 py-2 rounded-lg\">← Back to Libraries</button> <input type=\"text\" id=\"search-input\" placeholder=\"Search media...\" class=\"flex-1 px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)\" onkeyup=\"filterMedia()\"> <select id=\"sort-select\" class=\"px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)\" onchange=\"loadMediaItems()\"><option value=\"created_at DESC\">Newest Added</option> <option value=\"created_at ASC\">Oldest Added</option> <option value=\"title ASC\">Title A-Z</option> <option value=\"title DESC\">Title Z-A</option> <option value=\"author ASC\">Author A-Z</option> <option value=\"author DESC\">Author Z-A</option> <option value=\"series ASC\">Series Order</option> <option value=\"date_published DESC\">Newest Published</option> <option value=\"copyright_year DESC\">Copyright Year (Newest)</option> <option value=\"page_count ASC\">Shortest First</option> <option value=\"page_count DESC\">Longest First</option> <option value=\"genre ASC\">Genre A-Z</option></select></div></div><!-- Filters Panel --><div class=\"filter-panel mb-6\"><details><summary class=\"cursor-pointer font-semibold mb-4 flex items-center gap-2\" style=\"color: var(--text-primary)\"><svg class=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z\"></path></svg> Filters</summary><div class=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-4\"><div><label class=\"block text-sm mb-1\" style=\"color: var(--text-secondary)\">Author</label> <select id=\"filter-author\" class=\"w-full px-3 py-2 border rounded-lg\" style=\"background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)\"><option value=\"\">All Authors</option></select></div><div><label class=\"block text-sm mb-1\" style=\"color: var(--text-secondary)\">Genre</label> <select id=\"filter-genre\" class=\"w-full px-3 py-2 border rounded-lg\" style=\"background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)\"><option value=\"\">All Genres</option></select></div><div><label class=\"block text-sm mb-1\" style=\"color: var(--text-secondary)\">Series</label> <select id=\"filter-series\" class=\"w-full px-3 py-2 border rounded-lg\" style=\"background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)\"><option value=\"\">All Series</option></select></div><div><label class=\"block text-sm mb-1\" style=\"color: var(--text-secondary)\">Language</label> <select id=\"filter-language\" class=\"w-full px-3 py-2 border rounded-lg\" style=\"background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)\"><option value=\"\">All Languages</option> <option value=\"en\">English</option> <option value=\"es\">Spanish</option> <option value=\"fr\">French</option> <option value=\"de\">German</option> <option value=\"ja\">Japanese</option> <option value=\"zh\">Chinese</option> <option value=\"ko\">Korean</option> <option value=\"ru\">Russian</option> <option value=\"it\">Italian</option> <option value=\"pt\">Portuguese</option></select></div><div><label class=\"block text-sm mb-1\" style=\"color: var(--text-secondary)\">Year Range</label><div class=\"flex gap-2\"><input type=\"number\" id=\"filter-year-min\" placeholder=\"From\" class=\"w-1/2 px-3 py-2 border rounded-lg\" style=\"background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)\" min=\"1800\" max=\"2100\"> <input type=\"number\" id=\"filter-year-max\" placeholder=\"To\" class=\"w-1/2 px-3 py-2 border rounded-lg\" style=\"background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)\" min=\"1800\" max=\"2100\"></div></div><div class=\"flex items-end\"><label class=\"flex items-center gap-2 cursor-pointer\"><input type=\"checkbox\" id=\"filter-has-cover\" class=\"w-4 h-4 rounded\"> <span class=\"text-sm\" style=\"coLine truncated
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user