new dashboard page

This commit is contained in:
2025-12-31 20:01:57 +01:00
parent 991c58d91d
commit c9c4cc074c
18 changed files with 3400 additions and 2041 deletions

View File

@@ -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 = '<div class="empty-text" style="grid-column: 1/-1; text-align:center;">No matches found</div>';
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(`<span class="meta-pill repeat-pill">🔁 ${repeatCount}</span>`);
if (item.is_private) extraInfo.push('<span class="meta-pill private-pill">🔒 Private</span>');
div.innerHTML = `
<a href="${itemLink}" class="item-poster-link">
<img src="${posterUrl}" alt="${item.title || 'Entry'}" class="item-poster" onerror="this.src='/public/assets/placeholder.svg'">
</a>
<div class="item-content">
<div>
<a href="${itemLink}" style="text-decoration:none; color:inherit;">
<h3 class="item-title">${item.title || 'Unknown Title'}</h3>
</a>
<div class="item-meta">
<span class="meta-pill status-pill">${statusLabels[item.status] || item.status}</span>
<span class="meta-pill type-pill">${entryType}</span>
<span class="meta-pill source-pill">${item.source.toUpperCase()}</span>
${extraInfo.join('')}
</div>
</div>
<div>
<div class="progress-bar-container">
<div class="progress-bar" style="width: ${progressPercent}%"></div>
</div>
<div class="progress-text">
<span>${progress}${totalUnits > 0 ? ` / ${totalUnits}` : ''} ${unitLabel}</span> ${score ? `<span class="score-badge">⭐ ${score}</span>` : ''}
</div>
</div>
</div>
<button class="edit-icon-btn">
<svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
<path d="M15.232 5.232l3.536 3.536m-2.036-5.808a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.536L15.232 5.232z"/>
</svg>
</button>
`;
// 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();
};

View File

@@ -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 = `
<option value="all">All Sources</option>
<option value="anilist">AniList</option>
`;
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 = '<div class="empty-state"><p>No entries match your filters</p></div>';
}
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(`<span class="meta-pill repeat-pill">🔁 ${repeatCount}</span>`);
}
if (item.is_private) {
extraInfo.push('<span class="meta-pill private-pill">🔒 Private</span>');
}
const entryDataString = JSON.stringify(item).replace(/'/g, '&#39;');
div.innerHTML = `
<a href="${itemLink}" class="item-poster-link">
<img src="${posterUrl}" alt="${item.title || 'Entry'}" class="item-poster" onerror="this.src='/public/assets/placeholder.png'">
</a>
<div class="item-content">
<div>
<a href="${itemLink}" style="text-decoration:none; color:inherit;">
<h3 class="item-title">${item.title || 'Unknown Title'}</h3>
</a>
<div class="item-meta">
<span class="meta-pill status-pill">${statusLabels[item.status] || item.status}</span>
<span class="meta-pill type-pill">${entryType}</span>
<span class="meta-pill source-pill">${item.source.toUpperCase()}</span>
${extraInfo.join('')}
</div>
</div>
<div>
<div class="progress-bar-container">
<div class="progress-bar" style="width: ${progressPercent}%"></div>
</div>
<div class="progress-text">
<span>${progress}${totalUnits > 0 ? ` / ${totalUnits}` : ''} ${unitLabel}</span> ${score ? `<span class="score-badge">⭐ ${score}</span>` : ''}
</div>
</div>
</div>
<button class="edit-icon-btn" data-entry='${entryDataString}'>
<svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
<path d="M15.232 5.232l3.536 3.536m-2.036-5.808a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.536L15.232 5.232z"/>
</svg>
</button>
`;
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;
}

View File

@@ -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');

View File

@@ -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(`(<button class="nav-button[^"]*)"\\s+data-page="${page}"`, 'g');
if (page === activePage) {
@@ -51,10 +51,10 @@ async function viewsRoutes(fastify: FastifyInstance) {
reply.type('text/html').send(htmlWithNavbar);
});
fastify.get('/my-list', (req: FastifyRequest, reply: FastifyReply) => {
const htmlPath = path.join(__dirname, '..', '..', 'views', 'list.html');
fastify.get('/dashboard', (req: FastifyRequest, reply: FastifyReply) => {
const htmlPath = path.join(__dirname, '..', '..', 'views', 'dashboard.html');
const html = fs.readFileSync(htmlPath, 'utf-8');
const htmlWithNavbar = injectNavbar(html, 'my-list', false);
const htmlWithNavbar = injectNavbar(html, 'dashboard', false);
reply.type('text/html').send(htmlWithNavbar);
});