diff --git a/.eslintrc.json b/.eslintrc.json index 3fb6a540..fb7e68bd 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -53,6 +53,7 @@ "no-var": 2, "prefer-arrow-callback": 1, "prefer-const": 2, + "prefer-template": "error", "quotes": [1, "single"], "semi": [1, "never"], "space-before-blocks": ["error", { "functions": "always", "keywords": "always", "classes": "always" }], diff --git a/.gitignore b/.gitignore index 008f8e67..6dcdeeca 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ coverage/ docs/ sessions/ screenshots/* +out/ # sqlite databases # *.db diff --git a/01 Setup Win10.md b/01 Setup Win10.md new file mode 100644 index 00000000..5b478f49 --- /dev/null +++ b/01 Setup Win10.md @@ -0,0 +1,60 @@ + +# Setup for Windows 10 Users + +The tools used in this module are designed to be used in a Unix or Linux environment. Whilst this will create challenges for Windows 10 user there are some steps you will need to take. Please read and follow the steps below carefully: + +Install the Windows Subsystem for Linux (WSL). Open PowerShell as Administrator and run: + +``` shell +Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux +``` + +Restart your computer when prompted. + +Now install Visual Studio Code and once launched install the [remote-wsl](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-wsl) component by Microsoft. You should also install the [browser-preview](https://marketplace.visualstudio.com/items?itemName=auchenberg.vscode-browser-preview) component which will allow you to open html web pages. + +![remote-wsl](exercises/.images/remote-wsl.png) + +Now you need to install Ubuntu. This can be found by searching for `run linux on windows` in the Microsoft Store. You will be presented with the following screen. + +![Microsoft Store](exercises/.images/store.png) + +Choose the Ubuntu operating system (v18.04 LTS) and install. Once installed, click on the **Launch** button, this will open a console window and you will need to wait for a few minutes for the installation to complete. + +## Cloning the Forked Repository + +You will now need to fork the foundation lab by clicking on the Fork button. This will create a copy of the repository. See the standard setup instructions for more details. + +Now you can clone your forked repository by running the following command in the Ubuntu console, replacing xxx with the URL of your repository. + +```shell +git clone xxx +``` + +This will create a directory called `foundation` in the Ubuntu console. The final step is to launch VS Code from within the WSL environment by running the following command: + +```shell +code foundation +``` + +This will launch VS Code from within the WSL with the contents of the `foundation/` directory. If you open the integrated terminal (using the **Terminal** menu) you will see that you have the full ubuntu bash shell. You can now run all the remaining steps from this integrated terminal, just as you would for a standard Linux install. + +## Installing NodeJS + +These steps are identical to those used on a full Ubuntu installation. Start by installing the Node Version Manager (NVM) tool: + +```shell +curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.0/install.sh | bash +nvm --version +nvm install node +``` + +If the `nvm` command is not found you will need to reload the shell: + +```shell +source ~/.profile +``` + +Now try to install again. + +Now you can go directly to step 4 in the standard setup instructions. diff --git a/01 Setup.md b/01 Setup.md index a1a4e284..3bd171e2 100644 --- a/01 Setup.md +++ b/01 Setup.md @@ -3,7 +3,9 @@ In this worksheet you will learn how to configure your work environment using VS Code. You should start by installing **Visual Studio Code** (NOT VISUAL STUDIO!) from the [website](https://code.visualstudio.com), note that it might already be installed. If you are using a Coventry University computer and the software is not installed you can do this using AppsAnywhere. -If you are using Windows 10 you will also need to install [Git](https://git-scm.com/download/win), this may already be installed on a Coventry University computer. +If you are using Windows 10 you will also need to install [Git](https://git-scm.com/download/win), this may already be installed on a Coventry University computer. If you are on MacOS you may already have it installed as it comes with the XCode IDE but if you have recently updated your OS you may need to run the `xcode-select --install` command to update the **Xcode Command-line Tools** but if you don't want to install XCode you can install git using [HomeBrew](http://brew.sh/) using the `brew install git` command. If you are running Ubuntu you can install it using the `sudo apt install git` command. + +DO NOT INSTALL THE GIT GUI TOOLS! Visual Studio Code comes with an integrated **Terminal** that can be used instead of the standard _Command Prompt_ or _Terminal_. If you are using Linux or MacOS this will give you a Bash prompt however on Windows 10 it defaults to the _Command Prompt_ and will need to be changed to the _Bash Shell_. @@ -139,14 +141,43 @@ $ node index.js Now we have the server up and running so the final task is to view the web page using the web browser. Simply open the Chrome browser and navigate to localhost:8080 where you should see a message. If this works you are ready to start the lab exercises. -## 5 Pushing the Changes to GitHub +## 5 Using npm and its package.json + +**Have a look at your `package.json` file and understand its contents**. +- When you are starting a new _project_, **you have to create this yourself**: + +1. Use the `npm init` command to create the package.json file + - You can leave all the options default, just leave them empty and keep pressing enter + - Alternatively you can run `npm init --yes`, it will simply leave everything default +2. Install the dependencies for the first time using the `-S` _switch_ with `npm install` + - e.g.: `npm install -S koa koa-bodyparser` + - This would add something like this to the `package.json` file: + ```json + "dependencies": { + "koa": "^2.11.0", + "koa-bodyparser": "^4.2.1" + } + ``` + - When you next run `npm install -S `, the given package's _newest version_ will be simply added to the list, or in case of a new version for a _previously listed_ dependency, the _version number_ will be updated + - This `-S` is just a short version of the `--save` _switch_ + - **You can use the alternative `--save-dev` switch to install and save them as _development dependencies_** + - Something that you wouldn't want to deploy, but is necessary for development + - E.g. we will use `jest` for testing, and `eslint` for linting during the development phase, but these shouldn't be required to be installed during deployment + - You are done with adding the dependencies, now simply use `npm install` to install all of them at once! + - If you don't want to install development dependencies, use `npm install --production` +3. This `package.json` file is also useful to store _scripts_: + - Look at the following line: ` "start": "nodemon index.js"` + - This lets us simply type `npm start` in the terminal to start our application via _nodemon_ + - Nodemon is a great tool that restarts the application every time we save a file in the given folder, so we don't actually have to manually stop and restart it after every modification. + +## 6 Pushing the Changes to GitHub As you work through the lab activities two things are likely to happen: 1. You make changes to the code that you want to push to the forked copy of your repository. 2. You will need to pull any bug fixes from the original repository. -### 5.1 Configuring the Repository +### 6.1 Configuring the Repository Before you start interacting with the GitHub server you need to configure the local repository. Open the Bash Shell and run the following commands: @@ -157,7 +188,7 @@ git config user.email 'doej@coventry.ac.uk' remember to replace the values with you own name and your university email (but without the uni part). -### 5.2 Pushing Changes +### 6.2 Pushing Changes **NOTE: You only need to carry out this step when you have make changes to the code! This will normally need to take place each time you complete a "Test Your Understanding" section.** @@ -175,7 +206,7 @@ Now you should click on the **Sync** icon (shown below) to push the new commit u At this point you should be able to refresh your GitHub repository page to see the changes. -### 5.3 Pulling from Upstream +### 6.3 Pulling from Upstream As new materials and resources are added to the original repository (and bugs fixed) you will want to merge these into your forked repository. Before you can do this you will need to add a link to the upstream repository. Open a bash shell: diff --git a/02 HTTP.md b/02 HTTP.md index 338c6333..d1817184 100644 --- a/02 HTTP.md +++ b/02 HTTP.md @@ -14,7 +14,7 @@ HTTP uses a [request-response](https://en.wikipedia.org/wiki/Request–response) When this request is received by the server it analyses the information provided and sends a _response_ back to the client. This typically might include: -1. The [HTTP message body](https://en.wikipedia.org/wiki/HTTP_message_body), typically the data that was requested such as a html page or image. +1. The [HTTP message body](https://en.wikipedia.org/wiki/HTTP_message_body), typically the data that was requested such as an HTML page or image. 2. A [status code](https://searchengineland.com/the-ultimate-guide-to-http-status-codes-and-headers-for-seo-302786), a number representing the success or otherwise of the request. 3. Extra information such as the format the data is in sent as [response headers](https://developer.mozilla.org/en-US/docs/Glossary/Response_header). @@ -47,22 +47,26 @@ We will be working through some exercises that make use of all of these. Study the `index.js` script in the `exercises/02_http/01_url/` directory. -1. If you study lines 4-10 of `index.js` you will see a list of the modules we need to install. Refer the the previous lab if you get stuck at this point: +1. If you study lines 11-15 of `index.js` you will see a list of the modules we need to install. Refer the the previous lab if you get stuck at this point: 1. `koa` 2. `koa-router` 3. `koa-bodyparser` 4. `koa-static` 5. `js2xmlparser` + - **You can install all these dependencies by simply running `npm install` in the terminal.** + - This command installs all the documented dependencies from the `package.json` file, which is already preconfigured in this case. + - Refer to the `01_Setup.md` document's `Using npm and its package.json` chapter for more information + 2. The first line is the _shebang_, it tells the script what application is needed to run it. -3. Lines 4-10 import the module packages we need for our script to work. Koa is a modular framework, on its own it does very little but depends on plugins (middleware) for its functionality. -4. Lines 11-15 are where we configure the koa _middleware_. +3. Lines 11-15 import the module packages we need for our script to work. Koa is a modular framework, on its own it does very little but depends on plugins (middleware) for its functionality. +4. Lines 18-22 are where we configure the koa _middleware_. 5. We need two global variables in our script, one to store a list of items and the second to store the port number we will be using: 1. The `let` keyword defines a _mutable variable_ which can change its value. 2. The `const` keyword defines an _immutable variable_. Once a value is assigned it cannot be changed, these are sometime called _constants_. 6. The main part of the script defines the _routes_ and we will be covering these in more detail as we progress through the lab. -7. Right at the end (line 123) we start the server on the defined port and _export_ the _koa object_ `app`. By exporting it we can import the script into our automated test suite (briefly covered in the previous lab). +7. Right at the end (line 138) we start the server on the defined port and _export_ the _koa object_ `app`. By exporting it we can import the script into our automated test suite (briefly covered in the previous lab). -Now start the server: +Now start the server by typing `npm start` into the terminal while in the correct directory and hitting enter: 1. Access the root url, notice that the message **Hello World** is displayed in the browser. 2. Access the `/anon` url. This should result in the same message being displayed. @@ -70,13 +74,13 @@ Now start the server: ### 1.2 URL Parameters -In the HTTP protocol URLs represent resources on the server. Up to this point each URL has matched a different _route_ (defined by an `router.get()` function) but on a real server there might be a database containing many thousands of records that we want to be able to access. For this to work, each record would need a different, unique, URL! Since each record in an SQL database has a unique key (primary key), a simple solution would be to include the primary key in the URL thus creating a different URL for each record. +In the HTTP protocol URLs represent resources on the server. Up to this point each URL has matched a different _route_ (defined by a `router.get()` function), but on a real server there might be a database containing many thousands of records that we want to be able to access. For this to work, each record would need a different, unique, URL! Since each record in an SQL database has a unique key (primary key), a simple solution would be to include the primary key in the URL thus creating a different URL for each record. To do this we need to extract some data from the http object `ctx` which is the parameter passed to the _anonymous callback function_. The `ctx` object is a _JavaScript Object_ and so we need to fully understand how these work. Start the server and access the `/books/1` route. What is displayed in the web browser? -1. Since we have not yet covered how to connect to a database, on line 34 we have defined a JavaScript array which contains 3 indexes, these are numbered 0-2. +1. Since we have not yet covered how to connect to a database, on line 41 we have defined a JavaScript array which contains 3 indexes, these are numbered 0-2. 2. Directly underneath this we have defined a route `/books/:index` which contains two **segments**: 1. The first matches the text `/books`, this works like all the routes we have seen previously. 2. The second, beginning with the colon (`:`) represents a **URL Parameter**. This represents one or more characters. @@ -87,7 +91,7 @@ Start the server and access the `/books/1` route. What is displayed in the web b 4. This index value is used to look up the title of the book in the array, the book title is stored in a constant. 5. Finally the book title is sent to the web browser. -#### 1.3.1 Core Knowledge +#### 1.2.1 Core Knowledge JavaScript objects comprise one or more **property-value pairs**. There is an example below to illustrate this. @@ -112,9 +116,9 @@ Each value in an object can be extracted using one or two different syntaxes: ```javascript const firstName = name.first -const lastName = name['last'] -const prop = year -const dobYear = name.dob.[prop] +const lastName = name['last name'] +const prop = 'year' +const dobYear = name.dob[prop] ``` New properties can be added and removed from an object. @@ -125,31 +129,31 @@ delete name.first delete name['last name'] ``` -#### 1.3.2 Test Your Understanding +#### 1.2.2 Test Your Understanding In this section you will learn about a number of JavaScript functions. In each case you will be provided with a link to the documentation. 1. Add a fourth book to the array and make sure you can access this by passing its index in the URL. 2. We are not restricted to only having two URL segments: 1. Modify the route to add a second parameter called `index2`. - 2. Make sure this is triggered by restarting the server and accessing this by passing a second parameter. + 2. Make sure this is triggered by restarting the server (which should happen automatically if you used `npm start`, which uses `nodemon`) and accessing this by passing a second parameter. 3. Modify the script to print out both book titles. 3. Next you need to add some validation to make sure the script does not crash (note that if you are using GoormIDE you will not be able to test it): - 1. If the index in the URL exceeds to number of books in the array you get an error. Insert an [`if...else` statement](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/if...else) that sends a suitable message to the browser if the index number in the URL is too high. - 2. The index must be a number. Use the [`isNaN()` function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/isNaN) to check and send a suitable message to the browser if it is not. if it is, use the [`parseInt()` function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt) to convert it to a number. - 3. The number needs to be a whole number (integer). All JavaScript numbers are objects and have a number of useful functions. Use the [`Number.isInteger()` function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger) to check it is indeed an integer. Send a suitable message to the browser if it is not. + 1. If the index in the URL exceeds the number of books in the array, you get an error. Insert an [`if...else` statement](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/if...else) that sends a suitable message to the browser if the index number in the URL is too high. + 2. The index must be a number. Use the [`isNaN()` function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/isNaN) to check and send a suitable message to the browser if it is not. If it is, use the [`parseFloat()` function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseFloat) to convert it to a number. + 3. The number needs to be a whole number (integer). All JavaScript numbers are objects and have a number of useful functions. Use the [`Number.isInteger()` function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger) to check it is indeed an integer. Send a suitable message to the browser if it is not. You can convert it to an integer using the [`parseInt()` function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt). ### 1.3 URL Query Strings -Whilst URL parameters are used to define unique URLS to identify online resources, sometimes we want to be able to pass additional information and this is done through the use of **query strings**. +Whilst URL parameters are used to define unique URLs to identify online resources, sometimes we want to be able to pass additional information and this is done through the use of **query strings**. 1. Restart the server and access the `/hello/John%20Doe` route. - 1. Since spaces are not permitted in a URL we have to replace spaces with special codes. This is known as **URL Encoding** and there are specific [codes](https://www.degraeve.com/reference/urlencoding.php) to use. + 1. Since spaces are not permitted in a URL we have to replace spaces with special codes. This is known as **URL Encoding** and there are specific [codes](https://www.degraeve.com/reference/urlencoding.php) to use; however, your browser will probably do this automatically. 2. Notice that the message `hello John Doe` is displayed in the browser. 2. Now change the URL to `/hello/John%20Doe?format=upper`. 1. Notice that the same data has been displayed just the format has changed. -Open the `index.js` file. The route is between lines 48-52. +Open the `index.js` file. The route is between lines 50-57. 1. Notice that the query string(s) are not part of the route. 2. The query string comprises name-value pairs. @@ -175,12 +179,11 @@ Headers allow for additional information to be passed: Make sure the server is still running and use the [Chrome web browser](https://www.google.com/chrome/) to access the root URL `/`. -1. Open the [Chrome Developer Tools](https://developers.google.com/web/tools/chrome-devtools/) by clicking on the _customise and control Google Chrome tools_ button (the three dots to the extreme right of the address bar). -2. Choose **More tools** from the menu and then **Developer tools** from the submenu. -3. Locate the **Network** tab in the Chrome developer tools. -4. Reload the web page. (Press F5) -5. You should now see the resource sent in the HTTP response together with some data dealing with response times. -6. Click on the file name (as shown) to display the HTTP headers. +1. Open the [Chrome Developer Tools](https://developers.google.com/web/tools/chrome-devtools/) by pressing `ctrl+shift+i` or by clicking on the _Customise and control Chrome_ button (the three dots to the extreme right of the address bar), choosing **More tools** from the menu and then **Developer tools** from the submenu. +2. Locate the **Network** tab in the Chrome developer tools. +3. Reload the web page. (Press F5) +4. You should now see the resource sent in the HTTP response together with some data dealing with response times. +5. Click on the file name (as shown) to display the HTTP headers. ![how to show the headers in Chrome](exercises/.images/chrome_02.png) @@ -198,7 +201,7 @@ In the **Request Headers** note that: 2. Next it specifies the host the request is being sent to. 3. Then there are multiple request headers. These are always property-value pairs. Lets look at a few of the more interesting ones: 1. The `User-Agent` header sends information about the browser being used. - 2. The `Accepts` header tells the server what [MIME](https://goo.gl/W2SWtZ) data types our browser prefers. + 2. The `Accepts` header tells the server what [MIME data types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) our browser prefers. In addition to the **Response Body** (the information in the web browser window), the server has returned a **Status Code** (in this case it is `200` meaning `OK`) plus a set of **Response Headers** that provide additional information. @@ -206,20 +209,18 @@ In addition to the **Response Body** (the information in the web browser window) 2. The `Date` header is a string representing the date and time the response was sent. 3. The `ETag` header contains a hash of the contents and is used to see if the content has changed since the last request. -### 1.1 Test Your Knowledge +#### 1.4.1 Test Your Knowledge As part of the worksheets you will be given some exercises to carry out to make sure you fully understand the content covered. 1. Go to the [University website](http://www.coventry.ac.uk) and use the Chrome Developer Tools to examine the request and response headers. 1. Are there any headers that were missing from the example above? - 2. [Look up](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers) the meaning of all the headers. + 2. Look up the meaning of all the [headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers). ## 2 Using the POST Method Now we have a good understanding of the request-response process used by the HTTP protocol and have learned about the headers we will extend this by examining the use of the HTTP POST method. -Before completing this section in the lab sheet you will need to install the `koa-static` package. - 1. Start by accessing the list of names on the `/names` url Make sure you have the Chrome Developer Tools open on the Network tab.: 1. Notice the browser displays an error message. 2. If you look at the HTTP Status Code you will see it is `404 Not Found` which makes sense as there are no names in the list (yet) @@ -228,12 +229,12 @@ Before completing this section in the lab sheet you will need to install the `ko 1. Notice that the Request Method has changed to `POST`. 2. Notice that the Response Code is `201 Created` to indicate that your data has been uploaded and stored. 3. There is a section called **Form Data** which is the request body and contains the data you submitted using the form. Use the _View Source_ link to display the raw data. This is how your data is sent to the server. -4. Use the back button to return to the form and use this to add the names of everyone else on your table. +4. Use the back button to return to the form and use this to add the names of everyone else at your table. 5. Access the `/names` url which should now respond with the HTTP Status Code `200 OK` and display your names in an HTML table. ## 3 Meta-Data -The path in the Uniform Resource Locator (URL) represents a _resource_ on the server however sometimes you need to pass _metadata_ to tell the server what you want it to do with this resource. There are two ways to pass metadata in an HTTP request, query strings and request headers. They differ in that anything in the URL is part of a book mark whilst the request headers are not. You need to decide whether this is important before deciding which to use. +The path in the Uniform Resource Locator (URL) represents a _resource_ on the server; however, sometimes you need to pass _metadata_ to tell the server what you want it to do with this resource. There are two ways to pass metadata in an HTTP request, query strings and request headers. They differ in that anything in the URL is part of a bookmark whilst the request headers are not. You need to decide whether this is important before deciding which to use. ### 3.1 Query Strings @@ -259,7 +260,7 @@ To complete this section of the lab you need to install the [Modify Headers](htt ![editing the request headers](exercises/.images/chrome_05.png) -You can also modify the standard request headers. In this example we will change the `Accept` header which defines the prefered format of the response data. The data formats are defined as [MIME types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) The default settings look something like: +You can also modify the standard request headers. In this example we will change the `Accept` header which defines the prefered format of the response data. The data formats are defined as [MIME types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types). The default settings look something like: ``` text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 @@ -268,7 +269,7 @@ text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*; This specified that the browser would prefer data in HTML/XML format (0.9 weighting) but will accept certain image formats and everything else. We will be changing this to request our data in specific formats. 1. Still on the `/names` URL, modify the request headers by removing any search terms. -2. Add a custom `Accept` header which will modify the default and set its value to `text/html` (see the screenshot below). If you refresh the browser you will see that the data is still displayed in an HTML table (because this format has been provided by the server). +2. Add a custom `Accept` header which will modify the default and set its value to `text/html` (see the screenshot below). If you refresh the browser you will see that the data is still displayed in an HTML table (because this format was used previously as well). 3. Now try the following MIME types: 1. `application/json` 2. `application/xml` diff --git a/03 HTML5.md b/03 HTML5.md index 26d6eda5..15f2d85f 100644 --- a/03 HTML5.md +++ b/03 HTML5.md @@ -8,18 +8,17 @@ In this worksheet you will be learning about the markup language HTML which is c ``` └── 03_html    ├── 01_syntax -    ├── 02_hypermedia    ├── 02_lists    ├── 03_hypermedia    ├── 04_tables -    ├── 05_forms - ├── 06_semantic +    ├── 05_semantic + ├── 06_markdown    └── template.html ``` ## 1 Syntax -Lets take a look at some basic HTML syntax. Start by locating the `exercises/03_html/01_syntax/` directory. Navigate to this using the SSH Terminal, install the all the necessary `koa` packages and start the web server. If you navigate to the base route `/` you will see a screen full of text. +Lets take a look at some basic HTML syntax. Start by locating the `exercises/03_html/01_syntax/` directory. Navigate here using the terminal, install all the necessary `koa` packages with `npm install` and start the web server with `node index.js`. If you navigate to the base route `/`, you will see a screen full of text. ![the unformatted text](exercises/.images/chrome_07.png) @@ -36,7 +35,7 @@ As you work, save the html file and refresh the browser, you don't need to resta ## 2 Lists -Now you have mastered the basics of html its time to move on to how it can be used to render lists of different types. Start by locating the files in the `exercises/02_html/02_lists/` directory. Now install the necessary packages, start the `koa` server and view the root URL `/`. This should display a simple page with the title **1980's Home Computers**. +Now that you have mastered the basics of HTML, it's time to move on to how it can be used to render lists of different types. Start by locating the files in the `exercises/02_html/02_lists/` directory. Now install the necessary packages, start the `koa` server and view the root URL `/`. This should display a simple page with the title **1980s' Home Computers**. Now, add a list on your web page. Insert the following lines of code after the paragraph describing clever uses for home computers: @@ -59,7 +58,7 @@ See what happens, when you change the list type to ordered list (`ol`). Next, try a definition list. Add the following piece of code at the very end of the document, right before the `` end tag: ```html -

There are two types of memory:

+

There are two types of memory:

RAM
@@ -84,7 +83,7 @@ The definitions list contains two elements for each item: a `dt` for the term an ## 3 Hypermedia -Now you have mastered the basics of HTML markup we will look at one of the most important features, the ability to link resources together (known as **hypermedia**), this is one of the cornerstones of the World Wide Web (WWW). +Now that you have mastered the basics of HTML markup, we will look at one of the most important features, the ability to link resources together (known as **hypermedia**), this is one of the cornerstones of the World Wide Web (WWW). ### 3.1 Routes @@ -92,16 +91,16 @@ Every resource on the WWW (such as html documents and images) has a unique URL a 1. Some resources need to be directly accessible in the web browser (they are accessed either directly or loaded by the html web page). These need to have their own directly accessible URL. 1. One directory needs to be specified as being publicly available. In this example we have created a directory called `public/` - 2. Koa needs to make this directory public. This is achieved on line by importing a static module and using this to specify the public directory. - 3. assuming the server is running you can directly view the image using the URL `http://xxx:8080/paradox.jpeg`, remembering to substitute your server's URL. + 2. Koa needs to make this directory public. This is achieved on line 8 by importing a static module and using this to specify the public directory. + 3. Assuming the server is running you can directly view the image using the URL `http://localhost:8080/paradox.jpeg`, remembering to substitute your server's URL. 2. Some resources are _virtual_ and are generated by a script running on the server before being sent back to the browser client. 1. There are some simple examples in `index.js`. Both the `/` and `/paradoxes` routes trigger scripts that take the contents of html files and send this back to the browser. - 2. When you access the `/date` route it displays today's date. Look at the script that does this (you don't need to understand the code details). + 2. When you access the `/date` route it, displays today's date. Look at the script that does this (you don't need to understand the code details). #### 3.1.1 Test Your Knowledge 1. There is a file called `coventry_cathedral.jpg` in the `public/` directory. Display this in the browser. -2. Create a route called `/cathedral` which should return the contents of the `cathedral.html` file located in the `html/` directory. +2. Create a route called `/cathedral` which should return the contents of the `cathedral.html` file located in the `views/` directory. 3. Create a route called `/time` that displays the current time in hours and minutes. There is detailed documentation on the [Date](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) function on the Mozilla website. ### 3.2 Hyperlinks @@ -116,23 +115,21 @@ Add a link to the appropriate Wikipedia article to this web page, in a suitable To test the functionality in your browser you will need to make sure the server is running and navigate to the correct URL. -A link, defined by the `a` element contains the URL of the linked web page as its `href` attribute. The link above contains an absolute path to a document on the external server. The absolute path begins will full protocol identifier and domain name. +A link, defined by the `a` element contains the URL of the linked web page as its `href` attribute. The link above contains an absolute path to a document on the external server. The absolute path begins with full protocol identifier and domain name. Unlike block elements (such as `h1` or `p`), links are inline elements: they always need a block element as a container and they are considered as a running part of the content of the parent block element. -A relative link is intended for links within the same domain. The parsing of the file path starts from the default directory, which is the directory where the containing HTML document is located. For instance: +A relative link is intended for links within the same domain. The parsing of the file path starts from the default directory, which is the directory where the containing HTML document is located, or as it is in this case, the directory specified with `koa-static`, which is `public/`. For instance: - `"document.html"` points to a document of that name in the default directory. - `"info/document.html"` points to a document in `info` subdirectory of the default directory. - `"../document.html"` points to a document in the parent directory of the default directory. (Note that, for security reasons, web servers prohibit the traversal of server's directory structure outside the dedicated document root folder.) -Add a link or two to suitable places in **computers80.html**. - ### Test your understanding -1. Create a relative link between the list item on the home page and the page on the Commodore 64. -2. Add a link at the bottom of the Commodore 64 page called **Back** which returns the user to the home page. -3. Create a file called **spectrum.html** in the `html/` directory with the content shown below correctly marked up as HTML 5. +1. Create a relative link between the list item on the home page and the page on the `Commodore 64`. +2. Add a link at the bottom of the `Commodore 64` page called **Back** which returns the user to the home page. +3. Create a file called **spectrum.html** in the `views/` directory with the content shown below correctly marked up as HTML 5. 4. Create a new `/spectrum` route in the `index.js`. 5. Add new link to the list on the home page to send the user to this new page. 6. Make sure there is a link on the Spectrum page to return the user to the home page! @@ -157,7 +154,7 @@ Sinclair ZX Spectrum ### 3.3 Images -In HTML5, images are put inside a `figure` element. The `figure` element normally contains one image, possibly with a caption, but technically it can serve as a container for multiple images. because the images are loaded by the HTML web page (and not directly by the web server) they need to be in a publicly accessible directory. On our web server this has been defined as the `public/` directory. You should create subdirectories within this to organise your files. +In HTML5, images are put inside a `figure` element. The `figure` element normally contains one image, possibly with a caption, but technically it can serve as a container for multiple images. Because the images are loaded by the HTML web page (and not directly by the web server) they need to be in a publicly accessible directory. On our web server this has been defined as the `public/` directory. You should create subdirectories within this to organise your files. Locate the `03_hypermedia/public/` directory and create a new folder inside this called `images/`. Find an image of a 1980s computer and drag it into this new directory. You can use the image below if you wish. @@ -185,7 +182,7 @@ There are a couple of notable things about the `img` element: | Attribute | Purpose | |-----------|---------------------------------------------------------------------------------------------------------------------------------------------| -| src | For locating the image file. The file path is normally given as relative path, starting from the directory where the HTML file is located. | +| src | For locating the image file. The file path is normally given as relative path, starting from the default directory. | | alt | For displaying an alternative text if the image cannot be displayed, or if the user prefers using a screen reader for accessibility purposes. | #### 3.3.1 Test Your Understanding @@ -207,7 +204,7 @@ Now we will learn how to embed audio in a website. There are a number of resourc 2. In a similar way, video can also be embedded: 1. Most browsers require a video file encoded as `.mp4`. 2. Firefox needs an `.ogg` encoded version. -3. You can also embed a [YouTube](https://www.youtube.com) video in an ` +

Embedded youtube video via object

+ + + \ No newline at end of file diff --git a/solutions/03_html/03_hypermedia/views/spectrum.html b/solutions/03_html/03_hypermedia/views/spectrum.html new file mode 100644 index 00000000..332d19a8 --- /dev/null +++ b/solutions/03_html/03_hypermedia/views/spectrum.html @@ -0,0 +1,65 @@ + + + + + Sinclair ZX Spectrum + + +

Sinclair ZX Spectrum

+
+ A Sinclair ZX Spectrum computer +
Photo: Bill Bertram, CC via https://commons.wikimedia.org/
+
+ + + \ No newline at end of file diff --git a/solutions/03_html/04_tables/comparison.html b/solutions/03_html/04_tables/comparison.html new file mode 100644 index 00000000..b512bad0 --- /dev/null +++ b/solutions/03_html/04_tables/comparison.html @@ -0,0 +1,62 @@ + + + + + Comparison of three computers + + + + +

A comparison of technical data of three popular 80's home computers:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Technical comparison
PropertyCommodore 64ZX SpectrumCommodore VIC-20Spectravideo SV-328
CPUMOS Technology 6510Zilog Z80MOS Technology 6502Zilog Z80A
RAM64 kilobytes16 or 64 kilobytes5 kilobytes64 kilobytes
ROM20 kilobytes16 kilobytes16 kilobytes32 kilobytes
Main usagehome computing
Killer gameJet Set WillyJelly MonstersArmoured Assault
+ +
+ + + + \ No newline at end of file diff --git a/solutions/03_html/05_semantic/cafe.html b/solutions/03_html/05_semantic/cafe.html new file mode 100644 index 00000000..05a525ab --- /dev/null +++ b/solutions/03_html/05_semantic/cafe.html @@ -0,0 +1,43 @@ + + + + + + The Java Bakery Cafe + + + + + Logo of Java Bakery Cafe + +

Welcome to the Java Bakery Cafe

+ +

+ The Java Cafe is passionate in providing quality home-baked goods, deliciously innovative + classic dishes and excellent beverages that evoke the atmosphere of the 1990s. +

+ +

+ Our cafés in Coventry and Cupertino aim to provide a wonderful place to relax, socialise, work, + hold meetings and above all enjoy exceptional-quality food and drinks at an unbeatable price. +

+ +

Payment in GBP only.

+ +

Price range: $$

+ +

Hours: Mo-Fri 10am-7pm Sa 10am-22pm Su 10am-21pm

+ +

Payment accepted: Cash, Credit Card

+ +

Priory Street, Coventry, CV1 5FB

+ +

Tourist Board 4 star rating

+ +

Serves Lavazza coffee

+ +

javacafe@example.com

+ +

Tel: 024 7655 5555

+ + diff --git a/solutions/03_html/06_markdown/computers.md b/solutions/03_html/06_markdown/computers.md new file mode 100644 index 00000000..60eeef7c --- /dev/null +++ b/solutions/03_html/06_markdown/computers.md @@ -0,0 +1,41 @@ + +# 80s Computers + +Here is a list of the best computers from the 1980s. + +1. Sinclair ZX80 +1. Commodore 64 +1. ZX Spectrum +1. Compaq Portable + +| Computer | Year | +| --------------- | ----: | +| Sinclair ZX80 | 1980 | +| Commodore64 | 1982 | +| ZX Spectrum | 1982 | +| Compaq Portable | 1982 | + +## Hardware +- Sinclair + - Z80 @ 3.25 MHz + - 1 KB (16 KB max.) +- Commodore 64 + - MOS Technology 6510/8500 + - MOS Technology VIC-II + - MOS Technology 6581/8580 SID + - Two 6526 Complex Interface Adapters + - 64 KB RAM + - 20 KB ROM +- ZX Spectrum + - Z80A @ 3.5 MHz + - 16 KB / 48 KB / 128 KB +- Compaq Portable + - Intel 8088, 4.77MHz + - 128 kilobytes RAM (expandable to 640 KiB) + +## Software +- Microsoft Word +- Pac-man +- Lotus 1-2-3 +- HyperCard +- Adobe Photoshop diff --git a/solutions/04_css/01_formatting/comparison.css b/solutions/04_css/01_formatting/comparison.css new file mode 100644 index 00000000..995aa2d0 --- /dev/null +++ b/solutions/04_css/01_formatting/comparison.css @@ -0,0 +1,47 @@ +body { + font-family: Arial, Helvetica, sans-serif; +} + +p { + text-align: center; +} + +table { + border: 1px solid black; + border-collapse: collapse; + margin-left: auto; + margin-right: auto; +} + +caption { + text-transform: uppercase; + border: 0.5px solid black; + font-weight: bold; + letter-spacing: 0.2em; +} + +thead { + background-color: red; + color: white; +} + +th { + text-transform: uppercase; + font-weight: bold; + padding-left: 0.5em; + padding-right: 0.5em; +} +th:first-child { + border-right: 1px solid black; + text-align: left; +} + +td { + padding-left: 0.5em; + padding-right: 0.5em; +} + +em { + font-weight: bold; + font-style: normal; +} \ No newline at end of file diff --git a/exercises/04_css/01_formatting/html/comparison.html b/solutions/04_css/01_formatting/comparison.html similarity index 76% rename from exercises/04_css/01_formatting/html/comparison.html rename to solutions/04_css/01_formatting/comparison.html index 42b0dda9..67f01085 100644 --- a/exercises/04_css/01_formatting/html/comparison.html +++ b/solutions/04_css/01_formatting/comparison.html @@ -1,13 +1,13 @@ - + Comparison of three computers - + + - -

A comparison of technical data of three popular 80's home computers:

+

A comparison of technical data of three popular 80's home computers:

@@ -21,28 +21,24 @@ - + - + - +
Technical comparison
CPUCPU MOS Technology 6510 Zilog Z80 MOS Technology 6502
RAMRAM 64 kilobytes 16 or 64 kilobytes 5 kilobytes
ROMROM 20 kilobytes 16 kilobytes 16 kilobytes
- -
- - \ No newline at end of file diff --git a/exercises/04_css/01_formatting/html/csstest.html b/solutions/04_css/01_formatting/csstest.html similarity index 74% rename from exercises/04_css/01_formatting/html/csstest.html rename to solutions/04_css/01_formatting/csstest.html index 0791d1a4..73a04ea9 100644 --- a/exercises/04_css/01_formatting/html/csstest.html +++ b/solutions/04_css/01_formatting/csstest.html @@ -1,18 +1,21 @@ - - + + Vintage Home Computers - + + + +

This is different from all the other paragraphs

1980's Home Computers

In early 1980's, home computers became mainstream. For the first time, computers could be purchased by an average family household.

- A computer with a monitor -
Photo: Piotr Siedlecki, public domain via http://www.publicdomainpictures.net.
+ A computer with a monitor +
Photo: Piotr Siedlecki, public domain via http://www.publicdomainpictures.net.

Novel uses

The main usage for a home computer was, of course, games. As cool games may not be a valid reason for purchasing an expensive gadget, @@ -46,11 +49,14 @@

Key concepts

Read-only memory. This was normally smaller in size than RAM and roughly corresponded to the hardware implementation of the operating system.
- - -
-

Disclaimer: All opinions in this page reflect the views of their author(s), not the organization.

- +

Basic programming

+

Basic was a common language for programming 80s' home computers +


+        10 PRINT "Hello, World"
+        20 INPUT "Continue (y/n)?", R$
+        30 IF R$="y" THEN GOTO 10
+        
+
+

Disclaimer: All opinions in this page reflect the views of their author(s), not the organization.

- - + \ No newline at end of file diff --git a/solutions/04_css/01_formatting/selectors.css b/solutions/04_css/01_formatting/selectors.css new file mode 100644 index 00000000..f34d61f7 --- /dev/null +++ b/solutions/04_css/01_formatting/selectors.css @@ -0,0 +1,23 @@ +body { + font-family: Arial, Helvetica, sans-serif; +} + +p { + color: blue; +} + +h1.alert { + font-size: 3em; +} + +.alert { + color: red; +} + +p#unique { + font-size: 0.8em; +} + +.underlined { + text-decoration: underline; +} \ No newline at end of file diff --git a/exercises/04_css/01_formatting/html/selectors.html b/solutions/04_css/01_formatting/selectors.html similarity index 53% rename from exercises/04_css/01_formatting/html/selectors.html rename to solutions/04_css/01_formatting/selectors.html index 771ea37f..a789d4bc 100644 --- a/exercises/04_css/01_formatting/html/selectors.html +++ b/solutions/04_css/01_formatting/selectors.html @@ -1,22 +1,22 @@ - + + Selectors - - + +

Classes and identifiers.

-

This is a regular paragraph.

+

This is a regular paragraph.

This is a regular paragraph, too.

-

This paragraph belongs to a special group of paragraphs. It is a member of 'alert' class.

+

This paragraph belongs to a special group of paragraphs. It is a member of 'alert' class.

This paragraph should be uniquely formatted. It is assigned an identifier.

-

This paragraph is in 'alert' class, too.

- +

This paragraph is in 'alert' class, too.

\ No newline at end of file diff --git a/solutions/04_css/01_formatting/test.css b/solutions/04_css/01_formatting/test.css new file mode 100644 index 00000000..f877a654 --- /dev/null +++ b/solutions/04_css/01_formatting/test.css @@ -0,0 +1,60 @@ +body { + background-color: lightcyan; + color: rgba(65, 105, 225, 0.7); +} + +h1 { + color: orange; +} + +p { + text-indent: 10pt; +} + +ol, ul { + border-style: solid; + border-width: 1pt; + text-indent: 10pt; +} + +li { + color: darkgray; +} + +figcaption { + font-size: smaller; + font-weight: bold; +} + +p.copyright-notice { + font-size: 0.8em; +} + +#special { + font-size: 0.8em; + color: yellowgreen; + font-style: italic; + border-left: 2px solid black; + margin-left: 40px; + padding-left: 10px; +} + +pre { + white-space: pre-wrap; +} + +p::first-letter { + font-size:8em; +} + +p::first-line { + font-weight: bold; +} + +p::before { + content: "Note! "; +} + +p::after { + content: url(/images/tickmark.png); +} \ No newline at end of file diff --git a/solutions/04_css/02_layout/berries.css b/solutions/04_css/02_layout/berries.css new file mode 100644 index 00000000..e8abcd43 --- /dev/null +++ b/solutions/04_css/02_layout/berries.css @@ -0,0 +1,56 @@ +body { + font-family: Arial, Helvetica, sans-serif; + text-align: center; +} + +img { + border: 2px solid lightblue; +} + +figcaption { + font-weight: bold; + margin-top: 1em; + margin-bottom: 3em; +} + +h1, h2, h3 { + padding-top: 0em; + padding-bottom: 0em; + margin: 0em; + font-weight: normal; +} + +h1 { + font-size: 6em; +} + +h2 { + padding-top: 8pt; + font-size: 2em; +} + +h3 { + padding-bottom: 8pt; + font-style: italic; + font-size: 1em; +} + +div { + width: 15em; + margin: auto; +} + +div div { + border-bottom: thin solid black; +} + +div div:first-child { + border-top: thin solid black; + background-color: lightblue; +} +div:last-child { + background-color: pink; +} +div:nth-child(2) { + background-color: beige; +} \ No newline at end of file diff --git a/solutions/04_css/02_layout/berries.html b/solutions/04_css/02_layout/berries.html new file mode 100644 index 00000000..b49c6289 --- /dev/null +++ b/solutions/04_css/02_layout/berries.html @@ -0,0 +1,30 @@ + + + + + Berries + + + + +

Berries

+
+ berries +
Image: Scott Bauer, USDA ARS, public domain
+
+
+
+

Blueberry

+

Vaccinium corymbosum

+
+
+

Cloudberry

+

Rubus chamaemorus

+
+
+

Lingonberry

+

Vaccinium vitis-idaea

+
+
+ + \ No newline at end of file diff --git a/solutions/04_css/02_layout/boxstyles.css b/solutions/04_css/02_layout/boxstyles.css new file mode 100644 index 00000000..92198cb0 --- /dev/null +++ b/solutions/04_css/02_layout/boxstyles.css @@ -0,0 +1,25 @@ +p.one { + border: 1px solid black; + padding: 3em; + margin: 1em; + margin-bottom: 0em; +} + +p.two { + border: 1px solid black; + margin: 5em; + margin-top: 0em; +} + +p.three { + border-left: 3px solid black; + padding: 1em; + padding-top: 0em; + margin-top: 2em; + margin-bottom: 8em; +} + +p.four { + border: 5px dashed red; + border-radius: 1em; +} \ No newline at end of file diff --git a/solutions/04_css/02_layout/columnstyles.css b/solutions/04_css/02_layout/columnstyles.css new file mode 100644 index 00000000..eb5b85d9 --- /dev/null +++ b/solutions/04_css/02_layout/columnstyles.css @@ -0,0 +1,13 @@ +body { + column-count: 3; + /*alternatively: column-width: 15em; */ + column-gap: 2em; + column-rule-style: solid; + column-rule-width: 1px; + column-rule-color: red; + /*alternatively: column-rule: 1px solid red; */ +} + +h1 { + column-span: all; +} \ No newline at end of file diff --git a/solutions/04_css/02_layout/floating.html b/solutions/04_css/02_layout/floating.html new file mode 100644 index 00000000..1f3d8aa8 --- /dev/null +++ b/solutions/04_css/02_layout/floating.html @@ -0,0 +1,64 @@ + + + + + Illustration of a floating element + + + + +

