From c9c4cc074c5b568d9197a23a820b5bf5d736bca9 Mon Sep 17 00:00:00 2001 From: lenafx Date: Wed, 31 Dec 2025 20:01:57 +0100 Subject: [PATCH] new dashboard page --- desktop/src/scripts/dashboard.js | 468 ++++++++++++ desktop/src/scripts/list.js | 380 ---------- desktop/src/scripts/users.js | 2 +- desktop/src/views/views.routes.ts | 8 +- desktop/views/components/navbar.html | 2 +- desktop/views/css/dashboard.css | 1031 ++++++++++++++++++++++++++ desktop/views/css/list.css | 485 ------------ desktop/views/dashboard.html | 195 +++++ desktop/views/list.html | 145 ---- docker/src/scripts/dashboard.js | 468 ++++++++++++ docker/src/scripts/list.js | 380 ---------- docker/src/scripts/users.js | 2 +- docker/src/views/views.routes.ts | 8 +- docker/views/components/navbar.html | 2 +- docker/views/css/dashboard.css | 1031 ++++++++++++++++++++++++++ docker/views/css/list.css | 507 ------------- docker/views/dashboard.html | 195 +++++ docker/views/list.html | 132 ---- 18 files changed, 3400 insertions(+), 2041 deletions(-) create mode 100644 desktop/src/scripts/dashboard.js delete mode 100644 desktop/src/scripts/list.js create mode 100644 desktop/views/css/dashboard.css delete mode 100644 desktop/views/css/list.css create mode 100644 desktop/views/dashboard.html delete mode 100644 desktop/views/list.html create mode 100644 docker/src/scripts/dashboard.js delete mode 100644 docker/src/scripts/list.js create mode 100644 docker/views/css/dashboard.css delete mode 100644 docker/views/css/list.css create mode 100644 docker/views/dashboard.html delete mode 100644 docker/views/list.html diff --git a/desktop/src/scripts/dashboard.js b/desktop/src/scripts/dashboard.js new file mode 100644 index 0000000..1c7b341 --- /dev/null +++ b/desktop/src/scripts/dashboard.js @@ -0,0 +1,468 @@ +const API_BASE = '/api'; +let currentList = []; +let filteredList = []; +let currentUserId = null; + +// Configuración de paginación +const ITEMS_PER_PAGE = 50; +let visibleCount = ITEMS_PER_PAGE; + +// Inicialización +document.addEventListener('DOMContentLoaded', async () => { + await initUser(); + await loadList(); + setupEventListeners(); + setupTabSystem(); +}); + +async function initUser() { + try { + const headers = window.AuthUtils.getSimpleAuthHeaders(); + const res = await fetch(`${API_BASE}/me`, { headers }); + + if (res.ok) { + const data = await res.json(); + document.getElementById('user-username').textContent = data.username; + document.getElementById('setting-username').value = data.username; + + if (data.avatar) { + document.getElementById('user-avatar').src = data.avatar; + if (data.avatar.startsWith('http')) { + document.getElementById('setting-avatar-url').value = data.avatar; + } else { + document.getElementById('setting-avatar-url').placeholder = "Image uploaded via file (Base64)"; + document.getElementById('setting-avatar-url').value = ""; + } + } + } + + const token = localStorage.getItem('token'); + if (token) { + const payload = JSON.parse(atob(token.split('.')[1])); + currentUserId = payload.id; + await checkIntegrations(currentUserId); + } + + } catch (err) { + console.error("Error loading user profile:", err); + } +} + +async function checkIntegrations(userId) { + if (!userId) return; + try { + const res = await fetch(`${API_BASE}/users/${userId}/integration`); + let data = { connected: false }; + if (res.ok) data = await res.json(); + + const statusEl = document.getElementById('anilist-status'); + const btn = document.getElementById('anilist-action-btn'); + const headerBadge = document.getElementById('header-anilist-link'); + + if (data.connected) { + if (headerBadge) { + headerBadge.style.display = 'flex'; + headerBadge.href = `https://anilist.co/user/${data.anilistUserId}`; + headerBadge.title = `Connected as ${data.anilistUserId}`; + } + if (statusEl) { + statusEl.textContent = `Connected as ID: ${data.anilistUserId}`; + statusEl.style.color = 'var(--color-success)'; + } + if (btn) { + btn.textContent = 'Disconnect'; + btn.classList.add('btn-danger-outline'); + btn.classList.remove('btn-blur'); + btn.onclick = () => disconnectAniList(userId); + } + } else { + if (headerBadge) headerBadge.style.display = 'none'; + if (statusEl) { + statusEl.textContent = 'Not connected'; + statusEl.style.color = 'var(--color-text-secondary)'; + } + if (btn) { + btn.textContent = 'Connect'; + btn.classList.remove('btn-danger-outline'); + btn.classList.add('btn-blur'); + btn.onclick = () => redirectToAniListLogin(); + } + } + } catch (e) { console.error("Integration check error:", e); } +} + +async function redirectToAniListLogin() { + if (!currentUserId) return; + try { + const clientId = 32898; + const redirectUri = encodeURIComponent(window.location.origin + '/api/anilist'); + const state = encodeURIComponent(currentUserId); + + window.location.href = `https://anilist.co/api/v2/oauth/authorize?client_id=${clientId}&response_type=code&redirect_uri=${redirectUri}&state=${state}`; + } catch (err) { console.error(err); alert('Error starting AniList login'); } +} + +async function disconnectAniList(userId) { + if(!confirm("Disconnect AniList?")) return; + try { + const token = localStorage.getItem('token'); + await fetch(`${API_BASE}/users/${userId}/integration`, { + method: 'DELETE', + headers: { 'Authorization': `Bearer ${token}` } + }); + checkIntegrations(userId); + } catch (e) { alert("Failed to disconnect"); } +} + +function setupTabSystem() { + const tabs = document.querySelectorAll('.nav-pill'); + const sections = document.querySelectorAll('.tab-section'); + + tabs.forEach(tab => { + tab.addEventListener('click', () => { + tabs.forEach(t => t.classList.remove('active')); + tab.classList.add('active'); + + const targetId = `section-${tab.dataset.target}`; + sections.forEach(sec => { + sec.classList.remove('active'); + if (sec.id === targetId) sec.classList.add('active'); + }); + + if (tab.dataset.target === 'local') loadLocalStats(); + }); + }); +} + +async function loadLocalStats() { + const types = ['anime', 'manga', 'novels']; + const elements = { 'anime': 'local-anime-count', 'manga': 'local-manga-count', 'novels': 'local-novel-count' }; + + for (const type of types) { + try { + const res = await fetch(`${API_BASE}/library/${type}`, { headers: window.AuthUtils.getSimpleAuthHeaders() }); + if(res.ok) { + const data = await res.json(); + const elId = elements[type]; + if (document.getElementById(elId)) document.getElementById(elId).textContent = `${data.length} items`; + } + } catch (e) { console.error(e); } + } +} + +async function triggerScan(mode) { + const consoleDiv = document.getElementById('scan-console'); + const statusText = document.getElementById('scan-status-text'); + consoleDiv.style.display = 'flex'; + statusText.textContent = `Starting ${mode} scan...`; + + try { + const res = await fetch(`${API_BASE}/library/scan?mode=${mode}`, { + method: 'POST', + headers: window.AuthUtils.getSimpleAuthHeaders() + }); + if (res.ok) { + statusText.textContent = "Scan completed successfully!"; + setTimeout(() => { consoleDiv.style.display = 'none'; loadLocalStats(); }, 3000); + } else throw new Error('Scan failed'); + } catch (e) { + statusText.textContent = "Error during scan."; + statusText.style.color = 'var(--color-danger)'; + } +} + +async function handleProfileUpdate(e) { + e.preventDefault(); + if (!currentUserId) return; + + const username = document.getElementById('setting-username').value; + const urlInput = document.getElementById('setting-avatar-url').value; + const fileInput = document.getElementById('avatar-upload'); + let finalAvatar = null; + + if (fileInput.files && fileInput.files[0]) { + const toBase64 = file => new Promise((res, rej) => { + const r = new FileReader(); r.readAsDataURL(file); + r.onload = () => res(r.result); r.onerror = rej; + }); + try { finalAvatar = await toBase64(fileInput.files[0]); } catch (err) { alert("Error reading file"); return; } + } else if (urlInput.trim() !== "") { + finalAvatar = urlInput.trim(); + } + + const bodyData = { username }; + if (finalAvatar) bodyData.profilePictureUrl = finalAvatar; + + try { + const res = await fetch(`${API_BASE}/users/${currentUserId}`, { + method: 'PUT', + headers: { ...window.AuthUtils.getSimpleAuthHeaders(), 'Content-Type': 'application/json' }, + body: JSON.stringify(bodyData) + }); + if (res.ok) alert('Profile updated successfully!'); + else { const err = await res.json(); alert(err.error || 'Update failed'); } + } catch (e) { console.error(e); } +} + +async function handlePasswordChange(e) { + e.preventDefault(); + if (!currentUserId) return; + const currentPassword = document.getElementById('current-password').value; + const newPassword = document.getElementById('new-password').value; + + try { + const res = await fetch(`${API_BASE}/users/${currentUserId}/password`, { + method: 'PUT', + headers: { ...window.AuthUtils.getSimpleAuthHeaders(), 'Content-Type': 'application/json' }, + body: JSON.stringify({ currentPassword, newPassword }) + }); + const data = await res.json(); + if (res.ok) { alert("Password updated successfully"); document.getElementById('password-form').reset(); } + else alert(data.error || "Failed to update password"); + } catch (e) { console.error(e); } +} + +function setupEventListeners() { + document.getElementById('scan-incremental-btn')?.addEventListener('click', () => triggerScan('incremental')); + document.getElementById('scan-full-btn')?.addEventListener('click', () => triggerScan('full')); + document.getElementById('profile-form')?.addEventListener('submit', handleProfileUpdate); + document.getElementById('password-form')?.addEventListener('submit', handlePasswordChange); + document.getElementById('logout-btn')?.addEventListener('click', () => window.AuthUtils.logout()); + + const fileInput = document.getElementById('avatar-upload'); + if (fileInput) { + fileInput.addEventListener('change', function(e) { + const file = e.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = evt => { + document.getElementById('user-avatar').src = evt.target.result; + document.getElementById('setting-avatar-url').value = ''; + }; + reader.readAsDataURL(file); + } + }); + } + + document.querySelector('.search-input').addEventListener('input', () => applyFilters()); + ['status-filter', 'type-filter', 'sort-filter'].forEach(id => { + document.getElementById(id).addEventListener('change', () => applyFilters()); + }); + + document.querySelectorAll('.view-btn').forEach(btn => { + btn.addEventListener('click', () => { + document.querySelectorAll('.view-btn').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + const view = btn.dataset.view; + const container = document.getElementById('list-container'); + if (view === 'list') container.classList.add('list-view'); + else container.classList.remove('list-view'); + }); + }); +} + +// --- LOGICA DE LISTA --- + +async function loadList() { + const loadingState = document.getElementById('loading-state'); + const emptyState = document.getElementById('empty-state'); + const container = document.getElementById('list-container'); + + try { + loadingState.style.display = 'flex'; + emptyState.style.display = 'none'; + container.innerHTML = ''; + + const response = await fetch(`${API_BASE}/list`, { headers: window.AuthUtils.getSimpleAuthHeaders() }); + if (!response.ok) throw new Error('Failed'); + + const data = await response.json(); + currentList = data.results || []; + + const animeCount = currentList.filter(item => item.entry_type === 'ANIME').length; + const mangaCount = currentList.filter(item => item.entry_type === 'MANGA').length; + + document.getElementById('total-stat').textContent = currentList.length; + if (document.getElementById('anime-stat')) document.getElementById('anime-stat').textContent = animeCount; + if (document.getElementById('manga-stat')) document.getElementById('manga-stat').textContent = mangaCount; + + loadingState.style.display = 'none'; + if (currentList.length === 0) emptyState.style.display = 'flex'; + else applyFilters(); + + } catch (error) { + console.error(error); + loadingState.style.display = 'none'; + } +} + +function applyFilters() { + const statusFilter = document.getElementById('status-filter').value; + const typeFilter = document.getElementById('type-filter').value; + const sortFilter = document.getElementById('sort-filter').value; + const searchQuery = document.querySelector('.search-input').value.toLowerCase().trim(); + + let result = [...currentList]; + + if (searchQuery) result = result.filter(item => (item.title || '').toLowerCase().includes(searchQuery)); + if (statusFilter !== 'all') result = result.filter(item => item.status === statusFilter); + if (typeFilter !== 'all') result = result.filter(item => item.entry_type === typeFilter); + + if (sortFilter === 'title') result.sort((a, b) => (a.title || '').localeCompare(b.title || '')); + else if (sortFilter === 'score') result.sort((a, b) => (b.score || 0) - (a.score || 0)); + else result.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at)); + + filteredList = result; + visibleCount = ITEMS_PER_PAGE; + renderList(); +} + +function renderList() { + const container = document.getElementById('list-container'); + container.innerHTML = ''; + + if (filteredList.length === 0) { + container.innerHTML = '
No matches found
'; + return; + } + + const itemsToShow = filteredList.slice(0, visibleCount); + itemsToShow.forEach(item => container.appendChild(createListItem(item))); + + if (visibleCount < filteredList.length) { + const remaining = filteredList.length - visibleCount; + const btnContainer = document.createElement('div'); + btnContainer.style.gridColumn = "1 / -1"; + btnContainer.style.display = "flex"; + btnContainer.style.justifyContent = "center"; + btnContainer.style.padding = "2rem 0"; + + const loadMoreBtn = document.createElement('button'); + loadMoreBtn.className = "btn-blur"; + loadMoreBtn.textContent = `Show All (${remaining} more)`; + loadMoreBtn.onclick = () => { visibleCount = filteredList.length; renderList(); }; + btnContainer.appendChild(loadMoreBtn); + container.appendChild(btnContainer); + } +} + +function createListItem(item) { + const div = document.createElement('div'); + div.className = 'list-item'; + + const itemLink = getEntryLink(item); + const posterUrl = item.poster || '/public/assets/placeholder.svg'; + const progress = item.progress || 0; + const totalUnits = item.entry_type === 'ANIME' ? item.total_episodes || 0 : item.total_chapters || 0; + const progressPercent = totalUnits > 0 ? (progress / totalUnits) * 100 : 0; + const score = item.score ? item.score.toFixed(1) : null; + const repeatCount = item.repeat_count || 0; + const entryType = (item.entry_type).toUpperCase(); + const unitLabel = entryType === 'ANIME' ? 'episodes' : 'chapters'; + + const statusLabels = { + 'CURRENT': entryType === 'ANIME' ? 'Watching' : 'Reading', + 'COMPLETED': 'Completed', + 'PLANNING': 'Planning', + 'PAUSED': 'Paused', + 'DROPPED': 'Dropped', + 'REPEATING': entryType === 'ANIME' ? 'Rewatching' : 'Rereading' + }; + + const extraInfo = []; + if (repeatCount > 0) extraInfo.push(`🔁 ${repeatCount}`); + if (item.is_private) extraInfo.push('🔒 Private'); + + div.innerHTML = ` + + ${item.title || 'Entry'} + +
+
+ +

${item.title || 'Unknown Title'}

