From b38728985d21838821940e9ad002b7bc293486fe Mon Sep 17 00:00:00 2001 From: Nesaku <78386413+nesaku@users.noreply.github.com> Date: Sat, 15 Jul 2023 13:55:52 -0400 Subject: [PATCH] add option to show all author books --- CHANGELOG.md | 19 +- components/aboutpage/AboutHero.js | 54 +++-- components/authorpage/AuthorBookList.js | 193 ++++++++++++++++++ components/authorpage/AuthorBooks.js | 41 +++- components/authorpage/AuthorResultData.js | 1 + components/contactpage/ContactForm.js | 156 +++++++------- components/contactpage/ContactHero.js | 60 +++--- components/global/Footer.js | 4 +- components/global/ThemeToggle.js | 97 +++++---- components/listpage/BookList.js | 166 ++++++++------- package-lock.json | 4 +- package.json | 2 +- pages/api/author/books.js | 92 +++++++++ .../api/{author-scraper.js => author/info.js} | 0 pages/author/list/[...slug].js | 74 +++++++ pages/author/show/[...slug].js | 2 +- pages/list/show/[...slug].js | 6 +- 17 files changed, 703 insertions(+), 268 deletions(-) create mode 100644 components/authorpage/AuthorBookList.js create mode 100644 pages/api/author/books.js rename pages/api/{author-scraper.js => author/info.js} (100%) create mode 100644 pages/author/list/[...slug].js diff --git a/CHANGELOG.md b/CHANGELOG.md index 315244b..c94ca3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.15.0] - Jul 15, 2023 + +### Added + +- Add "View More Books" option for author books - [(ISSUE)](https://github.com/nesaku/BiblioReads/issues/11) +- Add the author book list route +- Add a loader to the `BookList` component + +### Changed + +- Change the author scraper file location +- Remove unneeded fragments + +### Fixed + +- Fix incorrect fallback URL for errors on the list route + ## [2.14.2] - Jun 30, 2023 ### Added @@ -198,7 +215,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add book series redirector info to the README -- Add search by buttons and path forwarding for search types (_Functionality not implemented yet_) +- Add search by buttons and path forwarding for search types (~~_Functionality not implemented yet_~~) - Add Codeberg information for issues on the privacy policy ### Fixed diff --git a/components/aboutpage/AboutHero.js b/components/aboutpage/AboutHero.js index 76dc6f7..70d22e2 100644 --- a/components/aboutpage/AboutHero.js +++ b/components/aboutpage/AboutHero.js @@ -3,35 +3,33 @@ import React from "react"; const AboutHero = () => { return ( - <> -
-
-

- A{" "} - - free{" "} - - and{" "} - - open - {" "} - - source - {" "} - alternative Goodreads front-end focused on{" "} - - privacy - - . -

- - - -
+
+
+

+ A{" "} + + free{" "} + + and{" "} + + open + {" "} + + source + {" "} + alternative Goodreads front-end focused on{" "} + + privacy + + . +

+ + +
- +
); }; diff --git a/components/authorpage/AuthorBookList.js b/components/authorpage/AuthorBookList.js new file mode 100644 index 0000000..75d1380 --- /dev/null +++ b/components/authorpage/AuthorBookList.js @@ -0,0 +1,193 @@ +/* eslint-disable @next/next/no-img-element */ +import { useState } from "react"; +import Meta from "../global/Meta"; +import BookList from "../listpage/BookList"; +import ErrorMessage from "../global/ErrorMessage"; + +const ListResults = ({ scrapedData }) => { + const [updatedData, setUpdatedData] = useState({}); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(false); + + const previousPageURL = + Object.keys(updatedData).length === 0 + ? scrapedData.previousPage + : updatedData.previousPage; + + const nextPageURL = + Object.keys(updatedData).length === 0 + ? scrapedData.nextPage + : updatedData.nextPage; + + const fetchPreviousPage = async () => { + setIsLoading(true); + const res = await fetch(`/api/author/books`, { + method: "POST", + headers: { + "content-type": "application/json", + }, + body: JSON.stringify({ + page: 1, + queryURL: `https://www.goodreads.com${previousPageURL}`, + }), + }); + if (res.ok) { + const data = await res.json(); + setUpdatedData(data); + setIsLoading(false); + } else { + setError(true); + } + }; + + const fetchNextPage = async () => { + setIsLoading(true); + const res = await fetch(`/api/author/books`, { + method: "POST", + headers: { + "content-type": "application/json", + }, + body: JSON.stringify({ + page: 1, + queryURL: `https://www.goodreads.com${nextPageURL}`, + }), + }); + if (res.ok) { + const data = await res.json(); + setUpdatedData(data); + setIsLoading(false); + } else { + setError(true); + } + }; + + return ( +
+ + {error ? ( + + ) : ( +
+

+ {scrapedData.title && `${scrapedData.title}:`} +

+
+ {scrapedData.works && ( +
+

+ Works: +

+ {scrapedData.works} +
+ )} + + {scrapedData.desc && ( +
+

+ Description:{" "} +

+

+ {scrapedData.desc + .replace("Average", " ยท Average") + .replace("rating", "rating:")} +

+
+ )} +
+ {Object.keys(updatedData).length === 0 && scrapedData.books && ( + + )} + {Object.keys(updatedData).length != 0 && ( + + )} + {!isLoading && ( + + )} +
+ )} +
+ ); +}; + +export default ListResults; diff --git a/components/authorpage/AuthorBooks.js b/components/authorpage/AuthorBooks.js index 2a66e43..1297aa8 100644 --- a/components/authorpage/AuthorBooks.js +++ b/components/authorpage/AuthorBooks.js @@ -93,7 +93,7 @@ const AuthorBooks = (props) => { {data.rating.split("avg")[0]}
-
+
{data.title.slice(0, 40)} @@ -106,6 +106,45 @@ const AuthorBooks = (props) => { )}
))} + {props.scrapeURL && ( + + +
+ + + + + + +

+ View More Books +

+
+
+ + )} + + {/* END */}
- -
- + Subject + + + +
+ + +
+

