Objective:
For this assignment, we will continue our development effort from Assignment 4.Β Before you begin, make a copy of your assignment 4 directory and begin working from there.
Β
Note: If you require a working version of assignment 4 to continue with this assignment, please email your professor.
Β
The main focus for this assignment will be to leverage our knowledge of Jotai to add additional features to our βMet Artworkβ application created in Assignment 4.Β This will primarily consist of βFavouritesβ and βSearch Historyβ functionality.Β We will also address some issues around data integrity / usability from Assignment 4, including the issue of certain search queries returning invalid object IDβs (430610, 430542, 430524, etc) and the navigation menu not highlighting / collapsing when browsing the site on smaller devices.
Sample Solution:
https://web422βa5βfallβ2022.vercel.app/
Β
Please refer to the sample solution regularly to help you design your components and check your solution.Β Β It is there to help supplement the following requirements.
Β
Step 1: Solving the invalid objectID problem
Β
As you have seen from your own testing of Assignment 4, there are some search queries that result in invalid objectIDβs from the collectionapi.metmuseum.org/public/collection/v1 API.Β Fortunately, in addition to querying the API for a subset of artwork (ie: objectIDβs), the API also has another route available that provides a list of all 480000+ valid objectIDβs, via the following:
Β
https://collectionapi.metmuseum.org/public/collection/v1/objects
Β
For us to solve the invalid objectID issue, we must make sure our system is aware of all of the valid object ID values so that we may compute the intersection of our search result list and the valid objectID list.Β This will ensure that any invalid objectIDβs are excluded from the UI.
Β
Since there is so much data returned from the above URL and itβs not likely to change frequently, the fastest way for us to use it (ie: not slow down our searches) is to download it ahead of time and include it in our solution as a .json file.
Β
Β
Β
Β
To do this, you must:
Β
- Create a βdataβ folder in your appβs βpublicβ folder, for example β/WEB422-A5/my-app/public/dataβ
- Visit the https://collectionapi.metmuseum.org/public/collection/v1/objects route in a web browser
- (in Chrome / Firefox) Choose βFileβ > βSave Page Asβ (it should show the filename as βobjects.jsonβ)
- Navigate to your newly created βpublic/dataβ folder for your app and save the file as: json
With validObjectIDList.json now in your βpublicβ folder, we can now update our β/pages/artwork/index.jsβ page to use it in our searches:
Β
- Near the top of the file, add the line to import the json file from the βdataβ folder:
import validObjectIDList from β../../public/data/validObjectIDList.jsonβ
Β
- Next, we must use the βobjectIDsβ property from the βvalidObjectIDListβ to help filter our search results (recall: this is the array containing over 480000 objectIDs). This must be done in the βuseEffectβ hook, before calculating the βresultsβ array and updating the βartworkListβ state value.Β This can be done using the following code (NOTE: Please feel free to calculate βfilteredResultsβ using an alternative strategy, if you feel that it yields faster results):
Β
let filteredResults = validObjectIDList.objectIDs.filter(x => data.objectIDs?.includes(x));
Β
Essentially, we filter all objectID values in the validObjectIDList such that it only contains values that are also returned from our search.Β This has the effect of eliminating objectIDs from our search results that are not in the validObjectIDList.
Β
- Now, you can use the βfilteredResultsβ value in your loop to build the results array instead ofobjectIDs, ie:
Β
for (let i = 0; i < filteredResults.length; i += PER_PAGE) {Β Β Β Β const chunk = filteredResults.slice(i, i + PER_PAGE);Β Β Β Β results.push(chunk);
}
Β
If you try searching for results now, you should see that the β404β errors are eliminated and only valid results are rendered.
Β
Step 2: Automatically Hiding the Expanded Navbar (Mobile / Small Screens)
Β
The second issue has more to do with usability.Β If you try to navigate the site when viewing it on a mobile device (or any screen where the βcollapsedβ menu bar is shown), you will notice that the navbar does not close automatically.Β This causes a large fraction of the page to be obscured when navigating the site, unless the user manually clicks the βhamburgerβ icon.
Β
Instead, we would like any action taken in the navbar (clicking a link, searching for artwork) to automatically close the navbar.Β To achieve this, we must update our βMainNavβ component according to the following specification:
Β
- Set an βisExpandedβ value in the state, with a default value of false
Β
- Add an βexpandedβ property to the <Navbar> component with the value βisExpandedβ, ie:
expanded={isExpanded}
Β
- When the user searches for art (ie: when the form is submitted), set the βisExpandedβ value in the state to false
Β
- When the user clicks the <Navbar.Toggle> Component, toggle the βisExpandedβ value (ie: if itβs true, set it to false and vice versa) HINT: you can βtoggleβ a Boolean value in JS using someBoolean = !someBoolean.
Β
- Ensure that every <Nav.Link> element will set the βisExpandedβ value to false
Β
NOTE: To help space out the elements in the navbar when collapsed, you can place characters before and after the <Form></Form> element, ie <Form>β¦</Form>
Β
Β
Step 3: Adding βFavouritesβ Functionality (Part 1)
Β
For this assignment, we would like users to be able to add specific artwork to a βfavouritesβ list.Β This will involve:
Β
- Creating a store.js file in the root of your application (ie: βmy-app/store.jsβ)
- Using Jotai to create a βfavouritesAtomβ with the default value of [] (an empty array), defined within the store.js
file
Β
With our βatomβ in place, we must now update our βArtworkCardDetailβ component to use it, and provide a mechanism (button) to allow the user to specify a piece of artwork as one of their βfavouritesβ and add it to the list:
Β
- In the βArtworkCardDetailβ component, import both the βuseAtomβ hook, and the βfavouritesAtomβ We will also need the βuseStateβ hook
Β
With the imports in place, we can now focus on the additional logic required for this component, specifically:
Β
- Get a reference to the βfavouritesListβ from the βfavouritesAtomβ (HINT: this can be done using the βuseAtomβ hook)
Β
- Add a βshowAddedβ value to the state (this will control how the button (defined below) is displayed) with a default value of true if the βfavouritesListβ includes the objectID (passed in βpropsβ) and false if it does not
Β
- Add a βfavouritesClickedβ function (to be invoked when the button (defined below) is clicked), according to the following specification:
Β
- If the βshowAddedβ value in the state is true, then we must remove the objectID (passed in βpropsβ) from the βfavouritesListβ (effectively removing this piece of artwork from the favourites list). This can be done using the code:
Β
setFavouritesList(current => current.filter(fav => fav != objectID));
Β
(assuming that you have defined the function βsetFavouritesListβ when using the βuseAtomβ hook with the βfavouritesAtomβ)
Β
We must also set the βshowAddedβ value in the state to false (since the artwork is no longer in the favourites list)
Β
- If the βshowAddedβ value in the state is false, then we must add the objectID (passed in βpropsβ) to the βfavouritesListβ (effectively adding this piece of artwork to the favourites list). This can be done using the code:
Β
setFavouritesList(current => [β¦current, objectID]);
Β
(assuming that you have defined the function βsetFavouritesListβ when using the βuseAtomβ hook with the βfavouritesAtomβ)
Β
We must also set the βshowAddedβ value in the state to true (since the artwork is now in the favourites list)
Β
- Add a <Button> Element after the βDimensionsβ value in the βCardβ element according to the following:
Β
- Has a βvariantβ attribute set to βprimaryβ if the βshowAddedβ value in the state is true, or βoutlineprimaryβ if the βshowAddedβ value in the state is false
Β
- When the button is βClickedβ invoke the βfavouritesClickedβ function (defined above)
Β
- Shows the text β+ Favourite (added)β if the βshowAddedβ value in the state is set to true, or just β+ Favouriteβ if the βshowAdded value in the state is set to false
Β
NOTE: To ensure that SWR only starts the request for βDetailed Artwork Dataβ if it has an βobjectIDβ value, you can update the request to use βConditional Fetchingβ. For example,Β instead of executing the SWR function as:
Β
- const { data, error } = useSWR(`someUrl/${objectID}`);
You can instead use:
Β
- const { data, error } = useSWR(objectID ? `someUrl/${objectID}` : null);
Β
Step 4: Adding βFavouritesβ Functionality (Part 2)
Β
If you test the UI now, you will see that you can toggle the β+ Favouriteβ button for an individual piece of artwork to add / remove it as a βfavouriteβ.Β However, there doesnβt exist a page for us to view all of the favourites added.Β To remedy this, we must add a βFavouritesβ component in the βpagesβ directory using file name (favourites.js), such that:
Β
- It gets a reference to the βfavouritesListβ from the βfavouritesAtomβ (HINT: this can be done using the
βuseAtomβ hook)
Β
- It renders all of the items in the βfavouritsListβ in exactly the same way as the component in
β/pages/artwork/index.jsβ, ie: shows an βArtworkCardβ component for every element in the βfavouritesListβ using the same React-Bootstrap elements (ie: βRowβ, βColβ, etc).
Β
- Also like the component in β/pages/artwork/index.jsβ, if the βfavouritesListβ is empty, a message must be shown to the user β in this case, something along the lines of: βNothing Here Try adding some new artwork to the list.β
Β
- Important Note: Pagination is not required for this component
Β
Step 5: Adding βFavouritesβ Functionality (Part 3)
Β
To truly test whether or not our new β/favouritesβ page works correctly, we will need to provide a link to the page somewhere in our application.Β Since βfavouritesβ will eventually be stored for each individual user, it makes sense to introduce a βUser Nameβ dropdown menu to the βMainNavβ component, and place it there:
Β
- To get started, add a new <Nav>β¦</Nav> element below the </form> content
Β
- Next, use the code from: the documentation for Navbar to copy the βNavDropdownβ component and paste it into our new <Nav>β¦</Nav> element, changing the βtitleβ attribute from βDropdownβ to βUser Nameβ
Β
- Modify it so that it only has a single <NavDropdown.Item> component that links to β/favouritesβ with the text βFavouritesβ
Β
- Important Note: Do not forget to wrap your <NavDropdown.Item> component in a <Link>β¦</Link> tag (follow the same code that you used for your other <Nav.Link> components). If the page refreshes when navigating to β/favouritesβ, then we will not see any of our favourites, since theyβre currently only persisted in memory
Β
- Finally, ensure that when the user clicks on the <NavDropdown.Item> component, the βisExpandedβ value in the state to false (this is the same logic as your other <Nav.Link> components)
Β
If you test out your app now, you should see a βUser Nameβ drop down to the right of the search form in the MainNav component with the βFavouritesβ link.Β Try clicking β+ favouriteβ for some artwork, then navigating to β/favouritesβ via the navbar to test the add / remove functionality.
Β
Β
Step 6: Adding βHistoryβ Functionality (Part 1)
Β
The next major piece of functionality that we will add is a βSearch Historyβ mechanism to allow users to keep track of previous searches and re-run / delete them if they wish.Β Like the βfavouritesβ functionality, we are going to use Jotai to store the list of previous searches so that we can update / read it from anywhere within our app:
Β
- In the store.js file, add a βsearchHistoryAtomβ with a default value of [] (empty array)
Β
- Next, in the βsearch.jsβ file (ie: the βAdvancedSearchβ component), get a reference to the βsearchHistoryβ from the βsearchHistoryAtomβ (HINT: this can be done using the βuseAtomβ hook)
Β
- In the βsubmitFormβ function (ie: once the search form has been submitted), add the computed queryString value to the searchHistory. This can be done using the code:
Β
setSearchHistory(current => [β¦current, queryString]);
Β
(assuming that you have defined the function βsetSearchHistoryβ when using the βuseAtomβ hook with the βsearchHistoryAtomβ)
Β
- In the MainNav component, we must also get a reference to the βsearchHistoryβ from the βsearchHistoryAtomβ, since it is also possible to search from artwork with the βsearchβ form in the navbar.
Β
- In the βsubmitFormβ function (ie: once the search form in the navbar has been submitted), once again add the computed queryString value to the searchHistory (this should be βtitle=true&q=searchValueβ where searchValue is the value from the search field). This can once again be done using the code:
Β
setSearchHistory(current => [β¦current, queryString]);
Β
(assuming that you have defined the function βsetSearchHistoryβ when using the βuseAtomβ hook with the βsearchHistoryAtomβ)
Β
Step 7: Adding βHistoryβ Functionality (Part 2)
Β
With both search possibilities now adding to the βsearchHistoryβ array, we can concentrate on writing the page responsible for showing the values.Β This will be a βHistoryβ component in the βpagesβ directory using file name (history.js), with the following logic:
Β
- The component must get a reference to the βsearchHistoryβ from the βsearchHistoryAtomβ (HINT: this can be done using the βuseAtomβ hook).
Β
- In the body of the function, loop through the βsearchHistoryβ list to generate a list of βparsedβ search queries, ie:
Β
let parsedHistory = [];
Β
searchHistory.forEach(h => {
let params = new URLSearchParams(h);
let entries = params.entries();
parsedHistory.push(Object.fromEntries(entries));
});
Β
- The component must have a function called βhistoryClickedβ that takes two parameters: e (the event) and index (a number). This function must cause the user to navigate (using the βuseRouterβ hook) to the page
Β
/artwork?searchHistory[index]
Β
where index is the number passed to the function and searchHistory is the βsearchHistoryβ list from the βsearchHistoryAtomβ
Β
- The component must have a second function called βremoveHistoryClickedβ that also takes two parameters:
e (the event) and index (a number).Β The purpose of this function is to remove an element from the βsearchHistoryβ list.Β This can be done using the following logic:
Β
e.stopPropagation(); // stop the event from trigging other events setSearchHistory(current => {Β Β Β Β let x = [β¦current];Β Β Β Β x.splice(index, 1)Β Β Β Β return x;
});
Β
- The component must render the βparsedHistoryβ (created above) in the component according to the following specification:
Β
- If the βparsedHistoryβ array is empty, render a message, ie: βNothing Here Try searching for some artworkβ. To match the style used in the example, place this text in a <Card> Component
Β
- If the βparsedHistoryβ array is not empty, render each βparsedHistoryβ object from the array inside a
<ListGroup.Item> component.Β Each of these components will be contained within a single, parent <ListGroup> component (see the documentation for the <ListGroup> Component from reactbootstrap)
Β
The contents of each <ListGroup.Item> component should be a list of all properties present in the current βparsedHistoryβ object, for example:
Β
{Object.keys(historyItem).map(key => (<>{key}: <strong>{historyItem[key]}</strong> </>))}
Β
where historyItem is the current βparsedHistoryβ object
Β
- Additionally, each <ListGroup.Item> component must itself have a βclickβ event that invokes the
βhistoryClickedβ function with the event object and current index of the array (Recall: When using Array.map(), the callback function can take two parameters, the current element and the array index, ie: parsedHistory.map((historyItem, index) => ( β¦ ))
Β
- Finally, each <ListGroup.Item> component must also include a means to remove the current element from the βsearchHistoryβ list. This can be done with the following button that invokes βremoveHistoryClickedβ with the current index of the array:
Β
<Button className=βfloat-endβ variant=βdangerβ size=βsmβΒ onClick={e => removeHistoryClicked(e, index)}>×</Button>
Β
- To ensure that users are aware that they can click on each <ListGroup.Item> element to re-run the search, we should add some CSS to highlight the component, as well as change the cursor to βpointerβ. Here, we will use the CSS Module support from Next.js:
Β
- Create a βHistory.module.cssβ file within the βstylesβ folder (ie: my-app/styles), with the content:
Β
.historyListItem:hover{
background-color: rgb(244, 244, 244);Β Β Β Β cursor:pointer;
- }
Β
NOTE: The above background-color is just a suggestion β please feel free to use different colors / styles for your own application to match your design
Β
- Import the module in the history.js file using the code:
Β
import styles from β../styles/History.module.cssβ;
Β
- Finally, ensure that the <ListGroup.Item> elements have the className value: {styles.historyListItem}
Β
Step 8: Adding βHistoryβ Functionality (Part 3)
Β
Once again, to test whether or not our new β/historyβ page works correctly, we will need to provide a link to the page somewhere in our application.Β Since βhistoryβ will also eventually be stored for each individual user, it makes sense to place it within the βUser Nameβ dropdown menu in the βMainNavβ component.
Β
To accomplish this, you may use the exact same <Link> component that was used for βfavouritesβ, except it must link to β/historyβ and show the text βSearch Historyβ.
Β
If you test the app now, you should be able to make searches via either the nav bar or advanced search page and see the searches listed on the β/historyβ page.
Β
Step 9: Final βBugβ fix (Highlighting Navbar Items)
Β
You will have noticed that when navigating the site, the menu items do not necessarily highlight / remain highlighted on the current page that we are on.Β For example, when we first visit the site, βHomeβ is not highlighted and when a page is refreshed, none of the navbar text is highlighted.
Β
Fortunately, this is a quick fix for us to implement:
Β
- Ensure that each <Nav.Link> and <NavDropdown.Item> element in the MainNav component correctly sets the βactiveβ attribute, using the current path, ie:
Β
- <Nav.Link active={router.pathname === β/searchβ}>Advanced Search</Nav.Link
- <NavDropdown.Item active={router.pathname === β/historyβ>Search History</NavDropdown.Item> o
Β
Β
Β
Β
Β
Β
Β
Β
Β
Β
Β
Β
Β
Β
Β
Β
Β
Β
Assignment Submission:
- For this assignment, you will once again be required to build your assignment before submitting. This will involve running the command:
o npm run build
to create a production build in the β.nextβ folder to be included in your submission.Β Please Note: you will be required to fix any errors that are identified during the build process.
- Next, add the following declaration at the top of your index.js file
Β
/********************************************************************************* *Β WEB422 β Assignment 5
- I declare that this assignment is my own work in accordance with Seneca Academic Policy. *Β No part of this assignment has been copied manually or electronically from any other source *Β (including web sites) or distributed to other students.
*
- Name: ______________________ Student ID: __________________ Date: ____________________
*
*
********************************************************************************/
- Compress (.zip) the files in your Visual Studio working directory without node_modules (this is the folder that you opened in Visual Studio to create your client side code).
Β
Important Note:
- NO LATE SUBMISSIONS for assignments. Late assignment submissions will not be accepted and will receive a grade of zero (0).
- After the end (11:59PM) of the due date, the assignment submission link on My.Seneca will no longer be available.
- Submitted assignments must run locally, ie: start up errors causing the assignment/app to fail on startup will result in a grade of zero (0) for the assignment.





