this repo has no description

Display book covers

+68 -11
+37
app/api/blob/route.ts
··· 1 + import { getOAuthClient } from "@/lib/auth/client"; 2 + import { getSession } from "@/lib/auth/session"; 3 + import { Agent } from "@atproto/api"; 4 + import { NextRequest, NextResponse } from "next/server"; 5 + 6 + export async function GET(request: NextRequest) { 7 + const { searchParams } = new URL(request.url); 8 + const cid = searchParams.get("cid"); 9 + const did = searchParams.get("did"); 10 + 11 + if (!cid || !did) { 12 + return new NextResponse("Missing cid or did", { status: 400 }); 13 + } 14 + 15 + const client = await getOAuthClient(); 16 + const session = await client.restore(did); 17 + 18 + if (!session) { 19 + return new NextResponse("Unauthorized", { status: 401 }); 20 + } 21 + 22 + try { 23 + const agent = new Agent(session); 24 + const response = await agent.com.atproto.sync.getBlob({ cid, did }); 25 + 26 + return new NextResponse(response.data, { 27 + headers: { 28 + "Cache-Control": "public, max-age=31536000, immutable", 29 + "Content-Type": response.headers["content-type"] || "image/jpeg" 30 + } 31 + }); 32 + } catch (error) { 33 + console.error("Error fetching blob:", error); 34 + 35 + return new NextResponse("Failed to fetch blob", { status: 500 }); 36 + } 37 + }
+24 -6
app/library/page.tsx
··· 25 25 repo: session.did 26 26 }); 27 27 28 - books = booksResponse.data.records; 28 + books = booksResponse.data.records.filter( 29 + (book) => 30 + book.value.status === "buzz.bookhive.defs#reading" || 31 + book.value.status === "buzz.bookhive.defs#wantToRead" 32 + ); 29 33 } catch (error) { 30 34 console.error("Error fetching profile:", error); 31 35 ··· 39 43 <h1 className="text-amber-100 text-4xl">bambü</h1> 40 44 <div className="h-16 relative w-16"> 41 45 <Image 42 - alt="User avatar" 46 + alt={`Profile picture for ${profile?.handle}`} 43 47 className="object-cover rounded-full" 44 48 fill 45 49 priority ··· 47 51 /> 48 52 </div> 49 53 </header> 50 - <div className="bg-emerald-900 rounded-2xl row-span-9"> 51 - {books?.map((book, index) => ( 52 - <p key={index}>{JSON.stringify(book)}</p> 53 - ))} 54 + <div className="auto-rows-min bg-emerald-900 gap-4 grid grid-cols-5 overflow-y-scroll p-4 rounded-2xl row-span-9"> 55 + {books?.map((book) => { 56 + return ( 57 + <div 58 + className="aspect-2/3 overflow-hidden relative rounded-2xl" 59 + key={book.value.hiveId} 60 + > 61 + <Image 62 + alt={`Book cover for ${book.value.title} by ${book.value.authors}`} 63 + className="object-cover" 64 + fill 65 + src={`/api/blob?cid=${book.value.cover?.ref?.toString()}&did=${ 66 + session.did 67 + }`} 68 + /> 69 + </div> 70 + ); 71 + })} 54 72 </div> 55 73 </main> 56 74 );
+6 -5
lib/auth/client.ts
··· 9 9 import { getDB } from "../db"; 10 10 11 11 declare global { 12 - var _oauthClient: NodeOAuthClient | undefined 12 + var _oauthClient: NodeOAuthClient | undefined; 13 13 } 14 14 15 15 const PRIVATE_KEY = process.env.PRIVATE_KEY; 16 16 const PUBLIC_URL = process.env.PUBLIC_URL; 17 17 18 - export const SCOPE = "atproto"; 18 + export const SCOPE = 19 + "atproto rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app#bsky_appview"; 19 20 20 21 const getClientMetadata = () => { 21 22 if (PUBLIC_URL) { ··· 49 50 }; 50 51 51 52 export const getOAuthClient = async () => { 52 - if(globalThis._oauthClient){ 53 - return globalThis._oauthClient 53 + if (globalThis._oauthClient) { 54 + return globalThis._oauthClient; 54 55 } 55 56 56 57 globalThis._oauthClient = new NodeOAuthClient({ ··· 104 105 .select("value") 105 106 .where("key", "=", key) 106 107 .executeTakeFirst(); 107 - 108 + 108 109 return row ? JSON.parse(row.value) : undefined; 109 110 }, 110 111 set: async (key: string, value: NodeSavedState) => {
+1
next.config.ts
··· 2 2 3 3 const nextConfig: NextConfig = { 4 4 images: { 5 + localPatterns: [{ pathname: "/api/blob" }], 5 6 remotePatterns: [new URL("https://cdn.bsky.app/img/**")] 6 7 } 7 8 };