's HTML into the #app element before we send the response.
18 | // 3. That's a little better, but we're still just sending a lonely tag
19 | // down to the client and then fetching the data once we mount. We can do
20 | // better. Move the data-fetching out of 's componentDidMount and into
21 | // the request handler on the server (hint: inject the contacts into
22 | // via a prop instead).
23 | // 4. There's a warning in your browser console! The HTML we're sending from
24 | // the server doesn't match what React expected on the initial render client-side.
25 | // To fix this, send the data along with the response in the HTML and pick it
26 | // up when we render the on the client.
27 | //
28 | // Note: As you go through the steps, try using the "view source" feature of
29 | // your web browser to see the actual HTML you're rendering on the server.
30 | ////////////////////////////////////////////////////////////////////////////////
31 | import React from "react";
32 | import ReactDOM from "react-dom";
33 | import App from "./exercise/App";
34 |
35 | // TODO: Pass contacts data into the via a prop.
36 | ReactDOM.render( , document.getElementById("app"));
37 |
--------------------------------------------------------------------------------
/subjects/13-Server-Rendering/exercise/App.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import fetchContacts from "./fetchContacts";
3 |
4 | class App extends React.Component {
5 | // TODO: Move this state to a prop. That will make it
6 | // possible to render on the server.
7 | state = {
8 | contacts: []
9 | };
10 |
11 | componentDidMount() {
12 | // TODO: Move this call into the request handler on the server.
13 | fetchContacts((error, contacts) => {
14 | this.setState({ contacts });
15 | });
16 | }
17 |
18 | render() {
19 | const { contacts } = this.state;
20 |
21 | return (
22 |
23 |
¡Universal App!
24 | {contacts ? (
25 |
26 | {contacts.map(contact => (
27 |
28 | {contact.first} {contact.last}
29 |
30 | ))}
31 |
32 | ) : (
33 |
Loading...
34 | )}
35 |
36 | );
37 | }
38 | }
39 |
40 | export default App;
41 |
--------------------------------------------------------------------------------
/subjects/13-Server-Rendering/exercise/fetchContacts.js:
--------------------------------------------------------------------------------
1 | import "isomorphic-fetch";
2 |
3 | const fetchContacts = cb =>
4 | fetch("https://addressbook-api.herokuapp.com/contacts")
5 | .then(res => res.json())
6 | .then(data => {
7 | cb(null, data.contacts);
8 | }, cb);
9 |
10 | export default fetchContacts;
11 |
--------------------------------------------------------------------------------
/subjects/13-Server-Rendering/exercise/server.js:
--------------------------------------------------------------------------------
1 | import http from "http";
2 | import React from "react";
3 | import ReactDOMServer from "react-dom/server";
4 |
5 | import fetchContacts from "./fetchContacts";
6 | import App from "./App";
7 |
8 | const webpackServer = "http://localhost:8080";
9 | const port = 8090;
10 |
11 | const createPage = () => `
12 |
13 |
14 |
15 |
16 | My Universal App
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | `;
28 |
29 | const app = http.createServer((req, res) => {
30 | // TODO: We'd like to render the on the server
31 | // instead of just sending a practically empty page.
32 | const html = createPage();
33 |
34 | res.writeHead(200, {
35 | "Content-Type": "text/html",
36 | "Content-Length": html.length
37 | });
38 |
39 | res.end(html);
40 | });
41 |
42 | app.listen(port, () => {
43 | console.log("\nOpen http://localhost:%s", port);
44 | });
45 |
--------------------------------------------------------------------------------
/subjects/13-Server-Rendering/solution.js:
--------------------------------------------------------------------------------
1 | ////////////////////////////////////////////////////////////////////////////////
2 | // Solution
3 | //
4 | // First, fire up the server:
5 | //
6 | // 1. Run `npm run ssr-solution` from the root of this repository
7 | // 2. Open http://localhost:8090 (not 8080)
8 | //
9 | // Now let's write some code:
10 | //
11 | // 1. Right now we're rendering the entire application client-side. Check
12 | // out the source of . Our server is essentially sending an empty
13 | // tag down to the client. Use the "view source" feature in your
14 | // web browser to see the HTML the server is sending.
15 | // 2. We'd like to render *something* on the server. Use one of react-dom/server's
16 | // render methods inside the server's request handler (see `server.js`) to
17 | // inject 's HTML into the #app element before we send the response.
18 | // 3. That's a little better, but we're still just sending a lonely tag
19 | // down to the client and then fetching the data once we mount. We can do
20 | // better. Move the data-fetching out of 's componentDidMount and into
21 | // the request handler on the server (hint: inject the contacts into
22 | // via a prop instead).
23 | // 4. There's a warning in your browser console! The HTML we're sending from
24 | // the server doesn't match what React expected on the initial render client-side.
25 | // To fix this, send the data along with the response in the HTML and pick it
26 | // up when we render the on the client.
27 | //
28 | // Note: As you go through the steps, try using the "view source" feature of
29 | // your web browser to see the actual HTML you're rendering on the server.
30 | ////////////////////////////////////////////////////////////////////////////////
31 | import React from "react";
32 | import ReactDOM from "react-dom";
33 | import App from "./solution/App";
34 |
35 | const contacts = window.__DATA__.contacts;
36 |
37 | ReactDOM.render(
38 | ,
39 | document.getElementById("app")
40 | );
41 |
--------------------------------------------------------------------------------
/subjects/13-Server-Rendering/solution/App.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | class App extends React.Component {
5 | static propTypes = {
6 | contacts: PropTypes.array
7 | };
8 |
9 | render() {
10 | const { contacts } = this.props;
11 |
12 | return (
13 |
14 |
¡Universal App!
15 | {contacts ? (
16 |
17 | {contacts.map(contact => (
18 |
19 | {contact.first} {contact.last}
20 |
21 | ))}
22 |
23 | ) : (
24 |
Loading...
25 | )}
26 |
27 | );
28 | }
29 | }
30 |
31 | export default App;
32 |
--------------------------------------------------------------------------------
/subjects/13-Server-Rendering/solution/fetchContacts.js:
--------------------------------------------------------------------------------
1 | import "isomorphic-fetch";
2 |
3 | const fetchContacts = cb =>
4 | fetch("https://addressbook-api.herokuapp.com/contacts")
5 | .then(res => res.json())
6 | .then(data => {
7 | cb(null, data.contacts);
8 | }, cb);
9 |
10 | export default fetchContacts;
11 |
--------------------------------------------------------------------------------
/subjects/13-Server-Rendering/solution/server.js:
--------------------------------------------------------------------------------
1 | import http from "http";
2 | import React from "react";
3 | import ReactDOMServer from "react-dom/server";
4 |
5 | import fetchContacts from "./fetchContacts";
6 | import App from "./App";
7 |
8 | const webpackServer = "http://localhost:8080";
9 | const port = 8090;
10 |
11 | const createPage = (markup, data) => `
12 |
13 |
14 |
15 |
16 | My Universal App
17 |
18 |
19 |
20 | ${markup}
21 |
22 |
23 |
24 |
25 |
26 |
27 | `;
28 |
29 | const app = http.createServer((req, res) => {
30 | fetchContacts((error, contacts) => {
31 | const markup = ReactDOMServer.renderToString(
32 |
33 | );
34 | const html = createPage(markup, { contacts });
35 |
36 | res.writeHead(200, {
37 | "Content-Type": "text/html",
38 | "Content-Length": html.length
39 | });
40 |
41 | res.end(html);
42 | });
43 | });
44 |
45 | app.listen(port, () => {
46 | console.log("\nOpen http://localhost:%s", port);
47 | });
48 |
--------------------------------------------------------------------------------
/subjects/14-Transitions/components/HeightFader.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Mixin as TweenStateMixin } from "react-tween-state";
3 |
4 | function getHeight(node) {
5 | return node.scrollHeight;
6 | }
7 |
8 | const HeightFader = React.createClass({
9 | mixins: [TweenStateMixin],
10 |
11 | getDefaultProps() {
12 | return {
13 | component: "li"
14 | };
15 | },
16 |
17 | getInitialState() {
18 | return {
19 | opacity: 0,
20 | height: 0
21 | };
22 | },
23 |
24 | componentWillEnter(cb) {
25 | this.tweenState("opacity", {
26 | duration: 250,
27 | endValue: 1
28 | });
29 |
30 | this.tweenState("height", {
31 | duration: 250,
32 | endValue: getHeight(React.findDOMNode(this)),
33 | onEnd: cb
34 | });
35 | },
36 |
37 | componentWillLeave(cb) {
38 | this.tweenState("opacity", {
39 | duration: 250,
40 | endValue: 0
41 | });
42 |
43 | this.tweenState("height", {
44 | duration: 250,
45 | endValue: 0,
46 | onEnd: cb
47 | });
48 | },
49 |
50 | render() {
51 | return React.createElement(this.props.component, {
52 | ...this.props,
53 | style: {
54 | opacity: this.getTweeningValue("opacity"),
55 | height: this.getTweeningValue("height")
56 | }
57 | });
58 | }
59 | });
60 |
61 | export default HeightFader;
62 |
--------------------------------------------------------------------------------
/subjects/14-Transitions/exercise.js:
--------------------------------------------------------------------------------
1 | ////////////////////////////////////////////////////////////////////////////////
2 | // Exercise:
3 | //
4 | // - Use TweenStateMixin to animate a sliding animation
5 | // - Experiment with different types of easing (hint: use easingTypes at
6 | // https://github.com/chenglou/tween-functions/blob/master/index.js)
7 | ////////////////////////////////////////////////////////////////////////////////
8 | import "./styles.css";
9 |
10 | import React from "react";
11 | import ReactDOM from "react-dom";
12 | import PropTypes from "prop-types";
13 | import {
14 | easingTypes,
15 | Mixin as TweenStateMixin
16 | } from "react-tween-state";
17 |
18 | const ToggleSwitch = React.createClass({
19 | propTypes: {
20 | animationDuration: PropTypes.number
21 | },
22 |
23 | getDefaultProps() {
24 | return {
25 | animationDuration: 350
26 | };
27 | },
28 |
29 | getInitialState() {
30 | return {
31 | knobLeft: 0
32 | };
33 | },
34 |
35 | toggle() {
36 | this.setState({
37 | knobLeft: this.state.knobLeft === 0 ? 400 : 0
38 | });
39 | },
40 |
41 | handleClick() {
42 | this.toggle();
43 | },
44 |
45 | render() {
46 | const knobStyle = {
47 | WebkitTransform: `translate3d(${this.state.knobLeft}px,0,0)`,
48 | transform: `translate3d(${this.state.knobLeft}px,0,0)`
49 | };
50 |
51 | return (
52 |
55 | );
56 | }
57 | });
58 |
59 | ReactDOM.render( , document.getElementById("app"));
60 |
--------------------------------------------------------------------------------
/subjects/14-Transitions/lecture.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render } from "react-dom";
3 | import TransitionGroup from "react-addons-transition-group";
4 | import HeightFader from "./components/HeightFader";
5 |
6 | const List = React.createClass({
7 | getInitialState() {
8 | return {
9 | items: []
10 | };
11 | },
12 |
13 | addItem(e) {
14 | if (e.key === "Enter") {
15 | if (this.guid == null) this.guid = 1;
16 |
17 | const newItem = {
18 | id: this.guid++,
19 | label: e.target.value
20 | };
21 |
22 | this.setState({
23 | items: [newItem].concat(this.state.items)
24 | });
25 |
26 | e.target.value = "";
27 | }
28 | },
29 |
30 | removeItem(item) {
31 | this.setState({
32 | items: this.state.items.filter(i => i !== item)
33 | });
34 | },
35 |
36 | render() {
37 | return (
38 |
39 |
{this.props.name}
40 |
41 |
42 | {this.state.items.map(item => (
43 |
44 | {item.label}{" "}
45 | this.removeItem(item)}>
46 | remove
47 |
48 |
49 | ))}
50 |
51 |
52 | );
53 | }
54 | });
55 |
56 | const App = React.createClass({
57 | render() {
58 | return (
59 |
60 |
61 |
62 | );
63 | }
64 | });
65 |
66 | render( , document.getElementById("app"));
67 |
--------------------------------------------------------------------------------
/subjects/14-Transitions/solution.js:
--------------------------------------------------------------------------------
1 | ////////////////////////////////////////////////////////////////////////////////
2 | // Exercise:
3 | //
4 | // - Use TweenStateMixin to animate a sliding animation
5 | // - Experiment with different types of easing (hint: use easingTypes at
6 | // https://github.com/chenglou/tween-functions/blob/master/index.js)
7 | //
8 | // Got more time?
9 | //
10 | // - Use a to animate the transition
11 | ////////////////////////////////////////////////////////////////////////////////
12 | import "./styles.css";
13 |
14 | import React from "react";
15 | import ReactDOM from "react-dom";
16 | import PropTypes from "prop-types";
17 | import { Mixin as TweenStateMixin } from "react-tween-state";
18 | import { Motion, spring } from "react-motion";
19 |
20 | const TweenToggleSwitch = React.createClass({
21 | propTypes: {
22 | animationDuration: PropTypes.number,
23 | isActive: PropTypes.bool.isRequired
24 | },
25 |
26 | mixins: [TweenStateMixin],
27 |
28 | getDefaultProps() {
29 | return {
30 | animationDuration: 350
31 | };
32 | },
33 |
34 | getInitialState() {
35 | return {
36 | knobLeft: 0
37 | };
38 | },
39 |
40 | componentWillReceiveProps(nextProps) {
41 | this.tweenState("knobLeft", {
42 | duration: this.props.animationDuration,
43 | endValue: nextProps.isActive ? 400 : 0
44 | });
45 | },
46 |
47 | render() {
48 | const knobLeft = this.getTweeningValue("knobLeft");
49 | const knobStyle = {
50 | WebkitTransform: `translate3d(${knobLeft}px,0,0)`,
51 | transform: `translate3d(${knobLeft}px,0,0)`
52 | };
53 |
54 | return (
55 |
58 | );
59 | }
60 | });
61 |
62 | const SpringToggleSwitch = React.createClass({
63 | propTypes: {
64 | isActive: PropTypes.bool.isRequired
65 | },
66 |
67 | render() {
68 | const x = this.props.isActive ? 400 : 0;
69 |
70 | return (
71 |
72 | {s => (
73 |
86 | )}
87 |
88 | );
89 | }
90 | });
91 |
92 | const App = React.createClass({
93 | getInitialState() {
94 | return {
95 | isActive: false
96 | };
97 | },
98 |
99 | toggle() {
100 | this.setState({
101 | isActive: !this.state.isActive
102 | });
103 | },
104 |
105 | handleClick() {
106 | this.toggle();
107 | },
108 |
109 | render() {
110 | return (
111 |
112 |
113 |
114 | Toggle
115 |
116 | );
117 | }
118 | });
119 |
120 | ReactDOM.render( , document.getElementById("app"));
121 |
--------------------------------------------------------------------------------
/subjects/14-Transitions/styles.css:
--------------------------------------------------------------------------------
1 | body {
2 | padding: 10px;
3 | }
4 |
5 | .toggle-switch {
6 | border-radius: 4px;
7 | background-color: #ccc;
8 | position: relative;
9 | width: 450px;
10 | height: 50px;
11 | margin: 10px 0;
12 | }
13 |
14 | .toggle-switch-knob {
15 | position: absolute;
16 | width: 50px;
17 | height: 50px;
18 | border-radius: 4px;
19 | background-color: rgb(130, 181, 198);
20 | }
21 |
--------------------------------------------------------------------------------
/subjects/15-Motion/components/Draggable.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | class Draggable extends React.Component {
5 | static propTypes = {
6 | component: PropTypes.oneOfType([PropTypes.string, PropTypes.func])
7 | .isRequired,
8 | onDragStart: PropTypes.func,
9 | onDrag: PropTypes.func,
10 | onDrop: PropTypes.func
11 | };
12 |
13 | static defaultProps = {
14 | component: "div"
15 | };
16 |
17 | componentDidMount() {
18 | this.isDragging = false;
19 | document.addEventListener("mouseup", this.handleMouseUp);
20 | document.addEventListener("mousemove", this.handleMouseMove);
21 | }
22 |
23 | componentWillUnmount() {
24 | document.removeEventListener("mousemove", this.handleMouseMove);
25 | document.removeEventListener("mouseup", this.handleMouseUp);
26 | }
27 |
28 | handleMouseDown = event => {
29 | if (!this.isDragging) {
30 | this.isDragging = true;
31 |
32 | // Prevent Chrome from displaying a text cursor
33 | event.preventDefault();
34 |
35 | if (this.props.onDragStart) this.props.onDragStart(event);
36 | }
37 | };
38 |
39 | handleMouseMove = event => {
40 | if (this.isDragging && this.props.onDrag) this.props.onDrag(event);
41 | };
42 |
43 | handleMouseUp = event => {
44 | if (this.isDragging) {
45 | this.isDragging = false;
46 |
47 | if (this.props.onDrop) this.props.onDrop(event);
48 | }
49 | };
50 |
51 | render() {
52 | const { component, ...otherProps } = this.props;
53 |
54 | return React.createElement(component, {
55 | ...otherProps,
56 | onMouseDown: this.handleMouseDown
57 | });
58 | }
59 | }
60 |
61 | export default Draggable;
62 |
--------------------------------------------------------------------------------
/subjects/15-Motion/components/Tone.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import createOscillator from "../utils/createOscillator";
4 |
5 | class Tone extends React.Component {
6 | static propTypes = {
7 | isPlaying: PropTypes.bool.isRequired,
8 | pitch: PropTypes.number.isRequired,
9 | volume: PropTypes.number.isRequired
10 | };
11 |
12 | componentDidMount() {
13 | this.oscillator = createOscillator();
14 | this.doImperativeWork();
15 | }
16 |
17 | componentDidUpdate() {
18 | this.doImperativeWork();
19 | }
20 |
21 | doImperativeWork() {
22 | if (this.props.isPlaying) {
23 | this.oscillator.play();
24 | } else {
25 | this.oscillator.stop();
26 | }
27 |
28 | this.oscillator.setPitchBend(this.props.pitch);
29 | this.oscillator.setVolume(this.props.volume);
30 | }
31 |
32 | render() {
33 | return null;
34 | }
35 | }
36 |
37 | export default Tone;
38 |
--------------------------------------------------------------------------------
/subjects/15-Motion/exercise.js:
--------------------------------------------------------------------------------
1 | ///////////////////////////////////////////////////////////////////////////////
2 | // Exercise:
3 | //
4 | // - Use a to animate the transition of the red "marker" to its
5 | // destination when it is dropped
6 | //
7 | // Got extra time?
8 | //
9 | // - If you didn't already, use a custom spring to give the animation
10 | // an elastic, bouncy feel
11 | // - Add a "drop hint" element that indicates which element will receive
12 | // the marker when it is dropped to improve usability
13 | ////////////////////////////////////////////////////////////////////////////////
14 | import "./styles.css";
15 |
16 | import React from "react";
17 | import ReactDOM from "react-dom";
18 | import PropTypes from "prop-types";
19 | import { Motion, spring } from "react-motion";
20 | import Draggable from "./components/Draggable";
21 |
22 | class DropGrid extends React.Component {
23 | state = {
24 | isDraggingMarker: false,
25 | startX: 0,
26 | startY: 0,
27 | mouseX: 0,
28 | mouseY: 0
29 | };
30 |
31 | getRelativeXY({ clientX, clientY }) {
32 | const { offsetLeft, offsetTop } = this.node;
33 |
34 | return {
35 | x: clientX - offsetLeft,
36 | y: clientY - offsetTop
37 | };
38 | }
39 |
40 | handleDragStart = event => {
41 | const { x, y } = this.getRelativeXY(event);
42 | const { offsetLeft, offsetTop } = event.target;
43 |
44 | // Prevent Chrome from displaying a text cursor
45 | event.preventDefault();
46 |
47 | this.setState({
48 | isDraggingMarker: true,
49 | startX: x - offsetLeft,
50 | startY: y - offsetTop,
51 | mouseX: x,
52 | mouseY: y
53 | });
54 | };
55 |
56 | handleDrag = event => {
57 | const { x, y } = this.getRelativeXY(event);
58 |
59 | this.setState({
60 | mouseX: x,
61 | mouseY: y
62 | });
63 | };
64 |
65 | handleDrop = () => {
66 | this.setState({ isDraggingMarker: false });
67 | };
68 |
69 | render() {
70 | const {
71 | isDraggingMarker,
72 | startX,
73 | startY,
74 | mouseX,
75 | mouseY
76 | } = this.state;
77 |
78 | let markerLeft, markerTop;
79 | if (isDraggingMarker) {
80 | markerLeft = mouseX - startX;
81 | markerTop = mouseY - startY;
82 | } else {
83 | markerLeft =
84 | Math.floor(Math.max(0, Math.min(449, mouseX)) / 150) * 150;
85 | markerTop =
86 | Math.floor(Math.max(0, Math.min(449, mouseY)) / 150) * 150;
87 | }
88 |
89 | const markerStyle = {
90 | left: markerLeft,
91 | top: markerTop
92 | };
93 |
94 | return (
95 | (this.node = node)}>
96 |
103 |
1
104 |
2
105 |
3
106 |
4
107 |
5
108 |
6
109 |
7
110 |
8
111 |
9
112 |
113 | );
114 | }
115 | }
116 |
117 | ReactDOM.render( , document.getElementById("app"));
118 |
--------------------------------------------------------------------------------
/subjects/15-Motion/styles.css:
--------------------------------------------------------------------------------
1 | body {
2 | display: flex;
3 | justify-content: center;
4 | align-items: center;
5 | height: 100vh;
6 | margin: 0;
7 | }
8 |
9 | .toggle-switch {
10 | border-radius: 4px;
11 | background-color: #ccc;
12 | position: relative;
13 | height: 50px;
14 | margin: 10px auto;
15 |
16 | -moz-user-select: none;
17 | -ms-user-select: none;
18 | -webkit-user-select: none;
19 | }
20 | .toggle-switch-knob {
21 | position: absolute;
22 | width: 50px;
23 | height: 50px;
24 | border-radius: 4px;
25 | background-color: rgb(130, 181, 198);
26 | }
27 |
28 | .switch1 {
29 | width: 450px;
30 | }
31 | .switch2 {
32 | width: 650px;
33 | }
34 | .switch3 {
35 | width: 350px;
36 | }
37 |
38 | .grid {
39 | position: absolute;
40 | margin: auto;
41 | top: 0;
42 | right: 0;
43 | bottom: 0;
44 | left: 0;
45 | border: 1px solid #999;
46 | border-width: 1px 1px 0 0;
47 | width: 451px;
48 | height: 451px;
49 | }
50 | .grid-cell {
51 | border: 1px solid #999;
52 | border-width: 0 0 1px 1px;
53 | width: 150px;
54 | height: 150px;
55 | line-height: 150px;
56 | text-align: center;
57 | color: #aaa;
58 | float: left;
59 | -webkit-user-select: none;
60 | user-select: none;
61 | }
62 |
63 | .grid-marker, .grid .drop-hint {
64 | position: absolute;
65 | background: red;
66 | width: 150px;
67 | height: 150px;
68 | opacity: 0.6;
69 | }
70 | .grid .drop-hint {
71 | background: #999;
72 | opacity: 0.3;
73 | }
74 |
--------------------------------------------------------------------------------
/subjects/15-Motion/utils/createOscillator.js:
--------------------------------------------------------------------------------
1 | import "./AudioContextMonkeyPatch";
2 |
3 | function Oscillator(audioContext) {
4 | // TODO make more things not use this.
5 | const oscillatorNode = audioContext.createOscillator();
6 | oscillatorNode.start(0);
7 |
8 | const gainNode = audioContext.createGain();
9 | this.pitchBase = 50;
10 | this.pitchBend = 0;
11 | this.pitchRange = 2000;
12 | this.volume = 0.5;
13 | this.maxVolume = 1;
14 | this.frequency = this.pitchBase;
15 |
16 | let hasConnected = false;
17 | let frequency = this.pitchBase;
18 |
19 | this.play = function() {
20 | oscillatorNode.connect(gainNode);
21 | hasConnected = true;
22 | };
23 |
24 | this.stop = function() {
25 | if (hasConnected) {
26 | oscillatorNode.disconnect(gainNode);
27 | hasConnected = false;
28 | }
29 | };
30 |
31 | this.setType = function(type) {
32 | oscillatorNode.type = type;
33 | };
34 |
35 | this.setPitchBend = function(v) {
36 | this.pitchBend = v;
37 | frequency = this.pitchBase + this.pitchBend * this.pitchRange;
38 | oscillatorNode.frequency.value = frequency;
39 | this.frequency = frequency;
40 | };
41 |
42 | this.setVolume = function(v) {
43 | this.volume = this.maxVolume * v;
44 | gainNode.gain.value = this.volume;
45 | };
46 |
47 | this.connect = function(output) {
48 | gainNode.connect(output);
49 | };
50 |
51 | return this;
52 | }
53 |
54 | function createOscillator() {
55 | const audioContext = new AudioContext();
56 | const theremin = new Oscillator(audioContext);
57 |
58 | theremin.connect(audioContext.destination);
59 |
60 | return theremin;
61 | }
62 |
63 | export default createOscillator;
64 |
--------------------------------------------------------------------------------
/subjects/16-Redux/Redux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ReactTraining/classic-react-workshop/251d8ba4c6f1da0c5815947fdb40bc32453d0443/subjects/16-Redux/Redux.png
--------------------------------------------------------------------------------
/subjects/16-Redux/exercise.js:
--------------------------------------------------------------------------------
1 | import "./exercise/index.js";
2 |
--------------------------------------------------------------------------------
/subjects/16-Redux/exercise/actions.js:
--------------------------------------------------------------------------------
1 | import { fetchContacts, deleteContactById } from "./utils/api";
2 |
3 | export const ADD_CONTACT = "ADD_CONTACT";
4 | export const LOAD_CONTACTS = "LOAD_CONTACTS";
5 | export const CONTACTS_WERE_LOADED = "CONTACTS_WERE_LOADED";
6 |
7 | export function addContact(contact) {
8 | return {
9 | type: ADD_CONTACT,
10 | contact
11 | };
12 | }
13 |
14 | export function loadContacts(dispatch) {
15 | dispatch({ type: LOAD_CONTACTS });
16 |
17 | fetchContacts((error, contacts) => {
18 | dispatch({
19 | type: CONTACTS_WERE_LOADED,
20 | contacts
21 | });
22 | });
23 | }
24 |
--------------------------------------------------------------------------------
/subjects/16-Redux/exercise/components/App.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Provider } from "react-redux";
3 | import store from "../store";
4 | import ContactList from "./ContactList";
5 |
6 | class App extends React.Component {
7 | render() {
8 | return (
9 |
10 |
11 |
Contacts!
12 |
13 |
14 |
15 | );
16 | }
17 | }
18 |
19 | export default App;
20 |
--------------------------------------------------------------------------------
/subjects/16-Redux/exercise/components/ContactList.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import { addContact, loadContacts } from "../actions";
4 | import CreateContactForm from "./CreateContactForm";
5 |
6 | class ContactList extends React.Component {
7 | static defaultProps = {
8 | contacts: [],
9 | dispatch: () => {
10 | console.log(
11 | "Dispatch failed; you still need to connect() your ContactList"
12 | );
13 | }
14 | };
15 |
16 | componentDidMount() {
17 | loadContacts(this.props.dispatch);
18 | }
19 |
20 | createContact(contact) {
21 | this.props.dispatch(addContact(contact));
22 | }
23 |
24 | render() {
25 | const { contacts } = this.props;
26 |
27 | return (
28 |
29 | {contacts.map(contact => (
30 |
31 | {contact.first}{" "}
32 | {contact.last}
33 |
34 | ))}
35 |
36 | this.createContact(contact)}
38 | />
39 |
40 |
41 | );
42 | }
43 | }
44 |
45 | export default ContactList;
46 |
--------------------------------------------------------------------------------
/subjects/16-Redux/exercise/components/CreateContactForm.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import serializeForm from "form-serialize";
4 |
5 | const transparentGif =
6 | "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
7 |
8 | function generateId() {
9 | return Math.random()
10 | .toString(36)
11 | .substring(7);
12 | }
13 |
14 | class CreateContactForm extends React.Component {
15 | static propTypes = {
16 | onCreate: PropTypes.func.isRequired
17 | };
18 |
19 | handleSubmit = event => {
20 | event.preventDefault();
21 | const contact = serializeForm(event.target, { hash: true });
22 | contact.id = generateId();
23 | this.props.onCreate(contact);
24 | event.target.reset();
25 | };
26 |
27 | render() {
28 | return (
29 |
56 | );
57 | }
58 | }
59 |
60 | export default CreateContactForm;
61 |
--------------------------------------------------------------------------------
/subjects/16-Redux/exercise/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | The goal of this exercise is to gain some hands-on experience using Redux to
3 | manage the state of a React application. In the process, we'll also learn how to
4 | use Redux to communicate changes to a real API server.
5 |
6 | - Get the the props it needs by using connect() to connect it to
7 | the in its parent, the .
8 | - Once it's connected, if you open the console you'll see the contacts being
9 | loaded from the API.
10 | - Add a delete next to each contact that dispatches a DELETE_CONTACT
11 | action. Check the console to make sure the action is being dispatched.
12 | - Add some logic to the contacts reducer to remove that contact when it sees
13 | the DELETE_CONTACT action come through. Also, send a request to the API server
14 | to actually delete the contact on the server. When you refresh the page, that
15 | contact should not appear.
16 | - To start with a fresh list of contacts, use `localStorage.clear()` in the
17 | browser's console.
18 |
19 | We've added the logger middleware to show you all changes as they are happening
20 | in the browser console. If you get stuck, be sure to check the logs!
21 |
22 | Got extra time?
23 |
24 | - Use the throttling feature in Chrome's Network tab to simulate a really slow
25 | connection. How does the UI respond to your actions? What can you do to improve
26 | the UX?
27 | */
28 | import React from "react";
29 | import ReactDOM from "react-dom";
30 | import App from "./components/App";
31 |
32 | ReactDOM.render( , document.getElementById("app"));
33 |
--------------------------------------------------------------------------------
/subjects/16-Redux/exercise/logger.js:
--------------------------------------------------------------------------------
1 | const logger = store => next => action => {
2 | console.group(action.type);
3 | console.info("dispatching", action);
4 |
5 | let result = next(action);
6 |
7 | console.log("next state", store.getState());
8 | console.groupEnd(action.type);
9 |
10 | return result;
11 | };
12 |
13 | export default logger;
14 |
--------------------------------------------------------------------------------
/subjects/16-Redux/exercise/reducers.js:
--------------------------------------------------------------------------------
1 | import { ADD_CONTACT, CONTACTS_WERE_LOADED } from "./actions";
2 |
3 | export function contacts(state = [], action) {
4 | if (action.type === ADD_CONTACT) {
5 | return state.concat([action.contact]);
6 | } else if (action.type === CONTACTS_WERE_LOADED) {
7 | return action.contacts;
8 | } else {
9 | return state;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/subjects/16-Redux/exercise/store.js:
--------------------------------------------------------------------------------
1 | import {
2 | createStore,
3 | combineReducers,
4 | applyMiddleware,
5 | compose
6 | } from "redux";
7 | import * as reducers from "./reducers";
8 | import logger from "./logger";
9 |
10 | const composeEnhancers =
11 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
12 |
13 | export default createStore(
14 | combineReducers(reducers),
15 | composeEnhancers(applyMiddleware(logger))
16 | );
17 |
--------------------------------------------------------------------------------
/subjects/16-Redux/exercise/utils/api.js:
--------------------------------------------------------------------------------
1 | import { getJSON, deleteJSON } from "./xhr";
2 |
3 | const serverURL = "http://addressbook-api.herokuapp.com";
4 |
5 | export function fetchContacts(cb) {
6 | getJSON(`${serverURL}/contacts`, (error, res) => {
7 | cb(error, res.contacts);
8 | });
9 | }
10 |
11 | export function deleteContactById(contactId, cb) {
12 | deleteJSON(`${serverURL}/contacts/${contactId}`, cb);
13 | }
14 |
--------------------------------------------------------------------------------
/subjects/16-Redux/exercise/utils/xhr.js:
--------------------------------------------------------------------------------
1 | localStorage.token = localStorage.token || Date.now() * Math.random();
2 |
3 | function setToken(req) {
4 | req.setRequestHeader("authorization", localStorage.token);
5 | }
6 |
7 | export function getJSON(url, callback) {
8 | const req = new XMLHttpRequest();
9 | req.onload = function() {
10 | if (req.status === 404) {
11 | callback(new Error("not found"));
12 | } else {
13 | callback(null, JSON.parse(req.response));
14 | }
15 | };
16 | req.open("GET", url);
17 | setToken(req);
18 | req.send();
19 | }
20 |
21 | export function postJSON(url, obj, callback) {
22 | const req = new XMLHttpRequest();
23 | req.onload = function() {
24 | callback(JSON.parse(req.response));
25 | };
26 | req.open("POST", url);
27 | req.setRequestHeader(
28 | "Content-Type",
29 | "application/json;charset=UTF-8"
30 | );
31 | setToken(req);
32 | req.send(JSON.stringify(obj));
33 | }
34 |
35 | export function deleteJSON(url, callback) {
36 | const req = new XMLHttpRequest();
37 | req.onload = function() {
38 | setTimeout(() => {
39 | if (req.status === 500) {
40 | callback(new Error(req.responseText));
41 | } else {
42 | callback(null, req.responseText);
43 | }
44 | }, Math.random() * 5000);
45 | };
46 | req.open("DELETE", url);
47 | setToken(req);
48 | req.send();
49 | }
50 |
--------------------------------------------------------------------------------
/subjects/16-Redux/lecture.js:
--------------------------------------------------------------------------------
1 | import { createStore } from "redux";
2 |
3 | const store = createStore((state = 0, action) => {
4 | if (action.type === "INCREMENT") {
5 | return state + (action.by || 1);
6 | } else {
7 | return state;
8 | }
9 | });
10 |
11 | store.subscribe(() => {
12 | console.log(store.getState());
13 | });
14 |
15 | store.dispatch({ type: "INCREMENT" });
16 | store.dispatch({ type: "INCREMENT", by: 5 });
17 | store.dispatch({ type: "INCREMENT" });
18 | store.dispatch({ type: "INCREMENT" });
19 |
20 | /*
21 | - Flux is an architecture, not a framework
22 | - DO NOT START BUILDING STUFF WITH FLUX WHEN YOU'RE FIRST GETTING STARTED WITH REACT
23 | - It can be difficult to understand why the patterns in Flux are useful if you haven't
24 | already tried to solve problems w/out Flux
25 | - You'll most likely hate Flux unless you're already fighting with your current JS
26 | framework. If you're not, stick with what's working for you
27 |
28 | - Flux is good at:
29 | - Making it easy to reason about changes to state
30 |
31 | - Remember our 2 questions:
32 | - What state is there?
33 | - When does it change?
34 |
35 | Open Redux.png
36 |
37 | - Views
38 | - React components (see components)
39 | - Create actions (see actions)
40 |
41 | - Actions
42 | - Create "actions" with meaningful names (e.g. "load contacts", "delete contact").
43 | These are the verbs. Ask yourself, "what actions can the user take?"
44 | - Send actions through the dispatcher
45 | - Possibly trigger API requests (side effect)
46 |
47 | - Store
48 | - Synchronous dispatch of actions to ALL registered listeners (stores)
49 |
50 | - Reducers
51 | - Compute new state values
52 | */
53 |
--------------------------------------------------------------------------------
/subjects/16-Redux/notes.md:
--------------------------------------------------------------------------------
1 | - what state is there, when does it change?
2 | - create a store, mess w/ it in the console with a counter
3 | - connect it to the app
4 | - logger middleware
5 | - explicit actions
6 | - move contacts into a reducer
7 | - connect it to the app
8 | - combineReducers
9 | - load contacts from server
10 | - async actions and thunk
11 |
12 |
--------------------------------------------------------------------------------
/subjects/16-Redux/solution.js:
--------------------------------------------------------------------------------
1 | import "./solution/index.js";
2 |
--------------------------------------------------------------------------------
/subjects/16-Redux/solution/actions.js:
--------------------------------------------------------------------------------
1 | import { fetchContacts, deleteContactById } from "./utils/api";
2 |
3 | export const ADD_CONTACT = "ADD_CONTACT";
4 | export const LOAD_CONTACTS = "LOAD_CONTACTS";
5 | export const CONTACTS_WERE_LOADED = "CONTACTS_WERE_LOADED";
6 | export const DELETE_CONTACT = "DELETE_CONTACT";
7 | export const CONTACT_WAS_DELETED = "CONTACT_WAS_DELETED";
8 | export const ERROR_DELETING_CONTACT = "ERROR_DELETING_CONTACT";
9 |
10 | export function addContact(contact) {
11 | return {
12 | type: ADD_CONTACT,
13 | contact
14 | };
15 | }
16 |
17 | export function loadContacts(dispatch) {
18 | dispatch({ type: LOAD_CONTACTS });
19 |
20 | fetchContacts((error, contacts) => {
21 | dispatch({
22 | type: CONTACTS_WERE_LOADED,
23 | contacts
24 | });
25 | });
26 | }
27 |
28 | export function deleteContact(contactId, dispatch) {
29 | // We can handle latency with two actions: one when we begin...
30 | dispatch({ type: DELETE_CONTACT, contactId });
31 |
32 | deleteContactById(contactId, error => {
33 | if (error) {
34 | // ...and another when we fail.
35 | dispatch({
36 | type: ERROR_DELETING_CONTACT,
37 | message: error.message,
38 | contactId
39 | });
40 | } else {
41 | // ...and another when we finish!
42 | dispatch({ type: CONTACT_WAS_DELETED, contactId });
43 | }
44 | });
45 | }
46 |
--------------------------------------------------------------------------------
/subjects/16-Redux/solution/components/App.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Provider } from "react-redux";
3 | import store from "../store";
4 | import ContactList from "./ContactList";
5 |
6 | class App extends React.Component {
7 | render() {
8 | return (
9 |
10 |
11 |
Contacts!
12 |
13 |
14 |
15 | );
16 | }
17 | }
18 |
19 | export default App;
20 |
--------------------------------------------------------------------------------
/subjects/16-Redux/solution/components/ContactList.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import { addContact, loadContacts, deleteContact } from "../actions";
4 | import CreateContactForm from "./CreateContactForm";
5 |
6 | class ContactList extends React.Component {
7 | static defaultProps = {
8 | contacts: []
9 | };
10 |
11 | componentDidMount() {
12 | loadContacts(this.props.dispatch);
13 | }
14 |
15 | createContact(contact) {
16 | this.props.dispatch(addContact(contact));
17 | }
18 |
19 | deleteContact(contact) {
20 | deleteContact(contact.id, this.props.dispatch);
21 | }
22 |
23 | render() {
24 | const {
25 | contacts,
26 | contactsWithErrors,
27 | contactsBeingDeleted
28 | } = this.props;
29 |
30 | return (
31 |
60 | );
61 | }
62 | }
63 |
64 | export default connect(state => state)(ContactList);
65 |
--------------------------------------------------------------------------------
/subjects/16-Redux/solution/components/CreateContactForm.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import serializeForm from "form-serialize";
4 |
5 | const transparentGif =
6 | "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
7 |
8 | function generateId() {
9 | return Math.random()
10 | .toString(36)
11 | .substring(7);
12 | }
13 |
14 | class CreateContactForm extends React.Component {
15 | static propTypes = {
16 | onCreate: PropTypes.func.isRequired
17 | };
18 |
19 | handleSubmit = event => {
20 | event.preventDefault();
21 | const contact = serializeForm(event.target, { hash: true });
22 | contact.id = generateId();
23 | this.props.onCreate(contact);
24 | event.target.reset();
25 | };
26 |
27 | render() {
28 | return (
29 |
56 | );
57 | }
58 | }
59 |
60 | export default CreateContactForm;
61 |
--------------------------------------------------------------------------------
/subjects/16-Redux/solution/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | The goal of this exercise is to gain some hands-on experience using Redux to
3 | manage the state of a React application. In the process, we'll also learn how to
4 | use Redux to communicate changes to a real API server.
5 |
6 | - Get the the props it needs by using connect() to connect it to
7 | the in its parent, the .
8 | - Once it's connected, if you open the console you'll see the contacts being
9 | loaded from the API.
10 | - Add a delete next to each contact that dispatches a DELETE_CONTACT
11 | action. Check the console to make sure the action is being dispatched.
12 | - Add some logic to the contacts reducer to remove that contact when it sees
13 | the DELETE_CONTACT action come through. Also, send a request to the API server
14 | to actually delete the contact on the server. When you refresh the page, that
15 | contact should not appear.
16 | - To start with a fresh list of contacts, use `localStorage.clear()` in the
17 | browser's console.
18 |
19 | We've added the logger middleware to show you all changes as they are happening
20 | in the browser console. If you get stuck, be sure to check the logs!
21 |
22 | Got extra time?
23 |
24 | - Use the throttling feature in Chrome's Network tab to simulate a really slow
25 | connection. How does the UI respond to your actions? What can you do to improve
26 | the UX?
27 | */
28 | import React from "react";
29 | import ReactDOM from "react-dom";
30 | import App from "./components/App";
31 |
32 | ReactDOM.render( , document.getElementById("app"));
33 |
--------------------------------------------------------------------------------
/subjects/16-Redux/solution/logger.js:
--------------------------------------------------------------------------------
1 | const logger = store => next => action => {
2 | console.group(action.type);
3 | console.info("dispatching", action);
4 |
5 | let result = next(action);
6 |
7 | console.log("next state", store.getState());
8 | console.groupEnd(action.type);
9 |
10 | return result;
11 | };
12 |
13 | export default logger;
14 |
--------------------------------------------------------------------------------
/subjects/16-Redux/solution/reducers.js:
--------------------------------------------------------------------------------
1 | import {
2 | ADD_CONTACT,
3 | CONTACTS_WERE_LOADED,
4 | DELETE_CONTACT,
5 | CONTACT_WAS_DELETED,
6 | ERROR_DELETING_CONTACT
7 | } from "./actions";
8 |
9 | export function contacts(state = [], action) {
10 | if (action.type === ADD_CONTACT) {
11 | return state.concat([action.contact]);
12 | } else if (action.type === CONTACTS_WERE_LOADED) {
13 | return action.contacts;
14 | } else if (action.type === CONTACT_WAS_DELETED) {
15 | return state.filter(contact => contact.id !== action.contactId);
16 | } else {
17 | return state;
18 | }
19 | }
20 |
21 | export function contactsBeingDeleted(state = {}, action) {
22 | if (action.type === DELETE_CONTACT) {
23 | return {
24 | ...state,
25 | [action.contactId]: true
26 | };
27 | } else if (action.type === ERROR_DELETING_CONTACT) {
28 | delete state[action.contactId];
29 | return { ...state };
30 | } else {
31 | return state;
32 | }
33 | }
34 |
35 | export function contactsWithErrors(state = {}, action) {
36 | if (action.type === ERROR_DELETING_CONTACT) {
37 | return {
38 | ...state,
39 | [action.contactId]: action.message
40 | };
41 | } else {
42 | return state;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/subjects/16-Redux/solution/store.js:
--------------------------------------------------------------------------------
1 | import {
2 | createStore,
3 | combineReducers,
4 | applyMiddleware,
5 | compose
6 | } from "redux";
7 | import * as reducers from "./reducers";
8 | import logger from "./logger";
9 |
10 | const composeEnhancers =
11 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
12 |
13 | export default createStore(
14 | combineReducers(reducers),
15 | composeEnhancers(applyMiddleware(logger))
16 | );
17 |
--------------------------------------------------------------------------------
/subjects/16-Redux/solution/utils/api.js:
--------------------------------------------------------------------------------
1 | import { getJSON, deleteJSON } from "./xhr";
2 |
3 | const serverURL = "http://addressbook-api.herokuapp.com";
4 |
5 | export function fetchContacts(cb) {
6 | getJSON(`${serverURL}/contacts`, (error, res) => {
7 | cb(error, res.contacts);
8 | });
9 | }
10 |
11 | export function deleteContactById(contactId, cb) {
12 | deleteJSON(`${serverURL}/contacts/${contactId}`, cb);
13 | }
14 |
--------------------------------------------------------------------------------
/subjects/16-Redux/solution/utils/xhr.js:
--------------------------------------------------------------------------------
1 | localStorage.token = localStorage.token || Date.now() * Math.random();
2 |
3 | function setToken(req) {
4 | req.setRequestHeader("authorization", localStorage.token);
5 | }
6 |
7 | export function getJSON(url, callback) {
8 | const req = new XMLHttpRequest();
9 | req.onload = function() {
10 | if (req.status === 404) {
11 | callback(new Error("not found"));
12 | } else {
13 | callback(null, JSON.parse(req.response));
14 | }
15 | };
16 | req.open("GET", url);
17 | setToken(req);
18 | req.send();
19 | }
20 |
21 | export function postJSON(url, obj, callback) {
22 | const req = new XMLHttpRequest();
23 | req.onload = function() {
24 | callback(JSON.parse(req.response));
25 | };
26 | req.open("POST", url);
27 | req.setRequestHeader(
28 | "Content-Type",
29 | "application/json;charset=UTF-8"
30 | );
31 | setToken(req);
32 | req.send(JSON.stringify(obj));
33 | }
34 |
35 | export function deleteJSON(url, callback) {
36 | const req = new XMLHttpRequest();
37 | req.onload = function() {
38 | setTimeout(() => {
39 | if (req.status === 500) {
40 | callback(new Error(req.responseText));
41 | } else {
42 | callback(null, req.responseText);
43 | }
44 | }, Math.random() * 5000);
45 | };
46 | req.open("DELETE", url);
47 | setToken(req);
48 | req.send();
49 | }
50 |
--------------------------------------------------------------------------------
/subjects/17-Chat-App/exercise.js:
--------------------------------------------------------------------------------
1 | ////////////////////////////////////////////////////////////////////////////////
2 | // Exercise:
3 | //
4 | // - Create a chat application using the utility methods we give you
5 | //
6 | // Tip: The app uses a pop-up window for auth with GitHub. You may need to
7 | // make sure that you aren't blocking pop-up windows on localhost!
8 | //
9 | // Need some ideas?
10 | //
11 | // - Group subsequent messages from the same sender
12 | // - Highlight messages from you to make them easy to find
13 | // - Highlight messages that mention you by your GitHub username
14 | // - Cause the message list to automatically scroll as new messages come in
15 | // - Create a filter that lets you filter messages in the chat by
16 | // sender and/or content
17 | ////////////////////////////////////////////////////////////////////////////////
18 | import "./styles.css";
19 |
20 | import React, { useEffect, useRef, useState } from "react";
21 | import ReactDOM from "react-dom";
22 |
23 | import { login, sendMessage, subscribeToMessages } from "./utils";
24 |
25 | /*
26 | Here's how to use the utils:
27 |
28 | login(user => {
29 | // do something with the user object
30 | })
31 |
32 | sendMessage({
33 | userId: user.id,
34 | photoURL: user.photoURL,
35 | text: 'hello, this is a message'
36 | })
37 |
38 | const unsubscribe = subscribeToMessages(messages => {
39 | // here are your messages as an array, it will be called
40 | // every time the messages change
41 | })
42 |
43 | unsubscribe() // stop listening for new messages
44 |
45 | The world is your oyster!
46 | */
47 |
48 | function Chat() {
49 | return (
50 |
51 |
52 | HipReact
53 | # messages: 8
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | Hey, Bruce!
63 |
64 | So, a QA Engineer walks into a bar.
65 |
66 | Orders a beer.
67 | Orders 0 beers.
68 | Orders 999999999 beers.
69 | Orders -1 beers.
70 | Orders a sfdeljknesv.
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | Ha 😅
79 |
80 |
81 |
82 |
83 |
88 |
89 | );
90 | }
91 |
92 | ReactDOM.render( , document.getElementById("app"));
93 |
--------------------------------------------------------------------------------
/subjects/17-Chat-App/styles.css:
--------------------------------------------------------------------------------
1 | html {
2 | overflow: hidden; /* prevent elastic scrolling in WebKit */
3 | }
4 |
5 | body {
6 | margin: 0;
7 | line-height: 1.5;
8 | }
9 |
10 | .chat {
11 | height: 100vh;
12 | display: flex;
13 | flex-direction: column;
14 | }
15 |
16 | .chat-header {
17 | display: flex;
18 | justify-content: space-between;
19 | align-items: center;
20 | padding: 10px;
21 | background-color: lightblue;
22 | border-bottom: 1px solid #666;
23 | color: #666;
24 | }
25 | .chat-title,
26 | .chat-message-count {
27 | display: inline-block;
28 | margin: 0;
29 | }
30 | .chat-title {
31 | font-size: 1.5em;
32 | }
33 |
34 | .messages {
35 | overflow: auto;
36 | flex: 1;
37 | }
38 |
39 | .new-message-form {
40 | margin: 0;
41 | }
42 | .new-message {
43 | padding: 2px 5px 4px;
44 | border-top: 1px solid #666;
45 | background: white;
46 | }
47 | .new-message input {
48 | border: 0;
49 | width: 100%;
50 | outline: none;
51 | font-size: 1.2em;
52 | padding: 0.5em;
53 | }
54 |
55 | ol.message-groups,
56 | ol.messages {
57 | list-style-type: none;
58 | margin: 0;
59 | padding: 0;
60 | }
61 |
62 | .message-group {
63 | position: relative;
64 | min-height: 40px;
65 | padding: 0 10px;
66 | margin: 10px 0;
67 | }
68 | .message-group-avatar {
69 | position: absolute;
70 | background-color: pink;
71 | line-height: 0;
72 | width: 40px;
73 | overflow: hidden;
74 | }
75 | .message-group-avatar img {
76 | height: 40px;
77 | }
78 |
79 | ol.messages {
80 | margin-left: 40px;
81 | }
82 |
83 | .message {
84 | padding: 8px 10px;
85 | }
86 |
--------------------------------------------------------------------------------
/subjects/17-Chat-App/utils.js:
--------------------------------------------------------------------------------
1 | import firebase from "firebase/app";
2 | import "firebase/auth";
3 | import "firebase/database";
4 |
5 | import invariant from "invariant";
6 |
7 | firebase.initializeApp({
8 | apiKey: "AIzaSyAT4OaC1A_Soy0f4x-YugeDrBgD6Nt7ZyE",
9 | authDomain: "hip-react.firebaseapp.com",
10 | databaseURL: "https://hip-react.firebaseio.com"
11 | });
12 |
13 | export function login(callback) {
14 | let alreadyLoggedIn = false;
15 |
16 | firebase.auth().onAuthStateChanged(data => {
17 | if (data) {
18 | alreadyLoggedIn = true;
19 |
20 | const providerData = data.providerData[0];
21 |
22 | callback({
23 | id: data.uid,
24 | name: providerData.displayName,
25 | email: providerData.email,
26 | photoURL: providerData.photoURL
27 | });
28 | } else if (!alreadyLoggedIn) {
29 | firebase
30 | .auth()
31 | .signInWithPopup(new firebase.auth.GithubAuthProvider());
32 | }
33 | });
34 | }
35 |
36 | const messagesRef = firebase.database().ref("messages");
37 |
38 | export function subscribeToMessages(callback) {
39 | function emitMessages(snapshot) {
40 | const messages = [];
41 |
42 | snapshot.forEach(s => {
43 | const message = s.val();
44 | message.id = s.key;
45 | messages.push(message);
46 | });
47 |
48 | callback(messages);
49 | }
50 |
51 | messagesRef.on("value", emitMessages);
52 |
53 | return () => {
54 | messagesRef.off("value", emitMessages);
55 | };
56 | }
57 |
58 | export function sendMessage({ userId, photoURL, text }) {
59 | invariant(
60 | typeof userId === "string",
61 | "New messages must have a userId"
62 | );
63 |
64 | invariant(
65 | typeof photoURL === "string",
66 | "New messages must have a photoURL"
67 | );
68 |
69 | invariant(
70 | typeof text === "string",
71 | "New messages must have some text"
72 | );
73 |
74 | if (text) {
75 | messagesRef.push({
76 | timestamp: Date.now(),
77 | userId,
78 | photoURL,
79 | text
80 | });
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/subjects/18-Mini-Router/exercise.js:
--------------------------------------------------------------------------------
1 | ////////////////////////////////////////////////////////////////////////////////
2 | // Exercise:
3 | //
4 | // Implement the core components of React Router to make this app work:
5 | //
6 | //
7 | // 1. Add some state w/ `location` as a key
8 | // 2. Set `location`'s initial value to `this.history.location`
9 | // 3. Listen to `history` and put the location into state
10 | // 4. Use context to provide API to descendants
11 | //
12 | //
13 | // 1. Get the location from context
14 | // 2. Figure out if the path matches location.pathname
15 | // (hint: location.pathname.startsWith(...)
16 | // 3. If there is a match, figure out which prop to render
17 | // `component` or `render`
18 | // 4. If there is no match, render null
19 | //
20 | //
21 | // 1. Get a "push" method from context
22 | // 2. Use `push(...)` with the `to` prop
23 | //
24 | // Got extra time?
25 | //
26 | // - Implement or
27 | ////////////////////////////////////////////////////////////////////////////////
28 | import React from "react";
29 | import ReactDOM from "react-dom";
30 |
31 | // You will be working in mini-router.js. This file is just an example app that
32 | // uses the components from that library.
33 | import { Router, Route, Link } from "./exercise/mini-router";
34 |
35 | function App() {
36 | return (
37 |
38 |
39 |
40 |
41 | Dashboard
42 |
43 |
44 | About
45 |
46 |
47 | Topics
48 |
49 |
50 |
51 |
52 |
53 |
(
56 |
57 |
Dashboard
58 |
59 | )}
60 | />
61 |
62 |
63 |
64 |
65 | );
66 | }
67 |
68 | function About() {
69 | return (
70 |
71 |
About
72 |
73 | );
74 | }
75 |
76 | function Topics() {
77 | return (
78 |
79 |
Topics
80 |
81 | Rendering with React
82 | Components
83 | Props v. State
84 |
85 |
86 | );
87 | }
88 |
89 | ReactDOM.render( , document.getElementById("app"));
90 |
--------------------------------------------------------------------------------
/subjects/18-Mini-Router/exercise/mini-router.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { createHashHistory } from "history";
4 |
5 | /*
6 | How to use the history library:
7 |
8 | // read the current URL
9 | history.location
10 |
11 | // listen for changes to the URL
12 | history.listen(() => {
13 | history.location // is now different
14 | })
15 |
16 | // change the URL
17 | history.push('/something')
18 | */
19 |
20 | class Router extends React.Component {
21 | history = createHashHistory();
22 |
23 | render() {
24 | return this.props.children;
25 | }
26 | }
27 |
28 | class Route extends React.Component {
29 | render() {
30 | const { path, render, component: Component } = this.props;
31 | return null;
32 | }
33 | }
34 |
35 | class Link extends React.Component {
36 | handleClick = event => {
37 | event.preventDefault();
38 | };
39 |
40 | render() {
41 | return (
42 |
43 | {this.props.children}
44 |
45 | );
46 | }
47 | }
48 |
49 | export { Router, Route, Link };
50 |
--------------------------------------------------------------------------------
/subjects/18-Mini-Router/solution.js:
--------------------------------------------------------------------------------
1 | ////////////////////////////////////////////////////////////////////////////////
2 | // Exercise:
3 | //
4 | // Implement the core components of React Router to make this app work:
5 | //
6 | //
7 | // 1. Add some state w/ `location` as a key
8 | // 2. Set `location`'s initial value to `this.history.location`
9 | // 3. Listen to `history` and put the location into state
10 | // 4. Use context to provide API to descendants
11 | //
12 | //
13 | // 1. Get the location from context
14 | // 2. Figure out if the path matches location.pathname
15 | // (hint: location.pathname.startsWith(...)
16 | // 3. If there is a match, figure out which prop to render
17 | // `component` or `render`
18 | // 4. If there is no match, render null
19 | //
20 | //
21 | // 1. Get a "push" method from context
22 | // 2. Use `push(...)` with the `to` prop
23 | //
24 | // Got extra time?
25 | //
26 | // - Implement or
27 | ////////////////////////////////////////////////////////////////////////////////
28 | import React from "react";
29 | import ReactDOM from "react-dom";
30 |
31 | // You will be working in mini-router.js. This file is just an example app that
32 | // uses the components from that library.
33 | import { Router, Route, Link } from "./solution/mini-router";
34 |
35 | function App() {
36 | return (
37 |
38 |
39 |
40 |
41 | Dashboard
42 |
43 |
44 | About
45 |
46 |
47 | Topics
48 |
49 |
50 |
51 |
52 |
53 |
(
56 |
57 |
Dashboard
58 |
59 | )}
60 | />
61 |
62 |
63 |
64 |
65 | );
66 | }
67 |
68 | function About() {
69 | return (
70 |
71 |
About
72 |
73 | );
74 | }
75 |
76 | function Topics() {
77 | return (
78 |
79 |
Topics
80 |
81 | Rendering with React
82 | Components
83 | Props v. State
84 |
85 |
86 | );
87 | }
88 |
89 | ReactDOM.render( , document.getElementById("app"));
90 |
--------------------------------------------------------------------------------
/subjects/18-Mini-Router/solution/mini-router-hooks.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { createHashHistory } from "history";
4 |
5 | /*
6 | How to use the history library:
7 |
8 | // read the current URL
9 | history.location
10 |
11 | // listen for changes to the URL
12 | history.listen(() => {
13 | history.location // is now different
14 | })
15 |
16 | // change the URL
17 | history.push('/something')
18 | */
19 |
20 | const RouterContext = React.createContext();
21 |
22 | import { useState, useEffect, useContext } from "react";
23 |
24 | function Router({ children }) {
25 | const history = createHashHistory();
26 | const [location, updateLocation] = useState(history.location);
27 |
28 | useEffect(() => {
29 | return history.listen(updateLocation);
30 | }, []);
31 |
32 | const handlePush = to => history.push(to);
33 |
34 | return (
35 |
36 | {children}
37 |
38 | );
39 | }
40 |
41 | function Route({ path, render, component: Component }) {
42 | const { location } = useContext(RouterContext);
43 |
44 | if (location.pathname.startsWith(path)) {
45 | if (render) return render();
46 | if (Component) return ;
47 | }
48 |
49 | return null;
50 | }
51 |
52 | function Link({ to, children }) {
53 | const { push } = useContext(RouterContext);
54 |
55 | const handleClick = event => {
56 | event.preventDefault(); // prevent page refresh
57 | push(to);
58 | };
59 |
60 | return (
61 |
62 | {children}
63 |
64 | );
65 | }
66 |
67 | export { Router, Route, Link };
68 |
--------------------------------------------------------------------------------
/subjects/18-Mini-Router/solution/mini-router.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { createHashHistory } from "history";
4 |
5 | /*
6 | How to use the history library:
7 |
8 | // read the current URL
9 | history.location
10 |
11 | // listen for changes to the URL
12 | history.listen(() => {
13 | history.location // is now different
14 | })
15 |
16 | // change the URL
17 | history.push('/something')
18 | */
19 |
20 | const RouterContext = React.createContext();
21 |
22 | class Router extends React.Component {
23 | history = createHashHistory();
24 |
25 | state = {
26 | location: this.history.location
27 | };
28 |
29 | componentDidMount() {
30 | this.history.listen(location => {
31 | this.setState({ location });
32 | });
33 | }
34 |
35 | handlePush = to => {
36 | this.history.push(to);
37 | };
38 |
39 | render() {
40 | return (
41 |
45 | );
46 | }
47 | }
48 |
49 | class Route extends React.Component {
50 | render() {
51 | return (
52 |
53 | {router => {
54 | const { path, render, component: Component } = this.props;
55 |
56 | if (router.location.pathname.startsWith(path)) {
57 | if (render) return render();
58 | if (Component) return ;
59 | }
60 |
61 | return null;
62 | }}
63 |
64 | );
65 | }
66 | }
67 |
68 | class Link extends React.Component {
69 | handleClick = (event, router) => {
70 | event.preventDefault();
71 | router.push(this.props.to);
72 | };
73 |
74 | render() {
75 | return (
76 |
77 | {router => (
78 | this.handleClick(event, router)}
81 | >
82 | {this.props.children}
83 |
84 | )}
85 |
86 | );
87 | }
88 | }
89 |
90 | export { Router, Route, Link };
91 |
--------------------------------------------------------------------------------
/subjects/19-Mini-Redux/exercise.js:
--------------------------------------------------------------------------------
1 | ////////////////////////////////////////////////////////////////////////////////
2 | // Exercise:
3 | //
4 | // Implement the React bindings for the Redux state manager using context and
5 | // a higher-order component.
6 | //
7 | // 1. Implement to make the store accessible on context to the rest
8 | // of the components rendered below it
9 | // 2. Implement `connect`. It should:
10 | // a) Return a function that takes a component
11 | // b) The new function should return a new component that wraps the component
12 | // passed to it
13 | // c) The new component, when rendered, will pass state from
14 | // the store as props to your App component. You'll use the function
15 | // passed to `connect` to map store state to component props
16 | import React from "react";
17 | import ReactDOM from "react-dom";
18 |
19 | import createStore from "./exercise/mini-redux/createStore";
20 | import Provider from "./exercise/mini-redux/Provider";
21 |
22 | import App from "./exercise/App";
23 |
24 | const store = createStore((state = 0, action) => {
25 | if (action.type === "INCREMENT") {
26 | return state + 1;
27 | } else if (action.type === "DECREMENT") {
28 | return state - 1;
29 | } else {
30 | return state;
31 | }
32 | });
33 |
34 | ReactDOM.render(
35 |
36 |
37 | ,
38 | document.getElementById("app")
39 | );
40 |
--------------------------------------------------------------------------------
/subjects/19-Mini-Redux/exercise/App.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import connect from "./mini-redux/connect";
4 |
5 | class App extends React.Component {
6 | static defaultProps = {
7 | counter: "Missing the `counter` prop!"
8 | };
9 |
10 | increment = () => {
11 | this.props.dispatch({ type: "INCREMENT" });
12 | };
13 |
14 | decrement = () => {
15 | this.props.dispatch({ type: "DECREMENT" });
16 | };
17 |
18 | render() {
19 | return (
20 |
21 |
Mini Redux!
22 | Increment {" "}
23 | {this.props.counter}{" "}
24 | Decrement
25 |
26 | );
27 | }
28 | }
29 |
30 | export default connect(state => {
31 | return { counter: state };
32 | })(App);
33 |
--------------------------------------------------------------------------------
/subjects/19-Mini-Redux/exercise/mini-redux/Provider.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | class Provider extends React.Component {
4 | render() {
5 | return {this.props.children}
;
6 | }
7 | }
8 |
9 | export default Provider;
10 |
--------------------------------------------------------------------------------
/subjects/19-Mini-Redux/exercise/mini-redux/connect.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | function connect(mapStateToProps) {
4 | return Component => {
5 | return Component;
6 | };
7 | }
8 |
9 | export default connect;
10 |
--------------------------------------------------------------------------------
/subjects/19-Mini-Redux/exercise/mini-redux/createStore.js:
--------------------------------------------------------------------------------
1 | function createStore(reducer) {
2 | let state = reducer(undefined, { type: "@INIT" });
3 | let listeners = [];
4 |
5 | const getState = () => state;
6 |
7 | const dispatch = action => {
8 | state = reducer(state, action);
9 | listeners.forEach(listener => listener());
10 | };
11 |
12 | const subscribe = listener => {
13 | listeners.push(listener);
14 |
15 | return () => {
16 | listeners = listeners.filter(item => item !== listener);
17 | };
18 | };
19 |
20 | return {
21 | getState,
22 | dispatch,
23 | subscribe
24 | };
25 | }
26 |
27 | export default createStore;
28 |
--------------------------------------------------------------------------------
/subjects/19-Mini-Redux/solution.js:
--------------------------------------------------------------------------------
1 | ////////////////////////////////////////////////////////////////////////////////
2 | // Exercise:
3 | //
4 | // Implement the React bindings for the Redux state manager using context and
5 | // a higher-order component.
6 | //
7 | // 1. Implement to make the store accessible on context to the rest
8 | // of the components rendered below it
9 | // 2. Implement `connect`. It should:
10 | // a) Return a function that takes a component
11 | // b) The new function should return a new component that wraps the component
12 | // passed to it
13 | // c) The new component, when rendered, will pass state from
14 | // the store as props to your App component. You'll use the function
15 | // passed to `connect` to map store state to component props
16 | import React from "react";
17 | import ReactDOM from "react-dom";
18 |
19 | import createStore from "./solution/mini-redux/createStore";
20 | import Provider from "./solution/mini-redux/Provider";
21 |
22 | import App from "./solution/App";
23 |
24 | const store = createStore((state = 0, action) => {
25 | if (action.type === "INCREMENT") {
26 | return state + 1;
27 | } else if (action.type === "DECREMENT") {
28 | return state - 1;
29 | } else {
30 | return state;
31 | }
32 | });
33 |
34 | ReactDOM.render(
35 |
36 |
37 | ,
38 | document.getElementById("app")
39 | );
40 |
--------------------------------------------------------------------------------
/subjects/19-Mini-Redux/solution/App.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import connect from "./mini-redux/connect";
4 |
5 | class App extends React.Component {
6 | increment = () => {
7 | this.props.dispatch({ type: "INCREMENT" });
8 | };
9 |
10 | decrement = () => {
11 | this.props.dispatch({ type: "DECREMENT" });
12 | };
13 |
14 | render() {
15 | return (
16 |
17 |
Mini Redux!
18 | Increment {" "}
19 | {this.props.counter}{" "}
20 | Decrement
21 |
22 | );
23 | }
24 | }
25 |
26 | export default connect(state => {
27 | return { counter: state };
28 | })(App);
29 |
--------------------------------------------------------------------------------
/subjects/19-Mini-Redux/solution/mini-redux/Provider.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import ReduxContext from "./ReduxContext";
4 |
5 | class Provider extends React.Component {
6 | state = {
7 | storeState: this.props.store.getState()
8 | };
9 |
10 | componentDidMount() {
11 | this.unsub = this.props.store.subscribe(() => {
12 | this.setState({
13 | storeState: this.props.store.getState()
14 | });
15 | });
16 | }
17 |
18 | componentWillUnmount() {
19 | this.unsub();
20 | }
21 |
22 | handleDispatch = action => {
23 | this.props.store.dispatch(action);
24 | };
25 |
26 | render() {
27 | return (
28 |
34 | {this.props.children}
35 |
36 | );
37 | }
38 | }
39 |
40 | export default Provider;
41 |
--------------------------------------------------------------------------------
/subjects/19-Mini-Redux/solution/mini-redux/ReduxContext.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const ReduxContext = React.createContext();
4 |
5 | export default ReduxContext;
6 |
--------------------------------------------------------------------------------
/subjects/19-Mini-Redux/solution/mini-redux/connect.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import ReduxContext from "./ReduxContext";
4 |
5 | function connect(mapStateToProps) {
6 | return Component => {
7 | return props => {
8 | return (
9 |
10 | {redux => (
11 |
16 | )}
17 |
18 | );
19 | };
20 | };
21 | }
22 |
23 | export default connect;
24 |
--------------------------------------------------------------------------------
/subjects/19-Mini-Redux/solution/mini-redux/createStore.js:
--------------------------------------------------------------------------------
1 | function createStore(reducer) {
2 | let state = reducer(undefined, { type: "@INIT" });
3 | let listeners = [];
4 |
5 | const getState = () => state;
6 |
7 | const dispatch = action => {
8 | state = reducer(state, action);
9 | listeners.forEach(listener => listener());
10 | };
11 |
12 | const subscribe = listener => {
13 | listeners.push(listener);
14 |
15 | return () => {
16 | listeners = listeners.filter(item => item !== listener);
17 | };
18 | };
19 |
20 | return {
21 | getState,
22 | dispatch,
23 | subscribe
24 | };
25 | }
26 |
27 | export default createStore;
28 |
--------------------------------------------------------------------------------
/subjects/20-Select/exercise.js:
--------------------------------------------------------------------------------
1 | ////////////////////////////////////////////////////////////////////////////////
2 | // Exercise:
3 | //
4 | // Make this work like a normal box!
5 | ////////////////////////////////////////////////////////////////////////////////
6 | import "./styles.css";
7 |
8 | import React, { useState, useEffect } from "react";
9 | import ReactDOM from "react-dom";
10 |
11 | function Select({ children }) {
12 | return (
13 |
14 |
15 | label ▾
16 |
17 |
{children}
18 |
19 | );
20 | }
21 |
22 | function Option({ children }) {
23 | return {children}
;
24 | }
25 |
26 | function App() {
27 | const [selectValue, setSelectValue] = useState("dosa");
28 |
29 | function setToMintChutney() {
30 | setSelectValue("mint-chutney");
31 | }
32 |
33 | return (
34 |
35 |
Select
36 |
37 |
Uncontrolled
38 |
39 |
40 | Tikka Masala
41 | Tandoori Chicken
42 | Dosa
43 | Mint Chutney
44 |
45 |
46 |
Controlled
47 |
48 |
Current value: {selectValue}
49 |
50 | Set to Mint Chutney
51 |
52 |
53 |
54 | Tikka Masala
55 | Tandoori Chicken
56 | Dosa
57 | Mint Chutney
58 |
59 |
60 | );
61 | }
62 |
63 | ReactDOM.render( , document.getElementById("app"));
64 |
--------------------------------------------------------------------------------
/subjects/20-Select/solution.js:
--------------------------------------------------------------------------------
1 | ////////////////////////////////////////////////////////////////////////////////
2 | // Exercise:
3 | //
4 | // Make this work like a normal box!
5 | ////////////////////////////////////////////////////////////////////////////////
6 | import "./styles.css";
7 |
8 | import React, { useState, useEffect } from "react";
9 | import ReactDOM from "react-dom";
10 |
11 | function Select({ children, defaultValue, value, onChange }) {
12 | const [showOptions, setShowOptions] = useState(false);
13 | const [currentValue, setCurrentValue] = useState(defaultValue);
14 |
15 | function toggleOptions() {
16 | setShowOptions(!showOptions);
17 | }
18 |
19 | const isControlled = value != null;
20 | const displayValue = isControlled ? value : currentValue;
21 |
22 | function selectValue(value) {
23 | if (isControlled) {
24 | onChange(value);
25 | } else {
26 | setCurrentValue(value);
27 |
28 | if (onChange) {
29 | onChange(value);
30 | }
31 | }
32 | }
33 |
34 | let label;
35 | React.Children.forEach(children, child => {
36 | if (child.props.value === displayValue) {
37 | label = child.props.children;
38 | }
39 | });
40 |
41 | useEffect(() => {
42 | if (isControlled && !onChange) {
43 | console.warn(
44 | "You rendered a with a `value` prop but no `onChange`, so it will be read-only..."
45 | );
46 | }
47 | }, []);
48 |
49 | return (
50 |
51 |
52 | {label} ▾
53 |
54 | {showOptions && (
55 |
56 | {React.Children.map(children, child =>
57 | React.cloneElement(child, {
58 | onSelect: () => selectValue(child.props.value)
59 | })
60 | )}
61 |
62 | )}
63 |
64 | );
65 | }
66 |
67 | function Option({ children, onSelect }) {
68 | return (
69 |
70 | {children}
71 |
72 | );
73 | }
74 |
75 | function App() {
76 | const [selectValue, setSelectValue] = useState("dosa");
77 |
78 | function setToMintChutney() {
79 | setSelectValue("mint-chutney");
80 | }
81 |
82 | return (
83 |
84 |
Select
85 |
86 |
Uncontrolled
87 |
88 |
89 | Tikka Masala
90 | Tandoori Chicken
91 | Dosa
92 | Mint Chutney
93 |
94 |
95 |
Controlled
96 |
97 |
Current value: {selectValue}
98 |
99 | Set to Mint Chutney
100 |
101 |
102 |
103 | Tikka Masala
104 | Tandoori Chicken
105 | Dosa
106 | Mint Chutney
107 |
108 |
109 | );
110 | }
111 |
112 | ReactDOM.render( , document.getElementById("app"));
113 |
--------------------------------------------------------------------------------
/subjects/20-Select/styles.css:
--------------------------------------------------------------------------------
1 | .select {
2 | border: 1px solid #ccc;
3 | display: inline-block;
4 | margin: 4px;
5 | cursor: pointer;
6 | }
7 |
8 | .label {
9 | padding: 4px;
10 | }
11 |
12 | .arrow {
13 | float: right;
14 | padding-left: 4;
15 | }
16 |
17 | .options {
18 | position: absolute;
19 | background: #fff;
20 | border: 1px solid #ccc;
21 | }
22 |
23 | .option {
24 | padding: 4px;
25 | }
26 |
27 | .option:hover {
28 | background: #eee;
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/subjects/assert.js:
--------------------------------------------------------------------------------
1 | function assert(pass, description) {
2 | if (pass) {
3 | console.log("%c✔︎ ok", "color: green", description);
4 | } else {
5 | console.assert(pass, description);
6 | }
7 | }
8 |
9 | export default assert;
10 |
--------------------------------------------------------------------------------
/subjects/index.js:
--------------------------------------------------------------------------------
1 | import "./styles.css";
2 |
3 | import React from "react";
4 | import ReactDOM from "react-dom";
5 |
6 | import logoURL from "./logo.png";
7 |
8 | function Index() {
9 | const subjects = (window.__DATA__ || {}).subjects || [];
10 |
11 | return (
12 |
13 |
18 |
23 |
24 | {subjects.map((subject, index) => (
25 |
26 | {subject.number}
27 |
28 | {subject.lecture ? (
29 |
33 | {subject.name}
34 |
35 | ) : (
36 | subject.name
37 | )}
38 |
39 |
40 | {subject.exercise && (
41 |
45 | exercise
46 |
47 | )}
48 |
49 |
50 | {subject.solution && (
51 |
55 | solution
56 |
57 | )}
58 |
59 |
60 | ))}
61 |
62 |
63 |
70 |
71 | );
72 | }
73 |
74 | ReactDOM.render( , document.getElementById("app"));
75 |
--------------------------------------------------------------------------------
/subjects/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ReactTraining/classic-react-workshop/251d8ba4c6f1da0c5815947fdb40bc32453d0443/subjects/logo.png
--------------------------------------------------------------------------------
/subjects/styles.css:
--------------------------------------------------------------------------------
1 | .index-header {
2 | max-width: 800px;
3 | margin: 30px auto 40px;
4 | text-align: center;
5 | }
6 | .index-header img {
7 | width: 300px;
8 | }
9 | @media (max-width: 600px) {
10 | .index-header {
11 | margin: 10px auto 20px;
12 | }
13 | }
14 |
15 | .index-subjectsTable {
16 | width: 100%;
17 | max-width: 800px;
18 | margin: 0 auto;
19 | }
20 | .index-subjectsTable a:link,
21 | .index-subjectsTable a:visited {
22 | color: blue;
23 | }
24 | .index-subjectsTable tr:nth-child(even) {
25 | background: #eef;
26 | }
27 | .index-subjectsTable td {
28 | padding: 10px;
29 | text-align: left;
30 | }
31 |
32 | .index-subjectNumber {
33 | font-size: 1.5em;
34 | color: #aaa;
35 | }
36 |
37 | .index-lecture {
38 | font-size: 1.5em;
39 | }
40 | .index-lecture a:link,
41 | .index-lecture a:visited {
42 | color: black;
43 | }
44 |
45 | .index-exercise {
46 | padding-left: 20px;
47 | }
48 |
49 | .index-footer {
50 | max-width: 800px;
51 | margin: 40px auto 60px;
52 | text-align: center;
53 | font-size: 0.8em;
54 | color: #ccc;
55 | }
56 | .index-footer a:link,
57 | .index-footer a:visited {
58 | color: #ccc;
59 | }
60 | @media (max-width: 600px) {
61 | .index-footer {
62 | margin: 20px auto 40px;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const path = require("path");
3 | const webpack = require("webpack");
4 |
5 | const subjectsDir = path.join(__dirname, "subjects");
6 | const subjectDirs = fs
7 | .readdirSync(subjectsDir)
8 | .map(file => path.join(subjectsDir, file))
9 | .filter(file => fs.lstatSync(file).isDirectory());
10 |
11 | module.exports = {
12 | devtool: "source-map",
13 | mode: "development",
14 |
15 | entry: subjectDirs.reduce(
16 | (chunks, dir) => {
17 | const base = path.basename(dir);
18 |
19 | ["lecture", "exercise", "solution"].forEach(name => {
20 | const file = path.join(dir, `${name}.js`);
21 |
22 | if (fs.existsSync(file)) {
23 | chunks[`${base}-${name}`] = file;
24 | }
25 | });
26 |
27 | return chunks;
28 | },
29 | {
30 | shared: ["react", "react-dom"],
31 | index: path.join(subjectsDir, "index.js")
32 | }
33 | ),
34 |
35 | output: {
36 | path: __dirname + "public",
37 | filename: "[name].js",
38 | chunkFilename: "[id].chunk.js",
39 | publicPath: "/"
40 | },
41 |
42 | module: {
43 | rules: [
44 | {
45 | test: /\.js$/,
46 | exclude: /node_modules|mocha-browser\.js/,
47 | loader: "babel-loader"
48 | },
49 | { test: /\.css$/, use: ["style-loader", "css-loader"] },
50 | { test: /\.(ttf|eot|svg|png|jpg)$/, loader: "file-loader" },
51 | {
52 | test: /\.woff(2)?$/,
53 | loader: "url-loader?limit=10000&mimetype=application/font-woff"
54 | },
55 | {
56 | test: require.resolve("jquery"),
57 | loader: "expose-loader?jQuery"
58 | }
59 | ]
60 | },
61 |
62 | devServer: {
63 | quiet: false,
64 | noInfo: false,
65 | overlay: true,
66 | historyApiFallback: {
67 | rewrites: []
68 | },
69 | stats: {
70 | // Config for minimal console.log mess.
71 | assets: true,
72 | colors: true,
73 | version: true,
74 | hash: true,
75 | timings: true,
76 | chunks: false,
77 | chunkModules: false
78 | }
79 | },
80 |
81 | optimization: {
82 | splitChunks: {
83 | name: "shared"
84 | }
85 | }
86 | };
87 |
--------------------------------------------------------------------------------