+
+
+ ${statusLabels[item.status] || item.status} + ${entryType} + ${item.source.toUpperCase()} + ${extraInfo.join('')} +
+
+
+
+
+
+
+ ${progress}${totalUnits > 0 ? ` / ${totalUnits}` : ''} ${unitLabel} ${score ? `⭐ ${score}` : ''} +
+
+
+ + `; + + // Lógica para abrir el Modal (Estilo book.js: seteamos currentData) + const editBtn = div.querySelector('.edit-icon-btn'); + editBtn.onclick = (e) => { + e.preventDefault(); + e.stopPropagation(); + + // 1. Configuramos el modal manager con los datos de ESTE item + window.ListModalManager.currentData = item; + window.ListModalManager.isInList = true; + window.ListModalManager.currentEntry = item; // Ya tenemos los datos de la lista + + // 2. Abrimos el modal + window.ListModalManager.open(item, item.source || 'anilist'); + }; + + return div; +} + +function getEntryLink(item) { + const isAnime = item.entry_type?.toUpperCase() === 'ANIME'; + const baseRoute = isAnime ? '/anime' : '/book'; + return `${baseRoute}/${item.entry_id}`; +} + +// ========================================================= +// EXPORTS GLOBALES (Estilo book.js) +// Estas funciones son llamadas por los onclick del HTML del Modal +// ========================================================= + +window.saveToList = async () => { + // Recuperamos los datos que seteamos en el onclick del botón editar + const data = window.ListModalManager.currentData; + if (!data) return; + + // En la vista de lista, el ID suele ser 'entry_id', pero usamos un fallback + const idToSave = data.entry_id || data.id; + const source = data.source || 'anilist'; + + // Llamamos al manager pasando ID y Source explícitamente como en book.js + await window.ListModalManager.save(idToSave, source); + + // IMPORTANTE: Recargar la lista para ver los cambios + await loadList(); +}; + +window.deleteFromList = async () => { + const data = window.ListModalManager.currentData; + if (!data) return; + + const idToDelete = data.entry_id || data.id; + const source = data.source || 'anilist'; + + await window.ListModalManager.delete(idToDelete, source); + + // IMPORTANTE: Recargar la lista + await loadList(); +}; + +window.closeAddToListModal = () => { + window.ListModalManager.close(); +}; \ No newline at end of file diff --git a/desktop/src/scripts/list.js b/desktop/src/scripts/list.js deleted file mode 100644 index 3271bdb..0000000 --- a/desktop/src/scripts/list.js +++ /dev/null @@ -1,380 +0,0 @@ -const API_BASE = '/api'; -let currentList = []; -let filteredList = []; - -document.addEventListener('DOMContentLoaded', async () => { - await loadList(); - setupEventListeners(); -}); - -function getEntryLink(item) { - const isAnime = item.entry_type?.toUpperCase() === 'ANIME'; - const baseRoute = isAnime ? '/anime' : '/book'; - const source = item.source || 'anilist'; - - if (source === 'anilist') { - return `${baseRoute}/${item.entry_id}`; - } else { - return `${baseRoute}/${source}/${item.entry_id}`; - } -} - -async function populateSourceFilter() { - const select = document.getElementById('source-filter'); - if (!select) return; - - select.innerHTML = ` - - - `; - - try { - const [animeRes, bookRes] = await Promise.all([ - fetch(`${API_BASE}/extensions/anime`), - fetch(`${API_BASE}/extensions/book`) - ]); - - const extensions = new Set(); - - if (animeRes.ok) { - const data = await animeRes.json(); - (data.extensions || []).forEach(ext => extensions.add(ext)); - } - - if (bookRes.ok) { - const data = await bookRes.json(); - (data.extensions || []).forEach(ext => extensions.add(ext)); - } - - extensions.forEach(extName => { - const lower = extName.toLowerCase(); - if (lower !== 'anilist' && lower !== 'local') { - const option = document.createElement('option'); - option.value = extName; - option.textContent = extName.charAt(0).toUpperCase() + extName.slice(1); - select.appendChild(option); - } - }); - } catch (error) { - console.error('Error loading extensions:', error); - } -} - -function updateLocalList(entryData, action) { - const entryId = entryData.entry_id; - const source = entryData.source; - - const findIndex = (list) => list.findIndex(e => - e.entry_id === entryId && e.source === source - ); - - const currentIndex = findIndex(currentList); - if (currentIndex !== -1) { - if (action === 'update') { - - currentList[currentIndex] = { ...currentList[currentIndex], ...entryData }; - } else if (action === 'delete') { - currentList.splice(currentIndex, 1); - } - } else if (action === 'update') { - - currentList.push(entryData); - } - - filteredList = [...currentList]; - - updateStats(); - applyFilters(); - window.ListModalManager.close(); -} - -function setupEventListeners() { - - document.querySelectorAll('.view-btn').forEach(btn => { - btn.addEventListener('click', () => { - document.querySelectorAll('.view-btn').forEach(b => b.classList.remove('active')); - btn.classList.add('active'); - const view = btn.dataset.view; - const container = document.getElementById('list-container'); - if (view === 'list') { - container.classList.add('list-view'); - } else { - container.classList.remove('list-view'); - } - }); - }); - - document.getElementById('status-filter').addEventListener('change', applyFilters); - document.getElementById('source-filter').addEventListener('change', applyFilters); - document.getElementById('type-filter').addEventListener('change', applyFilters); - document.getElementById('sort-filter').addEventListener('change', applyFilters); - - document.querySelector('.search-input').addEventListener('input', (e) => { - const query = e.target.value.toLowerCase(); - if (query) { - filteredList = currentList.filter(item => - item.title?.toLowerCase().includes(query) - ); - } else { - filteredList = [...currentList]; - } - applyFilters(); - }); - - document.getElementById('modal-save-btn')?.addEventListener('click', async () => { - - const entryToSave = window.ListModalManager.currentEntry || window.ListModalManager.currentData; - - if (!entryToSave) return; - - const success = await window.ListModalManager.save(entryToSave.entry_id, entryToSave.source); - - if (success) { - - const updatedEntry = window.ListModalManager.currentEntry; - updatedEntry.updated_at = new Date().toISOString(); - - updateLocalList(updatedEntry, 'update'); - } - - }); - - document.getElementById('modal-delete-btn')?.addEventListener('click', async () => { - const entryToDelete = window.ListModalManager.currentEntry || window.ListModalManager.currentData; - - if (!entryToDelete) return; - - const success = await window.ListModalManager.delete(entryToDelete.entry_id, entryToDelete.source); - - if (success) { - updateLocalList(entryToDelete, 'delete'); - } - - }); - - document.getElementById('add-list-modal')?.addEventListener('click', (e) => { - if (e.target.id === 'add-list-modal') { - window.ListModalManager.close(); - } - }); -} - -async function loadList() { - const loadingState = document.getElementById('loading-state'); - const emptyState = document.getElementById('empty-state'); - const container = document.getElementById('list-container'); - - await populateSourceFilter(); - - try { - loadingState.style.display = 'flex'; - emptyState.style.display = 'none'; - container.innerHTML = ''; - - const response = await fetch(`${API_BASE}/list`, { - headers: window.AuthUtils.getSimpleAuthHeaders() - }); - - if (!response.ok) { - throw new Error('Failed to load list'); - } - - const data = await response.json(); - currentList = data.results || []; - filteredList = [...currentList]; - - loadingState.style.display = 'none'; - - if (currentList.length === 0) { - emptyState.style.display = 'flex'; - } else { - updateStats(); - applyFilters(); - } - } catch (error) { - console.error('Error loading list:', error); - loadingState.style.display = 'none'; - if (window.NotificationUtils) { - window.NotificationUtils.error('Failed to load your list. Please try again.'); - } else { - alert('Failed to load your list. Please try again.'); - } - } -} - -function updateStats() { - - const total = currentList.length; - const watching = currentList.filter(item => item.status === 'WATCHING').length; - const completed = currentList.filter(item => item.status === 'COMPLETED').length; - const planning = currentList.filter(item => item.status === 'PLANNING').length; - - document.getElementById('total-count').textContent = total; - document.getElementById('watching-count').textContent = watching; - document.getElementById('completed-count').textContent = completed; - document.getElementById('planned-count').textContent = planning; -} - -function applyFilters() { - const statusFilter = document.getElementById('status-filter').value; - const sourceFilter = document.getElementById('source-filter').value; - const typeFilter = document.getElementById('type-filter').value; - const sortFilter = document.getElementById('sort-filter').value; - - let filtered = [...filteredList]; - - if (statusFilter !== 'all') { - filtered = filtered.filter(item => item.status === statusFilter); - } - - if (sourceFilter !== 'all') { - filtered = filtered.filter(item => item.source === sourceFilter); - } - - if (typeFilter !== 'all') { - filtered = filtered.filter(item => item.entry_type === typeFilter); - } - - switch (sortFilter) { - case 'title': - filtered.sort((a, b) => (a.title || '').localeCompare(b.title || '')); - break; - case 'score': - filtered.sort((a, b) => (b.score || 0) - (a.score || 0)); - break; - case 'progress': - filtered.sort((a, b) => (b.progress || 0) - (a.progress || 0)); - break; - case 'updated': - default: - - filtered.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at)); - break; - } - - renderList(filtered); -} - -function renderList(items) { - const container = document.getElementById('list-container'); - container.innerHTML = ''; - - if (items.length === 0) { - - if (currentList.length === 0) { - document.getElementById('empty-state').style.display = 'flex'; - } else { - - container.innerHTML = '

No entries match your filters

'; - } - return; - } - - document.getElementById('empty-state').style.display = 'none'; - - items.forEach(item => { - const element = createListItem(item); - container.appendChild(element); - }); -} - -function createListItem(item) { - const div = document.createElement('div'); - div.className = 'list-item'; - - const itemLink = getEntryLink(item); - - const posterUrl = item.poster || '/public/assets/placeholder.svg'; - const progress = item.progress || 0; - - const totalUnits = item.entry_type === 'ANIME' ? - item.total_episodes || 0 : - item.total_chapters || 0; - - const progressPercent = totalUnits > 0 ? (progress / totalUnits) * 100 : 0; - const score = item.score ? item.score.toFixed(1) : null; - const repeatCount = item.repeat_count || 0; - - const entryType = (item.entry_type).toUpperCase(); - let unitLabel = 'units'; - if (entryType === 'ANIME') { - unitLabel = 'episodes'; - } else if (entryType === 'MANGA') { - unitLabel = 'chapters'; - } else if (entryType === 'NOVEL') { - unitLabel = 'chapters/volumes'; - } - - const statusLabels = { - 'CURRENT': entryType === 'ANIME' ? 'Watching' : 'Reading', - 'COMPLETED': 'Completed', - 'PLANNING': 'Planning', - 'PAUSED': 'Paused', - 'DROPPED': 'Dropped', - 'REPEATING': entryType === 'ANIME' ? 'Rewatching' : 'Rereading' - }; - - const extraInfo = []; - if (repeatCount > 0) { - extraInfo.push(`🔁 ${repeatCount}`); - } - if (item.is_private) { - extraInfo.push('🔒 Private'); - } - - const entryDataString = JSON.stringify(item).replace(/'/g, '''); - - div.innerHTML = ` - - ${item.title || 'Entry'} - -
-
- -

${item.title || 'Unknown Title'}

