this repo has no description

good username handling

karitham.dev c9c9697d 0b31bf34

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