├── ch04 ├── jsx_example │ ├── .babelrc │ ├── test.jsx │ └── package.json ├── babel_example │ ├── test.js │ ├── .babelrc │ └── package.json └── all_together_example │ ├── .babelrc │ ├── package.json │ ├── index.html │ └── main.jsx ├── ch07 ├── third_example │ ├── src │ │ ├── module1.js │ │ ├── module2.js │ │ └── index.js │ ├── README.md │ ├── package.json │ ├── webpack.config.js │ └── .yo-rc.json ├── fourth_example │ ├── src │ │ ├── module1.ts │ │ ├── module2.ts │ │ └── index.ts │ ├── README.md │ ├── package.json │ ├── webpack.config.js │ ├── .yo-rc.json │ └── tsconfig.json ├── first_example │ ├── src │ │ └── index.js │ └── package.json └── second example │ ├── src │ └── index.js │ ├── README.md │ ├── package.json │ ├── webpack.config.js │ └── .yo-rc.json ├── .gitattributes ├── mailbag ├── client │ ├── .babelrc │ ├── src │ │ ├── code │ │ │ ├── config.ts │ │ │ ├── components │ │ │ │ ├── WelcomeView.tsx │ │ │ │ ├── MailboxList.tsx │ │ │ │ ├── Toolbar.tsx │ │ │ │ ├── ContactList.tsx │ │ │ │ ├── MessageList.tsx │ │ │ │ ├── ContactView.tsx │ │ │ │ ├── BaseLayout.tsx │ │ │ │ └── MessageView.tsx │ │ │ ├── SMTP.ts │ │ │ ├── main.tsx │ │ │ ├── Contacts.ts │ │ │ ├── IMAP.ts │ │ │ └── state.ts │ │ ├── index.html │ │ └── css │ │ │ └── main.css │ ├── tsconfig.json │ ├── webpack.config.js │ └── package.json └── server │ ├── serverInfo.json │ ├── src │ ├── ServerInfo.ts │ ├── SMTP.ts │ ├── Contacts.ts │ ├── main.ts │ └── IMAP.ts │ ├── package.json │ └── tsconfig.json ├── 9781484257371.jpg ├── ch01 ├── test.js ├── server.js └── server_time.js ├── ch05 ├── app.ts └── index.html ├── battlejong ├── client │ ├── src │ │ ├── img │ │ │ ├── tile101.png │ │ │ ├── tile102.png │ │ │ ├── tile103.png │ │ │ ├── tile104.png │ │ │ ├── tile105.png │ │ │ ├── tile106.png │ │ │ ├── tile107.png │ │ │ ├── tile108.png │ │ │ ├── tile109.png │ │ │ ├── tile110.png │ │ │ ├── tile111.png │ │ │ ├── tile112.png │ │ │ ├── tile113.png │ │ │ ├── tile114.png │ │ │ ├── tile115.png │ │ │ ├── tile116.png │ │ │ ├── tile117.png │ │ │ ├── tile118.png │ │ │ ├── tile119.png │ │ │ ├── tile120.png │ │ │ ├── tile121.png │ │ │ ├── tile122.png │ │ │ ├── tile123.png │ │ │ ├── tile124.png │ │ │ ├── tile125.png │ │ │ ├── tile126.png │ │ │ ├── tile127.png │ │ │ ├── tile128.png │ │ │ ├── tile129.png │ │ │ ├── tile130.png │ │ │ ├── tile131.png │ │ │ ├── tile132.png │ │ │ ├── tile133.png │ │ │ ├── tile134.png │ │ │ ├── tile135.png │ │ │ ├── tile136.png │ │ │ ├── tile137.png │ │ │ ├── tile138.png │ │ │ ├── tile139.png │ │ │ ├── tile140.png │ │ │ ├── tile141.png │ │ │ └── tile142.png │ │ ├── index.html │ │ ├── code │ │ │ ├── d.ts │ │ │ ├── main.tsx │ │ │ ├── components │ │ │ │ ├── BaseLayout.tsx │ │ │ │ ├── ControlArea.tsx │ │ │ │ └── PlayerBoard.tsx │ │ │ ├── socketComm.ts │ │ │ └── state.ts │ │ └── css │ │ │ └── main.css │ ├── package.json │ ├── webpack.config.js │ └── tsconfig.json └── server │ ├── package.json │ ├── tsconfig.json │ └── src │ └── server.ts ├── ch12 └── dockernode │ ├── Dockerfile │ ├── server.js │ └── package.json ├── ch03 ├── listing_3-01.html ├── listing_3-02.html ├── listing_3-03.html ├── listing_3-04.html └── listing_3-05.html ├── errata.md ├── README.md ├── Contributing.md └── LICENSE.txt /ch04/jsx_example/.babelrc: -------------------------------------------------------------------------------- 1 | { "presets": [ "@babel/preset-react" ] } 2 | -------------------------------------------------------------------------------- /ch04/babel_example/test.js: -------------------------------------------------------------------------------- 1 | const newArray = [ 44, 55, 66].map((num) => n * 2); 2 | -------------------------------------------------------------------------------- /ch07/third_example/src/module1.js: -------------------------------------------------------------------------------- 1 | export default function getA() { return 20; } 2 | -------------------------------------------------------------------------------- /ch07/third_example/src/module2.js: -------------------------------------------------------------------------------- 1 | export default function getB() { return 30; } 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /ch07/fourth_example/src/module1.ts: -------------------------------------------------------------------------------- 1 | export default function getA() { return 20; } 2 | -------------------------------------------------------------------------------- /ch07/fourth_example/src/module2.ts: -------------------------------------------------------------------------------- 1 | export default function getB() { return 30; } 2 | -------------------------------------------------------------------------------- /ch07/first_example/src/index.js: -------------------------------------------------------------------------------- 1 | let a = 2; 2 | let b = 5; 3 | let c = a * b; 4 | alert(c); 5 | -------------------------------------------------------------------------------- /ch07/second example/src/index.js: -------------------------------------------------------------------------------- 1 | let a = 2; 2 | let b = 5; 3 | let c = a * b; 4 | alert(c); 5 | -------------------------------------------------------------------------------- /mailbag/client/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"] 3 | } 4 | -------------------------------------------------------------------------------- /9781484257371.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/9781484257371.jpg -------------------------------------------------------------------------------- /ch01/test.js: -------------------------------------------------------------------------------- 1 | const a = 5; 2 | const b = 3; 3 | const c = a * b; 4 | console.log(`${a} * ${b} = ${c}`); 5 | -------------------------------------------------------------------------------- /ch05/app.ts: -------------------------------------------------------------------------------- 1 | function sayHi(humanName: string) { 2 | alert(`Hello, ${humanName}!`); 3 | } 4 | sayHi("Luke Skywalker"); 5 | -------------------------------------------------------------------------------- /ch05/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /ch07/third_example/src/index.js: -------------------------------------------------------------------------------- 1 | import getA from "./module1"; 2 | import getB from "./module2"; 3 | 4 | alert(getA() * getB()); 5 | -------------------------------------------------------------------------------- /ch07/fourth_example/src/index.ts: -------------------------------------------------------------------------------- 1 | import getA from "./module1.ts"; 2 | import getB from "./module2.ts"; 3 | 4 | alert(getA() * getB()); 5 | -------------------------------------------------------------------------------- /battlejong/client/src/img/tile101.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile101.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile102.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile102.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile103.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile103.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile104.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile104.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile105.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile105.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile106.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile106.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile107.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile107.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile108.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile108.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile109.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile109.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile110.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile110.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile111.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile111.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile112.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile112.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile113.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile113.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile114.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile115.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile115.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile116.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile116.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile117.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile117.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile118.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile118.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile119.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile119.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile120.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile121.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile121.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile122.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile122.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile123.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile123.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile124.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile124.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile125.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile126.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile126.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile127.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile127.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile128.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile129.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile129.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile130.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile130.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile131.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile131.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile132.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile132.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile133.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile133.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile134.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile134.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile135.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile135.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile136.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile136.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile137.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile137.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile138.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile138.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile139.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile139.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile140.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile140.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile141.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile141.png -------------------------------------------------------------------------------- /battlejong/client/src/img/tile142.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/modern-full-stack-development/HEAD/battlejong/client/src/img/tile142.png -------------------------------------------------------------------------------- /ch04/jsx_example/test.jsx: -------------------------------------------------------------------------------- 1 | const button = 3 | Click Me 4 | ; 5 | -------------------------------------------------------------------------------- /ch04/all_together_example/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "@babel/preset-react" ], 3 | "plugins": [ "@babel/plugin-proposal-class-properties" ] 4 | } 5 | -------------------------------------------------------------------------------- /ch01/server.js: -------------------------------------------------------------------------------- 1 | require("http").createServer((inRequest, inResponse) => { 2 | inResponse.end("Hello from my first Node Web server"); 3 | }).listen(80); 4 | -------------------------------------------------------------------------------- /ch04/babel_example/.babelrc: -------------------------------------------------------------------------------- 1 | { "presets": [ 2 | [ "@babel/preset-env", { 3 | "targets" : { "browsers" : [ "last 3 versions", "safari >= 6" ] } 4 | }] 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /ch12/dockernode/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10 2 | WORKDIR /usr/src/app 3 | COPY package*.json ./ 4 | COPY server.js ./ 5 | RUN npm install 6 | EXPOSE 8080 7 | CMD [ "node", "server.js" ] 8 | -------------------------------------------------------------------------------- /mailbag/client/src/code/config.ts: -------------------------------------------------------------------------------- 1 | export const config: { serverAddress: string, userEmail: string } = 2 | { 3 | serverAddress : "http://localhost", 4 | userEmail : "name@domain.com" 5 | }; 6 | -------------------------------------------------------------------------------- /ch03/listing_3-01.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Intro To React 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /mailbag/client/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | MailBag 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /battlejong/client/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | BattleJong 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /battlejong/client/src/code/d.ts: -------------------------------------------------------------------------------- 1 | // Declare a type so that we can import image files as modules. Without this, you'd be forced to use 2 | // require() instead. 3 | declare module "*.png" { 4 | const value: any; 5 | export = value; 6 | } 7 | -------------------------------------------------------------------------------- /ch12/dockernode/server.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const app = express(); 3 | app.get("/", (inRequest, inResponse) => { 4 | inResponse.send("I am running inside a container!"); 5 | }); 6 | app.listen("8080", "0.0.0.0"); 7 | console.log("dockernode ready"); 8 | -------------------------------------------------------------------------------- /ch07/fourth_example/README.md: -------------------------------------------------------------------------------- 1 | # 🚀 Welcome to your new awesome project! 2 | 3 | This project has been created using **webpack scaffold**, you can now run 4 | 5 | ``` 6 | npm run build 7 | ``` 8 | 9 | or 10 | 11 | ``` 12 | yarn build 13 | ``` 14 | 15 | to bundle your application 16 | -------------------------------------------------------------------------------- /ch07/second example/README.md: -------------------------------------------------------------------------------- 1 | # 🚀 Welcome to your new awesome project! 2 | 3 | This project has been created using **webpack scaffold**, you can now run 4 | 5 | ``` 6 | npm run build 7 | ``` 8 | 9 | or 10 | 11 | ``` 12 | yarn build 13 | ``` 14 | 15 | to bundle your application 16 | -------------------------------------------------------------------------------- /ch07/third_example/README.md: -------------------------------------------------------------------------------- 1 | # 🚀 Welcome to your new awesome project! 2 | 3 | This project has been created using **webpack scaffold**, you can now run 4 | 5 | ``` 6 | npm run build 7 | ``` 8 | 9 | or 10 | 11 | ``` 12 | yarn build 13 | ``` 14 | 15 | to bundle your application 16 | -------------------------------------------------------------------------------- /errata.md: -------------------------------------------------------------------------------- 1 | # Errata for *Book Title* 2 | 3 | On **page xx** [Summary of error]: 4 | 5 | Details of error here. Highlight key pieces in **bold**. 6 | 7 | *** 8 | 9 | On **page xx** [Summary of error]: 10 | 11 | Details of error here. Highlight key pieces in **bold**. 12 | 13 | *** -------------------------------------------------------------------------------- /battlejong/client/src/code/main.tsx: -------------------------------------------------------------------------------- 1 | // Style imports. 2 | import "normalize.css"; 3 | import "../css/main.css"; 4 | 5 | // React imports. 6 | import React from "react"; 7 | import ReactDOM from "react-dom"; 8 | 9 | // App imports. 10 | import BaseLayout from "./components/BaseLayout"; 11 | 12 | 13 | // Render UI. 14 | ReactDOM.render(, document.body); 15 | -------------------------------------------------------------------------------- /ch12/dockernode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodedocker", 3 | "version": "1.0.0", 4 | "description": "dockernode", 5 | "repository": "dockernode", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "express": "^4.17.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ch01/server_time.js: -------------------------------------------------------------------------------- 1 | require("http").createServer((inRequest, inResponse) => { 2 | const requestModule = require("request"); 3 | requestModule( 4 | "http://worldtimeapi.org/api/timezone/America/New_York", 5 | function (inErr, inResp, inBody) { 6 | inResponse.end( 7 | `Hello from my first Node Web server: ${inBody}` 8 | ); 9 | } 10 | ); 11 | }).listen(80); 12 | -------------------------------------------------------------------------------- /ch07/first_example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "etc", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "webpack": "^4.41.2", 14 | "webpack-cli": "^3.3.10" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /mailbag/client/src/code/components/WelcomeView.tsx: -------------------------------------------------------------------------------- 1 | // React imports. 2 | import React from "react"; 3 | 4 | 5 | /** 6 | * WelcomeView. 7 | */ 8 | const WelcomeView = () => ( 9 | 10 |
11 |

Welcome to MailBag!

