+116
-81
src/App.tsx
+116
-81
src/App.tsx
···
4
4
createResource,
5
5
createSignal,
6
6
For,
7
+
type JSXElement,
8
+
type Resource,
7
9
Show,
8
10
Suspense,
9
11
} from "solid-js";
···
20
22
21
23
export default () => {
22
24
const [settings, setSettings] = createSignal<Settings>({
23
-
username: "karitham",
25
+
username: "",
24
26
range: "this_month",
25
27
count: 10,
26
28
});
27
29
const [artistFetcher] = makeCache(
28
-
(set: Settings) => artists(set.username, undefined, set.range, set.count),
30
+
(set: Settings) => {
31
+
if (set.username)
32
+
return artists(set.username, undefined, set.range, set.count);
33
+
},
29
34
{
30
35
storage: localStorage,
31
36
sourceHash(source) {
···
35
40
);
36
41
const [artistRes] = createResource(settings, artistFetcher);
37
42
const [releasesFetcher] = makeCache(
38
-
(set: Settings) => releases(set.username, undefined, set.range, set.count),
43
+
(set: Settings) => {
44
+
if (set.username)
45
+
return releases(set.username, undefined, set.range, set.count);
46
+
},
39
47
{
40
48
storage: localStorage,
41
49
sourceHash(source) {
···
72
80
range: e.target.value as Range,
73
81
}));
74
82
}}
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"
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"
76
84
>
77
85
{ranges.map((r) => (
78
-
<option value={r}>{r}</option>
86
+
<option value={r} class="capitalize">
87
+
{r.split("_").join(" ")}
88
+
</option>
79
89
))}
80
90
</select>
81
91
</div>
82
92
<Suspense
83
93
fallback={<p class="text-center text-gray-400">Loading...</p>}
84
94
>
85
-
<div>
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
+
>
86
103
<div>
87
-
<h2 class="text-2xl font-semibold my-6 text-center">
88
-
Top Artists
89
-
</h2>
90
-
<Artists artists={artistRes()?.artists || []} />
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>
91
116
</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>
117
+
</Show>
99
118
</Suspense>
100
119
</div>
101
120
</main>
···
106
125
return (
107
126
<div class="flex flex-wrap justify-center gap-4">
108
127
<For each={props.artists}>
109
-
{(artist) => <ArtistsItem artist={artist} />}
128
+
{(artist) => <ArtistsItem item={artist} />}
110
129
</For>
111
130
</div>
112
131
);
113
132
};
114
133
115
-
const ArtistsItem: Component<{ artist: ArtistStatsArtist }> = (props) => {
134
+
export const ArtistsItem: Component<{ item: ArtistStatsArtist }> = (props) => {
116
135
const [imageFetcher] = makeCache(
117
136
async (artist: ArtistStatsArtist) => {
118
-
if (!artist.artist_mbid) {
119
-
return null;
120
-
}
121
-
137
+
if (!artist.artist_mbid) return null;
122
138
return await getWikidataURL(artist.artist_mbid).then((s) =>
123
139
s ? getWikidataThumbnail(s) : null,
124
140
);
125
141
},
126
-
{
127
-
storage: localStorage,
128
-
},
142
+
{ storage: localStorage },
129
143
);
130
-
const [image] = createResource(props.artist, imageFetcher);
144
+
const [image] = createResource(() => props.item, imageFetcher);
131
145
132
146
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>
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
+
/>
161
156
);
162
157
};
163
158
164
-
const ReleaseGroups: Component<{ groups: Release[] }> = (props) => {
159
+
const Releases: Component<{ groups: Release[] }> = (props) => {
165
160
return (
166
161
<div class="flex flex-wrap justify-center gap-4">
167
162
<For each={props.groups}>
168
-
{(group) => <ReleaseGroupItem release={group} />}
163
+
{(release) => <ReleaseItem item={release} />}
169
164
</For>
170
165
</div>
171
166
);
172
167
};
173
168
174
-
const ReleaseGroupItem: Component<{ release: Release }> = (props) => {
169
+
export const ReleaseItem: Component<{ item: Release }> = (props) => {
175
170
const [imageFetcher] = makeCache(
176
171
async (release: Release) => {
177
-
if (!release.caa_release_mbid) {
178
-
return null;
179
-
}
172
+
if (!release.caa_release_mbid) return null;
180
173
const result = await getReleaseImageURL(
181
174
"release",
182
175
release.caa_release_mbid,
183
176
);
184
177
return result[0]?.image.replace("http://", "https://");
185
178
},
186
-
{
187
-
storage: localStorage,
188
-
},
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
+
/>
189
192
);
190
-
const [image] = createResource(props.release, imageFetcher);
193
+
};
191
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) => {
192
233
return (
193
234
<div class="group relative w-40 p-4 rounded-lg bg-gray-900 transition-all duration-300 hover:bg-gray-800 cursor-pointer">
194
235
<Show
195
-
when={!image.loading}
236
+
when={!props.imageLoading}
196
237
fallback={
197
-
<div class="relative w-32 h-32 mx-auto rounded-full mb-4 bg-gray-700 animate-pulse"></div>
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>
198
241
}
199
242
>
200
243
<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"
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"}`}
207
247
/>
208
248
</Show>
209
249
<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}
250
+
<a target="_blank" href={props.linkUrl}>
251
+
{props.title}
215
252
</a>
216
253
</p>
217
-
<p class="text-gray-500 text-base truncate mb-1">
218
-
{props.release.listen_count} listens
219
-
</p>
254
+
<p class="text-gray-500 text-base truncate mb-1">{props.subtitle}</p>
220
255
</div>
221
256
);
222
257
};