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 = `
+
+
+
+
+
+
+
+
+ ${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 = `
-
-
-
-
-
-
-
-
-
- ${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(`(