mrflen / index.html
flen-crypto's picture
fix the api code so it operates correctly and add the additional pages - Initial Deployment
0bd5ed6 verified
<!DOCTYPE html>
<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>