this repo has no description

Submit ratings from Amazon and Goodreads

+209 -34
+36
app/api/books/rating/route.ts
···
··· 1 + import { supabase } from "@/lib/supabase/server"; 2 + import { NextRequest, NextResponse } from "next/server"; 3 + 4 + export async function POST(request: NextRequest) { 5 + try { 6 + const body = await request.json(); 7 + const { hiveID, ratingType, value } = body; 8 + 9 + if (!hiveID || !ratingType || !value) { 10 + return NextResponse.json( 11 + { error: "Missing hiveId, type, or value" }, 12 + { status: 400 } 13 + ); 14 + } 15 + 16 + const column = 17 + ratingType === "amazon" ? "amazon_rating" : "goodreads_rating"; 18 + const { error } = await supabase 19 + .from("books") 20 + .update({ [column]: value }) 21 + .eq("hive_id", hiveID); 22 + 23 + if (error) { 24 + return NextResponse.json({ error: error.message }, { status: 500 }); 25 + } 26 + 27 + return NextResponse.json({ success: true }); 28 + } catch (error) { 29 + console.error(error); 30 + 31 + return NextResponse.json( 32 + { error: "Unexpected error" }, 33 + { status: 500 } 34 + ); 35 + } 36 + }
+82 -25
app/book/[bookID]/page.tsx
··· 1 import Header from "@/components/Header"; 2 import { getSession } from "@/lib/auth/session"; 3 - import { fetchNumberOfPages } from "@/lib/books/google"; 4 import { Agent } from "@atproto/api"; 5 import Image from "next/image"; 6 import { redirect } from "next/navigation"; ··· 16 17 const agent = new Agent(session); 18 19 - let book, numberOfPages; 20 21 try { 22 const response = await agent.com.atproto.repo.listRecords({ ··· 29 (record) => record.value.hiveId === `bk_${bookID}` 30 ); 31 32 - numberOfPages = await fetchNumberOfPages( 33 - book.value.authors, 34 - book.value.hiveId, 35 - book.value.title 36 ); 37 } catch (error) { 38 console.error("Error fetching book:", error); 39 } 40 41 return ( 42 <> 43 <Header /> 44 <div className="gap-4 grid grid-cols-2 row-span-9"> 45 - <div className="bg-emerald-900 col-span-1 gap-4 grid grid-rows-3 p-4 rounded-2xl"> 46 - <div className="bg-amber-100 flex items-center justify-center p-4 rounded-2xl row-span-2"> 47 - <div className="aspect-2/3 border-4 border-emerald-900 h-full overflow-hidden relative rounded-2xl"> 48 - <Image 49 - alt={`Book cover for ${book.value.title} by ${book.value.authors}`} 50 - className="object-cover" 51 - fill 52 - src={`/api/blob?cid=${book.value.cover?.ref?.toString()}&did=${ 53 - session.did 54 - }`} 55 - /> 56 </div> 57 </div> 58 - <div className="bg-amber-100 rounded-2xl row-span-1"> 59 - <p className="text-emerald-900">{`Title: ${book?.value.title}`}</p> 60 - <p className="text-emerald-900">Amazon Rating:</p> 61 - <p className="text-emerald-900">Goodreads Rating:</p> 62 - <p className="text-emerald-900"> 63 - Number of Pages: {numberOfPages} 64 - </p> 65 </div> 66 </div> 67 - <div className="bg-emerald-900 col-span-1 p-4 rounded-2xl"></div> 68 </div> 69 </> 70 );
··· 1 import Header from "@/components/Header"; 2 + import RatingInput from "@/components/RatingInput"; 3 import { getSession } from "@/lib/auth/session"; 4 + import { getBookMeta } from "@/lib/books/meta"; 5 import { Agent } from "@atproto/api"; 6 import Image from "next/image"; 7 import { redirect } from "next/navigation"; ··· 17 18 const agent = new Agent(session); 19 20 + let book, meta; 21 22 try { 23 const response = await agent.com.atproto.repo.listRecords({ ··· 30 (record) => record.value.hiveId === `bk_${bookID}` 31 ); 32 33 + meta = await getBookMeta( 34 + book?.value.authors, 35 + book?.value.hiveId, 36 + book?.value.title 37 ); 38 } catch (error) { 39 console.error("Error fetching book:", error); 40 } 41 42 + const urls = { 43 + amazon: `https://www.amazon.com/s?k=${encodeURIComponent( 44 + book.value.title + " " + book.value.authors 45 + )}`, 46 + goodreads: `https://www.goodreads.com/search?q=${encodeURIComponent( 47 + book.value.title + " " + book.value.authors 48 + )}` 49 + }; 50 + 51 return ( 52 <> 53 <Header /> 54 <div className="gap-4 grid grid-cols-2 row-span-9"> 55 + <div className="bg-emerald-900 col-span-1 flex gap-4 h-full items-center justify-center p-4 rounded-2xl w-full"> 56 + <div className="aspect-2/3 h-full overflow-hidden relative rounded-2xl"> 57 + <Image 58 + alt={`Book cover for ${book.value.title} by ${book.value.authors}`} 59 + className="object-cover" 60 + fill 61 + src={`/api/blob?cid=${book.value.cover?.ref?.toString()}&did=${ 62 + session.did 63 + }`} 64 + /> 65 + </div> 66 + </div> 67 + <div className="bg-emerald-900 col-span-1 gap-4 grid grid-cols-2 grid-rows-2 p-4 rounded-2xl"> 68 + <div className="bg-amber-100 grid grid-rows-5 p-4 rounded-2xl"> 69 + <div className="flex items-center justify-center row-span-1"> 70 + <h2 className="font-semibold text-emerald-900 text-center text-2xl"> 71 + Title 72 + </h2> 73 + </div> 74 + <div className="flex items-center justify-center row-span-4"> 75 + <p className="text-center text-xl text-emerald-900"> 76 + {book?.value.title} 77 + </p> 78 + </div> 79 + </div> 80 + <div className="bg-amber-100 grid grid-rows-5 p-4 rounded-2xl"> 81 + <div className="flex items-center justify-center row-span-1"> 82 + <h2 className="font-semibold text-emerald-900 text-center text-2xl"> 83 + Page Count 84 + </h2> 85 + </div> 86 + <div className="flex items-center justify-center row-span-4"> 87 + <p className="text-center text-xl text-emerald-900"> 88 + {meta.page_count} 89 + </p> 90 + </div> 91 + </div> 92 + <div className="bg-amber-100 grid grid-rows-5 p-4 rounded-2xl"> 93 + <div className="flex items-center justify-center row-span-1"> 94 + <h2 className="font-semibold text-emerald-900 text-center text-2xl"> 95 + Amazon Rating 96 + </h2> 97 + </div> 98 + <div className="flex items-center justify-center row-span-4"> 99 + {meta.amazon_rating ? ( 100 + <p className="text-center text-xl text-emerald-900"> 101 + {meta.amazon_rating} 102 + </p> 103 + ) : ( 104 + <RatingInput hiveID={book?.value.hiveId} url={urls.amazon} /> 105 + )} 106 </div> 107 </div> 108 + <div className="bg-amber-100 grid grid-rows-5 p-4 rounded-2xl"> 109 + <div className="flex items-center justify-center row-span-1"> 110 + <h2 className="font-semibold text-emerald-900 text-center text-2xl"> 111 + Goodreads Rating 112 + </h2> 113 + </div> 114 + <div className="flex items-center justify-center row-span-4"> 115 + {meta.goodreads_rating ? ( 116 + <p className="text-center text-xl text-emerald-900"> 117 + {meta.goodreads_rating} 118 + </p> 119 + ) : ( 120 + <RatingInput hiveID={book?.value.hiveId} url={urls.goodreads} /> 121 + )} 122 + </div> 123 </div> 124 </div> 125 </div> 126 </> 127 );
+52
components/RatingInput.tsx
···
··· 1 + "use client"; 2 + 3 + import Link from "next/link"; 4 + import { useState } from "react"; 5 + 6 + const RatingInput = ({ hiveID, url }) => { 7 + const [value, setValue] = useState(0); 8 + 9 + const handleSubmit = async () => { 10 + await fetch("/api/books/rating", { 11 + body: JSON.stringify({ 12 + hiveID, 13 + ratingType: url.includes("amazon") ? "amazon" : "goodreads", 14 + value 15 + }), 16 + method: "POST" 17 + }); 18 + }; 19 + 20 + return ( 21 + <div className="flex flex-col gap-4"> 22 + <input 23 + className="border-2 border-emerald-900 h-12 px-4 py-2 rounded-lg text-emerald-900" 24 + min={0} 25 + max={5} 26 + name="" 27 + onChange={(e) => setValue(Number(e.currentTarget.value))} 28 + placeholder="Add Rating" 29 + step={url.includes("amazon") ? 0.1 : 0.01} 30 + type="number" 31 + value={value} 32 + /> 33 + <button 34 + className="active:bg-emerald-800 bg-emerald-900 hover:cursor-pointer flex items-center justify-center h-12 px-4 py-2 text-amber-100 rounded-lg" 35 + onClick={handleSubmit} 36 + > 37 + Submit 38 + </button> 39 + <Link 40 + className="bg-emerald-900 h-12 px-4 py-2 rounded-lg text-amber-100 text-center" 41 + href={url} 42 + target="_blank" 43 + > 44 + {`${ 45 + url.includes("amazon") ? "Amazon" : "Goodreads" 46 + } Search Results`} 47 + </Link> 48 + </div> 49 + ); 50 + }; 51 + 52 + export default RatingInput;
+17 -9
lib/books/google.ts
··· 5 hiveID: string, 6 title: string 7 ) => { 8 - const { data: existing} = await supabase 9 .from("books") 10 .select("page_count") 11 - .eq("hive_id", hiveID); 12 13 if (existing?.page_count) { 14 return existing.page_count; ··· 25 }&maxResults=1` 26 ); 27 const json = await response.json(); 28 - const volumeInfo = json.items?.[0].volumeInfo; 29 30 - const {data, error: e} = await supabase.from("books").upsert({ 31 - authors, 32 - hive_id: hiveID, 33 - page_count: Number(volumeInfo.pageCount), 34 - title 35 - }, {onConflict: "hive_id"}); 36 37 if (e) { 38 console.error("Supabase upsert error:", e);
··· 5 hiveID: string, 6 title: string 7 ) => { 8 + const { data: existing } = await supabase 9 .from("books") 10 .select("page_count") 11 + .eq("hive_id", hiveID) 12 + .single(); 13 14 if (existing?.page_count) { 15 return existing.page_count; ··· 26 }&maxResults=1` 27 ); 28 const json = await response.json(); 29 + const item = json.items?.[0]; 30 + const volumeInfo = item.volumeInfo; 31 32 + console.log(volumeInfo); 33 + 34 + const { data, error: e } = await supabase.from("books").upsert( 35 + { 36 + authors, 37 + google_books_volume_id: item.id, 38 + hive_id: hiveID, 39 + page_count: Number(volumeInfo.pageCount), 40 + title 41 + }, 42 + { onConflict: "hive_id" } 43 + ); 44 45 if (e) { 46 console.error("Supabase upsert error:", e);
+22
lib/books/meta.ts
···
··· 1 + import { supabase } from "../supabase/server"; 2 + import { fetchNumberOfPages } from "./google"; 3 + 4 + export const getBookMeta = async ( 5 + authors: string, 6 + hiveID: string, 7 + title: string 8 + ) => { 9 + const { data: existing } = await supabase 10 + .from("books") 11 + .select("*") 12 + .eq("hive_id", hiveID) 13 + .single(); 14 + 15 + if (existing) { 16 + return existing; 17 + } 18 + 19 + const numberOfPages = await fetchNumberOfPages(authors, hiveID, title); 20 + 21 + return {numberOfPages} 22 + };