@rmdes/indiekit-endpoint-lastfm 1.0.6 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,224 @@
1
+ /* Last.fm endpoint styles */
2
+
3
+ /* Settings form */
4
+ .lastfm-form {
5
+ display: flex;
6
+ flex-direction: column;
7
+ gap: var(--space-m);
8
+ }
9
+
10
+ .lastfm-field {
11
+ display: flex;
12
+ flex-direction: column;
13
+ gap: var(--space-3xs);
14
+ }
15
+
16
+ .lastfm-field label {
17
+ font-weight: var(--font-weight-semibold);
18
+ }
19
+
20
+ .lastfm-field__hint {
21
+ color: var(--color-text-secondary);
22
+ font-size: var(--step--1);
23
+ }
24
+
25
+ .lastfm-field input {
26
+ appearance: none;
27
+ background-color: var(--color-background);
28
+ border: 1px solid var(--color-border);
29
+ border-radius: var(--radius-s);
30
+ font-size: var(--step--1);
31
+ padding: var(--space-2xs) var(--space-s);
32
+ width: 100%;
33
+ }
34
+
35
+ .lastfm-field input:focus {
36
+ border-color: var(--color-accent);
37
+ outline: 2px solid var(--color-accent);
38
+ outline-offset: 1px;
39
+ }
40
+
41
+ /* Stats grid */
42
+ .lastfm-stats {
43
+ display: grid;
44
+ gap: var(--space-s);
45
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
46
+ }
47
+
48
+ .lastfm-stat {
49
+ align-items: center;
50
+ background: var(--color-offset);
51
+ border-radius: var(--radius-m);
52
+ display: flex;
53
+ flex-direction: column;
54
+ padding: var(--space-s);
55
+ text-align: center;
56
+ }
57
+
58
+ .lastfm-stat__value {
59
+ color: var(--color-accent);
60
+ font-size: var(--step-2);
61
+ font-weight: var(--font-weight-bold);
62
+ }
63
+
64
+ .lastfm-stat__label {
65
+ color: var(--color-text-secondary);
66
+ font-size: var(--step--2);
67
+ letter-spacing: 0.05em;
68
+ text-transform: uppercase;
69
+ }
70
+
71
+ /* Featured track */
72
+ .lastfm-featured {
73
+ align-items: flex-start;
74
+ background: var(--color-offset);
75
+ border-radius: var(--radius-m);
76
+ display: flex;
77
+ gap: var(--space-m);
78
+ padding: var(--space-s);
79
+ }
80
+
81
+ .lastfm-featured img {
82
+ border-radius: var(--radius-s);
83
+ flex-shrink: 0;
84
+ height: 80px;
85
+ object-fit: cover;
86
+ width: 80px;
87
+ }
88
+
89
+ .lastfm-featured__info {
90
+ flex: 1;
91
+ }
92
+
93
+ .lastfm-featured__title {
94
+ color: inherit;
95
+ display: block;
96
+ font-weight: var(--font-weight-semibold);
97
+ text-decoration: none;
98
+ }
99
+
100
+ .lastfm-featured__title:hover {
101
+ text-decoration: underline;
102
+ }
103
+
104
+ .lastfm-featured__album {
105
+ color: var(--color-text-secondary);
106
+ font-size: var(--step--1);
107
+ margin: var(--space-3xs) 0;
108
+ }
109
+
110
+ .lastfm-meta {
111
+ color: var(--color-text-secondary);
112
+ display: flex;
113
+ font-size: var(--step--2);
114
+ gap: var(--space-xs);
115
+ }
116
+
117
+ /* Now playing animation */
118
+ .lastfm-playing {
119
+ align-items: center;
120
+ color: #d51007;
121
+ display: inline-flex;
122
+ gap: var(--space-xs);
123
+ }
124
+
125
+ .lastfm-bars {
126
+ align-items: flex-end;
127
+ display: flex;
128
+ gap: 2px;
129
+ height: 16px;
130
+ }
131
+
132
+ .lastfm-bars span {
133
+ animation: lastfm-bar 0.5s infinite ease-in-out alternate;
134
+ background: currentColor;
135
+ width: 3px;
136
+ }
137
+
138
+ .lastfm-bars span:nth-child(2) {
139
+ animation-delay: 0.2s;
140
+ }
141
+
142
+ .lastfm-bars span:nth-child(3) {
143
+ animation-delay: 0.4s;
144
+ }
145
+
146
+ @keyframes lastfm-bar {
147
+ from { height: 4px; }
148
+ to { height: 16px; }
149
+ }
150
+
151
+ /* Loved indicator */
152
+ .lastfm-loved {
153
+ color: #d51007;
154
+ font-size: var(--step--1);
155
+ }
156
+
157
+ /* List */
158
+ .lastfm-list {
159
+ list-style: none;
160
+ margin: 0;
161
+ padding: 0;
162
+ }
163
+
164
+ .lastfm-list__item {
165
+ align-items: center;
166
+ border-block-end: 1px solid var(--color-border);
167
+ display: flex;
168
+ gap: var(--space-s);
169
+ padding: var(--space-s) 0;
170
+ }
171
+
172
+ .lastfm-list__item:last-child {
173
+ border-block-end: none;
174
+ }
175
+
176
+ .lastfm-list__item img {
177
+ border-radius: var(--radius-s);
178
+ flex-shrink: 0;
179
+ height: 48px;
180
+ object-fit: cover;
181
+ width: 48px;
182
+ }
183
+
184
+ .lastfm-list__placeholder {
185
+ background: var(--color-border);
186
+ border-radius: var(--radius-s);
187
+ flex-shrink: 0;
188
+ height: 48px;
189
+ width: 48px;
190
+ }
191
+
192
+ .lastfm-list__info {
193
+ flex: 1;
194
+ min-width: 0;
195
+ }
196
+
197
+ .lastfm-list__title {
198
+ color: inherit;
199
+ display: block;
200
+ font-weight: var(--font-weight-medium);
201
+ overflow: hidden;
202
+ text-decoration: none;
203
+ text-overflow: ellipsis;
204
+ white-space: nowrap;
205
+ }
206
+
207
+ .lastfm-list__title:hover {
208
+ text-decoration: underline;
209
+ }
210
+
211
+ /* Public link */
212
+ .lastfm-public-link {
213
+ align-items: center;
214
+ display: flex;
215
+ flex-wrap: wrap;
216
+ gap: var(--space-m);
217
+ justify-content: space-between;
218
+ }
219
+
220
+ .lastfm-public-link p {
221
+ color: var(--color-text-secondary);
222
+ font-size: var(--step--1);
223
+ margin: 0;
224
+ }
@@ -3,6 +3,22 @@ import { getEffectiveConfig } from "../config.js";
3
3
  import { runSync, getCachedStats, refreshStatsCache } from "../sync.js";
