diff --git a/src/api/list/list.controller.ts b/src/api/list/list.controller.ts index 6ccb6cb..3490c04 100644 --- a/src/api/list/list.controller.ts +++ b/src/api/list/list.controller.ts @@ -145,4 +145,32 @@ export async function deleteEntry(req: UserRequest, reply: FastifyReply) { console.error(err); return reply.code(500).send({ error: "Failed to delete list entry" }); } +} + +export async function getListByFilter(req: UserRequest, reply: FastifyReply) { + const userId = req.user?.id; + const { status, entry_type } = req.query as any; + + if (!userId) { + return reply.code(401).send({ error: "Unauthorized" }); + } + + if (!status && !entry_type) { + return reply.code(400).send({ + error: "At least one filter is required (status or entry_type)." + }); + } + + try { + const results = await listService.getUserListByFilter( + userId, + status, + entry_type + ); + + return { results }; + } catch (err) { + console.error(err); + return reply.code(500).send({ error: "Failed to retrieve filtered list" }); + } } \ No newline at end of file diff --git a/src/api/list/list.routes.ts b/src/api/list/list.routes.ts index ca16fe7..9f28aa6 100644 --- a/src/api/list/list.routes.ts +++ b/src/api/list/list.routes.ts @@ -6,6 +6,7 @@ async function listRoutes(fastify: FastifyInstance) { fastify.get('/list/entry/:entryId', controller.getSingleEntry); fastify.post('/list/entry', controller.upsertEntry); fastify.delete('/list/entry/:entryId', controller.deleteEntry); + fastify.get('/list/filter', controller.getListByFilter); } export default listRoutes; \ No newline at end of file diff --git a/src/api/list/list.service.ts b/src/api/list/list.service.ts index 9c98e89..4a3b000 100644 --- a/src/api/list/list.service.ts +++ b/src/api/list/list.service.ts @@ -385,4 +385,156 @@ export async function getActiveAccessToken(userId: number): Promise { const token = await getActiveAccessToken(userId); return !!token; +} + +export async function getUserListByFilter( + userId: number, + status?: string, + entryType?: string +): Promise { + + let sql = ` + SELECT * FROM ListEntry + WHERE user_id = ? + ORDER BY updated_at DESC; + `; + + const params: any[] = [userId]; + + try { + const dbList = await queryAll(sql, params, USER_DB) as ListEntryData[]; + const connected = await isConnected(userId); + + const statusMap: any = { + watching: 'CURRENT', + reading: 'CURRENT', + completed: 'COMPLETED', + paused: 'PAUSED', + dropped: 'DROPPED', + planning: 'PLANNING' + }; + + const mappedStatus = status ? statusMap[status.toLowerCase()] : null; + + let finalList: any[] = []; + + // ✅ FILTRADO LOCAL (MANGA + NOVEL) + const filteredLocal = dbList.filter((entry) => { + if (mappedStatus && entry.status !== mappedStatus) return false; + + if (entryType) { + if (entryType === 'MANGA') { + // ✅ AHORA ACEPTA MANGA Y NOVEL + if (!['MANGA', 'NOVEL'].includes(entry.entry_type)) return false; + } else { + if (entry.entry_type !== entryType) return false; + } + } + + return true; + }); + + // ✅ FILTRADO ANILIST (MANGA + NOVEL TAMBIÉN) + let filteredAniList: any[] = []; + + if (connected) { + const anilistEntries = await aniListService.getUserAniList(userId); + + filteredAniList = anilistEntries.filter((entry: any) => { + if (mappedStatus && entry.status !== mappedStatus) return false; + + if (entryType) { + if (entryType === 'MANGA') { + if (!['MANGA', 'NOVEL'].includes(entry.entry_type)) return false; + } else { + if (entry.entry_type !== entryType) return false; + } + } + + return true; + }); + } + + finalList = [...filteredAniList, ...filteredLocal]; + + const enrichedListPromises = finalList.map(async (entry) => { + + // ✅ AniList directo + if (entry.source === 'anilist') { + let finalTitle = entry.title; + + if (typeof finalTitle === 'object' && finalTitle !== null) { + finalTitle = + finalTitle.userPreferred || + finalTitle.english || + finalTitle.romaji || + 'Unknown Title'; + } + + return { + ...entry, + title: finalTitle, + poster: entry.poster || 'https://placehold.co/400x600?text=No+Cover', + }; + } + + // ✅ LOCAL → FETCH EXTERNO + let contentDetails: any | null = null; + const id = entry.entry_id; + const type = entry.entry_type; + const ext = getExtension(entry.source); + + try { + if (type === 'ANIME') { + const anime: any = await animeService.getAnimeInfoExtension(ext, id.toString()); + + contentDetails = { + title: anime?.title || 'Unknown Anime Title', + poster: anime?.image || '', + total_episodes: anime?.episodes || 0, + }; + + } else if (type === 'MANGA' || type === 'NOVEL') { + const book: any = await booksService.getBookInfoExtension(ext, id.toString()); + + contentDetails = { + title: book?.title || 'Unknown Book Title', + poster: book?.image || '', + total_chapters: book?.chapters || book?.volumes * 10 || 0, + }; + } + + } catch { + contentDetails = { + title: 'Error Loading Details', + poster: 'https://placehold.co/400x600?text=No+Cover', + }; + } + + let finalTitle = contentDetails?.title || 'Unknown Title'; + let finalPoster = contentDetails?.poster || 'https://placehold.co/400x600?text=No+Cover'; + + if (typeof finalTitle === 'object' && finalTitle !== null) { + finalTitle = + finalTitle.userPreferred || + finalTitle.english || + finalTitle.romaji || + 'Unknown Title'; + } + + return { + ...entry, + title: finalTitle, + poster: finalPoster, + total_episodes: contentDetails?.total_episodes, + total_chapters: contentDetails?.total_chapters, + }; + }); + + return await Promise.all(enrichedListPromises); + + } catch (error) { + console.error("Error al filtrar la lista del usuario:", error); + throw new Error("Error en la base de datos al obtener la lista filtrada."); + } } \ No newline at end of file diff --git a/src/scripts/anime/animes.js b/src/scripts/anime/animes.js index fd5edc6..5eaf2d4 100644 --- a/src/scripts/anime/animes.js +++ b/src/scripts/anime/animes.js @@ -71,6 +71,70 @@ async function fetchSearh(query) { } } +async function loadContinueWatching() { + const token = localStorage.getItem('token'); + if (!token) return; + + try { + const res = await fetch('/api/list/filter?status=watching&entry_type=ANIME', { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + } + }); + + if (!res.ok) return; + + const data = await res.json(); + const list = data.results || []; + + renderContinueWatching('my-status', list); + + } catch (err) { + console.error('Continue Watching Error:', err); + } +} + +function renderContinueWatching(id, list) { + const container = document.getElementById(id); + if (!container) return; + + container.innerHTML = ''; + + if (list.length === 0) { + container.innerHTML = `
No watching anime
`; + return; + } + + // ✅ ordenar por fecha + list.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at)); + + list.forEach(item => { + const el = document.createElement('div'); + el.className = 'card'; + + el.onclick = () => window.location.href = + item.source === 'anilist' + ? `/anime/${item.entry_id}` + : `/anime/${item.source}/${item.entry_id}`; + + const progressText = item.total_episodes + ? `${item.progress || 0}/${item.total_episodes}` + : `${item.progress || 0}`; + + el.innerHTML = ` +
+ +
+
+

${item.title}

+

Ep ${progressText} - ${item.source}

+ `; + + container.appendChild(el); + }); +} + function renderSearchResults(results) { searchResults.innerHTML = ''; if (results.length === 0) { @@ -263,4 +327,5 @@ function renderList(id, list) { } fetchContent(); +loadContinueWatching(); setInterval(() => fetchContent(true), 60000); \ No newline at end of file diff --git a/src/scripts/books/books.js b/src/scripts/books/books.js index 6ebc17d..14fab28 100644 --- a/src/scripts/books/books.js +++ b/src/scripts/books/books.js @@ -199,6 +199,72 @@ function updateHeroUI(book) { } } +async function loadContinueReading() { + const token = localStorage.getItem('token'); + if (!token) return; + + try { + const res = await fetch('/api/list/filter?status=reading&entry_type=MANGA', { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + } + }); + + if (!res.ok) return; + + const data = await res.json(); + const list = data.results || []; + + renderContinueReading('my-status-books', list); + + } catch (err) { + console.error('Continue Reading Error:', err); + } +} + +function renderContinueReading(id, list) { + const container = document.getElementById(id); + if (!container) return; + + container.innerHTML = ''; + + if (list.length === 0) { + container.innerHTML = `
No reading manga
`; + return; + } + + // ordenar por updated_at DESC + list.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at)); + + list.forEach(item => { + const el = document.createElement('div'); + el.className = 'card'; + + el.onclick = () => window.location.href = + item.source === 'anilist' + ? `/book/${item.entry_id}` + : `/book/${item.source}/${item.entry_id}`; + + const progressText = item.total_episodes + ? `${item.progress || 0}/${item.total_episodes}` + : `${item.progress || 0}`; + + el.innerHTML = ` +
+ +
+
+

${item.title}

+

Ch ${progressText} - ${item.source}

+
+ `; + + container.appendChild(el); + }); +} + + function renderList(id, list) { const container = document.getElementById(id); container.innerHTML = ''; @@ -226,4 +292,5 @@ function renderList(id, list) { }); } -init(); \ No newline at end of file +init(); +loadContinueReading(); \ No newline at end of file diff --git a/views/anime/animes.html b/views/anime/animes.html index 0b80d58..a2dcbf9 100644 --- a/views/anime/animes.html +++ b/views/anime/animes.html @@ -122,6 +122,22 @@
+
+
+
Continue watching
+
+ +
+
Trending This Season
diff --git a/views/books/books.html b/views/books/books.html index 416af47..4d4f237 100644 --- a/views/books/books.html +++ b/views/books/books.html @@ -111,6 +111,17 @@
+
+
+
Continue Reading
+
+ +
+
Trending Books