Shopping Cart Exercise
We will do an exercise in which we will build a working "shopping cart" app
- I'll give you starting code in a directory that you can copy
- I'll demo the initial version and the working version.
- You will implement the working version.
The steps:
- run the start version
- buy a soda; this succeeds. Note the cart contents and the flashed message
- buy a beer. this also succeeds. It shouldn't.
- buy a glass of wine.
- try refreshing the page; note that because the form is POSTed, we get the warning
- "hide cart" button is not yet implemented
- "clear cart" button is not yet implemented
- the "I am 21" button is not yet implemented
Shopping Cart Code¶
Unlike the demo in the reading, for the shopping cart code I used POST for almost all form submissions, since they all change the state of the app, and I used the POST-REDIRECT-GET pattern, using flashing for messages. The one exception is ordering drinks.
This has the effect of breaking up the code over several routes, so I'll discuss these each in turn. Fortunately, the code for each is fairly short.
Main Route¶
This route renders the page that allows customers to order items. Everything else will redirect to this route.
app.get("/", (req, res) => {
// use these defaults if cart isn't in the session
let cart = req.session.cart || {'beer': 0, 'wine': 0, 'soda': 0};
let showCart = req.session.showCart || 'yes';
console.log('showCart', showCart);
console.log('cart', JSON.stringify(cart));
return res.render("cart.ejs", {cart, showCart});
});
Cart EJS¶
Let's look at that EJS file:
<!doctype html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name=author content="Olivia Giandrea">
<title>Cookie/Session Demo</title>
<link rel='stylesheet' href="/mystyle.css">
</head>
<body>
<%- include("partials/flashes.ejs") %>
<h1>Welcome to Our Place</h1>
<p>Where thirsty people drink.
<!-- Clip art from
https://www.uclipart.com/beer-mug-beer-glass-cup-clipart-5hoogu/
https://www.uclipart.com/wine-champagne-stemware-stemware-glass-clipart-1hnml3/
https://www.uclipart.com/soda-line-angle-clipart-r9zdy2/
-->
<% ['beer', 'wine', 'soda'].forEach(item => {%>
<div class="item">
<p><img src="<%= item+'.png'%>" width=200 alt="<%=item%>">
<form method=post action="/add">
<input type=hidden name="item" value="<%=item%>">
<input type=submit value="add to cart">
</form>
</div>
<% }) %>
<p><form method=post action="/clearCart">
<input type=submit value="Clear Cart">
</form></p>
<% if(showCart == 'no') { %>
<p><form method=post action="/showCart">
<input type=submit value="Show Cart">
</form></p>
<% } else { %>
<p>Your cart contains:</p>
<ul>
<% ['beer', 'wine', 'soda'].forEach( item => { %>
<li><%= cart[item]%> glasses of <%= item %></li>
<% }) %>
</ul>
<p><form method="post" action="/hideCart/">
<input type=submit value="Hide Cart">
</form></p>
<% } %>
<!-- another way to do buttons -->
<p><form method="post" action="/ofAge">
<button type="submit">I am 21</button>
</form></p>
</body>
</html>
- Most of it is ordinary HTML
- There are several tiny one-button forms (see below) to do the operations of the app.
- It needs a boolean input,
showCart
, that determines whether to show the cart or not. That value will live in the session and you'll have to manage its value. - It needs the cart dictionary, so it can show the cart (if desired)
The one-button forms could use a little explanation. Here's one:
<p><form method=post action="/clearCart">
<input type=submit value="Clear Cart">
</form></p>
This form submits no data to the back end. Merely submitting the form
is sufficient information. The backend route then acts
appropriately. For example, the following handles the /clearCart
button:
app.post('/showCart', (req, res) => {
req.session.showCart = 'yes';
req.flash('info', 'showing cart');
return res.redirect('/');
})
A slightly more interesting form is one that submits a single value, namely the item to add to the order:
<form method=post action="/add">
<input type=hidden name="item" value="<%=item%>">
<input type=submit value="add to cart">
</form>
That form sends just one piece of information, such as item=soda
:
the name
of the input is item
and the value is one of our drink
offerings, dynamically put on the page. This input is a hidden
input, so the user doesn't even see the input
. They only see the
"add to cart" button.
Ordering¶
const RESTRICTED = ['beer', 'wine'];
app.post('/add', (req, res) => {
let item = req.body.item;
let cart = req.session.cart || {'beer': 0, 'wine': 0, 'soda': 0};
// TODO: determine from the session whether to show the cart
// Also, whether the customer is of age
console.log('ordering', item);
// Restrict sales of beer & wine to those of age
if(true) {
console.log('ordering', item);
cart[item] += 1;
req.session.cart = cart; // store back into session
req.flash('info', `Thank you for buying a glass of ${item}`)
}
// Use POST-REDIRECT-GET to avoid double-ordering
return res.redirect('/');
});
Showing and Hiding the Cart
There are two related routes, one for when the user wants to hide the cart and one for when the user wants to show the cart. Remember that that's a sticky setting: The cart will be shown or hidden every time, until the user changes the mode.
app.post('/hideCart', (req, res) => {
// TODO: make the cart hidden
req.flash('info', 'hiding cart');
return res.redirect('/');
})
app.post('/showCart', (req, res) => {
req.session.showCart = 'yes';
req.flash('info', 'showing cart');
return res.redirect('/');
})
In the exercise, you'll implement the code to hide the cart.
Setting that the User is Of Age¶
The session will also store whether the user is of age (21+). We'll assume that someone checks the customer's ID card and verifies and makes the setting. Again, this is a sticky setting: the customer doesn't have to keep verifying their age with each purchase.
app.post('/ofAge', (req, res) => {
// TODO: make the cart hidden
req.flash('info', 'you are of age');
return res.redirect('/');
})
In the exercise, you'll have to implement this.
Clearing the Cart¶
Finally, the customer should be able to clear the cart (or maybe the server does it when all the drinks have been delivered). Here's the code:
app.post('/clearCart', (req, res) => {
// TODO: clear the cart and reset the ofAge
req.flash('info', 'cleared cart');
return res.redirect('/');
})
Again, you'll have to implement that.
Exercise¶
The shopping cart example has several non-working buttons. One is "I am 21"; if the user has pressed that button, they should be allowed to order beer and wine, otherwise, they have to stick to soft drinks. The app should remember that behavior over the course of the session.
Implement that behavior.
Also implement the buttons to hide/show the cart and to clear the cart.
Copying the App¶
cd ~/cs304/apps
cp -rd ~cs304node/apps/sessionCart/ sessionCart/
cd sessionCart/
Then:
- Work with the
server.py
example. Run it and test it. - You'll need to implement a way to set the "I am 21" value
- You'll need to read the "I am 21" value (true/false) out of the session, including appropriate defaults
- You'll need to modify the ordering part of the logic to respond to the "I am 21" value
Note that we don't (yet) have a way to reset the contents of the session. You can use the inspector to just delete the cookie. You can also implement the "clear cart" button, which should also reset the "I am 21" value.
My solution is available in solution.js
. We can use "diff" to compare
server.js
and solution.js
.