From 3772295a07f3b46bdc12ad94dcdebe854a1fccf1 Mon Sep 17 00:00:00 2001 From: Samidy Date: Fri, 6 Mar 2026 08:58:24 +0300 Subject: [PATCH] feat(home): explore page --- index.html | 327 ++++++++++++++++++++++++++++------------------------- js/ui.js | 229 +++++++++++++++++++++++++++++++++++++ styles.css | 32 ++++++ 3 files changed, 431 insertions(+), 157 deletions(-) diff --git a/index.html b/index.html index 07cbe4f..876068f 100644 --- a/index.html +++ b/index.html @@ -2151,166 +2151,20 @@
- diff --git a/js/ui.js b/js/ui.js index 269dab4..8473d9a 100644 --- a/js/ui.js +++ b/js/ui.js @@ -1629,6 +1629,7 @@ export class UIRenderer { try { this.showPage('home'); + this.setupHomeTabs(); const welcomeEl = document.getElementById('home-welcome'); const contentEl = document.getElementById('home-content'); @@ -1698,6 +1699,234 @@ export class UIRenderer { } } + setupHomeTabs() { + const tabs = document.querySelectorAll('.home-tab'); + if (tabs.length === 0) return; + + if (tabs[0].dataset.initialized) return; + + tabs.forEach((tab) => { + tab.dataset.initialized = 'true'; + tab.addEventListener('click', () => { + document.querySelectorAll('.home-tab').forEach((t) => t.classList.remove('active')); + document.querySelectorAll('.home-view').forEach((v) => { + v.style.display = 'none'; + v.classList.remove('active'); + }); + + tab.classList.add('active'); + const viewId = `home-view-${tab.dataset.tab}`; + const view = document.getElementById(viewId); + if (view) { + view.style.display = 'block'; + view.classList.add('active'); + } + + if (tab.dataset.tab === 'explore') { + this.renderExplorePage(); + } + }); + }); + } + + async renderExplorePage() { + const container = document.getElementById('explore-grid'); + if (!container) return; + + if (container.children.length > 0) return; + + container.classList.remove('card-grid'); + + container.innerHTML = `
${this.createSkeletonCards(12)}
`; + + try { + const response = await fetch('https://hot.monochrome.tf/'); + if (!response.ok) throw new Error('Failed to load explore data'); + const data = await response.json(); + + container.innerHTML = ''; + + const GENRES = [ + { id: 'hip_hop', name: 'Hip Hop / Rap' }, + { id: 'pop', name: 'Pop' }, + { id: 'rock', name: 'Rock' }, + { id: 'electronic', name: 'Electronic' }, + { id: 'country', name: 'Country' }, + { id: 'jazz', name: 'Jazz' }, + { id: 'classical', name: 'Classical' }, + { id: 'latin', name: 'Latin' }, + { id: 'reggae', name: 'Reggae / Dancehall' }, + { id: 'blues', name: 'Blues' }, + { id: 'soundtrack', name: 'Soundtrack' }, + { id: 'alternative', name: 'Alternative' }, + ]; + + if (GENRES.length > 0) { + const genresSection = document.createElement('section'); + genresSection.className = 'content-section'; + genresSection.innerHTML = `

Genres

`; + + const genresGrid = document.createElement('div'); + genresGrid.className = 'card-grid'; + genresGrid.innerHTML = GENRES + .map( + (genre) => ` +
+

${escapeHtml(genre.name)}

+
+ ` + ) + .join(''); + + genresSection.appendChild(genresGrid); + container.appendChild(genresSection); + + genresGrid.querySelectorAll('.genre-card').forEach((card) => { + card.addEventListener('click', () => { + this.renderGenrePage(card.dataset.genreId, card.dataset.genreName); + }); + }); + } + + if (data.top_albums && data.top_albums.length > 0) { + this.renderExploreSection(container, 'Trending Albums', data.top_albums, 'album'); + } + + if (data.top_tracks && data.top_tracks.length > 0) { + this.renderExploreSection(container, 'Trending Tracks', data.top_tracks, 'track'); + } + + if (data.featured_playlists && data.featured_playlists.length > 0) { + this.renderExploreSection(container, 'Featured Playlists', data.featured_playlists, 'playlist'); + } + + if (data.sections && data.sections.length > 0) { + data.sections.forEach((section) => { + if (section.items && section.items.length > 0) { + let type = null; + if (section.type === 'ALBUM_LIST') type = 'album'; + else if (section.type === 'TRACK_LIST') type = 'track'; + else if (section.type === 'PLAYLIST_LIST') type = 'playlist'; + + if (type) { + this.renderExploreSection(container, section.title, section.items, type); + } + } + }); + } + + if (container.children.length === 0) { + container.innerHTML = createPlaceholder('No explore content available.'); + } + } catch (e) { + console.error(e); + container.innerHTML = createPlaceholder('Failed to load explore content.'); + } + } + + renderExploreSection(container, title, items, type) { + const section = document.createElement('section'); + section.className = 'content-section'; + section.innerHTML = `

${title}

`; + + if (type === 'track') { + const list = document.createElement('div'); + list.className = 'track-list'; + this.renderListWithTracks(list, items, true); + section.appendChild(list); + } else { + const grid = document.createElement('div'); + grid.className = 'card-grid'; + grid.innerHTML = items + .map((item) => { + if (type === 'album') return this.createAlbumCardHTML(item); + if (type === 'playlist') return this.createPlaylistCardHTML(item); + return ''; + }) + .join(''); + + items.forEach((item) => { + let selector; + if (type === 'album') selector = `[data-album-id="${item.id}"]`; + if (type === 'playlist') selector = `[data-playlist-id="${item.uuid}"]`; + + if (selector) { + const el = grid.querySelector(selector); + if (el) { + trackDataStore.set(el, item); + if (type === 'album') this.updateLikeState(el, 'album', item.id); + if (type === 'playlist') this.updateLikeState(el, 'playlist', item.uuid); + } + } + }); + section.appendChild(grid); + } + container.appendChild(section); + } + + async renderGenrePage(genreId, genreName) { + const container = document.getElementById('explore-grid'); + if (!container) return; + + container.classList.remove('card-grid'); + + container.innerHTML = ` +
+ +

${escapeHtml(genreName)}

+
+
${this.createSkeletonCards(12)}
+ `; + + container.querySelector('.explore-back-btn').addEventListener('click', () => { + container.innerHTML = ''; + this.renderExplorePage(); + }); + + try { + const response = await fetch(`https://hot.monochrome.tf/explore/genre/?id=${genreId}`); + if (!response.ok) throw new Error('Failed to load genre data'); + const data = await response.json(); + + const header = container.firstElementChild; + container.innerHTML = ''; + container.appendChild(header); + + const contentContainer = document.createElement('div'); + container.appendChild(contentContainer); + + if (data.sections && data.sections.length > 0) { + data.sections.forEach((section) => { + if (section.items && section.items.length > 0) { + let type = null; + if (section.type === 'ALBUM_LIST') type = 'album'; + else if (section.type === 'TRACK_LIST') type = 'track'; + else if (section.type === 'PLAYLIST_LIST') type = 'playlist'; + + if (type) { + this.renderExploreSection(contentContainer, section.title, section.items, type); + } + } + }); + } + + if (contentContainer.children.length === 0) { + contentContainer.innerHTML = createPlaceholder('No content found for this genre.'); + } + } catch (e) { + console.error(e); + const header = container.firstElementChild; + container.innerHTML = ''; + container.appendChild(header); + const errorDiv = document.createElement('div'); + errorDiv.innerHTML = createPlaceholder('Failed to load genre content.'); + container.appendChild(errorDiv); + } + } + async getSeeds() { const history = await db.getHistory(); const favorites = await db.getFavorites('track'); diff --git a/styles.css b/styles.css index 5540c85..cc6ad72 100644 --- a/styles.css +++ b/styles.css @@ -7835,3 +7835,35 @@ textarea:focus { display: flex; flex-direction: column; } + +.home-header-tabs { + display: flex; + gap: 1.5rem; + margin-bottom: 1.5rem; + border-bottom: 1px solid var(--border); +} + +.home-tab { + background: transparent; + border: none; + color: var(--muted-foreground); + padding: 0.75rem 0.5rem; + cursor: pointer; + font-size: 1.1rem; + font-weight: 600; + border-bottom: 2px solid transparent; + transition: all var(--transition); +} + +.home-tab:hover { + color: var(--foreground); +} + +.home-tab.active { + color: var(--foreground); + border-bottom-color: var(--highlight); +} + +.home-view { + animation: fade-in 0.3s ease; +}