A decentralized music tracking and discovery platform built on AT Protocol 🎵

Add profile skeleton loader

+118 -91
+118 -91
apps/web/src/pages/profile/Profile.tsx
··· 34 34 import { useArtistsQuery } from "../../hooks/useLibrary"; 35 35 import { getLastDays } from "../../lib/date"; 36 36 import { Link } from "@tanstack/react-router"; 37 + import ContentLoader from "react-content-loader"; 37 38 38 39 const Group = styled.div` 39 40 display: flex; ··· 204 205 <Main> 205 206 <div className="pb-[100px] pt-[75px]"> 206 207 <div className="mb-[50px]"> 207 - <Group> 208 - <ProfileInfo> 209 - <div className="mr-[20px]"> 210 - <Avatar 211 - name={profiles[did]?.displayName} 212 - src={profiles[did]?.avatar} 213 - size="150px" 214 - /> 215 - </div> 216 - <div style={{ marginTop: profiles[did]?.displayName ? 10 : 30 }}> 217 - <HeadingMedium 218 - marginTop="0px" 219 - marginBottom={0} 220 - className="!text-[var(--color-text)]" 208 + {profile.isLoading && ( 209 + <ContentLoader 210 + width="100%" 211 + height={200} 212 + viewBox="0 0 800 200" 213 + backgroundColor="var(--color-skeleton-background)" 214 + foregroundColor="var(--color-skeleton-foreground)" 215 + > 216 + {/* Avatar circle */} 217 + <circle cx="75" cy="75" r="75" /> 218 + {/* Display name */} 219 + <rect x="180" y="30" rx="4" ry="4" width="250" height="24" /> 220 + {/* Handle */} 221 + <rect x="180" y="70" rx="3" ry="3" width="180" height="16" /> 222 + {/* Scrobbling since text */} 223 + <rect x="370" y="70" rx="3" ry="3" width="200" height="16" /> 224 + {/* View on PDSls button */} 225 + <rect x="180" y="120" rx="8" ry="8" width="180" height="48" /> 226 + {/* Follow button */} 227 + <rect x="680" y="30" rx="20" ry="20" width="120" height="40" /> 228 + </ContentLoader> 229 + )} 230 + {!profile.isLoading && ( 231 + <Group> 232 + <ProfileInfo> 233 + <div className="mr-[20px]"> 234 + <Avatar 235 + name={profiles[did]?.displayName} 236 + src={profiles[did]?.avatar} 237 + size="150px" 238 + /> 239 + </div> 240 + <div 241 + style={{ marginTop: profiles[did]?.displayName ? 10 : 30 }} 221 242 > 222 - {profiles[did]?.displayName} 223 - </HeadingMedium> 224 - <LabelLarge> 225 - <a 226 - href={`https://bsky.app/profile/${profiles[did]?.handle}`} 227 - className="no-underline text-[var(--color-primary)]" 228 - target="_blank" 229 - > 230 - @{profiles[did]?.handle} 231 - </a> 232 - <span className="text-[var(--color-text-muted)] text-[15px]"> 233 - {" "} 234 - • scrobbling since{" "} 235 - {dayjs(profiles[did]?.createdAt).format("DD MMM YYYY")} 236 - </span> 237 - </LabelLarge> 238 - <div className="flex-1 mt-[30px] mr-[10px]"> 239 - <a 240 - href={`https://pdsls.dev/at/${profiles[did]?.did}`} 241 - target="_blank" 242 - className="no-underline text-[var(--color-text)] bg-[var(--color-default-button)] p-[16px] rounded-[10px] pl-[25px] pr-[25px]" 243 + <HeadingMedium 244 + marginTop="0px" 245 + marginBottom={0} 246 + className="!text-[var(--color-text)]" 243 247 > 244 - <ExternalLink size={24} style={{ marginRight: 10 }} /> 245 - View on PDSls 246 - </a> 248 + {profiles[did]?.displayName} 249 + </HeadingMedium> 250 + <LabelLarge> 251 + <a 252 + href={`https://bsky.app/profile/${profiles[did]?.handle}`} 253 + className="no-underline text-[var(--color-primary)]" 254 + target="_blank" 255 + > 256 + @{profiles[did]?.handle} 257 + </a> 258 + <span className="text-[var(--color-text-muted)] text-[15px]"> 259 + {" "} 260 + • scrobbling since{" "} 261 + {dayjs(profiles[did]?.createdAt).format("DD MMM YYYY")} 262 + </span> 263 + </LabelLarge> 264 + <div className="flex-1 mt-[30px] mr-[10px]"> 265 + <a 266 + href={`https://pdsls.dev/at/${profiles[did]?.did}`} 267 + target="_blank" 268 + className="no-underline text-[var(--color-text)] bg-[var(--color-default-button)] p-[16px] rounded-[10px] pl-[25px] pr-[25px]" 269 + > 270 + <ExternalLink size={24} style={{ marginRight: 10 }} /> 271 + View on PDSls 272 + </a> 273 + </div> 247 274 </div> 248 - </div> 249 - </ProfileInfo> 250 - {(profile.data?.did !== localStorage.getItem("did") || 251 - !localStorage.getItem("did")) && ( 252 - <> 253 - {!follows.has(profile.data?.did || "") && !isLoading && ( 254 - <Button 255 - shape="pill" 256 - size="compact" 257 - startEnhancer={<IconPlus size={18} />} 258 - onClick={onFollow} 259 - overrides={{ 260 - BaseButton: { 261 - style: { 262 - marginTop: "12px", 263 - minWidth: "120px", 264 - backgroundColor: "#ff2876", 265 - ":hover": { 266 - backgroundColor: "#ff2876", 267 - }, 268 - ":focus": { 275 + </ProfileInfo> 276 + {(profile.data?.did !== localStorage.getItem("did") || 277 + !localStorage.getItem("did")) && ( 278 + <> 279 + {!follows.has(profile.data?.did || "") && !isLoading && ( 280 + <Button 281 + shape="pill" 282 + size="compact" 283 + startEnhancer={<IconPlus size={18} />} 284 + onClick={onFollow} 285 + overrides={{ 286 + BaseButton: { 287 + style: { 288 + marginTop: "12px", 289 + minWidth: "120px", 269 290 backgroundColor: "#ff2876", 291 + ":hover": { 292 + backgroundColor: "#ff2876", 293 + }, 294 + ":focus": { 295 + backgroundColor: "#ff2876", 296 + }, 270 297 }, 271 298 }, 272 - }, 273 - }} 274 - > 275 - Follow 276 - </Button> 277 - )} 278 - {follows.has(profile.data?.did || "") && !isLoading && ( 279 - <Button 280 - shape="pill" 281 - size="compact" 282 - startEnhancer={<IconCheck size={18} />} 283 - onClick={onUnfollow} 284 - overrides={{ 285 - BaseButton: { 286 - style: { 287 - marginTop: "12px", 288 - minWidth: "120px", 289 - backgroundColor: "var(--color-default-button)", 290 - color: "var(--color-text)", 291 - ":hover": { 292 - backgroundColor: "var(--color-default-button)", 293 - }, 294 - ":focus": { 299 + }} 300 + > 301 + Follow 302 + </Button> 303 + )} 304 + {follows.has(profile.data?.did || "") && !isLoading && ( 305 + <Button 306 + shape="pill" 307 + size="compact" 308 + startEnhancer={<IconCheck size={18} />} 309 + onClick={onUnfollow} 310 + overrides={{ 311 + BaseButton: { 312 + style: { 313 + marginTop: "12px", 314 + minWidth: "120px", 295 315 backgroundColor: "var(--color-default-button)", 316 + color: "var(--color-text)", 317 + ":hover": { 318 + backgroundColor: "var(--color-default-button)", 319 + }, 320 + ":focus": { 321 + backgroundColor: "var(--color-default-button)", 322 + }, 296 323 }, 297 324 }, 298 - }, 299 - }} 300 - > 301 - Following 302 - </Button> 303 - )} 304 - </> 305 - )} 306 - </Group> 325 + }} 326 + > 327 + Following 328 + </Button> 329 + )} 330 + </> 331 + )} 332 + </Group> 333 + )} 307 334 {tags.length > 0 && ( 308 335 <div className="mt-[30px] mb-[35px] flex flex-wrap"> 309 336 {tags.map((genre) => (