From c0602fbe3b476ea568c855250d82bd13f61aef4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaro=20Mari=C3=B1o?= Date: Mon, 16 Oct 2023 22:04:03 +0200 Subject: [PATCH] feat: add BookList UI with fake data and books factory - Install @faker-js/faker dependency for generating fake data - Add BookList component with mock data using the books factory - Add custom favicon to the app --- index.html | 4 +- package-lock.json | 16 +++++++ package.json | 1 + public/favicon.ico | Bin 0 -> 15406 bytes src/App.test.tsx | 4 +- src/App.tsx | 4 +- src/components/BookList/BookList.styles.ts | 26 ++++++++++++ src/components/BookList/BookList.test.tsx | 39 ++++++++++++++++++ src/components/BookList/BookList.tsx | 31 ++++++++++++++ .../components/CircularProgressWithLabel.tsx | 32 ++++++++++++++ .../CircularProgressWithlabel.test.tsx | 14 +++++++ src/components/BookList/components/index.ts | 1 + src/components/BookList/index.ts | 1 + src/components/index.ts | 1 + src/data/books.ts | 20 +++++++++ src/index.css | 1 + 16 files changed, 190 insertions(+), 5 deletions(-) create mode 100644 public/favicon.ico create mode 100644 src/components/BookList/BookList.styles.ts create mode 100644 src/components/BookList/BookList.test.tsx create mode 100644 src/components/BookList/BookList.tsx create mode 100644 src/components/BookList/components/CircularProgressWithLabel.tsx create mode 100644 src/components/BookList/components/CircularProgressWithlabel.test.tsx create mode 100644 src/components/BookList/components/index.ts create mode 100644 src/components/BookList/index.ts create mode 100644 src/components/index.ts create mode 100644 src/data/books.ts diff --git a/index.html b/index.html index e4b78ea..bb4dc19 100644 --- a/index.html +++ b/index.html @@ -1,8 +1,8 @@ - + - + Vite + React + TS diff --git a/package-lock.json b/package-lock.json index 8c1ca3f..c2219e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", + "@faker-js/faker": "^8.2.0", "@fontsource/roboto": "^5.0.8", "@mui/material": "^5.14.13", "react": "^18.2.0", @@ -1010,6 +1011,21 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@faker-js/faker": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.2.0.tgz", + "integrity": "sha512-VacmzZqVxdWdf9y64lDOMZNDMM/FQdtM9IsaOPKOm2suYwEatb8VkdHqOzXcDnZbk7YDE2BmsJmy/2Hmkn563g==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0", + "npm": ">=6.14.13" + } + }, "node_modules/@floating-ui/core": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.0.tgz", diff --git a/package.json b/package.json index 004f213..cd3d47f 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "dependencies": { "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", + "@faker-js/faker": "^8.2.0", "@fontsource/roboto": "^5.0.8", "@mui/material": "^5.14.13", "react": "^18.2.0", diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..e62a9c7d371a39896acdbd07d1ff7d2261e3ab59 GIT binary patch literal 15406 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 wVsg6nFxhF*mNNajC*7W*Z|Y { diff --git a/src/App.tsx b/src/App.tsx index caa0929..0a881da 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,3 +1,5 @@ -const App = () =>

Welcome to Bukie!

; +import { Booklist } from "./components/"; + +const App = () => ; export default App; diff --git a/src/components/BookList/BookList.styles.ts b/src/components/BookList/BookList.styles.ts new file mode 100644 index 0000000..13095ab --- /dev/null +++ b/src/components/BookList/BookList.styles.ts @@ -0,0 +1,26 @@ +import styled from "@emotion/styled"; +import { Card, CardMedia, CardMediaProps } from "@mui/material"; +export const Root = styled.div` + display: flex; + justify-self: center; + align-items: center; +`; + +export const CardWrapper = styled(Card)` + display: flex; + flex-direction: column; + justify-content: space-between; + width: 80%; + margin: 0 auto; + margin-bottom: 36px; + padding: 8px; + border: 1px solid #ccc; + border-radius: 8px; +`; + +export const Cover = styled(CardMedia)` + height: auto; + width: 100%; + margin-bottom: 4px; + border-radius: 8px; +`; diff --git a/src/components/BookList/BookList.test.tsx b/src/components/BookList/BookList.test.tsx new file mode 100644 index 0000000..8f10be0 --- /dev/null +++ b/src/components/BookList/BookList.test.tsx @@ -0,0 +1,39 @@ +import { render } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { Book } from "../../data/books"; +import BookList from "./BookList"; + +describe("BookList", () => { + it("renders a list of books", () => { + vi.mock("../../data/books", () => ({ + books: [ + { + title: "The Great Gatsby", + author: "F. Scott Fitzgerald", + year: "1925", + image: "https://picsum.photos/150/150", + rating: 80, + }, + { + title: "To Kill a Mockingbird", + author: "Harper Lee", + year: "1960", + image: "https://picsum.photos/150/150", + rating: 90, + }, + { + title: "1984", + author: "George Orwell", + year: "1949", + image: "https://picsum.photos/150/150", + rating: 70, + }, + ] as Book[], + })); + + const { getAllByRole } = render(); + const bookTitles = getAllByRole("heading").map((heading) => heading.textContent); + + expect(bookTitles).toEqual(["The Great Gatsby", "To Kill a Mockingbird", "1984"]); + }); +}); diff --git a/src/components/BookList/BookList.tsx b/src/components/BookList/BookList.tsx new file mode 100644 index 0000000..8d1b022 --- /dev/null +++ b/src/components/BookList/BookList.tsx @@ -0,0 +1,31 @@ +import { CardActionArea, CardContent, Grid, Typography } from "@mui/material"; +import { Book, books } from "../../data/books"; +import { Root, CardWrapper, Cover } from "./BookList.styles"; +import { CircularProgressWithLabel } from "./components"; + +const BookList = () => ( + + + {books.map((book: Book) => ( + + + + + + + {book.title} + + + {book.author} ({book.year}) + + + + + + + ))} + + +); + +export default BookList; diff --git a/src/components/BookList/components/CircularProgressWithLabel.tsx b/src/components/BookList/components/CircularProgressWithLabel.tsx new file mode 100644 index 0000000..d3464ed --- /dev/null +++ b/src/components/BookList/components/CircularProgressWithLabel.tsx @@ -0,0 +1,32 @@ +import { Box, CircularProgress, CircularProgressProps, Typography } from "@mui/material"; + +const CircularProgressWithLabel = (props: CircularProgressProps & { value: number }) => { + let color: CircularProgressProps["color"] = "error"; + if (props.value >= 70) { + color = "success"; + } else if (props.value >= 40) { + color = "warning"; + } + + return ( + + + + + {`${Math.round(props.value)}%`} + + + + ); +}; + +export default CircularProgressWithLabel; diff --git a/src/components/BookList/components/CircularProgressWithlabel.test.tsx b/src/components/BookList/components/CircularProgressWithlabel.test.tsx new file mode 100644 index 0000000..5b45a99 --- /dev/null +++ b/src/components/BookList/components/CircularProgressWithlabel.test.tsx @@ -0,0 +1,14 @@ +import { render } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import CircularProgressWithLabel from "./CircularProgressWithLabel"; + +describe("CircularProgressWithLabel", () => { + it("should render a circular progress bar with a label", () => { + const value = 50; + + const { getByText } = render(); + const progress = getByText(`${value}%`); + + expect(progress.textContent).toBe("50%"); + }); +}); diff --git a/src/components/BookList/components/index.ts b/src/components/BookList/components/index.ts new file mode 100644 index 0000000..a77d78a --- /dev/null +++ b/src/components/BookList/components/index.ts @@ -0,0 +1 @@ +export { default as CircularProgressWithLabel } from "./CircularProgressWithLabel"; diff --git a/src/components/BookList/index.ts b/src/components/BookList/index.ts new file mode 100644 index 0000000..d96408d --- /dev/null +++ b/src/components/BookList/index.ts @@ -0,0 +1 @@ +export { default as Booklist } from "./BookList"; diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 0000000..7053507 --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1 @@ +export { Booklist } from "./BookList"; diff --git a/src/data/books.ts b/src/data/books.ts new file mode 100644 index 0000000..9831f38 --- /dev/null +++ b/src/data/books.ts @@ -0,0 +1,20 @@ +import { faker } from "@faker-js/faker"; + +export type Book = { + title: string; + author: string; + year: string; + image: string; + rating: number; +}; + +export const generateBooks = (count: number): Book[] => + Array.from({ length: count }, () => ({ + title: faker.lorem.words(3), + author: faker.person.fullName(), + year: String(faker.date.past({ years: 100 }).getFullYear()), + image: faker.image.urlLoremFlickr({ width: 150, height: 150, category: "book" }), + rating: faker.number.int({ min: 0, max: 100 }), + })); + +export const books: Book[] = generateBooks(30); diff --git a/src/index.css b/src/index.css index 2c3fac6..b20208f 100644 --- a/src/index.css +++ b/src/index.css @@ -29,6 +29,7 @@ body { place-items: center; min-width: 320px; min-height: 100vh; + justify-content: center; } h1 {