<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VibeStream - Music Player</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script src="https://unpkg.com/@phosphor-icons/web"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;800&display=swap');
body {
background-color: #f8fafc;
color: #0f172a;
font-family: 'Plus Jakarta Sans', sans-serif;
height: 100vh;
margin: 0;
overflow: hidden; /* Lock the body */
}
/* Layout Grid: Header (auto) | Main (1fr) | Player (auto) */
.app-container {
display: grid;
grid-template-rows: auto 1fr auto;
height: 100vh;
padding: 20px; /* Internal Padding */
gap: 20px;
}
.main-layout {
display: grid;
grid-template-columns: 260px 1fr;
gap: 20px;
min-height: 0; /* Critical for inner scrolling */
}
.custom-scroll::-webkit-scrollbar { width: 6px; }
.custom-scroll::-webkit-scrollbar-thumb { background: #e2e8f0; border-radius: 10px; }
.hero-tall { min-height: 400px; }
#avatar-dropdown {
display: none; position: absolute; right: 0; top: 110%; width: 220px;
background: white; border: 1px solid #e2e8f0; border-radius: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1); padding: 8px; z-index: 100;
}
.avatar-active #avatar-dropdown { display: block; animation: slideDown 0.2s ease-out; }
@keyframes slideDown { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } }
#modal-overlay { display: none; position: fixed; inset: 0; background: rgba(15, 23, 42, 0.6); backdrop-filter: blur(4px); z-index: 200; align-items: center; justify-content: center; }
.modal-active #modal-overlay { display: flex; }
.shimmer {
background: #f1f5f9;
background-image: linear-gradient(to right, #f1f5f9 0%, #e2e8f0 20%, #f1f5f9 40%, #f1f5f9 100%);
background-repeat: no-repeat;
background-size: 800px 100%;
animation: shimmerInfinite 1.5s linear infinite forwards;
}
@keyframes shimmerInfinite { 0% { background-position: -468px 0; } 100% { background-position: 468px 0; } }
</style>
</head>
<body onclick="closeDropdowns()">
<div id="modal-overlay">
<div class="bg-white rounded-[32px] p-5 w-96 shadow-2xl border border-slate-100" onclick="event.stopPropagation()">
<h3 id="modal-title" class="text-xl font-extrabold text-slate-900 mb-1">Playlist</h3>
<p id="modal-desc" class="text-xs text-slate-400 mb-5 font-medium">Configure your vibes.</p>
<input type="text" id="modal-input" class="w-full px-4 py-3 bg-slate-100 rounded-xl border-none focus:ring-2 focus:ring-indigo-500 mb-5 font-bold text-sm" placeholder="Name...">
<div id="modal-list" class="max-h-48 overflow-y-auto mb-5 hidden space-y-2 custom-scroll pr-2"></div>
<div class="flex gap-3">
<button onclick="closeModal()" class="flex-1 py-3 bg-slate-100 text-slate-600 rounded-xl font-bold text-sm">Cancel</button>
<button id="modal-action" class="flex-1 py-3 bg-indigo-600 text-white rounded-xl font-bold text-sm">Confirm</button>
</div>
</div>
</div>
<div class="app-container">
<header class="flex justify-between items-center bg-white rounded-[24px] border border-slate-200 p-4 shadow-sm">
<div class="flex items-center gap-3 text-indigo-600 font-extrabold tracking-tight px-2">
<i class="ph-bold ph-waveform text-3xl"></i> VibeStream
</div>
<div class="relative w-96">
<i class="ph ph-magnifying-glass absolute left-4 top-1/2 -translate-y-1/2 text-slate-400"></i>
<input type="text" placeholder="Search your 100 tracks..." class="w-full pl-11 pr-4 py-2 bg-slate-100 border-none rounded-2xl focus:ring-2 focus:ring-indigo-500 transition">
</div>
<div class="relative" id="avatar-container">
<button onclick="toggleAvatar(event)" class="flex items-center gap-2 bg-slate-50 p-1 pr-4 rounded-full border border-slate-100 hover:bg-white transition">
<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=Felix" class="w-8 h-8 rounded-full">
<span class="text-sm font-bold text-slate-700">Felix</span>
</button>
<div id="avatar-dropdown">
<div onclick="resetToMockData()" class="p-2 hover:bg-indigo-50 rounded-xl cursor-pointer text-sm font-semibold text-indigo-600"><i class="ph ph-arrows-clockwise mr-2"></i>Reset 100 Tracks</div>
<div onclick="location.reload()" class="p-2 hover:bg-red-50 rounded-xl cursor-pointer text-sm font-semibold text-red-500"><i class="ph ph-sign-out mr-2"></i>Logout</div>
</div>
</div>
</header>
<div class="main-layout">
<aside class="flex flex-col gap-4 min-h-0">
<div class="bg-white rounded-[32px] p-5 border border-slate-200 shadow-sm">
<nav class="space-y-1">
<div onclick="loadTrending()" class="flex items-center gap-3 px-4 py-3 bg-indigo-50 text-indigo-600 rounded-xl font-bold cursor-pointer">
<i class="ph-bold ph-fire"></i> Trending
</div>
<div onclick="showFavorites()" class="flex items-center gap-3 px-4 py-3 text-slate-500 hover:bg-slate-50 rounded-xl font-bold cursor-pointer">
<i class="ph-bold ph-heart text-red-400"></i> Favorites
</div>
</nav>
</div>
<div class="flex-1 bg-white rounded-[32px] border border-slate-200 p-5 flex flex-col overflow-hidden shadow-sm">
<div class="flex justify-between items-center mb-4 flex-none">
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">Library</p>
<button onclick="openCreatePlaylist()" class="text-indigo-600 hover:scale-110 transition"><i class="ph-bold ph-plus-circle text-xl"></i></button>
</div>
<div id="playlist-sidebar-list" class="space-y-1 overflow-y-auto custom-scroll pr-2"></div>
</div>
</aside>
<main class="bg-white rounded-[32px] border border-slate-200 shadow-sm overflow-y-auto custom-scroll min-h-0">
<div class="p-6">
<div id="hero-banner" class="hero-tall mb-10 p-10 rounded-[48px] bg-slate-900 text-white relative overflow-hidden flex flex-col justify-end">
<img id="hero-bg" src="https://images.unsplash.com/photo-1470225620780-dba8ba36b745?q=80&w=1200" class="absolute inset-0 w-full h-full object-cover opacity-40">
<div class="relative z-10">
<h2 id="view-title" class="text-7xl font-black mb-2 leading-tight">Trending</h2>
<p id="view-desc" class="text-slate-300 text-lg font-medium">Scroll down to see all 100 tracks.</p>
</div>
</div>
<div id="track-container" class="space-y-1"></div>
<div id="missing-section" class="mt-12 hidden">
<div class="flex items-center gap-3 mb-6"><div class="h-[1px] flex-1 bg-slate-100"></div><span class="text-[10px] font-black text-slate-400 uppercase tracking-widest">Library Matches</span><div class="h-[1px] flex-1 bg-slate-100"></div></div>
<div id="missing-container" class="space-y-1"></div>
</div>
</div>
</main>
</div>
<footer class="h-24 bg-white border border-slate-200 rounded-[24px] flex items-center justify-between px-10 shadow-lg">
<div class="flex items-center gap-4 w-1/3">
<div id="footer-art" class="w-14 h-14 bg-slate-100 rounded-xl overflow-hidden shadow-inner"></div>
<div class="truncate">
<div id="footer-title" class="font-bold text-slate-900 text-sm truncate w-48">Silence</div>
<div id="footer-artist" class="text-[10px] text-slate-400 font-bold uppercase tracking-widest">Select a song</div>
</div>
</div>
<div class="flex flex-col items-center gap-2 w-1/3">
<div class="flex items-center gap-8 text-slate-400">
<i class="ph-bold ph-skip-back text-2xl cursor-pointer hover:text-slate-900"></i>
<button onclick="togglePlayback()" class="w-12 h-12 bg-slate-900 text-white rounded-full flex items-center justify-center shadow-lg"><i id="play-icon" class="ph-fill ph-play text-xl ml-1"></i></button>
<i class="ph-bold ph-skip-forward text-2xl cursor-pointer hover:text-slate-900"></i>
</div>
<div class="w-full h-1.5 bg-slate-100 rounded-full overflow-hidden"><div id="progress-bar" class="h-full bg-indigo-600 w-0 transition-all duration-300"></div></div>
</div>
<div class="w-1/3 text-right text-slate-300"><i class="ph-bold ph-speaker-high text-xl"></i></div>
</footer>
</div>
<audio id="audio-engine" ontimeupdate="handleRealTimeUpdate()"></audio>
<script>
const audio = document.getElementById('audio-engine');
let favorites = JSON.parse(localStorage.getItem('vibe_favs') || '[]');
let playlists = JSON.parse(localStorage.getItem('vibe_playlists') || '[]');
let localTracks = JSON.parse(localStorage.getItem('vibe_local_tracks') || '[]');
let isFakePlaying = false, fakeProgress = 0, fakeInterval = null;
function resetToMockData() {
const genres = ["Jazz", "Soul", "Hip-Hop", "Electronic", "Lo-Fi", "Funk"];
const artists = ["The Collective", "Urban Drift", "Neon Nights", "Silk Waves", "Midnight Crew", "Velvet Echo"];
const images = [
"https://images.unsplash.com/photo-1511192336575-5a79af67a629?w=300",
"https://images.unsplash.com/photo-1493225255756-d9584f8606e9?w=300",
"https://images.unsplash.com/photo-1508700115892-45ecd05ae2ad?w=300",
"https://images.unsplash.com/photo-1520527053377-4710dbf6c0df?w=300",
"https://images.unsplash.com/photo-1453733190371-0a9bedd82893?w=300",
"https://images.unsplash.com/photo-1470225620780-dba8ba36b745?w=300"
];
let tracks = [];
for (let i = 1; i <= 100; i++) {
tracks.push({
id: `track-${i}`,
name: `${genres[i % genres.length]} Anthem #${i}`,
artist_name: artists[i % artists.length],
album_image: images[i % images.length],
audio: ""
});
}
localTracks = tracks;
localStorage.setItem('vibe_local_tracks', JSON.stringify(localTracks));
loadTrending();
}
if(localTracks.length === 0) resetToMockData();
function openCreatePlaylist() {
showModal("New Playlist", "Name your collection.", true);
document.getElementById('modal-action').onclick = () => {
const name = document.getElementById('modal-input').value;
if(name) { playlists.push({id: Date.now(), name, tracks: []}); savePlaylists(); renderSidebar(); closeModal(); }
};
}
function openEditPlaylist(id) {
const pl = playlists.find(p => p.id === id);
showModal("Edit", "Update name.", true);
document.getElementById('modal-input').value = pl.name;
document.getElementById('modal-action').onclick = () => {
const name = document.getElementById('modal-input').value;
if(name) { pl.name = name; savePlaylists(); renderSidebar(); closeModal(); }
};
}
function openAddSong(track) {
if(playlists.length === 0) return alert("Create a playlist first!");
showModal("Add to Playlist", "Select destination.", false);
const list = document.getElementById('modal-list');
list.classList.remove('hidden');
list.innerHTML = playlists.map(p => `<div onclick="confirmAddSong(${p.id}, ${JSON.stringify(track).replace(/"/g, '"')})" class="p-3 bg-slate-50 hover:bg-indigo-50 rounded-xl cursor-pointer font-bold text-sm transition">${p.name}</div>`).join('');
document.getElementById('modal-action').classList.add('hidden');
}
function confirmAddSong(plId, track) {
const pl = playlists.find(p => p.id === plId);
if(!pl.tracks.some(t => t.id === track.id)) { pl.tracks.push(track); savePlaylists(); }
closeModal();
}
function showModal(title, desc, showInput) {
document.body.classList.add('modal-active');
document.getElementById('modal-title').innerText = title;
document.getElementById('modal-desc').innerText = desc;
document.getElementById('modal-input').classList.toggle('hidden', !showInput);
document.getElementById('modal-list').classList.add('hidden');
document.getElementById('modal-action').classList.remove('hidden');
}
function closeModal() { document.body.classList.remove('modal-active'); }
function savePlaylists() { localStorage.setItem('vibe_playlists', JSON.stringify(playlists)); }
function renderSidebar() {
document.getElementById('playlist-sidebar-list').innerHTML = playlists.map(p => `
<div class="flex items-center justify-between group p-2 hover:bg-slate-50 rounded-lg cursor-pointer">
<div onclick="showPlaylist(${p.id})" class="text-sm font-bold text-slate-600 flex items-center gap-2 truncate">
<i class="ph ph-music-notes text-slate-300"></i> ${p.name}
</div>
<div class="hidden group-hover:flex gap-2">
<i onclick="openEditPlaylist(${p.id})" class="ph ph-pencil-simple text-slate-400 hover:text-indigo-600"></i>
<i onclick="deletePlaylist(${p.id})" class="ph ph-trash text-slate-400 hover:text-red-500"></i>
</div>
</div>`).join('');
}
function deletePlaylist(id) { if(confirm("Delete?")) { playlists = playlists.filter(p => p.id !== id); savePlaylists(); renderSidebar(); loadTrending(); } }
function showPlaylist(id) { const pl = playlists.find(p => p.id === id); updateHeader(pl.name, `${pl.tracks.length} tracks.`); processTracks(pl.tracks, id); }
function loadTrending() {
updateHeader('Trending', 'Top 100 local tracks.');
showShimmer(10);
setTimeout(() => { processTracks(localTracks); }, 500);
}
function processTracks(tracks, activePlId = null) {
const playable = tracks.filter(t => t.audio);
const missing = tracks.filter(t => !t.audio);
renderList('track-container', playable, false, activePlId);
if(missing.length > 0) {
document.getElementById('missing-section').classList.remove('hidden');
renderList('missing-container', missing, true, activePlId);
} else document.getElementById('missing-section').classList.add('hidden');
}
function renderList(targetId, tracks, isMissing, plId) {
document.getElementById(targetId).innerHTML = tracks.map((t, i) => `
<div class="flex items-center justify-between p-3 hover:bg-slate-50 rounded-2xl cursor-pointer group" onclick="initiatePlayback('${t.audio}', '${t.name}', '${t.artist_name}', '${t.album_image}')">
<div class="flex items-center gap-4">
<span class="text-slate-300 font-black w-8 text-xs">${i+1}</span>
<img src="${t.album_image}" class="w-11 h-11 rounded-xl object-cover">
<div class="truncate w-40 sm:w-64">
<div class="font-bold text-slate-800 text-sm truncate">${t.name}</div>
<div class="text-[9px] text-slate-400 font-bold uppercase tracking-widest">${t.artist_name}</div>
</div>
</div>
<div class="flex items-center gap-4">
<i onclick="event.stopPropagation(); openAddSong(${JSON.stringify(t).replace(/"/g, '"')})" class="ph-bold ph-plus-circle text-slate-300 hover:text-indigo-500 text-xl"></i>
${plId ? `<i onclick="event.stopPropagation(); removeFromPlaylist(${plId}, '${t.id}')" class="ph-bold ph-minus-circle text-slate-300 hover:text-red-500 text-xl"></i>` : ''}
<i onclick="toggleFav(event, ${JSON.stringify(t).replace(/"/g, '"')})" class="ph-fill ph-heart ${favorites.some(f => f.id === t.id) ? 'text-red-500' : 'text-slate-200'} text-xl"></i>
</div>
</div>`).join('');
}
function removeFromPlaylist(plId, trackId) { const pl = playlists.find(p => p.id === plId); pl.tracks = pl.tracks.filter(t => t.id !== trackId); savePlaylists(); showPlaylist(plId); }
function initiatePlayback(url, title, artist, img) { stopFakePlay(); document.getElementById('footer-title').innerText = title; document.getElementById('footer-artist').innerText = artist; document.getElementById('footer-art').innerHTML = `<img src="${img}" class="w-full h-full object-cover">`; if (url) { isFakePlaying = false; audio.src = url; audio.play(); document.getElementById('play-icon').className = 'ph-fill ph-pause text-xl'; } else { startFakePlay(); } }
function startFakePlay() { isFakePlaying = true; fakeProgress = 0; audio.pause(); audio.src = ""; document.getElementById('play-icon').className = 'ph-fill ph-pause text-xl'; fakeInterval = setInterval(() => { fakeProgress += 0.5; if (fakeProgress >= 100) fakeProgress = 0; document.getElementById('progress-bar').style.width = fakeProgress + '%'; }, 100); }
function stopFakePlay() { isFakePlaying = false; clearInterval(fakeInterval); document.getElementById('progress-bar').style.width = '0%'; }
function togglePlayback() { if (isFakePlaying) { if (fakeInterval) { clearInterval(fakeInterval); fakeInterval = null; document.getElementById('play-icon').className = 'ph-fill ph-play text-xl ml-1'; } else { startFakePlay(); } } else { audio.paused ? (audio.play(), document.getElementById('play-icon').className = 'ph-fill ph-pause text-xl') : (audio.pause(), document.getElementById('play-icon').className = 'ph-fill ph-play text-xl ml-1'); } }
function handleRealTimeUpdate() { if (!isFakePlaying) document.getElementById('progress-bar').style.width = (audio.currentTime / (audio.duration || 1)) * 100 + '%'; }
function showFavorites() { updateHeader('Favorites', `${favorites.length} saved tracks.`); processTracks(favorites); }
function toggleFav(e, track) { e.stopPropagation(); const idx = favorites.findIndex(f => f.id === track.id); idx > -1 ? favorites.splice(idx, 1) : favorites.push(track); localStorage.setItem('vibe_favs', JSON.stringify(favorites)); e.target.className = `ph-fill ph-heart ${favorites.some(f => f.id === track.id) ? 'text-red-500' : 'text-slate-200'} text-xl`; }
function showShimmer(count) { document.getElementById('track-container').innerHTML = Array.from({length:count}).map(() => `<div class="p-3 flex items-center gap-4"><div class="w-6 h-4 shimmer rounded"></div><div class="w-11 h-11 shimmer rounded-xl"></div><div class="space-y-2"><div class="w-32 h-4 shimmer rounded"></div><div class="w-20 h-2 shimmer rounded"></div></div></div>`).join(''); }
function toggleAvatar(e) { e.stopPropagation(); document.getElementById('avatar-container').classList.toggle('avatar-active'); }
function closeDropdowns() { document.getElementById('avatar-container').classList.remove('avatar-active'); }
function updateHeader(title, desc) { document.getElementById('view-title').innerText = title; document.getElementById('view-desc').innerText = desc; }
renderSidebar(); loadTrending();
</script>
</body>
</html>“Create a "VibeStream - Music Player" component. Use GSAP for advanced animations, Phosphor Icons. Apply keyframe animations, glass-morphism / backdrop blur, gradients, layered shadows, hover transitions. Layout: CSS Grid, Flexbox, full-viewport sizing. Interactivity: click interactions, scroll-based effects. Includes form input fields. Includes media playback elements.”