Music — Listener Profile (top artists · stats)
A celebratory, data-rich listener profile and year-in-review page for the fictional listener River Halcyon, built in plain HTML, CSS, and vanilla JS. A CSS-drawn gradient banner with an animated conic avatar ring frames a display name, follower and following counts, and a follow toggle that nudges the count live. A 4 weeks, 6 months, and all-time range toggle re-renders the top-artists rank list, animated genre bars, count-up listening stats, and a top-tracks list with hover-play and a live equalizer, all wrapped up by a scrollable public-playlists strip.
MCP
الكود
:root {
--bg: #0b0b0f;
--bg-2: #13131a;
--surface: #1a1a22;
--surface-2: #22222c;
--text: #f4f4f7;
--muted: #a0a0ad;
--line: rgba(255, 255, 255, 0.1);
--line-2: rgba(255, 255, 255, 0.18);
--accent: #1db954;
--accent-2: #8b5cf6;
--accent-3: #ff3d71;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--r-full: 999px;
--shadow: 0 18px 50px -20px rgba(0, 0, 0, 0.7);
--shadow-sm: 0 8px 24px -14px rgba(0, 0, 0, 0.7);
--font-display: "Sora", system-ui, sans-serif;
--font-body: "Inter", system-ui, sans-serif;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: var(--font-body);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.app {
max-width: 1040px;
margin: 0 auto;
padding: 22px 18px 56px;
display: flex;
flex-direction: column;
gap: 22px;
}
/* ===== Buttons ===== */
.btn {
font-family: var(--font-body);
font-weight: 700;
font-size: 0.9rem;
border: 1px solid var(--line);
border-radius: var(--r-full);
padding: 10px 20px;
cursor: pointer;
color: var(--text);
background: var(--surface-2);
transition: transform 0.16s ease, background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
}
.btn:hover {
transform: translateY(-2px);
}
.btn:active {
transform: translateY(0);
}
.btn:focus-visible {
outline: 2px solid var(--accent-2);
outline-offset: 2px;
}
.btn--accent {
background: var(--accent);
border-color: transparent;
color: #042a13;
box-shadow: 0 10px 26px -12px var(--accent);
}
.btn--accent[aria-pressed="true"] {
background: transparent;
color: var(--text);
border-color: var(--line-2);
box-shadow: none;
}
.btn--ghost {
background: transparent;
}
.btn--icon {
padding: 10px;
width: 42px;
height: 42px;
display: grid;
place-items: center;
}
.btn--icon svg {
width: 18px;
height: 18px;
fill: var(--text);
}
/* ===== Profile header ===== */
.profile {
position: relative;
border-radius: var(--r-lg);
overflow: hidden;
border: 1px solid var(--line);
background: var(--bg-2);
box-shadow: var(--shadow);
}
.profile__art {
position: absolute;
inset: 0;
z-index: 0;
}
.orb {
position: absolute;
border-radius: 50%;
filter: blur(46px);
opacity: 0.85;
animation: drift 16s ease-in-out infinite;
}
.orb--a {
width: 360px;
height: 360px;
left: -60px;
top: -120px;
background: var(--accent-2);
}
.orb--b {
width: 320px;
height: 320px;
right: -40px;
top: -90px;
background: var(--accent);
animation-delay: -4s;
}
.orb--c {
width: 260px;
height: 260px;
left: 38%;
top: -60px;
background: var(--accent-3);
opacity: 0.6;
animation-delay: -8s;
}
.grain {
position: absolute;
inset: 0;
background-image: radial-gradient(rgba(255, 255, 255, 0.16) 1px, transparent 1px);
background-size: 4px 4px;
opacity: 0.22;
-webkit-mask-image: linear-gradient(180deg, #000 0%, transparent 78%);
mask-image: linear-gradient(180deg, #000 0%, transparent 78%);
}
.profile::after {
content: "";
position: absolute;
inset: 0;
z-index: 1;
background: linear-gradient(180deg, rgba(11, 11, 15, 0.25) 0%, rgba(11, 11, 15, 0.92) 70%, var(--bg-2) 100%);
}
.profile__body {
position: relative;
z-index: 2;
display: flex;
align-items: flex-end;
gap: 22px;
padding: 64px 28px 28px;
}
.avatar {
position: relative;
flex: 0 0 auto;
width: 132px;
height: 132px;
display: grid;
place-items: center;
}
.avatar__ring {
position: absolute;
inset: 0;
border-radius: 50%;
background: conic-gradient(from 120deg, var(--accent), var(--accent-2), var(--accent-3), var(--accent));
padding: 4px;
-webkit-mask: radial-gradient(circle, transparent 60px, #000 61px);
mask: radial-gradient(circle, transparent 60px, #000 61px);
animation: spin 14s linear infinite;
}
.avatar__face {
width: 116px;
height: 116px;
border-radius: 50%;
display: grid;
place-items: center;
font-family: var(--font-display);
font-weight: 800;
font-size: 2.4rem;
color: var(--text);
background: radial-gradient(120% 120% at 30% 20%, #2b2350, #161029 70%);
box-shadow: inset 0 0 0 1px var(--line-2);
}
.profile__meta {
min-width: 0;
}
.profile__kicker {
font-size: 0.74rem;
font-weight: 700;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--accent);
}
.profile__name {
font-family: var(--font-display);
font-weight: 800;
font-size: clamp(2.2rem, 6vw, 3.6rem);
line-height: 1.04;
margin: 6px 0 4px;
letter-spacing: -0.02em;
}
.profile__handle {
color: var(--muted);
font-size: 0.92rem;
margin: 0 0 16px;
}
.profile__counts {
display: flex;
flex-wrap: wrap;
gap: 26px;
margin-bottom: 18px;
}
.count {
display: inline-flex;
flex-direction: column;
gap: 1px;
border: 0;
background: none;
padding: 0;
text-align: left;
color: inherit;
font-family: inherit;
}
.count--btn {
cursor: pointer;
}
.count--btn:hover .count__num {
color: var(--accent);
}
.count__num {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.32rem;
transition: color 0.2s ease;
}
.count__label {
font-size: 0.78rem;
color: var(--muted);
}
.profile__actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
/* ===== Range toggle ===== */
.range {
position: relative;
display: inline-flex;
gap: 2px;
padding: 5px;
border-radius: var(--r-full);
border: 1px solid var(--line);
background: var(--surface);
align-self: flex-start;
}
.range__btn {
position: relative;
z-index: 1;
border: 0;
background: none;
color: var(--muted);
font-family: inherit;
font-weight: 600;
font-size: 0.86rem;
padding: 8px 18px;
border-radius: var(--r-full);
cursor: pointer;
transition: color 0.2s ease;
}
.range__btn.is-active {
color: var(--text);
}
.range__btn:focus-visible {
outline: 2px solid var(--accent-2);
outline-offset: 2px;
}
.range__pill {
position: absolute;
z-index: 0;
top: 5px;
bottom: 5px;
border-radius: var(--r-full);
background: var(--surface-2);
box-shadow: inset 0 0 0 1px var(--line-2);
transition: left 0.32s cubic-bezier(0.5, 1.4, 0.4, 1), width 0.32s cubic-bezier(0.5, 1.4, 0.4, 1);
}
/* ===== Grid ===== */
.grid {
display: grid;
grid-template-columns: 1.4fr 1fr;
gap: 18px;
align-items: start;
}
.card {
background: linear-gradient(180deg, var(--surface), var(--bg-2));
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 20px;
box-shadow: var(--shadow-sm);
}
.card--artists {
grid-row: span 2;
}
.card__head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
margin-bottom: 14px;
}
.card__title {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.18rem;
margin: 0;
}
.card__hint {
font-size: 0.78rem;
color: var(--muted);
}
/* ===== Top artists ===== */
.artists {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.artist {
display: grid;
grid-template-columns: 26px 52px 1fr auto;
align-items: center;
gap: 14px;
padding: 8px 10px;
border-radius: var(--r-md);
cursor: pointer;
border: 1px solid transparent;
animation: rise 0.4s both;
transition: background 0.18s ease, border-color 0.18s ease;
}
.artist:hover {
background: var(--surface-2);
border-color: var(--line);
}
.artist__rank {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.05rem;
color: var(--muted);
text-align: center;
}
.artist:nth-child(1) .artist__rank {
color: var(--accent);
}
.artist__cover {
position: relative;
width: 52px;
height: 52px;
border-radius: var(--r-md);
overflow: hidden;
box-shadow: inset 0 0 0 1px var(--line);
}
.artist__cover::before {
content: "";
position: absolute;
inset: 0;
background: var(--cv, linear-gradient(135deg, #555, #222));
}
.artist__cover .play {
position: absolute;
inset: 0;
margin: auto;
width: 30px;
height: 30px;
border-radius: 50%;
border: 0;
background: rgba(0, 0, 0, 0.55);
backdrop-filter: blur(2px);
display: grid;
place-items: center;
opacity: 0;
transform: scale(0.8);
transition: opacity 0.18s ease, transform 0.18s ease;
cursor: pointer;
}
.artist:hover .artist__cover .play {
opacity: 1;
transform: scale(1);
}
.artist__cover .play svg {
width: 13px;
height: 13px;
fill: #fff;
}
.artist__name {
font-weight: 600;
font-size: 0.98rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.artist__sub {
font-size: 0.78rem;
color: var(--muted);
}
.artist__plays {
font-variant-numeric: tabular-nums;
font-size: 0.82rem;
color: var(--muted);
text-align: right;
}
/* ===== Stats ===== */
.stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
}
.stat {
background: linear-gradient(180deg, var(--surface), var(--bg-2));
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 18px;
box-shadow: var(--shadow-sm);
display: flex;
flex-direction: column;
gap: 4px;
}
.stat--wide {
grid-column: span 2;
}
.stat__icon {
width: 38px;
height: 38px;
border-radius: var(--r-md);
display: grid;
place-items: center;
margin-bottom: 6px;
}
.stat__icon svg {
width: 20px;
height: 20px;
fill: none;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.stat__icon--mins {
background: rgba(29, 185, 84, 0.16);
}
.stat__icon--mins svg {
stroke: var(--accent);
}
.stat__icon--tracks {
background: rgba(139, 92, 246, 0.16);
}
.stat__icon--tracks svg {
stroke: var(--accent-2);
}
.stat__icon--day {
background: rgba(255, 61, 113, 0.16);
}
.stat__icon--day svg {
stroke: var(--accent-3);
}
.stat__num {
font-family: var(--font-display);
font-weight: 800;
font-size: 1.9rem;
font-variant-numeric: tabular-nums;
letter-spacing: -0.02em;
}
.stat__num--text {
font-size: 1.5rem;
}
.stat__label {
font-size: 0.8rem;
color: var(--muted);
}
/* ===== Genre bars ===== */
.bars {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 14px;
}
.bar__top {
display: flex;
justify-content: space-between;
font-size: 0.86rem;
margin-bottom: 6px;
}
.bar__name {
font-weight: 600;
}
.bar__pct {
color: var(--muted);
font-variant-numeric: tabular-nums;
}
.bar__track {
height: 9px;
border-radius: var(--r-full);
background: var(--surface-2);
overflow: hidden;
box-shadow: inset 0 0 0 1px var(--line);
}
.bar__fill {
height: 100%;
width: 0;
border-radius: var(--r-full);
background: linear-gradient(90deg, var(--accent-2), var(--accent));
transition: width 0.9s cubic-bezier(0.2, 0.8, 0.2, 1);
}
/* ===== Tracks ===== */
.tracks {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.track {
display: grid;
grid-template-columns: 22px 44px 1fr auto auto;
align-items: center;
gap: 12px;
padding: 7px 8px;
border-radius: var(--r-md);
cursor: pointer;
animation: rise 0.4s both;
transition: background 0.16s ease;
}
.track:hover,
.track.is-playing {
background: var(--surface-2);
}
.track__idx {
position: relative;
text-align: center;
font-size: 0.86rem;
color: var(--muted);
font-variant-numeric: tabular-nums;
}
.track:hover .track__idx .num,
.track.is-playing .track__idx .num {
opacity: 0;
}
.track__idx .eq {
position: absolute;
inset: 0;
margin: auto;
width: 16px;
height: 14px;
display: none;
align-items: flex-end;
gap: 2px;
justify-content: center;
}
.track:hover .track__idx .eq,
.track.is-playing .track__idx .eq {
display: flex;
}
.track__idx .eq i {
width: 3px;
background: var(--accent);
border-radius: 2px;
animation: eq 0.9s ease-in-out infinite;
}
.track__idx .eq i:nth-child(1) { animation-delay: -0.2s; }
.track__idx .eq i:nth-child(2) { animation-delay: -0.5s; }
.track__idx .eq i:nth-child(3) { animation-delay: 0s; }
.track:not(.is-playing):hover .eq i { animation-play-state: paused; height: 60%; }
.track__cover {
width: 44px;
height: 44px;
border-radius: var(--r-sm);
background: var(--cv, linear-gradient(135deg, #555, #222));
box-shadow: inset 0 0 0 1px var(--line);
}
.track__info {
min-width: 0;
}
.track__title {
font-weight: 600;
font-size: 0.94rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.track__artist {
font-size: 0.78rem;
color: var(--muted);
}
.track__like {
border: 0;
background: none;
padding: 4px;
cursor: pointer;
display: grid;
place-items: center;
opacity: 0;
transition: opacity 0.16s ease, transform 0.16s ease;
}
.track:hover .track__like,
.track__like[aria-pressed="true"] {
opacity: 1;
}
.track__like svg {
width: 16px;
height: 16px;
fill: none;
stroke: var(--muted);
stroke-width: 2;
}
.track__like[aria-pressed="true"] svg {
fill: var(--accent-3);
stroke: var(--accent-3);
}
.track__like:hover {
transform: scale(1.15);
}
.track__dur {
font-size: 0.82rem;
color: var(--muted);
font-variant-numeric: tabular-nums;
}
/* ===== Playlists strip ===== */
.strip {
display: flex;
gap: 14px;
overflow-x: auto;
padding-bottom: 6px;
scroll-snap-type: x mandatory;
scrollbar-width: thin;
}
.pl {
scroll-snap-align: start;
flex: 0 0 168px;
text-align: left;
border: 0;
background: var(--surface);
border-radius: var(--r-md);
padding: 12px;
cursor: pointer;
color: inherit;
font-family: inherit;
box-shadow: inset 0 0 0 1px var(--line);
transition: transform 0.16s ease, background 0.2s ease;
}
.pl:hover {
transform: translateY(-3px);
background: var(--surface-2);
}
.pl:focus-visible {
outline: 2px solid var(--accent-2);
outline-offset: 2px;
}
.pl__cover {
position: relative;
height: 144px;
border-radius: var(--r-sm);
overflow: hidden;
margin-bottom: 10px;
background: var(--cv, linear-gradient(135deg, #444, #111));
}
.pl__cover::after {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(60% 60% at 70% 25%, rgba(255, 255, 255, 0.22), transparent 60%);
}
.pl__play {
position: absolute;
right: 10px;
bottom: 10px;
width: 38px;
height: 38px;
border-radius: 50%;
background: var(--accent);
display: grid;
place-items: center;
box-shadow: 0 8px 18px -6px rgba(0, 0, 0, 0.8);
opacity: 0;
transform: translateY(8px);
transition: opacity 0.18s ease, transform 0.18s ease;
}
.pl:hover .pl__play {
opacity: 1;
transform: translateY(0);
}
.pl__play svg {
width: 15px;
height: 15px;
fill: #042a13;
}
.pl__title {
font-weight: 700;
font-size: 0.92rem;
}
.pl__meta {
font-size: 0.76rem;
color: var(--muted);
}
.foot {
text-align: center;
color: var(--muted);
font-size: 0.78rem;
padding-top: 8px;
}
/* ===== Toast ===== */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 18px);
background: var(--surface-2);
color: var(--text);
border: 1px solid var(--line-2);
border-radius: var(--r-full);
padding: 11px 20px;
font-size: 0.86rem;
font-weight: 600;
box-shadow: var(--shadow);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
z-index: 50;
}
.toast.is-show {
opacity: 1;
transform: translate(-50%, 0);
}
/* ===== Animations ===== */
@keyframes drift {
0%, 100% { transform: translate(0, 0) scale(1); }
50% { transform: translate(26px, 18px) scale(1.08); }
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes eq {
0%, 100% { height: 30%; }
50% { height: 100%; }
}
@keyframes rise {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
/* ===== Responsive ===== */
@media (max-width: 820px) {
.grid {
grid-template-columns: 1fr;
}
.card--artists {
grid-row: auto;
}
}
@media (max-width: 520px) {
.app {
padding: 16px 12px 48px;
}
.profile__body {
flex-direction: column;
align-items: flex-start;
gap: 16px;
padding: 48px 18px 22px;
}
.avatar {
width: 100px;
height: 100px;
}
.avatar__ring {
-webkit-mask: radial-gradient(circle, transparent 45px, #000 46px);
mask: radial-gradient(circle, transparent 45px, #000 46px);
}
.avatar__face {
width: 88px;
height: 88px;
font-size: 1.9rem;
}
.profile__counts {
gap: 18px;
}
.range {
width: 100%;
justify-content: space-between;
}
.range__btn {
flex: 1;
padding: 8px 6px;
text-align: center;
}
.stats {
grid-template-columns: 1fr;
}
.stat--wide {
grid-column: auto;
}
.artist {
grid-template-columns: 22px 46px 1fr auto;
gap: 10px;
}
.track {
grid-template-columns: 20px 40px 1fr auto;
}
.track__like {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}(function () {
"use strict";
/* ===== Data per time range ===== */
var COVERS = {
teal: "linear-gradient(135deg,#1db954,#0a6e3a)",
violet: "linear-gradient(135deg,#8b5cf6,#3b1f86)",
rose: "linear-gradient(135deg,#ff3d71,#7a1233)",
amber: "linear-gradient(135deg,#ffb347,#b65f00)",
ice: "linear-gradient(135deg,#5ad1ff,#1b4f7a)",
plum: "linear-gradient(135deg,#c46bff,#5a1d7a)",
lime: "linear-gradient(135deg,#9be15d,#2f7a1d)"
};
var DATA = {
"4w": {
hint: "Last 4 weeks",
stats: { mins: 14820, tracks: 3942, day: "Thursday" },
artists: [
{ n: "Neon Tides", g: "Synth-pop", p: 412, c: "teal" },
{ n: "Velvet Static", g: "Dream pop", p: 388, c: "violet" },
{ n: "Paper Lanterns", g: "Indie folk", p: 274, c: "amber" },
{ n: "Glass Coast", g: "Ambient", p: 233, c: "ice" },
{ n: "Midnight Reservoir", g: "Electronica", p: 201, c: "rose" }
],
genres: [
{ n: "Synth-pop", v: 34 },
{ n: "Dream pop", v: 26 },
{ n: "Indie folk", v: 18 },
{ n: "Ambient", v: 13 },
{ n: "Electronica", v: 9 }
],
tracks: [
{ t: "Paper Lanterns", a: "Neon Tides", d: "3:42", c: "teal" },
{ t: "Cobalt Hour", a: "Velvet Static", d: "4:08", c: "violet" },
{ t: "Driftwood", a: "Paper Lanterns", d: "3:15", c: "amber" },
{ t: "Slow Aurora", a: "Glass Coast", d: "5:21", c: "ice" },
{ t: "Reservoir", a: "Midnight Reservoir", d: "3:57", c: "rose" }
]
},
"6m": {
hint: "Last 6 months",
stats: { mins: 86240, tracks: 21405, day: "Saturday" },
artists: [
{ n: "Velvet Static", g: "Dream pop", p: 2104, c: "violet" },
{ n: "Neon Tides", g: "Synth-pop", p: 1987, c: "teal" },
{ n: "Glass Coast", g: "Ambient", p: 1442, c: "ice" },
{ n: "Saffron Echo", g: "Neo-soul", p: 1188, c: "plum" },
{ n: "Paper Lanterns", g: "Indie folk", p: 1043, c: "amber" }
],
genres: [
{ n: "Dream pop", v: 29 },
{ n: "Synth-pop", v: 27 },
{ n: "Ambient", v: 19 },
{ n: "Neo-soul", v: 15 },
{ n: "Indie folk", v: 10 }
],
tracks: [
{ t: "Cobalt Hour", a: "Velvet Static", d: "4:08", c: "violet" },
{ t: "Paper Lanterns", a: "Neon Tides", d: "3:42", c: "teal" },
{ t: "Honeyglass", a: "Saffron Echo", d: "4:33", c: "plum" },
{ t: "Slow Aurora", a: "Glass Coast", d: "5:21", c: "ice" },
{ t: "Tin Roof Rain", a: "Paper Lanterns", d: "3:29", c: "amber" }
]
},
all: {
hint: "All time",
stats: { mins: 612900, tracks: 154820, day: "Sunday" },
artists: [
{ n: "Glass Coast", g: "Ambient", p: 9842, c: "ice" },
{ n: "Velvet Static", g: "Dream pop", p: 9120, c: "violet" },
{ n: "Neon Tides", g: "Synth-pop", p: 8755, c: "teal" },
{ n: "Bramble & Bone", g: "Folk rock", p: 6310, c: "lime" },
{ n: "Saffron Echo", g: "Neo-soul", p: 5988, c: "plum" }
],
genres: [
{ n: "Ambient", v: 31 },
{ n: "Dream pop", v: 24 },
{ n: "Synth-pop", v: 20 },
{ n: "Folk rock", v: 14 },
{ n: "Neo-soul", v: 11 }
],
tracks: [
{ t: "Slow Aurora", a: "Glass Coast", d: "5:21", c: "ice" },
{ t: "Cobalt Hour", a: "Velvet Static", d: "4:08", c: "violet" },
{ t: "Paper Lanterns", a: "Neon Tides", d: "3:42", c: "teal" },
{ t: "Hollow Pines", a: "Bramble & Bone", d: "3:50", c: "lime" },
{ t: "Honeyglass", a: "Saffron Echo", d: "4:33", c: "plum" }
]
}
};
var PLAYLISTS = [
{ t: "Late Drive", n: "62 tracks · 4h 12m", c: "teal" },
{ t: "Soft Focus", n: "38 tracks · 2h 31m", c: "violet" },
{ t: "Folk Porch", n: "51 tracks · 3h 05m", c: "amber" },
{ t: "Deep Blue", n: "44 tracks · 3h 44m", c: "ice" },
{ t: "Heartbeat", n: "29 tracks · 1h 58m", c: "rose" },
{ t: "Golden Hour", n: "47 tracks · 2h 49m", c: "plum" }
];
/* ===== Helpers ===== */
var $ = function (sel, ctx) { return (ctx || document).querySelector(sel); };
var fmt = function (n) { return n.toLocaleString("en-US"); };
var PLAY_SVG = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg>';
var EQ = '<span class="eq" aria-hidden="true"><i></i><i></i><i></i></span>';
var HEART = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 20s-7-4.5-9.5-8A4.5 4.5 0 0 1 12 7a4.5 4.5 0 0 1 9.5 5C19 15.5 12 20 12 20Z"/></svg>';
var toastEl = $("#toast");
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("is-show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { toastEl.classList.remove("is-show"); }, 2200);
}
var reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
/* ===== Count-up ===== */
function countTo(el, target, suffix) {
if (reduceMotion) { el.textContent = fmt(target) + (suffix || ""); return; }
var start = 0;
var dur = 900;
var t0 = performance.now();
function step(now) {
var p = Math.min(1, (now - t0) / dur);
var eased = 1 - Math.pow(1 - p, 3);
el.textContent = fmt(Math.round(target * eased)) + (suffix || "");
if (p < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
}
/* ===== Renderers ===== */
var likeState = {};
function renderArtists(d) {
var list = $("#artists-list");
list.innerHTML = "";
d.artists.forEach(function (a, i) {
var li = document.createElement("li");
li.className = "artist";
li.style.animationDelay = i * 0.05 + "s";
li.innerHTML =
'<span class="artist__rank">' + (i + 1) + "</span>" +
'<span class="artist__cover" style="--cv:' + COVERS[a.c] + '">' +
'<button class="play" type="button" aria-label="Play ' + a.n + '">' + PLAY_SVG + "</button></span>" +
'<span class="artist__info"><span class="artist__name">' + a.n + "</span>" +
'<span class="artist__sub">' + a.g + "</span></span>" +
'<span class="artist__plays">' + fmt(a.p) + " plays</span>";
li.querySelector(".play").addEventListener("click", function (e) {
e.stopPropagation();
toast("Playing top tracks by " + a.n);
});
li.addEventListener("click", function () { toast("Opened " + a.n); });
list.appendChild(li);
});
$("#artists-hint").textContent = d.hint;
}
function renderGenres(d) {
var list = $("#genres-list");
list.innerHTML = "";
d.genres.forEach(function (g) {
var li = document.createElement("li");
li.className = "bar";
li.innerHTML =
'<div class="bar__top"><span class="bar__name">' + g.n + "</span>" +
'<span class="bar__pct">' + g.v + "%</span></div>" +
'<div class="bar__track"><span class="bar__fill"></span></div>';
list.appendChild(li);
var fill = li.querySelector(".bar__fill");
requestAnimationFrame(function () {
requestAnimationFrame(function () { fill.style.width = g.v + "%"; });
});
});
}
function renderTracks(d) {
var list = $("#tracks-list");
list.innerHTML = "";
d.tracks.forEach(function (tr, i) {
var key = tr.t + tr.a;
var liked = !!likeState[key];
var li = document.createElement("li");
li.className = "track";
li.style.animationDelay = i * 0.05 + "s";
li.innerHTML =
'<span class="track__idx"><span class="num">' + (i + 1) + "</span>" + EQ + "</span>" +
'<span class="track__cover" style="--cv:' + COVERS[tr.c] + '"></span>' +
'<span class="track__info"><span class="track__title">' + tr.t + "</span>" +
'<span class="track__artist">' + tr.a + "</span></span>" +
'<button class="track__like" type="button" aria-pressed="' + liked + '" aria-label="Like ' + tr.t + '">' + HEART + "</button>" +
'<span class="track__dur">' + tr.d + "</span>";
var likeBtn = li.querySelector(".track__like");
likeBtn.addEventListener("click", function (e) {
e.stopPropagation();
var on = likeBtn.getAttribute("aria-pressed") !== "true";
likeBtn.setAttribute("aria-pressed", String(on));
likeState[key] = on;
toast(on ? "Liked " + tr.t : "Removed from Liked Songs");
});
li.addEventListener("mouseenter", function () { setPlaying(li, true); });
li.addEventListener("mouseleave", function () { if (!li.classList.contains("pinned")) setPlaying(li, false); });
li.addEventListener("click", function () {
list.querySelectorAll(".track").forEach(function (n) { n.classList.remove("pinned"); setPlaying(n, false); });
li.classList.add("pinned");
setPlaying(li, true);
toast("Now playing · " + tr.t + " — " + tr.a);
});
list.appendChild(li);
});
}
function setPlaying(li, on) {
li.classList.toggle("is-playing", on);
}
/* ===== Stats ===== */
function renderStats(d) {
var nums = document.querySelectorAll(".stat__num[data-count]");
var values = [d.stats.mins, d.stats.tracks];
nums.forEach(function (el, i) {
el.setAttribute("data-count", values[i]);
countTo(el, values[i], el.getAttribute("data-suffix"));
});
$("#top-day").textContent = d.stats.day;
}
/* ===== Playlists (static once) ===== */
function renderPlaylists() {
var strip = $("#playlists-strip");
PLAYLISTS.forEach(function (p) {
var btn = document.createElement("button");
btn.className = "pl";
btn.type = "button";
btn.innerHTML =
'<span class="pl__cover" style="--cv:' + COVERS[p.c] + '">' +
'<span class="pl__play">' + PLAY_SVG + "</span></span>" +
'<span class="pl__title">' + p.t + "</span>" +
'<span class="pl__meta">' + p.n + "</span>";
btn.addEventListener("click", function () { toast("Shuffling " + p.t); });
strip.appendChild(btn);
});
}
/* ===== Range toggle ===== */
var rangeWrap = $(".range");
var pill = $(".range__pill");
function movePill(btn) {
pill.style.left = btn.offsetLeft + "px";
pill.style.width = btn.offsetWidth + "px";
}
function applyRange(key, btn) {
var d = DATA[key];
renderArtists(d);
renderGenres(d);
renderTracks(d);
renderStats(d);
movePill(btn);
}
rangeWrap.querySelectorAll(".range__btn").forEach(function (btn) {
btn.addEventListener("click", function () {
if (btn.classList.contains("is-active")) return;
rangeWrap.querySelectorAll(".range__btn").forEach(function (b) {
b.classList.remove("is-active");
b.setAttribute("aria-selected", "false");
});
btn.classList.add("is-active");
btn.setAttribute("aria-selected", "true");
applyRange(btn.dataset.range, btn);
toast("Showing " + btn.textContent.trim().toLowerCase());
});
});
/* ===== Follow + header actions ===== */
var followBtn = $("#follow-btn");
var followersNum = $("#followers-num");
var baseFollowers = 2184;
followBtn.addEventListener("click", function () {
var on = followBtn.getAttribute("aria-pressed") !== "true";
followBtn.setAttribute("aria-pressed", String(on));
followBtn.textContent = on ? "Following" : "Follow";
followersNum.textContent = fmt(baseFollowers + (on ? 1 : 0));
toast(on ? "You now follow River Halcyon" : "Unfollowed");
});
$("#edit-btn").addEventListener("click", function () { toast("Edit profile (demo)"); });
$("#share-btn").addEventListener("click", function () { toast("Profile link copied"); });
$("#followers-btn").addEventListener("click", function () { toast("2,184 followers"); });
/* ===== Boot ===== */
renderPlaylists();
var firstBtn = rangeWrap.querySelector(".range__btn.is-active");
applyRange("4w", firstBtn);
window.addEventListener("resize", function () {
var active = rangeWrap.querySelector(".range__btn.is-active");
if (active) movePill(active);
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Listener Profile — Year in Review</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&family=Inter:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="app" id="app">
<!-- ===== Profile header ===== -->
<header class="profile" aria-label="Listener profile">
<div class="profile__art" aria-hidden="true">
<span class="orb orb--a"></span>
<span class="orb orb--b"></span>
<span class="orb orb--c"></span>
<span class="grain"></span>
</div>
<div class="profile__body">
<div class="avatar" aria-hidden="true">
<span class="avatar__ring"></span>
<span class="avatar__face">RH</span>
</div>
<div class="profile__meta">
<span class="profile__kicker">Listener Profile</span>
<h1 class="profile__name">River Halcyon</h1>
<p class="profile__handle">@riverhalcyon · Premium member since 2021</p>
<div class="profile__counts">
<button class="count count--btn" id="followers-btn" type="button">
<span class="count__num" id="followers-num">2,184</span>
<span class="count__label">Followers</span>
</button>
<span class="count">
<span class="count__num">317</span>
<span class="count__label">Following</span>
</span>
<span class="count">
<span class="count__num">48</span>
<span class="count__label">Playlists</span>
</span>
</div>
<div class="profile__actions">
<button class="btn btn--accent" id="follow-btn" type="button" aria-pressed="false">
Follow
</button>
<button class="btn btn--ghost" id="edit-btn" type="button">Edit profile</button>
<button class="btn btn--icon" id="share-btn" type="button" aria-label="Share profile">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M18 8a3 3 0 1 0-2.8-4H15a3 3 0 0 0 .2 1.1L8.9 8.2a3 3 0 1 0 0 3.6l6.3 3.1A3 3 0 1 0 18 13a3 3 0 0 0-2.1.9L9.6 10.8a3 3 0 0 0 0-1.6l6.3-3.1A3 3 0 0 0 18 8Z"/></svg>
</button>
</div>
</div>
</div>
</header>
<!-- ===== Time range toggle ===== -->
<div class="range" role="tablist" aria-label="Time range">
<button class="range__btn is-active" role="tab" aria-selected="true" data-range="4w" type="button">4 weeks</button>
<button class="range__btn" role="tab" aria-selected="false" data-range="6m" type="button">6 months</button>
<button class="range__btn" role="tab" aria-selected="false" data-range="all" type="button">All time</button>
<span class="range__pill" aria-hidden="true"></span>
</div>
<div class="grid">
<!-- ===== Top artists ===== -->
<section class="card card--artists" aria-labelledby="artists-h">
<div class="card__head">
<h2 id="artists-h" class="card__title">Top artists this month</h2>
<span class="card__hint" id="artists-hint">Last 4 weeks</span>
</div>
<ol class="artists" id="artists-list"></ol>
</section>
<!-- ===== Stats ===== -->
<section class="stats" aria-label="Listening stats">
<article class="stat">
<span class="stat__icon stat__icon--mins" aria-hidden="true">
<svg viewBox="0 0 24 24"><path d="M12 7v5l3 2M12 21a9 9 0 1 1 0-18 9 9 0 0 1 0 18Z"/></svg>
</span>
<span class="stat__num" data-count="14820" data-suffix="">0</span>
<span class="stat__label">Minutes listened</span>
</article>
<article class="stat">
<span class="stat__icon stat__icon--tracks" aria-hidden="true">
<svg viewBox="0 0 24 24"><path d="M9 18V6l10-2v12M9 18a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm10-2a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"/></svg>
</span>
<span class="stat__num" data-count="3942" data-suffix="">0</span>
<span class="stat__label">Tracks played</span>
</article>
<article class="stat stat--wide">
<span class="stat__icon stat__icon--day" aria-hidden="true">
<svg viewBox="0 0 24 24"><path d="M7 3v3m10-3v3M4 9h16M5 7h14a1 1 0 0 1 1 1v11a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1Z"/></svg>
</span>
<span class="stat__num stat__num--text" id="top-day">Thursday</span>
<span class="stat__label">Top listening day · 9:14 PM peak</span>
</article>
</section>
<!-- ===== Top genres ===== -->
<section class="card card--genres" aria-labelledby="genres-h">
<div class="card__head">
<h2 id="genres-h" class="card__title">Top genres</h2>
<span class="card__hint">Share of plays</span>
</div>
<ul class="bars" id="genres-list"></ul>
</section>
<!-- ===== Top tracks ===== -->
<section class="card card--tracks" aria-labelledby="tracks-h">
<div class="card__head">
<h2 id="tracks-h" class="card__title">Top tracks</h2>
<span class="card__hint" id="tracks-hint">Hover to preview</span>
</div>
<ul class="tracks" id="tracks-list"></ul>
</section>
</div>
<!-- ===== Public playlists ===== -->
<section class="card card--playlists" aria-labelledby="pl-h">
<div class="card__head">
<h2 id="pl-h" class="card__title">Public playlists</h2>
<span class="card__hint">48 total</span>
</div>
<div class="strip" id="playlists-strip"></div>
</section>
<footer class="foot">
Illustrative UI only — fictional artists, albums, tracks, and data. No real audio playback.
</footer>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Listener Profile (top artists · stats)
A streaming-style listener profile and year-in-review page for the fictional listener River Halcyon, rendered entirely in CSS — no images. The header pairs a full-bleed banner of drifting gradient orbs and a halftone grain with a circular avatar wrapped in a slowly rotating conic-gradient ring. Beside the giant display-font name sit a handle, follower / following / playlist counts, and an action row with a Follow toggle (it bumps the follower count and flips to Following), an edit-profile button, and a share button — each confirmed by a small toast() helper.
The body is a responsive grid of cards. A Top artists this month rank list shows CSS cover art with a hover-reveal play button, genre, and play counts; Top genres animates a set of gradient bars from zero on each render; three listening-stats cards count up minutes listened and tracks played and surface the top listening day; and a Top tracks list flips its index into a four-bar animated equalizer on hover or when pinned by a click. A horizontally scrollable public-playlists strip rounds out the page.
Every interaction is vanilla JS. A 4 weeks / 6 months / all time range toggle slides an animated pill and re-renders the top lists, re-animates the genre bars, and replays the stat count-ups with a fresh data set for each window. Per-track like hearts and the follow button toggle their aria-pressed state, the range buttons drive an aria-selected tablist, and motion respects prefers-reduced-motion while the layout reflows cleanly down to ~360px.
Illustrative UI only — fictional artists, albums, tracks, and data. No real audio playback.