-
-
- ${statusLabels[item.status] || item.status} - ${entryType} - ${item.source.toUpperCase()} - ${extraInfo.join('')} -
-
- -
-
-
-
-
- ${progress}${totalUnits > 0 ? ` / ${totalUnits}` : ''} ${unitLabel} ${score ? `⭐ ${score}` : ''} -
-
-
- - - `; - - const editBtn = div.querySelector('.edit-icon-btn'); - editBtn.addEventListener('click', (e) => { - try { - const entryData = JSON.parse(e.currentTarget.dataset.entry); - - window.ListModalManager.isInList = true; - window.ListModalManager.currentEntry = entryData; - window.ListModalManager.currentData = entryData; - - window.ListModalManager.open(entryData, entryData.source); - } catch (error) { - console.error('Error parsing entry data for modal:', error); - if (window.NotificationUtils) { - window.NotificationUtils.error('Could not open modal. Check HTML form IDs.'); - } - } - }); - - return div; -} \ No newline at end of file diff --git a/desktop/src/scripts/users.js b/desktop/src/scripts/users.js index ea09f67..fbeb7c1 100644 --- a/desktop/src/scripts/users.js +++ b/desktop/src/scripts/users.js @@ -1038,7 +1038,7 @@ async function performLogin(userId, password = null) { const data = await res.json(); localStorage.setItem('token', data.token); - window.location.href = '/anime'; + window.location.href = '/dashboard'; } catch (err) { console.error('Login error', err); showUserToast(err.message || 'Login failed', 'error'); diff --git a/desktop/src/views/views.routes.ts b/desktop/src/views/views.routes.ts index 529f5df..bb202c8 100644 --- a/desktop/src/views/views.routes.ts +++ b/desktop/src/views/views.routes.ts @@ -12,7 +12,7 @@ function getNavbarHTML(activePage: string, showSearch: boolean = true): string { let navbar = cachedNavbar; - const pages = ['anime', 'books', 'gallery', 'schedule', 'my-list', 'marketplace']; + const pages = ['dashboard', 'anime', 'books', 'gallery', 'schedule' , 'marketplace']; pages.forEach(page => { const regex = new RegExp(`( - diff --git a/desktop/views/css/dashboard.css b/desktop/views/css/dashboard.css new file mode 100644 index 0000000..19d193b --- /dev/null +++ b/desktop/views/css/dashboard.css @@ -0,0 +1,1031 @@ +/* ========================================= + 1. LAYOUT & ESTRUCTURA BASE + ========================================= */ +.main-wrapper { + width: 100%; + min-height: 100vh; + padding-bottom: 6rem; + position: relative; + overflow-x: hidden; +} + +/* Efecto de fondo ambiental global (Glow superior) */ +.main-wrapper::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 60vh; + background: radial-gradient(circle at 50% 0%, rgba(139, 92, 246, 0.15), transparent 70%); + z-index: -1; + pointer-events: none; +} + +.content-container { + max-width: 1600px; + margin: 0 auto; + padding: 0 3rem; + animation: fadeInUp 0.6s cubic-bezier(0.2, 0.8, 0.2, 1); +} + +@keyframes fadeInUp { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} + +/* ========================================= + 2. PROFILE HERO SECTION (Estilo Apple TV Profile) + ========================================= */ +.profile-hero { + position: relative; + padding: 8rem 3rem 2rem 3rem; /* Espacio extra arriba para titlebar/navbar */ + margin-bottom: 2rem; + display: flex; + flex-direction: column; + justify-content: flex-end; +} + +.profile-content { + display: flex; + align-items: flex-end; + gap: 2.5rem; + position: relative; + z-index: 2; +} + +.profile-avatar-container { + position: relative; + width: 160px; + height: 160px; + flex-shrink: 0; +} + +.profile-avatar { + width: 100%; + height: 100%; + border-radius: 50%; + object-fit: cover; + border: 4px solid rgba(255, 255, 255, 0.08); + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5); + transition: transform 0.3s ease; +} + +.profile-avatar:hover { + transform: scale(1.02); + border-color: var(--color-primary); +} + +.edit-avatar-btn { + position: absolute; + bottom: 5px; + right: 5px; + background: var(--color-bg-elevated); + border: 1px solid rgba(255,255,255,0.2); + color: white; + width: 40px; + height: 40px; + border-radius: 50%; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 4px 10px rgba(0,0,0,0.3); + transition: all 0.2s; +} + +.edit-avatar-btn:hover { + background: var(--color-primary); + transform: scale(1.1); +} + +.profile-info { + margin-bottom: 1.5rem; +} + +.profile-name { + font-size: 3.5rem; + font-weight: 800; + margin: 0 0 0.5rem 0; + line-height: 1; + letter-spacing: -0.02em; + background: linear-gradient(to bottom right, #fff, #a1a1aa); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.profile-stats-pills { + display: flex; + gap: 0.8rem; + flex-wrap: wrap; +} + +.stat-pill { + background: rgba(255, 255, 255, 0.06); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.1); + padding: 0.4rem 1rem; + border-radius: 99px; + font-size: 0.85rem; + font-weight: 600; + color: var(--color-text-secondary); + display: flex; + align-items: center; + gap: 0.5rem; + transition: 0.2s; +} + +.stat-pill:hover { + background: rgba(255, 255, 255, 0.1); + color: white; +} + +.stat-pill.highlight { + background: rgba(139, 92, 246, 0.15); + color: #a78bfa; + border-color: rgba(139, 92, 246, 0.3); +} + +/* ========================================= + 3. NAVEGACIÓN (Tabs) + ========================================= */ +.hub-navigation { + display: flex; + gap: 2.5rem; + margin-top: 3rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + padding-left: 3rem; +} + +.nav-tab { + background: none; + border: none; + color: var(--color-text-muted); + font-size: 1.1rem; + font-weight: 600; + padding: 1rem 0; + cursor: pointer; + position: relative; + display: flex; + align-items: center; + gap: 0.6rem; + transition: color 0.3s ease; +} + +.nav-tab svg { + opacity: 0.7; + transition: 0.3s; +} + +.nav-tab:hover { + color: white; +} + +.nav-tab:hover svg { + opacity: 1; +} + +.nav-tab.active { + color: white; +} + +.nav-tab.active::after { + content: ''; + position: absolute; + bottom: -1px; + left: 0; + width: 100%; + height: 3px; + background: var(--color-primary); + border-radius: 3px 3px 0 0; + box-shadow: 0 -4px 15px var(--color-primary-glow); +} + +.tab-section { + display: none; + opacity: 0; + transform: translateY(10px); + transition: opacity 0.3s, transform 0.3s; +} + +.tab-section.active { + display: block; + opacity: 1; + transform: translateY(0); +} + +/* ========================================= + 4. TOOLBAR & FILTROS + ========================================= */ +.toolbar { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2.5rem; + background: rgba(255,255,255,0.03); + border: 1px solid rgba(255,255,255,0.05); + padding: 0.75rem 1rem; + border-radius: 12px; + backdrop-filter: blur(5px); + flex-wrap: wrap; + gap: 1rem; +} + +.search-box { + position: relative; + flex-grow: 1; + max-width: 400px; +} + +.search-box svg { + position: absolute; + left: 14px; + top: 50%; + transform: translateY(-50%); + color: var(--color-text-muted); + pointer-events: none; +} + +.search-input { + width: 100%; + background: rgba(0,0,0,0.2); + border: 1px solid transparent; + padding: 0.7rem 1rem 0.7rem 2.8rem; + border-radius: 8px; + color: white; + font-family: inherit; + font-size: 0.95rem; + transition: all 0.2s; +} + +.search-input:focus { + background: rgba(0,0,0,0.4); + border-color: var(--color-primary); + box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.1); +} + +.filters-inline { + display: flex; + gap: 0.8rem; + align-items: center; + flex-wrap: wrap; +} + +.minimal-select { + background: rgba(255,255,255,0.05); + border: 1px solid rgba(255,255,255,0.08); + color: var(--color-text-secondary); + padding: 0.6rem 2rem 0.6rem 1rem; /* Padding derecho para la flecha */ + border-radius: 8px; + cursor: pointer; + font-family: inherit; + font-size: 0.9rem; + font-weight: 500; + transition: 0.2s; + appearance: none; + -webkit-appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23a1a1aa'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 0.7rem center; + background-size: 1rem; +} + +.minimal-select:hover { + background: rgba(255,255,255,0.1); + color: white; +} + +.minimal-select:focus { + border-color: var(--color-primary); + color: white; +} + +.view-toggle { + display: flex; + background: rgba(0,0,0,0.3); + border-radius: 8px; + padding: 2px; +} + +.view-btn { + background: transparent; + border: none; + color: var(--color-text-muted); + padding: 0.5rem 0.8rem; + border-radius: 6px; + cursor: pointer; + transition: 0.2s; + font-size: 1.1rem; + line-height: 1; +} + +.view-btn:hover { + color: white; +} + +.view-btn.active { + background: rgba(255,255,255,0.1); + color: white; +} + +/* ========================================= + 5. LIST GRID (Estilo Netflix Cards) + ========================================= */ +.list-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 2rem 1.5rem; +} + +.list-item { + position: relative; + border-radius: 8px; + transition: transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 0.3s; + cursor: pointer; + background: transparent; +} + +.list-item:hover { + transform: scale(1.05); + z-index: 10; +} + +/* Poster */ +.item-poster-link { + display: block; + position: relative; + border-radius: 8px; + overflow: hidden; + aspect-ratio: 2/3; + box-shadow: 0 4px 15px rgba(0,0,0,0.3); +} + +.item-poster { + width: 100%; + height: 100%; + object-fit: cover; + transition: filter 0.3s; +} + +/* Oscurecer ligeramente la imagen en hover para destacar texto si se desea, o brillo */ +.list-item:hover .item-poster { + filter: brightness(1.1); +} + +/* Contenido debajo del poster (Título, etc) */ +.item-content { + padding-top: 0.8rem; +} + +.item-title { + font-size: 0.95rem; + font-weight: 700; + margin: 0 0 0.4rem 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: rgba(255,255,255,0.9); +} + +.list-item:hover .item-title { + color: var(--color-primary); +} + +.item-meta { + display: flex; + align-items: center; + gap: 0.5rem; + flex-wrap: wrap; + margin-bottom: 0.5rem; +} + +.meta-pill { + font-size: 0.65rem; + padding: 0.15rem 0.4rem; + border-radius: 4px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.status-pill { background: rgba(34, 197, 94, 0.15); color: #4ade80; } +.type-pill { background: rgba(255, 255, 255, 0.1); color: #e4e4e7; } + +.progress-text { + font-size: 0.8rem; + color: var(--color-text-secondary); + display: flex; + justify-content: space-between; +} + +/* Botón Editar flotante (solo visible en hover) */ +.edit-icon-btn { + position: absolute; + top: 10px; + right: 10px; + background: rgba(0,0,0,0.7); + backdrop-filter: blur(4px); + border: 1px solid rgba(255,255,255,0.2); + color: white; + width: 32px; + height: 32px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transform: translateY(-5px); + transition: all 0.2s; + z-index: 20; +} + +.list-item:hover .edit-icon-btn { + opacity: 1; + transform: translateY(0); +} + +.edit-icon-btn:hover { + background: var(--color-primary); + border-color: var(--color-primary); +} + +/* --- LIST VIEW MODE --- */ +.list-grid.list-view { + grid-template-columns: 1fr; + gap: 1rem; +} + +.list-grid.list-view .list-item { + display: flex; + align-items: center; + background: var(--color-bg-elevated); + padding: 1rem; + border: 1px solid rgba(255,255,255,0.05); +} + +.list-grid.list-view .list-item:hover { + transform: scale(1.01); + border-color: var(--color-primary); + background: var(--color-bg-elevated-hover); +} + +.list-grid.list-view .item-poster-link { + width: 60px; + aspect-ratio: 2/3; + margin-right: 1.5rem; + flex-shrink: 0; +} + +.list-grid.list-view .item-content { + padding-top: 0; + flex-grow: 1; + display: flex; + align-items: center; + justify-content: space-between; +} + +.list-grid.list-view .item-title { + font-size: 1.1rem; + margin-bottom: 0.2rem; +} + +.list-grid.list-view .edit-icon-btn { + position: static; + opacity: 1; + transform: none; + background: transparent; + border: 1px solid rgba(255,255,255,0.1); +} + +/* ========================================= + 6. LOCAL LIBRARY (Dashboard Widgets) + ========================================= */ +.local-stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.local-card { + background: var(--color-bg-elevated); + border: 1px solid rgba(255,255,255,0.05); + padding: 1.5rem; + border-radius: 16px; + position: relative; + overflow: hidden; + transition: 0.3s; +} + +.local-card::after { + content: ''; + position: absolute; + top: 0; right: 0; width: 100px; height: 100px; + background: radial-gradient(circle at top right, rgba(139, 92, 246, 0.1), transparent 70%); +} + +.local-card:hover { + border-color: rgba(139, 92, 246, 0.3); + transform: translateY(-5px); +} + +.local-card h3 { + margin: 0; + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 1px; + color: var(--color-text-secondary); +} + +.local-card p { + font-size: 2.5rem; + font-weight: 800; + margin: 0.5rem 0 0 0; + color: white; +} + +.section-header-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; + padding-bottom: 1rem; + border-bottom: 1px solid rgba(255,255,255,0.05); +} + +.console-output { + background: #09090b; + border: 1px solid #27272a; + padding: 1.5rem; + border-radius: 12px; + font-family: 'JetBrains Mono', monospace; + font-size: 0.9rem; + color: #4ade80; + display: flex; + align-items: center; + gap: 1rem; + box-shadow: inset 0 2px 10px rgba(0,0,0,0.5); +} + +/* ========================================= + 7. SETTINGS (Clean Forms) + ========================================= */ +.settings-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); + gap: 2rem; +} + +.settings-card { + background: rgba(20, 20, 23, 0.6); + backdrop-filter: blur(10px); + border: 1px solid rgba(255,255,255,0.08); + padding: 2.5rem; + border-radius: 20px; +} + +.settings-card.full-width { + grid-column: 1 / -1; +} + +.settings-card h3 { + margin-top: 0; + font-size: 1.3rem; + border-bottom: 1px solid rgba(255,255,255,0.05); + padding-bottom: 1rem; + margin-bottom: 1.5rem; +} + +.form-group { + margin-bottom: 1.5rem; +} + +.form-group label { + display: block; + margin-bottom: 0.6rem; + font-size: 0.9rem; + color: var(--color-text-secondary); + font-weight: 500; +} + +.input-field { + width: 100%; + background: rgba(0,0,0,0.2); + border: 1px solid rgba(255,255,255,0.1); + padding: 0.9rem 1rem; + border-radius: 10px; + color: white; + font-size: 1rem; + transition: 0.2s; +} + +.input-field:focus { + background: rgba(0,0,0,0.4); + border-color: var(--color-primary); + box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.15); +} + +.integration-row { + display: flex; + justify-content: space-between; + align-items: center; + background: rgba(255,255,255,0.03); + padding: 1.5rem; + border-radius: 12px; + border: 1px solid rgba(255,255,255,0.05); +} + +/* Botones específicos de settings */ +.btn-danger-outline { + background: transparent; + border: 1px solid rgba(239, 68, 68, 0.5); + color: #ef4444; + padding: 0.8rem 1.5rem; + border-radius: 99px; + cursor: pointer; + font-weight: 600; + transition: 0.2s; +} + +.btn-danger-outline:hover { + background: rgba(239, 68, 68, 0.1); + border-color: #ef4444; +} + +/* ========================================= + 8. ESTADOS DE CARGA Y VACÍO + ========================================= */ +.loading-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 4rem 0; + color: var(--color-text-secondary); +} + +.spinner { + width: 40px; + height: 40px; + border: 3px solid rgba(139, 92, 246, 0.1); + border-top-color: var(--color-primary); + border-radius: 50%; + animation: spin 0.8s linear infinite; + margin-bottom: 1rem; +} + +.spinner.small { + width: 20px; + height: 20px; + border-width: 2px; + margin-bottom: 0; +} + +@keyframes spin { to { transform: rotate(360deg); } } + +.empty-state { + text-align: center; + padding: 5rem 2rem; + background: rgba(255,255,255,0.02); + border-radius: 20px; + border: 2px dashed rgba(255,255,255,0.05); +} + +.empty-state p { + font-size: 1.2rem; + color: var(--color-text-secondary); + margin-bottom: 1.5rem; +} + +/* ========================================= + 9. MOBILE RESPONSIVE + ========================================= */ +@media (max-width: 768px) { + .profile-hero { + padding: 6rem 1.5rem 1rem 1.5rem; + align-items: center; + text-align: center; + } + + .profile-content { + flex-direction: column; + align-items: center; + gap: 1.5rem; + } + + .profile-avatar-container { + width: 120px; + height: 120px; + } + + .profile-name { + font-size: 2.5rem; + } + + .hub-navigation { + justify-content: center; + padding-left: 0; + gap: 1.5rem; + font-size: 0.9rem; + } + + .toolbar { + flex-direction: column; + align-items: stretch; + } + + .search-box { + max-width: 100%; + } + + .filters-inline { + justify-content: space-between; + } + + .minimal-select { + flex: 1; + } + + .settings-grid { + grid-template-columns: 1fr; + } +} + +/* ========================================= + NUEVO DISEÑO DE PERFIL (Modern Header) + ========================================= */ +.profile-header { + background: var(--color-bg-elevated); + border-bottom: 1px solid var(--border-subtle); + padding-bottom: 0; + margin-bottom: 2rem; + position: relative; +} + +.profile-banner { + height: 200px; + background: linear-gradient(120deg, #2e1065, #8b5cf6); + width: 100%; + position: relative; +} + +.profile-body { + max-width: 1400px; + margin: 0 auto; + padding: 0 3rem; + display: flex; + align-items: flex-end; + gap: 2rem; + margin-top: -60px; /* Superposición sobre el banner */ + position: relative; + z-index: 2; +} + +.profile-avatar-wrapper { + position: relative; + width: 160px; + height: 160px; +} + +.avatar-img { + width: 100%; + height: 100%; + border-radius: 50%; + border: 5px solid var(--color-bg-base); + object-fit: cover; + background: var(--color-bg-elevated); +} + +.avatar-edit-overlay { + position: absolute; + inset: 0; + background: rgba(0,0,0,0.5); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: white; + opacity: 0; + cursor: pointer; + transition: 0.2s; + border: 5px solid transparent; +} + +.profile-avatar-wrapper:hover .avatar-edit-overlay { + opacity: 1; +} + +.profile-details { + flex-grow: 1; + display: flex; + justify-content: space-between; + align-items: flex-end; + padding-bottom: 1rem; + flex-wrap: wrap; + gap: 1rem; +} + +.profile-text h1 { + font-size: 2.5rem; + margin: 0; + line-height: 1.2; +} + +.user-badge { + background: rgba(139, 92, 246, 0.2); + color: #a78bfa; + border: 1px solid rgba(139, 92, 246, 0.3); + padding: 0.2rem 0.6rem; + border-radius: 6px; + font-size: 0.75rem; + font-weight: 700; + text-transform: uppercase; +} + +.profile-stats-grid { + display: flex; + gap: 1.5rem; +} + +.stat-card { + display: flex; + flex-direction: column; + align-items: center; + background: rgba(255,255,255,0.03); + padding: 0.5rem 1.5rem; + border-radius: 12px; + border: 1px solid rgba(255,255,255,0.05); +} + +.stat-value { + font-size: 1.4rem; + font-weight: 800; + color: white; +} + +.stat-label { + font-size: 0.8rem; + color: var(--color-text-secondary); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +/* NAVIGATION TABS (Modern Pills) */ +.hub-navigation-modern { + display: flex; + gap: 1rem; + max-width: 1400px; + margin: 1rem auto 0 auto; + padding: 0 3rem 1rem 3rem; +} + +.nav-pill { + background: transparent; + border: none; + color: var(--color-text-secondary); + padding: 0.6rem 1.2rem; + font-size: 0.95rem; + font-weight: 600; + border-radius: 8px; + cursor: pointer; + transition: 0.2s; +} + +.nav-pill:hover { + background: rgba(255,255,255,0.05); + color: white; +} + +.nav-pill.active { + background: var(--color-primary); + color: white; +} + +/* SETTINGS REDESIGN */ +.settings-layout { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2rem; + max-width: 1200px; + margin: 0 auto; +} + +.settings-card { + background: var(--color-bg-elevated); + border: 1px solid var(--border-medium); + padding: 2rem; + border-radius: 16px; + height: fit-content; +} + +.settings-stack { + display: flex; + flex-direction: column; + gap: 2rem; +} + +.input-modern { + width: 100%; + background: var(--color-bg-base); + border: 1px solid rgba(255,255,255,0.1); + padding: 0.8rem 1rem; + border-radius: 8px; + color: white; + font-size: 0.95rem; + transition: 0.2s; +} + +.input-modern:focus { + border-color: var(--color-primary); + box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.2); +} + +.full-width { width: 100%; justify-content: center; margin-top: 1rem;} + +.avatar-options { + display: flex; + align-items: center; + gap: 1rem; +} +.divider-text { color: var(--color-text-muted); font-size: 0.8rem; font-weight: bold; } + +/* Integration Item */ +.integration-item { + display: flex; + align-items: center; + gap: 1rem; + background: var(--color-bg-base); + padding: 1rem; + border-radius: 12px; +} + +.int-icon { + width: 40px; height: 40px; border-radius: 8px; overflow: hidden; +} +.int-icon img { width: 100%; height: 100%; } + +.int-details { flex-grow: 1; display: flex; flex-direction: column; } +.int-details strong { font-size: 0.95rem; } +.int-details span { font-size: 0.8rem; color: var(--color-text-secondary); } + +.btn-sm { + padding: 0.4rem 1rem; + border-radius: 6px; + background: var(--color-bg-elevated); + border: 1px solid rgba(255,255,255,0.2); + color: white; + cursor: pointer; +} + +.btn-danger { + background: rgba(239, 68, 68, 0.1); + color: #ef4444; + border: 1px solid rgba(239, 68, 68, 0.3); + width: 100%; + padding: 0.8rem; + border-radius: 8px; + font-weight: 700; + cursor: pointer; + transition: 0.2s; +} +.btn-danger:hover { background: #ef4444; color: white; } + +@media (max-width: 900px) { + .settings-layout { grid-template-columns: 1fr; } + .profile-body { flex-direction: column; align-items: center; margin-top: -100px; text-align: center; } + .profile-details { flex-direction: column; align-items: center; width: 100%; } + .profile-stats-grid { justify-content: center; width: 100%; } +} + +.username-wrapper { + display: flex; + align-items: center; + gap: 1rem; /* Espacio entre nombre e icono */ +} + +.header-anilist-badge { + width: 32px; + height: 32px; + border-radius: 8px; + overflow: hidden; + transition: transform 0.2s, box-shadow 0.2s; + display: flex; /* Para centrar la imagen */ +} + +.header-anilist-badge img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.header-anilist-badge:hover { + transform: scale(1.1); + box-shadow: 0 0 15px rgba(61, 180, 242, 0.5); /* Color azul AniList */ +} + +/* Ajuste Mobile */ +@media (max-width: 768px) { + .username-wrapper { + justify-content: center; + } +} \ No newline at end of file diff --git a/desktop/views/css/list.css b/desktop/views/css/list.css deleted file mode 100644 index 9ba9228..0000000 --- a/desktop/views/css/list.css +++ /dev/null @@ -1,485 +0,0 @@ -.container { - max-width: 1600px; - margin: 0 auto; - padding: 3rem; -} - -.header-section { - margin-bottom: 3rem; - margin-top: 3rem; -} - -.page-title { - font-size: 3rem; - font-weight: 900; - margin-bottom: 2rem; - background: linear-gradient(135deg, var(--color-primary), #a78bfa); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.stats-row { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 1.5rem; -} - -.stat-card { - background: var(--color-bg-elevated); - border: 1px solid rgba(255,255,255,0.1); - border-radius: var(--radius-lg); - padding: 1.5rem; - display: flex; - flex-direction: column; - gap: 0.5rem; - transition: transform 0.3s, box-shadow 0.3s; - box-shadow: 0 5px 20px rgba(0,0,0,0.2); -} - -.stat-card:hover { - transform: translateY(-5px); - box-shadow: 0 15px 35px var(--color-primary-glow); -} - -.stat-value { - font-size: 2.5rem; - font-weight: 900; - color: var(--color-primary); -} - -.stat-label { - font-size: 0.9rem; - color: var(--color-text-secondary); - font-weight: 600; -} - -/* --- Filtros mejorados --- */ -.filters-section { - display: flex; - gap: 1.5rem; - margin-bottom: 2rem; - padding: 1.5rem; - background: var(--color-bg-elevated); - border-radius: var(--radius-md); - border: 1px solid rgba(255,255,255,0.05); - flex-wrap: wrap; - box-shadow: 0 4px 15px rgba(0,0,0,0.3); -} - -.filter-group { - display: flex; - flex-direction: column; - gap: 0.5rem; - flex: 1; - min-width: 150px; -} - -.filter-group label { - font-size: 0.8rem; - font-weight: 700; - color: var(--color-text-secondary); - text-transform: uppercase; - letter-spacing: 1px; -} - -.filter-select { - background: var(--color-bg-base); - border: 1px solid rgba(255,255,255,0.1); - color: var(--color-text-primary); - padding: 0.7rem 1rem; - border-radius: 8px; - font-family: inherit; - cursor: pointer; - transition: 0.2s; - -webkit-appearance: none; - appearance: none; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='%23a1a1aa'%3E%3Cpath fill-rule='evenodd' d='M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z' clip-rule='evenodd'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 0.7rem center; - background-size: 1.2em; - padding-right: 2.5rem; -} - -.filter-select:hover { - border-color: var(--color-primary); -} - -.filter-select:focus { - outline: none; - border-color: var(--color-primary); - box-shadow: 0 0 10px var(--color-primary-glow); -} - -.view-toggle { - display: flex; - gap: 0.5rem; -} - -.view-btn { - background: var(--color-bg-base); - border: 1px solid rgba(255,255,255,0.1); - color: var(--color-text-secondary); - padding: 0.7rem; - border-radius: 8px; - cursor: pointer; - transition: 0.2s; - display: flex; - align-items: center; - justify-content: center; -} - -.view-btn:hover { - border-color: var(--color-primary); - color: white; -} - -.view-btn.active { - background: var(--color-primary); - border-color: var(--color-primary); - color: white; -} - -.loading-state { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 5rem 0; - gap: 1.5rem; -} - -.spinner { - width: 50px; - height: 50px; - border: 4px solid rgba(139, 92, 246, 0.1); - border-top-color: var(--color-primary); - border-radius: 50%; - animation: spin 1s linear infinite; -} - -@keyframes spin { - to { transform: rotate(360deg); } -} - -.empty-state { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 5rem 0; - gap: 1.5rem; - color: var(--color-text-secondary); -} - -.empty-state svg { - opacity: 0.3; -} - -.empty-state h2 { - font-size: 1.8rem; - color: var(--color-text-primary); -} - -.list-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); - gap: 2rem; -} - -.list-grid.list-view { - grid-template-columns: 1fr; - gap: 1rem; -} - -.list-item { - background: var(--color-bg-elevated-hover); - border: 1px solid rgba(255,255,255,0.08); - border-radius: var(--radius-md); - overflow: hidden; - transition: all 0.3s cubic-bezier(0.2, 0.8, 0.2, 1); - display: flex; - flex-direction: column; - position: relative; - box-shadow: 0 4px 15px rgba(0,0,0,0.3); -} - -.list-item:hover { - transform: translateY(-8px); - border-color: var(--color-primary); - box-shadow: 0 15px 30px var(--color-primary-glow); -} - -.list-grid.list-view .list-item { - flex-direction: row; - align-items: center; - padding-right: 1rem; - transition: all 0.3s ease; -} - -.list-grid.list-view .list-item:hover { - transform: none; - box-shadow: 0 4px 20px var(--color-primary-glow); -} - -.item-poster-link { - display: block; - cursor: pointer; - flex-shrink: 0; -} - -.item-poster { - width: 100%; - aspect-ratio: 2/3; - object-fit: cover; - background: #222; -} - -.list-grid.list-view .item-poster { - /* Cambiar el ancho y alto */ - width: 120px; /* Antes: 100px */ - height: 180px; /* Antes: 150px */ - aspect-ratio: auto; - border-radius: 8px; - margin: 1rem; -} - -.item-content { - padding: 1rem; /* Antes: 1.2rem */ - display: flex; - flex-direction: column; - flex-grow: 1; - justify-content: space-between; -} - -.list-grid.list-view .item-content { - padding: 1rem 0; - flex-direction: row; - align-items: center; -} -.list-grid.list-view .item-content > div:first-child { - flex-basis: 75%; -} - -.item-title { - font-size: 1rem; /* Antes: 1.1rem */ - font-weight: 800; - margin-bottom: 0.5rem; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - color: white; -} - -.list-grid.list-view .item-title { - font-size: 1.3rem; - white-space: normal; - overflow: hidden; - text-overflow: ellipsis; - max-width: 400px; -} - -.item-meta { - display: flex; - gap: 0.3rem; /* Antes: 0.75rem. Espacio entre los pills */ - margin-bottom: 0.5rem; /* Antes: 0.8rem */ - flex-wrap: wrap; - /* Añadir: Asegura que si se envuelven, lo hagan con poco margen vertical */ - line-height: 1.4; -} - -.meta-pill { - font-size: 0.65rem; /* Antes: 0.7rem */ - padding: 0.15rem 0.4rem; /* Antes: 0.25rem 0.6rem. Reduce el padding interno */ - border-radius: 999px; - font-weight: 700; - white-space: nowrap; - text-transform: uppercase; -} - -.status-pill { - background: rgba(34, 197, 94, 0.2); - color: var(--color-success); - border: 1px solid rgba(34, 197, 94, 0.3); -} - -.type-pill { - background: rgba(139, 92, 246, 0.15); - color: var(--color-primary); - border: 1px solid rgba(139, 92, 246, 0.3); -} - -.source-pill { - background: rgba(255, 255, 255, 0.1); - color: var(--color-text-primary); - border: 1px solid rgba(255, 255, 255, 0.2); -} - -.repeat-pill { - background: rgba(59, 130, 246, 0.15); - color: #3b82f6; - border: 1px solid rgba(59, 130, 246, 0.3); - text-transform: none; -} -.private-pill { - background: rgba(251, 191, 36, 0.15); - color: #facc15; - border: 1px solid rgba(251, 191, 36, 0.3); - text-transform: none; -} - -.progress-bar-container { - background: rgba(255,255,255,0.08); - border-radius: 999px; - height: 10px; - overflow: hidden; - margin-bottom: 0.5rem; - box-shadow: inset 0 1px 3px rgba(0,0,0,0.5); -} - -.progress-bar { - height: 100%; - background: linear-gradient(90deg, var(--color-primary), #a78bfa); - border-radius: 999px; - transition: width 0.3s; -} - -.progress-text { - font-size: 0.9rem; - color: var(--color-text-secondary); - display: flex; - justify-content: space-between; - align-items: center; - font-weight: 500; -} - -.score-badge { - display: inline-flex; - align-items: center; - gap: 0.3rem; - font-weight: 700; - color: #facc15; - background: rgba(250, 204, 21, 0.1); - padding: 0.1rem 0.5rem; - border-radius: 4px; -} - -/* --- Botón de edición flotante --- */ -.edit-icon-btn { - position: absolute; - top: 1rem; - right: 1rem; - z-index: 50; - background: rgba(18, 18, 21, 0.9); - backdrop-filter: blur(8px); - border: 1px solid rgba(255, 255, 255, 0.2); - color: white; - width: 40px; - height: 40px; - border-radius: 50%; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - opacity: 0; - transition: opacity 0.3s, transform 0.2s, background 0.2s; -} - -.list-item:hover .edit-icon-btn { - opacity: 1; - transform: scale(1.05); -} - -.edit-icon-btn:hover { - background: var(--color-primary); - border-color: var(--color-primary); -} - -.list-grid.list-view .edit-icon-btn { - position: relative; - top: auto; - right: auto; - margin-left: auto; - opacity: 1; - transform: none; - background: var(--color-bg-elevated); - border: 1px solid rgba(255, 255, 255, 0.1); -} -.list-grid.list-view .list-item:hover .edit-icon-btn { - opacity: 1; - background: var(--color-primary); - border-color: var(--color-primary); - transform: none; -} - -/* --- Modal de Edición Mejorado (Estilo Anilist + AMOLED) --- */ - -@media (max-width: 550px) { - /* Layout de lista (card view) */ - .list-grid { - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); - } - .list-grid.list-view .list-item { - flex-direction: column; - align-items: flex-start; - padding-right: 0; - } - .list-grid.list-view .item-poster { - width: 100%; - height: auto; - margin: 0; - border-radius: 0; - aspect-ratio: 16/9; - } - .list-grid.list-view .item-content { - flex-direction: column; - padding: 1rem; - } - .list-grid.list-view .item-content > div:first-child { - flex-basis: auto; - } - .list-grid.list-view .edit-icon-btn { - position: absolute; - top: 1rem; - right: 1rem; - opacity: 1; - background: rgba(18, 18, 21, 0.8); - } - - /* Modal en móvil */ - .modal-content { - margin: 0.5rem; - width: auto; - } - .modal-fields-grid { - grid-template-columns: 1fr; - gap: 1rem; - padding-bottom: 0; - } - .form-group.notes-group, - .form-group.checkbox-group { - grid-column: auto; - } - .modal-actions { - flex-direction: column; - align-items: stretch; - } - .btn-danger { - margin-right: 0; - order: 3; - } - .btn-secondary { - order: 2; - } - .btn-primary { - order: 1; - } -} - -.edit-btn-card { - display: none; -} - -.item-poster-link { - z-index: 1; -} \ No newline at end of file diff --git a/desktop/views/dashboard.html b/desktop/views/dashboard.html new file mode 100644 index 0000000..5c91425 --- /dev/null +++ b/desktop/views/dashboard.html @@ -0,0 +1,195 @@ + + + + + + + Dashboard - WaifuBoard + + + + + + + + + +
+
+ + WaifuBoard +
+
+ + + +
+
+ +
+
+
+
+ Profile + + +
+ +
+
+
+

Loading...

+ +
+
+ +
+
+ 0 + Total Entries +
+
+ - + Anime +
+
+ - + Manga +
+
+
+
+ +
+ + + +
+
+ +
+ +
+
+ +
+ + + +
+ + +
+
+
+
+ +
+
+ +
+
+

Local Library

+
+ + +
+
+
+

Anime Files

0

+

Manga Files

0

+

Novel Files

0

+
+ +
+ +
+
+
+

Edit Profile

+
+
+ + +
+ +
+ +
+ + OR + +
+ + * If you upload a file, it will be converted automatically. The text box above is only for external URLs. + +
+ +
+
+ +
+
+

Security

+
+
+ + +
+
+ + +
+ +
+
+ +
+

Integrations

+
+
AL
+
+ AniList + Checking... +
+ +
+
+ +
+

Danger Zone

+ +
+
+
+
+ +
+
+ + + + + + + + + + + + \ No newline at end of file diff --git a/desktop/views/list.html b/desktop/views/list.html deleted file mode 100644 index dc660e8..0000000 --- a/desktop/views/list.html +++ /dev/null @@ -1,145 +0,0 @@ - - - - - - - My Lists - WaifuBoard - - - - - - - - - -
- - WaifuBoard -
-
- - - -
-
- -
-
-

My List

-
-
- 0 - Total Entries -
-
- 0 - Watching -
-
- 0 - Completed -
-
- 0 - Planning -
-
-
- -
-
- - -
- -
- - -
- -
- - -
-
- - -
- -
- -
- - -
-
-
- -
-
-

Loading your list...

-
- - - -
-
- - - - - - - - - - - - \ No newline at end of file diff --git a/docker/src/scripts/dashboard.js b/docker/src/scripts/dashboard.js new file mode 100644 index 0000000..1c7b341 --- /dev/null +++ b/docker/src/scripts/dashboard.js @@ -0,0 +1,468 @@ +const API_BASE = '/api'; +let currentList = []; +let filteredList = []; +let currentUserId = null; + +// Configuración de paginación +const ITEMS_PER_PAGE = 50; +let visibleCount = ITEMS_PER_PAGE; + +// Inicialización +document.addEventListener('DOMContentLoaded', async () => { + await initUser(); + await loadList(); + setupEventListeners(); + setupTabSystem(); +}); + +async function initUser() { + try { + const headers = window.AuthUtils.getSimpleAuthHeaders(); + const res = await fetch(`${API_BASE}/me`, { headers }); + + if (res.ok) { + const data = await res.json(); + document.getElementById('user-username').textContent = data.username; + document.getElementById('setting-username').value = data.username; + + if (data.avatar) { + document.getElementById('user-avatar').src = data.avatar; + if (data.avatar.startsWith('http')) { + document.getElementById('setting-avatar-url').value = data.avatar; + } else { + document.getElementById('setting-avatar-url').placeholder = "Image uploaded via file (Base64)"; + document.getElementById('setting-avatar-url').value = ""; + } + } + } + + const token = localStorage.getItem('token'); + if (token) { + const payload = JSON.parse(atob(token.split('.')[1])); + currentUserId = payload.id; + await checkIntegrations(currentUserId); + } + + } catch (err) { + console.error("Error loading user profile:", err); + } +} + +async function checkIntegrations(userId) { + if (!userId) return; + try { + const res = await fetch(`${API_BASE}/users/${userId}/integration`); + let data = { connected: false }; + if (res.ok) data = await res.json(); + + const statusEl = document.getElementById('anilist-status'); + const btn = document.getElementById('anilist-action-btn'); + const headerBadge = document.getElementById('header-anilist-link'); + + if (data.connected) { + if (headerBadge) { + headerBadge.style.display = 'flex'; + headerBadge.href = `https://anilist.co/user/${data.anilistUserId}`; + headerBadge.title = `Connected as ${data.anilistUserId}`; + } + if (statusEl) { + statusEl.textContent = `Connected as ID: ${data.anilistUserId}`; + statusEl.style.color = 'var(--color-success)'; + } + if (btn) { + btn.textContent = 'Disconnect'; + btn.classList.add('btn-danger-outline'); + btn.classList.remove('btn-blur'); + btn.onclick = () => disconnectAniList(userId); + } + } else { + if (headerBadge) headerBadge.style.display = 'none'; + if (statusEl) { + statusEl.textContent = 'Not connected'; + statusEl.style.color = 'var(--color-text-secondary)'; + } + if (btn) { + btn.textContent = 'Connect'; + btn.classList.remove('btn-danger-outline'); + btn.classList.add('btn-blur'); + btn.onclick = () => redirectToAniListLogin(); + } + } + } catch (e) { console.error("Integration check error:", e); } +} + +async function redirectToAniListLogin() { + if (!currentUserId) return; + try { + const clientId = 32898; + const redirectUri = encodeURIComponent(window.location.origin + '/api/anilist'); + const state = encodeURIComponent(currentUserId); + + window.location.href = `https://anilist.co/api/v2/oauth/authorize?client_id=${clientId}&response_type=code&redirect_uri=${redirectUri}&state=${state}`; + } catch (err) { console.error(err); alert('Error starting AniList login'); } +} + +async function disconnectAniList(userId) { + if(!confirm("Disconnect AniList?")) return; + try { + const token = localStorage.getItem('token'); + await fetch(`${API_BASE}/users/${userId}/integration`, { + method: 'DELETE', + headers: { 'Authorization': `Bearer ${token}` } + }); + checkIntegrations(userId); + } catch (e) { alert("Failed to disconnect"); } +} + +function setupTabSystem() { + const tabs = document.querySelectorAll('.nav-pill'); + const sections = document.querySelectorAll('.tab-section'); + + tabs.forEach(tab => { + tab.addEventListener('click', () => { + tabs.forEach(t => t.classList.remove('active')); + tab.classList.add('active'); + + const targetId = `section-${tab.dataset.target}`; + sections.forEach(sec => { + sec.classList.remove('active'); + if (sec.id === targetId) sec.classList.add('active'); + }); + + if (tab.dataset.target === 'local') loadLocalStats(); + }); + }); +} + +async function loadLocalStats() { + const types = ['anime', 'manga', 'novels']; + const elements = { 'anime': 'local-anime-count', 'manga': 'local-manga-count', 'novels': 'local-novel-count' }; + + for (const type of types) { + try { + const res = await fetch(`${API_BASE}/library/${type}`, { headers: window.AuthUtils.getSimpleAuthHeaders() }); + if(res.ok) { + const data = await res.json(); + const elId = elements[type]; + if (document.getElementById(elId)) document.getElementById(elId).textContent = `${data.length} items`; + } + } catch (e) { console.error(e); } + } +} + +async function triggerScan(mode) { + const consoleDiv = document.getElementById('scan-console'); + const statusText = document.getElementById('scan-status-text'); + consoleDiv.style.display = 'flex'; + statusText.textContent = `Starting ${mode} scan...`; + + try { + const res = await fetch(`${API_BASE}/library/scan?mode=${mode}`, { + method: 'POST', + headers: window.AuthUtils.getSimpleAuthHeaders() + }); + if (res.ok) { + statusText.textContent = "Scan completed successfully!"; + setTimeout(() => { consoleDiv.style.display = 'none'; loadLocalStats(); }, 3000); + } else throw new Error('Scan failed'); + } catch (e) { + statusText.textContent = "Error during scan."; + statusText.style.color = 'var(--color-danger)'; + } +} + +async function handleProfileUpdate(e) { + e.preventDefault(); + if (!currentUserId) return; + + const username = document.getElementById('setting-username').value; + const urlInput = document.getElementById('setting-avatar-url').value; + const fileInput = document.getElementById('avatar-upload'); + let finalAvatar = null; + + if (fileInput.files && fileInput.files[0]) { + const toBase64 = file => new Promise((res, rej) => { + const r = new FileReader(); r.readAsDataURL(file); + r.onload = () => res(r.result); r.onerror = rej; + }); + try { finalAvatar = await toBase64(fileInput.files[0]); } catch (err) { alert("Error reading file"); return; } + } else if (urlInput.trim() !== "") { + finalAvatar = urlInput.trim(); + } + + const bodyData = { username }; + if (finalAvatar) bodyData.profilePictureUrl = finalAvatar; + + try { + const res = await fetch(`${API_BASE}/users/${currentUserId}`, { + method: 'PUT', + headers: { ...window.AuthUtils.getSimpleAuthHeaders(), 'Content-Type': 'application/json' }, + body: JSON.stringify(bodyData) + }); + if (res.ok) alert('Profile updated successfully!'); + else { const err = await res.json(); alert(err.error || 'Update failed'); } + } catch (e) { console.error(e); } +} + +async function handlePasswordChange(e) { + e.preventDefault(); + if (!currentUserId) return; + const currentPassword = document.getElementById('current-password').value; + const newPassword = document.getElementById('new-password').value; + + try { + const res = await fetch(`${API_BASE}/users/${currentUserId}/password`, { + method: 'PUT', + headers: { ...window.AuthUtils.getSimpleAuthHeaders(), 'Content-Type': 'application/json' }, + body: JSON.stringify({ currentPassword, newPassword }) + }); + const data = await res.json(); + if (res.ok) { alert("Password updated successfully"); document.getElementById('password-form').reset(); } + else alert(data.error || "Failed to update password"); + } catch (e) { console.error(e); } +} + +function setupEventListeners() { + document.getElementById('scan-incremental-btn')?.addEventListener('click', () => triggerScan('incremental')); + document.getElementById('scan-full-btn')?.addEventListener('click', () => triggerScan('full')); + document.getElementById('profile-form')?.addEventListener('submit', handleProfileUpdate); + document.getElementById('password-form')?.addEventListener('submit', handlePasswordChange); + document.getElementById('logout-btn')?.addEventListener('click', () => window.AuthUtils.logout()); + + const fileInput = document.getElementById('avatar-upload'); + if (fileInput) { + fileInput.addEventListener('change', function(e) { + const file = e.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = evt => { + document.getElementById('user-avatar').src = evt.target.result; + document.getElementById('setting-avatar-url').value = ''; + }; + reader.readAsDataURL(file); + } + }); + } + + document.querySelector('.search-input').addEventListener('input', () => applyFilters()); + ['status-filter', 'type-filter', 'sort-filter'].forEach(id => { + document.getElementById(id).addEventListener('change', () => applyFilters()); + }); + + document.querySelectorAll('.view-btn').forEach(btn => { + btn.addEventListener('click', () => { + document.querySelectorAll('.view-btn').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + const view = btn.dataset.view; + const container = document.getElementById('list-container'); + if (view === 'list') container.classList.add('list-view'); + else container.classList.remove('list-view'); + }); + }); +} + +// --- LOGICA DE LISTA --- + +async function loadList() { + const loadingState = document.getElementById('loading-state'); + const emptyState = document.getElementById('empty-state'); + const container = document.getElementById('list-container'); + + try { + loadingState.style.display = 'flex'; + emptyState.style.display = 'none'; + container.innerHTML = ''; + + const response = await fetch(`${API_BASE}/list`, { headers: window.AuthUtils.getSimpleAuthHeaders() }); + if (!response.ok) throw new Error('Failed'); + + const data = await response.json(); + currentList = data.results || []; + + const animeCount = currentList.filter(item => item.entry_type === 'ANIME').length; + const mangaCount = currentList.filter(item => item.entry_type === 'MANGA').length; + + document.getElementById('total-stat').textContent = currentList.length; + if (document.getElementById('anime-stat')) document.getElementById('anime-stat').textContent = animeCount; + if (document.getElementById('manga-stat')) document.getElementById('manga-stat').textContent = mangaCount; + + loadingState.style.display = 'none'; + if (currentList.length === 0) emptyState.style.display = 'flex'; + else applyFilters(); + + } catch (error) { + console.error(error); + loadingState.style.display = 'none'; + } +} + +function applyFilters() { + const statusFilter = document.getElementById('status-filter').value; + const typeFilter = document.getElementById('type-filter').value; + const sortFilter = document.getElementById('sort-filter').value; + const searchQuery = document.querySelector('.search-input').value.toLowerCase().trim(); + + let result = [...currentList]; + + if (searchQuery) result = result.filter(item => (item.title || '').toLowerCase().includes(searchQuery)); + if (statusFilter !== 'all') result = result.filter(item => item.status === statusFilter); + if (typeFilter !== 'all') result = result.filter(item => item.entry_type === typeFilter); + + if (sortFilter === 'title') result.sort((a, b) => (a.title || '').localeCompare(b.title || '')); + else if (sortFilter === 'score') result.sort((a, b) => (b.score || 0) - (a.score || 0)); + else result.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at)); + + filteredList = result; + visibleCount = ITEMS_PER_PAGE; + renderList(); +} + +function renderList() { + const container = document.getElementById('list-container'); + container.innerHTML = ''; + + if (filteredList.length === 0) { + container.innerHTML = '
No matches found
'; + return; + } + + const itemsToShow = filteredList.slice(0, visibleCount); + itemsToShow.forEach(item => container.appendChild(createListItem(item))); + + if (visibleCount < filteredList.length) { + const remaining = filteredList.length - visibleCount; + const btnContainer = document.createElement('div'); + btnContainer.style.gridColumn = "1 / -1"; + btnContainer.style.display = "flex"; + btnContainer.style.justifyContent = "center"; + btnContainer.style.padding = "2rem 0"; + + const loadMoreBtn = document.createElement('button'); + loadMoreBtn.className = "btn-blur"; + loadMoreBtn.textContent = `Show All (${remaining} more)`; + loadMoreBtn.onclick = () => { visibleCount = filteredList.length; renderList(); }; + btnContainer.appendChild(loadMoreBtn); + container.appendChild(btnContainer); + } +} + +function createListItem(item) { + const div = document.createElement('div'); + div.className = 'list-item'; + + const itemLink = getEntryLink(item); + const posterUrl = item.poster || '/public/assets/placeholder.svg'; + const progress = item.progress || 0; + const totalUnits = item.entry_type === 'ANIME' ? item.total_episodes || 0 : item.total_chapters || 0; + const progressPercent = totalUnits > 0 ? (progress / totalUnits) * 100 : 0; + const score = item.score ? item.score.toFixed(1) : null; + const repeatCount = item.repeat_count || 0; + const entryType = (item.entry_type).toUpperCase(); + const unitLabel = entryType === 'ANIME' ? 'episodes' : 'chapters'; + + const statusLabels = { + 'CURRENT': entryType === 'ANIME' ? 'Watching' : 'Reading', + 'COMPLETED': 'Completed', + 'PLANNING': 'Planning', + 'PAUSED': 'Paused', + 'DROPPED': 'Dropped', + 'REPEATING': entryType === 'ANIME' ? 'Rewatching' : 'Rereading' + }; + + const extraInfo = []; + if (repeatCount > 0) extraInfo.push(`🔁 ${repeatCount}`); + if (item.is_private) extraInfo.push('🔒 Private'); + + div.innerHTML = ` + + ${item.title || 'Entry'} + +
+
+ +

${item.title || 'Unknown Title'}

+
+
+ ${statusLabels[item.status] || item.status} + ${entryType} + ${item.source.toUpperCase()} + ${extraInfo.join('')} +
+
+
+
+
+
+
+ ${progress}${totalUnits > 0 ? ` / ${totalUnits}` : ''} ${unitLabel} ${score ? `⭐ ${score}` : ''} +
+
+
+ + `; + + // Lógica para abrir el Modal (Estilo book.js: seteamos currentData) + const editBtn = div.querySelector('.edit-icon-btn'); + editBtn.onclick = (e) => { + e.preventDefault(); + e.stopPropagation(); + + // 1. Configuramos el modal manager con los datos de ESTE item + window.ListModalManager.currentData = item; + window.ListModalManager.isInList = true; + window.ListModalManager.currentEntry = item; // Ya tenemos los datos de la lista + + // 2. Abrimos el modal + window.ListModalManager.open(item, item.source || 'anilist'); + }; + + return div; +} + +function getEntryLink(item) { + const isAnime = item.entry_type?.toUpperCase() === 'ANIME'; + const baseRoute = isAnime ? '/anime' : '/book'; + return `${baseRoute}/${item.entry_id}`; +} + +// ========================================================= +// EXPORTS GLOBALES (Estilo book.js) +// Estas funciones son llamadas por los onclick del HTML del Modal +// ========================================================= + +window.saveToList = async () => { + // Recuperamos los datos que seteamos en el onclick del botón editar + const data = window.ListModalManager.currentData; + if (!data) return; + + // En la vista de lista, el ID suele ser 'entry_id', pero usamos un fallback + const idToSave = data.entry_id || data.id; + const source = data.source || 'anilist'; + + // Llamamos al manager pasando ID y Source explícitamente como en book.js + await window.ListModalManager.save(idToSave, source); + + // IMPORTANTE: Recargar la lista para ver los cambios + await loadList(); +}; + +window.deleteFromList = async () => { + const data = window.ListModalManager.currentData; + if (!data) return; + + const idToDelete = data.entry_id || data.id; + const source = data.source || 'anilist'; + + await window.ListModalManager.delete(idToDelete, source); + + // IMPORTANTE: Recargar la lista + await loadList(); +}; + +window.closeAddToListModal = () => { + window.ListModalManager.close(); +}; \ No newline at end of file diff --git a/docker/src/scripts/list.js b/docker/src/scripts/list.js deleted file mode 100644 index 6691863..0000000 --- a/docker/src/scripts/list.js +++ /dev/null @@ -1,380 +0,0 @@ -const API_BASE = '/api'; -let currentList = []; -let filteredList = []; - -document.addEventListener('DOMContentLoaded', async () => { - await loadList(); - setupEventListeners(); -}); - -function getEntryLink(item) { - const isAnime = item.entry_type?.toUpperCase() === 'ANIME'; - const baseRoute = isAnime ? '/anime' : '/book'; - const source = item.source || 'anilist'; - - if (source === 'anilist') { - return `${baseRoute}/${item.entry_id}`; - } else { - return `${baseRoute}/${source}/${item.entry_id}`; - } -} - -async function populateSourceFilter() { - const select = document.getElementById('source-filter'); - if (!select) return; - - select.innerHTML = ` - - - `; - - try { - const [animeRes, bookRes] = await Promise.all([ - fetch(`${API_BASE}/extensions/anime`), - fetch(`${API_BASE}/extensions/book`) - ]); - - const extensions = new Set(); - - if (animeRes.ok) { - const data = await animeRes.json(); - (data.extensions || []).forEach(ext => extensions.add(ext)); - } - - if (bookRes.ok) { - const data = await bookRes.json(); - (data.extensions || []).forEach(ext => extensions.add(ext)); - } - - extensions.forEach(extName => { - const lower = extName.toLowerCase(); - if (lower !== 'anilist' && lower !== 'local') { - const option = document.createElement('option'); - option.value = extName; - option.textContent = extName.charAt(0).toUpperCase() + extName.slice(1); - select.appendChild(option); - } - }); - } catch (error) { - console.error('Error loading extensions:', error); - } -} - -function updateLocalList(entryData, action) { - const entryId = entryData.entry_id; - const source = entryData.source; - - const findIndex = (list) => list.findIndex(e => - e.entry_id === entryId && e.source === source - ); - - const currentIndex = findIndex(currentList); - if (currentIndex !== -1) { - if (action === 'update') { - - currentList[currentIndex] = { ...currentList[currentIndex], ...entryData }; - } else if (action === 'delete') { - currentList.splice(currentIndex, 1); - } - } else if (action === 'update') { - - currentList.push(entryData); - } - - filteredList = [...currentList]; - - updateStats(); - applyFilters(); - window.ListModalManager.close(); -} - -function setupEventListeners() { - - document.querySelectorAll('.view-btn').forEach(btn => { - btn.addEventListener('click', () => { - document.querySelectorAll('.view-btn').forEach(b => b.classList.remove('active')); - btn.classList.add('active'); - const view = btn.dataset.view; - const container = document.getElementById('list-container'); - if (view === 'list') { - container.classList.add('list-view'); - } else { - container.classList.remove('list-view'); - } - }); - }); - - document.getElementById('status-filter').addEventListener('change', applyFilters); - document.getElementById('source-filter').addEventListener('change', applyFilters); - document.getElementById('type-filter').addEventListener('change', applyFilters); - document.getElementById('sort-filter').addEventListener('change', applyFilters); - - document.querySelector('.search-input').addEventListener('input', (e) => { - const query = e.target.value.toLowerCase(); - if (query) { - filteredList = currentList.filter(item => - item.title?.toLowerCase().includes(query) - ); - } else { - filteredList = [...currentList]; - } - applyFilters(); - }); - - document.getElementById('modal-save-btn')?.addEventListener('click', async () => { - - const entryToSave = window.ListModalManager.currentEntry || window.ListModalManager.currentData; - - if (!entryToSave) return; - - const success = await window.ListModalManager.save(entryToSave.entry_id, entryToSave.source); - - if (success) { - - const updatedEntry = window.ListModalManager.currentEntry; - updatedEntry.updated_at = new Date().toISOString(); - - updateLocalList(updatedEntry, 'update'); - } - - }); - - document.getElementById('modal-delete-btn')?.addEventListener('click', async () => { - const entryToDelete = window.ListModalManager.currentEntry || window.ListModalManager.currentData; - - if (!entryToDelete) return; - - const success = await window.ListModalManager.delete(entryToDelete.entry_id, entryToDelete.source); - - if (success) { - updateLocalList(entryToDelete, 'delete'); - } - - }); - - document.getElementById('add-list-modal')?.addEventListener('click', (e) => { - if (e.target.id === 'add-list-modal') { - window.ListModalManager.close(); - } - }); -} - -async function loadList() { - const loadingState = document.getElementById('loading-state'); - const emptyState = document.getElementById('empty-state'); - const container = document.getElementById('list-container'); - - await populateSourceFilter(); - - try { - loadingState.style.display = 'flex'; - emptyState.style.display = 'none'; - container.innerHTML = ''; - - const response = await fetch(`${API_BASE}/list`, { - headers: window.AuthUtils.getSimpleAuthHeaders() - }); - - if (!response.ok) { - throw new Error('Failed to load list'); - } - - const data = await response.json(); - currentList = data.results || []; - filteredList = [...currentList]; - - loadingState.style.display = 'none'; - - if (currentList.length === 0) { - emptyState.style.display = 'flex'; - } else { - updateStats(); - applyFilters(); - } - } catch (error) { - console.error('Error loading list:', error); - loadingState.style.display = 'none'; - if (window.NotificationUtils) { - window.NotificationUtils.error('Failed to load your list. Please try again.'); - } else { - alert('Failed to load your list. Please try again.'); - } - } -} - -function updateStats() { - - const total = currentList.length; - const watching = currentList.filter(item => item.status === 'WATCHING').length; - const completed = currentList.filter(item => item.status === 'COMPLETED').length; - const planning = currentList.filter(item => item.status === 'PLANNING').length; - - document.getElementById('total-count').textContent = total; - document.getElementById('watching-count').textContent = watching; - document.getElementById('completed-count').textContent = completed; - document.getElementById('planned-count').textContent = planning; -} - -function applyFilters() { - const statusFilter = document.getElementById('status-filter').value; - const sourceFilter = document.getElementById('source-filter').value; - const typeFilter = document.getElementById('type-filter').value; - const sortFilter = document.getElementById('sort-filter').value; - - let filtered = [...filteredList]; - - if (statusFilter !== 'all') { - filtered = filtered.filter(item => item.status === statusFilter); - } - - if (sourceFilter !== 'all') { - filtered = filtered.filter(item => item.source === sourceFilter); - } - - if (typeFilter !== 'all') { - filtered = filtered.filter(item => item.entry_type === typeFilter); - } - - switch (sortFilter) { - case 'title': - filtered.sort((a, b) => (a.title || '').localeCompare(b.title || '')); - break; - case 'score': - filtered.sort((a, b) => (b.score || 0) - (a.score || 0)); - break; - case 'progress': - filtered.sort((a, b) => (b.progress || 0) - (a.progress || 0)); - break; - case 'updated': - default: - - filtered.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at)); - break; - } - - renderList(filtered); -} - -function renderList(items) { - const container = document.getElementById('list-container'); - container.innerHTML = ''; - - if (items.length === 0) { - - if (currentList.length === 0) { - document.getElementById('empty-state').style.display = 'flex'; - } else { - - container.innerHTML = '

No entries match your filters

'; - } - return; - } - - document.getElementById('empty-state').style.display = 'none'; - - items.forEach(item => { - const element = createListItem(item); - container.appendChild(element); - }); -} - -function createListItem(item) { - const div = document.createElement('div'); - div.className = 'list-item'; - - const itemLink = getEntryLink(item); - - const posterUrl = item.poster || '/public/assets/placeholder.png'; - const progress = item.progress || 0; - - const totalUnits = item.entry_type === 'ANIME' ? - item.total_episodes || 0 : - item.total_chapters || 0; - - const progressPercent = totalUnits > 0 ? (progress / totalUnits) * 100 : 0; - const score = item.score ? item.score.toFixed(1) : null; - const repeatCount = item.repeat_count || 0; - - const entryType = (item.entry_type).toUpperCase(); - let unitLabel = 'units'; - if (entryType === 'ANIME') { - unitLabel = 'episodes'; - } else if (entryType === 'MANGA') { - unitLabel = 'chapters'; - } else if (entryType === 'NOVEL') { - unitLabel = 'chapters/volumes'; - } - - const statusLabels = { - 'CURRENT': entryType === 'ANIME' ? 'Watching' : 'Reading', - 'COMPLETED': 'Completed', - 'PLANNING': 'Planning', - 'PAUSED': 'Paused', - 'DROPPED': 'Dropped', - 'REPEATING': entryType === 'ANIME' ? 'Rewatching' : 'Rereading' - }; - - const extraInfo = []; - if (repeatCount > 0) { - extraInfo.push(`🔁 ${repeatCount}`); - } - if (item.is_private) { - extraInfo.push('🔒 Private'); - } - - const entryDataString = JSON.stringify(item).replace(/'/g, '''); - - div.innerHTML = ` - - ${item.title || 'Entry'} - -
-
- -

${item.title || 'Unknown Title'}

-
-
- ${statusLabels[item.status] || item.status} - ${entryType} - ${item.source.toUpperCase()} - ${extraInfo.join('')} -
-
- -
-
-
-
-
- ${progress}${totalUnits > 0 ? ` / ${totalUnits}` : ''} ${unitLabel} ${score ? `⭐ ${score}` : ''} -
-
-
- - - `; - - const editBtn = div.querySelector('.edit-icon-btn'); - editBtn.addEventListener('click', (e) => { - try { - const entryData = JSON.parse(e.currentTarget.dataset.entry); - - window.ListModalManager.isInList = true; - window.ListModalManager.currentEntry = entryData; - window.ListModalManager.currentData = entryData; - - window.ListModalManager.open(entryData, entryData.source); - } catch (error) { - console.error('Error parsing entry data for modal:', error); - if (window.NotificationUtils) { - window.NotificationUtils.error('Could not open modal. Check HTML form IDs.'); - } - } - }); - - return div; -} \ No newline at end of file diff --git a/docker/src/scripts/users.js b/docker/src/scripts/users.js index ea09f67..fbeb7c1 100644 --- a/docker/src/scripts/users.js +++ b/docker/src/scripts/users.js @@ -1038,7 +1038,7 @@ async function performLogin(userId, password = null) { const data = await res.json(); localStorage.setItem('token', data.token); - window.location.href = '/anime'; + window.location.href = '/dashboard'; } catch (err) { console.error('Login error', err); showUserToast(err.message || 'Login failed', 'error'); diff --git a/docker/src/views/views.routes.ts b/docker/src/views/views.routes.ts index 529f5df..bb202c8 100644 --- a/docker/src/views/views.routes.ts +++ b/docker/src/views/views.routes.ts @@ -12,7 +12,7 @@ function getNavbarHTML(activePage: string, showSearch: boolean = true): string { let navbar = cachedNavbar; - const pages = ['anime', 'books', 'gallery', 'schedule', 'my-list', 'marketplace']; + const pages = ['dashboard', 'anime', 'books', 'gallery', 'schedule' , 'marketplace']; pages.forEach(page => { const regex = new RegExp(`( - diff --git a/docker/views/css/dashboard.css b/docker/views/css/dashboard.css new file mode 100644 index 0000000..19d193b --- /dev/null +++ b/docker/views/css/dashboard.css @@ -0,0 +1,1031 @@ +/* ========================================= + 1. LAYOUT & ESTRUCTURA BASE + ========================================= */ +.main-wrapper { + width: 100%; + min-height: 100vh; + padding-bottom: 6rem; + position: relative; + overflow-x: hidden; +} + +/* Efecto de fondo ambiental global (Glow superior) */ +.main-wrapper::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 60vh; + background: radial-gradient(circle at 50% 0%, rgba(139, 92, 246, 0.15), transparent 70%); + z-index: -1; + pointer-events: none; +} + +.content-container { + max-width: 1600px; + margin: 0 auto; + padding: 0 3rem; + animation: fadeInUp 0.6s cubic-bezier(0.2, 0.8, 0.2, 1); +} + +@keyframes fadeInUp { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} + +/* ========================================= + 2. PROFILE HERO SECTION (Estilo Apple TV Profile) + ========================================= */ +.profile-hero { + position: relative; + padding: 8rem 3rem 2rem 3rem; /* Espacio extra arriba para titlebar/navbar */ + margin-bottom: 2rem; + display: flex; + flex-direction: column; + justify-content: flex-end; +} + +.profile-content { + display: flex; + align-items: flex-end; + gap: 2.5rem; + position: relative; + z-index: 2; +} + +.profile-avatar-container { + position: relative; + width: 160px; + height: 160px; + flex-shrink: 0; +} + +.profile-avatar { + width: 100%; + height: 100%; + border-radius: 50%; + object-fit: cover; + border: 4px solid rgba(255, 255, 255, 0.08); + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5); + transition: transform 0.3s ease; +} + +.profile-avatar:hover { + transform: scale(1.02); + border-color: var(--color-primary); +} + +.edit-avatar-btn { + position: absolute; + bottom: 5px; + right: 5px; + background: var(--color-bg-elevated); + border: 1px solid rgba(255,255,255,0.2); + color: white; + width: 40px; + height: 40px; + border-radius: 50%; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 4px 10px rgba(0,0,0,0.3); + transition: all 0.2s; +} + +.edit-avatar-btn:hover { + background: var(--color-primary); + transform: scale(1.1); +} + +.profile-info { + margin-bottom: 1.5rem; +} + +.profile-name { + font-size: 3.5rem; + font-weight: 800; + margin: 0 0 0.5rem 0; + line-height: 1; + letter-spacing: -0.02em; + background: linear-gradient(to bottom right, #fff, #a1a1aa); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.profile-stats-pills { + display: flex; + gap: 0.8rem; + flex-wrap: wrap; +} + +.stat-pill { + background: rgba(255, 255, 255, 0.06); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.1); + padding: 0.4rem 1rem; + border-radius: 99px; + font-size: 0.85rem; + font-weight: 600; + color: var(--color-text-secondary); + display: flex; + align-items: center; + gap: 0.5rem; + transition: 0.2s; +} + +.stat-pill:hover { + background: rgba(255, 255, 255, 0.1); + color: white; +} + +.stat-pill.highlight { + background: rgba(139, 92, 246, 0.15); + color: #a78bfa; + border-color: rgba(139, 92, 246, 0.3); +} + +/* ========================================= + 3. NAVEGACIÓN (Tabs) + ========================================= */ +.hub-navigation { + display: flex; + gap: 2.5rem; + margin-top: 3rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + padding-left: 3rem; +} + +.nav-tab { + background: none; + border: none; + color: var(--color-text-muted); + font-size: 1.1rem; + font-weight: 600; + padding: 1rem 0; + cursor: pointer; + position: relative; + display: flex; + align-items: center; + gap: 0.6rem; + transition: color 0.3s ease; +} + +.nav-tab svg { + opacity: 0.7; + transition: 0.3s; +} + +.nav-tab:hover { + color: white; +} + +.nav-tab:hover svg { + opacity: 1; +} + +.nav-tab.active { + color: white; +} + +.nav-tab.active::after { + content: ''; + position: absolute; + bottom: -1px; + left: 0; + width: 100%; + height: 3px; + background: var(--color-primary); + border-radius: 3px 3px 0 0; + box-shadow: 0 -4px 15px var(--color-primary-glow); +} + +.tab-section { + display: none; + opacity: 0; + transform: translateY(10px); + transition: opacity 0.3s, transform 0.3s; +} + +.tab-section.active { + display: block; + opacity: 1; + transform: translateY(0); +} + +/* ========================================= + 4. TOOLBAR & FILTROS + ========================================= */ +.toolbar { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2.5rem; + background: rgba(255,255,255,0.03); + border: 1px solid rgba(255,255,255,0.05); + padding: 0.75rem 1rem; + border-radius: 12px; + backdrop-filter: blur(5px); + flex-wrap: wrap; + gap: 1rem; +} + +.search-box { + position: relative; + flex-grow: 1; + max-width: 400px; +} + +.search-box svg { + position: absolute; + left: 14px; + top: 50%; + transform: translateY(-50%); + color: var(--color-text-muted); + pointer-events: none; +} + +.search-input { + width: 100%; + background: rgba(0,0,0,0.2); + border: 1px solid transparent; + padding: 0.7rem 1rem 0.7rem 2.8rem; + border-radius: 8px; + color: white; + font-family: inherit; + font-size: 0.95rem; + transition: all 0.2s; +} + +.search-input:focus { + background: rgba(0,0,0,0.4); + border-color: var(--color-primary); + box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.1); +} + +.filters-inline { + display: flex; + gap: 0.8rem; + align-items: center; + flex-wrap: wrap; +} + +.minimal-select { + background: rgba(255,255,255,0.05); + border: 1px solid rgba(255,255,255,0.08); + color: var(--color-text-secondary); + padding: 0.6rem 2rem 0.6rem 1rem; /* Padding derecho para la flecha */ + border-radius: 8px; + cursor: pointer; + font-family: inherit; + font-size: 0.9rem; + font-weight: 500; + transition: 0.2s; + appearance: none; + -webkit-appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23a1a1aa'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 0.7rem center; + background-size: 1rem; +} + +.minimal-select:hover { + background: rgba(255,255,255,0.1); + color: white; +} + +.minimal-select:focus { + border-color: var(--color-primary); + color: white; +} + +.view-toggle { + display: flex; + background: rgba(0,0,0,0.3); + border-radius: 8px; + padding: 2px; +} + +.view-btn { + background: transparent; + border: none; + color: var(--color-text-muted); + padding: 0.5rem 0.8rem; + border-radius: 6px; + cursor: pointer; + transition: 0.2s; + font-size: 1.1rem; + line-height: 1; +} + +.view-btn:hover { + color: white; +} + +.view-btn.active { + background: rgba(255,255,255,0.1); + color: white; +} + +/* ========================================= + 5. LIST GRID (Estilo Netflix Cards) + ========================================= */ +.list-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 2rem 1.5rem; +} + +.list-item { + position: relative; + border-radius: 8px; + transition: transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 0.3s; + cursor: pointer; + background: transparent; +} + +.list-item:hover { + transform: scale(1.05); + z-index: 10; +} + +/* Poster */ +.item-poster-link { + display: block; + position: relative; + border-radius: 8px; + overflow: hidden; + aspect-ratio: 2/3; + box-shadow: 0 4px 15px rgba(0,0,0,0.3); +} + +.item-poster { + width: 100%; + height: 100%; + object-fit: cover; + transition: filter 0.3s; +} + +/* Oscurecer ligeramente la imagen en hover para destacar texto si se desea, o brillo */ +.list-item:hover .item-poster { + filter: brightness(1.1); +} + +/* Contenido debajo del poster (Título, etc) */ +.item-content { + padding-top: 0.8rem; +} + +.item-title { + font-size: 0.95rem; + font-weight: 700; + margin: 0 0 0.4rem 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: rgba(255,255,255,0.9); +} + +.list-item:hover .item-title { + color: var(--color-primary); +} + +.item-meta { + display: flex; + align-items: center; + gap: 0.5rem; + flex-wrap: wrap; + margin-bottom: 0.5rem; +} + +.meta-pill { + font-size: 0.65rem; + padding: 0.15rem 0.4rem; + border-radius: 4px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.status-pill { background: rgba(34, 197, 94, 0.15); color: #4ade80; } +.type-pill { background: rgba(255, 255, 255, 0.1); color: #e4e4e7; } + +.progress-text { + font-size: 0.8rem; + color: var(--color-text-secondary); + display: flex; + justify-content: space-between; +} + +/* Botón Editar flotante (solo visible en hover) */ +.edit-icon-btn { + position: absolute; + top: 10px; + right: 10px; + background: rgba(0,0,0,0.7); + backdrop-filter: blur(4px); + border: 1px solid rgba(255,255,255,0.2); + color: white; + width: 32px; + height: 32px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transform: translateY(-5px); + transition: all 0.2s; + z-index: 20; +} + +.list-item:hover .edit-icon-btn { + opacity: 1; + transform: translateY(0); +} + +.edit-icon-btn:hover { + background: var(--color-primary); + border-color: var(--color-primary); +} + +/* --- LIST VIEW MODE --- */ +.list-grid.list-view { + grid-template-columns: 1fr; + gap: 1rem; +} + +.list-grid.list-view .list-item { + display: flex; + align-items: center; + background: var(--color-bg-elevated); + padding: 1rem; + border: 1px solid rgba(255,255,255,0.05); +} + +.list-grid.list-view .list-item:hover { + transform: scale(1.01); + border-color: var(--color-primary); + background: var(--color-bg-elevated-hover); +} + +.list-grid.list-view .item-poster-link { + width: 60px; + aspect-ratio: 2/3; + margin-right: 1.5rem; + flex-shrink: 0; +} + +.list-grid.list-view .item-content { + padding-top: 0; + flex-grow: 1; + display: flex; + align-items: center; + justify-content: space-between; +} + +.list-grid.list-view .item-title { + font-size: 1.1rem; + margin-bottom: 0.2rem; +} + +.list-grid.list-view .edit-icon-btn { + position: static; + opacity: 1; + transform: none; + background: transparent; + border: 1px solid rgba(255,255,255,0.1); +} + +/* ========================================= + 6. LOCAL LIBRARY (Dashboard Widgets) + ========================================= */ +.local-stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.local-card { + background: var(--color-bg-elevated); + border: 1px solid rgba(255,255,255,0.05); + padding: 1.5rem; + border-radius: 16px; + position: relative; + overflow: hidden; + transition: 0.3s; +} + +.local-card::after { + content: ''; + position: absolute; + top: 0; right: 0; width: 100px; height: 100px; + background: radial-gradient(circle at top right, rgba(139, 92, 246, 0.1), transparent 70%); +} + +.local-card:hover { + border-color: rgba(139, 92, 246, 0.3); + transform: translateY(-5px); +} + +.local-card h3 { + margin: 0; + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 1px; + color: var(--color-text-secondary); +} + +.local-card p { + font-size: 2.5rem; + font-weight: 800; + margin: 0.5rem 0 0 0; + color: white; +} + +.section-header-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; + padding-bottom: 1rem; + border-bottom: 1px solid rgba(255,255,255,0.05); +} + +.console-output { + background: #09090b; + border: 1px solid #27272a; + padding: 1.5rem; + border-radius: 12px; + font-family: 'JetBrains Mono', monospace; + font-size: 0.9rem; + color: #4ade80; + display: flex; + align-items: center; + gap: 1rem; + box-shadow: inset 0 2px 10px rgba(0,0,0,0.5); +} + +/* ========================================= + 7. SETTINGS (Clean Forms) + ========================================= */ +.settings-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); + gap: 2rem; +} + +.settings-card { + background: rgba(20, 20, 23, 0.6); + backdrop-filter: blur(10px); + border: 1px solid rgba(255,255,255,0.08); + padding: 2.5rem; + border-radius: 20px; +} + +.settings-card.full-width { + grid-column: 1 / -1; +} + +.settings-card h3 { + margin-top: 0; + font-size: 1.3rem; + border-bottom: 1px solid rgba(255,255,255,0.05); + padding-bottom: 1rem; + margin-bottom: 1.5rem; +} + +.form-group { + margin-bottom: 1.5rem; +} + +.form-group label { + display: block; + margin-bottom: 0.6rem; + font-size: 0.9rem; + color: var(--color-text-secondary); + font-weight: 500; +} + +.input-field { + width: 100%; + background: rgba(0,0,0,0.2); + border: 1px solid rgba(255,255,255,0.1); + padding: 0.9rem 1rem; + border-radius: 10px; + color: white; + font-size: 1rem; + transition: 0.2s; +} + +.input-field:focus { + background: rgba(0,0,0,0.4); + border-color: var(--color-primary); + box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.15); +} + +.integration-row { + display: flex; + justify-content: space-between; + align-items: center; + background: rgba(255,255,255,0.03); + padding: 1.5rem; + border-radius: 12px; + border: 1px solid rgba(255,255,255,0.05); +} + +/* Botones específicos de settings */ +.btn-danger-outline { + background: transparent; + border: 1px solid rgba(239, 68, 68, 0.5); + color: #ef4444; + padding: 0.8rem 1.5rem; + border-radius: 99px; + cursor: pointer; + font-weight: 600; + transition: 0.2s; +} + +.btn-danger-outline:hover { + background: rgba(239, 68, 68, 0.1); + border-color: #ef4444; +} + +/* ========================================= + 8. ESTADOS DE CARGA Y VACÍO + ========================================= */ +.loading-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 4rem 0; + color: var(--color-text-secondary); +} + +.spinner { + width: 40px; + height: 40px; + border: 3px solid rgba(139, 92, 246, 0.1); + border-top-color: var(--color-primary); + border-radius: 50%; + animation: spin 0.8s linear infinite; + margin-bottom: 1rem; +} + +.spinner.small { + width: 20px; + height: 20px; + border-width: 2px; + margin-bottom: 0; +} + +@keyframes spin { to { transform: rotate(360deg); } } + +.empty-state { + text-align: center; + padding: 5rem 2rem; + background: rgba(255,255,255,0.02); + border-radius: 20px; + border: 2px dashed rgba(255,255,255,0.05); +} + +.empty-state p { + font-size: 1.2rem; + color: var(--color-text-secondary); + margin-bottom: 1.5rem; +} + +/* ========================================= + 9. MOBILE RESPONSIVE + ========================================= */ +@media (max-width: 768px) { + .profile-hero { + padding: 6rem 1.5rem 1rem 1.5rem; + align-items: center; + text-align: center; + } + + .profile-content { + flex-direction: column; + align-items: center; + gap: 1.5rem; + } + + .profile-avatar-container { + width: 120px; + height: 120px; + } + + .profile-name { + font-size: 2.5rem; + } + + .hub-navigation { + justify-content: center; + padding-left: 0; + gap: 1.5rem; + font-size: 0.9rem; + } + + .toolbar { + flex-direction: column; + align-items: stretch; + } + + .search-box { + max-width: 100%; + } + + .filters-inline { + justify-content: space-between; + } + + .minimal-select { + flex: 1; + } + + .settings-grid { + grid-template-columns: 1fr; + } +} + +/* ========================================= + NUEVO DISEÑO DE PERFIL (Modern Header) + ========================================= */ +.profile-header { + background: var(--color-bg-elevated); + border-bottom: 1px solid var(--border-subtle); + padding-bottom: 0; + margin-bottom: 2rem; + position: relative; +} + +.profile-banner { + height: 200px; + background: linear-gradient(120deg, #2e1065, #8b5cf6); + width: 100%; + position: relative; +} + +.profile-body { + max-width: 1400px; + margin: 0 auto; + padding: 0 3rem; + display: flex; + align-items: flex-end; + gap: 2rem; + margin-top: -60px; /* Superposición sobre el banner */ + position: relative; + z-index: 2; +} + +.profile-avatar-wrapper { + position: relative; + width: 160px; + height: 160px; +} + +.avatar-img { + width: 100%; + height: 100%; + border-radius: 50%; + border: 5px solid var(--color-bg-base); + object-fit: cover; + background: var(--color-bg-elevated); +} + +.avatar-edit-overlay { + position: absolute; + inset: 0; + background: rgba(0,0,0,0.5); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: white; + opacity: 0; + cursor: pointer; + transition: 0.2s; + border: 5px solid transparent; +} + +.profile-avatar-wrapper:hover .avatar-edit-overlay { + opacity: 1; +} + +.profile-details { + flex-grow: 1; + display: flex; + justify-content: space-between; + align-items: flex-end; + padding-bottom: 1rem; + flex-wrap: wrap; + gap: 1rem; +} + +.profile-text h1 { + font-size: 2.5rem; + margin: 0; + line-height: 1.2; +} + +.user-badge { + background: rgba(139, 92, 246, 0.2); + color: #a78bfa; + border: 1px solid rgba(139, 92, 246, 0.3); + padding: 0.2rem 0.6rem; + border-radius: 6px; + font-size: 0.75rem; + font-weight: 700; + text-transform: uppercase; +} + +.profile-stats-grid { + display: flex; + gap: 1.5rem; +} + +.stat-card { + display: flex; + flex-direction: column; + align-items: center; + background: rgba(255,255,255,0.03); + padding: 0.5rem 1.5rem; + border-radius: 12px; + border: 1px solid rgba(255,255,255,0.05); +} + +.stat-value { + font-size: 1.4rem; + font-weight: 800; + color: white; +} + +.stat-label { + font-size: 0.8rem; + color: var(--color-text-secondary); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +/* NAVIGATION TABS (Modern Pills) */ +.hub-navigation-modern { + display: flex; + gap: 1rem; + max-width: 1400px; + margin: 1rem auto 0 auto; + padding: 0 3rem 1rem 3rem; +} + +.nav-pill { + background: transparent; + border: none; + color: var(--color-text-secondary); + padding: 0.6rem 1.2rem; + font-size: 0.95rem; + font-weight: 600; + border-radius: 8px; + cursor: pointer; + transition: 0.2s; +} + +.nav-pill:hover { + background: rgba(255,255,255,0.05); + color: white; +} + +.nav-pill.active { + background: var(--color-primary); + color: white; +} + +/* SETTINGS REDESIGN */ +.settings-layout { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2rem; + max-width: 1200px; + margin: 0 auto; +} + +.settings-card { + background: var(--color-bg-elevated); + border: 1px solid var(--border-medium); + padding: 2rem; + border-radius: 16px; + height: fit-content; +} + +.settings-stack { + display: flex; + flex-direction: column; + gap: 2rem; +} + +.input-modern { + width: 100%; + background: var(--color-bg-base); + border: 1px solid rgba(255,255,255,0.1); + padding: 0.8rem 1rem; + border-radius: 8px; + color: white; + font-size: 0.95rem; + transition: 0.2s; +} + +.input-modern:focus { + border-color: var(--color-primary); + box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.2); +} + +.full-width { width: 100%; justify-content: center; margin-top: 1rem;} + +.avatar-options { + display: flex; + align-items: center; + gap: 1rem; +} +.divider-text { color: var(--color-text-muted); font-size: 0.8rem; font-weight: bold; } + +/* Integration Item */ +.integration-item { + display: flex; + align-items: center; + gap: 1rem; + background: var(--color-bg-base); + padding: 1rem; + border-radius: 12px; +} + +.int-icon { + width: 40px; height: 40px; border-radius: 8px; overflow: hidden; +} +.int-icon img { width: 100%; height: 100%; } + +.int-details { flex-grow: 1; display: flex; flex-direction: column; } +.int-details strong { font-size: 0.95rem; } +.int-details span { font-size: 0.8rem; color: var(--color-text-secondary); } + +.btn-sm { + padding: 0.4rem 1rem; + border-radius: 6px; + background: var(--color-bg-elevated); + border: 1px solid rgba(255,255,255,0.2); + color: white; + cursor: pointer; +} + +.btn-danger { + background: rgba(239, 68, 68, 0.1); + color: #ef4444; + border: 1px solid rgba(239, 68, 68, 0.3); + width: 100%; + padding: 0.8rem; + border-radius: 8px; + font-weight: 700; + cursor: pointer; + transition: 0.2s; +} +.btn-danger:hover { background: #ef4444; color: white; } + +@media (max-width: 900px) { + .settings-layout { grid-template-columns: 1fr; } + .profile-body { flex-direction: column; align-items: center; margin-top: -100px; text-align: center; } + .profile-details { flex-direction: column; align-items: center; width: 100%; } + .profile-stats-grid { justify-content: center; width: 100%; } +} + +.username-wrapper { + display: flex; + align-items: center; + gap: 1rem; /* Espacio entre nombre e icono */ +} + +.header-anilist-badge { + width: 32px; + height: 32px; + border-radius: 8px; + overflow: hidden; + transition: transform 0.2s, box-shadow 0.2s; + display: flex; /* Para centrar la imagen */ +} + +.header-anilist-badge img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.header-anilist-badge:hover { + transform: scale(1.1); + box-shadow: 0 0 15px rgba(61, 180, 242, 0.5); /* Color azul AniList */ +} + +/* Ajuste Mobile */ +@media (max-width: 768px) { + .username-wrapper { + justify-content: center; + } +} \ No newline at end of file diff --git a/docker/views/css/list.css b/docker/views/css/list.css deleted file mode 100644 index 187b937..0000000 --- a/docker/views/css/list.css +++ /dev/null @@ -1,507 +0,0 @@ -.container { - max-width: 1600px; - margin: 0 auto; - padding: 3rem; -} - -.header-section { - margin-bottom: 3rem; - margin-top: 3rem; -} - -.page-title { - font-size: 3rem; - font-weight: 900; - margin-bottom: 2rem; - background: linear-gradient(135deg, var(--color-primary), #a78bfa); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.stats-row { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 1.5rem; -} - -.stat-card { - background: var(--color-bg-elevated); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: var(--radius-lg); - padding: 1.5rem; - display: flex; - flex-direction: column; - gap: 0.5rem; - transition: - transform 0.3s, - box-shadow 0.3s; - box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2); -} - -.stat-card:hover { - transform: translateY(-5px); - box-shadow: 0 15px 35px var(--color-primary-glow); -} - -.stat-value { - font-size: 2.5rem; - font-weight: 900; - color: var(--color-primary); -} - -.stat-label { - font-size: 0.9rem; - color: var(--color-text-secondary); - font-weight: 600; -} - -.filters-section { - display: flex; - gap: 1.5rem; - margin-bottom: 2rem; - padding: 1.5rem; - background: var(--color-bg-elevated); - border-radius: var(--radius-md); - border: 1px solid rgba(255, 255, 255, 0.05); - flex-wrap: wrap; - box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); -} - -.filter-group { - display: flex; - flex-direction: column; - gap: 0.5rem; - flex: 1; - min-width: 150px; -} - -.filter-group label { - font-size: 0.8rem; - font-weight: 700; - color: var(--color-text-secondary); - text-transform: uppercase; - letter-spacing: 1px; -} - -.filter-select { - background: var(--color-bg-base); - border: 1px solid rgba(255, 255, 255, 0.1); - color: var(--color-text-primary); - padding: 0.7rem 1rem; - border-radius: 8px; - font-family: inherit; - cursor: pointer; - transition: 0.2s; - -webkit-appearance: none; - appearance: none; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='%23a1a1aa'%3E%3Cpath fill-rule='evenodd' d='M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z' clip-rule='evenodd'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 0.7rem center; - background-size: 1.2em; - padding-right: 2.5rem; -} - -.filter-select:hover { - border-color: var(--color-primary); -} - -.filter-select:focus { - outline: none; - border-color: var(--color-primary); - box-shadow: 0 0 10px var(--color-primary-glow); -} - -.view-toggle { - display: flex; - gap: 0.5rem; -} - -.view-btn { - background: var(--color-bg-base); - border: 1px solid rgba(255, 255, 255, 0.1); - color: var(--color-text-secondary); - padding: 0.7rem; - border-radius: 8px; - cursor: pointer; - transition: 0.2s; - display: flex; - align-items: center; - justify-content: center; -} - -.view-btn:hover { - border-color: var(--color-primary); - color: white; -} - -.view-btn.active { - background: var(--color-primary); - border-color: var(--color-primary); - color: white; -} - -.loading-state { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 5rem 0; - gap: 1.5rem; -} - -.spinner { - width: 50px; - height: 50px; - border: 4px solid rgba(139, 92, 246, 0.1); - border-top-color: var(--color-primary); - border-radius: 50%; - animation: spin 1s linear infinite; -} - -@keyframes spin { - to { - transform: rotate(360deg); - } -} - -.empty-state { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 5rem 0; - gap: 1.5rem; - color: var(--color-text-secondary); -} - -.empty-state svg { - opacity: 0.3; -} - -.empty-state h2 { - font-size: 1.8rem; - color: var(--color-text-primary); -} - -.list-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); - gap: 2rem; -} - -.list-grid.list-view { - grid-template-columns: 1fr; - gap: 1rem; -} - -.list-item { - background: var(--color-bg-elevated-hover); - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: var(--radius-md); - overflow: hidden; - transition: all 0.3s cubic-bezier(0.2, 0.8, 0.2, 1); - display: flex; - flex-direction: column; - position: relative; - box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); -} - -.list-item:hover { - transform: translateY(-8px); - border-color: var(--color-primary); - box-shadow: 0 15px 30px var(--color-primary-glow); -} - -.list-grid.list-view .list-item { - flex-direction: row; - align-items: center; - padding-right: 1rem; - transition: all 0.3s ease; -} - -.list-grid.list-view .list-item:hover { - transform: none; - box-shadow: 0 4px 20px var(--color-primary-glow); -} - -.item-poster-link { - display: block; - cursor: pointer; - flex-shrink: 0; -} - -.item-poster { - width: 100%; - aspect-ratio: 2/3; - object-fit: cover; - background: #222; -} - -.list-grid.list-view .item-poster { - width: 120px; - height: 180px; - aspect-ratio: auto; - border-radius: 8px; - margin: 1rem; -} - -.item-content { - padding: 1rem; - display: flex; - flex-direction: column; - flex-grow: 1; - justify-content: space-between; -} - -.list-grid.list-view .item-content { - padding: 1rem 0; - flex-direction: row; - align-items: center; -} -.list-grid.list-view .item-content > div:first-child { - flex-basis: 75%; -} - -.item-title { - font-size: 1rem; - font-weight: 800; - margin-bottom: 0.5rem; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - color: white; -} - -.list-grid.list-view .item-title { - font-size: 1.3rem; - white-space: normal; - overflow: hidden; - text-overflow: ellipsis; - max-width: 400px; -} - -.item-meta { - display: flex; - gap: 0.3rem; - margin-bottom: 0.5rem; - flex-wrap: wrap; - - line-height: 1.4; -} - -.meta-pill { - font-size: 0.65rem; - padding: 0.15rem 0.4rem; - border-radius: 999px; - font-weight: 700; - white-space: nowrap; - text-transform: uppercase; -} - -.status-pill { - background: rgba(34, 197, 94, 0.2); - color: var(--color-success); - border: 1px solid rgba(34, 197, 94, 0.3); -} - -.type-pill { - background: rgba(139, 92, 246, 0.15); - color: var(--color-primary); - border: 1px solid rgba(139, 92, 246, 0.3); -} - -.source-pill { - background: rgba(255, 255, 255, 0.1); - color: var(--color-text-primary); - border: 1px solid rgba(255, 255, 255, 0.2); -} - -.repeat-pill { - background: rgba(59, 130, 246, 0.15); - color: #3b82f6; - border: 1px solid rgba(59, 130, 246, 0.3); - text-transform: none; -} -.private-pill { - background: rgba(251, 191, 36, 0.15); - color: #facc15; - border: 1px solid rgba(251, 191, 36, 0.3); - text-transform: none; -} - -.progress-bar-container { - background: rgba(255, 255, 255, 0.08); - border-radius: 999px; - height: 10px; - overflow: hidden; - margin-bottom: 0.5rem; - box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.5); -} - -.progress-bar { - height: 100%; - background: linear-gradient(90deg, var(--color-primary), #a78bfa); - border-radius: 999px; - transition: width 0.3s; -} - -.progress-text { - font-size: 0.9rem; - color: var(--color-text-secondary); - display: flex; - justify-content: space-between; - align-items: center; - font-weight: 500; -} - -.score-badge { - display: inline-flex; - align-items: center; - gap: 0.3rem; - font-weight: 700; - color: #facc15; - background: rgba(250, 204, 21, 0.1); - padding: 0.1rem 0.5rem; - border-radius: 4px; -} - -.edit-icon-btn { - position: absolute; - top: 1rem; - right: 1rem; - z-index: 50; - background: rgba(18, 18, 21, 0.9); - backdrop-filter: blur(8px); - border: 1px solid rgba(255, 255, 255, 0.2); - color: white; - width: 40px; - height: 40px; - border-radius: 50%; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - opacity: 0; - transition: - opacity 0.3s, - transform 0.2s, - background 0.2s; -} - -.list-item:hover .edit-icon-btn { - opacity: 1; - transform: scale(1.05); -} - -.edit-icon-btn:hover { - background: var(--color-primary); - border-color: var(--color-primary); -} - -.list-grid.list-view .edit-icon-btn { - position: relative; - top: auto; - right: auto; - margin-left: auto; - opacity: 1; - transform: none; - background: var(--color-bg-elevated); - border: 1px solid rgba(255, 255, 255, 0.1); -} -.list-grid.list-view .list-item:hover .edit-icon-btn { - opacity: 1; - background: var(--color-primary); - border-color: var(--color-primary); - transform: none; -} - -@media (max-width: 768px) { - .container { - padding: 1rem; - width: 100%; - } - - .header-section { - margin-top: 1rem; - margin-bottom: 1.5rem; - } - - .page-title { - font-size: 2rem; - text-align: center; - margin-bottom: 1.5rem; - } - - .stats-row { - grid-template-columns: 1fr; - gap: 1rem; - } - - .stat-card { - padding: 1rem; - flex-direction: row; - align-items: center; - justify-content: space-between; - } - - .stat-value { - font-size: 1.8rem; - order: 2; - } - - .stat-label { - font-size: 1rem; - order: 1; - } - - .filters-section { - flex-direction: column; - padding: 1rem; - gap: 1rem; - } - - .filter-group { - min-width: 100%; - } - - .filter-select { - width: 100%; - padding: 0.8rem; - } - - .view-toggle { - width: 100%; - display: grid; - grid-template-columns: 1fr 1fr; - } - - .view-btn { - justify-content: center; - } - - .list-grid { - grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); - gap: 1rem; - } - - .item-title { - font-size: 0.9rem; - } - - .item-meta { - gap: 0.2rem; - } - - .meta-pill { - font-size: 0.6rem; - padding: 0.1rem 0.3rem; - } - - .edit-icon-btn { - opacity: 1; - background: rgba(0, 0, 0, 0.6); - width: 35px; - height: 35px; - } -} diff --git a/docker/views/dashboard.html b/docker/views/dashboard.html new file mode 100644 index 0000000..5c91425 --- /dev/null +++ b/docker/views/dashboard.html @@ -0,0 +1,195 @@ + + + + + + + Dashboard - WaifuBoard + + + + + + + + + +
+
+ + WaifuBoard +
+
+ + + +
+
+ +
+
+
+
+ Profile + + +
+ +
+
+
+

Loading...

+ +
+
+ +
+
+ 0 + Total Entries +
+
+ - + Anime +
+
+ - + Manga +
+
+
+
+ +
+ + + +
+
+ +
+ +
+
+ +
+ + + +
+ + +
+
+
+
+ +
+
+ +
+
+

Local Library

+
+ + +
+
+
+

Anime Files

0

+

Manga Files

0

+

Novel Files

0

+
+ +
+ +
+
+
+

Edit Profile

+
+
+ + +
+ +
+ +
+ + OR + +
+ + * If you upload a file, it will be converted automatically. The text box above is only for external URLs. + +
+ +
+
+ +
+
+

Security

+
+
+ + +
+
+ + +
+ +
+
+ +
+

Integrations

+
+
AL
+
+ AniList + Checking... +
+ +
+
+ +
+

Danger Zone

+ +
+
+
+
+ +
+
+ + + + + + + + + + + + \ No newline at end of file diff --git a/docker/views/list.html b/docker/views/list.html deleted file mode 100644 index 4e46327..0000000 --- a/docker/views/list.html +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - My Lists - WaifuBoard - - - - - - - - -
-
-

My List

-
-
- 0 - Total Entries -
-
- 0 - Watching -
-
- 0 - Completed -
-
- 0 - Planning -
-
-
- -
-
- - -
- -
- - -
- -
- - -
-
- - -
- -
- -
- - -
-
-
- -
-
-

Loading your list...

-
- - - -
-
- - - - - - - - - - - \ No newline at end of file