+116
-81
src/App.tsx
+116
-81
src/App.tsx
···
4
createResource,
5
createSignal,
6
For,
7
Show,
8
Suspense,
9
} from "solid-js";
···
20
21
export default () => {
22
const [settings, setSettings] = createSignal<Settings>({
23
-
username: "karitham",
24
range: "this_month",
25
count: 10,
26
});
27
const [artistFetcher] = makeCache(
28
-
(set: Settings) => artists(set.username, undefined, set.range, set.count),
29
{
30
storage: localStorage,
31
sourceHash(source) {
···
35
);
36
const [artistRes] = createResource(settings, artistFetcher);
37
const [releasesFetcher] = makeCache(
38
-
(set: Settings) => releases(set.username, undefined, set.range, set.count),
39
{
40
storage: localStorage,
41
sourceHash(source) {
···
72
range: e.target.value as Range,
73
}));
74
}}
75
-
class="p-4 rounded-lg bg-gray-800 text-white focus:outline-none focus:ring-2 focus:ring-green-500 w-full sm:w-auto"
76
>
77
{ranges.map((r) => (
78
-
<option value={r}>{r}</option>
79
))}
80
</select>
81
</div>
82
<Suspense
83
fallback={<p class="text-center text-gray-400">Loading...</p>}
84
>
85
-
<div>
86
<div>
87
-
<h2 class="text-2xl font-semibold my-6 text-center">
88
-
Top Artists
89
-
</h2>
90
-
<Artists artists={artistRes()?.artists || []} />
91
</div>
92
-
<div>
93
-
<h2 class="text-2xl font-semibold my-6 text-center">
94
-
Top Albums
95
-
</h2>
96
-
<ReleaseGroups groups={releasesRes()?.releases || []} />
97
-
</div>
98
-
</div>
99
</Suspense>
100
</div>
101
</main>
···
106
return (
107
<div class="flex flex-wrap justify-center gap-4">
108
<For each={props.artists}>
109
-
{(artist) => <ArtistsItem artist={artist} />}
110
</For>
111
</div>
112
);
113
};
114
115
-
const ArtistsItem: Component<{ artist: ArtistStatsArtist }> = (props) => {
116
const [imageFetcher] = makeCache(
117
async (artist: ArtistStatsArtist) => {
118
-
if (!artist.artist_mbid) {
119
-
return null;
120
-
}
121
-
122
return await getWikidataURL(artist.artist_mbid).then((s) =>
123
s ? getWikidataThumbnail(s) : null,
124
);
125
},
126
-
{
127
-
storage: localStorage,
128
-
},
129
);
130
-
const [image] = createResource(props.artist, imageFetcher);
131
132
return (
133
-
<div class="group relative w-40 p-4 rounded-lg bg-gray-900 transition-all duration-300 hover:bg-gray-800 cursor-pointer">
134
-
<Show
135
-
when={!image.loading}
136
-
fallback={
137
-
<div class="relative w-32 h-32 mx-auto rounded-full mb-4 bg-gray-700 animate-pulse"></div>
138
-
}
139
-
>
140
-
<img
141
-
src={
142
-
image() ||
143
-
`https://placehold.co/100x100/000000/ffffff?text=${props.artist.artist_name}`
144
-
}
145
-
alt={`Thumbnail for ${props.artist.artist_name}`}
146
-
class="w-32 h-32 mx-auto object-cover rounded-full shadow-lg mb-4 transition-all duration-300 group-hover:scale-105"
147
-
/>
148
-
</Show>
149
-
<p class="text-white text-center font-bold text-base truncate mb-1">
150
-
<a
151
-
target="_blank"
152
-
href={`${MB_API_URL}/artist/${props.artist.artist_mbid}`}
153
-
>
154
-
{props.artist.artist_name}
155
-
</a>
156
-
</p>
157
-
<p class="text-gray-500 text-base truncate mb-1">
158
-
{props.artist.listen_count} listens
159
-
</p>
160
-
</div>
161
);
162
};
163
164
-
const ReleaseGroups: Component<{ groups: Release[] }> = (props) => {
165
return (
166
<div class="flex flex-wrap justify-center gap-4">
167
<For each={props.groups}>
168
-
{(group) => <ReleaseGroupItem release={group} />}
169
</For>
170
</div>
171
);
172
};
173
174
-
const ReleaseGroupItem: Component<{ release: Release }> = (props) => {
175
const [imageFetcher] = makeCache(
176
async (release: Release) => {
177
-
if (!release.caa_release_mbid) {
178
-
return null;
179
-
}
180
const result = await getReleaseImageURL(
181
"release",
182
release.caa_release_mbid,
183
);
184
return result[0]?.image.replace("http://", "https://");
185
},
186
-
{
187
-
storage: localStorage,
188
-
},
189
);
190
-
const [image] = createResource(props.release, imageFetcher);
191
192
return (
193
<div class="group relative w-40 p-4 rounded-lg bg-gray-900 transition-all duration-300 hover:bg-gray-800 cursor-pointer">
194
<Show
195
-
when={!image.loading}
196
fallback={
197
-
<div class="relative w-32 h-32 mx-auto rounded-full mb-4 bg-gray-700 animate-pulse"></div>
198
}
199
>
200
<img
201
-
src={
202
-
image() ||
203
-
`https://placehold.co/100x100/000000/ffffff?text=${props.release.release_name}`
204
-
}
205
-
alt={`Cover for ${props.release.release_name}`}
206
-
class="w-32 h-32 mx-auto object-cover rounded-full shadow-lg mb-4 transition-all duration-300 group-hover:scale-105"
207
/>
208
</Show>
209
<p class="text-white text-center font-bold text-base truncate mb-1">
210
-
<a
211
-
target="_blank"
212
-
href={`${MB_API_URL}/release/${props.release.caa_release_mbid}`}
213
-
>
214
-
{props.release.release_name}
215
</a>
216
</p>
217
-
<p class="text-gray-500 text-base truncate mb-1">
218
-
{props.release.listen_count} listens
219
-
</p>
220
</div>
221
);
222
};
···
4
createResource,
5
createSignal,
6
For,
7
+
type JSXElement,
8
+
type Resource,
9
Show,
10
Suspense,
11
} from "solid-js";
···
22
23
export default () => {
24
const [settings, setSettings] = createSignal<Settings>({
25
+
username: "",
26
range: "this_month",
27
count: 10,
28
});
29
const [artistFetcher] = makeCache(
30
+
(set: Settings) => {
31
+
if (set.username)
32
+
return artists(set.username, undefined, set.range, set.count);
33
+
},
34
{
35
storage: localStorage,
36
sourceHash(source) {
···
40
);
41
const [artistRes] = createResource(settings, artistFetcher);
42
const [releasesFetcher] = makeCache(
43
+
(set: Settings) => {
44
+
if (set.username)
45
+
return releases(set.username, undefined, set.range, set.count);
46
+
},
47
{
48
storage: localStorage,
49
sourceHash(source) {
···
80
range: e.target.value as Range,
81
}));
82
}}
83
+
class="p-4 rounded-lg bg-gray-800 text-white focus:outline-none focus:ring-2 focus:ring-green-500 w-full sm:w-auto capitalize"
84
>
85
{ranges.map((r) => (
86
+
<option value={r} class="capitalize">
87
+
{r.split("_").join(" ")}
88
+
</option>
89
))}
90
</select>
91
</div>
92
<Suspense
93
fallback={<p class="text-center text-gray-400">Loading...</p>}
94
>
95
+
<Show
96
+
when={artistRes()}
97
+
fallback={
98
+
<p class="text-center text-gray-400">
99
+
Waiting for a valid username...
100
+
</p>
101
+
}
102
+
>
103
<div>
104
+
<div>
105
+
<h2 class="text-2xl font-semibold my-6 text-center">
106
+
Top Artists
107
+
</h2>
108
+
<Artists artists={artistRes()?.artists || []} />
109
+
</div>
110
+
<div>
111
+
<h2 class="text-2xl font-semibold my-6 text-center">
112
+
Top Albums
113
+
</h2>
114
+
<Releases groups={releasesRes()?.releases || []} />
115
+
</div>
116
</div>
117
+
</Show>
118
</Suspense>
119
</div>
120
</main>
···
125
return (
126
<div class="flex flex-wrap justify-center gap-4">
127
<For each={props.artists}>
128
+
{(artist) => <ArtistsItem item={artist} />}
129
</For>
130
</div>
131
);
132
};
133
134
+
export const ArtistsItem: Component<{ item: ArtistStatsArtist }> = (props) => {
135
const [imageFetcher] = makeCache(
136
async (artist: ArtistStatsArtist) => {
137
+
if (!artist.artist_mbid) return null;
138
return await getWikidataURL(artist.artist_mbid).then((s) =>
139
s ? getWikidataThumbnail(s) : null,
140
);
141
},
142
+
{ storage: localStorage },
143
);
144
+
const [image] = createResource(() => props.item, imageFetcher);
145
146
return (
147
+
<CardItem
148
+
imageUrl={image()}
149
+
imageFallbackUrl={`https://placehold.co/100x100/000000/ffffff?text=${props.item.artist_name}`}
150
+
imageLoading={image.loading}
151
+
title={props.item.artist_name}
152
+
subtitle={`${props.item.listen_count} listens`}
153
+
linkUrl={`${MB_API_URL}/artist/${props.item.artist_mbid}`}
154
+
isCircularImage={true}
155
+
/>
156
);
157
};
158
159
+
const Releases: Component<{ groups: Release[] }> = (props) => {
160
return (
161
<div class="flex flex-wrap justify-center gap-4">
162
<For each={props.groups}>
163
+
{(release) => <ReleaseItem item={release} />}
164
</For>
165
</div>
166
);
167
};
168
169
+
export const ReleaseItem: Component<{ item: Release }> = (props) => {
170
const [imageFetcher] = makeCache(
171
async (release: Release) => {
172
+
if (!release.caa_release_mbid) return null;
173
const result = await getReleaseImageURL(
174
"release",
175
release.caa_release_mbid,
176
);
177
return result[0]?.image.replace("http://", "https://");
178
},
179
+
{ storage: localStorage },
180
+
);
181
+
const [image] = createResource(() => props.item, imageFetcher);
182
+
183
+
return (
184
+
<CardItem
185
+
imageUrl={image()}
186
+
imageFallbackUrl={`https://placehold.co/100x100/000000/ffffff?text=${props.item.release_name}`}
187
+
imageLoading={image.loading}
188
+
title={props.item.release_name}
189
+
subtitle={`${props.item.listen_count} listens`}
190
+
linkUrl={`${MB_API_URL}/release/${props.item.caa_release_mbid}`}
191
+
/>
192
);
193
+
};
194
195
+
type CardSectionProps<T> = {
196
+
title: string;
197
+
resource: Resource<T[] | undefined>;
198
+
ItemComponent: (props: { item: T }) => JSXElement;
199
+
fallbackMessage: string;
200
+
};
201
+
202
+
export const CardSection = <T,>(props: CardSectionProps<T>) => {
203
+
return (
204
+
<div>
205
+
<h2 class="text-2xl font-semibold my-6 text-center">{props.title}</h2>
206
+
<Show
207
+
when={(props.resource()?.length || 0) > 0}
208
+
fallback={
209
+
<p class="text-center text-gray-400">{props.fallbackMessage}</p>
210
+
}
211
+
>
212
+
<div class="flex flex-wrap justify-center gap-4">
213
+
<For each={props.resource()}>
214
+
{(item) => <props.ItemComponent item={item} />}
215
+
</For>
216
+
</div>
217
+
</Show>
218
+
</div>
219
+
);
220
+
};
221
+
222
+
type CardItemProps = {
223
+
imageUrl: string | null | undefined;
224
+
imageFallbackUrl: string;
225
+
imageLoading: boolean;
226
+
title: string;
227
+
subtitle: string;
228
+
linkUrl: string;
229
+
isCircularImage?: boolean;
230
+
};
231
+
232
+
export const CardItem: Component<CardItemProps> = (props) => {
233
return (
234
<div class="group relative w-40 p-4 rounded-lg bg-gray-900 transition-all duration-300 hover:bg-gray-800 cursor-pointer">
235
<Show
236
+
when={!props.imageLoading}
237
fallback={
238
+
<div
239
+
class={`relative w-32 h-32 mx-auto mb-4 bg-gray-700 animate-pulse ${props.isCircularImage ? "rounded-full" : "rounded-lg"}`}
240
+
></div>
241
}
242
>
243
<img
244
+
src={props.imageUrl || props.imageFallbackUrl}
245
+
alt={`Thumbnail for ${props.title}`}
246
+
class={`w-32 h-32 mx-auto object-cover shadow-lg mb-4 transition-all duration-300 group-hover:scale-105 ${props.isCircularImage ? "rounded-full" : "rounded-lg"}`}
247
/>
248
</Show>
249
<p class="text-white text-center font-bold text-base truncate mb-1">
250
+
<a target="_blank" href={props.linkUrl}>
251
+
{props.title}
252
</a>
253
</p>
254
+
<p class="text-gray-500 text-base truncate mb-1">{props.subtitle}</p>
255
</div>
256
);
257
};