Skip to content

Commit cfa2875

Browse files
committed
statistics page
1 parent 21ea545 commit cfa2875

File tree

3 files changed

+217
-87
lines changed

3 files changed

+217
-87
lines changed

main.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@ func notNull(s string) sql.NullString {
285285
}
286286

287287
type TemplatePlaylist struct {
288+
ID string
288289
Name string
289290
Url string
290291
}
@@ -293,7 +294,7 @@ func mapPlaylists(playlists []db.Playlist) []TemplatePlaylist {
293294
result := make([]TemplatePlaylist, 0, len(playlists))
294295
for _, playlist := range playlists {
295296
if playlist.Name.Valid && playlist.Url.Valid {
296-
result = append(result, TemplatePlaylist{Name: playlist.Name.String, Url: playlist.Url.String})
297+
result = append(result, TemplatePlaylist{ID: playlist.ID, Name: playlist.Name.String, Url: playlist.Url.String})
297298
}
298299
}
299300
return result

stats.go

Lines changed: 4 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
package main
22

33
import (
4-
"context"
54
"fmt"
6-
"maps"
75
"net/http"
8-
"slices"
96

10-
"github.com/bafto/FindFavouriteSong/db"
117
"github.com/gin-gonic/gin"
128
)
139

@@ -19,58 +15,12 @@ func statsPageHandler(c *gin.Context) {
1915
}
2016
defer tx.Rollback()
2117

22-
winners, err := getWinnerMap(c, queries, user.ID)
18+
logger.Debug("fetching user playlists", "user", user.ID)
19+
playlists, err := queries.GetPlaylistsForUser(c, user.ID)
2320
if err != nil {
24-
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("failed to get winners from db: %w", err))
21+
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("error fetching user playlists: %w", err))
2522
return
2623
}
27-
logger.Debug("retrieved winners from DB", "n-winners", len(winners))
2824

29-
slices.SortFunc(winners, func(a, b Winner) int {
30-
return int(b.N - a.N)
31-
})
32-
33-
c.HTML(http.StatusOK, "stats.gohtml", winners)
34-
}
35-
36-
type Winner struct {
37-
Title string
38-
Artists string
39-
Image string
40-
N int64
41-
}
42-
43-
func getWinnerMap(ctx context.Context, queries *db.Queries, userID string) ([]Winner, error) {
44-
allWinnerIDs, err := queries.GetAllWinnersForUser(ctx, userID)
45-
if err != nil {
46-
return nil, fmt.Errorf("failed to get all winner IDs from DB: %w", err)
47-
}
48-
49-
winners := map[string]Winner{}
50-
51-
for _, id := range allWinnerIDs {
52-
if winner, ok := winners[id.String]; ok {
53-
winners[id.String] = Winner{
54-
Title: winner.Title,
55-
Artists: winner.Artists,
56-
Image: winner.Image,
57-
N: winner.N + 1,
58-
}
59-
continue
60-
}
61-
62-
dbWinner, err := queries.GetPlaylistItem(ctx, id.String)
63-
if err != nil {
64-
return nil, fmt.Errorf("failed to get winner for ID %s from DB: %w", id.String, err)
65-
}
66-
67-
winners[id.String] = Winner{
68-
Title: dbWinner.Title.String,
69-
Artists: dbWinner.Artists.String,
70-
Image: dbWinner.Image.String,
71-
N: 1,
72-
}
73-
}
74-
75-
return slices.Collect(maps.Values(winners)), nil
25+
c.HTML(http.StatusOK, "stats.gohtml", mapPlaylists(playlists))
7626
}

stats.gohtml

Lines changed: 211 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,160 @@
22
<html>
33

