Spaces:
Running
Running
fix the api code so it operates correctly and add the additional pages - Initial Deployment
0bd5ed6 verified | <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width,initial-scale=1" /> | |
| <title>Mr.FLEN — Audius Library</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet"> | |
| <script src="https://unpkg.com/aos@2.3.1/dist/aos.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/animejs/lib/anime.iife.min.js"></script> | |
| <script src="https://unpkg.com/feather-icons"></script> | |
| <link rel="stylesheet" href="/base.css" /> | |
| <style> | |
| :root { | |
| --glow: #5cf; | |
| --ink: #0a0a0a; | |
| --glass: rgba(255, 255, 255, 0.06); | |
| --neon-pink: #ff2a6d; | |
| --neon-blue: #05d9e8; | |
| --neon-purple: #d16bff; | |
| } | |
| body { | |
| background: radial-gradient(1200px 600px at 20% -10%, rgba(0, 255, 255, 0.2), transparent 60%), #05060a; | |
| color: #e8f0ff; | |
| font-family: 'Inter', system-ui, sans-serif; | |
| min-height: 100vh; | |
| } | |
| .glass { | |
| backdrop-filter: saturate(1.4) blur(12px); | |
| background: var(--glass); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| border-radius: 16px; | |
| box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); | |
| } | |
| .neon-glow { | |
| box-shadow: 0 0 5px var(--neon-blue), 0 0 10px var(--neon-blue), 0 0 15px var(--neon-blue); | |
| } | |
| .neon-text { | |
| text-shadow: 0 0 5px currentColor, 0 0 10px currentColor; | |
| } | |
| .track-card { | |
| transition: all 0.3s ease; | |
| } | |
| .track-card:hover { | |
| transform: translateY(-4px); | |
| box-shadow: 0 10px 25px rgba(92, 207, 255, 0.15); | |
| } | |
| .play-button { | |
| transition: all 0.3s ease; | |
| } | |
| .play-button:hover { | |
| transform: scale(1.1); | |
| color: var(--neon-blue); | |
| } | |
| .skeleton { | |
| background: linear-gradient(90deg, rgba(255, 255, 255, 0.1) 25%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.1) 75%); | |
| background-size: 200% 100%; | |
| animation: loading 1.5s infinite; | |
| } | |
| @keyframes loading { | |
| 0% { background-position: 200% 0; } | |
| 100% { background-position: -200% 0; } | |
| } | |
| .toast { | |
| animation: slideIn 0.3s forwards, slideOut 0.3s forwards 2.7s; | |
| } | |
| @keyframes slideIn { | |
| from { transform: translateX(100%); opacity: 0; } | |
| to { transform: translateX(0); opacity: 1; } | |
| } | |
| @keyframes slideOut { | |
| from { transform: translateX(0); opacity: 1; } | |
| to { transform: translateX(100%); opacity: 0; } | |
| } | |
| </style> | |
| </head> | |
| <body class="min-h-screen flex flex-col"> | |
| <!-- Header --> | |
| <header class="glass sticky top-0 z-50 p-4 mb-6"> | |
| <div class="container mx-auto flex flex-col md:flex-row items-center justify-between gap-4"> | |
| <div class="flex items-center gap-3"> | |
| <div class="w-12 h-12 rounded-full bg-gradient-to-br from-purple-500 to-blue-400 flex items-center justify-center neon-glow"> | |
| <span class="text-xl">🎵</span> | |
| </div> | |
| <h1 class="text-2xl font-bold neon-text text-blue-300">Mr.FLEN Library</h1> | |
| </div> | |
| <div class="relative w-full md:w-96"> | |
| <input | |
| id="search" | |
| type="text" | |
| placeholder="Search Audius (Ctrl/⌘-K)" | |
| autocomplete="off" | |
| class="w-full py-2 px-4 pr-10 rounded-full bg-black/30 border border-white/10 focus:border-blue-400 focus:ring-2 focus:ring-blue-500/30 focus:outline-none transition-all" | |
| /> | |
| <i data-feather="search" class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4"></i> | |
| </div> | |
| <nav class="flex gap-2"> | |
| <button class="px-4 py-2 rounded-lg bg-white/5 hover:bg-white/10 transition-all">Library</button> | |
| <button class="px-4 py-2 rounded-lg bg-white/5 hover:bg-white/10 transition-all">Likes</button> | |
| <button class="px-4 py-2 rounded-lg bg-white/5 hover:bg-white/10 transition-all">Playlists</button> | |
| <button class="px-4 py-2 rounded-lg bg-white/5 hover:bg-white/10 transition-all">Discover</button> | |
| </nav> | |
| </div> | |
| </header> | |
| <!-- Main Content --> | |
| <main class="flex-1 container mx-auto px-4 pb-24"> | |
| <div id="results" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"> | |
| <!-- Results will be populated here --> | |
| </div> | |
| <div id="empty-state" class="hidden flex-col items-center justify-center py-20 text-center"> | |
| <div class="w-24 h-24 rounded-full bg-white/5 flex items-center justify-center mb-6"> | |
| <i data-feather="music" class="w-12 h-12 text-blue-400"></i> | |
| </div> | |
| <h2 class="text-2xl font-bold mb-2">No tracks found</h2> | |
| <p class="text-gray-400 max-w-md">Search for music on Audius to start listening to Mr.FLEN's library</p> | |
| </div> | |
| </main> | |
| <!-- Player Bar --> | |
| <footer class="glass fixed bottom-0 left-0 right-0 p-4 border-t border-white/10"> | |
| <div class="container mx-auto flex items-center gap-4"> | |
| <div class="flex items-center gap-3 flex-1 min-w-0"> | |
| <img id="now-playing-art" src="" class="w-14 h-14 rounded-lg object-cover hidden"> | |
| <div class="min-w-0 flex-1"> | |
| <div id="now-playing-title" class="font-semibold truncate">Not playing</div> | |
| <div id="now-playing-artist" class="text-sm text-gray-400 truncate">Search for music to begin</div> | |
| </div> | |
| </div> | |
| <div class="flex items-center gap-2"> | |
| <button id="prev-btn" class="p-2 rounded-full hover:bg-white/10"> | |
| <i data-feather="skip-back" class="w-5 h-5"></i> | |
| </button> | |
| <button id="play-pause-btn" class="p-3 rounded-full bg-blue-500 hover:bg-blue-400"> | |
| <i data-feather="play" class="w-5 h-5" id="play-icon"></i> | |
| </button> | |
| <button id="next-btn" class="p-2 rounded-full hover:bg-white/10"> | |
| <i data-feather="skip-forward" class="w-5 h-5"></i> | |
| </button> | |
| </div> | |
| <div class="hidden md:flex items-center gap-2 flex-1 max-w-md"> | |
| <span class="text-xs text-gray-400" id="current-time">0:00</span> | |
| <input type="range" id="progress" class="flex-1 h-1 bg-gray-700 rounded-lg appearance-none cursor-pointer" value="0"> | |
| <span class="text-xs text-gray-400" id="duration">0:00</span> | |
| </div> | |
| <div class="flex items-center gap-2"> | |
| <button class="p-2 rounded-full hover:bg-white/10"> | |
| <i data-feather="heart" class="w-5 h-5"></i> | |
| </button> | |
| <div class="flex items-center gap-1"> | |
| <i data-feather="volume" class="w-4 h-4"></i> | |
| <input type="range" class="w-20 h-1 bg-gray-700 rounded-lg appearance-none cursor-pointer" value="80"> | |
| </div> | |
| </div> | |
| </div> | |
| </footer> | |
| <!-- Toast Container --> | |
| <div id="toast-container" class="fixed top-4 right-4 z-50 space-y-2"></div> | |
| <script> | |
| // Initialize animations and icons | |
| AOS.init(); | |
| feather.replace(); | |
| const { animate } = anime; | |
| </script> | |
| <script type="module"> | |
| /** ENV (replace before deploy) **/ | |
| const AUDIUS_APP_NAME = "MrFLEN-Library"; | |
| const AUDIUS_API_KEY = "YOUR_AUDIUS_API_KEY"; // required (client safe, read-only) | |
| // BACKEND-ONLY: const AUDIUS_API_SECRET = "YOUR_AUDIUS_API_SECRET"; | |
| // State management | |
| let currentTrackIndex = 0; | |
| let searchResults = []; | |
| let abortController = null; | |
| let currentHost = null; | |
| let isPlaying = false; | |
| // DOM Elements | |
| const resultsContainer = document.querySelector('#results'); | |
| const emptyState = document.querySelector('#empty-state'); | |
| const player = new Audio(); | |
| const searchInput = document.querySelector('#search'); | |
| const playPauseBtn = document.querySelector('#play-pause-btn'); | |
| const playIcon = document.querySelector('#play-icon'); | |
| const prevBtn = document.querySelector('#prev-btn'); | |
| const nextBtn = document.querySelector('#next-btn'); | |
| const progressBar = document.querySelector('#progress'); | |
| const currentTimeEl = document.querySelector('#current-time'); | |
| const durationEl = document.querySelector('#duration'); | |
| const nowPlayingArt = document.querySelector('#now-playing-art'); | |
| const nowPlayingTitle = document.querySelector('#now-playing-title'); | |
| const nowPlayingArtist = document.querySelector('#now-playing-artist'); | |
| const toastContainer = document.querySelector('#toast-container'); | |
| /** Try SDK first; fall back to REST **/ | |
| let sdk; | |
| try { | |
| // If bundling, use: import AudiusSdk from '@audius/sdk' | |
| // For this demo we will REST if import fails. | |
| // sdk = await import('https://cdn.skypack.dev/@audius/sdk').then(m => m.default({ appName: AUDIUS_APP_NAME, apiKey: AUDIUS_API_KEY })); | |
| } catch(e) { | |
| console.log("SDK not available, using REST API"); | |
| } | |
| // Utility functions | |
| function formatTime(seconds) { | |
| const mins = Math.floor(seconds / 60); | |
| const secs = Math.floor(seconds % 60); | |
| return `${mins}:${secs < 10 ? '0' : ''}${secs}`; | |
| } | |
| function showToast(message, type = 'info') { | |
| const toast = document.createElement('div'); | |
| toast.className = `toast glass px-4 py-3 rounded-lg flex items-center gap-2 ${type === 'error' ? 'text-red-300' : 'text-blue-300'}`; | |
| toast.innerHTML = ` | |
| <i data-feather="${type === 'error' ? 'alert-circle' : 'info'}" class="w-5 h-5"></i> | |
| <span>${message}</span> | |
| `; | |
| toastContainer.appendChild(toast); | |
| feather.replace(); | |
| setTimeout(() => { | |
| toast.remove(); | |
| }, 3000); | |
| } | |
| async function pickHost() { | |
| if (currentHost) return currentHost; | |
| try { | |
| const res = await fetch('https://api.audius.co'); | |
| const { data } = await res.json(); | |
| currentHost = data[Math.floor(Math.random() * data.length)]; | |
| return currentHost; | |
| } catch (error) { | |
| showToast('Failed to connect to Audius', 'error'); | |
| throw error; | |
| } | |
| } | |
| async function searchTracks(query) { | |
| if (abortController) { | |
| abortController.abort(); | |
| } | |
| abortController = new AbortController(); | |
| try { | |
| // Show loading state | |
| resultsContainer.innerHTML = ''; | |
| for (let i = 0; i < 8; i++) { | |
| resultsContainer.innerHTML += ` | |
| <div class="track-card glass p-4 animate-pulse"> | |
| <div class="skeleton w-full h-40 rounded-lg mb-4"></div> | |
| <div class="skeleton h-5 w-3/4 mb-2"></div> | |
| <div class="skeleton h-4 w-1/2"></div> | |
| </div> | |
| `; | |
| } | |
| if (sdk) { | |
| const { data } = await sdk.tracks.searchTracks({ query }); | |
| return data || []; | |
| } else { | |
| const host = await pickHost(); | |
| const url = `${host}/v1/tracks/search?query=${encodeURIComponent(query)}&app_name=${encodeURIComponent(AUDIUS_APP_NAME)}`; | |
| const res = await fetch(url, { | |
| headers: { 'Accept': 'application/json' }, | |
| signal: abortController.signal | |
| }); | |
| const json = await res.json(); | |
| return json.data || []; | |
| } | |
| } catch (error) { | |
| if (error.name !== 'AbortError') { | |
| showToast('Search failed', 'error'); | |
| console.error('Search error:', error); | |
| } | |
| return []; | |
| } | |
| } | |
| async function streamUrl(trackId) { | |
| try { | |
| if (sdk) { | |
| return await sdk.tracks.getTrackStreamUrl({ trackId }); | |
| } else { | |
| const host = await pickHost(); | |
| return `${host}/v1/tracks/${trackId}/stream?app_name=${encodeURIComponent(AUDIUS_APP_NAME)}`; | |
| } | |
| } catch (error) { | |
| showToast('Failed to get stream URL', 'error'); | |
| console.error('Stream URL error:', error); | |
| throw error; | |
| } | |
| } | |
| async function playTrack(track, index) { | |
| try { | |
| currentTrackIndex = index; | |
| const streamUrl = await streamUrl(track.id); | |
| player.src = streamUrl; | |
| player.play(); | |
| isPlaying = true; | |
| playIcon.setAttribute('data-feather', 'pause'); | |
| feather.replace(); | |
| // Update now playing info | |
| nowPlayingArt.src = track.artwork?.['150x150'] || ''; | |
| nowPlayingArt.classList.remove('hidden'); | |
| nowPlayingTitle.textContent = track.title; | |
| nowPlayingArtist.textContent = `by ${track.user.handle}`; | |
| // Preload next track for seamless playback | |
| if (searchResults[index + 1]) { | |
| const nextTrackUrl = await streamUrl(searchResults[index + 1].id); | |
| // Just preload, don't assign to player | |
| } | |
| } catch (error) { | |
| showToast('Failed to play track', 'error'); | |
| console.error('Play error:', error); | |
| } | |
| } | |
| function renderResults(tracks) { | |
| if (tracks.length === 0) { | |
| resultsContainer.classList.add('hidden'); | |
| emptyState.classList.remove('hidden'); | |
| return; | |
| } | |
| resultsContainer.classList.remove('hidden'); | |
| emptyState.classList.add('hidden'); | |
| searchResults = tracks; | |
| resultsContainer.innerHTML = tracks.map((track, index) => ` | |
| <div class="track-card glass p-4" data-aos="fade-up"> | |
| <div class="relative mb-4 group"> | |
| <img | |
| src="${track.artwork?.['150x150'] || ''}" | |
| alt="${track.title}" | |
| class="w-full aspect-square object-cover rounded-lg" | |
| loading="lazy" | |
| > | |
| <button | |
| class="play-button absolute inset-0 w-full h-full flex items-center justify-center bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity rounded-lg" | |
| data-index="${index}" | |
| > | |
| <div class="w-12 h-12 rounded-full bg-blue-500/90 flex items-center justify-center"> | |
| <i data-feather="play" class="w-6 h-6 ml-1 text-white"></i> | |
| </div> | |
| </button> | |
| </div> | |
| <h3 class="font-semibold truncate mb-1">${track.title}</h3> | |
| <p class="text-sm text-gray-400 truncate">by ${track.user.handle}</p> | |
| <div class="flex items-center justify-between mt-3"> | |
| <span class="text-xs text-gray-500">${formatTime(track.duration)}</span> | |
| <button class="p-2 rounded-full hover:bg-white/10"> | |
| <i data-feather="heart" class="w-4 h-4"></i> | |
| </button> | |
| </div> | |
| </div> | |
| `).join(''); | |
| feather.replace(); | |
| // Add event listeners to play buttons | |
| document.querySelectorAll('.play-button').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| const index = parseInt(btn.dataset.index); | |
| playTrack(tracks[index], index); | |
| }); | |
| }); | |
| } | |
| // Event listeners | |
| let searchTimeout; | |
| searchInput.addEventListener('input', (e) => { | |
| const query = e.target.value.trim(); | |
| clearTimeout(searchTimeout); | |
| if (query.length < 2) { | |
| resultsContainer.innerHTML = ''; | |
| emptyState.classList.remove('hidden'); | |
| return; | |
| } | |
| searchTimeout = setTimeout(async () => { | |
| const tracks = await searchTracks(query); | |
| renderResults(tracks); | |
| }, 250); | |
| }); | |
| // Keyboard shortcuts | |
| document.addEventListener('keydown', (e) => { | |
| // Cmd/Ctrl+K to focus search | |
| if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k') { | |
| e.preventDefault(); | |
| searchInput.focus(); | |
| } | |
| // Spacebar to play/pause | |
| if (e.code === 'Space' && e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA') { | |
| e.preventDefault(); | |
| if (isPlaying) { | |
| player.pause(); | |
| } else if (player.src) { | |
| player.play(); | |
| } | |
| } | |
| // Arrow keys for search navigation (future enhancement) | |
| }); | |
| // Player controls | |
| playPauseBtn.addEventListener('click', () => { | |
| if (player.src) { | |
| if (isPlaying) { | |
| player.pause(); | |
| } else { | |
| player.play(); | |
| } | |
| } else if (searchResults.length > 0) { | |
| playTrack(searchResults[0], 0); | |
| } | |
| }); | |
| prevBtn.addEventListener('click', () => { | |
| if (searchResults.length > 0) { | |
| const newIndex = (currentTrackIndex - 1 + searchResults.length) % searchResults.length; | |
| playTrack(searchResults[newIndex], newIndex); | |
| } | |
| }); | |
| nextBtn.addEventListener('click', () => { | |
| if (searchResults.length > 0) { | |
| const newIndex = (currentTrackIndex + 1) % searchResults.length; | |
| playTrack(searchResults[newIndex], newIndex); | |
| } | |
| }); | |
| // Player events | |
| player.addEventListener('play', () => { | |
| isPlaying = true; | |
| playIcon.setAttribute('data-feather', 'pause'); | |
| feather.replace(); | |
| }); | |
| player.addEventListener('pause', () => { | |
| isPlaying = false; | |
| playIcon.setAttribute('data-feather', 'play'); | |
| feather.replace(); | |
| }); | |
| player.addEventListener('timeupdate', () => { | |
| if (player.duration) { | |
| const percent = (player.currentTime / player.duration) * 100; | |
| progressBar.value = percent; | |
| currentTimeEl.textContent = formatTime(player.currentTime); | |
| durationEl.textContent = formatTime(player.duration); | |
| } | |
| }); | |
| player.addEventListener('ended', () => { | |
| // Auto-play next track | |
| if (searchResults.length > 0) { | |
| const newIndex = (currentTrackIndex + 1) % searchResults.length; | |
| playTrack(searchResults[newIndex], newIndex); | |
| } | |
| }); | |
| progressBar.addEventListener('input', (e) => { | |
| if (player.duration) { | |
| player.currentTime = (e.target.value / 100) * player.duration; | |
| } | |
| }); | |
| // Initialize | |
| feather.replace(); | |
| </script> | |
| </body> | |
| </html> | |