12 |
13 | 14 | ); /* WelcomeView. */ 15 | 16 | 17 | export default WelcomeView; 18 | -------------------------------------------------------------------------------- /ch04/babel_example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ch04", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "@babel/cli": "^7.6.2", 13 | "@babel/core": "^7.6.2", 14 | "@babel/preset-env": "^7.6.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ch04/jsx_example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ch04", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "@babel/cli": "^7.6.2", 13 | "@babel/core": "^7.6.2", 14 | "@babel/preset-react": "^7.0.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /mailbag/server/serverInfo.json: -------------------------------------------------------------------------------- 1 | { 2 | "smtp" : { 3 | "host" : "my_smtp_server_address", 4 | "port" : 465, 5 | "auth" : { 6 | "user" : "my_smtp_username", 7 | "pass" : "my_smtp_password" 8 | } 9 | }, 10 | "imap" : { 11 | "host" : "my_imap_server_address", 12 | "port" : 993, 13 | "auth" : { 14 | "user" : "my_imap_username", 15 | "pass" : "my_imap_password" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /mailbag/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions" : { 3 | "esModuleInterop" : true, 4 | "sourceMap" : true, 5 | "noImplicitAny" : false, 6 | "module" : "commonjs", 7 | "target" : "es6", 8 | "lib" : [ "es2015", "es2017", "dom" ], 9 | "removeComments" : true, 10 | "jsx" : "react", 11 | "allowJs" : true, 12 | "baseUrl" : "./", 13 | "paths" : { "components/*" : [ "src/components/*" ] } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ch04/all_together_example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ch04", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "@babel/cli": "^7.6.2", 13 | "@babel/core": "^7.6.2", 14 | "@babel/plugin-proposal-class-properties": "^7.5.5", 15 | "@babel/preset-react": "^7.0.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ch04/all_together_example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Intro To React 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apress Source Code 2 | 3 | This repository accompanies [*Modern Full-Stack Development*](http://www.apress.com/9781484257371) by Frank Zammetti (Apress, 2020). 4 | 5 | [comment]: #cover 6 | ![Cover image](9781484257371.jpg) 7 | 8 | Download the files as a zip using the green button, or clone the repository to your machine using Git. 9 | 10 | ## Releases 11 | 12 | Release v1.0 corresponds to the code in the published book, without corrections or updates. 13 | 14 | ## Contributions 15 | 16 | See the file Contributing.md for more information on how you can contribute to this repository. -------------------------------------------------------------------------------- /battlejong/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "compile": "npx tsc && node ./dist/server.js", 8 | "dev": "node ./node_modules/nodemon/bin/nodemon.js -e ts --exec \"npm run compile\"" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@types/express": "^4.17.2", 14 | "@types/node": "^13.1.0", 15 | "@types/ws": "^6.0.4", 16 | "nodemon": "^2.0.2", 17 | "typescript": "^3.7.3" 18 | }, 19 | "dependencies": { 20 | "express": "^4.17.1", 21 | "ws": "^7.2.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ch07/second example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "etc", 3 | "version": "1.0.0", 4 | "description": "My webpack project", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "webpack", 9 | "start": "webpack-dev-server" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "@babel/core": "^7.7.2", 16 | "@babel/preset-env": "^7.7.1", 17 | "babel-loader": "^8.0.6", 18 | "babel-plugin-syntax-dynamic-import": "^6.18.0", 19 | "html-webpack-plugin": "^3.2.0", 20 | "webpack": "^4.41.2", 21 | "webpack-cli": "^3.3.10", 22 | "webpack-dev-server": "^3.9.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ch07/third_example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "etc", 3 | "version": "1.0.0", 4 | "description": "My webpack project", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "webpack", 9 | "start": "webpack-dev-server" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "@babel/core": "^7.7.2", 16 | "@babel/preset-env": "^7.7.1", 17 | "babel-loader": "^8.0.6", 18 | "babel-plugin-syntax-dynamic-import": "^6.18.0", 19 | "html-webpack-plugin": "^3.2.0", 20 | "webpack": "^4.41.2", 21 | "webpack-cli": "^3.3.10", 22 | "webpack-dev-server": "^3.9.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to Apress Source Code 2 | 3 | Copyright for Apress source code belongs to the author(s). However, under fair use you are encouraged to fork and contribute minor corrections and updates for the benefit of the author(s) and other readers. 4 | 5 | ## How to Contribute 6 | 7 | 1. Make sure you have a GitHub account. 8 | 2. Fork the repository for the relevant book. 9 | 3. Create a new branch on which to make your change, e.g. 10 | `git checkout -b my_code_contribution` 11 | 4. Commit your change. Include a commit message describing the correction. Please note that if your commit message is not clear, the correction will not be accepted. 12 | 5. Submit a pull request. 13 | 14 | Thank you for your contribution! -------------------------------------------------------------------------------- /mailbag/client/src/code/components/MailboxList.tsx: -------------------------------------------------------------------------------- 1 | // React imports. 2 | import React from "react"; 3 | 4 | // Material-UI imports. 5 | import Chip from "@material-ui/core/Chip"; 6 | import List from "@material-ui/core/List"; 7 | 8 | 9 | /** 10 | * Mailboxes. 11 | */ 12 | const MailboxList = ({ state }) => ( 13 | 14 | 15 | 16 | { state.mailboxes.map(value => { 17 | return ( 18 | state.setCurrentMailbox(value.path) } 19 | style={{ width:128, marginBottom:10 }} 20 | color={ state.currentMailbox === value.path ? "secondary" : "primary" } /> 21 | ); 22 | } ) } 23 | 24 | 25 | 26 | ); /* Mailboxes. */ 27 | 28 | 29 | export default MailboxList; 30 | -------------------------------------------------------------------------------- /ch07/fourth_example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "etc", 3 | "version": "1.0.0", 4 | "description": "My webpack project", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "webpack", 9 | "start": "webpack-dev-server" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "@babel/core": "^7.7.2", 16 | "@babel/preset-env": "^7.7.1", 17 | "babel-loader": "^8.0.6", 18 | "babel-plugin-syntax-dynamic-import": "^6.18.0", 19 | "html-webpack-plugin": "^3.2.0", 20 | "ts-loader": "^6.2.1", 21 | "typescript": "^3.7.2", 22 | "webpack": "^4.41.2", 23 | "webpack-cli": "^3.3.10", 24 | "webpack-dev-server": "^3.9.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /mailbag/server/src/ServerInfo.ts: -------------------------------------------------------------------------------- 1 | // Note imports. 2 | const path = require("path"); 3 | const fs = require("fs"); 4 | 5 | 6 | // Define interface for server information. 7 | export interface IServerInfo { 8 | smtp : { 9 | host: string, 10 | port: number, 11 | auth: { 12 | user: string, 13 | pass: string 14 | } 15 | }, 16 | imap : { 17 | host: string, 18 | port: number, 19 | auth: { 20 | user: string, 21 | pass: string 22 | } 23 | } 24 | } 25 | 26 | 27 | // The configured server info. 28 | export let serverInfo: IServerInfo; 29 | 30 | 31 | // Read in the server information file. 32 | const rawInfo: string = fs.readFileSync(path.join(__dirname, "../serverInfo.json")); 33 | serverInfo = JSON.parse(rawInfo); 34 | console.log("ServerInfo: ", serverInfo); 35 | -------------------------------------------------------------------------------- /mailbag/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "compile": "npx tsc && node ./dist/main.js", 8 | "dev": "node ./node_modules/nodemon/bin/nodemon.js -e ts --exec \"npm run compile\"" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "emailjs-imap-client": "^3.0.7", 14 | "express": "^4.17.1", 15 | "mailparser": "^2.7.1", 16 | "nedb": "^1.8.0", 17 | "nodemailer": "^6.3.0" 18 | }, 19 | "devDependencies": { 20 | "@types/express": "^4.17.1", 21 | "@types/mailparser": "^2.4.0", 22 | "@types/nedb": "^1.8.9", 23 | "@types/node": "^12.7.4", 24 | "@types/nodemailer": "^6.2.2", 25 | "nodemon": "^1.19.4", 26 | "typescript": "^3.6.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /battlejong/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "npx webpack" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "@types/react": "^16.9.16", 13 | "@types/react-dom": "^16.9.4", 14 | "@types/socket.io-client": "^1.4.32", 15 | "awesome-typescript-loader": "^5.2.1", 16 | "css-loader": "^3.4.0", 17 | "html-loader": "^0.5.5", 18 | "html-webpack-plugin": "^3.2.0", 19 | "style-loader": "^1.0.2", 20 | "typescript": "^3.7.3", 21 | "url-loader": "^3.0.0", 22 | "webpack": "^4.41.3", 23 | "webpack-cli": "^3.3.10" 24 | }, 25 | "dependencies": { 26 | "normalize.css": "^8.0.1", 27 | "react": "^16.12.0", 28 | "react-dom": "^16.12.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /mailbag/client/webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebPackPlugin = require("html-webpack-plugin"); 2 | 3 | module.exports = { 4 | 5 | entry : "./src/code/main.tsx", 6 | 7 | resolve : { 8 | extensions : [ ".ts", ".tsx", ".js" ] 9 | }, 10 | 11 | module : { 12 | rules : [ 13 | { 14 | test : /\.html$/, 15 | use : { loader : "html-loader" } 16 | }, 17 | { 18 | test : /\.css$/, 19 | use : [ "style-loader", "css-loader"] 20 | }, 21 | { 22 | test : /\.tsx?$/, 23 | loader: 'awesome-typescript-loader' 24 | } 25 | ] 26 | 27 | }, 28 | 29 | plugins : [ 30 | new HtmlWebPackPlugin({ template : "./src/index.html", filename : "./index.html" }) 31 | ], 32 | 33 | performance : { hints : false }, 34 | watch : true, 35 | devtool : "source-map" 36 | 37 | }; 38 | -------------------------------------------------------------------------------- /mailbag/client/src/code/components/Toolbar.tsx: -------------------------------------------------------------------------------- 1 | // React imports. 2 | import React from "react"; 3 | 4 | // Material-UI imports. 5 | import Button from "@material-ui/core/Button"; 6 | import NewContactIcon from "@material-ui/icons/ContactMail"; 7 | import NewMessageIcon from "@material-ui/icons/Email"; 8 | 9 | 10 | /** 11 | * Toolbar. 12 | */ 13 | const Toolbar = ({ state }) => ( 14 | 15 |
16 | 20 | 24 |
25 | 26 | ); 27 | 28 | 29 | export default Toolbar; 30 | -------------------------------------------------------------------------------- /mailbag/client/src/code/SMTP.ts: -------------------------------------------------------------------------------- 1 | // Library imports. 2 | import axios from "axios"; 3 | 4 | // App imports. 5 | import { config } from "./config"; 6 | 7 | 8 | // The worker that will perform SMTP operations. 9 | export class Worker { 10 | 11 | 12 | /** 13 | * Sand a message via SMTP. 14 | * 15 | * @param inTo The Email to send the message to. 16 | * @param inFrom The Email address it's from (from config). 17 | * @param inSubject The subject of the message. 18 | * @param inMessage The message itself. 19 | */ 20 | public async sendMessage(inTo: string, inFrom: string, inSubject: string, inMessage: string): Promise { 21 | 22 | console.log("SMTP.Worker.sendMessage()"); 23 | 24 | await axios.post(`${config.serverAddress}/messages`, { to : inTo, from : inFrom, subject : inSubject, 25 | text : inMessage 26 | }); 27 | 28 | } /* End sendMessage(). */ 29 | 30 | 31 | } /* End class. */ 32 | -------------------------------------------------------------------------------- /battlejong/client/webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebPackPlugin = require("html-webpack-plugin"); 2 | 3 | module.exports = { 4 | 5 | entry : "./src/code/main.tsx", 6 | 7 | resolve : { 8 | extensions : [ ".ts", ".tsx", ".js" ] 9 | }, 10 | 11 | module : { 12 | rules : [ 13 | { 14 | test: /\.png$/, 15 | use : { loader : "url-loader", options : { limit : 65536, esModule : false, } } 16 | }, 17 | { 18 | test : /\.html$/, 19 | use : { loader : "html-loader" } 20 | }, 21 | { 22 | test : /\.css$/, 23 | use : [ "style-loader", "css-loader"] 24 | }, 25 | { 26 | test : /\.tsx?$/, 27 | loader: 'awesome-typescript-loader' 28 | } 29 | ] 30 | 31 | }, 32 | 33 | plugins : [ 34 | new HtmlWebPackPlugin({ template : "./src/index.html", filename : "./index.html" }) 35 | ], 36 | 37 | performance : { hints : false }, 38 | watch : true, 39 | devtool : "source-map" 40 | 41 | }; 42 | -------------------------------------------------------------------------------- /mailbag/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mailbag", 3 | "version": "1.0.0", 4 | "description": "MailBag", 5 | "main": "main.tsx", 6 | "scripts": { 7 | "build": "webpack --mode production" 8 | }, 9 | "author": "Frank W. Zammetti", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "awesome-typescript-loader": "^5.2.1", 13 | "babel-plugin-syntax-dynamic-import": "^6.18.0", 14 | "css-loader": "^3.3.0", 15 | "html-loader": "^0.5.5", 16 | "html-webpack-plugin": "^3.2.0", 17 | "style-loader": "^1.0.1", 18 | "ts-loader": "^6.2.1", 19 | "typescript": "^3.7.3", 20 | "webpack": "^4.41.2", 21 | "webpack-cli": "^3.3.10", 22 | "webpack-dev-server": "^3.9.0" 23 | }, 24 | "dependencies": { 25 | "@material-ui/core": "^4.6.0", 26 | "@material-ui/icons": "^4.5.1", 27 | "@types/material-ui": "^0.21.7", 28 | "@types/react": "^16.9.16", 29 | "axios": "^0.19.0", 30 | "normalize.css": "^8.0.1", 31 | "react": "^16.11.0", 32 | "react-dom": "^16.11.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /mailbag/client/src/code/components/ContactList.tsx: -------------------------------------------------------------------------------- 1 | // React imports. 2 | import React from "react"; 3 | 4 | // Material-UI imports. 5 | import List from "@material-ui/core/List"; 6 | import ListItem from "@material-ui/core/ListItem"; 7 | import ListItemAvatar from "@material-ui/core/ListItemAvatar"; 8 | import Avatar from "@material-ui/core/Avatar"; 9 | import Person from "@material-ui/icons/Person"; 10 | import ListItemText from "@material-ui/core/ListItemText"; 11 | 12 | 13 | /** 14 | * Contacts. 15 | */ 16 | const ContactList = ({ state }) => ( 17 | 18 | 19 | 20 | {state.contacts.map(value => { 21 | return ( 22 | state.showContact(value._id, value.name, value.email) }> 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | })} 32 | 33 | 34 | 35 | ); /* End Contacts. */ 36 | 37 | 38 | export default ContactList; 39 | -------------------------------------------------------------------------------- /ch04/all_together_example/main.jsx: -------------------------------------------------------------------------------- 1 | function start() { 2 | 3 | class Bookmark extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | console.log("Bookmark component created"); 7 | } 8 | title = this.props.title; 9 | titleStyle = { color : "red" } 10 | render() { 11 | return ( 12 |
  • 13 |

    {this.title}

    14 | {this.props.description} 15 | 21 |
  • 22 | ); 23 | } 24 | } 25 | 26 | ReactDOM.render( 27 |
    28 |

    Bookmarks

    29 |
      30 | 33 | 36 |
    37 |
    , 38 | document.getElementById("mainContainer") 39 | ); 40 | 41 | } 42 | -------------------------------------------------------------------------------- /battlejong/client/src/code/components/BaseLayout.tsx: -------------------------------------------------------------------------------- 1 | // React imports. 2 | import React, { Component } from "react"; 3 | 4 | // Library imports. 5 | 6 | // App imports. 7 | import ControlArea from "./ControlArea"; 8 | import PlayerBoard from "./PlayerBoard"; 9 | import { createState } from "../state"; 10 | 11 | 12 | /** 13 | * The parent component of the entire app. 14 | */ 15 | class BaseLayout extends Component { 16 | 17 | 18 | /** 19 | * State data for the app. This also includes all mutator functions for manipulating state. That way, we only 20 | * ever have to pass this entire object down through props (not necessarily the best design in terms of data 21 | * encapsulation, but it does have the benefit of being quite a bit simpler). 22 | */ 23 | state = createState(this); 24 | 25 | 26 | /** 27 | * Render(). 28 | */ 29 | render() { 30 | 31 | return ( 32 | 33 |
    34 |
    35 |
    36 |
    37 | 38 | ); 39 | 40 | } /* End render(). */ 41 | 42 | 43 | } /* End class. */ 44 | 45 | 46 | export default BaseLayout; 47 | -------------------------------------------------------------------------------- /mailbag/client/src/code/components/MessageList.tsx: -------------------------------------------------------------------------------- 1 | // React imports. 2 | import React from "react"; 3 | 4 | 5 | import Table from "@material-ui/core/Table"; 6 | import TableBody from "@material-ui/core/TableBody"; 7 | import TableCell from "@material-ui/core/TableCell"; 8 | import TableHead from "@material-ui/core/TableHead"; 9 | import TableRow from "@material-ui/core/TableRow"; 10 | 11 | /** 12 | * MessageList. 13 | */ 14 | const MessageList = ({ state }) => ( 15 | 16 | 17 | 18 | 19 | Date 20 | From 21 | Subject 22 | 23 | 24 | 25 | { state.messages.map(message => ( 26 | state.showMessage(message) }> 27 | { new Date(message.date).toLocaleDateString() } 28 | { message.from } 29 | { message.subject } 30 | 31 | ) ) } 32 | 33 |
    34 | 35 | ); /* Mailboxes. */ 36 | 37 | 38 | export default MessageList; 39 | -------------------------------------------------------------------------------- /ch03/listing_3-02.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Intro To React 6 | 7 | 8 | 29 | 30 | 31 |
    32 | 33 | 34 | -------------------------------------------------------------------------------- /mailbag/client/src/code/main.tsx: -------------------------------------------------------------------------------- 1 | // Style imports. 2 | import "normalize.css"; 3 | import "../css/main.css"; 4 | 5 | // React imports. 6 | import React from "react"; 7 | import ReactDOM from "react-dom"; 8 | 9 | // App imports. 10 | import BaseLayout from "./components/BaseLayout"; 11 | import * as IMAP from "./IMAP"; 12 | import * as Contacts from "./Contacts"; 13 | 14 | 15 | // Render the UI. 16 | const baseComponent = ReactDOM.render(, document.body); 17 | 18 | 19 | // Now go fetch the user's mailboxes, and then their contacts. 20 | baseComponent.state.showHidePleaseWait(true); 21 | async function getMailboxes() { 22 | const imapWorker: IMAP.Worker = new IMAP.Worker(); 23 | const mailboxes: IMAP.IMailbox[] = await imapWorker.listMailboxes(); 24 | mailboxes.forEach((inMailbox) => { 25 | baseComponent.state.addMailboxToList(inMailbox); 26 | }); 27 | } 28 | getMailboxes().then(function() { 29 | // Now go fetch the user's contacts. 30 | async function getContacts() { 31 | const contactsWorker: Contacts.Worker = new Contacts.Worker(); 32 | const contacts: Contacts.IContact[] = await contactsWorker.listContacts(); 33 | contacts.forEach((inContact) => { 34 | baseComponent.state.addContactToList(inContact); 35 | }); 36 | } 37 | getContacts().then(() => baseComponent.state.showHidePleaseWait(false)); 38 | }); 39 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Freeware License, some rights reserved 2 | 3 | Copyright (c) 2020 Frank Zammetti 4 | 5 | Permission is hereby granted, free of charge, to anyone obtaining a copy 6 | of this software and associated documentation files (the "Software"), 7 | to work with the Software within the limits of freeware distribution and fair use. 8 | This includes the rights to use, copy, and modify the Software for personal use. 9 | Users are also allowed and encouraged to submit corrections and modifications 10 | to the Software for the benefit of other users. 11 | 12 | It is not allowed to reuse, modify, or redistribute the Software for 13 | commercial use in any way, or for a user’s educational materials such as books 14 | or blog articles without prior permission from the copyright holder. 15 | 16 | The above copyright notice and this permission notice need to be included 17 | in all copies or substantial portions of the software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS OR APRESS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | SOFTWARE. 26 | 27 | 28 | -------------------------------------------------------------------------------- /battlejong/client/src/code/components/ControlArea.tsx: -------------------------------------------------------------------------------- 1 | // React imports. 2 | import React from "react"; 3 | 4 | 5 | const ControlArea = ({ state }: any) => ( 6 | 7 | 8 |
    Your score:
    {state.scores.player}
    9 |
    Opponent score:
    {state.scores.opponent}
    10 |
    11 |
    12 |
    13 | { state.gameState === "awaitingOpponent" && 14 |
    Waiting for opponent to join
    15 | } 16 | { state.gameState === "deadEnd" && 17 |
    18 | You have no moves left.

    Waiting for opponent to finish. 19 |
    20 | } 21 | { state.gameState === "cleared" && 22 |
    23 | Congratulations!

    You've cleared the board!

    Waiting for opponent to finish. 24 |
    25 | } 26 | { state.gameState === "gameOver" && 27 |
    28 | The game is over.

    29 | { state.gameOutcome } 30 |
    31 | } 32 | 33 |
    34 | 35 | ); /* End ControlArea. */ 36 | 37 | 38 | export default ControlArea; 39 | -------------------------------------------------------------------------------- /battlejong/client/src/css/main.css: -------------------------------------------------------------------------------- 1 | .appContainer { 2 | position : absolute; 3 | height : 750px; 4 | /* Align upper-left corner to center on page. */ 5 | left : 50%; 6 | top : 50%; 7 | /* Now transform it to shift it up and left by 50% it's dimensions, thereby centering it. */ 8 | transform : translate(-50%, -50%); 9 | /* Border. */ 10 | border : 2px solid #0000ff; 11 | /* Round corners. */ 12 | border-radius : 10px 10px 10px 10px; 13 | -moz-border-radius : 10px 10px 10px 10px; 14 | -webkit-border-radius : 10px 10px 10px 10px; 15 | /* Define grid layout. */ 16 | display : grid; 17 | grid-template-columns : 960px 300px; 18 | /* Shadow. */ 19 | -webkit-box-shadow : 0 0 25px 0 rgba(255, 200, 0, 0.75); 20 | -moz-box-shadow : 0 0 25px 0 rgba(255, 200, 0, 0.75); 21 | box-shadow : 0 0 25px 0 rgba(255, 200, 0, 0.75); 22 | } 23 | 24 | /* The player's board area. */ 25 | .playerBoard { 26 | grid-column : 1 / 2; 27 | } 28 | 29 | /* Control area. */ 30 | .controlArea { 31 | height : 240px; 32 | margin : 10px; 33 | padding : 10px; 34 | grid-column : 2 / 3; 35 | border : 1px solid #000000; 36 | /* Round corners. */ 37 | border-radius : 10px 10px 10px 10px; 38 | -moz-border-radius : 10px 10px 10px 10px; 39 | -webkit-border-radius : 10px 10px 10px 10px; 40 | } 41 | 42 | .highlightTile { 43 | -webkit-filter : drop-shadow(0px 0px 50px #ff0000) contrast(150%) saturate(200%); 44 | filter : drop-shadow(0px 0px 10px #ff0000) contrast(150%) saturate(200%); 45 | } 46 | -------------------------------------------------------------------------------- /ch03/listing_3-03.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Intro To React 6 | 7 | 8 | 40 | 41 | 42 |
    43 | 44 | 45 | -------------------------------------------------------------------------------- /mailbag/client/src/css/main.css: -------------------------------------------------------------------------------- 1 | /* Container that the entire UI is in. */ 2 | .appContainer { 3 | display : grid; 4 | grid-template-columns : 150px 1fr 240px; 5 | grid-template-rows : 50px 1fr; 6 | width : 100vw; 7 | height : 100vh; 8 | } 9 | 10 | /* The toolbar up top. */ 11 | .toolbar { 12 | grid-column : 1 / 4; 13 | grid-row : 1 / 1; 14 | border-bottom : 1px solid #e0e0e0; 15 | padding-top : 8px; 16 | padding-left : 4px; 17 | } 18 | 19 | /* List of mailboxes on the left. */ 20 | .mailboxList { 21 | grid-column : 1 / 1; 22 | grid-row : 2 / 2; 23 | border-right : 1px solid #e0e0e0; 24 | padding-top : 6px; 25 | padding-left : 4px; 26 | overflow : auto; 27 | } 28 | 29 | /* The center area (includes message list and message/contact views). */ 30 | .centerArea { 31 | display : grid; 32 | grid-template-rows : 200px 1fr; 33 | } 34 | 35 | /* The message list at the top of the center area. */ 36 | .messageList { 37 | border-bottom : 1px solid #e0e0e0; 38 | grid-row : 1 / 1; 39 | overflow : auto; 40 | } 41 | 42 | /* The container for the message and contact views below the message list. */ 43 | .centerViews { 44 | grid-row : 2 / 2; 45 | padding-top : 4px; 46 | padding-left : 4px; 47 | padding-right : 4px; 48 | overflow : auto; 49 | } 50 | 51 | /* The contact list on the right. */ 52 | .contactList { 53 | grid-column : 3 / 3; 54 | grid-row : 2 / 2; 55 | border-left : 1px solid #e0e0e0; 56 | padding-top : 4px; 57 | padding-left : 4px; 58 | overflow : auto; 59 | } 60 | 61 | /* Style to override default gray disabled color of the message ID and date fields in the message view. */ 62 | .messageInfoField { 63 | color : #000000!important; 64 | } 65 | -------------------------------------------------------------------------------- /ch07/fourth_example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | /* 5 | * SplitChunksPlugin is enabled by default and replaced 6 | * deprecated CommonsChunkPlugin. It automatically identifies modules which 7 | * should be splitted of chunk by heuristics using module duplication count and 8 | * module category (i. e. node_modules). And splits the chunks… 9 | * 10 | * It is safe to remove "splitChunks" from the generated configuration 11 | * and was added as an educational example. 12 | * 13 | * https://webpack.js.org/plugins/split-chunks-plugin/ 14 | * 15 | */ 16 | 17 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 18 | 19 | /* 20 | * We've enabled HtmlWebpackPlugin for you! This generates a html 21 | * page for you when you compile webpack, which will make you start 22 | * developing and prototyping faster. 23 | * 24 | * https://github.com/jantimon/html-webpack-plugin 25 | * 26 | */ 27 | 28 | module.exports = { 29 | mode: 'development', 30 | entry: './src/index.ts', 31 | 32 | output: { 33 | filename: '[name].[chunkhash].js', 34 | path: path.resolve(__dirname, 'dist') 35 | }, 36 | 37 | plugins: [new webpack.ProgressPlugin(), new HtmlWebpackPlugin()], 38 | 39 | module: { 40 | rules: [ 41 | { 42 | test: /\.ts?$/, 43 | use: 'ts-loader', 44 | exclude: /node_modules/, 45 | } 46 | ] 47 | }, 48 | 49 | optimization: { 50 | splitChunks: { 51 | cacheGroups: { 52 | vendors: { 53 | priority: -10, 54 | test: /[\\/]node_modules[\\/]/ 55 | } 56 | }, 57 | 58 | chunks: 'async', 59 | minChunks: 1, 60 | minSize: 30000, 61 | name: true 62 | } 63 | }, 64 | 65 | devServer: { 66 | open: true 67 | } 68 | }; 69 | -------------------------------------------------------------------------------- /mailbag/client/src/code/Contacts.ts: -------------------------------------------------------------------------------- 1 | // Library imports. 2 | import axios, { AxiosResponse } from "axios"; 3 | 4 | // App imports. 5 | import { config } from "./config"; 6 | 7 | // Define interface to describe a contact. Note that we'll only have an _id field when retrieving or adding, so 8 | // it has to be optional. 9 | export interface IContact { _id?: number, name: string, email: string } 10 | 11 | 12 | // The worker that will perform contact operations. 13 | export class Worker { 14 | 15 | 16 | /** 17 | * Returns a list of all contacts from the server. 18 | * 19 | * @return An array of objects, on per contact. 20 | */ 21 | public async listContacts(): Promise { 22 | 23 | console.log("Contacts.Worker.listContacts()"); 24 | 25 | const response: AxiosResponse = await axios.get(`${config.serverAddress}/contacts`); 26 | return response.data; 27 | 28 | } /* End listContacts(). */ 29 | 30 | 31 | /** 32 | * Add a contact to the server. 33 | * 34 | * @oaram inContact The contact to add. 35 | * @return The inContact object, but now with a _id field added. 36 | */ 37 | public async addContact(inContact: IContact): Promise { 38 | 39 | console.log("Contacts.Worker.addContact()", inContact); 40 | 41 | const response: AxiosResponse = await axios.post(`${config.serverAddress}/contacts`, inContact); 42 | return response.data; 43 | 44 | } /* End addContact(). */ 45 | 46 | 47 | /** 48 | * Delete a contact from the server. 49 | * 50 | * @oaram inID The ID (_id) of the contact to add. 51 | */ 52 | public async deleteContact(inID): Promise { 53 | 54 | console.log("Contacts.Worker.deleteContact()", inID); 55 | 56 | await axios.delete(`${config.serverAddress}/contacts/${inID}`); 57 | 58 | } /* End deleteContact(). */ 59 | 60 | 61 | } /* End class. */ 62 | -------------------------------------------------------------------------------- /mailbag/server/src/SMTP.ts: -------------------------------------------------------------------------------- 1 | // Library imports. 2 | import Mail from "nodemailer/lib/mailer"; 3 | import * as nodemailer from "nodemailer"; 4 | import { SendMailOptions, SentMessageInfo } from "nodemailer"; 5 | 6 | // App imports. 7 | import { IServerInfo } from "./ServerInfo"; 8 | 9 | 10 | // The worker that will perform SMTP operations. 11 | export class Worker { 12 | 13 | 14 | // Server information. 15 | private static serverInfo: IServerInfo; 16 | 17 | 18 | /** 19 | * Constructor. 20 | */ 21 | constructor(inServerInfo: IServerInfo) { 22 | 23 | console.log("SMTP.Worker.constructor", inServerInfo); 24 | Worker.serverInfo = inServerInfo; 25 | 26 | } /* End constructor. */ 27 | 28 | 29 | /** 30 | * Send a message. 31 | * 32 | * @param inOptions An object containing to, from, subject and text properties (matches the IContact interface, 33 | * but can't be used since the type comes from nodemailer, not app code). 34 | * @return A Promise that eventually resolves to a string (null for success, error message for an error). 35 | */ 36 | public sendMessage(inOptions: SendMailOptions): Promise { 37 | 38 | console.log("SMTP.Worker.sendMessage()", inOptions); 39 | 40 | return new Promise((inResolve, inReject) => { 41 | const transport: Mail = nodemailer.createTransport(Worker.serverInfo.smtp); 42 | transport.sendMail( 43 | inOptions, 44 | (inError: Error | null, inInfo: SentMessageInfo) => { 45 | if (inError) { 46 | console.log("SMTP.Worker.sendMessage(): Error", inError); 47 | inReject(inError); 48 | } else { 49 | console.log("SMTP.Worker.sendMessage(): Ok", inInfo); 50 | inResolve(); 51 | } 52 | } 53 | ); 54 | }); 55 | 56 | } /* End sendMessage(). */ 57 | 58 | 59 | } /* End class. */ 60 | -------------------------------------------------------------------------------- /mailbag/client/src/code/components/ContactView.tsx: -------------------------------------------------------------------------------- 1 | // React imports. 2 | import React from "react"; 3 | 4 | // Material-UI imports. 5 | import Button from "@material-ui/core/Button"; 6 | import { TextField } from "@material-ui/core"; 7 | 8 | 9 | /** 10 | * ContactView. 11 | */ 12 | const ContactView = ({ state }) => ( 13 | 14 |
    15 | 16 | 19 |
    20 | 23 |
    24 | { /* Hide.show buttons as appropriate. Note that we have to use this form of onClick() otherwise the event */ } 25 | { /* object would be passed to addContact() and the branching logic would fail. */ } 26 | { state.currentView === "contactAdd" && 27 | 31 | } 32 | { state.currentView === "contact" && 33 | 37 | } 38 | { state.currentView === "contact" && 39 | 41 | } 42 | 43 | 44 | 45 | ); /* ContactView. */ 46 | 47 | 48 | export default ContactView; 49 | -------------------------------------------------------------------------------- /battlejong/client/src/code/socketComm.ts: -------------------------------------------------------------------------------- 1 | // React imports. 2 | import React from "react"; 3 | 4 | 5 | export function createSocketComm(inParentComponent: React.Component) { 6 | 7 | 8 | // Make initial contact with the server. 9 | const connection: WebSocket = new WebSocket("ws://localhost:8080"); 10 | 11 | 12 | // Handle initial connection. 13 | connection.onopen = () => { 14 | console.log("Connection opened to server"); 15 | }; 16 | 17 | 18 | // Handle errors. 19 | connection.onerror = error => { 20 | console.log(`WebSocket error: ${error}`) 21 | }; 22 | 23 | 24 | // Handle messages. Note: objects received. 25 | connection.onmessage = function(inMessage: any) { 26 | 27 | console.log(`WS received: ${inMessage.data}`); 28 | 29 | const msgParts: string[] = inMessage.data.split("_"); 30 | const message: string = msgParts[0]; 31 | 32 | switch (message) { 33 | 34 | case "connected": // connected_ 35 | this.state.handleMessage_connected(msgParts[1]); 36 | break; 37 | 38 | case "start": // start_ 39 | this.state.handleMessage_start(JSON.parse(msgParts[1])); 40 | break; 41 | 42 | case "update": // update__ 43 | this.state.handleMessage_update(msgParts[1], parseInt(msgParts[2])); 44 | break; 45 | 46 | case "gameOver": // gameOver_ 47 | this.state.handleMessage_gameOver(msgParts[1]); 48 | break; 49 | 50 | } 51 | 52 | }.bind(inParentComponent); /* End message handler. */ 53 | 54 | 55 | // Send an object to the server. Note: send is always as a string. 56 | this.send = function(inMessage: string) { 57 | 58 | console.log(`WS sending: ${inMessage}`); 59 | connection.send(inMessage); 60 | 61 | }; /* End send(). */ 62 | 63 | 64 | // Return a reference to this function to the caller so the send() method can be used. 65 | return this; 66 | 67 | 68 | } /* End createSocketComm(). */ 69 | -------------------------------------------------------------------------------- /ch07/second example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | /* 5 | * SplitChunksPlugin is enabled by default and replaced 6 | * deprecated CommonsChunkPlugin. It automatically identifies modules which 7 | * should be splitted of chunk by heuristics using module duplication count and 8 | * module category (i. e. node_modules). And splits the chunks… 9 | * 10 | * It is safe to remove "splitChunks" from the generated configuration 11 | * and was added as an educational example. 12 | * 13 | * https://webpack.js.org/plugins/split-chunks-plugin/ 14 | * 15 | */ 16 | 17 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 18 | 19 | /* 20 | * We've enabled HtmlWebpackPlugin for you! This generates a html 21 | * page for you when you compile webpack, which will make you start 22 | * developing and prototyping faster. 23 | * 24 | * https://github.com/jantimon/html-webpack-plugin 25 | * 26 | */ 27 | 28 | module.exports = { 29 | mode: 'development', 30 | entry: './src/index.js', 31 | 32 | output: { 33 | filename: '[name].[chunkhash].js', 34 | path: path.resolve(__dirname, 'dist') 35 | }, 36 | 37 | plugins: [new webpack.ProgressPlugin(), new HtmlWebpackPlugin()], 38 | 39 | module: { 40 | rules: [ 41 | { 42 | test: /.(js|jsx)$/, 43 | include: [path.resolve(__dirname, 'src')], 44 | loader: 'babel-loader', 45 | 46 | options: { 47 | plugins: ['syntax-dynamic-import'], 48 | 49 | presets: [ 50 | [ 51 | '@babel/preset-env', 52 | { 53 | modules: false 54 | } 55 | ] 56 | ] 57 | } 58 | } 59 | ] 60 | }, 61 | 62 | optimization: { 63 | splitChunks: { 64 | cacheGroups: { 65 | vendors: { 66 | priority: -10, 67 | test: /[\\/]node_modules[\\/]/ 68 | } 69 | }, 70 | 71 | chunks: 'async', 72 | minChunks: 1, 73 | minSize: 30000, 74 | name: true 75 | } 76 | }, 77 | 78 | devServer: { 79 | open: true 80 | } 81 | }; 82 | -------------------------------------------------------------------------------- /ch07/third_example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | /* 5 | * SplitChunksPlugin is enabled by default and replaced 6 | * deprecated CommonsChunkPlugin. It automatically identifies modules which 7 | * should be splitted of chunk by heuristics using module duplication count and 8 | * module category (i. e. node_modules). And splits the chunks… 9 | * 10 | * It is safe to remove "splitChunks" from the generated configuration 11 | * and was added as an educational example. 12 | * 13 | * https://webpack.js.org/plugins/split-chunks-plugin/ 14 | * 15 | */ 16 | 17 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 18 | 19 | /* 20 | * We've enabled HtmlWebpackPlugin for you! This generates a html 21 | * page for you when you compile webpack, which will make you start 22 | * developing and prototyping faster. 23 | * 24 | * https://github.com/jantimon/html-webpack-plugin 25 | * 26 | */ 27 | 28 | module.exports = { 29 | mode: 'development', 30 | entry: './src/index.js', 31 | 32 | output: { 33 | filename: '[name].[chunkhash].js', 34 | path: path.resolve(__dirname, 'dist') 35 | }, 36 | 37 | plugins: [new webpack.ProgressPlugin(), new HtmlWebpackPlugin()], 38 | 39 | module: { 40 | rules: [ 41 | { 42 | test: /.(js|jsx)$/, 43 | include: [path.resolve(__dirname, 'src')], 44 | loader: 'babel-loader', 45 | 46 | options: { 47 | plugins: ['syntax-dynamic-import'], 48 | 49 | presets: [ 50 | [ 51 | '@babel/preset-env', 52 | { 53 | modules: false 54 | } 55 | ] 56 | ] 57 | } 58 | } 59 | ] 60 | }, 61 | 62 | optimization: { 63 | splitChunks: { 64 | cacheGroups: { 65 | vendors: { 66 | priority: -10, 67 | test: /[\\/]node_modules[\\/]/ 68 | } 69 | }, 70 | 71 | chunks: 'async', 72 | minChunks: 1, 73 | minSize: 30000, 74 | name: true 75 | } 76 | }, 77 | 78 | devServer: { 79 | open: true 80 | } 81 | }; 82 | -------------------------------------------------------------------------------- /ch03/listing_3-04.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Intro To React 6 | 7 | 8 | 54 | 55 | 56 |
    57 | 58 | 59 | -------------------------------------------------------------------------------- /ch03/listing_3-05.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Intro To React 6 | 7 | 8 | 52 | 53 | 54 |
    55 | 56 | 57 | -------------------------------------------------------------------------------- /mailbag/client/src/code/components/BaseLayout.tsx: -------------------------------------------------------------------------------- 1 | // React imports. 2 | import React, { Component } from "react"; 3 | 4 | // Library imports. 5 | import Dialog from "@material-ui/core/Dialog"; 6 | import DialogContent from "@material-ui/core/DialogContent"; 7 | import DialogContentText from "@material-ui/core/DialogContentText"; 8 | import DialogTitle from "@material-ui/core/DialogTitle"; 9 | 10 | // App imports. 11 | import Toolbar from "./Toolbar"; 12 | import MailboxList from "./MailboxList"; 13 | import MessageList from "./MessageList"; 14 | import ContactList from "./ContactList"; 15 | import WelcomeView from "./WelcomeView"; 16 | import ContactView from "./ContactView"; 17 | import MessageView from "./MessageView"; 18 | import { createState } from "../state"; 19 | 20 | 21 | /** 22 | * BaseLayout. 23 | */ 24 | class BaseLayout extends Component { 25 | 26 | 27 | /** 28 | * State data for the app. This also includes all mutator functions for manipulating state. That way, we only 29 | * ever have to pass this entire object down through props (not necessarily the best design in terms of data 30 | * encapsulation, but it does have the benefit of being quite a bit simpler). 31 | */ 32 | state = createState(this); 33 | 34 | 35 | /** 36 | * Render(). 37 | */ 38 | render() { 39 | 40 | return ( 41 | 42 |
    43 | 44 | 46 | Please Wait 47 | ...Contacting server... 48 | 49 | 50 |
    51 | 52 |
    53 | 54 |
    55 |
    56 |
    57 | { this.state.currentView === "welcome" && } 58 | { (this.state.currentView === "message" || this.state.currentView === "compose") && 59 | 60 | } 61 | { (this.state.currentView === "contact" || this.state.currentView === "contactAdd") && 62 | 63 | } 64 |
    65 |
    66 | 67 |
    68 | 69 |
    70 | ); 71 | 72 | } /* End render(). */ 73 | 74 | 75 | } /* End class. */ 76 | 77 | 78 | export default BaseLayout; 79 | -------------------------------------------------------------------------------- /mailbag/client/src/code/IMAP.ts: -------------------------------------------------------------------------------- 1 | // Library imports. 2 | import axios, { AxiosResponse } from "axios"; 3 | 4 | // App imports. 5 | import { config } from "./config"; 6 | 7 | 8 | // Define interface to describe a mailbox. 9 | export interface IMailbox { name: string, path: string } 10 | 11 | 12 | // Define interface to describe a received message. Note that body is optional since it isn't sent when listing 13 | // messages. 14 | export interface IMessage { 15 | id: string, 16 | date: string, 17 | from: string, 18 | subject: string, 19 | body?: string 20 | } 21 | 22 | 23 | // The worker that will perform IMAP operations. 24 | export class Worker { 25 | 26 | 27 | /** 28 | * Returns a list of all (top-level) mailboxes. 29 | * 30 | * @return An array of objects, on per mailbox, that describes the mailbox. 31 | */ 32 | public async listMailboxes(): Promise { 33 | 34 | console.log("IMAP.Worker.listMailboxes()"); 35 | 36 | const response: AxiosResponse = await axios.get(`${config.serverAddress}/mailboxes`); 37 | return response.data; 38 | 39 | } /* End listMailboxes(). */ 40 | 41 | 42 | /** 43 | * Returns a list of messages in a named mailbox 44 | * 45 | * @param inMailbox The name of the mailbox. 46 | * @return An array of objects, on per message. 47 | */ 48 | public async listMessages(inMailbox: string): Promise { 49 | 50 | console.log("IMAP.Worker.listMessages()"); 51 | 52 | const response: AxiosResponse = await axios.get(`${config.serverAddress}/mailboxes/${inMailbox}`); 53 | return response.data; 54 | 55 | } /* End listMessages(). */ 56 | 57 | 58 | /** 59 | * Returns the body of a specified message. 60 | * 61 | * @param inID The ID of the message to get the body of. 62 | * @param inMailbox The path of the mailbox the message is in. 63 | * @return The body of the message. 64 | */ 65 | public async getMessageBody(inID: string, inMailbox: String): Promise { 66 | 67 | console.log("IMAP.Worker.getMessageBody()", inID); 68 | 69 | const response: AxiosResponse = await axios.get(`${config.serverAddress}/messages/${inMailbox}/${inID}`); 70 | return response.data; 71 | 72 | } /* End getMessageBody(). */ 73 | 74 | 75 | /** 76 | * Returns the body of a specified message. 77 | * 78 | * @param inID The ID of the message to delete. 79 | * @param inMailbox The path of the mailbox the message is in. 80 | */ 81 | public async deleteMessage(inID: string, inMailbox: String): Promise { 82 | 83 | console.log("IMAP.Worker.getMessageBody()", inID); 84 | 85 | await axios.delete(`${config.serverAddress}/messages/${inMailbox}/${inID}`); 86 | 87 | } /* End deleteMessage(). */ 88 | 89 | 90 | } /* End class. */ 91 | -------------------------------------------------------------------------------- /ch07/third_example/.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "etc": { 3 | "configuration": { 4 | "config": { 5 | "configName": "config", 6 | "topScope": [ 7 | "const path = require('path');", 8 | "const webpack = require('webpack');", 9 | "\n", 10 | "/*\n * SplitChunksPlugin is enabled by default and replaced\n * deprecated CommonsChunkPlugin. It automatically identifies modules which\n * should be splitted of chunk by heuristics using module duplication count and\n * module category (i. e. node_modules). And splits the chunks…\n *\n * It is safe to remove \"splitChunks\" from the generated configuration\n * and was added as an educational example.\n *\n * https://webpack.js.org/plugins/split-chunks-plugin/\n *\n */", 11 | "const HtmlWebpackPlugin = require('html-webpack-plugin')", 12 | "\n", 13 | "/*\n * We've enabled HtmlWebpackPlugin for you! This generates a html\n * page for you when you compile webpack, which will make you start\n * developing and prototyping faster.\n * \n * https://github.com/jantimon/html-webpack-plugin\n * \n */" 14 | ], 15 | "webpackOptions": { 16 | "mode": "'development'", 17 | "entry": "'./src/index.js'", 18 | "output": { 19 | "filename": "'[name].[chunkhash].js'", 20 | "path": "path.resolve(__dirname, 'dist')" 21 | }, 22 | "plugins": [ 23 | "new webpack.ProgressPlugin()", 24 | "new HtmlWebpackPlugin()" 25 | ], 26 | "module": { 27 | "rules": [ 28 | { 29 | "test": "/.(js|jsx)$/", 30 | "include": [ 31 | "path.resolve(__dirname, 'src')" 32 | ], 33 | "loader": "'babel-loader'", 34 | "options": { 35 | "plugins": [ 36 | "'syntax-dynamic-import'" 37 | ], 38 | "presets": [ 39 | [ 40 | "'@babel/preset-env'", 41 | { 42 | "'modules'": false 43 | } 44 | ] 45 | ] 46 | } 47 | } 48 | ] 49 | }, 50 | "optimization": { 51 | "splitChunks": { 52 | "cacheGroups": { 53 | "vendors": { 54 | "priority": -10, 55 | "test": "/[\\\\/]node_modules[\\\\/]/" 56 | } 57 | }, 58 | "chunks": "'async'", 59 | "minChunks": 1, 60 | "minSize": 30000, 61 | "name": true 62 | } 63 | }, 64 | "devServer": { 65 | "open": true 66 | } 67 | } 68 | } 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /ch07/fourth_example/.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "etc": { 3 | "configuration": { 4 | "config": { 5 | "configName": "config", 6 | "topScope": [ 7 | "const path = require('path');", 8 | "const webpack = require('webpack');", 9 | "\n", 10 | "/*\n * SplitChunksPlugin is enabled by default and replaced\n * deprecated CommonsChunkPlugin. It automatically identifies modules which\n * should be splitted of chunk by heuristics using module duplication count and\n * module category (i. e. node_modules). And splits the chunks…\n *\n * It is safe to remove \"splitChunks\" from the generated configuration\n * and was added as an educational example.\n *\n * https://webpack.js.org/plugins/split-chunks-plugin/\n *\n */", 11 | "const HtmlWebpackPlugin = require('html-webpack-plugin')", 12 | "\n", 13 | "/*\n * We've enabled HtmlWebpackPlugin for you! This generates a html\n * page for you when you compile webpack, which will make you start\n * developing and prototyping faster.\n * \n * https://github.com/jantimon/html-webpack-plugin\n * \n */" 14 | ], 15 | "webpackOptions": { 16 | "mode": "'development'", 17 | "entry": "'./src/index.js'", 18 | "output": { 19 | "filename": "'[name].[chunkhash].js'", 20 | "path": "path.resolve(__dirname, 'dist')" 21 | }, 22 | "plugins": [ 23 | "new webpack.ProgressPlugin()", 24 | "new HtmlWebpackPlugin()" 25 | ], 26 | "module": { 27 | "rules": [ 28 | { 29 | "test": "/.(js|jsx)$/", 30 | "include": [ 31 | "path.resolve(__dirname, 'src')" 32 | ], 33 | "loader": "'babel-loader'", 34 | "options": { 35 | "plugins": [ 36 | "'syntax-dynamic-import'" 37 | ], 38 | "presets": [ 39 | [ 40 | "'@babel/preset-env'", 41 | { 42 | "'modules'": false 43 | } 44 | ] 45 | ] 46 | } 47 | } 48 | ] 49 | }, 50 | "optimization": { 51 | "splitChunks": { 52 | "cacheGroups": { 53 | "vendors": { 54 | "priority": -10, 55 | "test": "/[\\\\/]node_modules[\\\\/]/" 56 | } 57 | }, 58 | "chunks": "'async'", 59 | "minChunks": 1, 60 | "minSize": 30000, 61 | "name": true 62 | } 63 | }, 64 | "devServer": { 65 | "open": true 66 | } 67 | } 68 | } 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /ch07/second example/.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "etc": { 3 | "configuration": { 4 | "config": { 5 | "configName": "config", 6 | "topScope": [ 7 | "const path = require('path');", 8 | "const webpack = require('webpack');", 9 | "\n", 10 | "/*\n * SplitChunksPlugin is enabled by default and replaced\n * deprecated CommonsChunkPlugin. It automatically identifies modules which\n * should be splitted of chunk by heuristics using module duplication count and\n * module category (i. e. node_modules). And splits the chunks…\n *\n * It is safe to remove \"splitChunks\" from the generated configuration\n * and was added as an educational example.\n *\n * https://webpack.js.org/plugins/split-chunks-plugin/\n *\n */", 11 | "const HtmlWebpackPlugin = require('html-webpack-plugin')", 12 | "\n", 13 | "/*\n * We've enabled HtmlWebpackPlugin for you! This generates a html\n * page for you when you compile webpack, which will make you start\n * developing and prototyping faster.\n * \n * https://github.com/jantimon/html-webpack-plugin\n * \n */" 14 | ], 15 | "webpackOptions": { 16 | "mode": "'development'", 17 | "entry": "'./src/index.js'", 18 | "output": { 19 | "filename": "'[name].[chunkhash].js'", 20 | "path": "path.resolve(__dirname, 'dist')" 21 | }, 22 | "plugins": [ 23 | "new webpack.ProgressPlugin()", 24 | "new HtmlWebpackPlugin()" 25 | ], 26 | "module": { 27 | "rules": [ 28 | { 29 | "test": "/.(js|jsx)$/", 30 | "include": [ 31 | "path.resolve(__dirname, 'src')" 32 | ], 33 | "loader": "'babel-loader'", 34 | "options": { 35 | "plugins": [ 36 | "'syntax-dynamic-import'" 37 | ], 38 | "presets": [ 39 | [ 40 | "'@babel/preset-env'", 41 | { 42 | "'modules'": false 43 | } 44 | ] 45 | ] 46 | } 47 | } 48 | ] 49 | }, 50 | "optimization": { 51 | "splitChunks": { 52 | "cacheGroups": { 53 | "vendors": { 54 | "priority": -10, 55 | "test": "/[\\\\/]node_modules[\\\\/]/" 56 | } 57 | }, 58 | "chunks": "'async'", 59 | "minChunks": 1, 60 | "minSize": 30000, 61 | "name": true 62 | } 63 | }, 64 | "devServer": { 65 | "open": true 66 | } 67 | } 68 | } 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /mailbag/client/src/code/components/MessageView.tsx: -------------------------------------------------------------------------------- 1 | // React imports. 2 | import React from "react"; 3 | import { InputBase } from "@material-ui/core"; 4 | import { TextField } from "@material-ui/core"; 5 | import Button from "@material-ui/core/Button"; 6 | 7 | 8 | /** 9 | * MessageView. 10 | */ 11 | const MessageView = ({ state }) => ( 12 | 13 |
    14 | 15 | { /* ----- Message ID and date, just for informational purposes. ----- */ } 16 | { state.currentView === "message" && 17 | 19 | } 20 | { state.currentView === "message" &&
    } 21 | { state.currentView === "message" && 22 | 24 | } 25 | { state.currentView === "message" &&
    } 26 | 27 | { /* ----- From. ----- */ } 28 | { state.currentView === "message" && 29 | 31 | } 32 | { state.currentView === "message" &&
    } 33 | 34 | { /* ----- To. ----- */ } 35 | { state.currentView === "compose" && 36 | 39 | } 40 | { state.currentView === "compose" &&
    } 41 | 42 | { /* ----- Subject. ----- */ } 43 | 46 |
    47 | 48 | { /* ----- Message body. ----- */ } 49 | 52 | 53 | { /* ----- Buttons. ----- */ } 54 | 55 | { state.currentView === "compose" && 56 | 60 | } 61 | { state.currentView === "message" && 62 | 66 | } 67 | { state.currentView === "message" && 68 | 72 | } 73 | 74 | 75 | 76 | ); /* MessageView. */ 77 | 78 | 79 | export default MessageView; 80 | -------------------------------------------------------------------------------- /mailbag/server/src/Contacts.ts: -------------------------------------------------------------------------------- 1 | // Node imports. 2 | import * as path from "path"; 3 | 4 | 5 | // Library imports. 6 | const Datastore = require("nedb"); 7 | 8 | 9 | // Define interface to describe a contact. Note that we'll only have an _id field when retrieving or adding, so 10 | // it has to be optional. 11 | export interface IContact { 12 | _id?: number, 13 | name: string, 14 | email: string 15 | } 16 | 17 | 18 | // The worker that will perform contact operations. 19 | export class Worker { 20 | 21 | 22 | // The Nedb Datastore instance for contacts. 23 | private db: Nedb; 24 | 25 | 26 | /** 27 | * Constructor. 28 | */ 29 | constructor() { 30 | 31 | this.db = new Datastore({ 32 | filename : path.join(__dirname, "contacts.db"), 33 | autoload : true 34 | }); 35 | 36 | } /* End constructor. */ 37 | 38 | 39 | /** 40 | * Lists all contacts. 41 | * 42 | * @return A promise that eventually resolves to an array of IContact objects. 43 | */ 44 | public listContacts(): Promise { 45 | 46 | console.log("Contacts.Worker.listContacts()"); 47 | 48 | return new Promise((inResolve, inReject) => { 49 | this.db.find( 50 | {}, 51 | (inError: Error, inDocs: IContact[]) => { 52 | if (inError) { 53 | console.log("Contacts.Worker.listContacts(): Error", inError); 54 | inReject(inError); 55 | } else { 56 | console.log("Contacts.Worker.listContacts(): Ok", inDocs); 57 | inResolve(inDocs); 58 | } 59 | } 60 | ); 61 | }); 62 | 63 | } /* End listContacts(). */ 64 | 65 | 66 | /** 67 | * Add a new contact. 68 | * 69 | * @param inContact The contact to add. 70 | * @return A promise that eventually resolves to an IContact object. 71 | */ 72 | public addContact(inContact: IContact): Promise { 73 | 74 | console.log("Contacts.Worker.addContact()", inContact); 75 | 76 | return new Promise((inResolve, inReject) => { 77 | this.db.insert( 78 | inContact, 79 | (inError: Error, inNewDoc: IContact) => { 80 | if (inError) { 81 | console.log("Contacts.Worker.addContact(): Error", inError); 82 | inReject(inError); 83 | } else { 84 | console.log("Contacts.Worker.addContact(): Ok", inNewDoc); 85 | inResolve(inNewDoc); 86 | } 87 | } 88 | ); 89 | }); 90 | 91 | } /* End addContact(). */ 92 | 93 | 94 | /** 95 | * Delete a contact. 96 | * 97 | * @param inID The ID of the contact to delete. 98 | * @return A promise that eventually resolves to a string (null for success, or the error message for an error). 99 | */ 100 | public deleteContact(inID: string): Promise { 101 | 102 | console.log("Contacts.Worker.deleteContact()", inID); 103 | 104 | return new Promise((inResolve, inReject) => { 105 | this.db.remove( 106 | { _id : inID }, 107 | { }, 108 | (inError: Error, inNumRemoved: number) => { 109 | if (inError) { 110 | console.log("Contacts.Worker.deleteContact(): Error", inError); 111 | inReject(inError); 112 | } else { 113 | console.log("Contacts.Worker.deleteContact(): Ok", inNumRemoved); 114 | inResolve(); 115 | } 116 | } 117 | ); 118 | }); 119 | 120 | } /* End deleteContact(). */ 121 | 122 | 123 | } /* End class. */ 124 | -------------------------------------------------------------------------------- /mailbag/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "./dist", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | }, 63 | "include": [ "src/**/*" ] 64 | } 65 | -------------------------------------------------------------------------------- /ch07/fourth_example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | // "outDir": "./", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | 63 | /* Advanced Options */ 64 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /battlejong/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "./dist", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | "strictNullChecks": true, /* Enable strict null checks. */ 29 | "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | 63 | /* Advanced Options */ 64 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 65 | }, 66 | "include": [ "src/**/*" ] 67 | } 68 | -------------------------------------------------------------------------------- /battlejong/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 7 | "lib": [ "es2015", "es2017", "dom" ], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": false, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "./dist", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | "strictNullChecks": true, /* Enable strict null checks. */ 29 | "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | "noImplicitThis": false, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | 63 | /* Advanced Options */ 64 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 65 | }, 66 | "paths" : { "components/*" : [ "src/components/*" ] } 67 | } 68 | -------------------------------------------------------------------------------- /mailbag/server/src/main.ts: -------------------------------------------------------------------------------- 1 | // Node imports. 2 | import path from "path"; 3 | 4 | // Library imports. 5 | import express, { Express, NextFunction, Request, Response } from "express"; 6 | 7 | // App imports. 8 | import { serverInfo } from "./ServerInfo"; 9 | import * as IMAP from "./IMAP"; 10 | import * as SMTP from "./SMTP"; 11 | import * as Contacts from "./Contacts"; 12 | import { IContact } from "./Contacts"; 13 | 14 | 15 | // Our Express app. 16 | const app: Express = express(); 17 | 18 | 19 | // Handle JSON in request bodies. 20 | app.use(express.json()); 21 | 22 | 23 | // Serve the client. 24 | app.use("/", express.static(path.join(__dirname, "../../client/dist"))); 25 | 26 | 27 | // Enable CORS so that we can call the API even from anywhere. 28 | app.use(function(inRequest: Request, inResponse: Response, inNext: NextFunction) { 29 | inResponse.header("Access-Control-Allow-Origin", "*"); 30 | inResponse.header("Access-Control-Allow-Methods", "GET,POST,DELETE,OPTIONS"); 31 | inResponse.header("Access-Control-Allow-Headers", "Origin,X-Requested-With,Content-Type,Accept"); 32 | inNext(); 33 | }); 34 | 35 | 36 | // ---------- RESTful endpoint operations begin. ---------- 37 | 38 | 39 | // Get list of mailboxes. 40 | app.get("/mailboxes", 41 | async (inRequest: Request, inResponse: Response) => { 42 | console.log("GET /mailboxes (1)"); 43 | try { 44 | const imapWorker: IMAP.Worker = new IMAP.Worker(serverInfo); 45 | const mailboxes: IMAP.IMailbox[] = await imapWorker.listMailboxes(); 46 | console.log("GET /mailboxes (1): Ok", mailboxes); 47 | inResponse.json(mailboxes); 48 | } catch (inError) { 49 | console.log("GET /mailboxes (1): Error", inError); 50 | inResponse.send("error"); 51 | } 52 | } 53 | ); 54 | 55 | 56 | // Get list of messages in a mailbox (does NOT include bodies). 57 | app.get("/mailboxes/:mailbox", 58 | async (inRequest: Request, inResponse: Response) => { 59 | console.log("GET /mailboxes (2)", inRequest.params.mailbox); 60 | try { 61 | const imapWorker: IMAP.Worker = new IMAP.Worker(serverInfo); 62 | const messages: IMAP.IMessage[] = await imapWorker.listMessages({ 63 | mailbox : inRequest.params.mailbox 64 | }); 65 | console.log("GET /mailboxes (2): Ok", messages); 66 | inResponse.json(messages); 67 | } catch (inError) { 68 | console.log("GET /mailboxes (2): Error", inError); 69 | inResponse.send("error"); 70 | } 71 | } 72 | ); 73 | 74 | 75 | // Get a message's plain text body. 76 | app.get("/messages/:mailbox/:id", 77 | async (inRequest: Request, inResponse: Response) => { 78 | console.log("GET /messages (3)", inRequest.params.mailbox, inRequest.params.id); 79 | try { 80 | const imapWorker: IMAP.Worker = new IMAP.Worker(serverInfo); 81 | const messageBody: string = await imapWorker.getMessageBody({ 82 | mailbox : inRequest.params.mailbox, 83 | id : parseInt(inRequest.params.id, 10) 84 | }); 85 | console.log("GET /messages (3): Ok", messageBody); 86 | inResponse.send(messageBody); 87 | } catch (inError) { 88 | console.log("GET /messages (3): Error", inError); 89 | inResponse.send("error"); 90 | } 91 | } 92 | ); 93 | 94 | 95 | // Delete a message. 96 | app.delete("/messages/:mailbox/:id", 97 | async (inRequest: Request, inResponse: Response) => { 98 | console.log("DELETE /messages"); 99 | try { 100 | const imapWorker: IMAP.Worker = new IMAP.Worker(serverInfo); 101 | await imapWorker.deleteMessage({ 102 | mailbox : inRequest.params.mailbox, 103 | id : parseInt(inRequest.params.id, 10) 104 | }); 105 | console.log("DELETE /messages: Ok"); 106 | inResponse.send("ok"); 107 | } catch (inError) { 108 | console.log("DELETE /messages: Error", inError); 109 | inResponse.send("error"); 110 | } 111 | } 112 | ); 113 | 114 | 115 | // Send a message. 116 | app.post("/messages", 117 | async (inRequest: Request, inResponse: Response) => { 118 | console.log("POST /messages", inRequest.body); 119 | try { 120 | const smtpWorker: SMTP.Worker = new SMTP.Worker(serverInfo); 121 | await smtpWorker.sendMessage(inRequest.body); 122 | console.log("POST /messages: Ok"); 123 | inResponse.send("ok"); 124 | } catch (inError) { 125 | console.log("POST /messages: Error", inError); 126 | inResponse.send("error"); 127 | } 128 | } 129 | ); 130 | 131 | 132 | // List contacts. 133 | app.get("/contacts", 134 | async (inRequest: Request, inResponse: Response) => { 135 | console.log("GET /contacts"); 136 | try { 137 | const contactsWorker: Contacts.Worker = new Contacts.Worker(); 138 | const contacts: IContact[] = await contactsWorker.listContacts(); 139 | console.log("GET /contacts: Ok", contacts); 140 | inResponse.json(contacts); 141 | } catch (inError) { 142 | console.log("GET /contacts: Error", inError); 143 | inResponse.send("error"); 144 | } 145 | } 146 | ); 147 | 148 | 149 | // Add a contact. 150 | app.post("/contacts", 151 | async (inRequest: Request, inResponse: Response) => { 152 | console.log("POST /contacts", inRequest.body); 153 | try { 154 | const contactsWorker: Contacts.Worker = new Contacts.Worker(); 155 | const contact: IContact = await contactsWorker.addContact(inRequest.body); 156 | console.log("POST /contacts: Ok", contact); 157 | inResponse.json(contact); 158 | } catch (inError) { 159 | console.log("POST /contacts: Error", inError); 160 | inResponse.send("error"); 161 | } 162 | } 163 | ); 164 | 165 | 166 | // Delete a contact. 167 | app.delete("/contacts/:id", 168 | async (inRequest: Request, inResponse: Response) => { 169 | console.log("DELETE /contacts", inRequest.body); 170 | try { 171 | const contactsWorker: Contacts.Worker = new Contacts.Worker(); 172 | await contactsWorker.deleteContact(inRequest.params.id); 173 | console.log("Contact deleted"); 174 | inResponse.send("ok"); 175 | } catch (inError) { 176 | console.log(inError); 177 | inResponse.send("error"); 178 | } 179 | } 180 | ); 181 | 182 | 183 | // Start app listening. 184 | app.listen(80, () => { 185 | console.log("MailBag server open for requests"); 186 | }); 187 | -------------------------------------------------------------------------------- /mailbag/server/src/IMAP.ts: -------------------------------------------------------------------------------- 1 | // Library imports. 2 | import { ParsedMail } from "mailparser"; 3 | const ImapClient = require("emailjs-imap-client"); 4 | import { simpleParser } from "mailparser"; 5 | 6 | // App imports. 7 | import { IServerInfo } from "./ServerInfo"; 8 | 9 | 10 | // Define interface to describe a mailbox and optionally a specific message 11 | // to be supplied to various methods here. 12 | export interface ICallOptions { 13 | mailbox: string, 14 | id?: number 15 | } 16 | 17 | 18 | // Define interface to describe a received message. Note that body is optional since it isn't sent when listing 19 | // messages. 20 | export interface IMessage { 21 | id: string, 22 | date: string, 23 | from: string, 24 | subject: string, 25 | body?: string 26 | } 27 | 28 | 29 | // Define interface to describe a mailbox. 30 | export interface IMailbox { 31 | name: string, 32 | path: string 33 | } 34 | 35 | 36 | // Disable certificate validation (less secure, but needed for some servers). 37 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; 38 | 39 | 40 | // The worker that will perform IMAP operations. 41 | export class Worker { 42 | 43 | 44 | // Server information. 45 | private static serverInfo: IServerInfo; 46 | 47 | 48 | /** 49 | * Constructor. 50 | */ 51 | constructor(inServerInfo: IServerInfo) { 52 | 53 | console.log("IMAP.Worker.constructor", inServerInfo); 54 | Worker.serverInfo = inServerInfo; 55 | 56 | } /* End constructor. */ 57 | 58 | 59 | /** 60 | * Connect to the SMTP server and return a client object for operations to use. 61 | * 62 | * @return An ImapClient instance. 63 | */ 64 | private async connectToServer(): Promise { 65 | 66 | // noinspection TypeScriptValidateJSTypes 67 | const client: any = new ImapClient.default( 68 | Worker.serverInfo.imap.host, 69 | Worker.serverInfo.imap.port, 70 | { auth : Worker.serverInfo.imap.auth } 71 | ); 72 | client.logLevel = client.LOG_LEVEL_NONE; 73 | client.onerror = (inError: Error) => { 74 | console.log("IMAP.Worker.listMailboxes(): Connection error", inError); 75 | }; 76 | await client.connect(); 77 | console.log("IMAP.Worker.listMailboxes(): Connected"); 78 | 79 | return client; 80 | 81 | } /* End connectToServer(). */ 82 | 83 | 84 | /** 85 | * Returns a list of all (top-level) mailboxes. 86 | * 87 | * @return An array of objects, on per mailbox, that describes the nmilbox. 88 | */ 89 | public async listMailboxes(): Promise { 90 | 91 | console.log("IMAP.Worker.listMailboxes()"); 92 | 93 | const client: any = await this.connectToServer(); 94 | 95 | const mailboxes: any = await client.listMailboxes(); 96 | 97 | await client.close(); 98 | 99 | // Translate from emailjs-imap-client mailbox objects to app-specific objects. At the same time, flatten the list 100 | // of mailboxes via recursion. 101 | const finalMailboxes: IMailbox[] = []; 102 | const iterateChildren: Function = (inArray: any[]): void => { 103 | inArray.forEach((inValue: any) => { 104 | finalMailboxes.push({ 105 | name : inValue.name, 106 | path : inValue.path 107 | }); 108 | iterateChildren(inValue.children); 109 | }); 110 | }; 111 | iterateChildren(mailboxes.children); 112 | 113 | return finalMailboxes; 114 | 115 | } /* End listMailboxes(). */ 116 | 117 | 118 | /** 119 | * Lists basic information about messages in a named mailbox. 120 | * 121 | * @param inCallOptions An object implementing the ICallOptions interface. 122 | * @return An array of objects, one per message. 123 | */ 124 | public async listMessages(inCallOptions: ICallOptions): Promise { 125 | 126 | console.log("IMAP.Worker.listMessages()", inCallOptions); 127 | 128 | const client: any = await this.connectToServer(); 129 | 130 | // We have to select the mailbox first. This gives us the message count. 131 | const mailbox: any = await client.selectMailbox(inCallOptions.mailbox); 132 | console.log(`IMAP.Worker.listMessages(): Message count = ${mailbox.exists}`); 133 | 134 | // If there are no messages then just return an empty array. 135 | if (mailbox.exists === 0) { 136 | await client.close(); 137 | return [ ]; 138 | } 139 | 140 | // Okay, there are messages, let's get 'em! Note that they are returned in order by uid, so it's FIFO. 141 | // noinspection TypeScriptValidateJSTypes 142 | const messages: any[] = await client.listMessages( 143 | inCallOptions.mailbox, 144 | "1:*", 145 | [ "uid", "envelope" ] 146 | ); 147 | 148 | await client.close(); 149 | 150 | // Translate from emailjs-imap-client message objects to app-specific objects. 151 | const finalMessages: IMessage[] = []; 152 | messages.forEach((inValue: any) => { 153 | finalMessages.push({ 154 | id : inValue.uid, 155 | date: inValue.envelope.date, 156 | from: inValue.envelope.from[0].address, 157 | subject: inValue.envelope.subject 158 | }); 159 | }); 160 | 161 | return finalMessages; 162 | 163 | } /* End listMessages(). */ 164 | 165 | 166 | /** 167 | * Gets the plain text body of a single message. 168 | * 169 | * @param inCallOptions An object implementing the ICallOptions interface. 170 | * @return The plain text body of the message. 171 | */ 172 | public async getMessageBody(inCallOptions: ICallOptions): Promise { 173 | 174 | console.log("IMAP.Worker.getMessageBody()", inCallOptions); 175 | 176 | const client: any = await this.connectToServer(); 177 | 178 | // noinspection TypeScriptValidateJSTypes 179 | const messages: any[] = await client.listMessages( 180 | inCallOptions.mailbox, 181 | inCallOptions.id, 182 | [ "body[]" ], 183 | { byUid : true } 184 | ); 185 | const parsed: ParsedMail = await simpleParser(messages[0]["body[]"]); 186 | 187 | await client.close(); 188 | 189 | return parsed.text; 190 | 191 | } /* End getMessageBody(). */ 192 | 193 | 194 | /** 195 | * Deletes a single message. 196 | * 197 | * @param inCallOptions An object implementing the ICallOptions interface. 198 | */ 199 | public async deleteMessage(inCallOptions: ICallOptions): Promise { 200 | 201 | console.log("IMAP.Worker.deleteMessage()", inCallOptions); 202 | 203 | const client: any = await this.connectToServer(); 204 | 205 | await client.deleteMessages( 206 | inCallOptions.mailbox, 207 | inCallOptions.id, 208 | { byUid : true } 209 | ); 210 | 211 | await client.close(); 212 | 213 | } /* End deleteMessage(). */ 214 | 215 | 216 | } /* End class. */ 217 | -------------------------------------------------------------------------------- /battlejong/server/src/server.ts: -------------------------------------------------------------------------------- 1 | // Node imports. 2 | import path from "path"; 3 | 4 | // Library imports. 5 | import express, { Express } from "express"; 6 | import WebSocket from "ws"; 7 | 8 | 9 | // Our collection players. Each element is an object: { pid, score, stillPlaying } 10 | const players: any = { }; 11 | 12 | 13 | // Construct Express server for client resources. 14 | const app: Express = express(); 15 | app.use("/", express.static(path.join(__dirname, "../../client/dist"))); 16 | app.listen(80, () => { 17 | console.log("BattleJong Express server ready"); 18 | }); 19 | 20 | 21 | // Construct WebSocket server. 22 | const wsServer = new WebSocket.Server({ port : 8080 }, function() { 23 | console.log("BattleJong WebSocket server ready"); 24 | }); 25 | wsServer.on("connection", (socket: WebSocket) => { 26 | 27 | console.log("Player connected"); 28 | 29 | // First things first: hook up message handler. 30 | socket.on("message", (inMsg: string) => { 31 | 32 | console.log(`Message: ${inMsg}`); 33 | 34 | const msgParts: string[] = inMsg.toString().split("_"); 35 | const message: string = msgParts[0]; 36 | const pid: string = msgParts[1]; 37 | switch (message) { 38 | 39 | // When a tile pair is matched: match__ 40 | case "match": 41 | players[pid].score += parseInt(msgParts[2]); 42 | // Broadcast score updates to both players. 43 | wsServer.clients.forEach(function each(inClient: WebSocket) { 44 | inClient.send(`update_${pid}_${players[pid].score}`); 45 | }); 46 | break; 47 | 48 | // When the player dead-ends or clears: done_ 49 | case "done": 50 | players[pid].stillPlaying = false; 51 | // See if both players are done playing. 52 | let playersDone: number = 0; 53 | for (const player in players) { 54 | if (players.hasOwnProperty(player)) { 55 | if (!players[player].stillPlaying) { 56 | playersDone++; 57 | } 58 | } 59 | } 60 | // They are both done playing, now see who won. 61 | if (playersDone === 2) { 62 | let winningPID: string; 63 | const pids: string[] = Object.keys(players); 64 | if (players[pids[0]].score > players[pids[1]].score) { 65 | winningPID = pids[0]; 66 | } else { 67 | winningPID = pids[1]; 68 | } 69 | // Broadcast the outcome to both players. 70 | wsServer.clients.forEach(function each(inClient: WebSocket) { 71 | inClient.send(`gameOver_${winningPID}`); 72 | }); 73 | } 74 | break; 75 | 76 | } /* End switch. */ 77 | 78 | }); /* End message handler. */ 79 | 80 | // Now, construct PID for this player and add the player to the collection. 81 | const pid: string = `pid${new Date().getTime()}`; 82 | players[pid] = { score : 0, stillPlaying : true }; 83 | 84 | // Inform the player that we're connected and give them their ID. 85 | socket.send(`connected_${pid}`); 86 | 87 | // If there are now two players, transition state on the clients. This broadcasts to ALL clients, even the one 88 | // that sent the current message, which is what we want in this case. 89 | if (Object.keys(players).length === 2) { 90 | // Shuffle the tiles in the layout so we can send the layout to both clients. 91 | const shuffledLayout: number[][][] = shuffle(); 92 | wsServer.clients.forEach(function each(inClient: WebSocket) { 93 | inClient.send(`start_${JSON.stringify(shuffledLayout)}`); 94 | }); 95 | } 96 | 97 | 98 | }); /* End WebSocket server construction. */ 99 | 100 | 101 | // ---------------------------------------- Game code. ---------------------------------------- 102 | 103 | // 0 = no tile, 1 = tile. 104 | // Each layer is 15x9 (135 per layer, 675 total). Tiles are 36x44. 105 | // When board is shuffled, all 1's become 101-142 (matching the 42 tile type filenames). 106 | // Tile 101 is wildcard. 107 | const layout: number[][][] = [ 108 | /* Layer 1. */ 109 | [ 110 | [ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0 ], 111 | [ 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0 ], 112 | [ 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0 ], 113 | [ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0 ], 114 | [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ], 115 | [ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0 ], 116 | [ 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0 ], 117 | [ 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0 ], 118 | [ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0 ], 119 | ], 120 | /* Layer 2. */ 121 | [ 122 | [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], 123 | [ 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0 ], 124 | [ 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0 ], 125 | [ 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0 ], 126 | [ 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0 ], 127 | [ 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0 ], 128 | [ 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0 ], 129 | [ 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0 ], 130 | [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] 131 | ], 132 | /* Layer 3. */ 133 | [ 134 | [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], 135 | [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], 136 | [ 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0 ], 137 | [ 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0 ], 138 | [ 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0 ], 139 | [ 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0 ], 140 | [ 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0 ], 141 | [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], 142 | [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] 143 | ], 144 | /* Layer 4. */ 145 | [ 146 | [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], 147 | [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], 148 | [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], 149 | [ 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0 ], 150 | [ 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0 ], 151 | [ 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0 ], 152 | [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], 153 | [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], 154 | [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] 155 | ], 156 | /* Layer 5. */ 157 | [ 158 | [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], 159 | [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], 160 | [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], 161 | [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], 162 | [ 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0 ], 163 | [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], 164 | [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], 165 | [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], 166 | [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] 167 | ] 168 | ]; /* End layout. */ 169 | 170 | 171 | /** 172 | * Shuffles the tiles in the layout, randomizing tile placement. Note that this uses the "American-style 173 | * totally random approach, which means that not every shuffle will be "winnable" (meaning that there may be no 174 | * path to completely clear the board). 175 | * 176 | * @return A shuffled layout. 177 | */ 178 | function shuffle(): number[][][] { 179 | 180 | // Clone the layout. 181 | const cl: number[][][] = layout.slice(0); 182 | 183 | // We need to ensure no more than 4 wildcards are placed, so this will count them. 184 | let numWildcards: number = 0; 185 | 186 | // Iterate over the entire board, randomly choosing a tile for each position that isn't supposed to be blank. 187 | const numTileTypes: number = 42; 188 | for (let l: number = 0; l < cl.length; l++) { 189 | const layer: number[][] = cl[l]; 190 | for (let r: number = 0; r < layer.length; r++) { 191 | const row: number[] = layer[r]; 192 | for (let c: number = 0; c < row.length; c++) { 193 | const tileVal: number = row[c]; 194 | // tileVal > 0 indicates there is supposed to be a tile at this position. 195 | if (tileVal === 1) { 196 | row[c] = (Math.floor(Math.random() * numTileTypes)) + 101; 197 | // If this is a wildcard and no more are allowed then bump to the next tile type, otherwise bump 198 | // wildcard count. Doing this is a cheap way of having to randomly select a tile again, which at this 199 | // point could actually be a little tricky if we want to avoid duplicate code. 200 | if (row[c] === 101 && numWildcards === 3) { 201 | row[c] = 102; 202 | } else { 203 | numWildcards += numWildcards; 204 | } 205 | } /* End tileVal > 0 check. */ 206 | } /* End column iteration. */ 207 | } /* End row iteration. */ 208 | } /* End layer iteration. */ 209 | 210 | return cl; 211 | 212 | } /* End shuffle(). */ 213 | -------------------------------------------------------------------------------- /battlejong/client/src/code/components/PlayerBoard.tsx: -------------------------------------------------------------------------------- 1 | // React imports. 2 | import React, { CSSProperties, ReactElement } from "react"; 3 | 4 | // Library imports. 5 | 6 | // App imports. 7 | import tile101 from "../../img/tile101.png"; 8 | import tile102 from "../../img/tile102.png"; 9 | import tile103 from "../../img/tile103.png"; 10 | import tile104 from "../../img/tile104.png"; 11 | import tile105 from "../../img/tile105.png"; 12 | import tile106 from "../../img/tile106.png"; 13 | import tile107 from "../../img/tile107.png"; 14 | import tile108 from "../../img/tile108.png"; 15 | import tile109 from "../../img/tile109.png"; 16 | import tile110 from "../../img/tile110.png"; 17 | import tile111 from "../../img/tile111.png"; 18 | import tile112 from "../../img/tile112.png"; 19 | import tile113 from "../../img/tile113.png"; 20 | import tile114 from "../../img/tile114.png"; 21 | import tile115 from "../../img/tile115.png"; 22 | import tile116 from "../../img/tile116.png"; 23 | import tile117 from "../../img/tile117.png"; 24 | import tile118 from "../../img/tile118.png"; 25 | import tile119 from "../../img/tile119.png"; 26 | import tile120 from "../../img/tile120.png"; 27 | import tile121 from "../../img/tile121.png"; 28 | import tile122 from "../../img/tile122.png"; 29 | import tile123 from "../../img/tile123.png"; 30 | import tile124 from "../../img/tile124.png"; 31 | import tile125 from "../../img/tile125.png"; 32 | import tile126 from "../../img/tile126.png"; 33 | import tile127 from "../../img/tile127.png"; 34 | import tile128 from "../../img/tile128.png"; 35 | import tile129 from "../../img/tile129.png"; 36 | import tile130 from "../../img/tile130.png"; 37 | import tile131 from "../../img/tile131.png"; 38 | import tile132 from "../../img/tile132.png"; 39 | import tile133 from "../../img/tile133.png"; 40 | import tile134 from "../../img/tile134.png"; 41 | import tile135 from "../../img/tile135.png"; 42 | import tile136 from "../../img/tile136.png"; 43 | import tile137 from "../../img/tile137.png"; 44 | import tile138 from "../../img/tile138.png"; 45 | import tile139 from "../../img/tile139.png"; 46 | import tile140 from "../../img/tile140.png"; 47 | import tile141 from "../../img/tile141.png"; 48 | import tile142 from "../../img/tile142.png"; 49 | 50 | 51 | const PlayerBoard = ({ state }: any) => { 52 | 53 | // Details about the tiles. 54 | const tileWidth: number = 72; 55 | const tileHeight: number = 88; 56 | const tileShadowWidth: number = 11; 57 | const tileShadowHeight: number = 11; 58 | 59 | // The entire board gets shifted a bit right and down to keep it off the borders of the container. 60 | const xAdjust: number = 10; 61 | const yAdjust: number = 36; 62 | 63 | // Array of tile components that React will render. 64 | const tiles: ReactElement[] = [ ]; 65 | 66 | // The tiles in each layer get shifted right and up to maintain the 3D effect. 67 | let xOffset: number = 0; 68 | let yOffset: number = 0; 69 | 70 | // Iterate over over all layers in the layout. 71 | for (let l: number = 0; l < state.layout.length; l++) { 72 | // Shift the tiles in this layer. 73 | xOffset = xOffset + tileShadowWidth; 74 | yOffset = yOffset - tileShadowHeight; 75 | const layer: number[][] = state.layout[l]; 76 | // Iterator over all rows in the layout. 77 | for (let r: number = 0; r < layer.length; r++) { 78 | const row: number[] = layer[r]; 79 | // Iterate over all columns in the layout. 80 | for (let c: number = row.length; c >= 0; c--) { 81 | let tileVal: number = row[c]; 82 | // We only need to do work if there's a tile in this grid square. 83 | if (tileVal > 0) { 84 | // Calculate position based on the virtual 15x9 grid, taking into account the offsets to overlap shadows and 85 | // the adjustment to push the grid off the container edges. 86 | const xLoc: number = ((c * tileWidth) - (c * tileShadowWidth)) + xOffset + xAdjust; 87 | const yLoc: number = ((r * tileHeight) - (r * tileShadowHeight)) + yOffset + yAdjust; 88 | // Build style for the tile. 89 | const style: CSSProperties = { 90 | position : "absolute", 91 | left : `${xLoc}px`, 92 | top : `${yLoc}px` 93 | }; 94 | // If the tile is currently selected, add the highlight style. 95 | let className: string = ""; 96 | if (tileVal > 1000) { 97 | className = "highlightTile"; 98 | tileVal = tileVal - 1000; 99 | } 100 | // Now render the appropriate img tag. 101 | switch (tileVal) { 102 | case 101 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 103 | case 102 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 104 | case 103 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 105 | case 104 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 106 | case 105 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 107 | case 106 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 108 | case 107 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 109 | case 108 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 110 | case 109 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 111 | case 110 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 112 | case 111 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 113 | case 112 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 114 | case 113 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 115 | case 114 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 116 | case 115 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 117 | case 116 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 118 | case 117 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 119 | case 118 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 120 | case 119 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 121 | case 120 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 122 | case 121 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 123 | case 122 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 124 | case 123 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 125 | case 124 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 126 | case 125 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 127 | case 126 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 128 | case 127 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 129 | case 128 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 130 | case 129 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 131 | case 130 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 132 | case 131 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 133 | case 132 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 134 | case 133 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 135 | case 134 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 136 | case 135 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 137 | case 136 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 138 | case 137 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 139 | case 138 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 140 | case 139 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 141 | case 140 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 142 | case 141 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 143 | case 142 : tiles.push(state.tileClick(l, r, c)} alt="" />); break; 144 | } 145 | } /* End tileVal > 0 check. */ 146 | } /* End column iteration. */ 147 | } /* End row iteration. */ 148 | } /* End layer iteration. */ 149 | 150 | return ({ tiles }); 151 | 152 | }; /* End PlayerBoard. */ 153 | 154 | export default PlayerBoard; 155 | -------------------------------------------------------------------------------- /battlejong/client/src/code/state.ts: -------------------------------------------------------------------------------- 1 | // React imports. 2 | import React from "react"; 3 | 4 | // App imports. 5 | import { createSocketComm } from "./socketComm"; 6 | 7 | 8 | // Interfaces. 9 | interface ISelectedTile { layer: number, row: number, column: number, type: number } 10 | interface IScores { player: number, opponent: number } 11 | 12 | 13 | /** 14 | * This function must be called once and only once from BaseLayout. 15 | */ 16 | export function createState(inParentComponent: React.Component) { 17 | 18 | return { 19 | 20 | // The current board layout. 21 | // During play, -1 replaces a 1 when tiles are cleared. 22 | // When selected (highlighted), they become 1001-1042. 23 | layout : [ ], 24 | 25 | // Used to keep track of what tiles, if any, are currently selected. 26 | selectedTiles : [ ], 27 | 28 | // Player and opponent's current score. 29 | scores : { player : 0, opponent : 0 }, 30 | 31 | // What state the game is currently in: "awaitingOpponent", "playing", "deadEnd", "cleared" or "gameOver". 32 | gameState : "awaitingOpponent", 33 | 34 | // The outcome of the game. 35 | gameOutcome : "", 36 | 37 | // The player ID as returned by the server. 38 | pid : "", 39 | 40 | // The socketComm object used to make server calls. 41 | socketComm : createSocketComm(inParentComponent), 42 | 43 | // The time since the player last matched a tile pair. 44 | timeSinceLastMatch : 0, 45 | 46 | 47 | // ---------------------------------------- Message Handler Functions ---------------------------------------- 48 | 49 | 50 | /** 51 | * Handle 'connected' message. Occurs when this player connects to the server. 52 | * 53 | * @parain inPID The player's ID. 54 | */ 55 | handleMessage_connected : function(inPID: string) { 56 | 57 | this.setState({ pid : inPID }); 58 | 59 | }.bind(inParentComponent), /* End handleMessage_connected(). */ 60 | 61 | 62 | /** 63 | * Handle 'start' message. Occurs when two players have connected to the server. 64 | * 65 | * @param inLayout The board layout. 66 | */ 67 | handleMessage_start: function(inLayout: number[][][]) { 68 | 69 | this.setState({ 70 | timeSinceLastMatch : new Date().getTime(), 71 | layout : inLayout, 72 | gameState : "playing" 73 | }); 74 | 75 | }.bind(inParentComponent), /* End handleMessage_start(). */ 76 | 77 | 78 | /** 79 | * Handle 'update' message. Occurs when a player matches a tile pair. 80 | * 81 | * @param inPID The player ID of the player. 82 | * @param inScore The new score for the player. 83 | */ 84 | handleMessage_update: function(inPID: string, inScore: number) { 85 | 86 | // This player's score is always up to date, so we only need to update the opponent's score. 87 | if (inPID !== this.state.pid) { 88 | const scores: IScores = { ...this.state.scores }; 89 | scores.opponent = inScore; 90 | this.setState({ scores : scores }); 91 | } 92 | 93 | }.bind(inParentComponent), /* End handleMessage_update(). */ 94 | 95 | 96 | /** 97 | * Handle 'gameOver' message. Occurs when both players have each either dead-ended or cleared the board. 98 | * 99 | * @param inPID The player ID of the winning player. 100 | */ 101 | handleMessage_gameOver: function(inPID: string) { 102 | 103 | if (inPID === this.state.pid) { 104 | this.setState({ gameState : "gameOver", gameOutcome : "**** YOU WON! ****" }); 105 | } else { 106 | this.setState({ gameState : "gameOver", gameOutcome : "Tough luck, you lost :(" }); 107 | } 108 | 109 | }.bind(inParentComponent), /* End handleMessage_update(). */ 110 | 111 | 112 | // ---------------------------------------- Game Functions ---------------------------------------- 113 | 114 | 115 | /** 116 | * Called when a tile is clicked.. 117 | * 118 | * @param inLayer The layer number the tile is in. 119 | * @param inRow The row number the tile is in. 120 | * @param inColumn The column number the tile is in. 121 | */ 122 | tileClick : function(inLayer: number, inRow: number, inColumn: number) { 123 | 124 | // Abort if not in "playing" state. 125 | if (this.state.gameState !== "playing") { 126 | return; 127 | } 128 | 129 | // See if this tile can be selected (vis a vis, if it's "free"), abort if not. 130 | if (!this.state.canTileBeSelected(inLayer, inRow, inColumn)) { 131 | return; 132 | } 133 | 134 | // Find the current value of the clicked tile. 135 | const layout: number[][][] = this.state.layout.slice(0); 136 | const currentTileValue: number = layout[inLayer][inRow][inColumn]; 137 | 138 | // Make sure they can only click tiles, not blank spaces (or removed tiles). 139 | if (currentTileValue <= 0) { 140 | return; 141 | } 142 | 143 | // Grab some other values out of state that we're going to need. 144 | const scores: IScores = { ...this.state.scores }; 145 | let gameState: string = this.state.gameState; 146 | let timeSinceLastMatch: number = this.state.timeSinceLastMatch; 147 | let selectedTiles: ISelectedTile[] = this.state.selectedTiles.slice(0); 148 | 149 | // Tile is currently highlighted, so de-highlight it and remove its selected status. 150 | if (currentTileValue > 1000) { 151 | layout[inLayer][inRow][inColumn] = currentTileValue - 1000; 152 | for (let i: number = 0; i < selectedTiles.length; i++) { 153 | const selectedTile: ISelectedTile = selectedTiles[i]; 154 | if (selectedTile.layer == inLayer && selectedTile.row == inRow && selectedTile.column == inColumn) { 155 | selectedTiles.splice(i, 1); 156 | break; 157 | } 158 | } 159 | // Not currently highlighted, so highlight it now and record it as selected. 160 | } else { 161 | layout[inLayer][inRow][inColumn] = currentTileValue + 1000; 162 | selectedTiles.push({ layer : inLayer, row : inRow, column : inColumn, type : currentTileValue }); 163 | } 164 | 165 | // Now, if two tiles are selected, we have to see if we have a match. If so, remove the tiles and bump 166 | // score. If not, de-highlight them both and clear selectedTiles. Remember to take wildcard (101) into account! 167 | if (selectedTiles.length === 2) { 168 | // Matched. 169 | if (selectedTiles[0].type === selectedTiles[1].type || 170 | selectedTiles[0].type == 101 || selectedTiles[1].type == 101 171 | ) { 172 | // "Clear" the pair. 173 | layout[selectedTiles[0].layer][selectedTiles[0].row][selectedTiles[0].column] = -1; 174 | layout[selectedTiles[1].layer][selectedTiles[1].row][selectedTiles[1].column] = -1; 175 | // Calculate how many points they get. For every half a second they took since their last match, 176 | // deduct 1 point. Ensure they get at least 1 point, 'cause we're nice! 177 | // Also, don't forget to update timeSinceLastMatch! 178 | let calculatedPoints: number = 10; 179 | const now: number = new Date().getTime(); 180 | const timeTaken: number = now - timeSinceLastMatch; 181 | const numHalfSeconds: number = Math.trunc(timeTaken / 500); 182 | calculatedPoints -= numHalfSeconds; 183 | if (calculatedPoints <= 0) { 184 | calculatedPoints = 1; 185 | } 186 | scores.player += calculatedPoints; 187 | timeSinceLastMatch = now; 188 | // Send score to server. 189 | this.state.socketComm.send(`match_${this.state.pid}_${calculatedPoints}`); 190 | // See if the board has dead-ended, or if they just cleared it. Note that we have to pass cl to 191 | // anyMovesLeft() because state won't have been updated at this point and isn't current. 192 | const anyMovesLeft: string = this.state.anyMovesLeft(layout); 193 | switch (anyMovesLeft) { 194 | case "no": 195 | gameState = "deadEnd"; 196 | this.state.socketComm.send(`done_${this.state.pid}`); 197 | break; 198 | case "cleared": 199 | scores.player += 100; 200 | gameState = "cleared"; 201 | this.state.socketComm.send(`match_${this.state.pid}_100`); 202 | this.state.socketComm.send(`done_${this.state.pid}`); 203 | break; 204 | } 205 | // Not matched. 206 | } else { 207 | layout[selectedTiles[0].layer][selectedTiles[0].row][selectedTiles[0].column] = 208 | layout[selectedTiles[0].layer][selectedTiles[0].row][selectedTiles[0].column] - 1000; 209 | layout[selectedTiles[1].layer][selectedTiles[1].row][selectedTiles[1].column] = 210 | layout[selectedTiles[1].layer][selectedTiles[1].row][selectedTiles[1].column] - 1000; 211 | } 212 | selectedTiles = [ ]; 213 | } 214 | 215 | // Set state so React re-draws the tile(s) as appropriate. 216 | this.setState({ 217 | gameState : gameState, 218 | layout : layout, 219 | selectedTiles : selectedTiles, 220 | scores : scores, 221 | timeSinceLastMatch : timeSinceLastMatch 222 | }); 223 | 224 | }.bind(inParentComponent), /* End tileClick(). */ 225 | 226 | 227 | /** 228 | * Determines if a given tile can be selected according to the game rules. This means that the tiles must have 229 | * no tile on top of it (which shouldn't really be possible anyway given the click handler) and that it is free 230 | * on at least one side. 231 | * 232 | * @param inLayer The layer number the tile is in. 233 | * @param inRow The row number the tile is in. 234 | * @param inColumn The column number the tile is in. 235 | * @return True if the tile can be selected, false if not. 236 | */ 237 | canTileBeSelected : function(inLayer: number, inRow: number, inColumn: number): boolean { 238 | 239 | // If the tile is in the top layer or there is no tile above it, AND if it's in the first column, last column, 240 | // or there's no tile to the left or right of it, then it's "free". 241 | return (inLayer == 4 || this.state.layout[inLayer + 1][inRow][inColumn] <= 0) && 242 | (inColumn === 0 || inColumn === 14 || this.state.layout[inLayer][inRow][inColumn - 1] <= 0 || 243 | this.state.layout[inLayer][inRow][inColumn + 1] <= 0); 244 | 245 | }.bind(inParentComponent), /* End canTileBeSelected(). */ 246 | 247 | 248 | /** 249 | * Determines if there is at least one move left for the player to make. 250 | * 251 | * @param inLayout The clone of the current layout from tileClick(). 252 | * @return "yes" if there is at least one move left, "no" if there are none left and "cleared" if there 253 | * are none left as a result of the player clearing the board. 254 | */ 255 | anyMovesLeft : function(inLayout: number[][][]): string { 256 | 257 | // First, find all free tiles, that is, tiles that can be selected. Simultaneously, count how many tiles there 258 | // are left at all. 259 | let numTiles: number = 0; 260 | const selectableTiles: number[] = [ ]; 261 | for (let l: number = 0; l < inLayout.length; l++) { 262 | const layer = inLayout[l]; 263 | for (let r: number = 0; r < layer.length; r++) { 264 | const row = layer[r]; 265 | for (let c: number = 0; c < row.length; c++) { 266 | const tileVal: number = row[c]; 267 | if (tileVal > 0) { 268 | numTiles += 1; 269 | if (this.state.canTileBeSelected(l, r, c)) { 270 | // If any of them is a wildcard then we can short-circuit this whole mess because there will always be 271 | // at least one move left when there's a wildcard. 272 | if (tileVal === 101) { 273 | return "yes"; 274 | } 275 | // Otherwise, we need to keep checking and record this selectable tile. 276 | selectableTiles.push(tileVal); 277 | } 278 | } 279 | } /* End column iteration. */ 280 | } /* End row iteration. */ 281 | } /* End layer iteration. */ 282 | 283 | // If there are no tiles at all left then they cleared the board. 284 | if (numTiles === 0) { 285 | return "cleared"; 286 | } 287 | 288 | // Now, iterate over selectable tiles and for each, count how many times they occur. 289 | const counts: number[] = []; 290 | for (let i: number = 0; i < selectableTiles.length; i++) { 291 | if (counts[selectableTiles[i]] === undefined) { 292 | counts[selectableTiles[i]] = 1; 293 | } else { 294 | counts[selectableTiles[i]]++; 295 | } 296 | } 297 | 298 | // Finally, make sure we have at least one count >= 2. 299 | for (let i: number = 0; i < counts.length; i++) { 300 | if (counts[i] >= 2) { 301 | return "yes"; 302 | } 303 | } 304 | 305 | // If we're here, then there are no free tile pairs, so the game cannot continue. 306 | return "no"; 307 | 308 | }.bind(inParentComponent) /* End anyMovesLeft(). */ 309 | 310 | 311 | }; /* End state object. */ 312 | 313 | } /* End createState(). */ 314 | -------------------------------------------------------------------------------- /mailbag/client/src/code/state.ts: -------------------------------------------------------------------------------- 1 | // App imports. 2 | import * as Contacts from "./Contacts"; 3 | import { config } from "./config"; 4 | import * as IMAP from "./IMAP"; 5 | import * as SMTP from "./SMTP"; 6 | 7 | 8 | /** 9 | * This function must be called once and only once from BaseLayout. 10 | */ 11 | export function createState(inParentComponent) { 12 | 13 | return { 14 | 15 | 16 | // Flag: Is the please wait dialog visible? 17 | pleaseWaitVisible : false, 18 | 19 | // List of contacts. 20 | contacts : [ ], 21 | 22 | // List of mailboxes. 23 | mailboxes : [ ], 24 | 25 | // List of messages in the current mailbox. 26 | messages : [ ], 27 | 28 | // The view that is currently showing ("welcome", "message", "compose", "contact" or "contactAdd"). 29 | currentView : "welcome", 30 | 31 | // The currently selected mailbox, if any. 32 | currentMailbox : null, 33 | 34 | // The details of the message currently being viewed or composed, if any. 35 | messageID : null, 36 | messageDate : null, 37 | messageFrom : null, 38 | messageTo : null, 39 | messageSubject : null, 40 | messageBody : null, 41 | 42 | // The details of the contact currently being viewed or added, if any. 43 | contactID : null, 44 | contactName : null, 45 | contactEmail : null, 46 | 47 | 48 | // ------------------------------------------------------------------------------------------------ 49 | // ------------------------------------ View Switch functions ------------------------------------- 50 | // ------------------------------------------------------------------------------------------------ 51 | 52 | 53 | /** 54 | * Shows or hides the please wait dialog during server calls. 55 | * 56 | * @param inVisible True to show the dialog, false to hide it. 57 | */ 58 | showHidePleaseWait : function(inVisible: boolean): void { 59 | 60 | this.setState({ pleaseWaitVisible : inVisible }); 61 | 62 | }.bind(inParentComponent), /* End showHidePleaseWait(). */ 63 | 64 | 65 | /** 66 | * Show ContactView in view mode. 67 | * 68 | * @param inID The ID of the contact to show. 69 | * @param inName The name of the contact to show. 70 | * @oaram inEmail The Email address of the contact to show. 71 | */ 72 | showContact : function(inID: string, inName: string, inEmail: string): void { 73 | 74 | console.log("state.showContact()", inID, inName, inEmail); 75 | 76 | this.setState({ currentView : "contact", contactID : inID, contactName : inName, contactEmail : inEmail }); 77 | 78 | }.bind(inParentComponent), /* End showContact(). */ 79 | 80 | 81 | /** 82 | * Show ContactView in add mode. 83 | */ 84 | showAddContact : function(): void { 85 | 86 | console.log("state.showAddContact()"); 87 | 88 | this.setState({ currentView : "contactAdd", contactID : null, contactName : "", contactEmail : "" }); 89 | 90 | }.bind(inParentComponent), /* End showAddContact(). */ 91 | 92 | 93 | /** 94 | * Show MessageView in view mode. 95 | * 96 | * @param inMessage The message object that was clicked. 97 | */ 98 | showMessage : async function(inMessage: IMAP.IMessage): Promise { 99 | 100 | console.log("state.showMessage()", inMessage); 101 | 102 | // Get the message's body. 103 | this.state.showHidePleaseWait(true); 104 | const imapWorker: IMAP.Worker = new IMAP.Worker(); 105 | const mb: String = await imapWorker.getMessageBody(inMessage.id, this.state.currentMailbox); 106 | this.state.showHidePleaseWait(false); 107 | 108 | // Update state. 109 | this.setState({ currentView : "message", 110 | messageID : inMessage.id, messageDate : inMessage.date, messageFrom : inMessage.from, 111 | messageTo : "", messageSubject : inMessage.subject, messageBody : mb 112 | }); 113 | 114 | }.bind(inParentComponent), /* End showMessage(). */ 115 | 116 | 117 | /** 118 | * Show MessageView in compose mode. 119 | * 120 | * @param inType Pass "new" if this is a new message, "reply" if it's a reply to the message currently being 121 | * viewed, and "contact" if it's a message to the contact currently being viewed. 122 | */ 123 | showComposeMessage : function(inType: string): void { 124 | 125 | console.log("state.showComposeMessage()"); 126 | 127 | switch (inType) { 128 | 129 | case "new": 130 | this.setState({ currentView : "compose", 131 | messageTo : "", messageSubject : "", messageBody : "", 132 | messageFrom : config.userEmail 133 | }); 134 | break; 135 | 136 | case "reply": 137 | this.setState({ currentView : "compose", 138 | messageTo : this.state.messageFrom, messageSubject : `Re: ${this.state.messageSubject}`, 139 | messageBody : `\n\n---- Original Message ----\n\n${this.state.messageBody}`, messageFrom : config.userEmail 140 | }); 141 | break; 142 | 143 | case "contact": 144 | this.setState({ currentView : "compose", 145 | messageTo : this.state.contactEmail, messageSubject : "", messageBody : "", 146 | messageFrom : config.userEmail 147 | }); 148 | break; 149 | 150 | } 151 | 152 | }.bind(inParentComponent), /* End showComposeMessage(). */ 153 | 154 | 155 | // ------------------------------------------------------------------------------------------------ 156 | // ---------------------------------------- List functions ---------------------------------------- 157 | // ------------------------------------------------------------------------------------------------ 158 | 159 | 160 | /** 161 | * Add a mailbox to the list of mailboxes. 162 | * 163 | * @param inMailbox A mailbox descriptor object. 164 | */ 165 | addMailboxToList : function(inMailbox: IMAP.IMailbox): void { 166 | 167 | console.log("state.addMailboxToList()", inMailbox); 168 | 169 | // Copy list. 170 | const cl: IMAP.IMailbox[] = this.state.mailboxes.slice(0); 171 | 172 | // Add new element. 173 | cl.push(inMailbox); 174 | 175 | // Update list in state. 176 | this.setState({ mailboxes : cl }); 177 | 178 | }.bind(inParentComponent), /* End addMailboxToList(). */ 179 | 180 | 181 | /** 182 | * Add a contact to the list of contacts. 183 | * 184 | * @param inContact A contact descriptor object. 185 | */ 186 | addContactToList : function(inContact: Contacts.IContact): void { 187 | 188 | console.log("state.addContactToList()", inContact); 189 | 190 | // Copy list. 191 | const cl = this.state.contacts.slice(0); 192 | 193 | // Add new element. 194 | cl.push({ _id : inContact._id, name : inContact.name, email : inContact.email }); 195 | 196 | // Update list in state. 197 | this.setState({ contacts : cl }); 198 | 199 | }.bind(inParentComponent), /* End addContactToList(). */ 200 | 201 | 202 | /** 203 | * Add a message to the list of messages in the current mailbox. 204 | * 205 | * @param inMessage A message descriptor object. 206 | */ 207 | addMessageToList : function(inMessage: IMAP.IMessage): void { 208 | 209 | console.log("state.addMessageToList()", inMessage); 210 | 211 | // Copy list. 212 | const cl = this.state.messages.slice(0); 213 | 214 | // Add new element. 215 | cl.push({ id : inMessage.id, date : inMessage.date, from : inMessage.from, subject : inMessage.subject }); 216 | 217 | // Update list in state. 218 | this.setState({ messages : cl }); 219 | 220 | }.bind(inParentComponent), /* End addMessageToList(). */ 221 | 222 | 223 | /** 224 | * Clear the list of messages currently displayed. 225 | */ 226 | clearMessages : function(): void { 227 | 228 | console.log("state.clearMessages()"); 229 | 230 | this.setState({ messages : [ ] }); 231 | 232 | }.bind(inParentComponent), /* End clearMessages(). */ 233 | 234 | 235 | // ------------------------------------------------------------------------------------------------ 236 | // ------------------------------------ Event Handler functions ----------------------------------- 237 | // ------------------------------------------------------------------------------------------------ 238 | 239 | 240 | /** 241 | * Set the current mailbox. 242 | * 243 | * @param inPath The path of the current mailbox. 244 | */ 245 | setCurrentMailbox : function(inPath: String): void { 246 | 247 | console.log("state.setCurrentMailbox()", inPath); 248 | 249 | // Update state. 250 | this.setState({ currentView : "welcome", currentMailbox : inPath }); 251 | 252 | // Now go get the list of messages for the mailbox. 253 | this.state.getMessages(inPath); 254 | 255 | }.bind(inParentComponent), /* End setCurrentMailbox(). */ 256 | 257 | 258 | /** 259 | * Get a list of messages in the currently selected mailbox, if any. 260 | * 261 | * @param inPath The path to the mailbox to get messages for. Note that because this method is called when the 262 | * current mailbox is set, we can't count on state having been updated by the time this is called, 263 | * hence why the mailbox is passed in. This avoids the problem with setState() being asynchronous. 264 | */ 265 | getMessages : async function(inPath: string): Promise { 266 | 267 | console.log("state.getMessages()"); 268 | 269 | this.state.showHidePleaseWait(true); 270 | const imapWorker: IMAP.Worker = new IMAP.Worker(); 271 | const messages: IMAP.IMessage[] = await imapWorker.listMessages(inPath); 272 | this.state.showHidePleaseWait(false); 273 | 274 | this.state.clearMessages(); 275 | messages.forEach((inMessage: IMAP.IMessage) => { 276 | this.state.addMessageToList(inMessage); 277 | }); 278 | 279 | }.bind(inParentComponent), /* End getMessages(). */ 280 | 281 | 282 | /** 283 | * Fires any time the user types in an editable field. 284 | * 285 | * @param inEvent The event object generated by the keypress. 286 | */ 287 | fieldChangeHandler : function(inEvent: any): void { 288 | 289 | console.log("state.fieldChangeHandler()", inEvent.target.id, inEvent.target.value); 290 | 291 | // Enforce max length for contact name. 292 | if (inEvent.target.id === "contactName" && inEvent.target.value.length > 16) { return; } 293 | 294 | this.setState({ [inEvent.target.id] : inEvent.target.value }); 295 | 296 | }.bind(inParentComponent), /* End fieldChangeHandler(). */ 297 | 298 | 299 | /** 300 | * Save contact. 301 | */ 302 | saveContact : async function(): Promise { 303 | 304 | console.log("state.saveContact()", this.state.contactID, this.state.contactName, this.state.contactEmail); 305 | 306 | // Copy list. 307 | const cl = this.state.contacts.slice(0); 308 | 309 | // Save to server. 310 | this.state.showHidePleaseWait(true); 311 | const contactsWorker: Contacts.Worker = new Contacts.Worker(); 312 | const contact: Contacts.IContact = 313 | await contactsWorker.addContact({ name : this.state.contactName, email : this.state.contactEmail }); 314 | this.state.showHidePleaseWait(false); 315 | 316 | // Add to list. 317 | cl.push(contact); 318 | 319 | // Update state. 320 | this.setState({ contacts : cl, contactID : null, contactName : "", contactEmail : "" }); 321 | 322 | }.bind(inParentComponent), /* End saveContact(). */ 323 | 324 | 325 | /** 326 | * Delete the currently viewed contact. 327 | */ 328 | deleteContact : async function(): Promise { 329 | 330 | console.log("state.deleteContact()", this.state.contactID); 331 | 332 | // Delete from server. 333 | this.state.showHidePleaseWait(true); 334 | const contactsWorker: Contacts.Worker = new Contacts.Worker(); 335 | await contactsWorker.deleteContact(this.state.contactID); 336 | this.state.showHidePleaseWait(false); 337 | 338 | // Remove from list. 339 | const cl = this.state.contacts.filter((inElement) => inElement._id != this.state.contactID); 340 | 341 | // Update state. 342 | this.setState({ contacts : cl, contactID : null, contactName : "", contactEmail : "" }); 343 | 344 | }.bind(inParentComponent), /* End deleteContact(). */ 345 | 346 | 347 | /** 348 | * Delete the currently viewed message. 349 | */ 350 | deleteMessage : async function(): Promise { 351 | 352 | console.log("state.deleteMessage()", this.state.messageID); 353 | 354 | // Delete from server. 355 | this.state.showHidePleaseWait(true); 356 | const imapWorker: IMAP.Worker = new IMAP.Worker(); 357 | await imapWorker.deleteMessage(this.state.messageID, this.state.currentMailbox); 358 | this.state.showHidePleaseWait(false); 359 | 360 | // Remove from list. 361 | const cl = this.state.messages.filter((inElement) => inElement.id != this.state.messageID); 362 | 363 | // Update state. 364 | this.setState({ messages : cl, currentView : "welcome" }); 365 | 366 | }.bind(inParentComponent), /* End deleteMessage(). */ 367 | 368 | 369 | /** 370 | * Delete a message (from the server and the contact list). 371 | */ 372 | sendMessage : async function(): Promise { 373 | 374 | console.log("state.sendMessage()", this.state.messageTo, this.state.messageFrom, this.state.messageSubject, 375 | this.state.messageBody 376 | ); 377 | 378 | // Send the message. 379 | this.state.showHidePleaseWait(true); 380 | const smtpWorker: SMTP.Worker = new SMTP.Worker(); 381 | await smtpWorker.sendMessage(this.state.messageTo, this.state.messageFrom, this.state.messageSubject, 382 | this.state.messageBody 383 | ); 384 | this.state.showHidePleaseWait(false); 385 | 386 | // Update state. 387 | this.setState({ currentView : "welcome" }); 388 | 389 | }.bind(inParentComponent) /* End sendMessage(). */ 390 | 391 | 392 | }; 393 | 394 | } /* End createState(). */ 395 | --------------------------------------------------------------------------------