# Final Project
# Multi Page Application
Joint Final Project
The final project for MAD9014 is a joint project with MAD9013. It is one project that you will submit for both courses. There may be different submission requirements for each course, so pay close attention to the submission requirements.
# Recipe Website Overview
You will be creating a responsive website that uses the dummyjson.com
API to gather recipe and blog post data. The responsive design using best practices for HTML and CSS will be primarily reviewed and graded as the MAD9013 portion of the final project. The MAD9014 portion of the project is primarily the functionality of the site - navigation, form submission, API fetch calls, search results management, data storage, etc. Basic good design practices and usability will also be part of the grade for MAD9014. Well-written valid HTML will also be part of the requirements for MAD9014 too.
The pages for this project, as defined in MAD9013, are Home page, Recipes search results, Recipe details page, and the Blog page.
The functionality required in the recipe website pages is as follows:
# Home page
This is the index.html
file. It will include the top of page navigation to the home, recipes search results, and blog pages plus a search form that also sends the user to the recipes search results page.
It will have a form that submits a request to join a mailing list. This form submits to formspree.io
. See the MAD9013 requirements for more details about this. Make sure that your inputs have both id
and name
attributes.
It will need to fetch SIX recipes from the dummyjson.com
API. Fetch the first six results from the API sorted by top rated.
The user submitting the search form at the top of the page should send them to the recipes.html
page.
# Recipes search results page
This is the recipes.html
file. It will check the value of the querystring to see if a search is being run.
The querystring will potentially have parameters for the following:
- one parameter with the keyword that will be used for searching for recipes.
- one parameter to determine which page the user is currently viewing
- one parameter to determine whether the name or rating is used for sorting the results.
- one parameter to determine if the results should be filtered by a meal type.
Running a new search from the top search form will clear out any previous results and start a new search.
When a search is done from ANY page in the website, the search form should submit and send the user to recipes.html. The request to the API will always get ALL the matches for the keyword. Getting all the results means setting a high number for the limit parameter, to be able to get more than 30 records.
Fetching ALL results with no keyword uses the URL https://dummyjson.com/recipes
.
Fetching results for a recipe search will use the URL https://dummyjson.com/recipes/search?q=
where the value of the q
parameter in the querystring will be the keyword for the search.
Those results need to be saved with either localStorage or the Cache API. The keyword for the search or some variation of that, like the search url, will be used as the key for saving the search results. If using localStorage the results array needs to be an Array converted to a JSON string. If using the Cache API, then the results array could be turned into a JSON String and saved in the cache as a JSON file.
Saving the results in localStorage
would work best if there were one entry for each keyword, for example cheese
. cheese
would be the key and the recipes array, converted into a JSON string, would be the value. If the user searches for 12 different keywords then there will be 12 different entries in localStorage with 12 different keys.
Saving the results with the cache API can be done in a couple ways. One, do a fetch that includes /recipes/search?q=
and then save a copy of that response object in the Cache, using the fetch URL as the key. Two, take a valid fetch response and extract the recipes array. Then turn the recipes array into a new JSON file that you wrap in a Response object and save in the cache using something like /recipes/search?q=
as the Request key. Remember to include the keyword as part of that key in the querystring. Three, you can use the cache.add()
method instead of cache.put()
. The add method will do the fetch call and save the results automatically in the cache. Then you can use cache.match()
to extract the response from the cache for use on your page.
Each time a fetch call is about to be made to the dummyjson.com API for recipes, your script needs to check for a matching keyword or url in localStorage or the Cache. If a match is found, then just retrieve that results array instead of making the fetch call.
We want to be reloading the recipes.html page every time:
- the keyword changes
- the pagination page changes
- the sort by value changes
- the filter value changes
Remember, HTML pages reload when the user clicks an anchor tag or submits a form. No JavaScript is required to reload the page. Just let the anchor or form do what they want to do.
The recipes page has two forms. One is the search form at the top, the second form is the filtering and sorting for the search results displayed on the page. The filtering and sorting form should use an <input type="hidden">
element to hold the search keyword. This means that all three values will be sent through the querystring when the filtering and sorting form is submitted. You actually need to have the two <form>
elements in your page.
Remember that all input elements that are going to be submitted must have a name
attribute or they will not be added to the querystring or POST data. The id
attribute in an <input>
element is just for JS and CSS.
When the page loads, check for the values in the querystring and, if they exist, add those to the input and select elements in both forms.
Each time the user selects a new value from either of the <select>
elements in the filtering and sorting form, you need to submit the filtering and sorting form by calling the built-in .submit()
method.
If the user picks a new sort by value, it is up to you whether or not you remove the page
value from the querystring. The number of recipes doesn't change when you sort so, it can be done with the old value or by returning to page zero.
If the user picks a new filter value, then you must remove or set the page
value back to the first page. The number of recipes COULD change when you change the filter. So, the old page
value might become invalid. Now, since the user is submitting a form, we do NOT need to alter the querystring directly, we can just double check that the page number from the querystring is valid when the page loads. If you want, you can use a second <input type="hidden"/>
field inside the sort/filter <form>
which can be used to hold the value for the page. This way, when the user triggers the change
event on a <select>
and your listener function runs, it can update the value of the page before submitting. Each time the recipes.html page loads, you can set the page value in the form, just like you are setting the input for the keyword in that same form.
To do the sorting and filtering of the array from the fetch/storage/cache. Use the the array filter()
method to filter the full array and then the toSorted()
method to sort the filtered array. Both methods create a new array.
//example of the three steps
let resultsArray = currentResultsObject[keyword]; //extract a single array from the global object
let filteredArray = resultsArray.filter((recipe) => {}); //filter the array from the global object
let sortedArray = filteredArray.toSorted((a, b) => {}); //sort the array that was filtered
//AFTER filtering and sorting is when you do that paginating of the results
2
3
4
5
On the Recipes page we also need the user to be able to step through the results a few at a time. This process is known as pagination. When we fetch the results we create an array with the results. We can set the number of records that we want to display on page at one time. Pick a number like 3 or 6 for this. Then by dividing the total number of recipes by that number we know how many pages of recipes there are and as the user steps through the pages, we can calculate which records to show.
We want our page to have Previous and Next links. When the user clicks the previous or next link, the browser will reload the current page. We need to dynamically build the href value for each link when the page loads. We need to determine what to put in the query string. Say we use page
as the querystring parameter. If it doesn't exist then we will use zero as its value. If the value is zero then we won't show the previous link but the value of the page parameter inside the next link will be one. If the value in the querystring of the page
parameter is one then clicking on the previous button will reload the recipes page with page=0
and clicking the next link will reload the page with page=2
in the querystring.
These querystring values for the links also need to include the values for keyword, filter, and sort by, if they exist in the current URL.
By links, we mean actual <a>
anchor tags. Not <div>
. Not <button>
. Not <li>
. They must be actual anchor tags so the page reloads.
Let's say that there are 13 recipes returned by a search and we have our per page value set as 3. We can calculate the values that we need with code like the following.
let url = new URL(location.href);
let params = url.searchParams;
let page = params.has('page') ? +params.get('page') : 0;
//also check that the page param has not exceeded the largest allowed page number
let perPage = 3;
//say our array of results is called `recipes`
let numPages = Math.ceil(recipes.length / perPage); //total number of pages
if (page === 0) {
//we are on the first page... don't show the previous link
//or show the previous link as disabled
} else {
//not on the first page need to know what number to add to previous link
let gotoPage = page - 1;
//build a new URL() object with a querystring page param
}
if (page === numPages - 1) {
//we are on the last page... don't show the next link
//or show it as disabled
} else {
//not on the last page, need to know what number to add to the next link
let gotoPage = page + 1;
//build a new URL() object with a querystring page param
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
If we want, we can also display direct links to specific pages within the result set. We would need to highlight the current page in some way. This would look something like:
Prev 1 [2] 3 4 Next
For this assignment, at a minimum we need to have Previous and Next links. As an end goal, try to have the links to the pages too. This is full pagination.
# Recipe details page
This is the recipe.html
file. It displays all the information about one selected recipe. The one recipe is determined by an id value being passed through the querystring.
The appearance of the page should meet the requirements for MAD9013, but you should be retrieving and displaying all the details for the one recipe from the dummyjson.com API. Do not take the details from localStorage or the Cache, make a new fetch call to the dummyjson.com API.
The user submitting the search form at the top of the page should send them to the recipes.html
page.
# Blog page
This is the blog.html
file.
In addition to the blog information, it will display THREE recipe results from the dummyjson.com API too.
Each call to get the THREE recipes will ask for the top 3 sorted by rating. It will be a new fetch each time.
The user submitting the search form at the top of the page should send them to the recipes.html
page.
# General Requirements
The things that must be done in the <head>
of every HTML file:
- A
Content-Security-Policy
meta tag that defines the sources of ALL types of files used in the site. It must NOT contain any values that start withunsafe-
. - The Google font preconnects and stylesheet link.
- A reset/normalize CSS stylesheet must be used.
- A CSS file that contains all your own styles for the website.
- A single
<script>
tag that loads themain.js
script using thetype="module"
attribute.
In the body of every HTML file:
- Any
<input>
or group of<input>
elements must be wrapped inside a<form>
element. - No script, style, or link tags should be inside the
<body>
. - The top of each page should have, in addition to the title and navigation, a search form that submits and sends the user to the recipes search results page.
- All the recipe cards should use the same styles and the user should be able to click anywhere on the card, to get to the recipe details page.
In every javascript file:
- There should be fewer than 4 global variables (not counting functions).
- There should be either an
IIFE
or a single initial function triggered by aDOMContentLoaded
event listener. - All event listener functions that are more than two lines of code, must be named functions, not anonymous functions.
- Functions should NOT be nested inside other functions.
JavaScript general requirements:
- The
main.js
script will be the primary script loaded into every HTML file via the script tag. - There should be a single JS file that makes all the fetch calls to the dummyjson.com API. This file will always be imported into
main.js
. - There should be a separate JS file for each of the four HTML pages. These scripts will only be imported if the
main.js
file was loaded by the matching html file. eg: On blog.html the main.js file is loaded. Main.js will load the dummyjson.com API script file plus the blog.js script that is for the blob page. The page specific scripts can be used to handle functionality that is specific to the single HTML file. - In
main.js
, use a switch case statement that looks at theid
attribute of the<body>
to determine which page loaded themain.js
file. - Any time a call to the dummyjson.com API is made, a loader/spinner graphic or animated message should be shown in the place where the search results will be displayed.
- In
main.js
you should be using theimport()
method to load the page specific scripts. - Since
main.js
is importing the other scripts, none of them should ever importmain.js
. We don't want an endless import loop. - Any time the results from a dummyjson.com API are being turned into HTML, your script should use either a
<template>
from the HTML file plus thetextContent
property andsetAttribute
method, or thecreateElement
method plus thetextContent
property andsetAttribute
method to create the content. There should NOT be any use ofinnerHTML
to build content. - Never write all of your code inside one function.
- NO DOM elements should be saved in variables that are created outside of a function that uses them. If a function needs a reference to a DOM element then it should declare a local variable and use getElementById, querySelector, or querySelectorAll to get the reference to the DOM element(s). Or the element(s) are passed to the function as a parameter from another function that already referenced the element.
The Git repo must:
- include a
.gitignore
file that lists, at least,.DS_Store
and.vscode/
. - Have a minimum of 5 commits. This project will take multiple days to complete. You should be making a commit at least once per coding session.
# Demo video
# Submission
Due Date **Week 15**
(See BS LMS for exact date)
Your github repo needs to be a private repo.
Invite prof3ssorSt3v3
as a collaborator to the repo.
Activate github pages for your private repo.
Open BS LMS and go to the Activities > Assignments
page.
Go to the Final Project and submit BOTH the repo url AND the github pages url into the text area.