4
4
  import * as utils from "../utils.js";
5
5
 
6
+ /**
7
+ * Extract and clear flash messages from session
8
+ * Returns { success, error } for Indiekit's native notificationBanner
9
+ */
10
+ function consumeFlashMessage(request) {
11
+ const result = {};
12
+ if (request.session?.messages?.length) {
13
+ const msg = request.session.messages[0];
14
+ if (msg.type === "success") result.success = msg.content;
15
+ else if (msg.type === "error" || msg.type === "warning")
16
+ result.error = msg.content;
17
+ request.session.messages = null;
18
+ }
19
+ return result;
20
+ }
21
+
6
22
  /**
7
23
  * Dashboard controller
8
24
  */
@@ -27,6 +43,9 @@ export const dashboardController = {
27
43
  const config = await getEffectiveConfig(db, lastfmConfig);
28
44
  const { apiKey, username } = config;
29
45
 
46
+ // Extract flash messages for native Indiekit notification banner
47
+ const flash = consumeFlashMessage(request);
48
+
30
49
  // If no credentials, show settings form only
31
50
  if (!apiKey || !username) {
32
51
  return response.render("lastfm", {
@@ -34,6 +53,7 @@ export const dashboardController = {
34
53
  configError: response.__("lastfm.error.noConfig"),
35
54
  settings: { apiKey: "", username: "" },
36
55
  mountPath: request.baseUrl,
56
+ ...flash,
37
57
  });
38
58
  }
39
59
 
@@ -75,6 +95,7 @@ export const dashboardController = {
75
95
  configError: response.__("lastfm.error.connection"),
76
96
  settings: { apiKey, username },
77
97
  mountPath: request.baseUrl,
98
+ ...flash,
78
99
  });
79
100
  }
