├── .eslintignore ├── .github ├── crush-pics │ └── config.json └── workflows │ ├── auto-merge.yml │ ├── codeql-analysis.yml │ ├── node.js-solution.yml │ ├── node.js.yml │ ├── optimize-images.yml │ └── ts-nightly-tests.yml ├── .gitignore ├── .vscode └── settings.json ├── COURSE_FILES ├── 01-yarn-workspaces │ └── types │ │ ├── package.json │ │ ├── src │ │ ├── index.ts │ │ ├── type-guards.ts │ │ └── types.ts │ │ ├── tests │ │ ├── tsconfig.json │ │ └── type-guard.test.ts │ │ └── tsconfig.json ├── 02-second-package │ └── utils │ │ ├── package.json │ │ ├── src │ │ ├── api.ts │ │ ├── date.ts │ │ ├── deferred.ts │ │ ├── error.ts │ │ ├── http-error.ts │ │ ├── index.ts │ │ └── networking.ts │ │ ├── tests │ │ ├── date.test.ts │ │ ├── deferred.test.ts │ │ ├── error.test.ts │ │ ├── http-error.test.ts │ │ └── tsconfig.json │ │ └── tsconfig.json ├── 04-linting │ └── .eslintrc ├── 05-lerna │ └── lerna.json ├── 06-scripty │ └── scripts │ │ ├── packages │ │ ├── build.sh │ │ ├── lint.sh │ │ └── test.sh │ │ └── workspace │ │ ├── build.sh │ │ ├── clean.sh │ │ ├── lint.sh │ │ └── test.sh ├── 07-commitlint-and-changelogs │ └── commitlint.config.js ├── 09-internal-dependents │ ├── data │ │ ├── .babelrc │ │ ├── .eslintrc │ │ ├── package.json │ │ ├── src │ │ │ ├── channels.ts │ │ │ ├── index.ts │ │ │ ├── messages.ts │ │ │ └── teams.ts │ │ ├── tests │ │ │ ├── channel.test.ts │ │ │ ├── teams.test.ts │ │ │ └── tsconfig.json │ │ └── tsconfig.json │ └── ui │ │ ├── .babelrc │ │ ├── .eslintrc │ │ ├── API_EXAMPLES.http │ │ ├── assets │ │ ├── app.css │ │ └── 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 │ │ ├── db.json │ │ ├── index.html │ │ ├── package.json │ │ ├── server │ │ ├── api-server.js │ │ └── server.js │ │ ├── src │ │ ├── App.tsx │ │ ├── components │ │ │ ├── Channel.tsx │ │ │ ├── Channel │ │ │ │ ├── Footer.tsx │ │ │ │ ├── Header.tsx │ │ │ │ └── Message.tsx │ │ │ ├── Loading.tsx │ │ │ ├── SelectedChannel.tsx │ │ │ ├── SelectedTeam.tsx │ │ │ ├── Team.tsx │ │ │ ├── TeamSelector.tsx │ │ │ ├── TeamSelector │ │ │ │ └── TeamLink.tsx │ │ │ ├── TeamSidebar.tsx │ │ │ └── TeamSidebar │ │ │ │ └── ChannelLink.tsx │ │ └── index.ts │ │ ├── tests │ │ ├── components │ │ │ ├── Channel.test.tsx │ │ │ ├── ChannelFooter.test.tsx │ │ │ ├── ChannelHeader.test.tsx │ │ │ ├── ChannelMessage.test.tsx │ │ │ ├── TeamSelector.test.tsx │ │ │ ├── TeamSidebar.test.tsx │ │ │ └── __snapshots__ │ │ │ │ ├── Channel.test.tsx.snap │ │ │ │ ├── ChannelFooter.test.tsx.snap │ │ │ │ ├── ChannelHeader.test.tsx.snap │ │ │ │ ├── ChannelMessage.test.tsx.snap │ │ │ │ ├── TeamSelector.test.tsx.snap │ │ │ │ └── TeamSidebar.test.tsx.snap │ │ └── tsconfig.json │ │ └── tsconfig.json ├── 11-api-report │ ├── api-extractor-base.json │ ├── packages │ │ └── PKG │ │ │ └── api-extractor.json │ └── scripts │ │ └── packages │ │ └── api-report.sh └── 12-api-docs │ └── scripts │ └── workspace │ └── api-docs.sh ├── LICENSE ├── README.md ├── notes ├── 00-intro.md ├── 01-yarn-workspaces.md ├── 02-composite-project.md ├── 03-tests.md ├── 04-linting.md ├── 05-lerna.md ├── 06-scripty.md ├── 07-changelogs.md ├── 08-publishing-and-versioning.md ├── 09-internal-dependencies.md ├── 10-independent-versioning.md ├── 11-api-report.md ├── 12-api-docs.md ├── README.md └── img │ └── dherman-monorepo-1.png ├── package.json ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | packages/*/tests/*.ts -------------------------------------------------------------------------------- /.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-solution.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 (solution) 5 | 6 | on: 7 | schedule: 8 | # * is a special character in YAML so you have to quote this string 9 | - cron: '20 * * * *' 10 | 11 | jobs: 12 | build: 13 | 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 | with: 24 | ref: solution 25 | - name: Cache Setup 26 | uses: actions/cache@v2 27 | with: 28 | path: | 29 | ~/cache 30 | ~/.dts/ 31 | !~/cache/exclude 32 | **/node_modules 33 | key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} 34 | 35 | - name: Use (Volta) Node.js ${{ matrix.node-version }} 36 | uses: volta-cli/action@v1 37 | with: 38 | node-version: ${{ matrix.node-version }} 39 | - name: Install dependencies 40 | run: yarn 41 | - name: Build 42 | run: yarn build 43 | - name: Build Docs 44 | run: yarn api-report && yarn api-docs 45 | - name: Run tests 46 | run: yarn test 47 | -------------------------------------------------------------------------------- /.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 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [10.x, 12.x, 14.x] 20 | 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v2 24 | - name: Cache Setup 25 | uses: actions/cache@v2 26 | with: 27 | path: | 28 | ~/cache 29 | ~/.dts/ 30 | !~/cache/exclude 31 | **/node_modules 32 | key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} 33 | 34 | - name: Use (Volta) Node.js ${{ matrix.node-version }} 35 | uses: volta-cli/action@v1 36 | with: 37 | node-version: ${{ matrix.node-version }} 38 | - name: Install dependencies 39 | run: yarn 40 | - name: Build 41 | run: yarn build 42 | - name: Build Docs 43 | run: yarn api-report && yarn api-docs 44 | - name: Run tests 45 | run: yarn test 46 | -------------------------------------------------------------------------------- /.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 (solution) 3 | 4 | on: 5 | schedule: 6 | # * is a special character in YAML so you have to quote this string 7 | - cron: '20 * * * *' 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [14.x] 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v2 20 | with: 21 | ref: solution 22 | - name: Cache Setup 23 | uses: actions/cache@v2 24 | with: 25 | path: | 26 | ~/cache 27 | ~/.dts/ 28 | !~/cache/exclude 29 | **/node_modules 30 | key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} 31 | 32 | - name: Use (Volta) Node.js ${{ matrix.node-version }} 33 | uses: volta-cli/action@v1 34 | with: 35 | node-version: ${{ matrix.node-version }} 36 | - name: Install dependencies 37 | run: yarn 38 | - name: Install latest TypeScript nightly 39 | run: yarn add -WD typescript@next 40 | - name: Build 41 | run: yarn build 42 | - name: Run tests 43 | run: yarn test 44 | -------------------------------------------------------------------------------- /.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 | **/temp 118 | **/temp-* 119 | _config.yml -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "api-extractor*.json": "jsonc" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /COURSE_FILES/01-yarn-workspaces/types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@shlack/types", 3 | "version": "0.0.1", 4 | "main": "dist/index.js", 5 | "types": "dist/index.d.ts", 6 | "publishConfig": { 7 | "access": "public" 8 | }, 9 | "scripts": { 10 | "build": "tsc -b ." 11 | }, 12 | "devDependencies": { 13 | "typescript": "^4.0.3" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /COURSE_FILES/01-yarn-workspaces/types/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @packageDocumentation 3 | * 4 | * 5 | * 6 | *

Why does this library exist?

