Listen to and share the music in the Atmosphere. musicsky.up.railway.app/
nextjs atproto music typescript react
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: handle page (wip)

+111 -2
+28 -2
package-lock.json
··· 9 9 "version": "0.1.0", 10 10 "dependencies": { 11 11 "@atproto/api": "^0.19.0", 12 + "@atproto/identity": "^0.4.12", 12 13 "@atproto/lexicon": "^0.6.1", 13 14 "@atproto/oauth-client-node": "^0.3.17", 14 15 "@atproto/xrpc": "^0.7.7", ··· 257 258 "url": "https://github.com/sponsors/colinhacks" 258 259 } 259 260 }, 261 + "node_modules/@atproto/crypto": { 262 + "version": "0.4.5", 263 + "resolved": "https://registry.npmjs.org/@atproto/crypto/-/crypto-0.4.5.tgz", 264 + "integrity": "sha512-n40aKkMoCatP0u9Yvhrdk6fXyOHFDDbkdm4h4HCyWW+KlKl8iXfD5iV+ECq+w5BM+QH25aIpt3/j6EUNerhLxw==", 265 + "license": "MIT", 266 + "dependencies": { 267 + "@noble/curves": "^1.7.0", 268 + "@noble/hashes": "^1.6.1", 269 + "uint8arrays": "3.0.0" 270 + }, 271 + "engines": { 272 + "node": ">=18.7.0" 273 + } 274 + }, 260 275 "node_modules/@atproto/did": { 261 276 "version": "0.3.0", 262 277 "resolved": "https://registry.npmjs.org/@atproto/did/-/did-0.3.0.tgz", ··· 273 288 "license": "MIT", 274 289 "funding": { 275 290 "url": "https://github.com/sponsors/colinhacks" 291 + } 292 + }, 293 + "node_modules/@atproto/identity": { 294 + "version": "0.4.12", 295 + "resolved": "https://registry.npmjs.org/@atproto/identity/-/identity-0.4.12.tgz", 296 + "integrity": "sha512-P+Jn0HvKhIh1tps5n3xGrCxt+XiFWzp4kdgloyFhFmVLwjDU547DQkWx4r5Vhuiah7fRZGVSlk39R4U6SPrACg==", 297 + "license": "MIT", 298 + "dependencies": { 299 + "@atproto/common-web": "^0.4.17", 300 + "@atproto/crypto": "^0.4.5" 301 + }, 302 + "engines": { 303 + "node": ">=18.7.0" 276 304 } 277 305 }, 278 306 "node_modules/@atproto/jwk": { ··· 2297 2325 "version": "1.9.7", 2298 2326 "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", 2299 2327 "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", 2300 - "dev": true, 2301 2328 "license": "MIT", 2302 2329 "dependencies": { 2303 2330 "@noble/hashes": "1.8.0" ··· 2313 2340 "version": "1.8.0", 2314 2341 "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", 2315 2342 "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", 2316 - "dev": true, 2317 2343 "license": "MIT", 2318 2344 "engines": { 2319 2345 "node": "^14.21.3 || >=16"
+1
package.json
··· 12 12 }, 13 13 "dependencies": { 14 14 "@atproto/api": "^0.19.0", 15 + "@atproto/identity": "^0.4.12", 15 16 "@atproto/lexicon": "^0.6.1", 16 17 "@atproto/oauth-client-node": "^0.3.17", 17 18 "@atproto/xrpc": "^0.7.7",
+82
src/app/[handle]/page.tsx
··· 1 + import { Agent } from "@atproto/api"; 2 + import { IdResolver } from "@atproto/identity"; 3 + 4 + export async function getSongs(handle: string) { 5 + const agent = new Agent("https://public.api.bsky.app"); 6 + const resolver = new IdResolver(); 7 + 8 + try { 9 + const { data: identity } = await agent.resolveHandle({ handle }); 10 + const did = identity.did; 11 + 12 + const doc = await resolver.did.resolve(did); 13 + 14 + const pdsService = doc?.service?.find( 15 + (service) => service.id === "#atproto_pds", 16 + ); 17 + if (!pdsService?.serviceEndpoint) { 18 + throw new Error("No PDS endpoint found for this user"); 19 + } 20 + 21 + const pdsAgent = new Agent(pdsService.serviceEndpoint as string); 22 + 23 + const { data } = await pdsAgent.com.atproto.repo.listRecords({ 24 + repo: did, 25 + collection: "app.musicsky.temp.track", 26 + limit: 50, 27 + }); 28 + 29 + return data.records; 30 + } catch (error) { 31 + console.error("Failed to fetch songs for", handle, error); 32 + return []; 33 + } 34 + } 35 + 36 + export default async function UserPage({ 37 + params, 38 + }: { 39 + params: Promise<{ handle: string }>; 40 + }) { 41 + const { handle } = await params; 42 + const songs = await getSongs(handle); 43 + 44 + return ( 45 + <main className="flex flex-col gap-6 w-full max-w-2xl mx-auto p-8"> 46 + <h1 className="text-3xl font-bold">Songs by {handle}</h1> 47 + 48 + {songs.length === 0 ? ( 49 + <p className="text-muted-foreground">No songs found for this user.</p> 50 + ) : ( 51 + <ul className="space-y-4"> 52 + {songs.map((song) => { 53 + const record = song.value as unknown as { 54 + title: string; 55 + description?: string; 56 + genre?: string; 57 + duration: number; 58 + createdAt: string; 59 + }; 60 + return ( 61 + <li key={song.uri} className="bg-card p-4 rounded-lg border"> 62 + <h2 className="text-xl font-semibold">{record.title}</h2> 63 + {record.description && ( 64 + <p className="text-muted-foreground mt-2"> 65 + {record.description} 66 + </p> 67 + )} 68 + <div className="mt-4 flex gap-4 text-sm text-muted-foreground"> 69 + {record.genre && <span>{record.genre}</span>} 70 + <span> 71 + {Math.floor(record.duration / 60)}: 72 + {String(record.duration % 60).padStart(2, "0")} 73 + </span> 74 + </div> 75 + </li> 76 + ); 77 + })} 78 + </ul> 79 + )} 80 + </main> 81 + ); 82 + }