Skip to content

Commit

Permalink
feat: add BookList UI with fake data and books factory
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
Amaro Mariño committed Oct 16, 2023
1 parent ebdd859 commit c0602fb
Show file tree
Hide file tree
Showing 16 changed files with 190 additions and 5 deletions.
4 changes: 2 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
Expand Down
16 changes: 16 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Binary file added public/favicon.ico
Binary file not shown.
4 changes: 2 additions & 2 deletions src/App.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import { describe, it } from "vitest";
import { render } from "@testing-library/react";
import App from "./App";

describe("App", () => {
Expand Down
4 changes: 3 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const App = () => <h1>Welcome to Bukie!</h1>;
import { Booklist } from "./components/";

const App = () => <Booklist />;

export default App;
26 changes: 26 additions & 0 deletions src/components/BookList/BookList.styles.ts
Original file line number Diff line number Diff line change
@@ -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)<CardMediaProps>`
height: auto;
width: 100%;
margin-bottom: 4px;
border-radius: 8px;
`;
39 changes: 39 additions & 0 deletions src/components/BookList/BookList.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<BookList />);
const bookTitles = getAllByRole("heading").map((heading) => heading.textContent);

expect(bookTitles).toEqual(["The Great Gatsby", "To Kill a Mockingbird", "1984"]);
});
});
31 changes: 31 additions & 0 deletions src/components/BookList/BookList.tsx
Original file line number Diff line number Diff line change
@@ -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 = () => (
<Root>
<Grid container spacing={2} maxWidth='100%' mx='auto'>
{books.map((book: Book) => (
<Grid item xs={12} sm={6} md={4} key={book.title}>
<CardWrapper>
<CardActionArea>
<Cover component='img' image={book.image} aria-label={book.title} />
<CardContent>
<Typography gutterBottom variant='h5' component='h2' align='center'>
{book.title}
</Typography>
<Typography variant='body2' color='textSecondary' component='p' align='center'>
{book.author} ({book.year})
</Typography>
</CardContent>
</CardActionArea>
<CircularProgressWithLabel value={book.rating} />
</CardWrapper>
</Grid>
))}
</Grid>
</Root>
);

export default BookList;
32 changes: 32 additions & 0 deletions src/components/BookList/components/CircularProgressWithLabel.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Box display='flex' alignItems='center' justifyContent='center' position='relative'>
<CircularProgress variant='determinate' {...props} value={props.value} color={color} />
<Box
display='flex'
alignItems='center'
justifyContent='center'
position='absolute'
top={0}
bottom={0}
left={0}
right={0}
>
<Typography variant='caption' component='div' color='text.secondary'>
{`${Math.round(props.value)}%`}
</Typography>
</Box>
</Box>
);
};

export default CircularProgressWithLabel;
Original file line number Diff line number Diff line change
@@ -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(<CircularProgressWithLabel value={value} />);
const progress = getByText(`${value}%`);

expect(progress.textContent).toBe("50%");
});
});
1 change: 1 addition & 0 deletions src/components/BookList/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as CircularProgressWithLabel } from "./CircularProgressWithLabel";
1 change: 1 addition & 0 deletions src/components/BookList/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as Booklist } from "./BookList";
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Booklist } from "./BookList";
20 changes: 20 additions & 0 deletions src/data/books.ts
Original file line number Diff line number Diff line change
@@ -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);
1 change: 1 addition & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ body {
place-items: center;
min-width: 320px;
min-height: 100vh;
justify-content: center;
}

h1 {
Expand Down

0 comments on commit c0602fb

Please sign in to comment.