80
101
 
@@ -105,9 +126,7 @@ export const dashboardController = {
105
126
  publicUrl,
106
127
  mountPath: request.baseUrl,
107
128
  settings: { apiKey, username },
108
- synced: request.query.synced,
109
- saved: request.query.saved,
110
- queryError: request.query.error,
129
+ ...flash,
111
130
  });
112
131
  } catch (error) {
113
132
  console.error("[Last.fm] Dashboard error:", error);
@@ -144,12 +163,16 @@ export const dashboardController = {
144
163
  );
145
164
 
146
165
  console.log("[Last.fm] Settings saved");
147
- response.redirect(request.baseUrl + "?saved=true");
166
+ request.session.messages = [
167
+ { type: "success", content: request.__("lastfm.settingsSaved") },
168
+ ];
169
+ response.redirect(request.baseUrl);
148
170
  } catch (error) {
149
171
  console.error("[Last.fm] Settings save error:", error);
150
- response.redirect(
151
- request.baseUrl + "?error=" + encodeURIComponent(error.message),
152
- );
172
+ request.session.messages = [
173
+ { type: "error", content: error.message },
174
+ ];
175
+ response.redirect(request.baseUrl);
153
176
  }
154
177
  },
155
178
 
@@ -176,17 +199,21 @@ export const dashboardController = {
176
199
  const syncOptions = { ...lastfmConfig, ...config };
177
200
 
178
201
  // Build a minimal Indiekit-like object for runSync
179
- const result = await runSync(
180
- { database: db },
181
- syncOptions,
182
- );
202
+ const result = await runSync({ database: db }, syncOptions);
183
203
 
184
- response.redirect(request.baseUrl + "?synced=" + (result.synced || 0));
204
+ request.session.messages = [
205
+ {
206
+ type: "success",
207
+ content: `Synced ${result.synced || 0} new scrobbles`,
208
+ },
209
+ ];
210
+ response.redirect(request.baseUrl);
185
211
  } catch (error) {
186
212
  console.error("[Last.fm] Manual sync error:", error);
187
- response.redirect(
188
- request.baseUrl + "?error=" + encodeURIComponent(error.message),
189
- );
213
+ request.session.messages = [
214
+ { type: "error", content: error.message },
215
+ ];
216
+ response.redirect(request.baseUrl);
190
217
  }
191
218
  },
192
219
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-lastfm",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "Last.fm scrobble and listening activity endpoint for Indiekit. Display listening history, loved tracks, and statistics.",
5
5
  "keywords": [
6
6
  "indiekit",
@@ -35,6 +35,7 @@
35
35
  ".": "./index.js"
36
36
  },
