diff --git a/db/entities.go b/db/entities.go
index c095631..dc39ef8 100644
--- a/db/entities.go
+++ b/db/entities.go
@@ -7,6 +7,7 @@ import (
"time"
"github.com/jackc/pgtype"
+ "github.com/jackc/pgx/v5"
)
type Artist struct {
@@ -504,6 +505,80 @@ func GetArtistStats(userId, artistId int) (int, error) {
return count, err
}
+type TopArtist struct {
+ Artist Artist
+ ListenCount int
+}
+
+func GetTopArtists(userId int, limit int, startDate, endDate *time.Time) ([]TopArtist, error) {
+ var err error
+ var rows pgx.Rows
+
+ if startDate == nil && endDate == nil {
+ rows, err = Pool.Query(context.Background(),
+ `SELECT a.id, a.user_id, a.name, a.image_url, a.bio, a.spotify_id, a.musicbrainz_id, COUNT(*) as listen_count
+ FROM artists a
+ JOIN history h ON h.user_id = a.user_id AND a.id = ANY(h.artist_ids)
+ WHERE h.user_id = $1
+ GROUP BY a.id
+ ORDER BY listen_count DESC
+ LIMIT $2`,
+ userId, limit)
+ } else if startDate != nil && endDate == nil {
+ rows, err = Pool.Query(context.Background(),
+ `SELECT a.id, a.user_id, a.name, a.image_url, a.bio, a.spotify_id, a.musicbrainz_id, COUNT(*) as listen_count
+ FROM artists a
+ JOIN history h ON h.user_id = a.user_id AND a.id = ANY(h.artist_ids)
+ WHERE h.user_id = $1 AND h.timestamp >= $2
+ GROUP BY a.id
+ ORDER BY listen_count DESC
+ LIMIT $3`,
+ userId, startDate, limit)
+ } else if startDate == nil && endDate != nil {
+ rows, err = Pool.Query(context.Background(),
+ `SELECT a.id, a.user_id, a.name, a.image_url, a.bio, a.spotify_id, a.musicbrainz_id, COUNT(*) as listen_count
+ FROM artists a
+ JOIN history h ON h.user_id = a.user_id AND a.id = ANY(h.artist_ids)
+ WHERE h.user_id = $1 AND h.timestamp <= $2
+ GROUP BY a.id
+ ORDER BY listen_count DESC
+ LIMIT $3`,
+ userId, endDate, limit)
+ } else {
+ rows, err = Pool.Query(context.Background(),
+ `SELECT a.id, a.user_id, a.name, a.image_url, a.bio, a.spotify_id, a.musicbrainz_id, COUNT(*) as listen_count
+ FROM artists a
+ JOIN history h ON h.user_id = a.user_id AND a.id = ANY(h.artist_ids)
+ WHERE h.user_id = $1 AND h.timestamp >= $2 AND h.timestamp <= $3
+ GROUP BY a.id
+ ORDER BY listen_count DESC
+ LIMIT $4`,
+ userId, startDate, endDate, limit)
+ }
+
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var topArtists []TopArtist
+ for rows.Next() {
+ var a Artist
+ var count int
+ var imageUrlPg, bioPg, spotifyIdPg, musicbrainzIdPg pgtype.Text
+ err := rows.Scan(&a.Id, &a.UserId, &a.Name, &imageUrlPg, &bioPg, &spotifyIdPg, &musicbrainzIdPg, &count)
+ if err != nil {
+ return nil, err
+ }
+ a.ImageUrl = imageUrlPg.String
+ a.Bio = bioPg.String
+ a.SpotifyId = spotifyIdPg.String
+ a.MusicbrainzId = musicbrainzIdPg.String
+ topArtists = append(topArtists, TopArtist{Artist: a, ListenCount: count})
+ }
+ return topArtists, nil
+}
+
func GetSongStats(userId, songId int) (int, error) {
var count int
err := Pool.QueryRow(context.Background(),
diff --git a/static/profile.js b/static/profile.js
new file mode 100644
index 0000000..44a0d50
--- /dev/null
+++ b/static/profile.js
@@ -0,0 +1,60 @@
+function updateTopArtists() {
+ const period = document.getElementById('period-select').value;
+ const limit = document.getElementById('limit-select').value;
+ const view = document.getElementById('view-select').value;
+
+ const customDates = document.getElementById('custom-dates');
+ if (period === 'custom') {
+ customDates.style.display = 'inline-block';
+ } else {
+ customDates.style.display = 'none';
+ }
+
+ const params = new URLSearchParams(window.location.search);
+ params.set('period', period);
+ params.set('limit', limit);
+ params.set('view', view);
+
+ if (period === 'custom') {
+ const startDate = document.getElementById('start-date').value;
+ const endDate = document.getElementById('end-date').value;
+ if (startDate) params.set('start', startDate);
+ if (endDate) params.set('end', endDate);
+ }
+
+ window.location.search = params.toString();
+}
+
+function updateLimitOptions() {
+ const view = document.getElementById('view-select').value;
+ const limitSelect = document.getElementById('limit-select');
+ const maxLimit = view === 'grid' ? 8 : 30;
+
+ for (let option of limitSelect.options) {
+ const value = parseInt(option.value);
+ if (value > maxLimit) {
+ option.style.display = 'none';
+ } else {
+ option.style.display = '';
+ }
+ }
+
+ if (parseInt(limitSelect.value) > maxLimit) {
+ limitSelect.value = maxLimit;
+ }
+}
+
+function syncGridHeights() {}
+
+document.addEventListener('DOMContentLoaded', function() {
+ const customDates = document.getElementById('custom-dates');
+ const periodSelect = document.getElementById('period-select');
+
+ if (periodSelect && customDates) {
+ if (periodSelect.value === 'custom') {
+ customDates.style.display = 'inline-block';
+ }
+ }
+
+ updateLimitOptions();
+});
diff --git a/static/style.css b/static/style.css
index da2be00..b386932 100644
--- a/static/style.css
+++ b/static/style.css
@@ -760,3 +760,450 @@ a.button:hover {
margin: 0;
line-height: 1.6;
}
+
+/* Top Artists Section */
+.top-artists {
+ width: 50%;
+ margin: 15px 0;
+ padding: 10px;
+ background: #1a1a1a;
+ border-radius: 10px;
+}
+
+.top-artists-controls {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin-bottom: 10px;
+}
+
+#top-artists-display {
+ min-height: 150px;
+}
+
+.top-artists-controls h3 {
+ margin: 0;
+ color: #fff;
+}
+
+.controls-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ align-items: center;
+ max-width: 100%;
+}
+
+.controls-row label {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: #888;
+ font-size: 14px;
+}
+
+.controls-row select {
+ padding: 6px 10px;
+ border: 1px solid #444;
+ border-radius: 4px;
+ background: #333;
+ color: #AFA;
+ font-size: 14px;
+ cursor: pointer;
+}
+
+.controls-row select:hover {
+ border-color: #AFA;
+}
+
+.controls-row input[type="date"] {
+ padding: 6px 10px;
+ border: 1px solid #444;
+ border-radius: 4px;
+ background: #333;
+ color: #AFA;
+ font-size: 14px;
+}
+
+/* Artist Grid - New layout */
+.artist-grid {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.artist-grid-odd {
+ flex-direction: row;
+}
+
+.artist-grid-odd .artist-cell-first {
+ aspect-ratio: 1;
+ flex: 1;
+}
+
+.artist-grid-odd .artist-right-col {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.artist-row {
+ display: flex;
+ gap: 8px;
+ flex: 1;
+}
+
+.artist-grid-odd .artist-row {
+ flex: 1;
+}
+
+.artist-cell {
+ flex: 1;
+ display: block;
+ background: #2a2a2a;
+ transition: background 0.2s;
+ transition: transform 0.1s ease;
+ border-radius: 8px;
+ min-width: 0;
+}
+
+.artist-cell-first {
+ display: block;
+ transition: background 0.2s;
+ transition: transform 0.1s ease;
+ border-radius: 8px;
+}
+
+.artist-cell a,
+.artist-cell-first a {
+ display: block;
+ text-decoration: none;
+}
+
+.artist-cell,
+.artist-cell-first {
+ position: relative;
+ aspect-ratio: 1;
+}
+
+.artist-cell .grid-items-cover-image,
+.artist-cell-first .grid-items-cover-image {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+}
+
+.artist-cell .grid-items-cover-image-image,
+.artist-cell-first .grid-items-cover-image-image {
+ position: relative;
+ width: 100%;
+ height: 100%;
+}
+
+.artist-cell .grid-items-cover-image-image img,
+.artist-cell-first .grid-items-cover-image-image img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
+ border-radius: 10px;
+}
+
+.artist-cell .artist-placeholder,
+.artist-cell-first .artist-placeholder {
+ width: 100%;
+ height: 100%;
+ background: #333;
+ border-radius: 10px;
+}
+
+.artist-cell:hover .grid-items-cover-image-image img,
+.artist-cell-first:hover .grid-items-cover-image-image img,
+.artist-cell:hover .artist-placeholder,
+.artist-cell-first:hover .artist-placeholder {
+ opacity: 0.9;
+}
+
+.artist-cell .grid-items-item-details,
+.artist-cell-first .grid-items-item-details {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ padding: 8px;
+ background: linear-gradient(#0000008c, rgba(0,0,0,0.8));
+ box-sizing: border-box;
+ border-radius: 0 0 10px 10px;
+}
+
+.artist-cell .grid-items-item-main-text,
+.artist-cell-first .grid-items-item-main-text {
+ color: #fff;
+ font-weight: bold;
+ font-size: 14px;
+ margin: 0;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.artist-cell .grid-items-item-aux-text,
+.artist-cell-first .grid-items-item-aux-text {
+ color: #aaa;
+ font-size: 12px;
+ margin: 0;
+}
+
+/* List View */
+.artist-list {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+ overflow: hidden;
+ text-decoration: none;
+}
+
+.grid-large .artist-card-large img,
+.grid-large .artist-card-large .artist-placeholder-large {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.grid-large .small-group {
+ display: contents;
+}
+
+.grid-large .small-row {
+ display: contents;
+}
+
+.grid-large .artist-card-small {
+ position: relative;
+ border-radius: 8px;
+ overflow: hidden;
+ text-decoration: none;
+}
+
+.grid-large .artist-card-small img,
+.grid-large .artist-card-small .artist-placeholder-small {
+ width: 100%;
+ aspect-ratio: 1;
+ object-fit: cover;
+ display: block;
+}
+}
+
+.artist-placeholder-large {
+ width: 100%;
+ height: 100%;
+ min-height: 300px;
+ background: #333;
+ border-radius: 8px;
+}
+
+.grid-small-group {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.artist-card-large,
+.artist-card-small {
+ display: block;
+ position: relative;
+ border-radius: 8px;
+ overflow: hidden;
+ text-decoration: none;
+}
+
+.artist-card-large img,
+.artist-card-small img {
+ width: 100%;
+ height: 100px;
+ object-fit: cover;
+ transition: transform 0.2s;
+}
+
+.artist-card-large:hover img,
+.artist-card-small:hover img {
+ transform: scale(1.05);
+}
+
+.artist-card-large img,
+.artist-card-small img {
+ width: 100%;
+ height: 200px;
+ object-fit: cover;
+ transition: transform 0.2s;
+}
+
+.artist-card-large:hover img,
+.artist-card-small:hover img {
+ transform: scale(1.05);
+}
+
+.artist-placeholder-small {
+ width: 100%;
+ height: 150px;
+ background: #333;
+ border-radius: 8px;
+}
+
+.artist-info {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ padding: 12px 6px 4px;
+ background: linear-gradient(transparent, rgba(0,0,0,0.9));
+ display: flex;
+ flex-direction: column;
+ gap: 1px;
+}
+
+.artist-name {
+ color: #fff;
+ font-weight: bold;
+ font-size: 9px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.artist-count {
+ color: #AFA;
+ font-size: 8px;
+}
+
+/* Grid View - Even */
+.grid-even {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 7px;
+}
+
+.grid-even .small-row {
+ display: contents;
+}
+
+.grid-even .artist-card-small {
+ position: relative;
+ border-radius: 8px;
+ overflow: hidden;
+ text-decoration: none;
+}
+
+.grid-even .artist-card-small img,
+.grid-even .artist-card-small .artist-placeholder-small {
+ width: 100%;
+ aspect-ratio: 1;
+ object-fit: cover;
+ display: block;
+}
+
+.grid-even .artist-card-small img,
+.grid-even .artist-card-small .artist-placeholder-small {
+ width: 100%;
+ height: 100px;
+ object-fit: cover;
+}
+
+/* List View */
+.artist-list {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.artist-row {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 6px;
+ border-radius: 8px;
+ text-decoration: none;
+}
+
+.artist-cell:hover {
+ background: #333;
+ transform: scale(0.98);
+}
+
+.artist-cell-first:hover {
+ background: #333;
+ transform: scale(0.98);
+}
+
+.artist-row img {
+ width: 35px;
+ height: 35px;
+ object-fit: cover;
+ border-radius: 50%;
+ flex-shrink: 0;
+}
+
+.artist-placeholder-row {
+ width: 35px;
+ height: 35px;
+ background: #444;
+ border-radius: 50%;
+ flex-shrink: 0;
+}
+
+.artist-row .artist-name {
+ flex: 1;
+ text-align: left;
+ font-size: 12px;
+}
+
+.artist-row .artist-count {
+ text-align: right;
+ font-size: 11px;
+}
+
+/* Responsive Artist Grid */
+@media (max-width: 600px) {
+ .artist-grid {
+ max-width: 100%;
+ }
+
+ .artist-grid-odd {
+ flex-direction: column;
+ }
+
+ .artist-grid-odd .artist-cell-first {
+ width: 100%;
+ max-width: 300px;
+ margin: 0 auto;
+ }
+
+ .artist-grid-odd .artist-right-col {
+ flex-direction: row;
+ }
+
+ .artist-row {
+ flex-wrap: wrap;
+ }
+
+ .artist-cell {
+ min-width: calc(50% - 4px);
+ }
+}
+
+@media (max-width: 400px) {
+ .artist-grid-odd .artist-right-col {
+ flex-direction: column;
+ }
+
+ .artist-cell {
+ min-width: 100%;
+ }
+}
diff --git a/templates/base.gohtml b/templates/base.gohtml
index 4bc10b0..61fe3f8 100644
--- a/templates/base.gohtml
+++ b/templates/base.gohtml
@@ -55,6 +55,9 @@
{{ if eq .TemplateName "album"}}{{block "album" .}}{{end}}{{end}}
+ {{if eq .TemplateName "profile"}}
+
+ {{end}}