├── README.md ├── assets ├── LiftStateUp.png ├── LiftStateUp2.png ├── guides.png ├── logo.png ├── react-ui-important.png ├── react-ui.png ├── should-component-update.png ├── states.png └── tools.png ├── index.html ├── reveal ├── .gitignore ├── Gruntfile.js ├── css-themes-build.bat ├── css-themes-install.bat ├── css │ ├── highlight │ │ ├── darkula-for-dark.css │ │ └── idea-for-light.css │ ├── reveal.css │ ├── reveal.scss │ └── theme │ │ ├── kontur-dark.css │ │ ├── kontur-light.css │ │ ├── source │ │ ├── kontur-dark.scss │ │ └── kontur-light.scss │ │ └── template │ │ ├── mixins.scss │ │ ├── settings.scss │ │ └── theme.scss ├── initialize.js ├── js │ ├── classList.js │ ├── d3.min.js │ ├── desktop.ini │ ├── head.min.js │ ├── html5shiv.js │ └── reveal.js ├── package.json └── plugin │ ├── highlight │ └── highlight.js │ ├── markdown-modified │ ├── example.html │ ├── example.md │ ├── markdown.js │ └── marked.js │ ├── menu-modified │ ├── font-awesome-4.3.0 │ │ ├── css │ │ │ ├── font-awesome.css │ │ │ └── font-awesome.min.css │ │ ├── desktop.ini │ │ └── fonts │ │ │ ├── FontAwesome.otf │ │ │ ├── fontawesome-webfont.eot │ │ │ ├── fontawesome-webfont.svg │ │ │ ├── fontawesome-webfont.ttf │ │ │ ├── fontawesome-webfont.woff │ │ │ └── fontawesome-webfont.woff2 │ ├── lib │ │ ├── bowser.min.js │ │ └── jeesh.min.js │ ├── menu.css │ └── menu.js │ └── notes │ ├── notes.html │ └── notes.js ├── tasks ├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── index.html ├── package-lock.json ├── package.json ├── src │ ├── 1.1.SimpleHtml │ │ ├── .solved │ │ │ └── index.js │ │ ├── from.html │ │ ├── index.html │ │ ├── index.js │ │ └── styles.css │ ├── 1.2.Button │ │ ├── .solved │ │ │ └── index.js │ │ ├── index.html │ │ ├── index.js │ │ └── styles.css │ ├── 1.3.Input │ │ ├── .solved │ │ │ └── index.js │ │ ├── index.html │ │ ├── index.js │ │ └── styles.css │ ├── 2.1.ExtractFunction │ │ ├── .solved │ │ │ └── index.js │ │ ├── index.html │ │ ├── index.js │ │ └── styles.css │ ├── 2.2.List │ │ ├── .solved │ │ │ └── index.js │ │ ├── index.html │ │ ├── index.js │ │ └── styles.css │ ├── 2.3.Condition │ │ ├── .solved │ │ │ └── index.js │ │ ├── index.html │ │ ├── index.js │ │ └── styles.css │ ├── 3.1.ExtractComponent │ │ ├── .solved │ │ │ └── index.js │ │ ├── index.html │ │ ├── index.js │ │ └── styles.css │ ├── 3.2.Toggle │ │ ├── .solved │ │ │ └── index.js │ │ ├── index.html │ │ ├── index.js │ │ ├── styles.css │ │ └── toggle.css │ ├── 3.3.MoneyConverter │ │ ├── .solved │ │ │ └── index.js │ │ ├── index.html │ │ ├── index.js │ │ └── styles.css │ ├── 4.1.Timer │ │ ├── .solved │ │ │ └── index.js │ │ ├── index.html │ │ ├── index.js │ │ └── styles.css │ ├── 4.2.CreditCardInput │ │ ├── .solved │ │ │ └── index.js │ │ ├── Api.js │ │ ├── CreditCardNumber.js │ │ ├── index.html │ │ ├── index.js │ │ └── styles.css │ ├── 5.Focus │ │ ├── .solved │ │ │ └── index.js │ │ ├── index.html │ │ ├── index.js │ │ └── styles.css │ ├── 6.Hooks │ │ ├── .solved │ │ │ └── index.js │ │ ├── index.html │ │ ├── index.js │ │ └── styles.css │ ├── 7.UsersTable │ │ ├── .solved │ │ │ └── index.js │ │ ├── EditUserForm.jsx │ │ ├── defaultUsers.js │ │ ├── helpers.js │ │ ├── index.html │ │ ├── index.js │ │ └── styles.css │ ├── 8.FormRow │ │ ├── .solved │ │ │ └── index.js │ │ ├── Input.js │ │ ├── Toggle.js │ │ ├── enhance.js │ │ ├── index.html │ │ ├── index.js │ │ ├── styles.css │ │ └── toggle.css │ └── 9.ColorsOfTime │ │ ├── .solved │ │ └── index.js │ │ ├── Button.js │ │ ├── TimeDisplay.js │ │ ├── Timer.js │ │ ├── helpers.js │ │ ├── index.html │ │ ├── index.js │ │ ├── styles.css │ │ └── themes.js ├── tasks.js └── webpack.config.js └── userProfile ├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .solved ├── index.html └── src │ ├── Form.js │ ├── index.js │ └── style.css ├── drafts ├── reactDiff.png ├── reactForm.png └── reactModal.png ├── index.html ├── package-lock.json ├── package.json ├── src ├── index.js └── style.css └── webpack.config.js /README.md: -------------------------------------------------------------------------------- 1 | # React 2 | 3 | ⚠ Эта версия больше не обновляется. Актуальная версия — [React-TS](https://github.com/kontur-web-courses/react-ts). 4 | 5 | При создании клиентской части современных веб-приложений стоит использовать какой-нибудь фреймворк. Один из лучших таких фреймворков — React. Компонентный подход, смешение HTML и JS в виде JSX, VirtualDOM позволяют быстро разрабатывать быстрые приложения. 6 | 7 | Пройдя блок, ты поймешь разницу между HTML и JSX, научишься создавать компоненты, узнаешь как оптимизировать производительность React, познакомишься с Higher Order Components и Context. 8 | 9 | 10 | ## Необходимые знания 11 | 12 | Понадобится знание JS 13 | 14 | 15 | 16 | 17 | ## Самостоятельная подготовка 18 | 19 | Рекомендуется пройти блок [Frontend Starter](https://github.com/kontur-web-courses/frontend-starter-tutorial) 20 | 21 | Предполагаем, что ты уже знаком с ES2015+ синтаксисом JavaScript. Если нет, прочти эту [статью](http://www.js-craft.io/blog/10-The-10-min-ES6-course-for-the-beginner-React-Developer/) или потренируйся в новом синтаксисе [тут](http://es6katas.org/). 22 | 23 | ## Очная встреча 24 | 25 | ~ 8 часов 26 | 27 | [Презентация](https://kontur-web-courses.github.io/react/) 28 | -------------------------------------------------------------------------------- /assets/LiftStateUp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontur-web-courses/react/4e7a0a120b58c4fa650f8e68da0e1220f7513362/assets/LiftStateUp.png -------------------------------------------------------------------------------- /assets/LiftStateUp2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontur-web-courses/react/4e7a0a120b58c4fa650f8e68da0e1220f7513362/assets/LiftStateUp2.png -------------------------------------------------------------------------------- /assets/guides.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontur-web-courses/react/4e7a0a120b58c4fa650f8e68da0e1220f7513362/assets/guides.png -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontur-web-courses/react/4e7a0a120b58c4fa650f8e68da0e1220f7513362/assets/logo.png -------------------------------------------------------------------------------- /assets/react-ui-important.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontur-web-courses/react/4e7a0a120b58c4fa650f8e68da0e1220f7513362/assets/react-ui-important.png -------------------------------------------------------------------------------- /assets/react-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontur-web-courses/react/4e7a0a120b58c4fa650f8e68da0e1220f7513362/assets/react-ui.png -------------------------------------------------------------------------------- /assets/should-component-update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontur-web-courses/react/4e7a0a120b58c4fa650f8e68da0e1220f7513362/assets/should-component-update.png -------------------------------------------------------------------------------- /assets/states.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontur-web-courses/react/4e7a0a120b58c4fa650f8e68da0e1220f7513362/assets/states.png -------------------------------------------------------------------------------- /assets/tools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontur-web-courses/react/4e7a0a120b58c4fa650f8e68da0e1220f7513362/assets/tools.png -------------------------------------------------------------------------------- /reveal/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.iml 3 | *.iws 4 | *.eml 5 | out/ 6 | .DS_Store 7 | .svn 8 | log/*.log 9 | tmp/** 10 | node_modules/ 11 | .sass-cache 12 | css/reveal.min.css 13 | js/reveal.min.js -------------------------------------------------------------------------------- /reveal/Gruntfile.js: -------------------------------------------------------------------------------- 1 | /* global module:false */ 2 | module.exports = function(grunt) { 3 | var port = grunt.option('port') || 8000; 4 | var root = grunt.option('root') || '.'; 5 | 6 | if (!Array.isArray(root)) root = [root]; 7 | 8 | // Project configuration 9 | grunt.initConfig({ 10 | pkg: grunt.file.readJSON('package.json'), 11 | meta: { 12 | banner: 13 | '/*!\n' + 14 | ' * reveal.js <%= pkg.version %> (<%= grunt.template.today("yyyy-mm-dd, HH:MM") %>)\n' + 15 | ' * http://lab.hakim.se/reveal-js\n' + 16 | ' * MIT licensed\n' + 17 | ' *\n' + 18 | ' * Copyright (C) 2016 Hakim El Hattab, http://hakim.se\n' + 19 | ' */' 20 | }, 21 | 22 | qunit: { 23 | files: [ 'test/*.html' ] 24 | }, 25 | 26 | uglify: { 27 | options: { 28 | banner: '<%= meta.banner %>\n' 29 | }, 30 | build: { 31 | src: 'js/reveal.js', 32 | dest: 'js/reveal.min.js' 33 | } 34 | }, 35 | 36 | sass: { 37 | core: { 38 | files: { 39 | 'css/reveal.css': 'css/reveal.scss', 40 | } 41 | }, 42 | themes: { 43 | files: [ 44 | { 45 | expand: true, 46 | cwd: 'css/theme/source', 47 | src: ['*.scss'], 48 | dest: 'css/theme', 49 | ext: '.css' 50 | } 51 | ] 52 | } 53 | }, 54 | 55 | autoprefixer: { 56 | dist: { 57 | src: 'css/reveal.css' 58 | } 59 | }, 60 | 61 | cssmin: { 62 | compress: { 63 | files: { 64 | 'css/reveal.min.css': [ 'css/reveal.css' ] 65 | } 66 | } 67 | }, 68 | 69 | jshint: { 70 | options: { 71 | curly: false, 72 | eqeqeq: true, 73 | immed: true, 74 | esnext: true, 75 | latedef: true, 76 | newcap: true, 77 | noarg: true, 78 | sub: true, 79 | undef: true, 80 | eqnull: true, 81 | browser: true, 82 | expr: true, 83 | globals: { 84 | head: false, 85 | module: false, 86 | console: false, 87 | unescape: false, 88 | define: false, 89 | exports: false 90 | } 91 | }, 92 | files: [ 'Gruntfile.js', 'js/reveal.js' ] 93 | }, 94 | 95 | connect: { 96 | server: { 97 | options: { 98 | port: port, 99 | base: root, 100 | livereload: true, 101 | open: true 102 | } 103 | }, 104 | 105 | }, 106 | 107 | zip: { 108 | 'reveal-js-presentation.zip': [ 109 | 'index.html', 110 | 'css/**', 111 | 'js/**', 112 | 'lib/**', 113 | 'images/**', 114 | 'plugin/**', 115 | '**.md' 116 | ] 117 | }, 118 | 119 | watch: { 120 | js: { 121 | files: [ 'Gruntfile.js', 'js/reveal.js' ], 122 | tasks: 'js' 123 | }, 124 | theme: { 125 | files: [ 'css/theme/source/*.scss', 'css/theme/template/*.scss' ], 126 | tasks: 'css-themes' 127 | }, 128 | css: { 129 | files: [ 'css/reveal.scss' ], 130 | tasks: 'css-core' 131 | }, 132 | html: { 133 | files: root.map(path => path + '/*.html') 134 | }, 135 | markdown: { 136 | files: root.map(path => path + '/*.md') 137 | }, 138 | options: { 139 | livereload: true 140 | } 141 | }, 142 | 143 | retire: { 144 | js: ['js/reveal.js', 'lib/js/*.js', 'plugin/**/*.js'], 145 | node: ['.'], 146 | options: {} 147 | } 148 | 149 | }); 150 | 151 | // Dependencies 152 | grunt.loadNpmTasks( 'grunt-contrib-qunit' ); 153 | grunt.loadNpmTasks( 'grunt-contrib-jshint' ); 154 | grunt.loadNpmTasks( 'grunt-contrib-cssmin' ); 155 | grunt.loadNpmTasks( 'grunt-contrib-uglify' ); 156 | grunt.loadNpmTasks( 'grunt-contrib-watch' ); 157 | grunt.loadNpmTasks( 'grunt-sass' ); 158 | grunt.loadNpmTasks( 'grunt-contrib-connect' ); 159 | grunt.loadNpmTasks( 'grunt-autoprefixer' ); 160 | grunt.loadNpmTasks( 'grunt-zip' ); 161 | grunt.loadNpmTasks( 'grunt-retire' ); 162 | 163 | // Default task 164 | grunt.registerTask( 'default', [ 'css', 'js' ] ); 165 | 166 | // JS task 167 | grunt.registerTask( 'js', [ 'jshint', 'uglify', 'qunit' ] ); 168 | 169 | // Theme CSS 170 | grunt.registerTask( 'css-themes', [ 'sass:themes' ] ); 171 | 172 | // Core framework CSS 173 | grunt.registerTask( 'css-core', [ 'sass:core', 'autoprefixer', 'cssmin' ] ); 174 | 175 | // All CSS 176 | grunt.registerTask( 'css', [ 'sass', 'autoprefixer', 'cssmin' ] ); 177 | 178 | // Package presentation to archive 179 | grunt.registerTask( 'package', [ 'default', 'zip' ] ); 180 | 181 | // Serve presentation locally 182 | grunt.registerTask( 'serve', [ 'connect', 'watch' ] ); 183 | 184 | // Run tests 185 | grunt.registerTask( 'test', [ 'jshint', 'qunit' ] ); 186 | 187 | }; 188 | -------------------------------------------------------------------------------- /reveal/css-themes-build.bat: -------------------------------------------------------------------------------- 1 | grunt css-themes 2 | -------------------------------------------------------------------------------- /reveal/css-themes-install.bat: -------------------------------------------------------------------------------- 1 | npm install & npm install grunt -g & grunt css-themes 2 | -------------------------------------------------------------------------------- /reveal/css/highlight/darkula-for-dark.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Darcula color scheme from the JetBrains family of IDEs 4 | 5 | */ 6 | 7 | section.has-dark-background .hljs { 8 | display: block; 9 | overflow-x: auto; 10 | padding: 0.5em; 11 | background: #2b2b2b; 12 | } 13 | 14 | section.has-dark-background .hljs { 15 | color: #bababa; 16 | } 17 | 18 | section.has-dark-background .hljs-strong, 19 | section.has-dark-background .hljs-emphasis { 20 | color: #a8a8a2; 21 | } 22 | 23 | section.has-dark-background .hljs-bullet, 24 | section.has-dark-background .hljs-quote, 25 | section.has-dark-background .hljs-link, 26 | section.has-dark-background .hljs-number, 27 | section.has-dark-background .hljs-regexp, 28 | section.has-dark-background .hljs-literal { 29 | color: #6896ba; 30 | } 31 | 32 | section.has-dark-background .hljs-code, 33 | section.has-dark-background .hljs-selector-class { 34 | color: #a6e22e; 35 | } 36 | 37 | section.has-dark-background .hljs-emphasis { 38 | font-style: italic; 39 | } 40 | 41 | section.has-dark-background .hljs-keyword, 42 | section.has-dark-background .hljs-selector-tag, 43 | section.has-dark-background .hljs-section, 44 | section.has-dark-background .hljs-attribute, 45 | section.has-dark-background .hljs-name, 46 | section.has-dark-background .hljs-variable { 47 | color: #cb7832; 48 | } 49 | 50 | section.has-dark-background .hljs-params { 51 | color: #b9b9b9; 52 | } 53 | 54 | section.has-dark-background .hljs-string { 55 | color: #6a8759; 56 | } 57 | 58 | section.has-dark-background .hljs-subst, 59 | section.has-dark-background .hljs-type, 60 | section.has-dark-background .hljs-built_in, 61 | section.has-dark-background .hljs-builtin-name, 62 | section.has-dark-background .hljs-symbol, 63 | section.has-dark-background .hljs-selector-id, 64 | section.has-dark-background .hljs-selector-attr, 65 | section.has-dark-background .hljs-selector-pseudo, 66 | section.has-dark-background .hljs-template-tag, 67 | section.has-dark-background .hljs-template-variable, 68 | section.has-dark-background .hljs-addition { 69 | color: #e0c46c; 70 | } 71 | 72 | section.has-dark-background .hljs-comment, 73 | section.has-dark-background .hljs-deletion, 74 | section.has-dark-background .hljs-meta { 75 | color: #7f7f7f; 76 | } 77 | -------------------------------------------------------------------------------- /reveal/css/highlight/idea-for-light.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Intellij Idea-like styling (c) Vasily Polovnyov 4 | 5 | */ 6 | 7 | .has-light-background .hljs { 8 | display: block; 9 | overflow-x: auto; 10 | padding: 0.5em; 11 | color: #000; 12 | background: #f7f7f7; 13 | } 14 | 15 | .has-light-background .hljs-subst, { 16 | font-weight: normal; 17 | color: #000; 18 | } 19 | 20 | .has-light-background .hljs-comment, 21 | .has-light-background .hljs-quote { 22 | color: #808080; 23 | font-style: italic; 24 | } 25 | 26 | .has-light-background .hljs-meta { 27 | color: #808000; 28 | } 29 | 30 | .has-light-background .hljs-tag { 31 | background: #efefef; 32 | } 33 | 34 | .has-light-background .hljs-section, 35 | .has-light-background .hljs-name, 36 | .has-light-background .hljs-literal, 37 | .has-light-background .hljs-keyword, 38 | .has-light-background .hljs-selector-tag, 39 | .has-light-background .hljs-type, 40 | .has-light-background .hljs-selector-id, 41 | .has-light-background .hljs-selector-class { 42 | font-weight: bold; 43 | color: #000080; 44 | } 45 | 46 | .has-light-background .hljs-attribute, 47 | .has-light-background .hljs-number, 48 | .has-light-background .hljs-regexp, 49 | .has-light-background .hljs-link { 50 | font-weight: bold; 51 | color: #0000ff; 52 | } 53 | 54 | .has-light-background .hljs-number, 55 | .has-light-background .hljs-regexp, 56 | .has-light-background .hljs-link { 57 | font-weight: normal; 58 | } 59 | 60 | .has-light-background .hljs-string { 61 | color: #008000; 62 | font-weight: bold; 63 | } 64 | 65 | .has-light-background .hljs-symbol, 66 | .has-light-background .hljs-bullet, 67 | .has-light-background .hljs-formula { 68 | color: #000; 69 | background: #d0eded; 70 | font-style: italic; 71 | } 72 | 73 | .has-light-background .hljs-doctag { 74 | text-decoration: underline; 75 | } 76 | 77 | .has-light-background .hljs-variable, 78 | .has-light-background .hljs-template-variable { 79 | color: #660e7a; 80 | } 81 | 82 | .has-light-background .hljs-addition { 83 | background: #baeeba; 84 | } 85 | 86 | .has-light-background .hljs-deletion { 87 | background: #ffc8bd; 88 | } 89 | 90 | .has-light-background .hljs-emphasis { 91 | font-style: italic; 92 | } 93 | 94 | .has-light-background .hljs-strong { 95 | font-weight: bold; 96 | } 97 | -------------------------------------------------------------------------------- /reveal/css/theme/kontur-dark.css: -------------------------------------------------------------------------------- 1 | /** 2 | * White theme for reveal.js. This is the opposite of the 'black' theme. 3 | * 4 | * By Hakim El Hattab, http://hakim.se 5 | */ 6 | section.has-white-background { 7 | color: #000; } 8 | 9 | section.has-white-background h1, section.has-white-background h2, section.has-white-background h3, section.has-white-background h4, section.has-white-background h5, section.has-white-background h6 { 10 | color: #D94440; } 11 | 12 | /********************************************* 13 | * GLOBAL STYLES 14 | *********************************************/ 15 | body { 16 | background: #000; 17 | background-color: #000; } 18 | 19 | .reveal { 20 | font-family: "Segoe UI", Helvetica, sans-serif; 21 | font-size: 30px; 22 | font-weight: normal; 23 | color: #fff; } 24 | 25 | ::selection { 26 | color: #fff; 27 | background: #6dc3f0; 28 | text-shadow: none; } 29 | 30 | ::-moz-selection { 31 | color: #fff; 32 | background: #6dc3f0; 33 | text-shadow: none; } 34 | 35 | .reveal .slides { 36 | text-align: left; } 37 | 38 | .reveal .slides > section, 39 | .reveal .slides > section > section { 40 | line-height: 1.3; 41 | font-weight: inherit; } 42 | 43 | /********************************************* 44 | * HEADERS 45 | *********************************************/ 46 | .reveal h1, 47 | .reveal h2, 48 | .reveal h3, 49 | .reveal h4, 50 | .reveal h5, 51 | .reveal h6 { 52 | margin: 0 0 20px 0; 53 | color: #D94440; 54 | font-family: "Segoe UI Light", Helvetica, sans-serif; 55 | font-weight: normal; 56 | line-height: 1.2; 57 | letter-spacing: normal; 58 | text-transform: normal; 59 | text-shadow: none; 60 | word-wrap: break-word; } 61 | 62 | .reveal h1 { 63 | font-size: 3.5em; 64 | text-align: center; } 65 | 66 | .reveal h2 { 67 | font-size: 2.3em; 68 | text-align: center; } 69 | 70 | .reveal h3 { 71 | font-size: 1.7em; } 72 | 73 | .reveal h4 { 74 | font-size: 1.4em; } 75 | 76 | .reveal h1 { 77 | text-shadow: none; } 78 | 79 | /********************************************* 80 | * OTHER 81 | *********************************************/ 82 | .reveal p { 83 | margin: 20px 0; 84 | line-height: 1.3; } 85 | 86 | /* Ensure certain elements are never larger than the slide itself */ 87 | .reveal img, 88 | .reveal video, 89 | .reveal iframe { 90 | max-width: 95%; 91 | max-height: 95%; } 92 | 93 | .reveal strong, 94 | .reveal b { 95 | font-weight: bold; } 96 | 97 | .reveal em { 98 | font-style: italic; } 99 | 100 | .reveal ol, 101 | .reveal dl, 102 | .reveal ul { 103 | display: inline-block; 104 | text-align: left; 105 | margin: 0 0 20px 1em; } 106 | 107 | .reveal ol ol, 108 | .reveal dl ol, 109 | .reveal ul ol, 110 | .reveal ol dl, 111 | .reveal dl dl, 112 | .reveal ul dl, 113 | .reveal ol ul, 114 | .reveal dl ul, 115 | .reveal ul ul { 116 | margin-bottom: 0; } 117 | 118 | .reveal ul { 119 | list-style-type: none; } 120 | 121 | .reveal ol { 122 | list-style-type: none; 123 | counter-reset: li; } 124 | 125 | .reveal ol > li:before { 126 | content: "." counter(li); 127 | color: #D94440; 128 | display: inline-block; 129 | width: 1em; 130 | margin-left: -1.5em; 131 | margin-right: 0.5em; 132 | text-align: right; 133 | direction: rtl; } 134 | 135 | .reveal ol > li { 136 | counter-increment: li; } 137 | 138 | .reveal ul > li:before { 139 | content: "\2022"; 140 | color: #D94440; 141 | display: inline-block; 142 | width: 1em; 143 | margin-left: -1em; } 144 | 145 | .reveal ul ul, 146 | .reveal ul ol, 147 | .reveal ol ol, 148 | .reveal ol ul { 149 | display: block; 150 | margin-left: 40px; } 151 | 152 | .reveal dt { 153 | font-weight: bold; } 154 | 155 | .reveal dd { 156 | margin-left: 40px; } 157 | 158 | .reveal q, 159 | .reveal blockquote { 160 | quotes: none; } 161 | 162 | .reveal blockquote { 163 | display: block; 164 | position: relative; 165 | width: auto; 166 | margin: 20px auto; 167 | padding: 5px 20px 5px 20px; 168 | font-style: italic; 169 | background: rgba(127, 127, 127, 0.3); } 170 | 171 | .reveal blockquote p:first-child, 172 | .reveal blockquote p:last-child { 173 | display: block; } 174 | 175 | .reveal q { 176 | font-style: italic; } 177 | 178 | .reveal pre { 179 | display: block; 180 | position: relative; 181 | width: auto; 182 | margin: 20px auto; 183 | text-align: left; 184 | font-size: 0.70em; 185 | font-family: "Consolas", monospace; 186 | line-height: 1.2em; 187 | word-wrap: break-word; 188 | border: 0px solid rgba(0, 0, 0, 0.3); } 189 | 190 | .reveal code { 191 | font-family: "Consolas", monospace; 192 | color: #D94440; } 193 | 194 | .reveal pre code { 195 | display: block; 196 | padding: 5px; 197 | overflow: auto; 198 | max-height: 400px; 199 | word-wrap: normal; } 200 | 201 | .reveal table { 202 | margin: auto; 203 | border-collapse: collapse; 204 | border-spacing: 0; } 205 | 206 | .reveal table th { 207 | font-weight: bold; } 208 | 209 | .reveal table th, 210 | .reveal table td { 211 | text-align: left; 212 | padding: 0.2em 0.5em 0.2em 0.5em; 213 | border-bottom: 1px solid; } 214 | 215 | .reveal table th[align="center"], 216 | .reveal table td[align="center"] { 217 | text-align: center; } 218 | 219 | .reveal table th[align="right"], 220 | .reveal table td[align="right"] { 221 | text-align: right; } 222 | 223 | .reveal table tbody tr:last-child th, 224 | .reveal table tbody tr:last-child td { 225 | border-bottom: none; } 226 | 227 | .reveal sup { 228 | vertical-align: super; } 229 | 230 | .reveal sub { 231 | vertical-align: sub; } 232 | 233 | .reveal small { 234 | display: inline-block; 235 | font-size: 0.6em; 236 | line-height: 1.2em; 237 | vertical-align: top; } 238 | 239 | .reveal small * { 240 | vertical-align: top; } 241 | 242 | .reveal .left { 243 | text-align: left !important; } 244 | 245 | .reveal .center { 246 | text-align: center !important; } 247 | 248 | .reveal .right { 249 | text-align: right !important; } 250 | 251 | .reveal em { 252 | color: #D94440; } 253 | 254 | /********************************************* 255 | * LINKS 256 | *********************************************/ 257 | .reveal a { 258 | color: #158BC8; 259 | text-decoration: none; 260 | -webkit-transition: color .15s ease; 261 | -moz-transition: color .15s ease; 262 | transition: color .15s ease; } 263 | 264 | .reveal a:hover { 265 | color: #3fb0eb; 266 | text-shadow: none; 267 | border: none; } 268 | 269 | .reveal .roll span:after { 270 | color: #fff; 271 | background: #0e5b83; } 272 | 273 | /********************************************* 274 | * IMAGES 275 | *********************************************/ 276 | .reveal section img { 277 | margin: 15px 0px; 278 | background: transparent; 279 | border: 0px solid #fff; } 280 | 281 | .reveal section img.plain { 282 | border: 0; 283 | box-shadow: none; } 284 | 285 | .reveal a img { 286 | -webkit-transition: all .15s linear; 287 | -moz-transition: all .15s linear; 288 | transition: all .15s linear; } 289 | 290 | .reveal a:hover img { 291 | background: rgba(255, 255, 255, 0.2); 292 | border-color: #158BC8; } 293 | 294 | /********************************************* 295 | * NAVIGATION CONTROLS 296 | *********************************************/ 297 | .reveal .controls .navigate-left, 298 | .reveal .controls .navigate-left.enabled { 299 | border-right-color: #158BC8; } 300 | 301 | .reveal .controls .navigate-right, 302 | .reveal .controls .navigate-right.enabled { 303 | border-left-color: #158BC8; } 304 | 305 | .reveal .controls .navigate-up, 306 | .reveal .controls .navigate-up.enabled { 307 | border-bottom-color: #158BC8; } 308 | 309 | .reveal .controls .navigate-down, 310 | .reveal .controls .navigate-down.enabled { 311 | border-top-color: #158BC8; } 312 | 313 | .reveal .controls .navigate-left.enabled:hover { 314 | border-right-color: #3fb0eb; } 315 | 316 | .reveal .controls .navigate-right.enabled:hover { 317 | border-left-color: #3fb0eb; } 318 | 319 | .reveal .controls .navigate-up.enabled:hover { 320 | border-bottom-color: #3fb0eb; } 321 | 322 | .reveal .controls .navigate-down.enabled:hover { 323 | border-top-color: #3fb0eb; } 324 | 325 | /********************************************* 326 | * PROGRESS BAR 327 | *********************************************/ 328 | .reveal .progress { 329 | background: rgba(0, 0, 0, 0.2); } 330 | 331 | .reveal .progress span { 332 | background: #D94440; 333 | -webkit-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); 334 | -moz-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); 335 | transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); } 336 | -------------------------------------------------------------------------------- /reveal/css/theme/kontur-light.css: -------------------------------------------------------------------------------- 1 | /** 2 | * White theme for reveal.js. This is the opposite of the 'black' theme. 3 | * 4 | * By Hakim El Hattab, http://hakim.se 5 | */ 6 | section.has-dark-background { 7 | color: #fff; } 8 | 9 | section.has-dark-background h1, section.has-dark-background h2, section.has-dark-background h3, section.has-dark-background h4, section.has-dark-background h5, section.has-dark-background h6 { 10 | color: #D94440; } 11 | 12 | /********************************************* 13 | * GLOBAL STYLES 14 | *********************************************/ 15 | body { 16 | background: #fff; 17 | background-color: #fff; } 18 | 19 | .reveal { 20 | font-family: "Segoe UI", Helvetica, sans-serif; 21 | font-size: 30px; 22 | font-weight: normal; 23 | color: #000; } 24 | 25 | ::selection { 26 | color: #fff; 27 | background: #6dc3f0; 28 | text-shadow: none; } 29 | 30 | ::-moz-selection { 31 | color: #fff; 32 | background: #6dc3f0; 33 | text-shadow: none; } 34 | 35 | .reveal .slides { 36 | text-align: left; } 37 | 38 | .reveal .slides > section, 39 | .reveal .slides > section > section { 40 | line-height: 1.3; 41 | font-weight: inherit; } 42 | 43 | /********************************************* 44 | * HEADERS 45 | *********************************************/ 46 | .reveal h1, 47 | .reveal h2, 48 | .reveal h3, 49 | .reveal h4, 50 | .reveal h5, 51 | .reveal h6 { 52 | margin: 0 0 20px 0; 53 | color: #D94440; 54 | font-family: "Segoe UI Light", Helvetica, sans-serif; 55 | font-weight: normal; 56 | line-height: 1.2; 57 | letter-spacing: normal; 58 | text-transform: normal; 59 | text-shadow: none; 60 | word-wrap: break-word; } 61 | 62 | .reveal h1 { 63 | font-size: 3.5em; 64 | text-align: center; } 65 | 66 | .reveal h2 { 67 | font-size: 2.3em; 68 | text-align: center; } 69 | 70 | .reveal h3 { 71 | font-size: 1.7em; } 72 | 73 | .reveal h4 { 74 | font-size: 1.4em; } 75 | 76 | .reveal h1 { 77 | text-shadow: none; } 78 | 79 | /********************************************* 80 | * OTHER 81 | *********************************************/ 82 | .reveal p { 83 | margin: 20px 0; 84 | line-height: 1.3; } 85 | 86 | /* Ensure certain elements are never larger than the slide itself */ 87 | .reveal img, 88 | .reveal video, 89 | .reveal iframe { 90 | max-width: 95%; 91 | max-height: 95%; } 92 | 93 | .reveal strong, 94 | .reveal b { 95 | font-weight: bold; } 96 | 97 | .reveal em { 98 | font-style: italic; } 99 | 100 | .reveal ol, 101 | .reveal dl, 102 | .reveal ul { 103 | display: inline-block; 104 | text-align: left; 105 | margin: 0 0 20px 1em; } 106 | 107 | .reveal ol ol, 108 | .reveal dl ol, 109 | .reveal ul ol, 110 | .reveal ol dl, 111 | .reveal dl dl, 112 | .reveal ul dl, 113 | .reveal ol ul, 114 | .reveal dl ul, 115 | .reveal ul ul { 116 | margin-bottom: 0; } 117 | 118 | .reveal ul { 119 | list-style-type: none; } 120 | 121 | .reveal ol { 122 | list-style-type: none; 123 | counter-reset: li; } 124 | 125 | .reveal ol > li:before { 126 | content: "." counter(li); 127 | color: #D94440; 128 | display: inline-block; 129 | width: 1em; 130 | margin-left: -1.5em; 131 | margin-right: 0.5em; 132 | text-align: right; 133 | direction: rtl; } 134 | 135 | .reveal ol > li { 136 | counter-increment: li; } 137 | 138 | .reveal ul > li:before { 139 | content: "\2022"; 140 | color: #D94440; 141 | display: inline-block; 142 | width: 1em; 143 | margin-left: -1em; } 144 | 145 | .reveal ul ul, 146 | .reveal ul ol, 147 | .reveal ol ol, 148 | .reveal ol ul { 149 | display: block; 150 | margin-left: 40px; } 151 | 152 | .reveal dt { 153 | font-weight: bold; } 154 | 155 | .reveal dd { 156 | margin-left: 40px; } 157 | 158 | .reveal q, 159 | .reveal blockquote { 160 | quotes: none; } 161 | 162 | .reveal blockquote { 163 | display: block; 164 | position: relative; 165 | width: auto; 166 | margin: 20px auto; 167 | padding: 5px 20px 5px 20px; 168 | font-style: italic; 169 | background: rgba(127, 127, 127, 0.3); } 170 | 171 | .reveal blockquote p:first-child, 172 | .reveal blockquote p:last-child { 173 | display: block; } 174 | 175 | .reveal q { 176 | font-style: italic; } 177 | 178 | .reveal pre { 179 | display: block; 180 | position: relative; 181 | width: auto; 182 | margin: 20px auto; 183 | text-align: left; 184 | font-size: 0.70em; 185 | font-family: "Consolas", monospace; 186 | line-height: 1.2em; 187 | word-wrap: break-word; 188 | border: 0px solid rgba(0, 0, 0, 0.3); } 189 | 190 | .reveal code { 191 | font-family: "Consolas", monospace; 192 | color: #D94440; } 193 | 194 | .reveal pre code { 195 | display: block; 196 | padding: 5px; 197 | overflow: auto; 198 | max-height: 400px; 199 | word-wrap: normal; } 200 | 201 | .reveal table { 202 | margin: auto; 203 | border-collapse: collapse; 204 | border-spacing: 0; } 205 | 206 | .reveal table th { 207 | font-weight: bold; } 208 | 209 | .reveal table th, 210 | .reveal table td { 211 | text-align: left; 212 | padding: 0.2em 0.5em 0.2em 0.5em; 213 | border-bottom: 1px solid; } 214 | 215 | .reveal table th[align="center"], 216 | .reveal table td[align="center"] { 217 | text-align: center; } 218 | 219 | .reveal table th[align="right"], 220 | .reveal table td[align="right"] { 221 | text-align: right; } 222 | 223 | .reveal table tbody tr:last-child th, 224 | .reveal table tbody tr:last-child td { 225 | border-bottom: none; } 226 | 227 | .reveal sup { 228 | vertical-align: super; } 229 | 230 | .reveal sub { 231 | vertical-align: sub; } 232 | 233 | .reveal small { 234 | display: inline-block; 235 | font-size: 0.6em; 236 | line-height: 1.2em; 237 | vertical-align: top; } 238 | 239 | .reveal small * { 240 | vertical-align: top; } 241 | 242 | .reveal .left { 243 | text-align: left !important; } 244 | 245 | .reveal .center { 246 | text-align: center !important; } 247 | 248 | .reveal .right { 249 | text-align: right !important; } 250 | 251 | .reveal em { 252 | color: #D94440; } 253 | 254 | /********************************************* 255 | * LINKS 256 | *********************************************/ 257 | .reveal a { 258 | color: #158BC8; 259 | text-decoration: none; 260 | -webkit-transition: color .15s ease; 261 | -moz-transition: color .15s ease; 262 | transition: color .15s ease; } 263 | 264 | .reveal a:hover { 265 | color: #3fb0eb; 266 | text-shadow: none; 267 | border: none; } 268 | 269 | .reveal .roll span:after { 270 | color: #fff; 271 | background: #0e5b83; } 272 | 273 | /********************************************* 274 | * IMAGES 275 | *********************************************/ 276 | .reveal section img { 277 | margin: 15px 0px; 278 | background: transparent; 279 | border: 0px solid #000; } 280 | 281 | .reveal section img.plain { 282 | border: 0; 283 | box-shadow: none; } 284 | 285 | .reveal a img { 286 | -webkit-transition: all .15s linear; 287 | -moz-transition: all .15s linear; 288 | transition: all .15s linear; } 289 | 290 | .reveal a:hover img { 291 | background: rgba(255, 255, 255, 0.2); 292 | border-color: #158BC8; } 293 | 294 | /********************************************* 295 | * NAVIGATION CONTROLS 296 | *********************************************/ 297 | .reveal .controls .navigate-left, 298 | .reveal .controls .navigate-left.enabled { 299 | border-right-color: #158BC8; } 300 | 301 | .reveal .controls .navigate-right, 302 | .reveal .controls .navigate-right.enabled { 303 | border-left-color: #158BC8; } 304 | 305 | .reveal .controls .navigate-up, 306 | .reveal .controls .navigate-up.enabled { 307 | border-bottom-color: #158BC8; } 308 | 309 | .reveal .controls .navigate-down, 310 | .reveal .controls .navigate-down.enabled { 311 | border-top-color: #158BC8; } 312 | 313 | .reveal .controls .navigate-left.enabled:hover { 314 | border-right-color: #3fb0eb; } 315 | 316 | .reveal .controls .navigate-right.enabled:hover { 317 | border-left-color: #3fb0eb; } 318 | 319 | .reveal .controls .navigate-up.enabled:hover { 320 | border-bottom-color: #3fb0eb; } 321 | 322 | .reveal .controls .navigate-down.enabled:hover { 323 | border-top-color: #3fb0eb; } 324 | 325 | /********************************************* 326 | * PROGRESS BAR 327 | *********************************************/ 328 | .reveal .progress { 329 | background: rgba(0, 0, 0, 0.2); } 330 | 331 | .reveal .progress span { 332 | background: #D94440; 333 | -webkit-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); 334 | -moz-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); 335 | transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); } 336 | -------------------------------------------------------------------------------- /reveal/css/theme/source/kontur-dark.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * White theme for reveal.js. This is the opposite of the 'black' theme. 3 | * 4 | * By Hakim El Hattab, http://hakim.se 5 | */ 6 | 7 | 8 | // Default mixins and settings ----------------- 9 | @import "../template/mixins"; 10 | @import "../template/settings"; 11 | // --------------------------------------------- 12 | 13 | 14 | // Include theme-specific fonts 15 | //@import url(../../lib/font/source-sans-pro/source-sans-pro.css); 16 | 17 | 18 | // Override theme settings (see ../template/settings.scss) 19 | $backgroundColor: #000; 20 | 21 | $mainColor: #fff; 22 | $accentColor: #D94440; 23 | 24 | $mainFontSize: 30px; 25 | $mainFont: 'Segoe UI', Helvetica, sans-serif; 26 | $headingColor: $accentColor; 27 | $headingFont: 'Segoe UI Light', Helvetica, sans-serif; 28 | $headingTextShadow: none; 29 | $headingLetterSpacing: normal; 30 | $headingTextTransform: normal; 31 | $headingFontWeight: normal; 32 | $linkColor: #158BC8; 33 | $linkColorHover: lighten( $linkColor, 15% ); 34 | $selectionBackgroundColor: lighten( $linkColor, 25% ); 35 | $codeFont: 'Consolas', monospace; 36 | 37 | $heading1Size: 3.5em; //2.5 38 | $heading2Size: 2.3em; //1.6 39 | $heading3Size: 1.7em; //1.3 40 | $heading4Size: 1.4em; //1.0 41 | 42 | section.has-white-background { 43 | & { 44 | color: #000; 45 | } 46 | h1, h2, h3, h4, h5, h6 { 47 | color: #D94440; 48 | } 49 | } 50 | 51 | 52 | // Theme template ------------------------------ 53 | @import "../template/theme"; 54 | // --------------------------------------------- -------------------------------------------------------------------------------- /reveal/css/theme/source/kontur-light.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * White theme for reveal.js. This is the opposite of the 'black' theme. 3 | * 4 | * By Hakim El Hattab, http://hakim.se 5 | */ 6 | 7 | 8 | // Default mixins and settings ----------------- 9 | @import "../template/mixins"; 10 | @import "../template/settings"; 11 | // --------------------------------------------- 12 | 13 | 14 | // Include theme-specific fonts 15 | //@import url(../../lib/font/source-sans-pro/source-sans-pro.css); 16 | 17 | 18 | // Override theme settings (see ../template/settings.scss) 19 | $backgroundColor: #fff; 20 | 21 | $mainColor: #000; 22 | $accentColor: #D94440; 23 | 24 | $mainFontSize: 30px; 25 | $mainFont: 'Segoe UI', Helvetica, sans-serif; 26 | $headingColor: $accentColor; 27 | $headingFont: 'Segoe UI Light', Helvetica, sans-serif; 28 | $headingTextShadow: none; 29 | $headingLetterSpacing: normal; 30 | $headingTextTransform: normal; 31 | $headingFontWeight: normal; 32 | $linkColor: #158BC8; 33 | $linkColorHover: lighten( $linkColor, 15% ); 34 | $selectionBackgroundColor: lighten( $linkColor, 25% ); 35 | $codeFont: 'Consolas', monospace; 36 | 37 | $heading1Size: 3.5em; //2.5 38 | $heading2Size: 2.3em; //1.6 39 | $heading3Size: 1.7em; //1.3 40 | $heading4Size: 1.4em; //1.0 41 | 42 | section.has-dark-background { 43 | & { 44 | color: #fff; 45 | } 46 | h1, h2, h3, h4, h5, h6 { 47 | color: #D94440; 48 | } 49 | } 50 | 51 | 52 | // Theme template ------------------------------ 53 | @import "../template/theme"; 54 | // --------------------------------------------- -------------------------------------------------------------------------------- /reveal/css/theme/template/mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin vertical-gradient( $top, $bottom ) { 2 | background: $top; 3 | background: -moz-linear-gradient( top, $top 0%, $bottom 100% ); 4 | background: -webkit-gradient( linear, left top, left bottom, color-stop(0%,$top), color-stop(100%,$bottom) ); 5 | background: -webkit-linear-gradient( top, $top 0%, $bottom 100% ); 6 | background: -o-linear-gradient( top, $top 0%, $bottom 100% ); 7 | background: -ms-linear-gradient( top, $top 0%, $bottom 100% ); 8 | background: linear-gradient( top, $top 0%, $bottom 100% ); 9 | } 10 | 11 | @mixin horizontal-gradient( $top, $bottom ) { 12 | background: $top; 13 | background: -moz-linear-gradient( left, $top 0%, $bottom 100% ); 14 | background: -webkit-gradient( linear, left top, right top, color-stop(0%,$top), color-stop(100%,$bottom) ); 15 | background: -webkit-linear-gradient( left, $top 0%, $bottom 100% ); 16 | background: -o-linear-gradient( left, $top 0%, $bottom 100% ); 17 | background: -ms-linear-gradient( left, $top 0%, $bottom 100% ); 18 | background: linear-gradient( left, $top 0%, $bottom 100% ); 19 | } 20 | 21 | @mixin radial-gradient( $outer, $inner, $type: circle ) { 22 | background: $outer; 23 | background: -moz-radial-gradient( center, $type cover, $inner 0%, $outer 100% ); 24 | background: -webkit-gradient( radial, center center, 0px, center center, 100%, color-stop(0%,$inner), color-stop(100%,$outer) ); 25 | background: -webkit-radial-gradient( center, $type cover, $inner 0%, $outer 100% ); 26 | background: -o-radial-gradient( center, $type cover, $inner 0%, $outer 100% ); 27 | background: -ms-radial-gradient( center, $type cover, $inner 0%, $outer 100% ); 28 | background: radial-gradient( center, $type cover, $inner 0%, $outer 100% ); 29 | } -------------------------------------------------------------------------------- /reveal/css/theme/template/settings.scss: -------------------------------------------------------------------------------- 1 | // Base settings for all themes that can optionally be 2 | // overridden by the super-theme 3 | 4 | // Background of the presentation 5 | $backgroundColor: #2b2b2b; 6 | 7 | // Primary/body text 8 | $mainFont: 'Lato', sans-serif; 9 | $mainFontSize: 40px; 10 | $mainColor: #eee; 11 | 12 | // Accent text 13 | $accentColor: #eee; 14 | 15 | // Vertical spacing between blocks of text 16 | $blockMargin: 20px; 17 | 18 | // Headings 19 | $headingMargin: 0 0 $blockMargin 0; 20 | $headingFont: 'League Gothic', Impact, sans-serif; 21 | $headingColor: $accentColor; 22 | $headingLineHeight: 1.2; 23 | $headingLetterSpacing: normal; 24 | $headingTextTransform: uppercase; 25 | $headingTextShadow: none; 26 | $headingFontWeight: normal; 27 | $heading1TextShadow: $headingTextShadow; 28 | 29 | $heading1Size: 3.77em; 30 | $heading2Size: 2.11em; 31 | $heading3Size: 1.55em; 32 | $heading4Size: 1.00em; 33 | 34 | // Links and actions 35 | $linkColor: #13DAEC; 36 | $linkColorHover: lighten( $linkColor, 20% ); 37 | 38 | // Text selection 39 | $selectionBackgroundColor: #FF5E99; 40 | $selectionColor: #fff; 41 | 42 | // Generates the presentation background, can be overridden 43 | // to return a background image or gradient 44 | @mixin bodyBackground() { 45 | background: $backgroundColor; 46 | } -------------------------------------------------------------------------------- /reveal/css/theme/template/theme.scss: -------------------------------------------------------------------------------- 1 | // Base theme template for reveal.js 2 | 3 | /********************************************* 4 | * GLOBAL STYLES 5 | *********************************************/ 6 | 7 | body { 8 | @include bodyBackground(); 9 | background-color: $backgroundColor; 10 | } 11 | 12 | .reveal { 13 | font-family: $mainFont; 14 | font-size: $mainFontSize; 15 | font-weight: normal; 16 | color: $mainColor; 17 | } 18 | 19 | ::selection { 20 | color: $selectionColor; 21 | background: $selectionBackgroundColor; 22 | text-shadow: none; 23 | } 24 | 25 | ::-moz-selection { 26 | color: $selectionColor; 27 | background: $selectionBackgroundColor; 28 | text-shadow: none; 29 | } 30 | 31 | .reveal .slides { 32 | text-align: left; 33 | } 34 | 35 | .reveal .slides>section, 36 | .reveal .slides>section>section { 37 | line-height: 1.3; 38 | font-weight: inherit; 39 | } 40 | 41 | /********************************************* 42 | * HEADERS 43 | *********************************************/ 44 | 45 | .reveal h1, 46 | .reveal h2, 47 | .reveal h3, 48 | .reveal h4, 49 | .reveal h5, 50 | .reveal h6 { 51 | margin: $headingMargin; 52 | color: $headingColor; 53 | 54 | font-family: $headingFont; 55 | font-weight: $headingFontWeight; 56 | line-height: $headingLineHeight; 57 | letter-spacing: $headingLetterSpacing; 58 | 59 | text-transform: $headingTextTransform; 60 | text-shadow: $headingTextShadow; 61 | 62 | word-wrap: break-word; 63 | } 64 | 65 | .reveal h1 {font-size: $heading1Size; text-align: center; } 66 | .reveal h2 {font-size: $heading2Size; text-align: center; } 67 | .reveal h3 {font-size: $heading3Size; } 68 | .reveal h4 {font-size: $heading4Size; } 69 | 70 | .reveal h1 { 71 | text-shadow: $heading1TextShadow; 72 | } 73 | 74 | 75 | /********************************************* 76 | * OTHER 77 | *********************************************/ 78 | 79 | .reveal p { 80 | margin: $blockMargin 0; 81 | line-height: 1.3; 82 | } 83 | 84 | /* Ensure certain elements are never larger than the slide itself */ 85 | .reveal img, 86 | .reveal video, 87 | .reveal iframe { 88 | max-width: 95%; 89 | max-height: 95%; 90 | } 91 | .reveal strong, 92 | .reveal b { 93 | font-weight: bold; 94 | } 95 | 96 | .reveal em { 97 | font-style: italic; 98 | } 99 | 100 | .reveal ol, 101 | .reveal dl, 102 | .reveal ul { 103 | display: inline-block; 104 | text-align: left; 105 | margin: 0 0 $blockMargin 1em; 106 | } 107 | 108 | .reveal ol ol, 109 | .reveal dl ol, 110 | .reveal ul ol, 111 | .reveal ol dl, 112 | .reveal dl dl, 113 | .reveal ul dl, 114 | .reveal ol ul, 115 | .reveal dl ul, 116 | .reveal ul ul { 117 | margin-bottom: 0; 118 | } 119 | 120 | .reveal ul { 121 | list-style-type: none; 122 | } 123 | 124 | .reveal ol { 125 | list-style-type: none; 126 | counter-reset: li 127 | } 128 | 129 | .reveal ol > li:before { 130 | content: "." counter(li); 131 | color: $accentColor; 132 | display: inline-block; 133 | width: 1em; 134 | margin-left: -1.5em; 135 | margin-right: 0.5em; 136 | text-align: right; 137 | direction: rtl 138 | } 139 | 140 | .reveal ol > li { 141 | counter-increment: li 142 | } 143 | 144 | .reveal ul > li:before { 145 | content: "\2022"; 146 | color: $accentColor; 147 | display: inline-block; 148 | width: 1em; 149 | margin-left: -1em 150 | } 151 | 152 | .reveal ul ul, 153 | .reveal ul ol, 154 | .reveal ol ol, 155 | .reveal ol ul { 156 | display: block; 157 | margin-left: 40px; 158 | } 159 | 160 | .reveal dt { 161 | font-weight: bold; 162 | } 163 | 164 | .reveal dd { 165 | margin-left: 40px; 166 | } 167 | 168 | .reveal q, 169 | .reveal blockquote { 170 | quotes: none; 171 | } 172 | 173 | .reveal blockquote { 174 | display: block; 175 | position: relative; 176 | width: auto; 177 | margin: $blockMargin auto; 178 | padding: 5px 20px 5px 20px; 179 | font-style: italic; 180 | background: rgba(127, 127, 127, 0.3); 181 | } 182 | .reveal blockquote p:first-child, 183 | .reveal blockquote p:last-child { 184 | display: block; 185 | } 186 | 187 | .reveal q { 188 | font-style: italic; 189 | } 190 | 191 | .reveal pre { 192 | display: block; 193 | position: relative; 194 | width: auto; 195 | margin: $blockMargin auto; 196 | 197 | text-align: left; 198 | font-size: 0.70em; 199 | font-family: $codeFont; 200 | line-height: 1.2em; 201 | 202 | word-wrap: break-word; 203 | 204 | border: 0px solid rgba(0,0,0,0.3); 205 | } 206 | .reveal code { 207 | font-family: $codeFont; 208 | color: $accentColor; 209 | } 210 | 211 | .reveal pre code { 212 | display: block; 213 | padding: 5px; 214 | overflow: auto; 215 | max-height: 400px; 216 | word-wrap: normal; 217 | } 218 | 219 | .reveal table { 220 | margin: auto; 221 | border-collapse: collapse; 222 | border-spacing: 0; 223 | } 224 | 225 | .reveal table th { 226 | font-weight: bold; 227 | } 228 | 229 | .reveal table th, 230 | .reveal table td { 231 | text-align: left; 232 | padding: 0.2em 0.5em 0.2em 0.5em; 233 | border-bottom: 1px solid; 234 | } 235 | 236 | .reveal table th[align="center"], 237 | .reveal table td[align="center"] { 238 | text-align: center; 239 | } 240 | 241 | .reveal table th[align="right"], 242 | .reveal table td[align="right"] { 243 | text-align: right; 244 | } 245 | 246 | .reveal table tbody tr:last-child th, 247 | .reveal table tbody tr:last-child td { 248 | border-bottom: none; 249 | } 250 | 251 | .reveal sup { 252 | vertical-align: super; 253 | } 254 | .reveal sub { 255 | vertical-align: sub; 256 | } 257 | 258 | .reveal small { 259 | display: inline-block; 260 | font-size: 0.6em; 261 | line-height: 1.2em; 262 | vertical-align: top; 263 | } 264 | 265 | .reveal small * { 266 | vertical-align: top; 267 | } 268 | 269 | .reveal .left { 270 | text-align: left !important; 271 | } 272 | 273 | .reveal .center { 274 | text-align: center !important; 275 | } 276 | 277 | .reveal .right { 278 | text-align: right !important; 279 | } 280 | 281 | .reveal em { 282 | color: $accentColor; 283 | } 284 | 285 | /********************************************* 286 | * LINKS 287 | *********************************************/ 288 | 289 | .reveal a { 290 | color: $linkColor; 291 | text-decoration: none; 292 | 293 | -webkit-transition: color .15s ease; 294 | -moz-transition: color .15s ease; 295 | transition: color .15s ease; 296 | } 297 | .reveal a:hover { 298 | color: $linkColorHover; 299 | 300 | text-shadow: none; 301 | border: none; 302 | } 303 | 304 | .reveal .roll span:after { 305 | color: #fff; 306 | background: darken( $linkColor, 15% ); 307 | } 308 | 309 | 310 | /********************************************* 311 | * IMAGES 312 | *********************************************/ 313 | 314 | .reveal section img { 315 | margin: 15px 0px; 316 | background: transparent; 317 | border: 0px solid $mainColor; 318 | } 319 | 320 | .reveal section img.plain { 321 | border: 0; 322 | box-shadow: none; 323 | } 324 | 325 | .reveal a img { 326 | -webkit-transition: all .15s linear; 327 | -moz-transition: all .15s linear; 328 | transition: all .15s linear; 329 | } 330 | 331 | .reveal a:hover img { 332 | background: rgba(255,255,255,0.2); 333 | border-color: $linkColor; 334 | } 335 | 336 | 337 | /********************************************* 338 | * NAVIGATION CONTROLS 339 | *********************************************/ 340 | 341 | .reveal .controls .navigate-left, 342 | .reveal .controls .navigate-left.enabled { 343 | border-right-color: $linkColor; 344 | } 345 | 346 | .reveal .controls .navigate-right, 347 | .reveal .controls .navigate-right.enabled { 348 | border-left-color: $linkColor; 349 | } 350 | 351 | .reveal .controls .navigate-up, 352 | .reveal .controls .navigate-up.enabled { 353 | border-bottom-color: $linkColor; 354 | } 355 | 356 | .reveal .controls .navigate-down, 357 | .reveal .controls .navigate-down.enabled { 358 | border-top-color: $linkColor; 359 | } 360 | 361 | .reveal .controls .navigate-left.enabled:hover { 362 | border-right-color: $linkColorHover; 363 | } 364 | 365 | .reveal .controls .navigate-right.enabled:hover { 366 | border-left-color: $linkColorHover; 367 | } 368 | 369 | .reveal .controls .navigate-up.enabled:hover { 370 | border-bottom-color: $linkColorHover; 371 | } 372 | 373 | .reveal .controls .navigate-down.enabled:hover { 374 | border-top-color: $linkColorHover; 375 | } 376 | 377 | 378 | /********************************************* 379 | * PROGRESS BAR 380 | *********************************************/ 381 | 382 | .reveal .progress { 383 | background: rgba(0,0,0,0.2); 384 | } 385 | .reveal .progress span { 386 | background: $accentColor; 387 | 388 | -webkit-transition: width 800ms cubic-bezier(0.260, 0.860, 0.440, 0.985); 389 | -moz-transition: width 800ms cubic-bezier(0.260, 0.860, 0.440, 0.985); 390 | transition: width 800ms cubic-bezier(0.260, 0.860, 0.440, 0.985); 391 | } 392 | 393 | 394 | -------------------------------------------------------------------------------- /reveal/initialize.js: -------------------------------------------------------------------------------- 1 | // Full list of configuration options available at: 2 | // https://github.com/hakimel/reveal.js#configuration 3 | 4 | var defaultRevealConfiguration = { 5 | controls: true, 6 | progress: true, 7 | slideNumber: true, 8 | history: true, 9 | keyboard: true, 10 | overview: false, 11 | center: true, 12 | showNotes: false, 13 | width: 960, 14 | height: 700, 15 | margin: 0.1, 16 | minScale: 0.2, 17 | maxScale: 1.5, 18 | markdown: { 19 | configureRenderer: function (renderer) { 20 | var widthAndHeightRegExp = /\s+\=\s*([0-9]+\%?)?((x)|(x([0-9]+\%?)))?\s*$/i; 21 | renderer.image = function (href, title, text) { 22 | var out = '' + text + '" : ">"; 36 | return out; 37 | } 38 | }, 39 | breaks: true, 40 | gfm: true, 41 | smartList: true, 42 | smartypants: true 43 | }, 44 | dependencies: [ 45 | { src: 'reveal/plugin/markdown-modified/marked.js' }, 46 | { src: 'reveal/plugin/markdown-modified/markdown.js' }, 47 | { src: 'reveal/plugin/notes/notes.js', async: true }, 48 | { src: 'reveal/plugin/highlight/highlight.js', async: true, callback: function () { hljs.initHighlightingOnLoad(); } }, 49 | { src: 'reveal/plugin/menu-modified/menu.js' } 50 | ], 51 | menu: { 52 | side: 'left', 53 | numbers: false, 54 | titleSelector: 'h1, h2, h3', 55 | hideMissingTitles: true, 56 | markers: false, 57 | custom: false, 58 | themes: [ 59 | { name: 'Светлая', theme: 'reveal/css/theme/kontur-light.css' }, 60 | { name: 'Темная', theme: 'reveal/css/theme/kontur-dark.css' } 61 | ], 62 | transitions: false, 63 | openButton: true, 64 | openSlideNumber: true, 65 | keyboard: true 66 | } 67 | }; 68 | 69 | Reveal.addEventListener('ready', function (event) { 70 | window.revealReady && window.revealReady(event); 71 | }); 72 | 73 | Reveal.initialize(window.revealConfigure 74 | ? revealConfigure(defaultRevealConfiguration) 75 | : defaultRevealConfiguration); -------------------------------------------------------------------------------- /reveal/js/classList.js: -------------------------------------------------------------------------------- 1 | /*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js*/ 2 | if(typeof document!=="undefined"&&!("classList" in document.createElement("a"))){(function(j){var a="classList",f="prototype",m=(j.HTMLElement||j.Element)[f],b=Object,k=String[f].trim||function(){return this.replace(/^\s+|\s+$/g,"")},c=Array[f].indexOf||function(q){var p=0,o=this.length;for(;p=4.0.0" 23 | }, 24 | "dependencies": { 25 | "express": "~4.14.0", 26 | "grunt-cli": "~1.2.0", 27 | "mustache": "~2.2.1", 28 | "socket.io": "^1.4.8" 29 | }, 30 | "devDependencies": { 31 | "grunt": "~1.0.1", 32 | "grunt-autoprefixer": "~3.0.3", 33 | "grunt-contrib-connect": "~0.11.2", 34 | "grunt-contrib-cssmin": "~0.14.0", 35 | "grunt-contrib-jshint": "~0.11.3", 36 | "grunt-contrib-qunit": "~1.2.0", 37 | "grunt-contrib-uglify": "~0.9.2", 38 | "grunt-contrib-watch": "~1.0.0", 39 | "grunt-sass": "~1.2.0", 40 | "grunt-retire": "~0.3.10", 41 | "grunt-zip": "~0.17.1", 42 | "node-sass": "~3.13.0" 43 | }, 44 | "license": "MIT" 45 | } 46 | -------------------------------------------------------------------------------- /reveal/plugin/markdown-modified/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | reveal.js - Markdown Demo 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 |
20 | 21 | 22 |
23 | 24 | 25 |
26 | 36 |
37 | 38 | 39 |
40 | 54 |
55 | 56 | 57 |
58 | 69 |
70 | 71 | 72 |
73 | 77 |
78 | 79 | 80 |
81 | 86 |
87 | 88 | 89 |
90 | 100 |
101 | 102 |
103 |
104 | 105 | 106 | 107 | 108 | 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /reveal/plugin/markdown-modified/example.md: -------------------------------------------------------------------------------- 1 | # Markdown Demo 2 | 3 | 4 | 5 | ## External 1.1 6 | 7 | Content 1.1 8 | 9 | Note: This will only appear in the speaker notes window. 10 | 11 | 12 | ## External 1.2 13 | 14 | Content 1.2 15 | 16 | 17 | 18 | ## External 2 19 | 20 | Content 2.1 21 | 22 | 23 | 24 | ## External 3.1 25 | 26 | Content 3.1 27 | 28 | 29 | ## External 3.2 30 | 31 | Content 3.2 32 | -------------------------------------------------------------------------------- /reveal/plugin/menu-modified/font-awesome-4.3.0/desktop.ini: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontur-web-courses/react/4e7a0a120b58c4fa650f8e68da0e1220f7513362/reveal/plugin/menu-modified/font-awesome-4.3.0/desktop.ini -------------------------------------------------------------------------------- /reveal/plugin/menu-modified/font-awesome-4.3.0/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontur-web-courses/react/4e7a0a120b58c4fa650f8e68da0e1220f7513362/reveal/plugin/menu-modified/font-awesome-4.3.0/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /reveal/plugin/menu-modified/font-awesome-4.3.0/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontur-web-courses/react/4e7a0a120b58c4fa650f8e68da0e1220f7513362/reveal/plugin/menu-modified/font-awesome-4.3.0/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /reveal/plugin/menu-modified/font-awesome-4.3.0/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontur-web-courses/react/4e7a0a120b58c4fa650f8e68da0e1220f7513362/reveal/plugin/menu-modified/font-awesome-4.3.0/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /reveal/plugin/menu-modified/font-awesome-4.3.0/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontur-web-courses/react/4e7a0a120b58c4fa650f8e68da0e1220f7513362/reveal/plugin/menu-modified/font-awesome-4.3.0/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /reveal/plugin/menu-modified/font-awesome-4.3.0/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontur-web-courses/react/4e7a0a120b58c4fa650f8e68da0e1220f7513362/reveal/plugin/menu-modified/font-awesome-4.3.0/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /reveal/plugin/menu-modified/lib/bowser.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bowser - a browser detector 3 | * https://github.com/ded/bowser 4 | * MIT License | (c) Dustin Diaz 2014 5 | */ 6 | !function(e,t){typeof module!="undefined"&&module.exports?module.exports.browser=t():typeof define=="function"&&define.amd?define(t):this[e]=t()}("bowser",function(){function t(t){function n(e){var n=t.match(e);return n&&n.length>1&&n[1]||""}function r(e){var n=t.match(e);return n&&n.length>1&&n[2]||""}var i=n(/(ipod|iphone|ipad)/i).toLowerCase(),s=/like android/i.test(t),o=!s&&/android/i.test(t),u=n(/edge\/(\d+(\.\d+)?)/i),a=n(/version\/(\d+(\.\d+)?)/i),f=/tablet/i.test(t),l=!f&&/[^-]mobi/i.test(t),c;/opera|opr/i.test(t)?c={name:"Opera",opera:e,version:a||n(/(?:opera|opr)[\s\/](\d+(\.\d+)?)/i)}:/yabrowser/i.test(t)?c={name:"Yandex Browser",yandexbrowser:e,version:a||n(/(?:yabrowser)[\s\/](\d+(\.\d+)?)/i)}:/windows phone/i.test(t)?(c={name:"Windows Phone",windowsphone:e},u?(c.msedge=e,c.version=u):(c.msie=e,c.version=n(/iemobile\/(\d+(\.\d+)?)/i))):/msie|trident/i.test(t)?c={name:"Internet Explorer",msie:e,version:n(/(?:msie |rv:)(\d+(\.\d+)?)/i)}:/chrome.+? edge/i.test(t)?c={name:"Microsoft Edge",msedge:e,version:u}:/chrome|crios|crmo/i.test(t)?c={name:"Chrome",chrome:e,version:n(/(?:chrome|crios|crmo)\/(\d+(\.\d+)?)/i)}:i?(c={name:i=="iphone"?"iPhone":i=="ipad"?"iPad":"iPod"},a&&(c.version=a)):/sailfish/i.test(t)?c={name:"Sailfish",sailfish:e,version:n(/sailfish\s?browser\/(\d+(\.\d+)?)/i)}:/seamonkey\//i.test(t)?c={name:"SeaMonkey",seamonkey:e,version:n(/seamonkey\/(\d+(\.\d+)?)/i)}:/firefox|iceweasel/i.test(t)?(c={name:"Firefox",firefox:e,version:n(/(?:firefox|iceweasel)[ \/](\d+(\.\d+)?)/i)},/\((mobile|tablet);[^\)]*rv:[\d\.]+\)/i.test(t)&&(c.firefoxos=e)):/silk/i.test(t)?c={name:"Amazon Silk",silk:e,version:n(/silk\/(\d+(\.\d+)?)/i)}:o?c={name:"Android",version:a}:/phantom/i.test(t)?c={name:"PhantomJS",phantom:e,version:n(/phantomjs\/(\d+(\.\d+)?)/i)}:/blackberry|\bbb\d+/i.test(t)||/rim\stablet/i.test(t)?c={name:"BlackBerry",blackberry:e,version:a||n(/blackberry[\d]+\/(\d+(\.\d+)?)/i)}:/(web|hpw)os/i.test(t)?(c={name:"WebOS",webos:e,version:a||n(/w(?:eb)?osbrowser\/(\d+(\.\d+)?)/i)},/touchpad\//i.test(t)&&(c.touchpad=e)):/bada/i.test(t)?c={name:"Bada",bada:e,version:n(/dolfin\/(\d+(\.\d+)?)/i)}:/tizen/i.test(t)?c={name:"Tizen",tizen:e,version:n(/(?:tizen\s?)?browser\/(\d+(\.\d+)?)/i)||a}:/safari/i.test(t)?c={name:"Safari",safari:e,version:a}:c={name:n(/^(.*)\/(.*) /),version:r(/^(.*)\/(.*) /)},!c.msedge&&/(apple)?webkit/i.test(t)?(c.name=c.name||"Webkit",c.webkit=e,!c.version&&a&&(c.version=a)):!c.opera&&/gecko\//i.test(t)&&(c.name=c.name||"Gecko",c.gecko=e,c.version=c.version||n(/gecko\/(\d+(\.\d+)?)/i)),!c.msedge&&(o||c.silk)?c.android=e:i&&(c[i]=e,c.ios=e);var h="";c.windowsphone?h=n(/windows phone (?:os)?\s?(\d+(\.\d+)*)/i):i?(h=n(/os (\d+([_\s]\d+)*) like mac os x/i),h=h.replace(/[_\s]/g,".")):o?h=n(/android[ \/-](\d+(\.\d+)*)/i):c.webos?h=n(/(?:web|hpw)os\/(\d+(\.\d+)*)/i):c.blackberry?h=n(/rim\stablet\sos\s(\d+(\.\d+)*)/i):c.bada?h=n(/bada\/(\d+(\.\d+)*)/i):c.tizen&&(h=n(/tizen[\/\s](\d+(\.\d+)*)/i)),h&&(c.osversion=h);var p=h.split(".")[0];if(f||i=="ipad"||o&&(p==3||p==4&&!l)||c.silk)c.tablet=e;else if(l||i=="iphone"||i=="ipod"||o||c.blackberry||c.webos||c.bada)c.mobile=e;return c.msedge||c.msie&&c.version>=10||c.yandexbrowser&&c.version>=15||c.chrome&&c.version>=20||c.firefox&&c.version>=20||c.safari&&c.version>=6||c.opera&&c.version>=10||c.ios&&c.osversion&&c.osversion.split(".")[0]>=6||c.blackberry&&c.version>=10.1?c.a=e:c.msie&&c.version<10||c.chrome&&c.version<20||c.firefox&&c.version<20||c.safari&&c.version<6||c.opera&&c.version<10||c.ios&&c.osversion&&c.osversion.split(".")[0]<6?c.c=e:c.x=e,c}var e=!0,n=t(typeof navigator!="undefined"?navigator.userAgent:"");return n.test=function(e){for(var t=0;t li { 127 | display: table-cell; 128 | line-height: 150%; 129 | text-align: center; 130 | vertical-align: middle; 131 | cursor: pointer; 132 | color: #aaa; 133 | border-radius: 3px; 134 | } 135 | 136 | .reveal .slide-menu-toolbar > li.active-toolbar-button { 137 | color: white; 138 | text-shadow: 0 1px black; 139 | } 140 | 141 | .slide-menu-toolbar > li:hover { 142 | color: white; 143 | } 144 | 145 | /* 146 | * Panels 147 | */ 148 | .reveal .slide-menu-panel { 149 | position: absolute; 150 | width: 100%; 151 | visibility: hidden; 152 | height: calc(100% - 60px); 153 | overflow-x: hidden; 154 | overflow-y: auto; 155 | color: #AAA; 156 | } 157 | 158 | .reveal .slide-menu-panel.active-menu-panel { 159 | visibility: visible; 160 | } 161 | 162 | .reveal .slide-menu-panel h1, 163 | .reveal .slide-menu-panel h2, 164 | .reveal .slide-menu-panel h3, 165 | .reveal .slide-menu-panel h4, 166 | .reveal .slide-menu-panel h5, 167 | .reveal .slide-menu-panel h6 { 168 | margin: 20px 0 10px 0; 169 | color: #FFF; 170 | line-height: 1.2; 171 | letter-spacing: normal; 172 | text-shadow: none; 173 | } 174 | 175 | .reveal .slide-menu-panel h1 { 176 | font-size: 1.6em; 177 | } 178 | .reveal .slide-menu-panel h2 { 179 | font-size: 1.4em; 180 | } 181 | .reveal .slide-menu-panel h3 { 182 | font-size: 1.3em; 183 | } 184 | .reveal .slide-menu-panel h4 { 185 | font-size: 1.1em; 186 | } 187 | .reveal .slide-menu-panel h5 { 188 | font-size: 1em; 189 | } 190 | .reveal .slide-menu-panel h6 { 191 | font-size: 0.9em; 192 | } 193 | 194 | .reveal .slide-menu-panel p { 195 | margin: 10px 0 5px 0; 196 | } 197 | 198 | .reveal .slide-menu-panel a { 199 | color: #CCC; 200 | text-decoration: underline; 201 | } 202 | 203 | .reveal .slide-menu-panel a:hover { 204 | color: white; 205 | } 206 | 207 | .reveal .slide-menu-item a { 208 | text-decoration: none; 209 | } 210 | 211 | .reveal .slide-menu-custom-panel { 212 | width: calc(100% - 20px); 213 | padding-left: 10px; 214 | padding-right: 10px; 215 | } 216 | 217 | .reveal .slide-menu-custom-panel .slide-menu-items { 218 | width: calc(100% + 20px); 219 | margin-left: -10px; 220 | margin-right: 10px; 221 | } 222 | 223 | 224 | /* 225 | * Theme and Transitions buttons 226 | */ 227 | 228 | .reveal div[data-panel="Themes"] li, 229 | .reveal div[data-panel="Transitions"] li { 230 | display: block; 231 | text-align: left; 232 | cursor: pointer; 233 | color: #848484; 234 | } 235 | 236 | /* 237 | * Menu controls 238 | */ 239 | .reveal .slide-menu-button { 240 | position: fixed; 241 | left: 30px; 242 | bottom: 30px; 243 | z-index: 30; 244 | font-size: 24px; 245 | } 246 | 247 | /* 248 | * Menu overlay 249 | */ 250 | 251 | .reveal .slide-menu-overlay { 252 | position: fixed; 253 | z-index: 199; 254 | top: 0; 255 | left: 0; 256 | overflow: hidden; 257 | width: 0; 258 | height: 0; 259 | background-color: #000; 260 | opacity: 0; 261 | transition: opacity 0.3s, width 0s 0.3s, height 0s 0.3s; 262 | } 263 | 264 | .reveal .slide-menu-overlay.active { 265 | width: 100%; 266 | height: 100%; 267 | opacity: 0.7; 268 | transition: opacity 0.3s; 269 | } 270 | -------------------------------------------------------------------------------- /reveal/plugin/notes/notes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Handles opening of and synchronization with the reveal.js 3 | * notes window. 4 | * 5 | * Handshake process: 6 | * 1. This window posts 'connect' to notes window 7 | * - Includes URL of presentation to show 8 | * 2. Notes window responds with 'connected' when it is available 9 | * 3. This window proceeds to send the current presentation state 10 | * to the notes window 11 | */ 12 | var RevealNotes = (function() { 13 | 14 | function openNotes( notesFilePath ) { 15 | 16 | if( !notesFilePath ) { 17 | var jsFileLocation = document.querySelector('script[src$="notes.js"]').src; // this js file path 18 | jsFileLocation = jsFileLocation.replace(/notes\.js(\?.*)?$/, ''); // the js folder path 19 | notesFilePath = jsFileLocation + 'notes.html'; 20 | } 21 | 22 | var notesPopup = window.open( notesFilePath, 'reveal.js - Notes', 'width=1100,height=700' ); 23 | 24 | /** 25 | * Connect to the notes window through a postmessage handshake. 26 | * Using postmessage enables us to work in situations where the 27 | * origins differ, such as a presentation being opened from the 28 | * file system. 29 | */ 30 | function connect() { 31 | // Keep trying to connect until we get a 'connected' message back 32 | var connectInterval = setInterval( function() { 33 | notesPopup.postMessage( JSON.stringify( { 34 | namespace: 'reveal-notes', 35 | type: 'connect', 36 | url: window.location.protocol + '//' + window.location.host + window.location.pathname + window.location.search, 37 | state: Reveal.getState() 38 | } ), '*' ); 39 | }, 500 ); 40 | 41 | window.addEventListener( 'message', function( event ) { 42 | var data = JSON.parse( event.data ); 43 | if( data && data.namespace === 'reveal-notes' && data.type === 'connected' ) { 44 | clearInterval( connectInterval ); 45 | onConnected(); 46 | } 47 | } ); 48 | } 49 | 50 | /** 51 | * Posts the current slide data to the notes window 52 | */ 53 | function post() { 54 | 55 | var slideElement = Reveal.getCurrentSlide(), 56 | notesElement = slideElement.querySelector( 'aside.notes' ); 57 | 58 | var messageData = { 59 | namespace: 'reveal-notes', 60 | type: 'state', 61 | notes: '', 62 | markdown: false, 63 | whitespace: 'normal', 64 | state: Reveal.getState() 65 | }; 66 | 67 | // Look for notes defined in a slide attribute 68 | if( slideElement.hasAttribute( 'data-notes' ) ) { 69 | messageData.notes = slideElement.getAttribute( 'data-notes' ); 70 | messageData.whitespace = 'pre-wrap'; 71 | } 72 | 73 | // Look for notes defined in an aside element 74 | if( notesElement ) { 75 | messageData.notes = notesElement.innerHTML; 76 | messageData.markdown = typeof notesElement.getAttribute( 'data-markdown' ) === 'string'; 77 | } 78 | 79 | notesPopup.postMessage( JSON.stringify( messageData ), '*' ); 80 | 81 | } 82 | 83 | /** 84 | * Called once we have established a connection to the notes 85 | * window. 86 | */ 87 | function onConnected() { 88 | 89 | // Monitor events that trigger a change in state 90 | Reveal.addEventListener( 'slidechanged', post ); 91 | Reveal.addEventListener( 'fragmentshown', post ); 92 | Reveal.addEventListener( 'fragmenthidden', post ); 93 | Reveal.addEventListener( 'overviewhidden', post ); 94 | Reveal.addEventListener( 'overviewshown', post ); 95 | Reveal.addEventListener( 'paused', post ); 96 | Reveal.addEventListener( 'resumed', post ); 97 | 98 | // Post the initial state 99 | post(); 100 | 101 | } 102 | 103 | connect(); 104 | 105 | } 106 | 107 | if( !/receiver/i.test( window.location.search ) ) { 108 | 109 | // If the there's a 'notes' query set, open directly 110 | if( window.location.search.match( /(\?|\&)notes/gi ) !== null ) { 111 | openNotes(); 112 | } 113 | 114 | // Open the notes when the 's' key is hit 115 | document.addEventListener( 'keydown', function( event ) { 116 | // Disregard the event if the target is editable or a 117 | // modifier is present 118 | if ( document.querySelector( ':focus' ) !== null || event.shiftKey || event.altKey || event.ctrlKey || event.metaKey ) return; 119 | 120 | // Disregard the event if keyboard is disabled 121 | if ( Reveal.getConfig().keyboard === false ) return; 122 | 123 | if( event.keyCode === 83 ) { 124 | event.preventDefault(); 125 | openNotes(); 126 | } 127 | }, false ); 128 | 129 | // Show our keyboard shortcut in the reveal.js help overlay 130 | if( window.Reveal ) Reveal.registerKeyboardShortcut( 'S', 'Speaker notes view' ); 131 | 132 | } 133 | 134 | return { open: openNotes }; 135 | 136 | })(); 137 | -------------------------------------------------------------------------------- /tasks/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": ["@babel/plugin-proposal-class-properties"] 4 | } 5 | -------------------------------------------------------------------------------- /tasks/.eslintignore: -------------------------------------------------------------------------------- 1 | webpack.config.js 2 | build/* 3 | -------------------------------------------------------------------------------- /tasks/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:react/recommended" 5 | ], 6 | "parser": "babel-eslint", 7 | "rules": { 8 | "no-console": "off", 9 | "no-useless-escape": "off", 10 | "arrow-body-style": "off", 11 | "no-useless-computed-key": "off", 12 | "object-shorthand": "off", 13 | "prefer-template": "off", 14 | "no-debugger": "off", 15 | "no-unused-vars": "off" 16 | }, 17 | "env": { 18 | "browser": true, 19 | "commonjs": true, 20 | "node": true, 21 | "es6": true 22 | }, 23 | "parserOptions": { 24 | "ecmaVersion": 2015, 25 | "sourceType": "module", 26 | "ecmaFeatures": { 27 | "jsx": true 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /tasks/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | node_modules 3 | build 4 | -------------------------------------------------------------------------------- /tasks/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React 6 | 7 | 27 | 28 | 29 | 30 |
31 |

