Skip to content

One Last Exercise

Alright, let's take what we've learned so far and use it to build a more real-world UI:

As with previous exercises, we'll start with an implementation that includes all of the standard React logic, but no animation. Your job is to set up the layout animation.

Like with all layout animations, the devil is in the details here. There are a couple of things to pay attention to.

When a book is moving from the grid to the reading list, it should have a slower transition. Notice in the GIF above that the selected book is still moving after the grid has settled.

Also, the “Close” buttons in the reading list shouldn't jump up or down awkwardly:

And finally, like we dealt with in the WidgetProcessor exercise, we want to be intentional about the order of the books. You'll find the exact specification in the Acceptance Criteria:

Acceptance Criteria:

  • Layout animations should be used for all book movement, including transitioning to/from the Reading List.
  • A smoother, more lethargic transition should be used for books moving to the reading list. You can use the default transition for when books shift around in the grid.
  • When books are removed from the reading list, the “X” buttons should smoothly slide as well, they shouldn't snap to their new position.
  • When adding a new book to the reading list, it should always be added to the end of the stack.
  • When dismissing a book from the reading list, it should always return to the end of the grid.

Code Playground

import React from 'react';

import DATA from './data';

import BookGrid from './BookGrid';
import ReadingList from './ReadingList';
import styles from './BookPage.module.css';

function BookPage() {
const [books, setBooks] = React.useState(DATA);

function toggleBook(toggledBook) {
const nextBooks = books.map((book) => {
if (book.isbn !== toggledBook.isbn) {
return book;
}

return {
...book,
selected: !book.selected,
};
})
setBooks(nextBooks);
}

const selectedBooks = books.filter(book => book.selected);
const unselectedBooks = books.filter(book => !book.selected);

return (
<div className={styles.wrapper}>
<BookGrid
className={styles.grid}
books={unselectedBooks}
handleSelectBook={toggleBook}
/>
{selectedBooks.length > 0 && (
<ReadingList
books={selectedBooks}
handleRemoveBook={toggleBook}
/>
)}
</div>
);
}

export default BookPage;

Solution:

Stretch goal: Reading list optimizations

As it stands, each item in the “Reading List” has a fixed height of 50px. This means we can only fit a handful of books before we run out of space!

What if we did something more dynamic?

In this stretch goal, you'll add two enhancements to the Reading List. First, you'll update the height of each list item, so that books are given less space the further back they are from the front of the stack:

Notice how the books further up in the stack get compressed, as each new book is added?

This makes it possible for us to fit more books, but it also makes it harder to see what the books actually are. Let's add the ability to expand the books, on hover/focus:

Your mission in this stretch goal is to add these two enhancements. This is not an easy challenge. Unless you've done stuff like this in the past, you probably won't be able to come up with a solution on your own. Don't let that discourage you from trying, though! You'll learn a ton either way. 😄

Acceptance Criteria:

  • Instead of each list item having a fixed height of 50px, the height should be dynamically calculated based on its distance from the top of the stack.
    • Specifically, the range should be from 50px to 10px, in 5px increments.
  • Hovering over a particular item should increase its height to 100px.
  • Focusing a particular book's “X” button should also increase that book's height to 100px, as a fallback for folks who don't use a pointer device.

To complete: Either keep working on your solution in the playground above, or build on my original solution in the playground below. Whichever you prefer!

This is a particularly tricky challenge, so I've provided some hints:

Code Playground

import React from 'react';

import DATA from './data';

import BookGrid from './BookGrid';
import ReadingList from './ReadingList';
import styles from './BookPage.module.css';

function BookPage() {
const [books, setBooks] = React.useState(DATA);

function toggleBook(toggledBook) {
const nextBooks = books.filter(
(book) => book.isbn !== toggledBook.isbn
);

nextBooks.push({
...toggledBook,
selected: !toggledBook.selected,
});

setBooks(nextBooks);
}

const selectedBooks = books.filter(
(book) => book.selected
);
const unselectedBooks = books.filter(
(book) => !book.selected
);

return (
<div className={styles.wrapper}>
<BookGrid
className={styles.grid}
books={unselectedBooks}
handleSelectBook={toggleBook}
/>
{selectedBooks.length > 0 && (
<ReadingList
books={selectedBooks}
handleRemoveBook={toggleBook}
/>
)}
</div>
);
}

export default BookPage;

Solution: