tangled
alpha
login
or
join now
modamo.xyz
/
bambu
1
fork
atom
this repo has no description
1
fork
atom
overview
issues
pulls
pipelines
Submit ratings from Amazon and Goodreads
modamo-gh
1 month ago
a867ea89
6c157162
+209
-34
5 changed files
expand all
collapse all
unified
split
app
api
books
rating
route.ts
book
[bookID]
page.tsx
components
RatingInput.tsx
lib
books
google.ts
meta.ts
+36
app/api/books/rating/route.ts
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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";
0
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
0
0
0
0
0
0
0
0
0
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
-
/>
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
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>
0
0
0
0
0
0
0
0
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>
0
125
</div>
126
</>
127
);
+52
components/RatingInput.tsx
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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);
0
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;
0
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"});
0
0
0
0
0
0
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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
+
};