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:

  1. run the start version
  2. buy a soda; this succeeds. Note the cart contents and the flashed message
  3. buy a beer. this also succeeds. It shouldn't.
  4. buy a glass of wine.
  5. try refreshing the page; note that because the form is POSTed, we get the warning
  6. "hide cart" button is not yet implemented
  7. "clear cart" button is not yet implemented
  8. 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:

  1. Work with the server.py example. Run it and test it.
  2. You'll need to implement a way to set the "I am 21" value
  3. You'll need to read the "I am 21" value (true/false) out of the session, including appropriate defaults
  4. 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.