diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5447c55
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,18 @@
+# See https://help.github.com/ignore-files/ for more about ignoring files.
+
+# dependencies
+node_modules
+
+# testing
+coverage
+
+# production
+build
+
+# misc
+.DS_Store
+.env
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..0891e90
--- /dev/null
+++ b/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "foobar",
+ "version": "0.1.0",
+ "private": true,
+ "devDependencies": {
+ "react-scripts": "0.9.5"
+ },
+ "dependencies": {
+ "prop-types": "^15.5.8",
+ "react": "^15.5.4",
+ "react-dom": "^15.5.4",
+ "react-router": "^4.1.1",
+ "react-router-dom": "^4.1.1"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test --env=jsdom",
+ "eject": "react-scripts eject"
+ }
+}
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000..5c125de
Binary files /dev/null and b/public/favicon.ico differ
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..aab5e3b
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
diff --git a/src/CompleteProps/User.js b/src/CompleteProps/User.js
new file mode 100644
index 0000000..4842c6e
--- /dev/null
+++ b/src/CompleteProps/User.js
@@ -0,0 +1,17 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+
+const User = (props) => (
+
+ {props.preferredName || props.name}: {props.email}
+
+)
+
+// https://facebook.github.io/react/docs/typechecking-with-proptypes.html
+User.propTypes = {
+ name: PropTypes.string.isRequired,
+ email: PropTypes.string.isRequired,
+ preferredName: PropTypes.string
+}
+
+export default User
diff --git a/src/CompleteProps/UserList.js b/src/CompleteProps/UserList.js
new file mode 100644
index 0000000..c16297a
--- /dev/null
+++ b/src/CompleteProps/UserList.js
@@ -0,0 +1,20 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import User from './User'
+
+const UserList = (props) => (
+
+ {props.users.map((user) => (
+
+
+
+ ))}
+
+)
+
+// https://facebook.github.io/react/docs/typechecking-with-proptypes.html
+UserList.propTypes = {
+ users: PropTypes.array.isRequired
+}
+
+export default UserList
diff --git a/src/CompleteProps/index.js b/src/CompleteProps/index.js
new file mode 100644
index 0000000..75ce057
--- /dev/null
+++ b/src/CompleteProps/index.js
@@ -0,0 +1,16 @@
+import React from 'react'
+import UserList from './UserList'
+
+const users = [
+ { name: 'Michael', preferredName: 'Mike', email: 'mike@mikesmart.co.uk' },
+ { name: 'Hugo', email: 'hugo.giraudel@gmail.com' }
+]
+
+const UserIndex = () => (
+
+
Users:
+
+
+)
+
+export default UserIndex
diff --git a/src/Forms/container.js b/src/Forms/container.js
new file mode 100644
index 0000000..327b266
--- /dev/null
+++ b/src/Forms/container.js
@@ -0,0 +1,30 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import UserList from '../CompleteProps/UserList'
+
+class Search extends React.Component {
+ static propTypes = {
+ users: PropTypes.arrayOf(PropTypes.object).isRequired,
+ search: PropTypes.string
+ }
+
+ getFilteredUsers = () => {
+ if (!this.props.search) {
+ return this.props.users
+ }
+
+ return this.props.users.filter(this.isUserMatching)
+ }
+
+ isUserMatching = (user) => {
+ return user.name.toLowerCase().indexOf(this.props.search.toLowerCase()) > -1
+ }
+
+ render () {
+ return (
+
+ )
+ }
+}
+
+export default Search
diff --git a/src/Forms/data.json b/src/Forms/data.json
new file mode 100644
index 0000000..b6b49a9
--- /dev/null
+++ b/src/Forms/data.json
@@ -0,0 +1,363 @@
+[
+ {
+ "id": "59076466809fe801e7797e0f",
+ "name": "Dillon",
+ "email": "dillonlevy@devit.com",
+ "preferredName": "Dil"
+ },
+ {
+ "id": "590764669327dac55f7cb30a",
+ "name": "Harmon",
+ "email": "harmonlevy@devit.com",
+ "preferredName": "Har"
+ },
+ {
+ "id": "5907646655d0a24dab735af4",
+ "name": "Randolph",
+ "email": "randolphlevy@devit.com",
+ "preferredName": "Ran"
+ },
+ {
+ "id": "590764662c63becda266b333",
+ "name": "Moreno",
+ "email": "morenolevy@devit.com",
+ "preferredName": "Mor"
+ },
+ {
+ "id": "590764664a1536562d34a689",
+ "name": "Annmarie",
+ "email": "annmarielevy@devit.com",
+ "preferredName": "Ann"
+ },
+ {
+ "id": "5907646626d66705fdf1fa6a",
+ "name": "Alfreda",
+ "email": "alfredalevy@devit.com",
+ "preferredName": "Alf"
+ },
+ {
+ "id": "590764660e3f274f81c57736",
+ "name": "Savage",
+ "email": "savagelevy@devit.com",
+ "preferredName": "Sav"
+ },
+ {
+ "id": "590764667e35768cac45b7fe",
+ "name": "Rita",
+ "email": "ritalevy@devit.com"
+ },
+ {
+ "id": "59076466b8322c086ba4f323",
+ "name": "Calderon",
+ "email": "calderonlevy@devit.com",
+ "preferredName": "Cal"
+ },
+ {
+ "id": "5907646641df9a12a2d1aa9d",
+ "name": "Reid",
+ "email": "reidlevy@devit.com"
+ },
+ {
+ "id": "59076466071b5dbf7674cfbe",
+ "name": "Lynn",
+ "email": "lynnlevy@devit.com"
+ },
+ {
+ "id": "590764660b5a81c8248aceea",
+ "name": "Evangelina",
+ "email": "evangelinalevy@devit.com",
+ "preferredName": "Eva"
+ },
+ {
+ "id": "590764663d1d79db2415284e",
+ "name": "Hawkins",
+ "email": "hawkinslevy@devit.com",
+ "preferredName": "Haw"
+ },
+ {
+ "id": "59076466a1e99271c54adc4f",
+ "name": "Willis",
+ "email": "willislevy@devit.com",
+ "preferredName": "Wil"
+ },
+ {
+ "id": "590764664815382df91e95a0",
+ "name": "Eaton",
+ "email": "eatonlevy@devit.com"
+ },
+ {
+ "id": "59076466b33b8b7a140e5a72",
+ "name": "Frye",
+ "email": "fryelevy@devit.com"
+ },
+ {
+ "id": "590764669b82ce873d7c4a54",
+ "name": "Odom",
+ "email": "odomlevy@devit.com"
+ },
+ {
+ "id": "5907646680b6d535a8ed0478",
+ "name": "Peterson",
+ "email": "petersonlevy@devit.com",
+ "preferredName": "Pet"
+ },
+ {
+ "id": "59076466de0401403df67b15",
+ "name": "Mays",
+ "email": "mayslevy@devit.com"
+ },
+ {
+ "id": "590764663a1c992d567aacc3",
+ "name": "Jimenez",
+ "email": "jimenezlevy@devit.com",
+ "preferredName": "Jim"
+ },
+ {
+ "id": "59076466add165939e7c84ed",
+ "name": "Boyd",
+ "email": "boydlevy@devit.com"
+ },
+ {
+ "id": "59076466311639aca415f399",
+ "name": "Lottie",
+ "email": "lottielevy@devit.com",
+ "preferredName": "Lot"
+ },
+ {
+ "id": "5907646603e11d0b93fb1a33",
+ "name": "Cornelia",
+ "email": "cornelialevy@devit.com",
+ "preferredName": "Cor"
+ },
+ {
+ "id": "59076466ccb7cf217400cff4",
+ "name": "Gracie",
+ "email": "gracielevy@devit.com",
+ "preferredName": "Gra"
+ },
+ {
+ "id": "59076466f78544075058be06",
+ "name": "Stacey",
+ "email": "staceylevy@devit.com",
+ "preferredName": "Sta"
+ },
+ {
+ "id": "59076466721c31e7bc0057eb",
+ "name": "Rhoda",
+ "email": "rhodalevy@devit.com"
+ },
+ {
+ "id": "590764668c73bd15506c912b",
+ "name": "Riddle",
+ "email": "riddlelevy@devit.com",
+ "preferredName": "Rid"
+ },
+ {
+ "id": "590764662fd1c144ec145116",
+ "name": "Clayton",
+ "email": "claytonlevy@devit.com",
+ "preferredName": "Cla"
+ },
+ {
+ "id": "5907646601b148f674289116",
+ "name": "Jocelyn",
+ "email": "jocelynlevy@devit.com",
+ "preferredName": "Joc"
+ },
+ {
+ "id": "5907646673e1949f5b7dafd7",
+ "name": "Puckett",
+ "email": "puckettlevy@devit.com",
+ "preferredName": "Puc"
+ },
+ {
+ "id": "590764668c3d40d54fdd0b0b",
+ "name": "Leona",
+ "email": "leonalevy@devit.com"
+ },
+ {
+ "id": "59076466da0bb7c7611ffa79",
+ "name": "Mai",
+ "email": "mailevy@devit.com"
+ },
+ {
+ "id": "5907646647b06bfd655c5965",
+ "name": "Schneider",
+ "email": "schneiderlevy@devit.com",
+ "preferredName": "Sch"
+ },
+ {
+ "id": "59076466aac4f06526149dd1",
+ "name": "Lupe",
+ "email": "lupelevy@devit.com"
+ },
+ {
+ "id": "5907646668cc0cc8b41134c2",
+ "name": "Ruiz",
+ "email": "ruizlevy@devit.com"
+ },
+ {
+ "id": "59076466efd2214a723d2de9",
+ "name": "Walters",
+ "email": "walterslevy@devit.com",
+ "preferredName": "Wal"
+ },
+ {
+ "id": "5907646643217651d903e376",
+ "name": "Blake",
+ "email": "blakelevy@devit.com"
+ },
+ {
+ "id": "590764664c78d611f4466fe8",
+ "name": "Whitehead",
+ "email": "whiteheadlevy@devit.com",
+ "preferredName": "Whi"
+ },
+ {
+ "id": "590764664e6ade01a4c16d39",
+ "name": "Doyle",
+ "email": "doylelevy@devit.com"
+ },
+ {
+ "id": "59076466820f73920ff64ae7",
+ "name": "Porter",
+ "email": "porterlevy@devit.com",
+ "preferredName": "Por"
+ },
+ {
+ "id": "590764663aad1dc6e5125745",
+ "name": "Gilliam",
+ "email": "gilliamlevy@devit.com",
+ "preferredName": "Gil"
+ },
+ {
+ "id": "59076466c5581dfc8f49b9c8",
+ "name": "Katheryn",
+ "email": "katherynlevy@devit.com",
+ "preferredName": "Kat"
+ },
+ {
+ "id": "59076466b1de9d6c39ce906f",
+ "name": "Sherrie",
+ "email": "sherrielevy@devit.com",
+ "preferredName": "She"
+ },
+ {
+ "id": "590764667d6d4989c0a37bf5",
+ "name": "Karina",
+ "email": "karinalevy@devit.com",
+ "preferredName": "Kar"
+ },
+ {
+ "id": "5907646660e24ef89eb7d686",
+ "name": "Lilly",
+ "email": "lillylevy@devit.com"
+ },
+ {
+ "id": "59076466b069b0bfdf48e64b",
+ "name": "Dawson",
+ "email": "dawsonlevy@devit.com",
+ "preferredName": "Daw"
+ },
+ {
+ "id": "59076466d4cb5131f7768df6",
+ "name": "Verna",
+ "email": "vernalevy@devit.com"
+ },
+ {
+ "id": "59076466dee5c3ce0ee737c3",
+ "name": "Dollie",
+ "email": "dollielevy@devit.com",
+ "preferredName": "Dol"
+ },
+ {
+ "id": "59076466aafed31706977002",
+ "name": "Hebert",
+ "email": "hebertlevy@devit.com",
+ "preferredName": "Heb"
+ },
+ {
+ "id": "5907646674e2dadd6ab3409c",
+ "name": "Madeline",
+ "email": "madelinelevy@devit.com",
+ "preferredName": "Mad"
+ },
+ {
+ "id": "5907646684cad8d00598db96",
+ "name": "Crystal",
+ "email": "crystallevy@devit.com",
+ "preferredName": "Cry"
+ },
+ {
+ "id": "5907646685936858fd819ca9",
+ "name": "Clements",
+ "email": "clementslevy@devit.com",
+ "preferredName": "Cle"
+ },
+ {
+ "id": "5907646649705e762c467a0b",
+ "name": "Kayla",
+ "email": "kaylalevy@devit.com"
+ },
+ {
+ "id": "5907646697faed3ff14a58f7",
+ "name": "Teri",
+ "email": "terilevy@devit.com"
+ },
+ {
+ "id": "590764664fc3e30c5c3100ba",
+ "name": "Cote",
+ "email": "cotelevy@devit.com"
+ },
+ {
+ "id": "59076466d4549311ae634858",
+ "name": "Gwendolyn",
+ "email": "gwendolynlevy@devit.com",
+ "preferredName": "Gwe"
+ },
+ {
+ "id": "59076466fe6d792ce337ae1e",
+ "name": "Marsh",
+ "email": "marshlevy@devit.com"
+ },
+ {
+ "id": "59076466219e119c74a71e43",
+ "name": "Farley",
+ "email": "farleylevy@devit.com",
+ "preferredName": "Far"
+ },
+ {
+ "id": "5907646648720a7dfdbbb875",
+ "name": "Renee",
+ "email": "reneelevy@devit.com"
+ },
+ {
+ "id": "59076466d6c22ac851510cf7",
+ "name": "Rosanna",
+ "email": "rosannalevy@devit.com",
+ "preferredName": "Ros"
+ },
+ {
+ "id": "5907646608c0655a72a57916",
+ "name": "Walton",
+ "email": "waltonlevy@devit.com",
+ "preferredName": "Wal"
+ },
+ {
+ "id": "590764667afde087e4d03bbf",
+ "name": "Romero",
+ "email": "romerolevy@devit.com",
+ "preferredName": "Rom"
+ },
+ {
+ "id": "590764667258d71a474bf55a",
+ "name": "Selma",
+ "email": "selmalevy@devit.com"
+ },
+ {
+ "id": "590764666011136e283266c0",
+ "name": "Stephenson",
+ "email": "stephensonlevy@devit.com",
+ "preferredName": "Ste"
+ }
+]
diff --git a/src/Forms/getSearchParams.js b/src/Forms/getSearchParams.js
new file mode 100644
index 0000000..c5ba319
--- /dev/null
+++ b/src/Forms/getSearchParams.js
@@ -0,0 +1,21 @@
+const getSearchParams = (searchString = '?') => {
+ // Remove question mark
+ // (e.g. `?foo=bar&baz=qux` -> `foo=bar&baz=qux`)
+ const searchItems = searchString.slice(1)
+
+ // Split variables
+ // (e.g. `foo=bar&baz=qux` -> [ 'foo=bar', 'baz=qux' ])
+ const searchPairs = searchItems.split('&')
+
+ // Construct an object from pairs
+ // (e.g. [ 'foo=bar', 'baz=qux' ] -> { foo: 'bar', baz: 'qux' })
+ const searchParams = searchPairs.reduce((params, pair) => {
+ const items = pair.split('=')
+ params[items[0]] = items[1]
+ return params
+ }, {})
+
+ return searchParams
+}
+
+export default getSearchParams
diff --git a/src/Forms/index.js b/src/Forms/index.js
new file mode 100644
index 0000000..03130d6
--- /dev/null
+++ b/src/Forms/index.js
@@ -0,0 +1,64 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import List from './container'
+import users from './data'
+import getSearchParams from './getSearchParams'
+
+class Search extends React.Component {
+ static contextTypes = {
+ router: PropTypes.object.isRequired
+ }
+
+ constructor (props) {
+ super(props)
+
+ const params = getSearchParams(this.props.location.search)
+
+ this.state = {
+ search: params.search
+ }
+ }
+
+ componentDidUpdate (prevProps) {
+ const prevParams = getSearchParams(prevProps.location.search)
+ const params = getSearchParams(this.props.location.search)
+ if (prevParams.search !== params.search) {
+ // console.log(prevParams.search, params.search)
+ // this.updateSearch(params.search)
+ }
+ }
+
+ updateSearch = (value) => {
+ this.context.router.history.push('?search=' + encodeURIComponent(value))
+ }
+
+ handleChange = (event) => {
+ this.setState({ search: event.target.value })
+ }
+
+ handleSubmit = (event) => {
+ event.preventDefault()
+ this.updateSearch(this.state.search)
+ }
+
+ render () {
+ return (
+
+
+
+
+ )
+ }
+}
+
+export default Search
diff --git a/src/HelloWorld/index.js b/src/HelloWorld/index.js
new file mode 100644
index 0000000..7c20ada
--- /dev/null
+++ b/src/HelloWorld/index.js
@@ -0,0 +1,5 @@
+import React from 'react'
+
+const HelloWorld = () => Hello world
+
+export default HelloWorld
diff --git a/src/HigherOrder/index.js b/src/HigherOrder/index.js
new file mode 100644
index 0000000..383c3a0
--- /dev/null
+++ b/src/HigherOrder/index.js
@@ -0,0 +1,11 @@
+import React from 'react'
+import Trackable from './trackable'
+
+const Page = (props) => (
+
+
Tasks (15 minutes):
+
Write a higher-order function that provides the viewport width to the wrapped component as a viewportWidth
property. Do not forget to unregister the listeners when/if component unmounts!
+
+)
+
+export default Trackable(Page)
diff --git a/src/HigherOrder/trackable.js b/src/HigherOrder/trackable.js
new file mode 100644
index 0000000..efc2695
--- /dev/null
+++ b/src/HigherOrder/trackable.js
@@ -0,0 +1,34 @@
+import React from 'react'
+
+const Trackable = (Component) => {
+ class TrackableComponent extends React.Component {
+ componentDidMount () {
+ console.log(`Component ${Component.name} mounted.`)
+ }
+
+ componentWillUnmount () {
+ console.log(`Component ${Component.name} will unmount.`)
+ }
+
+ handleClick = (event) => {
+ console.log(`Component ${Component.name} clicked.`)
+
+ if (typeof this.props.onClick === 'function') {
+ this.props.onClick(event)
+ }
+ }
+
+ render () {
+ return (
+
+ )
+ }
+ }
+
+ return TrackableComponent
+}
+
+export default Trackable
diff --git a/src/HigherOrder/viewport-width.js b/src/HigherOrder/viewport-width.js
new file mode 100644
index 0000000..4e3e390
--- /dev/null
+++ b/src/HigherOrder/viewport-width.js
@@ -0,0 +1,15 @@
+import React from 'react'
+
+const ViewportWidthProvider = (Component) => {
+ class ViewportWidth extends React.Component {
+ render () {
+ return (
+ // Replace `viewportWidth` prop value with the actual viewportWidth
+
+ )
+ }
+ }
+}
+
+
+export default ViewportWidthProvider
diff --git a/src/Home/index.js b/src/Home/index.js
new file mode 100644
index 0000000..f4460ed
--- /dev/null
+++ b/src/Home/index.js
@@ -0,0 +1,111 @@
+import React from 'react'
+import { Link } from 'react-router-dom'
+
+export default () => (
+
+
React is undefined
+
+
What is it, how does it work, what’s the point? (30 mins)
+
+
+
+ Why choose React, what does it help with?
+
+
+ Declarative
+ Library, not a framework
+ Rich third-party ecosystem
+
+
+
+ Reverse-tree based architecture
+
+
+ Root component
+ Sub-components…
+
+
+
+ Component based approach with props
+
+
+ Hello World (without props)
+
+
+ Hello World (with props)
+
+
+ Moar props
+
+
+
+
+ React component lifecycle
+
+
+ Stateful component
+
+
+
+
+ Higher Order Components
+
+
+ Higher-order components
+
+
+
+ Component state - container
+
+
+ https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0
+ More complex example, text filter component
+
+
+
+
Use this knowledge to build something (60 mins)
+
+
+ Simple index.html/CodePen, build a simple example, css block
+ CRA
+
+
+
--- break ---
+
+
Build systems
+
+
+ Webpack the basics
+ Webpack the complicated
+
+
+
Isomorphism
+
+
+ Server setup (ReactDOMServer, JSX in Node)
+ Caching?
+
+
+
Styling
+
+
+ CSS
+ Modules?
+ CSS in JS (Fela, styled-components)
+
+
+
--- break ---
+
+
Bonus content
+
+
+ Flux based state management
+ Router
+ Refs and third parties
+ Events (Keyboard Click Events)
+ Context - when to use it, and when really not to use it
+ Controlled components
+ Prop/State Hoisting (Data flow bottom to top)
+
+
+)
diff --git a/src/SimpleProps/index.js b/src/SimpleProps/index.js
new file mode 100644
index 0000000..9368832
--- /dev/null
+++ b/src/SimpleProps/index.js
@@ -0,0 +1,6 @@
+import React from 'react'
+
+const Hello = (props) => Hello {props.name}
+const HelloWithProps = (props) =>
+
+export default HelloWithProps
diff --git a/src/SimpleProps/moar.js b/src/SimpleProps/moar.js
new file mode 100644
index 0000000..9f079af
--- /dev/null
+++ b/src/SimpleProps/moar.js
@@ -0,0 +1,42 @@
+import React from 'react'
+import PropTypes 'prop-types'
+
+const User = (props) => (
+
+ {props.preferredName || props.name}: {props.email}
+
+)
+
+User.propTypes = {
+ name: PropTypes.string.isRequired,
+ email: PropTypes.string.isRequired,
+ preferredName: Proptypes.string
+}
+
+const UserList = (props) => (
+
+ {props.users.map((user) => (
+
+
+
+ ))}
+
+)
+
+UserList.propTypes = {
+ users: PropTypes.array.isRequired
+}
+
+const users = [
+ { name: 'Michael', preferredName: 'Mike', email: 'mike@mikesmart.co.uk' },
+ { name: 'Hugo', email: 'hugo.giraudel@gmail.com' }
+]
+
+const UserIndex = () => (
+
+
Users:
+
+
+)
+
+export default UserIndex
diff --git a/src/State/ask-before-unload.js b/src/State/ask-before-unload.js
new file mode 100644
index 0000000..24c5696
--- /dev/null
+++ b/src/State/ask-before-unload.js
@@ -0,0 +1,11 @@
+import React from 'react'
+
+class AskBeforeUnload extends React.Component {
+ render () {
+ return (
+ The browser should ask for confirmation before reloading the page.
+ )
+ }
+}
+
+export default AskBeforeUnload
diff --git a/src/State/index.js b/src/State/index.js
new file mode 100644
index 0000000..173a484
--- /dev/null
+++ b/src/State/index.js
@@ -0,0 +1,18 @@
+import Timer from './timer'
+import React from 'react'
+
+const Page = () => (
+
+
+
+
Pick a task below (15 minutes):
+
+ [Easy] Make the Timer
component accept an initial property to set the default value for seconds.
+ [Medium] Build a component that ask for confirmation before leaving the page.
+ [Medium] Display the live mouse position on the page.
+ [Hard] Find a simple third-party library and initialise it inside your component (e.g.: Flickity, a11y-dialog…).
+
+
+)
+
+export default Page
diff --git a/src/State/mouse-position.js b/src/State/mouse-position.js
new file mode 100644
index 0000000..6920857
--- /dev/null
+++ b/src/State/mouse-position.js
@@ -0,0 +1,12 @@
+import React from 'react'
+
+class MousePosition extends React.Component {
+ render () {
+ return (
+ // Adjust this to log the current mouse position on screen.
+ Mouse is currently at position X:Y.
+ )
+ }
+}
+
+export default MousePosition
diff --git a/src/State/timer.js b/src/State/timer.js
new file mode 100644
index 0000000..2f8bc4c
--- /dev/null
+++ b/src/State/timer.js
@@ -0,0 +1,37 @@
+import React from 'react'
+
+class Timer extends React.Component {
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ seconds: 0
+ }
+ }
+
+ componentWillMount () {
+ this.timer = setInterval(this.increment, 1000)
+ }
+
+ componentWillUnmount () {
+ clearInterval(this.timer)
+ }
+
+ increment = () => {
+ this.setState({
+ seconds: this.state.seconds + 1
+ })
+ }
+
+ pluraliseSeconds = () => {
+ return this.state.seconds === 1 ? 'second' : 'seconds'
+ }
+
+ render () {
+ return (
+ 👋🏻 I have been loaded for {this.state.seconds} {this.pluraliseSeconds()}.
+ )
+ }
+}
+
+export default Timer
diff --git a/src/__internals/Page/index.js b/src/__internals/Page/index.js
new file mode 100644
index 0000000..1c6f705
--- /dev/null
+++ b/src/__internals/Page/index.js
@@ -0,0 +1,10 @@
+import React from 'react'
+import './styles.css'
+
+const Page = (props) => (
+
+ {props.children}
+
+)
+
+export default Page
diff --git a/src/__internals/Page/styles.css b/src/__internals/Page/styles.css
new file mode 100644
index 0000000..6b7b190
--- /dev/null
+++ b/src/__internals/Page/styles.css
@@ -0,0 +1,8 @@
+.Page {
+ max-width: 800px;
+ width: 100%;
+ margin: 0 auto;
+ padding: 1em;
+ background-color: white;
+ height: 100%;
+}
diff --git a/src/__internals/Router/index.js b/src/__internals/Router/index.js
new file mode 100644
index 0000000..8cc57ed
--- /dev/null
+++ b/src/__internals/Router/index.js
@@ -0,0 +1,38 @@
+import React from 'react'
+import { Link, Route } from 'react-router-dom'
+import Home from '../../Home'
+import HelloWorld from '../../HelloWorld'
+import Props from '../../SimpleProps'
+import MoreProps from '../../CompleteProps'
+import State from '../../State'
+import HigherOrder from '../../HigherOrder'
+import Forms from '../../Forms'
+import './styles.css'
+
+class Navigation extends React.Component {
+ render() {
+ return (
+
+
+ Home
+ Hello World
+ Components with props
+ Components with more props
+ Stateful components
+ Higher-order functions
+ Forms
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+}
+
+export default Navigation
diff --git a/src/__internals/Router/styles.css b/src/__internals/Router/styles.css
new file mode 100644
index 0000000..8d2b8f5
--- /dev/null
+++ b/src/__internals/Router/styles.css
@@ -0,0 +1,13 @@
+.Navigation {
+ border-bottom: 1px solid silver;
+ padding-bottom: 1em;
+ margin: 0 -0.5ch;
+}
+
+.Link {
+ display: inline-block;
+ text-decoration: none;
+ color: deepskyblue;
+ border-bottom: 1px solid;
+ margin: 0 0.5ch;
+}
diff --git a/src/index.css b/src/index.css
new file mode 100644
index 0000000..e8c8e4a
--- /dev/null
+++ b/src/index.css
@@ -0,0 +1,23 @@
+html {
+ box-sizing: border-box;
+ background-color: #EFEFEF;
+ height: 100%;
+}
+
+* {
+ box-sizing: inherit;
+}
+
+body {
+ margin: 0;
+ height: 100%;
+ font: 120% / 1.4 sans-serif;
+}
+
+ul {
+ padding: 0 0 0 1em;
+}
+
+#root {
+ height: 100%;
+}
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 0000000..bd2ce3b
--- /dev/null
+++ b/src/index.js
@@ -0,0 +1,15 @@
+import React from 'react'
+import ReactDOM from 'react-dom'
+import { HashRouter } from 'react-router-dom'
+import Router from './__internals/Router'
+import Page from './__internals/Page'
+import './index.css'
+
+ReactDOM.render(
+
+
+
+
+ ,
+ document.getElementById('root')
+)