Onions

+ +
+

Did you know?

+ +
+ +
+

Onions in cooking

+ +
+ Onions +
Image: Petr Kratochvil, public domain via www.publicdomainpictures.net.
+
+ +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce in malesuada libero. Etiam consectetur enim enim, id porta neque facilisis non. Proin consequat a sapien eu consectetur. Quisque bibendum lorem tortor, nec facilisis risus venenatis vel. Nullam et nunc varius, elementum mi vitae, mattis lacus. Mauris et lacinia magna. Nam faucibus purus ac felis vulputate suscipit. Sed vitae est egestas, euismod neque eu, molestie leo. +

+ +

+ Fusce viverra enim ante, vel dapibus metus maximus et. Proin ac metus justo. Donec nec scelerisque ante. Vestibulum a lectus nisl. Curabitur dictum a purus non sagittis. Cras semper ipsum ut eros lobortis vestibulum. Aenean eu urna nulla. Quisque mattis ex nec odio sollicitudin, a pulvinar nibh faucibus. Nullam quis rutrum purus, nec condimentum est. Aenean sagittis feugiat ante, sit amet dignissim elit vulputate ac. Aenean nec erat blandit, posuere orci malesuada, aliquam sem. Ut rutrum neque nec sapien consectetur dignissim. +