React

32 |
33 |
34 | 35 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /tasks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-tasks", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "webpack --mode development && exit 0", 8 | "start": "webpack-dev-server --mode development" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@babel/core": "^7.6.2", 15 | "@babel/plugin-proposal-class-properties": "^7.5.5", 16 | "@babel/preset-env": "^7.6.2", 17 | "@babel/preset-react": "^7.0.0", 18 | "babel-eslint": "^8.2.2", 19 | "babel-loader": "^8.0.6", 20 | "css-loader": "^0.28.11", 21 | "eslint": "^4.19.1", 22 | "eslint-loader": "^2.0.0", 23 | "eslint-plugin-import": "^2.9.0", 24 | "eslint-plugin-react": "^7.7.0", 25 | "prop-types": "^15.6.1", 26 | "regenerator-runtime": "^0.13.3", 27 | "style-loader": "^0.20.3", 28 | "webpack": "^4.41.0", 29 | "webpack-cli": "^3.3.9", 30 | "webpack-dev-server": "^3.8.1" 31 | }, 32 | "dependencies": { 33 | "react": "^16.9.0", 34 | "react-dom": "^16.9.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tasks/src/1.1.SimpleHtml/.solved/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import '../styles.css'; 4 | 5 | ReactDom.render( 6 |
7 |

Добавление отзыва

8 |
9 |
10 |
11 | 12 |
13 | 14 |
15 |
16 |
17 | 18 |
19 | 20 |
21 |
22 |
23 | 24 |
25 | 26 |
27 |
28 |
29 | 30 |
31 | 37 |
38 |
39 |
40 | 41 |
42 | 43 |
44 |
45 |
46 | 47 |
48 |
49 |
50 | 51 | 52 | -------------------------------------------------------------------------------- /tasks/src/1.1.SimpleHtml/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SimpleHtml 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tasks/src/1.1.SimpleHtml/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import './styles.css'; 4 | 5 | /** 6 | 1. Отрендери эту форму из from.html с использованием React. 7 | 8 | Помни про: 9 | - Обязательное закрытие одиночных тэгов: 10 | -

11 | - 12 | - Переименование некоторых атрибутов: 13 | - class → className 14 | - for → htmlFor 15 | - checked → defaultChecked 16 | - value → defaultValue 17 | - {} и "" для передачи значений 18 | - Оформление style в виде объекта:
19 | - Общее правило именование атрибутов и свойств — camelCase 20 | 21 | 2. Убедись, что новая форма выглядит также как старая, поля редактируются, 22 | флажок переключается при нажатии на слово «Согласие» 23 | */ 24 | 25 | ReactDom.render( 26 |
27 |
, 28 | document.getElementById('app') 29 | ); 30 | -------------------------------------------------------------------------------- /tasks/src/1.1.SimpleHtml/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: arial; 3 | } 4 | 5 | body { 6 | padding: 0; 7 | margin: 0; 8 | width: 100%; 9 | height: 100%; 10 | background-color: #e5e5e5; 11 | } 12 | 13 | .form { 14 | color: rgba(0, 0, 0, 0.87); 15 | background-color: rgb(255, 255, 255); 16 | box-shadow: rgba(0, 0, 0, 0.12) 0px 1px 6px, rgba(0, 0, 0, 0.12) 0px 1px 4px; 17 | border-radius: 2px; 18 | padding: 15px; 19 | margin: 15px; 20 | } 21 | 22 | .caption { 23 | display: inline-block; 24 | width: 80px; 25 | display: inline-block; 26 | vertical-align: top; 27 | } 28 | 29 | .row { 30 | padding: 5px; 31 | } 32 | 33 | .button { 34 | background: linear-gradient(#2899ea, #167ac1); 35 | color: #fff; 36 | height: 34px; 37 | border-radius: 2px 2px 2px 2px; 38 | padding: 0 15px; 39 | border: 0; 40 | } 41 | 42 | .button:hover { 43 | background: linear-gradient(#0087d5, #167ac1); 44 | cursor: pointer; 45 | } 46 | -------------------------------------------------------------------------------- /tasks/src/1.2.Button/.solved/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import '../styles.css'; 4 | 5 | ReactDom.render( 6 |
7 |
8 |
Нажми отправить
9 | alert('Отправлено')} 14 | /> 15 |
16 |
, 17 | document.getElementById('app') 18 | ); 19 | -------------------------------------------------------------------------------- /tasks/src/1.2.Button/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Button 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tasks/src/1.2.Button/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import './styles.css'; 4 | 5 | /** 6 | Напиши обработчик нажатия на кнопку. 7 | При нажатии должно выводиться диалоговое окно с сообщением «Отправлено». 8 | */ 9 | 10 | ReactDom.render( 11 |
12 |
13 |
Нажми отправить
14 | 15 |
16 |
, 17 | document.getElementById('app') 18 | ); 19 | 20 | /** 21 | Подсказки: 22 | - alert(msg) — создает простое диалоговое окно с сообщением msg 23 | - Компоненты React, соответствующие HTML, поддерживают атрибуты onClick, onChange и т.д. 24 | В них можно передать функцию-обработчик события. 25 | - Стрелочные функции: (x, y) => { return x + y; } — «непроизводительный», 26 | но быстрый способ написать обработчик событий 27 | */ 28 | -------------------------------------------------------------------------------- /tasks/src/1.2.Button/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: arial; 3 | } 4 | 5 | .root { 6 | margin: auto; 7 | width: 600px; 8 | } 9 | 10 | .form { 11 | padding: 10px; 12 | background: #fda; 13 | text-align: center; 14 | } 15 | 16 | .button { 17 | background: linear-gradient(#2899ea, #167ac1); 18 | color: #fff; 19 | height: 34px; 20 | border-radius: 2px 2px 2px 2px; 21 | padding: 0 15px; 22 | border: 0; 23 | } 24 | 25 | .button:hover { 26 | background: linear-gradient(#0087d5, #167ac1); 27 | cursor: pointer; 28 | } 29 | -------------------------------------------------------------------------------- /tasks/src/1.3.Input/.solved/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import '../styles.css'; 4 | 5 | let userName = 'По умолчанию'; 6 | 7 | ReactDom.render( 8 |
9 |
10 |
11 | 12 |
13 | { 18 | userName = event.target.value; 19 | }} 20 | onBlur={() => alert(`userName: ${userName}`)} 21 | /> 22 |
23 |
, 24 | document.getElementById('app') 25 | ); 26 | -------------------------------------------------------------------------------- /tasks/src/1.3.Input/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Input 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tasks/src/1.3.Input/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import './styles.css'; 4 | 5 | /** 6 | Сделай так, чтобы в переменной userName сохранялось введенное пользователем значение. 7 | */ 8 | 9 | let userName = 'По умолчанию'; 10 | 11 | let mydom = ( 12 |
13 |
14 |
15 | 16 |
17 | { 22 | const target = event.target; 23 | debugger; 24 | }} 25 | onBlur={() => alert(`userName: ${userName}`)} 26 | /> 27 |
28 |
29 | ); 30 | 31 | ReactDom.render(mydom, document.getElementById('app')); 32 | 33 | /** 34 | Подсказки: 35 | - Chrome DevTools содержит прекрасный отладчик. Открывается через Ctrl+Shift+I. 36 | - Инструкция debugger останавливает отладчик, если открыты DevTools. 37 | - Посмотри, что приходит первым аргументом в обработчик onChange. 38 | - onBlur вызывается при потере фокуса. 39 | Фокус - это куда будет осуществляться ввод с клавиатуры. 40 | Часто элементы с фокусом подсвечивают. 41 | */ 42 | -------------------------------------------------------------------------------- /tasks/src/1.3.Input/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: arial; 3 | } 4 | 5 | .root { 6 | margin: auto; 7 | width: 600px; 8 | } 9 | 10 | .form { 11 | padding: 10px; 12 | background: #fda; 13 | text-align: center; 14 | } 15 | 16 | .button { 17 | background: linear-gradient(#2899ea, #167ac1); 18 | color: #fff; 19 | height: 34px; 20 | border-radius: 2px 2px 2px 2px; 21 | padding: 0 15px; 22 | border: 0; 23 | } 24 | 25 | .button:hover { 26 | background: linear-gradient(#0087d5, #167ac1); 27 | cursor: pointer; 28 | } 29 | 30 | input[type="text"] { 31 | padding: 5px; 32 | margin: 10px 0; 33 | border: 1px solid #aaa; 34 | border-radius: 3px; 35 | height: 24px; 36 | } 37 | 38 | input[type="text"]:focus { 39 | border: 1px solid #555; 40 | color: #000000; 41 | } 42 | -------------------------------------------------------------------------------- /tasks/src/2.1.ExtractFunction/.solved/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import '../styles.css'; 4 | 5 | function renderLot() { 6 | return ( 7 |
8 |
Форма для выпекания
9 |
Идеальна для приготовления десертов!
10 |
11 | ); 12 | } 13 | 14 | function renderPost(author, time, message) { 15 | return ( 16 |
17 |
18 | {author} 19 |
20 | {time} 21 |
22 |
{message}
23 |
24 | ); 25 | } 26 | 27 | ReactDom.render( 28 |
29 | {renderLot()} 30 |
31 | {renderPost( 32 | 'Парень не промах', 33 | '2 часа назад', 34 | 'Попробую с удовольствием ;)' 35 | )} 36 | {renderPost( 37 | 'Милая девушка', 38 | '3 часа назад', 39 | 'Можно использовать для выпекания чизкейков :)' 40 | )} 41 |
42 |
, 43 | document.getElementById('app') 44 | ); 45 | -------------------------------------------------------------------------------- /tasks/src/2.1.ExtractFunction/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ExtractFunction 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tasks/src/2.1.ExtractFunction/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import './styles.css'; 4 | 5 | /** 6 | Выдели метод отрисовки лота (renderLot), метод отрисовки поста (renderPost) и используй их. 7 | */ 8 | 9 | ReactDom.render( 10 |
11 |
12 |
Форма для выпекания
13 |
Идеальна для приготовления десертов!
14 |
15 |
16 |
17 |
18 | Парень не промах 19 |
20 | 2 часа назад 21 |
22 |
Попробую с удовольствием ;)
23 |
24 |
25 |
26 | Милая девушка 27 |
28 | 3 часа назад 29 |
30 |
31 | Можно использовать для выпекания чизкейков :) 32 |
33 |
34 |
35 |
, 36 | document.getElementById('app') 37 | ); 38 | 39 | /** 40 | Подсказки: 41 | - Чтобы вставить какое-то значение из JavaScript в верстку используй фигурные скобки: 42 |
{surname + ' ' + name}
43 | - Воспринимай тэг верстки как литерал, описывающий значение некоторого типа данных. 44 | - Это значение можно положить в переменную или вернуть: 45 | const label = Надпись; 46 | - Из эстетических соображений возвращаемый тэг часто оборачивается в круглые скобки: 47 | return ( 48 | Надпись 49 | ); 50 | - Используй автоформатирование кода. Например, в Visual Studio Code оно вызывается сочетанием Shift+Alt+F 51 | */ 52 | -------------------------------------------------------------------------------- /tasks/src/2.1.ExtractFunction/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: arial; 3 | } 4 | 5 | .page { 6 | margin: auto; 7 | width: 600px; 8 | } 9 | 10 | .lot { 11 | padding: 10px; 12 | background: #fda; 13 | } 14 | 15 | .lotName { 16 | font-weight: bold; 17 | font-size: 24px; 18 | } 19 | 20 | .lotDescription { 21 | font-size: 16px; 22 | padding: 20px; 23 | } 24 | 25 | .posts { 26 | padding-top: 20px; 27 | padding-left: 40px; 28 | } 29 | 30 | .post { 31 | padding: 10px; 32 | background: #eee; 33 | margin-bottom: 10px; 34 | } 35 | 36 | .postHeader { 37 | margin-bottom: 10px; 38 | } 39 | 40 | .postAuthor { 41 | font-weight: bold; 42 | font-size: 20px; 43 | } 44 | 45 | .postTime { 46 | color: #777; 47 | font-size: 16px; 48 | } 49 | 50 | .postMessage { 51 | font-size: 16px; 52 | } 53 | 54 | .authors { 55 | padding-top: 20px; 56 | padding-left: 40px; 57 | } 58 | 59 | .authors span { 60 | margin: 5px; 61 | padding: 5px; 62 | background: #fda; 63 | } 64 | -------------------------------------------------------------------------------- /tasks/src/2.2.List/.solved/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import '../styles.css'; 4 | 5 | const posts = [ 6 | { 7 | id: '1', 8 | author: 'Парень не промах', 9 | time: '2 часа назад', 10 | message: 'Попробую с удовольствием ;)' 11 | }, 12 | { 13 | id: '2', 14 | author: 'Милая девушка', 15 | time: '3 часа назад', 16 | message: 'Можно использовать для выпекания чизкейков :)' 17 | }, 18 | { 19 | id: '3', 20 | author: 'Скупец', 21 | time: 'вчера', 22 | message: 'Цену-то загнули!' 23 | } 24 | ]; 25 | 26 | function renderPost(post) { 27 | return ( 28 |
29 |
30 | {post.author} 31 |
32 | {post.time} 33 |
34 |
{post.message}
35 |
36 | ); 37 | } 38 | 39 | function renderAuthors(posts) { 40 | const authors = []; 41 | for (const post of posts) { 42 | authors.push({post.author}); 43 | } 44 | return
{authors}
; 45 | } 46 | 47 | ReactDom.render( 48 |
49 |
{posts.map(post => renderPost(post))}
50 | {renderAuthors(posts)} 51 |
, 52 | document.getElementById('app') 53 | ); 54 | -------------------------------------------------------------------------------- /tasks/src/2.2.List/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | List 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tasks/src/2.2.List/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import './styles.css'; 4 | 5 | /** 6 | 1. Разбери ручные переборы массивов в верстке. 7 | Для постов используй map без циклов, для авторов цикл for без map. 8 | 9 | 2. Посмотри ошибки в Chrome DevTools: React должен требовать наличия атрибутов key. 10 | Добавь в post поле id и присвой каждому полю уникальный строковый идентификатор. 11 | Используй id в качестве значения key в основном тэге поста и основном тэге автора. 12 | */ 13 | 14 | const posts = [ 15 | { 16 | author: 'Парень не промах', 17 | time: '2 часа назад', 18 | message: 'Попробую с удовольствием ;)' 19 | }, 20 | { 21 | author: 'Милая девушка', 22 | time: '3 часа назад', 23 | message: 'Можно использовать для выпекания чизкейков :)' 24 | }, 25 | { 26 | author: 'Скупец', 27 | time: 'вчера', 28 | message: 'Цену-то загнули!' 29 | } 30 | ]; 31 | 32 | function renderPost(post) { 33 | return ( 34 |
35 |
36 | {post.author} 37 |
38 | {post.time} 39 |
40 |
{post.message}
41 |
42 | ); 43 | } 44 | 45 | function renderAuthors(posts) { 46 | return ( 47 |
48 | {posts[0].author} 49 | {posts[1].author} 50 | {posts[2].author} 51 |
52 | ); 53 | } 54 | 55 | ReactDom.render( 56 |
57 |
58 | {renderPost(posts[0])} 59 | {renderPost(posts[1])} 60 | {renderPost(posts[2])} 61 |
62 | {renderAuthors(posts)} 63 |
, 64 | document.getElementById('app') 65 | ); 66 | 67 | /** 68 | Подсказки: 69 | - Отображение массива в другой массив записывается так: 70 | const values = items.map(item => item.field); 71 | - В конец массивов можно добавлять значения методом push: 72 | const numbers = []; 73 | numbers.push(1); 74 | - Выбери подходящий цикл for: 75 | - for (let i = 0; i < items.length; i++) {} 76 | - for (let key in items) {} 77 | - for (const item of items) {} 78 | */ 79 | -------------------------------------------------------------------------------- /tasks/src/2.2.List/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: arial; 3 | } 4 | 5 | .page { 6 | margin: auto; 7 | width: 600px; 8 | } 9 | 10 | .lot { 11 | padding: 10px; 12 | background: #fda; 13 | } 14 | 15 | .lotName { 16 | font-weight: bold; 17 | font-size: 24px; 18 | } 19 | 20 | .lotDescription { 21 | font-size: 16px; 22 | padding: 20px; 23 | } 24 | 25 | .posts { 26 | padding-top: 20px; 27 | padding-left: 40px; 28 | } 29 | 30 | .post { 31 | padding: 10px; 32 | background: #eee; 33 | margin-bottom: 10px; 34 | } 35 | 36 | .postHeader { 37 | margin-bottom: 10px; 38 | } 39 | 40 | .postAuthor { 41 | font-weight: bold; 42 | font-size: 20px; 43 | } 44 | 45 | .postTime { 46 | color: #777; 47 | font-size: 16px; 48 | } 49 | 50 | .postMessage { 51 | font-size: 16px; 52 | } 53 | 54 | .authors { 55 | padding-top: 20px; 56 | padding-left: 40px; 57 | } 58 | 59 | .authors span { 60 | margin: 5px; 61 | padding: 5px; 62 | background: #fda; 63 | } 64 | -------------------------------------------------------------------------------- /tasks/src/2.3.Condition/.solved/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import '../styles.css'; 4 | 5 | function renderPosts(posts) { 6 | if (posts.length === 0) { 7 | return
Нет откликов
; 8 | } 9 | if (posts.length === 1) { 10 | return
Единственный отклик
; 11 | } 12 | return
Отклики в количестве {posts.length}
; 13 | } 14 | 15 | function renderLot(name, description, tags) { 16 | return ( 17 |
18 |
{name || '<Неизвестный предмет>'}
19 | {description &&
{description}
} 20 | {renderTags(tags)} 21 |
22 | ); 23 | } 24 | 25 | function renderTags(tags) { 26 | if (!tags || tags.length === 0) return null; 27 | const content = tags.join(', '); 28 | return
{content}
; 29 | } 30 | 31 | ReactDom.render( 32 |
33 | {renderLot('', '', [])} 34 | {renderPosts([])} 35 |
, 36 | document.getElementById('app') 37 | ); 38 | -------------------------------------------------------------------------------- /tasks/src/2.3.Condition/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Condition 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tasks/src/2.3.Condition/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import './styles.css'; 4 | 5 | /** 6 | Не выделяя дополнительных методов 7 | 1. Сделай так, чтобы renderPosts возвращал: 8 | - div с классом emptyPosts и текстом "Нет откликов", 9 | если posts пуст:
Нет откликов
10 | - div с классом singlePost и текстом "Единственный отклик", 11 | если в posts ровно 1 элемент:
Единственный отклик
12 | - div с классом posts в остальных случаях:
Отклики в количестве {posts.length}
13 | 2. Если name лота пустое или неопределено, то вместо него должна появляться надпись '<Неизвестный предмет>' 14 | 3. Если description лота пустое или неопределено, то тэг с классом lotDescription должен отсутствовать 15 | 4. Если у лота нет тэгов, то div с классом lotTags должен отсутствовать 16 | */ 17 | 18 | function renderPosts(posts) { 19 | //
Нет откликов
20 | //
Единственный отклик
21 | return
Отклики в количестве {posts.length}
; 22 | } 23 | 24 | function renderLot(name, description, tags) { 25 | return ( 26 |
27 |
{name}
28 |
{description}
29 | {renderTags(tags)} 30 |
31 | ); 32 | } 33 | 34 | function renderTags(tags) { 35 | const content = tags.join(', '); 36 | return
{content}
; 37 | } 38 | 39 | ReactDom.render( 40 |
41 |
42 | {renderLot('', 'красный, красивый, твой!', [])} 43 | {renderPosts([])} 44 |
45 |
46 | {renderLot('Пирожок с капустой', undefined, ['#свежий', '#ручнаяРабота'])} 47 | {renderPosts(['Тут ровно один отклик'])} 48 |
49 |
50 | {renderLot('', '', ['#большой', '#Яркий'])} 51 | {renderPosts(['Класс!', 'Хочу еще!', 'Отстой'])} 52 |
53 |
, 54 | document.getElementById('app') 55 | ); 56 | -------------------------------------------------------------------------------- /tasks/src/2.3.Condition/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: arial; 3 | } 4 | 5 | .page { 6 | margin: 0 auto 100px; 7 | width: 600px; 8 | } 9 | 10 | .lot { 11 | padding: 10px; 12 | background: #fda; 13 | } 14 | 15 | .lotName { 16 | font-weight: bold; 17 | font-size: 24px; 18 | } 19 | 20 | .lotDescription { 21 | font-size: 16px; 22 | padding: 20px; 23 | } 24 | 25 | .lotTags { 26 | text-decoration: underline; 27 | } 28 | 29 | .posts { 30 | padding-top: 20px; 31 | padding-left: 40px; 32 | } 33 | 34 | .emptyPosts { 35 | padding-top: 20px; 36 | padding-left: 40px; 37 | text-align: center; 38 | font-style: italic; 39 | } 40 | 41 | .singlePost { 42 | padding-top: 20px; 43 | } 44 | 45 | .post { 46 | padding: 10px; 47 | background: #eee; 48 | margin-bottom: 10px; 49 | } 50 | 51 | .postHeader { 52 | margin-bottom: 10px; 53 | } 54 | 55 | .postAuthor { 56 | font-weight: bold; 57 | font-size: 20px; 58 | } 59 | 60 | .postTime { 61 | color: #777; 62 | font-size: 16px; 63 | } 64 | 65 | .postMessage { 66 | font-size: 16px; 67 | } 68 | 69 | .authors { 70 | padding-top: 20px; 71 | padding-left: 40px; 72 | } 73 | 74 | .authors span { 75 | margin: 5px; 76 | padding: 5px; 77 | background: #fda; 78 | } 79 | -------------------------------------------------------------------------------- /tasks/src/3.1.ExtractComponent/.solved/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import PropTypes from 'prop-types'; 4 | import './styles.css'; 5 | 6 | function Post(props) { 7 | return ( 8 |
9 |
10 | {props.author} 11 |
12 | {props.time} 13 |
14 |
{props.children}
15 |
16 | ); 17 | } 18 | 19 | Post.propTypes = { 20 | author: PropTypes.string.isRequired, 21 | time: PropTypes.string.isRequired, 22 | children: PropTypes.node 23 | }; 24 | 25 | Post.defaultProps = { 26 | author: '<Неизвестный автор>' 27 | }; 28 | 29 | ReactDom.render( 30 |
31 |
32 | 33 | Можно использовать для выпекания чизкейков :) 34 | 35 |
36 |
, 37 | document.getElementById('app') 38 | ); 39 | -------------------------------------------------------------------------------- /tasks/src/3.1.ExtractComponent/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ExtractComponent 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tasks/src/3.1.ExtractComponent/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | //import PropTypes from 'prop-types'; 4 | import './styles.css'; 5 | 6 | /** 7 | 1. Переделай renderPost в функциональный компонент Post 8 | 9 | 2. ESLint настроен так, чтобы проверять переданные атрибуты. Поэтому задай propTypes. 10 | У нас везде атрибуты — это строки. Сделай свойства author и time обязательными. 11 | 12 | 3. Сделай так, чтобы в author подставлялось значение <Неизвестный автор>, 13 | если атрибут не передали. 14 | Используй для этого defaultProps. 15 | Проверь что работает, убрав имя автора. 16 | 17 | 4. Переделай компонент так, чтобы message передавался через props.children. 18 | */ 19 | 20 | // Эта строка нужна, чтобы ESLint не сильно ругался, пока не написаны PropTypes. 21 | /*eslint react/prop-types: "warn" */ 22 | 23 | function renderPost(post) { 24 | return ( 25 |
26 |
27 | {post.author} 28 |
29 | {post.time} 30 |
31 |
{post.message}
32 |
33 | ); 34 | } 35 | 36 | ReactDom.render( 37 |
38 |
39 | {renderPost({ 40 | author: 'Милая девушка', 41 | time: '3 часа назад', 42 | message: 'Можно использовать для выпекания чизкейков :)' 43 | })} 44 |
45 |
, 46 | document.getElementById('app') 47 | ); 48 | 49 | /** 50 | Подсказки к 1: 51 | - {renderMyComponent({a: 1, b: 'some'})} → 52 | - Первый аргумент функции компонента обычно называется props 53 | 54 | Подсказки к 2: 55 | - В начале файла нужно импортировать PropTypes 56 | - MyComponent.propTypes = { 57 | a: PropTypes.number.isRequired, 58 | b: PropTypes.string, 59 | onFire: PropTypes.func 60 | } 61 | 62 | Подсказки к 3: 63 | - MyComponent.defaultProps = { 64 | b: 'default value' 65 | } 66 | 67 | Подсказки к 4: 68 | - Дети — это вложенные узлы тэга. 69 | Пример с одним ребенком: Значение 70 | - Дети попадают в props в виде массива props.children. 71 | - При использовании надо добавлять в propTypes компонента: 72 | children: PropTypes.node 73 | */ 74 | -------------------------------------------------------------------------------- /tasks/src/3.1.ExtractComponent/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: arial; 3 | } 4 | 5 | .page { 6 | margin: auto; 7 | width: 600px; 8 | } 9 | 10 | .lot { 11 | padding: 10px; 12 | background: #fda; 13 | } 14 | 15 | .lotName { 16 | font-weight: bold; 17 | font-size: 24px; 18 | } 19 | 20 | .lotDescription { 21 | font-size: 16px; 22 | } 23 | 24 | .posts { 25 | padding-top: 20px; 26 | padding-left: 40px; 27 | } 28 | 29 | .post { 30 | padding: 10px; 31 | background: #eee; 32 | margin-bottom: 10px; 33 | } 34 | 35 | .postHeader { 36 | margin-bottom: 10px; 37 | } 38 | 39 | .postAuthor { 40 | font-weight: bold; 41 | font-size: 20px; 42 | } 43 | 44 | .postTime { 45 | color: #777; 46 | font-size: 16px; 47 | } 48 | 49 | .postMessage { 50 | font-size: 16px; 51 | } -------------------------------------------------------------------------------- /tasks/src/3.2.Toggle/.solved/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import PropTypes from 'prop-types'; 4 | import '../styles.css'; 5 | import '../toggle.css'; 6 | 7 | class Toggle extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | checked: false 12 | }; 13 | } 14 | 15 | render() { 16 | const { checked } = this.state; 17 | return ( 18 | 22 | 23 |
24 | 25 | 26 | 27 | ); 28 | } 29 | 30 | handleClick = () => { 31 | const newChecked = !this.state.checked; 32 | if (this.props.onChange) { 33 | this.props.onChange(newChecked); 34 | } 35 | this.setState({ 36 | checked: newChecked 37 | }); 38 | }; 39 | } 40 | 41 | Toggle.propTypes = { 42 | onChange: PropTypes.func 43 | }; 44 | 45 | ReactDom.render( 46 |
47 | console.log(value)} /> Использовать умные 48 | компоненты 49 |
, 50 | document.getElementById('app') 51 | ); 52 | -------------------------------------------------------------------------------- /tasks/src/3.2.Toggle/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Toggle 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tasks/src/3.2.Toggle/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import PropTypes from 'prop-types'; 4 | import './styles.css'; 5 | import './toggle.css'; 6 | 7 | /** 8 | Допиши компонент Toggle. 9 | Пусть флаг хранится во внутреннем состоянии, 10 | а при изменении передается наружу через onChange. 11 | */ 12 | 13 | class Toggle extends React.Component { 14 | // constructor(props) { 15 | // } 16 | 17 | render() { 18 | const checked = true; 19 | return ( 20 | 24 | 25 |
26 | 27 | 28 | 29 | ); 30 | } 31 | 32 | handleClick = () => {}; 33 | } 34 | 35 | Toggle.propTypes = { 36 | onChange: PropTypes.func 37 | }; 38 | 39 | ReactDom.render( 40 |
41 | console.log(value)} /> Использовать умные 42 | компоненты 43 |
, 44 | document.getElementById('app') 45 | ); 46 | 47 | /** 48 | Подсказки: 49 | - Начальное состояние компонента хранится в this.state и обычно инициируется в конструкторе. 50 | - Не забудь добавить super(props) первой строчкой конструктора, чтобы вызвать конструктор базового типа. 51 | - this.setState({property: value}) обновляет часть состояния и инициирует перерисовку. 52 | */ 53 | -------------------------------------------------------------------------------- /tasks/src/3.2.Toggle/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: arial; 3 | } 4 | 5 | .page { 6 | margin: auto; 7 | width: 600px; 8 | } 9 | 10 | .lot { 11 | padding: 10px; 12 | background: #fda; 13 | } 14 | 15 | .lotName { 16 | font-weight: bold; 17 | font-size: 24px; 18 | } 19 | 20 | .lotDescription { 21 | font-size: 16px; 22 | } 23 | 24 | .posts { 25 | padding-top: 20px; 26 | padding-left: 40px; 27 | } 28 | 29 | .post { 30 | padding: 10px; 31 | background: #eee; 32 | margin-bottom: 10px; 33 | } 34 | 35 | .postHeader { 36 | margin-bottom: 10px; 37 | } 38 | 39 | .postAuthor { 40 | font-weight: bold; 41 | font-size: 20px; 42 | } 43 | 44 | .postTime { 45 | color: #777; 46 | font-size: 16px; 47 | } 48 | 49 | .postMessage { 50 | font-size: 16px; 51 | } 52 | 53 | .formHeader { 54 | font-size: 24px; 55 | font-weight: bold; 56 | padding: 10px; 57 | } 58 | 59 | .formField { 60 | font-size: 16px; 61 | padding: 10px; 62 | } 63 | 64 | .formFooter { 65 | font-size: 16px; 66 | padding: 10px; 67 | } -------------------------------------------------------------------------------- /tasks/src/3.2.Toggle/toggle.css: -------------------------------------------------------------------------------- 1 | .container { 2 | position: relative; 3 | 4 | display: inline-block; 5 | width: 34px; 6 | height: 20px; 7 | box-sizing: border-box; 8 | border: 1px solid #d0d0d0; 9 | 10 | transition: border .2s ease-in; 11 | transition-delay: .05s; 12 | 13 | border-radius: 10px; 14 | background: white; 15 | 16 | cursor: pointer; 17 | 18 | margin: 15px 15px; 19 | } 20 | 21 | .container:after { 22 | content: ' '; 23 | display: inline-block; 24 | } 25 | 26 | .background { 27 | position: absolute; 28 | right: 100%; 29 | top: 0; 30 | width: 100%; 31 | height: 100%; 32 | background: #3072c4; 33 | transition: right 0.2s ease-in; 34 | } 35 | 36 | .isChecked { 37 | border: 1px solid #3072c4; 38 | } 39 | 40 | .handle { 41 | position: absolute; 42 | top: 0px; 43 | bottom: 0px; 44 | left: 0px; 45 | z-index: 2; 46 | 47 | width: 18px; 48 | margin-left: 0; 49 | 50 | transition: width 0.2s cubic-bezier(0.4, 0, 1, 1); 51 | } 52 | 53 | .isChecked .handle { 54 | width: 32px; 55 | } 56 | 57 | .bg { 58 | height: 100%; 59 | overflow: hidden; 60 | position: relative; 61 | border-radius: 9px; 62 | left: 0; 63 | transition: left 0.2s ease-in; 64 | transition-delay: 0.05s; 65 | } 66 | 67 | .isChecked .bg { 68 | left: -1px; 69 | } 70 | 71 | .bg:after { 72 | content: ' '; 73 | position: absolute; 74 | right: 10px; 75 | top: 0px; 76 | width: 23px; 77 | border-radius: 9 0 0 9; 78 | height: 18px; 79 | background: #3072c4; 80 | } 81 | 82 | .hinge { 83 | display: inline-block; 84 | content: ' '; 85 | position: absolute; 86 | top: 0; 87 | right: 0; 88 | bottom: 0; 89 | border-radius: 9px; 90 | width: 18px; 91 | background-image: linear-gradient(-180deg, #FFFFFF, #EBEBEB); 92 | box-shadow: 0 1px 0 0 rgba(0, 0, 0, .15), 0 0 0 1px rgba(0, 0, 0, .15); 93 | } 94 | -------------------------------------------------------------------------------- /tasks/src/3.3.MoneyConverter/.solved/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import PropTypes from 'prop-types'; 4 | import '../styles.css'; 5 | 6 | const RUBLES_IN_ONE_EURO = 70; 7 | 8 | class MoneyConverter extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | valueInRubles: 0, 13 | valueInEuros: 0 14 | }; 15 | } 16 | 17 | render() { 18 | return ( 19 |
20 |
21 |

Конвертер валют

22 |
23 | 24 | 28 | — 29 | 33 | 34 |
35 |
36 |
37 | ); 38 | } 39 | 40 | handleChangeRubles = value => { 41 | this.setState({ 42 | valueInRubles: value, 43 | valueInEuros: Math.round(100 * value / RUBLES_IN_ONE_EURO) / 100 44 | }); 45 | }; 46 | 47 | handleChangeEuros = value => { 48 | this.setState({ 49 | valueInRubles: Math.round(100 * value * RUBLES_IN_ONE_EURO) / 100, 50 | valueInEuros: value 51 | }); 52 | }; 53 | } 54 | 55 | function Money({ value, onChange }) { 56 | const handleChange = event => { 57 | onChange(extractNumberString(event.target.value)); 58 | }; 59 | 60 | return ; 61 | } 62 | 63 | Money.propTypes = { 64 | value: PropTypes.number.isRequired, 65 | onChange: PropTypes.func 66 | }; 67 | 68 | function extractNumberString(value) { 69 | const str = value.replace(/^0+/g, '').replace(/[^\.0-9]/g, ''); 70 | const parts = str.split('.'); 71 | return parts.length > 2 ? parts[0] + '.' + parts.slice(1).join('') : str; 72 | } 73 | 74 | ReactDom.render(, document.getElementById('app')); 75 | -------------------------------------------------------------------------------- /tasks/src/3.3.MoneyConverter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MoneyConverter 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tasks/src/3.3.MoneyConverter/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import PropTypes from 'prop-types'; 4 | import './styles.css'; 5 | 6 | /** 7 | Допиши конвертер валют. 8 | - Если пользователь ввел значение в рублях, то количество евро обновляется согласно курсу 9 | - Если пользователь ввел значение в евро, то количество рублей обновляется согласно курсу 10 | */ 11 | 12 | const RUBLES_IN_ONE_EURO = 70; 13 | 14 | class MoneyConverter extends React.Component { 15 | render() { 16 | return ( 17 |
18 |
19 |

Конвертер валют

20 |
21 | 22 | 23 | — 24 | 25 | 26 |
27 |
28 |
29 | ); 30 | } 31 | } 32 | 33 | class Money extends React.Component { 34 | constructor(props) { 35 | super(props); 36 | this.state = { 37 | value: 0 38 | }; 39 | } 40 | 41 | render() { 42 | return ( 43 | 48 | ); 49 | } 50 | 51 | handleChangeValue = event => { 52 | const value = extractNumberString(event.target.value); 53 | this.setState({ value }); 54 | }; 55 | } 56 | 57 | Money.propTypes = {}; 58 | 59 | function extractNumberString(value) { 60 | const str = value.replace(/^0+/g, '').replace(/[^\.0-9]/g, ''); 61 | const parts = str.split('.'); 62 | return parts.length > 2 ? parts[0] + '.' + parts.slice(1).join('') : str; 63 | } 64 | 65 | ReactDom.render(, document.getElementById('app')); 66 | 67 | /** 68 | Подсказки: 69 | - Сейчас каждый компонент Money хранит свое значение в собственном состоянии, 70 | чтобы конвертер работал, нужно уметь обновлять значение извне, поэтому нужно получать его из props. 71 | - В MoneyConverter наоборот надо создать состояние, которое будет хранить значения в обеих валютах. 72 | Таким образом ты сделаешь Lift State Up. 73 | - Заметь, что компонент Money теперь не содержит состояние и его можно переделать в функциональный компонент. 74 | */ 75 | -------------------------------------------------------------------------------- /tasks/src/3.3.MoneyConverter/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: arial; 3 | } 4 | 5 | .root { 6 | margin: auto; 7 | width: 400px; 8 | } 9 | 10 | .form { 11 | padding: 10px; 12 | background: #fda; 13 | text-align: center; 14 | } 15 | 16 | input[type="text"] { 17 | padding: 5px; 18 | margin: 10px 15px; 19 | border: 1px solid #aaa; 20 | border-radius: 3px; 21 | height: 24px; 22 | width: 100px; 23 | font-size: 20px; 24 | } 25 | 26 | input[type="text"]:focus { 27 | border: 1px solid #555; 28 | color: #000000; 29 | } 30 | 31 | span { 32 | font-size: 20px; 33 | } 34 | -------------------------------------------------------------------------------- /tasks/src/4.1.Timer/.solved/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import '../styles.css'; 4 | 5 | class Timer extends React.Component { 6 | constructor() { 7 | super(); 8 | this.state = { timeVisible: true }; 9 | } 10 | 11 | render() { 12 | const { timeVisible } = this.state; 13 | return ( 14 |
15 | { 20 | this.setState({ timeVisible: !timeVisible }); 21 | }} 22 | /> 23 | {this.state.timeVisible && } 24 |
25 | ); 26 | } 27 | } 28 | 29 | class TimeDisplay extends React.Component { 30 | constructor() { 31 | super(); 32 | this.state = { 33 | localTime: new Date() 34 | }; 35 | this.localTickInterval = null; 36 | } 37 | 38 | componentDidMount() { 39 | this.localTickInterval = setInterval(() => { 40 | console.log('tick'); 41 | this.setState({ 42 | localTime: new Date() 43 | }); 44 | }, 1000); 45 | } 46 | 47 | componentWillUnmount() { 48 | if (this.localTickInterval) { 49 | clearInterval(this.localTickInterval); 50 | this.localTickInterval = null; 51 | } 52 | } 53 | 54 | render() { 55 | return ( 56 |
{this.state.localTime.toLocaleTimeString()}
57 | ); 58 | } 59 | } 60 | 61 | ReactDom.render(, document.getElementById('app')); 62 | -------------------------------------------------------------------------------- /tasks/src/4.1.Timer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Timer 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tasks/src/4.1.Timer/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import './styles.css'; 4 | 5 | /** 6 | 1. Допиши TimeDisplay так, чтобы он показывал текущее время пользователя и сам обновляется каждую секунду. 7 | 2. Пусть при каждом обновлении времени в консоль пишется сообщение: 8 | console.log('tick'); 9 | 3. Позаботься об освобождении ресурсов в случае удаления элемента. 10 | Убедись, что если компонент скрыть кнопкой, то в консоль не будут писаться тики. 11 | */ 12 | 13 | class Timer extends React.Component { 14 | constructor() { 15 | super(); 16 | this.state = { timeVisible: true }; 17 | } 18 | 19 | render() { 20 | const { timeVisible } = this.state; 21 | return ( 22 |
23 | { 28 | this.setState({ timeVisible: !timeVisible }); 29 | }} 30 | /> 31 | {this.state.timeVisible && } 32 |
33 | ); 34 | } 35 | } 36 | 37 | class TimeDisplay extends React.Component { 38 | constructor() { 39 | super(); 40 | this.state = { 41 | localTime: new Date() 42 | }; 43 | } 44 | 45 | render() { 46 | return ( 47 |
{this.state.localTime.toLocaleTimeString()}
48 | ); 49 | } 50 | } 51 | 52 | ReactDom.render(, document.getElementById('app')); 53 | 54 | /** 55 | Подсказки: 56 | - Функция setInterval регистрирует обработчик handler, 57 | который будет вызываться не чаще, чем в заданное количество миллисекунд. 58 | Оформляется так: 59 | const intervalId = setInterval(handler, intervalInMilliseconds); 60 | - intervalId можно передать в функцию clearInterval, чтобы остановить вызов обработчика: 61 | clearInterval(intervalId); 62 | - this.setState({property: value}) обновляет часть состояния и инициирует перерисовку. 63 | - componentDidMount вызывается сразу после того, как компонент размещен на странице. 64 | В нем можно делать запросы на получение данных или подписываться на события. 65 | - componentWillUnmount вызывается перед тем как удалить компонент. 66 | Гарантированно вызовется, если элемент «did mount». Отличное место, чтобы освобождать ресурсы. 67 | */ 68 | -------------------------------------------------------------------------------- /tasks/src/4.1.Timer/styles.css: -------------------------------------------------------------------------------- 1 | /* Styling it all now! */ 2 | * { 3 | font-family: arial; 4 | } 5 | 6 | html, 7 | body { 8 | width: 100%; 9 | height: 100%; 10 | overflow: hidden; 11 | background: #b9b7b1; 12 | } 13 | 14 | .page { 15 | margin: auto; 16 | width: 300px; 17 | text-align: center; 18 | } 19 | 20 | .time { 21 | position: relative; 22 | left: 50%; 23 | margin-left: -150px; 24 | width: 300px; 25 | padding: 20px; 26 | text-align: center; 27 | font-size: 30px; 28 | border-top: 3px solid #e54b6b; 29 | background: #fff; 30 | color: #707070; 31 | } 32 | 33 | .button { 34 | width: 100px; 35 | height: 34px; 36 | border-radius: 2px 2px 2px 2px; 37 | padding: 0 15px; 38 | margin: 5px; 39 | border: solid 2px; 40 | } 41 | 42 | .button:hover { 43 | cursor: pointer; 44 | } -------------------------------------------------------------------------------- /tasks/src/4.2.CreditCardInput/.solved/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import PropTypes from 'prop-types'; 4 | import 'regenerator-runtime'; 5 | import '../styles.css'; 6 | import Api from '../Api'; 7 | import CreditCardNumber from '../CreditCardNumber'; 8 | 9 | class CreditCardInputWithRestore extends React.Component { 10 | constructor() { 11 | super(); 12 | this.state = { 13 | value: '0000 0000 0000 0000' 14 | }; 15 | } 16 | 17 | componentDidMount() { 18 | this.restoreFromApi(); 19 | } 20 | 21 | render() { 22 | return ( 23 | console.log(val)} 26 | /> 27 | ); 28 | } 29 | 30 | async restoreFromApi() { 31 | const value = await Api.getValue(); 32 | this.setState({ value: value }); 33 | } 34 | } 35 | 36 | class CreditCardInput extends React.Component { 37 | constructor(props) { 38 | super(props); 39 | this.state = {}; 40 | } 41 | 42 | static getDerivedStateFromProps(nextProps, prevState) { 43 | if (prevState.changed || nextProps.value === prevState.value) { 44 | return null; 45 | } 46 | return { value: nextProps.value, changed: false }; 47 | } 48 | 49 | render() { 50 | return ( 51 |
52 |
53 | 62 |
63 |
64 | ); 65 | } 66 | 67 | handleFocus = () => { 68 | this.setState({ value: '' }); 69 | }; 70 | 71 | handleChange = event => { 72 | const formattedValue = CreditCardNumber.format(event.target.value); 73 | this.setState({ value: formattedValue, changed: true }); 74 | }; 75 | 76 | handleBlur = () => { 77 | if (CreditCardNumber.isValid(this.state.value)) { 78 | this.props.onChange(this.state.value); 79 | } 80 | }; 81 | } 82 | 83 | CreditCardInput.propTypes = { 84 | value: PropTypes.string, 85 | onChange: PropTypes.func 86 | }; 87 | 88 | ReactDom.render(, document.getElementById('app')); 89 | -------------------------------------------------------------------------------- /tasks/src/4.2.CreditCardInput/Api.js: -------------------------------------------------------------------------------- 1 | export default class Api { 2 | static async getValue() { 3 | return new Promise(resolve => { 4 | setTimeout(() => { 5 | resolve('1234 5678 9012 3456'); 6 | }, 3000); 7 | }); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tasks/src/4.2.CreditCardInput/CreditCardNumber.js: -------------------------------------------------------------------------------- 1 | export default class CreditCardNumber { 2 | static isValid(value) { 3 | if (!value) { 4 | return false; 5 | } 6 | const number = value.replace(/\s+/g, '').replace(/[^0-9]/gi, ''); 7 | return number.length === 16; 8 | } 9 | 10 | static format(value) { 11 | const number = (value || '').replace(/\s+/g, '').replace(/[^0-9]/gi, ''); 12 | 13 | const parts = []; 14 | for (let i = 0; i < number.length && i < 16; i += 4) { 15 | parts.push(number.substring(i, i + 4)); 16 | } 17 | 18 | return parts.length === 0 ? number : parts.join(' '); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tasks/src/4.2.CreditCardInput/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CreditCardInput 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tasks/src/4.2.CreditCardInput/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import PropTypes from 'prop-types'; 4 | import 'regenerator-runtime'; 5 | import './styles.css'; 6 | import Api from './Api'; 7 | import CreditCardNumber from './CreditCardNumber'; 8 | 9 | /** 10 | CreditCardInput не просто показывает переданное value, 11 | а использует внутреннее состояние для форматирования ввода пользователя. 12 | CreditCardInputWithRestore должен обеспечить восстановление номера кредитной карты с сервера, 13 | но не работает из-за ошибки в CreditCardInput. 14 | 15 | Исправь ошибку в CreditCardInput. 16 | Если сервер ответит до того как пользователь успел что-то поредактировать, 17 | значение в поле ввода должно быть перетерто полученным из api значением 18 | (наш «сервер» всегда отвечает 1234 5678 9012 3456). 19 | Иначе значение с сервера нужно проигнорировать. 20 | */ 21 | 22 | class CreditCardInputWithRestore extends React.Component { 23 | constructor() { 24 | super(); 25 | this.state = { 26 | value: '0000 0000 0000 0000' 27 | }; 28 | } 29 | 30 | componentDidMount() { 31 | this.restoreFromApi(); 32 | } 33 | 34 | render() { 35 | return ( 36 | console.log(val)} 39 | /> 40 | ); 41 | } 42 | 43 | async restoreFromApi() { 44 | const value = await Api.getValue(); 45 | this.setState({ value: value }); 46 | } 47 | } 48 | 49 | class CreditCardInput extends React.Component { 50 | constructor(props) { 51 | super(props); 52 | this.state = { value: props.value }; 53 | } 54 | 55 | render() { 56 | return ( 57 |
58 |
59 | 68 |
69 |
70 | ); 71 | } 72 | 73 | handleFocus = () => { 74 | this.setState({ value: '' }); 75 | }; 76 | 77 | handleChange = event => { 78 | const formattedValue = CreditCardNumber.format(event.target.value); 79 | this.setState({ value: formattedValue }); 80 | }; 81 | 82 | handleBlur = () => { 83 | if (CreditCardNumber.isValid(this.state.value)) { 84 | this.props.onChange(this.state.value); 85 | } 86 | }; 87 | } 88 | 89 | CreditCardInput.propTypes = { 90 | value: PropTypes.string, 91 | onChange: PropTypes.func 92 | }; 93 | 94 | ReactDom.render(, document.getElementById('app')); 95 | 96 | /** 97 | Подсказки: 98 | - static getDerivedStateFromProps(nextProps, prevState) вызывается сразу после вызова конструктора, 99 | а также при получении компонентом измененных props. Он тебе поможет. Из него нужно вернуть новый state, 100 | полученный умным объединением старого состояния и новых свойств. 101 | - Даже при задании getDerivedStateFromProps состояние должно инициализироваться в конструкторе. 102 | */ 103 | -------------------------------------------------------------------------------- /tasks/src/4.2.CreditCardInput/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: arial; 3 | } 4 | 5 | .root { 6 | margin: auto; 7 | width: 400px; 8 | } 9 | 10 | .form { 11 | padding: 10px; 12 | background: #fda; 13 | text-align: center; 14 | } 15 | 16 | input[type="text"] { 17 | padding: 5px; 18 | margin: 10px 15px; 19 | border: 1px solid #aaa; 20 | border-radius: 3px; 21 | height: 24px; 22 | width: 200px; 23 | font-size: 20px; 24 | } 25 | 26 | input[type="text"]:focus { 27 | border: 1px solid #555; 28 | color: #000000; 29 | } 30 | 31 | span { 32 | font-size: 20px; 33 | } 34 | -------------------------------------------------------------------------------- /tasks/src/5.Focus/.solved/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import PropTypes from 'prop-types'; 4 | import '../styles.css'; 5 | 6 | class InputFormRow extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.inputRef = React.createRef(); 10 | } 11 | 12 | render() { 13 | const { label, ...rest } = this.props; 14 | return ( 15 |
16 |
{label}
17 | 18 |
19 | ); 20 | } 21 | 22 | handleClick = () => { 23 | this.inputRef.current.focus(); 24 | }; 25 | } 26 | 27 | InputFormRow.propTypes = { 28 | label: PropTypes.string.isRequired 29 | }; 30 | 31 | ReactDom.render( 32 |
33 |
34 | 35 | 36 | 37 | 38 | 39 |
40 | 41 |
42 |
, 43 | document.getElementById('app') 44 | ); 45 | -------------------------------------------------------------------------------- /tasks/src/5.Focus/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Focus 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tasks/src/5.Focus/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import PropTypes from 'prop-types'; 4 | import './styles.css'; 5 | 6 | /** 7 | InputFormRow позволяет клепать формы еще быстрее, чем раньше! 8 | Количество дублирования кода уменьшается, а еще благодаря нему 9 | можно добавить новые фишки во все поля формы сразу. 10 | 11 | Сделай так, чтобы при клике по любому месту InputFormRow фокус переводился в поле ввода. 12 | 13 | Обрати внимание: 14 | - Как все props, кроме нужных, элегантно пробрасываются в input. 15 | */ 16 | 17 | class InputFormRow extends React.Component { 18 | constructor(props) { 19 | super(props); 20 | } 21 | 22 | render() { 23 | const { label, ...rest } = this.props; 24 | return ( 25 |
26 |
{label}
27 | 28 |
29 | ); 30 | } 31 | 32 | handleClick = () => {}; 33 | } 34 | 35 | InputFormRow.propTypes = { 36 | label: PropTypes.string.isRequired 37 | }; 38 | 39 | ReactDom.render( 40 |
41 |
42 | 43 | 44 | 45 | 46 | 47 |
48 | 49 |
50 |
, 51 | document.getElementById('app') 52 | ); 53 | 54 | /** 55 | Подсказки: 56 | - У элемента input есть метод focus(), но нужна ссылка. 57 | - Есть два актуальных способа получить ссылку: 58 | -
, но надо заранее создать this.myRef = React.createRef(); 59 | -
this.myRef = r} и тогда при вызове render в свойстве this.myRef окажется ссылка. 60 | В зависимости от выбранного способа в myRef будут немного разные объекты. 61 | - Чтобы пользователь догадался, что он может кликнуть по ряду 62 | и что-то произойдет, добавь в div с css-классом row класс pointer. 63 | */ 64 | -------------------------------------------------------------------------------- /tasks/src/5.Focus/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: arial; 3 | } 4 | 5 | .root { 6 | margin: auto; 7 | width: 800px; 8 | } 9 | 10 | input { 11 | padding: 5px; 12 | margin: 10px 15px; 13 | border: 1px solid #aaa; 14 | border-radius: 3px; 15 | height: 18px; 16 | width: 200px; 17 | font-size: 18px; 18 | } 19 | 20 | input:focus { 21 | border: 1px solid #555; 22 | } 23 | 24 | .openContainer { 25 | text-align: center; 26 | } 27 | 28 | .saveContainer { 29 | margin-top: 20px; 30 | text-align: center; 31 | } 32 | 33 | .addContainer { 34 | margin-top: 20px; 35 | text-align: right; 36 | } 37 | 38 | .actionButton { 39 | background: linear-gradient(#2899ea, #167ac1); 40 | color: #fff; 41 | height: 34px; 42 | border-radius: 2px 2px 2px 2px; 43 | padding: 0 15px; 44 | border: 0; 45 | } 46 | 47 | .actionButton:hover { 48 | background: linear-gradient(#0087d5, #167ac1); 49 | cursor: pointer; 50 | } 51 | 52 | .editButton { 53 | background: linear-gradient(#2899ea, #167ac1); 54 | color: #fff; 55 | height: 28px; 56 | width: 140px; 57 | border-radius: 2px 2px 2px 2px; 58 | padding: 0 15px; 59 | border: 0; 60 | font-size: 16px; 61 | } 62 | 63 | .editButton:hover { 64 | background: linear-gradient(#0087d5, #167ac1); 65 | cursor: pointer; 66 | } 67 | 68 | table { 69 | width: 100%; 70 | border: 1px solid #eee; 71 | border-collapse: collapse; 72 | table-layout: fixed; 73 | } 74 | 75 | th { 76 | background-color: #eee; 77 | border-bottom: 1px solid #eee; 78 | font-weight: 600; 79 | padding: 0.75rem; 80 | text-align: left; 81 | vertical-align: middle; 82 | } 83 | 84 | td { 85 | border-bottom: 1px solid #eee; 86 | padding: 0.75rem; 87 | border: 1px solid #eee; 88 | vertical-align: middle; 89 | } 90 | 91 | tr:hover { 92 | background-color: #daeffc; 93 | cursor: pointer; 94 | } 95 | 96 | .form { 97 | margin: 0px auto 20px auto; 98 | width: 600px; 99 | border: 1px solid #ddd; 100 | background-color: #eee; 101 | padding: 10px; 102 | } 103 | 104 | .row { 105 | height: 40px; 106 | } 107 | 108 | .label { 109 | display: inline-block; 110 | font-size: 18px; 111 | width: 200px; 112 | } 113 | 114 | .table { 115 | width: 800px; 116 | display: block; 117 | padding: 20px; 118 | border: 1px solid #eee; 119 | margin: auto; 120 | } 121 | 122 | .pointer { 123 | cursor: pointer; 124 | } 125 | -------------------------------------------------------------------------------- /tasks/src/6.Hooks/.solved/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect } from "react"; 2 | import ReactDom from "react-dom"; 3 | import "./styles.css"; 4 | 5 | const App = () => { 6 | const lastBlockIdRef = useRef(0); 7 | const [blockIds, setBlockIds] = useState([]); 8 | 9 | const addNew = () => { 10 | setBlockIds(ids => [...ids, lastBlockIdRef.current++]); 11 | }; 12 | 13 | const removeLast = () => { 14 | setBlockIds(ids => ids.slice(0, ids.length - 1)); 15 | }; 16 | 17 | return ( 18 |
19 |
20 | 23 | 26 |
27 |
28 | {blockIds.map(blockId => ( 29 | 30 | ))} 31 |
32 |
33 | ); 34 | }; 35 | 36 | const CounterBlock = () => { 37 | const [value, setValue] = useState(0); 38 | const timerRef = useRef(null); 39 | 40 | useEffect(() => { 41 | timerRef.current = setInterval(setValue, 1000, v => v + 1); 42 | 43 | return () => { 44 | clearInterval(timerRef.current); 45 | }; 46 | }, []); 47 | 48 | return
{value}
; 49 | }; 50 | 51 | ReactDom.render(, document.getElementById("app")); 52 | -------------------------------------------------------------------------------- /tasks/src/6.Hooks/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hooks 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tasks/src/6.Hooks/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDom from "react-dom"; 3 | import "./styles.css"; 4 | 5 | /** 6 | Сделай так, чтобы в приложении все классы заменились на функциональные компоненты, для этого используй Hooks 7 | 8 | Импортировать нужные хуки можно так: 9 | import React, { useState } from "react"; 10 | 11 | Список хуков, которые могут пригодиться: useState, useRef, useEffect 12 | */ 13 | 14 | class App extends React.Component { 15 | constructor(props) { 16 | super(props); 17 | this.lastBlockId = 0; 18 | this.state = { 19 | blockIds: [] 20 | }; 21 | } 22 | 23 | addNew = () => { 24 | this.lastBlockId++; 25 | this.setState({ 26 | blockIds: [...this.state.blockIds, this.lastBlockId] 27 | }); 28 | }; 29 | 30 | removeLast = () => { 31 | this.setState({ 32 | blockIds: this.state.blockIds.slice(0, this.state.blockIds.length - 1) 33 | }); 34 | }; 35 | 36 | render() { 37 | return ( 38 |
39 |
40 | 47 | 50 |
51 |
52 | {this.state.blockIds.map(blockId => ( 53 | 54 | ))} 55 |
56 |
57 | ); 58 | } 59 | } 60 | 61 | class CounterBlock extends React.Component { 62 | constructor(props) { 63 | super(props); 64 | this.state = { 65 | value: 0 66 | }; 67 | } 68 | 69 | componentDidMount() { 70 | this.timer = setInterval(() => { 71 | this.setState({ value: this.state.value + 1 }); 72 | }, 1000); 73 | } 74 | 75 | componentWillUnmount() { 76 | clearInterval(this.timer); 77 | } 78 | 79 | render() { 80 | return
{this.state.value}
; 81 | } 82 | } 83 | 84 | ReactDom.render(, document.getElementById("app")); 85 | -------------------------------------------------------------------------------- /tasks/src/6.Hooks/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: arial; 3 | } 4 | 5 | .page { 6 | margin: auto; 7 | width: 600px; 8 | padding: 20px; 9 | background: lightblue; 10 | } 11 | 12 | .controlPanel { 13 | margin: auto; 14 | width: 100px; 15 | text-align: center; 16 | } 17 | 18 | .actionButton { 19 | background: linear-gradient(#2899ea, #167ac1); 20 | color: #fff; 21 | height: 34px; 22 | border-radius: 2px 2px 2px 2px; 23 | padding: 0 15px; 24 | border: 0; 25 | } 26 | 27 | .actionButton:hover { 28 | background: linear-gradient(#0087d5, #167ac1); 29 | cursor: pointer; 30 | } 31 | 32 | .actionButton + .actionButton { 33 | margin-left: 10px; 34 | } 35 | 36 | .container:not(:empty) { 37 | margin-top: 20px; 38 | display: inline-block; 39 | border: solid 1px #f3f3f3; 40 | padding: 10px; 41 | width: 100%; 42 | box-sizing: border-box; 43 | } 44 | 45 | .block { 46 | display: inline-flex; 47 | padding: 20px 20px; 48 | margin: 2px; 49 | background: #76c1ec; 50 | width: 28px; 51 | justify-content: center; 52 | align-items: center; 53 | overflow: hidden; 54 | } -------------------------------------------------------------------------------- /tasks/src/7.UsersTable/.solved/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import PropTypes from 'prop-types'; 4 | import '../styles.css'; 5 | import EditUserForm from '../EditUserForm'; 6 | import * as helpers from '../helpers'; 7 | import defaultUsers from '../defaultUsers'; 8 | 9 | /** 10 | Проблемы в исходной версии. 11 | 12 | Проблема 1. Использовать editingUser &&, иначе лишние маунты при попытке редактирования. 13 | Проблема 2. UserTable надо сделать PureComponent, иначе при попытке редактирования таблица перерисовывается. 14 | Проблема 3. Использовать user.id вместо index. 15 | Чтобы помогло надо сначала UserTableRow сделать PureComponent, 16 | либо написать в нем собственный shouldComponentUpdate 17 | После этого добавление станет работать эффективнее. 18 | Проблема 4. Написать свой shouldComponentUpdate в UserTableRow все же придется, 19 | потому что редактирование невидимых полей не должно приводить к рендерингу. 20 | Заметь, что shouldComponentUpdate нельзя опредилить у PureComponent. 21 | */ 22 | 23 | let generation = 1; 24 | let generationEvents = 1; 25 | 26 | function updateGeneration() { 27 | generation++; 28 | generationEvents = 1; 29 | } 30 | 31 | function logEvent(msg) { 32 | console.log(` ${generation}.${generationEvents++}\t${msg}`); 33 | } 34 | 35 | class Users extends React.Component { 36 | constructor() { 37 | super(); 38 | this.state = { 39 | users: defaultUsers, 40 | editingUser: null 41 | }; 42 | } 43 | 44 | render() { 45 | const { users, editingUser } = this.state; 46 | return ( 47 |
48 | {editingUser && ( 49 | 50 | )} 51 | 56 |
57 | ); 58 | } 59 | 60 | handleAddUser = () => { 61 | const newId = helpers.getNewId(this.state.users); 62 | updateGeneration(); 63 | this.setState({ 64 | users: [{ id: newId }, ...this.state.users] 65 | }); 66 | }; 67 | 68 | handleEditUser = user => { 69 | updateGeneration(); 70 | this.setState({ 71 | editingUser: user 72 | }); 73 | }; 74 | 75 | handleSaveUser = user => { 76 | updateGeneration(); 77 | this.setState({ 78 | editingUser: null, 79 | users: this.state.users.map(u => (u.id === user.id ? user : u)) 80 | }); 81 | }; 82 | } 83 | 84 | class UserTable extends React.PureComponent { 85 | componentDidMount() { 86 | logEvent('UserTable\t\t did mount'); 87 | } 88 | 89 | componentWillUnmount() { 90 | logEvent('UserTable\t\t will unmount'); 91 | } 92 | 93 | render() { 94 | logEvent('UserTable\t\t render'); 95 | const { users, onEditUser, onAddUser } = this.props; 96 | return ( 97 |
98 | 99 | 100 | 101 | 102 | 103 | 104 | 112 | 113 | 114 | 115 | {users.map((user, index) => ( 116 | 117 | ))} 118 | 119 |
ФамилияИмяВозраст 105 | 111 |
120 |
121 | ); 122 | } 123 | } 124 | 125 | UserTable.propTypes = { 126 | users: PropTypes.array, 127 | onEditUser: PropTypes.func, 128 | onAddUser: PropTypes.func 129 | }; 130 | 131 | class UserTableRow extends React.Component { 132 | componentDidMount() { 133 | logEvent('UserTableRow\t did mount with id=' + this.props.user.id); 134 | } 135 | 136 | componentWillUnmount() { 137 | logEvent('UserTableRow\t will unmount with id=' + this.props.user.id); 138 | } 139 | 140 | shouldComponentUpdate(nextProps, nextState) { 141 | if (!this.props) { 142 | return true; 143 | } 144 | const prevUser = this.props.user; 145 | const nextUser = nextProps.user; 146 | return ( 147 | prevUser.surname !== nextUser.surname || 148 | prevUser.surname !== nextUser.surname || 149 | (prevUser.dateOfBirth !== nextUser.dateOfBirth && 150 | helpers.calculateAge(prevUser.dateOfBirth) !== 151 | helpers.calculateAge(nextUser.dateOfBirth)) 152 | ); 153 | } 154 | 155 | render() { 156 | const { user } = this.props; 157 | logEvent('UserTableRow\t render with id=' + user.id); 158 | return ( 159 | 160 | {user.surname} 161 | {user.firstName} 162 | {helpers.calculateAge(user.dateOfBirth)} 163 | 164 | 170 | 171 | 172 | ); 173 | } 174 | 175 | handleEditUser = () => { 176 | this.props.onEditUser(this.props.user); 177 | }; 178 | } 179 | 180 | UserTableRow.propTypes = { 181 | user: PropTypes.object, 182 | onEditUser: PropTypes.func 183 | }; 184 | 185 | ReactDom.render(, document.getElementById('app')); 186 | -------------------------------------------------------------------------------- /tasks/src/7.UsersTable/EditUserForm.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import PropTypes from 'prop-types'; 4 | import * as helpers from './helpers'; 5 | 6 | export default class EditUserForm extends React.Component { 7 | constructor() { 8 | super(); 9 | this.state = { 10 | user: {} 11 | }; 12 | } 13 | 14 | static getDerivedStateFromProps(nextProps, prevState) { 15 | if (nextProps.user && prevState.user !== nextProps.user && !prevState.changed) { 16 | return { user: nextProps.user }; 17 | } 18 | return null; 19 | } 20 | 21 | render() { 22 | const { user } = this.state; 23 | return ( 24 |
25 |
26 |
27 |
Фамилия
28 | this.handleUserChange({ surname: e.target.value })} 32 | /> 33 |
34 |
35 |
Имя
36 | 40 | this.handleUserChange({ firstName: e.target.value }) 41 | } 42 | /> 43 |
44 |
45 |
Отчество
46 | 50 | this.handleUserChange({ patronymic: e.target.value }) 51 | } 52 | /> 53 |
54 |
55 |
Дата рождения
56 | 60 | this.handleUserChange({ dateOfBirth: new Date(e.target.value) }) 61 | } 62 | /> 63 |
64 |
65 |
Вегетарианец
66 | 70 | this.handleUserChange({ isVegetarian: e.target.checked }) 71 | } 72 | /> 73 |
74 |
75 |
Пожелания
76 | this.handleUserChange({ wishes: e.target.value })} 80 | /> 81 |
82 |
83 |
84 | 90 |
91 |
92 | ); 93 | } 94 | 95 | handleUserChange = change => { 96 | this.setState({ 97 | changed: true, 98 | user: { ...this.state.user, ...change } 99 | }); 100 | }; 101 | 102 | handleSave = () => { 103 | this.props.onSave(this.state.user); 104 | }; 105 | } 106 | 107 | EditUserForm.propTypes = { 108 | user: PropTypes.object, 109 | onSave: PropTypes.func 110 | }; 111 | -------------------------------------------------------------------------------- /tasks/src/7.UsersTable/defaultUsers.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | id: 0, 4 | surname: 'Иванов', 5 | firstName: 'Петр', 6 | patronymic: 'Сергеевич', 7 | dateOfBirth: new Date(2000, 10, 7), 8 | isVegetarian: false, 9 | wishes: '' 10 | }, 11 | { 12 | id: 1, 13 | surname: 'Петров', 14 | firstName: 'Александр', 15 | patronymic: 'Дмитриевич', 16 | dateOfBirth: new Date(1998, 7, 5), 17 | isVegetarian: true, 18 | wishes: 'Хочу сидеть рядом с Петей' 19 | } 20 | ]; 21 | -------------------------------------------------------------------------------- /tasks/src/7.UsersTable/helpers.js: -------------------------------------------------------------------------------- 1 | export function calculateAge(birthday) { 2 | if (!birthday || isNaN(+birthday)) { 3 | return ''; 4 | } 5 | const ageDifMs = Date.now() - new Date(birthday).getTime(); 6 | const ageDate = new Date(ageDifMs); 7 | return Math.abs(ageDate.getUTCFullYear() - 1970); 8 | } 9 | 10 | export function getNewId(withIdObjects) { 11 | const maxId = Math.max(...withIdObjects.map(obj => obj.id)); 12 | return maxId + 1; 13 | } 14 | 15 | export function formatDate(date) { 16 | if (!date) { 17 | return ''; 18 | } 19 | const d = padZero(date.getDate()); 20 | const m = padZero(date.getMonth() + 1); 21 | const y = date.getFullYear(); 22 | return y + '-' + m + '-' + d; 23 | } 24 | 25 | function padZero(value) { 26 | const str = value.toString(); 27 | return str.length === 1 ? '0' + str : str; 28 | } 29 | -------------------------------------------------------------------------------- /tasks/src/7.UsersTable/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UsersTable 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tasks/src/7.UsersTable/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import PropTypes from 'prop-types'; 4 | import EditUserForm from './EditUserForm'; 5 | import './styles.css'; 6 | import * as helpers from './helpers'; 7 | import defaultUsers from './defaultUsers'; 8 | 9 | /** 10 | Есть таблица гостей и форма для добавления гостя. В таблице отображена только основная информация. 11 | Всё работает, но неэффективно — при любом действии пользователя происходит множество лишних операций внутри React. 12 | 13 | В этом задании при вызове важных событий жизненного цикла выводятся сообщения в консоль. 14 | Эти сообщения можно увидеть в Developer Tools. 15 | 16 | Добейся того, чтобы лишние события не происходили. 17 | 18 | Допустимые события React: 19 | 1. В начале происходит 6 событий и это нормально: 20 | UserTable render, UserTableRow render (2), UserTableRow mount (2), UserTable mount 21 | 2. При добавлении новой строки должно быть 3 события: 22 | UserTable render, UserTableRow render и UserTableRow mount для новой строки 23 | 3. При нажатии на кнопку изменить: никаких событий 24 | 4. При сохранении после изменения видимого поля: UserTable render, UserTableRow render этого ряда 25 | 5. При сохранении после изменения невидимого поля : UserTable render 26 | 27 | FYI, в коде использованы такие фишки JS: 28 | - «Spread-оператор для массива» 29 | Создает новый массив, причем сначала в него добаляются все элементы objs, а затем еще один элемент. 30 | [...objs, { id: 1 }] 31 | - «Spread-оператор для объекта» 32 | Создает новый объект, причем сначала заполняет его свойствами из obj, а затем добавляет новое свойство. 33 | { ...obj, key: value } 34 | - «Деконструкция» 35 | Создает локальные переменные const a = this.state.a и const b = this.state.b. 36 | const { a, b } = this.state; 37 | */ 38 | 39 | let generation = 1; 40 | let generationEvents = 1; 41 | 42 | function updateGeneration() { 43 | generation++; 44 | generationEvents = 1; 45 | } 46 | 47 | function logEvent(msg) { 48 | console.log(` ${generation}.${generationEvents++}\t${msg}`); 49 | } 50 | 51 | class Users extends React.Component { 52 | constructor() { 53 | super(); 54 | this.state = { 55 | users: defaultUsers, 56 | editingUser: null 57 | }; 58 | } 59 | 60 | render() { 61 | const { users, editingUser } = this.state; 62 | if (editingUser) { 63 | return ( 64 |
65 | 66 | 71 |
72 | ); 73 | } 74 | return ( 75 |
76 | 81 |
82 | ); 83 | } 84 | 85 | handleAddUser = () => { 86 | const newId = helpers.getNewId(this.state.users); 87 | updateGeneration(); 88 | this.setState({ 89 | users: [{ id: newId }, ...this.state.users] 90 | }); 91 | }; 92 | 93 | handleEditUser = user => { 94 | updateGeneration(); 95 | this.setState({ 96 | editingUser: user 97 | }); 98 | }; 99 | 100 | handleSaveUser = user => { 101 | updateGeneration(); 102 | this.setState({ 103 | editingUser: null, 104 | users: this.state.users.map(u => (u.id === user.id ? user : u)) 105 | }); 106 | }; 107 | } 108 | 109 | class UserTable extends React.Component { 110 | componentDidMount() { 111 | logEvent('UserTable\t\t did mount'); 112 | } 113 | 114 | componentWillUnmount() { 115 | logEvent('UserTable\t\t will unmount'); 116 | } 117 | 118 | render() { 119 | logEvent('UserTable\t\t render'); 120 | const { users, onEditUser, onAddUser } = this.props; 121 | return ( 122 |
123 | 124 | 125 | 126 | 127 | 128 | 129 | 137 | 138 | 139 | 140 | {users.map((user, index) => ( 141 | 142 | ))} 143 | 144 |
ФамилияИмяВозраст 130 | 136 |
145 |
146 | ); 147 | } 148 | } 149 | 150 | UserTable.propTypes = { 151 | users: PropTypes.array, 152 | onEditUser: PropTypes.func, 153 | onAddUser: PropTypes.func 154 | }; 155 | 156 | class UserTableRow extends React.Component { 157 | componentDidMount() { 158 | logEvent('UserTableRow\t did mount with id=' + this.props.user.id); 159 | } 160 | 161 | componentWillUnmount() { 162 | logEvent('UserTableRow\t will unmount with id=' + this.props.user.id); 163 | } 164 | 165 | render() { 166 | const { user } = this.props; 167 | logEvent('UserTableRow\t render with id=' + user.id); 168 | return ( 169 | 170 | {user.surname} 171 | {user.firstName} 172 | {helpers.calculateAge(user.dateOfBirth)} 173 | 174 | 180 | 181 | 182 | ); 183 | } 184 | 185 | handleEditUser = () => { 186 | this.props.onEditUser(this.props.user); 187 | }; 188 | } 189 | 190 | UserTableRow.propTypes = { 191 | user: PropTypes.object, 192 | onEditUser: PropTypes.func 193 | }; 194 | 195 | ReactDom.render(, document.getElementById('app')); 196 | 197 | /** 198 | Подсказки: 199 | 200 | - React перерисовывает узлы по порядку. 201 | Если он увидит, что на месте div стоит span, то div будет полностью удален (unmount), 202 | даже если нужный div идет следом за этим span. 203 | Чтобы сохранить порядок узлов, оставляй «дырки» из null-узлов, undefined-узлов, false-узлов вот так: 204 | {showSpan && A little hint} 205 |
Main text
206 | Если span не нужен, то вместо него встанет невидимый false-узел, а div останется на своем месте. 207 | 208 | - Изменение setState в компоненте приводит к его перерисовке. Часто вместе с детьми. 209 | Но если дочерний компонент наследует PureComponent, то он не будет перерисован 210 | если его props не поменялись. Это можно использовать для оптимизации рендеринга 211 | 212 | - Ключ к производительности — в правильном задании key. 213 | 214 | - В конце тебе пригодится shouldComponentUpdate(nextProps, nextState). 215 | */ 216 | -------------------------------------------------------------------------------- /tasks/src/7.UsersTable/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: arial; 3 | } 4 | 5 | .root { 6 | margin: auto; 7 | width: 800px; 8 | } 9 | 10 | input { 11 | padding: 5px; 12 | margin: 10px 15px; 13 | border: 1px solid #aaa; 14 | border-radius: 3px; 15 | height: 18px; 16 | width: 200px; 17 | font-size: 18px; 18 | } 19 | 20 | input:focus { 21 | border: 1px solid #555; 22 | } 23 | 24 | .saveContainer { 25 | margin-top: 20px; 26 | text-align: center; 27 | } 28 | 29 | .addContainer { 30 | margin-top: 20px; 31 | text-align: right; 32 | } 33 | 34 | .actionButton { 35 | background: linear-gradient(#2899ea, #167ac1); 36 | color: #fff; 37 | height: 34px; 38 | border-radius: 2px 2px 2px 2px; 39 | padding: 0 15px; 40 | border: 0; 41 | } 42 | 43 | .actionButton:hover { 44 | background: linear-gradient(#0087d5, #167ac1); 45 | cursor: pointer; 46 | } 47 | 48 | .editButton { 49 | background: linear-gradient(#2899ea, #167ac1); 50 | color: #fff; 51 | height: 28px; 52 | width: 140px; 53 | border-radius: 2px 2px 2px 2px; 54 | padding: 0 15px; 55 | border: 0; 56 | font-size: 16px; 57 | } 58 | 59 | .editButton:hover { 60 | background: linear-gradient(#0087d5, #167ac1); 61 | cursor: pointer; 62 | } 63 | 64 | table { 65 | width: 100%; 66 | border: 1px solid #eee; 67 | border-collapse: collapse; 68 | table-layout: fixed; 69 | } 70 | 71 | th { 72 | background-color: #eee; 73 | border-bottom: 1px solid #eee; 74 | font-weight: 600; 75 | padding: 0.75rem; 76 | text-align: left; 77 | vertical-align: middle; 78 | } 79 | 80 | td { 81 | border-bottom: 1px solid #eee; 82 | padding: 0.75rem; 83 | border: 1px solid #eee; 84 | vertical-align: middle; 85 | } 86 | 87 | tr:hover { 88 | background-color: #daeffc; 89 | cursor: pointer; 90 | } 91 | 92 | .form { 93 | margin: 0px auto 20px auto; 94 | width: 600px; 95 | border: 1px solid #ddd; 96 | background-color: #eee; 97 | padding: 10px; 98 | } 99 | 100 | .row { 101 | height: 40px; 102 | } 103 | 104 | .label { 105 | display: inline-block; 106 | font-size: 18px; 107 | width: 200px; 108 | } 109 | 110 | .table { 111 | width: 800px; 112 | display: block; 113 | padding: 20px; 114 | border: 1px solid #eee; 115 | margin: auto; 116 | } 117 | -------------------------------------------------------------------------------- /tasks/src/8.FormRow/.solved/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import PropTypes from 'prop-types'; 4 | import '../styles.css'; 5 | import Toggle from '../Toggle'; 6 | 7 | function createFormRow(WrappedComponent) { 8 | class FormRow extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | } 12 | 13 | render() { 14 | const { label, forwardedRef, ...rest } = this.props; 15 | return ( 16 |
17 |
{label}
18 | 19 |
20 | ); 21 | } 22 | } 23 | 24 | FormRow.propTypes = { 25 | label: PropTypes.string.isRequired, 26 | forwardedRef: PropTypes.object 27 | }; 28 | 29 | const wrappedName = 30 | WrappedComponent.displayName || WrappedComponent.name || 'Component'; 31 | FormRow.displayName = `FormRow(${wrappedName})`; 32 | 33 | const forward = (props, ref) => ; 34 | forward.displayName = FormRow.displayName; 35 | return React.forwardRef(forward); 36 | } 37 | 38 | const InputFormRow = createFormRow(Input); 39 | const ToggleFormRow = createFormRow(Toggle); 40 | 41 | class Form extends React.Component { 42 | constructor() { 43 | super(); 44 | 45 | this.firstRowRef = React.createRef(); 46 | 47 | this.state = { 48 | opened: false 49 | }; 50 | } 51 | 52 | render() { 53 | const { opened } = this.state; 54 | return ( 55 |
56 | {!opened && this.renderOpenButton()} 57 | {opened && this.renderForm()} 58 |
59 | ); 60 | } 61 | 62 | renderOpenButton() { 63 | return ( 64 |
65 | 71 |
72 | ); 73 | } 74 | 75 | componentDidMount() { 76 | this.setFocusOnOpen(); 77 | } 78 | 79 | componentDidUpdate() { 80 | this.setFocusOnOpen(); 81 | } 82 | 83 | renderForm() { 84 | return ( 85 |
86 |
87 | 88 | 89 | 90 | 91 | 92 |
93 | 99 |
100 |
101 | ); 102 | } 103 | 104 | handleOpen = () => { 105 | this.setState({ 106 | opened: true 107 | }); 108 | }; 109 | 110 | handleSave = () => { 111 | this.setState({ 112 | opened: false 113 | }); 114 | }; 115 | 116 | setFocusOnOpen = () => { 117 | if (this.state.opened) { 118 | // Проверка перед вызовом нужна, 119 | // пока this.firstRowRef не устанавливается корректно. 120 | this.firstRowRef.current.focus && this.firstRowRef.current.focus(); 121 | } 122 | }; 123 | } 124 | 125 | Form.propTypes = { 126 | user: PropTypes.object 127 | }; 128 | 129 | ReactDom.render(
, document.getElementById('app')); 130 | -------------------------------------------------------------------------------- /tasks/src/8.FormRow/Input.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | 4 | export default class Input extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.ref = React.createRef(); 8 | } 9 | 10 | render() { 11 | return ; 12 | } 13 | 14 | focus = () => { 15 | this.ref.current.focus(); 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /tasks/src/8.FormRow/Toggle.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import './toggle.css'; 4 | 5 | export default class Toggle extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | checked: false 10 | }; 11 | this.ref = React.createRef(); 12 | } 13 | 14 | render() { 15 | const { checked } = this.state; 16 | return ( 17 | 24 | 25 |
26 | 27 | 28 | 29 | ); 30 | } 31 | 32 | handleClick = () => { 33 | this.setState({ 34 | checked: !this.state.checked 35 | }); 36 | }; 37 | 38 | focus = () => { 39 | this.ref.current.focus(); 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /tasks/src/8.FormRow/enhance.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import PropTypes from 'prop-types'; 4 | 5 | // Часто «улучшаемый» компонент называют WrappedComponent. 6 | // Первая прописная буква подчеркивает, что это компонент. 7 | function enhance(WrappedComponent) { 8 | // Внутри HOC определяет компонент-обертку с помощью класса или функции. 9 | class Enhanced extends React.Component { 10 | render() { 11 | //Свойства разделяются на две части вот так: 12 | const { value1, value2, ...rest } = this.props; 13 | 14 | //value1 и value2 может использоваться в Enhanced. 15 | return ( 16 | //А все остальное надо передать в оборачиваемый компонент. 17 | //В результате HOC можно будет для улучшения компонентов 18 | //с самыми разнообразными наборами свойств. 19 | 20 | ); 21 | } 22 | } 23 | 24 | Enhanced.propTypes = { 25 | value1: PropTypes.any, 26 | value2: PropTypes.any 27 | }; 28 | 29 | // Заданное displayName делает отладку удобнее. 30 | // В частности, это имя будет отображаться в Chrome Developer Tools на вкладке React. 31 | const wrappedName = 32 | WrappedComponent.displayName || WrappedComponent.name || 'Component'; 33 | Enhanced.displayName = `Enhanced(${wrappedName})`; 34 | 35 | //Этот компонент-обертка возвращается в качестве результата работы HOC. 36 | return Enhanced; 37 | } 38 | -------------------------------------------------------------------------------- /tasks/src/8.FormRow/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FormRow 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tasks/src/8.FormRow/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import PropTypes from 'prop-types'; 4 | import './styles.css'; 5 | import Input from './Input'; 6 | import Toggle from './Toggle'; 7 | 8 | /** 9 | InputFormRow — штука классная, но поддерживает только обычные input. 10 | В новой форме понадобилось поддержать самописный Toggle — пришлось написать ToggleFormRow. 11 | Получилось много дублирующегося кода и это грустно :( 12 | 13 | На помощь могут прийти Higher Order Components (HOC) — функции вида Component → Component. 14 | Используя HOC, можно создавать новые улучшенные компоненты из обычных: 15 | const EnhancedComponent = enhance(JustComponent); // enhance — это HOC 16 | 17 | HOC не получится использовать с элементами, например с input, 18 | поэтому он уже был обернут в компонент Input. 19 | 20 | 1. Напиши HOC createFormRow, который можно будет использовать так: 21 | const InputFormRow = createFormRow(Input); 22 | const ToggleFormRow = createFormRow(Toggle); 23 | В качестве примера используй HOC из enhance.js. 24 | 25 | 2. Используй createFormRow, удалив старые реализации InputFormRow, ToggleFormRow. 26 | 27 | 3. Открой React в Developer Tools в Chrome и убедись, 28 | что благодаря заданию displayName получающиеся компоненты называются красиво. 29 | Если у тебя нет пункта React в Developer Tools, поставь расширение для хрома React Developer Tools. 30 | 31 | 4. Сделай так, чтобы при открытии формы фокус устанавливался в первом поле формы. 32 | Весь необходимый код в Form уже написан: 33 | - firstRowRef установлен на первый ряд формы; 34 | - при открытии формы вызывается this.firstRowRef.current.focus(); 35 | Но this.firstRowRef.current не указывает на input или Toggle, у которых определен метод focus. 36 | HOC должен пересылать ref на WrappedComponent. 37 | 38 | ЗАМЕТЬ, что реализовать метод focus в HOC — это плохая идея. 39 | Если следовать ей, то надо в HOC добавлять все методы, которые хочется использовать «снаружи» 40 | для всех возможных WrappedComponent, с которыми будет использоваться HOC. 41 | */ 42 | 43 | class Form extends React.Component { 44 | constructor() { 45 | super(); 46 | 47 | this.firstRowRef = React.createRef(); 48 | 49 | this.state = { 50 | opened: false 51 | }; 52 | } 53 | 54 | render() { 55 | const { opened } = this.state; 56 | return ( 57 |
58 | {!opened && this.renderOpenButton()} 59 | {opened && this.renderForm()} 60 |
61 | ); 62 | } 63 | 64 | renderOpenButton() { 65 | return ( 66 |
67 | 73 |
74 | ); 75 | } 76 | 77 | componentDidMount() { 78 | this.setFocusOnOpen(); 79 | } 80 | 81 | componentDidUpdate() { 82 | this.setFocusOnOpen(); 83 | } 84 | 85 | renderForm() { 86 | return ( 87 |
88 | 89 | 90 | 91 | 92 | 93 | 94 |
95 | 101 |
102 |
103 | ); 104 | } 105 | 106 | handleOpen = () => { 107 | this.setState({ 108 | opened: true 109 | }); 110 | }; 111 | 112 | handleSave = () => { 113 | this.setState({ 114 | opened: false 115 | }); 116 | }; 117 | 118 | setFocusOnOpen = () => { 119 | if (this.state.opened) { 120 | // Проверка перед вызовом нужна, 121 | // пока this.firstRowRef не устанавливается корректно. 122 | this.firstRowRef.current.focus && this.firstRowRef.current.focus(); 123 | } 124 | }; 125 | } 126 | 127 | Form.propTypes = { 128 | user: PropTypes.object 129 | }; 130 | 131 | class InputFormRow extends React.Component { 132 | constructor(props) { 133 | super(props); 134 | } 135 | 136 | render() { 137 | const { label, ...rest } = this.props; 138 | return ( 139 |
140 |
{label}
141 | 142 |
143 | ); 144 | } 145 | } 146 | 147 | InputFormRow.propTypes = { 148 | label: PropTypes.string.isRequired 149 | }; 150 | 151 | class ToggleFormRow extends React.Component { 152 | constructor(props) { 153 | super(props); 154 | } 155 | 156 | render() { 157 | const { label, ...rest } = this.props; 158 | return ( 159 |
160 |
{label}
161 | 162 |
163 | ); 164 | } 165 | } 166 | 167 | ToggleFormRow.propTypes = { 168 | label: PropTypes.string.isRequired 169 | }; 170 | 171 | ReactDom.render(
, document.getElementById('app')); 172 | 173 | /** 174 | Подсказки к 4: 175 | - Нельзя пробросить переменную для ref из внешнего компонента во внутренний через атрибут ref, 176 | потому что он зарезервирован — придется использовать другой атрибут. Например, forwardedRef. 177 | 178 | - Чтобы React пробрасывал ref, в конец HOC придется добавить такой код: 179 | 180 | const forward = (props, ref) => ; 181 | forward.displayName = FormRow.displayName; 182 | return React.forwardRef(forward); 183 | 184 | Предполагается, что компонент-обертка называется FormRow. 185 | Заметь, что React.forwardRef — это почти HOC, а forward — это почти функция-компонент. 186 | Подробнее про ForwardedRef написано в документации https://reactjs.org/docs/forwarding-refs.html 187 | 188 | - forwardedRef — это PropTypes.object 189 | 190 | - Когда ref уже лежит в отдельном атрибуте, его несложно использовать: 191 | 192 | */ 193 | -------------------------------------------------------------------------------- /tasks/src/8.FormRow/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: arial; 3 | } 4 | 5 | .root { 6 | margin: auto; 7 | width: 800px; 8 | } 9 | 10 | input { 11 | padding: 5px; 12 | margin: 10px 15px; 13 | border: 1px solid #aaa; 14 | border-radius: 3px; 15 | height: 18px; 16 | width: 200px; 17 | font-size: 18px; 18 | } 19 | 20 | input:focus { 21 | border: 1px solid #555; 22 | } 23 | 24 | .openContainer { 25 | text-align: center; 26 | } 27 | 28 | .saveContainer { 29 | margin-top: 20px; 30 | text-align: center; 31 | } 32 | 33 | .addContainer { 34 | margin-top: 20px; 35 | text-align: right; 36 | } 37 | 38 | .actionButton { 39 | background: linear-gradient(#2899ea, #167ac1); 40 | color: #fff; 41 | height: 34px; 42 | border-radius: 2px 2px 2px 2px; 43 | padding: 0 15px; 44 | border: 0; 45 | } 46 | 47 | .actionButton:hover { 48 | background: linear-gradient(#0087d5, #167ac1); 49 | cursor: pointer; 50 | } 51 | 52 | .editButton { 53 | background: linear-gradient(#2899ea, #167ac1); 54 | color: #fff; 55 | height: 28px; 56 | width: 140px; 57 | border-radius: 2px 2px 2px 2px; 58 | padding: 0 15px; 59 | border: 0; 60 | font-size: 16px; 61 | } 62 | 63 | .editButton:hover { 64 | background: linear-gradient(#0087d5, #167ac1); 65 | cursor: pointer; 66 | } 67 | 68 | table { 69 | width: 100%; 70 | border: 1px solid #eee; 71 | border-collapse: collapse; 72 | table-layout: fixed; 73 | } 74 | 75 | th { 76 | background-color: #eee; 77 | border-bottom: 1px solid #eee; 78 | font-weight: 600; 79 | padding: 0.75rem; 80 | text-align: left; 81 | vertical-align: middle; 82 | } 83 | 84 | td { 85 | border-bottom: 1px solid #eee; 86 | padding: 0.75rem; 87 | border: 1px solid #eee; 88 | vertical-align: middle; 89 | } 90 | 91 | tr:hover { 92 | background-color: #daeffc; 93 | cursor: pointer; 94 | } 95 | 96 | .form { 97 | margin: 0px auto 20px auto; 98 | width: 600px; 99 | border: 1px solid #ddd; 100 | background-color: #eee; 101 | padding: 10px; 102 | } 103 | 104 | .row { 105 | height: 40px; 106 | } 107 | 108 | .label { 109 | display: inline-block; 110 | font-size: 18px; 111 | width: 200px; 112 | } 113 | 114 | .table { 115 | width: 800px; 116 | display: block; 117 | padding: 20px; 118 | border: 1px solid #eee; 119 | margin: auto; 120 | } 121 | -------------------------------------------------------------------------------- /tasks/src/8.FormRow/toggle.css: -------------------------------------------------------------------------------- 1 | .container { 2 | position: relative; 3 | 4 | display: inline-block; 5 | width: 34px; 6 | height: 20px; 7 | box-sizing: border-box; 8 | border: 1px solid #d0d0d0; 9 | 10 | transition: border .2s ease-in; 11 | transition-delay: .05s; 12 | 13 | border-radius: 10px; 14 | background: white; 15 | 16 | cursor: pointer; 17 | 18 | margin: 15px 15px; 19 | } 20 | 21 | .container:after { 22 | content: ' '; 23 | display: inline-block; 24 | } 25 | 26 | .background { 27 | position: absolute; 28 | right: 100%; 29 | top: 0; 30 | width: 100%; 31 | height: 100%; 32 | background: #3072c4; 33 | transition: right 0.2s ease-in; 34 | } 35 | 36 | .isChecked { 37 | border: 1px solid #3072c4; 38 | } 39 | 40 | .handle { 41 | position: absolute; 42 | top: 0px; 43 | bottom: 0px; 44 | left: 0px; 45 | z-index: 2; 46 | 47 | width: 18px; 48 | margin-left: 0; 49 | 50 | transition: width 0.2s cubic-bezier(0.4, 0, 1, 1); 51 | } 52 | 53 | .isChecked .handle { 54 | width: 32px; 55 | } 56 | 57 | .bg { 58 | height: 100%; 59 | overflow: hidden; 60 | position: relative; 61 | border-radius: 9px; 62 | left: 0; 63 | transition: left 0.2s ease-in; 64 | transition-delay: 0.05s; 65 | } 66 | 67 | .isChecked .bg { 68 | left: -1px; 69 | } 70 | 71 | .bg:after { 72 | content: ' '; 73 | position: absolute; 74 | right: 10px; 75 | top: 0px; 76 | width: 23px; 77 | border-radius: 9 0 0 9; 78 | height: 18px; 79 | background: #3072c4; 80 | } 81 | 82 | .hinge { 83 | display: inline-block; 84 | content: ' '; 85 | position: absolute; 86 | top: 0; 87 | right: 0; 88 | bottom: 0; 89 | border-radius: 9px; 90 | width: 18px; 91 | background-image: linear-gradient(-180deg, #FFFFFF, #EBEBEB); 92 | box-shadow: 0 1px 0 0 rgba(0, 0, 0, .15), 0 0 0 1px rgba(0, 0, 0, .15); 93 | } 94 | -------------------------------------------------------------------------------- /tasks/src/9.ColorsOfTime/.solved/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import PropTypes from 'prop-types'; 4 | import '../styles.css'; 5 | import * as helpers from '../helpers'; 6 | import * as themes from '../themes'; 7 | import TimeDisplay from '../TimeDisplay'; 8 | import Timer from '../Timer'; 9 | 10 | const CurrentTimeContext = React.createContext(); 11 | const ThemeContext = themes.Context; 12 | const ChangeThemeContext = React.createContext(); 13 | 14 | const ThemedButton = themes.withTheme(Button); 15 | 16 | class ColorsOfTime extends React.Component { 17 | constructor(props) { 18 | super(props); 19 | this.state = { 20 | currentTime: null, 21 | theme: themes.red 22 | }; 23 | } 24 | 25 | componentDidMount() { 26 | this.props.timer.addUpdated(this.handleTimerUpdated); 27 | } 28 | 29 | componentWillUnmount() { 30 | this.props.timer.removeUpdated(this.handleTimerUpdated); 31 | } 32 | 33 | render() { 34 | const { currentTime, theme } = this.state; 35 | return ( 36 | 37 | 38 | 39 |
40 |

Цвета времени

41 | 42 | 43 | 44 |
45 |
46 |
47 |
48 | ); 49 | } 50 | 51 | handleTimerUpdated = currentTime => { 52 | this.setState({ currentTime: currentTime }); 53 | }; 54 | 55 | dispatchChangeTheme = type => { 56 | let newTheme = null; 57 | switch (type) { 58 | case 'prev': 59 | newTheme = themes.getPrevTheme(this.state.theme); 60 | break; 61 | case 'next': 62 | newTheme = themes.getNextTheme(this.state.theme); 63 | break; 64 | } 65 | this.setState({ theme: newTheme }); 66 | }; 67 | } 68 | 69 | ColorsOfTime.propTypes = { 70 | timer: PropTypes.object 71 | }; 72 | 73 | class Top extends React.PureComponent { 74 | render() { 75 | registerRenderForDebug('Top'); 76 | return ( 77 |
78 | 79 | 80 | 81 | 82 |
83 | ); 84 | } 85 | } 86 | 87 | Top.propTypes = {}; 88 | 89 | class Middle extends React.PureComponent { 90 | render() { 91 | return ( 92 |
93 | 94 | {theme => ( 95 | 96 | )} 97 | 98 |
99 | ); 100 | } 101 | } 102 | 103 | Middle.propTypes = {}; 104 | 105 | class Bottom extends React.PureComponent { 106 | render() { 107 | return ( 108 |
109 | 110 | {dispatchChangeTheme => ( 111 | dispatchChangeTheme('prev')} 114 | /> 115 | )} 116 | 117 | 118 | {dispatchChangeTheme => ( 119 | dispatchChangeTheme('next')} 122 | /> 123 | )} 124 | 125 |
126 | ); 127 | } 128 | } 129 | 130 | Bottom.propTypes = {}; 131 | 132 | class Card extends React.Component { 133 | render() { 134 | registerRenderForDebug('Card'); 135 | const { title, timezone, color } = this.props; 136 | return ( 137 |
138 |

{title}

139 |
140 | 141 | {currentTime => ( 142 | 150 | )} 151 | 152 |
153 |
154 | ); 155 | } 156 | } 157 | 158 | Card.propTypes = { 159 | title: PropTypes.string.isRequired, 160 | color: PropTypes.string, 161 | timezone: PropTypes.number 162 | }; 163 | 164 | function registerRenderForDebug(name) { 165 | console.log(`render ${name} at ${new Date().toLocaleTimeString()}`); 166 | } 167 | 168 | const timer = new Timer(); 169 | ReactDom.render(, document.getElementById('app')); 170 | 171 | /** 172 | Подсказки: 173 | - Создание контекста: 174 | const CakeContext = React.createContext(); 175 | - Поставка значения: 176 | 177 | ... 178 | 179 | - Потребление значения: 180 | 181 | {cake => } 182 | 183 | */ 184 | -------------------------------------------------------------------------------- /tasks/src/9.ColorsOfTime/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import PropTypes from 'prop-types'; 4 | import './styles.css'; 5 | 6 | export default function Button({ value, theme, onClick }) { 7 | return ( 8 | 15 | ); 16 | } 17 | 18 | Button.propTypes = { 19 | value: PropTypes.string.isRequired, 20 | theme: PropTypes.object.isRequired, 21 | onClick: PropTypes.func 22 | }; 23 | -------------------------------------------------------------------------------- /tasks/src/9.ColorsOfTime/TimeDisplay.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import PropTypes from 'prop-types'; 4 | import './styles.css'; 5 | 6 | export default function TimeDisplay({ time, color }) { 7 | return ( 8 |
9 | {time ? time.toLocaleTimeString() : '--:--:--'} 10 |
11 | ); 12 | } 13 | 14 | TimeDisplay.propTypes = { 15 | time: PropTypes.object, 16 | color: PropTypes.string 17 | }; 18 | -------------------------------------------------------------------------------- /tasks/src/9.ColorsOfTime/Timer.js: -------------------------------------------------------------------------------- 1 | export default class Timer { 2 | constructor() { 3 | this.tickInterval = null; 4 | this.handlers = []; 5 | } 6 | 7 | addUpdated(handler) { 8 | this.handlers.push(handler); 9 | if (!this.tickInterval) { 10 | this.tickInterval = setInterval(() => { 11 | const currentTime = new Date(); 12 | for (const handler of this.handlers) { 13 | handler(currentTime); 14 | } 15 | }, 1000); 16 | } 17 | } 18 | 19 | removeUpdated(handler) { 20 | this.handlers = this.handlers.filter(h => h !== handler); 21 | if (this.handlers.length === 0 && this.tickInterval) { 22 | clearInterval(this.tickInterval); 23 | this.tickInterval = null; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tasks/src/9.ColorsOfTime/helpers.js: -------------------------------------------------------------------------------- 1 | export function toUtc(date) { 2 | if (!date) { 3 | return date; 4 | } 5 | return new Date(date.getTime() + date.getTimezoneOffset() * 60000); 6 | } 7 | 8 | export function toTimezone(date, offset) { 9 | if (!date) { 10 | return date; 11 | } 12 | const utc = date.getTime() + date.getTimezoneOffset() * 60000; 13 | return new Date(utc + 3600000 * offset); 14 | } 15 | -------------------------------------------------------------------------------- /tasks/src/9.ColorsOfTime/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ColorsOfTime 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tasks/src/9.ColorsOfTime/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import PropTypes from 'prop-types'; 4 | import './styles.css'; 5 | import * as helpers from './helpers'; 6 | import * as themes from './themes'; 7 | import Button from './Button'; 8 | import TimeDisplay from './TimeDisplay'; 9 | import Timer from './Timer'; 10 | 11 | /** 12 | Автор кода явно сделал много лишней работы, 13 | прокидывая информацию о времени и настройках цвета через все компоненты. 14 | А все потому, что не знал про context! 15 | 16 | Для начала разведка ситуации: 17 | 1. Открой Developer Tools и убедись, что render в Card вызывается по 5 раз каждую секунду. 18 | 2. Убедись, что render в Card вызывается при использовании кнопок смены цвета. 19 | 3. Почему render в Top вызывается каждую секунду, если Top — это PureComponent у которого в props нет currentTime? 20 | 4. Подумай, что нужно сделать, чтобы перенести карточку Нью-Йорка в блок Top, а кнопки смены цвета в блок Bottom. 21 | 22 | Отрефактори код по шагам: 23 | 1. Создай CurrentTimeContext. 24 | 2. В компоненте ColorsOfTime в методе render оберни
...
в CurrentTimeContext.Provider, 25 | чтобы предоставить максимально большой доступ к value провайдера. В качестве value в тэге Provider задай currentTime. 26 | 3. Используй CurrentTimeContext.Consumer, чтобы не прокидывать currentTime через свойства. 27 | Тут стратегия минимизации: надо оборачивать в Consumer только те компоненты, которым ресурс требуется. 28 | Потому что при обновлении значения контекста будет перерисовываться все, что внутри Consumer'ов. 29 | 4. Не забудь убрать ненужное теперь простаскивание currentTime через параметры! 30 | 5. Открой Developer Tools и посмотри, как часто вызывается render в Card с течением времени. 31 | Попробуй объяснить, почему использование context привело к такому эффекту. 32 | 6. Проделай то же самое для ThemeContext: 33 | - Создай ThemeContext 34 | - Оберни CurrentTimeContext.Provider в ThemeContext.Provider 35 | - Используй ThemeContext.Consumer для передачи темы в кнопки и в Card с цветным локальным временем 36 | - Снова приберись в коде! 37 | 7. Добавь ChangeThemeContext. Пусть он хранит ссылку на функцию dispatchChangeTheme. 38 | Пусть кнопки смены цвета теперь создают обработчики на основе ChangeThemeContext, 39 | а не получают их через onPrevTheme и onNextTheme. 40 | Приберись в коде. 41 | 8. Открой Developer Tools, и убедись, перестал происходить render в Top. Объясни, почему так. 42 | 9. Перенеси Лондон в блок Top, за ним в блок Top перенеси Нью-Йорк, Париж и Пекин. 43 | А кнопки смены цвета перенеси в блок Bottom. 44 | Удобно ли было переносить эти компоненты сейчас? 45 | 10. Если контекст используется часто, можно создать специальный HOC компонент, чтобы оборачивать компоненты в Consumer. 46 | Найди в themes.js Context и используй в качесте ThemeContext: 47 | const ThemeContext = themes.Context; 48 | Теперь ты можешь определить кнопку так: 49 | const ThemedButton = themes.withTheme(Button); 50 | Используй ее! 51 | */ 52 | 53 | class ColorsOfTime extends React.Component { 54 | constructor(props) { 55 | super(props); 56 | this.state = { 57 | currentTime: null, 58 | theme: themes.red 59 | }; 60 | } 61 | 62 | componentDidMount() { 63 | this.props.timer.addUpdated(this.handleTimerUpdated); 64 | } 65 | 66 | componentWillUnmount() { 67 | this.props.timer.removeUpdated(this.handleTimerUpdated); 68 | } 69 | 70 | render() { 71 | const { currentTime, theme } = this.state; 72 | return ( 73 |
74 |

Цвета времени

75 | this.dispatchChangeTheme('prev')} 78 | onNextTheme={() => this.dispatchChangeTheme('next')} 79 | /> 80 | 81 | 82 |
83 | ); 84 | } 85 | 86 | handleTimerUpdated = currentTime => { 87 | this.setState({ currentTime: currentTime }); 88 | }; 89 | 90 | dispatchChangeTheme = type => { 91 | let newTheme = null; 92 | switch (type) { 93 | case 'prev': 94 | newTheme = themes.getPrevTheme(this.state.theme); 95 | break; 96 | case 'next': 97 | newTheme = themes.getNextTheme(this.state.theme); 98 | break; 99 | } 100 | this.setState({ theme: newTheme }); 101 | }; 102 | } 103 | 104 | ColorsOfTime.propTypes = { 105 | timer: PropTypes.object 106 | }; 107 | 108 | class Top extends React.PureComponent { 109 | render() { 110 | registerRenderForDebug('Top'); 111 | const { theme, onPrevTheme, onNextTheme } = this.props; 112 | return ( 113 |
114 |
117 | ); 118 | } 119 | } 120 | 121 | Top.propTypes = { 122 | theme: PropTypes.object.isRequired, 123 | onPrevTheme: PropTypes.func, 124 | onNextTheme: PropTypes.func 125 | }; 126 | 127 | class Middle extends React.PureComponent { 128 | render() { 129 | const { currentTime, theme } = this.props; 130 | return ( 131 |
132 | 137 | 138 |
139 | ); 140 | } 141 | } 142 | 143 | Middle.propTypes = { 144 | theme: PropTypes.object.isRequired, 145 | currentTime: PropTypes.object 146 | }; 147 | 148 | class Bottom extends React.PureComponent { 149 | render() { 150 | const { currentTime } = this.props; 151 | return ( 152 |
153 | 159 | 165 | 171 |
172 | ); 173 | } 174 | } 175 | 176 | Bottom.propTypes = { 177 | currentTime: PropTypes.object 178 | }; 179 | 180 | class Card extends React.PureComponent { 181 | render() { 182 | registerRenderForDebug('Card'); 183 | const { title, timezone, currentTime, color } = this.props; 184 | return ( 185 |
186 |

{title}

187 |
188 | 194 |
195 |
196 | ); 197 | } 198 | } 199 | 200 | Card.propTypes = { 201 | title: PropTypes.string.isRequired, 202 | color: PropTypes.string, 203 | timezone: PropTypes.number, 204 | currentTime: PropTypes.object 205 | }; 206 | 207 | function registerRenderForDebug(name) { 208 | console.log(`render ${name} at ${new Date().toLocaleTimeString()}`); 209 | } 210 | 211 | const timer = new Timer(); 212 | ReactDom.render(, document.getElementById('app')); 213 | 214 | /** 215 | Подсказки: 216 | - Создание контекста: 217 | const CakeContext = React.createContext(); 218 | - Поставка значения: 219 | 220 | ... 221 | 222 | - Потребление значения: 223 | 224 | {cake => } 225 | 226 | */ 227 | -------------------------------------------------------------------------------- /tasks/src/9.ColorsOfTime/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: arial; 3 | } 4 | 5 | html, 6 | body { 7 | background: #b9b7b1; 8 | } 9 | 10 | h1, h2, h3 { 11 | text-align: center; 12 | } 13 | 14 | h3 { 15 | margin-top: 5px; 16 | margin-bottom: 10px; 17 | } 18 | 19 | .page { 20 | margin: auto; 21 | width: 300px; 22 | } 23 | 24 | .card { 25 | background: #cccccc; 26 | padding: 10px; 27 | margin: 10px 0px; 28 | } 29 | 30 | .time { 31 | position: relative; 32 | left: 50%; 33 | margin-left: -75px; 34 | width: 150px; 35 | padding: 10px 0px; 36 | text-align: center; 37 | font-size: 24px; 38 | background: #fff; 39 | color: #707070; 40 | } 41 | 42 | .red { 43 | color: hsl(0, 100%, 30%); 44 | } 45 | 46 | .redBack { 47 | background-color: hsl(0, 70%, 90%); 48 | } 49 | 50 | .yellow { 51 | color: hsl(60, 100%, 30%); 52 | } 53 | 54 | .yellowBack { 55 | background-color: hsl(60, 70%, 90%); 56 | } 57 | 58 | .green { 59 | color: hsl(120, 100%, 30%); 60 | } 61 | 62 | .greenBack { 63 | background-color: hsl(120, 70%, 90%); 64 | } 65 | 66 | .cyan { 67 | color: hsl(180, 100%, 30%); 68 | } 69 | 70 | .cyanBack { 71 | background-color: hsl(180, 70%, 90%); 72 | } 73 | 74 | .blue { 75 | color: hsl(240, 100%, 30%); 76 | } 77 | 78 | .blueBack { 79 | background-color: hsl(240, 70%, 90%); 80 | } 81 | 82 | .magenta { 83 | color: hsl(300, 100%, 30%); 84 | } 85 | 86 | .magentaBack { 87 | background-color: hsl(300, 70%, 90%); 88 | } 89 | 90 | .button { 91 | height: 34px; 92 | border-radius: 2px 2px 2px 2px; 93 | padding: 0 15px; 94 | margin: 5px; 95 | border: solid 2px; 96 | } 97 | 98 | .button:hover { 99 | cursor: pointer; 100 | } 101 | 102 | .block { 103 | margin-top: 30px; 104 | text-align: center; 105 | margin-bottom: 30px; 106 | } 107 | -------------------------------------------------------------------------------- /tasks/src/9.ColorsOfTime/themes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const red = { 4 | foregroundColor: 'red', 5 | backgroundColor: 'redBack' 6 | }; 7 | 8 | export const yellow = { 9 | foregroundColor: 'yellow', 10 | backgroundColor: 'yellowBack' 11 | }; 12 | 13 | export const green = { 14 | foregroundColor: 'green', 15 | backgroundColor: 'greenBack' 16 | }; 17 | 18 | export const cyan = { 19 | foregroundColor: 'cyan', 20 | backgroundColor: 'cyanBack' 21 | }; 22 | 23 | export const blue = { 24 | foregroundColor: 'blue', 25 | backgroundColor: 'blueBack' 26 | }; 27 | 28 | export const magenta = { 29 | foregroundColor: 'magenta', 30 | backgroundColor: 'magentaBack' 31 | }; 32 | 33 | export const all = [red, yellow, green, cyan, blue, magenta]; 34 | 35 | export function getPrevTheme(theme) { 36 | return all[(all.indexOf(theme) + all.length - 1) % all.length]; 37 | } 38 | 39 | export function getNextTheme(theme) { 40 | return all[(all.indexOf(theme) + all.length + 1) % all.length]; 41 | } 42 | 43 | // Пригодится в конце. 44 | export const Context = React.createContext(); 45 | export const Provider = Context.Provider; 46 | export const Consumer = Context.Consumer; 47 | 48 | export function withTheme(WrappedComponent) { 49 | class Themed extends React.Component { 50 | render() { 51 | return ( 52 | 53 | {theme => } 54 | 55 | ); 56 | } 57 | } 58 | 59 | const wrappedName = 60 | WrappedComponent.displayName || WrappedComponent.name || 'Component'; 61 | Themed.displayName = `withTheme(${wrappedName})`; 62 | 63 | return Themed; 64 | } 65 | -------------------------------------------------------------------------------- /tasks/tasks.js: -------------------------------------------------------------------------------- 1 | var tasks = [ 2 | { order: '1.1', id: 'SimpleHtml' }, 3 | { order: '1.2', id: 'Button' }, 4 | { order: '1.3', id: 'Input' }, 5 | { order: '2.1', id: 'ExtractFunction' }, 6 | { order: '2.2', id: 'List' }, 7 | { order: '2.3', id: 'Condition' }, 8 | { order: '3.1', id: 'ExtractComponent' }, 9 | { order: '3.2', id: 'Toggle' }, 10 | { order: '3.3', id: 'MoneyConverter' }, 11 | { order: '4.1', id: 'Timer' }, 12 | { order: '4.2', id: 'CreditCardInput' }, 13 | { order: '5', id: 'Focus' }, 14 | { order: '6', id: 'Hooks' }, 15 | { order: '7', id: 'UsersTable' }, 16 | { order: '8', id: 'FormRow' }, 17 | { order: '9', id: 'ColorsOfTime' }, 18 | ]; 19 | 20 | if (typeof exports !== 'undefined') { 21 | exports.tasks = tasks; 22 | } 23 | -------------------------------------------------------------------------------- /tasks/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var tasks = require('./tasks').tasks; 3 | 4 | 5 | var entries = {}; 6 | var rewrites = []; 7 | for (var k in tasks) { 8 | addTask(tasks[k].order, tasks[k].id); 9 | } 10 | 11 | function addTask(order, id) { 12 | var orderAndId = order + '.' + id; 13 | entries[id] = ['./src/' + orderAndId + '/index.js']; 14 | rewrites.push({ 15 | from: new RegExp('^\/(' + orderAndId.replace(/\./, '\\.') + ')|(' + order + ')$', 'i'), 16 | to: '/src/' + orderAndId + '/index.html' 17 | }); 18 | } 19 | 20 | module.exports = { 21 | entry: entries, 22 | output: { 23 | path: path.resolve('build'), 24 | publicPath: 'build', 25 | filename: '[name].js', 26 | }, 27 | devtool: 'source-map', 28 | module: { 29 | rules: [ 30 | { 31 | test: /\.jsx?$/, 32 | exclude: /node_modules/, 33 | use: ['babel-loader', 'eslint-loader'] 34 | }, 35 | { 36 | test: /\.css$/, 37 | loader: 'style-loader!css-loader' 38 | } 39 | ] 40 | }, 41 | resolve: { 42 | modules: ['node_modules'], 43 | extensions: ['.js', '.jsx'], 44 | }, 45 | devServer: { 46 | historyApiFallback: { 47 | rewrites: rewrites, 48 | }, 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /userProfile/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": ["@babel/plugin-proposal-class-properties"] 4 | } 5 | -------------------------------------------------------------------------------- /userProfile/.eslintignore: -------------------------------------------------------------------------------- 1 | webpack.config.js 2 | build/* 3 | -------------------------------------------------------------------------------- /userProfile/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:react/recommended" 5 | ], 6 | "parser": "babel-eslint", 7 | "rules": { 8 | "no-console": "off", 9 | "no-useless-escape": "off", 10 | "arrow-body-style": "off", 11 | "no-useless-computed-key": "off", 12 | "object-shorthand": "off", 13 | "prefer-template": "off", 14 | "no-debugger": "off", 15 | "no-unused-vars": "off" 16 | }, 17 | "env": { 18 | "browser": true, 19 | "commonjs": true, 20 | "node": true, 21 | "es6": true 22 | }, 23 | "parserOptions": { 24 | "ecmaVersion": 2015, 25 | "sourceType": "module", 26 | "ecmaFeatures": { 27 | "jsx": true 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /userProfile/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | node_modules 3 | build 4 | -------------------------------------------------------------------------------- /userProfile/.solved/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React Interface 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /userProfile/.solved/src/Form.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from '@skbkontur/react-ui/Button'; 3 | import Input from '@skbkontur/react-ui/Input'; 4 | import Select from '@skbkontur/react-ui/Select'; 5 | import Gapped from '@skbkontur/react-ui/Gapped'; 6 | import Modal from '@skbkontur/react-ui/Modal'; 7 | 8 | const cities = ['Москва', 'Урюпинск', 'Новосибирск', 'Екатеринбург', 'Тагиииил']; 9 | 10 | const defaultData = { 11 | name: '', 12 | surname: '', 13 | city: 'Екатеринбург', 14 | }; 15 | 16 | export default class Form extends React.Component { 17 | state = { 18 | modalOpened: false, 19 | saved: { ...defaultData }, 20 | current: { ...defaultData }, 21 | }; 22 | 23 | render () { 24 | const { modalOpened } = this.state; 25 | return ( 26 |
27 |

Информация о пользователе

28 | { this.renderForm() } 29 | {modalOpened && this.renderModal()} 30 |
31 | ); 32 | } 33 | 34 | renderForm() { 35 | const { name, surname, city } = this.state.current; 36 | return ( 37 | 38 | 39 | 47 | 55 |