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

Add Recent Scrobbles embed and header option

Introduce RecentScrobblesEmbedPage: renders a table of recent
scrobbles with album art, links to rocksky.app, title/artist and
relative timestamps. Add Header's withoutRange prop (used by the
embed). Use dayjs with the relativeTime plugin for fromNow display and
uuid v4 for row keys. Normalize createdAt to ensure a trailing Z.

+65 -2
+7 -2
apps/embed/src/components/Header/Header.tsx
··· 3 3 4 4 export type HeaderProps = { 5 5 profile: Profile; 6 + withoutRange?: boolean; 6 7 }; 7 8 8 9 function Header(props: HeaderProps) { ··· 30 31 > 31 32 <div className="text-[14px] mt-[-6px]">@{props.profile.handle}</div> 32 33 </a> 33 - <span className="text-[14px] mt-[-3px] ml-[5px] mr-[5px]">|</span> 34 - <span className="text-[13px] mt-[-3px]">{range}</span> 34 + {!props.withoutRange && ( 35 + <> 36 + <span className="text-[14px] mt-[-3px] ml-[5px] mr-[5px]">|</span> 37 + <span className="text-[13px] mt-[-3px]">{range}</span> 38 + </> 39 + )} 35 40 </div> 36 41 37 42 <a
+58
apps/embed/src/embeds/RecentScrobblesEmbedPage.tsx
··· 1 + import { v4 } from "uuid"; 2 + import Header from "../components/Header"; 1 3 import type { Profile } from "../types/profile"; 2 4 import type { Scrobble } from "../types/scrobble"; 5 + import dayjs from "dayjs"; 6 + import relativeTime from "dayjs/plugin/relativeTime"; 7 + 8 + dayjs.extend(relativeTime); 3 9 4 10 export type RecentScrobblesEmbedPageProps = { 5 11 profile: Profile; ··· 9 15 export function RecentScrobblesEmbedPage(props: RecentScrobblesEmbedPageProps) { 10 16 return ( 11 17 <div className="p-[15px]"> 18 + <Header profile={props.profile} withoutRange /> 12 19 <h2 className="m-[0px]">Recent Listens</h2> 20 + 21 + <div className="w-full overflow-x-auto"> 22 + <table className="table-borderless table"> 23 + <tbody> 24 + {props.scrobbles.map((scrobble, index) => ( 25 + <tr key={v4()}> 26 + <td> 27 + <div className="flex flex-row items-center"> 28 + <a 29 + href={`https://rocksky.app/${scrobble.uri?.split("at://")[1]?.replace("app.rocksky.", "")}`} 30 + target="_blank" 31 + className="flex flex-row items-center no-underline text-inherit" 32 + > 33 + {scrobble.albumArt && ( 34 + <img 35 + className="max-w-[60px] max-h-[60px] mr-[20px] rounded-[5px]" 36 + src={scrobble.albumArt!} 37 + /> 38 + )} 39 + {!scrobble.albumArt && ( 40 + <div className="w-[60px] h-[60px] bg-[var(--color-avatar-background)] flex items-center justify-center mr-[20px]"> 41 + <div className="h-[30px] w-[30px]"></div> 42 + </div> 43 + )} 44 + <div> 45 + <div>{scrobble.title}</div> 46 + <a 47 + href={`https://rocksky.app/${scrobble.artistUri?.split("at://")[1]?.replace("app.rocksky.", "")}`} 48 + target="_blank" 49 + className="no-underline text-inherit" 50 + > 51 + <div className="font-rockford-light opacity-60"> 52 + {scrobble.albumArtist} 53 + </div> 54 + </a> 55 + </div> 56 + </a> 57 + </div> 58 + </td> 59 + <td className="font-rockford-light opacity-60"> 60 + {dayjs( 61 + scrobble.createdAt.endsWith("Z") 62 + ? scrobble.createdAt 63 + : `${scrobble.createdAt}Z`, 64 + ).fromNow()} 65 + </td> 66 + </tr> 67 + ))} 68 + </tbody> 69 + </table> 70 + </div> 13 71 </div> 14 72 ); 15 73 }