+ +

+ Nam odio lectus, accumsan vitae dignissim vitae, condimentum eu ipsum. Ut tristique vel leo at condimentum. Quisque tincidunt et tellus ut feugiat. Proin consectetur nulla eu fringilla cursus. Vivamus mi tellus, consectetur vitae commodo vitae, placerat sit amet nisl. Vestibulum et porttitor est. Cras varius quam a enim commodo consequat. Ut egestas dapibus est, a varius enim tincidunt ac. Quisque scelerisque ante quis porta mollis. Mauris nec maximus orci. Nunc ac justo ut nisl euismod vehicula in lacinia ante. Nulla sollicitudin nisi posuere, volutpat lorem nec, sagittis purus. Duis suscipit vulputate purus ac vulputate. Pellentesque suscipit ornare mi, et finibus tortor rutrum eget. +

+ +

+ In at ipsum id ligula vehicula porttitor. Suspendisse in risus vitae nunc lacinia lacinia. Cras finibus diam odio, vel posuere justo vestibulum tempor. Integer sed pharetra lacus. Integer elit mauris, vehicula ut tortor ut, vehicula iaculis odio. Integer tincidunt tincidunt quam, a laoreet sem mollis porta. Sed consequat sagittis urna quis aliquet. Proin vitae orci ultrices nisi rutrum blandit. +

