feat: two-column top section — tracks + releases side-by-side
Replace full-width top tracks with a split layout: top tracks on the left (5 default), top releases grid on the right (2×2 = 4 default). A 'Show more' toggle below both columns expands to 10 tracks and 8 releases. Top releases are sorted newest-first from the existing discography data. The full discography section remains below for browsing by type.
This commit is contained in:
@@ -92,6 +92,7 @@ export class ExploreArtistDetails extends LitElement {
|
|||||||
@state() private loadingSimilar = true;
|
@state() private loadingSimilar = true;
|
||||||
@state() private artistImageURL = '';
|
@state() private artistImageURL = '';
|
||||||
@state() private similarImageURLs = new Map<string, string>();
|
@state() private similarImageURLs = new Map<string, string>();
|
||||||
|
@state() private topSectionExpanded = false;
|
||||||
private libraryMBIDs = new Set<string>();
|
private libraryMBIDs = new Set<string>();
|
||||||
|
|
||||||
/* ── Styles ── */
|
/* ── Styles ── */
|
||||||
@@ -316,6 +317,54 @@ export class ExploreArtistDetails extends LitElement {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Top section (tracks + releases side-by-side) ── */
|
||||||
|
.top-section-columns {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-section-column {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-releases-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-section-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
margin-top: 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--yj-bg-overlay, rgba(255, 255, 255, 0.06));
|
||||||
|
color: var(--yj-text-secondary, #b3b3b3);
|
||||||
|
font-size: var(--yj-text-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s ease, color 0.15s ease;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-section-toggle:hover {
|
||||||
|
background: var(--yj-bg-hover, rgba(255, 255, 255, 0.1));
|
||||||
|
color: var(--yj-text-primary, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-section-toggle wa-icon {
|
||||||
|
font-size: 12px;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-section-toggle[aria-expanded='true'] wa-icon {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Discography grid ── */
|
/* ── Discography grid ── */
|
||||||
.disco-group {
|
.disco-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -828,7 +877,7 @@ export class ExploreArtistDetails extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
${this.renderTopTracks()} ${this.renderDiscography()}
|
${this.renderTopSection()} ${this.renderDiscography()}
|
||||||
${this.renderSimilarArtists()}
|
${this.renderSimilarArtists()}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -866,54 +915,113 @@ export class ExploreArtistDetails extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Top Tracks Section ── */
|
/* ── Top Section (tracks + releases side-by-side) ── */
|
||||||
|
|
||||||
private renderTopTracks() {
|
private toggleTopSection() {
|
||||||
if (this.loadingTracks) {
|
this.topSectionExpanded = !this.topSectionExpanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Top releases = all release groups sorted newest-first. */
|
||||||
|
private get topReleases(): MBReleaseGroup[] {
|
||||||
|
return [...this.releaseGroups].sort((a, b) => {
|
||||||
|
const da = a.firstReleaseDate || '';
|
||||||
|
const db = b.firstReleaseDate || '';
|
||||||
|
return db.localeCompare(da);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderTopSection() {
|
||||||
|
const hasTracks = !this.loadingTracks && this.topTracks.length > 0;
|
||||||
|
const hasReleases = !this.loadingReleases && this.releaseGroups.length > 0;
|
||||||
|
const isLoading = this.loadingTracks || this.loadingReleases;
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
return html`
|
return html`
|
||||||
<section>
|
<section>
|
||||||
<h3 class="section-header">Top Tracks</h3>
|
<h3 class="section-header">Popular</h3>
|
||||||
<div class="section-loading">Loading\u2026</div>
|
<div class="section-loading">Loading\u2026</div>
|
||||||
</section>
|
</section>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
if (this.errorTracks) {
|
|
||||||
return html`
|
if (!hasTracks && !hasReleases) return nothing;
|
||||||
<section>
|
|
||||||
<h3 class="section-header">Top Tracks</h3>
|
const expanded = this.topSectionExpanded;
|
||||||
<div class="section-error">
|
const trackLimit = expanded ? 10 : 5;
|
||||||
<wa-icon name="triangle-exclamation"></wa-icon>
|
const releaseLimit = expanded ? 8 : 4;
|
||||||
${this.errorTracks}
|
|
||||||
</div>
|
const tracks = this.topTracks.slice(0, trackLimit);
|
||||||
</section>
|
const releases = this.topReleases.slice(0, releaseLimit);
|
||||||
`;
|
|
||||||
}
|
const canExpand =
|
||||||
if (this.topTracks.length === 0) return nothing;
|
this.topTracks.length > 5 || this.releaseGroups.length > 4;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<section>
|
<section>
|
||||||
<h3 class="section-header">Top Tracks</h3>
|
<div class="top-section-columns">
|
||||||
<div class="track-list">
|
${hasTracks
|
||||||
${this.topTracks.map(
|
? html`
|
||||||
(t, i) => html`
|
<div class="top-section-column">
|
||||||
<div class="track-item">
|
<h3 class="section-header">Top Tracks</h3>
|
||||||
<span class="track-rank">${i + 1}</span>
|
<div class="track-list">
|
||||||
<div class="track-info">
|
${tracks.map(
|
||||||
<div class="track-title">
|
(t, i) => html`
|
||||||
${t.trackName}
|
<div class="track-item">
|
||||||
</div>
|
<span class="track-rank"
|
||||||
<div class="track-artist">
|
>${i + 1}</span
|
||||||
${t.artistName}
|
>
|
||||||
</div>
|
<div class="track-info">
|
||||||
</div>
|
<div
|
||||||
<span class="track-listens">
|
class="track-title"
|
||||||
${formatListenCount(t.totalListenCount)}
|
>
|
||||||
plays
|
${t.trackName}
|
||||||
</span>
|
</div>
|
||||||
</div>
|
<div
|
||||||
`,
|
class="track-artist"
|
||||||
)}
|
>
|
||||||
|
${t.artistName}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="track-listens">
|
||||||
|
${formatListenCount(
|
||||||
|
t.totalListenCount,
|
||||||
|
)}
|
||||||
|
plays
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
${hasReleases
|
||||||
|
? html`
|
||||||
|
<div class="top-section-column">
|
||||||
|
<h3 class="section-header">Top Releases</h3>
|
||||||
|
<div class="top-releases-grid">
|
||||||
|
${releases.map((rg) =>
|
||||||
|
this.renderAlbumCard(rg),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
|
${canExpand
|
||||||
|
? html`
|
||||||
|
<button
|
||||||
|
class="top-section-toggle"
|
||||||
|
@click=${this.toggleTopSection}
|
||||||
|
aria-expanded="${expanded}"
|
||||||
|
>
|
||||||
|
${expanded ? 'Show less' : 'Show more'}
|
||||||
|
<wa-icon
|
||||||
|
name="chevron-down"
|
||||||
|
></wa-icon>
|
||||||
|
</button>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
</section>
|
</section>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user