├── .babelrc ├── .gitignore ├── .stylelintrc ├── LICENSE ├── README.md ├── conf ├── webpack.base.config.js ├── webpack.development.config.js ├── webpack.makeup.config.js └── webpack.production.config.js ├── npm-shrinkwrap.json ├── package.json ├── src ├── components │ ├── activity-card │ │ ├── activity-card.js │ │ ├── activity-card.pug │ │ ├── activity-card.scss │ │ └── img │ │ │ ├── default.jpg │ │ │ └── img1.jpg │ ├── arrow-button │ │ ├── __tests__ │ │ │ ├── index.pug │ │ │ └── makeup-data.js │ │ ├── arrow-button.js │ │ ├── arrow-button.pug │ │ ├── arrow-button.scss │ │ └── img │ │ │ ├── down.svg │ │ │ ├── left.svg │ │ │ ├── right.svg │ │ │ └── up.svg │ ├── button │ │ ├── __tests__ │ │ │ ├── img │ │ │ │ └── button-default.png │ │ │ ├── index.pug │ │ │ └── makeup-data.js │ │ ├── button.js │ │ ├── button.pug │ │ └── button.scss │ ├── calendar │ │ ├── __tests__ │ │ │ ├── index.pug │ │ │ └── makeup-data.js │ │ ├── calendar.js │ │ ├── calendar.pug │ │ ├── calendar.scss │ │ └── img │ │ │ ├── left.svg │ │ │ └── right.svg │ ├── drop-down │ │ ├── drop-down.js │ │ ├── drop-down.pug │ │ ├── drop-down.scss │ │ └── img │ │ │ ├── down.svg │ │ │ └── up.svg │ ├── event │ │ ├── event.js │ │ ├── event.pug │ │ └── event.scss │ ├── feedback │ │ ├── feedback.js │ │ ├── feedback.pug │ │ └── feedback.scss │ ├── footer │ │ ├── footer.js │ │ ├── footer.pug │ │ └── footer.scss │ ├── header │ │ ├── header.js │ │ ├── header.pug │ │ └── header.scss │ ├── input │ │ ├── input.js │ │ ├── input.pug │ │ └── input.scss │ ├── layout │ │ ├── layout.js │ │ ├── layout.pug │ │ └── layout.scss │ ├── location │ │ ├── img │ │ │ ├── crosshair.svg │ │ │ ├── map-marker.png │ │ │ └── map-marker.svg │ │ ├── location.js │ │ ├── location.pug │ │ └── location.scss │ ├── menu │ │ ├── img │ │ │ └── menu-options.svg │ │ ├── menu.js │ │ ├── menu.pug │ │ └── menu.scss │ ├── messenger │ │ ├── img │ │ │ ├── message.svg │ │ │ └── photo-camera.svg │ │ ├── messenger.js │ │ ├── messenger.pug │ │ ├── messenger.scss │ │ └── template.pug │ ├── news │ │ ├── news.js │ │ ├── news.pug │ │ └── news.scss │ ├── percentage │ │ ├── percentage.js │ │ ├── percentage.pug │ │ └── percentage.scss │ ├── pie-chart │ │ ├── pie-chart.js │ │ ├── pie-chart.pug │ │ └── pie-chart.scss │ ├── search │ │ ├── img │ │ │ └── glass-finder.svg │ │ ├── search.js │ │ ├── search.pug │ │ └── search.scss │ ├── signin │ │ ├── signin.js │ │ ├── signin.pug │ │ └── signin.scss │ ├── signup │ │ ├── signup.js │ │ ├── signup.pug │ │ └── signup.scss │ ├── slider │ │ ├── slider.js │ │ ├── slider.pug │ │ └── slider.scss │ ├── stages │ │ ├── stages.js │ │ ├── stages.pug │ │ └── stages.scss │ ├── textarea │ │ ├── textarea.js │ │ ├── textarea.pug │ │ └── textarea.scss │ ├── tick-box │ │ ├── img │ │ │ └── check.svg │ │ ├── tick-box.js │ │ ├── tick-box.pug │ │ └── tick-box.scss │ ├── toggle │ │ ├── toggle.js │ │ ├── toggle.pug │ │ └── toggle.scss │ └── user-profile │ │ ├── img │ │ ├── default-avatar.png │ │ ├── face1.jpg │ │ ├── face2.jpg │ │ ├── facebook-logo.svg │ │ ├── instagram-logo.svg │ │ └── twitter-logo.svg │ │ ├── user-profile.js │ │ ├── user-profile.pug │ │ └── user-profile.scss ├── entry.js ├── entryMakeupTests.js ├── pages │ ├── activity │ │ ├── activity.js │ │ ├── activity.pug │ │ ├── activity.scss │ │ └── data.json │ ├── base │ │ ├── base-data.pug │ │ ├── base-guest.pug │ │ └── base.pug │ ├── demo │ │ ├── data.json │ │ ├── demo.js │ │ ├── demo.pug │ │ └── demo.scss │ ├── home │ │ ├── home-data.pug │ │ └── home.pug │ ├── index │ │ └── index.pug │ ├── layout-test │ │ └── layout-test.pug │ ├── messages │ │ ├── data.json │ │ ├── messages.js │ │ ├── messages.pug │ │ └── messages.scss │ ├── profile │ │ ├── profile.js │ │ ├── profile.pug │ │ └── profile.scss │ ├── signin │ │ ├── signin.js │ │ ├── signin.pug │ │ └── signin.scss │ └── signup │ │ └── signup.pug └── theme │ ├── favicon.png │ ├── global.scss │ └── variables.scss └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["es2015", { "modules": false }] 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | node_modules/ 4 | dist/ 5 | distTests/ 6 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "stylelint-order" 4 | ], 5 | "rules": { 6 | "at-rule-empty-line-before": [ "always", { 7 | except: [ 8 | "blockless-after-same-name-blockless", 9 | "first-nested", 10 | ], 11 | ignore: ["after-comment"], 12 | } ], 13 | "at-rule-name-case": "lower", 14 | "at-rule-name-space-after": "always-single-line", 15 | "at-rule-semicolon-newline-after": "always", 16 | "block-closing-brace-empty-line-before": "never", 17 | "block-closing-brace-newline-after": "always", 18 | "block-closing-brace-newline-before": "always-multi-line", 19 | "block-closing-brace-space-before": "always-single-line", 20 | "block-no-empty": true, 21 | "block-opening-brace-newline-after": "always-multi-line", 22 | "block-opening-brace-space-after": "always-single-line", 23 | "block-opening-brace-space-before": "always", 24 | "color-hex-case": "lower", 25 | "color-hex-length": "short", 26 | "color-no-invalid-hex": true, 27 | "comment-empty-line-before": [ "always", { 28 | except: ["first-nested"], 29 | ignore: ["stylelint-commands"], 30 | } ], 31 | "comment-no-empty": true, 32 | "comment-whitespace-inside": "always", 33 | "custom-property-empty-line-before": [ "always", { 34 | except: [ 35 | "after-custom-property", 36 | "first-nested", 37 | ], 38 | ignore: [ 39 | "after-comment", 40 | "inside-single-line-block", 41 | ], 42 | } ], 43 | "declaration-bang-space-after": "never", 44 | "declaration-bang-space-before": "always", 45 | "declaration-block-no-duplicate-properties": [ true, { 46 | ignore: ["consecutive-duplicates-with-different-values"], 47 | } ], 48 | "declaration-block-no-redundant-longhand-properties": true, 49 | "declaration-block-no-shorthand-property-overrides": true, 50 | "order/properties-order": [[ 51 | { 52 | properties: [ 53 | "position", 54 | "z-index", 55 | "top", 56 | "right", 57 | "bottom", 58 | "left" 59 | ], 60 | }, { 61 | properties: [ 62 | "display", 63 | "visibility", 64 | "float", 65 | "clear", 66 | "overflow", 67 | "overflow-x", 68 | "overflow-y", 69 | "clip", 70 | "zoom", 71 | "flex-direction", 72 | "flex-order", 73 | "flex-pack", 74 | "flex-align", 75 | "flex" 76 | ], 77 | }, { 78 | properties: [ 79 | "box-sizing", 80 | "width", 81 | "min-width", 82 | "max-width", 83 | "height", 84 | "min-height", 85 | "max-height", 86 | "margin", 87 | "margin-top", 88 | "margin-right", 89 | "margin-bottom", 90 | "margin-left", 91 | "padding", 92 | "padding-top", 93 | "padding-right", 94 | "padding-bottom", 95 | "padding-left" 96 | ], 97 | }, { 98 | properties: [ 99 | "table-layout", 100 | "empty-cells", 101 | "caption-side", 102 | "border-spacing", 103 | "border-collapse", 104 | "list-style", 105 | "list-style-position", 106 | "list-style-type", 107 | "list-style-image" 108 | ], 109 | }, { 110 | properties: [ 111 | "content", 112 | "quotes", 113 | "counter-reset", 114 | "counter-increment", 115 | "resize", 116 | "cursor", 117 | "user-select", 118 | "nav-index", 119 | "nav-up", 120 | "nav-right", 121 | "nav-down", 122 | "nav-left", 123 | "transition", 124 | "transition-delay", 125 | "transition-timing-function", 126 | "transition-duration", 127 | "transition-property", 128 | "transform", 129 | "transform-origin", 130 | "animation", 131 | "animation-name", 132 | "animation-duration", 133 | "animation-play-state", 134 | "animation-timing-function", 135 | "animation-delay", 136 | "animation-iteration-count", 137 | "animation-direction", 138 | "text-align", 139 | "text-align-last", 140 | "vertical-align", 141 | "white-space", 142 | "text-decoration", 143 | "text-emphasis", 144 | "text-emphasis-color", 145 | "text-emphasis-style", 146 | "text-emphasis-position", 147 | "text-indent", 148 | "text-justify", 149 | "letter-spacing", 150 | "word-spacing", 151 | "writing-mode", 152 | "text-outline", 153 | "text-transform", 154 | "text-wrap", 155 | "text-overflow", 156 | "text-overflow-ellipsis", 157 | "text-overflow-mode", 158 | "word-wrap", 159 | "word-break", 160 | "tab-size", 161 | "hyphens", 162 | "pointer-events" 163 | ], 164 | }, { 165 | properties: [ 166 | "opacity", 167 | "filter:progid:DXImageTransform.Microsoft.Alpha(Opacity", 168 | "-ms-filter:\\'progid:DXImageTransform.Microsoft.Alpha", 169 | "-ms-interpolation-mode", 170 | "color", 171 | "border", 172 | "border-width", 173 | "border-style", 174 | "border-color", 175 | "border-top", 176 | "border-top-width", 177 | "border-top-style", 178 | "border-top-color", 179 | "border-right", 180 | "border-right-width", 181 | "border-right-style", 182 | "border-right-color", 183 | "border-bottom", 184 | "border-bottom-width", 185 | "border-bottom-style", 186 | "border-bottom-color", 187 | "border-left", 188 | "border-left-width", 189 | "border-left-style", 190 | "border-left-color", 191 | "border-radius", 192 | "border-top-left-radius", 193 | "border-top-right-radius", 194 | "border-bottom-right-radius", 195 | "border-bottom-left-radius", 196 | "border-image", 197 | "border-image-source", 198 | "border-image-slice", 199 | "border-image-width", 200 | "border-image-outset", 201 | "border-image-repeat", 202 | "outline", 203 | "outline-width", 204 | "outline-style", 205 | "outline-color", 206 | "outline-offset", 207 | "background", 208 | "filter:progid:DXImageTransform.Microsoft.AlphaImageLoader", 209 | "background-color", 210 | "background-image", 211 | "background-repeat", 212 | "background-attachment", 213 | "background-position", 214 | "background-position-x", 215 | "background-position-y", 216 | "background-clip", 217 | "background-origin", 218 | "background-size", 219 | "box-decoration-break", 220 | "box-shadow", 221 | "filter:progid:DXImageTransform.Microsoft.gradient", 222 | "-ms-filter:\\'progid:DXImageTransform.Microsoft.gradient", 223 | "text-shadow" 224 | ] 225 | }, { 226 | properties: [ 227 | "font", 228 | "font-family", 229 | "font-size", 230 | "font-weight", 231 | "font-style", 232 | "font-variant", 233 | "font-size-adjust", 234 | "font-stretch", 235 | "font-effect", 236 | "font-emphasize", 237 | "font-emphasize-position", 238 | "font-emphasize-style", 239 | "font-smooth", 240 | "line-height" 241 | ] 242 | }], { unspecified: "bottom" } 243 | ], 244 | "declaration-block-semicolon-newline-after": "always-multi-line", 245 | "declaration-block-semicolon-space-after": "always-single-line", 246 | "declaration-block-semicolon-space-before": "never", 247 | "declaration-block-single-line-max-declarations": 1, 248 | "declaration-block-trailing-semicolon": "always", 249 | "declaration-colon-newline-after": "always-multi-line", 250 | "declaration-colon-space-after": "always-single-line", 251 | "declaration-colon-space-before": "never", 252 | "declaration-empty-line-before": [ "always", { 253 | except: [ 254 | "after-declaration", 255 | "first-nested", 256 | ], 257 | ignore: [ 258 | "after-comment", 259 | "inside-single-line-block", 260 | ], 261 | } ], 262 | "font-family-no-duplicate-names": true, 263 | "function-calc-no-unspaced-operator": true, 264 | "function-comma-newline-after": "always-multi-line", 265 | "function-comma-space-after": "always-single-line", 266 | "function-comma-space-before": "never", 267 | "function-linear-gradient-no-nonstandard-direction": true, 268 | "function-max-empty-lines": 0, 269 | "function-name-case": "lower", 270 | "function-parentheses-newline-inside": "always-multi-line", 271 | "function-parentheses-space-inside": "never-single-line", 272 | "function-whitespace-after": "always", 273 | "indentation": 2, 274 | "keyframe-declaration-no-important": true, 275 | "length-zero-no-unit": true, 276 | "max-empty-lines": 1, 277 | "media-feature-colon-space-after": "always", 278 | "media-feature-colon-space-before": "never", 279 | "media-feature-name-case": "lower", 280 | "media-feature-name-no-unknown": true, 281 | "media-feature-parentheses-space-inside": "never", 282 | "media-feature-range-operator-space-after": "always", 283 | "media-feature-range-operator-space-before": "always", 284 | "media-query-list-comma-newline-after": "always-multi-line", 285 | "media-query-list-comma-space-after": "always-single-line", 286 | "media-query-list-comma-space-before": "never", 287 | "no-empty-source": true, 288 | "no-eol-whitespace": true, 289 | "no-extra-semicolons": true, 290 | "no-invalid-double-slash-comments": true, 291 | "no-missing-end-of-source-newline": true, 292 | "number-leading-zero": "always", 293 | "number-no-trailing-zeros": true, 294 | "property-case": "lower", 295 | "property-no-unknown": true, 296 | "rule-empty-line-before": [ "always-multi-line", { 297 | except: ["first-nested"], 298 | ignore: ["after-comment"], 299 | } ], 300 | "selector-attribute-brackets-space-inside": "never", 301 | "selector-attribute-operator-space-after": "never", 302 | "selector-attribute-operator-space-before": "never", 303 | "selector-combinator-space-after": "always", 304 | "selector-combinator-space-before": "always", 305 | "selector-descendant-combinator-no-non-space": true, 306 | "selector-list-comma-newline-after": "always", 307 | "selector-list-comma-space-before": "never", 308 | "selector-max-empty-lines": 0, 309 | "selector-pseudo-class-case": "lower", 310 | "selector-pseudo-class-no-unknown": true, 311 | "selector-pseudo-class-parentheses-space-inside": "never", 312 | "selector-pseudo-element-case": "lower", 313 | "selector-pseudo-element-colon-notation": "double", 314 | "selector-pseudo-element-no-unknown": true, 315 | "selector-type-case": "lower", 316 | "selector-type-no-unknown": [ true, { 317 | ignoreTypes: ['ymaps'], 318 | } ], 319 | "shorthand-property-no-redundant-values": true, 320 | "string-no-newline": true, 321 | "unit-case": "lower", 322 | "unit-no-unknown": true, 323 | "value-list-comma-newline-after": "always-multi-line", 324 | "value-list-comma-space-after": "always-single-line", 325 | "value-list-comma-space-before": "never", 326 | "value-list-max-empty-lines": 0, 327 | } 328 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Fullstack Development 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flat-starter-kit 2 | Starter Kit for PSD-to-HTML projects with Webpack, Jade and Stylus 3 | 4 | ## Features 5 | We use Pug and Stylus to create static layout page for our clients who desires qualified "PSD to HTML" work. 6 | ### File structure 7 | ``` 8 | conf 9 | ``` 10 | Contains all webpack configs, they are imported in the webpack.config.js from the root directory 11 | 12 | #### `src` 13 | ``` 14 | src 15 | | entry.js 16 | └─── pages 17 | └─── components 18 | └─── theme 19 | ``` 20 | `entry.js` is a main file that import every `.js` file in the `src` directory. So when you create new `.js` file it will be automatically added to the bundle. 21 | 22 | `pages` is a folder with all possible pages of the project. 23 | 24 | `components` is a folder with all possible components shared between all pages. 25 | 26 | #### `pages` 27 | `pages` has the following structure: 28 | 29 | ``` 30 | pages 31 | └─── activity 32 | | | activity.js 33 | | | activity.pug 34 | | | activity-data.pug 35 | | | activity.scss 36 | └─── base 37 | | | base.pug 38 | | | base-data.pug 39 | | | base-guest.pug 40 | └─── home 41 | | | home.pug 42 | | | home-data.pug 43 | └─── signup 44 | | | signup.js 45 | | | signup.pug 46 | | | signup.scss 47 | 48 | ``` 49 | * `activity.pug`, `home.pug`, `signup.pug` are independent separated pages, extended from `pages/base/base.pug`, so they have ``, ``, `` tags. 50 | * Page ideally should NOT contain any element except of imports of different components. 51 | * Usually page get some data from external sources (your backend written in PHP, Python, Ruby, whatever), we imitate this with special variable `locals` in the separate file (`activity-data.pug` or `home-data.pug` for example) and contain all fake data there (including all lists, parameters, links to social media etc). 52 | 53 | #### `components` 54 | `components` has the following structure: 55 | ``` 56 | components 57 | └─── arrow-button 58 | | | arrow-button.pug 59 | | | arrow-button.js 60 | | | arrow-button.scss 61 | | └─── img 62 | | | | top.png 63 | | | | left.png 64 | | └─── __tests__ 65 | | | | index.pug 66 | | | | makeup-data.js 67 | └─── calendar 68 | | | calendar.pug 69 | | | calendar.js 70 | | | calendar.scss 71 | | └─── img 72 | | | | left.svg 73 | | | | right.svg 74 | | └─── __tests__ 75 | | | | index.pug 76 | | | | makeup-data.js 77 | ``` 78 | * `components` contains only directories per component. 79 | * Each component contains main `.pug` file with the template, `.js` that is dynamically loaded in the `entry.js` and contains all scripts for the component (and only the component) and `.scss` file. 80 | * `.scss` should be imported in the `.js` and contains **one BEM block** in the root of the files and all possible elements and modificators inside this block's definition. 81 | * `__tests__` is a special directory that contain all info regarding auto-testing, `makeup-data.js` is intended for Makeup visual declaration (see below), `index.pug` just to create samples of the component. 82 | 83 | ## How to work 84 | #### Install dependencies 85 | ```commandline 86 | npm install 87 | ``` 88 | 89 | #### Start dev server 90 | ```commandline 91 | npm run dev 92 | ``` 93 | 94 | Visit http://localhost:8080/ to see all possible pages of the project, click any page and start working. 95 | 96 | #### On the production server create the bundle files 97 | ```commandline 98 | npm run build 99 | ``` 100 | 101 | ## How to test 102 | Now we use Makeup from 2GIS. From the docs (https://github.com/2gis/makeup): 103 | ``` 104 | Makeup is a tool for development and comfortable quality assurance of markup on web projects. 105 | You'll certainly find it useful if your design is based on independent blocks 106 | and you prioritize stability and reliability. 107 | ``` 108 | 109 | ``` 110 | Makeup lets you: 111 | 112 | - Compare page design with the sample layout, 113 | - Monitor blocks for modifications and mismatching content, 114 | - Develop isolated blocks with ease. 115 | ``` 116 | 117 | To check design of each component separately in all possible states, run 118 | ```commandline 119 | npm run test 120 | ``` 121 | and go to `http://localhost:8090/` 122 | -------------------------------------------------------------------------------- /conf/webpack.base.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const webpack = require('webpack'); 4 | const configurator = require('webpack-config'); 5 | const CleanPlugin = require('clean-webpack-plugin'); 6 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 7 | const HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin'); 8 | const FaviconsWebpackPlugin = require('favicons-webpack-plugin'); 9 | 10 | const pages = []; 11 | 12 | fs 13 | .readdirSync(path.resolve(__dirname, '..', 'src', 'pages')) 14 | .filter((file) => { 15 | return file.indexOf('base') !== 0; 16 | }) 17 | .forEach((file) => { 18 | pages.push(file.split('/', 2)); 19 | }); 20 | 21 | const htmlPlugins = pages.map(fileName => new HtmlWebpackPlugin({ 22 | getData: () => { 23 | try { 24 | return JSON.parse(fs.readFileSync(`./src/pages/${fileName}/data.json`, 'utf8')); 25 | } catch (e) { 26 | console.warn(`data.json was not provided for page ${fileName}`); 27 | return {}; 28 | } 29 | }, 30 | filename: `${fileName}.html`, 31 | template: `./src/pages/${fileName}/${fileName}.pug`, 32 | alwaysWriteToDisk: true, 33 | inject: 'body', 34 | hash: true, 35 | })); 36 | 37 | module.exports = new configurator.default().merge({ 38 | entry: './src/entry.js', 39 | output: { 40 | filename: '[name].js', 41 | path: path.resolve(__dirname, '..', 'dist'), 42 | publicPath: '/static/' 43 | }, 44 | resolve: { 45 | modules: [ 46 | 'src', 47 | 'node_modules' 48 | ] 49 | }, 50 | plugins: [ 51 | new CleanPlugin(['./dist'], { root: path.resolve(__dirname, '..') }), 52 | new webpack.DefinePlugin({ 53 | NODE_ENV: JSON.stringify(process.env.NODE_ENV) 54 | }), 55 | new FaviconsWebpackPlugin('./src/theme/favicon.png'), 56 | new webpack.ProgressPlugin(), 57 | new webpack.ProvidePlugin({ 58 | $: 'jquery', 59 | jQuery: 'jquery', 60 | }), 61 | new HtmlWebpackHarddiskPlugin(), 62 | ].concat(htmlPlugins), 63 | }); -------------------------------------------------------------------------------- /conf/webpack.development.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const configurator = require('webpack-config'); 4 | 5 | const postcssReporter = require('postcss-reporter'); 6 | const postcssSCSS = require('postcss-scss'); 7 | const autoprefixer = require('autoprefixer'); 8 | const stylelint = require('stylelint'); 9 | const doiuse = require('doiuse'); 10 | 11 | module.exports = new configurator.default().extend('conf/webpack.base.config.js').merge({ 12 | devtool: 'eval', 13 | output: { 14 | pathinfo: true 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.js$/, 20 | loader: 'babel-loader', 21 | exclude: [ 22 | /node_modules/, 23 | ], 24 | query: { 25 | presets: ['es2015'] 26 | } 27 | }, 28 | { 29 | test: /\.css/, 30 | use: [ 31 | { 32 | loader: 'style-loader', 33 | }, 34 | { 35 | loader: 'css-loader', 36 | options: { 37 | importLoaders: 2, 38 | sourceMap: false, 39 | }, 40 | }, 41 | ], 42 | }, 43 | { 44 | test: /\.scss$/, 45 | use: [ 46 | 'style-loader', 47 | 'css-loader?importLoaders=1', 48 | { 49 | loader: 'postcss-loader', 50 | options: { 51 | plugins: function () { 52 | return [ 53 | autoprefixer({browsers: ['last 2 versions']}), 54 | ]; 55 | }, 56 | }, 57 | }, 58 | 'sass-loader', 59 | { 60 | loader: 'postcss-loader', 61 | options: { 62 | syntax: postcssSCSS, 63 | plugins: function () { 64 | return [ 65 | stylelint(), 66 | doiuse({ 67 | browsers:['ie >= 11', 'last 2 versions'], 68 | ignore: ['flexbox', 'rem', 'css-resize', 'css-masks', 'object-fit'], 69 | ignoreFiles: ['**/normalize.css'], 70 | }), 71 | postcssReporter({ 72 | clearReportedMessages: true, 73 | throwError: true, 74 | }), 75 | ]; 76 | }, 77 | }, 78 | }, 79 | ], 80 | }, 81 | { 82 | test: /\.pug$/, 83 | loader: 'pug-loader', 84 | }, 85 | { 86 | test: /\.(png|jpg|svg|ttf|eot|woff|woff2)$/, 87 | loader: 'file-loader?name=[path][name].[ext]' 88 | } 89 | ] 90 | }, 91 | plugins: [ 92 | new webpack.HotModuleReplacementPlugin(), 93 | ], 94 | devServer: { 95 | inline: true, 96 | hot: true, 97 | contentBase: 'dist', 98 | host: '0.0.0.0', 99 | }, 100 | }); -------------------------------------------------------------------------------- /conf/webpack.makeup.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const configurator = require('webpack-config'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const CleanPlugin = require('clean-webpack-plugin'); 5 | 6 | module.exports = new configurator.default() 7 | .extend({ 8 | 'conf/webpack.development.config.js': config => { 9 | config.plugins = config.plugins.filter((item) => { 10 | return !((item instanceof HtmlWebpackPlugin) || (item instanceof CleanPlugin)) 11 | }); 12 | config.plugins.unshift( 13 | new CleanPlugin(['./distTests'], { root: path.resolve(__dirname, '..') }) 14 | ); 15 | config.plugins.push( 16 | new HtmlWebpackPlugin({ alwaysWriteToDisk: true }) 17 | ); 18 | 19 | return config; 20 | } 21 | }) 22 | .merge({ 23 | entry: './src/entryMakeupTests.js', 24 | output: { 25 | path: path.resolve(__dirname, '..', 'distTests'), 26 | }, 27 | devServer: { 28 | port: 8090, 29 | contentBase: 'distTests', 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /conf/webpack.production.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const configurator = require('webpack-config'); 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 5 | 6 | const postcssReporter = require('postcss-reporter'); 7 | const postcssSCSS = require('postcss-scss'); 8 | const autoprefixer = require('autoprefixer'); 9 | const stylelint = require('stylelint'); 10 | const doiuse = require('doiuse'); 11 | 12 | module.exports = new configurator.default().extend('conf/webpack.base.config.js').merge({ 13 | output: { 14 | filename: '[name]-[hash].js', 15 | path: path.resolve(__dirname, '..', 'dist'), 16 | publicPath: '/', 17 | }, 18 | filename: __filename, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.js$/, 23 | loader: 'babel-loader', 24 | exclude: [ 25 | /node_modules/, 26 | ], 27 | query: { 28 | presets: ['es2015'] 29 | } 30 | }, 31 | { 32 | test: /\.css/, 33 | use: ExtractTextPlugin.extract({ 34 | fallback: 'style-loader', 35 | use: [ 36 | { 37 | loader: 'css-loader', 38 | options: { 39 | importLoaders: 2, 40 | sourceMap: false, 41 | }, 42 | }, 43 | ], 44 | }) 45 | }, 46 | { 47 | test: /\.scss$/, 48 | use: ExtractTextPlugin.extract({ 49 | fallback: 'style-loader', 50 | use: [ 51 | { 52 | loader: 'css-loader', 53 | options: { 54 | importLoaders: 2, 55 | sourceMap: false, 56 | }, 57 | }, 58 | { 59 | loader: 'postcss-loader', 60 | options: { 61 | plugins: function () { 62 | return [ 63 | autoprefixer({browsers: ['last 2 versions']}), 64 | ]; 65 | }, 66 | }, 67 | }, 68 | 'sass-loader', 69 | { 70 | loader: 'postcss-loader', 71 | options: { 72 | syntax: postcssSCSS, 73 | plugins: function () { 74 | return [ 75 | stylelint(), 76 | doiuse({ 77 | browsers:['ie >= 11', 'last 2 versions'], 78 | ignore: ['flexbox', 'rem', 'css-resize', 'css-masks', 'object-fit'], 79 | ignoreFiles: ['**/normalize.css'], 80 | }), 81 | postcssReporter({ 82 | clearReportedMessages: true, 83 | throwError: true, 84 | }), 85 | ]; 86 | }, 87 | }, 88 | }, 89 | ], 90 | }), 91 | }, 92 | { 93 | test: /\.pug$/, 94 | loader: 'pug-loader', 95 | }, 96 | { 97 | test: /\.(png|jpg|svg|ttf|eot|woff|woff2)$/, 98 | loader: 'file-loader?name=[path][name].[ext]' 99 | } 100 | ] 101 | }, 102 | plugins: [ 103 | new ExtractTextPlugin({ 104 | filename: "[name]-[contenthash].css", 105 | allChunks: true 106 | }), 107 | new webpack.optimize.UglifyJsPlugin({ 108 | mangle: true, 109 | output: { 110 | comments: false 111 | }, 112 | compress: { 113 | warnings: false 114 | } 115 | }) 116 | ] 117 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fsd-flat-starter-kit", 3 | "version": "1.0.0", 4 | "description": "Starter Kit for simple HTML+CSS pages (flat-pages) built by FSD team", 5 | "engines": { 6 | "node": ">=7.0.0" 7 | }, 8 | "scripts": { 9 | "dev": "better-npm-run start-dev", 10 | "prod": "better-npm-run build-prod", 11 | "test": "better-npm-run start-test" 12 | }, 13 | "betterScripts": { 14 | "build-prod": { 15 | "command": "./node_modules/.bin/webpack", 16 | "env": { 17 | "NODE_PATH": "./src", 18 | "NODE_ENV": "production" 19 | } 20 | }, 21 | "start-dev": { 22 | "command": "./node_modules/.bin/webpack-dev-server", 23 | "env": { 24 | "NODE_PATH": "./src", 25 | "NODE_ENV": "development", 26 | "PORT": 8080 27 | } 28 | }, 29 | "start-test": { 30 | "command": "./node_modules/.bin/webpack-dev-server", 31 | "env": { 32 | "NODE_PATH": "./src", 33 | "NODE_ENV": "makeup", 34 | "PORT": 8090 35 | } 36 | } 37 | }, 38 | "repository": { 39 | "type": "git", 40 | "url": "git+https://github.com/fullstack-development/flat-starter-kit.git" 41 | }, 42 | "keywords": [ 43 | "Starter Kit", 44 | "Flat Pages", 45 | "Pug", 46 | "Stylus", 47 | "Webpack", 48 | "BEM" 49 | ], 50 | "author": "Fullstack Development Llc", 51 | "license": "MIT", 52 | "bugs": { 53 | "url": "https://github.com/fullstack-development/flat-starter-kit/issues" 54 | }, 55 | "homepage": "https://github.com/fullstack-development/flat-starter-kit#readme", 56 | "dependencies": { 57 | "autoprefixer": "^6.7.7", 58 | "babel-core": "^6.24.0", 59 | "babel-loader": "^6.4.1", 60 | "babel-preset-es2015": "^6.24.0", 61 | "better-npm-run": "0.0.13", 62 | "clean-webpack-plugin": "^0.1.16", 63 | "css-loader": "^0.25.0", 64 | "doiuse": "^2.6.0", 65 | "express": "^4.15.2", 66 | "extract-text-webpack-plugin": "^2.1.0", 67 | "favicons-webpack-plugin": "0.0.7", 68 | "file-loader": "^0.9.0", 69 | "html-webpack-harddisk-plugin": "0.0.2", 70 | "html-webpack-plugin": "^2.28.0", 71 | "jquery": "^3.2.1", 72 | "jquery-ui": "^1.12.1", 73 | "node-sass": "^4.5.2", 74 | "normalize.css": "^5.0.0", 75 | "peity": "^3.2.1", 76 | "postcss-loader": "^1.3.3", 77 | "postcss-reporter": "^3.0.0", 78 | "postcss-scss": "^0.4.1", 79 | "pug": "^2.0.0-beta11", 80 | "pug-html-loader": "^1.1.4", 81 | "pug-loader": "^2.3.0", 82 | "sass-loader": "^6.0.3", 83 | "style-loader": "^0.13.2", 84 | "stylelint": "^7.9.0", 85 | "stylelint-order": "^0.4.2", 86 | "webpack": "^2.3.2", 87 | "webpack-config": "^6.2.1", 88 | "webpack-dev-middleware": "^1.10.1", 89 | "webpack-dev-server": "^2.4.2", 90 | "webpack-hot-middleware": "^2.17.1" 91 | }, 92 | "devDependencies": { 93 | "makeup": "^1.0.1", 94 | "script-loader": "^0.7.0" 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/components/activity-card/activity-card.js: -------------------------------------------------------------------------------- 1 | import './activity-card.scss'; -------------------------------------------------------------------------------- /src/components/activity-card/activity-card.pug: -------------------------------------------------------------------------------- 1 | //- 2 | options.image - обложка 3 | 4 | mixin activity-card(options) 5 | if !options 6 | - options = {} 7 | 8 | .activity-card 9 | .activity-card__image-frame 10 | if options.image 11 | img.activity-card__image( 12 | src = require('../../components/activity-card/img/' + options.image), 13 | alt = 'activity image' 14 | ) 15 | .activity-card__preview 16 | block -------------------------------------------------------------------------------- /src/components/activity-card/activity-card.scss: -------------------------------------------------------------------------------- 1 | .activity-card { 2 | display: flex; 3 | height: 17.65rem; 4 | border-radius: 0.3rem; 5 | align-items: stretch; 6 | 7 | &__image-frame { 8 | display: flex; 9 | overflow: hidden; 10 | flex: 3 1; 11 | border-top-left-radius: 0.3rem; 12 | border-bottom-left-radius: 0.3rem; 13 | background: url(./img/default.jpg) no-repeat; 14 | background-size: cover; 15 | align-items: stretch; 16 | } 17 | 18 | &__image { 19 | max-width: 100%; 20 | object-fit: cover; 21 | } 22 | 23 | &__preview { 24 | display: flex; 25 | overflow: hidden; 26 | flex: 1 0 12.5rem; 27 | border-top-right-radius: 0.3rem; 28 | border-bottom-right-radius: 0.3rem; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/activity-card/img/default.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstack-development/flat-starter-kit/5d4e6be5b18685405b20846c709fd661611b3c06/src/components/activity-card/img/default.jpg -------------------------------------------------------------------------------- /src/components/activity-card/img/img1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstack-development/flat-starter-kit/5d4e6be5b18685405b20846c709fd661611b3c06/src/components/activity-card/img/img1.jpg -------------------------------------------------------------------------------- /src/components/arrow-button/__tests__/index.pug: -------------------------------------------------------------------------------- 1 | include ../arrow-button 2 | 3 | +arrow-button(options) -------------------------------------------------------------------------------- /src/components/arrow-button/__tests__/makeup-data.js: -------------------------------------------------------------------------------- 1 | const pugTemplate = require('./index.pug'); 2 | 3 | const data = { 4 | name: 'arrow-button', 5 | type: 'module', 6 | items: [{ 7 | name: 'default.arrow-button.component', 8 | type: 'module' 9 | }, { 10 | name: 'condition-inactive.arrow-button.component', 11 | type: 'module' 12 | }, { 13 | name: 'condition-disabled.arrow-button.component', 14 | type: 'module' 15 | }] 16 | }; 17 | 18 | function template({module: blockName}) { 19 | switch (blockName) { 20 | case 'default.arrow-button.component': { 21 | return pugTemplate({}); 22 | } 23 | case 'condition-inactive.arrow-button.component': { 24 | return pugTemplate({options: { 25 | condition: 'inactive' 26 | }}); 27 | } 28 | case 'condition-disabled.arrow-button.component': { 29 | return pugTemplate({options: { 30 | condition: 'disabled' 31 | }}); 32 | } 33 | } 34 | } 35 | 36 | export { 37 | data, 38 | template, 39 | } -------------------------------------------------------------------------------- /src/components/arrow-button/arrow-button.js: -------------------------------------------------------------------------------- 1 | import './arrow-button.scss'; -------------------------------------------------------------------------------- /src/components/arrow-button/arrow-button.pug: -------------------------------------------------------------------------------- 1 | //- 2 | options.direction - направление стрелки ('left', 'right', 'up', 'down') 3 | options.condition - состояние кнопки ('', 'inactive', 'disabled') 4 | 5 | mixin arrow-button(options) 6 | if !options 7 | - options = {} 8 | if !options.direction 9 | - options.direction = 'left' 10 | - 11 | var classes = '' 12 | classes += options.condition ? ' arrow-button_' + options.condition : '' 13 | classes += options.direction ? ' arrow-button_' + options.direction : ' arrow-button_left' 14 | 15 | button.arrow-button(class = classes) 16 | case options.direction 17 | when 'left' 18 | include ./img/left.svg 19 | when 'right' 20 | include ./img/right.svg 21 | when 'up' 22 | include ./img/up.svg 23 | when 'down' 24 | include ./img/down.svg -------------------------------------------------------------------------------- /src/components/arrow-button/arrow-button.scss: -------------------------------------------------------------------------------- 1 | @import '~theme/variables.scss'; 2 | 3 | .arrow-button { 4 | display: flex; 5 | width: 4.2rem; 6 | height: 4.2rem; 7 | cursor: pointer; 8 | text-align: -moz-center; 9 | border: 3px solid $color-first; 10 | border-radius: 50%; 11 | outline: none; 12 | background-color: $color-first; 13 | justify-content: center; 14 | 15 | & svg { 16 | width: 65%; 17 | height: 65%; 18 | } 19 | 20 | & svg path { 21 | fill: white; 22 | } 23 | 24 | &_inactive { 25 | background-color: white; 26 | 27 | & svg path { 28 | fill: $color-first; 29 | } 30 | } 31 | 32 | &_disabled { 33 | border-color: $color-disabled; 34 | background-color: $color-disabled; 35 | } 36 | 37 | &_left { 38 | & svg { 39 | margin-right: 0.3rem; 40 | } 41 | } 42 | 43 | &_right { 44 | & svg { 45 | margin-left: 0.3rem; 46 | } 47 | } 48 | 49 | &_up { 50 | & svg { 51 | margin-bottom: 0.3rem; 52 | } 53 | } 54 | 55 | &_down { 56 | & svg { 57 | margin-top: 0.3rem; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/components/arrow-button/img/down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 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 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/components/arrow-button/img/left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 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 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/components/arrow-button/img/right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 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 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/components/arrow-button/img/up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 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 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/components/button/__tests__/img/button-default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstack-development/flat-starter-kit/5d4e6be5b18685405b20846c709fd661611b3c06/src/components/button/__tests__/img/button-default.png -------------------------------------------------------------------------------- /src/components/button/__tests__/index.pug: -------------------------------------------------------------------------------- 1 | include ../button 2 | 3 | +button(options) -------------------------------------------------------------------------------- /src/components/button/__tests__/makeup-data.js: -------------------------------------------------------------------------------- 1 | import pugTemplate from './index.pug'; 2 | import render from '../button'; 3 | 4 | const data = { 5 | name: 'button', 6 | type: 'module', 7 | snippet: render, 8 | items: [{ 9 | name: 'default.button.component', 10 | type: 'module', 11 | image: require('./img/button-default.png') 12 | }, { 13 | name: 'size-small.button.component', 14 | type: 'module' 15 | }, { 16 | name: 'type-error.button.component', 17 | type: 'module' 18 | }] 19 | }; 20 | 21 | function template({module: blockName}) { 22 | switch (blockName) { 23 | case 'default.button.component': { 24 | return pugTemplate({}); 25 | } 26 | case 'size-small.button.component': { 27 | return pugTemplate({options: { 28 | size: 'small' 29 | }}); 30 | } 31 | case 'type-error.button.component': { 32 | return pugTemplate({options: { 33 | type: 'error' 34 | }}); 35 | } 36 | } 37 | } 38 | 39 | export { 40 | data, 41 | template, 42 | } -------------------------------------------------------------------------------- /src/components/button/button.js: -------------------------------------------------------------------------------- 1 | import './button.scss'; 2 | import $ from 'jquery'; 3 | 4 | class Button { 5 | constructor($component) { 6 | this.$component = $component; 7 | this._attachEventHandlers(); 8 | } 9 | 10 | _attachEventHandlers() { 11 | this.$component 12 | .on('click', (event) => this._showRippleEffect(event)) 13 | .on('mousedown', () => this.$component.addClass('button_pressed')) 14 | .on('mouseup', () => this.$component.removeClass('button_pressed')) 15 | .on('mouseout', () => this.$component.removeClass('button_pressed')); 16 | } 17 | 18 | _showRippleEffect(event) { 19 | const $div = $('
'); 20 | const btnOffset = this.$component.offset(); 21 | const xPos = event.pageX - btnOffset.left; 22 | const yPos = event.pageY - btnOffset.top; 23 | 24 | $div.addClass('button__ripple-effect'); 25 | $div.css({ 26 | top: yPos, 27 | left: xPos 28 | }); 29 | $div.appendTo(this.$component); 30 | 31 | window.setTimeout(function () { 32 | $div.remove(); 33 | }, 1000); 34 | } 35 | } 36 | 37 | export default function renderComponent(callbackWhenInitialized) { 38 | $(() => { 39 | const buttons = $('.js-button').map((index, node) => { 40 | return new Button($(node)); 41 | }); 42 | 43 | if (callbackWhenInitialized && typeof callbackWhenInitialized === 'function') { 44 | callbackWhenInitialized(buttons); 45 | } 46 | }); 47 | } 48 | 49 | renderComponent(); -------------------------------------------------------------------------------- /src/components/button/button.pug: -------------------------------------------------------------------------------- 1 | //- 2 | options.text - текст 3 | options.size - размер ('', 'small') 4 | options.type - тип ('', 'error') 5 | options.isInverted - инвертирование цвета (bool) 6 | options.isSubmit - задает тип кнопки Submit (bool) 7 | options.tabindex - задает tabindex 8 | 9 | mixin button(options) 10 | if !options 11 | - options = {} 12 | - 13 | var classes = '' 14 | classes += options.type ? ' button_' + options.type : '' 15 | classes += options.size ? ' button_' + options.size : '' 16 | classes += options.isInverted ? ' button_inverted' : '' 17 | 18 | .button.js-button(class=classes) 19 | button.button__element( 20 | type = options.isSubmit ? 'submit' : false, 21 | tabindex = options.tabindex || false 22 | )= options.text || 'Button' 23 | -------------------------------------------------------------------------------- /src/components/button/button.scss: -------------------------------------------------------------------------------- 1 | @import '~theme/variables.scss'; 2 | 3 | .button { 4 | position: relative; 5 | display: inline-block; 6 | padding-top: 0; 7 | padding-bottom: 0.25rem; 8 | -webkit-user-select: none; 9 | -moz-user-select: none; 10 | -ms-user-select: none; 11 | vertical-align: bottom; 12 | color: $color-first; 13 | 14 | &__element { 15 | width: 100%; 16 | min-width: 9.8rem; 17 | padding: 0.75rem 1.1rem 0.55rem; 18 | cursor: pointer; 19 | text-align: center; 20 | vertical-align: middle; 21 | text-transform: uppercase; 22 | color: white; 23 | border: 0.1rem solid $color-first; 24 | border-radius: 0.25rem; 25 | outline: none; 26 | background-color: $color-first; 27 | box-shadow: 0 0.25rem 0 0 $color-first-dark; 28 | font-family: LatoBlack, sans-serif; 29 | font-size: 1.04rem; 30 | font-weight: bold; 31 | 32 | &[type='submit']:focus { 33 | outline: 0.3rem auto #aaa; 34 | } 35 | } 36 | 37 | &__ripple-effect { 38 | position: absolute; 39 | z-index: 1; 40 | width: 1px; 41 | height: 1px; 42 | animation: ripple-animation 1s; 43 | border-radius: 50%; 44 | background: #fafafa; 45 | } 46 | 47 | &_small &__element { 48 | min-width: 6.5rem; 49 | padding: 0.45rem 0.9rem 0.2rem; 50 | font-size: 0.87rem; 51 | } 52 | 53 | &_error { 54 | color: $color-second; 55 | } 56 | 57 | &_error &__element { 58 | border-color: $color-second; 59 | background-color: $color-second; 60 | box-shadow: 0 0.25rem 0 0 $color-second-dark; 61 | } 62 | 63 | &_pressed { 64 | padding-top: 0.25rem; 65 | padding-bottom: 0; 66 | } 67 | 68 | &_pressed &__element { 69 | box-shadow: none; 70 | } 71 | 72 | &.button_inverted &__element { 73 | color: inherit; 74 | background-color: white; 75 | } 76 | 77 | @keyframes ripple-animation { 78 | from { 79 | transform: scale(1); 80 | opacity: 1; 81 | } 82 | 83 | to { 84 | transform: scale(50); 85 | opacity: 0; 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/components/calendar/__tests__/index.pug: -------------------------------------------------------------------------------- 1 | include ../calendar 2 | 3 | +calendar() -------------------------------------------------------------------------------- /src/components/calendar/__tests__/makeup-data.js: -------------------------------------------------------------------------------- 1 | import pugTemplate from './index.pug'; 2 | import render from '../calendar'; 3 | 4 | const data = { 5 | name: 'calendar', 6 | snippet: render, 7 | items: [{ 8 | name: 'default.calendar.component', 9 | type: 'module', 10 | }] 11 | }; 12 | 13 | function template({module: blockName}) { 14 | switch (blockName) { 15 | case 'default.calendar.component': { 16 | return pugTemplate({}); 17 | } 18 | } 19 | } 20 | 21 | export { 22 | data, 23 | template, 24 | } -------------------------------------------------------------------------------- /src/components/calendar/calendar.js: -------------------------------------------------------------------------------- 1 | import './calendar.scss'; 2 | import $ from 'jquery'; 3 | 4 | if (!$.fn.datepicker) { 5 | require('jquery-ui/ui/widgets/datepicker'); 6 | require('jquery-ui/themes/base/datepicker.css'); 7 | } 8 | 9 | class Calendar { 10 | constructor($component) { 11 | this.$component = $component; 12 | this.render(); 13 | } 14 | 15 | render() { 16 | let $day = $('.js-calendar__day', this.$component); 17 | let $widget = $('.js-calendar__widget', this.$component); 18 | 19 | $widget.datepicker({ 20 | changeYear: false, 21 | altField: $day, 22 | altFormat: "dd", 23 | firstDay: 1 24 | }); 25 | 26 | $('.calendar__btn-today', this.$component).on('click', () => { 27 | $widget.datepicker('setDate', new Date()); 28 | }); 29 | } 30 | } 31 | 32 | export default function renderComponent() { 33 | $(() => { 34 | $('.js-calendar').each((index, node) => { 35 | new Calendar($(node)); 36 | }); 37 | }); 38 | } 39 | 40 | renderComponent(); 41 | -------------------------------------------------------------------------------- /src/components/calendar/calendar.pug: -------------------------------------------------------------------------------- 1 | mixin calendar() 2 | .calendar.js-calendar 3 | input.calendar__day.js-calendar__day(readonly = true) 4 | .calendar__widget.js-calendar__widget 5 | .calendar__btn-today today -------------------------------------------------------------------------------- /src/components/calendar/calendar.scss: -------------------------------------------------------------------------------- 1 | @import '~theme/variables.scss'; 2 | 3 | .calendar { 4 | overflow: hidden; 5 | border-radius: 0.3rem; 6 | 7 | &__day { 8 | display: block; 9 | width: 100%; 10 | padding-top: 1.2rem; 11 | text-align: center; 12 | color: white; 13 | border: none; 14 | background-color: $color-second; 15 | font-family: LatoLight, sans-serif; 16 | font-size: 6.5rem; 17 | 18 | &:focus { 19 | outline: none; 20 | } 21 | } 22 | 23 | &__btn-today { 24 | padding: 1.35rem 1.7rem; 25 | cursor: pointer; 26 | text-align: left; 27 | text-transform: uppercase; 28 | color: #868686; 29 | background-color: #e5e5e5; 30 | font-family: LatoBlack, sans-serif; 31 | font-size: 1.2rem; 32 | font-weight: bold; 33 | 34 | &:hover { 35 | background-color: #ddd; 36 | } 37 | } 38 | 39 | & &__widget .ui-datepicker { 40 | width: auto; 41 | padding: 0; 42 | border: none; 43 | 44 | & .ui-datepicker-header { 45 | padding: 0; 46 | padding-top: 0.25rem; 47 | color: white; 48 | border: none; 49 | border-radius: 0; 50 | background-color: #d54c2c; 51 | font-family: LatoLight, sans-serif; 52 | font-size: 1.9rem; 53 | font-weight: normal; 54 | } 55 | 56 | & .ui-datepicker-year { 57 | display: none; 58 | } 59 | 60 | & .ui-datepicker-prev { 61 | top: 1px; 62 | left: 1px; 63 | display: flex; 64 | padding-right: 0.2rem; 65 | cursor: pointer; 66 | justify-content: center; 67 | align-items: center; 68 | 69 | &.ui-datepicker-prev-hover { 70 | border: none; 71 | border-radius: 0; 72 | background-color: inherit; 73 | } 74 | 75 | & > * { 76 | display: none; 77 | } 78 | 79 | &::after { 80 | display: block; 81 | content: '1'; 82 | color: transparent; 83 | background: url(./img/left.svg) no-repeat center center; 84 | background-size: contain; 85 | } 86 | } 87 | 88 | & .ui-datepicker-next { 89 | top: 1px; 90 | right: 1px; 91 | display: flex; 92 | padding-left: 0.2rem; 93 | cursor: pointer; 94 | justify-content: center; 95 | align-items: center; 96 | 97 | &.ui-datepicker-next-hover { 98 | border: none; 99 | border-radius: 0; 100 | background-color: inherit; 101 | } 102 | 103 | & > * { 104 | display: none; 105 | } 106 | 107 | &::after { 108 | display: block; 109 | content: '1'; 110 | color: transparent; 111 | background: url(./img/right.svg) no-repeat center center; 112 | background-size: contain; 113 | } 114 | } 115 | 116 | & .ui-datepicker-calendar { 117 | margin: 0; 118 | 119 | & thead { 120 | text-transform: uppercase; 121 | color: white; 122 | background-color: $color-second; 123 | font-family: LatoBlack, sans-serif; 124 | font-size: 0.8rem; 125 | font-weight: bold; 126 | 127 | & th { 128 | padding: 0.7rem 0; 129 | } 130 | } 131 | 132 | & tbody { 133 | border-top: 0.1rem solid white; 134 | border-bottom: 0.1rem solid white; 135 | background-color: #f2f2f2; 136 | 137 | & td { 138 | padding: 0; 139 | border: 0.1rem solid white; 140 | } 141 | 142 | & td:first-child { 143 | border-left: none; 144 | } 145 | 146 | & td:last-child { 147 | border-right: none; 148 | } 149 | } 150 | } 151 | 152 | & .ui-state-default { 153 | padding: 0.5rem 0.1rem; 154 | text-align: center; 155 | color: #868686; 156 | border: none; 157 | font-family: SourceSansProRegular, sans-serif; 158 | font-size: 1.7rem; 159 | } 160 | 161 | & .ui-state-highlight, 162 | & .ui-state-active { 163 | color: white; 164 | border: none; 165 | background-color: $color-second; 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/components/calendar/img/left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 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 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/components/calendar/img/right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 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 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/components/drop-down/drop-down.js: -------------------------------------------------------------------------------- 1 | import './drop-down.scss'; 2 | 3 | if (!$.fn.selectmenu) { 4 | require('jquery-ui/ui/widgets/selectmenu'); 5 | require('jquery-ui/themes/base/selectmenu.css'); 6 | } 7 | 8 | class DropDown { 9 | constructor($component) { 10 | this.$component = $component; 11 | this.render(); 12 | } 13 | 14 | render() { 15 | $('.js-drop-down__select-box', this.$component).selectmenu(); 16 | } 17 | } 18 | 19 | $(() => { 20 | $('.drop-down').each((index, node) => { 21 | new DropDown($(node)); 22 | }); 23 | }); -------------------------------------------------------------------------------- /src/components/drop-down/drop-down.pug: -------------------------------------------------------------------------------- 1 | //- 2 | options.items - массив элементов выпадающего списка 3 | options.items[0].name - название элемента 4 | options.items[0].value - value элемента 5 | options.name - form element name 6 | options.tabindex - input tabindex 7 | options.required - input required 8 | 9 | mixin drop-down(options) 10 | if !options 11 | - options = {} 12 | 13 | label.drop-down.js-drop-down 14 | select.drop-down__select-box.js-drop-down__select-box( 15 | name = options.name 16 | tabindex = options.tabindex 17 | required = options.required 18 | ) 19 | if options.items 20 | each item in options.items 21 | option.drop-down__item( 22 | value = item.name 23 | ) #{item.value || item.name} 24 | -------------------------------------------------------------------------------- /src/components/drop-down/drop-down.scss: -------------------------------------------------------------------------------- 1 | $dropDownHeight: 2.3rem; 2 | 3 | .drop-down { 4 | display: block; 5 | height: $dropDownHeight; 6 | 7 | & .ui-button.ui-selectmenu-button { 8 | display: block; 9 | overflow: hidden; 10 | width: auto; 11 | border-radius: 0.3rem; 12 | background-color: #e5e5e5; 13 | 14 | &:focus { 15 | outline: 0.3rem auto #bbb; 16 | } 17 | 18 | &-open { 19 | border-bottom-right-radius: 0; 20 | border-bottom-left-radius: 0; 21 | 22 | & .ui-selectmenu-icon::after { 23 | background-image: url(./img/up.svg); 24 | } 25 | } 26 | } 27 | 28 | & .ui-selectmenu-icon { 29 | display: flex; 30 | width: 2.8rem; 31 | height: $dropDownHeight; 32 | background-color: #4eb7a8; 33 | justify-content: center; 34 | 35 | &::after { 36 | display: block; 37 | flex-basis: 50%; 38 | content: '1'; 39 | color: transparent; 40 | background: url(./img/down.svg) no-repeat center center; 41 | mask-size: contain; 42 | } 43 | } 44 | 45 | & .ui-selectmenu-text { 46 | height: $dropDownHeight; 47 | margin-right: 2.8rem; 48 | padding-left: 1.1rem; 49 | vertical-align: middle; 50 | color: #888; 51 | font-family: sans-serif; 52 | font-size: 1.1rem; 53 | line-height: $dropDownHeight; 54 | } 55 | } 56 | 57 | .ui-selectmenu-menu.ui-selectmenu-open { 58 | padding-top: 0.2rem; 59 | 60 | & .ui-menu { 61 | border: 2px solid #e5e5e5; 62 | border-bottom-right-radius: 0.3rem; 63 | border-bottom-left-radius: 0.3rem; 64 | background-color: white; 65 | } 66 | 67 | & .ui-menu-item-wrapper { 68 | padding: 0.3rem 1rem; 69 | color: #888; 70 | font-family: sans-serif; 71 | font-size: 1.1rem; 72 | 73 | &.ui-state-active { 74 | margin: -1px; 75 | border: 1px solid #d0d0d0; 76 | background-color: #e5e5e5; 77 | } 78 | } 79 | } 80 | 81 | -------------------------------------------------------------------------------- /src/components/drop-down/img/down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 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 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/components/drop-down/img/up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 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 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/components/event/event.js: -------------------------------------------------------------------------------- 1 | import './event.scss'; 2 | import $ from 'jquery'; 3 | 4 | class Event { 5 | constructor($component) { 6 | this.$component = $component; 7 | this.$description = $('.js-event__description', this.$component); 8 | this._setColumnWidth(); 9 | this._attachEventHandlers(); 10 | } 11 | 12 | _attachEventHandlers() { 13 | $(window).resize(() => this._setColumnWidth()); 14 | } 15 | 16 | _setColumnWidth() { 17 | const width = this.$description.width(); 18 | const height = this.$description.height(); 19 | this.$description 20 | .css({ 21 | '-webkit-column-width': `${width}px`, 22 | '-moz-column-width': `${width}px`, 23 | 'column-width': `${width}px`, 24 | 'height': `${height}px`, 25 | }); 26 | } 27 | } 28 | 29 | export default function renderComponent() { 30 | $(() => { 31 | $('.js-event').each((index, node) => { 32 | new Event($(node)); 33 | }); 34 | }); 35 | } 36 | 37 | renderComponent(); -------------------------------------------------------------------------------- /src/components/event/event.pug: -------------------------------------------------------------------------------- 1 | //- 2 | options.title - название 3 | options.dateDay - дата 4 | options.dateMonth - дата 5 | options.desc - описание 6 | 7 | include ../button/button 8 | 9 | mixin event(options) 10 | if !options 11 | - options = {} 12 | 13 | .event.js-event 14 | .event__date 15 | .event__day= options.dateDay 16 | .event__month= options.dateMonth.slice(0, 3) 17 | .event__title= options.title ? options.title : '' 18 | .event__description.js-event__description= options.desc ? options.desc : '' 19 | .event__button-add 20 | +button({text: "add to calendar", isInverted: true, size: "small"}) 21 | -------------------------------------------------------------------------------- /src/components/event/event.scss: -------------------------------------------------------------------------------- 1 | @import '~theme/variables.scss'; 2 | 3 | .event { 4 | display: flex; 5 | overflow: hidden; 6 | flex-direction: column; 7 | flex: 1 1; 8 | padding: 1.5rem; 9 | text-align: left; 10 | color: white; 11 | background-color: $color-first; 12 | align-items: stretch; 13 | 14 | & > *:not(:first-child) { 15 | margin-top: 0.3rem; 16 | } 17 | 18 | &__date { 19 | display: flex; 20 | flex-shrink: 0; 21 | align-items: baseline; 22 | } 23 | 24 | &__day { 25 | margin-right: 0.5rem; 26 | font-family: LatoLight, sans-serif; 27 | font-size: 6rem; 28 | line-height: 6rem; 29 | } 30 | 31 | &__month { 32 | font-family: LatoLight, sans-serif; 33 | font-size: 2.5rem; 34 | } 35 | 36 | &__title { 37 | overflow: hidden; 38 | white-space: nowrap; 39 | text-transform: uppercase; 40 | text-overflow: ellipsis; 41 | font-family: LatoBlack, sans-serif; 42 | font-size: 1.15rem; 43 | font-weight: bold; 44 | } 45 | 46 | &__description { 47 | overflow: hidden; 48 | flex: 1 1; 49 | font-family: LatoRegular, sans-serif; 50 | } 51 | 52 | &__button-add { 53 | text-align: center; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/components/feedback/feedback.js: -------------------------------------------------------------------------------- 1 | import './feedback.scss'; 2 | import $ from 'jquery'; 3 | 4 | class Feedback { 5 | constructor($component) { 6 | this.$component = $component; 7 | this._attachEventHandlers(); 8 | } 9 | 10 | _attachEventHandlers() { 11 | this.$component.on('submit', (event) => event.preventDefault()); 12 | } 13 | } 14 | 15 | export default function renderComponent() { 16 | $(() => { 17 | $('.js-feedback').each((index, node) => { 18 | new Feedback($(node)); 19 | }); 20 | }); 21 | } 22 | 23 | renderComponent(); -------------------------------------------------------------------------------- /src/components/feedback/feedback.pug: -------------------------------------------------------------------------------- 1 | include ../button/button 2 | include ../input/input 3 | include ../textarea/textarea 4 | 5 | mixin feedback() 6 | form.feedback.js-feedback 7 | +input({ 8 | type: 'text', 9 | name: 'feedback-name', 10 | placeholder: 'Your Name', 11 | pattern: '^[A-Za-zА-Яа-яЁё\\s]+$' 12 | }) 13 | +input({ 14 | type: 'email', 15 | name: 'feedback-email', 16 | placeholder: 'Your Email', 17 | required: true 18 | }) 19 | +textarea({ 20 | name: 'feedback-message', 21 | placeholder: 'Your Message' 22 | }) 23 | +input({ 24 | type: 'checkbox', 25 | name: 'feedback-subscribe', 26 | label: 'Subscribe to the newsletter' 27 | }) 28 | +input({ 29 | type: 'radio', 30 | name: 'feedback-age', 31 | label: 'My age is less than 14 years' 32 | }) 33 | +input({ 34 | type: 'radio', 35 | name: 'feedback-age', 36 | label: 'My age from 14 years to 18 years' 37 | }) 38 | +input({ 39 | type: 'radio', 40 | name: 'feedback-age', 41 | label: 'My age is 18 years or older' 42 | }) 43 | .feedback__submit 44 | +button({text: 'submit', isSubmit: true}) 45 | -------------------------------------------------------------------------------- /src/components/feedback/feedback.scss: -------------------------------------------------------------------------------- 1 | @import '~theme/variables.scss'; 2 | 3 | .feedback { 4 | & > *:not(:first-child) { 5 | margin-top: 1rem; 6 | } 7 | 8 | &__validation { 9 | position: relative; 10 | display: flex; 11 | min-width: 6rem; 12 | margin-left: 3%; 13 | border-radius: 4px; 14 | justify-content: center; 15 | align-items: center; 16 | 17 | &::after { 18 | position: absolute; 19 | left: -0.25rem; 20 | display: block; 21 | width: 0.5rem; 22 | height: 0.5rem; 23 | content: ''; 24 | transform: rotate(45deg); 25 | border-bottom-left-radius: 1px; 26 | background-color: inherit; 27 | } 28 | } 29 | 30 | &__validation-ok, 31 | &__validation-error { 32 | text-transform: uppercase; 33 | color: white; 34 | font-family: LatoBlack, sans-serif; 35 | font-size: 0.9rem; 36 | font-weight: bold; 37 | } 38 | 39 | &__input-text, 40 | &__input-email { 41 | &:valid + .feedback__validation { 42 | background-color: $color-first; 43 | 44 | & .feedback__validation-ok { 45 | display: inline; 46 | } 47 | 48 | & .feedback__validation-error { 49 | display: none; 50 | } 51 | } 52 | 53 | &:invalid + .feedback__validation { 54 | background-color: $color-second; 55 | 56 | & .feedback__validation-ok { 57 | display: none; 58 | } 59 | 60 | & .feedback__validation-error { 61 | display: inline; 62 | } 63 | } 64 | } 65 | 66 | &__submit { 67 | text-align: right; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/components/footer/footer.js: -------------------------------------------------------------------------------- 1 | import './footer.scss'; -------------------------------------------------------------------------------- /src/components/footer/footer.pug: -------------------------------------------------------------------------------- 1 | //- 2 | options.items - массив с элементами меню 3 | options.items[0].name - имя элемента меню 4 | options.items[0].href - ссылка элемента меню 5 | 6 | mixin footer(options) 7 | if !options 8 | - options = {} 9 | if !options.items 10 | - options.items = [] 11 | 12 | .footer 13 | nav.footer__nav 14 | for item in options.items 15 | a.footer__nav-item(href = item.href)= item.name 16 | .footer__copyright 17 | | © 2016-2017 -------------------------------------------------------------------------------- /src/components/footer/footer.scss: -------------------------------------------------------------------------------- 1 | .footer { 2 | display: flex; 3 | flex: 1; 4 | padding: 2rem; 5 | color: white; 6 | font-family: sans-serif; 7 | font-size: 1.2rem; 8 | justify-content: space-between; 9 | align-items: center; 10 | 11 | &__nav { 12 | display: flex; 13 | flex: 1; 14 | flex-wrap: wrap; 15 | } 16 | 17 | &__nav-item { 18 | margin-right: 1.5rem; 19 | white-space: nowrap; 20 | color: white; 21 | } 22 | 23 | &__copyright { 24 | white-space: nowrap; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/components/header/header.js: -------------------------------------------------------------------------------- 1 | import './header.scss'; -------------------------------------------------------------------------------- /src/components/header/header.pug: -------------------------------------------------------------------------------- 1 | include ../menu/menu 2 | 3 | //- 4 | options.menuItems - массив елементов меню 5 | 6 | mixin header(options) 7 | if !options 8 | - options = {} 9 | 10 | .header 11 | a.header__logo(href = '/home.html') 12 | | Flat 13 | .header__menu 14 | +menu({items: options.menuItems}) -------------------------------------------------------------------------------- /src/components/header/header.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | display: flex; 3 | flex: 1 0 auto; 4 | padding: 1rem 2rem; 5 | align-items: center; 6 | 7 | .header__logo { 8 | flex: 0 0 auto; 9 | text-decoration: none; 10 | color: white; 11 | font-family: sans-serif; 12 | font-size: 2.5rem; 13 | font-weight: bold; 14 | } 15 | 16 | .header__menu { 17 | flex: 1 0 auto; 18 | padding-left: 1rem; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/components/input/input.js: -------------------------------------------------------------------------------- 1 | import './input.scss'; -------------------------------------------------------------------------------- /src/components/input/input.pug: -------------------------------------------------------------------------------- 1 | include ../toggle/toggle 2 | include ../tick-box/tick-box 3 | 4 | //- 5 | options.name - form element name 6 | options.type - input type 7 | options.placeholder - input placeholder 8 | options.pattern - input pattern 9 | options.tabindex - input tabindex 10 | options.required - input required 11 | options.label - подпись к checkbox и radio 12 | 13 | mixin input(options) 14 | if !options 15 | - options = {} 16 | 17 | .input 18 | - if (options.type === 'text' || options.type === 'email' || options.type === 'password') 19 | input.input__element( 20 | type = options.type 21 | name = options.name 22 | placeholder = options.placeholder 23 | pattern = options.pattern 24 | tabindex = options.tabindex 25 | required = options.required 26 | ) 27 | - if (options.type === 'email' || options.pattern) 28 | .input__validation 29 | span.input__validation-ok thanks! 30 | span.input__validation-error error 31 | 32 | - if (options.type === 'checkbox' || options.type === 'radio') 33 | label.input__label 34 | input.input__element( 35 | type = options.type 36 | name = options.name 37 | tabindex = options.tabindex 38 | required = options.required 39 | ) 40 | if options.type === 'checkbox' 41 | +toggle({mix: 'input__toggle'}) 42 | else 43 | +tick-box({mix: 'input__tick-box'}) 44 | if options.label 45 | .input__label-text=options.label 46 | block 47 | else 48 | block 49 | 50 | -------------------------------------------------------------------------------- /src/components/input/input.scss: -------------------------------------------------------------------------------- 1 | @import '~theme/variables.scss'; 2 | 3 | .input { 4 | position: relative; 5 | display: flex; 6 | align-items: stretch; 7 | 8 | &__element { 9 | flex-grow: 1; 10 | width: 10rem; 11 | padding: 0.43rem 1rem; 12 | color: #888; 13 | border: none; 14 | border-radius: 4px; 15 | background-color: #e5e5e5; 16 | font-family: SourceSansProRegular, sans-serif; 17 | font-size: 1.2rem; 18 | 19 | &:focus { 20 | outline: 0.3rem auto #bbb; 21 | } 22 | 23 | &[type='checkbox'], 24 | &[type='radio'] { 25 | position: absolute; 26 | left: 1rem; 27 | width: 1px; 28 | height: 1px; 29 | 30 | @include firefox-only() { 31 | visibility: hidden; 32 | } 33 | } 34 | } 35 | 36 | &__label { 37 | display: flex; 38 | align-items: center; 39 | } 40 | 41 | &__label-text { 42 | padding: 0.43rem 1rem; 43 | color: #888; 44 | font-family: SourceSansProRegular, sans-serif; 45 | font-size: 1.2rem; 46 | } 47 | 48 | &__toggle, 49 | &__tick-box { 50 | flex: 0 0 auto; 51 | } 52 | 53 | &__validation { 54 | position: relative; 55 | display: flex; 56 | min-width: 6rem; 57 | margin-left: 1rem; 58 | border-radius: 4px; 59 | justify-content: center; 60 | align-items: center; 61 | 62 | &::after { 63 | position: absolute; 64 | top: calc(50% - 0.25rem); 65 | left: -0.25rem; 66 | display: block; 67 | width: 0.5rem; 68 | height: 0.5rem; 69 | content: ''; 70 | transform: rotate(45deg); 71 | border-bottom-left-radius: 1px; 72 | background-color: inherit; 73 | } 74 | } 75 | 76 | &__validation-ok, 77 | &__validation-error { 78 | text-transform: uppercase; 79 | color: white; 80 | font-family: LatoBlack, sans-serif; 81 | font-size: 0.9rem; 82 | font-weight: bold; 83 | } 84 | 85 | &__element { 86 | &:valid + .input__validation { 87 | background-color: $color-first; 88 | 89 | & .input__validation-ok { 90 | display: inline; 91 | } 92 | 93 | & .input__validation-error { 94 | display: none; 95 | } 96 | } 97 | 98 | &:invalid + .input__validation { 99 | background-color: $color-second; 100 | 101 | & .input__validation-ok { 102 | display: none; 103 | } 104 | 105 | & .input__validation-error { 106 | display: inline; 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/components/layout/layout.js: -------------------------------------------------------------------------------- 1 | import './layout.scss' -------------------------------------------------------------------------------- /src/components/layout/layout.pug: -------------------------------------------------------------------------------- 1 | mixin layout() 2 | .layout 3 | block 4 | 5 | mixin layout__header() 6 | header.layout__header 7 | block 8 | 9 | mixin layout__main() 10 | main.layout__main 11 | block 12 | 13 | mixin layout__footer() 14 | footer.layout__footer 15 | block 16 | 17 | //- 18 | options.display - задает display, возможное значение 'block', по-умолчанию 'flex' 19 | options.dasis - задает flex-basis в пикселях, от 50 до 4000 с шагом 50 или auto, по-умолчанию 100% 20 | options.grow - задает flex-grow, от 0 до 10, по-умолчанию 1 21 | options.shrink - задает flex-shrink, от 0 до 10, по-умолчанию 1 22 | options.wrap - задает flex-wrap, возможные значения 'wrap', 'wrap-reverse', по-умолчанию 'nowrap' 23 | options.direction - задает flex-direction, возможные значения 'column', 'column-reverse', 24 | 'row-reverse', по-умолчанию 'row' 25 | options.align - горизонтальное выравнивание относительно родительского контейнера, 26 | не сработает если у соседнего контейнера flex-grow > 0, 27 | возможные значения 'left', 'right', 'center' 28 | options.mix - любая строка, будет добавлена в классы контейнера 29 | mixin layout__container(options) 30 | if !options 31 | - options = {} 32 | 33 | - 34 | var classes = '' 35 | classes += options.display ? ' layout__container_' + options.display : '' 36 | classes += options.basis ? ' layout__container_basis-' + options.basis : '' 37 | classes += options.grow ? ' layout__container_grow-' + options.grow : '' 38 | classes += options.shrink ? ' layout__container_shrink-' + options.shrink : '' 39 | classes += options.wrap ? ' layout__container_' + options.wrap : '' 40 | classes += options.direction ? ' layout__container_' + options.direction : '' 41 | classes += options.align ? ' layout__container_' + options.align : '' 42 | classes += options.mix ? ' ' + options.mix : '' 43 | 44 | .layout__container(class= classes) 45 | block -------------------------------------------------------------------------------- /src/components/layout/layout.scss: -------------------------------------------------------------------------------- 1 | @import '~theme/variables.scss'; 2 | 3 | .layout { 4 | display: flex; 5 | flex-direction: column; 6 | height: 100%; 7 | 8 | &__header { 9 | display: flex; 10 | flex: 0 0 auto; 11 | background-color: $color-first; 12 | } 13 | 14 | &__main { 15 | display: flex; 16 | flex: 1 0 auto; 17 | } 18 | 19 | &__footer { 20 | display: flex; 21 | flex: 0 0 auto; 22 | background-color: $color-second; 23 | } 24 | 25 | &__container { 26 | display: flex; 27 | flex: 1 1 100%; 28 | align-content: flex-start; 29 | 30 | @for $i from 1 to 80 { 31 | $basis: $i * 50; 32 | 33 | &_basis-#{$basis}.layout__container { 34 | flex-basis: #{$basis}px; 35 | } 36 | } 37 | 38 | @for $grow from 0 to 10 { 39 | &_grow-#{$grow} { 40 | flex-grow: $grow; 41 | } 42 | } 43 | 44 | @for $shrink from 0 to 10 { 45 | &_shrink-#{$shrink} { 46 | flex-shrink: $shrink; 47 | } 48 | } 49 | 50 | &_block { 51 | display: block; 52 | } 53 | 54 | &_left { 55 | flex-grow: 0; 56 | margin-right: auto; 57 | } 58 | 59 | &_right { 60 | flex-grow: 0; 61 | margin-left: auto; 62 | } 63 | 64 | &_center { 65 | flex-grow: 0; 66 | margin-right: auto; 67 | margin-left: auto; 68 | } 69 | 70 | &_column { 71 | flex-direction: column; 72 | 73 | & > .layout__container { 74 | flex-basis: auto; 75 | } 76 | } 77 | 78 | &_column-reverse { 79 | flex-direction: column-reverse; 80 | 81 | & > .layout__container { 82 | flex-basis: auto; 83 | } 84 | } 85 | 86 | &_row-reverse { 87 | flex-direction: row-reverse; 88 | } 89 | 90 | &_wrap { 91 | flex-wrap: wrap; 92 | } 93 | 94 | &_wrap-reverse { 95 | flex-wrap: wrap-reverse; 96 | } 97 | 98 | &_basis-auto.layout__container { 99 | flex-basis: auto; 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/components/location/img/crosshair.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 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 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/components/location/img/map-marker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstack-development/flat-starter-kit/5d4e6be5b18685405b20846c709fd661611b3c06/src/components/location/img/map-marker.png -------------------------------------------------------------------------------- /src/components/location/img/map-marker.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 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 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/components/location/location.js: -------------------------------------------------------------------------------- 1 | import './location.scss'; 2 | import $ from 'jquery'; 3 | 4 | class Location { 5 | constructor($component) { 6 | this.$component = $component; 7 | this.render(); 8 | } 9 | 10 | render() { 11 | let coord = null; 12 | try { 13 | coord = JSON.parse(this.$component.data('coord')); 14 | } 15 | catch (e) { 16 | coord = [56.4531907, 84.9756513] 17 | } 18 | 19 | ymaps.ready(() => { 20 | let map = new ymaps.Map($('.js-location__widget', this.$component)[0], { 21 | center: coord, 22 | zoom: 15, 23 | controls: [], 24 | }); 25 | 26 | let placemark = new ymaps.Placemark(coord, {}, { 27 | iconLayout: 'default#image', 28 | iconImageHref: require('./img/map-marker.png'), 29 | iconImageSize: [56, 56], 30 | iconImageOffset: [-19, -56] 31 | }); 32 | 33 | map.geoObjects.add(placemark); 34 | }); 35 | } 36 | }; 37 | 38 | $(() => { 39 | $('.js-location').each((index, node) => { 40 | new Location($(node)); 41 | }); 42 | }); -------------------------------------------------------------------------------- /src/components/location/location.pug: -------------------------------------------------------------------------------- 1 | append scripts 2 | script(src='https://api-maps.yandex.ru/2.1/?lang=ru_RU', type='text/javascript') 3 | 4 | //- 5 | options.coord - массив c координатами ( [X, Y] ) 6 | options.address - адрес 7 | 8 | mixin location(options) 9 | if !options 10 | - options = {} 11 | 12 | .location.js-location(data-coord= JSON.stringify(options.coord || [])) 13 | .location__widget.js-location__widget 14 | .location__info 15 | span.location__slogan Meet us! 16 | .location__adress!= options.address || '' 17 | .location__icon 18 | include ./img/crosshair.svg 19 | .location__icon 20 | include ./img/map-marker.svg -------------------------------------------------------------------------------- /src/components/location/location.scss: -------------------------------------------------------------------------------- 1 | @import '~theme/variables.scss'; 2 | 3 | .location { 4 | position: relative; 5 | z-index: 3000; 6 | overflow: hidden; 7 | border-radius: 0.3rem; 8 | 9 | &__widget { 10 | height: 17rem; 11 | 12 | & > ymaps, 13 | & > ymaps > ymaps { 14 | width: 100% !important; 15 | } 16 | } 17 | 18 | &__info { 19 | display: flex; 20 | padding: 3% 4.5%; 21 | color: white; 22 | background-color: $color-second; 23 | font-family: LatoLight, sans-serif; 24 | font-size: 2.5rem; 25 | align-items: center; 26 | 27 | & > *:not(:first-child) { 28 | margin-left: 4%; 29 | } 30 | } 31 | 32 | &__slogan { 33 | flex-shrink: 0; 34 | } 35 | 36 | &__adress { 37 | flex-grow: 1; 38 | text-align: left; 39 | text-transform: uppercase; 40 | font-family: LatoBlack, sans-serif; 41 | font-size: 1.1rem; 42 | font-weight: bold; 43 | } 44 | 45 | &__icon { 46 | display: flex; 47 | flex-shrink: 0; 48 | flex-basis: 2.2rem; 49 | height: 2.2rem; 50 | 51 | & svg path { 52 | fill: white; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/components/menu/img/menu-options.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 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 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/components/menu/menu.js: -------------------------------------------------------------------------------- 1 | import './menu.scss'; 2 | import $ from 'jquery'; 3 | 4 | class Menu { 5 | constructor($component) { 6 | this.$component = $component; 7 | this._attachEventHandler(); 8 | this.$list = $('.js-menu__list', this.$component); 9 | } 10 | 11 | _attachEventHandler() { 12 | $('.js-menu__button', this.$component).on('click', () => this.$list.toggleClass('menu__list_open')); 13 | } 14 | } 15 | 16 | $(() => { 17 | $('.js-menu').each((index, node) => { 18 | new Menu($(node)); 19 | }); 20 | }); -------------------------------------------------------------------------------- /src/components/menu/menu.pug: -------------------------------------------------------------------------------- 1 | //- 2 | options.items - массив с элементами меню 3 | options.items[0].name - имя элемента меню 4 | options.items[0].href - ссылка элемента меню 5 | 6 | mixin menu(options) 7 | if !options 8 | - options = {} 9 | if !options.items 10 | - options.items = [] 11 | 12 | .menu.js-menu 13 | .menu__button.js-menu__button 14 | include ./img/menu-options.svg 15 | nav.menu__list.js-menu__list 16 | for item in options.items 17 | a.menu__item(href = item.href)= item.name -------------------------------------------------------------------------------- /src/components/menu/menu.scss: -------------------------------------------------------------------------------- 1 | @import '~theme/variables.scss'; 2 | 3 | .menu { 4 | position: relative; 5 | display: flex; 6 | flex: 1 0 auto; 7 | justify-content: flex-end; 8 | 9 | &__button { 10 | display: flex; 11 | padding: 0.4rem 0.6rem; 12 | border: 1px solid white; 13 | border-radius: 0.3rem; 14 | background-color: $color-first; 15 | 16 | @include media-breakpoint-up($sm) { 17 | display: none; 18 | } 19 | 20 | &:hover { 21 | background-color: $color-first-dark; 22 | } 23 | 24 | & svg { 25 | width: 2.5rem; 26 | } 27 | } 28 | 29 | &__list { 30 | display: none; 31 | 32 | @include media-breakpoint-up($sm) { 33 | display: flex; 34 | } 35 | 36 | @include media-breakpoint-down($sm) { 37 | &_open { 38 | position: absolute; 39 | z-index: 10; 40 | top: 100%; 41 | right: -2rem; 42 | display: flex; 43 | flex-direction: column; 44 | padding: 1rem 2rem 2rem; 45 | background-color: #4eb7a8; 46 | } 47 | } 48 | } 49 | 50 | &__item { 51 | padding: 1rem 2rem; 52 | text-align: center; 53 | text-decoration: none; 54 | text-transform: uppercase; 55 | color: white; 56 | border: 1px solid white; 57 | border-radius: 0.3rem; 58 | background-color: $color-first; 59 | font-family: sans-serif; 60 | font-size: 1.2rem; 61 | font-weight: bold; 62 | 63 | &:not(:first-child) { 64 | margin-top: 1rem; 65 | margin-left: 0; 66 | 67 | @include media-breakpoint-up($sm) { 68 | margin-top: 0; 69 | margin-left: 1rem; 70 | } 71 | } 72 | 73 | &:hover { 74 | background-color: $color-first-dark; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/components/messenger/img/message.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/components/messenger/img/photo-camera.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 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 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/components/messenger/messenger.js: -------------------------------------------------------------------------------- 1 | import './messenger.scss'; 2 | import $ from 'jquery'; 3 | import pugTemplate from './template.pug'; 4 | 5 | class Messenger { 6 | constructor($component) { 7 | this.$component = $component; 8 | this._hideScroll(); 9 | this._attachEventHandlers(); 10 | } 11 | 12 | static template(options) { 13 | return pugTemplate({options}); 14 | } 15 | 16 | _hideScroll() { 17 | const $scroller = $('.js-messenger__chat-scroller', this.$component); 18 | const $chat = $('.js-messenger__chat', this.$component); 19 | const scrollWidth = $scroller.width() - $chat.width(); 20 | 21 | $scroller.css('margin-right', -scrollWidth + 'px'); 22 | } 23 | 24 | _attachEventHandlers() { 25 | $('.js-messenger__btn-submit', this.$component).on('click', () => { 26 | const $chat = $('.js-messenger__chat', this.$component); 27 | const $input = $('.js-messenger__input', this.$component); 28 | let message = $input.val(); 29 | 30 | if (message == '') return; 31 | 32 | let $message = $(`
${message}
`); 33 | $chat.append($message); 34 | 35 | $input.val(''); 36 | }); 37 | } 38 | } 39 | 40 | 41 | $(() => { 42 | $('.js-messenger').each((index, node) => { 43 | new Messenger($(node)); 44 | }); 45 | }); 46 | 47 | export default Messenger; -------------------------------------------------------------------------------- /src/components/messenger/messenger.pug: -------------------------------------------------------------------------------- 1 | //- 2 | options.user - объект с данными пользователя идентичный передаваемому в User-profile 3 | options.messages - массив сообщений. 4 | options.messages[0].isIncoming - флаг входящего сообщения 5 | options.messages[0].text - тест сообщения 6 | 7 | include ../user-profile/user-profile 8 | include ../button/button 9 | 10 | mixin messenger(options) 11 | if !options 12 | - options = {} 13 | if !options.user 14 | - options.user = {} 15 | if !options.messages 16 | - options.messages = [] 17 | 18 | .messenger.js-messenger 19 | .messenger__user 20 | .messenger__user-name #{options.user.name} 21 | .messenger__user-profile 22 | +user-profile(options.user) 23 | .messenger__body 24 | .messenger__links 25 | a( 26 | href = '/mock-address/change-me' 27 | target = '_blank' 28 | rel = 'noopener noreferrer' 29 | ).messenger__link 30 | include ./img/message.svg 31 | a( 32 | href = '/mock-address/change-me' 33 | target = '_blank' 34 | rel = 'noopener noreferrer' 35 | ).messenger__link 36 | include ./img/photo-camera.svg 37 | .messenger__chat-wrapper 38 | .messenger__chat-scroller.js-messenger__chat-scroller 39 | .messenger__chat.js-messenger__chat 40 | for message in options.messages 41 | .messenger__message(class = message.isIncoming ? 'messenger__message_in' : 'messenger__message_out') 42 | | #{message.text} 43 | textarea.messenger__input.js-messenger__input 44 | .messenger__btn-submit.js-messenger__btn-submit 45 | +button({text: "reply"}) -------------------------------------------------------------------------------- /src/components/messenger/messenger.scss: -------------------------------------------------------------------------------- 1 | @import '~theme/variables.scss'; 2 | 3 | .messenger { 4 | display: flex; 5 | flex-direction: column; 6 | align-items: stretch; 7 | 8 | &__user-name { 9 | overflow: hidden; 10 | padding: 2rem; 11 | text-align: center; 12 | text-overflow: ellipsis; 13 | color: white; 14 | border-top-left-radius: 0.3rem; 15 | border-top-right-radius: 0.3rem; 16 | background-color: $color-first; 17 | font-family: LatoLight, sans-serif; 18 | font-size: 2.5rem; 19 | } 20 | 21 | &__user-profile { 22 | position: relative; 23 | height: 2.9rem; 24 | background-color: $color-first-dark; 25 | 26 | & .user-profile { 27 | position: absolute; 28 | bottom: 0; 29 | left: 50%; 30 | transform: translate(-50%, 50%); 31 | } 32 | } 33 | 34 | &__body { 35 | display: flex; 36 | flex-direction: column; 37 | min-height: 33rem; 38 | padding: 2rem; 39 | background-color: #f2f2f2; 40 | font-family: LatoBold, sans-serif; 41 | font-weight: bold; 42 | align-items: stretch; 43 | 44 | & > *:not(:last-child) { 45 | margin-bottom: 2rem; 46 | } 47 | } 48 | 49 | &__links { 50 | display: flex; 51 | padding: 0 0.25rem; 52 | font-size: 1.7rem; 53 | justify-content: space-between; 54 | } 55 | 56 | &__link { 57 | width: 2rem; 58 | text-decoration: none; 59 | 60 | & svg path { 61 | fill: #8e8d8d; 62 | } 63 | } 64 | 65 | &__chat-wrapper { 66 | position: relative; 67 | overflow: hidden; 68 | flex: 1 1 auto; 69 | 70 | &::after, 71 | &::before { 72 | position: absolute; 73 | z-index: 1; 74 | display: block; 75 | width: 100%; 76 | height: 0.5rem; 77 | content: ''; 78 | } 79 | 80 | &::before { 81 | top: 0; 82 | background-image: linear-gradient(to top, transparent, #f2f2f2); 83 | } 84 | 85 | &::after { 86 | bottom: 0; 87 | background-image: linear-gradient(to bottom, transparent, #f2f2f2); 88 | } 89 | } 90 | 91 | &__chat-scroller { 92 | overflow-y: scroll; 93 | max-height: 300px; 94 | 95 | &::-webkit-scrollbar { 96 | width: 0; 97 | } 98 | } 99 | 100 | &__message { 101 | position: relative; 102 | display: flex; 103 | margin-top: 0.5rem; 104 | margin-bottom: 0.5rem; 105 | padding: 1.5rem; 106 | text-align: left; 107 | white-space: pre-wrap; 108 | color: #868686; 109 | border-radius: 0.5rem; 110 | background-color: white; 111 | font-size: 1.2rem; 112 | line-height: 1.3rem; 113 | align-items: center; 114 | } 115 | 116 | &__message_in { 117 | margin-right: 2rem; 118 | margin-left: 0.5rem; 119 | 120 | &::after { 121 | position: absolute; 122 | left: -0.25rem; 123 | display: block; 124 | width: 0.5rem; 125 | height: 0.5rem; 126 | content: ''; 127 | transform: rotate(45deg); 128 | border-bottom-left-radius: 1px; 129 | background-color: inherit; 130 | } 131 | } 132 | 133 | &__message_out { 134 | margin-right: 0.5rem; 135 | margin-left: 2rem; 136 | 137 | &::after { 138 | position: absolute; 139 | right: -0.25rem; 140 | display: block; 141 | width: 0.5rem; 142 | height: 0.5rem; 143 | content: ''; 144 | transform: rotate(45deg); 145 | border-top-right-radius: 1px; 146 | background-color: inherit; 147 | } 148 | } 149 | 150 | &__input { 151 | width: 100%; 152 | height: 6rem; 153 | padding: 1.5rem; 154 | resize: none; 155 | text-align: left; 156 | color: #777; 157 | border: 0.3rem solid white; 158 | border-radius: 0.5rem; 159 | background-color: #e5e5e5; 160 | font-family: inherit; 161 | font-size: 1.2rem; 162 | line-height: 1.3rem; 163 | 164 | &:focus { 165 | outline: none; 166 | } 167 | } 168 | 169 | &__btn-submit .button { 170 | width: 100%; 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/components/messenger/template.pug: -------------------------------------------------------------------------------- 1 | include ./messenger 2 | 3 | +messenger(options) -------------------------------------------------------------------------------- /src/components/news/news.js: -------------------------------------------------------------------------------- 1 | import './news.scss'; 2 | import $ from 'jquery'; 3 | 4 | class News { 5 | constructor($component) { 6 | this.$component = $component; 7 | this.$description = $('.js-news__description', this.$component); 8 | this._setColumnWidth(); 9 | this._attachEventHandlers(); 10 | } 11 | 12 | _attachEventHandlers() { 13 | $(window).resize(() => this._setColumnWidth()); 14 | } 15 | 16 | _setColumnWidth() { 17 | const width = this.$description.width(); 18 | const height = this.$description.height(); 19 | this.$description 20 | .css({ 21 | '-webkit-column-width': `${width}px`, 22 | '-moz-column-width': `${width}px`, 23 | 'column-width': `${width}px`, 24 | 'height': `${height}px`, 25 | }); 26 | } 27 | } 28 | 29 | export default function renderComponent() { 30 | $(() => { 31 | $('.js-news').each((index, node) => { 32 | new News($(node)); 33 | }); 34 | }); 35 | } 36 | 37 | renderComponent(); -------------------------------------------------------------------------------- /src/components/news/news.pug: -------------------------------------------------------------------------------- 1 | //- 2 | options.title - название 3 | options.date - дата 4 | options.desc - описание 5 | options.text - обложка 6 | 7 | mixin news(options) 8 | if !options 9 | - options = {} 10 | 11 | .news.js-news 12 | .news__title= options.title || '' 13 | .news__date= options.date || '' 14 | .news__description.js-news__description= options.desc || '' 15 | .news__text= options.text || '' -------------------------------------------------------------------------------- /src/components/news/news.scss: -------------------------------------------------------------------------------- 1 | @import '~theme/variables.scss'; 2 | 3 | .news { 4 | display: flex; 5 | overflow: hidden; 6 | flex-direction: column; 7 | flex: 1 1; 8 | padding: 1.5rem; 9 | text-align: left; 10 | color: white; 11 | background-color: $color-second; 12 | align-items: stretch; 13 | justify-content: space-around; 14 | 15 | & > *:not(:first-child) { 16 | margin-top: 0.5rem; 17 | } 18 | 19 | &__title { 20 | overflow: hidden; 21 | flex-shrink: 0; 22 | text-overflow: ellipsis; 23 | font-family: LatoLight, sans-serif; 24 | font-size: 2.3rem; 25 | line-height: 2.3rem; 26 | } 27 | 28 | &__date { 29 | overflow: hidden; 30 | flex-shrink: 0; 31 | text-transform: uppercase; 32 | text-overflow: ellipsis; 33 | font-family: LatoBlack, sans-serif; 34 | font-weight: bold; 35 | } 36 | 37 | &__description { 38 | overflow: hidden; 39 | flex: 1 1; 40 | text-overflow: ellipsis; 41 | font-family: LatoRegular, sans-serif; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/components/percentage/percentage.js: -------------------------------------------------------------------------------- 1 | import './percentage.scss'; 2 | import $ from 'jquery'; 3 | 4 | class Percentage { 5 | constructor($component) { 6 | this.$component = $component; 7 | this.render(); 8 | } 9 | 10 | render() { 11 | const $activeBorder = $('.js-percentage__active-border', this.$component); 12 | let perc = +$activeBorder.data('percent'); 13 | if (isNaN(perc)) return; 14 | if (perc < 0) perc = 0; 15 | if (perc > 100) perc = 100; 16 | 17 | const timeTick = 1500 / perc; 18 | let i = 0; 19 | 20 | let timer = setTimeout( function changePerc() { 21 | $('.percentage__percent', $activeBorder).html(i); 22 | if (i * 3.6 <= 180) 23 | $activeBorder.css('background-image', 'linear-gradient(' + (90 + i * 3.6) + 'deg, transparent 50%, #fff 50%), linear-gradient(90deg, #fff 50%, transparent 50%)'); 24 | else 25 | $activeBorder.css('background-image', 'linear-gradient(' + (i * 3.6 - 90) + 'deg, transparent 50%, #e75735 50%), linear-gradient(90deg, #fff 50%, transparent 50%)'); 26 | if(++i <= perc) 27 | timer = setTimeout(changePerc, timeTick); 28 | }, timeTick); 29 | } 30 | }; 31 | 32 | $(() => { 33 | $('.js-percentage').each((index, node) => { 34 | new Percentage($(node)); 35 | }); 36 | }); -------------------------------------------------------------------------------- /src/components/percentage/percentage.pug: -------------------------------------------------------------------------------- 1 | //- options.percent - проценты (от 0 до 100) 2 | 3 | mixin percentage(options) 4 | if !options 5 | - options = {} 6 | 7 | .percentage.js-percentage 8 | .percentage__active-border.js-percentage__active-border(data-percent = options.percent || 0) 9 | .percentage__mask-circle 10 | span.percentage__percent= options.percent || 0 -------------------------------------------------------------------------------- /src/components/percentage/percentage.scss: -------------------------------------------------------------------------------- 1 | @import '~theme/variables.scss'; 2 | 3 | .percentage { 4 | display: inline-flex; 5 | width: 8rem; 6 | height: 8rem; 7 | 8 | &__active-border { 9 | display: flex; 10 | flex-basis: 100%; 11 | padding: 5px; 12 | border-radius: 50%; 13 | background-color: $color-second; 14 | background-image: linear-gradient(90deg, transparent 50%, #fff 50%), linear-gradient(90deg, #fff 50%, transparent 50%); 15 | } 16 | 17 | &__mask-circle { 18 | display: flex; 19 | flex-basis: 100%; 20 | border-radius: 50%; 21 | background-color: white; 22 | justify-content: center; 23 | align-items: center; 24 | } 25 | 26 | &__percent { 27 | margin-top: 0.3rem; 28 | color: #9d9d9d; 29 | font-family: LatoLight, sans-serif; 30 | font-size: 3.25rem; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/components/pie-chart/pie-chart.js: -------------------------------------------------------------------------------- 1 | import './pie-chart.scss'; 2 | import $ from 'jquery'; 3 | 4 | if (!$.fn.peity) { 5 | require('peity/jquery.peity'); 6 | } 7 | 8 | class PieChart { 9 | constructor($component) { 10 | this.$component = $component; 11 | this.render(); 12 | } 13 | 14 | render() { 15 | $('.js-pie-chart__items', this.$component).peity('donut', { 16 | fill: ['#747474', '#e75735', '#4eb7a8', '#e5e5e5'], 17 | radius: 47.5, 18 | innerRadius: 30.5 19 | }); 20 | } 21 | }; 22 | 23 | $(() => { 24 | $('.js-pie-chart').each((index, node) => { 25 | new PieChart($(node)); 26 | }); 27 | }); -------------------------------------------------------------------------------- /src/components/pie-chart/pie-chart.pug: -------------------------------------------------------------------------------- 1 | //- options.items - массив 2 | 3 | mixin pie-chart(options) 4 | if !options 5 | - options = {} 6 | if !options.items 7 | - options.items = [ 1 ] 8 | 9 | - var itemsStr = '' 10 | each item in options.items 11 | - itemsStr += item + ',' 12 | - itemsStr = itemsStr.slice(0, itemsStr.length - 1) 13 | 14 | .pie-chart.js-pie-chart 15 | span.pie-chart__items.js-pie-chart__items #{itemsStr} -------------------------------------------------------------------------------- /src/components/pie-chart/pie-chart.scss: -------------------------------------------------------------------------------- 1 | .pie-chart { 2 | display: inline-block; 3 | } 4 | -------------------------------------------------------------------------------- /src/components/search/img/glass-finder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 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 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/components/search/search.js: -------------------------------------------------------------------------------- 1 | import './search.scss'; 2 | import $ from 'jquery'; 3 | 4 | class Search { 5 | constructor($component) { 6 | this.$component = $component; 7 | this._attachEventHandlers(); 8 | } 9 | 10 | _attachEventHandlers() { 11 | const $text = $('.js-search__text', this.$component); 12 | 13 | $('.js-search__submit', this.$component).on('click', (event) => { 14 | event.preventDefault(); 15 | 16 | if ($text.val() == '') return; 17 | 18 | const result = this._search($text.val()); 19 | if (result) { 20 | //output results 21 | return; 22 | } 23 | 24 | this.$component.addClass('search_error'); 25 | $text.val(''); 26 | $text.attr('placeholder', 'I\'ve not found what I\'m looking for...'); 27 | }); 28 | 29 | $text.on('focusin', () => { 30 | if (!this.$component.hasClass('search_error')) return; 31 | 32 | this.$component.removeClass('search_error'); 33 | $text.attr('placeholder', 'Search'); 34 | }); 35 | } 36 | 37 | _search(text) { 38 | //find text and return results 39 | } 40 | }; 41 | 42 | $(() => { 43 | $('.js-search').each((index, node) => { 44 | new Search($(node)); 45 | }); 46 | }); -------------------------------------------------------------------------------- /src/components/search/search.pug: -------------------------------------------------------------------------------- 1 | mixin search() 2 | .search.js-search 3 | form.search__form 4 | input.search__text.js-search__text( 5 | name = 'search-text' 6 | type = 'text' 7 | placeholder = 'Search' 8 | tabindex = 5 9 | ) 10 | button( 11 | type = 'submit' 12 | tabindex = 6 13 | ).search__submit.js-search__submit 14 | include ./img/glass-finder.svg -------------------------------------------------------------------------------- /src/components/search/search.scss: -------------------------------------------------------------------------------- 1 | @import '~theme/variables.scss'; 2 | 3 | .search { 4 | font-family: SourceSansProRegular, sans-serif; 5 | font-size: 1.2rem; 6 | 7 | &_error &__text { 8 | background-color: $color-second; 9 | 10 | &[placeholder] { 11 | text-overflow: ellipsis; 12 | } 13 | 14 | &::-webkit-input-placeholder { 15 | text-overflow: ellipsis; 16 | color: white; 17 | } 18 | 19 | &::-moz-placeholder { 20 | text-overflow: ellipsis; 21 | color: white; 22 | } 23 | 24 | &:-moz-placeholder { 25 | text-overflow: ellipsis; 26 | color: white; 27 | } 28 | 29 | &:-ms-input-placeholder { 30 | text-overflow: ellipsis; 31 | color: white; 32 | } 33 | } 34 | 35 | &__form { 36 | display: flex; 37 | align-items: stretch; 38 | } 39 | 40 | &__text { 41 | display: block; 42 | width: 100%; 43 | padding: 0.55rem 1rem; 44 | resize: none; 45 | transition: background-color 0.2s; 46 | color: #888; 47 | border: none; 48 | border-top-left-radius: 3px; 49 | border-bottom-left-radius: 3px; 50 | background-color: #e5e5e5; 51 | 52 | &:focus { 53 | outline: 0.3rem auto #bbb; 54 | } 55 | } 56 | 57 | &__submit { 58 | width: 3.2rem; 59 | text-align: center; 60 | border: none; 61 | border-top-right-radius: 3px; 62 | border-bottom-right-radius: 3px; 63 | background-color: $color-second; 64 | 65 | &:focus { 66 | outline: 0.3rem auto #aaa; 67 | } 68 | 69 | & svg { 70 | width: 60%; 71 | 72 | & path { 73 | fill: white; 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/components/signin/signin.js: -------------------------------------------------------------------------------- 1 | import './signin.scss'; 2 | import $ from 'jquery'; 3 | 4 | class Signin { 5 | constructor($component) { 6 | this.$component = $component; 7 | this._attachEventHandlers(); 8 | } 9 | 10 | _attachEventHandlers() { 11 | this.$component.on('submit', (event) => { 12 | event.preventDefault(); 13 | document.location.replace('/demo.html'); 14 | }); 15 | } 16 | } 17 | 18 | export default function renderComponent() { 19 | $(() => { 20 | $('.js-signin').each((index, node) => { 21 | new Signin($(node)); 22 | }); 23 | }); 24 | } 25 | 26 | renderComponent(); -------------------------------------------------------------------------------- /src/components/signin/signin.pug: -------------------------------------------------------------------------------- 1 | include ../button/button 2 | include ../input/input 3 | 4 | mixin signin() 5 | form.signin.js-signin 6 | +input({ 7 | name: 'login', 8 | type: 'text', 9 | placeholder: 'Login or email', 10 | required: true 11 | }) 12 | +input({ 13 | name: 'password', 14 | type: 'password', 15 | placeholder: 'Password', 16 | required: true 17 | }) 18 | .signin__submit 19 | +input({ 20 | name: 'remember', 21 | type: 'checkbox', 22 | label: 'Remember me' 23 | }) 24 | +button({text: 'Sign in', isSubmit: true}) 25 | -------------------------------------------------------------------------------- /src/components/signin/signin.scss: -------------------------------------------------------------------------------- 1 | .signin { 2 | display: flex; 3 | flex-direction: column; 4 | 5 | & > *:not(:first-child) { 6 | margin-top: 1rem; 7 | } 8 | 9 | &__submit { 10 | display: flex; 11 | flex-wrap: wrap; 12 | align-items: center; 13 | justify-content: space-between; 14 | 15 | & > * { 16 | margin-bottom: 1rem; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/signup/signup.js: -------------------------------------------------------------------------------- 1 | import './signup.scss'; 2 | import $ from 'jquery'; 3 | 4 | class Signup { 5 | constructor($component) { 6 | this.$component = $component; 7 | this.$password = $('[name=password]', this.$component); 8 | this.$confirm = $('[name=confirm]', this.$component); 9 | this._attachEventHandlers(); 10 | } 11 | 12 | _attachEventHandlers() { 13 | this.$component.on('submit', (event) => { 14 | event.preventDefault(); 15 | document.location.replace('/profile.html'); 16 | }); 17 | this.$password.on('blur', () => { 18 | this.$confirm[0].setCustomValidity('Confirm Password don\'t equal Password'); 19 | this._validateConfirm(); 20 | }); 21 | this.$confirm.on('input', () => { 22 | this.$confirm[0].setCustomValidity('Confirm Password don\'t equal Password'); 23 | this._validateConfirm(); 24 | }); 25 | 26 | } 27 | 28 | _validateConfirm() { 29 | if (this.$password[0].value === this.$confirm[0].value) 30 | this.$confirm[0].setCustomValidity(''); 31 | } 32 | } 33 | 34 | export default function renderComponent() { 35 | $(() => { 36 | $('.js-signup').each((index, node) => { 37 | new Signup($(node)); 38 | }); 39 | }); 40 | } 41 | 42 | renderComponent(); -------------------------------------------------------------------------------- /src/components/signup/signup.pug: -------------------------------------------------------------------------------- 1 | include ../button/button 2 | include ../input/input 3 | 4 | mixin signup() 5 | form.signup.js-signup 6 | +input({ 7 | name: 'name', 8 | type: 'text', 9 | placeholder: 'Your name', 10 | required: true 11 | }) 12 | +input({ 13 | name: 'email', 14 | type: 'email', 15 | placeholder: 'Email', 16 | required: true 17 | }) 18 | +input({ 19 | name: 'login', 20 | type: 'text', 21 | placeholder: 'Login', 22 | required: true 23 | }) 24 | +input({ 25 | name: 'password', 26 | type: 'password', 27 | placeholder: 'Password', 28 | required: true 29 | }) 30 | +input({ 31 | name: 'confirm', 32 | type: 'password', 33 | placeholder: 'Confirm password', 34 | required: true 35 | }) 36 | .signup__confirm-error.js-signup__confirm-error= 'Confirm Password don\'t equal Password' 37 | +input({ 38 | name: 'remember', 39 | type: 'checkbox', 40 | label: 'I agree to the ', 41 | required: true 42 | }) 43 | a.signup__privacy(href = '/privacy.html')= 'Privacy Policy' 44 | +input({ 45 | name: 'remember', 46 | type: 'checkbox', 47 | label: 'Subscribe to the project news' 48 | }) 49 | .signup__submit 50 | +button({text: 'Sign in', isSubmit: true}) 51 | -------------------------------------------------------------------------------- /src/components/signup/signup.scss: -------------------------------------------------------------------------------- 1 | .signup { 2 | display: flex; 3 | flex-direction: column; 4 | 5 | & > *:not(:first-child):not([class*=error]) { 6 | margin-top: 1rem; 7 | } 8 | 9 | &__confirm-error { 10 | display: none; 11 | margin-top: 0.2rem; 12 | padding: 0.2rem 1rem; 13 | color: #ab4027; 14 | border-radius: 4px; 15 | background-color: #eaccc4; 16 | font-family: SourceSansProRegular, sans-serif; 17 | font-size: 1.2rem; 18 | 19 | &_active { 20 | display: block; 21 | } 22 | } 23 | 24 | &__submit { 25 | display: flex; 26 | flex-wrap: wrap; 27 | align-items: center; 28 | justify-content: flex-end; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/slider/slider.js: -------------------------------------------------------------------------------- 1 | import './slider.scss'; 2 | import $ from 'jquery'; 3 | 4 | if (!$.fn.slider) { 5 | require('jquery-ui/ui/widgets/slider'); 6 | require('jquery-ui/themes/base/slider.css'); 7 | } 8 | 9 | class Slider { 10 | constructor($component) { 11 | this.$component = $component; 12 | this.render(); 13 | } 14 | 15 | render() { 16 | let $handleTooltip = $('.js-slider__handle-tooltip', this.$component); 17 | let $sliderScale = $('.js-slider__scale', this.$component); 18 | let sliderColor = this.$component.data('slider-color'); 19 | 20 | const $widget = $('.js-slider__widget', this.$component); 21 | $widget.slider({ 22 | range: $sliderScale.length ? 'min' : false, 23 | 24 | create: $handleTooltip.length ? () => { 25 | $handleTooltip.text($widget.slider('value')); 26 | } : () => {}, 27 | 28 | slide: $handleTooltip.length ? (event, ui) => { 29 | $handleTooltip.text(ui.value); 30 | } : () => {} 31 | }); 32 | 33 | $('.ui-slider-range', this.$component).css('background-color', sliderColor); 34 | $('.js-slider__handle', this.$component).css('background-color', sliderColor); 35 | 36 | this._attachTooltipEventHandlers(); 37 | } 38 | 39 | _attachTooltipEventHandlers() { 40 | let $handleTooltip = $('.js-slider__handle-tooltip', this.$component); 41 | 42 | if ($handleTooltip.length) { 43 | $('.js-slider__handle', this.$component).on('mousedown', () => { 44 | $handleTooltip.addClass('slider__handle-tooltip_active') 45 | }); 46 | this.$component.on('mousedown', () => { 47 | $handleTooltip.addClass('slider__handle-tooltip_active') 48 | }); 49 | $('body').on('mouseup', () => { 50 | $handleTooltip.removeClass('slider__handle-tooltip_active') 51 | }); 52 | } 53 | } 54 | }; 55 | 56 | $(() => { 57 | $('.js-slider').each((index, node) => { 58 | new Slider($(node)); 59 | }); 60 | }); -------------------------------------------------------------------------------- /src/components/slider/slider.pug: -------------------------------------------------------------------------------- 1 | //- 2 | options.tooltipEnabled - наличие подсказки (bool) 3 | options.scaleEnabled - наличие шкалы (bool) 4 | options.color - цвет слайдера 5 | 6 | mixin slider(options) 7 | if !options 8 | - options = {} 9 | 10 | .slider.js-slider(data-slider-color = options.color || '#4eb7a8') 11 | .slider__widget.js-slider__widget 12 | .slider__handle.js-slider__handle.ui-slider-handle 13 | if options.tooltipEnabled 14 | .slider__handle-tooltip.js-slider__handle-tooltip 15 | if options.scaleEnabled 16 | .slider__scale.js-slider__scale 17 | span.slider__scale-item 0 18 | span.slider__scale-item 25 19 | span.slider__scale-item 50 20 | span.slider__scale-item 75 21 | span.slider__scale-item 100 -------------------------------------------------------------------------------- /src/components/slider/slider.scss: -------------------------------------------------------------------------------- 1 | .slider { 2 | & &__widget.ui-widget-content { 3 | height: 0.4rem; 4 | margin: 0.6rem 0.8rem; 5 | border: none; 6 | background-color: #e5e5e5; 7 | } 8 | 9 | & &__handle.ui-slider-handle { 10 | top: -0.6rem; 11 | width: 1.5rem; 12 | height: 1.5rem; 13 | margin-left: -0.8rem; 14 | cursor: pointer; 15 | border: none; 16 | border-radius: 50%; 17 | 18 | &:focus { 19 | outline: none; 20 | } 21 | 22 | &.ui-state-active { 23 | color: inherit; 24 | border: none; 25 | } 26 | } 27 | 28 | &__handle-tooltip { 29 | position: absolute; 30 | bottom: 140%; 31 | display: none; 32 | padding: 0.3rem 0.8rem; 33 | transform: translateX(calc((-100% + 1.5rem) / 2)); 34 | color: white; 35 | border-radius: 0.4rem; 36 | background-color: inherit; 37 | font-family: LatoBlack, sans-serif; 38 | font-size: 1.1rem; 39 | font-weight: bold; 40 | 41 | &_active { 42 | display: block; 43 | } 44 | 45 | &::before { 46 | position: absolute; 47 | bottom: -0.25rem; 48 | left: calc(50% - 0.25rem); 49 | display: block; 50 | width: 0.5rem; 51 | height: 0.5rem; 52 | content: ''; 53 | transform: rotate(45deg); 54 | border-bottom-right-radius: 1px; 55 | background-color: inherit; 56 | } 57 | } 58 | 59 | &__scale { 60 | display: flex; 61 | margin-top: 0.75rem; 62 | padding: 0 0.4rem; 63 | justify-content: space-between; 64 | } 65 | 66 | &__scale-item { 67 | color: #d1d1d1; 68 | font-family: LatoBlack, sans-serif; 69 | font-size: 0.9rem; 70 | font-weight: bold; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/components/stages/stages.js: -------------------------------------------------------------------------------- 1 | import './stages.scss'; 2 | import $ from 'jquery'; 3 | 4 | class Stages { 5 | constructor($component) { 6 | this.$component = $component; 7 | this.render(); 8 | } 9 | 10 | render() { 11 | let progress = this.$component.data('progress'); 12 | let itemsLength = $('.js-stages__item', this.$component).length; 13 | 14 | $('.js-stages__progress', this.$component) 15 | .css('width', (100 * (progress - 1) / (itemsLength - 1)) + '%'); 16 | } 17 | }; 18 | 19 | $(() => { 20 | $('.js-stages').each((index, node) => { 21 | new Stages($(node)); 22 | }); 23 | }); -------------------------------------------------------------------------------- /src/components/stages/stages.pug: -------------------------------------------------------------------------------- 1 | //- 2 | options.length - количество стадий 3 | options.progress - количество завершенных стадий 4 | 5 | mixin stages(options) 6 | if !options 7 | - options = {} 8 | 9 | - 10 | if (!options.length || options.length < 2) options.length = 2 11 | if (!options.progress || options.progress <= 0) options.progress = 1 12 | if (options.progress > options.length) options.progress = options.length 13 | 14 | .stages.js-stages(data-progress = options.progress) 15 | .stages__progress-bar 16 | .stages__progress.js-stages__progress 17 | .stages__items 18 | - for (var i = 0; i < options.length; i++) 19 | .stages__item.js-stages__item(class = (options.progress > i) ? 'stages__item_completed' : '') #{i + 1} 20 | -------------------------------------------------------------------------------- /src/components/stages/stages.scss: -------------------------------------------------------------------------------- 1 | @import '~theme/variables.scss'; 2 | 3 | $stages__progress-bar_height: 0.4rem; 4 | $stages__item_radius: 2.4rem; 5 | 6 | .stages { 7 | position: relative; 8 | display: flex; 9 | height: $stages__item_radius; 10 | align-content: center; 11 | align-items: center; 12 | 13 | &__progress-bar { 14 | flex-grow: 1; 15 | height: $stages__progress-bar_height; 16 | margin: 0 $stages__progress-bar_height; 17 | background-color: #e5e5e5; 18 | } 19 | 20 | &__progress { 21 | width: 0; 22 | height: $stages__progress-bar_height; 23 | background-color: $color-second; 24 | } 25 | 26 | &__items { 27 | position: absolute; 28 | top: 0; 29 | right: 0; 30 | left: 0; 31 | display: flex; 32 | justify-content: space-between; 33 | } 34 | 35 | &__item { 36 | display: flex; 37 | width: $stages__item_radius; 38 | height: $stages__item_radius; 39 | color: #888; 40 | border-radius: 50%; 41 | background-color: #e5e5e5; 42 | font-family: LatoBlack, sans-serif; 43 | font-size: 1rem; 44 | font-weight: bold; 45 | justify-content: center; 46 | align-items: center; 47 | 48 | &_completed { 49 | color: white; 50 | background-color: $color-second; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/components/textarea/textarea.js: -------------------------------------------------------------------------------- 1 | import './textarea.scss'; -------------------------------------------------------------------------------- /src/components/textarea/textarea.pug: -------------------------------------------------------------------------------- 1 | //- 2 | options.name - form element name 3 | options.placeholder - input placeholder 4 | options.tabindex - input tabindex 5 | options.required - input required 6 | 7 | mixin textarea(options) 8 | if !options 9 | - options = {} 10 | 11 | textarea.textarea( 12 | name = options.name, 13 | placeholder = options.placeholder, 14 | tabindex = options.tabindex, 15 | required = options.required 16 | ) -------------------------------------------------------------------------------- /src/components/textarea/textarea.scss: -------------------------------------------------------------------------------- 1 | .textarea { 2 | display: block; 3 | width: 100%; 4 | min-height: 7.3rem; 5 | padding: 0.43rem 1rem; 6 | padding-top: 0.8rem; 7 | resize: none; 8 | color: #888; 9 | border: none; 10 | border-radius: 4px; 11 | background-color: #e5e5e5; 12 | font-family: SourceSansProRegular, sans-serif; 13 | font-size: 1.2rem; 14 | 15 | &:focus { 16 | outline: 0.3rem auto #bbb; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/components/tick-box/img/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 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 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/components/tick-box/tick-box.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import './tick-box.scss'; 3 | 4 | class TickBox { 5 | constructor($component) { 6 | this.$component = $component; 7 | this._attachEventHandlers(); 8 | } 9 | 10 | _attachEventHandlers() { 11 | this.$component.on('click', () => this.$component.toggleClass('tick-box_enable')) 12 | } 13 | } 14 | 15 | $(() => { 16 | $('.js-tick-box').each((index, node) => { 17 | new TickBox($(node)); 18 | }); 19 | }); -------------------------------------------------------------------------------- /src/components/tick-box/tick-box.pug: -------------------------------------------------------------------------------- 1 | //- 2 | options.enabled - состояние tick-box'а (bool) 3 | options.mix - любая строка, будет добавлена в классы компонента 4 | 5 | mixin tick-box(options) 6 | if !options 7 | - options = {} 8 | - 9 | var classes = '' 10 | classes += options.mix || '' 11 | classes += ' ' + (options.enabled ? 'tick-box_enable' : '') 12 | 13 | .tick-box.js-tick-box(class = classes) 14 | .tick-box__check-icon 15 | include ./img/check.svg -------------------------------------------------------------------------------- /src/components/tick-box/tick-box.scss: -------------------------------------------------------------------------------- 1 | @import '~theme/variables.scss'; 2 | $tick-box_height: 2rem; 3 | 4 | .tick-box { 5 | position: relative; 6 | display: inline-flex; 7 | width: $tick-box_height; 8 | height: $tick-box_height; 9 | transition: background-color 0.2s, color 0.2s; 10 | border-radius: 50%; 11 | background-color: #e5e5e5; 12 | font-size: ($tick-box_height * 3 / 5); 13 | justify-content: center; 14 | align-items: center; 15 | 16 | input:not(:checked) + & { 17 | background-color: #e5e5e5; 18 | 19 | & .tick-box__check-icon svg path { 20 | fill: #c0c0c0; 21 | } 22 | } 23 | 24 | input:checked + &, 25 | &_enable { 26 | background-color: $color-first; 27 | 28 | & .tick-box__check-icon svg path { 29 | fill: white; 30 | } 31 | } 32 | 33 | &__check-icon { 34 | width: 60%; 35 | height: 60%; 36 | 37 | & svg path { 38 | fill: #c0c0c0; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/toggle/toggle.js: -------------------------------------------------------------------------------- 1 | import './toggle.scss'; 2 | import $ from 'jquery'; 3 | 4 | class Toggle { 5 | constructor($component) { 6 | this.$component = $component; 7 | this._attachEventHandlers(); 8 | } 9 | 10 | _attachEventHandlers() { 11 | this.$component.on('click', () => this.$component.toggleClass('toggle_on')); 12 | } 13 | }; 14 | 15 | $(() => { 16 | $('.js-toggle').each((index, node) => { 17 | new Toggle($(node)); 18 | }); 19 | }); -------------------------------------------------------------------------------- /src/components/toggle/toggle.pug: -------------------------------------------------------------------------------- 1 | //- 2 | options.textTrue - текст Вкл. (по-умолчанию 'yes') 3 | options.textFalse - текст Выкл. (по-умолчанию 'no') 4 | options.state - состояние выключателя (true - включен, false - выключен, по-умолчанию false) 5 | options.mix - любая строка, будет добавлена в классы компонента 6 | 7 | mixin toggle(options) 8 | if !options 9 | - options = {} 10 | - 11 | var classes = '' 12 | classes += options.mix || '' 13 | classes += ' ' + (options.state ? 'toggle_on' : '') 14 | 15 | .toggle.js-toggle(class = classes) 16 | .toggle__text 17 | .toggle__text-on= options.textTrue || 'yes' 18 | .toggle__text-off= options.textFalse || 'no' 19 | .toggle__switcher 20 | .toggle__switch -------------------------------------------------------------------------------- /src/components/toggle/toggle.scss: -------------------------------------------------------------------------------- 1 | @import '~theme/variables.scss'; 2 | $toggle_height: 2rem; 3 | 4 | .toggle { 5 | position: relative; 6 | display: inline-block; 7 | overflow: hidden; 8 | width: ($toggle_height * 2.5); 9 | height: $toggle_height; 10 | cursor: pointer; 11 | -ms-user-select: none; 12 | -moz-user-select: none; 13 | -webkit-user-select: none; 14 | transition: background-color 0.2s ease-in; 15 | border-radius: ($toggle_height / 2); 16 | background-color: #e5e5e5; 17 | 18 | &__text { 19 | position: absolute; 20 | top: 0; 21 | display: flex; 22 | width: 200%; 23 | height: $toggle_height; 24 | margin-left: -100%; 25 | transition: margin 0.2s ease-in; 26 | align-items: center; 27 | } 28 | 29 | &__switcher { 30 | width: $toggle_height; 31 | padding: ($toggle_height / 8); 32 | transition: width 0.2s ease-in; 33 | } 34 | 35 | &__switch { 36 | width: ($toggle_height * 6 / 8); 37 | height: ($toggle_height * 6 / 8); 38 | margin-left: auto; 39 | border-radius: 50%; 40 | background-color: white; 41 | } 42 | 43 | &__text-on, 44 | &__text-off { 45 | float: left; 46 | overflow: hidden; 47 | width: 50%; 48 | text-align: center; 49 | text-transform: uppercase; 50 | color: white; 51 | font-family: LatoBlack, sans-serif; 52 | font-size: ($toggle_height / 2); 53 | font-weight: bold; 54 | } 55 | 56 | &__text-on { 57 | padding-right: ($toggle_height * 6 / 8); 58 | } 59 | 60 | &__text-off { 61 | padding-left: ($toggle_height * 6 / 8); 62 | } 63 | 64 | input:not(:checked) + & { 65 | background-color: #e5e5e5; 66 | 67 | & .toggle__text { 68 | margin-left: -100%; 69 | } 70 | 71 | & .toggle__switcher { 72 | width: $toggle_height; 73 | } 74 | } 75 | 76 | input:checked + &, 77 | &_on { 78 | background-color: $color-first; 79 | 80 | & .toggle__text { 81 | margin-left: 0; 82 | } 83 | 84 | & .toggle__switcher { 85 | width: 100%; 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/components/user-profile/img/default-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstack-development/flat-starter-kit/5d4e6be5b18685405b20846c709fd661611b3c06/src/components/user-profile/img/default-avatar.png -------------------------------------------------------------------------------- /src/components/user-profile/img/face1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstack-development/flat-starter-kit/5d4e6be5b18685405b20846c709fd661611b3c06/src/components/user-profile/img/face1.jpg -------------------------------------------------------------------------------- /src/components/user-profile/img/face2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstack-development/flat-starter-kit/5d4e6be5b18685405b20846c709fd661611b3c06/src/components/user-profile/img/face2.jpg -------------------------------------------------------------------------------- /src/components/user-profile/img/facebook-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 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 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/components/user-profile/img/instagram-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 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 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/components/user-profile/img/twitter-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 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 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/components/user-profile/user-profile.js: -------------------------------------------------------------------------------- 1 | import './user-profile.scss'; -------------------------------------------------------------------------------- /src/components/user-profile/user-profile.pug: -------------------------------------------------------------------------------- 1 | //- 2 | options.photo - ссылка на фото 3 | options.name - имя 4 | options.profession - профессия 5 | options.facebook - ссылка на страничку 6 | options.twitter - ссылка на страничку 7 | options.instagram - ссылка на страничку 8 | 9 | mixin user-profile(options) 10 | if !options 11 | - options = {} 12 | 13 | .user-profile 14 | .user-profile__frame 15 | if options.photo 16 | img.user-profile__photo(src=require('../../components/user-profile/img/' + options.photo), alt = 'photo') 17 | .user-profile__tooltip-wrapper 18 | .user-profile__tooltip( 19 | class = (options.facebook || options.twitter || options.instagram) ? 'user-profile__tooltip_active' : '' 20 | ) 21 | span.user-profile__name= options.name || '' 22 | span.user-profile__profession= options.profession || '' 23 | .user-profile__social-container 24 | a.user-profile__social( 25 | href = options.facebook || false 26 | class = options.facebook ? 'user-profile__social_active' : '' 27 | target = '_blank' 28 | rel = "noopener noreferrer" 29 | ) 30 | include ./img/facebook-logo.svg 31 | a.user-profile__social( 32 | href = options.twitter || false 33 | class = options.twitter ? 'user-profile__social_active' : '' 34 | target = '_blank' 35 | rel = "noopener noreferrer" 36 | ) 37 | include ./img/twitter-logo.svg 38 | a.user-profile__social( 39 | href = options.instagram || false 40 | class = options.instagram ? 'user-profile__social_active' : '' 41 | target = '_blank' 42 | rel = "noopener noreferrer" 43 | ) 44 | include ./img/instagram-logo.svg -------------------------------------------------------------------------------- /src/components/user-profile/user-profile.scss: -------------------------------------------------------------------------------- 1 | @import '~theme/variables.scss'; 2 | $user-profile_height: 7.8rem; 3 | 4 | .user-profile { 5 | position: relative; 6 | display: inline-block; 7 | width: $user-profile_height; 8 | height: $user-profile_height; 9 | 10 | &__frame { 11 | display: flex; 12 | overflow: hidden; 13 | width: 100%; 14 | height: 100%; 15 | border: 0.4rem solid #e5e5e5; 16 | border-radius: 50%; 17 | background: url(./img/default-avatar.png) no-repeat center center; 18 | background-color: white; 19 | background-size: 96%; 20 | align-items: stretch; 21 | } 22 | 23 | &__photo { 24 | width: 100%; 25 | border-radius: 50%; 26 | } 27 | 28 | &__tooltip-wrapper { 29 | position: absolute; 30 | z-index: 2; 31 | bottom: 0; 32 | left: 100%; 33 | display: none; 34 | visibility: hidden; 35 | min-width: 1.74 * $user-profile_height; 36 | max-width: 2 * $user-profile_height; 37 | min-height: $user-profile_height; 38 | padding-left: 10%; 39 | transition: visibility 0s ease-in 0.1s; 40 | transform: translateY(calc((100% - #{$user-profile_height}) / 2)); 41 | } 42 | 43 | &__tooltip { 44 | position: relative; 45 | display: flex; 46 | flex-direction: column; 47 | flex: 1 1 auto; 48 | padding: 0.85rem; 49 | color: white; 50 | border-radius: 0.6rem; 51 | background-color: $color-first; 52 | justify-content: center; 53 | 54 | &::before { 55 | position: absolute; 56 | top: calc(50% - 0.35rem); 57 | left: -0.3rem; 58 | display: block; 59 | width: 0.7rem; 60 | height: 0.7rem; 61 | content: ''; 62 | transform: rotate(45deg); 63 | border-bottom-left-radius: 0.25rem; 64 | background-color: inherit; 65 | } 66 | 67 | &_active { 68 | color: #4f4f4f; 69 | background-color: #e5e5e5; 70 | 71 | & .user-profile__name { 72 | color: $color-second; 73 | } 74 | 75 | & .user-profile__social path { 76 | fill: #4f4f4f; 77 | } 78 | 79 | & .user-profile__social_active path { 80 | fill: $color-second; 81 | } 82 | } 83 | } 84 | 85 | &:hover &__tooltip-wrapper { 86 | display: flex; 87 | visibility: visible; 88 | } 89 | 90 | &__name { 91 | overflow: hidden; 92 | text-align: center; 93 | text-overflow: ellipsis; 94 | font-family: LatoRegular, sans-serif; 95 | font-size: 1.5rem; 96 | } 97 | 98 | &__profession { 99 | overflow: hidden; 100 | text-align: center; 101 | text-transform: uppercase; 102 | text-overflow: ellipsis; 103 | font-family: LatoBlack, sans-serif; 104 | font-size: 0.95rem; 105 | font-weight: bold; 106 | } 107 | 108 | &__social-container { 109 | display: flex; 110 | height: 2.3rem; 111 | margin-top: auto; 112 | padding: 0 10%; 113 | padding-top: 0.3rem; 114 | align-items: stretch; 115 | justify-content: space-around; 116 | } 117 | 118 | &__social { 119 | width: 2rem; 120 | height: 2rem; 121 | text-decoration: none; 122 | 123 | & path { 124 | fill: white; 125 | } 126 | 127 | &_active { 128 | cursor: pointer; 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/entry.js: -------------------------------------------------------------------------------- 1 | import 'normalize.css'; 2 | import './theme/global.scss'; 3 | 4 | function requireAll(requireContext) { 5 | return requireContext.keys().map(requireContext); 6 | } 7 | 8 | requireAll(require.context('./components', true, /^\.\/(?!.*(?:__tests__)).*\.(jsx?)$/)); // pattern to take each .js(x) files except of the ones with __tests__ directory https://regex101.com/r/J8NWTj/1 9 | requireAll(require.context('./pages', true, /^\.\/(?!.*(?:__tests__)).*\.(jsx?)$/)); 10 | 11 | -------------------------------------------------------------------------------- /src/entryMakeupTests.js: -------------------------------------------------------------------------------- 1 | import 'normalize.css'; 2 | import './theme/global.scss'; 3 | 4 | import 'script-loader?window=>global!makeup'; 5 | import 'makeup/dist/makeup.css'; 6 | 7 | function requireAll(requireContext) { 8 | return requireContext.keys().map(requireContext); 9 | } 10 | 11 | const dataSetComponents = requireAll(require.context('./components', true, /^\.\/.*(makeup-data\.js)$/)); 12 | const dataSetPages = requireAll(require.context('./pages', true, /^\.\/.*(makeup-data\.js)$/)); 13 | 14 | let dataSet = { 15 | label: 'Tests', 16 | items: [{ 17 | name: 'components', 18 | type: 'module', 19 | styles: { 20 | markup: 'background-color: transparent;' 21 | }, 22 | items: [], 23 | }, { 24 | name: 'pages', 25 | type: 'module', 26 | styles: { 27 | markup: 'background-color: transparent;' 28 | }, 29 | items: [], 30 | }] 31 | }; 32 | 33 | dataSet = dataSetComponents.reduce((acc, item) => { 34 | acc.items[0].items.push(item.data); 35 | return acc; 36 | }, dataSet); 37 | 38 | dataSet = dataSetPages.reduce((acc, item) => { 39 | acc.items[1].items.push(item.data); 40 | return acc; 41 | }, dataSet); 42 | 43 | function template(...args) { 44 | return dataSetComponents 45 | .concat(dataSetPages) 46 | .reduce((acc, item) => { 47 | return acc + (item.template(...args) || ''); 48 | }, ''); 49 | } 50 | 51 | window.Makeup(dataSet, template); 52 | 53 | requireAll(require.context('./components', true, /^\.\/.*\.(jsx?)$/)); 54 | requireAll(require.context('./pages', true, /^\.\/.*\.(jsx?)$/)); 55 | 56 | -------------------------------------------------------------------------------- /src/pages/activity/activity.js: -------------------------------------------------------------------------------- 1 | import './activity.scss'; -------------------------------------------------------------------------------- /src/pages/activity/activity.pug: -------------------------------------------------------------------------------- 1 | extends ../base/base 2 | 3 | include ../../components/activity-card/activity-card 4 | include ../../components/news/news 5 | include ../../components/event/event 6 | 7 | block title 8 | | Activity Page 9 | 10 | block main 11 | - data = htmlWebpackPlugin.options.getData() 12 | 13 | +layout__container({mix: 'p-activity', wrap: 'wrap'}) 14 | h1.p-activity__title News & events 15 | for item in data.activityes 16 | +activity-card({image: item.image}) 17 | if item.type === 'news' 18 | +news(item.data) 19 | else if item.type === 'event' 20 | +event(item.data) -------------------------------------------------------------------------------- /src/pages/activity/activity.scss: -------------------------------------------------------------------------------- 1 | @import '~theme/variables.scss'; 2 | 3 | .p-activity { 4 | &__title { 5 | flex-basis: 100%; 6 | } 7 | 8 | & > *:not(:first-child) { 9 | flex-basis: 100%; 10 | 11 | @include column($xs, 6); 12 | @include column($sm, 4); 13 | @include column($md, 3); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/pages/activity/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "activityes": [ 3 | { 4 | "type": "news", 5 | "image": "img1.jpg", 6 | "data": { 7 | "title": "It's all going downhill", 8 | "date": "24 august 2013", 9 | "desc": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." 10 | } 11 | }, 12 | { 13 | "type": "news", 14 | "data": { 15 | "title": "It's all going downhill", 16 | "date": "24 august 2013", 17 | "desc": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." 18 | } 19 | }, 20 | { 21 | "type": "event", 22 | "image": "img1.jpg", 23 | "data": { 24 | "title": "This is the title", 25 | "dateDay": "24", 26 | "dateMonth": "August", 27 | "desc": "Lorem ipsum dolor sit amet, consectetur adipiscing elit." 28 | } 29 | }, 30 | { 31 | "type": "event", 32 | "data": { 33 | "title": "This is the title", 34 | "dateDay": "24", 35 | "dateMonth": "August", 36 | "desc": "Lorem ipsum dolor sit amet, consectetur adipiscing elit." 37 | } 38 | }, 39 | { 40 | "type": "news", 41 | "data": { 42 | "title": "It's all going downhill", 43 | "date": "24 august 2013", 44 | "desc": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." 45 | } 46 | }, 47 | { 48 | "type": "event", 49 | "image": "img1.jpg", 50 | "data": { 51 | "title": "This is the title", 52 | "dateDay": "24", 53 | "dateMonth": "August", 54 | "desc": "Lorem ipsum dolor sit amet, consectetur adipiscing elit." 55 | } 56 | }, 57 | { 58 | "type": "news", 59 | "image": "img1.jpg", 60 | "data": { 61 | "title": "It's all going downhill", 62 | "date": "24 august 2013", 63 | "desc": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." 64 | } 65 | }, 66 | { 67 | "type": "news", 68 | "image": "img1.jpg", 69 | "data": { 70 | "title": "It's all going downhill", 71 | "date": "24 august 2013", 72 | "desc": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." 73 | } 74 | }, 75 | { 76 | "type": "event", 77 | "data": { 78 | "title": "This is the title", 79 | "dateDay": "24", 80 | "dateMonth": "August", 81 | "desc": "Lorem ipsum dolor sit amet, consectetur adipiscing elit." 82 | } 83 | } 84 | ] 85 | } -------------------------------------------------------------------------------- /src/pages/base/base-data.pug: -------------------------------------------------------------------------------- 1 | - 2 | locals.menuItems = [ 3 | { 4 | name: 'demo', 5 | href: '/demo.html' 6 | }, { 7 | name: 'profile', 8 | href: '/profile.html' 9 | }, { 10 | name: 'news & events', 11 | href: '/activity.html' 12 | }, { 13 | name: 'messages', 14 | href: '/messages.html' 15 | } 16 | ] 17 | locals.guestMneuItems = [ 18 | { 19 | name: 'sign in', 20 | href: '/signin.html' 21 | }, { 22 | name: 'sign up', 23 | href: '/signup.html' 24 | } 25 | ] -------------------------------------------------------------------------------- /src/pages/base/base-guest.pug: -------------------------------------------------------------------------------- 1 | extends ./base 2 | 3 | block header 4 | +header({menuItems: locals.guestMneuItems}) 5 | 6 | block footer 7 | +footer({items: locals.guestMneuItems}) -------------------------------------------------------------------------------- /src/pages/base/base.pug: -------------------------------------------------------------------------------- 1 | include ./base-data 2 | block variables 3 | 4 | include ../../components/layout/layout 5 | include ../../components/header/header 6 | include ../../components/footer/footer 7 | 8 | doctype html 9 | html 10 | head 11 | meta(charset = 'utf-8') 12 | meta(name='viewport', content="initial-scale=1.0, width=device-width") 13 | title 14 | block title 15 | body 16 | +layout() 17 | +layout__header() 18 | +layout__container({basis: 1000, align: 'center'}) 19 | block header 20 | +header({menuItems: locals.menuItems}) 21 | +layout__main() 22 | +layout__container({basis: 1000, align: 'center'}) 23 | block main 24 | +layout__footer() 25 | +layout__container({basis: 1000, align: 'center'}) 26 | block footer 27 | +footer({items: locals.menuItems}) 28 | block scripts -------------------------------------------------------------------------------- /src/pages/demo/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": [ 3 | { 4 | "login": "sara1", 5 | "name": "Sarah Brown", 6 | "profession": "developer" 7 | }, 8 | { 9 | "login": "john1", 10 | "name": "John Smith", 11 | "profession": "ux designer", 12 | "photo": "face1.jpg", 13 | "facebook": "https://fb.com" 14 | }, 15 | { 16 | "login": "sara2", 17 | "name": "Sarah Brown", 18 | "profession": "developer", 19 | "photo": "face2.jpg", 20 | "twitter": "https://twitter.com" 21 | } 22 | ], 23 | "dropDownOptions": [ 24 | { 25 | "name": "Choose an option" 26 | }, 27 | { 28 | "name": "First option" 29 | }, 30 | { 31 | "name": "Second option" 32 | }, 33 | { 34 | "name": "Third option" 35 | } 36 | ], 37 | "news": [ 38 | { 39 | "image": "img1.jpg", 40 | "title": "It's all going downhill", 41 | "date": "24 august 2013", 42 | "desc": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." 43 | } 44 | ], 45 | "events": [ 46 | { 47 | "image": "img1.jpg", 48 | "title": "This is the title", 49 | "dateDay": "24", 50 | "dateMonth": "August", 51 | "desc": "Lorem ipsum dolor sit amet, consectetur adipiscing elit." 52 | } 53 | ], 54 | "messengers": [ 55 | { 56 | "user": { 57 | "login": "john1", 58 | "name": "John Smith", 59 | "profession": "ux designer", 60 | "photo": "face1.jpg", 61 | "facebook": "https://fb.com" 62 | }, 63 | "messages": [ 64 | { 65 | "isIncoming": true, 66 | "text": "Hey! So are we cool to meet at the art gallery? Say 8pm" 67 | }, 68 | { 69 | "isIncoming": false, 70 | "text": "Ok))" 71 | } 72 | ] 73 | } 74 | ], 75 | "location": { 76 | "coord": [ 77 | 56.4531907, 78 | 84.9756513 79 | ], 80 | "address": "Tomsk, Krasnoarmeyskaya, 147
office 201" 81 | } 82 | } -------------------------------------------------------------------------------- /src/pages/demo/demo.js: -------------------------------------------------------------------------------- 1 | import './demo.scss' -------------------------------------------------------------------------------- /src/pages/demo/demo.pug: -------------------------------------------------------------------------------- 1 | extends ../base/base 2 | 3 | include ../../components/layout/layout 4 | include ../../components/button/button 5 | include ../../components/arrow-button/arrow-button 6 | include ../../components/percentage/percentage 7 | include ../../components/pie-chart/pie-chart 8 | include ../../components/slider/slider 9 | include ../../components/stages/stages 10 | include ../../components/feedback/feedback 11 | include ../../components/toggle/toggle 12 | include ../../components/tick-box/tick-box 13 | include ../../components/search/search 14 | include ../../components/user-profile/user-profile 15 | include ../../components/drop-down/drop-down 16 | include ../../components/activity-card/activity-card 17 | include ../../components/news/news 18 | include ../../components/event/event 19 | include ../../components/location/location 20 | include ../../components/calendar/calendar 21 | include ../../components/messenger/messenger 22 | 23 | block title 24 | | Flat Starter Kit Demo 25 | 26 | block main 27 | - data = htmlWebpackPlugin.options.getData() 28 | 29 | +layout__container({mix: 'p-demo', basis: 700, align: 'center', wrap: 'wrap'}) 30 | h2 standard buttons 31 | +layout__container({wrap: 'wrap', basis: 500, align: 'center'}) 32 | +layout__container({mix: 'p-demo__row', wrap: 'wrap'}) 33 | +button({size: 'small'}) 34 | +button() 35 | +button({type: 'error'}) 36 | +button({type: 'error', size: 'small'}) 37 | +layout__container({mix: 'p-demo__row', wrap: 'wrap'}) 38 | +button({isInverted: true, size: 'small'}) 39 | +button({isInverted: true}) 40 | +button({isInverted: true, type: 'error'}) 41 | +button({isInverted: true, type: 'error', size: 'small'}) 42 | h2 arrow buttons 43 | +layout__container({mix: 'p-demo__row', basis: 450, align: 'center', wrap: 'wrap'}) 44 | +arrow-button({direction: 'left', condition: 'inactive'}) 45 | +arrow-button({direction: 'right', condition: 'inactive'}) 46 | +arrow-button({direction: 'up', condition: 'disabled'}) 47 | +arrow-button({direction: 'down', condition: 'disabled'}) 48 | +arrow-button({direction: 'left'}) 49 | +arrow-button({direction: 'right'}) 50 | h2 percentages / pie chart 51 | +layout__container({mix: 'p-demo__row', wrap: 'wrap'}) 52 | +percentage() 53 | +percentage({percent: 38}) 54 | +percentage({percent: 62}) 55 | +percentage({percent: 89}) 56 | +pie-chart({items: [1, 3, 4, 5]}) 57 | h2 sliders 58 | +layout__container({mix: 'p-demo__row', wrap: 'wrap'}) 59 | +slider({tooltipEnabled: true, color: '#4eb7a8'}) 60 | +slider({scaleEnabled: true, color: '#e75735'}) 61 | h2 stages 62 | +stages({length: 5, progress: 3}) 63 | h2 form elements 64 | +layout__container({wrap: 'wrap'}) 65 | +layout__container({mix: 'p-demo__column', align: 'left', display: 'block'}) 66 | +feedback() 67 | +layout__container({mix: 'p-demo__column', align: 'right', wrap : 'wrap'}) 68 | h2 toggles 69 | +layout__container({mix: 'p-demo__row', basis: 150, align: 'center'}) 70 | +toggle({textTrue: 'on', textFalse: 'off'}) 71 | +toggle({state: true}) 72 | h2 tick boxes 73 | +layout__container({mix: 'p-demo__row', basis: 100, align: 'center'}) 74 | +tick-box() 75 | +tick-box({enabled: true}) 76 | +layout__container({wrap: 'wrap'}) 77 | +layout__container({mix: 'p-demo__column', align: 'left', display: 'block'}) 78 | h2 search / drop down 79 | +search() 80 | +drop-down({items: data.dropDownOptions, name: 'test'}) 81 | +layout__container({mix: 'p-demo__column', align: 'right', display: 'block'}) 82 | h2 user profile 83 | +layout__container({mix: 'p-demo__row', wrap: 'wrap'}) 84 | for profile in data.profiles 85 | +user-profile(profile) 86 | h2 news & events 87 | +layout__container({wrap: 'wrap'}) 88 | +layout__container({mix: 'p-demo__column', align: 'left', display: 'block'}) 89 | +activity-card({image: data.news[0].image}) 90 | +news(data.news[0]) 91 | +layout__container({mix: 'p-demo__column', align: 'right', display: 'block'}) 92 | +activity-card({image: data.events[0].image}) 93 | +event(data.events[0]) 94 | h2 location 95 | +location(data.location) 96 | +layout__container({wrap: 'wrap'}) 97 | +layout__container({mix: 'p-demo__column', align: 'left', display: 'block'}) 98 | h2 calendar 99 | +calendar() 100 | +layout__container({mix: 'p-demo__column', align: 'right', display: 'block'}) 101 | h2 messaging 102 | +messenger(data.messengers[0]) -------------------------------------------------------------------------------- /src/pages/demo/demo.scss: -------------------------------------------------------------------------------- 1 | @import '~theme/variables.scss'; 2 | 3 | .p-demo { 4 | padding: 2rem; 5 | 6 | & { 7 | h2 { 8 | flex-basis: 100%; 9 | } 10 | } 11 | 12 | .location, 13 | .stages { 14 | flex-basis: 100%; 15 | } 16 | 17 | .drop-down { 18 | margin-top: 1.5rem; 19 | } 20 | 21 | &__row { 22 | justify-content: space-around; 23 | align-items: flex-end; 24 | } 25 | 26 | & + & { 27 | margin-top: 1.4rem; 28 | } 29 | 30 | & .slider { 31 | flex-basis: 100%; 32 | align-self: flex-start; 33 | 34 | @include column($xs, 6); 35 | } 36 | 37 | &__column { 38 | @include column($xs, 6); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/pages/home/home-data.pug: -------------------------------------------------------------------------------- 1 | - 2 | -------------------------------------------------------------------------------- /src/pages/home/home.pug: -------------------------------------------------------------------------------- 1 | extends ../base/base-guest 2 | 3 | include ../../components/toggle/toggle 4 | 5 | block variables 6 | include ./home-data 7 | 8 | block title 9 | | Home Page 10 | 11 | block main 12 | -------------------------------------------------------------------------------- /src/pages/index/index.pug: -------------------------------------------------------------------------------- 1 | extends ../base/base 2 | 3 | block variables 4 | 5 | block title 6 | | Flat starter kit 7 | 8 | block main -------------------------------------------------------------------------------- /src/pages/layout-test/layout-test.pug: -------------------------------------------------------------------------------- 1 | extends ../base/base 2 | 3 | include ../../components/layout/layout 4 | 5 | block title 6 | | Test layout 7 | 8 | block header 9 | +layout__container({basis: 750, align: 'center'}) 10 | +layout__container({direction: 'column'}) 11 | div(style={background: 'green', height: '50px'}) 12 | 13 | block main 14 | +layout__container({direction: 'column'}) 15 | +layout__container() 16 | +layout__container() 17 | +layout__container() 18 | +layout__container({grow: 1, basis: 'auto'}) 19 | +layout__container({grow: 3, basis: 'auto'}) 20 | +layout__container() 21 | +layout__container({shrink: 1}) 22 | +layout__container({shrink: 3}) 23 | +layout__container() 24 | +layout__container({basis: 200}) 25 | +layout__container() 26 | 27 | block footer 28 | +layout__container({basis: 750, align: 'left'}) 29 | +layout__container({direction: 'column'}) 30 | div(style={background: 'red', height: '50px'}) -------------------------------------------------------------------------------- /src/pages/messages/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "users": [ 3 | { 4 | "login": "sara1", 5 | "name": "Sarah Brown", 6 | "profession": "developer" 7 | }, 8 | { 9 | "login": "john1", 10 | "name": "John Smith", 11 | "profession": "ux designer", 12 | "photo": "face1.jpg", 13 | "facebook": "https://fb.com" 14 | }, 15 | { 16 | "login": "sara2", 17 | "name": "Sarah Brown", 18 | "profession": "developer", 19 | "photo": "face2.jpg", 20 | "twitter": "https://twitter.com" 21 | } 22 | ], 23 | "messages": [{ 24 | "login": "john1", 25 | "messages": [ 26 | { 27 | "isIncoming": true, 28 | "text": "Hey! So are we cool to meet at the art gallery? Say 10pm" 29 | }, 30 | { 31 | "isIncoming": false, 32 | "text": "Good))" 33 | } 34 | ] 35 | }, { 36 | "login": "sara2", 37 | "messages": [ 38 | { 39 | "isIncoming": true, 40 | "text": "Hey! So are we cool to meet at the art gallery? Say 8pm" 41 | }, 42 | { 43 | "isIncoming": false, 44 | "text": "Ok))" 45 | } 46 | ] 47 | } 48 | ] 49 | } -------------------------------------------------------------------------------- /src/pages/messages/messages.js: -------------------------------------------------------------------------------- 1 | import './messages.scss'; 2 | import Messenger from '../../components/messenger/messenger'; 3 | import data from './data.json'; 4 | import $ from 'jquery'; 5 | 6 | class Messages { 7 | constructor($page) { 8 | this.$page = $page; 9 | this.$messenger = this.$page.find('.js-p-messages__messenger'); 10 | this.$activContact = this.$page.find('.js-p-messages__contact-item:first-child'); 11 | this._renderItemMessenger(this.$activContact); 12 | this._attachEventHandlers(); 13 | } 14 | 15 | _attachEventHandlers() { 16 | this.$page.find('.js-p-messages__contact-item').each((index, node) => { 17 | $(node).on('click', () => this._renderItemMessenger($(node))); 18 | }); 19 | } 20 | 21 | _renderItemMessenger($contactItem) { 22 | const login = $contactItem.data('login'); 23 | const messages = data.messages.find(item => item.login === login) || {}; 24 | const user = data.users.find(item => item.login === login); 25 | const html = Messenger.template({user, messages: messages.messages || []}); 26 | this.$messenger.html(html); 27 | new Messenger(this.$messenger.children()[0]); 28 | this._updateActiveContact($contactItem); 29 | } 30 | 31 | _updateActiveContact($contactItem) { 32 | this.$activContact.removeClass('p-messages__contact-item_active'); 33 | $contactItem.addClass('p-messages__contact-item_active'); 34 | this.$activContact = $contactItem; 35 | } 36 | } 37 | 38 | $(() => { 39 | $('.js-p-messages').each((index, node) => { 40 | new Messages($(node)); 41 | }); 42 | }); -------------------------------------------------------------------------------- /src/pages/messages/messages.pug: -------------------------------------------------------------------------------- 1 | extends ../base/base 2 | 3 | include ../../components/drop-down/drop-down 4 | include ../../components/messenger/messenger 5 | 6 | block title 7 | | Messages Page 8 | 9 | block main 10 | - data = htmlWebpackPlugin.options.getData() 11 | 12 | +layout__container({mix: 'p-messages js-p-messages', basis: 700, align: 'center', wrap: 'wrap'}) 13 | h1.p-messages__title Messages 14 | .p-messages__contacts 15 | for item in data.users 16 | .p-messages__contact-item.js-p-messages__contact-item(data-login = item.login)= item.name 17 | .p-messages__messenger.js-p-messages__messenger -------------------------------------------------------------------------------- /src/pages/messages/messages.scss: -------------------------------------------------------------------------------- 1 | @import '~theme/variables.scss'; 2 | 3 | .p-messages { 4 | & > * { 5 | flex-basis: 100%; 6 | } 7 | 8 | &__contacts { 9 | display: flex; 10 | overflow: hidden; 11 | flex-wrap: wrap; 12 | align-items: stretch; 13 | 14 | @include column($xs, 4); 15 | 16 | @include media-breakpoint-up($xs) { 17 | flex-direction: column; 18 | flex-wrap: nowrap; 19 | } 20 | } 21 | 22 | &__contact-item { 23 | overflow: hidden; 24 | margin: 0.5rem; 25 | padding: 1rem 2rem; 26 | text-align: center; 27 | white-space: nowrap; 28 | text-decoration: none; 29 | text-overflow: ellipsis; 30 | color: #fff; 31 | border-radius: 0.3rem; 32 | background-color: #4eb7a8; 33 | font-family: sans-serif; 34 | font-size: 1.2rem; 35 | font-weight: bold; 36 | 37 | @include media-breakpoint-up($xs) { 38 | margin: 0; 39 | margin-top: 0.5rem; 40 | margin-bottom: 0.5rem; 41 | } 42 | 43 | &:hover:not([class*=active]) { 44 | color: white; 45 | background-color: #28a290; 46 | } 47 | 48 | &_active { 49 | color: #4eb7a8; 50 | border: 1px solid #4eb7a8; 51 | background-color: white; 52 | } 53 | } 54 | 55 | &__messenger { 56 | @include column($xs, 8); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/pages/profile/profile.js: -------------------------------------------------------------------------------- 1 | import './profile.scss'; -------------------------------------------------------------------------------- /src/pages/profile/profile.pug: -------------------------------------------------------------------------------- 1 | extends ../base/base 2 | 3 | include ../../components/input/input 4 | include ../../components/drop-down/drop-down 5 | include ../../components/button/button 6 | 7 | block title 8 | | Profile Page 9 | 10 | block main 11 | +layout__container({mix: 'p-profile', wrap: 'wrap'}) 12 | h1.p-profile__title Settings 13 | form.p-profile__basic-info 14 | h2.p-profile__settings-title Basic Info 15 | +input({ 16 | name: 'firstName', 17 | type: 'text', 18 | placeholder: 'First Name' 19 | }) 20 | +input({ 21 | name: 'lastName', 22 | type: 'text', 23 | placeholder: 'Last Name' 24 | }) 25 | +drop-down({ 26 | name: 'sex', 27 | items: [ 28 | {name: 'Female'}, 29 | {name: 'Male'} 30 | ] 31 | }) 32 | +input({ 33 | name: 'profession', 34 | type: 'text', 35 | placeholder: 'Profession' 36 | }) 37 | +input({ 38 | name: 'hometown', 39 | type: 'text', 40 | placeholder: 'Hometown' 41 | }) 42 | +button({text: 'Save', isSubmit: true}) 43 | form.p-profile__contact-info 44 | h2.p-profile__settings-title Contact Info 45 | +input({ 46 | name: 'country', 47 | type: 'text', 48 | placeholder: 'Country' 49 | }) 50 | +input({ 51 | name: 'City', 52 | type: 'text', 53 | placeholder: 'City' 54 | }) 55 | +input({ 56 | name: 'mobile', 57 | type: 'text', 58 | placeholder: 'Mobile' 59 | }) 60 | +input({ 61 | name: 'skype', 62 | type: 'text', 63 | placeholder: 'Skype' 64 | }) 65 | +input({ 66 | name: 'website', 67 | type: 'text', 68 | placeholder: 'Website' 69 | }) 70 | +button({text: 'Save', isSubmit: true}) 71 | form.p-profile__interests 72 | h2.p-profile__settings-title Profile settings 73 | +input({ 74 | name: 'gifts', 75 | type: 'checkbox', 76 | label: 'Hide gifts section' 77 | }) 78 | +input({ 79 | name: 'posts', 80 | type: 'checkbox', 81 | label: 'Show only my posts by default' 82 | }) 83 | +input({ 84 | name: 'comments', 85 | type: 'checkbox', 86 | label: 'Disable wall comments' 87 | }) 88 | +input({ 89 | name: 'stickers', 90 | type: 'checkbox', 91 | label: 'Suggest stickers while typing' 92 | }) 93 | +input({ 94 | name: 'accessibility', 95 | type: 'checkbox', 96 | label: 'Accessibility features' 97 | }) 98 | +button({text: 'Save', isSubmit: true}) 99 | -------------------------------------------------------------------------------- /src/pages/profile/profile.scss: -------------------------------------------------------------------------------- 1 | @import '~theme/variables.scss'; 2 | 3 | .p-profile { 4 | padding: 0 1rem; 5 | 6 | & > * { 7 | flex-basis: 100%; 8 | } 9 | 10 | &__settings-title { 11 | padding-bottom: 0; 12 | } 13 | 14 | &__basic-info, 15 | &__contact-info, 16 | &__interests { 17 | @include column($xs, 6); 18 | @include column($sm, 4); 19 | 20 | & > *:not(:first-child) { 21 | margin-top: 1rem; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/pages/signin/signin.js: -------------------------------------------------------------------------------- 1 | import './signin.scss'; -------------------------------------------------------------------------------- /src/pages/signin/signin.pug: -------------------------------------------------------------------------------- 1 | extends ../base/base-guest 2 | 3 | include ../../components/layout/layout 4 | include ../../components/signin/signin 5 | 6 | block variables 7 | 8 | block title 9 | | Sign In Page 10 | 11 | block main 12 | +layout__container({mix: 'p-signin'}) 13 | +signin() 14 | 15 | -------------------------------------------------------------------------------- /src/pages/signin/signin.scss: -------------------------------------------------------------------------------- 1 | .p-signin { 2 | padding: 1rem 2rem; 3 | justify-content: center; 4 | } 5 | -------------------------------------------------------------------------------- /src/pages/signup/signup.pug: -------------------------------------------------------------------------------- 1 | extends ../base/base-guest 2 | 3 | include ../../components/signup/signup 4 | 5 | block variables 6 | 7 | block title 8 | | Sign Up Page 9 | 10 | block main 11 | +layout__container({mix: 'p-signin'}) 12 | +signup() 13 | -------------------------------------------------------------------------------- /src/theme/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstack-development/flat-starter-kit/5d4e6be5b18685405b20846c709fd661611b3c06/src/theme/favicon.png -------------------------------------------------------------------------------- /src/theme/global.scss: -------------------------------------------------------------------------------- 1 | @import '~theme/variables.scss'; 2 | 3 | * { 4 | box-sizing: border-box; 5 | margin: 0; 6 | padding: 0; 7 | } 8 | 9 | html { 10 | font-size: 8px; 11 | 12 | @include media-breakpoint-up($xxs) { 13 | font-size: 10px; 14 | } 15 | 16 | @include media-breakpoint-up($xs) { 17 | font-size: 12px; 18 | } 19 | } 20 | 21 | html, 22 | body { 23 | height: 100%; 24 | } 25 | 26 | h1, 27 | h2 { 28 | text-align: center; 29 | text-transform: uppercase; 30 | color: #777; 31 | font-family: LatoBlack, sans-serif; 32 | } 33 | 34 | h1 { 35 | font-size: 2rem; 36 | } 37 | 38 | h2 { 39 | padding: 1rem; 40 | font-size: 1.67rem; 41 | } 42 | -------------------------------------------------------------------------------- /src/theme/variables.scss: -------------------------------------------------------------------------------- 1 | /***** Variables *****/ 2 | $xxs: 320px; 3 | $xs: 575px; 4 | $sm: 767px; 5 | $md: 991px; 6 | $lg: 1199px; 7 | $xl: 1440px; 8 | 9 | $color-first: #4eb7a8; 10 | $color-second: #e75735; 11 | $color-first-dark: #28a290; 12 | $color-second-dark: #bf3e1f; 13 | $color-disabled: #e5e5e5; 14 | 15 | /***** Mixins *****/ 16 | @mixin media-breakpoint-up($width) { 17 | @media (min-width: $width + 1) { 18 | @content; 19 | } 20 | } 21 | 22 | @mixin media-breakpoint-down($width) { 23 | @media (max-width: $width) { 24 | @content; 25 | } 26 | } 27 | 28 | @mixin media-breakpoint-between($min-width, $max-width) { 29 | @media (min-width: $min-width + 1) and (max-width: $max-width) { 30 | @content; 31 | } 32 | } 33 | 34 | @mixin column($screen-width, $count) { 35 | flex-shrink: 0; 36 | padding-right: 1rem; 37 | padding-bottom: 1rem; 38 | padding-left: 1rem; 39 | 40 | @include media-breakpoint-up($screen-width) { 41 | flex-basis: (100% / 12 * $count); 42 | } 43 | } 44 | 45 | @mixin firefox-only { 46 | @at-root { 47 | @-moz-document url-prefix() { 48 | & { 49 | @content; 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const configurator = require('webpack-config'); 2 | 3 | configurator.environment.setAll({ 4 | env: () => process.env.NODE_ENV 5 | }); 6 | 7 | // Also you may use `'conf/webpack.[NODE_ENV].config.js'` 8 | module.exports = new configurator.default().extend('conf/webpack.[env].config.js'); 9 | --------------------------------------------------------------------------------