+ +

+ Mauris dapibus auctor diam sit amet euismod. Sed in nibh augue. Mauris porttitor consequat egestas. Mauris consectetur commodo blandit. Suspendisse faucibus tristique nulla, in maximus elit feugiat nec. Mauris sagittis orci eu elit mattis lacinia. Donec molestie risus sed urna maximus eleifend. Nunc accumsan facilisis eros et vestibulum. +

+ +

+ Sed dui turpis, pellentesque egestas eleifend eget, accumsan id dolor. Vestibulum suscipit in urna at semper. Curabitur luctus eros et luctus rhoncus. Sed ac ipsum dolor. Integer ex sapien, egestas quis sapien vel, fringilla aliquam libero. In sapien eros, feugiat quis auctor at, facilisis sit amet magna. Fusce leo ex, viverra quis gravida sit amet, faucibus a quam. Cras tempus lorem vitae metus cursus malesuada. +

+ +

+ Nullam egestas id augue nec eleifend. Curabitur facilisis malesuada orci at venenatis. Duis at porttitor felis, ac varius elit. Sed pellentesque ligula eros, eu sollicitudin dolor placerat quis. Vestibulum suscipit, leo sit amet tristique hendrerit, ante velit congue lorem, sit amet volutpat ante ligula eget ex. Sed at ultrices turpis. Nulla sit amet egestas sapien. Donec sed lacinia mauris. Morbi suscipit erat nec orci vehicula iaculis. Fusce rhoncus at nunc vitae pretium. Nam consectetur luctus est, sit amet rhoncus ex consectetur ac. Nam in pretium nisl. Vivamus ultricies interdum elit a mattis. Phasellus lectus tellus, molestie in ultricies a, volutpat in ipsum. Aenean maximus viverra congue. +

