├── .github ├── codeql.yml ├── dependabot.yml └── workflows │ ├── ci-website.yml │ └── nightly-compatibility.yml ├── .gitignore ├── .markdownlint.jsonc ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── .yarn └── plugins │ └── @yarnpkg │ ├── plugin-typescript.cjs │ └── plugin-workspace-tools.cjs ├── .yarnrc.yml ├── README.md ├── learn-typescript.code-workspace ├── package.json ├── packages ├── chat │ ├── .babelrc │ ├── .eslintignore │ ├── .eslintrc.js │ ├── .parcelrc │ ├── .postcssrc │ ├── .prettierrc │ ├── API_EXAMPLES.http │ ├── 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 │ ├── package.json │ ├── scripts │ │ ├── rename-to-ts.mjs │ │ └── tsconfig.json │ ├── server │ │ ├── api-server.mjs │ │ ├── index.mjs │ │ └── tsconfig.json │ ├── 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 │ │ │ │ ├── Team.jsx │ │ │ │ ├── TeamSelector.jsx │ │ │ │ ├── TeamSelector │ │ │ │ └── TeamLink.jsx │ │ │ │ ├── TeamSidebar.jsx │ │ │ │ └── TeamSidebar │ │ │ │ └── ChannelLink.jsx │ │ └── utils │ │ │ ├── api.js │ │ │ ├── date.cjs │ │ │ ├── deferred.js │ │ │ ├── error.js │ │ │ ├── http-error.cjs │ │ │ └── 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.json │ │ └── utils │ │ │ ├── date.test.js │ │ │ ├── deferred.test.js │ │ │ ├── error.test.js │ │ │ └── http-error.test.js │ └── tsconfig.json ├── hello-ts │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── http-error │ ├── index.js │ └── package.json ├── notes-enterprise-ts-v2 │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── notes-intermediate-ts-v2 │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── src │ │ ├── 02-declaration-merging.ts │ │ ├── 03-top-and-bottom-types.ts │ │ ├── 04-nullish-values.ts │ │ ├── 05-modules-and-cjs-interop │ │ │ ├── banana.js │ │ │ ├── berries │ │ │ │ ├── berry-base.ts │ │ │ │ ├── blueberry.ts │ │ │ │ ├── index.ts │ │ │ │ ├── raspberry.ts │ │ │ │ └── strawberry.ts │ │ │ ├── citrus.ts │ │ │ ├── global.d.ts │ │ │ ├── index.ts │ │ │ ├── kiwi.ts │ │ │ ├── melon.js │ │ │ └── ts-logo.png │ │ ├── 06-generic-scopes-and-constraints.ts │ │ ├── 07-conditional-types.ts │ │ ├── 08-infer │ │ │ ├── fruit-market.ts │ │ │ └── index.ts │ │ ├── 09-mapped-types.ts │ │ ├── 10-type-registry-revisited.ts │ │ ├── 10-type-registry-revisited │ │ │ ├── data │ │ │ │ ├── book.ts │ │ │ │ └── magazine.ts │ │ │ ├── index.ts │ │ │ └── lib │ │ │ │ └── registry.ts │ │ └── 11-covariance-contravariance.ts │ └── tsconfig.json ├── notes-ts-fundamentals-v4 │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── src │ │ ├── 01-intro.ts │ │ ├── 03-variables-and-values.ts │ │ ├── 04-objects-arrays-and-tuples.ts │ │ ├── 05-structural-vs-nominal-types.ts │ │ ├── 06-union-and-intersection-types.ts │ │ ├── 07-interfaces-and-type-aliases.ts │ │ ├── 08-json-types.ts │ │ ├── 09-type-queries.ts │ │ ├── 09-type-registry │ │ │ ├── data │ │ │ │ ├── book.ts │ │ │ │ └── magazine.ts │ │ │ ├── index.ts │ │ │ └── lib │ │ │ │ └── registry.ts │ │ ├── 10-callables-and-constructables.ts │ │ ├── 11-classes.ts │ │ ├── 12-type-guards.ts │ │ ├── 13-generics.ts │ │ └── 14-dict-map-filter-reduce.ts │ └── tsconfig.json ├── website │ ├── .eslintignore │ ├── .eslintrc │ ├── .gitignore │ ├── __mocks__ │ │ ├── file-mock.js │ │ └── gatsby.js │ ├── content │ │ ├── .prettierrc │ │ ├── assets │ │ │ ├── GitHub-Mark-32px.png │ │ │ ├── profile-pic.jpg │ │ │ └── ts-logo-512.png │ │ ├── blog │ │ │ ├── enterprise-v2 │ │ │ │ ├── 01-intro │ │ │ │ │ ├── img │ │ │ │ │ │ └── project_screenshot.png │ │ │ │ │ └── index.md │ │ │ │ ├── 02-ts-library-zero-to-one │ │ │ │ │ └── index.md │ │ │ │ ├── 03-tsconfig-strictness │ │ │ │ │ └── index.md │ │ │ │ ├── 04-app-vs-library-concerns │ │ │ │ │ └── index.md │ │ │ │ ├── 05-converting-to-ts │ │ │ │ │ ├── img │ │ │ │ │ │ ├── slide-018.png │ │ │ │ │ │ ├── slide-019.png │ │ │ │ │ │ ├── slide-020.png │ │ │ │ │ │ └── slide-021.png │ │ │ │ │ └── index.md │ │ │ │ ├── 06-steps-1-2-3 │ │ │ │ │ ├── img │ │ │ │ │ │ └── app-home.png │ │ │ │ │ └── index.md │ │ │ │ ├── 07-declaration-files │ │ │ │ │ └── index.md │ │ │ │ ├── 08-dealing-with-pure-type-info │ │ │ │ │ └── index.md │ │ │ │ ├── 09-null-undefined-and-boolean │ │ │ │ │ └── index.md │ │ │ │ ├── 10-types-at-runtime │ │ │ │ │ └── index.md │ │ │ │ ├── 11-in-repo-packages │ │ │ │ │ └── index.md │ │ │ │ ├── 12-tests-for-types │ │ │ │ │ └── index.md │ │ │ │ └── img │ │ │ │ │ └── eslint-error.png │ │ │ ├── full-stack-typescript │ │ │ │ ├── 01-intro │ │ │ │ │ ├── fullstack-ts.001.png │ │ │ │ │ ├── fullstack-ts.002.png │ │ │ │ │ └── index.md │ │ │ │ ├── 02-graphql-intro │ │ │ │ │ ├── doctor-ui.png │ │ │ │ │ └── index.md │ │ │ │ ├── 03-project-tour │ │ │ │ │ └── index.md │ │ │ │ ├── 04-hello-apollo │ │ │ │ │ └── index.md │ │ │ │ ├── 05-first-resolver │ │ │ │ │ └── index.md │ │ │ │ ├── 06-imported-resolver │ │ │ │ │ └── index.md │ │ │ │ ├── 07-imported-resolver │ │ │ │ │ ├── index.md │ │ │ │ │ └── restart-ts-server.png │ │ │ │ ├── 08-ui-consumes-data │ │ │ │ │ ├── index.md │ │ │ │ │ └── suggestions.png │ │ │ │ ├── 09-ui-consumes-data │ │ │ │ │ ├── 4-suggestions.png │ │ │ │ │ └── index.md │ │ │ │ ├── 10-nested-data │ │ │ │ │ ├── follow-count.png │ │ │ │ │ └── index.md │ │ │ │ ├── 11-nested-data copy │ │ │ │ │ ├── create-tweet.png │ │ │ │ │ └── index.md │ │ │ │ ├── 12-favorites │ │ │ │ │ ├── index.md │ │ │ │ │ └── liked.png │ │ │ │ ├── 13-favorites │ │ │ │ │ └── index.md │ │ │ │ └── 14-trends │ │ │ │ │ ├── index.md │ │ │ │ │ └── trends.png │ │ │ ├── fundamentals-v3 │ │ │ │ ├── 01-intro │ │ │ │ │ ├── graph.png │ │ │ │ │ └── index.md │ │ │ │ ├── 02-hello-typescript │ │ │ │ │ ├── cursor-tooltip-ts.gif │ │ │ │ │ └── index.md │ │ │ │ ├── 03-variables-and-values │ │ │ │ │ └── index.md │ │ │ │ ├── 04-objects-arrays-and-tuples │ │ │ │ │ └── index.md │ │ │ │ ├── 05-structural-vs-nominal-types │ │ │ │ │ └── index.md │ │ │ │ ├── 06-union-and-intersection-types │ │ │ │ │ ├── index.md │ │ │ │ │ └── venn.png │ │ │ │ ├── 07-interfaces-and-type-aliases │ │ │ │ │ └── index.md │ │ │ │ ├── 08-exercise-json-types │ │ │ │ │ └── index.md │ │ │ │ ├── 09-functions │ │ │ │ │ └── index.md │ │ │ │ ├── 10-classes │ │ │ │ │ └── index.md │ │ │ │ ├── 11-top-and-bottom-types │ │ │ │ │ └── index.md │ │ │ │ ├── 12-type-guards │ │ │ │ │ └── index.md │ │ │ │ ├── 13-nullish-values │ │ │ │ │ └── index.md │ │ │ │ ├── 14-generics │ │ │ │ │ └── index.md │ │ │ │ ├── 15-dict-map-filter-reduce │ │ │ │ │ └── index.md │ │ │ │ └── 16-type-param-scopes-and-constraints │ │ │ │ │ └── index.md │ │ │ ├── fundamentals-v4 │ │ │ │ ├── 01-intro │ │ │ │ │ ├── graph.png │ │ │ │ │ └── index.md │ │ │ │ ├── 02-hello-typescript │ │ │ │ │ ├── cursor-tooltip-ts.gif │ │ │ │ │ └── index.md │ │ │ │ ├── 03-variables-and-values │ │ │ │ │ └── index.md │ │ │ │ ├── 04-objects-arrays-and-tuples │ │ │ │ │ └── index.md │ │ │ │ ├── 05-structural-vs-nominal-types │ │ │ │ │ └── index.md │ │ │ │ ├── 06-union-and-intersection-types │ │ │ │ │ ├── index.md │ │ │ │ │ ├── union-intersection-preview.png │ │ │ │ │ ├── union-intersection.png │ │ │ │ │ └── venn.png │ │ │ │ ├── 07-interfaces-and-type-aliases │ │ │ │ │ └── index.md │ │ │ │ ├── 08-exercise-json-types │ │ │ │ │ └── index.md │ │ │ │ ├── 09-type-queries │ │ │ │ │ └── index.md │ │ │ │ ├── 10-callables │ │ │ │ │ └── index.md │ │ │ │ ├── 11-classes │ │ │ │ │ └── index.md │ │ │ │ ├── 12-type-guards │ │ │ │ │ └── index.md │ │ │ │ ├── 13-generics │ │ │ │ │ └── index.md │ │ │ │ └── 14-dict-map-filter-reduce │ │ │ │ │ └── index.md │ │ │ ├── intermediate-v1 │ │ │ │ ├── 01-project-setup │ │ │ │ │ └── index.md │ │ │ │ ├── 02-declaration-merging │ │ │ │ │ └── index.md │ │ │ │ ├── 03-modules │ │ │ │ │ └── index.md │ │ │ │ ├── 04-type-queries │ │ │ │ │ └── index.md │ │ │ │ ├── 05-conditional-types │ │ │ │ │ └── index.md │ │ │ │ ├── 06-extract-exclude │ │ │ │ │ └── index.md │ │ │ │ ├── 07-infer │ │ │ │ │ └── index.md │ │ │ │ ├── 08-indexed-access-types │ │ │ │ │ └── index.md │ │ │ │ └── 09-mapped-types │ │ │ │ │ └── index.md │ │ │ ├── intermediate-v2 │ │ │ │ ├── 01-project-setup │ │ │ │ │ └── index.md │ │ │ │ ├── 02-declaration-merging │ │ │ │ │ └── index.md │ │ │ │ ├── 03-top-and-bottom-types │ │ │ │ │ └── index.md │ │ │ │ ├── 04-nullish-values │ │ │ │ │ └── index.md │ │ │ │ ├── 05-modules │ │ │ │ │ └── index.md │ │ │ │ ├── 06-type-param-scopes-and-constraints │ │ │ │ │ └── index.md │ │ │ │ ├── 07-conditional-types │ │ │ │ │ └── index.md │ │ │ │ ├── 08-infer │ │ │ │ │ └── index.md │ │ │ │ ├── 09-mapped-types │ │ │ │ │ ├── index.md │ │ │ │ │ └── male-to-male.jpg │ │ │ │ ├── 10-type-registry-revisited │ │ │ │ │ └── index.md │ │ │ │ └── 11-covariance-contravariance │ │ │ │ │ ├── index.md │ │ │ │ │ ├── union-intersection.png │ │ │ │ │ └── venn.png │ │ │ └── making-typescript-stick │ │ │ │ ├── 01-intro │ │ │ │ ├── Making-TypeScript-Stick.001.jpeg │ │ │ │ ├── index.md │ │ │ │ └── making-it-stick.jpeg │ │ │ │ ├── 02-warm-up │ │ │ │ └── index.md │ │ │ │ ├── 03-recent-updates-to-typescript │ │ │ │ ├── index.md │ │ │ │ ├── throw-error.png │ │ │ │ ├── throw-string.png │ │ │ │ └── variadic-tuple-generic-TS3.png │ │ │ │ ├── 04-data-layer-challenge │ │ │ │ └── index.md │ │ │ │ ├── 05-does-it-compile │ │ │ │ └── index.md │ │ │ │ ├── 06-jquery-challenge │ │ │ │ └── index.md │ │ │ │ ├── 07-typepardy │ │ │ │ └── index.md │ │ │ │ ├── 08-type-challenges │ │ │ │ └── index.md │ │ │ │ ├── 09-guess-that-type │ │ │ │ └── index.md │ │ │ │ └── 10-penpal-types │ │ │ │ └── index.md │ │ ├── courses.yml │ │ └── typescript-jeopardy.yml │ ├── cypress.config.ts │ ├── cypress │ │ ├── e2e │ │ │ ├── code-snippets │ │ │ │ └── code-snippets.cy.ts │ │ │ ├── course-list │ │ │ │ └── course-list.cy.ts │ │ │ └── courses │ │ │ │ ├── enterprise-typescript-v2.cy.ts │ │ │ │ ├── full-stack-typescript.cy.ts │ │ │ │ ├── fundamentals-v3.cy.ts │ │ │ │ ├── fundamentals-v4.cy.ts │ │ │ │ ├── intermediate-v1.cy.ts │ │ │ │ ├── intermediate-v2.cy.ts │ │ │ │ └── making-typescript-stick.cy.ts │ │ ├── support │ │ │ ├── commands.ts │ │ │ └── e2e.ts │ │ └── tsconfig.json │ ├── gatsby-browser.js │ ├── gatsby-config.js │ ├── gatsby-node.js │ ├── global.d.ts │ ├── jest-preprocess.js │ ├── jest.config.js │ ├── loadershim.js │ ├── package.json │ ├── src │ │ ├── components │ │ │ ├── EditOnGitHub.tsx │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── bio.tsx.snap │ │ │ │ └── bio.tsx │ │ │ ├── bio.tsx │ │ │ ├── course-layout.tsx │ │ │ ├── layout.tsx │ │ │ └── seo.tsx │ │ ├── pages │ │ │ ├── 404.tsx │ │ │ └── index.tsx │ │ ├── templates │ │ │ ├── blog-post.tsx │ │ │ └── course-page.tsx │ │ └── utils │ │ │ ├── setup-two-slash.ts │ │ │ └── typography.ts │ ├── static │ │ ├── enterprise-ts-v2.png │ │ ├── favicon.png │ │ ├── fb-full-stack-ts.png │ │ ├── fb-intermediate-ts.png │ │ ├── fb-making-typescript-stick.png │ │ ├── fb-ts-fundamentals-v3.png │ │ ├── fem-logo.png │ │ ├── full-stack-ts.png │ │ ├── intermediate-ts-v2.png │ │ ├── intermediate-ts.png │ │ ├── making-typescript-stick.png │ │ ├── robots.txt │ │ ├── ts-fundamentals-v3.png │ │ ├── ts-fundamentals-v4.png │ │ ├── tw-full-stack-ts.png │ │ ├── tw-intermediate-ts.png │ │ ├── tw-making-typescript-stick.png │ │ ├── tw-ts-fundamentals-v3.png │ │ └── twitter-preview.mov │ ├── styles │ │ └── index.scss │ └── tsconfig.json └── welcome-to-ts │ ├── package.json │ ├── src │ └── index.ts │ └── tsconfig.json └── yarn.lock /.github/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "Code Scanning - Action" 2 | enabled: false 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | # ┌───────────── minute (0 - 59) 8 | # │ ┌───────────── hour (0 - 23) 9 | # │ │ ┌───────────── day of the month (1 - 31) 10 | # │ │ │ ┌───────────── month (1 - 12 or JAN-DEC) 11 | # │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT) 12 | # │ │ │ │ │ 13 | # │ │ │ │ │ 14 | # │ │ │ │ │ 15 | # * * * * * 16 | - cron: '30 1 * * 0' 17 | 18 | jobs: 19 | CodeQL-Build: 20 | # CodeQL runs on ubuntu-latest, windows-latest, and macos-latest 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - name: Checkout repository 25 | uses: actions/checkout@v2 26 | - uses: volta-cli/action@v1 27 | - name: Initialize CodeQL 28 | uses: github/codeql-action/init@v1 29 | with: 30 | languages: javascript-typescript 31 | - run: yarn 32 | - run: yarn build 33 | - name: Perform CodeQL Analysis 34 | uses: github/codeql-action/analyze@v1 35 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 2 | 3 | version: 2 4 | updates: 5 | # Maintain dependencies for GitHub Actions 6 | - package-ecosystem: "github-actions" 7 | directory: "/" 8 | schedule: 9 | interval: "weekly" 10 | - package-ecosystem: "npm" # See documentation for possible values 11 | directory: "/" # Location of package manifests 12 | schedule: 13 | interval: "daily" 14 | versioning-strategy: widen 15 | open-pull-requests-limit: 20 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env* 2 | # Build 3 | **/build 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | 12 | # Diagnostic reports (https://nodejs.org/api/report.html) 13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | *.lcov 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # Snowpack dependency directory (https://snowpack.dev/) 48 | web_modules/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Microbundle cache 60 | .rpt2_cache/ 61 | .rts2_cache_cjs/ 62 | .rts2_cache_es/ 63 | .rts2_cache_umd/ 64 | 65 | # Optional REPL history 66 | .node_repl_history 67 | 68 | # Output of 'npm pack' 69 | *.tgz 70 | 71 | # Yarn Integrity file 72 | .yarn-integrity 73 | 74 | # dotenv environment variables file 75 | .env 76 | .env.test 77 | 78 | # parcel-bundler cache (https://parceljs.org/) 79 | .cache 80 | .parcel-cache 81 | 82 | # Next.js build output 83 | .next 84 | out 85 | 86 | # Nuxt.js build / generate output 87 | .nuxt 88 | dist 89 | 90 | # Gatsby files 91 | .cache/ 92 | # Comment in the public line in if your project uses Gatsby and not Next.js 93 | # https://nextjs.org/blog/next-9-1#public-directory-support 94 | # public 95 | 96 | # vuepress build output 97 | .vuepress/dist 98 | 99 | # Serverless directories 100 | .serverless/ 101 | 102 | # FuseBox cache 103 | .fusebox/ 104 | 105 | # DynamoDB Local files 106 | .dynamodb/ 107 | 108 | # TernJS port file 109 | .tern-port 110 | 111 | # Stores VSCode versions used for testing VSCode extensions 112 | .vscode-test 113 | 114 | # yarn v2 115 | .yarn/cache 116 | .yarn/unplugged 117 | .yarn/build-state.yml 118 | .yarn/install-state.gz 119 | .pnp.* 120 | -------------------------------------------------------------------------------- /.markdownlint.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "MD033": false, 4 | "MD013": false 5 | } 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .cache 2 | yarn.lock 3 | public 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "semi": false, 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "useTabs": false, 7 | "trailingComma": "all", 8 | "quoteProps": "consistent", 9 | "arrowParens": "always", 10 | "printWidth": 70 11 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "humao.rest-client", 4 | "aaron-bond.better-comments", 5 | "orta.vscode-twoslash" 6 | ] 7 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.preferences.importModuleSpecifier": "relative", 3 | "better-comments.multilineComments": true, 4 | "better-comments.tags": [ 5 | { 6 | "tag": "//", 7 | "color": "#474747", 8 | "strikethrough": true, 9 | "underline": false, 10 | "backgroundColor": "transparent", 11 | "bold": false, 12 | "italic": false 13 | }, 14 | { 15 | "tag": "-", 16 | "color": "#474747", 17 | "strikethrough": true, 18 | "underline": false, 19 | "backgroundColor": "transparent", 20 | "bold": false, 21 | "italic": false 22 | }, 23 | { 24 | "tag": "!", 25 | "color": "#DA8C83", 26 | "strikethrough": false, 27 | "underline": false, 28 | "backgroundColor": "#481812", 29 | "bold": false, 30 | "italic": false 31 | }, 32 | { 33 | "tag": "✔️", 34 | "color": "#B5BD68", 35 | "strikethrough": false, 36 | "underline": false, 37 | "backgroundColor": "#1E4A2A", 38 | "bold": false, 39 | "italic": false 40 | }, 41 | { 42 | "tag": "?", 43 | "color": "#9BB0F9", 44 | "strikethrough": false, 45 | "underline": false, 46 | "backgroundColor": "#313A5C", 47 | "bold": false, 48 | "italic": false 49 | }, 50 | { 51 | "tag": "todo", 52 | "color": "#FF8C00", 53 | "strikethrough": false, 54 | "underline": false, 55 | "backgroundColor": "transparent", 56 | "bold": false, 57 | "italic": false 58 | }, 59 | { 60 | "tag": "*", 61 | "color": "#2F3139", 62 | "strikethrough": false, 63 | "underline": true, 64 | "backgroundColor": "#B1B3BD", 65 | "bold": true, 66 | "italic": false 67 | } 68 | ], 69 | "editor.defaultFormatter": "esbenp.prettier-vscode", 70 | "[typescript]": { 71 | "editor.defaultFormatter": "esbenp.prettier-vscode" 72 | }, 73 | "[javascript]": { 74 | "editor.defaultFormatter": "esbenp.prettier-vscode" 75 | }, 76 | "typescript.tsdk": "node_modules/typescript/lib" 77 | } -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | plugins: 4 | - path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs 5 | spec: "@yarnpkg/plugin-typescript" 6 | - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs 7 | spec: "@yarnpkg/plugin-workspace-tools" 8 | -------------------------------------------------------------------------------- /learn-typescript.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { "name": "website", "path": "packages/website" }, 4 | { "name": "hello-ts", "path": "packages/hello-ts" } 5 | ], 6 | "settings": { 7 | "typescript.tsdk": "node_modules/typescript/lib" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mike-north/typescript-courses", 3 | "version": "0.0.1", 4 | "description": "Mike's TypeScript Courses", 5 | "main": "index.js", 6 | "repository": "git@github.com:mike-north/typescript-courses.git", 7 | "author": "Mike North ", 8 | "license": "NOLICENSE", 9 | "private": true, 10 | "workspaces": [ 11 | "packages/*" 12 | ], 13 | "volta": { 14 | "node": "18.18.2", 15 | "yarn": "3.6.4" 16 | }, 17 | "devDependencies": { 18 | "@babel/core": "^7.23.2", 19 | "@babel/preset-typescript": "^7.23.2", 20 | "@types/node": "^22.15.8", 21 | "core-js": "^3.33.0", 22 | "markdownlint-cli": "^0.37.0", 23 | "replace-in-file": "^7.0.1" 24 | }, 25 | "scripts": { 26 | "postinstall": "replace-in-file 'import chalk' 'import * as chalk' node_modules/gatsby-cli/lib/reporter/reporter.d.ts", 27 | "dev-website": "yarn workspace website dev", 28 | "dev-chat": "yarn workspace chat dev", 29 | "dev-hello-ts": "yarn workspace hello-ts dev", 30 | "dev-welcome-to-ts": "yarn workspace welcome-to-ts dev", 31 | "build": "yarn workspaces foreach -vpi run build", 32 | "lint-local-md": "yarn markdownlint README.md", 33 | "lint-local": "yarn lint-local-md", 34 | "lint": "yarn lint-local && yarn workspaces foreach -vpi run lint", 35 | "typecheck": "yarn workspaces foreach -vpi run typecheck", 36 | "test": "yarn test-jest && yarn test-tsd && yarn test-dtslint", 37 | "test-jest": "yarn workspaces foreach -vpi run test-jest", 38 | "test-tsd": "yarn workspaces foreach -vpi run test-tsd", 39 | "test-dtslint": "yarn workspaces foreach -vpi run test-dtslint" 40 | }, 41 | "resolutions": { 42 | "@types/eslint": "8.44.3", 43 | "@types/react": "18.2.28" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/chat/.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 | -------------------------------------------------------------------------------- /packages/chat/.eslintignore: -------------------------------------------------------------------------------- 1 | ./.eslintrc.js -------------------------------------------------------------------------------- /packages/chat/.parcelrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@parcel/config-default" 3 | } -------------------------------------------------------------------------------- /packages/chat/.postcssrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "tailwindcss": {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/chat/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "semi": false, 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "useTabs": false, 7 | "trailingComma": "all", 8 | "quoteProps": "consistent", 9 | "arrowParens": "always", 10 | "printWidth": 70 11 | } -------------------------------------------------------------------------------- /packages/chat/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 | -------------------------------------------------------------------------------- /packages/chat/assets/img/angry-cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/typescript-courses/0e5648b0bdfe0e696163867580e7a8977b27205d/packages/chat/assets/img/angry-cat.jpg -------------------------------------------------------------------------------- /packages/chat/assets/img/avengers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/typescript-courses/0e5648b0bdfe0e696163867580e7a8977b27205d/packages/chat/assets/img/avengers.jpg -------------------------------------------------------------------------------- /packages/chat/assets/img/boss.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/typescript-courses/0e5648b0bdfe0e696163867580e7a8977b27205d/packages/chat/assets/img/boss.jpg -------------------------------------------------------------------------------- /packages/chat/assets/img/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/typescript-courses/0e5648b0bdfe0e696163867580e7a8977b27205d/packages/chat/assets/img/cat.jpg -------------------------------------------------------------------------------- /packages/chat/assets/img/clippy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/typescript-courses/0e5648b0bdfe0e696163867580e7a8977b27205d/packages/chat/assets/img/clippy.png -------------------------------------------------------------------------------- /packages/chat/assets/img/colonel-meow.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/typescript-courses/0e5648b0bdfe0e696163867580e7a8977b27205d/packages/chat/assets/img/colonel-meow.jpg -------------------------------------------------------------------------------- /packages/chat/assets/img/desk_flip.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/typescript-courses/0e5648b0bdfe0e696163867580e7a8977b27205d/packages/chat/assets/img/desk_flip.jpg -------------------------------------------------------------------------------- /packages/chat/assets/img/dilbert.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/typescript-courses/0e5648b0bdfe0e696163867580e7a8977b27205d/packages/chat/assets/img/dilbert.jpg -------------------------------------------------------------------------------- /packages/chat/assets/img/drstrange.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/typescript-courses/0e5648b0bdfe0e696163867580e7a8977b27205d/packages/chat/assets/img/drstrange.jpg -------------------------------------------------------------------------------- /packages/chat/assets/img/ironman.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/typescript-courses/0e5648b0bdfe0e696163867580e7a8977b27205d/packages/chat/assets/img/ironman.jpg -------------------------------------------------------------------------------- /packages/chat/assets/img/jquery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/typescript-courses/0e5648b0bdfe0e696163867580e7a8977b27205d/packages/chat/assets/img/jquery.png -------------------------------------------------------------------------------- /packages/chat/assets/img/js.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/typescript-courses/0e5648b0bdfe0e696163867580e7a8977b27205d/packages/chat/assets/img/js.png -------------------------------------------------------------------------------- /packages/chat/assets/img/linkedin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/typescript-courses/0e5648b0bdfe0e696163867580e7a8977b27205d/packages/chat/assets/img/linkedin.png -------------------------------------------------------------------------------- /packages/chat/assets/img/lisa.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/typescript-courses/0e5648b0bdfe0e696163867580e7a8977b27205d/packages/chat/assets/img/lisa.jpeg -------------------------------------------------------------------------------- /packages/chat/assets/img/maru.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/typescript-courses/0e5648b0bdfe0e696163867580e7a8977b27205d/packages/chat/assets/img/maru.jpg -------------------------------------------------------------------------------- /packages/chat/assets/img/microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/typescript-courses/0e5648b0bdfe0e696163867580e7a8977b27205d/packages/chat/assets/img/microsoft.png -------------------------------------------------------------------------------- /packages/chat/assets/img/mike.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/typescript-courses/0e5648b0bdfe0e696163867580e7a8977b27205d/packages/chat/assets/img/mike.jpeg -------------------------------------------------------------------------------- /packages/chat/assets/img/node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/typescript-courses/0e5648b0bdfe0e696163867580e7a8977b27205d/packages/chat/assets/img/node.png -------------------------------------------------------------------------------- /packages/chat/assets/img/office97.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/typescript-courses/0e5648b0bdfe0e696163867580e7a8977b27205d/packages/chat/assets/img/office97.png -------------------------------------------------------------------------------- /packages/chat/assets/img/thor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/typescript-courses/0e5648b0bdfe0e696163867580e7a8977b27205d/packages/chat/assets/img/thor.jpg -------------------------------------------------------------------------------- /packages/chat/assets/img/ts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/typescript-courses/0e5648b0bdfe0e696163867580e7a8977b27205d/packages/chat/assets/img/ts.png -------------------------------------------------------------------------------- /packages/chat/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 | -------------------------------------------------------------------------------- /packages/chat/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/chat/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chat", 3 | "version": "1.0.0", 4 | "description": "A chat app for Mike's Enterprise TS course", 5 | "repository": "https://github.com/mike-north/typescript-courses/", 6 | "author": "Mike North (michael.l.north@gmail.com)", 7 | "license": "NOLICENSE", 8 | "private": true, 9 | "browserslist": "> 0.5%, last 2 versions, not dead", 10 | "dependencies": { 11 | "@parcel/core": "^2.10.1", 12 | "@parcel/ts-utils": "^2.10.1", 13 | "date-fns": "^2.30.0", 14 | "execa": "^8.0.1", 15 | "express": "^4.18.2", 16 | "express-rate-limit": "^7.1.1", 17 | "http-error": "workspace:*", 18 | "json-server": "^0.17.4", 19 | "parcel": "^2.10.0", 20 | "pkg-up": "^4.0.0", 21 | "react": "^18.2.0", 22 | "react-dom": "^18.2.0", 23 | "react-router-dom": "^5.3.4" 24 | }, 25 | "devDependencies": { 26 | "@babel/core": "^7.23.2", 27 | "@parcel/config-default": "^2.10.1", 28 | "@parcel/optimizer-data-url": "^2.10.1", 29 | "@parcel/packager-ts": "^2.10.1", 30 | "@parcel/transformer-inline-string": "^2.10.1", 31 | "@parcel/transformer-typescript-tsc": "^2.10.1", 32 | "@parcel/transformer-typescript-types": "^2.10.1", 33 | "@types/express": "^4.17.19", 34 | "@types/json-server": "^0.14.5", 35 | "@types/react-test-renderer": "^18", 36 | "@typescript-eslint/eslint-plugin": "^6.7.5", 37 | "@typescript-eslint/parser": "^6.7.5", 38 | "babel-jest": "^29.7.0", 39 | "dtslint": "^4.2.1", 40 | "eslint": "^7.0.0", 41 | "eslint-config-prettier": "^9.0.0", 42 | "eslint-plugin-node": "^11.1.0", 43 | "eslint-plugin-prettier": "^5.0.1", 44 | "eslint-plugin-promise": "^6.1.1", 45 | "eslint-plugin-sonarjs": "^0.21.0", 46 | "jest": "^29.0.0", 47 | "postcss": "^8.4.31", 48 | "postcss-import": "^15.1.0", 49 | "postcss-nesting": "^12.0.1", 50 | "postcss-preset-env": "^9.2.0", 51 | "postcss-purgecss": "^5.0.0", 52 | "prettier": "^3.0.3", 53 | "react-test-renderer": "18.2.0", 54 | "tailwindcss": "^3.3.3", 55 | "tsd": "^0.29.0", 56 | "typescript": "~5.8.0" 57 | }, 58 | "scripts": { 59 | "build": "yarn parcel build index.html", 60 | "dev": "node ./server/index.mjs", 61 | "lint": "yarn eslint src server tests --ext .js,.ts,.jsx,.tsx,.mjs,.mts -c ./.eslintrc.js", 62 | "test": "yarn test-jest", 63 | "test-jest": "yarn jest tests", 64 | "typecheck": "yarn tsc -P ." 65 | }, 66 | "engines": { 67 | "node": ">=16.0.0" 68 | }, 69 | "volta": { 70 | "node": "18.18.2", 71 | "yarn": "3.6.4" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /packages/chat/scripts/rename-to-ts.mjs: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs' 2 | import * as execa from 'execa' 3 | import * as path from 'path'; 4 | 5 | 6 | /** 7 | * Rename all files in a folder that have one extension to another extension 8 | * 9 | * @param {string} dir directory to scan 10 | * @param {string} oldExt old extension 11 | * @param {string} newExt new extension 12 | */ 13 | const renameFiles = async (dir, oldExt, newExt) => { 14 | const files = await fs.readdir(dir, { withFileTypes: true }) 15 | 16 | for (const file of files) { 17 | const fullPath = path.join(dir, file.name) 18 | 19 | if (file.isDirectory()) { 20 | await renameFiles(fullPath, oldExt, newExt) 21 | } else if (file.isFile()) { 22 | const fileName = file.name 23 | 24 | // Check if the file name ends with the old extension 25 | if (fileName.endsWith(oldExt)) { 26 | const newFileName = fileName.replace(new RegExp(`${oldExt}$`), newExt) 27 | const newFullPath = path.join(dir, newFileName) 28 | 29 | // Using git mv to rename with execa 30 | try { 31 | execa.execaSync('git', ['mv', fullPath, newFullPath]) 32 | console.log(`Renamed ${fullPath} to ${newFullPath}`) 33 | } catch (error) { 34 | console.error( 35 | `Error renaming ${fullPath} to ${newFullPath}:`, 36 | error.message, 37 | ) 38 | } 39 | } 40 | } 41 | } 42 | } 43 | 44 | const main = async () => { 45 | await renameFiles('src', '.jsx', '.tsx') 46 | await renameFiles('src', '.js', '.ts') 47 | await renameFiles('tests', '.jsx', '.tsx') 48 | await renameFiles('tests', '.test.jsx.snap', '.test.tsx.snap') 49 | await renameFiles('tests', '.js', '.ts') 50 | } 51 | 52 | main().catch((err) => { 53 | console.error(err) 54 | }) -------------------------------------------------------------------------------- /packages/chat/scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "allowSyntheticDefaultImports": true, 5 | "esModuleInterop": true 6 | }, 7 | "include": ["."] 8 | } -------------------------------------------------------------------------------- /packages/chat/server/api-server.mjs: -------------------------------------------------------------------------------- 1 | import jsonServer from 'json-server' 2 | 3 | const router = jsonServer.router('db.json') 4 | const middlewares = jsonServer.defaults() 5 | 6 | /** 7 | * @param {import('express').Request} req 8 | * @param {import('express').Response} res 9 | * @param {import('express').NextFunction} next 10 | */ 11 | function SINGULAR_MIDDLEWARE(req, res, next) { 12 | const _send = res.send 13 | res.send = function (body) { 14 | if (req.url.indexOf('singular') >= 0) { 15 | try { 16 | const json = JSON.parse(body) 17 | if (Array.isArray(json)) { 18 | if (json.length === 1) { 19 | return _send.call(this, JSON.stringify(json[0])) 20 | } else if (json.length === 0) { 21 | return _send.call(this, '{}', 404) 22 | } 23 | } 24 | } catch (e) { 25 | throw new Error('Problem unwrapping array') 26 | } 27 | } 28 | return _send.call(this, body) 29 | } 30 | next() 31 | } 32 | 33 | /** 34 | * @param {import('express').Request} req 35 | * @param {import('express').Response} _res 36 | * @param {import('express').NextFunction} next 37 | */ 38 | function addDateToPost(req, _res, next) { 39 | if (req.method === 'POST') { 40 | req.body.createdAt = Date.now() 41 | } 42 | // Continue to JSON Server router 43 | next() 44 | } 45 | 46 | export function setupAPI(server) { 47 | server.use('/api', SINGULAR_MIDDLEWARE, ...middlewares) 48 | server.use(jsonServer.bodyParser) 49 | 50 | server.use(addDateToPost) 51 | server.use( 52 | jsonServer.rewriter({ 53 | '/api/teams': '/api/teams?_embed=channels', 54 | '/api/teams/:id': '/api/teams/:id?_embed=channels', 55 | '/api/teams/:id/channels': '/api/channels?teamId=:id', 56 | '/api/teams/:id/channels/:channelId': 57 | '/api/channels?id=:channelId&teamId=:id&singular=1', 58 | '/api/teams/:id/channels/:channelId/messages': 59 | '/api/messages?_expand=user&teamId=:id&channelId=:channelId', 60 | }), 61 | ) 62 | 63 | server.use('/api', router) 64 | return server 65 | } 66 | -------------------------------------------------------------------------------- /packages/chat/server/index.mjs: -------------------------------------------------------------------------------- 1 | import { Parcel } from '@parcel/core' 2 | import express from 'express' 3 | import jsonServer from 'json-server' 4 | import { setupAPI } from './api-server.mjs' 5 | import { join } from 'path' 6 | import * as pkgUp from 'pkg-up' 7 | import RateLimit from 'express-rate-limit' 8 | 9 | const limiter = RateLimit({ 10 | windowMs: 15 * 60 * 1000, // 15 minutes 11 | max: 1000, // max 100 requests per windowMs 12 | }) 13 | 14 | // apply rate limiter to all requests 15 | const server = jsonServer.create() 16 | 17 | const PORT = process.env['PORT'] || 3000 18 | const PKG_JSON_PATH = pkgUp.pkgUpSync() 19 | if (!PKG_JSON_PATH) 20 | throw new Error('Could not determine package.json path') 21 | console.log('Directory: ' + PKG_JSON_PATH) 22 | const DIR_NAME = join(PKG_JSON_PATH, '..') 23 | const DIST_INDEX_HTML = join(DIR_NAME, 'dist', 'index.html') 24 | const app = express() 25 | 26 | setupAPI(server) 27 | 28 | const file = join(DIR_NAME, 'index.html') // Pass an absolute path to the entrypoint here 29 | 30 | // Initialize a new bundler using a file and options 31 | const bundler = new Parcel({ 32 | entries: file, 33 | defaultConfig: '@parcel/config-default', 34 | defaultTargetOptions: { 35 | engines: { 36 | browsers: ['last 1 Chrome version'], 37 | }, 38 | }, 39 | }) 40 | 41 | await bundler.watch((err, buildEvent) => { 42 | if (buildEvent && buildEvent.type === 'buildSuccess') { 43 | const { buildTime } = buildEvent 44 | console.log(`🏗️ UI build complete: ${buildTime / 1000}s`) 45 | } 46 | if (err) console.error(err) 47 | }) 48 | console.log('UI Build Watcher Started') 49 | 50 | app.use(limiter) 51 | app.use('/assets', express.static(join(DIR_NAME, 'assets'))) 52 | app.use(server) 53 | app.get('*', (req, res, next) => { 54 | if (req.path.endsWith('.js')) { 55 | next() 56 | return 57 | } 58 | if (req.path.endsWith('.css')) { 59 | next() 60 | return 61 | } 62 | if (req.accepts('*/html') === '*/html') { 63 | res.sendFile(DIST_INDEX_HTML) 64 | } else next() 65 | }) 66 | app.get('*', express.static(join(DIR_NAME, 'dist'))) 67 | 68 | // Listen on port PORT 69 | app.listen(PORT, () => { 70 | console.log(`Serving on http://localhost:${PORT}`) 71 | }) 72 | -------------------------------------------------------------------------------- /packages/chat/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "allowSyntheticDefaultImports": true, 5 | "esModuleInterop": true 6 | }, 7 | "include": ["."] 8 | } -------------------------------------------------------------------------------- /packages/chat/src/data/channels.js: -------------------------------------------------------------------------------- 1 | import { apiCall } from '../utils/networking' 2 | 3 | const cachedChannelRecords = {} 4 | 5 | export async function getChannelById(id) { 6 | let cached = cachedChannelRecords[id] 7 | if (typeof cached !== 'undefined') return await cached 8 | cached = cachedChannelRecords[id] = apiCall(`Channels/${id}`) 9 | 10 | return await cached 11 | } 12 | -------------------------------------------------------------------------------- /packages/chat/src/data/messages.js: -------------------------------------------------------------------------------- 1 | import { apiCall } from '../utils/networking' 2 | 3 | const cachedMessageRecordArrays = {} 4 | 5 | export async function getChannelMessages(teamId, channelId) { 6 | let cached = cachedMessageRecordArrays[channelId] 7 | if (typeof cached === 'undefined') 8 | cached = cachedMessageRecordArrays[channelId] = apiCall( 9 | `teams/${teamId}/channels/${channelId}/messages`, 10 | ) 11 | return await cached 12 | } 13 | -------------------------------------------------------------------------------- /packages/chat/src/data/teams.js: -------------------------------------------------------------------------------- 1 | import { apiCall } from '../utils/networking' 2 | 3 | let cachedAllTeamsList 4 | export async function getAllTeams() { 5 | if (typeof cachedAllTeamsList === 'undefined') 6 | cachedAllTeamsList = apiCall('teams') 7 | 8 | return await cachedAllTeamsList 9 | } 10 | 11 | const cachedTeamRecords = {} 12 | 13 | export async function getTeamById(id) { 14 | let cached = cachedTeamRecords[id] 15 | if (typeof cached === 'undefined') 16 | cached = cachedTeamRecords[id] = apiCall(`teams/${id}`) 17 | return await cached 18 | } 19 | -------------------------------------------------------------------------------- /packages/chat/src/index.js: -------------------------------------------------------------------------------- 1 | // import 'regenerator-runtime/runtime'; 2 | import * as React from 'react' 3 | import { createRoot } from 'react-dom/client' 4 | import App from './ui/App' 5 | 6 | function initializeReactApp() { 7 | const appContainer = document.getElementById('appContainer') 8 | if (appContainer === false) 9 | throw new Error('No #appContainer found in DOM') 10 | const root = createRoot(appContainer) 11 | root.render(React.createElement(App)) 12 | } 13 | 14 | initializeReactApp() 15 | -------------------------------------------------------------------------------- /packages/chat/src/ui/App.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { 3 | BrowserRouter as Router, 4 | Route, 5 | Switch, 6 | } from 'react-router-dom' 7 | import { getAllTeams } from '../data/teams' 8 | import { useAsyncDataEffect } from '../utils/api' 9 | import Loading from './components/Loading' 10 | import TeamSelector from './components/TeamSelector' 11 | import Team from './components/Team' 12 | 13 | const { useState } = React 14 | 15 | const App = () => { 16 | const [teams, setTeams] = useState(null) 17 | 18 | useAsyncDataEffect(() => getAllTeams(), { 19 | setter: setTeams, 20 | stateName: 'teams', 21 | }) 22 | if (teams === null) return 23 | return ( 24 | 25 |
26 | 27 | 28 | 29 |
30 |

