Saturday, 25 January 2025

Web Tutorial: The NodeJS Wood Snake Fortune Teller (Part 1/2)

What'ssssssss up?! (see what I did there?)

It's about to be the Year of the Snake. Chinese New Year is upon us in a week. To that end, I'd like to continue my experimentations with NodeJS. Today, we're going to explore two separate things - how to manage assets in a NodeJS app, and how to call and retrieve data via a REST endpoint using the Fetch module.

To that end, we will be building a fortune teller app. Way to lean into the Chinese mysticism, eh?

Let's begin with some module installation. We will need Express and Handlebars for starters, just to render the layouts.
npm install --save express
npm install --save express-handlebars


Let's continue with a bit of file structure setup. We should have a directory called assets. Within, let's have three subfolders - img, css and js. No prizes for guesing what files they hold! Then create the views directory, and within it, have the layouts folder. Create these files in their respective folders.

views/layouts/main.handlebars
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>2025 Wood Snake Fortune Teller</title>
  </head>
  <body>
    {{{ body }}}  
  </body>
</html>


views/404.handlebars
<h1>404</h1>

<p>Not found!</p>


views/500.handlebars
<h1>500</h1>

<p>There was an error.</p>
<p><b>{{ errorMessage }}</b></p>


views/form.handlebars
<h1>Welcome to the Wood Snake Fortune Teller!</h1>

<form>

</form>


Those are all the views we're going to have. It's good to get them out of the way. Now let's work on the main app. Create app.js. This beginning code sets up Express as the middleware and Handlebars as the view engine. main.handlebars is specified as the layout template file.

app.js
var express = require("express");

var app = express();

var handlebars = require("express-handlebars").create({defaultLayout:"main"});
app.engine("handlebars", handlebars.engine);

app.set("view engine", "handlebars");
app.set("port", process.env.PORT || 3000);


We will then fill in routes for the default, the form handling POST route process, the 404 and 500 error handling views. At the end of it, we "start" the app by using the listen() method to listen on Port 3000.

app.js
app.set("view engine", "handlebars");
app.set("port", process.env.PORT || 3000);

app.get("/", (req, res)=> {

});

app.post("/fortune", (req, res)=> {

});

app.use((req, res, next)=> {
  res.status(404);
  res.render("404");
});

app.use((err, req, res, next)=> {
  res.status(500);
  res.render("500", { errorMessage: err.code });
});

app.listen(app.get("port"), ()=> {

});


Let's add some placeholders to this view.

views/form.handlebars
<h1>Welcome to the Wood Snake Fortune Teller!</h1>

<form>

</form>

{{ error }}
<br />
{{ fortune }}


For the default route, add this line to render the form view which we just modified.

app.js
app.get("/", (req, res)=> {
  res.render("form");
});


Add this object as a second argument, with the properties error and fortune, to mirror the placeholders.
app.js
app.get("/", (req, res)=> {
  res.render("form", { error: "", fortune: "" });
});


Then set the value of fortune as a string.

app.js
app.get("/", (req, res)=> {
  res.render("form", { error: "", fortune: "To get your fortune in the year 2025, please provide your birth date." });
});


Simple so far, right?

Let's link some styling and CSS, and images in. We first take this image and save it in the img folder of the assets directory.

assets/img/snake.jpg

Then we save styles.css in the css folder of the assets directory, and functions.js in the js folder of the assets directory. Keep the both balnk, for now. Next, we need to add this line to app.js. The static() method of express, with "assets" passed in as an argument, returns a reference to the directory specified. When that is passed into the use() method of app, that directory becomes the default location of every static file specified thereafter.

app.js
app.set("view engine", "handlebars");
app.set("port", process.env.PORT || 3000);

app.use(express.static("assets"));

app.get("/", (req, res)=> {
  res.render("form", { error: "", fortune: "To get your fortune in the year 2025, please provide your birth date." });
});


So if we add these lines, styles.css and functions.js would be links to the assets directory.

views/layouts/main.handlebars
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>2025 Wood Snake Fortune Teller</title>

    <link rel="stylesheet" type="text/css" href="css/styles.css">
    <script type="text/javascript" src="js/functions.js"></script>

  </head>
  <body>
    {{{ body }}}  
  </body>
</html>


Now, add these divs in the HTML. Your body placeholder should fit into the div styled using the body CSS class.

views/layouts/main.handlebars
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>2025 Wood Snake Fortune Teller</title>

    <link rel="stylesheet" type="text/css" href="css/styles.css">
    <script type="text/javascript" src="js/functions.js"></script>
  </head>
  <body>
    <div class="container">
      <div class="header">
        
      </div>

      <div class="body">

        {{{ body }}}  
      </div>    
    </div>

  </body>
</html>


Time to fill in the CSS file! I want this to be mobile-sized, so I've specified a modest width of 300 pixels. The margin property puts it in the middle of the screen if you view it on a larger screen. Font and alignment have been set, though those are more visual choices. I've given the div round corners, an set the overflow property to hidden so as to bring out the round corners, and even given the whole thing a shadow! The background color is a specific choice of pale grey - to match the background of snake.jpg as much as I can.

assets/css/styles.css
.container {
  width: 300px;
  font-size: 12px;
  font-family: Verdana;
  margin: 0 auto 20px auto;
  background-color: rgb(196, 195, 200);
  text-align: center;
  border-radius: 10px;
  overflow: hidden;
  box-shadow: 8px 8px 5px 2px rgba(0, 0, 0, 0.5);
}


