Spaces:
Running
Running
Manga reading website. The design is very intentional, with each section's layout tailored to its specific purpose.
6b53737 verified | // MangaVerse Dark - Main Script | |
| class MangaApp { | |
| constructor() { | |
| this.currentPage = 1; | |
| this.chaptersPerPage = 12; | |
| this.init(); | |
| } | |
| init() { | |
| this.loadTrending(); | |
| this.loadPopularAdditions(); | |
| this.loadNewChapters(); | |
| this.loadRecommendedLists(); | |
| this.loadBlogPosts(); | |
| this.initializeCarousels(); | |
| this.initializeFilters(); | |
| } | |
| // API helper | |
| async fetchMangaData(endpoint, params = {}) { | |
| try { | |
| const queryString = new URLSearchParams(params).toString(); | |
| const response = await fetch(`https://api.jikan.moe/v4/${endpoint}?${queryString}`); | |
| if (!response.ok) throw new Error('API request failed'); | |
| return await response.json(); | |
| } catch (error) { | |
| console.error('Error fetching manga data:', error); | |
| return { data: [] }; | |
| } | |
| } | |
| // Trending Section - 2-row grid, landscape cards | |
| async loadTrending() { | |
| const container = document.getElementById('trending-grid'); | |
| const data = await this.fetchMangaData('manga', { | |
| order_by: 'popularity', | |
| sort: 'desc', | |
| limit: 6 | |
| }); | |
| if (!data.data || data.data.length === 0) { | |
| container.innerHTML = this.generateSkeletons(6); | |
| return; | |
| } | |
| container.innerHTML = data.data.map((manga, index) => ` | |
| <a href="#manga/${manga.mal_id}" class="manga-card group relative rounded-xl overflow-hidden bg-gray-800 shadow-lg"> | |
| <div class="aspect-manga-landscape relative"> | |
| <img src="${manga.images?.jpg?.large_image_url || 'http://static.photos/nature/320x240/' + (index + 1)}" | |
| alt="${manga.title}" | |
| class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110"> | |
| <div class="absolute inset-0 gradient-overlay"></div> | |
| <div class="absolute bottom-0 left-0 right-0 p-4"> | |
| <span class="inline-block px-2 py-1 text-xs font-semibold bg-gray-700 rounded mb-2"> | |
| ${manga.type || 'Manga'} | |
| </span> | |
| <h3 class="font-bold text-sm line-clamp-2 group-hover:text-gray-300 transition-colors"> | |
| ${manga.title} | |
| </h3> | |
| </div> | |
| </div> | |
| </a> | |
| `).join(''); | |
| } | |
| // Popular Additions - Horizontal scroll, portrait cards with metadata | |
| async loadPopularAdditions() { | |
| const container = document.getElementById('popular-carousel'); | |
| const data = await this.fetchMangaData('manga', { | |
| order_by: 'score', | |
| sort: 'desc', | |
| limit: 8 | |
| }); | |
| if (!data.data || data.data.length === 0) { | |
| container.innerHTML = this.generateSkeletons(8, 'portrait'); | |
| return; | |
| } | |
| container.innerHTML = data.data.map((manga, index) => ` | |
| <a href="#manga/${manga.mal_id}" class="manga-card group flex-shrink-0 w-48 bg-gray-800 rounded-xl overflow-hidden shadow-lg"> | |
| <div class="aspect-manga-portrait relative overflow-hidden"> | |
| <img src="${manga.images?.jpg?.large_image_url || 'http://static.photos/nature/200x300/' + (index + 1)}" | |
| alt="${manga.title}" | |
| class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"> | |
| </div> | |
| <div class="p-4"> | |
| <div class="flex flex-wrap gap-1 mb-2"> | |
| ${(manga.genres?.slice(0, 2) || []).map(genre => ` | |
| <span class="text-xs px-2 py-1 bg-gray-700 rounded">${genre.name}</span> | |
| `).join('')} | |
| </div> | |
| <h3 class="font-bold text-sm mb-3 line-clamp-2 group-hover:text-gray-300"> | |
| ${manga.title} | |
| </h3> | |
| <div class="space-y-2 text-xs text-gray-400"> | |
| <div class="flex items-center justify-between"> | |
| <span class="flex items-center"> | |
| <i data-feather="book" class="w-3 h-3 mr-1"></i> | |
| ${manga.chapters || '??'} capítulos | |
| </span> | |
| <span class="flex items-center"> | |
| <i data-feather="eye" class="w-3 h-3 mr-1"></i> | |
| ${this.formatNumber(manga.scored_by || Math.floor(Math.random() * 50000) + 1000)} | |
| </span> | |
| </div> | |
| <div class="flex items-center justify-between"> | |
| <span class="flex items-center"> | |
| <i data-feather="heart" class="w-3 h-3 mr-1"></i> | |
| ${this.formatNumber(Math.floor(manga.members * 0.1) || Math.floor(Math.random() * 10000) + 500)} | |
| </span> | |
| <span class="flex items-center"> | |
| <i data-feather="star" class="w-3 h-3 mr-1"></i> | |
| ${manga.score || 'N/A'} | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| </a> | |
| `).join(''); | |
| // Re-render feather icons for new content | |
| feather.replace(); | |
| } | |
| // New Chapters - Two-column list view | |
| async loadNewChapters() { | |
| const container = document.getElementById('chapters-list'); | |
| const data = await this.fetchMangaData('manga', { | |
| order_by: 'start_date', | |
| sort: 'desc', | |
| limit: 20 | |
| }); | |
| if (!data.data || data.data.length === 0) { | |
| container.innerHTML = this.generateListSkeletons(12); | |
| return; | |
| } | |
| const chapters = data.data.flatMap(manga => { | |
| const chapterCount = manga.chapters || Math.floor(Math.random() * 80) + 10; | |
| const latestChapter = Math.floor(Math.random() * chapterCount) + 1; | |
| return [{ | |
| manga_id: manga.mal_id, | |
| title: manga.title, | |
| chapter: latestChapter, | |
| timestamp: this.generateRandomTimestamp(), | |
| scan_group: this.getRandomScanGroup(), | |
| thumbnail: manga.images?.jpg?.image_url | |
| }]; | |
| }); | |
| this.renderChapters(chapters.slice(0, this.chaptersPerPage)); | |
| this.setupChapterPagination(chapters.length); | |
| } | |
| renderChapters(chapters) { | |
| const container = document.getElementById('chapters-list'); | |
| container.innerHTML = chapters.map(chapter => ` | |
| <a href="#manga/${chapter.manga_id}/chapter/${chapter.chapter}" | |
| class="group flex items-center p-4 bg-gray-800 rounded-lg hover:bg-gray-750 transition-all duration-300 hover:pl-6 overflow-hidden"> | |
| <div class="w-16 h-20 flex-shrink-0 rounded overflow-hidden mr-4 bg-gray-700"> | |
| <img src="${chapter.thumbnail || 'http://static.photos/nature/200x250/' + (chapter.manga_id % 100)}" | |
| alt="${chapter.title}" | |
| class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"> | |
| </div> | |
| <div class="flex-grow min-w-0"> | |
| <h3 class="font-bold text-base mb-1 group-hover:text-gray-300 transition-colors truncate"> | |
| ${chapter.title} | |
| </h3> | |
| <div class="flex items-center flex-wrap gap-4 text-sm text-gray-400"> | |
| <span class="flex items-center"> | |
| <i data-feather="book-open" class="w-4 h-4 mr-1"></i> | |
| Capítulo ${chapter.chapter} | |
| </span> | |
| <span class="flex items-center"> | |
| <i data-feather="users" class="w-4 h-4 mr-1"></i> | |
| ${chapter.scan_group} | |
| </span> | |
| <span class="flex items-center"> | |
| <i data-feather="clock" class="w-4 h-4 mr-1"></i> | |
| ${chapter.timestamp} | |
| </span> | |
| </div> | |
| </div> | |
| <div class="flex-shrink-0 ml-4"> | |
| <span class="px-2 py-1 text-xs bg-gray-700 rounded-full pulse-new">Nuevo</span> | |
| </div> | |
| </a> | |
| `).join(''); | |
| feather.replace(); | |
| } | |
| // Recommended Lists - Horizontal scroll | |
| loadRecommendedLists() { | |
| const container = document.getElementById('lists-carousel'); | |
| const lists = [ | |
| { name: 'Fantasía Épica', count: 33, color: 'purple', icon: 'sword' }, | |
| { name: 'Romance Shoujo', count: 28, color: 'pink', icon: 'heart' }, | |
| { name: 'Cyberpunk', count: 15, color: 'blue', icon: 'cpu' }, | |
| { name: 'Misterio Psicológico', count: 22, color: 'gray', icon: 'eye' }, | |
| { name: 'Aventura Isekai', count: 41, color: 'green', icon: 'compass' }, | |
| { name: 'Horror Gore', count: 18, color: 'red', icon: 'alert-triangle' }, | |
| { name: 'Slice of Life', count: 35, color: 'yellow', icon: 'coffee' }, | |
| { name: 'Deportes', count: 12, color: 'orange', icon: 'activity' } | |
| ]; | |
| container.innerHTML = lists.map(list => ` | |
| <a href="#lista/${list.name.toLowerCase().replace(/\s+/g, '-')}" | |
| class="manga-card group flex-shrink-0 w-48 bg-gradient-to-br from-gray-800 to-gray-850 rounded-xl p-6 shadow-lg hover:shadow-xl transition-all duration-300"> | |
| <div class="text-center"> | |
| <div class="w-16 h-16 mx-auto mb-4 rounded-full bg-gray-700 flex items-center justify-center group-hover:scale-110 transition-transform"> | |
| <i data-feather="${list.icon}" class="w-8 h-8 text-gray-400"></i> | |
| </div> | |
| <h3 class="font-bold text-lg mb-2 group-hover:text-gray-300"> | |
| ${list.name} | |
| </h3> | |
| <p class="text-sm text-gray-400"> | |
| <span class="font-semibold text-gray-300">${list.count}</span> obras | |
| </p> | |
| </div> | |
| </a> | |
| `).join(''); | |
| feather.replace(); | |
| } | |
| // Blog Section - 3-column grid | |
| loadBlogPosts() { | |
| const container = document.getElementById('blog-grid'); | |
| const posts = [ | |
| { | |
| title: 'Los mejores animes de primavera 2024', | |
| excerpt: 'Descubre qué series están dominando la temporada y cuáles no te puedes perder.', | |
| image: 'http://static.photos/nature/640x360/101', | |
| category: 'Temporada', | |
| date: '2 días atrás' | |
| }, | |
| { | |
| title: 'Guía completa de Demon Slayer', | |
| excerpt: 'Todo lo que necesitas saber sobre el fenómeno que revolucionó el manga moderno.', | |
| image: 'http://static.photos/nature/640x360/102', | |
| category: 'Guía', | |
| date: '5 días atrás' | |
| }, | |
| { | |
| title: 'Entrevista exclusiva con Tatsuki Fujimoto', | |
| excerpt: 'El autor de Chainsaw Man nos habla de su proceso creativo y futuros proyectos.', | |
| image: 'http://static.photos/nature/640x360/103', | |
| category: 'Entrevista', | |
| date: '1 semana atrás' | |
| } | |
| ]; | |
| container.innerHTML = posts.map(post => ` | |
| <a href="#blog/${post.title.toLowerCase().replace(/\s+/g, '-')}" | |
| class="manga-card group bg-gray-800 rounded-xl overflow-hidden shadow-lg"> | |
| <div class="aspect-video relative overflow-hidden"> | |
| <img src="${post.image}" | |
| alt="${post.title}" | |
| class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110"> | |
| </div> | |
| <div class="p-6"> | |
| <div class="flex items-center justify-between mb-3"> | |
| <span class="text-xs px-3 py-1 bg-gray-700 rounded-full">${post.category}</span> | |
| <span class="text-xs text-gray-400 flex items-center"> | |
| <i data-feather="calendar" class="w-3 h-3 mr-1"></i> | |
| ${post.date} | |
| </span> | |
| </div> | |
| <h3 class="font-bold text-xl mb-3 line-clamp-2 group-hover:text-gray-300"> | |
| ${post.title} | |
| </h3> | |
| <p class="text-gray-400 line-clamp-3 mb-4"> | |
| ${post.excerpt} | |
| </p> | |
| <div class="flex items-center text-gray-400 group-hover:text-gray-300 font-semibold"> | |
| Leer artículo | |
| <i data-feather="arrow-right" class="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform"></i> | |
| </div> | |
| </div> | |
| </a> | |
| `).join(''); | |
| feather.replace(); | |
| } | |
| // Carousel controls | |
| initializeCarousels() { | |
| // Popular carousel | |
| const popularCarousel = document.getElementById('popular-carousel'); | |
| const popularPrev = document.getElementById('popular-prev'); | |
| const popularNext = document.getElementById('popular-next'); | |
| if (popularPrev && popularNext) { | |
| popularPrev.addEventListener('click', () => { | |
| popularCarousel.scrollBy({ left: -400, behavior: 'smooth' }); | |
| }); | |
| popularNext.addEventListener('click', () => { | |
| popularCarousel.scrollBy({ left: 400, behavior: 'smooth' }); | |
| }); | |
| } | |
| // Lists carousel | |
| const listsCarousel = document.getElementById('lists-carousel'); | |
| const listsPrev = document.getElementById('lists-prev'); | |
| const listsNext = document.getElementById('lists-next'); | |
| if (listsPrev && listsNext) { | |
| listsPrev.addEventListener('click', () => { | |
| listsCarousel.scrollBy({ left: -300, behavior: 'smooth' }); | |
| }); | |
| listsNext.addEventListener('click', () => { | |
| listsCarousel.scrollBy({ left: 300, behavior: 'smooth' }); | |
| }); | |
| } | |
| } | |
| // Chapter filter | |
| initializeFilters() { | |
| const filter = document.getElementById('chapter-filter'); | |
| if (filter) { | |
| filter.addEventListener('change', (e) => { | |
| // In a real app, this would filter the API results | |
| console.log('Filter changed to:', e.target.value); | |
| this.loadNewChapters(); | |
| }); | |
| } | |
| } | |
| // Pagination setup | |
| setupChapterPagination(totalItems) { | |
| const container = document.getElementById('chapters-pagination'); | |
| const totalPages = Math.ceil(totalItems / this.chaptersPerPage); | |
| container.innerHTML = ` | |
| <button class="px-4 py-2 bg-gray-800 hover:bg-gray-700 rounded-lg font-semibold transition-colors ${this.currentPage === 1 ? 'opacity-50 cursor-not-allowed' : ''}" | |
| ${this.currentPage === 1 ? 'disabled' : ''}> | |
| <i data-feather="chevron-left" class="w-4 h-4"></i> | |
| Anterior | |
| </button> | |
| ${Array.from({ length: Math.min(totalPages, 5) }, (_, i) => ` | |
| <button class="px-4 py-2 rounded-lg font-semibold transition-colors ${i + 1 === this.currentPage ? 'bg-gray-600' : 'bg-gray-800 hover:bg-gray-700'}"> | |
| ${i + 1} | |
| </button> | |
| `).join('')} | |
| <button class="px-4 py-2 bg-gray-800 hover:bg-gray-700 rounded-lg font-semibold transition-colors ${this.currentPage === totalPages ? 'opacity-50 cursor-not-allowed' : ''}" | |
| ${this.currentPage === totalPages ? 'disabled' : ''}> | |
| Siguiente | |
| <i data-feather="chevron-right" class="w-4 h-4"></i> | |
| </button> | |
| `; | |
| feather.replace(); | |
| } | |
| // Utility functions | |
| formatNumber(num) { | |
| if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M'; | |
| if (num >= 1000) return (num / 1000).toFixed(1) + 'k'; | |
| return num.toString(); | |
| } | |
| generateRandomTimestamp() { | |
| const times = ['Hace 15 min', 'Hace 1 hora', 'Hace 3 horas', 'Hace 6 horas', 'Hace 12 horas', 'Ayer', 'Hace 2 días']; | |
| return times[Math.floor(Math.random() * times.length)]; | |
| } | |
| getRandomScanGroup() { | |
| const groups = ['MangoScan', 'NeoFansub', 'KawaiiScans', 'DarkManga', 'SkyTranslations', 'MoonlitScans']; | |
| return groups[Math.floor(Math.random() * groups.length)]; | |
| } | |
| generateSkeletons(count, type = 'landscape') { | |
| const aspectClass = type === 'portrait' ? 'aspect-manga-portrait' : 'aspect-manga-landscape'; | |
| return Array(count).fill(` | |
| <div class="${aspectClass} bg-gray-800 rounded-xl skeleton"></div> | |
| `).join(''); | |
| } | |
| generateListSkeletons(count) { | |
| return Array(count).fill(` | |
| <div class="flex items-center p-4 bg-gray-800 rounded-lg skeleton"> | |
| <div class="w-16 h-20 rounded mr-4"></div> | |
| <div class="flex-grow"> | |
| <div class="h-4 w-3/4 rounded mb-2"></div> | |
| <div class="h-3 w-1/2 rounded"></div> | |
| </div> | |
| </div> | |
| `).join(''); | |
| } | |
| } | |
| // Initialize app when DOM is loaded | |
| document.addEventListener('DOMContentLoaded', () => { | |
| new MangaApp(); | |
| // Add scroll-based navbar effect | |
| const navbar = document.querySelector('manga-navbar'); | |
| if (navbar) { | |
| window.addEventListener('scroll', () => { | |
| const scrolled = window.pageYOffset > 50; | |
| navbar.shadowRoot.querySelector('nav').classList.toggle('bg-opacity-95', scrolled); | |
| navbar.shadowRoot.querySelector('nav').classList.toggle('backdrop-blur', scrolled); | |
| }); | |
| } | |
| }); | |
| // Handle navigation | |
| window.addEventListener('hashchange', () => { | |
| const hash = window.location.hash; | |
| if (hash.startsWith('#manga/')) { | |
| console.log('Navigate to manga:', hash); | |
| // In a real app, this would load manga detail page | |
| } | |
| }); |