Please select a team

31 |
32 |
33 | 34 |
35 |

Please select a team

36 |
37 |
38 | { 41 | if (!match) throw new Error('no match') 42 | 43 | const { params } = match 44 | if (!params) throw new Error('no match params') 45 | 46 | const { teamId: selectedTeamId } = params 47 | if (!selectedTeamId) throw new Error(`undefined teamId`) 48 | 49 | const selectedTeam = teams.find((t) => t.id === selectedTeamId) 50 | if (!selectedTeam) 51 | throw new Error( 52 | `Invalid could not find team with id {selectedTeamId}`, 53 | ) 54 | return 55 | }} 56 | /> 57 |
58 |
59 |
60 | ) 61 | } 62 | export default App 63 | -------------------------------------------------------------------------------- /packages/chat/src/ui/components/Channel.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { getChannelMessages } from '../../data/messages' 3 | import { useAsyncDataEffect } from '../../utils/api' 4 | import ChannelFooter from './Channel/Footer' 5 | import ChannelHeader from './Channel/Header' 6 | import ChannelMessage from './Channel/Message' 7 | import Loading from './Loading' 8 | 9 | const Channel = ({ channel }) => { 10 | const [messages, setMessages] = React.useState() 11 | useAsyncDataEffect( 12 | () => getChannelMessages(channel.teamId, channel.id), 13 | { 14 | setter: setMessages, 15 | stateName: 'messages', 16 | otherStatesToMonitor: [channel], 17 | }, 18 | ) 19 | if (!messages) return 20 | if (messages.length === 0) return 21 | console.log( 22 | `%c CHANNEL render: ${channel.name}`, 23 | 'background-color: purple; color: white', 24 | ) 25 | return ( 26 |
27 | 31 |
35 | {messages.map((m) => ( 36 | 42 | ))} 43 |
44 | 45 | 46 |
47 | ) 48 | } 49 | export default Channel 50 | -------------------------------------------------------------------------------- /packages/chat/src/ui/components/Channel/Footer.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | const Footer = ({ channel: { name: channelName } }) => ( 4 |
5 |
9 |

10 | Message Input 11 |

12 | 13 | 26 | 27 | 30 | 31 | 37 | 38 | 44 |
45 |
46 | ) 47 | 48 | export default Footer 49 | -------------------------------------------------------------------------------- /packages/chat/src/ui/components/Channel/Header.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | const Header = ({ title, description }) => ( 4 |
5 |
6 |

7 | 8 | {title} 9 |

10 |

11 | {description} 12 |

13 |
14 |
15 | ) 16 | 17 | export default Header 18 | -------------------------------------------------------------------------------- /packages/chat/src/ui/components/Channel/Message.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { formatTimestamp } from '../../../utils/date.cjs' 3 | 4 | const Message = ({ user, date, body }) => ( 5 |
9 |
10 | {user.name} 15 |
16 | 17 |
18 |
19 | 23 | {user.name} 24 | 25 | at 26 | 29 |
30 | 31 |

32 | {body} 33 |

34 |
35 | 36 | 42 |
43 | ) 44 | 45 | export default Message 46 | -------------------------------------------------------------------------------- /packages/chat/src/ui/components/Loading.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | const Loading = ({ message = 'Loading...', children }) => ( 4 |

5 | {message}... 6 | {children} 7 |

8 | ) 9 | export default Loading 10 | -------------------------------------------------------------------------------- /packages/chat/src/ui/components/Team.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Route, Switch } from 'react-router-dom' 3 | import TeamSidebar from './TeamSidebar' 4 | import Channel from './Channel' 5 | 6 | const Team = ({ team }) => { 7 | console.log( 8 | `%c TEAM render: ${team.name}`, 9 | 'background-color: blue; color: white', 10 | ) 11 | const { channels } = team 12 | return ( 13 |
14 | 15 | 16 | 17 |

Please select a channel

18 |
19 | { 23 | if (!channels) throw new Error('no channels') 24 | if (!match) throw new Error('no match') 25 | 26 | const { params } = match 27 | if (!match) return

No match params

28 | const { channelId: selectedChannelId } = params 29 | if (!selectedChannelId) return

Invalid channelId

30 | const selectedChannel = channels.find( 31 | (c) => c.id === selectedChannelId, 32 | ) 33 | if (!selectedChannel) 34 | return ( 35 |
36 |

Could not find channel with id {selectedChannelId}

37 |
{JSON.stringify(channels, null, '  ')}
38 |
39 | ) 40 | return 41 | }} 42 | /> 43 |
44 |
45 | ) 46 | } 47 | export default Team 48 | -------------------------------------------------------------------------------- /packages/chat/src/ui/components/TeamSelector.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import TeamLink from './TeamSelector/TeamLink' 3 | 4 | const TeamSelector = ({ teams }) => ( 5 | 24 | ) 25 | 26 | export default TeamSelector 27 | -------------------------------------------------------------------------------- /packages/chat/src/ui/components/TeamSelector/TeamLink.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Link, useRouteMatch } from 'react-router-dom' 3 | 4 | const TeamLink = ({ team }) => { 5 | const match = useRouteMatch({ 6 | path: `/team/${team.id}`, 7 | exact: false, 8 | }) 9 | 10 | return ( 11 | 18 |
19 | {`Join 24 |
25 | 26 | ) 27 | } 28 | 29 | export default TeamLink 30 | -------------------------------------------------------------------------------- /packages/chat/src/ui/components/TeamSidebar.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import ChannelLink from './TeamSidebar/ChannelLink' 3 | 4 | const TeamSidebar = ({ team }) => { 5 | return ( 6 |
7 |
8 |
9 |

10 | {team.name} 11 |

12 | 13 |
14 | 21 | 22 | Chris User 23 | 24 |
25 |
26 | 27 | 39 |
40 | 41 | 68 |
69 | 72 |
73 |
74 | ) 75 | } 76 | 77 | export default TeamSidebar 78 | -------------------------------------------------------------------------------- /packages/chat/src/ui/components/TeamSidebar/ChannelLink.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Link, useRouteMatch } from 'react-router-dom' 3 | 4 | const ChannelLink = ({ to, channel }) => { 5 | const match = useRouteMatch(to) 6 | return ( 7 | 14 | 15 | {channel.name} 16 | 17 | ) 18 | } 19 | 20 | export default ChannelLink 21 | -------------------------------------------------------------------------------- /packages/chat/src/utils/api.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable promise/always-return */ 2 | import { useEffect } from 'react' 3 | import { Deferred } from './deferred' 4 | 5 | /** 6 | * A custom React hook that fetches data asynchronously and updates the state with the result. 7 | * @param {() => Promise} getData - A function that returns a Promise that resolves to the data to be fetched. 8 | * @param {{ 9 | * stateName: string; 10 | * otherStatesToMonitor?: unknown[]; 11 | * setter: (arg: any) => void; 12 | * }} options - An object containing the state name, an optional array of other states to monitor, and a setter function to update the state. 13 | * @return {void} 14 | */ 15 | export function useAsyncDataEffect(getData, options) { 16 | let cancelled = false 17 | const { setter, stateName } = options 18 | useEffect(() => { 19 | const d = new Deferred() 20 | 21 | getData() 22 | .then((jsonData) => { 23 | if (cancelled) return 24 | else d.resolve(jsonData) 25 | }) 26 | .catch((err) => d.reject(err)) 27 | 28 | d.promise 29 | .then((data) => { 30 | if (!cancelled) { 31 | console.info( 32 | '%c Updating state: ' + stateName, 33 | 'background: green; color: white; display: block;', 34 | ) 35 | setter(data) 36 | } 37 | }) 38 | .catch(console.error) 39 | return () => { 40 | cancelled = true 41 | } 42 | }, [...(options.otherStatesToMonitor || []), stateName]) 43 | } 44 | -------------------------------------------------------------------------------- /packages/chat/src/utils/date.cjs: -------------------------------------------------------------------------------- 1 | const { format } = require('date-fns') 2 | 3 | /** 4 | * Formats a given date object into a string with the format 'MMM dd, yyyy HH:MM:SS a'. 5 | * 6 | * @param {Date} date - The date object to format. 7 | * @returns {string} The formatted date string. 8 | */ 9 | function formatTimestamp(date) { 10 | return format(date, 'MMM dd, yyyy HH:MM:SS a') 11 | } 12 | 13 | module.exports = { formatTimestamp } -------------------------------------------------------------------------------- /packages/chat/src/utils/deferred.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A class that represents a deferred operation. 3 | * @class 4 | */ 5 | export class Deferred { 6 | // The promise object associated with the deferred operation. 7 | #_promise 8 | /** 9 | * The function to call to resolve the deferred operation. 10 | * @type {(reason: any) => void} 11 | */ 12 | #_resolve 13 | /** 14 | * The function to call to reject the deferred operation. 15 | * @type {(reason: any) => void} 16 | */ 17 | #_reject 18 | /** 19 | * Creates a new instance of the Deferred class. 20 | * @constructor 21 | * @param {string} [description] - A description of the deferred operation. 22 | */ 23 | constructor(description) { 24 | /** 25 | * The promise object associated with the deferred operation. 26 | * @type {Promise} 27 | * @private 28 | */ 29 | this.#_promise = new Promise((resolve, reject) => { 30 | this.#_resolve = resolve 31 | this.#_reject = reject 32 | }) 33 | 34 | /** 35 | * The function to call to resolve the deferred operation. 36 | * @type {function} 37 | * @private 38 | */ 39 | this.#_resolve 40 | 41 | /** 42 | * The function to call to reject the deferred operation. 43 | * @type {function} 44 | * @private 45 | */ 46 | this.#_reject 47 | } 48 | 49 | /** 50 | * Gets the promise object associated with the deferred operation. 51 | * @type {Promise} 52 | */ 53 | get promise() { 54 | return this.#_promise 55 | } 56 | 57 | /** 58 | * Gets the function to call to resolve the deferred operation. 59 | * @type {function} 60 | */ 61 | get resolve() { 62 | return this.#_resolve 63 | } 64 | 65 | /** 66 | * Gets the function to call to reject the deferred operation. 67 | * @type {function} 68 | */ 69 | get reject() { 70 | return this.#_reject 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /packages/chat/src/utils/error.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Stringify an Error instance 3 | * @param {Error} err - The error to stringify 4 | * @return {string} 5 | */ 6 | function stringifyErrorValue(err) { 7 | return `${err.name.toUpperCase()}: ${err.message} 8 | ${err.stack || '(no stack trace information)'}` 9 | } 10 | 11 | /** 12 | * Stringify a thrown value 13 | * 14 | * @param {any} errorDescription 15 | * @param {any} err 16 | * @return {string} 17 | */ 18 | export function stringifyError(errorDescription, err) { 19 | return `${errorDescription}\n${ 20 | err instanceof Error 21 | ? stringifyErrorValue(err) 22 | : err 23 | ? '' + err 24 | : '(missing error information)' 25 | }` 26 | } 27 | -------------------------------------------------------------------------------- /packages/chat/src/utils/http-error.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @enum {number} 3 | */ 4 | const HTTPErrorKind = { 5 | Information: 100, 6 | Success: 200, 7 | Redirect: 300, 8 | Client: 400, 9 | Server: 500, 10 | } 11 | 12 | /** 13 | * 14 | * @param {number} status 15 | * @return {HTTPErrorKind} 16 | */ 17 | function determineKind(status) { 18 | const statusCode = status 19 | if (status >= 100 && status < 200) return HTTPErrorKind.Information 20 | else if (status < 300) return HTTPErrorKind.Success 21 | else if (status < 400) return HTTPErrorKind.Redirect 22 | else if (status < 500) return HTTPErrorKind.Client 23 | else if (status < 600) return HTTPErrorKind.Server 24 | else throw new Error(`Unknown HTTP status code ${status}`) 25 | } 26 | 27 | /** @param {HTTPErrorKind} kind */ 28 | class HTTPError extends Error { 29 | /** 30 | * 31 | * @param {Pick} info 32 | * @param {string} message 33 | */ 34 | constructor(info, message) { 35 | super( 36 | `HTTPError [status: ${info.statusText} (${info.status})]\n${message}`, 37 | ) 38 | this.kind = determineKind(info.status) 39 | } 40 | } 41 | 42 | module.exports = { HTTPError, HTTPErrorKind } 43 | -------------------------------------------------------------------------------- /packages/chat/src/utils/networking.js: -------------------------------------------------------------------------------- 1 | import { stringifyError } from './error' 2 | import { HTTPError } from './http-error.cjs' 3 | 4 | /** 5 | * 6 | * @param {RequestInfo} input 7 | * @param {RequestInit} [init] 8 | */ 9 | async function getJSON(input, init) { 10 | try { 11 | const response = await fetch(input, init) 12 | const responseJSON = await response.json() 13 | return { response, json: responseJSON } 14 | } catch (err) { 15 | throw new Error( 16 | stringifyError( 17 | `Networking/getJSON: An error was encountered while fetching ${JSON.stringify( 18 | input, 19 | )}`, 20 | err, 21 | ), 22 | ) 23 | } 24 | } 25 | 26 | /** 27 | * 28 | * @param {string} path 29 | * @param {RequestInit} [init] 30 | */ 31 | export async function apiCall(path, init) { 32 | let response 33 | /** @type {{}} */ 34 | let json 35 | try { 36 | const jsonRespInfo = await getJSON(`/api/${path}`, init) 37 | response = jsonRespInfo.response 38 | json = jsonRespInfo.json 39 | } catch (err) { 40 | if (err instanceof HTTPError) throw err 41 | throw new Error( 42 | stringifyError( 43 | `Networking/apiCall: An error was encountered while making api call to ${path}`, 44 | err, 45 | ), 46 | ) 47 | } 48 | if (!response.ok) { 49 | json = null 50 | throw new HTTPError(response, 'Problem while making API call') 51 | } 52 | return json 53 | } 54 | -------------------------------------------------------------------------------- /packages/chat/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const { colors } = require('tailwindcss/defaultTheme'); 2 | 3 | module.exports = { 4 | future: { 5 | removeDeprecatedGapUtilities: true, 6 | purgeLayersByDefault: true, 7 | }, 8 | content: ['index.html', './src/ui/**/*.jsx', './src/ui/**/*.tsx'], 9 | theme: { 10 | extend: { 11 | colors: { 12 | black: colors.black, 13 | white: colors.white, 14 | 15 | red: { 16 | ...colors.red, 17 | ...{ 18 | 400: '#ef5753', 19 | 100: '#fcebea', 20 | }, 21 | }, 22 | 23 | indigo: { 24 | ...colors.indigo, 25 | ...{ 26 | 800: '#2f365f', 27 | 900: '#191e38', 28 | }, 29 | }, 30 | 31 | purple: { 32 | ...colors.purple, 33 | ...{ 34 | 300: '#d6bbfc', 35 | }, 36 | }, 37 | }, 38 | }, 39 | }, 40 | variants: { 41 | appearance: [ 42 | 'responsive', 43 | 'hover', 44 | 'focus', 45 | 'active', 46 | 'group-hover', 47 | ], 48 | backgroundAttachment: ['responsive'], 49 | backgroundColors: ['responsive', 'hover', 'focus'], 50 | backgroundPosition: ['responsive'], 51 | backgroundRepeat: [], 52 | backgroundSize: ['responsive'], 53 | borderCollapse: [], 54 | borderColors: ['responsive', 'hover', 'focus'], 55 | borderRadius: ['responsive'], 56 | borderStyle: ['responsive', 'hover', 'focus'], 57 | borderWidths: ['responsive'], 58 | cursor: ['responsive'], 59 | display: ['responsive'], 60 | flexbox: ['responsive'], 61 | float: ['responsive'], 62 | fonts: ['responsive'], 63 | fontWeights: ['responsive', 'hover', 'focus'], 64 | height: ['responsive'], 65 | leading: ['responsive'], 66 | lists: ['responsive'], 67 | margin: ['responsive'], 68 | maxHeight: ['responsive'], 69 | maxWidth: ['responsive'], 70 | minHeight: ['responsive'], 71 | minWidth: ['responsive'], 72 | negativeMargin: ['responsive'], 73 | objectFit: false, 74 | objectPosition: false, 75 | opacity: ['responsive', 'hover'], 76 | outline: ['focus'], 77 | overflow: ['responsive'], 78 | padding: ['responsive'], 79 | pointerEvents: ['responsive'], 80 | position: ['responsive'], 81 | resize: ['responsive'], 82 | shadows: ['responsive', 'hover', 'focus'], 83 | svgFill: [], 84 | svgStroke: [], 85 | tableLayout: ['responsive'], 86 | textAlign: ['responsive'], 87 | textColors: ['responsive', 'hover', 'focus'], 88 | textSizes: ['responsive'], 89 | textStyle: ['responsive', 'hover', 'focus'], 90 | tracking: ['responsive'], 91 | userSelect: ['responsive'], 92 | verticalAlign: ['responsive'], 93 | visibility: ['responsive'], 94 | whitespace: ['responsive'], 95 | width: ['responsive'], 96 | zIndex: ['responsive'], 97 | }, 98 | plugins: [], 99 | }; 100 | -------------------------------------------------------------------------------- /packages/chat/tests/components/Channel.test.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as renderer from 'react-test-renderer' 3 | import Channel from '../../src/ui/components/Channel' 4 | 5 | test.skip('Link changes the class when hovered', () => { 6 | const component = renderer.create( 7 | , 15 | ) 16 | const tree = component.toJSON() 17 | expect(tree).toMatchSnapshot() 18 | }) 19 | -------------------------------------------------------------------------------- /packages/chat/tests/components/ChannelFooter.test.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as renderer from 'react-test-renderer' 3 | import Footer from '../../src/ui/components/Channel/Footer' 4 | 5 | test('Link changes the class when hovered', () => { 6 | const component = renderer.create( 7 |