...and we have this!

We'll style header and body next. header has a specified background image. Since assets is already the default root directory for all static content, we just need to go up one level from the css folder before going into img to find snake.jpg. body just has a smaller width and the padding property compensates for it.

assets/css/styles.css
.container {
  width: 300px;
  font-size: 12px;
  font-family: Verdana;
  margin: 0 auto 20px auto;
  background-color: rgb(196, 195, 200);
  text-align: center;
  border-radius: 10px;
  overflow: hidden;
  box-shadow: 8px 8px 5px 2px rgba(0, 0, 0, 0.5);
}

.header {
  width: 300px;
  height: 205px;
  background-repeat: no-repeat;
  background-size: cover;
  background-image: url("../img/snake.jpg");
}

.body {
  width: 280px;
  padding: 10px;
}


Here we go!


We'll work on the form next. Set it to call the fortune route when submitting and specify that the method is POST. Also, give it an id.

views/form.handlebars
<form action="/fortune" id="formFortune" method="POST">

</form>


Inside the form, we will need an input and label. The input will be of the type date and both name and id will be txtBd. The default value is the first day of 2025.

views/form.handlebars
<form action="/fortune" id="formFortune" method="POST">
  <label for="txtBd">Birth Date</label>
  <br />
  <input type="date" name="txtBd" id="txtBd" value="2025-01-01" />
</form>


Now in the CSS, let's style this. These are just to make things look good and don't really affect functionality.

assets/css/styles.css
.body {
  width: 280px;
  padding: 10px;
}

label {
  font-weight: bold;
}

input[type="date"] {
  font-size: 1.2em;
  width: 150px;
  height: 1.5em;
  padding: 0.5em;
}


Here's the input. The user is supposed to select a date from this.

Now let's do a bit of front-end scripting. In the input, set the onchange attribute to call bdChange().

views/form.handlebars
<form action="/fortune" id="formFortune" method="POST">
  <label for="txtBd">Birth Date</label>
  <br />
  <input type="date" name="txtBd" id="txtBd" value="2025-01-01" onChange="bdChange();" />
</form>


Add divs to enclose the error and fortune placeholders. The ids and classes are as follows.

views/form.handlebars
<form action="/fortune" id="formFortune" method="POST">
  <label for="txtBd">Birth Date</label>
  <br />
  <input type="date" name="txtBd" id="txtBd" value="2025-01-01" onChange="bdChange();" />
</form>

<div class="error" id="errorContainer">{{ error }}</div>
<br />
<div class="fortune" id="fortuneContainer">{{ fortune }}</div>


Now fill in the JavaScript file with the bdChange() function.

assets/js/functions.js
function bdChange() {

}


Declare these variables, referencing the divs, the form and the input.

assets/js/functions.js
function bdChange() {
  var errorContainer = document.getElementById("errorContainer");
  var txtBd = document.getElementById("txtBd");  
  var formFortune = document.getElementById("formFortune");
  var fortuneContainer = document.getElementById("fortuneContainer");

}


Then use JavaScript's Date class to declare d as today's date. And bd as the date from the input.

assets/js/functions.js
function bdChange() {
  var errorContainer = document.getElementById("errorContainer");
  var txtBd = document.getElementById("txtBd");  
  var formFortune = document.getElementById("formFortune");
  var fortuneContainer = document.getElementById("fortuneContainer");

  var d = new Date();
  var bd = new Date(txtBd.value);

}


Do a comparison using the getTime() method. If bd is later than today's date, display the error.

assets/js/functions.js
function bdChange() {
  var errorContainer = document.getElementById("errorContainer");
  var txtBd = document.getElementById("txtBd");  
  var formFortune = document.getElementById("formFortune");
  var fortuneContainer = document.getElementById("fortuneContainer");

  var d = new Date();
  var bd = new Date(txtBd.value);

  if (bd.getTime() > d.getTime()) {
    errorContainer.innerHTML = "Error! You can't possibly be born in the future.";
  }

}


If not, clear the errorContainer div and put this message into the fortuneContainer div.

assets/js/functions.js
function bdChange() {
  var errorContainer = document.getElementById("errorContainer");
  var txtBd = document.getElementById("txtBd");  
  var formFortune = document.getElementById("formFortune");
  var fortuneContainer = document.getElementById("fortuneContainer");

  var d = new Date();
  var bd = new Date(txtBd.value);

  if (bd.getTime() > d.getTime()) {
    errorContainer.innerHTML = "Error! You can't possibly be born in the future.";
  } else {
    errorContainer.innerHTML = "";
    fortuneContainer.innerHTML = "Please wait..."
  }

}


Just a bit more styling...

assets/css/styles.css
.body {
  width: 280px;
  padding: 10px;
}

.error {
  color: rgb(200, 0, 0);
  font-size: 0.8em;
  font-weight: bold;
}

.fortune {
  border-top: 3px double rgb(100, 100, 100);
  padding-top: 10px;
}


label {
  font-weight: bold;
}

input[type="date"] {
  font-size: 1.2em;
  width: 150px;
  height: 1.5em;
  padding: 0.5em;
}


Try this with an error.


And without.


Next

Submitting the form and getting your fortune!

No comments:

Post a Comment