├── exercises ├── 4-lists-solution │ ├── .babelrc │ ├── .gitignore │ ├── .watchmanconfig │ ├── App.js │ ├── Header.js │ ├── ModifyObjectForm.js │ ├── README.md │ ├── Row.js │ ├── app.json │ ├── assets │ │ ├── icon.png │ │ └── splash.png │ └── package.json ├── 4-lists │ ├── .babelrc │ ├── .gitignore │ ├── .watchmanconfig │ ├── App.js │ ├── README.md │ ├── app.json │ ├── assets │ │ ├── icon.png │ │ └── splash.png │ └── package.json ├── 5-debugging │ ├── .babelrc │ ├── .gitignore │ ├── .watchmanconfig │ ├── App.js │ ├── README.md │ ├── app.json │ ├── assets │ │ ├── icon.png │ │ └── splash.png │ ├── bugs │ │ ├── 1.js │ │ ├── 2.js │ │ ├── 3.js │ │ └── 4.js │ ├── package.json │ └── solutions │ │ ├── 1.js │ │ ├── 2.js │ │ ├── 3.js │ │ └── 4.js └── 6-navigation │ ├── .babelrc │ ├── .gitignore │ ├── .watchmanconfig │ ├── App.js │ ├── README.md │ ├── app.json │ ├── assets │ ├── icon.png │ └── splash.png │ ├── bugs │ ├── 1.js │ ├── 2.js │ ├── 3.js │ └── 4.js │ ├── package.json │ └── solutions │ ├── 1.js │ ├── 2.js │ ├── 3.js │ └── 4.js ├── lecture ├── 0-js │ ├── 0-syntax.js │ ├── 1-types.js │ ├── 2-objects.js │ ├── 3-objectMutation.js │ ├── 4-scopeVariables.js │ └── 5-scopeFunctions.js ├── 1-js │ ├── 0-closureBug.js │ ├── 1-closureExample.js │ ├── 2-iife.js │ ├── 3-iifeClosure.js │ ├── 4-hof.js │ ├── 5-hang.js │ ├── 6-stack.js │ ├── 7-overflow.js │ ├── 8-async.js │ ├── 9-callbacks.js │ ├── a-callbackAuth.js │ ├── b-promises.js │ ├── c-promiseAuth.js │ ├── d-asyncAwaitAuth.js │ ├── e-this.js │ └── simple.html ├── 10-async-redux-tools │ ├── .babelrc │ ├── .eslintrc.yml │ ├── .gitignore │ ├── .watchmanconfig │ ├── AddContactForm.js │ ├── App.js │ ├── FlatListContacts.js │ ├── Row.js │ ├── ScrollViewContacts.js │ ├── SectionListContacts.js │ ├── api.js │ ├── app.json │ ├── assets │ │ ├── icon.png │ │ └── splash.png │ ├── authServer │ │ ├── .gitignore │ │ ├── README.md │ │ ├── index.js │ │ ├── package-lock.json │ │ └── package.json │ ├── contacts.js │ ├── package-lock.json │ ├── package.json │ ├── redux │ │ ├── actions.js │ │ ├── reducer.js │ │ └── store.js │ ├── screens │ │ ├── AddContactScreen.js │ │ ├── ContactDetailsScreen.js │ │ ├── ContactListScreen.js │ │ ├── LoginScreen.js │ │ └── SettingsScreen.js │ └── simpleRedux │ │ ├── reducer.js │ │ ├── store.js │ │ ├── store2.js │ │ └── store3.js ├── 11-performance │ ├── contacts │ │ ├── .babelrc │ │ ├── .eslintrc.yml │ │ ├── .gitignore │ │ ├── .watchmanconfig │ │ ├── App.js │ │ ├── FlatListContacts.js │ │ ├── PureButton.js │ │ ├── PureButtonScreen.js │ │ ├── Row.js │ │ ├── ScrollViewContacts.js │ │ ├── SectionListContacts.js │ │ ├── api.js │ │ ├── app.json │ │ ├── assets │ │ │ ├── icon.png │ │ │ └── splash.png │ │ ├── authServer │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── index.js │ │ │ ├── package-lock.json │ │ │ └── package.json │ │ ├── contacts.js │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── redux │ │ │ ├── actions.js │ │ │ ├── reducer.js │ │ │ └── store.js │ │ ├── screens │ │ │ ├── AddContactScreen.js │ │ │ ├── ContactDetailsScreen.js │ │ │ ├── ContactListScreen.js │ │ │ ├── LoginScreen.js │ │ │ └── SettingsScreen.js │ │ └── simpleRedux │ │ │ ├── reducer.js │ │ │ ├── store.js │ │ │ ├── store2.js │ │ │ └── store3.js │ └── pomodoro-timer │ │ ├── .babelrc │ │ ├── .gitignore │ │ ├── .watchmanconfig │ │ ├── App.js │ │ ├── ProgressBar.js │ │ ├── ProgressBarAnimated.js │ │ ├── app.json │ │ ├── assets │ │ ├── icon.png │ │ └── splash.png │ │ ├── components │ │ ├── Countdown.js │ │ ├── TimeInput.js │ │ ├── TimerToggleButton.js │ │ └── index.js │ │ ├── package-lock.json │ │ ├── package.json │ │ └── utils │ │ ├── Timer.js │ │ ├── index.js │ │ └── vibrate.js ├── 12-deploying-testing │ ├── .babelrc │ ├── .eslintrc.yml │ ├── .gitignore │ ├── .watchmanconfig │ ├── AddContactForm.js │ ├── App.js │ ├── FlatListContacts.js │ ├── Row.js │ ├── ScrollViewContacts.js │ ├── SectionListContacts.js │ ├── api.js │ ├── app.json │ ├── assets │ │ ├── icon.png │ │ └── splash.png │ ├── authServer │ │ ├── .gitignore │ │ ├── README.md │ │ ├── index.js │ │ ├── package-lock.json │ │ └── package.json │ ├── components │ │ ├── MyButton.js │ │ ├── MyButton.test.js │ │ └── __snapshots__ │ │ │ └── MyButton.test.js.snap │ ├── contacts.js │ ├── package-lock.json │ ├── package.json │ ├── redux │ │ ├── __snapshots__ │ │ │ ├── actions.test.js.snap │ │ │ └── reducer.test.js.snap │ │ ├── actions.js │ │ ├── actions.test.js │ │ ├── reducer.js │ │ ├── reducer.test.js │ │ └── store.js │ ├── screens │ │ ├── AddContactScreen.js │ │ ├── ContactDetailsScreen.js │ │ ├── ContactListScreen.js │ │ ├── LoginScreen.js │ │ └── SettingsScreen.js │ ├── simpleRedux │ │ ├── reducer.js │ │ ├── store.js │ │ ├── store2.js │ │ └── store3.js │ └── testing │ │ ├── sum.js │ │ └── sum.test.js ├── 2-react │ ├── 1-Set.js │ ├── 2-Set.js │ ├── 3-Todo.js │ ├── 4-imperativeGuitar.js │ ├── 5-declarativeGuitar.js │ ├── 6-imperativeSlide.js │ ├── 7-declarativeSlide.js │ ├── 8-slideshow.html │ ├── 9-slideshowComponents.js │ ├── a-props.js │ ├── b-state.js │ ├── todoApp0.js │ ├── todoApp1.js │ ├── todoApp2.js │ ├── todoApp3.js │ └── todoApp4-react.js ├── 3-react-native │ ├── 0-rnBlockJs.js │ ├── 1-todoApp-rn.js │ ├── 2-mount.js │ ├── 3-update.js │ ├── 4-unmount.js │ └── 5-expo-app │ │ ├── App.js │ │ └── Count.js ├── 4-lists-input │ ├── .babelrc │ ├── .gitignore │ ├── .watchmanconfig │ ├── AddContactForm.js │ ├── App.js │ ├── FlatListContacts.js │ ├── Row.js │ ├── ScrollViewContacts.js │ ├── SectionListContacts.js │ ├── app.json │ ├── assets │ │ ├── icon.png │ │ └── splash.png │ ├── contacts.js │ └── package.json ├── 5-input-debugging │ ├── .babelrc │ ├── .gitignore │ ├── .watchmanconfig │ ├── AddContactForm.js │ ├── App.js │ ├── FlatListContacts.js │ ├── Row.js │ ├── ScrollViewContacts.js │ ├── SectionListContacts.js │ ├── app.json │ ├── assets │ │ ├── icon.png │ │ └── splash.png │ ├── contacts.js │ └── package.json ├── 6-navigation │ ├── .babelrc │ ├── .gitignore │ ├── .watchmanconfig │ ├── AddContactForm.js │ ├── App.js │ ├── FlatListContacts.js │ ├── Row.js │ ├── ScrollViewContacts.js │ ├── SectionListContacts.js │ ├── app.json │ ├── assets │ │ ├── icon.png │ │ └── splash.png │ ├── contacts.js │ ├── examples │ │ ├── 0-Switch.js │ │ └── 1-Stack.js │ ├── package.json │ └── screens │ │ ├── AddContactScreen.js │ │ ├── ContactDetailsScreen.js │ │ ├── ContactListScreen.js │ │ ├── LoginScreen.js │ │ └── SettingsScreen.js ├── 7-data │ ├── .babelrc │ ├── .gitignore │ ├── .watchmanconfig │ ├── AddContactForm.js │ ├── App.js │ ├── FlatListContacts.js │ ├── Row.js │ ├── ScrollViewContacts.js │ ├── SectionListContacts.js │ ├── api.js │ ├── app.json │ ├── assets │ │ ├── icon.png │ │ └── splash.png │ ├── authServer │ │ ├── .gitignore │ │ ├── README.md │ │ ├── index.js │ │ ├── package-lock.json │ │ └── package.json │ ├── contacts.js │ ├── package.json │ └── screens │ │ ├── AddContactScreen.js │ │ ├── ContactDetailsScreen.js │ │ ├── ContactListScreen.js │ │ ├── LoginScreen.js │ │ └── SettingsScreen.js └── 9-redux │ ├── .babelrc │ ├── .gitignore │ ├── .watchmanconfig │ ├── AddContactForm.js │ ├── App.js │ ├── FlatListContacts.js │ ├── Row.js │ ├── ScrollViewContacts.js │ ├── SectionListContacts.js │ ├── api.js │ ├── app.json │ ├── assets │ ├── icon.png │ └── splash.png │ ├── authServer │ ├── .gitignore │ ├── README.md │ ├── index.js │ ├── package-lock.json │ └── package.json │ ├── contacts.js │ ├── package-lock.json │ ├── package.json │ ├── redux │ ├── actions.js │ ├── reducer.js │ └── store.js │ ├── screens │ ├── AddContactScreen.js │ ├── ContactDetailsScreen.js │ ├── ContactListScreen.js │ ├── LoginScreen.js │ └── SettingsScreen.js │ └── simpleRedux │ ├── reducer.js │ ├── store.js │ └── store2.js └── project0 └── solution ├── basic ├── index.html ├── script.js └── styles.css ├── basic2 ├── index.html ├── script.js └── styles.css └── withDelete2 ├── index.html ├── script.js └── styles.css /exercises/4-lists-solution/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["babel-preset-expo"], 3 | "env": { 4 | "development": { 5 | "plugins": ["transform-react-jsx-source"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /exercises/4-lists-solution/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | -------------------------------------------------------------------------------- /exercises/4-lists-solution/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /exercises/4-lists-solution/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {StyleSheet, Text, View} from 'react-native' 3 | import PropTypes from 'prop-types' 4 | 5 | const styles = StyleSheet.create({ 6 | container: { 7 | paddingLeft: 10, 8 | backgroundColor: '#aaa', 9 | }, 10 | text: { 11 | fontWeight: 'bold', 12 | }, 13 | }) 14 | 15 | const Header = props => ( 16 | 17 | {props.text} 18 | 19 | ) 20 | 21 | Header.propTypes = { 22 | text: PropTypes.string, 23 | } 24 | 25 | export default Header 26 | -------------------------------------------------------------------------------- /exercises/4-lists-solution/ModifyObjectForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Button, StyleSheet, TextInput, View} from 'react-native' 3 | import {Constants} from 'expo' 4 | import PropTypes from 'prop-types' 5 | 6 | const styles = StyleSheet.create({ 7 | container: { 8 | flex: 1, 9 | backgroundColor: '#fff', 10 | paddingTop: Constants.statusBarHeight, 11 | }, 12 | input: { 13 | borderWidth: 1, 14 | borderColor: 'black', 15 | minWidth: 100, 16 | marginTop: 20, 17 | marginHorizontal: 20, 18 | paddingHorizontal: 10, 19 | paddingVertical: 5, 20 | borderRadius: 3, 21 | }, 22 | }) 23 | 24 | export default class AddContactForm extends React.Component { 25 | state = { 26 | key: '', 27 | val: '', 28 | } 29 | 30 | handleKeyChange = key => { 31 | this.setState({key}) 32 | } 33 | 34 | handleValChange = val => { 35 | this.setState({val}) 36 | } 37 | 38 | handleSubmit = () => { 39 | this.props.onSubmit(this.state.key, this.state.val) 40 | } 41 | 42 | render() { 43 | return ( 44 | 45 | 51 | 57 | 29 | 30 |

{this.state.count}

31 | 32 | ) 33 | } 34 | } 35 | 36 | render(, document.getElementById('root')) 37 | -------------------------------------------------------------------------------- /lecture/2-react/todoApp0.js: -------------------------------------------------------------------------------- 1 | const list = document.getElementById('todo-list') 2 | const itemCountSpan = document.getElementById('item-count') 3 | const uncheckedCountSpan = document.getElementById('unchecked-count') 4 | 5 | //
  • 6 | // 7 | // 8 | // text 9 | //
  • 10 | 11 | function newTodo() { 12 | // get text 13 | // create li 14 | // create input checkbox 15 | // create button 16 | // create span 17 | // update counts 18 | } 19 | 20 | function deleteTodo() { 21 | // find the todo to delete 22 | // delete 23 | // update the counts 24 | } 25 | -------------------------------------------------------------------------------- /lecture/2-react/todoApp1.js: -------------------------------------------------------------------------------- 1 | const list = document.getElementById('todo-list') 2 | const itemCountSpan = document.getElementById('item-count') 3 | const uncheckedCountSpan = document.getElementById('unchecked-count') 4 | 5 | //
  • 6 | // 7 | // 8 | // text 9 | //
  • 10 | 11 | function createTodo() { 12 | // make li 13 | 14 | // make input 15 | 16 | // make button 17 | 18 | // make span 19 | } 20 | 21 | function newTodo() { 22 | // get text 23 | 24 | // invoke createTodo() 25 | 26 | // update counts 27 | 28 | // apend to list 29 | } 30 | 31 | function deleteTodo() { 32 | // find the todo to delete 33 | // remove 34 | // update counts 35 | } 36 | -------------------------------------------------------------------------------- /lecture/2-react/todoApp2.js: -------------------------------------------------------------------------------- 1 | const list = document.getElementById('todo-list') 2 | const itemCountSpan = document.getElementById('item-count') 3 | const uncheckedCountSpan = document.getElementById('unchecked-count') 4 | 5 | //
  • 6 | // 7 | // 8 | // text 9 | //
  • 10 | 11 | function createTodo() { 12 | const li = document.createElement('li') 13 | li.innerHTML = ` 14 | 15 | 16 | text 17 | ` 18 | return li 19 | } 20 | 21 | function newTodo() { 22 | // get text 23 | 24 | // invoke createTodo() 25 | 26 | // update counts 27 | 28 | // apend to list 29 | } 30 | 31 | function deleteTodo() { 32 | // find the todo to delete 33 | // remove 34 | // update counts 35 | } 36 | -------------------------------------------------------------------------------- /lecture/2-react/todoApp3.js: -------------------------------------------------------------------------------- 1 | // store todos in memory 2 | let todos = [] 3 | 4 | function renderTodo(todo) { 5 | // render a single todo 6 | } 7 | 8 | function render() { 9 | // render the todos in memory to the page 10 | list.innerHTML = '' 11 | todos.map(renderTodo).forEach(todo => list.appendChild(todo)) 12 | 13 | // update counts 14 | 15 | return false 16 | } 17 | 18 | function addTodo(name) { 19 | const todo = new Todo(name) 20 | todos.push(todo) 21 | return render() 22 | } 23 | 24 | function removeTodo(todo) { 25 | todos = todos.filter(t => t !== todo) 26 | return render() 27 | } 28 | -------------------------------------------------------------------------------- /lecture/3-react-native/0-rnBlockJs.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Button, Text, ScrollView, StyleSheet } from 'react-native'; 3 | import { Constants } from 'expo'; 4 | 5 | export default class App extends Component { 6 | state = { 7 | count: 0, 8 | } 9 | 10 | // EDIT: this wasn't in the original lecture code, but it makes it more obvious 11 | // that the JS thread gets locked 12 | componentDidMount() { 13 | setInterval(() => this.setState(prevState => ({count: prevState.count + 1})), 500) 14 | } 15 | 16 | blockJS() { 17 | console.log('blocking') 18 | const end = Date.now() + 5000 19 | while (Date.now() < end) {} 20 | console.log('unblocked') 21 | } 22 | 23 | render() { 24 | return ( 25 | 26 | 15 |
      16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /project0/solution/basic/script.js: -------------------------------------------------------------------------------- 1 | const classNames = { 2 | TODO_ITEM: 'todo-container', 3 | TODO_CHECKBOX: 'todo-checkbox', 4 | TODO_TEXT: 'todo-text', 5 | } 6 | 7 | const list = document.getElementById('todo-list') 8 | const itemCountDiv = document.getElementById('item-count') 9 | const uncheckedCountDiv = document.getElementById('unchecked-count') 10 | 11 | let itemCount = 0 12 | let uncheckedCount = 0 13 | 14 | function updateItemCount(difference) { 15 | itemCount += difference 16 | itemCountDiv.innerHTML = itemCount 17 | } 18 | 19 | function updateUncheckedCount(difference) { 20 | uncheckedCount += difference 21 | uncheckedCountDiv.innerHTML = uncheckedCount 22 | } 23 | 24 | function createTodo(name) { 25 | const checkbox = document.createElement('input') 26 | checkbox.className = classNames.TODO_CHECKBOX 27 | checkbox.type = 'checkbox' 28 | checkbox.onchange = toggleCheckbox 29 | 30 | const span = document.createElement('span') 31 | span.className = classNames.TODO_TEXT 32 | span.setAttribute('contenteditable', 'true') 33 | span.innerHTML = name || 'New TODO' 34 | 35 | const li = document.createElement('li') 36 | li.className = classNames.TODO_ITEM 37 | li.appendChild(checkbox) 38 | li.appendChild(span) 39 | 40 | return li 41 | } 42 | 43 | function addTodo(name) { 44 | const todo = createTodo(name) 45 | list.appendChild(todo) 46 | updateItemCount(1) 47 | updateUncheckedCount(1) 48 | } 49 | 50 | function toggleCheckbox() { 51 | if (this.checked) updateUncheckedCount(-1) 52 | else updateUncheckedCount(1) 53 | } 54 | -------------------------------------------------------------------------------- /project0/solution/basic/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html, body { 6 | background-color: #eee; 7 | margin: 0; 8 | padding: 0; 9 | } 10 | 11 | ul { 12 | margin: 0; 13 | padding: 0; 14 | list-style-type: none; 15 | } 16 | 17 | .center { 18 | align-self: center; 19 | } 20 | 21 | .flow-right { 22 | display: flex; 23 | justify-content: space-around; 24 | } 25 | 26 | .container { 27 | max-width: 800px; 28 | margin: 0 auto; 29 | padding: 10px; 30 | display: flex; 31 | flex-direction: column; 32 | background-color: white; 33 | height: 100vh; 34 | } 35 | 36 | .title, .controls, .button { 37 | flex: none; 38 | } 39 | 40 | .button { 41 | padding: 10px 20px; 42 | } 43 | 44 | .todo-list { 45 | flex: 1 1 0; 46 | margin-top: 20px; 47 | padding: 20px; 48 | overflow-y: auto; 49 | } 50 | 51 | .delete-button { 52 | margin: 10px; 53 | } 54 | 55 | .todo-checkbox { 56 | margin: 10px; 57 | } 58 | 59 | .todo-container { 60 | padding: 20px; 61 | border-bottom: 1px solid #333; 62 | } 63 | 64 | .todo-container:first-of-type { 65 | border-top: 1px solid #333; 66 | } 67 | -------------------------------------------------------------------------------- /project0/solution/basic2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TODO App 5 | 6 | 7 | 8 |
      9 |

      My TODO App

      10 |
      11 | Item count: 0 12 | Unchecked count: 0 13 |
      14 | 15 |
        16 |
        17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /project0/solution/basic2/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html, body { 6 | background-color: #eee; 7 | margin: 0; 8 | padding: 0; 9 | } 10 | 11 | ul { 12 | margin: 0; 13 | padding: 0; 14 | list-style-type: none; 15 | } 16 | 17 | .center { 18 | align-self: center; 19 | } 20 | 21 | .flow-right { 22 | display: flex; 23 | justify-content: space-around; 24 | } 25 | 26 | .container { 27 | max-width: 800px; 28 | margin: 0 auto; 29 | padding: 10px; 30 | display: flex; 31 | flex-direction: column; 32 | background-color: white; 33 | height: 100vh; 34 | } 35 | 36 | .title, .controls, .button { 37 | flex: none; 38 | } 39 | 40 | .button { 41 | padding: 10px 20px; 42 | } 43 | 44 | .todo-list { 45 | flex: 1 1 0; 46 | margin-top: 20px; 47 | padding: 20px; 48 | overflow-y: auto; 49 | } 50 | 51 | .delete-button { 52 | margin: 10px; 53 | } 54 | 55 | .todo-checkbox { 56 | margin: 10px; 57 | } 58 | 59 | .todo-container { 60 | padding: 20px; 61 | border-bottom: 1px solid #333; 62 | } 63 | 64 | .todo-container:first-of-type { 65 | border-top: 1px solid #333; 66 | } 67 | -------------------------------------------------------------------------------- /project0/solution/withDelete2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TODO App 5 | 6 | 7 | 8 |
        9 |

        My TODO App

        10 |
        11 | Item count: 0 12 | Unchecked count: 0 13 |
        14 | 15 |
          16 |
          17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /project0/solution/withDelete2/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html, body { 6 | background-color: #eee; 7 | margin: 0; 8 | padding: 0; 9 | } 10 | 11 | ul { 12 | margin: 0; 13 | padding: 0; 14 | list-style-type: none; 15 | } 16 | 17 | .center { 18 | align-self: center; 19 | } 20 | 21 | .flow-right { 22 | display: flex; 23 | justify-content: space-around; 24 | } 25 | 26 | .container { 27 | max-width: 800px; 28 | margin: 0 auto; 29 | padding: 10px; 30 | display: flex; 31 | flex-direction: column; 32 | background-color: white; 33 | height: 100vh; 34 | } 35 | 36 | .title, .controls, .button { 37 | flex: none; 38 | } 39 | 40 | .button { 41 | padding: 10px 20px; 42 | } 43 | 44 | .todo-list { 45 | flex: 1 1 0; 46 | margin-top: 20px; 47 | padding: 20px; 48 | overflow-y: auto; 49 | } 50 | 51 | .delete-button { 52 | margin: 10px; 53 | } 54 | 55 | .todo-checkbox { 56 | margin: 10px; 57 | } 58 | 59 | .todo-container { 60 | padding: 20px; 61 | border-bottom: 1px solid #333; 62 | } 63 | 64 | .todo-container:first-of-type { 65 | border-top: 1px solid #333; 66 | } 67 | --------------------------------------------------------------------------------