├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── LICENSE-CODE ├── README.md ├── assets ├── css-syntax.png ├── fabric.jpg ├── flux.png ├── scripts.js ├── shared.css ├── step.css ├── todo-components.png └── todo_screenshot.jpg ├── azure-pipelines.pr.yml ├── azure-pipelines.yml ├── bonus-jest ├── demo │ ├── README.md │ ├── index.html │ └── src │ │ ├── TestMe.spec.tsx │ │ ├── TestMe.tsx │ │ ├── index.spec.tsx │ │ ├── index.ts │ │ └── multiply.ts └── exercise │ ├── README.md │ ├── index.html │ └── src │ ├── TestMe.spec.tsx │ ├── TestMe.tsx │ ├── index.ts │ ├── stack.spec.ts │ └── stack.ts ├── bonus-servicecalls └── demo │ ├── README.md │ ├── index.html │ └── src │ ├── actions │ └── index.ts │ ├── components │ ├── TodoApp.tsx │ ├── TodoFooter.tsx │ ├── TodoHeader.tsx │ ├── TodoList.tsx │ └── TodoListItem.tsx │ ├── index.tsx │ ├── reducers │ └── index.ts │ ├── service │ └── index.ts │ └── store │ └── index.ts ├── index.html ├── jest.config.js ├── jest.setup.js ├── markdownReadme └── src │ └── index.ts ├── package-lock.json ├── package.json ├── playground └── index.html ├── prettier.config.js ├── server ├── index.js └── now.json ├── step1-01 ├── demo │ ├── index.html │ └── style.css ├── exercise │ ├── README.md │ ├── answers.html │ ├── baked_beans.jpg │ └── index.html └── lesson │ ├── README.md │ ├── index.html │ └── src │ └── index.tsx ├── step1-02 ├── demo │ └── index.html ├── exercise │ ├── README.md │ ├── answers.css │ └── index.html └── lesson │ ├── README.md │ ├── index.html │ └── src │ └── index.tsx ├── step1-03 ├── demo │ └── index.html ├── exercise │ ├── README.md │ ├── answer.js │ └── index.html └── lesson │ ├── README.md │ ├── index.html │ └── src │ └── index.tsx ├── step1-04 ├── demo │ └── index.html ├── final │ ├── README.md │ ├── index.html │ └── src │ │ ├── App.tsx │ │ ├── components │ │ ├── Button.css │ │ ├── Button.tsx │ │ └── Counter.tsx │ │ └── index.tsx └── lesson │ ├── README.md │ ├── index.html │ └── src │ └── index.tsx ├── step1-05 ├── TodoApp.html ├── demo │ ├── README.md │ ├── index.html │ └── src │ │ ├── App.tsx │ │ ├── index.tsx │ │ └── style.css └── exercise │ ├── README.md │ ├── index.html │ └── src │ ├── App.tsx │ ├── components │ ├── TodoHeader.tsx │ └── TodoListItem.tsx │ ├── index.tsx │ └── style.css ├── step1-06 ├── demo │ ├── README.md │ ├── index.html │ └── src │ │ ├── TodoApp.tsx │ │ ├── components │ │ ├── TodoFooter.tsx │ │ ├── TodoHeader.tsx │ │ ├── TodoList.tsx │ │ └── TodoListItem.tsx │ │ ├── index.tsx │ │ └── style.css ├── exercise │ ├── README.md │ ├── index.html │ └── src │ │ ├── TodoApp.tsx │ │ ├── components │ │ ├── TodoFooter.tsx │ │ ├── TodoHeader.tsx │ │ ├── TodoList.tsx │ │ └── TodoListItem.tsx │ │ ├── index.tsx │ │ └── style.css └── index.html ├── step1-07 ├── demo │ ├── README.md │ ├── index.html │ └── src │ │ ├── TodoApp.tsx │ │ ├── TodoApp.types.ts │ │ ├── components │ │ ├── TodoFooter.tsx │ │ ├── TodoHeader.tsx │ │ ├── TodoList.tsx │ │ └── TodoListItem.tsx │ │ ├── index.tsx │ │ └── style.css ├── exercise │ ├── README.md │ ├── index.html │ └── src │ │ ├── TodoApp.tsx │ │ ├── TodoApp.types.ts │ │ ├── components │ │ ├── TodoFooter.tsx │ │ ├── TodoHeader.tsx │ │ ├── TodoList.tsx │ │ └── TodoListItem.tsx │ │ ├── index.tsx │ │ └── style.css └── final │ ├── index.html │ └── src │ ├── TodoApp.tsx │ ├── TodoApp.types.ts │ ├── components │ ├── TodoFooter.tsx │ ├── TodoHeader.tsx │ ├── TodoList.tsx │ └── TodoListItem.tsx │ ├── index.tsx │ └── style.css ├── step2-01 ├── demo │ ├── README.md │ ├── index.html │ └── src │ │ ├── async │ │ └── index.ts │ │ ├── generics │ │ └── index.ts │ │ ├── index.tsx │ │ ├── interfaces │ │ └── index.ts │ │ ├── modules │ │ ├── default.ts │ │ ├── index.ts │ │ └── named.ts │ │ ├── spread │ │ └── index.ts │ │ └── types │ │ └── index.ts ├── exercise │ ├── README.md │ ├── index.html │ └── src │ │ ├── fibonacci.ts │ │ ├── index.ts │ │ └── stack.ts └── final │ ├── README.md │ ├── index.html │ └── src │ ├── fibonacci.ts │ ├── index.ts │ └── stack.ts ├── step2-02 ├── demo │ ├── README.md │ ├── index.html │ └── src │ │ ├── components │ │ ├── TodoApp.tsx │ │ ├── TodoFooter.tsx │ │ ├── TodoHeader.tsx │ │ ├── TodoList.tsx │ │ └── TodoListItem.tsx │ │ ├── index.tsx │ │ └── store │ │ └── index.ts └── exercise │ ├── README.md │ ├── index.html │ └── src │ ├── components │ ├── TodoApp.tsx │ ├── TodoFooter.tsx │ ├── TodoHeader.tsx │ ├── TodoList.tsx │ └── TodoListItem.tsx │ ├── index.tsx │ └── store │ └── index.ts ├── step2-03 ├── demo │ ├── README.md │ ├── index.html │ └── src │ │ ├── components │ │ ├── TodoApp.tsx │ │ ├── TodoFooter.tsx │ │ ├── TodoHeader.tsx │ │ ├── TodoList.tsx │ │ └── TodoListItem.tsx │ │ ├── index.tsx │ │ └── store │ │ └── index.ts └── exercise │ ├── README.md │ ├── index.html │ └── src │ ├── components │ ├── TodoApp.tsx │ ├── TodoFooter.tsx │ ├── TodoHeader.tsx │ ├── TodoList.tsx │ └── TodoListItem.tsx │ ├── index.tsx │ └── store │ └── index.ts ├── step2-04 ├── demo │ ├── README.md │ ├── index.html │ └── src │ │ ├── TodoContext.ts │ │ ├── components │ │ ├── TodoApp.tsx │ │ ├── TodoFooter.tsx │ │ ├── TodoHeader.tsx │ │ ├── TodoList.tsx │ │ └── TodoListItem.tsx │ │ ├── index.tsx │ │ └── store │ │ └── index.ts └── exercise │ ├── README.md │ ├── index.html │ └── src │ ├── TodoContext.ts │ ├── components │ ├── TodoApp.tsx │ ├── TodoFooter.tsx │ ├── TodoHeader.tsx │ ├── TodoList.tsx │ └── TodoListItem.tsx │ ├── index.tsx │ └── store │ └── index.ts ├── step2-05 ├── demo │ ├── README.md │ ├── index.html │ └── src │ │ ├── actions │ │ └── index.ts │ │ ├── index.tsx │ │ ├── reducers │ │ └── index.ts │ │ └── store │ │ └── index.ts └── exercise │ ├── README.md │ ├── index.html │ └── src │ ├── actions │ └── index.ts │ ├── index.tsx │ ├── reducers │ └── index.ts │ └── store │ └── index.ts ├── step2-06 ├── demo │ ├── README.md │ ├── index.html │ └── src │ │ ├── actions │ │ └── index.ts │ │ ├── components │ │ ├── TodoApp.tsx │ │ ├── TodoFooter.tsx │ │ ├── TodoHeader.tsx │ │ ├── TodoList.tsx │ │ └── TodoListItem.tsx │ │ ├── index.tsx │ │ ├── reducers │ │ └── index.ts │ │ └── store │ │ └── index.ts └── exercise │ ├── README.md │ ├── index.html │ └── src │ ├── actions │ └── index.ts │ ├── components │ ├── TodoApp.tsx │ ├── TodoFooter.tsx │ ├── TodoHeader.tsx │ ├── TodoList.tsx │ └── TodoListItem.tsx │ ├── index.tsx │ ├── reducers │ └── index.ts │ └── store │ └── index.ts ├── tsconfig.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | lib 4 | *.log 5 | .DS_Store 6 | tmp.json 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 6 | "esbenp.prettier-vscode" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.printWidth": 140, 3 | "prettier.tabWidth": 2, 4 | "prettier.singleQuote": true, 5 | "editor.tabSize": 2, 6 | "editor.formatOnSave": true 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE-CODE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-present Microsoft Corporation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /assets/css-syntax.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/frontend-bootcamp/7cea32428e16b33a2ae4e54d2a87274a8171559b/assets/css-syntax.png -------------------------------------------------------------------------------- /assets/fabric.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/frontend-bootcamp/7cea32428e16b33a2ae4e54d2a87274a8171559b/assets/fabric.jpg -------------------------------------------------------------------------------- /assets/flux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/frontend-bootcamp/7cea32428e16b33a2ae4e54d2a87274a8171559b/assets/flux.png -------------------------------------------------------------------------------- /assets/scripts.js: -------------------------------------------------------------------------------- 1 | // prettier-ignore 2 | var appInsights = window.appInsights || function (a) { 3 | function b(a) { c[a] = function () { var b = arguments; c.queue.push(function () { c[a].apply(c, b) }) } } var c = { config: a }, d = document, e = window; setTimeout(function () { var b = d.createElement("script"); b.src = a.url || "https://az416426.vo.msecnd.net/scripts/a/ai.0.js", d.getElementsByTagName("script")[0].parentNode.appendChild(b) }); try { c.cookie = d.cookie } catch (a) { } c.queue = []; for (var f = ["Event", "Exception", "Metric", "PageView", "Trace", "Dependency"]; f.length;)b("track" + f.pop()); if (b("setAuthenticatedUserContext"), b("clearAuthenticatedUserContext"), b("startTrackEvent"), b("stopTrackEvent"), b("startTrackPage"), b("stopTrackPage"), b("flush"), !a.disableExceptionTracking) { f = "onerror", b("_" + f); var g = e[f]; e[f] = function (a, b, d, e, h) { var i = g && g(a, b, d, e, h); return !0 !== i && c["_" + f](a, b, d, e, h), i } } return c 4 | }({ 5 | instrumentationKey: "6ad37ae0-c4ab-4739-925c-1e2773c31f17" 6 | }); 7 | 8 | // prettier-ignore 9 | if (window.location.hostname !== 'localhost') { 10 | window.appInsights = appInsights, appInsights.queue && 0 === appInsights.queue.length && appInsights.trackPageView(null, null, { urlReferrer: document.referrer }); 11 | } 12 | -------------------------------------------------------------------------------- /assets/shared.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | } 4 | *, 5 | *:before, 6 | *:after { 7 | box-sizing: inherit; 8 | } 9 | 10 | body { 11 | background-color: #f3f2f1; 12 | } 13 | 14 | a { 15 | color: #0078d4; 16 | text-decoration: none; 17 | } 18 | 19 | a:hover { 20 | text-decoration: underline; 21 | } 22 | 23 | h1 { 24 | margin: 0; 25 | } 26 | 27 | h1 a { 28 | font-size: 16px; 29 | font-weight: 400; 30 | } 31 | 32 | .Container { 33 | justify-content: center; 34 | padding: 20px 0; 35 | max-width: 1040px; 36 | margin: 0 auto; 37 | } 38 | 39 | .Tiles { 40 | grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); 41 | grid-gap: 20px; 42 | display: grid; 43 | list-style-type: none; 44 | margin: 0; 45 | padding: 0; 46 | counter-reset: steps; 47 | } 48 | 49 | .Tile { 50 | background-color: white; 51 | border-radius: 2px; 52 | box-shadow: 0 3.2px 7.2px 0 rgba(0, 0, 0, 0.132), 0 0.6px 1.8px 0 rgba(0, 0, 0, 0.108); 53 | opacity: 0.96; 54 | transition: all 0.15s linear; 55 | position: relative; 56 | overflow: hidden; 57 | padding: 12px; 58 | } 59 | 60 | .Tile:not(.Tile--intro):hover { 61 | box-shadow: 0 6.4px 14.4px 0 rgba(0, 0, 0, 0.132), 0 1.2px 3.6px 0 rgba(0, 0, 0, 0.108); 62 | opacity: 1; 63 | } 64 | 65 | .Tile.Tile--numbered::after { 66 | counter-increment: steps; 67 | content: counter(steps); 68 | position: absolute; 69 | left: -20px; 70 | bottom: -3px; 71 | font-size: 220px; 72 | line-height: 188px; 73 | color: #eaeaea; 74 | z-index: 1; 75 | } 76 | 77 | .Tile-link { 78 | align-items: center; 79 | text-align: center; 80 | color: #323130; 81 | display: flex; 82 | flex-direction: column; 83 | height: 148px; 84 | justify-content: center; 85 | text-decoration: none; 86 | position: relative; 87 | font-size: 24px; 88 | font-weight: 200; 89 | z-index: 2; 90 | } 91 | 92 | a.Tile-link { 93 | color: #0078d7; 94 | } 95 | 96 | a.Tile-link:hover { 97 | text-decoration: underline; 98 | } 99 | 100 | .Tile-link i { 101 | font-size: 32px; 102 | margin-bottom: 12px; 103 | color: #605e5c; 104 | } 105 | 106 | .Tile-links { 107 | font-size: 16px; 108 | color: #605e5c; 109 | } 110 | 111 | .Tile-links a { 112 | text-decoration: none; 113 | color: #0078d4; 114 | } 115 | 116 | .Tile-links a:hover { 117 | text-decoration: underline; 118 | } 119 | 120 | .Tile--intro { 121 | grid-column: span 2; 122 | padding: 20px; 123 | } 124 | 125 | .Tile--intro h1 { 126 | font-size: 24px; 127 | font-weight: 300; 128 | margin: 8px 0; 129 | padding: 0; 130 | } 131 | 132 | .Tile--intro p { 133 | font-size: 14px; 134 | margin: 0; 135 | } 136 | 137 | .Tile--intro a, 138 | .Tile--intro a:visited { 139 | color: #0078d4; 140 | } 141 | -------------------------------------------------------------------------------- /assets/todo-components.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/frontend-bootcamp/7cea32428e16b33a2ae4e54d2a87274a8171559b/assets/todo-components.png -------------------------------------------------------------------------------- /assets/todo_screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/frontend-bootcamp/7cea32428e16b33a2ae4e54d2a87274a8171559b/assets/todo_screenshot.jpg -------------------------------------------------------------------------------- /azure-pipelines.pr.yml: -------------------------------------------------------------------------------- 1 | # Node.js 2 | # Build a general Node.js project with npm. 3 | # Add steps that analyze code, save build artifacts, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/javascript 5 | 6 | trigger: none 7 | 8 | pool: 9 | vmImage: 'Ubuntu-16.04' 10 | 11 | steps: 12 | - task: NodeTool@0 13 | inputs: 14 | versionSpec: '10.x' 15 | displayName: 'Install Node.js' 16 | 17 | - script: | 18 | npm install 19 | npm run build 20 | displayName: 'npm install, build' 21 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # Node.js 2 | # Build a general Node.js project with npm. 3 | # Add steps that analyze code, save build artifacts, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/javascript 5 | 6 | pr: none 7 | 8 | trigger: 9 | - master 10 | 11 | pool: 12 | vmImage: 'Ubuntu-16.04' 13 | 14 | steps: 15 | - task: NodeTool@0 16 | inputs: 17 | versionSpec: '10.x' 18 | displayName: 'Install Node.js' 19 | 20 | - script: | 21 | git config user.email "kchau@microsoft.com" 22 | git config user.name "Ken Chau" 23 | git remote set-url origin https://kenotron:$(git.pat)@github.com/Microsoft/frontend-bootcamp.git 24 | git checkout master 25 | git pull 26 | npm install 27 | git checkout -b build_$(Build.BuildId) 28 | npm run build 29 | git add . 30 | git commit -m "adding docs" 31 | git subtree split --prefix docs -b temp_$(Build.BuildId) 32 | git push origin temp_$(Build.BuildId):refs/heads/gh-pages --force 33 | displayName: 'npm install, build and push docs to gh-pages' 34 | -------------------------------------------------------------------------------- /bonus-jest/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 |
9 | For this step, we look at unit testing. Run 10 |
npm test
11 | in the command line. 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /bonus-jest/demo/src/TestMe.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount } from 'enzyme'; 3 | import { TestMe } from './TestMe'; 4 | 5 | describe('TestMe Component', () => { 6 | it('should have a non-clickable component when the original InnerMe is clicked', () => { 7 | const wrapper = mount(); 8 | wrapper.find('#innerMe').simulate('click'); 9 | expect(wrapper.find('#innerMe').text()).toBe('Clicked'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /bonus-jest/demo/src/TestMe.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export interface TestMeProps { 4 | name: string; 5 | } 6 | 7 | export interface TestMeState { 8 | clicked: boolean; 9 | } 10 | 11 | export const TestMe = (props: TestMeProps) => { 12 | return ( 13 |
14 | 15 |
16 | ); 17 | }; 18 | 19 | export class InnerMe extends React.Component { 20 | state = { 21 | clicked: false 22 | }; 23 | 24 | onClick = () => { 25 | this.setState({ clicked: true }); 26 | }; 27 | 28 | render() { 29 | return !this.state.clicked ? ( 30 |
31 | Hello {this.props.name}, Click Me 32 |
33 | ) : ( 34 |
Clicked
35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /bonus-jest/demo/src/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount } from 'enzyme'; 3 | 4 | describe('index', () => { 5 | it('placeholder', () => { 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /bonus-jest/demo/src/index.ts: -------------------------------------------------------------------------------- 1 | import { multiply } from './multiply'; 2 | 3 | let counter = 0; 4 | 5 | export function getCount() { 6 | return counter; 7 | } 8 | 9 | export function increment() { 10 | return ++counter; 11 | } 12 | 13 | export function decrement() { 14 | return --counter; 15 | } 16 | 17 | export function square(x: number) { 18 | return multiply(x, x); 19 | } 20 | -------------------------------------------------------------------------------- /bonus-jest/demo/src/multiply.ts: -------------------------------------------------------------------------------- 1 | export function multiply(x: number, y: number) { 2 | return x * y; 3 | } 4 | -------------------------------------------------------------------------------- /bonus-jest/exercise/README.md: -------------------------------------------------------------------------------- 1 | # Bonus: Testing TypeScript code with Jest (Exercise) 2 | 3 | [Lessons](../../) | [Demo](../demo/) 4 | 5 | Start the test runner by running `npm test` in the root of the `frontend-bootcamp` folder. 6 | 7 | ## Basic testing 8 | 9 | 1. Look at `exercise/src/stack.ts` for a sample implementation of a stack 10 | 11 | 2. Follow the instructions inside `stack.spec.ts` file to complete the two tests 12 | 13 | ## Enzyme Testing 14 | 15 | 1. Open up `exercise/src/TestMe.spec.tsx` 16 | 17 | 2. Fill in the test using Enzyme concepts introduced in the demo 18 | -------------------------------------------------------------------------------- /bonus-jest/exercise/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 |
9 | For this step, we look at unit testing. Run 10 |
npm test
11 | in the command line. 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /bonus-jest/exercise/src/TestMe.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount } from 'enzyme'; 3 | import { TestMe } from './TestMe'; 4 | 5 | describe('TestMe Component', () => { 6 | it('should render correctly when hovered', () => { 7 | // TODO: 8 | // 1. mount a Component here 9 | // 2. use enzyme wrapper's find() method to retrieve the #innerMe element 10 | // 3. simulate a hover with "mouseover" event via the simulate() API 11 | // 4. make assertions with expect on the text() of the #innerMe element 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /bonus-jest/exercise/src/TestMe.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export interface TestMeProps { 4 | name: string; 5 | } 6 | 7 | export interface TestMeState { 8 | enabled: boolean; 9 | } 10 | 11 | export const TestMe = (props: TestMeProps) => { 12 | return ( 13 |
14 | 15 |
16 | ); 17 | }; 18 | 19 | export class InnerMe extends React.Component { 20 | state = { 21 | enabled: false 22 | }; 23 | 24 | onMouseOver = () => { 25 | this.setState({ enabled: true }); 26 | }; 27 | 28 | render() { 29 | return !this.state.enabled ? ( 30 |
31 | Hello {this.props.name}, Hover Over Me 32 |
33 | ) : ( 34 |
Enabled
35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /bonus-jest/exercise/src/index.ts: -------------------------------------------------------------------------------- 1 | export { Stack } from './stack'; 2 | export { TestMe } from './TestMe'; 3 | -------------------------------------------------------------------------------- /bonus-jest/exercise/src/stack.spec.ts: -------------------------------------------------------------------------------- 1 | // TODO: Import the stack here 2 | 3 | describe('Stack', () => { 4 | it('should push item to the top of the stack', () => { 5 | // TODO: implement test here: 6 | // 1. Instantiate a new Stack - i.e. const stack = new Stack(); 7 | // 2. Use stack push calls to add some items to the stack 8 | // 3. Write assertions via the expect() API 9 | }); 10 | 11 | it('should pop the item from the top of stack', () => { 12 | // TODO: implement test here: 13 | // 1. Instantiate a new Stack - i.e. const stack = new Stack(); 14 | // 2. Use stack push calls to add some items to the stack 15 | // 3. pop a few items off the stack 16 | // 4. write assertions via the expect() API 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /bonus-jest/exercise/src/stack.ts: -------------------------------------------------------------------------------- 1 | export class Stack { 2 | private _items: T[] = []; 3 | 4 | /** Add an item to the top of the stack. */ 5 | push(item: T) { 6 | this._items.push(item); 7 | } 8 | 9 | /** Remove the top item from the stack and return it. */ 10 | pop(): T { 11 | if (this._items.length > 0) { 12 | return this._items.pop(); 13 | } 14 | } 15 | 16 | /** Return the top item from the stack without removing it. */ 17 | peek(): T { 18 | if (this._items.length > 0) { 19 | return this._items[this._items.length - 1]; 20 | } 21 | } 22 | 23 | /** Get the number of items in the stack/ */ 24 | get count(): number { 25 | return this._items.length; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /bonus-servicecalls/demo/README.md: -------------------------------------------------------------------------------- 1 | # Bonus: Service calls (Demo) 2 | 3 | [Lessons](../../) 4 | 5 | > Note: this step doesn't work with the live site on github.io. Clone the repo to try this step out. 6 | 7 | ## `redux-thunk`: side effects inside action creators 8 | 9 | The [Redux Thunk](https://github.com/reduxjs/redux-thunk) middleware allows writing actions that make service calls. 10 | 11 | Remember those simple little action functions? They're called action creators. These little functions can be charged with superpowers to allow asynchronous side effects to happen while creating the messages. Asynchronous side effects include service calls against APIs. 12 | 13 | Action creators are a natural place to put service calls. Redux Thunk middleware passes `dispatch()` and `getState()` from the store into the action creators. This allows the action creator itself to dispatch different actions in between async side effects. Combined with the async / await syntax, coding service calls is a cinch! 14 | 15 | Most of the time, in a single-page app, we apply **optimistic UI updates**. We can update the UI before the network call completes so the UI feels more responsive. 16 | 17 | ## Action creator with a thunk 18 | 19 | [What's a thunk?](https://daveceddia.com/what-is-a-thunk/) - it is a wrapper function that returns a function. What does it do? Let's find out! 20 | 21 | This action creator just returns an object: 22 | 23 | ```ts 24 | function addTodo(label: string) { 25 | return { type: 'addTodo', id: uuid(), label }; 26 | } 27 | ``` 28 | 29 | In order for us to make service calls, we need to supercharge this with the power of `redux-thunk` 30 | 31 | ```ts 32 | function addTodo(label: string) { 33 | return async (dispatch: any, getState: () => Store) => { 34 | const addAction = actions.addTodo(label); 35 | const id = addAction.id; 36 | dispatch(addAction); 37 | await service.add(id, getState().todos[id]); 38 | }; 39 | } 40 | ``` 41 | 42 | Let's make some observations: 43 | 44 | 1. The outer function has the same function signature as the previous one 45 | 2. It returns a function that has `dispatch` and `getState` as parameters 46 | 3. The inner function is `async` enabled, and can await on "side effects" like asynchronous service calls 47 | 4. This inner function has the ability to dispatch additional actions because it has been passed the `dispatch()` function from the store 48 | 5. This inner function also has access to the state tree via `getState()` 49 | -------------------------------------------------------------------------------- /bonus-servicecalls/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /bonus-servicecalls/demo/src/actions/index.ts: -------------------------------------------------------------------------------- 1 | import uuid from 'uuid/v4'; 2 | import { Store } from '../store'; 3 | import * as service from '../service'; 4 | 5 | export const actions = { 6 | addTodo: (label: string) => ({ type: 'addTodo', id: uuid(), label }), 7 | remove: (id: string) => ({ type: 'remove', id }), 8 | complete: (id: string) => ({ type: 'complete', id }), 9 | clear: () => ({ type: 'clear' }), 10 | setFilter: (filter: string) => ({ type: 'setFilter', filter }), 11 | edit: (id: string, label: string) => ({ type: 'edit', id, label }) 12 | }; 13 | 14 | export const actionsWithService = { 15 | addTodo: (label: string) => { 16 | return async (dispatch: any, getState: () => Store) => { 17 | const addAction = actions.addTodo(label); 18 | const id = addAction.id; 19 | dispatch(addAction); 20 | await service.add(id, getState().todos[id]); 21 | }; 22 | }, 23 | 24 | remove: (id: string) => { 25 | return async (dispatch: any, getState: () => Store) => { 26 | dispatch(actions.remove(id)); 27 | await service.remove(id); 28 | }; 29 | }, 30 | 31 | complete: (id: string) => { 32 | return async (dispatch: any, getState: () => Store) => { 33 | dispatch(actions.complete(id)); 34 | await service.update(id, getState().todos[id]); 35 | }; 36 | }, 37 | 38 | clear: () => { 39 | return async (dispatch: any, getState: () => Store) => { 40 | dispatch(actions.clear()); 41 | await service.updateAll(getState().todos); 42 | }; 43 | }, 44 | 45 | edit: (id: string, label: string) => { 46 | return async (dispatch: any, getState: () => Store) => { 47 | dispatch(actions.edit(id, label)); 48 | await service.update(id, getState().todos[id]); 49 | }; 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /bonus-servicecalls/demo/src/components/TodoApp.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Stack } from 'office-ui-fabric-react'; 3 | import { TodoFooter } from './TodoFooter'; 4 | import { TodoHeader } from './TodoHeader'; 5 | import { TodoList } from './TodoList'; 6 | 7 | export const TodoApp = () => { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /bonus-servicecalls/demo/src/components/TodoFooter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DefaultButton, Stack, Text } from 'office-ui-fabric-react'; 3 | import { actionsWithService } from '../actions'; 4 | import { connect } from 'react-redux'; 5 | import { Store } from '../store'; 6 | 7 | interface TodoFooterProps { 8 | todos: Store['todos']; 9 | clear: () => void; 10 | } 11 | 12 | const TodoFooter = (props: TodoFooterProps) => { 13 | const { todos, clear } = props; 14 | 15 | const itemCount = Object.keys(todos).filter(id => !todos[id].completed).length; 16 | 17 | return ( 18 | 19 | 20 | {itemCount} item{itemCount === 1 ? '' : 's'} left 21 | 22 | clear()}>Clear Completed 23 | 24 | ); 25 | }; 26 | 27 | const ConnectedTodoFooter = connect( 28 | (state: Store) => ({ 29 | todos: state.todos 30 | }), 31 | (dispatch: any) => ({ 32 | clear: () => dispatch(actionsWithService.clear()) 33 | }) 34 | )(TodoFooter); 35 | 36 | export { ConnectedTodoFooter as TodoFooter }; 37 | -------------------------------------------------------------------------------- /bonus-servicecalls/demo/src/components/TodoHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Stack, Text, Pivot, PivotItem, TextField, PrimaryButton } from 'office-ui-fabric-react'; 3 | import { FilterTypes } from '../store'; 4 | import { actions, actionsWithService } from '../actions'; 5 | import { connect } from 'react-redux'; 6 | 7 | interface TodoHeaderProps { 8 | addTodo: (label: string) => void; 9 | setFilter: (filter: FilterTypes) => void; 10 | } 11 | 12 | interface TodoHeaderState { 13 | labelInput: string; 14 | } 15 | 16 | class TodoHeader extends React.Component { 17 | constructor(props: TodoHeaderProps) { 18 | super(props); 19 | this.state = { labelInput: undefined }; 20 | } 21 | 22 | render() { 23 | return ( 24 | 25 | 26 | todos 27 | 28 | 29 | 30 | 31 | ({ 36 | ...(props.focused && { 37 | field: { 38 | backgroundColor: '#c7e0f4' 39 | } 40 | }) 41 | })} 42 | /> 43 | 44 | Add 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | ); 54 | } 55 | 56 | private onAdd = () => { 57 | this.props.addTodo(this.state.labelInput); 58 | this.setState({ labelInput: undefined }); 59 | }; 60 | 61 | private onChange = (evt: React.FormEvent, newValue: string) => { 62 | this.setState({ labelInput: newValue }); 63 | }; 64 | 65 | private onFilter = (item: PivotItem) => { 66 | this.props.setFilter(item.props.headerText as FilterTypes); 67 | }; 68 | } 69 | 70 | const ConnectedTodoHeader = connect( 71 | state => ({}), 72 | (dispatch: any) => ({ 73 | addTodo: label => dispatch(actionsWithService.addTodo(label)), 74 | setFilter: filter => dispatch(actions.setFilter(filter)) 75 | }) 76 | )(TodoHeader); 77 | 78 | export { ConnectedTodoHeader as TodoHeader }; 79 | -------------------------------------------------------------------------------- /bonus-servicecalls/demo/src/components/TodoList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Stack } from 'office-ui-fabric-react'; 3 | import { TodoListItem } from './TodoListItem'; 4 | import { connect } from 'react-redux'; 5 | import { Store } from '../store'; 6 | 7 | interface TodoListProps { 8 | todos: Store['todos']; 9 | filter: Store['filter']; 10 | } 11 | 12 | const TodoList = (props: TodoListProps) => { 13 | const { filter, todos } = props; 14 | const filteredTodos = Object.keys(todos).filter(id => { 15 | return filter === 'all' || (filter === 'completed' && todos[id].completed) || (filter === 'active' && !todos[id].completed); 16 | }); 17 | 18 | return ( 19 | 20 | {filteredTodos.map(id => ( 21 | 22 | ))} 23 | 24 | ); 25 | }; 26 | 27 | const ConnectedTodoList = connect((state: Store) => ({ ...state }))(TodoList); 28 | export { ConnectedTodoList as TodoList }; 29 | -------------------------------------------------------------------------------- /bonus-servicecalls/demo/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { reducer } from './reducers'; 4 | import { createStore, applyMiddleware } from 'redux'; 5 | import { TodoApp } from './components/TodoApp'; 6 | import { Provider } from 'react-redux'; 7 | import { initializeIcons } from '@uifabric/icons'; 8 | import { composeWithDevTools } from 'redux-devtools-extension'; 9 | import thunk from 'redux-thunk'; 10 | import * as service from './service'; 11 | import { Store, FilterTypes } from './store'; 12 | 13 | (async () => { 14 | const preloadStore = { 15 | todos: (await service.getAll()) as Store['todos'], 16 | filter: 'all' as FilterTypes 17 | }; 18 | 19 | const store = createStore(reducer, preloadStore, composeWithDevTools(applyMiddleware(thunk))); 20 | 21 | initializeIcons(); 22 | 23 | ReactDOM.render( 24 | 25 | 26 | , 27 | document.getElementById('app') 28 | ); 29 | })(); 30 | -------------------------------------------------------------------------------- /bonus-servicecalls/demo/src/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { Store } from '../store'; 2 | import { combineReducers } from 'redux'; 3 | import { createReducer } from 'redux-starter-kit'; 4 | 5 | export const todosReducer = createReducer( 6 | {}, 7 | { 8 | addTodo(state, action) { 9 | state[action.id] = { label: action.label, completed: false }; 10 | }, 11 | 12 | remove(state, action) { 13 | delete state[action.id]; 14 | }, 15 | 16 | clear(state, action) { 17 | Object.keys(state).forEach(key => { 18 | if (state[key].completed) { 19 | delete state[key]; 20 | } 21 | }); 22 | }, 23 | 24 | complete(state, action) { 25 | state[action.id].completed = !state[action.id].completed; 26 | }, 27 | 28 | edit(state, action) { 29 | state[action.id].label = action.label; 30 | } 31 | } 32 | ); 33 | 34 | export const filterReducer = createReducer('all', { 35 | setFilter(state, action) { 36 | return action.filter; 37 | } 38 | }); 39 | 40 | export const reducer = combineReducers({ 41 | todos: todosReducer, 42 | filter: filterReducer 43 | }); 44 | -------------------------------------------------------------------------------- /bonus-servicecalls/demo/src/service/index.ts: -------------------------------------------------------------------------------- 1 | import { TodoItem, Store } from '../store'; 2 | const HOST = 'http://localhost:3000'; 3 | 4 | export async function add(id: string, todo: TodoItem) { 5 | const response = await fetch(`${HOST}/todos/${id}`, { 6 | method: 'post', 7 | headers: { 'content-type': 'application/json' }, 8 | body: JSON.stringify(todo) 9 | }); 10 | 11 | return await response.json(); 12 | } 13 | 14 | export async function update(id: string, todo: TodoItem) { 15 | const response = await fetch(`${HOST}/todos/${id}`, { 16 | method: 'put', 17 | headers: { 'content-type': 'application/json' }, 18 | body: JSON.stringify(todo) 19 | }); 20 | 21 | return await response.json(); 22 | } 23 | 24 | export async function remove(id: string) { 25 | const response = await fetch(`${HOST}/todos/${id}`, { 26 | method: 'delete' 27 | }); 28 | 29 | return await response.json(); 30 | } 31 | 32 | export async function getAll() { 33 | const response = await fetch(`${HOST}/todos`, { 34 | method: 'get' 35 | }); 36 | 37 | return await response.json(); 38 | } 39 | 40 | export async function updateAll(todos: Store['todos']) { 41 | const response = await fetch(`${HOST}/todos`, { 42 | method: 'post', 43 | headers: { 'content-type': 'application/json' }, 44 | body: JSON.stringify(todos) 45 | }); 46 | 47 | return await response.json(); 48 | } 49 | -------------------------------------------------------------------------------- /bonus-servicecalls/demo/src/store/index.ts: -------------------------------------------------------------------------------- 1 | export type FilterTypes = 'all' | 'active' | 'completed'; 2 | 3 | export interface TodoItem { 4 | label: string; 5 | completed: boolean; 6 | } 7 | 8 | export interface Store { 9 | todos: { 10 | [id: string]: TodoItem; 11 | }; 12 | 13 | filter: FilterTypes; 14 | } 15 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | setupFiles: ['./jest.setup.js'], 4 | testEnvironment: 'jsdom', 5 | testPathIgnorePatterns: ['/node_modules/', '/docs/'] 6 | }; 7 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | // setup file 2 | var enzyme = require('enzyme'); 3 | var Adapter = require('enzyme-adapter-react-16'); 4 | 5 | enzyme.configure({ adapter: new Adapter() }); 6 | -------------------------------------------------------------------------------- /markdownReadme/src/index.ts: -------------------------------------------------------------------------------- 1 | import marked, { Renderer } from 'marked'; 2 | import hljs from 'highlight.js/lib/highlight'; 3 | import javascript from 'highlight.js/lib/languages/javascript'; 4 | import typescript from 'highlight.js/lib/languages/typescript'; 5 | import html from 'highlight.js/lib/languages/xml'; 6 | import css from 'highlight.js/lib/languages/css'; 7 | 8 | hljs.registerLanguage('javascript', javascript); 9 | hljs.registerLanguage('typescript', typescript); 10 | hljs.registerLanguage('html', html); 11 | hljs.registerLanguage('css', css); 12 | 13 | async function run() { 14 | const div = document.getElementById('markdownReadme'); 15 | 16 | // Create your custom renderer. 17 | const renderer = new Renderer(); 18 | renderer.code = (code, language) => { 19 | // Check whether the given language is valid for highlight.js. 20 | const validLang = !!(language && hljs.getLanguage(language)); 21 | // Highlight only if the language is valid. 22 | const highlighted = validLang ? hljs.highlight(language, code).value : code; 23 | // Render the highlighted code with `hljs` class. 24 | return `
${highlighted}
`; 25 | }; 26 | marked.setOptions({ renderer }); 27 | 28 | if (div) { 29 | const response = await fetch(div.dataset['src'] || '../README.md'); 30 | const markdownText = await response.text(); 31 | div.innerHTML = marked(markdownText); 32 | restoreScroll(div); 33 | 34 | div.addEventListener('scroll', evt => { 35 | saveScroll(div); 36 | }); 37 | 38 | window.addEventListener('resize', evt => { 39 | saveScroll(div); 40 | }); 41 | } 42 | } 43 | 44 | const scrollKey = `${window.location.pathname}_scrolltop`; 45 | 46 | function saveScroll(div: HTMLElement) { 47 | window.localStorage.setItem(scrollKey, String(div.scrollTop)); 48 | } 49 | 50 | function restoreScroll(div: HTMLElement) { 51 | const scrollTop = window.localStorage.getItem(scrollKey); 52 | if (scrollTop) { 53 | div.scrollTop = parseInt(scrollTop); 54 | } 55 | } 56 | 57 | run(); 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bootcamp", 3 | "version": "1.0.0", 4 | "description": "", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/Microsoft/frontend-bootcamp" 8 | }, 9 | "main": "index.js", 10 | "scripts": { 11 | "start:client": "webpack-dev-server --mode development --progress --open", 12 | "test": "jest --watch", 13 | "build": "rimraf docs && webpack --progress --mode production", 14 | "start:server": "nodemon -w server server/index.js", 15 | "start": "run-p start:server start:client", 16 | "deploy-ghpages": "git push origin :gh-pages && git subtree push --prefix docs origin gh-pages" 17 | }, 18 | "keywords": [], 19 | "author": "", 20 | "license": "ISC", 21 | "devDependencies": { 22 | "@types/body-parser": "^1.17.0", 23 | "@types/cors": "^2.8.4", 24 | "@types/enzyme": "^3.9.0", 25 | "@types/express": "^4.16.1", 26 | "@types/jest": "^23.3.13", 27 | "@types/node": "~10.12.21", 28 | "@types/react": "^16.7.20", 29 | "@types/react-dom": "^16.0.11", 30 | "@types/react-redux": "^7.0.0", 31 | "@types/redux": "^3.6.0", 32 | "@types/uuid": "^3.4.4", 33 | "body-parser": "^1.18.3", 34 | "copy-webpack-plugin": "^4.6.0", 35 | "cors": "^2.8.5", 36 | "css-loader": "^2.1.0", 37 | "fork-ts-checker-async-overlay-webpack-plugin": "^0.1.0", 38 | "fork-ts-checker-webpack-plugin": "^0.5.2", 39 | "html-webpack-plugin": "^4.0.0-beta.5", 40 | "jest": "^24.1.0", 41 | "nodemon": "^1.18.9", 42 | "npm-run-all": "^4.1.5", 43 | "rimraf": "^2.6.3", 44 | "style-loader": "^0.23.1", 45 | "ts-jest": "^24.0.0", 46 | "ts-loader": "^5.3.3", 47 | "tslint": "^5.13.0", 48 | "typescript": "^3.3.3", 49 | "uuid": "^3.3.2", 50 | "webpack": "^4.28.4", 51 | "webpack-cli": "^3.2.1", 52 | "webpack-dev-server": "^3.1.14" 53 | }, 54 | "dependencies": { 55 | "@uifabric/fluent-theme": "^0.14.1", 56 | "@uifabric/theme-samples": "^0.1.4", 57 | "enzyme": "^3.9.0", 58 | "enzyme-adapter-react-16": "^1.9.1", 59 | "express": "^4.16.4", 60 | "highlight.js": "^9.14.2", 61 | "immer": "^1.12.1", 62 | "marked": "^0.6.1", 63 | "office-ui-fabric-react": "^6.144.0", 64 | "react": "^16.8.3", 65 | "react-dom": "^16.8.3", 66 | "react-redux": "^6.0.0", 67 | "redux": "^4.0.1", 68 | "redux-devtools-extension": "^2.13.8", 69 | "redux-starter-kit": "^0.4.3", 70 | "redux-thunk": "^2.3.0" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | tabWidth: 2, 4 | printWidth: 140 5 | }; 6 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const fs = require('fs'); 3 | const express = require('express'); 4 | const bodyParser = require('body-parser'); 5 | const cors = require('cors'); 6 | const app = express(); 7 | 8 | const store = { 9 | /** @type {any} */ 10 | read() { 11 | if (fs.existsSync('tmp.json')) { 12 | store.todos = JSON.parse(fs.readFileSync('tmp.json').toString()); 13 | } else { 14 | store.todos = {}; 15 | } 16 | 17 | return store.todos; 18 | }, 19 | 20 | save() { 21 | fs.writeFileSync('tmp.json', JSON.stringify(store.todos)); 22 | }, 23 | 24 | todos: {} 25 | }; 26 | 27 | app.use(bodyParser.json()); 28 | app.use(cors()); 29 | 30 | app.get('/todos', (req, res) => { 31 | res.json(store.read()); 32 | }); 33 | 34 | app.put('/todos/:id', (req, res) => { 35 | store.todos[req.params.id] = req.body; 36 | store.save(); 37 | res.json('ok'); 38 | }); 39 | 40 | app.post('/todos/:id', (req, res) => { 41 | store.todos[req.params.id] = req.body; 42 | store.save(); 43 | res.json('ok'); 44 | }); 45 | 46 | app.delete('/todos/:id', (req, res) => { 47 | delete store.todos[req.params.id]; 48 | store.save(); 49 | res.json('ok'); 50 | }); 51 | 52 | app.post('/todos', (req, res) => { 53 | store.todos = req.body; 54 | store.save(); 55 | res.json('ok'); 56 | }); 57 | 58 | app.get('/hello', (req, res) => { 59 | res.send('world'); 60 | }); 61 | 62 | app.listen(process.env.NODE_ENV === 'production' ? undefined : 3000, () => { 63 | console.log('Listening at http://localhost:3000'); 64 | }); 65 | -------------------------------------------------------------------------------- /server/now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "name": "todo-server", 4 | "builds": [{ "src": "*.js", "use": "@now/node-server" }], 5 | "env": { 6 | "NODE_ENV": "production" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /step1-01/demo/style.css: -------------------------------------------------------------------------------- 1 | aside { 2 | float: right; 3 | width: 33%; 4 | padding: 10px; 5 | background: #eee; 6 | } 7 | 8 | form > div { 9 | margin-bottom: 20px; 10 | } 11 | 12 | h2 a { 13 | color: #0078d4; 14 | text-decoration: none; 15 | } 16 | 17 | h2 a:hover { 18 | text-decoration: underline; 19 | } 20 | 21 | body { 22 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 23 | } 24 | -------------------------------------------------------------------------------- /step1-01/exercise/README.md: -------------------------------------------------------------------------------- 1 | # Step 1.1 - Introduction to HTML (Exercise) 2 | 3 | See index.html from [npm start](http://localhost:8080/step1-01/exercise/) or the [live site](https://microsoft.github.io/frontend-bootcamp/step1-01/exercise/) for the exercise. -------------------------------------------------------------------------------- /step1-01/exercise/answers.html: -------------------------------------------------------------------------------- 1 |

The Recipe 4th of July Baked Beans

2 |

It's great how a single meal can take you back dozens of years. This is one of those recipes that never seems to fail to impress.

3 |

I learned this recipe from the cousin of one of my college friends back in Nashville Tennessee. We had an amazing 4th of July feast which included this recipe and some bratwurst like these Wisconsin Beer Brats

4 | 5 |
6 |
Prep Time:
7 |
10 minutes
8 |
Cook time:
9 |
3+ hours
10 |
Servings:
11 |
12
12 |
13 | 14 |

Ingredients:

15 |
    16 |
  • 1LB Bacon chopped
  • 17 |
  • 3 Cans Bush's Original Baked Beans
  • 18 |
  • 1 Walla Wall Onion chopped
  • 19 |
  • 3 ground garlic cloves
  • 20 |
  • 4 Tablespoons of mustard
  • 21 |
  • 3 Tablespoons of molasses
  • 22 |
  • 4 Tablespoons of brown sugar
  • 23 |
24 | 25 |

Directions:

26 |
    27 |
  1. Cook bacon until it is mostly cooked, then drain most of the grease and put aside
  2. 28 |
  3. Cook onion in remaining bacon grease
  4. 29 |
  5. Combine onions and bacon, then add garlic, cook for a few more minutes
  6. 30 |
  7. Add beans and get up to simmer temperature
  8. 31 |
  9. Add mustard until your beans are nice and yellow
  10. 32 |
  11. Add molasses until color darkens again
  12. 33 |
  13. Add brown sugar until properly sweet
  14. 34 |
  15. Simmer for a long time, occasionally stirring
  16. 35 |
36 | 37 |

Expert Tips:

38 |

Burning off most of the liquid gives you nice, hearty, sticky beans. If the beans get too dry, you can always add beer!

39 | 40 |

Nutritional Information:

41 |
42 |
Calories:
43 |
lots
44 |
Fat:
45 |
lots
46 |
Fun:
47 |
lots
48 |
49 | -------------------------------------------------------------------------------- /step1-01/exercise/baked_beans.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/frontend-bootcamp/7cea32428e16b33a2ae4e54d2a87274a8171559b/step1-01/exercise/baked_beans.jpg -------------------------------------------------------------------------------- /step1-01/exercise/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
12 |
13 | /*
14 | Step 1 Exercise
15 | 
16 | The power of HTML is its ability to represent complex information in a way that conveys meaning. In this exercise you are going to be creating an HTML page for my favorite recipe.
17 | 
18 | ## The Exercise
19 | 
20 | 1. Create a recipe page to host our recipe
21 | 2. Use header, main, footer, headings (h1/h2 etc), paragraphs, lists
22 | 3. Use ordered and unordered lists appropriately
23 | 4. Add the `baked_beans.jpg` image: https://raw.githubusercontent.com/Microsoft/frontend-bootcamp/master/step1-01/exercise/baked_beans.jpg
24 | 5. Add an anchor tag around 'Wisconsin Beer Brats'
25 | 
26 | > Note that CodePen takes care of the `HTML` and `Body` tags, so you can simply start with the content
27 | 
28 | ## The Recipe
29 | 
30 | Title:
31 | 4th of July Baked Beans
32 | 
33 | Description:
34 | It's great how a single meal can take you back dozens of years. This is one of those recipes that never seems to fail to impress.
35 | 
36 | I learned this recipe from the cousin of one of my college friends back in Nashville Tennessee. We had an amazing 4th of July feast which included this recipe and some bratwurst like these Wisconsin Beer Brats https://www.culinaryhill.com/wisconsin-beer-brats/
37 | 
38 | Prep Time: 10 minutes
39 | Cook time: 3+ hours
40 | Servings: 12
41 | 
42 | Ingredients:
43 | 1LB Bacon chopped
44 | 3 Cans Bush's Original Baked Beans
45 | 1 Walla Wall Onion chopped
46 | 3 ground garlic cloves
47 | 4 Tablespoons of mustard
48 | 3 Tablespoons of molasses
49 | 4 Tablespoons of brown sugar
50 | 
51 | Directions:
52 | Cook bacon until it is mostly cooked, then drain most of the grease and put aside
53 | Cook onion in remaining bacon grease
54 | Combine onions and bacon, then add garlic, cook for a few more minutes
55 | Add beans and get up to simmer temperature
56 | Add mustard until your beans are nice and yellow
57 | Add molasses until color darkens again
58 | Add brown sugar until properly sweet
59 | Simmer for a long time, occasionally stirring
60 | 
61 | Expert Tips:
62 | Burning off most of the liquid gives you nice, hearty, sticky beans.
63 | If the beans get too dry, you can always add beer!
64 | 
65 | Nutritional Information:
66 | Calories: lots
67 | Fat: lots
68 | Fun: lots
69 | 
70 | */
71 | 
72 |       
73 |
Add Recipe Here
74 |
75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /step1-01/lesson/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /step1-01/lesson/src/index.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/frontend-bootcamp/7cea32428e16b33a2ae4e54d2a87274a8171559b/step1-01/lesson/src/index.tsx -------------------------------------------------------------------------------- /step1-02/exercise/README.md: -------------------------------------------------------------------------------- 1 | # Step 1.2 - Introduction to CSS (Exercise) 2 | 3 | See index.html from [npm start](http://localhost:8080/step1-02/exercise/) or the [live site](https://microsoft.github.io/frontend-bootcamp/step1-02/exercise/) for the exercise. -------------------------------------------------------------------------------- /step1-02/exercise/answers.css: -------------------------------------------------------------------------------- 1 | /* 1. Text Color: Red */ 2 | h2 { 3 | color: red; 4 | } 5 | 6 | /* 2. Color Green (hint: Sibling Selector) */ 7 | h2 + div { 8 | color: green; 9 | } 10 | 11 | /* 3. Border Green*/ 12 | .myList li { 13 | border: 1px solid green; 14 | } 15 | 16 | /* 4. Background Green */ 17 | .myClass { 18 | background: green; 19 | } 20 | 21 | /* 5. Background Green & Color White (Hint Qualified Selector) */ 22 | .myClass.otherClass { 23 | color: white; 24 | } 25 | 26 | /* 6. Background Yellow */ 27 | #myId { 28 | background: yellow; 29 | } 30 | 31 | /* Bonus: Border Pink*/ 32 | section > div:last-child { 33 | border: 1px solid pink; 34 | } 35 | -------------------------------------------------------------------------------- /step1-02/exercise/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
12 |
13 | /* 1. */
14 | 
15 | /* 2. */
16 | 
17 | /* 3. */
18 | 
19 | /* 4. */
20 | 
21 | /* 5. */
22 | 
23 | /* 6. */
24 | 
25 | /* Bonus */
26 |       
27 |
28 | <!-- Without changing the HTML markup, apply the styles asked for in the markup. Do not apply styles that a tag doesn't ask for. -->
29 | 
30 | <section>
31 |   <h2>1. Text Color: Red</h2>
32 |   <div>2. Color Green (hint: Sibling Selector)</div>
33 |   <main>
34 |     <ul class="myList">
35 |       <li>
36 |         3. Border Green
37 |       </li>
38 |     </ul>
39 |     <div class="myClass">4. Background Green</div>
40 |     <div class="myClass otherClass">
41 |       5. Background Green & Color White
42 |       (Hint Qualified Selector)
43 |     </div>
44 |     <div id="myId" class="otherClass">6. Background Yellow</div>
45 |   </main>
46 |   <ul>
47 |     <li>
48 |       Don't Style Me
49 |     </li>
50 |   </ul>
51 |   <div>Bonus: Border Pink</div>
52 | </section>
53 |       
54 |
55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /step1-02/lesson/README.md: -------------------------------------------------------------------------------- 1 | # Step 1.2 - Introduction to CSS (Demo) 2 | 3 | [Demo](../demo/) | [Exercise](../exercise/) 4 | 5 | ## CSS properties 6 | 7 | Now that we've gone over adding HTML tags to the page, let's cover adding styles to those tags. We can do quite a lot with styles! We can change: 8 | 9 | - Typography 10 | - Colors 11 | - Appearance (corners, borders, decorations) 12 | - Layout 13 | - Position 14 | - Display format: inline vs block 15 | - Animations 16 | - and [many more](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference) 17 | 18 | CSS styles are always written in `property: value` pairs (like `background: blue;`) and terminated with a semicolon. 19 | 20 | ## Applying CSS to an HTML file 21 | 22 | CSS can be applied to HTML tags in three different ways. 23 | 24 | 1. Inline using an HTML tag's `style` attribute 25 | - `
Hello
` 26 | 2. Via a `