Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>CosplayVerse Showcase 🎭</title> | |
| <link rel="icon" type="image/x-icon" href="/static/favicon.ico"> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> | |
| <script src="https://unpkg.com/feather-icons"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/animejs@3.2.1/lib/anime.min.js"></script> | |
| <style> | |
| .card { | |
| transition: all 0.3s ease; | |
| transform: scale(1); | |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); | |
| } | |
| .card:hover { | |
| transform: scale(1.03); | |
| box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); | |
| } | |
| .image-container { | |
| height: 300px; | |
| overflow: hidden; | |
| } | |
| .gallery-image { | |
| transition: transform 0.5s ease; | |
| } | |
| .gallery-image:hover { | |
| transform: scale(1.05); | |
| } | |
| #loader { | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .detail-view { | |
| opacity: 0; | |
| transition: opacity 0.5s ease; | |
| } | |
| .detail-view.active { | |
| opacity: 1; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gradient-to-b from-purple-900 to-indigo-900 text-white min-h-screen"> | |
| <!-- Header Section --> | |
| <header class="bg-black bg-opacity-80 backdrop-filter backdrop-blur-lg sticky top-0 z-50"> | |
| <div class="container mx-auto px-4 py-4 flex justify-between items-center"> | |
| <div class="flex items-center space-x-2"> | |
| <i data-feather="aperture" class="text-pink-500"></i> | |
| <h1 class="text-2xl font-bold bg-gradient-to-r from-pink-500 to-purple-500 bg-clip-text text-transparent">CosplayVerse</h1> | |
| </div> | |
| <div class="relative w-1/3"> | |
| <input type="text" id="searchInput" placeholder="Search cosplayers..." | |
| class="w-full px-4 py-2 rounded-full bg-gray-800 text-white focus:outline-none focus:ring-2 focus:ring-pink-500"> | |
| <button id="searchBtn" class="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-pink-500"> | |
| <i data-feather="search"></i> | |
| </button> | |
| </div> | |
| <div class="flex space-x-4"> | |
| <button id="latestBtn" class="px-4 py-2 rounded-full bg-pink-600 hover:bg-pink-700 transition-colors flex items-center space-x-2"> | |
| <i data-feather="refresh-cw" class="w-4 h-4"></i> | |
| <span>Latest</span> | |
| </button> | |
| <button id="toggleTheme" class="p-2 rounded-full bg-gray-800 hover:bg-gray-700 transition-colors"> | |
| <i data-feather="moon" class="w-5 h-5"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- Main Content --> | |
| <main class="container mx-auto px-4 py-8"> | |
| <!-- Loading State --> | |
| <div id="loading" class="flex justify-center items-center py-20"> | |
| <div id="loader" class="w-12 h-12 border-4 border-pink-500 border-t-transparent rounded-full"></div> | |
| </div> | |
| <!-- Grid View --> | |
| <div id="gridView" class="hidden"> | |
| <h2 id="gridTitle" class="text-3xl font-bold mb-8 text-center bg-gradient-to-r from-pink-400 to-purple-400 bg-clip-text text-transparent">Latest Cosplays</h2> | |
| <div id="cosplayGrid" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6"></div> | |
| <div id="pagination" class="flex justify-center mt-8 space-x-2 hidden"> | |
| <button id="prevPage" class="px-4 py-2 bg-gray-800 rounded hover:bg-gray-700 disabled:opacity-50"> | |
| <i data-feather="chevron-left" class="w-5 h-5"></i> | |
| </button> | |
| <span id="pageInfo" class="px-4 py-2">Page 1</span> | |
| <button id="nextPage" class="px-4 py-2 bg-gray-800 rounded hover:bg-gray-700 disabled:opacity-50"> | |
| <i data-feather="chevron-right" class="w-5 h-5"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Detail View --> | |
| <div id="detailView" class="detail-view hidden"> | |
| <div class="flex justify-between items-center mb-6"> | |
| <button id="backBtn" class="flex items-center space-x-2 px-4 py-2 bg-gray-800 rounded-full hover:bg-gray-700 transition-colors"> | |
| <i data-feather="arrow-left" class="w-5 h-5"></i> | |
| <span>Back</span> | |
| </button> | |
| </div> | |
| <div class="bg-black bg-opacity-50 rounded-xl p-6 mb-8"> | |
| <h2 id="detailTitle" class="text-3xl font-bold mb-4"></h2> | |
| <div id="detailInfo" class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6"> | |
| <!-- Will be populated with details --> | |
| </div> | |
| <!-- Gallery --> | |
| <div class="mb-8"> | |
| <h3 class="text-2xl font-bold mb-6 text-center bg-gradient-to-r from-pink-400 to-purple-400 bg-clip-text text-transparent">Gallery</h3> | |
| <div id="imageGallery" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"> | |
| <!-- Images will be inserted here --> | |
| </div> | |
| </div> | |
| <!-- Videos --> | |
| <div class="mb-8" id="videoSection"> | |
| <h3 class="text-2xl font-bold mb-6 text-center bg-gradient-to-r from-pink-400 to-purple-400 bg-clip-text text-transparent">Videos</h3> | |
| <div id="videoGallery" class="grid grid-cols-1 lg:grid-cols-2 gap-6"> | |
| <!-- Videos will be inserted here --> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <!-- Footer --> | |
| <footer class="bg-black bg-opacity-80 py-8 mt-12"> | |
| <div class="container mx-auto px-4 text-center"> | |
| <div class="flex justify-center space-x-4 mb-4"> | |
| <a href="#" class="text-gray-400 hover:text-pink-500 transition-colors"> | |
| <i data-feather="instagram"></i> | |
| </a> | |
| <a href="#" class="text-gray-400 hover:text-pink-500 transition-colors"> | |
| <i data-feather="twitter"></i> | |
| </a> | |
| <a href="#" class="text-gray-400 hover:text-pink-500 transition-colors"> | |
| <i data-feather="github"></i> | |
| </a> | |
| </div> | |
| <p class="text-gray-400">© 2023 CosplayVerse Showcase. All rights reserved.</p> | |
| </div> | |
| </footer> | |
| <script> | |
| // Constants | |
| const API_BASE_URL = "https://restapi.rizk.my.id/sfwnsfw/cosplaytelensfw"; | |
| const API_KEY = "vip"; | |
| const PROXY_URL_BASE = "https://api.allorigins.win/get?url="; | |
| let currentPage = 1; | |
| let currentListData = []; | |
| let currentView = 'grid'; | |
| let currentSearchQuery = ''; | |
| // DOM Elements | |
| const gridView = document.getElementById('gridView'); | |
| const detailView = document.getElementById('detailView'); | |
| const cosplayGrid = document.getElementById('cosplayGrid'); | |
| const loading = document.getElementById('loading'); | |
| const gridTitle = document.getElementById('gridTitle'); | |
| const pagination = document.getElementById('pagination'); | |
| const pageInfo = document.getElementById('pageInfo'); | |
| const prevPage = document.getElementById('prevPage'); | |
| const nextPage = document.getElementById('nextPage'); | |
| const searchInput = document.getElementById('searchInput'); | |
| const searchBtn = document.getElementById('searchBtn'); | |
| const latestBtn = document.getElementById('latestBtn'); | |
| const backBtn = document.getElementById('backBtn'); | |
| const detailTitle = document.getElementById('detailTitle'); | |
| const detailInfo = document.getElementById('detailInfo'); | |
| const imageGallery = document.getElementById('imageGallery'); | |
| const videoGallery = document.getElementById('videoGallery'); | |
| const toggleTheme = document.getElementById('toggleTheme'); | |
| // Initialize | |
| document.addEventListener('DOMContentLoaded', () => { | |
| feather.replace(); | |
| loadLatest(); | |
| setupEventListeners(); | |
| // Check for saved theme preference | |
| if (localStorage.getItem('theme') === 'dark') { | |
| document.documentElement.classList.add('dark'); | |
| toggleTheme.innerHTML = feather.icons['sun'].toSvg(); | |
| } else { | |
| document.documentElement.classList.remove('dark'); | |
| toggleTheme.innerHTML = feather.icons['moon'].toSvg(); | |
| } | |
| }); | |
| function setupEventListeners() { | |
| // Navigation | |
| searchBtn.addEventListener('click', () => { | |
| currentSearchQuery = searchInput.value.trim(); | |
| if (currentSearchQuery) { | |
| currentPage = 1; | |
| loadSearch(currentSearchQuery); | |
| } | |
| }); | |
| searchInput.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') { | |
| currentSearchQuery = searchInput.value.trim(); | |
| if (currentSearchQuery) { | |
| currentPage = 1; | |
| loadSearch(currentSearchQuery); | |
| } | |
| } | |
| }); | |
| latestBtn.addEventListener('click', () => { | |
| currentSearchQuery = ''; | |
| searchInput.value = ''; | |
| currentPage = 1; | |
| loadLatest(); | |
| }); | |
| backBtn.addEventListener('click', () => { | |
| showGridView(); | |
| }); | |
| // Pagination | |
| prevPage.addEventListener('click', () => { | |
| if (currentPage > 1) { | |
| currentPage--; | |
| if (currentSearchQuery) { | |
| loadSearch(currentSearchQuery); | |
| } else { | |
| loadLatest(); | |
| } | |
| } | |
| }); | |
| nextPage.addEventListener('click', () => { | |
| currentPage++; | |
| if (currentSearchQuery) { | |
| loadSearch(currentSearchQuery); | |
| } else { | |
| loadLatest(); | |
| } | |
| }); | |
| // Theme toggle | |
| toggleTheme.addEventListener('click', () => { | |
| if (document.documentElement.classList.contains('dark')) { | |
| document.documentElement.classList.remove('dark'); | |
| localStorage.setItem('theme', 'light'); | |
| toggleTheme.innerHTML = feather.icons['moon'].toSvg(); | |
| } else { | |
| document.documentElement.classList.add('dark'); | |
| localStorage.setItem('theme', 'dark'); | |
| toggleTheme.innerHTML = feather.icons['sun'].toSvg(); | |
| } | |
| }); | |
| } | |
| // API Fetch Helper | |
| async function fetchWithProxy(apiEndpoint) { | |
| try { | |
| const encodedUrl = encodeURIComponent(apiEndpoint); | |
| const proxyUrl = `${PROXY_URL_BASE}${encodedUrl}`; | |
| const response = await fetch(proxyUrl); | |
| const data = await response.json(); | |
| if (data.contents) { | |
| return JSON.parse(data.contents); | |
| } | |
| return null; | |
| } catch (error) { | |
| console.error('Error fetching data:', error); | |
| return null; | |
| } | |
| } | |
| // Load Latest Cosplays | |
| async function loadLatest() { | |
| showLoading(); | |
| gridTitle.textContent = 'Latest Cosplays'; | |
| const endpoint = `${API_BASE_URL}/latest?page=${currentPage}&apikey=${API_KEY}`; | |
| const data = await fetchWithProxy(endpoint); | |
| if (data && data.status === 'success' && data.data) { | |
| currentListData = data.data; | |
| renderCosplayGrid(); | |
| updatePagination(); | |
| showGridView(); | |
| } else { | |
| showError('Failed to load latest cosplays'); | |
| } | |
| } | |
| // Load Search Results | |
| async function loadSearch(query) { | |
| showLoading(); | |
| gridTitle.textContent = `Search Results for "${query}"`; | |
| const endpoint = `${API_BASE_URL}/search?query=${encodeURIComponent(query)}&apikey=${API_KEY}`; | |
| const data = await fetchWithProxy(endpoint); | |
| if (data && data.status === 'success' && data.data) { | |
| currentListData = data.data; | |
| renderCosplayGrid(); | |
| pagination.classList.add('hidden'); // Search doesn't have pagination | |
| showGridView(); | |
| } else { | |
| showError('No results found'); | |
| } | |
| } | |
| // Load Detail View | |
| async function loadDetail(index) { | |
| showLoading(); | |
| const item = currentListData[index]; | |
| const endpoint = `${API_BASE_URL}/detail?url=${encodeURIComponent(item.url)}&apikey=${API_KEY}`; | |
| const data = await fetchWithProxy(endpoint); | |
| if (data && data.status === 'success' && data.data) { | |
| renderDetailView(data.data); | |
| showDetailView(); | |
| } else { | |
| showError('Failed to load cosplay details'); | |
| } | |
| } | |
| // Render Grid View | |
| function renderCosplayGrid() { | |
| cosplayGrid.innerHTML = ''; | |
| currentListData.forEach((item, index) => { | |
| const card = document.createElement('div'); | |
| card.className = 'card bg-gray-800 rounded-xl overflow-hidden hover:cursor-pointer transition-all duration-300'; | |
| card.addEventListener('click', () => loadDetail(index)); | |
| // Parse title to extract cosplayer and character | |
| let cosplayer = 'Unknown'; | |
| let character = 'Unknown'; | |
| if (item.title) { | |
| const parts = item.title.split('–'); | |
| if (parts.length >= 2) { | |
| cosplayer = parts[0].trim(); | |
| character = parts[1].trim(); | |
| } else { | |
| cosplayer = item.title.trim(); | |
| } | |
| } | |
| card.innerHTML = ` | |
| <div class="image-container"> | |
| <img src="${item.image}" alt="${item.title}" class="w-full h-full object-cover"> | |
| </div> | |
| <div class="p-4"> | |
| <h3 class="text-xl font-bold mb-1 truncate">${character}</h3> | |
| <p class="text-pink-400 mb-2">${cosplayer}</p> | |
| <p class="text-gray-400 text-sm line-clamp-2">${item.excerpt || ''}</p> | |
| </div> | |
| `; | |
| cosplayGrid.appendChild(card); | |
| }); | |
| // Animate cards | |
| anime({ | |
| targets: '.card', | |
| opacity: [0, 1], | |
| translateY: [20, 0], | |
| delay: anime.stagger(50), | |
| easing: 'easeOutExpo' | |
| }); | |
| } | |
| // Render Detail View | |
| function renderDetailView(data) { | |
| detailTitle.textContent = data.title || 'Cosplay Details'; | |
| // Clear previous content | |
| detailInfo.innerHTML = ''; | |
| imageGallery.innerHTML = ''; | |
| videoGallery.innerHTML = ''; | |
| // Parse title to extract cosplayer and character if available | |
| let cosplayer = 'Unknown'; | |
| let character = 'Unknown'; | |
| if (data.title) { | |
| const parts = data.title.split('–'); | |
| if (parts.length >= 2) { | |
| cosplayer = parts[0].trim(); | |
| character = parts[1].trim(); | |
| } else { | |
| cosplayer = data.title.trim(); | |
| } | |
| } | |
| // Add basic info | |
| detailInfo.innerHTML = ` | |
| <div class="bg-gray-800 p-4 rounded-lg"> | |
| <h4 class="text-lg font-bold text-pink-400 mb-2">Cosplayer</h4> | |
| <p>${cosplayer}</p> | |
| </div> | |
| <div class="bg-gray-800 p-4 rounded-lg"> | |
| <h4 class="text-lg font-bold text-pink-400 mb-2">Character</h4> | |
| <p>${character}</p> | |
| </div> | |
| <div class="bg-gray-800 p-4 rounded-lg"> | |
| <h4 class="text-lg font-bold text-pink-400 mb-2">Details</h4> | |
| <p>${data.details?.description || 'No additional details available'}</p> | |
| </div> | |
| `; | |
| // Add images | |
| if (data.images && data.images.length > 0) { | |
| // Filter out any non-image URLs that might be in the data | |
| const validImages = data.images.filter(img => | |
| img && (img.endsWith('.jpg') || img.endsWith('.jpeg') || img.endsWith('.png') || img.endsWith('.webp')) | |
| ); | |
| data.images.forEach((imageUrl, index) => { | |
| const imgContainer = document.createElement('div'); | |
| imgContainer.className = 'relative overflow-hidden rounded-lg gallery-image'; | |
| const imgContainer = document.createElement('div'); | |
| imgContainer.className = 'relative overflow-hidden rounded-xl group'; | |
| const img = document.createElement('img'); | |
| img.src = imageUrl; | |
| img.alt = `Cosplay image ${index + 1}`; | |
| img.className = 'w-full h-72 object-cover transition-transform duration-500 group-hover:scale-105'; | |
| const overlay = document.createElement('div'); | |
| overlay.className = 'absolute inset-0 bg-gradient-to-t from-black/70 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300'; | |
| imgContainer.appendChild(img); | |
| imgContainer.appendChild(overlay); | |
| imageGallery.appendChild(imgContainer); | |
| }); | |
| } else { | |
| imageGallery.innerHTML = ` | |
| <div class="col-span-full text-center py-12"> | |
| <i data-feather="image" class="w-12 h-12 mx-auto text-gray-400 mb-4"></i> | |
| <p class="text-gray-400">No cosplay images available</p> | |
| </div> | |
| `; | |
| feather.replace(); | |
| } | |
| // Add videos if available | |
| if (data.videos && data.videos.length > 0) { | |
| const videosContainer = videoGallery.querySelector('div'); | |
| data.videos.forEach(videoUrl => { | |
| const videoContainer = document.createElement('div'); | |
| videoContainer.className = 'relative overflow-hidden rounded-xl group'; | |
| videoContainer.innerHTML = ` | |
| <div class="aspect-w-16 aspect-h-9"> | |
| <iframe src="${videoUrl}" class="w-full h-full rounded-xl" frameborder="0" allowfullscreen></iframe> | |
| </div> | |
| <div class="absolute inset-0 bg-black/20 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div> | |
| `; | |
| videosContainer.appendChild(videoContainer); | |
| }); | |
| } else { | |
| videoGallery.innerHTML = ` | |
| <div class="col-span-full text-center py-12"> | |
| <i data-feather="video" class="w-12 h-12 mx-auto text-gray-400 mb-4"></i> | |
| <p class="text-gray-400">No cosplay videos available</p> | |
| </div> | |
| `; | |
| feather.replace(); | |
| } | |
| // Animate elements | |
| anime({ | |
| targets: [detailTitle, '#detailInfo > div', '#imageGallery > div', '#videoGallery'], | |
| opacity: [0, 1], | |
| translateY: [20, 0], | |
| delay: anime.stagger(50), | |
| easing: 'easeOutExpo' | |
| }); | |
| } | |
| // View Management | |
| function showLoading() { | |
| loading.classList.remove('hidden'); | |
| gridView.classList.add('hidden'); | |
| detailView.classList.add('hidden'); | |
| } | |
| function showGridView() { | |
| loading.classList.add('hidden'); | |
| detailView.classList.add('hidden'); | |
| gridView.classList.remove('hidden'); | |
| currentView = 'grid'; | |
| // Scroll to top | |
| window.scrollTo({ top: 0, behavior: 'smooth' }); | |
| } | |
| function showDetailView() { | |
| loading.classList.add('hidden'); | |
| gridView.classList.add('hidden'); | |
| detailView.classList.remove('hidden'); | |
| detailView.classList.add('active'); | |
| currentView = 'detail'; | |
| // Scroll to top | |
| window.scrollTo({ top: 0, behavior: 'smooth' }); | |
| } | |
| function showError(message) { | |
| loading.classList.add('hidden'); | |
| if (currentView === 'grid') { | |
| cosplayGrid.innerHTML = ` | |
| <div class="col-span-full text-center py-12"> | |
| <i data-feather="alert-triangle" class="w-12 h-12 mx-auto text-yellow-400 mb-4"></i> | |
| <h3 class="text-xl font-bold">${message}</h3> | |
| </div> | |
| `; | |
| feather.replace(); | |
| } else { | |
| detailView.innerHTML = ` | |
| <div class="text-center py-12"> | |
| <i data-feather="alert-triangle" class="w-12 h-12 mx-auto text-yellow-400 mb-4"></i> | |
| <h3 class="text-xl font-bold">${message}</h3> | |
| <button id="backBtn" class="mt-4 px-4 py-2 bg-pink-600 rounded-full hover:bg-pink-700 transition-colors"> | |
| Go Back | |
| </button> | |
| </div> | |
| `; | |
| feather.replace(); | |
| document.getElementById('backBtn').addEventListener('click', showGridView); | |
| } | |
| } | |
| function updatePagination() { | |
| if (currentListData.length > 0) { | |
| pagination.classList.remove('hidden'); | |
| pageInfo.textContent = `Page ${currentPage}`; | |
| prevPage.disabled = currentPage === 1; | |
| nextPage.disabled = currentListData.length < 20; // Assuming 20 items per page | |
| } else { | |
| pagination.classList.add('hidden'); | |
| } | |
| } | |
| // Theme management | |
| function toggleDarkMode() { | |
| document.documentElement.classList.toggle('dark'); | |
| const isDark = document.documentElement.classList.contains('dark'); | |
| localStorage.setItem('theme', isDark ? 'dark' : 'light'); | |
| toggleTheme.innerHTML = isDark ? feather.icons['sun'].toSvg() : feather.icons['moon'].toSvg(); | |
| } | |
| </script> | |
| </body> | |
| </html> | |