44
<head>
5+
<meta charset="UTF-8">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
57
<title>Find Favourite Song</title>
8+
<style>
9+
.button {
10+
background: linear-gradient(135deg, #ffffff, #f3f3f3);
11+
/* Subtle gradient */
12+
border: none;
13+
padding: 12px 20px;
14+
margin-bottom: 25px;
15+
font-size: 16px;
16+
font-weight: bold;
17+
color: #333;
18+
background: linear-gradient(135deg, #4a78ff, #6b94ff);
19+
border-radius: 4px;
20+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
21+
cursor: pointer;
22+
transition: all 0.2s ease-in-out;
23+
}
24+
25+
.button:hover {
26+
background: linear-gradient(135deg, #6b94ff, #4a78ff);
27+
transform: scale(1.05);
28+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
29+
}
30+
31+
.button:active {
32+
transform: scale(0.98);
33+
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
34+
}
35+
36+
37+
body {
38+
font-family: Arial, sans-serif;
39+
background-color: #f4f4f4;
40+
margin: 0;
41+
padding: 20px;
42+
display: flex;
43+
flex-direction: column;
44+
align-items: center;
45+
}
46+
47+
.playlist-container {
48+
max-width: 600px;
49+
width: 100%;
50+
}
51+
52+
.playlist {
53+
background: white;
54+
border-radius: 10px;
55+
margin-bottom: 10px;
56+
padding: 15px;
57+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
58+
cursor: pointer;
59+
transition: transform 0.2s ease-in-out;
60+
}
61+
62+
/*
63+
.playlist:hover {
64+
transform: scale(1.02);
65+
}
66+
*/
67+
68+
.playlist h2 {
69+
margin: 0;
70+
font-size: 18px;
71+
color: #333;
72+
}
73+
74+
.playlist-content {
75+
display: none;
76+
padding-top: 10px;
77+
}
78+
79+
#new_statistics {
80+
display: flex;
81+
flex-direction: column;
82+
gap: 20px;
83+
padding: 20px;
84+
max-width: 600px;
85+
margin: auto;
86+
}
87+
88+
.points-group {
89+
margin: 15px;
90+
background: #f9f9f9;
91+
border-radius: 10px;
92+
padding: 15px;
93+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
94+
}
95+
96+
.points-title {
97+
font-size: 20px;
98+
font-weight: bold;
99+
color: #333;
100+
margin-bottom: 10px;
101+
text-align: center;
102+
}
103+
104+
.song-list {
105+
display: flex;
106+
flex-direction: column;
107+
gap: 10px;
108+
}
109+
110+
.song-item {
111+
background: white;
112+
border-radius: 8px;
113+
padding: 10px;
114+
display: flex;
115+
align-items: center;
116+
gap: 10px;
117+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
118+
transition: transform 0.2s ease-in-out;
119+
}
120+
121+
/*
122+
.song-item:hover {
123+
transform: scale(1.02);
124+
}
125+
*/
126+
127+
.song-wrapper {
128+
display: flex;
129+
align-items: center;
130+
gap: 15px;
131+
width: 100%;
132+
}
133+
134+
.song-image {
135+
width: 60px;
136+
height: 60px;
137+
border-radius: 8px;
138+
object-fit: cover;
139+
}
140+
141+
.song-details {
142+
display: flex;
143+
flex-direction: column;
144+
}
145+
146+
.song-title {
147+
font-size: 16px;
148+
font-weight: bold;
149+
color: #222;
150+
margin: 0;
151+
}
152+
153+
.song-artists {
154+
font-size: 14px;
155+
color: #666;
156+
margin: 0;
157+
}
158+
</style>
6159
<script>
7160
async function fill_in_new_stats(playlist_id) {
8161
const resp = await fetch(`/api/playlist_statistics?playlist=${playlist_id}`);
@@ -14,55 +167,81 @@
14167
const json = await resp.json();
15168
console.log(json);
16169

17-
const points_map = Map.groupBy(json, ({ points }) => points);
18-
const new_stats = document.getElementById('new_statistics');
19-
[...points_map.keys()].sort((a, b) => a-b).reverse().forEach(points => {
170+
const points_map = Map.groupBy(json, ({points}) => points);
171+
const new_stats = document.getElementById(`new_statistics_${playlist_id}`);
172+
173+
const colors = ["#ff4d4d", "#ff944d", "#ffd24d", "#99cc66", "#66b3ff", "#8c66ff"];
174+
175+
function getColor(points) {
176+
const index = Math.min(Math.floor(points / 10), colors.length - 1);
177+
return colors[index];
178+
}
179+
180+
[...points_map.keys()].sort((a, b) => a - b).reverse().forEach(points => {
20181
const points_div = document.createElement('div');
21-
const points_h2 = document.createElement('h2');
22-
points_h2.innerText = `${points}`;
182+
points_div.classList.add('points-group');
183+
points_div.style.backgroundColor = getColor(points)
23184

185+
const points_h2 = document.createElement('h2');
186+
points_h2.innerText = `${points} Points`;
187+
points_h2.classList.add('points-title');
188+
points_h2.style.backgroundColor = getColor(points);
24189
points_div.appendChild(points_h2);
25190

191+
const song_list = document.createElement('div');
192+
song_list.classList.add('song-list');
193+
26194
for (const item of points_map.get(points)) {
27-
item_div = document.createElement('div');
195+
const item_div = document.createElement('div');
196+
item_div.classList.add('song-item');
197+
28198
item_div.innerHTML = `
29-
<div class="wrapper">
30-
<div class="left">
31-
<img src="${item.image}" />
32-
</div>
33-
<div class="right">
34-
<h3>${item.title}</h3>
35-
<h4>${item.artists}</h4>
36-
</div>
37-
</div>
38-
`;
39-
points_div.appendChild(item_div);
199+
<div class="song-wrapper">
200+
<img class="song-image" src="${item.image}" alt="${item.title}">
201+
<div class="song-details">
202+
<h3 class="song-title">${item.title}</h3>
203+
<h4 class="song-artists">${item.artists}</h4>
204+
</div>
205+
</div>
206+
`;
207+
208+
song_list.appendChild(item_div);
40209
}
41210

211+
points_div.appendChild(song_list);
42212
new_stats.appendChild(points_div);
43213
});
44214
}
215+
216+
const alreadyFetched = new Map();
217+
218+
function togglePlaylist(playlistId) {
219+
const contentDiv = document.getElementById(`playlist-${playlistId}`);
220+
if (contentDiv.style.display === "block") {
221+
contentDiv.style.display = "none";
222+
} else if (!alreadyFetched.get(playlistId)) {
223+
contentDiv.style.display = "block";
224+
fill_in_new_stats(playlistId);
225+
alreadyFetched.set(playlistId, true);
226+
} else {
227+
contentDiv.style.display = "block";
228+
}
229+
}
45230
</script>
46231
</head>
47232

48233
<body>
49234
<main>
50-
<button onclick="window.location.href = '/';">Select New Playlist</button>
51-
<h1>Winners</h1>
52-
{{ range . }}
53-
<div class="wrapper">
54-
<div class="left">
55-
<img src="{{ .Image }}" />
56-
</div>
57-
<div class="right">
58-
<h3>{{ .Title }}</h3>
59-
<h4>{{ .Artists }}</h4>
60-
<h4>{{ .N }}</h4>
235+
<div class="playlist-container">
236+
<button class="button" onclick="window.location.href = '/';">Select New Playlist</button>
237+
{{ range . }}
238+
<div class="playlist" onclick="togglePlaylist('{{ .ID }}')">
239+
<h2>{{ .Name }}</h2>
240+
<div id="playlist-{{ .ID }}" class="playlist-content">
241+
<div id="new_statistics_{{ .ID }}"></div>
242+
</div>
61243
</div>
62-
</div>
63-
{{ end }}
64-
<h1>New Statistics</h1>
65-
<div id="new_statistics">
244+
{{ end }}
66245
</div>
67246
</main>
68247
</body>

0 commit comments

Comments
 (0)