7 | * 8 | * `@shlack/types` is a package containing broadly-useful 9 | * types and type guards for our demo slack app. This is part of Mike's 10 | * JS & TS Monorepos Course which you can learn more about by visiting 11 | * 12 | * the repo on GitHub 13 | * 14 | * 15 | * If you want to watch a recorded video of this course, look for it 16 | * on FrontEnd Masters 17 | * 18 | * 19 | * @remarks 20 | * All interfaces are prefixed with `I` 21 | * 22 | * @packageDocumentation 23 | */ 24 | export { isChannel, isMessage, isTeam, isTypedArray } from "./type-guards"; 25 | export { IChannel, IMessage, ITeam, IUser } from "./types"; 26 | -------------------------------------------------------------------------------- /COURSE_FILES/01-yarn-workspaces/types/src/type-guards.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ 3 | import { IChannel, IMessage, ITeam } from "./types"; 4 | 5 | /** 6 | * Check whether a given value is an array where 7 | * each member is of a specified type 8 | * 9 | * @param arr - array to check 10 | * @param check - type guard to use when evaluating each item 11 | * @public 12 | */ 13 | export function isTypedArray( 14 | arr: unknown, 15 | check: (x: any) => x is T 16 | ): arr is T[] { 17 | if (!Array.isArray(arr)) return false; 18 | const mismatch = arr.filter((item) => !check(item)); 19 | if (mismatch.length > 0) return false; 20 | return true; 21 | } 22 | 23 | /** 24 | * Check whether a given value is an {@link @shlack/types#ITeam} 25 | * @param arg - value to check 26 | * @beta 27 | * 28 | * @example 29 | * Here's an example of how to use this guard 30 | * ```ts 31 | * const team = { id: 'li', name: 'LinkedIn' }; 32 | * isTeam(team); // true 33 | * ``` 34 | */ 35 | export function isTeam(arg: any): arg is ITeam { 36 | return ( 37 | typeof arg.name === "string" && 38 | typeof arg.id === "string" && 39 | Array.isArray(arg.channels) 40 | ); 41 | } 42 | 43 | /** 44 | * Check whether a given value is an {@link @shlack/types#IChannel} 45 | * @param arg - value to check 46 | * @beta 47 | */ 48 | export function isChannel(arg: any): arg is IChannel { 49 | return ( 50 | typeof arg.id === "string" && 51 | typeof arg.teamId === "string" && 52 | typeof arg.description === "string" && 53 | typeof arg.name === "string" 54 | ); 55 | } 56 | 57 | /** 58 | * Check whether a given value is an {@link @shlack/types#IMessage} 59 | * @param arg - value to check 60 | * @beta 61 | */ 62 | export function isMessage(arg: any): arg is IMessage { 63 | return ( 64 | typeof arg.teamId === "string" && 65 | typeof arg.channelId === "string" && 66 | typeof arg.userId === "string" && 67 | typeof arg.body === "string" 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /COURSE_FILES/01-yarn-workspaces/types/src/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A user participating in a chat 3 | * @public 4 | */ 5 | export interface IUser { 6 | id: number; 7 | username: string; 8 | name: string; 9 | iconUrl: string; 10 | } 11 | 12 | /** 13 | * A chat message 14 | * @public 15 | */ 16 | export interface IMessage { 17 | id: number; 18 | teamId: string; 19 | channelId: string; 20 | userId: string; 21 | createdAt: string; 22 | user: IUser; 23 | body: string; 24 | } 25 | 26 | /** 27 | * A team, containing one or more chat channels 28 | * @public 29 | * 30 | * @remarks 31 | * The {@link @shlack/types#isTeam} type guard may be used to 32 | * detect whether a value conforms to this interface 33 | */ 34 | export interface ITeam { 35 | iconUrl: string; 36 | name: string; 37 | id: string; 38 | channels: IChannel[]; 39 | } 40 | 41 | /** 42 | * A chat channel, containing many chat messages 43 | * @public 44 | */ 45 | export interface IChannel { 46 | teamId: string; 47 | name: string; 48 | description: string; 49 | id: string; 50 | messages: IMessage[]; 51 | } 52 | -------------------------------------------------------------------------------- /COURSE_FILES/01-yarn-workspaces/types/tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [{ "path": "..", "name": "@shlack/types" }], 3 | "compilerOptions": { 4 | "lib": ["DOM", "ES2018"], 5 | "types": ["jest"], 6 | "baseUrl": "..", 7 | "noEmit": true 8 | }, 9 | "include": ["."] 10 | } 11 | -------------------------------------------------------------------------------- /COURSE_FILES/01-yarn-workspaces/types/tests/type-guard.test.ts: -------------------------------------------------------------------------------- 1 | import { isChannel, isMessage, isTeam, isTypedArray } from "../src"; 2 | 3 | describe("isChannel() tests", function () { 4 | test("valid channel", () => { 5 | expect( 6 | isChannel({ 7 | id: "123", 8 | teamId: "12gh", 9 | description: "channel description", 10 | iconUrl: "", 11 | messages: [], 12 | name: "general", 13 | }) 14 | ).toEqual(true); 15 | }); 16 | test("invalid channel", () => { 17 | expect( 18 | isChannel({ 19 | description: "channel description", 20 | messages: [], 21 | name: "general", 22 | }) 23 | ).toEqual(false); 24 | }); 25 | }); 26 | 27 | describe("isMessage() tests", function () { 28 | test("valid message", () => { 29 | expect( 30 | isMessage({ 31 | id: 131, 32 | teamId: "12gh", 33 | channelId: "12gh", 34 | userId: "12gh", 35 | body: "hello, world", 36 | }) 37 | ).toEqual(true); 38 | }); 39 | test("invalid message", () => { 40 | expect( 41 | isMessage({ 42 | description: "message description", 43 | messages: [], 44 | name: "general", 45 | }) 46 | ).toEqual(false); 47 | }); 48 | }); 49 | 50 | describe("isTeam() tests", function () { 51 | test("valid message", () => { 52 | expect( 53 | isTeam({ 54 | name: "12gh", 55 | id: "12gh", 56 | channels: [], 57 | }) 58 | ).toEqual(true); 59 | }); 60 | test("invalid message", () => { 61 | expect( 62 | isTeam({ 63 | name: "12gh", 64 | id: "12gh", 65 | }) 66 | ).toEqual(false); 67 | }); 68 | }); 69 | 70 | describe("isTypedArray() tests", function () { 71 | test("non-array", () => { 72 | expect( 73 | // @ts-expect-error 74 | isTypedArray(null, () => true) 75 | ).toEqual(false); 76 | }); 77 | test("empty array", () => { 78 | expect(isTypedArray([], (x: any): x is any => true)).toEqual(true); 79 | }); 80 | test("homogenous array [1, 2, 3]", () => { 81 | expect( 82 | isTypedArray([1, 2, 3], (x): x is number => typeof x === "number") 83 | ).toEqual(true); 84 | }); 85 | test("mixed array [1, 'a', 3]", () => { 86 | expect( 87 | isTypedArray( 88 | [1, "a", 3], 89 | (x): x is number => ["number"].indexOf(typeof x) >= 0 90 | ) 91 | ).toEqual(false); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /COURSE_FILES/01-yarn-workspaces/types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "types": [], 5 | "sourceMap": true, 6 | "target": "ES2018", 7 | "strict": true, 8 | "noUnusedLocals": true, 9 | "noUnusedParameters": true, 10 | "noImplicitReturns": true, 11 | "declaration": true, 12 | "outDir": "dist", 13 | "rootDir": "src" 14 | }, 15 | "include": ["src"] 16 | } 17 | -------------------------------------------------------------------------------- /COURSE_FILES/02-second-package/utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@shlack/utils", 3 | "version": "0.0.1", 4 | "main": "dist/index.js", 5 | "types": "dist/index.d.ts", 6 | "publishConfig": { 7 | "access": "public" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /COURSE_FILES/02-second-package/utils/src/api.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import Deferred from "./deferred"; 3 | 4 | /** 5 | * 6 | * @param getData 7 | * @param options 8 | */ 9 | export function useAsyncDataEffect( 10 | getData: () => Promise, 11 | options: { 12 | stateName: string; 13 | otherStatesToMonitor?: unknown[]; 14 | setter: (arg: T) => void; 15 | } 16 | ): void { 17 | let cancelled = false; 18 | const { setter, stateName } = options; 19 | useEffect(() => { 20 | const d = new Deferred(); 21 | 22 | getData() 23 | .then((jsonData) => { 24 | if (cancelled) return; 25 | else d.resolve(jsonData); 26 | }) 27 | .catch(d.reject); 28 | 29 | d.promise 30 | .then((data) => { 31 | if (!cancelled) { 32 | console.info( 33 | "%c Updating state: " + stateName, 34 | "background: green; color: white; display: block;" 35 | ); 36 | setter(data); 37 | } 38 | }) 39 | .catch(console.error); 40 | return () => { 41 | cancelled = true; 42 | }; 43 | }, [...(options.otherStatesToMonitor || []), stateName]); 44 | } 45 | -------------------------------------------------------------------------------- /COURSE_FILES/02-second-package/utils/src/date.ts: -------------------------------------------------------------------------------- 1 | import { format } from "date-fns"; 2 | 3 | /** 4 | * Format a timestamp as a string 5 | * @param date - the date value to format 6 | * @public 7 | */ 8 | export function formatTimestamp(date: Date): string { 9 | return format(date, "MMM dd, yyyy HH:MM:SS a"); 10 | } 11 | -------------------------------------------------------------------------------- /COURSE_FILES/02-second-package/utils/src/deferred.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @public 3 | */ 4 | export type ResolveHandler = (value: T | PromiseLike) => void; 5 | /** 6 | * @public 7 | */ 8 | export type RejectHandler = (reason: unknown) => void; 9 | 10 | /** 11 | * An "inverted" promise, that can be resolved 12 | * from the outside 13 | * 14 | * @public 15 | */ 16 | class Deferred { 17 | #_promise: Promise; 18 | #_resolve!: ResolveHandler; 19 | #_reject!: RejectHandler; 20 | constructor() { 21 | this.#_promise = new Promise((resolve, reject) => { 22 | this.#_resolve = resolve; 23 | this.#_reject = reject; 24 | }); 25 | } 26 | get promise(): Promise { 27 | return this.#_promise; 28 | } 29 | get resolve(): ResolveHandler { 30 | return this.#_resolve; 31 | } 32 | get reject(): RejectHandler { 33 | return this.#_reject; 34 | } 35 | } 36 | export default Deferred; 37 | -------------------------------------------------------------------------------- /COURSE_FILES/02-second-package/utils/src/error.ts: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Stringify an Error instance 5 | * @param err - The error to stringify 6 | */ 7 | function stringifyErrorValue(err: Error): string { 8 | return `${err.name.toUpperCase()}: ${err.message} 9 | ${err.stack || '(no stack trace information)'}`; 10 | } 11 | 12 | /** 13 | * Stringify a thrown value 14 | * 15 | * @param errorDescription 16 | * @param err 17 | * 18 | */ 19 | export function stringifyError(errorDescription: string, err: unknown): string { 20 | return `${errorDescription}\n${ 21 | err instanceof Error 22 | ? stringifyErrorValue(err) 23 | : err 24 | // eslint-disable-next-line @typescript-eslint/restrict-plus-operands 25 | ? '' + err 26 | : '(missing error information)' 27 | }`; 28 | } 29 | -------------------------------------------------------------------------------- /COURSE_FILES/02-second-package/utils/src/http-error.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * High-level outcome of an HTTP status code 3 | * @public 4 | */ 5 | export enum HTTPErrorKind { 6 | Information = 100, 7 | Success = 200, 8 | Redirect = 300, 9 | Client = 400, 10 | Server = 500, 11 | } 12 | 13 | /** 14 | * Determine the high-level outcome of a HTTP status code 15 | * 16 | * @param status - http status code 17 | * @see HTTPErrorKind 18 | * @public 19 | */ 20 | function determineKind(status: number): HTTPErrorKind { 21 | if (status >= 100 && status < 200) return HTTPErrorKind.Information; 22 | else if (status < 300) return HTTPErrorKind.Success; 23 | else if (status < 400) return HTTPErrorKind.Redirect; 24 | else if (status < 500) return HTTPErrorKind.Client; 25 | else if (status < 600) return HTTPErrorKind.Server; 26 | else throw new Error(`Unknown HTTP status code ${status}`); 27 | } 28 | 29 | /** 30 | * An error that's associated with a HTTP response status code 31 | * @public 32 | */ 33 | export default class HTTPError extends Error { 34 | kind: HTTPErrorKind; 35 | 36 | constructor(info: { statusText: string; status: number }, message: string) { 37 | super( 38 | `HTTPError [status: ${info.statusText} (${info.status})]\n${message}` 39 | ); 40 | this.kind = determineKind(info.status); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /COURSE_FILES/02-second-package/utils/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./api"; 2 | export * from "./date"; 3 | export { default as Deferred, RejectHandler, ResolveHandler } from "./deferred"; 4 | export * from "./networking"; 5 | export * from "./error"; 6 | export { default as HTTPError, HTTPErrorKind } from "./http-error"; 7 | -------------------------------------------------------------------------------- /COURSE_FILES/02-second-package/utils/src/networking.ts: -------------------------------------------------------------------------------- 1 | import { stringifyError } from "./error"; 2 | import HTTPError from "./http-error"; 3 | 4 | /** 5 | * Make a GET request, and decode the response body as JSON 6 | * 7 | * @param input - request info 8 | * @param init - request options 9 | * @internal 10 | */ 11 | async function getJSON(input: RequestInfo, init?: RequestInit) { 12 | try { 13 | const response = await fetch(input, init); 14 | const responseJSON = await response.json(); 15 | return { response, json: responseJSON }; 16 | } catch (err) { 17 | throw new Error( 18 | stringifyError( 19 | `Networking/getJSON: An error was encountered while fetching ${JSON.stringify( 20 | input 21 | )}`, 22 | err 23 | ) 24 | ); 25 | } 26 | } 27 | 28 | /** 29 | * Make a same-origin GET request to the API 30 | * 31 | * @param path - API path 32 | * @param init - fetch options 33 | * @public 34 | */ 35 | export async function apiCall( 36 | path: string, 37 | init?: RequestInit 38 | ): Promise { 39 | let response; 40 | let json; 41 | try { 42 | const jsonRespInfo = await getJSON(`/api/${path}`, init); 43 | response = jsonRespInfo.response; 44 | json = jsonRespInfo.json; 45 | } catch (err) { 46 | if (err instanceof HTTPError) throw err; 47 | throw new Error( 48 | stringifyError( 49 | `Networking/apiCall: An error was encountered while making api call to ${path}`, 50 | err 51 | ) 52 | ); 53 | } 54 | if (!response.ok) 55 | throw new HTTPError(response, "Problem while making API call"); 56 | return json; 57 | } 58 | -------------------------------------------------------------------------------- /COURSE_FILES/02-second-package/utils/tests/date.test.ts: -------------------------------------------------------------------------------- 1 | import { formatTimestamp } from "@shlack/utils"; 2 | 3 | describe("formatTimestamp() tests", function () { 4 | let x = 4; 5 | test("01-01-2020", () => { 6 | expect(formatTimestamp(new Date("01-01-2020"))).toBe( 7 | "Jan 01, 2020 00:01:00 AM" 8 | ); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /COURSE_FILES/02-second-package/utils/tests/deferred.test.ts: -------------------------------------------------------------------------------- 1 | import { Deferred } from "@shlack/utils"; 2 | 3 | describe("Deferred tests", function () { 4 | test("Constructor does not error", () => { 5 | const d = new Deferred(); 6 | expect(d).toBeTruthy(); 7 | expect(typeof d.resolve).toBe("function"); 8 | expect(typeof d.reject).toBe("function"); 9 | expect(d.promise).toBeInstanceOf(Promise); 10 | }); 11 | test("promise resolves when Deferred#resolve() is called", async () => { 12 | expect.assertions(2); 13 | const d = new Deferred(); 14 | setTimeout(() => { 15 | d.resolve(42); 16 | expect(true).toBe(true); 17 | }, 30); 18 | const val = await d.promise; 19 | expect(val).toBe(42); 20 | }); 21 | test("promise rejects when Deferred#reject() is called", async () => { 22 | expect.assertions(3); 23 | const d = new Deferred(); 24 | setTimeout(() => { 25 | expect(true).toBe(true); 26 | d.reject(-42); 27 | }, 30); 28 | try { 29 | await d.promise; 30 | expect(true).toBe(true); // should never reach this line 31 | } catch (e) { 32 | expect(e).toBe(-42); 33 | } finally { 34 | expect(true).toBe(true); 35 | } 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /COURSE_FILES/02-second-package/utils/tests/error.test.ts: -------------------------------------------------------------------------------- 1 | import { HTTPError, stringifyError } from "@shlack/utils"; 2 | 3 | describe("Error tests", function () { 4 | test("stringifyError", () => { 5 | const e = new HTTPError( 6 | { status: 404, statusText: "Not Found" }, 7 | "Mock error for testing" 8 | ); 9 | const result = stringifyError("mock error for tests", e); 10 | expect(result.split("at").length).toBeGreaterThanOrEqual(5); 11 | expect(result).toContain("mock error for tests"); 12 | expect(result).toContain("HTTPError [status: Not Found (404)]"); 13 | expect(result).toContain("Mock error for testing"); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /COURSE_FILES/02-second-package/utils/tests/http-error.test.ts: -------------------------------------------------------------------------------- 1 | import { HTTPError, HTTPErrorKind } from "@shlack/utils"; 2 | 3 | describe("HTTPError tests", function () { 4 | test("creation", () => { 5 | const e = new HTTPError( 6 | { status: 404, statusText: "Not Found" }, 7 | "Mock error for testing" 8 | ); 9 | expect(e.kind).toBe(HTTPErrorKind.Client); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /COURSE_FILES/02-second-package/utils/tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["DOM", "ES2018"], 4 | "types": ["jest"], 5 | "baseUrl": "..", 6 | "noEmit": true 7 | }, 8 | "include": ["."] 9 | } 10 | -------------------------------------------------------------------------------- /COURSE_FILES/02-second-package/utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "types": [], 5 | "sourceMap": true, 6 | "target": "ES2018", 7 | "strict": true, 8 | "noUnusedLocals": true, 9 | "noUnusedParameters": true, 10 | "noImplicitReturns": true, 11 | "declaration": true 12 | }, 13 | "include": ["src"] 14 | } 15 | -------------------------------------------------------------------------------- /COURSE_FILES/04-linting/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2021": true 4 | }, 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/recommended", 8 | "plugin:@typescript-eslint/recommended-requiring-type-checking" 9 | ], 10 | "parser": "@typescript-eslint/parser", 11 | "parserOptions": { 12 | "ecmaVersion": 12 13 | }, 14 | "plugins": ["@typescript-eslint"], 15 | "rules": { 16 | "prefer-const": "error", 17 | "@typescript-eslint/no-unsafe-member-access": "off", 18 | "@typescript-eslint/no-unsafe-call": "off", 19 | "@typescript-eslint/no-unsafe-assignment": "off" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /COURSE_FILES/05-lerna/lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["packages/*"], 3 | "npmClient": "yarn", 4 | "version": "0.0.1", 5 | "useWorkspaces": true, 6 | "nohoist": ["parcel-bundler"] 7 | } 8 | -------------------------------------------------------------------------------- /COURSE_FILES/06-scripty/scripts/packages/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "┏━━━ 📦 Building $(pwd) ━━━━━━━━━━━━━━━━━━━" 3 | yarn tsc -b -------------------------------------------------------------------------------- /COURSE_FILES/06-scripty/scripts/packages/lint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "┏━━━ 🕵️‍♀️ LINT: eslint src --ext ts,js,tsx,jsx ━━━━━━━" 3 | yarn eslint src --ext ts,js,tsx,jsx -------------------------------------------------------------------------------- /COURSE_FILES/06-scripty/scripts/packages/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "┏━━━ 🎯 TEST: $(pwd) ━━━━━━━━━━━━━━━━━━━" 3 | yarn jest -------------------------------------------------------------------------------- /COURSE_FILES/06-scripty/scripts/workspace/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "┏━━━ 📦 Building Workspace ━━━━━━━━━━━━━━━━━━━" 3 | yarn tsc -b packages -------------------------------------------------------------------------------- /COURSE_FILES/06-scripty/scripts/workspace/clean.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "┏━━━ 🧹 CLEAN ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" 3 | yarn lerna run clean --concurrency 4 4 | -------------------------------------------------------------------------------- /COURSE_FILES/06-scripty/scripts/workspace/lint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "┏━━━ 🕵️‍♀️ LINT: eslint src --ext ts,js,tsx,jsx ━━━━━━━" 3 | yarn lerna run lint --stream --concurrency 1 -------------------------------------------------------------------------------- /COURSE_FILES/06-scripty/scripts/workspace/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "┏━━━ 🎯 TEST: $(pwd) ━━━━━━━━━━━━━━━━━━━" 3 | yarn lerna run test --stream -------------------------------------------------------------------------------- /COURSE_FILES/07-commitlint-and-changelogs/commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ["@commitlint/config-lerna-scopes"] }; 2 | -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/data/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.babelrc" 3 | } 4 | -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/data/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc", 3 | "parserOptions": { 4 | "project": "tsconfig.json" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/data/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@shlack/data", 3 | "version": "0.0.1", 4 | "main": "dist/index.js", 5 | "types": "dist/index.d.ts", 6 | "publishConfig": { 7 | "access": "public" 8 | }, 9 | "scripts": { 10 | "lint": "scripty", 11 | "clean": "rimraf dist *.tsbuildinfo", 12 | "build": "scripty", 13 | "test": "scripty" 14 | }, 15 | "devDependencies": { 16 | "@babel/preset-env": "^7.12.0", 17 | "@babel/preset-typescript": "^7.12.0", 18 | "@types/date-fns": "^2.6.0", 19 | "@types/react": "^16.9.52", 20 | "typescript": "^4.0.3" 21 | }, 22 | "scripty": { 23 | "path": "../../scripts/packages" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/data/src/channels.ts: -------------------------------------------------------------------------------- 1 | import { IChannel, isChannel } from "@shlack/types"; 2 | import { apiCall } from "@shlack/utils"; 3 | 4 | const cachedChannelRecords: Record> = {}; 5 | 6 | export async function getChannelById(id: string): Promise { 7 | let cached = cachedChannelRecords[id]; 8 | if (typeof cached !== "undefined") return await cached; 9 | cached = cachedChannelRecords[id] = apiCall(`Channels/${id}`).then( 10 | (rawData: unknown) => { 11 | if (isChannel(rawData)) return rawData; 12 | throw new Error( 13 | `Unexpected value for channel\n${JSON.stringify(rawData)}` 14 | ); 15 | } 16 | ); 17 | 18 | return await cached; 19 | } 20 | -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/data/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./channels"; 2 | export * from "./messages"; 3 | export * from "./teams"; 4 | -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/data/src/messages.ts: -------------------------------------------------------------------------------- 1 | import { isMessage, isTypedArray, IMessage } from "@shlack/types"; 2 | import { apiCall } from "@shlack/utils"; 3 | 4 | const cachedMessageRecordArrays: Record> = {}; 5 | 6 | export async function getChannelMessages( 7 | teamId: string, 8 | channelId: string 9 | ): Promise { 10 | let cached = cachedMessageRecordArrays[channelId]; 11 | if (typeof cached === "undefined") 12 | cached = cachedMessageRecordArrays[channelId] = apiCall( 13 | `teams/${teamId}/channels/${channelId}/messages` 14 | ).then((rawData) => { 15 | debugger; 16 | if (isTypedArray(rawData, isMessage)) { 17 | return rawData; 18 | } else 19 | throw new Error( 20 | `Unexpected value for message array\n${JSON.stringify(rawData)}` 21 | ); 22 | }); 23 | return await cached; 24 | } 25 | -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/data/src/teams.ts: -------------------------------------------------------------------------------- 1 | import { apiCall } from "@shlack/utils"; 2 | import { isTeam, isTypedArray, ITeam } from "@shlack/types"; 3 | 4 | let cachedAllTeamsList: Promise; 5 | export async function getAllTeams(): Promise { 6 | if (typeof cachedAllTeamsList === "undefined") 7 | cachedAllTeamsList = apiCall("teams").then((rawData: unknown) => { 8 | if (isTypedArray(rawData, isTeam)) return rawData; 9 | throw new Error( 10 | `Unexpected value for teams array\n${JSON.stringify(rawData)}` 11 | ); 12 | }); 13 | 14 | return await cachedAllTeamsList; 15 | } 16 | 17 | const cachedTeamRecords: Record> = {}; 18 | 19 | export async function getTeamById(id: string): Promise { 20 | let cached = cachedTeamRecords[id]; 21 | if (typeof cached === "undefined") 22 | cached = cachedTeamRecords[id] = apiCall(`teams/${id}`).then( 23 | (rawData: unknown) => { 24 | if (isTeam(rawData)) return rawData; 25 | throw new Error( 26 | `Unexpected value for team\n${JSON.stringify(rawData)}` 27 | ); 28 | } 29 | ); 30 | return await cached; 31 | } 32 | -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/data/tests/channel.test.ts: -------------------------------------------------------------------------------- 1 | import * as utils from "@shlack/utils"; 2 | import { getChannelById } from "@shlack/data"; 3 | 4 | jest.mock("@shlack/utils"); 5 | const mockedApiCall = (utils.apiCall as unknown) as jest.MockedFunction< 6 | typeof utils["apiCall"] 7 | >; 8 | mockedApiCall.mockResolvedValue({ 9 | id: "recruiting", 10 | name: "recruiting", 11 | description: "The Next Generation Of Recruiting. Find top talents today!", 12 | teamId: "linkedin", 13 | }); 14 | 15 | describe("getChannelById() tests", function () { 16 | test("fetching a single team", async (done) => { 17 | expect(mockedApiCall.mock.calls.length).toBe(0); 18 | const pr = getChannelById("14"); 19 | let resolvedVal: unknown; 20 | pr.then((val) => { 21 | resolvedVal = val; 22 | }); 23 | expect(pr).toBeInstanceOf(Promise); 24 | expect(resolvedVal).toBeUndefined(); 25 | expect(mockedApiCall.mock.calls.length).toBe(1); 26 | 27 | await pr; 28 | expect(resolvedVal).toMatchObject({ 29 | id: "recruiting", 30 | name: "recruiting", 31 | description: "The Next Generation Of Recruiting. Find top talents today!", 32 | teamId: "linkedin", 33 | }); 34 | done(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/data/tests/teams.test.ts: -------------------------------------------------------------------------------- 1 | import * as utils from "@shlack/utils"; 2 | import { getAllTeams } from "@shlack/data"; 3 | 4 | jest.mock("@shlack/utils"); 5 | const mockedApiCall = (utils.apiCall as unknown) as jest.MockedFunction< 6 | typeof utils["apiCall"] 7 | >; 8 | mockedApiCall.mockImplementation(async (path, _init) => { 9 | if (path === "teams") { 10 | return Promise.resolve([ 11 | { 12 | id: "linkedin", 13 | name: "LinkedIn", 14 | order: 2, 15 | iconUrl: "/assets/img/linkedin.png", 16 | channels: [], 17 | }, 18 | { 19 | id: "ms", 20 | name: "Microsoft", 21 | order: 3, 22 | iconUrl: "/assets/img/microsoft.png", 23 | channels: [], 24 | }, 25 | { 26 | id: "avengers", 27 | name: "Avengers", 28 | order: 4, 29 | iconUrl: "/assets/img/avengers.jpg", 30 | channels: [], 31 | }, 32 | ]); 33 | } else throw new Error("not implemented"); 34 | }); 35 | 36 | describe("getAllTeams() tests", function () { 37 | test("fetching all teams", async (done) => { 38 | expect(mockedApiCall.mock.calls.length).toBe(0); 39 | const pr = getAllTeams(); 40 | let resolvedVal: unknown; 41 | pr.then((val) => { 42 | resolvedVal = val; 43 | }); 44 | expect(pr).toBeInstanceOf(Promise); 45 | expect(resolvedVal).toBeUndefined(); 46 | expect(mockedApiCall.mock.calls.length).toBe(1); 47 | 48 | await pr; 49 | expect(resolvedVal).toMatchObject([ 50 | { 51 | id: "linkedin", 52 | name: "LinkedIn", 53 | order: 2, 54 | iconUrl: "/assets/img/linkedin.png", 55 | channels: [], 56 | }, 57 | { 58 | id: "ms", 59 | name: "Microsoft", 60 | order: 3, 61 | iconUrl: "/assets/img/microsoft.png", 62 | channels: [], 63 | }, 64 | { 65 | id: "avengers", 66 | name: "Avengers", 67 | order: 4, 68 | iconUrl: "/assets/img/avengers.jpg", 69 | channels: [], 70 | }, 71 | ]); 72 | done(); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/data/tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [{ "path": ".." }], 3 | "compilerOptions": { 4 | "lib": ["ES2018"], 5 | "types": ["jest"], 6 | "baseUrl": "..", 7 | "noEmit": true, 8 | "paths": { "@shlack/data": [".."] } 9 | }, 10 | "include": ["."] 11 | } 12 | -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/data/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.settings.json", 3 | "references": [{ "path": "../types" }, { "path": "../utils" }], 4 | "compilerOptions": { 5 | "composite": true, 6 | "outDir": "dist", 7 | "rootDir": "src" 8 | }, 9 | "include": ["src"] 10 | } 11 | -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { "targets": { "node": "10" } }], 4 | "@babel/preset-react", 5 | "@babel/preset-typescript" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc", 3 | "parserOptions": { 4 | "project": "tsconfig.json" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/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 | -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/assets/app.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | html { 12 | line-height: 1.15; /* 1 */ 13 | -webkit-text-size-adjust: 100%; /* 2 */ 14 | } 15 | 16 | /* Sections 17 | ========================================================================== */ 18 | 19 | /** 20 | * Remove the margin in all browsers. 21 | */ 22 | 23 | body { 24 | margin: 0; 25 | } 26 | 27 | /** 28 | * Render the `main` element consistently in IE. 29 | */ 30 | 31 | main { 32 | display: block; 33 | } 34 | 35 | /** 36 | * Correct the font size and margin on `h1` elements within `section` and 37 | * `article` contexts in Chrome, Firefox, and Safari. 38 | */ 39 | 40 | h1 { 41 | font-size: 2em; 42 | margin: 0.67em 0; 43 | } 44 | 45 | /* Grouping content 46 | ========================================================================== */ 47 | 48 | /** 49 | * 1. Add the correct box sizing in Firefox. 50 | * 2. Show the overflow in Edge and IE. 51 | */ 52 | 53 | hr { 54 | box-sizing: content-box; /* 1 */ 55 | height: 0; /* 1 */ 56 | overflow: visible; /* 2 */ 57 | } 58 | 59 | /** 60 | * 1. Correct the inheritance and scaling of font size in all browsers. 61 | * 2. Correct the odd `em` font sizing in all browsers. 62 | */ 63 | 64 | pre { 65 | font-family: monospace, monospace; /* 1 */ 66 | font-size: 1em; /* 2 */ 67 | } 68 | 69 | /* Text-level semantics 70 | ========================================================================== */ 71 | 72 | /** 73 | * Remove the gray background on active links in IE 10. 74 | */ 75 | 76 | a { 77 | background-color: transparent; 78 | } 79 | 80 | /** 81 | * 1. Remove the bottom border in Chrome 57- 82 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 83 | */ 84 | 85 | abbr[title] { 86 | border-bottom: none; /* 1 */ 87 | text-decoration: underline; /* 2 */ 88 | -webkit-text-decoration: underline dotted; 89 | text-decoration: underline dotted; /* 2 */ 90 | } 91 | 92 | /** 93 | * Add the correct font weight in Chrome, Edge, and Safari. 94 | */ 95 | 96 | b, 97 | strong { 98 | font-weight: bolder; 99 | } 100 | 101 | /** 102 | * 1. Correct the inheritance and scaling of font size in all browsers. 103 | * 2. Correct the odd `em` font sizing in all browsers. 104 | */ 105 | 106 | code, 107 | kbd, 108 | samp { 109 | font-family: monospace, monospace; /* 1 */ 110 | font-size: 1em; /* 2 */ 111 | } 112 | 113 | /** 114 | * Add the correct font size in all browsers. 115 | */ 116 | 117 | small { 118 | font-size: 80%; 119 | } 120 | 121 | /** 122 | * Prevent `sub` and `sup` elements from affecting the line height in 123 | * all browsers. 124 | */ 125 | 126 | sub, 127 | sup { 128 | font-size: 75%; 129 | line-height: 0; 130 | position: relative; 131 | vertical-align: baseline; 132 | } 133 | 134 | sub { 135 | bottom: -0.25em; 136 | } 137 | 138 | sup { 139 | top: -0.5em; 140 | } 141 | 142 | /* Embedded content 143 | ========================================================================== */ 144 | 145 | /** 146 | * Remove the border on images inside links in IE 10. 147 | */ 148 | 149 | img { 150 | border-style: none; 151 | } 152 | 153 | /* Forms 154 | ========================================================================== */ 155 | 156 | /** 157 | * 1. Change the font styles in all browsers. 158 | * 2. Remove the margin in Firefox and Safari. 159 | */ 160 | 161 | button, 162 | input, 163 | optgroup, 164 | select, 165 | textarea { 166 | font-family: inherit; /* 1 */ 167 | font-size: 100%; /* 1 */ 168 | line-height: 1.15; /* 1 */ 169 | margin: 0; /* 2 */ 170 | } 171 | 172 | /** 173 | * Show the overflow in IE. 174 | * 1. Show the overflow in Edge. 175 | */ 176 | 177 | button, 178 | input { 179 | /* 1 */ 180 | overflow: visible; 181 | } 182 | 183 | /** 184 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 185 | * 1. Remove the inheritance of text transform in Firefox. 186 | */ 187 | 188 | button, 189 | select { 190 | /* 1 */ 191 | text-transform: none; 192 | } 193 | 194 | /** 195 | * Correct the inability to style clickable types in iOS and Safari. 196 | */ 197 | 198 | button, 199 | [type="button"] { 200 | -webkit-appearance: button; 201 | } 202 | 203 | /** 204 | * Remove the inner border and padding in Firefox. 205 | */ 206 | 207 | button::-moz-focus-inner, 208 | [type="button"]::-moz-focus-inner { 209 | border-style: none; 210 | padding: 0; 211 | } 212 | 213 | /** 214 | * Restore the focus styles unset by the previous rule. 215 | */ 216 | 217 | button:-moz-focusring, 218 | [type="button"]:-moz-focusring { 219 | outline: 1px dotted ButtonText; 220 | } 221 | 222 | /** 223 | * Correct the padding in Firefox. 224 | */ 225 | 226 | fieldset { 227 | padding: 0.35em 0.75em 0.625em; 228 | } 229 | 230 | /** 231 | * 1. Correct the text wrapping in Edge and IE. 232 | * 2. Correct the color inheritance from `fieldset` elements in IE. 233 | * 3. Remove the padding so developers are not caught out when they zero out 234 | * `fieldset` elements in all browsers. 235 | */ 236 | 237 | legend { 238 | box-sizing: border-box; /* 1 */ 239 | color: inherit; /* 2 */ 240 | display: table; /* 1 */ 241 | max-width: 100%; /* 1 */ 242 | padding: 0; /* 3 */ 243 | white-space: normal; /* 1 */ 244 | } 245 | 246 | /** 247 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 248 | */ 249 | 250 | progress { 251 | vertical-align: baseline; 252 | } 253 | 254 | /** 255 | * Remove the default vertical scrollbar in IE 10+. 256 | */ 257 | 258 | textarea { 259 | overflow: auto; 260 | } 261 | 262 | /** 263 | * 1. Add the correct box sizing in IE 10. 264 | * 2. Remove the padding in IE 10. 265 | */ 266 | 267 | /** 268 | * Correct the cursor style of increment and decrement buttons in Chrome. 269 | */ 270 | 271 | /** 272 | * 1. Correct the odd appearance in Chrome and Safari. 273 | * 2. Correct the outline style in Safari. 274 | */ 275 | 276 | /** 277 | * Remove the inner padding in Chrome and Safari on macOS. 278 | */ 279 | 280 | /** 281 | * 1. Correct the inability to style clickable types in iOS and Safari. 282 | * 2. Change font properties to `inherit` in Safari. 283 | */ 284 | 285 | /* Interactive 286 | ========================================================================== */ 287 | 288 | /* 289 | * Add the correct display in Edge, IE 10+, and Firefox. 290 | */ 291 | 292 | details { 293 | display: block; 294 | } 295 | 296 | /* 297 | * Add the correct display in all browsers. 298 | */ 299 | 300 | summary { 301 | display: list-item; 302 | } 303 | 304 | /* Misc 305 | ========================================================================== */ 306 | 307 | /** 308 | * Add the correct display in IE 10+. 309 | */ 310 | 311 | template { 312 | display: none; 313 | } 314 | 315 | /** 316 | * Add the correct display in IE 10. 317 | */ 318 | 319 | [hidden] { 320 | display: none; 321 | } 322 | 323 | /** 324 | * Manually forked from SUIT CSS Base: https://github.com/suitcss/base 325 | * A thin layer on top of normalize.css that provides a starting point more 326 | * suitable for web applications. 327 | */ 328 | 329 | /** 330 | * Removes the default spacing and border for appropriate elements. 331 | */ 332 | 333 | blockquote, 334 | dl, 335 | dd, 336 | h1, 337 | h2, 338 | h3, 339 | h4, 340 | h5, 341 | h6, 342 | hr, 343 | figure, 344 | p, 345 | pre { 346 | margin: 0; 347 | } 348 | 349 | button { 350 | background-color: transparent; 351 | background-image: none; 352 | } 353 | 354 | /** 355 | * Work around a Firefox/IE bug where the transparent `button` background 356 | * results in a loss of the default `button` focus styles. 357 | */ 358 | 359 | button:focus { 360 | outline: 1px dotted; 361 | outline: 5px auto -webkit-focus-ring-color; 362 | } 363 | 364 | fieldset { 365 | margin: 0; 366 | padding: 0; 367 | } 368 | 369 | ol, 370 | ul { 371 | list-style: none; 372 | margin: 0; 373 | padding: 0; 374 | } 375 | 376 | /** 377 | * Tailwind custom reset styles 378 | */ 379 | 380 | /** 381 | * 1. Use the user's configured `sans` font-family (with Tailwind's default 382 | * sans-serif font stack as a fallback) as a sane default. 383 | * 2. Use Tailwind's default "normal" line-height so the user isn't forced 384 | * to override it to ensure consistency even when using the default theme. 385 | */ 386 | 387 | html { 388 | font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 389 | "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", 390 | "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* 1 */ 391 | line-height: 1.5; /* 2 */ 392 | } 393 | 394 | /** 395 | * 1. Prevent padding and border from affecting element width. 396 | * 397 | * We used to set this in the html element and inherit from 398 | * the parent element for everything else. This caused issues 399 | * in shadow-dom-enhanced elements like
where the content 400 | * is wrapped by a div with box-sizing set to `content-box`. 401 | * 402 | * https://github.com/mozdevs/cssremedy/issues/4 403 | * 404 | * 405 | * 2. Allow adding a border to an element by just adding a border-width. 406 | * 407 | * By default, the way the browser specifies that an element should have no 408 | * border is by setting it's border-style to `none` in the user-agent 409 | * stylesheet. 410 | * 411 | * In order to easily add borders to elements by just setting the `border-width` 412 | * property, we change the default border-style for all elements to `solid`, and 413 | * use border-width to hide them instead. This way our `border` utilities only 414 | * need to set the `border-width` property instead of the entire `border` 415 | * shorthand, making our border utilities much more straightforward to compose. 416 | * 417 | * https://github.com/tailwindcss/tailwindcss/pull/116 418 | */ 419 | 420 | *, 421 | ::before, 422 | ::after { 423 | box-sizing: border-box; /* 1 */ 424 | border-width: 0; /* 2 */ 425 | border-style: solid; /* 2 */ 426 | border-color: #e2e8f0; /* 2 */ 427 | } 428 | 429 | /* 430 | * Ensure horizontal rules are visible by default 431 | */ 432 | 433 | hr { 434 | border-top-width: 1px; 435 | } 436 | 437 | /** 438 | * Undo the `border-style: none` reset that Normalize applies to images so that 439 | * our `border-{width}` utilities have the expected effect. 440 | * 441 | * The Normalize reset is unnecessary for us since we default the border-width 442 | * to 0 on all elements. 443 | * 444 | * https://github.com/tailwindcss/tailwindcss/issues/362 445 | */ 446 | 447 | img { 448 | border-style: solid; 449 | } 450 | 451 | textarea { 452 | resize: vertical; 453 | } 454 | 455 | input::-moz-placeholder, 456 | textarea::-moz-placeholder { 457 | color: #a0aec0; 458 | } 459 | 460 | input:-ms-input-placeholder, 461 | textarea:-ms-input-placeholder { 462 | color: #a0aec0; 463 | } 464 | 465 | input::placeholder, 466 | textarea::placeholder { 467 | color: #a0aec0; 468 | } 469 | 470 | button, 471 | [role="button"] { 472 | cursor: pointer; 473 | } 474 | 475 | table { 476 | border-collapse: collapse; 477 | } 478 | 479 | h1, 480 | h2, 481 | h3, 482 | h4, 483 | h5, 484 | h6 { 485 | font-size: inherit; 486 | font-weight: inherit; 487 | } 488 | 489 | /** 490 | * Reset links to optimize for opt-in styling instead of 491 | * opt-out. 492 | */ 493 | 494 | a { 495 | color: inherit; 496 | text-decoration: inherit; 497 | } 498 | 499 | /** 500 | * Reset form element properties that are easy to forget to 501 | * style explicitly so you don't inadvertently introduce 502 | * styles that deviate from your design system. These styles 503 | * supplement a partial reset that is already applied by 504 | * normalize.css. 505 | */ 506 | 507 | button, 508 | input, 509 | optgroup, 510 | select, 511 | textarea { 512 | padding: 0; 513 | line-height: inherit; 514 | color: inherit; 515 | } 516 | 517 | /** 518 | * Use the configured 'mono' font family for elements that 519 | * are expected to be rendered with a monospace font, falling 520 | * back to the system monospace stack if there is no configured 521 | * 'mono' font family. 522 | */ 523 | 524 | pre, 525 | code, 526 | kbd, 527 | samp { 528 | font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", 529 | monospace; 530 | } 531 | 532 | /** 533 | * Make replaced elements `display: block` by default as that's 534 | * the behavior you want almost all of the time. Inspired by 535 | * CSS Remedy, with `svg` added as well. 536 | * 537 | * https://github.com/mozdevs/cssremedy/issues/14 538 | */ 539 | 540 | img, 541 | svg, 542 | video, 543 | canvas, 544 | audio, 545 | iframe, 546 | embed, 547 | object { 548 | display: block; 549 | vertical-align: middle; 550 | } 551 | 552 | /** 553 | * Constrain images and videos to the parent width and preserve 554 | * their instrinsic aspect ratio. 555 | * 556 | * https://github.com/mozdevs/cssremedy/issues/14 557 | */ 558 | 559 | img, 560 | video { 561 | max-width: 100%; 562 | height: auto; 563 | } 564 | 565 | .sr-only { 566 | position: absolute; 567 | width: 1px; 568 | height: 1px; 569 | padding: 0; 570 | margin: -1px; 571 | overflow: hidden; 572 | clip: rect(0, 0, 0, 0); 573 | white-space: nowrap; 574 | border-width: 0; 575 | } 576 | 577 | .bg-white { 578 | --bg-opacity: 1; 579 | background-color: #fff; 580 | background-color: rgba(255, 255, 255, var(--bg-opacity)); 581 | } 582 | 583 | .bg-gray-500 { 584 | --bg-opacity: 1; 585 | background-color: #a0aec0; 586 | background-color: rgba(160, 174, 192, var(--bg-opacity)); 587 | } 588 | 589 | .bg-gray-600 { 590 | --bg-opacity: 1; 591 | background-color: #718096; 592 | background-color: rgba(113, 128, 150, var(--bg-opacity)); 593 | } 594 | 595 | .bg-teal-700 { 596 | --bg-opacity: 1; 597 | background-color: #2c7a7b; 598 | background-color: rgba(44, 122, 123, var(--bg-opacity)); 599 | } 600 | 601 | .bg-indigo-800 { 602 | --bg-opacity: 1; 603 | background-color: #2f365f; 604 | background-color: rgba(47, 54, 95, var(--bg-opacity)); 605 | } 606 | 607 | .bg-indigo-900 { 608 | --bg-opacity: 1; 609 | background-color: #191e38; 610 | background-color: rgba(25, 30, 56, var(--bg-opacity)); 611 | } 612 | 613 | .hover\:bg-gray-100:hover { 614 | --bg-opacity: 1; 615 | background-color: #f7fafc; 616 | background-color: rgba(247, 250, 252, var(--bg-opacity)); 617 | } 618 | 619 | .hover\:bg-red-100:hover { 620 | --bg-opacity: 1; 621 | background-color: #fcebea; 622 | background-color: rgba(252, 235, 234, var(--bg-opacity)); 623 | } 624 | 625 | .hover\:bg-red-800:hover { 626 | --bg-opacity: 1; 627 | background-color: #9b2c2c; 628 | background-color: rgba(155, 44, 44, var(--bg-opacity)); 629 | } 630 | 631 | .border-transparent { 632 | border-color: transparent; 633 | } 634 | 635 | .border-teal-600 { 636 | --border-opacity: 1; 637 | border-color: #319795; 638 | border-color: rgba(49, 151, 149, var(--border-opacity)); 639 | } 640 | 641 | .border-indigo-900 { 642 | --border-opacity: 1; 643 | border-color: #191e38; 644 | border-color: rgba(25, 30, 56, var(--border-opacity)); 645 | } 646 | 647 | .hover\:border-red-400:hover { 648 | --border-opacity: 1; 649 | border-color: #ef5753; 650 | border-color: rgba(239, 87, 83, var(--border-opacity)); 651 | } 652 | 653 | .rounded { 654 | border-radius: 0.25rem; 655 | } 656 | 657 | .rounded-lg { 658 | border-radius: 0.5rem; 659 | } 660 | 661 | .border-2 { 662 | border-width: 2px; 663 | } 664 | 665 | .border-r-2 { 666 | border-right-width: 2px; 667 | } 668 | 669 | .border-b { 670 | border-bottom-width: 1px; 671 | } 672 | 673 | .cursor-pointer { 674 | cursor: pointer; 675 | } 676 | 677 | .block { 678 | display: block; 679 | } 680 | 681 | .inline-block { 682 | display: inline-block; 683 | } 684 | 685 | .flex { 686 | display: flex; 687 | } 688 | 689 | .table { 690 | display: table; 691 | } 692 | 693 | .hidden { 694 | display: none; 695 | } 696 | 697 | .flex-row { 698 | flex-direction: row; 699 | } 700 | 701 | .flex-col { 702 | flex-direction: column; 703 | } 704 | 705 | .items-start { 706 | align-items: flex-start; 707 | } 708 | 709 | .items-center { 710 | align-items: center; 711 | } 712 | 713 | .justify-center { 714 | justify-content: center; 715 | } 716 | 717 | .justify-between { 718 | justify-content: space-between; 719 | } 720 | 721 | .flex-1 { 722 | flex: 1 1 0%; 723 | } 724 | 725 | .flex-auto { 726 | flex: 1 1 auto; 727 | } 728 | 729 | .flex-none { 730 | flex: none; 731 | } 732 | 733 | .font-sans { 734 | font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 735 | "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", 736 | "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 737 | } 738 | 739 | .font-normal { 740 | font-weight: 400; 741 | } 742 | 743 | .font-semibold { 744 | font-weight: 600; 745 | } 746 | 747 | .font-bold { 748 | font-weight: 700; 749 | } 750 | 751 | .font-extrabold { 752 | font-weight: 800; 753 | } 754 | 755 | .h-2 { 756 | height: 0.5rem; 757 | } 758 | 759 | .h-4 { 760 | height: 1rem; 761 | } 762 | 763 | .h-6 { 764 | height: 1.5rem; 765 | } 766 | 767 | .h-10 { 768 | height: 2.5rem; 769 | } 770 | 771 | .h-12 { 772 | height: 3rem; 773 | } 774 | 775 | .h-full { 776 | height: 100%; 777 | } 778 | 779 | .h-screen { 780 | height: 100vh; 781 | } 782 | 783 | .text-xs { 784 | font-size: 0.75rem; 785 | } 786 | 787 | .text-sm { 788 | font-size: 0.875rem; 789 | } 790 | 791 | .text-lg { 792 | font-size: 1.125rem; 793 | } 794 | 795 | .text-xl { 796 | font-size: 1.25rem; 797 | } 798 | 799 | .text-2xl { 800 | font-size: 1.5rem; 801 | } 802 | 803 | .text-3xl { 804 | font-size: 1.875rem; 805 | } 806 | 807 | .leading-tight { 808 | line-height: 1.25; 809 | } 810 | 811 | .leading-normal { 812 | line-height: 1.5; 813 | } 814 | 815 | .m-12 { 816 | margin: 3rem; 817 | } 818 | 819 | .mx-4 { 820 | margin-left: 1rem; 821 | margin-right: 1rem; 822 | } 823 | 824 | .mb-1 { 825 | margin-bottom: 0.25rem; 826 | } 827 | 828 | .ml-1 { 829 | margin-left: 0.25rem; 830 | } 831 | 832 | .mt-2 { 833 | margin-top: 0.5rem; 834 | } 835 | 836 | .mr-2 { 837 | margin-right: 0.5rem; 838 | } 839 | 840 | .mb-2 { 841 | margin-bottom: 0.5rem; 842 | } 843 | 844 | .mt-3 { 845 | margin-top: 0.75rem; 846 | } 847 | 848 | .mr-3 { 849 | margin-right: 0.75rem; 850 | } 851 | 852 | .mb-4 { 853 | margin-bottom: 1rem; 854 | } 855 | 856 | .mb-6 { 857 | margin-bottom: 1.5rem; 858 | } 859 | 860 | .opacity-25 { 861 | opacity: 0.25; 862 | } 863 | 864 | .opacity-50 { 865 | opacity: 0.5; 866 | } 867 | 868 | .opacity-75 { 869 | opacity: 0.75; 870 | } 871 | 872 | .opacity-100 { 873 | opacity: 1; 874 | } 875 | 876 | .overflow-hidden { 877 | overflow: hidden; 878 | } 879 | 880 | .overflow-y-scroll { 881 | overflow-y: scroll; 882 | } 883 | 884 | .p-2 { 885 | padding: 0.5rem; 886 | } 887 | 888 | .p-4 { 889 | padding: 1rem; 890 | } 891 | 892 | .py-1 { 893 | padding-top: 0.25rem; 894 | padding-bottom: 0.25rem; 895 | } 896 | 897 | .py-2 { 898 | padding-top: 0.5rem; 899 | padding-bottom: 0.5rem; 900 | } 901 | 902 | .py-4 { 903 | padding-top: 1rem; 904 | padding-bottom: 1rem; 905 | } 906 | 907 | .px-4 { 908 | padding-left: 1rem; 909 | padding-right: 1rem; 910 | } 911 | 912 | .px-6 { 913 | padding-left: 1.5rem; 914 | padding-right: 1.5rem; 915 | } 916 | 917 | .pt-2 { 918 | padding-top: 0.5rem; 919 | } 920 | 921 | .pr-2 { 922 | padding-right: 0.5rem; 923 | } 924 | 925 | .pl-3 { 926 | padding-left: 0.75rem; 927 | } 928 | 929 | .pl-4 { 930 | padding-left: 1rem; 931 | } 932 | 933 | .pb-6 { 934 | padding-bottom: 1.5rem; 935 | } 936 | 937 | .fill-current { 938 | fill: currentColor; 939 | } 940 | 941 | .text-black { 942 | --text-opacity: 1; 943 | color: #000; 944 | color: rgba(0, 0, 0, var(--text-opacity)); 945 | } 946 | 947 | .text-white { 948 | --text-opacity: 1; 949 | color: #fff; 950 | color: rgba(255, 255, 255, var(--text-opacity)); 951 | } 952 | 953 | .text-gray-200 { 954 | --text-opacity: 1; 955 | color: #edf2f7; 956 | color: rgba(237, 242, 247, var(--text-opacity)); 957 | } 958 | 959 | .text-gray-500 { 960 | --text-opacity: 1; 961 | color: #a0aec0; 962 | color: rgba(160, 174, 192, var(--text-opacity)); 963 | } 964 | 965 | .text-gray-600 { 966 | --text-opacity: 1; 967 | color: #718096; 968 | color: rgba(113, 128, 150, var(--text-opacity)); 969 | } 970 | 971 | .text-gray-800 { 972 | --text-opacity: 1; 973 | color: #2d3748; 974 | color: rgba(45, 55, 72, var(--text-opacity)); 975 | } 976 | 977 | .text-green-500 { 978 | --text-opacity: 1; 979 | color: #48bb78; 980 | color: rgba(72, 187, 120, var(--text-opacity)); 981 | } 982 | 983 | .text-purple-300 { 984 | --text-opacity: 1; 985 | color: #d6bbfc; 986 | color: rgba(214, 187, 252, var(--text-opacity)); 987 | } 988 | 989 | .uppercase { 990 | text-transform: uppercase; 991 | } 992 | 993 | .no-underline { 994 | text-decoration: none; 995 | } 996 | 997 | .hover\:underline:hover { 998 | text-decoration: underline; 999 | } 1000 | 1001 | .antialiased { 1002 | -webkit-font-smoothing: antialiased; 1003 | -moz-osx-font-smoothing: grayscale; 1004 | } 1005 | 1006 | .truncate { 1007 | overflow: hidden; 1008 | text-overflow: ellipsis; 1009 | white-space: nowrap; 1010 | } 1011 | 1012 | .w-2 { 1013 | width: 0.5rem; 1014 | } 1015 | 1016 | .w-4 { 1017 | width: 1rem; 1018 | } 1019 | 1020 | .w-6 { 1021 | width: 1.5rem; 1022 | } 1023 | 1024 | .w-10 { 1025 | width: 2.5rem; 1026 | } 1027 | 1028 | .w-12 { 1029 | width: 3rem; 1030 | } 1031 | 1032 | .w-full { 1033 | width: 100%; 1034 | } 1035 | 1036 | @-webkit-keyframes spin { 1037 | to { 1038 | transform: rotate(360deg); 1039 | } 1040 | } 1041 | 1042 | @keyframes spin { 1043 | to { 1044 | transform: rotate(360deg); 1045 | } 1046 | } 1047 | 1048 | @-webkit-keyframes ping { 1049 | 75%, 1050 | 100% { 1051 | transform: scale(2); 1052 | opacity: 0; 1053 | } 1054 | } 1055 | 1056 | @keyframes ping { 1057 | 75%, 1058 | 100% { 1059 | transform: scale(2); 1060 | opacity: 0; 1061 | } 1062 | } 1063 | 1064 | @-webkit-keyframes pulse { 1065 | 50% { 1066 | opacity: 0.5; 1067 | } 1068 | } 1069 | 1070 | @keyframes pulse { 1071 | 50% { 1072 | opacity: 0.5; 1073 | } 1074 | } 1075 | 1076 | @-webkit-keyframes bounce { 1077 | 0%, 1078 | 100% { 1079 | transform: translateY(-25%); 1080 | -webkit-animation-timing-function: cubic-bezier(0.8, 0, 1, 1); 1081 | animation-timing-function: cubic-bezier(0.8, 0, 1, 1); 1082 | } 1083 | 1084 | 50% { 1085 | transform: none; 1086 | -webkit-animation-timing-function: cubic-bezier(0, 0, 0.2, 1); 1087 | animation-timing-function: cubic-bezier(0, 0, 0.2, 1); 1088 | } 1089 | } 1090 | 1091 | @keyframes bounce { 1092 | 0%, 1093 | 100% { 1094 | transform: translateY(-25%); 1095 | -webkit-animation-timing-function: cubic-bezier(0.8, 0, 1, 1); 1096 | animation-timing-function: cubic-bezier(0.8, 0, 1, 1); 1097 | } 1098 | 1099 | 50% { 1100 | transform: none; 1101 | -webkit-animation-timing-function: cubic-bezier(0, 0, 0.2, 1); 1102 | animation-timing-function: cubic-bezier(0, 0, 0.2, 1); 1103 | } 1104 | } 1105 | 1106 | .hover-target .show-on-hover { 1107 | opacity: 0; 1108 | filter: alpha(opacity=0); 1109 | } 1110 | 1111 | .hover-target:hover .show-on-hover, 1112 | .hover-target .show-on-hover:focus, 1113 | .hover-target .show-on-hover:active { 1114 | opacity: 1; 1115 | filter: alpha(opacity=1); 1116 | } 1117 | 1118 | .sr-only { 1119 | -webkit-clip-path: inset(50%); 1120 | clip-path: inset(50%); 1121 | clip: rect(1px, 1px, 1px, 1px); 1122 | height: 1px; 1123 | margin: -1px; 1124 | overflow: hidden; 1125 | padding: 0; 1126 | position: absolute; 1127 | width: 1px; 1128 | } 1129 | 1130 | @media (min-width: 640px) { 1131 | .sm\:block { 1132 | display: block; 1133 | } 1134 | 1135 | .sm\:flex-row { 1136 | flex-direction: row; 1137 | } 1138 | 1139 | .sm\:w-48 { 1140 | width: 12rem; 1141 | } 1142 | } 1143 | 1144 | @media (min-width: 768px) { 1145 | .md\:w-64 { 1146 | width: 16rem; 1147 | } 1148 | } 1149 | 1150 | /*# sourceMappingURL=/app.32da848e.css.map */ 1151 | -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/assets/img/angry-cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/js-ts-monorepos/79ee63390ce5138216ea0469035ec7868ae91318/COURSE_FILES/09-internal-dependents/ui/assets/img/angry-cat.jpg -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/assets/img/avengers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/js-ts-monorepos/79ee63390ce5138216ea0469035ec7868ae91318/COURSE_FILES/09-internal-dependents/ui/assets/img/avengers.jpg -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/assets/img/boss.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/js-ts-monorepos/79ee63390ce5138216ea0469035ec7868ae91318/COURSE_FILES/09-internal-dependents/ui/assets/img/boss.jpg -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/assets/img/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/js-ts-monorepos/79ee63390ce5138216ea0469035ec7868ae91318/COURSE_FILES/09-internal-dependents/ui/assets/img/cat.jpg -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/assets/img/clippy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/js-ts-monorepos/79ee63390ce5138216ea0469035ec7868ae91318/COURSE_FILES/09-internal-dependents/ui/assets/img/clippy.png -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/assets/img/colonel-meow.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/js-ts-monorepos/79ee63390ce5138216ea0469035ec7868ae91318/COURSE_FILES/09-internal-dependents/ui/assets/img/colonel-meow.jpg -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/assets/img/desk_flip.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/js-ts-monorepos/79ee63390ce5138216ea0469035ec7868ae91318/COURSE_FILES/09-internal-dependents/ui/assets/img/desk_flip.jpg -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/assets/img/dilbert.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/js-ts-monorepos/79ee63390ce5138216ea0469035ec7868ae91318/COURSE_FILES/09-internal-dependents/ui/assets/img/dilbert.jpg -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/assets/img/drstrange.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/js-ts-monorepos/79ee63390ce5138216ea0469035ec7868ae91318/COURSE_FILES/09-internal-dependents/ui/assets/img/drstrange.jpg -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/assets/img/ironman.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/js-ts-monorepos/79ee63390ce5138216ea0469035ec7868ae91318/COURSE_FILES/09-internal-dependents/ui/assets/img/ironman.jpg -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/assets/img/jquery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/js-ts-monorepos/79ee63390ce5138216ea0469035ec7868ae91318/COURSE_FILES/09-internal-dependents/ui/assets/img/jquery.png -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/assets/img/js.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/js-ts-monorepos/79ee63390ce5138216ea0469035ec7868ae91318/COURSE_FILES/09-internal-dependents/ui/assets/img/js.png -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/assets/img/linkedin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/js-ts-monorepos/79ee63390ce5138216ea0469035ec7868ae91318/COURSE_FILES/09-internal-dependents/ui/assets/img/linkedin.png -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/assets/img/lisa.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/js-ts-monorepos/79ee63390ce5138216ea0469035ec7868ae91318/COURSE_FILES/09-internal-dependents/ui/assets/img/lisa.jpeg -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/assets/img/maru.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/js-ts-monorepos/79ee63390ce5138216ea0469035ec7868ae91318/COURSE_FILES/09-internal-dependents/ui/assets/img/maru.jpg -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/assets/img/microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/js-ts-monorepos/79ee63390ce5138216ea0469035ec7868ae91318/COURSE_FILES/09-internal-dependents/ui/assets/img/microsoft.png -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/assets/img/mike.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/js-ts-monorepos/79ee63390ce5138216ea0469035ec7868ae91318/COURSE_FILES/09-internal-dependents/ui/assets/img/mike.jpeg -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/assets/img/node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/js-ts-monorepos/79ee63390ce5138216ea0469035ec7868ae91318/COURSE_FILES/09-internal-dependents/ui/assets/img/node.png -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/assets/img/office97.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/js-ts-monorepos/79ee63390ce5138216ea0469035ec7868ae91318/COURSE_FILES/09-internal-dependents/ui/assets/img/office97.png -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/assets/img/thor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/js-ts-monorepos/79ee63390ce5138216ea0469035ec7868ae91318/COURSE_FILES/09-internal-dependents/ui/assets/img/thor.jpg -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/assets/img/ts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/js-ts-monorepos/79ee63390ce5138216ea0469035ec7868ae91318/COURSE_FILES/09-internal-dependents/ui/assets/img/ts.png -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/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 | -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@shlack/ui", 3 | "version": "0.0.1", 4 | "main": "dist/index.js", 5 | "types": "dist/index.d.ts", 6 | "publishConfig": { 7 | "access": "public" 8 | }, 9 | "scripts": { 10 | "dev": "node server/server.js", 11 | "lint": "scripty", 12 | "clean": "rimraf dist *.tsbuildinfo", 13 | "build": "parcel build index.html", 14 | "test": "scripty" 15 | }, 16 | "devDependencies": { 17 | "@babel/core": "^7.12.0", 18 | "@babel/preset-env": "^7.12.0", 19 | "@babel/preset-react": "^7.10.4", 20 | "@babel/preset-typescript": "^7.12.0", 21 | "@types/date-fns": "^2.6.0", 22 | "@types/express": "^4.17.8", 23 | "@types/json-server": "^0.14.2", 24 | "@types/node": "^14.11.8", 25 | "@types/react": "^16.9.52", 26 | "@types/react-dom": "^16.9.8", 27 | "@types/react-router": "^5.1.8", 28 | "@types/react-router-dom": "^5.1.6", 29 | "react-router": "^5.2.0", 30 | "react-router-dom": "^5.2.0", 31 | "react-test-renderer": "^16.14.0", 32 | "typescript": "^4.0.3" 33 | }, 34 | "dependencies": { 35 | "express": "^4.17.1", 36 | "json-server": "^0.16.2", 37 | "parcel-bundler": "^1.12.4", 38 | "react": "^16.14.0", 39 | "react-dom": "^16.14.0" 40 | }, 41 | "scripty": { 42 | "path": "../../scripts/packages" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/server/api-server.js: -------------------------------------------------------------------------------- 1 | const jsonServer = require("json-server"); 2 | 3 | // const router = jsonServer.router("db.json"); 4 | const middlewares = jsonServer.defaults(); 5 | 6 | /** 7 | * 8 | * @param {import('express').Request} req 9 | * @param {import('express').Response} res 10 | * @param {import('express').NextFunction} next 11 | */ 12 | function SINGULAR_MIDDLEWARE(req, res, next) { 13 | const send = res.send; 14 | res.send = function (body) { 15 | if (req.url.indexOf("singular") >= 0) { 16 | try { 17 | const json = JSON.parse(body); 18 | if (Array.isArray(json)) { 19 | if (json.length === 1) { 20 | return send.call(this, JSON.stringify(json[0])); 21 | } else if (json.length === 0) { 22 | return send(JSON.stringify({ error: "NOT FOUND" })).status(404); 23 | } 24 | } 25 | } catch (e) { 26 | throw new Error("Problem unwrapping array"); 27 | } 28 | } 29 | return send.call(this, body); 30 | }; 31 | next(); 32 | } 33 | 34 | /** 35 | * 36 | * @param {import('express').Request} req 37 | * @param {import('express').Response} _res 38 | * @param {import('express').NextFunction} next 39 | */ 40 | function addDateToPost(req, _res, next) { 41 | if (req.method === "POST") { 42 | req.body.createdAt = Date.now(); 43 | } 44 | // Continue to JSON Server router 45 | next(); 46 | } 47 | 48 | /** 49 | * 50 | * @param {import('express').Router} router 51 | * @param {import('express').Router} server 52 | * 53 | * @returns {import('express').Router} 54 | */ 55 | function setupAPI(router, server) { 56 | server.use("/api", SINGULAR_MIDDLEWARE, ...middlewares); 57 | server.use(jsonServer.bodyParser); 58 | 59 | server.use(addDateToPost); 60 | server.use( 61 | jsonServer.rewriter({ 62 | "/api/teams": "/api/teams?_embed=channels", 63 | "/api/teams/:id": "/api/teams/:id?_embed=channels", 64 | "/api/teams/:id/channels": "/api/channels?teamId=:id", 65 | "/api/teams/:id/channels/:channelId": 66 | "/api/channels?id=:channelId&teamId=:id&singular=1", 67 | "/api/teams/:id/channels/:channelId/messages": 68 | "/api/messages?_expand=user&teamId=:id&channelId=:channelId", 69 | }) 70 | ); 71 | 72 | server.use("/api", router); 73 | return server; 74 | } 75 | 76 | module.exports = { setupAPI }; 77 | -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/server/server.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const Bundler = require("parcel-bundler"); 3 | const e = require("express"); 4 | const jsonServer = require("json-server"); 5 | const server = jsonServer.create(); 6 | const { setupAPI } = require("./api-server"); 7 | const { join } = require("path"); 8 | 9 | /** 10 | * Initialize an API server for shlack 11 | * 12 | * @public 13 | * @param {e.Application} a express application 14 | * @param {string} dbFilePath Path to json-server database file 15 | */ 16 | function initializeApiServer(a, dbFilePath) { 17 | const server = jsonServer.create(); 18 | const jsonApiServer = jsonServer.router(dbFilePath); 19 | setupAPI(jsonApiServer, server); 20 | a.use(server); 21 | } 22 | 23 | /** 24 | * Initialize a UI middleware for shlack 25 | * 26 | * @beta 27 | * @param {e.Application} a express application 28 | * @param {string} uiPkgPath path to the UI package 29 | */ 30 | function initializeUiMiddleware(a, uiPkgPath) { 31 | const file = join(uiPkgPath, "index.html"); // Pass an absolute path to the entrypoint here 32 | const options = {}; // See options section of api docs, for the possibilities 33 | // Initialize a new bundler using a file and options 34 | const bundler = new Bundler(file, options); 35 | a.use("/assets", e.static(join(uiPkgPath, "assets"))); 36 | a.use(bundler.middleware()); 37 | } 38 | 39 | const app = e(); 40 | 41 | const PORT = process.env.PORT || 1234; 42 | const DB_FILE_PATH = 43 | process.env.DB_FILE_PATH || join(__dirname, "..", "db.json"); 44 | const UI_PKG_PATH = join(__dirname, ".."); 45 | 46 | initializeApiServer(app, DB_FILE_PATH); 47 | initializeUiMiddleware(app, UI_PKG_PATH); 48 | 49 | // Listen on port 1234 50 | app.listen(PORT, () => { 51 | console.log(`Listening on http://localhost:${PORT}`); 52 | }); 53 | -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/src/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { 3 | BrowserRouter as Router, 4 | match, 5 | Route, 6 | Switch, 7 | } from "react-router-dom"; 8 | import { getAllTeams } from "@shlack/data"; 9 | import { ITeam } from "@shlack/types"; 10 | import { useAsyncDataEffect } from "@shlack/utils"; 11 | import Loading from "./components/Loading"; 12 | import SelectedTeam from "./components/SelectedTeam"; 13 | import TeamSelector from "./components/TeamSelector"; 14 | 15 | const { useState } = React; 16 | 17 | const App: React.FunctionComponent = () => { 18 | const [teams, setTeams] = useState(); 19 | 20 | useAsyncDataEffect(() => getAllTeams(), { 21 | setter: setTeams, 22 | stateName: "teams", 23 | }); 24 | if (!teams) return ; 25 | return ( 26 | 27 |
28 | 29 | 30 | 31 |
32 |

Please select a team

33 |
34 |
35 | 36 |
37 |

Please select a team

38 |
39 |
40 | }) => ( 43 | 44 | )} 45 | /> 46 |
47 |
48 |
49 | ); 50 | }; 51 | export default App; 52 | -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/src/components/Channel.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { getChannelMessages } from "@shlack/data"; 3 | import { IChannel, IMessage } from "@shlack/types"; 4 | import { useAsyncDataEffect } from "@shlack/utils"; 5 | import ChannelFooter from "./Channel/Footer"; 6 | import ChannelHeader from "./Channel/Header"; 7 | import ChannelMessage from "./Channel/Message"; 8 | import Loading from "./Loading"; 9 | 10 | const Channel: React.FunctionComponent<{ channel: IChannel }> = ({ 11 | channel, 12 | }) => { 13 | const [messages, setMessages] = React.useState(); 14 | useAsyncDataEffect(() => getChannelMessages(channel.teamId, channel.id), { 15 | setter: setMessages, 16 | stateName: "messages", 17 | otherStatesToMonitor: [channel], 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 | 28 |
32 | {messages.map((m: IMessage) => ( 33 | 39 | ))} 40 |
41 | 42 | 43 |
44 | ); 45 | }; 46 | export default Channel; 47 | -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/src/components/Channel/Footer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { IChannel } from "@shlack/types"; 3 | 4 | const Footer: React.FunctionComponent<{ channel: IChannel }> = ({ 5 | channel: { name: channelName }, 6 | }) => ( 7 |
8 |
12 |

13 | Message Input 14 |

15 | 16 | 29 | 30 | 33 | 34 | 40 | 41 | 47 |
48 |
49 | ); 50 | 51 | export default Footer; 52 | -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/src/components/Channel/Header.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const Header: React.FunctionComponent<{ 4 | title: string; 5 | description: string; 6 | }> = ({ title, description }) => ( 7 |
8 |
9 |

10 | 11 | {title} 12 |

13 |

14 | {description} 15 |

16 |
17 |
18 | ); 19 | 20 | export default Header; 21 | -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/src/components/Channel/Message.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { IUser } from "@shlack/types"; 3 | import { formatTimestamp } from "@shlack/utils"; 4 | 5 | const Message: React.FunctionComponent<{ 6 | user: IUser; 7 | date: Date; 8 | body: string; 9 | }> = ({ user, date, body }) => ( 10 |
14 |
15 | {user.name} 20 |
21 | 22 |
23 |
24 | 28 | {user.name} 29 | 30 | at 31 | 34 |
35 | 36 |

{body}

37 |
38 | 39 | 45 |
46 | ); 47 | 48 | export default Message; 49 | -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/src/components/Loading.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const Loading: React.FunctionComponent<{ message: string }> = ({ 4 | message = "Loading...", 5 | children, 6 | }) => ( 7 |

8 | {message}... 9 | {children} 10 |

11 | ); 12 | export default Loading; 13 | -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/src/components/SelectedChannel.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { match } from "react-router"; 3 | import { IChannel } from "@shlack/types"; 4 | import Channel from "./Channel"; 5 | 6 | const SelectedChannel: React.FunctionComponent<{ 7 | match: match<{ channelId: string }>; 8 | channels: IChannel[]; 9 | }> = ({ match, channels }) => { 10 | if (!channels) throw new Error("no channels"); 11 | if (!match) throw new Error("no match"); 12 | 13 | const { params } = match; 14 | if (!match) return

No match params

; 15 | const { channelId: selectedChannelId } = params; 16 | if (!selectedChannelId) return

Invalid channelId

; 17 | const selectedChannel = channels.find( 18 | (c: IChannel) => c.id === selectedChannelId 19 | ); 20 | if (!selectedChannel) 21 | return ( 22 |
23 |

Could not find channel with id {selectedChannelId}

24 |
{JSON.stringify(channels, null, "  ")}
25 |
26 | ); 27 | 28 | return ; 29 | }; 30 | 31 | export default SelectedChannel; 32 | -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/src/components/SelectedTeam.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { match } from "react-router"; 3 | import type { ITeam } from "@shlack/types"; 4 | import Team from "./Team"; 5 | 6 | const SelectedTeam: React.FunctionComponent<{ 7 | match: match<{ teamId: string }>; 8 | teams: ITeam[]; 9 | }> = ({ match, teams }) => { 10 | if (!match) throw new Error("no match"); 11 | 12 | const { params } = match; 13 | if (!params) throw new Error("no match params"); 14 | 15 | const { teamId: selectedTeamId } = params; 16 | if (!selectedTeamId) throw new Error(`undefined teamId`); 17 | 18 | const selectedTeam = teams.find((t: ITeam) => t.id === selectedTeamId); 19 | if (!selectedTeam) 20 | throw new Error(`Invalid could not find team with id {selectedTeamId}`); 21 | 22 | return ; 23 | }; 24 | 25 | export default SelectedTeam; 26 | -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/src/components/Team.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { match, Route, Switch } from "react-router-dom"; 3 | import type { ITeam } from "@shlack/types"; 4 | import SelectedChannel from "./SelectedChannel"; 5 | import TeamSidebar from "./TeamSidebar"; 6 | 7 | const Team: React.FunctionComponent<{ team: ITeam }> = ({ team }) => { 8 | console.log( 9 | `%c TEAM render: ${team.name}`, 10 | "background-color: blue; color: white" 11 | ); 12 | const { channels } = team; 13 | return ( 14 |
15 | 16 | 17 | 18 |

Please select a channel

19 |
20 | }) => ( 24 | 25 | )} 26 | /> 27 |
28 |
29 | ); 30 | }; 31 | export default Team; 32 | -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/src/components/TeamSelector.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { ITeam } from "@shlack/types"; 3 | import TeamLink from "./TeamSelector/TeamLink"; 4 | 5 | const TeamSelector: React.FunctionComponent<{ teams: ITeam[] }> = ({ 6 | teams, 7 | }) => ( 8 | 27 | ); 28 | 29 | export default TeamSelector; 30 | -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/src/components/TeamSelector/TeamLink.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Link, useRouteMatch } from "react-router-dom"; 3 | import { ITeam } from "@shlack/types"; 4 | 5 | const TeamLink: React.FunctionComponent<{ team: ITeam }> = ({ team }) => { 6 | const match = useRouteMatch({ 7 | path: `/team/${team.id}`, 8 | exact: false, 9 | }); 10 | 11 | return ( 12 | 19 |
20 | {`Join 25 |
26 | 27 | ); 28 | }; 29 | 30 | export default TeamLink; 31 | -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/src/components/TeamSidebar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { IChannel, ITeam } from "@shlack/types"; 3 | import ChannelLink from "./TeamSidebar/ChannelLink"; 4 | 5 | /** 6 | * Team Sidebar 7 | * 8 | * @public 9 | * @param param0 - props 10 | */ 11 | const TeamSidebar: React.FunctionComponent<{ team: ITeam }> = ({ team }) => { 12 | return ( 13 |
14 |
15 |
16 |

17 | {team.name} 18 |

19 | 20 |
21 | 28 | 29 | Chris User 30 | 31 |
32 |
33 | 34 | 46 |
47 | 48 | 75 |
76 | 79 |
80 |
81 | ); 82 | }; 83 | 84 | export default TeamSidebar; 85 | -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/src/components/TeamSidebar/ChannelLink.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Link, useRouteMatch } from "react-router-dom"; 3 | import { IChannel } from "@shlack/types"; 4 | 5 | const ChannelLink: React.FunctionComponent<{ 6 | to: string; 7 | channel: IChannel; 8 | }> = ({ to, channel }) => { 9 | const match = useRouteMatch(to); 10 | return ( 11 | 18 | 19 | {channel.name} 20 | 21 | ); 22 | }; 23 | 24 | export default ChannelLink; 25 | -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/src/index.ts: -------------------------------------------------------------------------------- 1 | import "regenerator-runtime/runtime"; 2 | import * as React from "react"; 3 | import { render } from "react-dom"; 4 | import App from "./App"; 5 | 6 | function initializeReactApp() { 7 | const appContainer = document.getElementById("appContainer"); 8 | if (!appContainer) throw new Error("No #appContainer found in DOM"); 9 | render(React.createElement(App), appContainer); 10 | } 11 | 12 | initializeReactApp(); 13 | 14 | export { default as Channel } from "./components/Channel"; 15 | -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/tests/components/Channel.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as renderer from "react-test-renderer"; 3 | import Channel from "../../src/components/Channel"; 4 | 5 | test("Link changes the class when hovered", () => { 6 | const component = renderer.create( 7 | 15 | ); 16 | const tree = component.toJSON(); 17 | expect(tree).toMatchSnapshot(); 18 | }); 19 | -------------------------------------------------------------------------------- /COURSE_FILES/09-internal-dependents/ui/tests/components/ChannelFooter.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as renderer from "react-test-renderer"; 3 | import Footer from "../../src/components/Channel/Footer"; 4 | 5 | test("Link changes the class when hovered", () => { 6 | const component = renderer.create( 7 |