feat(home): explore page

This commit is contained in:
Samidy 2026-03-06 08:58:24 +03:00
parent 73878b421d
commit 3772295a07
3 changed files with 431 additions and 157 deletions

View file

@ -2151,166 +2151,20 @@
</header>
<div id="page-home" class="page">
<div id="home-welcome" style="display: none; text-align: center; padding: 4rem 2rem">
<h2 style="margin-bottom: 1rem">Welcome to Monochrome</h2>
<p style="color: var(--muted-foreground)">
You haven't listened to anything yet. Search for your favorite songs to get started!
</p>
<div class="home-header-tabs">
<button class="home-tab active" data-tab="for-you">Home</button>
<button class="home-tab" data-tab="explore">Hot & New</button>
</div>
<section class="content-section" id="home-editors-picks-section-empty" style="margin-top: 0">
<div
style="
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
"
>
<h2 class="section-title" style="margin-bottom: 0">Editor's Picks</h2>
<div id="home-view-for-you" class="home-view active">
<div id="home-welcome" style="display: none; text-align: center; padding: 4rem 2rem">
<h2 style="margin-bottom: 1rem">Welcome to Monochrome</h2>
<p style="color: var(--muted-foreground)">
You haven't listened to anything yet. Search for your favorite songs to get started!
</p>
</div>
<div class="card-grid" id="home-editors-picks-empty"></div>
</section>
<div id="home-content" style="display: none">
<section class="content-section">
<div
style="
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
"
>
<h2 class="section-title" style="margin-bottom: 0">Recommended Songs</h2>
<button
class="btn-secondary"
id="refresh-songs-btn"
title="Refresh"
style="padding: 4px 8px"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
</svg>
</button>
</div>
<div class="track-list" id="home-recommended-songs"></div>
</section>
<section class="content-section">
<div
style="
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
"
>
<h2 class="section-title" style="margin-bottom: 0">Recommended Albums</h2>
<button
class="btn-secondary"
id="refresh-albums-btn"
title="Refresh"
style="padding: 4px 8px"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
</svg>
</button>
</div>
<div class="card-grid" id="home-recommended-albums"></div>
</section>
<section class="content-section">
<div
style="
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
"
>
<h2 class="section-title" style="margin-bottom: 0">Recommended Artists</h2>
<button
class="btn-secondary"
id="refresh-artists-btn"
title="Refresh"
style="padding: 4px 8px"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
</svg>
</button>
</div>
<div class="card-grid" id="home-recommended-artists"></div>
</section>
<section class="content-section">
<div
style="
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
"
>
<h2 class="section-title" style="margin-bottom: 0">Jump Back In</h2>
<button
class="btn-secondary"
id="clear-recent-btn"
title="Clear History"
style="padding: 4px 8px"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 6h18" />
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
</svg>
</button>
</div>
<div class="card-grid" id="home-recent-mixed"></div>
</section>
<section class="content-section" id="home-editors-picks-section">
<section class="content-section" id="home-editors-picks-section-empty" style="margin-top: 0">
<div
style="
display: flex;
@ -2321,8 +2175,167 @@
>
<h2 class="section-title" style="margin-bottom: 0">Editor's Picks</h2>
</div>
<div class="card-grid" id="home-editors-picks"></div>
<div class="card-grid" id="home-editors-picks-empty"></div>
</section>
<div id="home-content" style="display: none">
<section class="content-section">
<div
style="
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
"
>
<h2 class="section-title" style="margin-bottom: 0">Recommended Songs</h2>
<button
class="btn-secondary"
id="refresh-songs-btn"
title="Refresh"
style="padding: 4px 8px"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
</svg>
</button>
</div>
<div class="track-list" id="home-recommended-songs"></div>
</section>
<section class="content-section">
<div
style="
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
"
>
<h2 class="section-title" style="margin-bottom: 0">Recommended Albums</h2>
<button
class="btn-secondary"
id="refresh-albums-btn"
title="Refresh"
style="padding: 4px 8px"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
</svg>
</button>
</div>
<div class="card-grid" id="home-recommended-albums"></div>
</section>
<section class="content-section">
<div
style="
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
"
>
<h2 class="section-title" style="margin-bottom: 0">Recommended Artists</h2>
<button
class="btn-secondary"
id="refresh-artists-btn"
title="Refresh"
style="padding: 4px 8px"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
</svg>
</button>
</div>
<div class="card-grid" id="home-recommended-artists"></div>
</section>
<section class="content-section">
<div
style="
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
"
>
<h2 class="section-title" style="margin-bottom: 0">Jump Back In</h2>
<button
class="btn-secondary"
id="clear-recent-btn"
title="Clear History"
style="padding: 4px 8px"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 6h18" />
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
</svg>
</button>
</div>
<div class="card-grid" id="home-recent-mixed"></div>
</section>
<section class="content-section" id="home-editors-picks-section">
<div
style="
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
"
>
<h2 class="section-title" style="margin-bottom: 0">Editor's Picks</h2>
</div>
<div class="card-grid" id="home-editors-picks"></div>
</section>
</div>
</div>
<div id="home-view-explore" class="home-view" style="display: none;">
<div id="explore-content">
<div class="card-grid" id="explore-grid"></div>
</div>
</div>
</div>

229
js/ui.js
View file

@ -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 = `<div class="card-grid">${this.createSkeletonCards(12)}</div>`;
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 = `<h2 class="section-title">Genres</h2>`;
const genresGrid = document.createElement('div');
genresGrid.className = 'card-grid';
genresGrid.innerHTML = GENRES
.map(
(genre) => `
<div class="card genre-card" data-genre-id="${genre.id}" data-genre-name="${escapeHtml(genre.name)}" style="cursor: pointer; background: var(--secondary); padding: 1.5rem; display: flex; align-items: center; justify-content: center; text-align: center; min-height: 100px; border-radius: var(--radius); border: 1px solid var(--border);">
<h3 style="margin: 0; font-size: 1.1rem; font-weight: 600;">${escapeHtml(genre.name)}</h3>
</div>
`
)
.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 = `<h2 class="section-title">${title}</h2>`;
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 = `
<div style="margin-bottom: 1.5rem; display: flex; align-items: center; gap: 1rem;">
<button class="btn-secondary explore-back-btn" style="display: flex; align-items: center; gap: 0.5rem;">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
Back
</button>
<h2 class="section-title" style="margin: 0;">${escapeHtml(genreName)}</h2>
</div>
<div class="card-grid">${this.createSkeletonCards(12)}</div>
`;
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');

View file

@ -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;
}