37
37
  "files": [
38
+ "assets",
38
39
  "includes",
39
40
  "lib",
40
41
  "locales",
package/views/lastfm.njk CHANGED
@@ -1,414 +1,156 @@
1
- {% extends "document.njk" %}
1
+ {% extends "layouts/lastfm.njk" %}
2
2
 
3
- {% block content %}
4
- <style>
5
- .lfm-dashboard {
6
- display: flex;
7
- flex-direction: column;
8
- gap: var(--space-xl, 2rem);
9
- }
10
-
11
- .lfm-section {
12
- background: var(--color-offset, #f5f5f5);
13
- border-radius: var(--border-radius-small, 0.5rem);
14
- padding: var(--space-m, 1.5rem);
15
- }
16
-
17
- .lfm-section h2 {
18
- font: var(--font-heading, bold 1.25rem/1.4 sans-serif);
19
- margin-block-end: var(--space-s, 0.75rem);
20
- padding-block-end: var(--space-xs, 0.5rem);
21
- border-block-end: 1px solid var(--color-outline-variant, #ddd);
22
- }
23
-
24
- .lfm-section p.lfm-hint {
25
- color: var(--color-on-offset, #666);
26
- font: var(--font-caption, 0.875rem/1.4 sans-serif);
27
- margin-block-end: var(--space-m, 1rem);
28
- }
29
-
30
- /* Notifications */
31
- .lfm-notification {
32
- border-radius: var(--border-radius-small, 0.25rem);
33
- padding: var(--space-s, 0.75rem) var(--space-m, 1rem);
34
- margin-block-end: var(--space-s, 0.75rem);
35
- }
36
- .lfm-notification--success {
37
- background: var(--color-success, #d4edda);
38
- color: var(--color-on-success, #155724);
39
- }
40
- .lfm-notification--error {
41
- background: var(--color-error, #f8d7da);
42
- color: var(--color-on-error, #721c24);
43
- }
44
-
45
- /* Settings form */
46
- .lfm-form {
47
- display: flex;
48
- flex-direction: column;
49
- gap: var(--space-m, 1rem);
50
- }
51
- .lfm-field {
52
- display: flex;
53
- flex-direction: column;
54
- gap: var(--space-2xs, 0.25rem);
55
- }
56
- .lfm-field label {
57
- font: var(--font-label, bold 0.875rem/1.4 sans-serif);
58
- }
59
- .lfm-field .lfm-field-hint {
60
- color: var(--color-on-offset, #666);
61
- font: var(--font-caption, 0.875rem/1.4 sans-serif);
62
- }
63
- .lfm-field input {
64
- appearance: none;
65
- background-color: var(--color-background, #fff);
66
- border: 1px solid var(--color-outline-variant, #ccc);
67
- border-radius: var(--border-radius-small, 0.25rem);
68
- font: var(--font-body, 0.875rem/1.4 sans-serif);
69
- padding: calc(var(--space-s, 0.75rem) / 2) var(--space-s, 0.75rem);
70
- width: 100%;
71
- }
72
- .lfm-field input:focus {
73
- border-color: var(--color-primary, #0066cc);
74
- outline: 2px solid var(--color-primary, #0066cc);
75
- outline-offset: 1px;
76
- }
77
-
78
- /* Stats grid */
79
- .lfm-stats-grid {
80
- display: grid;
81
- grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
82
- gap: var(--space-s, 0.75rem);
83
- }
84
- .lfm-stat {
85
- display: flex;
86
- flex-direction: column;
87
- align-items: center;
88
- padding: var(--space-s, 0.75rem);
89
- background: var(--color-background, #fff);
90
- border-radius: var(--border-radius-small, 0.5rem);
91
- text-align: center;
92
- }
93
- .lfm-stat-value {
94
- font-size: 1.5rem;
95
- font-weight: 700;
96
- color: var(--color-accent, #d51007);
97
- }
98
- .lfm-stat-label {
99
- font-size: 0.75rem;
100
- color: var(--color-on-offset, #666);
101
- text-transform: uppercase;
102
- letter-spacing: 0.05em;
103
- }
104
-
105
- /* Featured track */
106
- .lfm-featured {
107
- display: flex;
108
- gap: 1rem;
109
- padding: var(--space-s, 0.75rem);
110
- background: var(--color-background, #fff);
111
- border-radius: var(--border-radius-small, 0.5rem);
112
- align-items: flex-start;
113
- }
114
- .lfm-featured img {
115
- width: 80px;
116
- height: 80px;
117
- object-fit: cover;
118
- border-radius: 0.25rem;
119
- flex-shrink: 0;
120
- }
121
- .lfm-featured-info { flex: 1; }
122
- .lfm-featured-title {
123
- font-weight: 600;
124
- text-decoration: none;
125
- color: inherit;
126
- display: block;
127
- }
128
- .lfm-featured-title:hover { text-decoration: underline; }
129
- .lfm-featured-album {
130
- margin: 0.25rem 0;
131
- color: var(--color-on-offset, #666);
132
- font-size: 0.875rem;
133
- }
134
- .lfm-meta {
135
- display: flex;
136
- gap: 0.5rem;
137
- color: var(--color-on-offset, #666);
138
- font-size: 0.75rem;
139
- }
140
-
141
- /* Now playing animation */
142
- .lfm-playing-status {
143
- display: inline-flex;
144
- align-items: center;
145
- gap: 0.5rem;
146
- color: #d51007;
147
- }
148
- .lfm-bars {
149
- display: flex;
150
- gap: 2px;
151
- align-items: flex-end;
152
- height: 16px;
153
- }
154
- .lfm-bars span {
155
- width: 3px;
156
- background: currentColor;
157
- animation: lfm-bar 0.5s infinite ease-in-out alternate;
158
- }
159
- .lfm-bars span:nth-child(2) { animation-delay: 0.2s; }
160
- .lfm-bars span:nth-child(3) { animation-delay: 0.4s; }
161
- @keyframes lfm-bar {
162
- from { height: 4px; }
163
- to { height: 16px; }
164
- }
165
-
166
- /* Loved indicator */
167
- .lfm-loved {
168
- color: #d51007;
169
- font-size: 0.875rem;
170
- }
171
-
172
- /* List */
173
- .lfm-list {
174
- list-style: none;
175
- padding: 0;
176
- margin: 0;
177
- }
178
- .lfm-list-item {
179
- display: flex;
180
- align-items: center;
181
- gap: 0.75rem;
182
- padding: 0.75rem 0;
183
- border-bottom: 1px solid var(--color-outline-variant, #eee);
184
- }
185
- .lfm-list-item:last-child { border-bottom: none; }
186
- .lfm-list-item img {
187
- width: 48px;
188
- height: 48px;
189
- object-fit: cover;
190
- border-radius: 0.25rem;
191
- flex-shrink: 0;
192
- }
193
- .lfm-list-item-placeholder {
194
- width: 48px;
195
- height: 48px;
196
- background: var(--color-outline-variant, #ddd);
197
- border-radius: 0.25rem;
198
- flex-shrink: 0;
199
- }
200
- .lfm-list-info {
201
- flex: 1;
202
- min-width: 0;
203
- }
204
- .lfm-list-title {
205
- display: block;
206
- font-weight: 500;
207
- text-decoration: none;
208
- color: inherit;
209
- white-space: nowrap;
210
- overflow: hidden;
211
- text-overflow: ellipsis;
212
- }
213
- .lfm-list-title:hover { text-decoration: underline; }
214
-
215
- /* Public link */
216
- .lfm-public-link {
217
- display: flex;
218
- align-items: center;
219
- justify-content: space-between;
220
- gap: var(--space-m, 1rem);
221
- flex-wrap: wrap;
222
- }
223
- .lfm-public-link p {
224
- margin: 0;
225
- color: var(--color-on-offset, #666);
226
- font: var(--font-caption, 0.875rem/1.4 sans-serif);
227
- }
228
- </style>
229
-
230
- {% if saved %}
231
- <div class="lfm-notification lfm-notification--success">
232
- {{ __("lastfm.settingsSaved") }}
233
- </div>
234
- {% endif %}
235
-
236
- {% if synced %}
237
- <div class="lfm-notification lfm-notification--success">
238
- Synced {{ synced }} new scrobbles
239
- </div>
240
- {% endif %}
241
-
242
- {% if queryError %}
243
- <div class="lfm-notification lfm-notification--error">
244
- {{ queryError }}
245
- </div>
246
- {% endif %}
247
-
248
- {% if configError %}
249
- <div class="lfm-notification lfm-notification--error">
250
- {{ configError }}
251
- </div>
252
- {% endif %}
253
-
254
- <div class="lfm-dashboard">
3
+ {% block lastfm %}
255
4
  {# Settings Section #}
256
- <section class="lfm-section">
257
- <h2>{{ __("lastfm.settings") }}</h2>
258
- <p class="lfm-hint">{{ __("lastfm.settingsHelp") }}</p>
259
- <form method="post" action="{{ mountPath }}/settings" class="lfm-form">
260
- <div class="lfm-field">
5
+ {% call section({ title: __("lastfm.settings") }) %}
6
+ <p class="lastfm-field__hint">{{ __("lastfm.settingsHelp") }}</p>
7
+ <form method="post" action="{{ mountPath }}/settings" class="lastfm-form">
8
+ <div class="lastfm-field">
261
9
  <label for="apiKey">{{ __("lastfm.apiKey") }}</label>
262
- <span class="lfm-field-hint" id="apiKey-hint">{{ __("lastfm.apiKeyHelp") }}</span>
10
+ <span class="lastfm-field__hint" id="apiKey-hint">{{ __("lastfm.apiKeyHelp") }}</span>
263
11
  <input type="password" id="apiKey" name="apiKey" value="{{ settings.apiKey }}" aria-describedby="apiKey-hint" placeholder="Your Last.fm API key" autocomplete="off">
264
12
  </div>
265
- <div class="lfm-field">
13
+ <div class="lastfm-field">
266
14
  <label for="username">{{ __("lastfm.username") }}</label>
267
- <span class="lfm-field-hint" id="username-hint">{{ __("lastfm.usernameHelp") }}</span>
15
+ <span class="lastfm-field__hint" id="username-hint">{{ __("lastfm.usernameHelp") }}</span>
268
16
  <input type="text" id="username" name="username" value="{{ settings.username }}" aria-describedby="username-hint" placeholder="Last.fm username">
269
17
  </div>
270
18
  <div>
271
- <button type="submit" class="button button--primary">
272
- {{ __("lastfm.saveSettings") }}
273
- </button>
19
+ {{ button({
20
+ type: "submit",
21
+ text: __("lastfm.saveSettings")
22
+ }) }}
274
23
  </div>
275
24
  </form>
276
- </section>
25
+ {% endcall %}
277
26
 
278
27
  {% if not configError %}
279
28
  {# Now Playing / Recently Played #}
280
29
  {% if nowPlaying %}
281
- <section class="lfm-section">
282
- <h2>
283
- {% if nowPlaying.status == "now-playing" %}
284
- <span class="lfm-playing-status">
285
- <span class="lfm-bars">
286
- <span></span><span></span><span></span>
287
- </span>
288
- {{ __("lastfm.nowPlaying") }}
289
- </span>
290
- {% else %}
291
- {{ __("lastfm.recentlyPlayed") }}
292
- {% endif %}
293
- </h2>
294
- <article class="lfm-featured">
30
+ {% call section({ title: nowPlaying.status == "now-playing" and __("lastfm.nowPlaying") or __("lastfm.recentlyPlayed") }) %}
31
+ {% if nowPlaying.status == "now-playing" %}
32
+ <p class="lastfm-playing">
33
+ <span class="lastfm-bars">
34
+ <span></span><span></span><span></span>
35
+ </span>
36
+ {{ __("lastfm.nowPlaying") }}
37
+ </p>
38
+ {% endif %}
39
+ <article class="lastfm-featured">
295
40
  {% if nowPlaying.coverUrl %}
296
41
  <img src="{{ nowPlaying.coverUrl }}" alt="" loading="lazy">
297
42
  {% endif %}
298
- <div class="lfm-featured-info">
299
- <a href="{{ nowPlaying.trackUrl }}" class="lfm-featured-title" target="_blank" rel="noopener">
43
+ <div class="lastfm-featured__info">
44
+ <a href="{{ nowPlaying.trackUrl }}" class="lastfm-featured__title" target="_blank" rel="noopener">
300
45
  {{ nowPlaying.artist }} - {{ nowPlaying.track }}
301
46
  </a>
302
47
  {% if nowPlaying.album %}
303
- <p class="lfm-featured-album">{{ nowPlaying.album }}</p>
48
+ <p class="lastfm-featured__album">{{ nowPlaying.album }}</p>
304
49
  {% endif %}
305
- <small class="lfm-meta">
306
- {% if nowPlaying.loved %}<span class="lfm-loved">&#9829;</span>{% endif %}
50
+ <small class="lastfm-meta">
51
+ {% if nowPlaying.loved %}<span class="lastfm-loved">&#9829;</span>{% endif %}
307
52
  <span>{{ nowPlaying.relativeTime }}</span>
308
53
  </small>
309
54
  </div>
310
55
  </article>
311
- </section>
56
+ {% endcall %}
312
57
  {% endif %}
313
58
 
314
59
  {# Quick Stats #}
315
60
  {% if hasStats %}
316
- <section class="lfm-section">
317
- <h2>{{ __("lastfm.stats") }}</h2>
318
- <div class="lfm-stats-grid">
319
- <div class="lfm-stat">
320
- <span class="lfm-stat-value">{{ totalPlays }}</span>
321
- <span class="lfm-stat-label">{{ __("lastfm.plays") }}</span>
61
+ {% call section({ title: __("lastfm.stats") }) %}
62
+ <div class="lastfm-stats">
63
+ <div class="lastfm-stat">
64
+ <span class="lastfm-stat__value">{{ totalPlays }}</span>
65
+ <span class="lastfm-stat__label">{{ __("lastfm.plays") }}</span>
322
66
  </div>
323
- <div class="lfm-stat">
324
- <span class="lfm-stat-value">{{ uniqueTracks }}</span>
325
- <span class="lfm-stat-label">{{ __("lastfm.tracks") }}</span>
67
+ <div class="lastfm-stat">
68
+ <span class="lastfm-stat__value">{{ uniqueTracks }}</span>
69
+ <span class="lastfm-stat__label">{{ __("lastfm.tracks") }}</span>
326
70
  </div>
327
- <div class="lfm-stat">
328
- <span class="lfm-stat-value">{{ uniqueArtists }}</span>
329
- <span class="lfm-stat-label">{{ __("lastfm.artists") }}</span>
71
+ <div class="lastfm-stat">
72
+ <span class="lastfm-stat__value">{{ uniqueArtists }}</span>
73
+ <span class="lastfm-stat__label">{{ __("lastfm.artists") }}</span>
330
74
  </div>
331
75
  </div>
332
- </section>
76
+ {% endcall %}
333
77
  {% endif %}
334
78
 
335
79
  {# Recent Scrobbles #}
336
80
  {% if scrobbles and scrobbles.length > 0 %}
337
- <section class="lfm-section">
338
- <h2>{{ __("lastfm.scrobbles") }}</h2>
339
- <ul class="lfm-list">
81
+ {% call section({ title: __("lastfm.scrobbles") }) %}
82
+ <ul class="lastfm-list">
340
83
  {% for scrobble in scrobbles %}
341
- <li class="lfm-list-item">
84
+ <li class="lastfm-list__item">
342
85
  {% if scrobble.coverUrl %}
343
86
  <img src="{{ scrobble.coverUrl }}" alt="" loading="lazy">
344
87
  {% else %}
345
- <div class="lfm-list-item-placeholder"></div>
88
+ <div class="lastfm-list__placeholder"></div>
346
89
  {% endif %}
347
- <div class="lfm-list-info">
348
- <a href="{{ scrobble.trackUrl }}" class="lfm-list-title" target="_blank" rel="noopener">
90
+ <div class="lastfm-list__info">
91
+ <a href="{{ scrobble.trackUrl }}" class="lastfm-list__title" target="_blank" rel="noopener">
349
92
  {{ scrobble.artist }} - {{ scrobble.track }}
350
93
  </a>
351
- <small class="lfm-meta">
352
- {% if scrobble.loved %}<span class="lfm-loved">&#9829;</span>{% endif %}
94
+ <small class="lastfm-meta">
95
+ {% if scrobble.loved %}<span class="lastfm-loved">&#9829;</span>{% endif %}
353
96
  {{ scrobble.relativeTime }}
354
97
  </small>
355
98
  </div>
356
99
  </li>
357
100
  {% endfor %}
358
101
  </ul>
359
- </section>
102
+ {% endcall %}
360
103
  {% endif %}
361
104
 
362
105
  {# Loved Tracks #}
363
106
  {% if lovedTracks and lovedTracks.length > 0 %}
364
- <section class="lfm-section">
365
- <h2>{{ __("lastfm.loved") }}</h2>
366
- <ul class="lfm-list">
107
+ {% call section({ title: __("lastfm.loved") }) %}
108
+ <ul class="lastfm-list">
367
109
  {% for track in lovedTracks %}
368
- <li class="lfm-list-item">
110
+ <li class="lastfm-list__item">
369
111
  {% if track.coverUrl %}
370
112
  <img src="{{ track.coverUrl }}" alt="" loading="lazy">
371
113
  {% else %}
372
- <div class="lfm-list-item-placeholder"></div>
114
+ <div class="lastfm-list__placeholder"></div>
373
115
  {% endif %}
374
- <div class="lfm-list-info">
375
- <a href="{{ track.trackUrl }}" class="lfm-list-title" target="_blank" rel="noopener">
116
+ <div class="lastfm-list__info">
117
+ <a href="{{ track.trackUrl }}" class="lastfm-list__title" target="_blank" rel="noopener">
376
118
  {{ track.artist }} - {{ track.track }}
377
119
  </a>
378
- <small class="lfm-meta">
379
- <span class="lfm-loved">&#9829;</span>
120
+ <small class="lastfm-meta">
121
+ <span class="lastfm-loved">&#9829;</span>
380
122
  {{ track.relativeTime }}
381
123
  </small>
382
124
  </div>
383
125
  </li>
384
126
  {% endfor %}
385
127
  </ul>
386
- </section>
128
+ {% endcall %}
387
129
  {% endif %}
388
130
 
389
131
  {# Actions #}
390
- <section class="lfm-section">
391
- <h2>{{ __("lastfm.actions") }}</h2>
392
- <div class="button-group">
393
- <form method="post" action="{{ mountPath }}/sync" style="display: inline;">
394
- <button type="submit" class="button button--primary">
395
- {{ __("lastfm.sync") }}
396
- </button>
397
- </form>
398
- </div>
399
- </section>
132
+ {% call section({ title: __("lastfm.actions") }) %}
133
+ <form method="post" action="{{ mountPath }}/sync">
134
+ {{ button({
135
+ type: "submit",
136
+ text: __("lastfm.sync")
137
+ }) }}
138
+ </form>
139
+ {% endcall %}
400
140
 
401
141
  {# Public Page Link #}
402
142
  {% if publicUrl %}
403
- <section class="lfm-section">
404
- <div class="lfm-public-link">
143
+ {% call section({ title: __("lastfm.widget.title") if __("lastfm.widget.title") else "Public page" }) %}
144
+ <div class="lastfm-public-link">
405
145
  <p>{{ __("lastfm.widget.description") }}</p>
406
- <a href="{{ publicUrl }}" class="button button--secondary" target="_blank">
407
- {{ __("lastfm.widget.view") }}
408
- </a>
146
+ {{ button({
147
+ classes: "button--secondary",
148
+ href: publicUrl,
149
+ text: __("lastfm.widget.view"),
150
+ target: "_blank"
151
+ }) }}
409
152
  </div>
410
- </section>
153
+ {% endcall %}
411
154
  {% endif %}
412
155
  {% endif %}
413
- </div>
414
156
  {% endblock %}
@@ -0,0 +1,6 @@
1
+ {% extends "document.njk" %}
2
+
3
+ {% block content %}
4
+ <link rel="stylesheet" href="/assets/@rmdes-indiekit-endpoint-lastfm/styles.css">
5
+ {% block lastfm %}{% endblock %}
6
+ {% endblock %}