this repo has no description

good username handling

Changed files
+116 -81
src
+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 };