Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Mr.FLENs Music Finder</title> | |
| <link rel="icon" type="image/x-icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🎧</text></svg>"> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script> | |
| tailwind.config = { | |
| theme: { | |
| extend: { | |
| colors: { | |
| primary: '#5CF3FF', | |
| secondary: '#9B5CFF', | |
| bgDark: '#05060A', | |
| glass: 'rgba(255, 255, 255, 0.06)', | |
| textPrimary: '#EAF4FF', | |
| textMuted: '#8A96B3' | |
| }, | |
| animation: { | |
| 'wave': 'wave 1.5s linear infinite', | |
| 'slide': 'slide 15s linear infinite' | |
| }, | |
| keyframes: { | |
| wave: { | |
| '0%': { 'transform': 'scale(1,0.8)' }, | |
| '50%': { 'transform': 'scale(1,1.2)' }, | |
| '100%': { 'transform': 'scale(1,0.8)' } | |
| }, | |
| slide: { | |
| '0%': { 'transform': 'translateX(0)' }, | |
| '100%': { 'transform': 'translateX(-50%)' } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| </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://unpkg.com/feather-icons"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/animejs@3.2.1/lib/anime.min.js"></script> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); | |
| @import url('https://fonts.googleapis.com/css2?family=Satoshi:wght@300;400;500;700&display=swap'); | |
| :root { | |
| --primary-neon: #5CF3FF; | |
| --secondary-accent: #9B5CFF; | |
| --background: #05060A; | |
| --glass: rgba(255, 255, 255, 0.06); | |
| --text-primary: #EAF4FF; | |
| --text-muted: #8A96B3; | |
| } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| margin: 0; | |
| background: | |
| radial-gradient(1200px 600px at 20% -10%, var(--secondary-accent)22, transparent 60%), | |
| var(--background); | |
| color: var(--text-primary); | |
| min-height: 100vh; | |
| overflow-x: hidden; | |
| line-height: 1.5; | |
| } | |
| .glass { | |
| background: var(--glass); | |
| backdrop-filter: saturate(1.4) blur(12px); | |
| -webkit-backdrop-filter: blur(12px); | |
| border: 1px solid rgba(255, 255, 255, 0.08); | |
| } | |
| .text-shadow { | |
| text-shadow: 0 0 8px rgba(92, 243, 255, 0.5); | |
| } | |
| .soundwave { | |
| animation: wave 1.5s infinite; | |
| animation-timing-function: cubic-bezier(0.3,0,0.7,1); | |
| } | |
| .marquee { | |
| white-space: nowrap; | |
| display: inline-block; | |
| animation: slide 15s linear infinite; | |
| } | |
| .track-card:hover .card-actions { | |
| transform: translateY(0); | |
| opacity: 1; | |
| } | |
| .glow-hover:hover { | |
| box-shadow: 0 0 15px rgba(155, 92, 255, 0.7); | |
| } | |
| #search { | |
| width: 100%; | |
| padding: 12px 14px; | |
| border-radius: 12px; | |
| border: 1px solid #ffffff22; | |
| background: rgba(0, 0, 0, 0.4); | |
| color: var(--text-primary); | |
| outline: none; | |
| transition: box-shadow 0.2s; | |
| } | |
| #search:focus { | |
| box-shadow: 0 0 0 2px var(--primary-neon); | |
| } | |
| ::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background-color: rgba(155, 92, 255, 0.5); | |
| border-radius: 4px; | |
| } | |
| </style> | |
| </head> | |
| <body class="relative"> | |
| <!-- App Navigation --> | |
| <nav class="glass fixed top-0 left-0 right-0 z-50 p-4"> | |
| <div class="container mx-auto flex items-center justify-between"> | |
| <div class="flex items-center space-x-2"> | |
| <div class="w-10 h-10 rounded-full bg-gradient-to-br from-primary to-secondary flex items-center justify-center"> | |
| <span class="font-bold">MF</span> | |
| </div> | |
| <h1 class="text-xl font-bold font-satoshi text-shadow">Mr.FLEN<span class="text-primary">s</span></h1> | |
| </div> | |
| <div class="hidden md:flex space-x-6"> | |
| <a href="#" class="nav-link font-medium text-primary border-b border-primary py-1" data-page="home">Home</a> | |
| <a href="#" class="nav-link font-medium text-textMuted hover:text-textPrimary transition-colors" data-page="search">Search</a> | |
| <a href="#" class="nav-link font-medium text-textMuted hover:text-textPrimary transition-colors" data-page="library">Library</a> | |
| <a href="#" class="nav-link font-medium text-textMuted hover:text-textPrimary transition-colors" data-page="analytics">Analytics</a> | |
| </div> | |
| <div class="flex items-center space-x-4"> | |
| <button class="search-trigger bg-glass rounded-full w-10 h-10 flex items-center justify-center glow-hover"> | |
| <i data-feather="search" class="text-textMuted"></i> | |
| </button> | |
| <div class="w-10 h-10 rounded-full bg-gradient-to-br from-glass to-glass flex items-center justify-center overflow-hidden"> | |
| <i data-feather="user" class="text-textPrimary"></i> | |
| </div> | |
| </div> | |
| </div> | |
| </nav> | |
| <!-- Main Content Sections --> | |
| <main class="pt-24 pb-24 md:pb-32 px-4 container mx-auto"> | |
| <!-- Home Section --> | |
| <section id="home" class="page-section active"> | |
| <!-- Featured Banner --> | |
| <div class="glass rounded-2xl p-6 md:p-8 overflow-hidden relative mb-10" data-aos="fade-up"> | |
| <div class="absolute inset-0 bg-gradient-to-r from-[#05060A]/100 to-[#5CF3FF]/10 z-0"></div> | |
| <div class="relative z-10"> | |
| <span class="text-xs text-primary font-medium bg-[#05060A] bg-opacity-50 rounded-full px-3 py-1 inline-block mb-3">NEW RELEASE</span> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-8"> | |
| <div> | |
| <h2 class="text-3xl md:text-4xl font-bold font-satoshi mb-2">Sonic Horizons</h2> | |
| <p class="text-textMuted mb-4 italic">Deep UKG vibes with cinematic synth waves</p> | |
| <p class="text-sm text-textMuted mb-6 max-w-lg">Fresh DnB-infused UK Garage track with experimental low-end bass structures. Featuring modular synth landscapes recorded live at FLEN Studio.</p> | |
| <div class="flex space-x-4"> | |
| <button class="bg-primary text-bgDark px-6 py-2 rounded-full font-medium flex items-center"> | |
| <i data-feather="play" class="mr-2"></i> Play Now | |
| </button> | |
| <button class="glass px-4 py-2 rounded-full flex items-center"> | |
| <i data-feather="plus" class="mr-2 text-textMuted"></i> Queue | |
| </button> | |
| </div> | |
| </div> | |
| <div class="flex items-center justify-center"> | |
| <div class="relative"> | |
| <div class="absolute inset-0 rounded-xl bg-gradient-to-br from-primary to-secondary opacity-30 blur-lg"></div> | |
| <img src="data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300' viewBox='0 0 300 300'%3E%3Crect fill='%2305060A' width='300' height='300'/%3E%3Cpath fill='%235CF3FF' d='M150,75l33.6,68.4H75l59.4,38.4L90.6,250L150,200l59.4,50-43.8-68.4L225,143.4h-41.4L150,75z'/%3E%3C/svg%3E" | |
| alt="Album cover" class="rounded-xl w-full max-w-xs relative z-10 shadow-xl"> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Trending Section --> | |
| <div class="mb-14" data-aos="fade-up"> | |
| <div class="flex justify-between items-center mb-6"> | |
| <h3 class="text-2xl font-bold font-satoshi">Trending Now</h3> | |
| <div class="flex space-x-2"> | |
| <button class="glass rounded-full p-2"> | |
| <i data-feather="chevron-left"></i> | |
| </button> | |
| <button class="glass rounded-full p-2"> | |
| <i data-feather="chevron-right"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4"> | |
| <!-- Trending Item 1 --> | |
| <div class="glass rounded-xl overflow-hidden group relative track-card"> | |
| <img src="data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200' viewBox='0 0 200 200'%3E%3Crect fill='%2305060A' width='200' height='200'/%3E%3Ccircle fill='%239B5CFF' cx='100' cy='100' r='60'/%3E%3Ccircle fill='%235CF3FF' cx='100' cy='100' r='30'/%3E%3C/svg%3E" | |
| alt="Track cover" class="w-full aspect-square object-cover"> | |
| <div class="p-4"> | |
| <h4 class="font-bold truncate">Radiant Echoes</h4> | |
| <p class="text-xs text-textMuted truncate">@chaseandstatus</p> | |
| </div> | |
| <div class="card-actions absolute inset-x-0 bottom-0 p-4 bg-gradient-to-t from-black/80 to-transparent transform translate-y-4 opacity-0 transition-all duration-300"> | |
| <div class="flex justify-between"> | |
| <button class="w-10 h-10 rounded-full bg-primary flex items-center justify-center"> | |
| <i data-feather="play" class="text-bgDark"></i> | |
| </button> | |
| <button class="w-8 h-8 rounded-full bg-glass flex items-center justify-center"> | |
| <i data-feather="heart" class="text-textMuted"></i> | |
| </button> | |
| <button class="w-8 h-8 rounded-full bg-glass flex items-center justify-center"> | |
| <i data-feather="plus" class="text-textMuted"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Trending Item 2-5 (truncated for brevity) --> | |
| </div> | |
| </div> | |
| <!-- Genre Filters --> | |
| <div class="mb-8"> | |
| <h3 class="text-2xl font-bold font-satoshi mb-4">Explore Genres</h3> | |
| <div class="flex flex-wrap gap-2" data-aos="fade-up"> | |
| <button class="px-4 py-2 rounded-full glass">All</button> | |
| <button class="px-4 py-2 rounded-full glass bg-gradient-to-r from-glass to-glass text-primary border border-primary">UK Garage</button> | |
| <button class="px-4 py-2 rounded-full glass">Grime</button> | |
| <button class="px-4 py-2 rounded-full glass">Drum & Bass</button> | |
| <button class="px-4 py-2 rounded-full glass">House</button> | |
| <button class="px-4 py-2 rounded-full glass">Bassline</button> | |
| <button class="px-4 py-2 rounded-full glass">Breakbeat</button> | |
| </div> | |
| </div> | |
| <!-- Infinite Grid --> | |
| <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"> | |
| <!-- Track Card 1 --> | |
| <div class="glass rounded-2xl p-4 relative track-card" data-aos="fade-up"> | |
| <div class="flex"> | |
| <div class="flex-shrink-0 w-16 h-16 bg-gradient-to-br from-primary to-secondary rounded-xl overflow-hidden"> | |
| <img src="data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='80' height='80' viewBox='0 0 80 80'%3E%3Crect fill='%2305060A' width='80' height='80'/%3E%3Cellipse fill='%239B5CFF' cx='40' cy='40' rx='20' ry='30'/%3E%3C/svg%3E" | |
| alt="Track cover" class="w-full h-full object-cover"> | |
| </div> | |
| <div class="ml-4 flex-1 min-w-0"> | |
| <h4 class="font-bold truncate">Midnight Drive</h4> | |
| <p class="text-sm text-textMuted truncate">Mr.FLEN</p> | |
| <div class="flex items-center mt-1"> | |
| <span class="text-xs text-textMuted">134k plays • 3:48</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card-actions flex gap-2 mt-4 transform translate-y-4 opacity-0 transition-all duration-300"> | |
| <button class="flex-1 glass py-2 rounded-lg flex items-center justify-center"> | |
| <i data-feather="play" class="mr-2 text-primary"></i> Play | |
| </button> | |
| <button class="w-10 h-10 rounded-lg bg-glass flex items-center justify-center"> | |
| <i data-feather="heart" class="text-textMuted"></i> | |
| </button> | |
| <button class="w-10 h-10 rounded-lg bg-glass flex items-center justify-center"> | |
| <i data-feather="plus" class="text-textMuted"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Additional track cards --> | |
| </div> | |
| </section> | |
| <!-- Search Section (initially hidden) --> | |
| <section id="search" class="page-section hidden"> | |
| <div class="mb-8"> | |
| <div class="relative max-w-2xl mx-auto"> | |
| <input type="text" id="search-main" placeholder="Search Mr.FLEN tracks..." class="w-full glass rounded-full px-6 py-4 pl-12 focus:outline-none focus:ring-2 focus:ring-primary"> | |
| <i data-feather="search" class="absolute left-4 top-1/2 transform -translate-y-1/2 text-textMuted"></i> | |
| </div> | |
| </div> | |
| <!-- Search Tabs --> | |
| <div class="flex border-b border-glass mb-6"> | |
| <button class="search-tab active px-4 py-2 font-medium text-primary border-b-2 border-primary" data-tab="tracks">Tracks</button> | |
| <button class="search-tab px-4 py-2 font-medium text-textMuted hover:text-textPrimary" data-tab="artists">Artists</button> | |
| <button class="search-tab px-4 py-2 font-medium text-textMuted hover:text-textPrimary" data-tab="playlists">Playlists</button> | |
| <button class="search-tab px-4 py-2 font-medium text-textMuted hover:text-textPrimary" data-tab="albums">Albums</button> | |
| </div> | |
| <!-- Search Results --> | |
| <div id="search-results-container"> | |
| <div id="tracks-results" class="search-results-tab active"> | |
| <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"> | |
| <!-- Results will be populated here --> | |
| </div> | |
| </div> | |
| <div id="artists-results" class="search-results-tab hidden"> | |
| <div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4"> | |
| <!-- Artist results will be populated here --> | |
| </div> | |
| </div> | |
| <div id="playlists-results" class="search-results-tab hidden"> | |
| <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"> | |
| <!-- Playlist results will be populated here --> | |
| </div> | |
| </div> | |
| <div id="albums-results" class="search-results-tab hidden"> | |
| <div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4"> | |
| <!-- Album results will be populated here --> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Library Section (initially hidden) --> | |
| <section id="library" class="page-section hidden"> | |
| <!-- Library section content --> | |
| </section> | |
| <!-- Analytics Section (initially hidden) --> | |
| <section id="analytics" class="page-section hidden"> | |
| <!-- Analytics section content --> | |
| </section> | |
| </main> | |
| <!-- Search Modal --> | |
| <div class="fixed inset-0 z-50 hidden" id="search-modal"> | |
| <div class="absolute inset-0 bg-bgDark bg-opacity-90"></div> | |
| <div class="relative z-10 max-w-2xl mx-auto mt-20"> | |
| <div class="glass rounded-xl overflow-hidden shadow-2xl"> | |
| <div class="flex items-center px-6 py-4 border-b border-glass"> | |
| <i data-feather="search" class="text-textMuted mr-3"></i> | |
| <input id="search-input" type="text" placeholder="Search Mr.FLEN tracks..." class="w-full focus:outline-none text-textPrimary placeholder:text-textMuted bg-transparent"> | |
| <span class="bg-glass text-xs text-textMuted px-2 py-1 rounded">⌘K</span> | |
| </div> | |
| <div class="flex justify-between items-center px-4 py-2 border-b border-glass"> | |
| <div class="flex items-center"> | |
| <span class="text-xs text-textMuted mr-2">Results per page:</span> | |
| <select id="results-per-page" class="bg-glass text-textPrimary text-xs rounded px-2 py-1"> | |
| <option value="20">20</option> | |
| <option value="50">50</option> | |
| <option value="100">100</option> | |
| </select> | |
| </div> | |
| <button id="next-page" class="text-xs text-primary bg-glass px-3 py-1 rounded disabled:opacity-50" disabled>Next Page</button> | |
| </div> | |
| <div id="search-results" class="max-h-[60vh] overflow-y-auto"> | |
| <!-- Search results will populate here --> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Player Bar --> | |
| <div class="fixed bottom-0 left-0 right-0 glass z-40 border-t border-glass pt-4 pb-4 px-4 backdrop-blur-lg"> | |
| <div class="container mx-auto"> | |
| <div class="flex items-center"> | |
| <div class="flex items-center w-1/3"> | |
| <div class="w-12 h-12 rounded-lg overflow-hidden flex-shrink-0 mr-4"> | |
| <img src="data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='80' height='80' viewBox='0 0 80 80'%3E%3Crect fill='%2305060A' width='80' height='80'/%3E%3Cpath fill='%235CF3FF' d='M20,20l40,40M60,20L20,60' stroke='%235CF3FF' stroke-width='8'/%3E%3C/svg%3E" | |
| alt="Now playing" class="w-full h-full object-cover"> | |
| </div> | |
| <div class="min-w-0"> | |
| <div class="overflow-hidden w-40"> | |
| <div class="marquee"> | |
| <span class="font-bold mr-20">Sonic Horizons - Extended Mix</span> | |
| </div> | |
| </div> | |
| <p class="text-xs text-textMuted truncate">Mr.FLEN</p> | |
| </div> | |
| <button class="ml-4 text-textMuted hover:text-textPrimary"> | |
| <i data-feather="heart" class="w-5 h-5"></i> | |
| </button> | |
| </div> | |
| <div class="flex-1 flex flex-col items-center"> | |
| <div class="flex items-center space-x-6 mb-2"> | |
| <button class="text-textMuted hover:text-primary"> | |
| <i data-feather="shuffle" class="w-5 h-5"></i> | |
| </button> | |
| <button class="text-textMuted hover:text-primary"> | |
| <i data-feather="skip-back" class="w-5 h-5"></i> | |
| </button> | |
| <button class="w-12 h-12 rounded-full bg-primary flex items-center justify-center text-bgDark"> | |
| <i data-feather="play" class="w-6 h-6"></i> | |
| </button> | |
| <button class="text-textMuted hover:text-primary"> | |
| <i data-feather="skip-forward" class="w-5 h-5"></i> | |
| </button> | |
| <button class="text-textMuted hover:text-primary"> | |
| <i data-feather="repeat" class="w-5 h-5"></i> | |
| </button> | |
| </div> | |
| <div class="w-full max-w-md flex items-center"> | |
| <span class="text-xs text-textMuted mr-2">1:24</span> | |
| <div class="h-1.5 flex-1 bg-glass rounded-full overflow-hidden"> | |
| <div class="h-full bg-gradient-to-r from-primary to-secondary rounded-full w-1/3"></div> | |
| </div> | |
| <span class="text-xs text-textMuted ml-2">3:56</span> | |
| </div> | |
| </div> | |
| <div class="w-1/3 flex justify-end items-center space-x-4"> | |
| <button class="text-textMuted hover:text-primary"> | |
| <i data-feather="list" class="w-5 h-5"></i> | |
| </button> | |
| <button id="visualizer-toggle" class="w-8 h-8 rounded-full bg-glass flex items-center justify-center"> | |
| <svg width="24" height="24" viewBox="0 0 24 24" class="soundwave"> | |
| <path stroke="currentColor" stroke-width="2" stroke-linecap="round" d="M3,10 L3,14 M7,7 L7,17 M11,4 L11,20 M15,7 L15,17 M19,10 L19,14" /> | |
| </svg> | |
| </button> | |
| <div class="w-24 bg-glass h-1 rounded-full overflow-hidden"> | |
| <div class="h-full bg-gradient-to-r from-primary to-secondary w-2/3"></div> | |
| </div> | |
| <button class="text-textMuted hover:text-primary"> | |
| <i data-feather="volume-2" class="w-5 h-5"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Queue Drawer --> | |
| <div class="fixed inset-y-0 right-0 w-full md:w-96 glass z-50 transform translate-x-full transition-transform" id="queue-drawer"> | |
| <div class="h-full flex flex-col"> | |
| <div class="p-5 border-b border-glass flex justify-between items-center"> | |
| <h3 class="text-lg font-bold font-satoshi">Playing Queue</h3> | |
| <div class="flex space-x-2"> | |
| <button class="p-2 glass rounded-lg text-textMuted hover:text-textPrimary"> | |
| <i data-feather="save" class="w-4 h-4"></i> | |
| </button> | |
| <button id="close-queue" class="p-2 glass rounded-lg text-textMuted hover:text-textPrimary"> | |
| <i data-feather="x" class="w-4 h-4"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="flex-1 overflow-y-auto p-4 space-y-4"> | |
| <!-- Queue items would populate here --> | |
| </div> | |
| <div class="p-4 border-t border-glass"> | |
| <button class="w-full glass py-3 rounded-lg flex items-center justify-center"> | |
| <i data-feather="save" class="mr-2"></i> Save Queue as Playlist | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Audius API Configuration | |
| const AUDIUS_API_KEY = 'e922e6edcae9856000bf6814a1ee5745bfb57734'; | |
| const ARTIST_HANDLE = 'Mr.FLEN'; | |
| let currentArtistId = null; | |
| // Initialize libraries | |
| document.addEventListener('DOMContentLoaded', function() { | |
| feather.replace(); | |
| AOS.init({ duration: 600 }); | |
| // First get the artist ID | |
| fetchArtistId(); | |
| // Navigation logic | |
| const navLinks = document.querySelectorAll('.nav-link'); | |
| const pageSections = document.querySelectorAll('.page-section'); | |
| navLinks.forEach(link => { | |
| link.addEventListener('click', function(e) { | |
| e.preventDefault(); | |
| const targetPage = this.dataset.page; | |
| // Update nav state | |
| navLinks.forEach(link => link.classList.remove('text-primary', 'border-b')); | |
| this.classList.add('text-primary', 'border-b'); | |
| // Show target section | |
| pageSections.forEach(section => { | |
| section.classList.add('hidden'); | |
| section.classList.remove('active'); | |
| if(section.id === targetPage) { | |
| section.classList.remove('hidden'); | |
| section.classList.add('active'); | |
| } | |
| }); | |
| // If going to search page, focus the search input | |
| if (targetPage === 'search') { | |
| setTimeout(() => { | |
| document.getElementById('search-main').focus(); | |
| }, 100); | |
| } | |
| }); | |
| }); | |
| // Search modal toggle | |
| const searchTrigger = document.querySelector('.search-trigger'); | |
| const searchModal = document.getElementById('search-modal'); | |
| const searchInput = document.querySelector('#search-modal input'); | |
| searchTrigger.addEventListener('click', () => { | |
| searchModal.classList.toggle('hidden'); | |
| if (!searchModal.classList.contains('hidden')) { | |
| searchInput.focus(); | |
| } | |
| }); | |
| // Search input handler | |
| document.getElementById('search-input').addEventListener('input', debounce(handleSearch, 300)); | |
| // Main search input handler | |
| document.getElementById('search-main').addEventListener('input', debounce(handleMainSearch, 300)); | |
| // Search tabs | |
| const searchTabs = document.querySelectorAll('.search-tab'); | |
| searchTabs.forEach(tab => { | |
| tab.addEventListener('click', function() { | |
| const tabType = this.dataset.tab; | |
| // Update active tab | |
| searchTabs.forEach(t => { | |
| t.classList.remove('text-primary', 'border-primary'); | |
| t.classList.add('text-textMuted'); | |
| }); | |
| this.classList.remove('text-textMuted'); | |
| this.classList.add('text-primary', 'border-primary'); | |
| // Show active results tab | |
| document.querySelectorAll('.search-results-tab').forEach(tab => { | |
| tab.classList.add('hidden'); | |
| tab.classList.remove('active'); | |
| }); | |
| document.getElementById(`${tabType}-results`).classList.remove('hidden'); | |
| document.getElementById(`${tabType}-results`).classList.add('active'); | |
| }); | |
| }); | |
| // Close modal when clicking outside | |
| searchModal.addEventListener('click', (e) => { | |
| if (e.target === searchModal) { | |
| searchModal.classList.add('hidden'); | |
| } | |
| }); | |
| // Queue drawer toggle | |
| const queueBtn = document.querySelector('[data-feather="list"]').closest('button'); | |
| const closeQueue = document.getElementById('close-queue'); | |
| const queueDrawer = document.getElementById('queue-drawer'); | |
| const queueContainer = document.querySelector('#queue-drawer > div > div:last-child'); | |
| queueBtn.addEventListener('click', () => { | |
| queueDrawer.style.transform = 'translateX(0)'; | |
| }); | |
| closeQueue.addEventListener('click', () => { | |
| queueDrawer.style.transform = 'translateX(100%)'; | |
| }); | |
| // Visualizer toggle effect | |
| const vizToggle = document.getElementById('visualizer-toggle'); | |
| vizToggle.addEventListener('click', function() { | |
| const wave = document.querySelector('.soundwave'); | |
| wave.classList.toggle('soundwave'); | |
| setTimeout(() => { | |
| wave.classList.toggle('soundwave'); | |
| }, 10); | |
| }); | |
| // Utility functions | |
| function debounce(func, wait) { | |
| let timeout; | |
| return function(...args) { | |
| clearTimeout(timeout); | |
| timeout = setTimeout(() => func.apply(this, args), wait); | |
| }; | |
| } | |
| async function fetchArtistId() { | |
| try { | |
| const response = await fetch(`https://audius-discovery-5.cultur3stake.com/v1/users/search?query=${encodeURIComponent(ARTIST_HANDLE)}`); | |
| if (!response.ok) { | |
| throw new Error(`HTTP error! status: ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| if (data.data && data.data.length > 0) { | |
| currentArtistId = data.data[0].id; | |
| loadArtistTracks(); | |
| } | |
| } catch (error) { | |
| console.error('Error fetching artist ID:', error); | |
| } | |
| } | |
| async function loadArtistTracks() { | |
| if (!currentArtistId) return; | |
| try { | |
| const response = await fetch(`https://audius-discovery-5.cultur3stake.com/v1/users/${currentArtistId}/tracks`); | |
| if (!response.ok) { | |
| throw new Error(`HTTP error! status: ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| if (data.data) { | |
| renderTracks(data.data, '#home .grid'); | |
| } | |
| } catch (error) { | |
| console.error('Error fetching artist tracks:', error); | |
| } | |
| } | |
| let currentPage = 1; | |
| let currentQuery = ''; | |
| let resultsPerPage = 20; | |
| let totalResults = 0; | |
| document.getElementById('results-per-page').addEventListener('change', function() { | |
| resultsPerPage = parseInt(this.value); | |
| currentPage = 1; | |
| if (currentQuery) { | |
| handleSearch({target: {value: currentQuery}}); | |
| } | |
| }); | |
| document.getElementById('next-page').addEventListener('click', function() { | |
| currentPage++; | |
| handleSearch({target: {value: currentQuery}}); | |
| }); | |
| async function handleSearch(e) { | |
| const query = e.target.value.trim(); | |
| currentQuery = query; | |
| if (!query) { | |
| document.getElementById('search-results').innerHTML = | |
| '<p class="p-6 text-textMuted text-center">Search for Mr.FLEN tracks</p>'; | |
| document.getElementById('next-page').disabled = true; | |
| return; | |
| } | |
| try { | |
| const offset = (currentPage - 1) * resultsPerPage; | |
| // Use a different API endpoint that doesn't require authentication | |
| const response = await fetch(`https://audius-discovery-5.cultur3stake.com/v1/tracks/search?query=${encodeURIComponent(query)}&limit=${resultsPerPage}&offset=${offset}`); | |
| if (!response.ok) { | |
| throw new Error(`HTTP error! status: ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| const resultsContainer = document.getElementById('search-results'); | |
| if (currentPage === 1) { | |
| resultsContainer.innerHTML = ''; | |
| } | |
| if (data.data && data.data.length > 0) { | |
| renderTracks(data.data, resultsContainer, true); | |
| document.getElementById('next-page').disabled = data.data.length < resultsPerPage; | |
| if (data.data.length < resultsPerPage) { | |
| const endMessage = document.createElement('div'); | |
| endMessage.className = 'p-4 text-center text-textMuted border-t border-glass'; | |
| endMessage.textContent = 'There are no more results'; | |
| resultsContainer.appendChild(endMessage); | |
| } | |
| } else if (currentPage === 1) { | |
| resultsContainer.innerHTML = '<p class="p-6 text-textMuted text-center">No tracks found</p>'; | |
| document.getElementById('next-page').disabled = true; | |
| } | |
| } catch (error) { | |
| console.error('Search error:', error); | |
| document.getElementById('search-results').innerHTML = | |
| '<p class="p-6 text-textMuted text-center">Error searching tracks. Please try again.</p>'; | |
| document.getElementById('next-page').disabled = true; | |
| } | |
| } | |
| async function handleMainSearch(e) { | |
| const query = e.target.value.trim(); | |
| if (!query) { | |
| // Clear all results | |
| document.querySelectorAll('.search-results-tab .grid').forEach(grid => { | |
| grid.innerHTML = ''; | |
| }); | |
| return; | |
| } | |
| try { | |
| // Search tracks | |
| const tracksResponse = await fetch(`https://audius-discovery-5.cultur3stake.com/v1/tracks/search?query=${encodeURIComponent(query)}&limit=12`); | |
| const tracksData = await tracksResponse.json(); | |
| // Search artists | |
| const artistsResponse = await fetch(`https://audius-discovery-5.cultur3stake.com/v1/users/search?query=${encodeURIComponent(query)}&limit=12`); | |
| const artistsData = await artistsResponse.json(); | |
| // Search playlists | |
| const playlistsResponse = await fetch(`https://audius-discovery-5.cultur3stake.com/v1/playlists/search?query=${encodeURIComponent(query)}&limit=12`); | |
| const playlistsData = await playlistsResponse.json(); | |
| // Render results | |
| if (tracksData.data) { | |
| renderTracks(tracksData.data, '#tracks-results .grid', true); | |
| } | |
| if (artistsData.data) { | |
| renderArtists(artistsData.data, '#artists-results .grid'); | |
| } | |
| if (playlistsData.data) { | |
| renderPlaylists(playlistsData.data, '#playlists-results .grid'); | |
| } | |
| } catch (error) { | |
| console.error('Search error:', error); | |
| } | |
| } | |
| function renderArtists(artists, containerSelector) { | |
| const container = document.querySelector(containerSelector); | |
| if (!container) return; | |
| container.innerHTML = ''; | |
| artists.forEach(artist => { | |
| const artistElement = document.createElement('div'); | |
| artistElement.className = 'glass rounded-xl overflow-hidden group relative track-card'; | |
| artistElement.innerHTML = ` | |
| <div class="aspect-square bg-gradient-to-br from-primary to-secondary flex items-center justify-center"> | |
| <span class="text-4xl font-bold">${artist.name.charAt(0)}</span> | |
| </div> | |
| <div class="p-4"> | |
| <h4 class="font-bold truncate">${artist.name}</h4> | |
| <p class="text-xs text-textMuted truncate">${artist.handle}</p> | |
| <p class="text-xs text-textMuted mt-1">${artist.track_count || 0} tracks</p> | |
| </div> | |
| <div class="card-actions absolute inset-x-0 bottom-0 p-4 bg-gradient-to-t from-black/80 to-transparent transform translate-y-4 opacity-0 transition-all duration-300"> | |
| <button class="w-full glass py-2 rounded-lg flex items-center justify-center"> | |
| <i data-feather="user-plus" class="mr-2"></i> Follow | |
| </button> | |
| </div> | |
| `; | |
| container.appendChild(artistElement); | |
| }); | |
| feather.replace(); | |
| } | |
| function renderPlaylists(playlists, containerSelector) { | |
| const container = document.querySelector(containerSelector); | |
| if (!container) return; | |
| container.innerHTML = ''; | |
| playlists.forEach(playlist => { | |
| const playlistElement = document.createElement('div'); | |
| playlistElement.className = 'glass rounded-2xl p-4 relative track-card'; | |
| playlistElement.innerHTML = ` | |
| <div class="flex"> | |
| <div class="flex-shrink-0 w-16 h-16 bg-gradient-to-br from-primary to-secondary rounded-xl overflow-hidden"> | |
| <img src="${playlist.artwork?.url || 'data:image/svg+xml;charset=UTF-8,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'80\' height=\'80\' viewBox=\'0 0 80 80\'%3E%3Crect fill=\'%2305060A\' width=\'80\' height=\'80\'/%3E%3Cpath fill=\'%235CF3FF\' d=\'M20,20l40,40M60,20L20,60\' stroke=\'%235CF3FF\' stroke-width=\'8\'/%3E%3C/svg%3E'}" | |
| alt="Playlist cover" class="w-full h-full object-cover"> | |
| </div> | |
| <div class="ml-4 flex-1 min-w-0"> | |
| <h4 class="font-bold truncate">${playlist.name}</h4> | |
| <p class="text-sm text-textMuted truncate">${playlist.user?.name || 'Unknown Artist'}</p> | |
| <div class="flex items-center mt-1"> | |
| <span class="text-xs text-textMuted">${playlist.track_count || 0} tracks</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card-actions flex gap-2 mt-4 transform translate-y-4 opacity-0 transition-all duration-300"> | |
| <button class="flex-1 glass py-2 rounded-lg flex items-center justify-center"> | |
| <i data-feather="play" class="mr-2 text-primary"></i> Play | |
| </button> | |
| <button class="w-10 h-10 rounded-lg bg-glass flex items-center justify-center"> | |
| <i data-feather="plus" class="text-textMuted"></i> | |
| </button> | |
| </div> | |
| `; | |
| container.appendChild(playlistElement); | |
| }); | |
| feather.replace(); | |
| } | |
| function renderTracks(tracks, containerSelector, isSearchResult = false, isMrFLEN = false) { | |
| // Reset page if it's a new search | |
| if (currentPage === 1 && isSearchResult) { | |
| const container = typeof containerSelector === 'string' | |
| ? document.querySelector(containerSelector) | |
| : containerSelector; | |
| container.innerHTML = ''; | |
| } | |
| // Format duration from milliseconds to MM:SS | |
| function formatDuration(ms) { | |
| const minutes = Math.floor(ms / 60000); | |
| const seconds = ((ms % 60000) / 1000).toFixed(0); | |
| return `${minutes}:${seconds.padStart(2, '0')}`; | |
| } | |
| const container = typeof containerSelector === 'string' | |
| ? document.querySelector(containerSelector) | |
| : containerSelector; | |
| if (!container) return; | |
| if (!isSearchResult) { | |
| container.innerHTML = ''; | |
| } | |
| tracks.forEach(track => { | |
| const duration = track.duration ? new Date(track.duration * 1000).toISOString().substr(14, 5) : '0:00'; | |
| const playCount = track.play_count ? `${Math.floor(track.play_count / 1000)}k` : '0'; | |
| const trackElement = document.createElement('div'); | |
| trackElement.className = isSearchResult | |
| ? 'p-4 hover:bg-glass cursor-pointer flex items-center border-b border-glass' | |
| : 'glass rounded-2xl p-4 relative track-card'; | |
| // Highlight Mr.FLEN tracks | |
| const isArtistMatch = track.user && track.user.handle && | |
| track.user.handle.toLowerCase() === 'mrflen'; | |
| trackElement.innerHTML = ` | |
| ${isSearchResult && isArtistMatch ? ` | |
| <div class="absolute left-0 top-0 bottom-0 w-1 bg-primary rounded-l"></div> | |
| ` : ''} | |
| <div class="flex ${isSearchResult ? 'items-center' : ''}"> | |
| <div class="flex-shrink-0 w-16 h-16 ${isSearchResult ? 'mr-4' : ''} bg-gradient-to-br from-primary to-secondary rounded-xl overflow-hidden"> | |
| <img src="${track.artwork?.url || 'data:image/svg+xml;charset=UTF-8,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'80\' height=\'80\' viewBox=\'0 0 80 80\'%3E%3Crect fill=\'%2305060A\' width=\'80\' height=\'80\'/%3E%3Cpath fill=\'%235CF3FF\' d=\'M20,20l40,40M60,20L20,60\' stroke=\'%235CF3FF\' stroke-width=\'8\'/%3E%3C/svg%3E'}" | |
| alt="Track cover" class="w-full h-full object-cover"> | |
| </div> | |
| <div class="${isSearchResult ? 'flex-1' : 'ml-4 flex-1 min-w-0'}"> | |
| <h4 class="font-bold ${isSearchResult ? 'text-lg' : 'truncate'} ${isArtistMatch ? 'text-primary' : ''}">${track.title}</h4> | |
| <p class="${isSearchResult ? (isArtistMatch ? 'text-primary' : 'text-textPrimary') : 'text-sm text-textMuted truncate'}"> | |
| ${track.user.name} | |
| ${isSearchResult && isArtistMatch ? ' <span class="text-xs bg-primary/20 text-primary px-2 py-0.5 rounded-full">Verified</span>' : ''} | |
| </p> | |
| ${!isSearchResult ? ` | |
| <div class="flex items-center mt-1"> | |
| <span class="text-xs text-textMuted">${playCount} plays • ${duration}</span> | |
| </div> | |
| ` : ''} | |
| </div> | |
| ${isSearchResult ? ` | |
| <button class="ml-4 w-10 h-10 rounded-full ${isArtistMatch ? 'bg-primary/20' : 'bg-glass'} flex items-center justify-center"> | |
| <i data-feather="play" class="${isArtistMatch ? 'text-primary' : 'text-textPrimary'}"></i> | |
| </button> | |
| ` : ''} | |
| </div> | |
| ${!isSearchResult ? ` | |
| <div class="card-actions flex gap-2 mt-4 transform translate-y-4 opacity-0 transition-all duration-300"> | |
| <button class="flex-1 glass py-2 rounded-lg flex items-center justify-center"> | |
| <i data-feather="play" class="mr-2 text-primary"></i> Play | |
| </button> | |
| <button class="w-10 h-10 rounded-lg bg-glass flex items-center justify-center"> | |
| <i data-feather="heart" class="text-textMuted"></i> | |
| </button> | |
| <button class="w-10 h-10 rounded-lg bg-glass flex items-center justify-center"> | |
| <i data-feather="plus" class="text-textMuted"></i> | |
| </button> | |
| </div> | |
| ` : ''} | |
| `; | |
| if (isSearchResult) { | |
| trackElement.querySelector('button').addEventListener('click', () => { | |
| // Play track logic | |
| console.log('Playing:', track.title); | |
| searchModal.classList.add('hidden'); | |
| }); | |
| } | |
| container.appendChild(trackElement); | |
| feather.replace(); | |
| }); | |
| } | |
| function updatePlayer(track) { | |
| const player = document.querySelector('.player-bar'); | |
| player.querySelector('img').src = track.artwork?.url || 'data:image/svg+xml;charset=UTF-8,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'80\' height=\'80\' viewBox=\'0 0 80 80\'%3E%3Crect fill=\'%2305060A\' width=\'80\' height=\'80\'/%3E%3Cpath fill=\'%235CF3FF\' d=\'M20,20l40,40M60,20L20,60\' stroke=\'%235CF3FF\' stroke-width=\'8\'/%3E%3C/svg%3E'; | |
| player.querySelector('.marquee span').textContent = track.title; | |
| player.querySelector('p').textContent = track.user.name; | |
| } | |
| // Initialize with empty search state | |
| document.getElementById('search-results').innerHTML = | |
| '<p class="p-6 text-textMuted text-center">Search for Mr.FLEN tracks</p>'; | |
| // Set up keyboard shortcut for search | |
| document.addEventListener('keydown', function(e) { | |
| // Ctrl/Cmd + K shortcut | |
| if ((e.ctrlKey || e.metaKey) && e.key === 'k') { | |
| e.preventDefault(); | |
| document.querySelector('.search-trigger').click(); | |
| } | |
| // Escape to close search modal | |
| if (e.key === 'Escape') { | |
| document.getElementById('search-modal').classList.add('hidden'); | |
| } | |
| }); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |
| Key features implemented: | |
| 1. **Glassmorphic UI** | |
| - Frosted glass panels with subtle borders | |
| - Gradient accents (#5CF3FF & #9B5CFF) | |
| - Dark cinematic background (#05060A) | |
| - Custom text shadows for headings | |
| 2. **Responsive Layout** | |
| - Flexible grid system using Tailwind | |
| - Mobile-optimized navigation | |
| - Adaptive card layouts | |
| 3. **Dynamic Animations** | |
| - Marquee scrolling for long track titles | |
| - Soundwave animation in player | |
| - AOS.js for scroll animations | |
| - Interactive card hovers with actions | |
| 4. **Functional Components** | |
| - Featured banner with gradient overlay | |
| - Trending carousel with playback controls | |
| - Genre filter chips | |
| - Track cards with hover effects | |
| - Interactive player bar with progress visualization | |
| 5. **Modals & Drawers** | |
| - Search modal with command shortcut | |
| - Queue drawer with playlist save option | |
| - Smooth transitions and layer management | |
| The interface follows the cinematic, glassmorphic aesthetic with focus on music discovery while keeping your tracks at the forefront. Animations enhance the music experience without overwhelming users. |