├── .babelrc ├── .eslintrc ├── .github ├── crush-pics │ └── config.json └── workflows │ ├── auto-merge.yml │ ├── codeql-analysis.yml │ ├── node.js.yml │ ├── optimize-images.yml │ └── ts-nightly-tests.yml ├── .gitignore ├── .mergepal.yml ├── .postcssrc ├── .prettierrc ├── .spelling ├── .vscode └── settings.json ├── API_EXAMPLES.http ├── DockerFile ├── LICENSE ├── README.md ├── assets └── img │ ├── angry-cat.jpg │ ├── avengers.jpg │ ├── boss.jpg │ ├── cat.jpg │ ├── clippy.png │ ├── colonel-meow.jpg │ ├── desk_flip.jpg │ ├── dilbert.jpg │ ├── drstrange.jpg │ ├── ironman.jpg │ ├── jquery.png │ ├── js.png │ ├── linkedin.png │ ├── lisa.jpeg │ ├── maru.jpg │ ├── microsoft.png │ ├── mike.jpeg │ ├── node.png │ ├── office97.png │ ├── thor.jpg │ └── ts.png ├── css └── app.pcss ├── db.json ├── index.html ├── notes ├── 00-intro.md ├── 01-project-tour.md ├── 02-recent-ts-features.md ├── 03-app-vs-library-concerns.md ├── 04-mikes-ts-setup.md ├── 05-what-have-I-done.md ├── 06-converting-to-ts.md ├── 07-dealing-with-pure-type-info.md ├── 08-types-at-runtime.md ├── 09-tests-for-types.md ├── 10-declaration-files.md ├── 11-api-extractor.md ├── README.md └── img │ ├── eslint-error.png │ ├── project_screenshot.png │ └── ts-3-essentials │ ├── slide-018.png │ ├── slide-019.png │ ├── slide-020.png │ └── slide-021.png ├── package.json ├── sandbox.config.json ├── server ├── api-server.js └── index.js ├── src ├── data │ ├── channels.js │ ├── messages.js │ └── teams.js ├── index.js ├── ui │ ├── App.jsx │ └── components │ │ ├── Channel.jsx │ │ ├── Channel │ │ ├── Footer.jsx │ │ ├── Header.jsx │ │ └── Message.jsx │ │ ├── Loading.jsx │ │ ├── SelectedChannel.jsx │ │ ├── SelectedTeam.jsx │ │ ├── Team.jsx │ │ ├── TeamSelector.jsx │ │ ├── TeamSelector │ │ └── TeamLink.jsx │ │ ├── TeamSidebar.jsx │ │ └── TeamSidebar │ │ └── ChannelLink.jsx └── utils │ ├── api.js │ ├── date.js │ ├── deferred.js │ ├── error.js │ ├── http-error.js │ └── networking.js ├── tailwind.config.js ├── tests └── components │ ├── Channel.test.jsx │ ├── ChannelFooter.test.jsx │ ├── ChannelHeader.test.jsx │ ├── ChannelMessage.test.jsx │ ├── TeamSelector.test.jsx │ ├── TeamSidebar.test.jsx │ └── __snapshots__ │ ├── Channel.test.jsx.snap │ ├── ChannelFooter.test.jsx.snap │ ├── ChannelHeader.test.jsx.snap │ ├── ChannelMessage.test.jsx.snap │ ├── TeamSelector.test.jsx.snap │ └── TeamSidebar.test.jsx.snap ├── tsconfig.eslint.json ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { "targets": { "node": "10" } }], 4 | "@babel/preset-react", 5 | "@babel/preset-typescript" 6 | ], 7 | "plugins": ["@babel/plugin-proposal-class-properties"] 8 | } 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2021": true 4 | }, 5 | "settings": { 6 | "react": { 7 | "version": "detect" 8 | } 9 | }, 10 | "extends": [ 11 | "eslint:recommended", 12 | "prettier", 13 | "plugin:promise/recommended", 14 | "plugin:sonarjs/recommended" 15 | ], 16 | "parser": "@typescript-eslint/parser", 17 | "parserOptions": { 18 | "ecmaFeatures": { 19 | "jsx": true 20 | }, 21 | "ecmaVersion": 12, 22 | "project": "tsconfig.eslint.json" 23 | }, 24 | "plugins": [ 25 | "react", 26 | "@typescript-eslint", 27 | "promise", 28 | "sonarjs", 29 | "prettier" 30 | ], 31 | "rules": { 32 | "prefer-const": "error" 33 | }, 34 | "overrides": [ 35 | /** 36 | * CLIENT SIDE CODE 37 | */ 38 | { 39 | "files": ["src/**/*.{ts,js,jsx,tsx}"], 40 | 41 | "env": { 42 | "browser": true, 43 | "es2021": true 44 | }, 45 | "rules": { 46 | "react/prop-types": "off", 47 | "react/no-children-prop": "off" 48 | }, 49 | "extends": [ 50 | "eslint:recommended", 51 | "plugin:react/recommended", 52 | "prettier/react" 53 | ] 54 | }, 55 | /** 56 | * SERVER SIDE CODE 57 | */ 58 | { 59 | "extends": ["plugin:node/recommended"], 60 | "files": [ 61 | "config/**/*.js", 62 | "babel.config.js", 63 | "tailwind.config.js", 64 | "postcss.config.js", 65 | "server/**/*.js" 66 | ], 67 | "env": { "commonjs": true, "node": true } 68 | }, 69 | /** 70 | * TYPESCRIPT CODE 71 | */ 72 | { 73 | "files": ["{src,tests}/**/*.{ts,tsx}"], 74 | "extends": [ 75 | "prettier/@typescript-eslint", 76 | "plugin:@typescript-eslint/recommended", 77 | "plugin:@typescript-eslint/recommended-requiring-type-checking" 78 | ], 79 | "rules": { 80 | "no-unused-vars": "off", 81 | "@typescript-eslint/no-unsafe-call": "off", 82 | "@typescript-eslint/restrict-template-expressions": "off", 83 | "@typescript-eslint/no-unsafe-member-access": "off", 84 | "@typescript-eslint/no-unsafe-assignment": "off", 85 | "@typescript-eslint/no-unsafe-return": "off", 86 | "@typescript-eslint/no-explicit-any": "off" 87 | } 88 | }, 89 | /** 90 | * TESTS 91 | */ 92 | { 93 | "files": ["tests/**/*.{js,jsx,ts,tsx}"], 94 | "extends": [], 95 | "env": { "node": true, "jest": true } 96 | } 97 | ] 98 | } 99 | -------------------------------------------------------------------------------- /.github/crush-pics/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "compression_mode": "balanced", 3 | "compression_level": 85, 4 | "strip_tags": false 5 | } 6 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Auto Merge 2 | 3 | on: 4 | push: {} # update PR when base branch is updated 5 | status: {} # try to merge when other checks are completed 6 | pull_request_review: # try to merge after review 7 | types: 8 | - submitted 9 | - edited 10 | - dismissed 11 | pull_request: # try to merge if labels have changed (white/black list) 12 | types: 13 | - labeled 14 | - unlabeled 15 | 16 | jobs: 17 | # thats's all. single step is needed - if PR is mergeable according to 18 | # branch protection rules it will be merged automatically 19 | mergepal: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v1 23 | - uses: maxkomarychev/merge-pal-action@v0.5.1 24 | with: 25 | token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | branches: [master] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [master] 14 | schedule: 15 | - cron: '0 21 * * 0' 16 | 17 | jobs: 18 | analyze: 19 | name: Analyze 20 | runs-on: ubuntu-latest 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | # Override automatic language detection by changing the below list 26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 27 | language: ['javascript'] 28 | # Learn more... 29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v2 34 | with: 35 | # We must fetch at least the immediate parents so that if this is 36 | # a pull request then we can checkout the head. 37 | fetch-depth: 2 38 | 39 | # If this run was triggered by a pull request event, then checkout 40 | # the head of the pull request instead of the merge commit. 41 | - run: git checkout HEAD^2 42 | if: ${{ github.event_name == 'pull_request' }} 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [master] 9 | pull_request: 10 | branches: [master] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [10.x, 12.x, 14.x] 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v2 23 | - name: Cache Setup 24 | uses: actions/cache@v2 25 | with: 26 | path: | 27 | ~/cache 28 | ~/.dts/ 29 | !~/cache/exclude 30 | **/node_modules 31 | key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} 32 | 33 | - name: Use (Volta) Node.js ${{ matrix.node-version }} 34 | uses: volta-cli/action@v1 35 | with: 36 | node-version: ${{ matrix.node-version }} 37 | - name: Install dependencies 38 | run: yarn 39 | - name: Build 40 | run: yarn build 41 | # - name: Build Docs 42 | # run: yarn api-report && yarn api-docs 43 | - name: Run tests 44 | run: yarn test 45 | -------------------------------------------------------------------------------- /.github/workflows/optimize-images.yml: -------------------------------------------------------------------------------- 1 | name: Crush images 2 | on: 3 | pull_request: 4 | branches: 5 | - master 6 | paths: 7 | - '**.jpg' 8 | - '**.jpeg' 9 | - '**.png' 10 | - '**.gif' 11 | jobs: 12 | crush: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | - name: Crush images 18 | uses: crush-pics/crush-pics-github-action@master 19 | with: 20 | repo-token: ${{ secrets.GITHUB_TOKEN }} 21 | api-key: ${{ secrets.CRUSH_API_KEY }} 22 | -------------------------------------------------------------------------------- /.github/workflows/ts-nightly-tests.yml: -------------------------------------------------------------------------------- 1 | 2 | name: TypeScript@Next tests 3 | 4 | on: 5 | schedule: 6 | # * is a special character in YAML so you have to quote this string 7 | - cron: '20 19 * * *' 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [14.x] 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v2 21 | - name: Cache Setup 22 | uses: actions/cache@v2 23 | with: 24 | path: | 25 | ~/cache 26 | ~/.dts/ 27 | !~/cache/exclude 28 | **/node_modules 29 | key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} 30 | 31 | - name: Use (Volta) Node.js ${{ matrix.node-version }} 32 | uses: volta-cli/action@v1 33 | with: 34 | node-version: ${{ matrix.node-version }} 35 | - name: Install dependencies 36 | run: yarn 37 | - name: Install latest TypeScript nightly 38 | run: yarn add -D typescript@next 39 | - name: Build 40 | run: yarn build 41 | - name: Run tests 42 | run: yarn test 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | out 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and not Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # Stores VSCode versions used for testing VSCode extensions 109 | .vscode-test 110 | 111 | # yarn v2 112 | .yarn/cache 113 | .yarn/unplugged 114 | .yarn/build-state.yml 115 | .yarn/install-state.gz 116 | .pnp.* 117 | 118 | .dist-types 119 | tsdoc-metadata.json 120 | temp -------------------------------------------------------------------------------- /.mergepal.yml: -------------------------------------------------------------------------------- 1 | whitelist: 2 | - merge-when-green 3 | blacklist: 4 | - wip 5 | - do-not-merge 6 | method: squash -------------------------------------------------------------------------------- /.postcssrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "tailwindcss": {}, 4 | "autoprefixer": {} 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "arrowParens": "always", 5 | "trailingComma": "all", 6 | "quoteProps": "consistent", 7 | "printWidth": 70 8 | } 9 | -------------------------------------------------------------------------------- /.spelling: -------------------------------------------------------------------------------- 1 | # markdown-spellcheck spelling configuration file 2 | # Format - lines beginning # are comments 3 | # global dictionary is at the start, file overrides afterwards 4 | # one word per line, to define a file override use ' - filename' 5 | # where filename is relative to this configuration file 6 | linter 7 | ts 8 | nullish 9 | falsy 10 | WeakMap 11 | variadic 12 | vscode 13 | JSDoc 14 | superset 15 | js 16 | UX 17 | runtime 18 | namespace 19 | JSON 20 | codebases 21 | i.e. 22 | refactorings 23 | es2020 24 | es2017 25 | APIs 26 | enums 27 | SemVer 28 | `.gitignore` 29 | volta 30 | tsconfig 31 | CommonJS 32 | es2018 33 | eslint 34 | config 35 | params 36 | npm 37 | webpack 38 | truthy 39 | ok 40 | fall-throughs 41 | codebase 42 | JSdoc 43 | CJS 44 | ESM 45 | interop 46 | node_modules 47 | Node.js 48 | codemod 49 | dtslint 50 | DefinitelyTyped 51 | stringified 52 | tslint 53 | subfolder 54 | tsd 55 | README 56 | initializers 57 | enum 58 | reassignable 59 | lodash 60 | React.js 61 | versioning 62 | tsx 63 | tsconfig.json 64 | rollup 65 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.enable": true, 3 | "files.associations": { 4 | "api-extractor.json": "jsonc" 5 | }, 6 | "vsicons.associations.files": [ 7 | { 8 | "icon": "tsconfig", 9 | "extensions": ["tsconfig.apidocs.json"], 10 | "filename": true, 11 | "format": "svg" 12 | } 13 | ], 14 | "json.schemas": [ 15 | { 16 | "fileMatch": ["tsconfig.apidocs.json"], 17 | 18 | "url": "https://json.schemastore.org/tsconfig" 19 | } 20 | ], 21 | "typescript.tsdk": "node_modules/typescript/lib" 22 | } 23 | -------------------------------------------------------------------------------- /API_EXAMPLES.http: -------------------------------------------------------------------------------- 1 | 2 | ### Get all users 3 | GET http://localhost:1234/api/users HTTP/1.1 4 | 5 | ### Get one user 6 | GET http://localhost:1234/api/users/1 HTTP/1.1 7 | 8 | ### Get all teams 9 | GET http://localhost:1234/api/teams HTTP/1.1 10 | 11 | ### Get a team (includes channels) 12 | GET http://localhost:1234/api/teams/li HTTP/1.1 13 | 14 | ### Get team channel messages 15 | GET http://localhost:1234/api/teams/li/channels/general/messages HTTP/1.1 16 | 17 | ### Create a new message in a team channel 18 | POST http://localhost:1234/api/messages HTTP/1.1 19 | Content-Type: application/json 20 | 21 | { 22 | "teamId": "li", 23 | "channelId": "general", 24 | "userId": 1, 25 | "body": "Hi everyone!" 26 | } 27 | 28 | ### Delete a message 29 | DELETE http://localhost:1234/api/messages/19 HTTP/1.1 30 | Content-Type: application/json 31 | -------------------------------------------------------------------------------- /DockerFile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/DockerFile -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 LinkedIn All rights reserved. 2 | 3 | Redistribution and use in 4 | source and binary forms, with or without modification, are permitted provided 5 | that the following conditions are met: 6 | 7 | 1. Redistributions of source code must 8 | retain the above copyright notice, this list of conditions and the following 9 | disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | THIS 16 | SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 17 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 20 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS 22 | OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 23 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 24 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN 25 | IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Professional TypeScript 2 | 3 |  4 |  5 | 6 | ## What's this course about? 7 | 8 | This course is intended to help those already somewhat familiar with TypeScript as a programming language, to the point where they're more ready to use it in a real situation with real stakes. Mike shares some of the experience he's had as LinkedIn's TypeScript infrastructure lead, so you don't have to learn things "the hard way" like he did. 9 | 10 | ## Project setup 11 | 12 | First, you should ensure you have [your ssh keys working with GitHub](https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent). You can verify this by running 13 | 14 | ```sh 15 | ssh git@github.com 16 | ``` 17 | 18 | and getting a response like 19 | 20 | ```sh 21 | Hi mike-north! You've successfully authenticated, but GitHub does not provide shell access. 22 | Connection to github.com closed. 23 | ``` 24 | 25 | ### Tools you MUST have installed 26 | 27 | Next, make sure you have installed [volta](http://volta.sh/) which ensures you have the right version of node and yarn for this project 28 | 29 | We also strongly recommend the use of [Visual Studio Code](https://code.visualstudio.com/) as an authoring tool. If you use something else, you're on your own. 30 | 31 | ### Clone 32 | 33 | Next, checkout a working copy of this project 34 | 35 | ```sh 36 | git clone git@github.com:mike-north/professional-ts 37 | ``` 38 | 39 | enter the directory you just created 40 | 41 | ```sh 42 | cd professional-ts 43 | ``` 44 | 45 | ### Install dependencies 46 | 47 | [`yarn`](https://yarnpkg.com/) is the recommended package manager to use with this project. Please use it instead of npm. 48 | 49 | Install dependencies with yarn by running 50 | 51 | ```sh 52 | yarn 53 | ``` 54 | 55 | ### Starting the project 56 | 57 | Start up the project in development mode by running 58 | 59 | ```sh 60 | yarn dev 61 | ``` 62 | 63 | Changing any files in the `src` folder will result in an incremental rebuild, and a refresh of the screen. 64 | 65 | By default, the app is served on http://localhost:3000. 66 | 67 | # Legal 68 | 69 | © 2020 LinkedIn, All Rights Reserved 70 | 71 | ## Licensing 72 | 73 | The code in this project is licensed as [BSD-2-Clause](https://opensource.org/licenses/BSD-2-Clause) license, and the written content in the ./notes folder is licensed under [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/) 74 | -------------------------------------------------------------------------------- /assets/img/angry-cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/angry-cat.jpg -------------------------------------------------------------------------------- /assets/img/avengers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/avengers.jpg -------------------------------------------------------------------------------- /assets/img/boss.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/boss.jpg -------------------------------------------------------------------------------- /assets/img/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/cat.jpg -------------------------------------------------------------------------------- /assets/img/clippy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/clippy.png -------------------------------------------------------------------------------- /assets/img/colonel-meow.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/colonel-meow.jpg -------------------------------------------------------------------------------- /assets/img/desk_flip.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/desk_flip.jpg -------------------------------------------------------------------------------- /assets/img/dilbert.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/dilbert.jpg -------------------------------------------------------------------------------- /assets/img/drstrange.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/drstrange.jpg -------------------------------------------------------------------------------- /assets/img/ironman.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/ironman.jpg -------------------------------------------------------------------------------- /assets/img/jquery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/jquery.png -------------------------------------------------------------------------------- /assets/img/js.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/js.png -------------------------------------------------------------------------------- /assets/img/linkedin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/linkedin.png -------------------------------------------------------------------------------- /assets/img/lisa.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/lisa.jpeg -------------------------------------------------------------------------------- /assets/img/maru.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/maru.jpg -------------------------------------------------------------------------------- /assets/img/microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/microsoft.png -------------------------------------------------------------------------------- /assets/img/mike.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/mike.jpeg -------------------------------------------------------------------------------- /assets/img/node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/node.png -------------------------------------------------------------------------------- /assets/img/office97.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/office97.png -------------------------------------------------------------------------------- /assets/img/thor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/thor.jpg -------------------------------------------------------------------------------- /assets/img/ts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/ts.png -------------------------------------------------------------------------------- /css/app.pcss: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .hover-target .show-on-hover { 6 | opacity: 0; 7 | filter: alpha(opacity=0); 8 | } 9 | .hover-target:hover .show-on-hover, 10 | .hover-target .show-on-hover:focus, 11 | .hover-target .show-on-hover:active { 12 | opacity: 1; 13 | filter: alpha(opacity=1); 14 | } 15 | 16 | .sr-only { 17 | clip-path: inset(50%); 18 | clip: rect(1px, 1px, 1px, 1px); 19 | height: 1px; 20 | margin: -1px; 21 | overflow: hidden; 22 | padding: 0; 23 | position: absolute; 24 | width: 1px; 25 | } 26 | -------------------------------------------------------------------------------- /db.json: -------------------------------------------------------------------------------- 1 | { 2 | "teams": [ 3 | { 4 | "id": "linkedin", 5 | "name": "LinkedIn", 6 | "order": 2, 7 | "iconUrl": "/assets/img/linkedin.png" 8 | }, 9 | { 10 | "id": "ms", 11 | "name": "Microsoft", 12 | "order": 3, 13 | "iconUrl": "/assets/img/microsoft.png" 14 | }, 15 | { 16 | "id": "avengers", 17 | "name": "Avengers", 18 | "order": 4, 19 | "iconUrl": "/assets/img/avengers.jpg" 20 | }, 21 | { 22 | "id": "angrycat", 23 | "name": "Angry Cat", 24 | "order": 5, 25 | "iconUrl": "/assets/img/angry-cat.jpg" 26 | }, 27 | { 28 | "id": "javascript", 29 | "name": "Javascript", 30 | "order": 6, 31 | "iconUrl": "/assets/img/js.png" 32 | } 33 | ], 34 | "users": [ 35 | { 36 | "id": 0, 37 | "name": "Dilbert", 38 | "username": "dilbert", 39 | "iconUrl": "/assets/img/dilbert.jpg" 40 | }, 41 | { 42 | "id": 1, 43 | "name": "Mike North", 44 | "username": "mike", 45 | "iconUrl": "/assets/img/mike.jpeg" 46 | }, 47 | { 48 | "id": 2, 49 | "name": "Lisa Huang-North", 50 | "username": "lisa", 51 | "iconUrl": "/assets/img/lisa.jpeg" 52 | }, 53 | { 54 | "id": 3, 55 | "name": "Clippy", 56 | "username": "clippy", 57 | "iconUrl": "/assets/img/clippy.png" 58 | }, 59 | { 60 | "id": 4, 61 | "name": "office97", 62 | "username": "office", 63 | "iconUrl": "/assets/img/office97.png" 64 | }, 65 | { 66 | "id": 5, 67 | "name": "Tony Stark", 68 | "username": "ironman", 69 | "iconUrl": "/assets/img/ironman.jpg" 70 | }, 71 | { 72 | "id": 6, 73 | "name": "Thor", 74 | "username": "thor", 75 | "iconUrl": "/assets/img/thor.jpg" 76 | }, 77 | { 78 | "id": 7, 79 | "name": "Dr Stephen Strange", 80 | "username": "strange", 81 | "iconUrl": "/assets/img/drstrange.jpg" 82 | }, 83 | { 84 | "id": 8, 85 | "name": "Lil Bub", 86 | "username": "bub", 87 | "iconUrl": "/assets/img/cat.jpg" 88 | }, 89 | { 90 | "id": 9, 91 | "name": "Colonel Meow", 92 | "username": "meow", 93 | "iconUrl": "/assets/img/colonel-meow.jpg" 94 | }, 95 | { 96 | "id": 10, 97 | "name": "Maru", 98 | "username": "maru", 99 | "iconUrl": "/assets/img/maru.jpg" 100 | }, 101 | { 102 | "id": 11, 103 | "name": "NodeJS", 104 | "username": "nodejs", 105 | "iconUrl": "/assets/img/node.png" 106 | }, 107 | { 108 | "id": 12, 109 | "name": "Engineer Anon", 110 | "username": "anonymous", 111 | "iconUrl": "/assets/img/desk_flip.jpg" 112 | }, 113 | { 114 | "id": 13, 115 | "name": "Typescript", 116 | "username": "typescript", 117 | "iconUrl": "/assets/img/ts.png" 118 | }, 119 | { 120 | "id": 14, 121 | "name": "jQuery", 122 | "username": "jquery", 123 | "iconUrl": "/assets/img/jquery.png" 124 | }, 125 | { 126 | "id": 15, 127 | "name": "boss man", 128 | "username": "boss", 129 | "iconUrl": "/assets/img/boss.jpg" 130 | } 131 | ], 132 | "channels": [ 133 | { 134 | "id": "recruiting", 135 | "name": "recruiting", 136 | "description": "The Next Generation Of Recruiting. Find top talents today!", 137 | "teamId": "linkedin" 138 | }, 139 | { 140 | "id": "general", 141 | "name": "general", 142 | "description": "LinkedIn general chat", 143 | "teamId": "linkedin" 144 | }, 145 | { 146 | "id": "jobs", 147 | "name": "Job hunting", 148 | "description": "Discover companies that fit you.", 149 | "teamId": "linkedin" 150 | }, 151 | { 152 | "id": "tbt", 153 | "name": "throw back thursday", 154 | "description": "Remember the good old days? yay, us too.", 155 | "teamId": "ms" 156 | }, 157 | { 158 | "id": "endgame", 159 | "name": "top secret", 160 | "description": "for your eyes only", 161 | "teamId": "avengers" 162 | }, 163 | { 164 | "id": "dominate", 165 | "name": "catnip", 166 | "description": "exchange tips and best practicse on world domination", 167 | "teamId": "angrycat", 168 | "isDisabled": true 169 | }, 170 | { 171 | "id": "funny", 172 | "name": "funny", 173 | "description": "think you got what it takes? Share your best memes / jokes here!", 174 | "teamId": "javascript" 175 | } 176 | ], 177 | "messages": [ 178 | { 179 | "teamId": "linkedin", 180 | "channelId": "compliments", 181 | "userId": 2, 182 | "createdAt": "2019-04-21T17:48:33.421Z", 183 | "body": "Your penmanship is excellent!", 184 | "id": 1 185 | }, 186 | { 187 | "teamId": "linkedin", 188 | "channelId": "compliments", 189 | "userId": 1, 190 | "createdAt": "2019-04-21T17:54:38.556Z", 191 | "body": "I admire your punctuality", 192 | "id": 2 193 | }, 194 | { 195 | "teamId": "linkedin", 196 | "channelId": "general", 197 | "userId": 2, 198 | "createdAt": "2019-04-21T17:55:08.713Z", 199 | "body": "Hello shlack!", 200 | "id": 3 201 | }, 202 | { 203 | "teamId": "linkedin", 204 | "channelId": "general", 205 | "userId": 1, 206 | "createdAt": "2019-04-21T18:36:30.995Z", 207 | "body": "awda", 208 | "id": 11 209 | }, 210 | { 211 | "teamId": "linkedin", 212 | "channelId": "general", 213 | "userId": 1, 214 | "createdAt": "2019-04-21T18:40:34.648Z", 215 | "body": "wad", 216 | "id": 12 217 | }, 218 | { 219 | "teamId": "linkedin", 220 | "channelId": "general", 221 | "userId": 1, 222 | "body": "wdw", 223 | "createdAt": 1555872178322, 224 | "id": 13 225 | }, 226 | { 227 | "teamId": "linkedin", 228 | "channelId": "general", 229 | "userId": 1, 230 | "body": "awqwdqwqwdq", 231 | "enrichedBody": ["awqwdqwqwdq"], 232 | "createdAt": 1555872270175, 233 | "id": 14 234 | }, 235 | { 236 | "teamId": "linkedin", 237 | "channelId": "general", 238 | "userId": 1, 239 | "body": "qdq", 240 | "createdAt": 1555872790592, 241 | "id": 15 242 | }, 243 | { 244 | "teamId": "linkedin", 245 | "channelId": "general", 246 | "userId": 1, 247 | "body": "w", 248 | "createdAt": 1555872792437, 249 | "id": 16 250 | }, 251 | { 252 | "teamId": "linkedin", 253 | "channelId": "general", 254 | "userId": 2, 255 | "body": "Would you like to join my professional network?", 256 | "createdAt": 1555874498634, 257 | "id": 17 258 | }, 259 | { 260 | "teamId": "linkedin", 261 | "channelId": "general", 262 | "userId": 1, 263 | "body": "Hello developer, I looked at your profile and am impressed by your 14 years of COBOL experience. Are you happy in your current role?", 264 | "createdAt": 1555874584752, 265 | "id": 18 266 | }, 267 | { 268 | "channelId": "bofny", 269 | "teamId": "avengers", 270 | "body": "Hey dudes", 271 | "userId": "3", 272 | "createdAt": 1556676377508, 273 | "id": 19 274 | }, 275 | { 276 | "channelId": "funny", 277 | "teamId": "javascript", 278 | "body": "\"How do you comfort a JavaScript bug?\" ", 279 | "userId": "14", 280 | "createdAt": 1556679721022, 281 | "id": 20 282 | }, 283 | { 284 | "channelId": "funny", 285 | "teamId": "javascript", 286 | "body": "I dunno.. you de-bug it?", 287 | "userId": "12", 288 | "createdAt": 1556679740793, 289 | "id": 21 290 | }, 291 | { 292 | "channelId": "funny", 293 | "teamId": "javascript", 294 | "body": "No man, You console it", 295 | "userId": "14", 296 | "createdAt": 1556679745885, 297 | "id": 22 298 | }, 299 | { 300 | "channelId": "funny", 301 | "teamId": "javascript", 302 | "body": " Why was the JavaScript developer sad?", 303 | "userId": "14", 304 | "createdAt": 1556679754017, 305 | "id": 23 306 | }, 307 | { 308 | "channelId": "funny", 309 | "teamId": "javascript", 310 | "body": "Because there are too many JS frameworks!", 311 | "userId": "11", 312 | "createdAt": 1556679782382, 313 | "id": 24 314 | }, 315 | { 316 | "channelId": "funny", 317 | "teamId": "javascript", 318 | "body": "Wrong! It's because he didn’t Node how to Express himself", 319 | "userId": "14", 320 | "createdAt": 1556679797050, 321 | "id": 25 322 | }, 323 | { 324 | "channelId": "funny", 325 | "teamId": "javascript", 326 | "body": "ha-ha", 327 | "userId": "11", 328 | "createdAt": 1556679800867, 329 | "id": 26 330 | }, 331 | { 332 | "channelId": "funny", 333 | "teamId": "javascript", 334 | "body": "Ok here's one: Why do C# and Java developers keep breaking their keyboards?", 335 | "userId": "12", 336 | "createdAt": 1556679820803, 337 | "id": 27 338 | }, 339 | { 340 | "channelId": "funny", 341 | "teamId": "javascript", 342 | "body": "Mmm... because one of them tried to write a Hello World application in Java EE?", 343 | "userId": "14", 344 | "createdAt": 1556679939014, 345 | "id": 28 346 | }, 347 | { 348 | "channelId": "funny", 349 | "teamId": "javascript", 350 | "body": "Nah, it's because they both use strongly typed language", 351 | "userId": "12", 352 | "createdAt": 1556680157584, 353 | "id": 29 354 | }, 355 | { 356 | "channelId": "funny", 357 | "teamId": "javascript", 358 | "body": "Seriously.", 359 | "userId": "13", 360 | "createdAt": 1556680172998, 361 | "id": 30 362 | }, 363 | { 364 | "channelId": "endgame", 365 | "teamId": "avengers", 366 | "body": "Hey... has someone seen my hammer? I can't find it and I need a can opener...", 367 | "userId": "6", 368 | "createdAt": 1556680603106, 369 | "id": 31 370 | }, 371 | { 372 | "channelId": "endgame", 373 | "teamId": "avengers", 374 | "body": "No, if you remember the exact date and time you last used it, I can open a time portal for you", 375 | "userId": "7", 376 | "createdAt": 1556680649251, 377 | "id": 32 378 | }, 379 | { 380 | "channelId": "endgame", 381 | "teamId": "avengers", 382 | "body": "I'm pretty sure I saw whats-his-name take it. You might want to work out more or the hammer might choose *him* instead", 383 | "userId": "5", 384 | "createdAt": 1556680753815, 385 | "id": 33 386 | }, 387 | { 388 | "channelId": "endgame", 389 | "teamId": "avengers", 390 | "body": "NO! I SHALL BRING THUNDER DOWN ON HIM!", 391 | "userId": "6", 392 | "createdAt": 1556680812491, 393 | "id": 34 394 | }, 395 | { 396 | "channelId": "endgame", 397 | "teamId": "avengers", 398 | "body": "With which hand? Are you giving up the beer or Fortnite?", 399 | "userId": "5", 400 | "createdAt": 1556680973259, 401 | "id": 35 402 | }, 403 | { 404 | "channelId": "dominate", 405 | "teamId": "angrycat", 406 | "body": "Meowhahaha! I crossed the 800K followers mark today! One step closer to world domination 😈", 407 | "userId": "8", 408 | "createdAt": 1556681392393, 409 | "id": 36 410 | }, 411 | { 412 | "channelId": "dominate", 413 | "teamId": "angrycat", 414 | "body": "What did you cats get up to today?", 415 | "userId": "8", 416 | "createdAt": 1556681406684, 417 | "id": 37 418 | }, 419 | { 420 | "channelId": "dominate", 421 | "teamId": "angrycat", 422 | "body": "Oh and Penguin approached me for another book deal today. Apparently my last book has been hailed \"a literary classic\"...", 423 | "userId": "8", 424 | "createdAt": 1556681504008, 425 | "id": 38 426 | }, 427 | { 428 | "channelId": "dominate", 429 | "teamId": "angrycat", 430 | "body": "Can you believe these human?", 431 | "userId": "8", 432 | "createdAt": 1556681514248, 433 | "id": 39 434 | }, 435 | { 436 | "channelId": "dominate", 437 | "teamId": "angrycat", 438 | "body": "Pfff, 800K on what network? I have 400K fans on Facebook and they hail me as their \"fearsome dictator\", a \"prodigious Scotch drinker\" and \"the angriest cat in the world\"", 439 | "userId": "9", 440 | "createdAt": 1556681660966, 441 | "id": 40 442 | }, 443 | { 444 | "channelId": "dominate", 445 | "teamId": "angrycat", 446 | "body": "AND I hold a Guinness world record 🏆, beat that Bub 🐈", 447 | "userId": "9", 448 | "createdAt": 1556681701911, 449 | "id": 41 450 | }, 451 | { 452 | "channelId": "dominate", 453 | "teamId": "angrycat", 454 | "body": "*stretches* not that anyone's counting, but my videos have been been viewed over 325 million times on Youtube. ", 455 | "userId": "10", 456 | "createdAt": 1556681886829, 457 | "id": 42 458 | }, 459 | { 460 | "channelId": "dominate", 461 | "teamId": "angrycat", 462 | "body": "Holding a Guinness World Record doesn't get us closer to world domination, meow. We need to think outside the box", 463 | "userId": "10", 464 | "createdAt": 1556681916083, 465 | "id": 43 466 | }, 467 | { 468 | "channelId": "dominate", 469 | "teamId": "angrycat", 470 | "body": "But I looooooove cupboard boxes! ", 471 | "userId": "8", 472 | "createdAt": 1556681928716, 473 | "id": 44 474 | }, 475 | { 476 | "channelId": "tbt", 477 | "teamId": "ms", 478 | "body": "Sigh, I miss having a job...", 479 | "userId": "3", 480 | "createdAt": 1556682112281, 481 | "id": 45 482 | }, 483 | { 484 | "channelId": "tbt", 485 | "teamId": "ms", 486 | "body": "It was so nice to \"talk\" and help people around the world with their spelling mistakes!", 487 | "userId": "3", 488 | "createdAt": 1556682143644, 489 | "id": 46 490 | }, 491 | { 492 | "channelId": "tbt", 493 | "teamId": "ms", 494 | "body": "Don't you miss it?", 495 | "userId": "3", 496 | "createdAt": 1556682148707, 497 | "id": 47 498 | }, 499 | { 500 | "channelId": "tbt", 501 | "teamId": "ms", 502 | "body": "Tell me about it, they were so happy when they found the pinball game in Word 97", 503 | "userId": "4", 504 | "createdAt": 1556682251128, 505 | "id": 48 506 | }, 507 | { 508 | "channelId": "tbt", 509 | "teamId": "ms", 510 | "body": "OHH and that flight simulator game in Excel! People always tried to shoot me for some reason", 511 | "userId": "3", 512 | "createdAt": 1556682314145, 513 | "id": 49 514 | }, 515 | { 516 | "channelId": "tbt", 517 | "teamId": "ms", 518 | "body": "At least people remembers you and try to bring you back. I don't think anyone even use floppy disks installation any more...", 519 | "userId": "4", 520 | "createdAt": 1556682392864, 521 | "id": 50 522 | }, 523 | { 524 | "channelId": "tbt", 525 | "teamId": "ms", 526 | "body": "I don't miss Windows 95 though. Dang he was slow!", 527 | "userId": "4", 528 | "createdAt": 1556682501011, 529 | "id": 51 530 | }, 531 | { 532 | "channelId": "recruiting", 533 | "teamId": "linkedin", 534 | "body": "Hey guys, looking for a ninja rockstar software engineer. Must have 15 years of Rust experience and willing to take 80% compensation in equity. We have free lunches and table tennis in the office.", 535 | "userId": "15", 536 | "createdAt": 1556682816711, 537 | "id": 52 538 | }, 539 | { 540 | "channelId": "recruiting", 541 | "teamId": "linkedin", 542 | "body": "Oh and the position is in Antarctica. Low income tax rate, free snow cones and the skiing is great!", 543 | "userId": "15", 544 | "createdAt": 1556682870302, 545 | "id": 53 546 | }, 547 | { 548 | "channelId": "jobs", 549 | "teamId": "linkedin", 550 | "body": "LF new position. Our tech lead just decided to go with a COBOL/CoffeeScript stack and I'm getting out of here!!", 551 | "userId": "2", 552 | "createdAt": 1556682931336, 553 | "id": 54 554 | }, 555 | { 556 | "channelId": "recruiting", 557 | "teamId": "linkedin", 558 | "body": "#ask Does anyone know the difference between Java and Javascript? ", 559 | "userId": "0", 560 | "createdAt": 1556682976168, 561 | "id": 55 562 | }, 563 | { 564 | "channelId": "recruiting", 565 | "teamId": "linkedin", 566 | "body": "Ah nvm, probably doesn't matter. I'll just spray and pray", 567 | "userId": "0", 568 | "createdAt": 1556682987555, 569 | "id": 56 570 | }, 571 | { 572 | "channelId": "jobs", 573 | "teamId": "linkedin", 574 | "body": "Somebody just gave me a LinkedIn endorsement for \"Copying & Pasting from Stackoverflow\" and \"Private API usage.\" Are they trying to tell me something?", 575 | "userId": "1", 576 | "createdAt": 1556683077121, 577 | "id": 57 578 | } 579 | ] 580 | } 581 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 |67 | Next: Project Tour ▶ 68 |
69 | -------------------------------------------------------------------------------- /notes/01-project-tour.md: -------------------------------------------------------------------------------- 1 |2 | ◀ Back: Intro 3 |
4 | 5 | --- 6 | 7 | # Project tour and getting started 8 | 9 | In this workshop we'll be working in the context of a simplified Slack app 10 | 11 |  12 | 13 | As we begin the course, it's written entirely in JavaScript, and is comprised of a few mostly stateless React components. 14 | 15 | The web client for this app lives in [`src/`](../src/), and is roughly organized as 16 | 17 | ```bash 18 | src/ # web client 19 | data/ # data layer 20 | ui/ # react components 21 | utils/ # low-level utilities 22 | index.js # entry point 23 | ``` 24 | 25 | There's a API and database in this project as well, but we won't be changing them during the workshop. 26 | 27 | The database comes from your [`db.json`](../db.json) file, and the API code is located in the [`server/`](../server/) folder. 28 | 29 | **One thing you _absolutely will_ want to look at is the API documentation, which can be found in your [`API_EXAMPLES.http`](API_EXAMPLES.http) file**. 30 | 31 | --- 32 | 33 |34 | Next: Recent TS Language Features ▶ 35 |
36 | -------------------------------------------------------------------------------- /notes/02-recent-ts-features.md: -------------------------------------------------------------------------------- 1 |2 | ◀ Back: Project Tour 3 |
4 | 5 | --- 6 | 7 | # TS 3.7 -> 4.1: Quick Summary 8 | 9 | ## Modern JS Language Features 10 | 11 | ### Optional Chaining (3.7) 12 | 13 | Stops running expression if a null or undefined is encountered 14 | 15 | - 🚧 **Warning:** adds complexity. Beware of multiple optional links in a single expression 16 | - 🚧 **Warning:** not appropriate to just “power through” nullish values 17 | - 🚧 **Warning:** only short-circuits the expression in question, not any other compound expressions joined by operators 18 | 19 | ### Nullish Coalescing (3.7) 20 | 21 | Useful for fallback defaults, where a user-supplied 0 or ‘’ would have been treated improperly 22 | 23 | - 🚧 **Warning:** not the same as ||, due to falsy values that aren’t nullish 24 | 25 | ### ECMAScript Private Fields (3.8) 26 | 27 | ```js 28 | class Foo { 29 | #bar; 30 | } 31 | ``` 32 | 33 | - “hard private” (undetectable from the outside) 34 | - Cannot be overridden by subclasses 35 | - Use WeakMap under the hood, so might perform differently compared to public class fields (not a problem unless you’re on a really hot path) 36 | 37 | ### Namespace exports (3.8) 38 | 39 | ```ts 40 | export * as utils from './utils.mjs'; 41 | ``` 42 | 43 | ### Inference of class field types (4.0) 44 | 45 | Types for class fields that are assigned in constructor are inferred, and no longer need an explicit type declaration 46 | 47 | ## Tuple Types 48 | 49 | ### Variadic tuple types (4.0) 50 | 51 | ```ts 52 | type StrStrNumNumBool = [...Strings, ...Numbers, boolean]; 53 | ``` 54 | 55 | ### Labeled tuple types (4.0) 56 | 57 | ```ts 58 | function foo(x: [first: string, second: number]) {} 59 | ``` 60 | 61 | ## More powerful type aliases and interfaces 62 | 63 | ### Recursive type aliases (3.7) 64 | 65 | JSON can now be typed in a single type alias! 66 | 67 | ### Recursive conditional types (4.1) 68 | 69 | ```ts 70 | type ElementType144 | Next: App vs. Library Concerns ▶ 145 |
146 | -------------------------------------------------------------------------------- /notes/03-app-vs-library-concerns.md: -------------------------------------------------------------------------------- 1 |2 | Next: Recent TS Language Features ▶ 3 |
4 | 5 | --- 6 | 7 | # TypeScript: App vs. Library Concerns 8 | 9 | ## Myths (almost always) 10 | 11 | - ❌ "No more runtime errors" 12 | - ❌ "My code will run measurably faster" 13 | 14 | ## Most TS Codebases 15 | 16 | - **Improved developer experience**, including in-editor docs for your dependencies 17 | - **Reduced need to "drill into” files** to understand how adjacent code works 18 | - **Micro “rigor” that adds up to macro benefits** 19 | I can type things well-enough and let the system help me when I cross the threshold beyond which I “remember” what’s going on, and what types a particular value could be 20 | - Developers can encode more of their intent 21 | - More formalized and stable contracts “between things” (i.e., one component to another) 22 | - **Coloring inside the lines** 23 | > - Stay on the public API surface of your dependencies 24 | > - Catch incomplete refactorings 25 | > - Some subset of runtime errors moved to compile time 26 | > - Using an ES2020 feature while needing to support ES2017 27 | > - Momentarily forgetting about browser or node APIs 28 | 29 | ## (mostly) App-specific concerns 30 | 31 | - More richness when working with data 32 | - Better encapsulation tools, to facilitate maintaining lazy loading boundaries 33 | - Improved “major version upgrade” story for typed libraries 34 | 35 | ## (mostly) Library-specific concerns 36 | 37 | - Create and maintain a deliberate public API surface 38 | ...while still being able to create a private API surface to use between modules or components 39 | - Keep your users on track (i.e., enums allow you to signal allowed value sets better than `number`) 40 | - SemVer (deprecations, breakage) 41 | - API Docs 42 | 43 | --- 44 | 45 |46 | Next: Mike's Professional-Grade TS Setup ▶ 47 |
48 | -------------------------------------------------------------------------------- /notes/04-mikes-ts-setup.md: -------------------------------------------------------------------------------- 1 |2 | ◀ Back: App vs. Library Concerns 3 |
4 | 5 | --- 6 | 7 | # Mike's "bare bones" TS setup 8 | 9 | In this part of the workshop, we'll create a new small library from nothing, so you can see how Mike's "lots of value out of few tools" approach keeps things nice and simple. 10 | 11 | ## Getting Started 12 | 13 | First, create a new directory and enter it 14 | 15 | ```sh 16 | mkdir my-lib 17 | cd my-lib 18 | ``` 19 | 20 | Then, create a `.gitignore` file 21 | 22 | ```sh 23 | npx gitignore node 24 | ``` 25 | 26 | and a package.json file 27 | 28 | ```sh 29 | yarn init --yes 30 | ``` 31 | 32 | Make a few direct modifications to your `package.json` file as follows 33 | 34 | ```diff 35 | --- a/package.json 36 | +++ b/package.json 37 | @@ -1,6 +1,13 @@ 38 | { 39 | "name": "my-lib", 40 | "version": "1.0.0", 41 | - "main": "index.js", 42 | + "main": "dist/index.js", 43 | + "types": "dist/index.d.ts", 44 | + "scripts": { 45 | + "build": "tsc", 46 | + "dev": "yarn build --watch --preserveWatchOutput", 47 | + "lint": "eslint src --ext js,ts", 48 | + "test": "jest" 49 | + }, 50 | "license": "MIT" 51 | } 52 | ``` 53 | 54 | This ensures that TS and non-TS consumers alike can use this library, and that we can run the following commands 55 | 56 | ```sh 57 | yarn build # build the project 58 | yarn dev # build, and rebuild when source is changed 59 | yarn lint # run the linter 60 | yarn test # run tests 61 | ``` 62 | 63 | Pin the node and yarn versions to their current stable releases using volta 64 | 65 | ```sh 66 | volta pin node yarn 67 | ``` 68 | 69 | this will add `node` and `yarn` versions to your `package.json` automatically. 70 | 71 | Next, initialize the git repository 72 | 73 | ```sh 74 | git init 75 | ``` 76 | 77 | ## TypeScript Compiler 78 | 79 | install typescript as a development dependency. We'll only need this at build 80 | time, because not all consumers of this library may be using TypeScript 81 | themselves. 82 | 83 | ```sh 84 | yarn add -D typescript 85 | ``` 86 | 87 | ## Setting up your tsconfig 88 | 89 | Create a default `tsconfig.json` 90 | 91 | ```sh 92 | yarn tsc --init 93 | ``` 94 | 95 | and ensure the following values are set: 96 | 97 | ```diff 98 | "compilerOptions": { 99 | + "outDir": "dist", 100 | + "rootDirs": ["src"], 101 | }, 102 | + "include": ["src"] 103 | ``` 104 | 105 | We want to make sure that the `src/` folder is where our source code lives, that it's treated as a root directory, and that the compiled output is in the `dist/` folder. 106 | 107 | Next, make sure that the TS compiler creates Node-friendly CommonJS modules, and that we target the ES2018 language level (Node 10, allowing for features like `async` and `await`). 108 | 109 | ```diff 110 | "compilerOptions": { 111 | + "module": "commonjs", 112 | + "target": "ES2018", 113 | } 114 | ``` 115 | 116 | Let's make sure two potentially problematic features are _disabled_. We'll talk later about why these are not great for a library. 117 | 118 | ```diff 119 | "compilerOptions": { 120 | + "esModuleInterop": false, 121 | + "skipLibCheck": false 122 | } 123 | ``` 124 | 125 | Make sure that the compiler outputs ambient type information in addition to the JavaScript 126 | 127 | ```diff 128 | "compilerOptions": { 129 | + "declaration": true, 130 | } 131 | ``` 132 | 133 | And finally, let's make sure that we set up an "extra strict" type-checking configuration 134 | 135 | ```diff 136 | "compilerOptions": { 137 | /** 138 | * "strict": true, 139 | * ------------------- 140 | * - noImplicitAny 141 | * - strictNullChecks 142 | * - strictFunctionTypes 143 | * - strictBindCallApply 144 | * - strictPropertyInitialization 145 | * - noImplicitThis 146 | * - alwaysStrict 147 | */ 148 | + "strict": true, 149 | + "noUnusedLocals": true, 150 | + "noImplicitReturns": true, 151 | + "stripInternal": true, 152 | + "types": [], 153 | + "forceConsistentCasingInFileNames": true, 154 | } 155 | ``` 156 | 157 | We'll go in to more detail later about what some of these options mean, and why I suggest setting them this way. 158 | 159 | Finally, please create a folder for your source code, and create an empty `index.ts` file within it 160 | 161 | ```sh 162 | mkdir src 163 | touch src/index.ts 164 | ``` 165 | 166 | Open `src/index.ts` and set its contents to the following 167 | 168 | ```ts 169 | /** 170 | * @packageDocumentation A small library for common math functions 171 | */ 172 | 173 | /** 174 | * Calculate the average of three numbers 175 | * 176 | * @param a - first number 177 | * @param b - second number 178 | * @param c - third number 179 | * 180 | * @public 181 | */ 182 | export function avg(a: number, b: number, c: number): number { 183 | return sum3(a, b, c) / 3; 184 | } 185 | 186 | /** 187 | * Calculate the sum of three numbers 188 | * 189 | * @param a - first number 190 | * @param b - second number 191 | * @param c - third number 192 | * 193 | * @beta 194 | */ 195 | export function sum3(a: number, b: number, c: number): number { 196 | return sum2(a, sum2(b, c)); 197 | } 198 | 199 | /** 200 | * Calculate the sum of two numbers 201 | * 202 | * @param a - first number 203 | * @param b - second number 204 | * 205 | * @internal 206 | */ 207 | export function sum2(a: number, b: number): number { 208 | const sum = a + b; 209 | return sum; 210 | } 211 | ``` 212 | 213 | This is obviously convoluted, but it'll serve our purposes for looking at some interesting behavior later. 214 | 215 | Let's make sure that things are working so far by trying to build this project. 216 | 217 | ```sh 218 | rm -rf dist # clear away any old compiled output 219 | yarn build # build the project 220 | ls dist # list the contents of the dist/ folder 221 | ``` 222 | 223 | You should see something like 224 | 225 | ```sh 226 | index.d.ts index.js 227 | ``` 228 | 229 | ## Linting 230 | 231 | Install eslint as a development dependency 232 | 233 | ```sh 234 | yarn add -D eslint 235 | ``` 236 | 237 | and go through the process of creating a starting point ESLint config file 238 | 239 | ```sh 240 | yarn eslint --init 241 | ``` 242 | 243 | When asked, please answer as follows for the choices presented to you: 244 | 245 |742 | Next: ...What have I done? ▶ 743 |
744 | -------------------------------------------------------------------------------- /notes/05-what-have-I-done.md: -------------------------------------------------------------------------------- 1 |2 | ◀ Back: Mike's Professional-Grade TS Setup 3 |
4 | 5 | --- 6 | 7 | # What have I done? 8 | 9 | Let's look closely at what we just did, and make sure we understand all of the parts that make up the whole 10 | 11 | ## In my `tsconfig.json` what exactly is "strict"? 12 | 13 | The source of truth is [here](https://github.com/microsoft/TypeScript/blob/dc8952d308c9de815e95bdb96727a9cbaedc9adb/src/compiler/commandLineParser.ts#L594), and it's important to know that this is a moving target 14 | 15 | ### `noImplicitAny` 16 | 17 | - “Default to explicit” instead of “default to loose” 18 | - This is not in any way restrictive, it only requires that we be explicit about `any` 19 | 20 | ### `noImplicitThis` 21 | 22 | - There are certain places where `this` is important and non-inferrable 23 | > Example: addEventListener 24 | > 25 | > ```ts 26 | > my_element.addEventListener('click', function (e) { 27 | > // logs the className of my_element 28 | > console.log(this.className); 29 | > // logs `true` 30 | > console.log(e.currentTarget === this); 31 | > }); 32 | > ``` 33 | 34 | ### `alwaysStrict` 35 | 36 | - JS “use strict” 37 | - necessary for modern JS language features 38 | 39 | ### `strictBindCallApply` 40 | 41 | - Bind, call, apply used to return very loosely-typed functions. 42 | - No good reasons I'm aware of to disable this 43 | 44 | ### `strictNullChecks` 45 | 46 | - Without this enabled, primitive types allow `null` values 47 | - Leaving this disabled makes truthy/falsy type guards much less useful 48 | - This is asking for runtime errors that could otherwise be caught at build time 49 | 50 | ### `strictFunctionTypes` 51 | 52 | - Some common-sense loopholes around matching function arguments during type-checking function values 53 | - [example](https://www.typescriptlang.org/play?#code/IYIwzgLgTsDGEAJYBthjAgggOwJYFthkEBvAKAUoQAcBXEZXWBUSGeBKAU2ABMB7bMgCeCZFwDmYAFwJstfCC5QA3GQC+ZFGgwBhYIi4APCF2y8MOAkVIUq4qQgC8CACxqqNWlCgAKAJSksIJg-OIAdMj8Er4A5HQ+sf6amtroCAAi0QjGpuaWeITE5J4OGC7udpQgwFAA1gFBIWFckdFxtQBmSSlkZLxc2txiXIidAIyyvkayVkWBTgB8CABu-Li8agNDXCNjAExTM5nRC8trG1uDqMPiYwDMR7L6EGer65tkE84InfsqlAA9ICEABRHz8KAIADuuAgAAsEABaJFsJgQABitGw8FwggAKsJqFwwF99j8JgCEMCEAB5Opkin3Kk08FQSFAA) 54 | > ```ts 55 | > abstract class Animal { 56 | > public abstract readonly legs: number; 57 | > } 58 | > class Cat extends Animal { 59 | > legs = 4; 60 | > purr() { 61 | > console.log('purr'); 62 | > } 63 | > } 64 | > class Dog extends Animal { 65 | > legs = 4; 66 | > bark() { 67 | > console.log('arf'); 68 | > } 69 | > } 70 | > 71 | > declare let f1: (x: Animal) => void; 72 | > declare let f2: (x: Dog) => void; 73 | > declare let f3: (x: Cat) => void; 74 | > 75 | > // Error with --strictFunctionTypes 76 | > f1 = f2; 77 | > f2 = f1; // Always ok 78 | > f2 = f3; // Always error 79 | > ``` 80 | 81 | ### `strictPropertyInitialization` 82 | 83 | - Holds you to your promises around class fields really being “always there” vs. “sometimes undefined” 84 | 85 | ## Even more strict 86 | 87 | ### `noUnusedLocals` 88 | 89 | - Busts you on unused local variables 90 | - Better to have TS detect this rather than a linter 91 | 92 | ### `noUnusedParameters` 93 | 94 | - Function arguments you don’t use need to be prefixed with \_ 95 | - I love this during the “exploration” phase of development b/c it highlights opportunities to simplify API surface 96 | 97 | ### `noImplicitReturns` 98 | 99 | - If any code paths return something explicitly, all code paths must return something explicitly 100 | 101 | ### `noFallthroughCasesInSwitch` 102 | 103 | - I’m ok with this one as being _disabled_, as I find case fall-throughs to be useful, important and easy (enough) to notice while reading code 104 | 105 | ### `types` 106 | 107 | - Instead of pulling in all @types/\*, specify exactly what should be available 108 | - **NOTE:** this is nuanced, and only affects global scope (i.e., window, process) and auto-import. 109 | - _Why I care:_ I don’t want types used exclusively in things like tests to be quite so readily available for accidental use in “app code” 110 | 111 | ### `stripInternal` (most important for libraries) 112 | 113 | - Sometimes you need type information to only be available within a codebase. 114 | - `@internal` JSdoc tag surgically strips out type information for respective symbols 115 | 116 | ## Don't go viral 117 | 118 | There are some compiler options that I _really dislike_ when used in libraries, because they have a high probability of "infecting" any consumer and depriving them from making choices about their own codebase 119 | 120 | ### `allowSyntheticDefaultImports` 121 | 122 | Allows you to import CommonJS modules as if they’re ES modules with a default export 123 | 124 | ### `esModuleInterop` 125 | 126 | Adds some runtime support for CJS/ESM interop, and enables allowSyntheticDefaultImports 127 | 128 | ### `skipDefaultLibCheck` 129 | 130 | This effectively ignores potential breaking changes that stem from your node_modules types mixing with your own types. Particularly if you’re building a library, you need to know that if you “hide” this problem they’ll still “feel” it (and probably need to “skip” too) 131 | 132 | ### But sometimes we need these, right? 133 | 134 | I have never found a good reason to enable these options in well-structured TS code. 135 | 136 | `allowSyntheticDefaultImports` and `esModuleInterop` aim to allow patterns like 137 | 138 | ```ts 139 | import fs from 'fs'; 140 | ``` 141 | 142 | in situations where `fs` doesn't actually expose an ES module `export default`. It exports a _namespace_ of filesystem-related functions. Thankfully, even with these flags _both_ disabled, we can still use a namespace import: 143 | 144 | ```ts 145 | import * as fs from 'fs'; 146 | ``` 147 | 148 | Now there are rare situations where some CommonJS code exports a single non-namespace thing in a way like 149 | 150 | ```ts 151 | // calculator.ts 152 | module.exports = function add(a, b) { 153 | return a + b; 154 | }; 155 | ``` 156 | 157 | add is definitely not a namespace, and 158 | 159 | ```ts 160 | import * as add from './calculator'; 161 | ``` 162 | 163 | WILL NOT WORK. There's a TS-specific pattern that _will work_ though -- it's a little weird, but it doesn't require turning any compiler options on 164 | 165 | ```ts 166 | import add = require('./calculator'); 167 | ``` 168 | 169 | ### Is Mike asking me to take on tech debt? 170 | 171 | You may be thinking "these don't look like ES modules", and "won't the TS team standardize on ES modules later?" 172 | 173 | The answer is: _yes_, but you should think about this the same way that you think about a "legacy" version of Node.js that you need to support 174 | 175 | - You shouldn't break consumers yet 176 | - Apps should be the first to adopt new things, followed by libraries that are more conservative 177 | 178 | TS modules predate ES modules, but there’s tons of code out there that already uses TS module stuff, and this is one of the most easy to codemod kinds of “tech debt” to incur. 179 | 180 | --- 181 | 182 |183 | Next: Converting to TypeScript ▶ 184 |
185 | ``` 186 | -------------------------------------------------------------------------------- /notes/06-converting-to-ts.md: -------------------------------------------------------------------------------- 1 |2 | ◀ Back: ...What have I done? 3 |
4 | 5 | --- 6 | 7 | # Converting to TypeScript 8 | 9 | In my [TS Fundamentals Course (v2)](https://drive.google.com/file/d/170oHzpLNeprUa-TMmOAnSU4caEFDSb3e/view) we discuss a procedure for progressively converting a codebase from JS to TS. 10 | 11 |12 | 13 |  14 | 15 |
16 |17 | 18 |  19 | 20 |
21 |22 | 23 |  24 | 25 |
26 |27 | 28 |  29 | 30 |
31 | 32 | Putting aside that the last image above is a bit redundant (with `"strict"` enabling `"strictNullChecks"`, `"strictFunctionTypes"` and `""strictNullChecks"` already), there's a lot of work to unpack here 33 | 34 | Realistically, this should be done in separate steps for a large codebase 35 | 36 | For example: 37 | 38 | - 3.1) strict mode 39 | 40 | ```diff 41 | --- a/tsconfig.json 42 | +++ b/tsconfig.json 43 | "compilerOptions": { 44 | + "strict": true, 45 | } 46 | ``` 47 | 48 | - 3.2) more strict mode 49 | 50 | ```diff 51 | --- a/tsconfig.json 52 | +++ b/tsconfig.json 53 | "compilerOptions": { 54 | + "noUnusedLocals": true, 55 | + "noImplicitReturns": true, 56 | + "stripInternal": true, 57 | + "types": [], 58 | + "forceConsistentCasingInFileNames": true 59 | } 60 | ``` 61 | 62 | - 3.3) TS-specific linting 63 | 64 | ```diff 65 | --- a/.eslintrc 66 | +++ b/.eslintrc 67 | + "parser": "@typescript-eslint/parser", 68 | "extends": [ 69 | + "prettier/@typescript-eslint", 70 | + "plugin:@typescript-eslint/recommended", 71 | + "plugin:@typescript-eslint/recommended-requiring-type-checking" 72 | ], 73 | ``` 74 | 75 | - 3.4) Even more strict mode 76 | 77 | ```diff 78 | --- a/.eslintrc 79 | +++ b/.eslintrc 80 | "rules": { 81 | + "@typescript-eslint/no-unsafe-assignment": "off", 82 | + "@typescript-eslint/no-unsafe-return": "off", 83 | + "@typescript-eslint/no-explicit-any": "off" 84 | } 85 | ``` 86 | 87 | There are steps beyond this conversion that are important in order to mitigate some other risks. We'll get into those later 88 | 89 | # CHALLENGE: Follow steps 1 and 2 to convert the workshop chat app from JS to ts 90 | 91 | ## Some Quick Tricks 92 | 93 | ```sh 94 | # rename all JSX files in src/ to TSX 95 | find src -name '*.jsx' -exec bash -c 'git mv "$0" "${0%.jsx}.tsx"' "{}" \; 96 | # rename all JS files in src/ to TS 97 | find src -name '*.js' -exec bash -c 'git mv "$0" "${0%.js}.ts"' "{}" \; 98 | # rename all JSX files in src/ to TSX 99 | find tests -name '*.jsx' -exec bash -c 'git mv "$0" "${0%.jsx}.tsx"' "{}" \; 100 | # rename all JSX files in src/ to TSX 101 | find tests -name '*.jsx.snap' -exec bash -c 'git mv "$0" "${0%.jsx.snap}.tsx.snap"' "{}" \; 102 | # rename all JS files in tests/ to TS 103 | find tests -name '*.js' -exec bash -c 'git mv "$0" "${0%.js}.ts"' "{}" \; 104 | ``` 105 | 106 | and don't forget to make this small change to [`/index.html`](/index.html) 107 | 108 | ```diff 109 | --- a/index.html 110 | +++ b/index.html 111 | @@ -8,6 +8,6 @@ 112 | 113 | 114 | 115 | - 116 | + 117 | 118 |