+ By submitting this form you agree to our{" "} + + Privacy Policy + + . +

+ + + ); }; diff --git a/components/contactpage/ContactHero.js b/components/contactpage/ContactHero.js index 6e795fd..991395c 100644 --- a/components/contactpage/ContactHero.js +++ b/components/contactpage/ContactHero.js @@ -2,38 +2,36 @@ import React from "react"; const ContactHero = () => { return ( - <> -
-
-

- Contact Us -

-

- Have something to say? Please feel free to open an issue on{" "} - - GitHub - {" "} - or{" "} - - Codeberg - - .{" "} - {process.env.NEXT_PUBLIC_DISABLE_CONTACT_FORM != "true" && - "Alternatively, you can use the contact form below."} -

-
+
+
+

+ Contact Us +

+

+ Have something to say? Please feel free to open an issue on{" "} + + GitHub + {" "} + or{" "} + + Codeberg + + .{" "} + {process.env.NEXT_PUBLIC_DISABLE_CONTACT_FORM != "true" && + "Alternatively, you can use the contact form below."} +

- +
); }; diff --git a/components/global/Footer.js b/components/global/Footer.js index 13f2682..5f4484d 100644 --- a/components/global/Footer.js +++ b/components/global/Footer.js @@ -2,8 +2,8 @@ import React from "react"; import Link from "next/link"; const Footer = () => { - const version = "v2.14.2"; - const versionSlug = "2142---jun-30-2023"; + const version = "v2.15.0"; + const versionSlug = "2150---jul-15-2023"; console.log(`%c${version} (Oreki)`, `color:green`); diff --git a/components/global/ThemeToggle.js b/components/global/ThemeToggle.js index 0694e65..c1d179f 100644 --- a/components/global/ThemeToggle.js +++ b/components/global/ThemeToggle.js @@ -22,55 +22,54 @@ const ThemeToggle = () => { return ( // Change the button icon based on the preferred theme - <> - - + + ); }; diff --git a/components/listpage/BookList.js b/components/listpage/BookList.js index aa6e0a8..8003523 100644 --- a/components/listpage/BookList.js +++ b/components/listpage/BookList.js @@ -7,82 +7,108 @@ const BookList = (props) => { return (
- {props.books.map((data, i) => ( -
- -
-
-

{data.bookNumber}

- -

- {data.title} -

- - -

{data.author}

- - -
- +
+
+
+
+
+
+ )} + {!props.loading && + props.books.map((data, i) => ( +
- -
- ))} + +
+ ))}
); }; diff --git a/package-lock.json b/package-lock.json index 4f2568a..ff1b3bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "biblioreads", - "version": "2.14.2", + "version": "2.15.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "biblioreads", - "version": "2.14.2", + "version": "2.15.0", "license": "AGPL-3.0-or-later", "dependencies": { "babel-loader": "^9.1.0", diff --git a/package.json b/package.json index d1850c8..4a15e21 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "biblioreads", - "version": "2.14.2", + "version": "2.15.0", "description": "An Alternative Goodreads Front-End", "private": true, "author": "Nesaku", diff --git a/pages/api/author/books.js b/pages/api/author/books.js new file mode 100644 index 0000000..a35b73a --- /dev/null +++ b/pages/api/author/books.js @@ -0,0 +1,92 @@ +const cheerio = require("cheerio"); + +const BooksScraper = async (req, res) => { + if (req.method === "POST") { + // The default sort is by popularity + // Use the URL parameter "per_page" to get 100 instead of the default 30 books + const scrapeURL = + req.body.queryURL.split("&")[0] + `?page=${req.body.page}&per_page=100`; + try { + const response = await fetch(`${scrapeURL}`, { + method: "GET", + headers: new Headers({ + "User-Agent": + process.env.NEXT_PUBLIC_USER_AGENT || + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36", + }), + }); + + const htmlString = await response.text(); + const $ = cheerio.load(htmlString); + const title = $("div.mainContentFloat > h1").text(); + /* + const author = $("div.leftContainer > div > a.authorName").text(); + const authorURL = $("div.leftContainer > div > a.authorName").attr( + "href" + ); + const authorIMG = $("div.leftContainer > a > img").attr("src"); + */ + const desc = $("div.leftContainer > div:nth-child(2)").text(); + const books = $("tbody > tr") + .map((i, el) => { + const $el = $(el); + const cover = $el.find("td > a > img.bookCover").attr("src"); + const title = $el.find("td > a > span").text(); + const bookURL = $el.find("td > a").attr("href"); + const author = $el + .find("td > span[itemprop = 'author'] > div > a > span") + .text(); + const authorURL = $el + .find("td > span[itemprop = 'author'] > div > a") + .attr("href"); + const rating = $el + .find("td > div > span.greyText.smallText.uitext > span") + .text(); + const id = i + 1; + return { + id: id, + cover: cover, + title: title, + bookURL: bookURL, + author: author, + authorURL: authorURL, + rating: rating, + }; + }) + .toArray(); + const previousPage = $( + "div.leftContainer > div[style='float: right'] > div > a.previous_page" + ).attr("href"); + const nextPage = $( + "div.leftContainer > div[style='float: right'] > div > a.next_page" + ).attr("href"); + const lastScraped = new Date().toISOString(); + res.statusCode = 200; + return res.json({ + status: "Received", + source: "https://github.com/nesaku/biblioreads", + scrapeURL: scrapeURL, + title: title, + desc: desc, + books: books, + previousPage: previousPage, + nextPage: nextPage, + lastScraped: lastScraped, + }); + } catch (error) { + res.statusCode = 404; + console.error("An Error Has Occurred"); + return res.json({ + status: "Error - Invalid Query", + scrapeURL: scrapeURL, + }); + } + } else { + res.statusCode = 405; + return res.json({ + status: "Error 405 - Method Not Allowed", + }); + } +}; + +export default BooksScraper; diff --git a/pages/api/author-scraper.js b/pages/api/author/info.js similarity index 100% rename from pages/api/author-scraper.js rename to pages/api/author/info.js diff --git a/pages/author/list/[...slug].js b/pages/author/list/[...slug].js new file mode 100644 index 0000000..3c173b8 --- /dev/null +++ b/pages/author/list/[...slug].js @@ -0,0 +1,74 @@ +import { useEffect, useState } from "react"; +import { useRouter } from "next/router"; + +import Header from "../../../components/global/Header"; +import Footer from "../../../components/global/Footer"; +import Loader from "../../../components/global/Loader"; +import ErrorMessage from "../../../components/global/ErrorMessage"; +import AuthorBookList from "../../../components/authorpage/AuthorBookList"; + +const Slug = () => { + const router = useRouter(); + const { slug } = router.query; + const [scrapedData, setScrapedData] = useState({}); + const [error, setError] = useState(false); + + useEffect(() => { + const fetchData = async () => { + const res = await fetch(`/api/author/books`, { + method: "POST", + headers: { + "content-type": "application/json", + }, + body: JSON.stringify({ + page: 1, + queryURL: `https://www.goodreads.com/author/list/${slug}`, + }), + }); + if (res.ok) { + const data = await res.json(); + setScrapedData(data); + } else { + setError(true); + } + }; + if (slug) { + fetchData(); + } + }, [slug]); + + return ( +
+
+
+ {error && ( + + )} + {!error && ( + <> + {scrapedData.title === undefined && } + {scrapedData.error && ( + + )} + {scrapedData.title === "" && ( + + )} + {scrapedData && } + + )} +
+
+
+ ); +}; + +export default Slug; diff --git a/pages/author/show/[...slug].js b/pages/author/show/[...slug].js index 32f0246..bcebf99 100644 --- a/pages/author/show/[...slug].js +++ b/pages/author/show/[...slug].js @@ -15,7 +15,7 @@ const Slug = () => { useEffect(() => { const fetchData = async () => { - const res = await fetch(`/api/author-scraper`, { + const res = await fetch(`/api/author/info`, { method: "POST", headers: { "content-type": "application/json", diff --git a/pages/list/show/[...slug].js b/pages/list/show/[...slug].js index 8b25141..4ba3273 100644 --- a/pages/list/show/[...slug].js +++ b/pages/list/show/[...slug].js @@ -44,7 +44,7 @@ const Slug = () => { {error && ( )} {!error && ( @@ -53,13 +53,13 @@ const Slug = () => { {scrapedData.error && ( )} {scrapedData.title === "" && ( )} {scrapedData && }