diff --git a/app/[id]/page.tsx b/app/[id]/page.tsx index 2a1a930..5f85b02 100644 --- a/app/[id]/page.tsx +++ b/app/[id]/page.tsx @@ -1,67 +1,72 @@ -import { fetchBookById } from '../lib/data'; -import Link from 'next/link'; -import Tile from '../components/tile'; +import { fetchBookById } from "../lib/data"; +import Link from "next/link"; +import Tile from "../components/tile"; export default async function Page({ params }: { params: { id: string } }) { - const book = await fetchBookById(params.id); - return ( -
- - ← Back to all books - -
-
- -
-
-
{book.title}
-
{book.author}
- -
{book.description}
-
-
-
- ); + const book = await fetchBookById(params.id); + return ( +
+ + ← Back to all books + +
+
+ +
+
+
{book.title}
+
{book.author}
+ +
{book.description}
+
+
+
+ ); } const StarRating = ({ rating }: { rating: number }) => { - const totalStars = 5; - let remainingRating = rating; + const totalStars = 5; + let remainingRating = rating; - // Generate stars based on the rating - const stars = Array.from({ length: totalStars }).map((_, index) => { - let fill = 'white'; - if (remainingRating >= 1) { - fill = 'gold'; - remainingRating -= 1; - } else if (remainingRating > 0) { - fill = 'half'; - remainingRating = 0; - } - return ; - }); + // Generate stars based on the rating + const stars = Array.from({ length: totalStars }).map((_, index) => { + let fill = "white"; + if (remainingRating >= 1) { + fill = "gold"; + remainingRating -= 1; + } else if (remainingRating > 0) { + fill = "half"; + remainingRating = 0; + } + return ; + }); - return
{stars}
; + return
{stars}
; }; -const SVGStar = ({ fill = 'none' }) => { - const fillColor = fill === 'half' ? 'url(#half)' : fill; +const SVGStar = ({ fill = "none" }) => { + const fillColor = fill === "half" ? "url(#half)" : fill; - return ( - - - - - - - - - - ); + return ( + + + + + + + + + + ); }; diff --git a/app/components/panel.tsx b/app/components/panel.tsx index 0150a65..fe7ca93 100644 --- a/app/components/panel.tsx +++ b/app/components/panel.tsx @@ -1,121 +1,126 @@ -'use client'; +"use client"; -import { useRouter } from 'next/navigation'; -import { useOptimistic, useTransition, useState } from 'react'; -import { ChevronDownIcon } from '@heroicons/react/24/outline'; +import { useRouter } from "next/navigation"; +import { useOptimistic, useTransition, useState } from "react"; +import { ChevronDownIcon } from "@heroicons/react/24/outline"; interface ExpandedSections { - [key: string]: boolean; + [key: string]: boolean; } export default function Panel({ - authors, - allAuthors + authors, + allAuthors, }: { - authors: string[]; - allAuthors: string[]; + authors: string[]; + allAuthors: string[]; }) { - let router = useRouter(); - let [pending, startTransition] = useTransition(); - let [optimisticAuthors, setOptimisticAuthors] = useOptimistic(authors); - let [expandedSections, setExpandedSections] = useState({}); + let router = useRouter(); + let [pending, startTransition] = useTransition(); + let [optimisticAuthors, setOptimisticAuthors] = useOptimistic(authors); + let [expandedSections, setExpandedSections] = useState({}); - const authorGroups = allAuthors.reduce( - (acc: { [key: string]: string[] }, author: string) => { - const firstLetter = author[0].toUpperCase(); // Get the first letter, capitalize it - if (!acc[firstLetter]) { - acc[firstLetter] = []; // Initialize the array if this is the first author with this letter - } - acc[firstLetter].push(author); // Add the author to the appropriate array - return acc; // Return the updated accumulator - }, - {} as { [key: string]: string[] } - ); - const toggleSection = (letter: string): void => { - setExpandedSections((prev: Record) => ({ - ...prev, - [letter]: !prev[letter] - })); - }; + const authorGroups = allAuthors.reduce( + (acc: { [key: string]: string[] }, author: string) => { + const firstLetter = author[0].toUpperCase(); // Get the first letter, capitalize it + if (!acc[firstLetter]) { + acc[firstLetter] = []; // Initialize the array if this is the first author with this letter + } + acc[firstLetter].push(author); // Add the author to the appropriate array + return acc; // Return the updated accumulator + }, + {} as { [key: string]: string[] }, + ); + const toggleSection = (letter: string): void => { + setExpandedSections((prev: Record) => ({ + ...prev, + [letter]: !prev[letter], + })); + }; - return ( -
-
-
-

Authors

- {Object.entries(authorGroups).map(([letter, authors]) => ( -
- -
- {expandedSections[letter] && - authors.map((author) => ( - +
+ {expandedSections[letter] && + authors.map((author) => ( + - ))} -
-
- ))} -
-
+ router.push(`?${newParams}`); + }); + }} + key={author} + className="flex items-center space-x-2 text-xs text-left" + > + +
{author}
+ + ))} +
+
+ ))} + + - {optimisticAuthors.length > 0 && ( -
-
- {optimisticAuthors.map((author) => ( -

{author}

- ))} -
- -
- )} - - ); + {optimisticAuthors.length > 0 && ( +
+
+ {optimisticAuthors.map((author) => ( +

{author}

+ ))} +
+ +
+ )} + + ); } diff --git a/app/components/tile.tsx b/app/components/tile.tsx index 8c5a3d9..37da216 100644 --- a/app/components/tile.tsx +++ b/app/components/tile.tsx @@ -1,41 +1,41 @@ -'use client'; -import { useState } from 'react'; -import Image from 'next/image'; -import { BookSkeleton } from './loading-skeleton'; -import { PhotoIcon } from '@heroicons/react/24/outline'; +"use client"; +import { useState } from "react"; +import Image from "next/image"; +import { BookSkeleton } from "./loading-skeleton"; +import { PhotoIcon } from "@heroicons/react/24/outline"; const Tile = ({ src, title }: { src: string; title: string }) => { - const [isOptimized, setIsOptimized] = useState(true); - const [isLoading, setIsLoading] = useState(true); - return ( -
- {src ? ( - <> - {isLoading && } - {title} { - setIsOptimized(false); - }} - onLoad={() => { - setIsLoading(false); - }} - /> - - ) : ( -
- -

No image available

-
- )} -
- ); + const [isOptimized, setIsOptimized] = useState(true); + const [isLoading, setIsLoading] = useState(true); + return ( +
+ {src ? ( + <> + {isLoading && } + {title} { + setIsOptimized(false); + }} + onLoad={() => { + setIsLoading(false); + }} + /> + + ) : ( +
+ +

No image available

+
+ )} +
+ ); }; export default Tile; diff --git a/app/globals.css b/app/globals.css index 20bc910..d420756 100644 --- a/app/globals.css +++ b/app/globals.css @@ -3,7 +3,7 @@ @tailwind utilities; .star { - color: gold; - font-size: 24px; - user-select: none; /* prevent text selection */ + color: gold; + font-size: 24px; + user-select: none; /* prevent text selection */ } diff --git a/app/lib/data.ts b/app/lib/data.ts index dc40953..626b861 100644 --- a/app/lib/data.ts +++ b/app/lib/data.ts @@ -1,20 +1,20 @@ -import { sql } from '@vercel/postgres'; -import { unstable_noStore as noStore } from 'next/cache'; +import { sql } from "@vercel/postgres"; +import { unstable_noStore as noStore } from "next/cache"; const ITEMS_PER_PAGE = 30; export async function fetchFilteredBooks( - selectedAuthors: string[], - query: string, - currentPage: number + selectedAuthors: string[], + query: string, + currentPage: number, ) { - noStore(); - const offset = (currentPage - 1) * ITEMS_PER_PAGE; - if (selectedAuthors.length > 0) { - try { - const authorsDelimited = selectedAuthors.join('|'); + noStore(); + const offset = (currentPage - 1) * ITEMS_PER_PAGE; + if (selectedAuthors.length > 0) { + try { + const authorsDelimited = selectedAuthors.join("|"); - const books = await sql` + const books = await sql` SELECT ALL id, isbn, @@ -38,15 +38,15 @@ export async function fetchFilteredBooks( ORDER BY "createdAt" DESC LIMIT ${ITEMS_PER_PAGE} OFFSET ${offset} `; - return books.rows; - } catch (error) { - console.error('Database Error:', error); - throw new Error('Failed to fetch books.'); - } - } + return books.rows; + } catch (error) { + console.error("Database Error:", error); + throw new Error("Failed to fetch books."); + } + } - try { - const books = await sql` + try { + const books = await sql` SELECT ALL id, isbn, @@ -68,39 +68,39 @@ export async function fetchFilteredBooks( ORDER BY "createdAt" DESC LIMIT ${ITEMS_PER_PAGE} OFFSET ${offset} `; - return books.rows; - } catch (error) { - console.error('Database Error:', error); - throw new Error('Failed to fetch books.'); - } + return books.rows; + } catch (error) { + console.error("Database Error:", error); + throw new Error("Failed to fetch books."); + } } export async function fetchBookById(id: string) { - const data = await sql`SELECT * FROM books WHERE id = ${id}`; - return data.rows[0]; + const data = await sql`SELECT * FROM books WHERE id = ${id}`; + return data.rows[0]; } export async function fetchAuthors() { - try { - const authors = await sql` + try { + const authors = await sql` SELECT DISTINCT "author" FROM books ORDER BY "author" `; - return authors.rows?.map((row) => row.author); - } catch (error) { - console.error('Database Error:', error); - throw new Error('Failed to fetch authors.'); - } + return authors.rows?.map((row) => row.author); + } catch (error) { + console.error("Database Error:", error); + throw new Error("Failed to fetch authors."); + } } export async function fetchPages(query: string, selectedAuthors: string[]) { - noStore(); - if (selectedAuthors.length > 0) { - try { - const authorsDelimited = selectedAuthors.join('|'); + noStore(); + if (selectedAuthors.length > 0) { + try { + const authorsDelimited = selectedAuthors.join("|"); - const count = await sql` + const count = await sql` SELECT COUNT(*) FROM books WHERE @@ -112,16 +112,18 @@ export async function fetchPages(query: string, selectedAuthors: string[]) { publisher ILIKE ${`%${query}%`} ) `; - const totalPages = Math.ceil(Number(count.rows[0].count) / ITEMS_PER_PAGE); - return totalPages; - } catch (error) { - console.error('Database Error:', error); - throw new Error('Failed to fetch books.'); - } - } + const totalPages = Math.ceil( + Number(count.rows[0].count) / ITEMS_PER_PAGE, + ); + return totalPages; + } catch (error) { + console.error("Database Error:", error); + throw new Error("Failed to fetch books."); + } + } - try { - const count = await sql` + try { + const count = await sql` SELECT COUNT(*) FROM books WHERE @@ -131,10 +133,10 @@ export async function fetchPages(query: string, selectedAuthors: string[]) { "year"::text ILIKE ${`%${query}%`} OR publisher ILIKE ${`%${query}%`} `; - const totalPages = Math.ceil(Number(count.rows[0].count) / ITEMS_PER_PAGE); - return totalPages; - } catch (error) { - console.error('Database Error:', error); - throw new Error('Failed to fetch total number of books.'); - } + const totalPages = Math.ceil(Number(count.rows[0].count) / ITEMS_PER_PAGE); + return totalPages; + } catch (error) { + console.error("Database Error:", error); + throw new Error("Failed to fetch total number of books."); + } } diff --git a/next.config.mjs b/next.config.mjs index fe45905..e6afc14 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,14 +1,14 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - images: { - remotePatterns: [ - { - protocol: 'https', - hostname: 'i.gr-assets.com', - port: '' - } - ] - } + images: { + remotePatterns: [ + { + protocol: "https", + hostname: "i.gr-assets.com", + port: "", + }, + ], + }, }; export default nextConfig; diff --git a/scripts/seed.mjs b/scripts/seed.mjs index 1ca77a7..1c347ec 100644 --- a/scripts/seed.mjs +++ b/scripts/seed.mjs @@ -1,24 +1,24 @@ -import { db } from '@vercel/postgres'; -import fs from 'fs'; -import path from 'path'; -import Papa from 'papaparse'; -import '../envConfig.mjs'; +import { db } from "@vercel/postgres"; +import fs from "fs"; +import path from "path"; +import Papa from "papaparse"; +import "../envConfig.mjs"; const parseCSV = async (filePath) => { - const csvFile = fs.readFileSync(path.resolve(filePath), 'utf8'); - return new Promise((resolve) => { - Papa.parse(csvFile, { - header: true, - complete: (results) => { - resolve(results.data); - } - }); - }); + const csvFile = fs.readFileSync(path.resolve(filePath), "utf8"); + return new Promise((resolve) => { + Papa.parse(csvFile, { + header: true, + complete: (results) => { + resolve(results.data); + }, + }); + }); }; async function seed(client) { - // Creating the books table - const createBooksTable = await client.sql` + // Creating the books table + const createBooksTable = await client.sql` CREATE TABLE IF NOT EXISTS books ( id SERIAL PRIMARY KEY, isbn VARCHAR(255) UNIQUE NOT NULL, @@ -32,39 +32,42 @@ async function seed(client) { "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); `; - console.log('Created "books" table'); + console.log('Created "books" table'); - const bookData = await parseCSV('./books.csv'); + const bookData = await parseCSV("./books.csv"); - // Inserting book data into the books table - const promises = bookData.map((book, index) => { - // An error occurred while attempting to seed the database: error: null value in column "isbn" of relation "books" violates not-null constraint - if (!book.isbn) { - console.error(`Skipping book at index ${index} due to missing ISBN`); - return Promise.resolve(); - } - return client.sql` + // Inserting book data into the books table + const promises = bookData.map((book, index) => { + // An error occurred while attempting to seed the database: error: null value in column "isbn" of relation "books" violates not-null constraint + if (!book.isbn) { + console.error(`Skipping book at index ${index} due to missing ISBN`); + return Promise.resolve(); + } + return client.sql` INSERT INTO books (isbn, "title", "author", "year", publisher, "image", "description", "rating") VALUES (${book.bookId}, ${book.title}, ${book.author}, ${book.publisherDate}, ${book.Publisher}, ${book.coverImg}, ${book.description}, ${book.rating}) ON CONFLICT (isbn) DO NOTHING; `; - }); + }); - const results = await Promise.all(promises); - console.log(`Seeded ${results.length} books`); + const results = await Promise.all(promises); + console.log(`Seeded ${results.length} books`); - return { - createBooksTable, - seededBooks: results.length - }; + return { + createBooksTable, + seededBooks: results.length, + }; } async function main() { - const client = await db.connect(); - await seed(client); - await client.end(); + const client = await db.connect(); + await seed(client); + await client.end(); } main().catch((err) => { - console.error('An error occurred while attempting to seed the database:', err); + console.error( + "An error occurred while attempting to seed the database:", + err, + ); });