+
+ + + \ No newline at end of file diff --git a/solutions/04_css/02_layout/floatstyles.css b/solutions/04_css/02_layout/floatstyles.css new file mode 100644 index 00000000..26efd179 --- /dev/null +++ b/solutions/04_css/02_layout/floatstyles.css @@ -0,0 +1,22 @@ +body { + font-family: Arial, Helvetica, sans-serif; + margin-left: 200px; +} + +figure#floating { + float: right; +} + +section#factbox { + float: left; + border: 1px solid black; + margin: 2em; + padding: 1em; +} + +nav { + position: absolute; + top: 120px; + left: 40px; + width: 100px; +} \ No newline at end of file diff --git a/solutions/04_css/02_layout/menu.html b/solutions/04_css/02_layout/menu.html new file mode 100644 index 00000000..32cf35e2 --- /dev/null +++ b/solutions/04_css/02_layout/menu.html @@ -0,0 +1,54 @@ + + + + + HTML Menus + + + + +

HTML/CSS Menu demonstration

+ + + \ No newline at end of file diff --git a/solutions/04_css/02_layout/menustyles.css b/solutions/04_css/02_layout/menustyles.css new file mode 100644 index 00000000..e97b659f --- /dev/null +++ b/solutions/04_css/02_layout/menustyles.css @@ -0,0 +1,53 @@ +body { + font-family: Arial, Helvetica, sans-serif; +} + +nav ul { + list-style: none; + font-size: 16pt; + padding: 0px; +} + +nav > ul > li{ + background-color: green; + font-weight: bold; + padding: 3pt; + float: left; +} + +nav ul li { + width: 100pt; + color: white; +} + +nav ul li ul { + list-style-position: inside; + visibility: hidden; + position: absolute; + border: 2pt solid black; + background-color: white; + font-weight: normal; + color: black; + margin-left: -2pt; +} + +nav > ul > li > ul { + margin-top: 3pt; + border-top: none; +} + +nav ul li:hover > ul { + visibility:visible; +} + +ul li ul li ul { + margin-top: -20.5pt; + margin-left: 100.5pt; +} +nav a { + color: white; + text-decoration: none; +} +nav ul li ul a { + color: black; +} \ No newline at end of file diff --git a/solutions/05_javascript/contact.js b/solutions/05_javascript/contact.js new file mode 100644 index 00000000..3d358404 --- /dev/null +++ b/solutions/05_javascript/contact.js @@ -0,0 +1,81 @@ +#!/usr/bin/env node + +'use strict' + +/* This script demonstrates how JavaScript can be used to handle exceptions. */ + +const readline = require('readline-sync') + +/* any code that could throw an exception needs to be wrapped in a try block. If any line of code in the try block throws an exception the program immediately jumps to the catch block, passing the Error object thrown. Regardless of whether an error was thrown or not, the code in the finally block is run. */ +try { + const email = String(readline.question('enter email address: ')) + console.log('email is a '+typeof email+' and contains '+email) + validateEmail(email) + const score = Number(readline.question('assign score 1-10: ')) + console.log('score is a '+typeof score+' and contains '+score) + validateScore(score) + const comment = String(readline.question('enter your comment : ')) + validateComment(comment) + console.log(`Thank you ${email}. You gave us a rating of ${score}/10 with the comment "${comment}"`) +} catch(err) { + console.log(`${err.name} thrown`) + console.log(`The error message is: ${err.message}`) + console.log(err.stack) +} finally { + console.log('the script has finished') +} + +/** + * Checks to see if the supplied parameter is formatted as a valid email + * address. + * @param {string} email - The email address to be checked. + * @returns {bool} the validity of the email address string + * @throws {TypeError} if the parameter is not a valid email address. + */ +function validateEmail(email) { + console.log(email) + const atIndex = email.lastIndexOf('@') + const dotIndex = email.lastIndexOf('.') + if ( email.length < 5 + || atIndex < 1 + || email.indexOf(' ') !== -1 + || email.indexOf('..') !== -1 + || dotIndex < atIndex + || dotIndex === email.length - 1 ) { + throw TypeError + } + return true +} + +/** + * Checks to see if the supplied parameter is a valid integer in the range 1-10. + * @param {string} score - The user-specified score. + * @returns {bool} whether the parameter is a valid integer in range + * @throws {TypeError} if the parameter is not a valid integer. + * @throws {RangeError} if the parameter is not in the range 1-10. + */ +function validateScore(score) { + const minScore = 0 + const maxScore = 10 + if (Number.isNaN(score) || score % 1 !== minScore) { + throw new TypeError('parameter is not a valid integer') + } + if (score < 1 || score > maxScore) { + throw new RangeError('parameter should be in the range 1-10') + } + return true +} + +/** + * Checks to see if the supplied comment is 'valid'. + * @param {string} comment - The user-specified score. + * @returns {bool} whether the comment is 'valid' + * @throws {RangeError} if the comment is not long enough. + */ +function validateComment(comment) { + const minLen = 5 + if (comment.length < minLen) { + throw new RangeError('comment should be at least 5 characters long') + } + return true +} diff --git a/solutions/05_javascript/employee.js b/solutions/05_javascript/employee.js new file mode 100644 index 00000000..54429d58 --- /dev/null +++ b/solutions/05_javascript/employee.js @@ -0,0 +1,78 @@ +#!/usr/bin/env node +/* eslint no-magic-numbers: 0 */ + +'use strict' + +const employee = { + firstName: 'Colin', + 'last name': 'Stephen', + startYear: 2010, + gender: 'male', + 'date of birth': '1980-01-01', + getName: function() { + return `${this.firstName} ${this["last name"]}` + }, + setName: function(fullname) { + console.log(fullname) + const words = fullname.toString().split(' ') + console.log(words) + console.log(this) + this.firstName = words[0] || '' + this['last name'] = words[1] || '' + }, + get details() { + return `firstName: ${this.firstName}, lastName: ${this["last name"]}, startYear: ${this.startYear}, gender: ${this.gender}, DoB: ${this["date of birth"]}` + }, + get yearsWorked() { + return (new Date().getFullYear() - this.startYear) + }, + set name(fullname) { + const names = fullname.toString().split(' ') + this.firstName = names[0] + this["last name"] = names[1] + } +} + +const jsonString = JSON.stringify(employee, null, 2) +console.log(jsonString) + +employee.setName('Micky Mouse') +console.log(JSON.stringify(employee, null, 2)) + +// TypeError: +// const postCode = employee.address.postCode +// console.log(postCode) + +const university = { + year1: { + a101: 'something11', + b104: 'something12', + c134: 'something13' + }, + year2: { + d201: 'something21', + e204: 'something22', + f234: 'something23' + }, + year3: { + g301: 'something31', + h304: 'something32', + i334: 'something33' + } +} + +const study01 = university.year1 +for(const code in study01) console.log(`${code} ${study01[code]}`) + +const {year1: year1, year2: year2, year3: year3} = university + +delete employee.startYear +employee.startYear = '2010' + +String.prototype.toArray = function() { + return this.split('') +} +Array.prototype.toStr = function() { + return this.join('') +} +console.log('foobar'.toArray().toStr()) \ No newline at end of file diff --git a/solutions/05_javascript/maths.js b/solutions/05_javascript/maths.js new file mode 100644 index 00000000..e7e6e769 --- /dev/null +++ b/solutions/05_javascript/maths.js @@ -0,0 +1,106 @@ +#!/usr/bin/env node +/* eslint no-magic-numbers: 0 */ + +'use strict' + +function largestNumber(a, b) { + if (a > b) return a + if (b > a) return b + return null +} + +const biggest = largestNumber(5, 8) +console.log(biggest) +// the code below achieves the same using the 'spread operator' +const nums = [5, 8] +const biggest2 = largestNumber(...nums) +console.log(biggest2) + +// example using the arguments object +function add() { + let total = 0 + console.log(arguments) + console.log(arguments['1']) + for(const arg of arguments) { + total += arg + } + return total +} + +const addNums = add(1, 2, 3, 4) +console.log(addNums) + + +// example using a rest parameter +function add2(...values) { + let total = 0 + console.log(values) + for (let i=0; i dividend - Math.floor(dividend / divisor) * divisor +console.log('remainder2: ' + remainder2(13, 4)) + +// function expression using arrow syntax and one parameter +const sqr = num => num * num +console.log(sqr(4)) + +function multiply(a = 1, b = 1) { + return a * b +} +console.log(multiply(3, 5)) + +function divideThis(dividend, divisor = 1) { + return dividend / divisor +} +console.log(divideThis(5, 2)) + +function average(...numbers) { + if (numbers.length === 0) return 0 + let sum = 0 + for (const number of numbers) sum += number + return sum / numbers.length +} +console.log(`average of [2, 4, 6]: ${average(2, 4, 6)}`) + +const squareRoot = a => Math.sqrt(a) +console.log(`squareRoot of 4096: ${squareRoot(4096)}`) + +const longest = (...strings) => strings.length === 0 ? null : strings.reduce((longestString, currentString) => currentString.length > longestString.length ? currentString : longestString) +// simple version without using reduce: +// const longest = (...strings) => { +// if (strings.length === 0) return null +// let longest = strings[0] +// for (const string of strings) if (string > longest) longest = string +// return longest +// } +console.log(`longest of ['a', 'aaa', 'aa']: ${longest('a', 'aaa', 'aa')}`) \ No newline at end of file diff --git a/solutions/05_javascript/quotes.js b/solutions/05_javascript/quotes.js new file mode 100644 index 00000000..45b2c6e4 --- /dev/null +++ b/solutions/05_javascript/quotes.js @@ -0,0 +1,12 @@ +#!/usr/bin/env node + +'use strict' + +require('fs').readFile('quotes.json', (err, data) => { + if (err) console.log(err) + const quotesArray = JSON.parse(data) + console.log(quotesArray) + for (const quoteObject of quotesArray) { + console.log(quoteObject.quote) + } +}) \ No newline at end of file diff --git a/solutions/05_javascript/quotes.json b/solutions/05_javascript/quotes.json new file mode 100644 index 00000000..bd394721 --- /dev/null +++ b/solutions/05_javascript/quotes.json @@ -0,0 +1,42 @@ +[ + { + "quote":"We know what we are, but know not what we may be.", + "author":"William Shakespeare" + }, + { + "quote":"Perfection is not attainable, but if we chase perfection we can catch excellence.", + "author":"Vince Lombardi" + }, + { + "quote":"Do your little bit of good where you are; it's those little bits of good put together that overwhelm the world.", + "author":"Desmond Tutu" + }, + { + "quote":"My mission in life is not merely to survive, but to thrive; and to do so with some passion, some compassion, some humor, and some style.", + "author":"Maya Angelou" + }, + { + "quote":"We must let go of the life we have planned, so as to accept the one that is waiting for us.", + "author":"Joseph Campbell" + }, + { + "quote":"The measure of who we are is what we do with what we have.", + "author":"Vince Lombardi" + }, + { + "quote":"Don't judge each day by the harvest you reap but by the seeds that you plant.", + "author":"Robert Louis Stevenson" + }, + { + "quote":"I hated every minute of training, but I said, 'Don't quit. Suffer now and live the rest of your life as a champion.'", + "author":"Muhammad Ali" + }, + { + "quote":"Thousands of candles can be lighted from a single candle, and the life of the candle will not be shortened. Happiness never decreases by being shared.", + "author":"Buddha" + }, + { + "quote":"How wonderful it is that nobody need wait a single moment before starting to improve the world.", + "author":"Anne Frank" + } +] \ No newline at end of file diff --git a/solutions/05_javascript/university.json b/solutions/05_javascript/university.json new file mode 100644 index 00000000..50ce27a3 --- /dev/null +++ b/solutions/05_javascript/university.json @@ -0,0 +1,17 @@ +{ + "year1": { + "a101": "something11", + "b104": "something12", + "c134": "something13" + }, + "year2": { + "d201": "something21", + "e204": "something22", + "f234": "something23" + }, + "year3": { + "g301": "something31", + "h304": "something32", + "i334": "something33" + } +} \ No newline at end of file diff --git a/solutions/05_javascript/vehicles.js b/solutions/05_javascript/vehicles.js new file mode 100644 index 00000000..d0dfeaf1 --- /dev/null +++ b/solutions/05_javascript/vehicles.js @@ -0,0 +1,49 @@ +#!/usr/bin/env node + +'use strict' + +function OldVehicle(make, model, price) { + this.make = make + this.model = model + this.price = price +} + +const OldVehicle1 = new OldVehicle('Jaguar', 'E-type', 5600) +console.log(OldVehicle1) +const OldVehicle2 = new OldVehicle('Volkswagen', 'Beetle', 3200) +console.log(OldVehicle2) + +function OldPickup(make, model, price, payload, seats) { + OldVehicle.call(this, make, model, price) + this.payload = payload + this.seats = seats +} + +const OldPickup1 = new OldPickup('GMC', 'Sonoma', 14000, 1500, 2) +console.log(OldPickup1) +const OldPickup2 = new OldPickup('Jeep', 'Comanche', 15900, 1800, 2) +console.log(OldPickup2) + +class NewVehicle { + constructor(make, model, price) { + this.make = make + this.model = model + this.price = price + } +} + +class NewPickup extends NewVehicle { + static totalValue = 0 + constructor(make, model, price, payload, seats) { + super(make, model, price) + this.payload = payload + this.seats = seats + NewPickup.totalValue += price + } +} + +const NewPickup1 = new NewPickup('Ford', 'F-150', 46900, 3000, 5) +console.log(NewPickup1) +const NewPickup2 = new NewPickup('Chevrolet', 'Silverado', 44900, 2500, 5) +console.log(NewPickup2) +console.log(NewPickup.totalValue) \ No newline at end of file diff --git a/solutions/05_javascript/weather.js b/solutions/05_javascript/weather.js new file mode 100644 index 00000000..d8f45dab --- /dev/null +++ b/solutions/05_javascript/weather.js @@ -0,0 +1,22 @@ +#!/usr/bin/env node + +'use strict' + +const request = require('request') + +const apiKey = '44c39f3fa462f86b3fc88f5678e5c5ff' +const cityName = 'coventry,uk' + +request(`https://api.openweathermap.org/data/2.5/weather?q=${cityName}&appid=${apiKey}`, (err, response, body) => { + if(err) console.log(err.message) + console.log(`the body variable contains a ${typeof body}`) + const data = JSON.parse(body) + console.log(`the data variable contains an ${typeof data}`) + console.log(data) + + request(`https://api.openweathermap.org/data/2.5/onecall?lat=${data.coord.lat}&lon=${data.coord.lon}&appid=${apiKey}`, (err, response, body) => { + if(err) console.log(err.message) + const data = JSON.parse(body) + console.log(data) + }) +}) \ No newline at end of file diff --git a/solutions/06_code_quality/asyncFunctions.js b/solutions/06_code_quality/asyncFunctions.js new file mode 100644 index 00000000..eaa1d1b3 --- /dev/null +++ b/solutions/06_code_quality/asyncFunctions.js @@ -0,0 +1,67 @@ + +'use strict' + +const request = require('request') +const readline = require('readline-sync') +const fs = require('fs') + +const baseURL = 'https://api.exchangeratesapi.io/latest' + +async function main() { + try { + const base = await getInput('enter base currency') + await checkValidCurrencyCode(base) + const data = await getData(`${baseURL}?base=${base}`) + const rates = JSON.parse(data).rates + await printObject(data) + const to = await getInput('convert to') + await checkValidCurrencyCode(to) + console.log(to) + const amount = await getInput('enter exchange amount') + const decoder = await readObjectFromFile('currencies.json') + console.log(`${amount} ${decoder[base]} (${base}) is worth ${ + (rates[to] * amount).toFixed(4)} ${decoder[to]} (${to})`) + process.exit() + } catch (err) { + console.log(`error: ${err.message}`) + } +} + +const getInput = async prompt => readline.question(`${prompt}: `) + +const checkValidCurrencyCode = code => new Promise( (resolve, reject) => { + code = code.trim() + request(baseURL, (err, res, body) => { + if (err) reject(new Error('invalid API call')) + const rates = JSON.parse(body).rates + if (!rates.hasOwnProperty(code)) throw new Error(`invalid currency code ${code}`) + resolve() + }) +}) + +const getData = url => new Promise( (resolve, reject) => { + request(url, (err, res, body) => { + if (err) reject(new Error('invalid API call')) + resolve(body) + }) +}) + +const printObject = async data => { + const indent = 2 + data = await JSON.parse(data) + const str = JSON.stringify(data, null, indent) + await new Promise( resolve => { + console.log(str) + resolve() + }) + +} + +const readObjectFromFile = fileName => new Promise( (resolve, reject) => { + fs.readFile(fileName, 'utf-8', (err, content) => { + if (err) reject(new Error(err)) + return resolve(JSON.parse(content)) + }) +}) + +main() diff --git a/solutions/06_code_quality/nestedCallbacks.js b/solutions/06_code_quality/nestedCallbacks.js new file mode 100644 index 00000000..789ae5c1 --- /dev/null +++ b/solutions/06_code_quality/nestedCallbacks.js @@ -0,0 +1,52 @@ +#!/usr/bin/env node + +/* eslint max-lines-per-function: 0 */ + +'use strict' + +const request = require('request') +const readline = require('readline') +const fs = require('fs') + +const io = { input: process.stdin, output: process.stdout } + +const read = readline.createInterface(io) +read.question('input base currency: ', base => { + console.log(`You entered ${base}`) + read.close() + base = base.trim() + // now we need to check the code is valid + request('https://api.exchangeratesapi.io/latest', (err, res, body) => { + if (err) { + console.error(err.message) + process.exit() + } + const rates = JSON.parse(body).rates + if (!rates.hasOwnProperty(base)) { + console.error(`invalid currency code ${base}`) + process.exit() + } + // now we can get the currency rates + request(`https://api.exchangeratesapi.io/latest?base=${base}`, (err, res, body) => { + if (err) { + console.error(err.message) + process.exit() + } + body = JSON.parse(body) + console.log(body) + // lets ask another question + const read = readline.createInterface(io) + read.question('convert to: ', convertTo => { + read.question('amount to convert: ', amount => { + read.close() + fs.readFile('currencies.json', 'utf8', (err, content) => { + if(err) console.error(error.message) + const decoder = JSON.parse(content) + console.log(`${amount} ${decoder[base]} (${base}) is worth ${ + (body.rates[convertTo] * amount).toFixed(4)} ${decoder[convertTo]} (${convertTo})`) + }) + }) + }) + }) + }) +}) diff --git a/solutions/06_code_quality/promises.js b/solutions/06_code_quality/promises.js new file mode 100644 index 00000000..82a6d831 --- /dev/null +++ b/solutions/06_code_quality/promises.js @@ -0,0 +1,70 @@ + +'use strict' + +const request = require('request') +const readline = require('readline') +const fs = require('fs') + +const baseURL = 'https://api.exchangeratesapi.io/latest' + +const getInput = prompt => new Promise(resolve => { + const read = readline.createInterface({ input: process.stdin, output: process.stdout }) + read.question(`${prompt}: `, value => { + console.log(`You entered ${value}`) + read.close() + return resolve(value) + }) +}) + +const checkValidCurrencyCode = code => new Promise( (resolve, reject) => { + code = code.trim() + request(baseURL, (err, res, body) => { + if (err) reject(new Error('invalid API call')) + const rates = JSON.parse(body).rates + if (!rates.hasOwnProperty(code)) reject(new Error(`invalid currency code ${code}`)) + return resolve(code) + }) +}) + +const getData = code => new Promise( (resolve, reject) => { + request(`${baseURL}?base=${code}`, (err, res, body) => { + if (err) reject(new Error('invalid API call')) + return resolve(body) + }) +}) + +const printObject = data => new Promise( resolve => { + const indent = 2 + data = JSON.parse(data) + const str = JSON.stringify(data, null, indent) + console.log(str) + return resolve() +}) + +const exit = () => new Promise( () => { + process.exit() +}) + +const readObjectFromFile = fileName => new Promise( (resolve, reject) => { + fs.readFile(fileName, 'utf-8', (err, content) => { + if (err) reject(new Error(err)) + return resolve(JSON.parse(content)) + }) +}) + +getInput('enter base currency') + .then(checkValidCurrencyCode) + .then(code => this.base = code) + .then( () => getData(this.base)) + .then( body => this.rates = JSON.parse(body).rates) + .then( () => getInput('convert to')) + .then(checkValidCurrencyCode) + .then( code => this.convertTo = code) + .then( () => readObjectFromFile('currencies.json')) + .then(fileObject => this.decoder = fileObject) + .then( () => getInput('enter exchange amount')) + .then( amount => console.log(`${amount} ${this.decoder[this.base]} (${this.base}) is worth ${ + (this.rates[this.convertTo] * amount).toFixed(4)} ${this.decoder[this.convertTo]} (${this.convertTo})`)) + .then(exit) + .catch(err => console.error(`error: ${err.message}`)) + .then(exit) diff --git a/solutions/06_code_quality/todo/index.js b/solutions/06_code_quality/todo/index.js new file mode 100644 index 00000000..2c514484 --- /dev/null +++ b/solutions/06_code_quality/todo/index.js @@ -0,0 +1,80 @@ +#!/usr/bin/env node + +/** + * Server index module + * @module index + */ + +const Koa = require('koa') +const Router = require('koa-router') +const stat = require('koa-static') +const bodyParser = require('koa-bodyparser') +const handlebars = require('koa-hbs-renderer') + +const app = new Koa() +const router = new Router() +app.use(stat('public')) +app.use(bodyParser()) +app.use(handlebars({ paths: { views: `${__dirname}/views` } })) +app.use(router.routes()) + +// The through which the server will communicate +const port = 8080 + +const List = require('./modules/list').List +const list = new List() + +/** + * Get the home page + * @name Home + * @route {GET} / + */ +router.get('/', ctx => { + try { + const items = list.getAll() + console.log(items) + const data = {items} + ctx.render('home', data) + } catch(err) { + console.log(err.message) + ctx.render('home', {msg: err.message}) + } +}) + +/** + * Add an item to the list + * @name Add item + * @route {POST} / + */ +router.post('/', ctx => { + try { + const body = ctx.request.body + console.log(body) + list.add(body.item, body.qty) + ctx.redirect('/') + } catch(err) { + console.log(err.message) + ctx.redirect(`/?msg=${err.message}`) + } +}) + +/** + * Delete an item from the list + * @name Delete item + * @route {get} /delete/:key + */ +router.get('/delete/:key', ctx => { + try { + const key = ctx.params.key + console.log(`key: ${key}`) + list.delete(key) + ctx.redirect('/?msg=item deleted') + } catch(err) { + console.log(err.message) + ctx.redirect(`/${err.message}`) + } +}) + +module.exports = app.listen(port, () => { + console.log(`listening on port ${port}`) +}) diff --git a/solutions/06_code_quality/todo/list.js b/solutions/06_code_quality/todo/list.js new file mode 100644 index 00000000..33b8dc9b --- /dev/null +++ b/solutions/06_code_quality/todo/list.js @@ -0,0 +1,53 @@ + +/** + * Class representing a list of items. + * */ +class List { + + /** + * Create a list. + */ + constructor() { + this.items = [] + } + + /** + * Add an item to the list. + * @param {String} item - The name of the eitem. + * @param {Number} qty - The number of items to add. + */ + add(item, qty) { + const data = {item: item, qty: qty} + this.items.push(data) + } + + /** + * Return the list of items. + * @return {Array.<{item: String, qty: Number}>} An array containing the items. + */ + getAll() { + return this.items.map( (element, index) => ({key: index, item: element.item, qty: element.qty})) + } + + /** + * Delete an item from the list. + * @param {Number} id - The index of the deletable item + */ + delete(id) { + this.items.splice(id, 1) + } + + /** + * Return the number of items in the list + * @return {Number} The number of items in the list + */ + count() { + return this.items.count + } + +} + +// exporting the class by name adds the name to the documentation +module.exports = { + List +} diff --git a/solutions/07_unit_testing/database/todo.js b/solutions/07_unit_testing/database/todo.js new file mode 100644 index 00000000..106f408d --- /dev/null +++ b/solutions/07_unit_testing/database/todo.js @@ -0,0 +1,59 @@ + +'use strict' + +const sqlite = require('sqlite-async') + +module.exports = class ToDo { + + constructor(dbName = ':memory:') { + return (async() => { + this.db = await sqlite.open(dbName) + const sql = 'CREATE TABLE IF NOT EXISTS items(id INTEGER PRIMARY KEY AUTOINCREMENT, item TEXT, qty NUMERIC)' + await this.db.run(sql) + return this + })() + } + + async add(item, qty) { + if (item === '') throw new Error('item cannot be empty') + if (qty === '') throw new Error('qty cannot be empty') + qty = Number(qty) + if(isNaN(qty)) throw new Error('the quantity must be a number') + let sql = `SELECT * FROM items WHERE ITEM = "${item}"` + const data = await this.db.all(sql) + if(data.length === 0) { + sql = `INSERT INTO items(item, qty) VALUES("${item}", ${qty})` + await this.db.run(sql) + } else { + const newQty = data[0].qty + qty + sql = `UPDATE items SET qty=${newQty} WHERE ITEM = "${item}"` + await this.db.run(sql) + } + } + + async getAll() { + const sql = 'SELECT * FROM items' + const data = await this.db.all(sql) + console.log(data) + return data + } + + async delete(id) { + if (id === undefined) throw new Error('id cannot be undefined') + id = Number(id) + if (isNaN(id)) throw new Error('id must be a number') + const sql = `DELETE FROM items WHERE id=${id}` + if ((await this.db.run(sql)).changes === 0) throw new Error('id has to exist') + } + + async countItems() { + const sql = 'SELECT COUNT(*) as items FROM items' + const data = await this.db.get(sql) + return data.items + } + + async clear() { + await this.db.run('DELETE FROM items') + } + +} diff --git a/solutions/07_unit_testing/database/todo.spec.js b/solutions/07_unit_testing/database/todo.spec.js new file mode 100644 index 00000000..7a3663af --- /dev/null +++ b/solutions/07_unit_testing/database/todo.spec.js @@ -0,0 +1,163 @@ + +'use strict' + +const ToDo = require('../modules/todo.js') + +beforeAll( async() => { + // stuff to do before any of the tests run +}) + +afterAll( async() => { + // runs after all the tests have completed +}) + +describe('add()', () => { + // block of tests + // beforeEach( async() => { + // todo.clear() + // }) + afterEach( async() => { + // runs after each test completes + }) + test('add a single item', async done => { + expect.assertions(1) + // ARRANGE + const todo = await new ToDo() // DB runs in-memory if no name supplied + // ACT + await todo.add('bread', 3) + const count = await todo.countItems() + // ASSERT + expect(count).toBe(1) + done() + }) + + test('qty must be a number', async done => { + expect.assertions(1) + const todo = await new ToDo() + await expect( todo.add('bread', 'three') ).rejects.toEqual( Error('the quantity must be a number') ) + done() + }) + + test('duplicates should increase qty', async done => { + expect.assertions(2) + // ARRANGE + const todo = await new ToDo() + // ACT + await todo.add('bread', 4) + await todo.add('bread', 2) + const data = await todo.getAll() + const qty = data[0].qty + // ASSERT (note there are two assertions as stated on line 42) + expect(data).toHaveLength(1) + expect(qty).toEqual(6) + done() + }) + + test('item cannot be empty', async done => { + expect.assertions(1) + const todo = await new ToDo() + await expect( todo.add('', 1) ).rejects.toEqual( Error('item cannot be empty') ) + done() + }) + + test('qty cannot be empty', async done => { + expect.assertions(1) + const todo = await new ToDo() + await expect( todo.add('bread', '') ).rejects.toEqual( Error('qty cannot be empty') ) + done() + }) + +}) + +describe('delete()', () => { + beforeEach( async() => { + this.todo = await new ToDo() + await this.todo.add('bread', 5) + }) + + test('id cannot be undefined', async done => { + expect.assertions(1) + await expect( this.todo.delete() ).rejects.toEqual( Error('id cannot be undefined') ) + done() + }) + + test('id must be a number', async done => { + expect.assertions(1) + await expect( this.todo.delete('a') ).rejects.toEqual( Error('id must be a number') ) + done() + }) + + test('id has to exist', async done => { + expect.assertions(1) + await expect( this.todo.delete(2) ).rejects.toEqual( Error('id has to exist') ) + done() + }) + + test('delete an item', async done => { + expect.assertions(2) + await this.todo.delete(1) + expect(await this.todo.countItems()).toEqual(0) + expect(await this.todo.getAll()).toEqual([]) + done() + }) +}) + +describe('getAll()', () => { + beforeEach( async() => { + this.todo = await new ToDo() + }) + + test('getAll() with no items', async done => { + expect.assertions(1) + expect(await this.todo.getAll()).toEqual([]) + done() + }) + + test('getAll() with a single item', async done => { + expect.assertions(1) + await this.todo.add('bread', 2) + expect(await this.todo.getAll()).toEqual([{"item":"bread", "qty":2, "id":1}]) + done() + }) + + test('getAll() with two items', async done => { + expect.assertions(1) + await this.todo.add('bread', 2) + await this.todo.add('ham', 3) + expect(await this.todo.getAll()).toEqual([{"item":"bread", "qty":2, "id":1}, {"item":"ham", "qty":3, "id":2}]) + done() + }) +}) + +describe('clear()', () => { + beforeEach( async() => { + this.todo = await new ToDo() + }) + + test('clear() with no items', async done => { + expect.assertions(2) + await this.todo.clear() + expect(await this.todo.countItems()).toEqual(0) + expect(await this.todo.getAll()).toEqual([]) + done() + }) + + test('clear() with a single item', async done => { + expect.assertions(2) + await this.todo.add('bread', 2) + await this.todo.clear() + expect(await this.todo.countItems()).toEqual(0) + expect(await this.todo.getAll()).toEqual([]) + done() + }) + + test('clear() with two items', async done => { + expect.assertions(2) + await this.todo.add('bread', 2) + await this.todo.add('ham', 3) + await this.todo.clear() + expect(await this.todo.countItems()).toEqual(0) + expect(await this.todo.getAll()).toEqual([]) + done() + }) +}) diff --git a/solutions/07_unit_testing/todo/todo.js b/solutions/07_unit_testing/todo/todo.js new file mode 100644 index 00000000..844c2a62 --- /dev/null +++ b/solutions/07_unit_testing/todo/todo.js @@ -0,0 +1,43 @@ + +'use strict' + +let data = [] + +module.exports.clear = () => { + data = [] +} + +module.exports.add = (item, qty) => { + if (item === '') throw new Error('item cannot be empty') + if (qty === '') throw new Error('qty cannot be empty') + qty = Number(qty) + if(isNaN(qty)) throw new Error('qty must be a number') + let flag = false + for(const index in data) { + if (data[index].item === item) { + data[index].qty += qty + flag = true + } + } + if(flag === false) data.push({item: item, qty: qty}) +} + +module.exports.getAll = () => { + for(const key in data) data[key].key = Number(key) + return data +} + +module.exports.delete = key => { + console.log(`delete key ${key}`) + if (key === undefined) throw new Error('key cannot be undefined') + key = Number(key) + if (isNaN(key)) throw new Error('key must be a number') + try { + this.getAll()[key].key // throws error unless the item with the target key exists + data.splice(key, 1) + } catch { + throw new Error('key has to exist') + } +} + +module.exports.countItems = () => data.length diff --git a/solutions/07_unit_testing/todo/todo.spec.js b/solutions/07_unit_testing/todo/todo.spec.js new file mode 100644 index 00000000..a0bdc7d4 --- /dev/null +++ b/solutions/07_unit_testing/todo/todo.spec.js @@ -0,0 +1,242 @@ + +'use strict' + +const todo = require('../modules/todo.js') + +beforeAll( async() => { + // stuff to do before any of the tests run +}) + +afterAll( async() => { + // runs after all the tests have completed +}) + +describe('add()', () => { + // block of tests + beforeEach( async() => { + todo.clear() + }) + afterEach( async() => { + // runs after each test completes + }) + test('add a single item', async done => { + expect.assertions(1) + try { + todo.add('bread', 3) + expect(todo.countItems()).toBe(1) + } catch(err) { + done.fail(err) + } finally { + done() + } + }) + test('qty must be a number', async done => { + expect.assertions(1) + try { + todo.add('bread', 'three') + done.fail('test failed') + } catch(err) { + expect(err.message).toBe('qty must be a number') + } finally { + done() + } + }) + + test('duplicates should increase qty', async done => { + expect.assertions(2) + try { + // ACT + todo.add('bread', 4) + todo.add('bread', 2) + // ASSERT + const count = todo.countItems() + expect(count).toBe(1) + const data = todo.getAll() + const qty = data[0].qty + expect(qty).toEqual(6) + } catch(err) { + done.fail('test failed') + } finally { + done() + } + }) + + test('item cannot be empty', async done => { + expect.assertions(1) + try { + todo.add('', 1) + done.fail('test failed') + } catch(err) { + expect(err.message).toBe('item cannot be empty') + } finally { + done() + } + }) + + test('qty cannot be empty', async done => { + expect.assertions(1) + try { + todo.add('bread', '') + done.fail('test failed') + } catch(err) { + expect(err.message).toBe('qty cannot be empty') + } finally { + done() + } + }) +}) + +describe('delete()', () => { + beforeEach( async() => { + todo.clear() + todo.add('bread', 2) + }) + afterEach( async() => { + }) + + test('key cannot be undefined', async done => { + expect.assertions(1) + try { + todo.delete() + done.fail('test failed') + } catch(err) { + expect(err.message).toBe('key cannot be undefined') + } finally { + done() + } + }) + + test('key must be a number', async done => { + expect.assertions(1) + try { + todo.delete('a') + done.fail('test failed') + } catch(err) { + expect(err.message).toBe('key must be a number') + } finally { + done() + } + }) + + test('key has to exist', async done => { + expect.assertions(1) + try { + todo.delete(1) + done.fail('test failed') + } catch(err) { + expect(err.message).toBe('key has to exist') + } finally { + done() + } + }) + + test('delete an item', async done => { + expect.assertions(1) + try { + todo.delete(0) + expect(todo.getAll()).toEqual([]) + } catch(err) { + done.fail('test failed') + console.log(err) + } finally { + done() + } + }) +}) + +describe('getAll()', () => { + beforeEach( async() => { + todo.clear() + }) + afterEach( async() => { + }) + + test('getAll() with no items', async done => { + expect.assertions(1) + try { + expect(todo.getAll()).toEqual([]) + } catch(err) { + done.fail('test failed') + console.log(err) + } finally { + done() + } + }) + + test('getAll() with a single item', async done => { + expect.assertions(1) + try { + todo.add('bread', 2) + expect(todo.getAll()).toEqual([{"item":"bread", "qty":2, "key":0}]) + } catch(err) { + done.fail('test failed') + console.log(err) + } finally { + done() + } + }) + + test('getAll() with two items', async done => { + expect.assertions(1) + try { + todo.add('bread', 2) + todo.add('ham', 3) + expect(todo.getAll()).toEqual([{"item":"bread", "qty":2, "key":0}, {"item":"ham", "qty":3, "key":1}]) + } catch(err) { + done.fail('test failed') + console.log(err) + } finally { + done() + } + }) +}) + +describe('clear()', () => { + beforeEach( async() => { + todo.clear() + }) + afterEach( async() => { + }) + + test('clear() with no items', async done => { + expect.assertions(1) + try { + todo.clear() + expect(todo.getAll()).toEqual([]) + } catch(err) { + done.fail('test failed') + console.log(err) + } finally { + done() + } + }) + + test('clear() with 1 item', async done => { + expect.assertions(1) + try { + todo.add('bread', 2) + todo.clear() + expect(todo.getAll()).toEqual([]) + } catch(err) { + done.fail('test failed') + console.log(err) + } finally { + done() + } + }) + + test('clear() with 2 items', async done => { + expect.assertions(1) + try { + todo.add('bread', 2) + todo.add('ham', 3) + todo.clear() + expect(todo.getAll()).toEqual([]) + } catch(err) { + done.fail('test failed') + console.log(err) + } finally { + done() + } + }) +})